Discover repositories in a namespace¶
Use this when your set of sources is a rule rather than a list: "every public
repository in this namespace that carries a zensical.toml". It grows as the
estate grows, with no configuration change.
Three optional capabilities serve it, and a provider may implement any subset.
Discover what you need with forge.As and fall back when it is absent.
Use forge.As, not a bare type assertion
forge.As walks a decorator chain, mirroring errors.As. A wrapper that
forwards only the four required Provider methods strips these interfaces
from a direct assertion, and since absence is a graceful fallback, that
turns discovery into a silent no-op rather than a visible failure.
Which backends implement these
The contract ships in forge v0.4.0. All four first-party providers
implement Repositories and Contents; Sites is answered by GitHub,
GitLab and Codeberg, refused by plain Gitea, and not implemented at all by
Bitbucket. See the
capability matrix.
Enumerate a namespace¶
var repos forge.Repositories
if !forge.As(provider, &repos) {
return errNoDiscovery // this backend cannot enumerate
}
all, err := forge.CollectRepositories(ctx, repos, "phpboyscout", forge.RepositoryListOptions{
IncludeSubgroups: true,
})
if errors.Is(err, forge.ErrNotSupported) {
return errNoDiscovery
}
if err != nil {
return err
}
CollectRepositories is the convenient path. To stop early, or to avoid holding
the whole namespace at once, call the method directly:
err := repos.ListRepositories(ctx, "phpboyscout", opts, func(r forge.Repository) bool {
if !interesting(r) {
return true // keep going
}
found = append(found, r)
return len(found) < 10 // false stops the enumeration
})
The error is not optional¶
A non-nil error means you hold a partial view. CollectRepositories returns
the repositories it managed to collect alongside the error so you can see how
far it got. A short list that looks complete is the failure mode this whole
API is shaped to prevent. Never treat a partial result as the namespace.
That is why enumeration takes a callback and returns an error, rather than
returning an iterator: an iter.Seq2[Repository, error] can be ranged with a
single variable, which compiles cleanly, passes go vet, and discards every
error.
Key on Path, never on anything else¶
Repository.Path is the canonical path the API answered with, which may not
be the path you asked for. Forges redirect renamed and transferred projects, so
two paths can resolve to one repository, and a directory name on disk can match
neither.
seen[r.Path] = true // right
seen[filepath.Base(dir)] = true // wrong: local names drift from canonical ones
Enumeration also guarantees containment: every Path is inside the namespace
you asked for. That is a real guarantee rather than an observation, because at
least one forge defaults to breaking it: GitLab's project listing includes
projects merely shared into a group, which carry foreign paths.
Test for a marker file without cloning¶
var contents forge.Contents
if !forge.As(provider, &contents) {
return false, errNoContents
}
data, err := contents.GetFile(
ctx, owner, repo, "zensical.toml", r.Git.DefaultBranch, forge.DefaultMaxFileSize,
)
switch {
case errors.Is(err, forge.ErrNotFound):
return false, nil // no marker: does not qualify, and that is not an error
case err != nil:
return false, err // could not check: retry, do NOT exclude
}
Use forge.SplitRepoPath(r.Path) to get owner and repo. It splits on the
last separator, which matters on forges with nested namespaces. Splitting on the
first instead requests a repository that does not exist, and the resulting 404 is
indistinguishable from the file being absent.
maxBytes is required, and the provider enforces it. Pass
forge.DefaultMaxFileSize unless you have a reason not to.
Treat visibility as a security boundary¶
If your predicate decides what gets published, republished or answered from, a loose visibility check is a disclosure rather than a bug.
// right: allowlist
if r.Visibility != forge.VisibilityPublic {
continue
}
// wrong: admits VisibilityUnknown AND VisibilityInternal
if r.Visibility != forge.VisibilityPrivate { … }
VisibilityUnknown is the zero value and is reachable in practice, because a provider
that cannot determine visibility reports it rather than guessing. internal
exists on GitLab and is not public.
Two further rules, neither of which forge can enforce for you:
- Evaluate exclusions first, and absolutely. A repository excluded by policy must never be reinstated by satisfying the predicate. Applied after discovery, an exclusion is one that can be forgotten.
- Re-verify every cycle. Visibility and namespace membership change. Do not cache the verdict indefinitely; a repository made private must leave your set on the next refresh.
Ask whether a site is published¶
var sites forge.Sites
if !forge.As(provider, &sites) {
return checkReachable(derivedURL) // fall back to your own probe
}
site, err := sites.GetSite(ctx, owner, repo)
switch {
case errors.Is(err, forge.ErrNotSupported), errors.Is(err, forge.ErrNotFound):
return false, nil
case err != nil:
return false, err // a permission failure is NOT "no site"
}
Before paying for that call, check the free signal. r.Site comes from the
enumeration payload:
SiteUnknown does not mean "no site". It means the provider could not say
from the listing alone.
Reachability is yours¶
Neither SiteStatus nor SiteState promises a browser gets a 200. SitePublished
means configured; SiteStateBuilt means the last build succeeded. If your rule
requires a reachable site, make the HTTP request yourself.
Check Site.URLSource before concluding anything from a failed probe:
SiteURLDerived: composed from a template rather than reported by the forge, so a custom domain living in DNS makes it the wrong address. A failed probe means unknown, not absent.SiteURLGenerated: the forge's own generated host. It resolves, but it is not necessarily where the project publishes.SiteURLCanonical: the project's configured domain. This is the one to cite.SiteURLUnknown: the provider did not say, so treat provenance as unknown rather than assuming any of the above.
Site.URL is already the canonical address where the forge reports one, so you
do not need to prefer anything yourself, because URLSource tells you which you got.
If you are citing URLs, check Site.TLS too. Where it is SiteTLSNotEnforced
the URL may be http://, because the provider reports the scheme the forge
recorded rather than guessing at TLS the forge will not promise. Upgrading it is
your policy call to make, not the provider's.
Pin credentials before cloning¶
Repository.Git.CloneURL, Repository.URL and Site.URL are API-reported data,
not constants. Attaching a credential to one without checking is the same mistake
credential pinning exists to prevent:
Related¶
- Optional capabilities: why type assertion
- Author a provider: implementing these
- Providers: which backends support what
- Credential pinning: the threat model