On this page
The assembly's model: the roster a site declares, the files each project published into it, and where a build's output lands once it has been grafted in.
#internal/blog/site
#internal/blog/site
Package site carries the assembly's model: what the unified documentation site declares, what each project published into it, and where a build's output lands once it is grafted in.
The assembly is one Cloudflare Pages site serving many projects. Every project deploys its own documentation into a subtree of it, one declared project is served at the site root, and posts from every project share a single site-level blog. Three questions follow from that arrangement, and this package answers all three:
- Which projects does the site serve? The roster ([Roster], [ParseRoster]) is the declaration, hand-edited in the assembly repository; the deploy reconciles the tree to it ([ReconcileMembership]) and can never add to it. - Which files does each project own? The published-file record ([ParseFilesManifest], [PrunePlan]) names them per publisher, so a release prunes only what it published before and does not publish now. - Where does a built file go? [SplitBuildOutput] decides: a post to the site-level blog, a home project's page to the site root, everything else under the project's slug.
Nothing here touches the network or the GitHub API. The operations that do -- the dispatch, the integrate, the Git Data API publishers, the verification fetches -- import this package rather than the reverse, which is what keeps the model readable and testable without a remote.
#OutboundPath
const OutboundPath = "outbound.toml"OutboundPath is the declared list of pages whose outbound links are checked. Absent means outbound checking is not configured, which the report says out loud rather than passing quietly.
#OutboundCachePath
const OutboundCachePath = "outbound-cache.json"OutboundCachePath is where the deploy keeps outbound results between runs.
#RosterHeader
const RosterHeader = `# The assembly's membership: every project the unified site serves.RosterHeader is the comment block [RenderRoster] writes above the declaration.
#FilesRecordVersion
const FilesRecordVersion = 2FilesRecordVersion is the published-file record's format.
Version 2 addresses every path from site/ rather than from the project's own subtree, because posts stopped living inside it: they are site-level, at blog/
#ChromeDir
const ChromeDir = "_chrome"ChromeDir is the site-level directory holding the shared chrome assets -- the stylesheet and scripts every project's pages address.
It is declared here rather than in the chrome package because [SiteReservedDirs] needs it and this package sits below chrome in the import graph.
#WorkflowPath
const WorkflowPath = ".github/workflows/deploy.yml"WorkflowPath is the one path in the assembly repo that holds the generated deploy workflow.
#RosterPath
const RosterPath = "roster.toml"RosterPath is the hand-edited file in the assembly repo that declares which projects the unified site serves.
#ProjectsPath
const ProjectsPath = "projects.json"ProjectsPath is the derived record of what each declared project last deployed.
#OutboundTables
var OutboundTables = []string{"page"}OutboundTables is every table the outbound declaration may carry.
#OutboundKeys
var OutboundKeys = []string{"cache_days", "page"}OutboundKeys is every top-level key the outbound declaration may carry.
#OutboundPageKeys
var OutboundPageKeys = []string{"path"}OutboundPageKeys is every key a [[page]] block may carry.
#HomeReservedDirs
var HomeReservedDirs = []string{"blog", "projects", "v", "pagefind"}HomeReservedDirs are the directory names at the site root the generator owns. A home project page emitting into one of them would be overwritten by, or would overwrite, the assembly's own listing, blog or archive space.
#HomeDroppedArtifacts
var HomeDroppedArtifacts = append([]string{HomeDroppedArtifacts are the files at the site root the assembly generates for the whole site on every deploy.
A project's own build writes its own copy of each at its own output root, for its own standalone hosting; for a project under site/
"projects.toml" is the curated listing's source document, which the build copies through as a static asset. The site serves the two renderings of it, not it.
#HomeDroppedDirs
var HomeDroppedDirs = []string{"pagefind"}HomeDroppedDirs are the directories at the home project's output root whose machine-written contents the assembly writes itself.
The home project's pages are at the site root, so its own Pagefind index lands where the site-wide one belongs -- and the site-wide one, written over the whole assembled tree, is the index those pages must answer from. Every other project keeps its own index inside its subtree, which is what its pages address.
Only the indexer's own files are dropped. An .html page under one of these directories is content -- a home page called pagefind.md builds to pagefind/index.html -- and stays a refused collision rather than disappearing quietly.
#RosterFields
var RosterFields = []string{"slug", "repo"}RosterFields is every key a [[project]] block may carry, all of them required. An unknown key is a hard error rather than a silently ignored line: a typo in a membership declaration would otherwise retire a project.
#RosterTopLevelKeys
var RosterTopLevelKeys = []string{"home", "project"}RosterTopLevelKeys is every top-level key the roster document may carry. "home" names the one project whose content root is the site root; see [Roster].
#DeployArtifactNames
var DeployArtifactNames = []string{"_headers", "_redirects", "_worker.js", "404.html"}DeployArtifactNames are the files a per-project selfdoc build emits for its own standalone hosting.
They are meaningless (and actively harmful) once the build is grafted into the assembly tree, which serves one set of headers, redirects, worker and not-found page for the whole site.
"404.html" belongs here for a reason of its own: the provider answers an unmatched address from the root of what it serves, so a copy buried in a project's subtree is never reached by anything. A mounted project stops emitting one; this filter is what keeps the ones already published from surviving into the tree, where they are unreachable pages that still have to satisfy every assertion made about a page.
#DeployArtifactSuffixes
var DeployArtifactSuffixes = []string{".gz", ".br"}DeployArtifactSuffixes are the pre-compressed copies a standalone build writes beside every asset.
#PublishOwners
var PublishOwners = []string{"release", "docs", "posts"}PublishOwners is who may have written a path inside a project's site subtree. Every publisher records the set of paths it produced, and prunes only paths it produced before and does not produce now -- see [PrunePlan].
release the full-scope integrate the deploy workflow runs from a tag docs the documentation publish, a documentation update with no release posts the post publish and the posts-scope integrate
The point of separating them is that a full build no longer knows how to destroy content it never produced: an out-of-band post or documentation page belongs to another owner, so a release that does not carry it leaves it alone.
#ManifestSidecarSuffixes
var ManifestSidecarSuffixes = []string{"-revisions.json", "-files.json", "-listing.json"}ManifestSidecarSuffixes are the files under manifests/ that are not project manifests and must not be loaded as one.
#SiteReservedDirs
var SiteReservedDirs = []string{"blog", "projects", "pagefind", ChromeDir}SiteReservedDirs are the directories under site/ that belong to the assembly itself rather than to any project, so membership reconciliation never mistakes one for a slug.
#IntegrateScopes
var IntegrateScopes = []string{"full", "posts", "shared-only"}IntegrateScopes are the scopes a dispatch may carry. "" from a client payload that omits the key means a full project build; the workflow always passes the flag.
#MembershipFields
var MembershipFields = []string{"repo", "ref", "version"}MembershipFields is every field an assembly integrate run records for a project in projects.json. A rebuild replays that record, so all three have to be there.
#RemoteTextFetcher
type RemoteTextFetcher func(repo, path, operation string) (string, error)RemoteTextFetcher reads the text of a path on a repository's default branch.
[StagePublishedRecord] takes one rather than calling the GitHub API itself: that is the one read in this whole package that would need a remote, and keeping it a parameter is what lets the model stay free of the network while the publisher that owns the remote supplies its own reader. An absent file is the empty string, and every other failure is an error -- a caller that read a failure as "nothing published yet" would write a record erasing what it could not read.
#ReconcileSummary
type ReconcileSummary structReconcileSummary names what a reconciliation retired and every path it removed.
#OutboundConfig
type OutboundConfig structOutboundConfig is the declared outbound check: which pages, and for how long.
Both fields are declared, neither has a default: which pages are worth the requests is a judgement about the site, and how long a result is trusted is a judgement about how fast its links rot.
#Collision
type Collision structCollision is one address a home project claimed that the assembly owns, and why the assembly owns it.
#RosterEntry
type RosterEntry structRosterEntry is one declared member of the assembly.
Repo is part of the declaration rather than derived from a dispatch so that a slug has one owning repository on record: a dispatch arriving for a declared slug from a different repository is a hard error instead of a silent takeover of that slug's section.
#Roster
type Roster structRoster is the declared membership, plus the one project that is the site root.
A roster is read as a mapping of slug -> [RosterEntry] everywhere membership is the question, which is most places. Home is the extra fact only the front page cares about: one declared slug whose content root emits at the site root rather than under site/
The home project is an ordinary project in every other respect -- a real repository that dispatches its own deploys. Being home is a flag on it, never a separate kind of thing and never the assembly repository itself.
#Error
type Error structError is the failure every operation in this package reports: a refused declaration, an unreadable record, a collision with an address the assembly owns, and every other hard error the Python surface raised as a RuntimeError or a ValueError.
It is the one error type a caller needs to recognize with errors.As to render an assembly refusal distinctly from an unexpected internal failure.
#FilesManifestPath
func FilesManifestPath(manifestsDir, slug string) stringFilesManifestPath returns the path of slug's published-file record.
#ParseFilesManifest
func ParseFilesManifest(raw string, source string) (map[string][]string, error)ParseFilesManifest returns owner -> published paths from the record text raw, naming source in every diagnostic.
Empty text is an empty mapping: nothing has published anything yet, so there is nothing anybody is entitled to remove. Everything else is strict, because a record read wrong hands paths to the wrong publisher: malformed JSON, an unknown publisher and a path list that is not a list of strings are each a hard error, and so is a record written in the version-1 format, whose paths meant something else.
#LoadFilesManifest
func LoadFilesManifest(path string) (map[string][]string, error)LoadFilesManifest returns owner -> published paths from the record at path.
An absent record is an empty mapping, which is not a fallback but the real initial state: nothing has published anything for this project yet.
#RenderFilesManifest
func RenderFilesManifest(slug string, owners map[string][]string) (string, error)RenderFilesManifest returns the JSON text of slug's published-file record.
Every path is relative to site/: the project's own pages are under
#StagePublishedRecord
func StagePublishedRecord(StagePublishedRecord records what owner now publishes for slug and returns the repo-relative paths it deletes.
The one path by which a publisher that writes through the Git Data API -- the documentation publish and the post publish -- keeps the published-file record honest. It reads the record on repo through fetch, prunes owner's entry against produced (site-relative paths), and stages the rewritten record in files alongside whatever else that publisher is pushing, so the record and the content it describes reach the same commit.
Without this, a publish leaves its files unclaimed: nothing accounts for them, the ownership-prune model cannot protect them from another publisher, retirement does not take them along, and the cross-project write refusal ([ForeignPostClaims]) cannot see them at all.
#PrunePlan
func PrunePlan(PrunePlan returns the paths owner must remove, and the updated owners map.
This is the whole of "prune instead of wipe". A publisher removes a path only when it published that path before and does not publish it now -- so a page a build dropped disappears, while content the build never produced is untouched, because nothing entitles this publisher to it. A path another publisher currently claims is never removed either: a documentation page or a post published between releases outlives a full build that happens not to carry it.
#GitBlobSHA1
func GitBlobSHA1(data []byte) stringGitBlobSHA1 returns git's object id for a blob holding data.
Git hashes "blob
#MergePostLists
func MergePostLists(basePosts, overlayPosts []any) []anyMergePostLists returns the union of a build's post list and the overlay's, by slug.
The build wins on a slug both carry -- it just re-rendered that post from the tag it was released at. What the overlay contributes is the posts the build does not carry at all: the ones published between releases.
This runs when a full build is grafted, not when the assembly is read: the overlay stays the one authority on a project's posts, and stays a complete list, so republishing after deleting a post still removes it from the site.
#LoadAssemblyManifests
func LoadAssemblyManifests(manifestsDir string) ([]map[string]any, error)LoadAssemblyManifests returns the assembly's per-project manifests with post overlays applied.
The -posts.json files are overlays written by the post publish: they carry a complete post list for their slug and replace the base manifest's posts array, which is how deleting a post and republishing removes it from the site. A full build folds its own posts into the overlay when it is grafted (see [MergePostLists]), so replacing here never hides a release's posts behind an older overlay. The -revisions.json and *-files.json sidecars are not manifests and are skipped.
The documents come back as decoded JSON rather than as typed manifests: the assembly reads fields the typed reader drops and writes the documents back out, so a tolerant read that discarded unknown keys would lose them. [manifest.Compat] still runs on every one, which is what refuses a document in a format this reader does not know.
#IsManifestDocument
func IsManifestDocument(name string) boolIsManifestDocument reports whether a file name under manifests/ is one of the per-project manifests rather than a sidecar.
The rule is one place because two readers ask it: the directory read, and the reader that asks a remote assembly for the same set over the Git Data API and has no directory to walk.
#ManifestsFromDocuments
func ManifestsFromDocuments(ManifestsFromDocuments returns the assembly's per-project manifests, decoded from documents keyed by their file name under manifests/, with post overlays applied.
This is what [LoadAssemblyManifests] is in terms of, and what a caller that read the same documents off a remote assembly calls with the bytes it fetched. source names where the documents came from, for the diagnostics.
#ListingSidecarPath
func ListingSidecarPath(manifestsDir, homeSlug string) stringListingSidecarPath is where the assembly keeps the home project's curated listing.
#LoadListingFor
func LoadListingFor(manifestsDir, homeSlug string) (*listing.Listing, error)LoadListingFor returns the home project's curated listing.
The listing is authored in the home project as its own projects.toml and copied here by that project's deploy. A roster that declares a home declares its listing with it, so the sidecar's absence is not a state to render around -- it means the step that should have pushed the file did not run, and the honest answer is to say which file and which step rather than to publish a listing nobody curated at the curated one's address.
Returns nil only when the tree declares no home project at all, which the deploy path never does: the roster requires one.
#HomePagePaths
func HomePagePaths(manifestsDir, homeSlug string) ([]string, error)HomePagePaths returns every site-relative HTML page the home project published.
Read from the published-file record rather than guessed from the tree: the home project's pages sit at the site root beside other projects' directories and the generated artifacts, so "which files are the home project's" is a question only its own record answers.
#HomeOwnedRootNames
func HomeOwnedRootNames(manifestsDir, homeSlug string) (map[string]bool, error)HomeOwnedRootNames returns the top-level names under site/ the home project published.
The home project's pages are at the site root, so its directories sit beside the other projects' subtrees. Membership reconciliation and the roster check both walk those directories looking for projects, and without this they would read the home project's cv/ as an undeclared project and delete it.
#LoadProjectsJSON
func LoadProjectsJSON(path string) (map[string]any, error)LoadProjectsJSON returns the derived membership record at path.
A malformed file is a hard error rather than a fresh empty mapping: rewriting it would silently drop every other project's record.
#RenderProjectsJSON
func RenderProjectsJSON(data map[string]any) (string, error)RenderProjectsJSON returns the JSON text of a derived membership record.
#RecordMembership
func RecordMembership(RecordMembership records what slug just deployed and returns the new mapping.
projects.json is derived state: it records what each declared project last deployed, and a deploy can only write a record for a slug the roster declares. Membership therefore cannot grow as a side effect of a dispatch -- an undeclared slug is refused, naming the file that would have to declare it.
#ManifestFilesFor
func ManifestFilesFor(manifestsDir, slug string) ([]string, error)ManifestFilesFor returns every file under manifestsDir that belongs to slug.
"Belongs" is the base manifest plus every kind sidecar -- the posts overlay, the revisions sidecar, the published-file record, and anything added later, since the rule is the "
#ReconcileMembership
func ReconcileMembership(ReconcileMembership removes every trace of a project the roster no longer declares.
A project drops out of the assembly by leaving the roster, and this is what leaving costs it: its site subtree, every one of its manifest kinds, its derived membership record, and -- because the search index is rebuilt from scratch whenever anything went -- its entries in the index.
It takes the whole [Roster] rather than the mapping alone, because which project is home decides which directories under site/ are project subtrees at all: the home project's pages sit at the site root, so telling its directories from a project subtree needs to know which project is home.
#ParseOutbound
func ParseOutbound(text string, source string) (OutboundConfig, error)ParseOutbound returns the outbound declaration in text, naming source in every diagnostic.
Strict in every direction: an unknown top-level key, an unknown key on a [[page]] block, a missing or empty path, a repeated path and a non-positive cache_days are each a hard error naming the offending declaration. A file with no [[page]] block is a hard error too -- an empty declaration is not a way of saying "check nothing", it is a file somebody forgot to finish.
#LoadOutbound
func LoadOutbound(assemblyDir string) (*OutboundConfig, error)LoadOutbound returns the outbound declaration, or nil when there is no file.
nil is not a default -- it is "this assembly has not configured outbound checking", which the report states out loud.
#LoadOutboundCache
func LoadOutboundCache(assemblyDir string) (map[string]any, error)LoadOutboundCache returns the outbound result store, keyed by address.
An absent store is an empty one: nothing has been checked yet. A malformed one is a hard error -- silently starting over would refetch every link on every deploy and never say why.
#RenderOutboundCache
func RenderOutboundCache(entries map[string]any) (string, error)RenderOutboundCache returns the JSON text of an outbound result store.
#BuildTargetVersion
func BuildTargetVersion(cfg map[string]any, source string) (string, error)BuildTargetVersion returns the version a selfdoc build of config would produce.
This is the one definition of "the version being built", and both the build ([DetectLatestVersion]) and the dispatch check ([CheckVersionIsDeclared]) read it, so they cannot disagree.
Empty when the project declares no versions at all -- a single implicit version, which a selfdoc build handles without a version flag. A declared versions array whose newest entry carries no version string is a hard error however long the array is: the build takes the last entry, so a blank newest entry would silently publish the docs unversioned, at the wrong address.
source is what to name in the error message (a directory, usually).
#DetectLatestVersion
func DetectLatestVersion(sourceDir string) (string, error)DetectLatestVersion returns the newest version declared by a source project's config.
A project with no selfdoc.json at all builds unversioned; see [BuildTargetVersion] for everything else.
#PruneDeployArtifacts
func PruneDeployArtifacts(root string, handle *effects.Handle) ([]string, error)PruneDeployArtifacts deletes per-project deploy artifacts under root and returns their paths, sorted.
A project build emits _headers, _redirects, _worker.js and pre-compressed .gz / .br copies for its own standalone hosting. Inside the assembly those files would fight the site-wide ones the shared generator writes.
#IsDeployArtifact
func IsDeployArtifact(name string) boolIsDeployArtifact reports whether a file name is a per-project deploy artifact.
#BuildOutputPaths
func BuildOutputPaths(root string, skipArtifacts bool) ([]string, error)BuildOutputPaths returns every file under root as a "/"-joined relative path, sorted.
This is the "what the build produces" set the prune is driven by, so with skipArtifacts it excludes the per-project deploy artifacts: those are filtered out on the way in and must not be recorded as though the assembly served them. Pass true at every call site that models a graft.
#PruneEmptyDirs
func PruneEmptyDirs(root string, handle *effects.Handle) ([]string, error)PruneEmptyDirs removes empty directories under root and returns the ones removed.
root itself always survives, even when the subtree ends up empty: the project still has a section, it just has no files in it.
The order is every directory path ascending, so a parent is examined before its children and a nest of empty directories collapses one level per call. That is what the Python did -- it sorted a bottom-up walk back into path order -- and a deploy runs this after every prune, so the nest empties out over the deploys that produced it.
#HomeCollisions
func HomeCollisions(siteRels []string) []CollisionHomeCollisions returns one [Collision] per reserved address in siteRels.
The home project emits at the site root, where the assembly's own generated pages live. A page called projects.md builds to projects/index.html, which is the generated project listing's address; one of the two would silently win. Neither does: the collision is refused, at the graft and again at verification.
Only the reserved directories can be refused this way, and they are the whole rule: a name in [HomeDroppedArtifacts] never reaches a graft to be checked, because every selfdoc build writes those for its own standalone hosting and the assembly writes the ones the site serves.
#CheckHomeCollisions
func CheckHomeCollisions(siteRels []string, slug string) errorCheckHomeCollisions returns an error when the home project claims an address the assembly owns.
#SplitBuildOutput
func SplitBuildOutput(buildRels []string, slug string, home bool) map[string]stringSplitBuildOutput maps each file a build produced to where the assembly serves it.
A project's build output lands in two places, and this is the rule that decides which:
- blog/
home is the one project the roster names home. Its documentation is not filed under a slug at all: the site root IS its content root, so index.html lands at site/index.html and cv/index.html at site/cv/index.html, beside the generated blog/ and projects/. Its posts follow the same site-level rule as everybody else's, and the site-wide artifacts its own build wrote for standalone hosting ([HomeDroppedArtifacts] and [HomeDroppedDirs], plus every compressed variant) are left behind -- the assembly writes the ones the site serves.
Returns build-relative path -> site-relative path, with the skipped standalone blog index simply absent.
#GraftSubtree
func GraftSubtree(GraftSubtree copies produced out of src into dest and deletes removed from it.
produced maps a source-relative path to the destination-relative path it lands at, because a build's output no longer reaches one place: its posts go to the site-level blog and everything else to the project's own subtree. removed is destination-relative, in the same addressing the published-file record uses.
#ClaimedSitePaths
func ClaimedSitePaths(manifestsDir, slug string) ([]string, error)ClaimedSitePaths returns the paths slug published outside its own subtree.
Its documentation goes with site/
#ForeignPostClaims
func ForeignPostClaims(manifestsDir, slug string) (map[string]string, error)ForeignPostClaims maps every site-level post path other projects claim to its claimant.
The manifest merge refuses two projects publishing the same post slug, but that refusal reads manifests; this one reads the published-file records, so the write itself can be refused too. Both are needed: a graft happens before the manifests are merged, and it is the graft that would overwrite the other project's file.
#RefuseForeignPostOverwrite
func RefuseForeignPostOverwrite(slug string, produced []string, claims map[string]string) errorRefuseForeignPostOverwrite returns an error if any of produced is a post path claims gives to someone else.
One refusal for every publisher: the integrate graft, which reads the records out of the assembly clone, and the two Git Data API publishers, which read them off the remote. Sharing the wording is the point -- three copies of a refusal are three chances for one of them to be quietly weaker than the others.
produced is site-relative, in the same addressing the published-file record uses, and claims maps such a path to the project that claims it.
#ProjectPaths
func ProjectPaths(paths []string, slug string, claimed []string) []stringProjectPaths returns every assembly path that belongs to slug.
That is its whole site subtree, plus every one of its manifest kinds -- the base manifest, the posts overlay, the revisions sidecar and the published-file record -- plus claimed, the site-relative paths its published-file record names outside that subtree. Its posts are all of the last kind: they sit at the site level under blog/, so removing the subtree alone would leave them on the blog with nothing left to explain where they came from.
#CollectSiteFiles
func CollectSiteFiles(outputDir, slug string, home bool) (map[string][]byte, error)CollectSiteFiles returns assembly path -> bytes for every file a local build produced.
Content travels as bytes because a documentation site is not all text: fonts, favicons and screenshots go through the same commit as the HTML, and decoding them as UTF-8 on the way past would destroy them. The same per-project deploy artifacts the deploy filters out are filtered here -- see [BuildOutputPaths] -- and the output is split the same way a deploy splits it, so a locally built post lands on the site-level blog rather than inside the project's subtree, and the home project's pages land at the site root. See [SplitBuildOutput].
#HomeListingSidecar
func HomeListingSidecar(sourceDir, slug string) (string, []byte, error)HomeListingSidecar returns where the home project's curated listing belongs in the assembly and the bytes to put there.
The listing is authored in the home project ("docs/projects.toml") because it is content, and it is copied into the assembly because both renderings of it -- the front page's cards and the generated "/projects/" page -- are produced on every deploy, including deploys the home project has nothing to do with.
A home project that declares no listing is a real state and returns an empty path; a malformed one is a hard error naming the file, reported here rather than at the far end where the document is no longer in reach.
#NewRoster
func NewRoster(projects map[string]RosterEntry, home string) *RosterNewRoster builds a [Roster] over a copy of projects.
#RenderRoster
func RenderRoster(entries []RosterEntry, home string) stringRenderRoster returns the TOML text for entries.
home is written as the top-level "home" key. An empty home leaves a commented placeholder instead: a roster with no home is refused when it is read, and a scaffolded file that silently named some project home would be choosing the front page on the author's behalf.
#ParseRoster
func ParseRoster(text string, source string) (*Roster, error)ParseRoster returns the [Roster] the roster document text declares, naming source in every diagnostic.
Validation is strict in every direction: an unknown top-level table, an unknown key on a block, a missing or empty required key, a duplicate slug, and a slug that collides with one of the assembly's own directories are each a hard error naming the offending declaration. A roster with no [[project]] block at all is legal and means an empty assembly.
The "home" key is required and names a declared slug. Both failures are hard errors: a missing key because a site needs a front page and no project may be picked for the author by default, and a key naming an undeclared slug because the front page has to be something the assembly actually serves.
#MissingRosterError
func MissingRosterError(path string) error { return missingRosterError(path) }MissingRosterError returns the refusal a missing roster at path earns.
It is exported because the remote roster read raises the same refusal for a roster that is not on the assembly repository, and one wording for the two is the point.
#LoadRoster
func LoadRoster(assemblyDir string) (*Roster, error)LoadRoster returns the roster declared in assemblyDir, or an error when it is absent.
#GenerateRedirectsFile
func GenerateRedirectsFile(slug, docsBase string) stringGenerateRedirectsFile returns the content of a Cloudflare Pages _redirects file that sends every path of the old per-project site to the assembly site under the project's slug prefix.
slug is the project's URL path segment (for example "selfdoc"); docsBase is the base URL of the assembly site (for example "https://docs.smmh.dev").
#ParseVersionTag
func ParseVersionTag(tag string) (family, version string, ok bool)ParseVersionTag splits tag into its family prefix and its version, reporting false when it is not a version tag at all.
"v1.2.3" -> "", "1.2.3" "[email protected]" -> "demo@", "0.3.1" "mypkg/v2.0.0-rc.1" -> "mypkg/", "2.0.0-rc.1"
#ResolveProjectTag
func ResolveProjectTag(tags []string, version string) (string, error)ResolveProjectTag returns the tag that names version for this project.
Tag resolution used to be "the repository's newest tag by creation date", which is wrong in any repo that releases more than one thing: a sibling package released an hour later owns the newest tag, and the assembly then builds that sibling's ref under this project's slug. That shipped a 404 stub to the live site once.
The version being dispatched decides instead: the tag has to carry that version, whatever family prefix it wears. Two families at the same version is a hard error rather than a coin flip, and a version with no tag is a hard error rather than a fallback onto something newer.
#ListRepoTags
func ListRepoTags(cwd string, handle *effects.Handle) ([]string, error)ListRepoTags returns the repository's tags, newest creation date first.
#CheckVersionIsDeclared
func CheckVersionIsDeclared(cfg map[string]any, version string) errorCheckVersionIsDeclared returns an error unless version is the version the build will actually produce.
Membership in the versions array was never the question. The assembly builds the last entry and records the dispatch under the version the payload carries, so dispatching a version that is merely present in the array publishes the newest version's docs under the dispatched version's name -- silently, and for as long as nobody compares the two. The two have to be the same version, and [BuildTargetVersion] is what says which one the build produces.
#VersionLabel
func VersionLabel(version string) stringVersionLabel renders a version the way a summary line names it.
A released version reads as "v1.2.3"; the unversioned literal reads as "(unversioned)", because "vunversioned" names nothing and a summary that prints it invites the reader to look for a release under that name.
#Roster.Get
func (r *Roster) Get(slug string) (RosterEntry, bool)Get returns the entry declared for slug, reporting whether the roster declares it at all.
#Roster.Has
func (r *Roster) Has(slug string) boolHas reports whether the roster declares slug.
#Roster.Len
func (r *Roster) Len() int { return len(r.projects) }Len is the number of declared projects.
#Roster.Slugs
func (r *Roster) Slugs() []stringSlugs returns every declared slug, sorted.
#Roster.Entries
func (r *Roster) Entries() map[string]RosterEntryEntries returns a copy of the declared mapping, for the call sites that take membership alone -- [RecordMembership] among them.
#Roster.String
func (r *Roster) String() stringString renders the roster the way the Python surface's repr did.
#Error.Error
func (e *Error) Error() string { return e.Message }Error returns the diagnostic.