feat: a reusable deploy workflow the six packages call - #6
Merged
Conversation
Six packages publish a subtree of one site from six repositories. Two did it with a hand-written ~100-line workflow each; the four with none were about to get four more copies, and duplicated build logic is what produced the incidents this closes over. The workflow owns the runner, the completeness guard, the hosting project, the custom domain and the upload — the parts that are identical everywhere and must not drift. The package owns its build: one named npm script whose steps this workflow never learns. The completeness guard survives and becomes two-sided. A content package must state a floor; a homepage-only package is held to exactly one page, and the caller cannot widen that bound, because for kethira and harnadventures it is a fan-content licensing boundary rather than a preference. Closes #5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Six packages must publish
https://www.heroiclands.org/<package>/. Two did, eachwith its own ~100-line workflow; four had none. This adds
.github/workflows/deploy-package-site.yml— the first reusable workflow here —so a package publishes with 13 lines instead of a copy.
The split: the workflow owns the runner, the completeness guard, the hosting
project, the custom domain and the upload — identical for every package, and
exactly the parts that must not drift. The package owns its build.
The three decisions
1. The build contract is a named npm script, not steps this workflow knows.
It runs
npm run build:siteand takes whatever tree that leaves. thalorna's isbuild:site-content && hugo && build:site-root; sohl's isdocs:prepare && docs:html && build:kb && site:assemble; the seventh packagewill differ again. Teaching this workflow any of those shapes means editing this
repository every time a package changes how it builds — the coupling the issue
exists to remove. What is required is only the output: the site under
build/site/<package>/(HugopublishDir), withbuild/siteuploaded whole,so the deployed tree carries its own prefix physically and the router passes the
path through unchanged.
One generalisation makes that contract sufficient rather than nearly so: the
checkout is
fetch-depth: 0(so tags come with it) andGH_TOKENis in thebuild step's environment. That is what lets a package's own build resolve and
check out another ref of its own repository — which is the one thing sohl's
current workflow does that a naive "run one script" contract could not express.
Nothing here knows that any package does it.
2. Hugo is pinned here, shared, overridable.
hugo-versiondefaults to0.163.3— the version both live workflows already pin, so adopting changesnothing — and a caller can override it to trial a build ahead of the others, or
set
""to skip installing Hugo at all. One bump for six packages beats sixbumps that drift; the override is what stops a shared pin becoming a shared
blocker.
3. Secrets are named, never
secrets: inherit. Inheriting hands a workflowin another repository every secret the caller holds — npm publish tokens,
Foundry credentials, release tokens — for a job that needs two. Naming them
documents the requirement at the call site and fails when one is missing rather
than three steps later.
Both are declared optional so a repository still being set up can call this for
the build-and-guard half — but it must say so with
allow-unpublished: true.Otherwise missing credentials fail the run. thalorna's workflow skips the
upload with a notice; that was right during a migration and is wrong now, since
it is a deploy workflow reporting success without deploying.
The guard, per mode
> 1000could not survive as written, and "no check" was not an option. So it isderived from
publish.site, read frompackage-build.config.yaml— never passedin, so the guard cannot be told one thing while the build does another.
site: contentsite: homepage<pkg>/index.htmlnon-empty<pkg>/404.htmlnon-emptybuild/site/_headersnon-emptyindex.htmlfiles)min-pages≤ n ≤max-pagesmin-pagesis requiredHomepage-only is the stricter check, not the absent one. It is two-sided: a
build that emitted nothing fails, and a build that emitted more than the
homepage fails.
kethiraandharnadventurespublish one page becausepublishing their content would breach the fan-content licences they ship under,
so passing
min-pagesormax-pagesin homepage mode fails the run — aboundary a caller can widen is not a boundary. That also gives
HeroicLands/package-build#55's "publishes exactly one page, asserted rather than
left to configuration drift" an assertion on the publishing side.
For content packages
min-pagesis required: a content package with no floor hasno guard, and a guard that cannot fail is the failure mode being removed. The two
fixed checks are not extras —
404.htmlis what stops Pages answering anunmatched path with a 200 carrying the home page, and
_headers(checked at thedeployment root, the only place Pages reads it) is what keeps the project's
other hostnames out of the index.
Project and custom domain
Both created if missing, idempotently, before the upload. The project check is a
lookup then a create, so a bad token or revoked scope still fails rather than
hiding inside a tolerated error. The domain —
<package>.pkg.heroiclands.org,the hostname
heroiclands-site's router derives and does not store — is added byinspection (list, add if absent) and verified rather than assumed if the create
loses a race. Doing it here is what makes a package self-publishing: the router
needs no edit to gain a package, but only if the hostname exists.
What was verified, and what was not
I cannot run a GitHub Actions workflow locally, so:
Verified by execution.
actionlint1.7.12 withshellcheck0.11.0: clean, on the workflow and onall six caller workflows. A negative control (renaming
min-pagestomin-pagez) producesproperty "min-pagez" is not defined in object type {...}, which proves actionlint really is resolving the hyphenated expressionsagainst the declared
workflow_callinterface rather than passing them over.package-build.config.yamlfiles.It correctly reads
kethira/hm3/harnensembleas homepage mode, andcorrectly refuses
sohl,thalornaandharnadventures, which still carry thepre-5.0 boolean. Nine validation cases: missing config, missing
contentPackage, unknown mode, absentpublish:block,min-pageson ahomepage package,
max-pageson a homepage package, and a content package withno floor — each refused with the intended message.
missing
404.html, missing_headers, an emptyindex.html, a missing packagedirectory (the "published
public/instead" mistake), content at 1715 / 1000 /2 pages against a 1000 floor (2 being the shape of the actual incident), and
1500 against a 1200 ceiling. All 11 behaved as intended.
That work found one real bug before it shipped:
yq -r '.publish.site // "homepage"'— the obvious spelling, and the one I wrote first — silently mapssite: falsetohomepage, because//is an alternative operator that fireson
falseas well as on absent. Three of the six repositories carrysite: falseor did, so the boolean refusal would never have fired for exactly the packages
that needed it. Fixed, and commented at the site.
Not verified. No run has happened: the Cloudflare calls (project lookup and
create, domain list and add),
peaceiris/actions-hugo,cloudflare/wrangler-action,the
workflow_callhandshake itself, and the presence ofyqon the runner (itships with the GitHub-hosted ubuntu images; the step checks for it and fails with
that message rather than assuming). The first adoption is where those are proven,
and it should be a package that is not live.
sohlandthalornacan adopt without changing what they publishNot done here — no package repository is touched by this PR.
build:sitealready producesbuild/site/thalorna/with a404.htmlandbuild/site/_headers, which isprecisely the contract; every check in the guard is one its own workflow already
ran, plus
404.html(which its workflow also checks) and the ceiling (which itcan omit). Its caller is
project: sohl-thalorna, min-pages: 1200.tag, build the API documentation from it — out of its workflow and into its own
build:site, which is why the checkout is unshallow with tags and whyGH_TOKENis in the build environment. That changes where the steps live, not what is
published: same tag resolved, same docs, same
site:assemble --api. It is alsothe correct home for them under decision 1 — the release-tag rule is sohl's
build, not every package's.
Both must additionally migrate
publish.sitefrom the pre-5.0 boolean, which theyowe the toolchain regardless.
Contradicting the issue
The issue reads as though
publish.sitemodes are available now. They are halflanded: package-build#51 and #55 are closed (the mode exists,
homepageis thedefault), but #52–#54 — requiring the homepage note, rejecting address fields on
it, link-checking it — are open, and
sohl,thalornaandharnadventuresstillcarry the boolean. So no homepage-only package can build its one page yet.
This workflow is ready ahead of them and refuses the boolean rather than guessing,
which is the sequencing #52 asks for. The four new packages adopt once the note
type is required; the two live ones can adopt as soon as they migrate the flag.
Closes #5