Skip to content

Ask for the deploy's missing ref gate, and refresh three rows that moved - #72

Merged
ptr727 merged 5 commits into
developfrom
todo-deploy-ref-guards
Aug 9, 2026
Merged

Ask for the deploy's missing ref gate, and refresh three rows that moved#72
ptr727 merged 5 commits into
developfrom
todo-deploy-ref-guards

Conversation

@ptr727

@ptr727 ptr727 commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Documentation only. Records a gate the maintainer asked for, and re-measures three State rows that moved with today's releases and deploys.

The asymmetry

The release workflow gates its branch in both directions:

  • publish-release.yml refuses a dispatch from anything but main or develop
  • validate-release fails loudly if the default branch carries a prerelease suffix, or a non-default branch carries none

So neither branch can cut the other's kind of release. The deploy has half of that:

Environment Allowed ref today
production main only, enforced by assert-ref
staging any ref at all

Which is how a staging deploy got dispatched from main in this session. The ask is the missing arm: staging accepts only develop, so develop deploys staging, main deploys production, and neither reaches the other's environment.

Why this is not a one-line change

The comment above assert-ref currently reads "Staging deploys from any ref, since proving a branch before it merges is what staging is for." That is the argument for the present behavior. Adding the gate without rewriting it leaves the file contradicting itself, so the item says the reason moves with the code: staging is the rehearsal for what develop will promote, and a deploy from a feature branch proves something that is not what ships.

The existing full-github.ref comparison stays, for the reason already recorded there — a tag and a branch can share a short name, so a tag called develop would satisfy a ref_name test while pointing at an arbitrary commit. The new arm inherits that hazard exactly.

Three rows that moved

Re-measured rather than adjusted:

  • Release pipeline1.0.54 from main (Latest) and 1.0.54-gcd9285a9fe from develop, each tag pinned to its own branch tip. The clean-versus-suffixed split is the classification gate working, not a coincidence.
  • VPS production — now serving 20260809-030521, read back from the live X-Blog-Release header rather than from the run's exit code.
  • The mtime consequence — that release is the first production build with mtimes restored, so the first whose static tree can arrive as hard links, and the first where a bad file mode could ride one forward into every later release. The shared-inode count is server-side, so it is recorded as something to ask the host for rather than something to infer from here.

The release workflow gates its branch in both directions: a dispatch from
anything but main or develop is refused, and validate-release fails loudly
if the default branch carries a prerelease suffix or a non-default branch
carries none. The deploy has half of that. assert-ref refuses production
from any ref but main, and staging accepts any ref at all, so a staging
deploy can be dispatched from main, which is what happened here today.

Recorded with the part that makes it more than a one-line change: the
comment above that job argues for the present behavior, so the code and
the reason move together or the file ends up contradicting itself. The
full-ref comparison stays, for the reason already written there.

Three state rows had moved and are re-measured rather than adjusted:

  the release row, now 1.0.54 from main and 1.0.54-gcd9285a9fe from
  develop, each pinned to its own branch tip

  the production row, now release 20260809-030521, read back from the
  live X-Blog-Release header

  and the note that this is the first production release built with the
  mtimes restored, so it is the first that can arrive as hard links and
  the first where a bad mode could ride one forward. The shared-inode
  count is the host's to report, so it is asked for rather than inferred.

Requested by the maintainer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 9, 2026 03:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the project’s running TODO/state documentation to reflect the current release/deploy reality and to record a requested missing deploy ref gate for staging vs production.

Changes:

  • Refreshes the Release pipeline state row with the latest release/prerelease tags and branch-tip pinning notes.
  • Refreshes the VPS production state row with the currently served release and notes about restored mtimes / hard-link implications.
  • Adds a new blocked item documenting the request to gate deploy refs by environment (staging: develop, production: main) and the rationale for updating the existing workflow comment accordingly.
Suppressed comments (1)

TODO.md:22

  • The "Production is deployed" item still uses present tense ("serves release 20260808-154717") even though the State table above now says production is serving 20260809-030521. Please adjust this line to past tense / clarify it refers to the first production release (M7a record).
