mirror of
https://github.com/hanzoai/deploy.git
synced 2026-10-07 08:44:50 +00:00
The Go port took 24 entries from a Python dict that held 40. Sixteen repos were in NEITHER sync leg afterwards -- not pull mirrors, so nothing brought them in, and absent from this table, so nothing pushed them forward -- which is the exact shape the port was written to eliminate, reintroduced by the port itself. The worst of them is hanzoai/ci. Every repo in three orgs imports it as `hanzoai/ci/...@v1`, and a stale pipeline runs GREEN, so a caller cannot tell a current pipeline from one months old. It had already sat 18 commits behind once for exactly this reason. The rest: console and namespace (shipping services whose inbound leg was never written down), the five hanzo-apps surfaces, hanzo-go/sdk and hanzo-rs/sdk (published SDKs that `go get` and `cargo` resolve against the forge copy -- 16 and 10 commits behind when they were found), hanzo-dart, hanzokv, hanzozt, hanzo-docs/team and luxfi/aml. Two guards, because a table that quietly loses rows is not a thing to notice twice. One asserts every declared org is in the allowlist -- without it an entry in a new org is refused and the only symptom is a smaller "N of M resolved". The other asserts the count and names five of the entries that went missing. The allowlist gains hanzo-inc, and the live run is why. Three declared repos -- auto, blog and visor -- have been transferred there, and the run refused all three by name rather than pushing into an org nobody had listed. That is the guard working: it is an allowlist so that an unknown org is a question, not an assumption. hanzo-inc is ours (created 2026-07-24, all three private and pushed within the week), so it is listed now and the refusal goes away. Measured against live GitHub: 40 of 40 resolve, exit 0. EIGHT only resolve because names are RESOLVED and never matched against a listing -- three moved to hanzo-apps, three to hanzo-inc, esign became sign, kms-v1 became kms. A listing would have reported eight absences, and an absent name raises nothing. Co-authored-by: Hanzo Dev <[email protected]>
125 lines
4.6 KiB
Go
125 lines
4.6 KiB
Go
package mirror
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Owned is the set of orgs this may touch. An ALLOWLIST, not a denylist, and the
|
|
// difference is the whole point.
|
|
//
|
|
// The script this replaces carried a regex of forbidden names, which meant the
|
|
// forbidden names were written down in our source — the exact thing the rule
|
|
// against them forbids. It needed that regex because it swept entire orgs, so
|
|
// anything could turn up in a listing.
|
|
//
|
|
// This never enumerates an org: it acts only on the explicitly declared table. The
|
|
// one residual risk is a declared repo being TRANSFERRED into an org we do not own,
|
|
// which Plan checks after resolution. Naming what we own answers that without
|
|
// naming anything we do not, and fails closed on an org nobody thought about —
|
|
// including ones that do not exist yet.
|
|
// Owned lists the orgs whose repos this program may touch. An ALLOWLIST, not a
|
|
// denylist: it refuses by default, so an org nobody has thought about yet is
|
|
// refused rather than swept, and a lookalike that merely contains one of our
|
|
// names is refused too.
|
|
//
|
|
// It is deliberately a SUPERSET of the orgs in repos.json. A declared repo can be
|
|
// renamed or transferred, and `owned` guards the name GitHub resolves it to — so
|
|
// an org with no entry today is still the org a repo may land in tomorrow. Three
|
|
// declared repos had already moved into hanzo-apps that way.
|
|
var Owned = map[string]bool{
|
|
"hanzo-apps": true,
|
|
"hanzo-dart": true,
|
|
"hanzo-docs": true,
|
|
"hanzo-go": true,
|
|
"hanzo-js": true,
|
|
"hanzo-ml": true,
|
|
"hanzo-rs": true,
|
|
"hanzoai": true,
|
|
"hanzo-inc": true,
|
|
"hanzobot": true,
|
|
"hanzodao": true,
|
|
"hanzoid": true,
|
|
"hanzokv": true,
|
|
"hanzozt": true,
|
|
"luxfi": true,
|
|
}
|
|
|
|
// owned reports whether a full name "org/repo" sits in an org we own.
|
|
func owned(full string) bool {
|
|
org, _, ok := strings.Cut(full, "/")
|
|
return ok && Owned[org]
|
|
}
|
|
|
|
// Entry is one declared repository: where it lives here, and which way it syncs.
|
|
//
|
|
// This is data, not code. Adding a repo used to be a source edit to a list literal
|
|
// inside the script; it is now a line of config, which is what "declared state"
|
|
// has to mean if the word is doing any work.
|
|
type Entry struct {
|
|
Org string `json:"org" yaml:"org"`
|
|
Name string `json:"name" yaml:"name"`
|
|
Direction Direction `json:"direction" yaml:"direction"`
|
|
}
|
|
|
|
func (e Entry) String() string { return e.Org + "/" + e.Name }
|
|
|
|
// Planned is one entry resolved against the remote: what we declared, and what the
|
|
// remote says that name is today.
|
|
type Planned struct {
|
|
Entry
|
|
// GitHub is the repo the entry resolves to NOW. Its full name differs from the
|
|
// entry whenever GitHub has renamed or moved the repo, which is the case the
|
|
// listing-based predecessor could not see at all.
|
|
GitHub *Repo
|
|
// Moved is true when the entry's name is no longer the remote's name.
|
|
Moved bool
|
|
}
|
|
|
|
// Plan resolves every declared entry against GitHub, in the order it will be acted
|
|
// on: smallest first, so a budget spent on one enormous repo cannot starve every
|
|
// small one behind it.
|
|
//
|
|
// Two entries resolving to ONE GitHub repo is a config bug, not a merge: both would
|
|
// push-mirror to the same remote and overwrite each other. The first wins and the
|
|
// collision is reported, because silently dropping one is how a repo stops syncing
|
|
// without anyone being told.
|
|
func Plan(ctx context.Context, gh *Client, entries []Entry) ([]Planned, []error) {
|
|
var out []Planned
|
|
var errs []error
|
|
claimed := map[string]Entry{}
|
|
|
|
for _, e := range entries {
|
|
if !Owned[e.Org] {
|
|
errs = append(errs, fmt.Errorf("%s: org %q is not one we own", e, e.Org))
|
|
continue
|
|
}
|
|
if e.Direction != Native && e.Direction != Mirror {
|
|
errs = append(errs, fmt.Errorf("%s: direction %q is neither %q nor %q", e, e.Direction, Native, Mirror))
|
|
continue
|
|
}
|
|
r, err := gh.Resolve(ctx, e.Org, e.Name)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("%s: %w", e, err))
|
|
continue
|
|
}
|
|
// A transfer can move a repo anywhere, including out of our estate entirely.
|
|
// Refusing loudly beats syncing a repo that is no longer ours.
|
|
if !owned(r.FullName) {
|
|
errs = append(errs, fmt.Errorf("%s now resolves to %s, outside the orgs we own — refusing", e, r.FullName))
|
|
continue
|
|
}
|
|
if prev, dup := claimed[r.FullName]; dup {
|
|
errs = append(errs, fmt.Errorf("%s and %s both resolve to %s — one repo, one entry", e, prev, r.FullName))
|
|
continue
|
|
}
|
|
claimed[r.FullName] = e
|
|
out = append(out, Planned{Entry: e, GitHub: r, Moved: !strings.EqualFold(r.FullName, e.String())})
|
|
}
|
|
|
|
sort.SliceStable(out, func(i, j int) bool { return out[i].GitHub.Size < out[j].GitHub.Size })
|
|
return out, errs
|
|
}
|