Skip to content

Promote develop to main: the deploy pipeline and hub conformance - #19

Merged
ptr727 merged 11 commits into
mainfrom
develop
Aug 4, 2026
Merged

Promote develop to main: the deploy pipeline and hub conformance#19
ptr727 merged 11 commits into
mainfrom
develop

Conversation

@ptr727

Copy link
Copy Markdown
Owner

Promotes nine commits, spanning two sessions. The merge method is a merge commit, per the main ruleset.

What lands

The deploy pipeline (#16). Two environments addressed by one artifact, shipped to a host by rsync with no restart. The bundle stamps its own release id, and the verifier asserts which release is answering before checking the URL contract, because a deploy is asynchronous and the transport succeeding proves nothing about what the host serves. Also X-Blog-Env and X-Robots-Tag from container variables, trusted_proxies with the bridge gateway excluded, and auth-gate support for a non-public environment.

The workflow contract (#17, #18). The carried WORKFLOW.md had drifted one revision behind the hub and is re-vendored. The deploy has no destination in that contract, which is recorded as debt and filed upstream as ProjectTemplate#558 with proposed wording. It is a variant of the existing registry-push leaf rather than a new release surface, so the release and branching models are unchanged.

Earlier session work (#10 through #15). Verbatim carries re-vendored, doc corrections, the conventions move, hub conformance state, and a Dependabot bump.

Why this matters beyond the diff

deploy-site.yml is workflow_dispatch, and GitHub only registers a dispatch workflow from the default branch. Until this promotion lands, the deploy pipeline exists on develop but cannot be triggered at all.

Verification

  • Both local mirrors: PASS - 1245 URLs honored, deploying with no restart.
  • VPS staging: release 20260804-190007 live behind the auth gate, pushed through the confined key, full contract verified.
  • Release 1.0.17-g4b2def3ee9 cut from develop, conforming to D4.1 through D4.3.
  • Merge is clean against main, and every changed file is LF, matching the repo's recorded lineEndings deviation.

🤖 Generated with Claude Code

dependabotBotand others added 9 commits August 3, 2026 12:37
Bumps the actions-deps group with 3 updates: [actions/checkout](https://github.com/actions/checkout), [softprops/action-gh-release](https://github.com/softprops/action-gh-release) and [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action).
Updates `actions/checkout` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@9c091bb...3d3c42e)
Updates `softprops/action-gh-release` from 3.0.1 to 3.0.2
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](softprops/action-gh-release@718ea10...3d0d988)
Updates `DavidAnson/markdownlint-cli2-action` from 24.0.0 to 24.1.0
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](DavidAnson/markdownlint-cli2-action@8de2aa0...6bf21b0)
---
updated-dependencies:
- dependency-name: actions/checkout
dependency-version: 7.0.1
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: actions-deps
- dependency-name: softprops/action-gh-release
dependency-version: 3.0.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: actions-deps
- dependency-name: DavidAnson/markdownlint-cli2-action
dependency-version: 24.1.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: actions-deps
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Re-vendor the eight verbatim carries the hub advanced past
The hub's first audit of this repo (ProjectTemplate#547) found eight
verbatim units behind the canonical at hub main `3a7cc64`. Six are stale
copies and two never arrived at all, and the distinction matters,
because a stale copy still states the rule in an older form while an
absent one states nothing.
## Absent, so this is the first time the rule lands here
- `AGENTS.md > Fleet Bootstrap` says where the canonical rules live and
routes by the state this repository is actually in. Without it, an agent
starting here has nothing telling it the hub exists.
- `GOVERNANCE.md > Representative Data in Agent-Authored Text` is the
rule that agent-authored text illustrates with constructed data rather
than with data observed in the maintainer's environment.
## Stale, so the rule was here in an older form
- `AGENTS.md > Context and Delegation Discipline` gains the rule that a
wait separates three outcomes and says which one it reached.
- `AGENTS.md > Where the Rules Live` gains the row routing to the
representative-data rule above.
- `GOVERNANCE.md > Git and Commit Rules` gains "Commit means commit and
push".
- `GOVERNANCE.md > Verification Discipline` gains "A launched process is
not a result, and a cause nobody observed is not a diagnosis".
- `GOVERNANCE.md > PR Review Etiquette` gains the whole "Every Finding
Ends in an Action" subsection, so the review loop arrives with the five
outcomes that close a finding rather than without them.
- `repo-config/configure.sh` takes the payload-driven check mode. The
copy here compared the merge methods and the required-status-check
contexts by name; the canonical compares every parameterized rule's
whole parameters object in both directions, so a rule added live that no
payload declares is now caught.
TODO.md says not to hand-edit `configure.sh` or the ruleset payloads.
Re-vendoring from the hub canonical is the sanctioned exception and the
reason that rule exists. The two ruleset payloads are deliberately not
touched: ProjectTemplate#545 changed them an hour ago on the hub's
`develop`, and ground truth is the hub's `main`, so that one converges
when it promotes.
The intro line is adjusted from two things to four, because the file now
holds the bootstrap as well.
## Verification
- Every verbatim unit byte-matches the canonical after LF normalization:
the three `AGENTS.md` sections, the eighteen `GOVERNANCE.md` sections,
`configure.sh`, and `.markdownlint-cli2.jsonc`. The only two that differ
are `Devcontainer` and `Repository Layout`, both intent fidelity.
- `repo-config/configure.sh check ptr727/Blog release` exits 0 under the
new stricter check mode, so the live rulesets and settings still
conform, which is the check that would have caught a bad re-vendor.
- markdownlint, editorconfig-checker, and the JSON and prose gates are
clean, and all 20 rows of the rules map resolve to a carried section
except the one the table itself marks hub-only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* State the hub ref the carry now matches
The report named `6501479`, the ref the baseline was carried from. The
re-vendor in this branch moves every verbatim unit to `3a7cc64`, so the
line named a ref the tree no longer matched the moment the rest of this
change landed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Correct the docs that stopped describing the repository
The hub's first audit of this repo (ProjectTemplate#547) found three
prose violations and, reading the docs alongside it, three claims that
were true when written and are not now. A doc that describes a past
state is worse than an absent one, because a reader cannot tell which
half still holds.
## Claims that outlived their state
- `reports/Blog/audit.md` said the release path was untested because the
publisher had never been dispatched. It was dispatched on 2026-08-01 and
cut release `1.0.11`, so the path is proven rather than untested.
- The same file said the registry entry stays `publish: []`. The hub now
carries an entry declaring the GitHub release, which is the only channel
that ships today. The VPS target is still deferred, and that half is
unchanged.
- `TODO.md` still listed dispatching the publisher as the next task.
Removed, and the State table gains the release-pipeline row, so the fact
that it is proven is recorded rather than merely no longer denied.
## The current-time rule
`HISTORY.md` described the 1.0 content as "having previously lived
inside a hosted blogging platform". Release notes are written in current
time, describing what the thing is: a reader of 1.0 never saw the
platform it came from, so the delta is noise that only makes sense to
whoever did the work.
## Prose
Two of the three findings are fixed: a comment opening on a lowercase
identifier in the merge-bot workflow, and a semicolon in `OPERATIONS.md`
prose. The third is the COPYRIGHT SIGN in `layouts/rss.xml`, left alone
deliberately. The character-set tiers classify it nowhere, so the linter
reports `charset-unknown` rather than pass or fail, and transliterating
it in a machine-read feed to satisfy a rule that does not exist yet is
the wrong way round. It is raised as an escalation in the hub's audit
report and is the hub's call.
## Verification
markdownlint, editorconfig-checker, and the JSON gates are clean, and
`prose_lint.py` over the repo excluding the imported archive now reports
only the escalated `charset-unknown`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Say which setting stops the cancellation, not which adjective
The reworded comment opened "The false cancel-in-progress", which reads
as an adjective modifying the key name rather than as the key's value.
Naming the setting and its value says the same thing without the
ambiguity, and it still opens on a capital, which is what the rewording
was for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Separate the release, which passes, from the deploy, which is deferred
The section read "Publish and Release: Deferred" over a paragraph saying
the release is proven end to end, which is a heading contradicting its
own body. A reader skimming headings got the opposite of the finding.
They are two surfaces and only one is outstanding. The release is
dispatch-only and proven by release `1.0.11`, and the hub registry
declares it as such. The deploy to the VPS is the deferred half, and it
is deferred because the VPS does not exist rather than because anything
failed.
Fixing the heading exposed two more places stating the merged claim: the
verdict paragraph and the dimension table, which carried one "Publish
and release: Deferred" row where there are two answers. Both now say
which surface they mean, and the stale "`publish` is empty and
`releaseTrigger` is `none`" line goes with them, since the registry
entry landing in ProjectTemplate#547 declares the GitHub release and
`dispatch-only`.
Found by a suppressed low-confidence Copilot finding on the previous
push, which was correct.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Name the deferred surface the VPS deploy, not the publish
The residual-delta bullet still called it the publish surface, from
before the section split release from deploy. That reading now includes
the GitHub release, which ships and is proven, so the bullet said the
proven half was deferred.
Found by a suppressed low-confidence Copilot finding, the second in a
row on this branch that was correct.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
)
The hub promoted `develop` to `main` as `7194f91` while the previous two
convergence pull requests were open, so the ground truth this repo is
measured against moved under them. ProjectTemplate#545 takes
`bypass_actors` out of all three payloads, and rewrites `configure.sh`
around that decision.
The rewrite is the substantive half. Apply now reads the live bypass
list and writes it back unchanged, because a PUT replaces the whole
document and omitting the field would delete the list rather than leave
it alone, and it aborts rather than proceeding if that read fails. Check
reports the live list and asserts nothing about it. Who may bypass a
ruleset decides who the rules do not apply to, which is a decision for
the maintainer rather than a value the fleet config hands out.
## What this deliberately does not do
Remove the live bypass. Both rulesets here carry the `RepositoryRole`
admin entry the old payload granted every repo, and nothing in this
change touches it. Removing it is a protected-branch settings write and
a decision the hub just declared human, so it is the maintainer's.
Worth knowing that the two tools now disagree about that state.
`configure.sh check ptr727/Blog release` exits 0 and reports the bypass
as unmanaged, while `spec/audit.py` still compares `bypass_actors` and
calls the same state a DEFECT on both branches. Raised as
ProjectTemplate#554, since it makes every fleet repo carrying a bypass
permanently not operational on a field the fleet decided not to manage.
## Verification
- All four verbatim units byte-match hub `main` `7194f91` after LF
normalization: the three re-vendored here plus `.markdownlint-cli2.jsonc`.
- `repo-config/configure.sh check ptr727/Blog release` exits 0 under the
rewritten check mode, and reports "bypass list: RepositoryRole 5 always
(not managed by this script)" on both rulesets rather than asserting it.
- The JSON and editorconfig gates are clean.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Put the repo's own conventions where the section model sends them
`AGENTS.md > Project Conventions` was a section the hub does not
declare, which the audit flags as drift to reconcile. Reconciling it
found that most of it was already reconciled elsewhere.
Three of the four rules were near-verbatim duplicates of entries already
in `TODO.md` Traps: never populating media over HTTP, `content/` being
an imported archive, and a gate proving itself by failing. Duplicated
rules are worse than absent ones, because the two copies drift and
nothing says which is current, so those three are deleted here rather
than moved.
The fourth was unique and is the load-bearing one. The URL contract
being ground truth and its lists being append-only appeared nowhere
else, and in particular not in `OPERATIONS.md`, which operates that
contract through the whole local-verification and deploy path without
ever stating the invariant behind it. It moves there, ahead of the
verification section that depends on it, and picks up the length-floor
clause that `TODO.md` carried separately, since a floor is what makes
append-only checkable rather than aspirational.
`AGENTS.md` is left carrying only the three sections the hub declares,
and its intro drops from four things to three, which is what the hub's
own intro says.
Note this does not resolve ProjectTemplate#552. That contradiction is
about the byte-locked `Fleet Bootstrap` section naming the template
repo, which is untouched here and still flagged.
## Verification
- Every verbatim unit still byte-matches the canonical, so removing the
local section disturbed none of the carried ones.
- markdownlint, editorconfig-checker, and the prose gate are clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Point at the doc that owns a convention, now that one moved
The preamble said this project's own conventions live "here", which was
true while `Project Conventions` was in this file and stopped being true
in the same change that moved it out. An agent reading the sentence
would look for repo invariants in `AGENTS.md` and find none.
The sentence's actual point is provider independence, that a rule a
reviewer must honor cannot live in the Copilot-specific file, and that
point survives intact. It now names the topical docs that own the
content instead of claiming this file does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Record the hub conformance state and what is open against the hub
This repo reconverged with the fleet hub on 2026-08-03, and four
findings are open at the hub afterwards. None is work this repo can do,
and each changes what a fleet audit of this repo means, so a reader who
runs one and sees two DEFECTs needs the reason in the repo rather than
only in an issue tracker.
The State table gains a fleet-conformance row. A new section records
where conformance stands, links the hub registry entry and the audit
report the hub authored, and lists the four open issues with what each
means here rather than restating the issue.
It also records that the live ruleset bypass is deliberate and stays.
The owner is automatically an admin and holds that capability anyway, so
the entry grants nothing new, and the payloads stopped declaring it
because code should not be in the business of granting a bypass at all.
That reasoning existed only in conversation, which is exactly the kind
of thing a later audit misreads as drift.
## Two traps
The carried-file trap gains its second half: a reviewer finding a real
defect in a verbatim file is answered by declining locally and filing it
at the hub, never by editing the file. That happened this session on a
correct jq portability finding, and editing would have broken the fleet
audit that governs the file.
The new one is that the hub's `main` can promote while a convergence
pull request is open, so ground truth moves underneath work that was
correct when it started. It happened twice in one session, and the
second time added drift the branch could not have known about. The
defense is to re-run the audit against the hub ref actually carried and
name that ref in the change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Name the hub ref this record was measured against, and pin to it
The section claimed a byte-match against the canonical, linked the hub
by `main`, and then two entries below warned that `main` moves under
work that was correct when it started. A record that cannot say which
ref it describes is the failure its own trap names.
It now states hub `main` `3b802b9`, and both hub links are pinned to
that ref rather than to `main`, so the claim stays checkable after the
hub moves again.
Re-measuring for the ref turned up one exception worth recording rather
than rounding away. Every verbatim section, both ruleset payloads, and
`.markdownlint-cli2.jsonc` match. `repo-config/configure.sh` is one
commit behind, on the change that fixes the jq portability defect this
repo reported, so the re-vendor closing that loop is still owed.
Found by a Copilot review that read the claim against the trap two
entries below it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Pin the hub ref by full SHA, since a short one can go ambiguous
GitHub resolves an abbreviated SHA only while it stays unique, and a
`blob/<ref>/` link built on one breaks when the hub's history grows
enough to collide. That is a record designed to expire, in the section
whose whole point is staying checkable after the hub moves.
All three references take the full 40 characters, the two link
definitions and the ref named in the prose.
Found by a suppressed low-confidence Copilot finding, the fourth this
session that was correct.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
)
* Address two environments instead of one
The release tooling could name a single deploy root, which was enough while
there was one mirror and will not be once the VPS grows a staging site and a
production site. Nothing here could address two at a time.
The blocker was not the obvious one. make-release.sh sourced secrets/.env
with `set -a`, which exports every assignment in the file and overwrites a
DEPLOY_ROOT the caller exported first, so sourcing a different environment
file silently deployed to the original root. On a host serving two sites that
failure is not an error, it publishes to the other site. ENV_FILE now selects
the file, the first argument still overrides the root because it is read
afterwards, and a named file that does not exist is a hard failure rather
than a fall-through to whatever the ambient environment holds.
check-live-urls.sh could not authenticate, so it could not check a site that
keeps its auth gate on. Staging keeps its gate on deliberately: it serves a
byte-identical copy of the public site, and an open one is a duplicate handed
to every crawler, which is the one thing a migration about preserving URLs
should not do. A Pangolin resource access token opens it instead.
The credential travels in a mode-600 curl config file rather than in -H
arguments. A command line is readable in ps for the life of the process and
this runs 1,245 of them, and the config file is also the only form that
survives the `export -f` the parallel checks run under, since bash cannot
export an array. It is sent to the base URL's own origin and nowhere else, so
a rule that one day redirects off-site cannot carry it away. A preflight
request runs first, because behind an auth gate a wrong token fails every URL
and the output then reads as a site that has vanished rather than as a bad
credential.
Two decisions the docs asserted the opposite of, both now recorded with their
reasoning rather than silently reversed. One deploy key covers both
environments instead of one per environment: the split pays off only where
the two keys never share a machine, and both sit on one workstation and in
one secret store. The cost is that the rrsync forced command can no longer
separate the environments, so the deploy roots move under a common parent and
one pinned command covers both. And staging takes the VPS wildcard at
blog.vps.insanegenius.net, which needs no new certificate and closes the open
FQDN decision.
Verified against both local mirrors, which are two containers in a sibling
repository's new blog stack: 1,245 URLs honored on each, each baking its own
base URL, neither disturbing the other's release history. Each new gate was
demonstrated failing before being trusted, per the rule in TODO.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Deploy to an environment and prove the release is the one answering
A release reaches a host by rsync and no restart, so nothing outside the
verifier observes whether the rules that answer are the rules shipped.
Content follows the current symlink per request and is live immediately,
while rules wait on an in-process config reload. A container that never
reloads therefore serves new content under a previous release's redirects,
and the URL contract passes against a config that was never deployed.
The bundle stamps its own version, substituted at install time, and the
check compares it before requesting any of the 1,245 URLs. It waits rather
than sampling once, because the reload is asynchronous and a check run
straight after a deploy races it. The timeout still catches a container
that never converges.
Caddy also stops watching permanently after one failed config load, without
logging that it has given up, so anything that breaks the symlink even
briefly ends that container's ability to pick up releases until it is
restarted. The failure reports as healthy, which is why the verifier names
both causes.
Two more headers identify the environment, since every environment serves
one bundle on one port and only the container distinguishes them. Robots
defaults to the value that is harmless on production, because the failures
are asymmetric: an unset staging container is still behind its auth gate,
where production inheriting noindex would deindex the site silently.
trusted_proxies takes its ranges from the container, and excludes the
bridge gateway. Trusting the subnet trusts the host, which was verified by
forging a client address from it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Anchor ENV_FILE under the repo and correct the restart note
A relative ENV_FILE resolved against the caller's working directory while
the default resolved against the repo, so the same name meant different
files depending on where the script ran.
The container comment described a restart as the way a config change goes
live, which the in-process reload replaced.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Scope the staging-only secrets and correct the environment prose
The manifest listed the access token beside a credential every environment
carries, which reads as required in both. A name audit would then report it
missing from production, where it is deliberately absent.
The README described configuration as coming from one file, which the
per-environment selection replaced.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Assert the release placeholder and constrain the environment name
sed reports success when it matches nothing, so a Caddyfile that lost the
placeholder shipped unstamped and the live check then blamed a dead config
watcher for a bundle that never carried a release id.
The environment name selects a GitHub Environment and lands in a remote
path, and a workflow_call caller is not bound by the dispatch choice list.
It is asserted in its own job, because the environment binding resolves
before any step in the deploying job runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Record the reviewer login trap that reads as a clean review
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Describe the accepted statuses, the env file, and the default name
The redirect failure named one status while the check accepted two, and the
usage text described a deploy root the environment file selection replaced.
The setup instruction named only the per-environment form, so a single
environment produced a file nothing reads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Require an origin boundary before sending the access token
The base URL has its trailing slash stripped, so a prefix test also matched
a host that merely starts with it. A redirect to a lookalike registered as
an attacker's subdomain would have received the credential.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Constrain the release version to a path-safe and substitution-safe form
The value becomes a directory name, a symlink target, and a sed
replacement, so a separator, a traversal, or a substitution metacharacter
would place the release outside releases/ or corrupt the release stamp.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Record the deploy scripts' Linux dependency ceiling
The scripts use GNU and bash 4.4 constructs that a stock macOS lacks, and
the platform rule requires a narrowed platform to be recorded so it reads
as a dependency ceiling rather than an oversight.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Report an unreachable host as transport rather than as a status
A name that does not resolve produced a status code, which the preflight
then explained as a bad credential or a broken symlink.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The carried CI/CD contract had fallen one revision behind the hub on the
release-classification guarantee, where the hub describes the smoke-build
case in terms the local copy had lost.
The deploy this repo now performs has no destination in that contract, and
the shape it needs is a variant of the existing registry-push leaf rather
than a new release surface: the same dispatch-gated leaf publishing to its
own destination, differing only in the build tool and the transport.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Point the hub debt at the issue carrying the proposed wording
The intake thread holds the questions and their answers, and the change
itself needs an issue that can be worked without reconstructing which
framing stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Name the repository in the hub issue reference
The reference sat beside a spelled-out one in the same sentence, so the
bare form read as a local issue number.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Record the review tooling the hub already authors
The review loop was hand-rolled, which re-discovered two failures the hub
script exists to prevent: findings suppressed into a review body reach no
thread, and a request nothing picks up is indistinguishable from a slow
reviewer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Drop the counted cross-reference from the tooling note
A count of the bullets that follow it is wrong as soon as one is added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings August 4, 2026 22:23

