mirror of
https://github.com/tiennm99/awesome-ai-dev-tools.git
synced 2026-10-05 00:13:12 +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.
113 lines
3.5 KiB
Go
113 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// repoMeta is the GitHub-sourced half of a ranking row: every field the
|
|
// dashboard needs that cannot be derived from data/agents.yml. It is written
|
|
// by the update step (which has a token) and committed, so the build step can
|
|
// render the site offline.
|
|
type repoMeta struct {
|
|
NameWithOwner string `json:"nameWithOwner"`
|
|
URL string `json:"url"`
|
|
Stars int `json:"stars"`
|
|
Language string `json:"language"`
|
|
PushedAt time.Time `json:"pushedAt"`
|
|
Description string `json:"description"`
|
|
IsArchived bool `json:"archived"`
|
|
}
|
|
|
|
// metadataFile is the on-disk shape of data/metadata.json. Repos is keyed by
|
|
// canonical owner/repo (the same key history.jsonl uses), so entries survive
|
|
// renames and Go's sorted map marshalling keeps commit diffs minimal.
|
|
type metadataFile struct {
|
|
FetchedAt time.Time `json:"fetchedAt"`
|
|
Repos map[string]repoMeta `json:"repos"`
|
|
}
|
|
|
|
// writeMetadata persists the fetched GitHub metadata. Unlike the old
|
|
// site/data.json this file IS committed: it is the handoff between the update
|
|
// step and the build step, and the only reason the build can run without a
|
|
// GITHUB_TOKEN.
|
|
func writeMetadata(path string, stats []Stat) error {
|
|
repos := make(map[string]repoMeta, len(stats))
|
|
for _, s := range stats {
|
|
repos[s.CanonicalKey] = repoMeta{
|
|
NameWithOwner: s.NameWithOwner,
|
|
URL: s.URL,
|
|
Stars: s.Stars,
|
|
Language: s.Language,
|
|
PushedAt: s.PushedAt,
|
|
Description: s.Description,
|
|
IsArchived: s.IsArchived,
|
|
}
|
|
}
|
|
|
|
return atomicWriteFile(path, func(w io.Writer) error {
|
|
enc := json.NewEncoder(w)
|
|
// Indented so the committed file reviews as a readable diff.
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(metadataFile{
|
|
FetchedAt: timeNow().UTC(),
|
|
Repos: repos,
|
|
})
|
|
})
|
|
}
|
|
|
|
func readMetadata(path string) (metadataFile, error) {
|
|
var m metadataFile
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return m, fmt.Errorf("%s not found — run the updater (`go run .` with GITHUB_TOKEN set) to generate it", path)
|
|
}
|
|
return m, err
|
|
}
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
return m, fmt.Errorf("parse %s: %w", path, err)
|
|
}
|
|
if len(m.Repos) == 0 {
|
|
return m, fmt.Errorf("%s contains no repos", path)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// statsFromMetadata rejoins the two halves of a row: curation (order-independent
|
|
// tags and notes) from agents.yml, live figures from metadata.json.
|
|
//
|
|
// A repo present in agents.yml but absent from metadata.json is skipped with a
|
|
// warning rather than failing the build. That is the normal state between
|
|
// "contributor adds an entry" and "the next nightly update fetches it", and a
|
|
// hard failure there would block every Cloudflare deploy in the meantime.
|
|
func statsFromMetadata(agents []Agent, meta metadataFile) []Stat {
|
|
stats := make([]Stat, 0, len(agents))
|
|
for _, a := range agents {
|
|
key := a.Owner + "/" + a.Repo
|
|
m, ok := meta.Repos[key]
|
|
if !ok {
|
|
fmt.Fprintf(os.Stderr, "warning: %s has no entry in metadata yet — omitted from this build; the next updater run will add it\n", key)
|
|
continue
|
|
}
|
|
stats = append(stats, Stat{
|
|
CanonicalKey: key,
|
|
Owner: a.Owner,
|
|
Repo: a.Repo,
|
|
Tags: a.Tags,
|
|
Notes: a.Notes,
|
|
Description: m.Description,
|
|
Stars: m.Stars,
|
|
Language: m.Language,
|
|
PushedAt: m.PushedAt,
|
|
URL: m.URL,
|
|
NameWithOwner: m.NameWithOwner,
|
|
IsArchived: m.IsArchived,
|
|
})
|
|
}
|
|
return stats
|
|
}
|