| VPS production | **M7a done 2026-08-08.** Serving release `20260809-030521` at `blog.insanegenius.net` as of 2026-08-09, deployed from `main` after the promotion and read back from the live `X-Blog-Release` header rather than from the run's exit code. That release is **the first built with the mtimes restored**, so it is the first whose static tree can arrive as hard links, and the first where a bad file mode could ride one forward. The shared-inode count is server-side and belongs to the host, so ask rather than inferring it from this side. The description below is the M7a record. Serving release `20260808-154717` at `blog.insanegenius.net`, answering `200` unauthenticated, deployed from `main` by pipeline with the 1,245-URL contract verified against the live site. `/robots.txt` answers 200 carrying a `.net` sitemap line, and the gallery fix is live. DNS for the public name is still on the old platform |

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread TODO.md Outdated
Comment thread TODO.md Outdated
Review caught three contradictions, all of them mine and all the same
mistake: new state was prepended to old prose instead of replacing it, so
each cell asserted both readings at once.

The Branches row said "Nothing published" and named 1.0.11 as newest,
while the row directly above it named 1.0.54. Both were written today.
What is actually true is narrower and is what the row now says: the merge
published nothing, because a human merge never auto-publishes, and the
releases were cut afterwards by explicit dispatch.

The VPS production row said "The description below is the M7a record"
with that record inlined immediately after it in the same cell, so the
sentence pointed at nothing a reader could locate. The row now leads with
what is served today and names M7a as history at the end.

The Next item still said production serves 20260808-154717 in the present
tense. It now names both and points at the State table as the current
value, so the number lives in one place.

Two more of the same class that review did not reach:

The "Publish a release from main, once the pipeline has soaked" item is
done and is retired. The soak reasoning is kept deliberately, because it
was overridden rather than satisfied and the same argument returns at the
next release: 1.0.54 names a working pipeline and a rehearsal host, not a
completed cutover.

The robots.txt item's reference to 20260808-154717 is left alone. It
reads "verified from the served bytes on release ...", which is a past
measurement and is still true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 03:14
@ptr727

ptr727 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Answering the suppressed finding, which carries no thread to resolve.

TODO.md:22 — The "Production is deployed" item still uses present tense ("serves release 20260808-154717") even though the State table above now says production is serving 20260809-030521. Please adjust this line to past tense / clarify it refers to the first production release (M7a record).

Correct, and fixed in a1dc721. The bullet now reads:

blog.insanegenius.net answers 200 unauthenticated on a Let's Encrypt certificate issued 2026-08-07, with the serving release read from the X-Blog-Release header rather than from a pipeline's exit code. It serves 20260809-030521 today; M7a itself was 20260808-154717, and the State table above is the current value, so read it there rather than from this paragraph.

The number now lives in one place, and the paragraph points at it rather than restating it — which is the actual fix, since a second copy of a moving value goes stale again on the next deploy.

Two more of the same class that the review did not reach

All three findings were one mistake: new state prepended to old prose instead of replacing it, so a cell asserted both readings at once. Looking for the rest of that class rather than only the flagged instances:

  • The release hold is retired. "Publish a release from main, once the pipeline has soaked" is done as of today. The soak reasoning is deliberately kept, because it was overridden by the maintainer rather than satisfied, and the same argument returns at the next release: 1.0.54 names a working pipeline and a rehearsal host, not a completed cutover.
  • The robots.txt item's 20260808-154717 is left alone. It reads "verified from the served bytes on release …", which is a past measurement and still true. Changing it would have been the same error in reverse.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Suppressed comments (2)

TODO.md:16

  • The Branches state row says “the releases in the row above”, but the release information is in the “Release pipeline” row below. This reads like a leftover directional reference and is confusing in the table.
