On this page
The assembly's network-facing half: the generated deploy workflow, its toolchain pins, the dispatches it sends, and the build-and-graft a deploy runs.
#internal/blog/assembly
#internal/blog/assembly
Package assembly carries the assembly's operations: the deploy workflow it generates, the dispatches it sends, the build-and-graft body that deploy runs, and the two publishers that write into it without cloning it.
The model half -- the roster, the published-file records, the graft rule, membership reconciliation, the tag resolution -- is [github.com/smm-h/selfdoc/internal/blog/site], which touches neither the network nor the GitHub API. This package is everything that does:
- The generated workflow. [GenerateWorkflowYAML] renders the one file the assembly repository holds at [site.WorkflowPath], pinned to an exact toolchain ([ToolchainPins]) that [CheckPinsArePublished] refuses unless both registries actually serve it. [AssemblyInit] is that file plus the three others a fresh assembly repository starts life with. - The dispatches. [AssemblyPush] and [AssemblyRebuild] return the endpoint and payload of a repository_dispatch event; the command layer sends them. [AssemblyStatus] returns the argv that reads recent runs. - The deploy body. [IntegrateProject] is what the workflow's one integrate step runs: build the cloned source project, graft it in, reconcile membership, regenerate the shared cross-project files, index, verify, commit and push -- inside a retry loop that re-syncs to the remote every attempt so two concurrent deploys converge instead of clobbering each other. - The Git Data API publishers. [PushFilesToRepo] writes a commit without a clone, uploading only the blobs whose bytes differ; [PublishProjectDocs] and [RetireProject] are the two operations built on it, and [FetchRemoteText] is how they read the assembly they never cloned -- with absence and failure kept apart, which is what [RemoteReadError] exists for.
Every subprocess launch and every write goes through an explicit [github.com/smm-h/selfdoc/internal/effects.Handle], passed in by the caller. The two registry reads are plain GETs: they change nothing, so they do not go through the handle -- a recorded read would have nothing to record and a preview still needs the answer.
#DispatchEventType
const DispatchEventType = "project-updated"DispatchEventType is the repository_dispatch event type the generated workflow listens for.
#SharedOnlyScope
const SharedOnlyScope = "shared-only"SharedOnlyScope is the scope a dispatch carries when it asks for the cross-project elements to be regenerated and nothing else.
It is what "post publish", "docs publish" and "assembly retire" send after their own commit: each writes content through the Git Data API, which no workflow ran, so the listing, the feed, the sitemap and the search index are stale until a deploy regenerates them.
#DefaultAttempts
const DefaultAttempts = 3DefaultAttempts is how many times a deploy re-syncs with the remote and retries the push before failing.
#DefaultRetryDelay
const DefaultRetryDelay = 5 * time.SecondDefaultRetryDelay is how long a deploy waits between attempts.
#DefaultBranch
const DefaultBranch = "main"DefaultBranch is the assembly branch the deploy commits and pushes to.
#DefaultGitUserName
const DefaultGitUserName = "github-actions[bot]"DefaultGitUserName is the identity the deploy's commit carries.
#DefaultGitUserEmail
const DefaultGitUserEmail = "github-actions[bot]@users.noreply.github.com"DefaultGitUserEmail is that identity's address.
#PushGrant
const PushGrant = "assembly-commit"PushGrant is the grant on the running command that authorizes the deploy's push to the assembly's branch -- the content the live site serves.
#PyPIJSONURL
const PyPIJSONURL = "https://pypi.org/pypi/{package}/json"PyPIJSONURL is PyPI's JSON metadata endpoint, the registry the pagefind pin comes from. A pin is checked against it before it is written into a workflow -- see [CheckPinsArePublished].
#GoProxyInfoURL
const GoProxyInfoURL = "https://proxy.golang.org/github.com/smm-h/selfdoc/@v/v{version}.info"GoProxyInfoURL is the Go module proxy's version-info endpoint for this module, the registry the selfdoc pin comes from.
The selfdoc pin names a Go module version rather than a PyPI distribution: the workflow installs the binary with "go install github.com/smm-h/selfdoc@v
#GoModulePath
const GoModulePath = "github.com/smm-h/selfdoc"GoModulePath is the module the generated workflow installs the binary from. The entry point is the module root, so installing the module installs the binary and the last path element names it.
#WorkflowGoVersion
const WorkflowGoVersion = "1.26"WorkflowGoVersion is the Go toolchain the generated workflow sets up before it installs the selfdoc binary.
A major.minor line rather than a patch: setup-go resolves it to the newest patch, which is what a fresh install of a Go program wants. The pinned thing is the program, not the compiler that builds it.
#WorkflowPythonVersion
const WorkflowPythonVersion = "3.12"WorkflowPythonVersion is the Python the generated workflow sets up before it installs pagefind, which is published as a Python distribution carrying a bundled binary.
#Dispatch
type Dispatch structDispatch is one repository_dispatch event: the endpoint it is POSTed to and the fields the workflow reads out of it.
A project dispatch names Slug, Version, Ref and Repo and leaves Scope empty, which the workflow reads as a full build. A shared-elements dispatch names Scope alone. [Dispatch.PayloadJSON] renders whichever of the two this is.
#Error
type Error structError is the failure most operations here report: a refused pin, an incomplete membership record, a step that exited non-zero, a tree that failed verification, 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. The one failure kept separate is [RemoteReadError], because a read that did not succeed and did not 404 must never be mistaken for an absent file.
#PushResult
type PushResult structPushResult is what one [PushFilesToRepo] call did.
Changed is false when every file already matched the branch and nothing was deleted -- no commit exists in that case and SHA is the untouched head. A caller that regenerates an artifact on every release reads this to tell "wrote it" from "it was already right".
#GraftOptions
type GraftOptions structGraftOptions is what one [ApplyProjectFiles] takes.
#VerifyOptions
type VerifyOptions structVerifyOptions is what one [VerifyBeforeDeploy] takes.
#IntegrateOptions
type IntegrateOptions structIntegrateOptions is what one [IntegrateProject] takes.
#IntegrateSummary
type IntegrateSummary structIntegrateSummary is what one [IntegrateProject] run did.
#ToolchainPins
type ToolchainPins structToolchainPins is the exact versions the generated deploy workflow installs.
Both tools on the install line are pinned, for one reason that applies equally to each: the deploy installs its toolchain fresh on every dispatch, so an unpinned name means "whatever was newest at dispatch time". A released flag change once broke every project's deploy at once that way.
The pins are not the banned kind of ceiling: "assembly sync-workflow" rewrites the whole file on every release, so this is a regenerated lock, not an upper bound a human wrote once and forgot.
One selfdoc version covers the whole toolchain now. The Python installed two distributions -- the docs generator and the former blog package -- and pinned each; one binary carries both, so there is one pin and one flag for it.
Every field is required. [ResolveToolchainPins] is what turns an environment into a set of pins; this type only carries them.
#PyPIFetcher
type PyPIFetcher func(pkg string) (map[string]any, error)PyPIFetcher returns PyPI's JSON metadata document for a distribution.
#GoModuleProbe
type GoModuleProbe func(version string) (bool, error)GoModuleProbe reports whether the Go module proxy serves a selfdoc version.
The two outcomes are kept apart from the error: false means the proxy answered and does not have that version, and an error means the question could not be asked at all. A probe that cannot answer is never read as "published".
#Registry
type Registry structRegistry is how the pin checks reach the two registries a pin comes from.
A nil field selects the real reader -- [FetchPyPIMetadata] and [GoModuleVersionExists]. That is explicit mode selection, not a fallback: the choice is made once, before anything runs, and nothing here ever tries the network, fails, and answers from somewhere else.
#PinOptions
type PinOptions structPinOptions is what [ResolveToolchainPins] takes.
A named version is taken verbatim. PagefindVersion is "" for "resolve it"; SelfdocVersion has nothing to resolve from here and is required.
#PublishSummary
type PublishSummary structPublishSummary is what one [PublishProjectDocs] call did.
#PublishOptions
type PublishOptions structPublishOptions is what one [PublishProjectDocs] takes.
#RemoteReadError
type RemoteReadError structRemoteReadError is a read from the assembly repository that did not succeed and did not 404.
It is kept distinct from the absent-file outcome because the two used to be the same value. Every publisher treats an absent published-file record or an absent membership record as the real initial state and writes a fresh one over it; a rate limit, an expired token or a 502 returned that same "nothing there" and the fresh record destroyed whatever the read failed to see. A failure now stops the operation before anything is written.
#RetireSummary
type RetireSummary structRetireSummary is what one [RetireProject] call did.
#SharedFilesOptions
type SharedFilesOptions structSharedFilesOptions is what one [GenerateSharedFiles] takes.
#BuildOptions
type BuildOptions structBuildOptions is what one [BuildSourceProject] takes.
#DispatchEndpoint
func DispatchEndpoint(repo string) stringDispatchEndpoint is the gh api path a repository_dispatch on repo is sent to.
#AssemblyPush
func AssemblyPush(assemblyRepo, sourceRepo, slug, version, ref string) DispatchAssemblyPush returns the dispatch that rebuilds one project in the assembly.
assemblyRepo is the assembly repository (for example "smm-h/docs-assembly"); sourceRepo is the source project's repository; slug is the project slug; version is the version being deployed; ref is the git ref the workflow clones the source at.
#SharedOnlyDispatch
func SharedOnlyDispatch(assemblyRepo string) DispatchSharedOnlyDispatch returns the dispatch that regenerates the assembly's cross-project elements without touching any project's files.
#AssemblyStatus
func AssemblyStatus(repo string) [][]stringAssemblyStatus returns the gh argv lists that read the assembly's recent workflow runs.
repo is the assembly repository identifier (for example "smm-h/docs-assembly").
#AssemblyRebuild
func AssemblyRebuild(repo string, projects map[string]any) ([]Dispatch, error)AssemblyRebuild returns one dispatch per project the assembly records.
repo is the assembly repository identifier. projects is the derived membership record "assembly integrate" wrote, which always carries repo, ref and version for every project.
An incomplete record is a hard error naming the project and the fields it lacks. A missing version used to become the literal string "latest", which travelled through the dispatch payload and back into the membership record -- so the assembly's own record then claimed a version nobody released, and every later rebuild replayed it.
The dispatches come back in slug order. The Python replayed the record's own key order, which a decoded JSON document no longer carries, and a sorted replay is the one order that is the same on every run.
#PushFilesToRepo
func PushFilesToRepo(PushFilesToRepo pushes files and deletions to a remote repository in one commit through the Git Data API.
It uses the GitHub REST API (through the gh CLI) to create blobs, a tree, a commit, and to update the branch ref -- all without cloning.
Content travels as bytes and reaches the blob API base64-encoded, so an image round-trips byte-identically.
Every path is hashed locally with git's own blob hash and compared against the remote tree: a file whose bytes already match uploads no blob and contributes no tree entry. When nothing differs and nothing is deleted, no commit is created at all and the current head SHA comes back -- which is what makes a regenerating writer idempotent.
deletePaths are removed from the branch in the same commit. A path that is already absent is not an error -- the commit just does not mention it.
Paths are pushed in sorted order. The Python pushed them in the mapping's own insertion order, which a Go map does not have, and sorted is the one order that is the same on every run.
It returns an error when there is neither a file nor a deletion to push, and when any API call fails.
#ListRemotePaths
func ListRemotePaths(h *effects.Handle, repo, branch string) ([]string, error)ListRemotePaths returns every file path on repo's branch, recursively.
This is how a publisher that never clones the assembly learns what is already there: which of a project's pages exist remotely, so the ones the local build no longer produces can be deleted in the same commit that uploads the ones it does.
#ApplyProjectFiles
func ApplyProjectFiles(opts GraftOptions, h *effects.Handle) ([]string, error)ApplyProjectFiles grafts a built project into the assembly tree and returns the paths it changed.
The graft prunes rather than wipes: what the build produces is what the build owns, and only paths this publisher produced before and does not produce now are removed. Everything else -- a post or a documentation page published between releases -- is somebody else's and remains. The published-file record at "manifests/
The build's output lands in two places, by the rule [site.SplitBuildOutput] states: the project's documentation under "site/
[GraftOptions.Home] routes the documentation to the site root instead. Its output is checked against the addresses the assembly owns first, and its curated listing is copied in beside the manifests.
#CopyHomeListing
func CopyHomeListing(assemblyDir, sourceDir, slug string, h *effects.Handle) (string, error)CopyHomeListing copies the home project's curated listing into the assembly.
The listing is authored in the home project ("docs/projects.toml") because it is content, and it is copied here 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 leaves no sidecar; 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.
It returns the sidecar's path, or "" when there was nothing to copy.
#FoldPostsIntoOverlay
func FoldPostsIntoOverlay(FoldPostsIntoOverlay adds a full build's posts to slug's post overlay and returns the overlay's path.
The overlay is the assembly's one authority on a project's posts, so a full build cannot simply ignore it -- an overlay written before the release would keep the release's own posts off the site. It used to delete the overlay outright for that reason, which threw away every post published between releases along with the staleness.
Folding is the version that keeps both: the build's posts go in, the overlay's posts that the build does not carry stay, and the file remains the complete list "post publish" overwrites wholesale. It returns "" when there is no overlay to fold into.
The rewritten overlay's keys come out sorted. The Python rewrote the document in its own key order, which a decoded JSON document no longer carries; the keys and their values are the same either way.
#VerifyBeforeDeploy
func VerifyBeforeDeploy(opts VerifyOptions, h *effects.Handle) ([]string, error)VerifyBeforeDeploy verifies the assembled tree, or refuses to let the deploy continue.
It returns the checks that ran. A check that could not run says so on stderr rather than passing quietly -- an assertion nobody made must not look like one that held.
The outbound results this produces are written back into the checkout so the next deploy inherits them; that write is the deploy's, not the verification's, which is why it happens here and not inside [verify.VerifyAssembly].
A tree that failed verification is an error naming every offender, before anything is committed or pushed.
#IntegrateProject
func IntegrateProject(opts IntegrateOptions, h *effects.Handle) (*IntegrateSummary, error)IntegrateProject integrates one dispatched project into the assembly repository checkout and pushes the result.
This is the body the generated deploy workflow used to embed as shell and inline interpreter snippets. It builds the cloned source project, then -- inside a retry loop that re-syncs to the remote every attempt, so two concurrent deploys converge instead of clobbering each other -- grafts the build into the tree, refreshes the project's manifest and membership record, regenerates the shared cross-project files, rebuilds the search index, verifies, commits and pushes.
#PyPIURL
func PyPIURL(pkg string) stringPyPIURL is the metadata address of one PyPI distribution.
#GoProxyURL
func GoProxyURL(version string) stringGoProxyURL is the module proxy's info address for one selfdoc version.
#FetchPyPIMetadata
func FetchPyPIMetadata(pkg string) (map[string]any, error)FetchPyPIMetadata returns PyPI's JSON metadata document for pkg.
It is a GET: it changes nothing, which is why it does not go through the effects handle -- a recorded read would have nothing to record and a preview still needs the answer.
#GoModuleVersionExists
func GoModuleVersionExists(version string) (bool, error)GoModuleVersionExists reports whether the Go module proxy serves version of this module.
The proxy answers 404 or 410 for a version it does not have; every other non-200 status is a read that did not happen and is an error rather than a "no", so a proxy outage can never be read as an unpublished pin.
#RegistryLatestVersion
func RegistryLatestVersion(pkg string, fetch PyPIFetcher) (string, error)RegistryLatestVersion returns the version PyPI currently serves as pkg's latest.
#CheckPinsArePublished
func CheckPinsArePublished(pins ToolchainPins, reg Registry) errorCheckPinsArePublished returns an error unless every pinned version can actually be installed.
"assembly sync-workflow" defaults the selfdoc pin to the running binary's version, which in a development checkout sits ahead of the registry the moment work starts on the next version. Writing that pin produces a workflow whose install cannot resolve, and the failure surfaces at the next dispatch, on the assembly repository, far from whoever wrote it. So the pins are checked here, before anything is written.
The two pins are checked against two different registries, because they name two different kinds of thing. The pagefind pin is a PyPI distribution: a version that exists but has no files is unpublished for this purpose, since pip cannot install it either. The selfdoc pin is a Go module version, and what has to exist is a tag the module proxy serves.
#ResolveToolchainPins
func ResolveToolchainPins(opts PinOptions) (ToolchainPins, error)ResolveToolchainPins resolves the two pins the generated workflow installs.
An explicitly supplied version is taken verbatim. Otherwise:
- selfdoc has no fallback here: an empty SelfdocVersion is refused by [ToolchainPins.Validate], naming the field. The version of the running binary is the binary's own fact, and it is handed down through internal/cli rather than read here. - pagefind is PyPI's current release. pagefind is a CI-only tool this module does not depend on, so there is no installed distribution to read a version from -- the honest options are the registry's current release or an explicitly named version, and the registry answer is the one that keeps regenerating on every release the way the selfdoc pin does. This is the one pin that does not describe the machine doing the generating; that difference is deliberate and there is nowhere better to read it from.
#PublishProjectDocs
func PublishProjectDocs(opts PublishOptions, h *effects.Handle) (*PublishSummary, error)PublishProjectDocs pushes a locally built documentation site into the assembly.
This is the documentation counterpart of publishing a post: a documentation change reaches the live site with no tag and no release, through the same Git Data API commit that a post takes. What it pushes is the project's subtree, its manifest, its published-file record and its derived membership entry; what it deletes is every page it published before and does not publish now, so a page removed locally disappears remotely.
It cannot create membership: publishing into a slug the roster does not declare is a hard error naming the block that would have to exist.
#FetchRemoteText
func FetchRemoteText(FetchRemoteText returns the text of path on repo's default branch.
Absence and failure are two outcomes, not one. Only an explicit HTTP 404 means the file is not there, and only then does missingOK turn it into the empty string -- the real initial state of a record nothing has written yet. Every other outcome is a [RemoteReadError] naming operation, the path and what gh said, because a caller that read "" as "nothing published yet" would then write a record that erases what it could not read.
operation describes what the read is for, so the error says which operation was abandoned rather than only which path was unreadable. Empty names the read itself.
#RemoteTextFetcher
func RemoteTextFetcher(h *effects.Handle) site.RemoteTextFetcherRemoteTextFetcher is [FetchRemoteText] bound to one effects handle, in the shape [site.StagePublishedRecord] takes.
The model half of the assembly never reaches the network; the publisher that owns the remote supplies its own reader, and this is that reader.
#LoadRemoteRoster
func LoadRemoteRoster(h *effects.Handle, repo string) (*site.Roster, error)LoadRemoteRoster returns the roster declared on the assembly repository.
An absent roster is its own error naming the block that has to exist; a failed read is a [RemoteReadError], never mistaken for one.
#RemotePostClaims
func RemotePostClaims(RemotePostClaims maps every site-level post path another project claims to its claimant.
The remote counterpart of [site.ForeignPostClaims]. A publisher that writes through the Git Data API has no assembly clone to read, so it asks the assembly for one published-file record per declared project other than its own. That is one API read per project, which is the price of the site-level blog being a single namespace: without it the publish would find out about the collision only after it had already overwritten the other project's post.
A project with no record yet claims nothing. A record that cannot be read is a [RemoteReadError], never an empty claim set.
#FetchRemoteManifests
func FetchRemoteManifests(FetchRemoteManifests returns the assembly's per-project manifests, read off the repository through the Git Data API.
It is the remote counterpart of [site.LoadAssemblyManifests], for the publishers and checks that never clone the assembly. The home project's pages render from these -- a version badge and a post highlight come from no project's own repository -- so a command that builds or checks the home project reads them from the assembly itself.
Only the per-project manifests are fetched: the sidecars beside them (published-file records, revision sidecars, the listing copy) are not manifests, and asking for each one is an API read that answers nothing.
#RetireProject
func RetireProject(h *effects.Handle, repo, slug, branch string) (*RetireSummary, error)RetireProject removes slug from the assembly's roster and tree in one commit.
Retirement is a roster edit plus the reconciliation that edit implies, done together so the published site never lags the declaration: the [[project]] block goes, the derived membership record loses its entry, and every path the project owns -- its whole section and all its manifest kinds -- is deleted in the same commit. What remains is the shared elements, which the caller regenerates by dispatching a shared-only rebuild ([SharedOnlyDispatch]); that pass also rebuilds the search index, so the retired project stops answering searches.
The rewritten roster lists the remaining projects in slug order. The Python rewrote them in the document's own declaration order, which the parsed roster no longer carries.
#RefreshHomePages
func RefreshHomePages(RefreshHomePages re-renders every site-level directive region the home project emitted, and returns the pages it looked at.
This is the second of the two moments a site-level directive resolves (the first is the home project's own build). It runs on every deploy, including deploys of other projects, which is the point: the front page's curated cards carry each project's live version, and a version changes when that project releases, not when the home project does.
Each page is re-rendered against its own hop back to the site root, so a region on a page one level down links a project as "../alpha/" and the same region on the front page links it as "alpha/".
#GenerateSharedFiles
func GenerateSharedFiles(opts SharedFilesOptions, h *effects.Handle) ([]string, error)GenerateSharedFiles writes the assembly's shared cross-project files and returns their paths.
The files are the project listing at "projects/index.html", the blog index at "blog/index.html", "nav.json", "feed.xml", "sitemap.xml", "robots.txt", "llms.txt", "404.html", "_headers" and "_worker.js". Both generated pages sit at fixed, generator-owned addresses; the site root belongs to the home project, whose own pages are grafted there and are never written by this function.
"robots.txt", "llms.txt" and "404.html" are the site's, not any project's: every constituent build writes its own set at its own output root, where they end up buried under "
[SharedFilesOptions.HomeSlug] is the roster's home project. It is left out of the generated listing and out of nav -- the front page does not list itself -- and its pages are addressed from the site root. Every site-level directive region in its emitted pages is re-rendered here, on every deploy, so a version badge on the front page is as current as the last deploy of the project it names rather than as the last deploy of the home project.
A missing required input is an error -- the CLI turns those into a usage error, the integrate command lets them abort the deploy.
#RelativizeSiteLinks
func RelativizeSiteLinks(RelativizeSiteLinks re-expresses every clickable link in the assembled tree that names the site's own base as a document-relative reference, and returns the site-relative paths it changed.
A link a reader clicks has to resolve under whatever mount the tree is served from -- production, a preview, a mirror -- so the one addressed at "https://
This runs over every page in the tree, beside the chrome re-pointing pass and for the same reason. The assembly is never rebuilt whole: a project's subtree is replaced only when that project deploys, so pages an older toolchain wrote outlive it, and a rule the verification applies to the whole tree would otherwise refuse every deploy over pages the dispatch did not write and could not fix. Sweeping them here is what lets the tree converge.
pages are site-relative HTML paths, as [chrome.EmittedPages] returns them. canonicalBase is the site's own base URL; a reference to any other host is somebody else's address and is left alone.
#BuildSourceProject
func BuildSourceProject(opts BuildOptions, h *effects.Handle) errorBuildSourceProject builds the cloned source project.
The home project builds through the home target, which is the one build that can resolve a site-level directive: its front page renders the curated listing with every project's live version, and the manifests those versions come from are the assembly's, not its own.
The build runs in this process rather than as a subprocess. The Python shelled out to two other commands because the former blog package never imported the docs generator; one binary carries both, so the build is a call. Nothing about which build runs changed: a posts-scope dispatch builds the posts alone, the home project builds with the assembly's manifests in scope, and every other project builds its newest declared version.
#IndexSite
func IndexSite(siteDir string, h *effects.Handle) errorIndexSite builds the Pagefind search index over the assembled site.
The indexer also writes its own search widget beside the index. An assembly whose pages all draw their own search surface -- every framework-theme page does -- references none of it, and the unwanted payload is pruned right after indexing. One page still loading the widget keeps it for the whole tree.
Two invocations are tried in order, the Python distribution's module entry point and the standalone binary, because the two ways pagefind is installed put it in two different places. A candidate that is not installed is skipped; a candidate that runs and fails is the answer, and is reported.
#GenerateWorkerJS
func GenerateWorkerJS(GenerateWorkerJS returns the Cloudflare Pages _worker.js for the assembly site.
The worker does two things, and every request answers to at most one 301 before it reaches an asset.
#One hostname
The site is bound to more than one host -- the canonical apex, the docs subdomain, the retired blog subdomain, the provider's own preview domain -- and one of them serves content. A request on any other host is redirected to the same path on the canonical host, 301, query string preserved. No path is ever served on two hosts, so nothing is duplicate content and no rel=canonical is asked to undo a hosting decision.
The one host that does not map to the same path is the retired blog subdomain, whose whole document space was the blog: "blog.example.com/x" is "
#Historical addresses
The site has changed address scheme twice, and the shapes below are the ones with links in the wild. Each is a 301 to the current address, generated as data from the manifests -- the assembly knows every project slug and every post slug, and a path that merely looks historical without naming one of them is not redirected at all: it falls through to the 404, which is the honest answer for an address that never existed.
- "/
The third historical shape, the flat "/
canonicalBase is the absolute base URL of the assembly site, taken from topology.docs_base. Required -- there is no default deploy target.
legacyBlogHost is the hostname of the retired blog subdomain, taken from topology.legacy_blog_host. An empty string means no such subdomain exists and it gets no prefix entry; it would still be redirected by the one-hostname rule if it resolved here.
projectSlugs is every project the assembly serves. A historical path is only rewritten when its first segment is one of these. postSlugs is every post the blog serves, same role.
#GenerateWorkflowYAML
func GenerateWorkflowYAML(GenerateWorkflowYAML returns the GitHub Actions workflow YAML for assembly deployment.
Every deploy-target value is templated from the project's selfdoc.json -- nothing about the destination is baked into selfdoc itself.
pagesProject is the Cloudflare Pages project the assembled site deploys to, from assembly.pages_project. Required.
canonicalBase is the absolute canonical base URL of the assembly site, from topology.docs_base. Required.
legacyBlogHost is the hostname of a retired blog subdomain, from topology.legacy_blog_host. Empty when none exists.
pins are the versions the install step names. Required and complete: this function renders pins, it never resolves them, so it reads neither the environment nor the network. [ResolveToolchainPins] does the resolving and [CheckPinsArePublished] refuses a pin nobody can install.
#GitignoreContent
func GitignoreContent() stringGitignoreContent returns a .gitignore suitable for a CI-only assembly repo.
#AssemblyInit
func AssemblyInit(AssemblyInit returns filename -> content for a new assembly repository.
pagesProject is the Cloudflare Pages project the workflow deploys to. canonicalBase is the absolute canonical base URL of the assembly site. legacyBlogHost is a retired blog subdomain, or "" when none exists. pins are the toolchain versions the generated workflow installs.
#Dispatch.PayloadJSON
func (d Dispatch) PayloadJSON() ([]byte, error)PayloadJSON renders the request body this dispatch POSTs.
The two shapes are distinct documents rather than one document with empty keys: a project dispatch states all four project fields and no scope, and a shared-elements dispatch states the scope and nothing else, which is what the workflow's own condition on the scope was written against.
#Error.Error
func (e *Error) Error() string { return e.Message }Error returns the diagnostic.
#ToolchainPins.Validate
func (p ToolchainPins) Validate() errorValidate reports the first empty field, naming it.
Go cannot refuse an incomplete struct literal at construction the way Python's frozen dataclass refused it in __post_init__, so every consumer of a ToolchainPins calls this first: the renderer, the publication check, and the resolver on its way out.
#ToolchainPins.PyPIPins
func (p ToolchainPins) PyPIPins() map[string]stringPyPIPins returns PyPI distribution name -> pinned version, for every pin PyPI actually serves.
That is pagefind alone. The keys are registry names, not install specifiers: pagefind is installed as "pagefind[bin]" but published as "pagefind". The selfdoc pin is a Go module version and is checked against the module proxy instead -- see [CheckPinsArePublished].
#RemoteReadError.Error
func (e *RemoteReadError) Error() string { return e.Message }Error returns the diagnostic.