CopilotAI 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

Promotes the deploy pipeline and related governance/conformance updates from develop to main, making the site deploy runnable from the default branch while tightening release/deploy verification and updating carried workflow contract documentation.

Changes:

  • Adds GitHub Actions deploy workflows (deploy-site.yml dispatch + reusable deploy-site-task.yml) that build, rsync, flip current, then verify the live URL contract against the running host.
  • Strengthens deploy verification by stamping releases into the served config (X-Blog-Release) and extending checks/check-live-urls.sh with preflight checks (auth-gate support, environment assertion, and release-id convergence wait).
  • Updates governance/docs/audit artifacts and ruleset tooling to reflect hub conformance state and “hands off” bypass actor management.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
WORKFLOW.mdUpdates workflow contract prose for D2.2 smoke-build behavior.
TODO.mdUpdates project status, hub-debt links, and conformance/traps documentation.
spec/secrets.jsonDocuments env secret expectations, including staging-only Pangolin token pair.
reports/Blog/audit.mdUpdates self-audit to reflect proven release vs deferred VPS deploy.
repo-config/main.jsonRemoves committed bypass_actors payload content (now preserved from live).
repo-config/develop.jsonRemoves committed bypass_actors payload content (now preserved from live).
repo-config/configure.shPreserves live bypass list on apply; strengthens drift checks for rulesets/settings.
README.mdRefreshes repo description/links/badges and clarifies environment-file usage.
OPERATIONS.mdExpands environment model and deploy verification rationale (watch reload, auth gate, etc.).
HISTORY.mdAdjusts release-history wording for v1.0 description.
GOVERNANCE.mdAdds “Representative Data” rule section and strengthens PR-review loop contract language.
deploy/README.mdDocuments Linux ceiling, environment-file selection, watch reload contract, and headers.
deploy/make-release.shAdds ENV_FILE selection, version validation, and stamps @@RELEASE@@ into shipped Caddyfile.
deploy/env.exampleUpdates env template for multi-environment usage and auth-gate verification inputs.
deploy/CaddyfileAdds trusted_proxies handling and stamps env/robots/release headers.
checks/check-live-urls.shAdds auth-gate token support + preflight env/release assertions and reload wait.
AGENTS.mdAdds fleet bootstrap section and updates session-scope guidance.
.github/workflows/validate-task.ymlBumps pinned action SHAs (checkout, markdownlint).
.github/workflows/publish-release.ymlBumps pinned action SHAs (checkout, gh-release).
.github/workflows/merge-bot-pull-request.ymlComment wording tweak for concurrency behavior.
.github/workflows/deploy-site.ymlAdds dispatch entrypoint workflow to deploy staging/production with gating.
.github/workflows/deploy-site-task.ymlAdds reusable deploy workflow: build bundle, rsync upload/flip, verify live contract.

