feat: write a default _headers so no package has to copy it - #8
Merged
Conversation
The deploy workflow requires `_headers` at the deployment root but does not write it, so every package produces its own — and the file is identical for all of them. `:project` and `:version` are Cloudflare's own placeholders, so not even the package name appears in it. Four more packages are about to adopt the workflow, and without a default each copies the same six lines and the reason they exist gets copied or lost with them. The default is written after the build and before the guard: the build is what may have produced one, and the guard is what insists on one. A package that emits its own keeps it byte for byte — the step only writes when there is no file at all, so an empty `_headers` is still the package's file and still fails the guard, which is unchanged. The reasoning is recorded at the step, because it currently lives only in `sohl-thalorna`'s `utils/build-site-root.mjs`, which this makes removable: why a Pages project's host-assigned addresses need `noindex`, why the rules are scoped to those hostnames rather than applied globally, and why `heroiclands-site`'s router strips `X-Robots-Tag` when it proxies. Closes #7
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.
deploy-package-site.ymlrequires_headersat the deployment root but does notwrite it, so every package produces its own. The file is identical for every
package —
:projectand:versionare Cloudflare's own placeholders, so noteven the package name appears in it. Four more packages are about to adopt the
workflow; without a default, each copies the same six lines and the reason they
exist gets copied or lost with them.
The change
One step, after the build and before the guard, writing the default when the
build produced no
_headers:That position is the whole of it: the build is what may have produced a
_headers, and the guard is what insists on one. Earlier, and a build thatclears its output directory clobbers it; later, and it is after the check it
exists to satisfy.
A package that emits its own keeps it, byte for byte. The step tests
-e,not
-s, so it does not touch an existing file at all — including an emptyone, which is still the package's own output and still fails the guard.
Treating empty as absent would replace a broken build's output with a passing
default and hide it. The guard is unchanged; its
_headerserror messagegains one sentence saying what a failure now means (the build wrote an empty
file, since writing none gets the default).
It also declines to create
<site-dir>when the build emitted no tree at all —the guard reports the missing tree, which is the more useful error.
The reasoning, recorded at the step
This is the half of the issue that is not code. The reasoning currently lives in
one consumer's build script (
sohl-thalorna,utils/build-site-root.mjs) —which this default makes removable — so it is written down where the default now
is:
<project>.pages.dev, and one<deployment>.<project>.pages.devperdeployment — as well as at its path on
www.heroiclands.org. Unadvertised, butthey serve the same pages, and left alone they are indexed and compete with the
canonical URL.
noindexglobally.
:projectand:versionare named single-segment wildcards, so thetwo rules match
*.pages.devand nothing else. That is what keeps a repositorycorrect if it is taken elsewhere: under its own domain the site stays
indexable, and only the host-assigned addresses do not. A page that must be
noindexat every address says so in the document (<meta name="robots">),which is body content and passes through untouched.
heroiclands-site's Worker stripsX-Robots-Tagwhen it proxies(
worker/src/router.js,canonicalHeaders), because the hosting cannot tellthe router's request from a reader's — same URL, same address. That is
deliberate and tested there, and unchanged by this PR. The default's job is to
emit exactly the header the router already knows to strip, which this payload
is.
The README's guard section and "Adding a package" checklist are updated to match:
a package no longer has to write
_headers.What was verified, and what was not
No GitHub Actions workflow can be run locally, so — as with #6 — this is
honest about the line.
Verified by execution.
actionlint1.7.12 withshellcheck0.11.0: clean on the whole workflow.A negative control confirms shellcheck really is reaching the new step's
script: unquoting
${SITE}/_headersin the newifproducesSC2086:info:7:9: Double quote to prevent globbing and word splitting. Bothextracted scripts also pass
shellcheck -s bashstandalone.The two scripts were extracted from the YAML with
yqand run againstsynthetic trees, so what was exercised is the workflow's own text, dedented
by the same block-scalar rules the runner applies. 18 assertions, all passing:
_headers→ default written; byte-identical tosohl-thalorna'sHEADERSconstant (cmp, sha25689ef3218…, 116 bytes); guard passes_headers(deliberately different content) → step exits without writing; content byte-identical, mtime unchanged after a 1s gap, so it is not even rewritten; guard passes_headerspresent → left at 0 bytes; guard fails, naming_headers<site-dir>at all → nothing conjured; guard fails on the missing tree, not on_headershomepagemode with the default written → exactly-one-page guard passesCase (a)'s byte-comparison is the acceptance criterion "
sohl-thalornacoulddelete its local copy and change nothing", checked against that repository's
actual exported constant rather than against a transcription of it.
Not verified. No run has happened. Unproven here: the
workflow_callhandshake, Cloudflare's interpretation of the payload at a live edge (the
scoping claim is from the
_headersplaceholder semantics and from the filesohl-thalornaalready deploys, not from an observed response header), therouter's strip in production, and the block-scalar dedent as the runner
performs it — reproduced faithfully by
yq, but not by GitHub. The firstadoption is where those are proven.
Contradicting the issue and the existing file
One thing worth a decision, found while writing the comment and not changed
here: the guard's comment and the README both claimed
_headersmarks theproject's
*.pages.devand*.pkg.heroiclands.orgaddressesnoindex. Thepayload does not — both rules are
*.pages.devpatterns, so<package>.pkg.heroiclands.org, the custom domain this workflow adds and therouter proxies, carries no
X-Robots-Tagand is indexable. I corrected the prosein both places rather than leave two adjacent comments disagreeing, and left the
payload exactly as the issue specifies, since a third rule would break the
byte-for-byte criterion. Whether that hostname should be covered is a separate
call: an argument exists either way, since it is also the origin the router
fetches from. Happy to file it.
Follow-up, not done here
No package repository is touched by this PR.
sohl-thalornacan now delete the_headerswriting fromutils/build-site-root.mjs(itsHEADERSconstant andthe
writeFileSync), which is its own change in its own repository — and worthdoing deliberately, since that file is where this reasoning has lived.
Closes #7