mirror of
https://github.com/tiennm99/awesome-ai-dev-tools.git
synced 2026-10-05 10:13:07 +00:00
The updater fetched GitHub metadata and rendered the dashboard payload in a single step, so anything that published the site needed a GITHUB_TOKEN. Cloudflare Pages builds from a Git webhook and has no business holding one. Split the tool into three modes. The default update step still fetches GitHub and now records every API-sourced field in data/metadata.json, which is committed alongside README.md and data/history.jsonl. The new -build mode joins that snapshot with data/agents.yml and data/history.jsonl to render dist/ with no network access and no token; -check is unchanged. Supporting changes: - computeDeltaAt anchors the delta windows to when the data was fetched rather than the wall clock, so a redeploy days later reproduces the same Δ7d instead of sliding the window past its slack allowance. - writeSiteData takes updatedAt explicitly for the same reason: the timestamp labels data freshness, not build time. - sortStats is extracted from fetchStats so the build step re-ranks identically from committed metadata. - An agents.yml entry with no metadata yet is omitted with a warning instead of failing the build, which would otherwise block every deploy between merging a new entry and the next nightly run. - update.yml drops the GitHub Pages deploy steps and commits data/metadata.json; ci.yml runs `go run . -build` so a build that would break on deploy breaks in CI first. - site/_headers stops the edge serving a stale data.json after a refresh. - docs/DEPLOY.md covers the Cloudflare setup, including the one-time bootstrap of data/metadata.json that the build depends on. Also carries the in-progress curation work already in the tree: the module rename to awesome-ai-dev-tools, retagged entries, and removal of the archived Roo-Code, void, continue and suna entries.
109 lines
3.1 KiB
Go
109 lines
3.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// runBuild renders the static dashboard into distDir from committed inputs
|
|
// only — data/agents.yml, data/metadata.json and data/history.jsonl. It makes
|
|
// no network calls and needs no GITHUB_TOKEN, which is what lets an untrusted
|
|
// build environment (Cloudflare Pages) run it.
|
|
//
|
|
// Splitting this out from the update step also means a pure curation change
|
|
// (retagging an entry, editing a note) republishes immediately on push,
|
|
// reusing the last fetched star figures instead of waiting for the nightly run.
|
|
func runBuild(agentsPath, metadataPath, historyPath, siteDir, distDir string) error {
|
|
agents, err := loadAgents(agentsPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
meta, err := readMetadata(metadataPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stats := statsFromMetadata(agents, meta)
|
|
stats = enforceStarFloor(stats)
|
|
if len(stats) == 0 {
|
|
return fmt.Errorf("no entries left to publish after joining %s with %s", agentsPath, metadataPath)
|
|
}
|
|
sortStats(stats)
|
|
|
|
history, err := readSnapshots(historyPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Anchor the delta windows to when the data was fetched, not to wall clock.
|
|
// A rebuild triggered days later (a docs push, a manual redeploy) must show
|
|
// the same "Δ7d" the nightly run computed, not a window silently slid
|
|
// forward past its slack allowance.
|
|
current := Snapshot{
|
|
Date: meta.FetchedAt.UTC().Format("2006-01-02"),
|
|
Stars: make(map[string]int, len(stats)),
|
|
}
|
|
for _, s := range stats {
|
|
current.Stars[s.CanonicalKey] = s.Stars
|
|
}
|
|
deltas7 := computeDeltaAt(history, current, meta.FetchedAt, 7, 3)
|
|
deltas30 := computeDeltaAt(history, current, meta.FetchedAt, 30, 5)
|
|
|
|
if err := copyDir(siteDir, distDir); err != nil {
|
|
return fmt.Errorf("copy %s to %s: %w", siteDir, distDir, err)
|
|
}
|
|
|
|
updatedAt := meta.FetchedAt.UTC().Format("2006-01-02 15:04 UTC")
|
|
if err := writeSiteData(filepath.Join(distDir, "data.json"), updatedAt, stats, deltas7, deltas30, history); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("built %s: %d entries, data fetched %s\n", distDir, len(stats), updatedAt)
|
|
return nil
|
|
}
|
|
|
|
// copyDir replaces dst with a fresh copy of src. The wipe matters: dist/ is
|
|
// build output, and a file deleted from site/ must not survive in a deploy
|
|
// because a previous build left it there.
|
|
func copyDir(src, dst string) error {
|
|
if err := os.RemoveAll(dst); err != nil {
|
|
return err
|
|
}
|
|
return filepath.WalkDir(src, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rel, err := filepath.Rel(src, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
target := filepath.Join(dst, rel)
|
|
if d.IsDir() {
|
|
return os.MkdirAll(target, 0o755)
|
|
}
|
|
if !d.Type().IsRegular() {
|
|
return nil // skip symlinks and other irregular entries
|
|
}
|
|
return copyFile(path, target)
|
|
})
|
|
}
|
|
|
|
func copyFile(src, dst string) error {
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = in.Close() }() // read-only; close error is not actionable
|
|
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
|
return err
|
|
}
|
|
return atomicWriteFile(dst, func(w io.Writer) error {
|
|
_, err := io.Copy(w, in)
|
|
return err
|
|
})
|
|
}
|