Comment threadchecks/check-live-urls.sh
* Separate a transport failure from a missing header while waiting
The reload wait read an unreachable host as a reply carrying no release,
so a connection lost mid-wait reported as a config that never reloaded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Keep a missing header from reporting as an unreachable host
Pipefail carries grep's no-match status out of the read, so a reachable
host serving no release header reported as a lost connection.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 4, 2026 22:42

CopilotAI 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 22 out of 22 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

.github/workflows/deploy-site.yml:26

  • The assert-ref job is documented as running before Hugo is installed, but validate-task.yml installs Hugo and validate currently runs in parallel. For a mis-dispatched production deploy (non-main ref), the workflow will still run the full validation (including Hugo install/build) before failing, which is slower and contradicts the comment.

Make validate depend on assert-ref so the branch/environment gate fails fast before any expensive validation runs.

 # Staging deploys from any ref, since proving a branch before it merges is what staging is for.
# Asserted before Hugo is installed and before the key reaches the runner.
assert-ref:

.github/workflows/deploy-site-task.yml:121

  • The deploy upload step forces directory permissions to D755, but deploy/make-release.sh intentionally uses --chmod=D2755 to preserve the setgid bit so the group propagates under the first directory rsync creates. Using D755 here can clear setgid on the remote release tree and make the on-host permission model diverge from what the bundle was built/tested with.