| Branches | converged 2026-08-09. [#68][pr-68] promoted `develop` to `main` as a merge commit, `main` carries every commit on `develop`, and the content diff is empty. **The merge itself published nothing**, which is the release model behaving as specified: a human merge never auto-publishes, and the releases in the row above were cut afterwards by explicit dispatch |

TODO.md:49

  • This item cites a validate-release gate, but this repository’s release workflow (.github/workflows/publish-release.yml) does not define a validate-release job/step. As written, the documentation suggests a check that doesn’t exist here; it would be clearer to reference what actually enforces release/prerelease classification in this repo (NBGV via version.json).
- **Gate the deploy ref by environment in both directions, the way the release workflow already gates its branch.** [`publish-release.yml`](./.github/workflows/publish-release.yml) refuses a dispatch from anything but `main` or `develop`, and `validate-release` fails loudly if the default branch carries a prerelease suffix or a non-default branch carries none, so neither branch can cut the other's release kind. The deploy has only half of that: [`deploy-site.yml`](./.github/workflows/deploy-site.yml)'s `assert-ref` job refuses production from any ref but `main`, and **staging accepts any ref at all**. Add the missing arm so staging accepts only `develop`, giving the same symmetry: `develop` deploys staging, `main` deploys production, and neither can reach the other's environment.

The item claimed publish-release.yml carries a validate-release job that
fails when a branch's version classification disagrees with it. It does
not. There is no validate-release anywhere under .github/ in this repo:
that is a build-repo construct in the fleet contract, and I described the
contract as though it were the implementation.

What is actually there is one guard, an Assert dispatch ref step refusing
any ref but main or develop. Everything else is derived from the ref with
nothing to disagree with it. NBGV reads publicReleaseRefSpec from
version.json to give main a clean X.Y.Z and every other branch a -g<sha>
suffix, and the release's prerelease flag is computed as ref_name is not
main. main cannot cut a prerelease because no input exists that could ask
it to.

That correction makes the item's argument better rather than weaker, so
the argument is rewritten rather than patched. The deploy is not lacking
symmetry with the release. It differs in kind: environment is an
independent dispatch input, so the ref and the target are two values that
can disagree, and only one of the two disagreements is caught today. A
workflow taking its target as a separate input has to check it; one
deriving its target from the ref has nothing to check.

Also: the Branches row pointed at "the row above" for the release
versions, which sit two rows below it.

Both found by review, as suppressed comments carrying no thread.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 03:20
@ptr727

ptr727 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Answering two suppressed findings from the last round, neither of which carries a thread. Both are right, and the second one is the substantive one.

1. validate-release does not exist in this repo

TODO.md:49 — This item cites a validate-release gate, but this repository's release workflow (.github/workflows/publish-release.yml) does not define a validate-release job/step. As written, the documentation suggests a check that doesn't exist here.

Correct. Measured:

$ grep -rn "validate-release" .github/
NONE in .github/

I described the fleet contract as though it were this repo's implementation. validate-release is a build-repo construct in WORKFLOW.md's D2.2; this is a source-only repo and ships nothing of the kind. Fixed in c20ac82.

What publish-release.yml actually has is one guard — an Assert dispatch ref step refusing any ref but main or develop — and everything else derived from the ref, with nothing to disagree with it:

Mechanism Source
clean X.Y.Z on main, -g<sha> elsewhere NBGV reading publicReleaseRefSpec in version.json
the release's prerelease: flag ${{ github.ref_name != 'main' }}

main cannot cut a prerelease because no input exists that could ask it to.

This makes the item's argument better, so I rewrote it rather than patching it

My original framing — "the deploy should be symmetric with the release" — was wrong even once the false claim is removed, because the two are not the same shape. The deploy takes environment as an independent workflow_dispatch input, so the ref and the target are two values that can disagree, and only one of the two possible disagreements is caught today.

That is the real argument: a workflow taking its target as a separate input has to check it; one deriving its target from the ref has nothing to check. Which is a reason the gate is needed, not an analogy to something else.

2. Directional reference

TODO.md:16 — The Branches state row says "the releases in the row above", but the release information is in the "Release pipeline" row below.

Right — Branches is line 16 and Release pipeline is line 18. Now reads "two rows down".

Thanks for both; the first one in particular was a claim I should have measured before writing.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Suppressed comments (2)

TODO.md:22

  • The state table’s URL contract row implies 328 render + 917 redirect = 1,245 URLs, but this row claims a “1,253-URL contract”. Either the underlying counts changed (and the URL contract row should be refreshed) or this number should be removed/clarified so the State section remains internally consistent.
| VPS production | serving release `20260809-030521` at `blog.insanegenius.net` as of 2026-08-09, deployed from `main` after the promotion and read back from the live `X-Blog-Release` header rather than from the run's exit code. `200` unauthenticated, `X-Robots-Tag: noindex, nofollow` still set for the rehearsal, and the 1,253-URL contract verified against the live site. That release is **the first built with the mtimes restored**, so the first whose static tree can arrive as hard links, and therefore the first where a bad file mode could ride one forward into every later release. The shared-inode count is server-side, so ask the host for it rather than inferring it here. **M7a, the first production deploy, was 2026-08-08 on release `20260808-154717`**, which established the unauthenticated `200`, the `.net` sitemap line in `/robots.txt`, and the gallery fix. DNS for the public name is still on the old platform |

TODO.md:50

  • This paragraph says publish-release.yml has “exactly one guard”, but the workflow also has the reusable validate job that gates the dispatch. If the intent is “exactly one ref/branch guard”, please say that to avoid a factual mismatch with the workflow.
  - **The release workflow is safe by derivation rather than by a gate, which is the distinction to get right before copying anything from it.** [`publish-release.yml`](./.github/workflows/publish-release.yml) has exactly one guard, an `Assert dispatch ref step` refusing any ref but `main` or `develop`. Everything else follows from the ref with nothing to disagree with it: NBGV reads `publicReleaseRefSpec` in [`version.json`](./version.json) to give `main` a clean `X.Y.Z` and every other branch a `-g<sha>` suffix, and the release's `prerelease:` is computed as `github.ref_name != 'main'`. **`main` cannot cut a prerelease because no input exists that could ask it to.** This repo has no `validate-release` job — that is a build-repo construct in the fleet contract, not something shipped here, and an earlier draft of this item claimed it did.

Two numbers in the State table disagreed without saying why. The URL
contract row implies 328 + 917 = 1,245, and the production row claimed a
1,253-URL contract. Both are right and they count different things:

  build gate  328 render, 917 redirect, 778 legacy image URLs, against
              files on disk
  live check  328 + 917 + 8 curated media = 1,253, against a running
              server

Verified by counting the lists rather than by arithmetic on the prose:
golden-urls.txt is 328, redirect-urls.txt is 917, golden-media-live.txt
is 8. The URL contract row now states that two gates read different
subsets and why the live one carries 8 images rather than 778, and the
production row names its total as that subset rather than as a second
contract.

The 1,245 under the HUGO_BASEURL item is deliberately left alone. It is a
counterfactual about 2026-08-07, when the live check was 1,245, so
updating it to today's number would make a true sentence false.

Separately, "exactly one guard" was wrong: publish-release.yml also runs
the reusable validate job the pull request runs, so a dispatch cannot
release a ref that fails validation. It is now "exactly one ref guard",
with the validate job named and the distinction stated, since it gates
the sources rather than the branch-to-classification relationship the
item is about.

Both found by review, as suppressed comments carrying no thread.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 03:25
@ptr727

ptr727 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Answering two more suppressed findings, both right, fixed in 7be7345.

1. Two URL totals in one table with no explanation

TODO.md:22 — The state table's URL contract row implies 328 render + 917 redirect = 1,245 URLs, but this row claims a "1,253-URL contract".

Both numbers are correct and they count different things, which the table never said. Counted from the lists rather than from the prose:

checks/golden-urls.txt        328
checks/redirect-urls.txt      917
checks/golden-media-live.txt    8
                             ----
                             1253
Gate Reads Total
build (check-url-parity.py) 328 render, 917 redirect, 778 legacy image URLs, against files on disk
live (check-live-urls.sh) 328 + 917 + 8 curated media, against a running server 1,253

The live check carries 8 images rather than 778 deliberately: proving 778 over the wire on every deploy buys nothing the build gate has not already proven, and the 8 exist to catch what the build gate structurally cannot — a media tree lost between the build and the server. The URL contract row now says that, and the production row names its total as that subset rather than as a second contract.

One 1,245 is deliberately left in the file. The HUGO_BASEURL item says a wrong base URL "would have ... run 1,245 requests at the live site to verify it". That is a counterfactual about 2026-08-07, when the live check was 1,245, so updating it to today's number would turn a true sentence false. Same class of error as the one you caught earlier, in the opposite direction.

2. "Exactly one guard" is wrong

TODO.md:50 — This paragraph says publish-release.yml has "exactly one guard", but the workflow also has the reusable validate job that gates the dispatch.

Correct — it does:

  # The same reusable gate the PR runs - a dispatch cannot release a ref that fails validation.
  validate:
    name: Validate sources job
    uses: ./.github/workflows/validate-task.yml

Now "exactly one ref guard", with the validate job named and the distinction stated: it gates the sources, not the branch-to-classification relationship the item is about. Your suggested phrasing, essentially.

Three rounds, six findings, all of them mine and all of them real — this file is the one place in the repo where nothing mechanical checks a claim, which is showing.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Suppressed comments (1)

TODO.md:45

  • This bullet still says “The next production deploy is the first to exercise it”, but the State table now records that production is already serving 20260809-030521 and that it’s the first production build with mtimes restored. Update this sentence so it reflects that the first production deploy exercising git-restore-mtime has already happened.
- **~~Restore file mtimes in CI so `--link-dest` links.~~ Done, [#65][pr-65], and on `main` since the 2026-08-09 promotion.** `deploy-site-task.yml` installs `git-restore-mtime` and runs `git restore-mtime static` before the upload. Measured with two independent clones each way: 0 of 1791 files linked without the restore, 1052 with, and that 1052 corroborates from three directions, being what Hugo reports as static files, what links on a locally built release, and the file count in `static/`. **The next production deploy is the first to exercise it**, so it is also the first where a badly moded file can ride a link into every later release, which is the trap below. The live media check landed first, deliberately, and is what watches for exactly that. The reasoning is kept below because it is what the next change to the deploy's linking behavior will need.

The item said "the next production deploy is the first to exercise it".
That was true when written and stopped being true an hour later in the
same session, when 20260809-030521 deployed. It now names that release
and dates it.

The sentence also carried the only forward-looking part worth keeping, so
that is stated as the open half rather than dropped: the shared-inode
count is server-side, the host had been measuring 0 across every pipeline
release, and roughly 1052 of 1791 is what the change predicts. It is
asked in the channel rather than inferred from this side, because nothing
here can read an inode on that host.

Swept for the rest of the class and found none: no other future-tense
claim in the file describes something already done.

Found by review, as a suppressed comment carrying no thread.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 03:28
@ptr727

ptr727 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Fixed in 4cd4fbf.

TODO.md:45 — This bullet still says "The next production deploy is the first to exercise it", but the State table now records that production is already serving 20260809-030521.

Right. That sentence was true when written and stopped being true about an hour later in the same session, when 20260809-030521 deployed. It now names the release and dates it.

The sentence also carried the one genuinely forward-looking thing in that item, so rather than just fixing the tense I split it out as the open half:

The shared-inode count is the open half: it is server-side, the host had been measuring 0 across every pipeline release, and roughly 1052 of 1791 is what the change predicts. Asked in the channel rather than inferred from this side.

Nothing here can read an inode on that host, so that stays a question rather than a claim.

Swept for the rest of the classThe next .* deploy, will be the first, is the first to exercise — and there is no other future-tense sentence in the file describing something already done.


Four rounds, seven findings, every one real and every one mine. They are all the same failure with different surfaces: this file records state, the session kept changing that state, and prose written at 02:30 was false by 03:10. Worth noting that the two suppressed findings from the previous round are re-reported above as "earlier round" — both were already answered and fixed in 7be7345, so no action on those.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

@ptr727
ptr727 merged commit 555e97a into develop Aug 9, 2026
5 checks passed
@ptr727
ptr727 deleted the todo-deploy-ref-guards branch August 9, 2026 03:33
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.

2 participants