Skip to content

Nothing here ever looked at the site this repository publishes - #93

Merged
ChelseaKR merged 1 commit into
mainfrom
chore/deploy-staleness-perimeter
Sep 13, 2026
Merged

ChelseaKR merged 1 commit into
mainfrom
chore/deploy-staleness-perimeter

Conversation

@ChelseaKR

@ChelseaKR ChelseaKR commented Sep 13, 2026

Copy link
Copy Markdown
Owner

The premise, stated accurately

The publisher is fine and this PR does not touch it. pages.yml fires on push: branches: [main] with no path filter, plus workflow_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.yml and no scheduled job that reaches https://chelseakr.github.io/perimeter/ at all — unlike cairn, chalkline, plumbline, disclosed, gauntlet, sprout, nearmiss, homeroom, id-churn-sentinel and tods-validate. ci.yml reads the working tree, codeql.yml reads the source, pages.yml publishes, and none of them asks what a visitor receives.

Measured over the 75 commits on main since pages.yml was added on 2026-08-07:

main commits since pages.yml landed 75
of those, with no github-pages deployment 23 (31%)
of those 23, publish-site run finished cancelled 13
of those 23, no publish-site run at all (batched push / pre-Pages-config) 10
of those 23, commits that changed files under site/ 24385957b9, edbfb9bd7, both 2026-08-29

The pages concurrency group is cancel-in-progress: false, which holds exactly one pending run: a second push while a publish is in flight evicts the first. (Counted over --event push runs only; the ten with no run at all arrived inside a push whose tip was another commit, and have no ci push run either.)

The sharpest single case is edbfb9bd7, one of the two that changed site/: its ci push run finished success and its publish-site push run finished cancelled. main got 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), d4d346a9d and a3aa215dc (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/pages reports build_type: workflow, which invites "deployed SHA versus main head". That comparison would be wrong here.

pages.yml does 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-validate and axe-core over site/ itself) and then hands actions/upload-pages-artifact exactly one thing: path: site.

So this is a committed-tree publisher, and the visitor-visible path set is the published directory itself:

git rev-parse <deployed_sha>:site   vs   git rev-parse <head>:site

Equal tree object ids mean the visitor holds exactly the bytes main holds, however many commits and days separate the two. The derivation, spelled out in the module docstring:

  • src/perimeter/render.py, artifacts.py, coverage.py and the field registry produce site/, but a change to any of them alters no published byte until somebody runs make site against data/raw/ and commits the result. make site-check exists because that gap is real.
  • .github/workflows/pages.yml decides 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 change main constantly 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 history

Thirteen 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. A github-pages deployment exists only because bytes were published, it names the commit they came from, and its newest status must read success before its commit may be treated as live — a deployment row on its own is a request to publish.

It publishes nothing

No pages: write, no id-token: write, no deploy credential, no workflow_dispatch of anything, no publish-enable variable read or set. Workflow-level permissions: {}; the one job takes contents: read, deployments: read, issues: write. persist-credentials: false, fetch-depth: 0, timeout-minutes: 15, concurrency with cancel-in-progress: false, actions/checkout pinned to the same 40-char SHA already used in ci.yml and pages.yml. Weekly, Mondays 06:29 UTC, plus workflow_dispatch with a max_age_days input.

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-pages deployment in the history, none reporting a successful status, a deployed commit this clone does not contain (shallow checkout), a diverged history, a site/ that is not where pages.yml says 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

Published build:  874107fca  (2026-09-13, deployment 6423957323)
main:             874107fca
Published bytes:  site/ tree 36e172463 served, 36e172463 on main
Since the deploy: 0 days, 0 commits, 0 of them touching site/

Up to date: the served site/ tree is byte-identical to main's.

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 changed git hash-object and a non-zero occurrence count, the full pytest output read, then restored and the hash confirmed byte-identical (9712934352b08279eaf7432f6691674f7c7568e4). __pycache__ cleared between runs.

# Sabotage hash before → after tokens Fired Assertion
1 A deployment row counts as a publish regardless of status 9712934…8a736e5… 1 yes, 3 tests Failed: DID NOT RAISE StalenessUnknown
2 Trust the checkout depth (drop the absent-commit refusal) 9712934…587b8ff… 1 yes, 1 test 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…'
3 Compare commit ids instead of site/ tree ids 9712934…acfadc2… 1 yes, 3 tests AssertionError: assert False … .serving_current_bytes
4 The deploy's own age is the verdict 9712934…a329fcc… 1 yes, 4 tests AssertionError: assert not True … deploy_age_days=200, unpublished_commits=0 … .overdue
5 A refusal returns 0 9712934…b4a93ba… 1 yes, 1 test assert 0 == 2
6 PUBLISHED_PATH drifts from what pages.yml uploads 9712934…ae8e87c… 1 yes, 11 tests AssertionError: pages.yml publishes 'site' and deploy_staleness.py compares 'docs'
7 Remove require_comparable entirely 9712934…e37fd43… 1 yes, 3 tests Failed: DID NOT RAISE StalenessUnknown (diverged history)

Two findings from the controls, recorded rather than smoothed over:

  • Control 2 fired on the message, not on the refusal. With the explicit absent-commit check removed, the shallow case still refuses — git merge-base fails on a commit that is not there — but with an unhelpful message. The refusal is double-guarded; the diagnosis is not.
  • Control 7 shows the ancestor check is the single guard that matters. With require_comparable gone, 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 refusalDID 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.py clean (16 files) · uv run pytest -n auto 990 passed, 25 skipped · zizmor 1.29.0 online over .github/workflows/ — no findings · semgrep 1.168.0 (p/python, p/security-audit) over both new files — 0 findings · uv lock --check clean.

Not run locally: make browser-sync / make a11y-browser (a Chromium download for a change that touches no markup) and make node-audit. CI runs the full make verify.

Deliberately not done

  • pages.yml is unchanged. Its pages concurrency group with cancel-in-progress: false is what drops those 13 publishes, and keying it per-commit the way ci.yml already 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.
  • Nothing was deployed, published, or dispatched, not once and not to test.
  • No analytics, beacons, cookies or outreach. No new dependency: standard library only, and it imports nothing from src/perimeter, so it runs on a bare python3 and cannot be broken by a dependency.

Prepared with AI assistance; reviewed before submission.

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.
@ChelseaKR
ChelseaKR merged commit 00ecb44 into main Sep 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant