Nothing here ever looked at the site this repository publishes - #93
Merged
Merged
Conversation
ci.yml reads the working tree, codeql.yml reads the source, pages.yml publishes, and none of them asks what a visitor receives. Unlike its siblings this repository carries no live-integrity.yml either, so the published site has no reader of any kind. The trigger is not the problem and nothing here changes it: pages.yml fires on every push to main with no path filter. What was missing is anything that notices a firing that did not land. Measured over the 75 commits on main since pages.yml was added on 2026-08-07: 23 produced no github-pages deployment. Thirteen had their publish-site run cancelled -- the `pages` concurrency group holds one pending run, so a second push evicts the first -- and ten arrived inside a push whose tip was another commit. Two of the 23, 4385957 and edbfb9b, changed files under site/. Every gate was green throughout, because a cancelled run is not a failed one. The comparison is the published subtree, not the deployed commit. pages.yml cannot render site/ -- it is built from CAL FIRE's files, which are never in git and never in CI -- so it hands actions/upload-pages-artifact the committed directory verbatim. site/ is therefore the whole of what a visitor receives, and two commits carrying the same site/ tree serve the same site however far apart they are. Comparing deployed SHA against main head would have reported drift on 21 of those 23 commits and said nothing useful about the two that mattered. Measured from the deployment record rather than publish-site's run history, because thirteen of those runs finished cancelled and a run list cannot tell a cancelled publish from a completed one. A deployment exists only because bytes were published; its newest status must read success before its commit may be treated as live. It publishes nothing: no pages: write, no id-token: write, no deploy credential, and it dispatches nothing. The output is an issue, not a permanent red; the run goes red only when the comparison cannot be made at all -- no deployment, none successful, a commit this clone does not contain, a diverged history, or a site/ that is not where pages.yml says it is. Every such case refuses and exits 2 rather than returning a comfortable zero. Current reading: deployment 6423957323 at 874107f is origin/main, and both carry site/ tree 36e1724. The site is correct today; nothing here could have said so, and nothing could have said otherwise.
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.
The premise, stated accurately
The publisher is fine and this PR does not touch it.
pages.ymlfires onpush: branches: [main]with no path filter, plusworkflow_dispatch. That trigger is correct and nothing here changes it, dispatches it, or touches publishing policy.What is missing is anything that notices when a firing did not land. This repository has no
live-integrity.ymland no scheduled job that reaches https://chelseakr.github.io/perimeter/ at all — unlikecairn,chalkline,plumbline,disclosed,gauntlet,sprout,nearmiss,homeroom,id-churn-sentinelandtods-validate.ci.ymlreads the working tree,codeql.ymlreads the source,pages.ymlpublishes, and none of them asks what a visitor receives.Measured over the 75 commits on
mainsincepages.ymlwas added on 2026-08-07:pages.ymllandedgithub-pagesdeploymentpublish-siterun finishedcancelledpublish-siterun at all (batched push / pre-Pages-config)site/4385957b9,edbfb9bd7, both 2026-08-29The
pagesconcurrency group iscancel-in-progress: false, which holds exactly one pending run: a second push while a publish is in flight evicts the first. (Counted over--event pushruns only; the ten with no run at all arrived inside a push whose tip was another commit, and have nocipush run either.)The sharpest single case is
edbfb9bd7, one of the two that changedsite/: itscipush run finished success and itspublish-sitepush run finished cancelled.maingot a full green verdict on a commit whose bytes never went out, and nothing said so — a cancelled run is not a failed one, and nothing was asking.Three published states of
site/were therefore never served by any deployment:d7c40cbf2(2026-08-07, before Pages was switched to Actions),d4d346a9danda3aa215dc(both 2026-08-29). The last two were superseded within minutes, so no visitor was wrong for long — but nothing in this repository could have said so either way.What this adds
tools/deploy_staleness.py+.github/workflows/deploy-staleness.yml+tests/test_deploy_staleness.py.The comparison: the published subtree, not the deployed commit
gh api repos/ChelseaKR/perimeter/pagesreportsbuild_type: workflow, which invites "deployed SHA versusmainhead". That comparison would be wrong here.pages.ymldoes not render the site. It cannot:site/is built from CAL FIRE's acquired files, which are never in git and never in CI — the workflow's own header says so. Its build job checks the committed bytes (determinism from fixtures,tests/test_published_site.py,html-validateand axe-core oversite/itself) and then handsactions/upload-pages-artifactexactly one thing:path: site.So this is a committed-tree publisher, and the visitor-visible path set is the published directory itself:
Equal tree object ids mean the visitor holds exactly the bytes
mainholds, however many commits and days separate the two. The derivation, spelled out in the module docstring:src/perimeter/render.py,artifacts.py,coverage.pyand the field registry producesite/, but a change to any of them alters no published byte until somebody runsmake siteagainstdata/raw/and commits the result.make site-checkexists because that gap is real..github/workflows/pages.ymldecides whether the upload happens, not what is in it. A change there that broke publishing shows up as a missing or unsuccessful deployment, which this module already refuses on.tests/,docs/,README.md,fixtures/, the lockfiles and every dependabot bump changemainconstantly and change nothing a visitor receives.Concretely: a SHA comparison would have cried wolf on 21 of those 23 undeployed commits and said nothing useful about the two that mattered. It would also call a change plus its revert permanently stale; tree ids do not.
The source: the deployment record, not
publish-site's run historyThirteen of those runs finished
cancelled. Count a cancelled run as a publish and the site reads as fresh every time; filter cancellations out and the newest surviving run may be days from the commit it is credited with. Agithub-pagesdeployment exists only because bytes were published, it names the commit they came from, and its newest status must readsuccessbefore its commit may be treated as live — a deployment row on its own is a request to publish.It publishes nothing
No
pages: write, noid-token: write, no deploy credential, noworkflow_dispatchof anything, no publish-enable variable read or set. Workflow-levelpermissions: {}; the one job takescontents: read,deployments: read,issues: write.persist-credentials: false,fetch-depth: 0,timeout-minutes: 15,concurrencywithcancel-in-progress: false,actions/checkoutpinned to the same 40-char SHA already used inci.ymlandpages.yml. Weekly, Mondays 06:29 UTC, plusworkflow_dispatchwith amax_age_daysinput.The verdict is an issue, not a permanent red
Overdue files or updates one issue and closes it when the site catches up. The run goes red only when the measurement cannot be made: no
github-pagesdeployment in the history, none reporting a successful status, a deployed commit this clone does not contain (shallow checkout), a diverged history, asite/that is not wherepages.ymlsays it is, or two readings that contradict each other. Every one of those raises and exits 2. None returns a comfortable zero.Age alone is never the verdict: a site nobody republished because nothing it publishes changed is correct, not stale. The clock starts at the oldest unpublished commit that touched
site/, not at the deploy — an old deploy plus a change made yesterday is one day of waiting, not two hundred.Current reading
The site is correct today. The point is that nothing could have said so, and nothing would have said otherwise.
Negative controls
Each sabotage was applied to
tools/deploy_staleness.py, its landing proved by a changedgit hash-objectand a non-zero occurrence count, the full pytest output read, then restored and the hash confirmed byte-identical (9712934352b08279eaf7432f6691674f7c7568e4).__pycache__cleared between runs.9712934…→8a736e5…Failed: DID NOT RAISE StalenessUnknown9712934…→587b8ff…Regex pattern did not match. Expected regex: 'not in this clone'; Actual message: 'git merge-base aaaa… failed: fatal: Not a valid commit name aaaa…'site/tree ids9712934…→acfadc2…AssertionError: assert False … .serving_current_bytes9712934…→a329fcc…AssertionError: assert not True … deploy_age_days=200, unpublished_commits=0 … .overdue9712934…→b4a93ba…assert 0 == 2PUBLISHED_PATHdrifts from whatpages.ymluploads9712934…→ae8e87c…AssertionError: pages.yml publishes 'site' and deploy_staleness.py compares 'docs'require_comparableentirely9712934…→e37fd43…Failed: DID NOT RAISE StalenessUnknown(diverged history)Two findings from the controls, recorded rather than smoothed over:
git merge-basefails on a commit that is not there — but with an unhelpful message. The refusal is double-guarded; the diagnosis is not.require_comparablegone, the shallow and malformed-SHA cases still refuse (via the subtree lookup, with the wrong message), but a diverged history produces a number instead of a refusal —DID NOT RAISE. That one line is load-bearing and has no backstop.Gates run locally
uv run ruff check .clean ·uv run ruff format --check .clean (65 files) ·uv run mypy --strict src tools/deploy_staleness.pyclean (16 files) ·uv run pytest -n auto990 passed, 25 skipped ·zizmor 1.29.0online over.github/workflows/— no findings ·semgrep 1.168.0(p/python,p/security-audit) over both new files — 0 findings ·uv lock --checkclean.Not run locally:
make browser-sync/make a11y-browser(a Chromium download for a change that touches no markup) andmake node-audit. CI runs the fullmake verify.Deliberately not done
pages.ymlis unchanged. Itspagesconcurrency group withcancel-in-progress: falseis what drops those 13 publishes, and keying it per-commit the wayci.ymlalready does would fix that — but changing publishing behaviour is a separate decision and not this PR's job. This sentinel makes the consequence visible; it does not alter the cause.src/perimeter, so it runs on a barepython3and cannot be broken by a dependency.Prepared with AI assistance; reviewed before submission.