Use the same chmod policy as the bundle assembly (D2755,F644) and keep --no-g so the destination group is assigned by the setgid parent.

 rsync -az --mkpath --chmod=D755,F644 \
--link-dest="/${ENVIRONMENT}/current/" \
-e "ssh -i ~/.ssh/deploy -o IdentitiesOnly=yes" \
"${RUNNER_TEMP}/bundle/releases/${RELEASE_ID}/" \
"${DEPLOY_SSH_USER}@${DEPLOY_SSH_HOST}:/${ENVIRONMENT}/releases/${RELEASE_ID}/"

Comment threaddeploy/make-release.sh
…ode (#21)
The ref assertion ran beside validation rather than before it, so a
mis-dispatched production deploy installed a toolchain and built a site
before refusing.
The upload cleared the setgid bit the bundle is assembled with, so the
group would stop propagating below the first directory the transfer
creates.
A relative environment file is refused rather than resolved when it
traverses, since a relative name is meant to reach the secrets directory.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 4, 2026 22:53
@ptr727

Copy link
Copy Markdown
OwnerAuthor

The two suppressed findings from the last round are fixed on develop in 6b2c739, via #21, so this promotion carries them.

Job orderingassert-ref was documented as running before Hugo is installed and ran beside validate, which installs it. A production deploy dispatched from the wrong ref paid for a full validation before refusing. The gate runs first now.

Directory mode — the upload used --chmod=D755 while the bundle is assembled with D2755, so the transfer would strip the setgid bit the on-host permission model depends on. Confirmed against a real assembled release, which carries drwxr-sr-x. The upload now matches assembly.

Also fixed the thread finding on ENV_FILE traversal in the same change.

CopilotAI 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 22 out of 22 changed files in this pull request and generated no new comments.

@ptr727
ptr727 merged commit 9c042c1 into mainAug 4, 2026
5 checks passed
Sign up for freeto 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

@ptr727