From e3ccd2232fb18d47e314784fabecb8877c1cf03f Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 5 Aug 2026 07:19:23 -0700 Subject: [PATCH 1/3] Re-vendor the ruleset comparator and mirror the README intro Two findings from the hub audit run against this repo, both mechanical. repo-config/configure.sh is carried verbatim and the hub's canonical advanced past the copy here: the ruleset normalizer now defines its own walk rather than calling the jq 1.6 builtin, so a host on jq 1.5 fails to compile the filter instead of silently reporting drift on every parameterized rule it never compared. That defect was reported from here as ProjectTemplate#549 and fixed there in #553, so this is the re-vendor that fix is owed. HISTORY.md opens as the README's twin, and the README grew a second intro paragraph that the changelog never took, which the readme-structure check reads as an intro that does not mirror. Co-Authored-By: Claude Opus 5 (1M context) --- HISTORY.md | 6 ++++++ repo-config/configure.sh | 10 +++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 5d16e86..12b4d39 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,8 @@ Pieter Viljoen's blog, and the tooling that builds, verifies, and deploys it. +The live blog is hosted at [blog.insanegenius.com][blog-link]. + ## Release History - Version 1.0: @@ -11,3 +13,7 @@ Pieter Viljoen's blog, and the tooling that builds, verifies, and deploys it. - CI gates that contract on every pull request, alongside the doc, shell, and workflow linters, with the Hugo version pinned by checksum so a build is reproducible. - A self-contained release bundle carrying the site, the web-server config, and the redirect maps together, so a rollback reverts the rules and the content they refer to as one unit. - The site is not yet serving its public address. This release is the source and its pipeline, not the cutover. + + + +[blog-link]: https://blog.insanegenius.com diff --git a/repo-config/configure.sh b/repo-config/configure.sh index 4842702..d0716da 100755 --- a/repo-config/configure.sh +++ b/repo-config/configure.sh @@ -239,7 +239,15 @@ check_ruleset() { # payload-file - the live ruleset must match the committed pol # Dropping that would turn array order into false drift. # A scalar array sorts directly, and required_status_checks sorts by context, its identifying field. local ptypes norm - norm='def n: walk(if type=="array" then (if length==0 then . elif (all(.[]; type=="string" or type=="number")) then sort elif (all(.[]; type=="object" and has("context"))) then sort_by(.context) else . end) else . end); n' + # The walk/1 builtin arrived in jq 1.6, so it is defined here rather than called. + # A host on jq 1.5 would otherwise not degrade, it would fail to compile the filter. + # The check_ruleset function would then report drift on every parameterized rule it never actually compared. + # That is the inverse of the false clean this comparison was written to close, so the portable definition is worth its length. + norm='def w(f): . as $in + | if type == "object" then reduce keys_unsorted[] as $k ({}; . + { ($k): ($in[$k] | w(f)) }) | f + elif type == "array" then map(w(f)) | f + else f end; + def n: w(if type=="array" then (if length==0 then . elif (all(.[]; type=="string" or type=="number")) then sort elif (all(.[]; type=="object" and has("context"))) then sort_by(.context) else . end) else . end); n' ptypes="$(jq -r '[.rules[] | select(has("parameters")) | .type] | .[]' "$file")" while IFS= read -r t; do [ -z "$t" ] && continue From de8ce32a3839ea571f6a61e10b09883df4df9a53 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 5 Aug 2026 07:25:25 -0700 Subject: [PATCH 2/3] Record the re-audit and retire what the hub now owns The self-audit was written before the deploy existed, before the environments held credentials, and against a hub that had no type for a site that builds and ships to a host it owns. All three have changed, so the run is replaced rather than edited, per the run-stamp discipline the report itself describes. The new section is the nine hugo checks, hand-evaluated with a citation each. Nothing mechanizes them: the hub's audit runner never reads project-types.json, so a clean run of it is not evidence about any of them, and reading one that way is the empty-query trap this repo has been caught by before. Eight pass. hugo.deploy.retention does not, because the count that binds the VPS destination is nowhere declared, and the ten releases recorded beside the ownership line describe the local mirrors instead. TODO.md drops the hub debt it carried, since the hub has authored the type from this repo's measured shape rather than from the intake's prediction, and names the two things that come due when that work promotes to the hub's main. Two hub findings close: the audit runner no longer reports a bypass DEFECT here, and the static-site type exists. Co-Authored-By: Claude Opus 5 (1M context) --- TODO.md | 37 +++++----- reports/Blog/audit.md | 152 ++++++++++++++++++++++++------------------ 2 files changed, 108 insertions(+), 81 deletions(-) diff --git a/TODO.md b/TODO.md index 8b6bdea..8619e19 100644 --- a/TODO.md +++ b/TODO.md @@ -26,6 +26,8 @@ The site is built, gated in CI, and deployed to staging by pipeline. It is not y ## Next, in dependency order +- **Retest the deploy transport against the real host**, which is [#33][issue-33] and is joint work with whoever holds the server. The transport now pins `StrictHostKeyChecking`, `UserKnownHostsFile`, and `BatchMode`, so it fails closed where it previously failed open, and a stale `DEPLOY_SSH_KNOWN_HOSTS` stops a deploy rather than being tolerated. Staging first, since a broken transport blocks the rollback path as well as the deploy. +- **Declare what the VPS keeps.** `hugo.deploy.retention` asks for a retention count declared at the destination, and this repo's deploy credential is write-only by design so the prune belongs to the host. The ownership is recorded in `OPERATIONS.md`, the count is not, and the "ten releases" beside it describes `deploy/make-release.sh` on the local mirrors rather than the containers on the VPS. Confirm the host's timer and its count, then write it next to the ownership line. - **Prove a rollback through the pipeline.** A forced mid-deploy failure, then a flip back to the previous release, verified by `EXPECT_RELEASE` rather than by the transport exiting zero. The server side has been measured at well under a second by hand; what is unproven is that a **pipeline** run leaves the site serving when its deploy fails part way. - **Deploy production once, to a name that is not the live one.** The production environment is configured and its Pangolin resource is deliberately disabled, so nothing has ever run against it. Validate there before the record moves. - Lower the `blog` A-record TTL to 60s a day ahead, then flip it to the VPS, unproxied. @@ -35,18 +37,16 @@ The site is built, gated in CI, and deployed to staging by pipeline. It is not y ## Owed to the hub -The hub is owed a spec update for this repo's publishing type. The change is [ProjectTemplate#558][hub-spec-issue], carrying proposed wording for both additions, and [ProjectTemplate#456][hub-issue] holds the intake questions and the measured answers. +Nothing. The spec update this repo owed the hub has landed: [ProjectTemplate#560][hub-type-pr] authored the `hugo` type, the `self-hosted` target, the `deploy-ssh` mechanism, guarantees D4.6 and D5.6, and a reference leaf pair, all measured from what this repo actually runs rather than from the prediction the intake carried. [#456][hub-issue] and [#558][hub-spec-issue] are closed with it. -**Frame it as a variant of the existing registry-push leaf, not a new release surface.** A NuGet or PyPI leaf builds an artifact and pushes it to its own destination, contributing no `release-asset-*`. This repo does exactly that. Only two things differ, and neither changes the seam: +**It is on the hub's `develop` and not on `main`, so it is not ground truth yet.** The registry entry that reclassifies this repo to `types: ["hugo", "source-only"]` with both publish targets sits on the same unpromoted branch. Until the hub promotes, this repo stays `source-only` for audit purposes, and the anticipatory evaluation of the nine `hugo` checks is in [the audit report](./reports/Blog/audit.md). -| Same as NuGet and PyPI | Unique here | -| --- | --- | -| A leaf builds, then pushes to its own destination | The build is Hugo rather than a language toolchain | -| No `release-asset-*` contributed | The transport is rsync over SSH to a host the project owns | -| Publish is dispatch-gated, never a merge | The destination is a filesystem, so the artifact carries its own version | -| Credentials come from a GitHub Environment | Two environments serve the same artifact, so a deploy must prove which one answered | +Two things become due at that promotion, neither of them work this repo can do first: + +- The two `driftNotes` the hub's registry carries for `hugo.vendored.provenance` and `hugo.generator.pinned` describe work [#30][pr-30] already finished, so they are reconciled away rather than carried. +- This repo's [`spec/secrets.json`](./spec/secrets.json) note states `types: ["source-only"]` in prose and needs the second type once the registry declares it. -What the type genuinely needs is therefore small: a destination row in `Output Seam by Destination`, and one guarantee that a deploy is verified against the running host by release rather than by transport success. The release model, the branching model, and the never-publish-on-merge rule all hold unchanged. +The reference leaf the hub now ships carries one step this repo's deploy does not, a prune of the remote release tree. That is the corrected form of the check rather than a gap here: this repo's credential cannot observe the destination, so the leaf's own comments say to delete the step and record the ownership on the host side, which is what the entry above tracks. ## Open decisions @@ -61,20 +61,20 @@ Both are recorded in [AUDIT.md](./AUDIT.md) and reported to [ProjectTemplate#456 ## Hub conformance, and what is open against the hub -Reconverged 2026-08-03. This repo is cataloged in [`registry/repos.json`][hub-registry] and the hub authored [`reports/blog/audit.md`][hub-report]. Before that it was in no registry, so no hub tool had ever measured it and the fleet ledger under-counted by exactly this repo. +Reconverged 2026-08-05, and the run is written up in [`reports/Blog/audit.md`](./reports/Blog/audit.md). This repo is cataloged in [`registry/repos.json`][hub-registry] and the hub authored [`reports/blog/audit.md`][hub-report]. Before that it was in no registry, so no hub tool had ever measured it and the fleet ledger under-counted by exactly this repo. -**Measured against hub `main` `3b802b9eb9a841c0149d018f4db6ffa1b9419051`**, and the ref is named because `main` moves, which is the trap below. Every verbatim section of `AGENTS.md` and `GOVERNANCE.md` byte-matches, as do both ruleset payloads and `.markdownlint-cli2.jsonc`. The one exception is `repo-config/configure.sh`, one commit behind on [ProjectTemplate#553][pr-553], which fixes the jq portability defect this repo reported as [#549][issue-549] and is owed a re-vendor. Both links above are pinned to that same ref rather than to `main`, so this record stays checkable after the hub moves again. +**Measured against hub `main` `3b802b9eb9a841c0149d018f4db6ffa1b9419051`**, and the ref is named because `main` moves, which is the trap below. Every verbatim unit now matches: the re-vendor of `repo-config/configure.sh` this record previously owed, for the jq portability defect reported as [#549][issue-549] and fixed at the hub in [#553][pr-553], landed with this change. The links above are pinned to that same ref rather than to `main`, so this record stays checkable after the hub moves again. -Four findings are open at the hub. None is work this repo can do, and each changes what a fleet audit of this repo means, which is why they are recorded here rather than only in the issues. +Two findings are open at the hub. Neither is work this repo can do, and each changes what a fleet audit of this repo means, which is why they are recorded here rather than only in the issues. | Issue | What it means here | | --- | --- | | [#550][issue-550] | Nothing detects a repo missing from the registry, which is how this repo stayed invisible. Three other repos are still absent. | -| [#552][issue-552] | The audit flags any carried `AGENTS.md` naming the template repo, and the byte-locked `Fleet Bootstrap` section names it. Carrying the canonical correctly cannot pass. | -| [#554][issue-554] | `spec/audit.py` still compares `bypass_actors` after the payloads stopped declaring it, so this repo reports two DEFECTs that no agent action can clear. | -| [#456][hub-issue] | The static-site type, still waiting on a measured deploy shape from the VPS work below. | +| [#552][issue-552] | The audit flags any carried `AGENTS.md` naming the template repo, and the byte-locked `Fleet Bootstrap` section names it. Carrying the canonical correctly cannot pass, and it is the one finding the current run cannot clear. | + +Two more are resolved and are named because their absence from the table would otherwise read as an oversight. [#554][issue-554] made `spec/audit.py` report two DEFECTs here that no agent action could clear, and the fix is in the hub `main` this run measured against, so those two findings are gone. [#456][hub-issue] and [#558][hub-spec-issue] were the static-site type, now authored, per the section above. -**The live ruleset bypass is deliberate and stays.** Both rulesets carry the `RepositoryRole` admin entry. The owner is automatically an admin and holds that capability regardless, 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. `configure.sh check` reports it as unmanaged and exits 0. Only `spec/audit.py` disagrees, which is [#554][issue-554]. +**The live ruleset bypass is deliberate and stays.** Both rulesets carry the `RepositoryRole` admin entry. The owner is automatically an admin and holds that capability regardless, 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. `configure.sh check` reports it as unmanaged and exits 0, and the hub audit no longer disagrees. ## Traps @@ -136,14 +136,17 @@ The deploy root is deliberately absent from this table. The rsync destination is +[issue-33]: https://github.com/ptr727/Blog/issues/33 [migration-post]: ./content/posts/2026/08/01/moving-this-blog-from-wordpress-to-hugo.md +[pr-30]: https://github.com/ptr727/Blog/pull/30 [hub-issue]: https://github.com/ptr727/ProjectTemplate/issues/456 -[hub-spec-issue]: https://github.com/ptr727/ProjectTemplate/issues/558 [hub-registry]: https://github.com/ptr727/ProjectTemplate/blob/3b802b9eb9a841c0149d018f4db6ffa1b9419051/registry/repos.json [hub-report]: https://github.com/ptr727/ProjectTemplate/blob/3b802b9eb9a841c0149d018f4db6ffa1b9419051/reports/blog/audit.md +[hub-spec-issue]: https://github.com/ptr727/ProjectTemplate/issues/558 +[hub-type-pr]: https://github.com/ptr727/ProjectTemplate/pull/560 [issue-549]: https://github.com/ptr727/ProjectTemplate/issues/549 [issue-550]: https://github.com/ptr727/ProjectTemplate/issues/550 [issue-552]: https://github.com/ptr727/ProjectTemplate/issues/552 diff --git a/reports/Blog/audit.md b/reports/Blog/audit.md index 2f33baf..4fd4f33 100644 --- a/reports/Blog/audit.md +++ b/reports/Blog/audit.md @@ -1,50 +1,53 @@ # Blog Audit -Self-audit of this repository against its own committed ground truth, per [AUDIT.md](../../AUDIT.md). Read-only, and confined to this repository. +Self-audit of this repository against its own committed ground truth, per [AUDIT.md](../../AUDIT.md), and against the fleet ground truth the hub publishes. Read-only, and confined to this repository. It replaces the 2026-08-01 run rather than editing it, which is what the run-stamp discipline asks for: a run records what it observed, and a later run supersedes the whole file. -**Date:** 2026-08-01 -**Hub ref carried:** `ptr727/ProjectTemplate` `main` `3a7cc64` -**Declared:** `types: ["source-only"]`, `workflowModel: release`, `lineEndings: "lf"` +**Date:** 2026-08-05 +**Hub ref carried:** `main` `3b802b9` +**Run stamps:** `audit run 2026-08-05T14:12:14Z | hub 3b802b9` and `audit run 2026-08-05T14:13:11Z | hub 2a1afc0` (the second reads the hub's unpromoted `develop`) +**Declared:** `types: ["source-only"]`, `workflowModel: release`, `lineEndings: "lf"`, `releaseTrigger: dispatch-only` ## Verdict -**Operational.** Every applicable check passes against the live repository. - -The release surface passes. The **deploy** to the VPS is **deferred**, not failed, and that deferral is declared rather than hidden, tracked in [ProjectTemplate#456][hub-issue], which [`STANDUP.md` section 5][standup] permits. +**Operational.** Every applicable check passes, and the two open drift findings are both blocked on something outside this repository. | Dimension | Result | | --- | --- | -| 1. Settings and rulesets | **Pass.** `configure.sh check` exits 0 | -| 2. Secrets | **Pass.** Both required present in both stores, forbidden one absent | -| 3. The URL contract | **Pass.** Enforced by CI, not only locally | -| Baseline file presence | **Pass.** 23 of 23 | -| Verbatim fidelity | **Pass.** 4 of 4 | -| Release | **Pass.** Dispatch-only, proven by release `1.0.11` | -| Deploy to the VPS | **Deferred**, deliberately | +| 1. Settings and rulesets | **Pass.** `configure.sh check` exits 0 on 22 assertions | +| 2. Secrets, repository scope | **Pass.** Both required present in both stores, the forbidden one absent | +| 3. Secrets, environment scope | **Pass.** Both environments carry every declared name | +| 4. The URL contract | **Pass.** Gated in CI on the build, and against the running site on a deploy | +| 5. Hub conformance run | **Pass** on the mechanized subset, two drifts outstanding | +| 6. The `hugo` type, hand-evaluated | **Pass** on eight of nine checks, one drift | +| Release | **Pass.** Dispatch-only, newest is `1.0.17-g4b2def3ee9` from `develop` | +| Deploy | **Pass.** Proven end to end against VPS staging by pipeline | + +## What Changed Since the Previous Run + +The 2026-08-01 run recorded the deploy as deferred and the VPS as not provisioned. Both have since happened, so the deferral this repository declared is closed rather than carried: + +- `deploy-site.yml` is dispatchable and has deployed staging from CI end to end, verified against the live site by release id rather than by the transport's exit status. +- The `staging` and `production` environments exist and hold their credentials. +- The hub authored the `hugo` type and the `self-hosted` target from this repository's measured shape, which is what [ProjectTemplate#456][hub-issue] and [#558][hub-spec-issue] were holding for. Both are closed. ## 1. Settings and Rulesets **Pass.** ```text -$ repo-config/configure.sh check ptr727/Blog release -... 31 assertions, all ok ... +$ repo-config/configure.sh check +... 22 assertions, all ok ... Configuration matches on ptr727/Blog. exit 0 ``` -Both rulesets are active and carry every expected rule. `develop` allows squash only, `main` allows merge only, and both bind the required check by the same name the workflow produces: - -```text -'develop' required checks = ["Check pull request workflow status job"] -'main' required checks = ["Check pull request workflow status job"] -``` +This run is the first with the re-vendored comparator, which compares `copilot_code_review` parameters as well as `pull_request` and `required_status_checks`, so three parameterized rules per ruleset are now compared rather than two. -The ordering constraint was honored at standup: the workflow was dispatched once and reported before any ruleset was applied. Applying first would have deadlocked the first pull request, because the required check binds by name and only appears after a run. +Both rulesets are active. `develop` allows squash only, `main` allows merge only, and both bind the required check by the name the workflow produces. `has_discussions = true` follows public visibility, `default_branch = main`, and both Dependabot security features are enabled. -`has_discussions = true`, derived by `configure.sh` from public visibility rather than from a committed setting. `default_branch = main`. Dependabot vulnerability alerts and automated security updates are enabled. +Each ruleset carries a `RepositoryRole 5` bypass entry that the payloads do not declare. `configure.sh check` reports it as unmanaged and exits 0, which is the correct reading: the owner holds that capability by role regardless, so the entry grants nothing the payload could withhold. -## 2. Secrets +## 2. Secrets, Repository Scope **Pass.** Names only. No secret value was read, printed, or logged. @@ -54,83 +57,104 @@ The ordering constraint was honored at standup: the workflow was dispatched once | `CODEGEN_APP_PRIVATE_KEY` | present | present | | `CODEGEN_APP_ID` (forbidden) | absent | absent | -`CODEGEN_APP_ID` is forbidden because the App-token action takes `client-id`, and the deprecated `app-id` name silently does nothing. +## 3. Secrets, Environment Scope -The `staging` and `production` environments do not exist yet, which is correct: they hold deploy credentials for a VPS that has not been provisioned, and `AUDIT.md` places them outside the baseline audit. +**Pass**, against [`spec/secrets.json`](../../spec/secrets.json)'s `environments` block. No fleet tool reads this block, because neither the hub's validator nor its audit runner can enumerate an environment-scoped store, so this section is the only thing that checks these names at all. -## 3. The URL Contract +| Name | Kind | `staging` | `production` | +| --- | --- | --- | --- | +| `DEPLOY_SSH_PRIVATE_KEY` | secret | present | present | +| `DEPLOY_SSH_HOST`, `DEPLOY_SSH_USER`, `DEPLOY_SSH_KNOWN_HOSTS` | variable | present | present | +| `HUGO_BASEURL` | variable | present | present | +| `PANGOLIN_ACCESS_TOKEN_ID`, `PANGOLIN_ACCESS_TOKEN` | secret, staging only | present | absent, as declared | -**Pass, and now enforced by CI rather than only locally**, which is the material change from the pre-standup state. +A third environment, `copilot`, exists and holds no variables, no secrets, and no protection rules. GitHub creates it for its coding agent. It is recorded here because an environment that appears without being declared is exactly what this section exists to notice, and because an empty one is the only safe shape for it: `secrets: inherit` in [`deploy-site.yml`](../../.github/workflows/deploy-site.yml) passes repository secrets to the callee, and no path binds `copilot` to a deploy. -From the first run on `main`: +## 4. The URL Contract -```text -hugo v0.164.0+extended (pinned by version and sha256) -Pages 514 | Total in 858 ms (zero warnings under --panicOnWarning) -render : 328/328 golden URLs built -media : 778/778 legacy image URLs resolve after the @uploads rewrite -assets : 1012/1012 local asset references resolve -PASS - the built site honors the URL contract -``` +**Pass, on both halves, and the second half now runs in CI.** + +The build half is gated on every pull request. [`checks/check-url-parity.py:16`](../../checks/check-url-parity.py) declares floors under the known-good counts, so a truncated list fails rather than passing while covering nothing, and [`.github/workflows/validate-task.yml:88`](../../.github/workflows/validate-task.yml) runs it against the built tree. -Every gate in the validation job passed on its first attempt: markdownlint, cspell, actionlint, `editorconfig-checker`, shellcheck, `shfmt -d`, config validation, the Hugo build, and the contract check. +The live half was a hand-run step against a local mirror at the previous audit. It is now the terminal step of the deploy ([`deploy-site-task.yml:155`](../../.github/workflows/deploy-site-task.yml)), which runs [`checks/check-live-urls.sh:18`](../../checks/check-live-urls.sh) against the environment it just wrote, with its own floors, `EXPECT_SITE_ENV`, and `EXPECT_RELEASE`. All 1,245 URLs were verified this way against VPS staging behind its auth gate, in run `30959030274` on 2026-08-04: 328 that must render, 917 that must redirect, `PASS - 1245 URLs honored`. -Floor assertions are present and below the real counts, so a truncated list fails rather than passing while covering nothing: +## 5. Hub Conformance Run + +The hub's `spec/audit.py` was run three times against this repository: `main`, `develop`, and the convergence branch, all from a full hub clone so the stale-versus-modified classification could walk the canonical's history. + +Findings before the fixes in this change, identical on `main` and `develop`: ```text -checks/check-live-urls.sh:18 FLOOR=(["golden-urls.txt"]=320 ["redirect-urls.txt"]=900) +DRIFT branch: 1 path(s) changed on both main and develop since the merge-base ... +DRIFT carried: AGENTS.md references the template repo by name or link +DRIFT verbatim: repo-config/configure.sh matches a past hub revision, not the current canonical +LETTER history: HISTORY.md intro does not mirror the README intro ``` -**The redirect half is proven, against a running server rather than a build.** `deploy/make-release.sh` installs the build on the local mirror, then `checks/check-live-urls.sh` follows all 1,245 URLs against it, checking each redirect's destination rather than trusting its status code: +After, on the convergence branch: ```text -==> checking 328 URLs that must render -==> checking 917 URLs that must redirect -PASS - 1245 URLs honored +DRIFT branch: 1 path(s) changed on both main and develop since the merge-base ... +DRIFT carried: AGENTS.md references the template repo by name or link +1 repo(s) audited; 0 defect/letter/error finding(s). ``` -That is a local mirror, not CI and not production. CI cannot run it, because the validation workflow has no server to point at, so this remains a pre-pull-request step documented in [OPERATIONS.md](../../OPERATIONS.md) rather than an automated gate. It becomes automatable once staging exists. +Both remaining findings are blocked outside this change: -## Baseline File Presence +- **The branch drift is a promotion, not a divergence.** `.github/workflows/validate-task.yml` changed on both branches since the merge-base, because the same Dependabot bump landed on each independently and `develop` also carries the generator-pin change. Compared directly, `develop` supersedes `main` on every line of that file, so the reconciliation is the `develop -> main` promotion and nothing else. +- **The `AGENTS.md` finding cannot be cleared from here.** The `Fleet Bootstrap` section is byte-locked across the fleet and names the hub, while the same audit forbids a carried file from naming it. Carrying the canonical correctly cannot pass, which is [ProjectTemplate#552][issue-552]. -**Pass, 23 of 23** applicable to `types: ["source-only"]` plus `workflowModel: release`. +## 6. The `hugo` Type, Hand-Evaluated -`OPERATIONS.md` is retained although it left the required set when the workflow model changed from `operational` to `release`. Carrying an extra file is not drift. +The hub authored a nine-check `hugo` type from this repository's measured deploy shape. It is on the hub's `develop` and **not** on `main`, so it is not ground truth yet and this section is anticipatory: it records what a promoted type would find, so the promotion is not the first time anyone looks. -## Verbatim Fidelity +**Nothing mechanizes these checks.** `spec/audit.py` does not read `spec/project-types.json` at all, so a clean run of it says nothing about any of the nine, and reading one as evidence would be the empty-query trap this repository has been caught by before. Every row below was evaluated by hand against the file it cites. -**Pass, 4 of 4**, compared after line-ending normalization as [`spec/fidelity-model.md`][fidelity] specifies: `.markdownlint-cli2.jsonc`, `repo-config/configure.sh`, `repo-config/main.json`, `repo-config/develop.json`. +| Check | Verdict | Evidence | +| --- | --- | --- | +| `hugo.build.strict` | **Pass** | `hugo --gc --minify --panicOnWarning` at [`validate-task.yml:83`](../../.github/workflows/validate-task.yml) and [`deploy/make-release.sh:94`](../../deploy/make-release.sh), the same command on both paths | +| `hugo.urls.parity` | **Pass** | Floors at [`check-url-parity.py:16`](../../checks/check-url-parity.py) and [`check-live-urls.sh:18`](../../checks/check-live-urls.sh), both under the committed counts | +| `hugo.output.uncommitted` | **Pass** | `public/` ignored at [`.gitignore:5`](../../.gitignore), nothing tracked under it, and the markdown glob excludes `content/**`, `themes/*/**`, and `public/**` | +| `hugo.generator.pinned` | **Pass** | Version and SHA256 declared once, in [`.github/actions/install-hugo/action.yml:26`](../../.github/actions/install-hugo/action.yml), verified before install | +| `hugo.vendored.provenance` | **Pass** | Upstream, commit, and local edits recorded at [`themes/README.md:12`](../../themes/README.md) | +| `hugo.deploy.environment` | **Pass** | Environment re-asserted in its own job at [`deploy-site-task.yml:38`](../../.github/workflows/deploy-site-task.yml), bound at `:62`, every host value from `vars` | +| `hugo.deploy.atomic` | **Pass** | Upload to `releases//` at `:124`, pointer flipped by `rsync` through a temporary and a rename at `:140`, no `--delete` anywhere | +| `hugo.deploy.verified` | **Pass** | `EXPECT_RELEASE` and `EXPECT_SITE_ENV` at `:155`, polled to a bounded timeout at [`check-live-urls.sh:155`](../../checks/check-live-urls.sh), with an unreachable host reported distinctly at `:144` | +| `hugo.deploy.retention` | **Drift** | See below | -Eight carried files arrived CRLF and were normalized to LF to satisfy this repository's declared `lineEndings`. That is governed drift rather than a fidelity deviation, and it is reported upstream as an onboarding trap, since nothing in the standup text says to normalize after carrying. +**`hugo.deploy.retention` is the one that does not pass cleanly.** The check accepts two shapes, and this repository is the second: the deploy credential is a forced `rsync` command confined write-only, so it can neither delete a release nor read the destination back to count one, and the prune therefore belongs to the host. That ownership is recorded, at [`OPERATIONS.md:176`](../../OPERATIONS.md). What is not recorded is the count that binds the host: the "Ten releases are kept" in the Retention section above it describes `deploy/make-release.sh`, which installs on the local mirrors and prunes there, and no line says what the VPS containers keep or that their timer exists. The check asks for a declared count at the destination, and the destination the pipeline writes to has none. -## Release Proven, Deploy Deferred +This is a documentation gap rather than a disk-space one, and it is not this repository's to fill alone, since the count is the host's to declare. It is carried as a residual delta below rather than guessed at here. -The two are separate and only one of them is outstanding. +**One observation that no check covers.** The build command is written out twice, at the two citations in the first row, with nothing asserting the two copies agree. That is the same shape as the generator pin before [#29][issue-29] moved it into a composite action, one class down in severity: a one-sided edit would validate with one command and ship a tree built by another. The pin itself is now single-sourced, so the exposure is the flag set rather than the generator. -**The release is proven.** `publish-release.yml` is dispatch-only, and release `1.0.11` on 2026-08-01 carries the tag, the source archive, the README, and the LICENSE. The hub registry declares it accordingly: `publish` names the GitHub release and `releaseTrigger` is `dispatch-only`. +## Baseline File Presence and Verbatim Fidelity -**The deploy is deferred.** This repository will deploy a built site to a VPS over SSH, which is a release surface the fleet spec has no type for. The measured shape will be reported to [ProjectTemplate#456][hub-issue] once CI has run a deploy, rather than predicted now. The VPS does not exist, so there is nothing to measure. +**Pass.** Every carried file the scope selectors resolve to is present, and every verbatim unit matches the hub canonical after line-ending normalization, which is what this change's re-vendor of `repo-config/configure.sh` restored. ## Deliberate Deviations -Both are recorded in [AUDIT.md](../../AUDIT.md) and reported upstream, so neither can later read as drift. +Both are unchanged, recorded in [AUDIT.md](../../AUDIT.md), and reported upstream, so neither reads as drift later. -1. **`lineEndings: "lf"` on a `release` repo.** `GOVERNANCE.md` "Line Endings" grants the native-platform default to operational repos only. Every consumer here is Linux, and the fleet CRLF default would need an LF override for the scripts, the workflow YAML, the Caddyfile, the generated maps, and the content tree, which is the over-normalization that rule exists to prevent. The rule keys on `workflowModel` when the determining factor is the consuming platform. -2. **`types: ["source-only"]` rather than `docs`.** Both `docs` predicates are false: it detects a "governance-only repo" and asserts lint-only CI with no build, while this repo builds a site and gates a URL contract. `source-only` detects "no `build-*-task.yml`", which is true. Both selectors resolve to the same baseline file set, so only one of them is honest and it costs nothing. +1. **`lineEndings: "lf"` on a `release` repo**, where the rule grants the native-platform default to operational repos only. Every consumer here is Linux. +2. **`types: ["source-only"]` rather than `docs`**, because both `docs` predicates are false for a repository that builds a site and gates a URL contract. The hub's unpromoted `develop` adds `hugo` alongside it, which resolves this deviation rather than replacing it. ## Residual Deltas Carried forward rather than closed: -- The redirect half of the contract is proven only against the local mirror, by hand, before a pull request. CI has no server to point at, so nothing enforces it automatically until staging exists. -- No deploy exists, so the VPS deploy surface stays deferred. The GitHub release is the only channel that currently ships, which is what the hub registry declares, and the VPS target is revisited when a deploy has actually run. -- `checks/README.md` carries a small prose backlog of `dash` and `semicolon` findings, left for the next edit of that file per the correct-as-you-next-edit rule. +- **The retention count at the VPS destination is undeclared**, per section 6. Confirming that the host's prune timer exists and what it keeps is a question for the host side, and the answer belongs in `OPERATIONS.md` next to the ownership line that already points there. +- **The deploy transport's new SSH options have not been exercised against the real host**, which is [#33][issue-33]. They fail closed where the previous configuration failed open, so a stale `DEPLOY_SSH_KNOWN_HOSTS` now stops a deploy rather than being tolerated. +- **A rollback through the pipeline is unproven.** The server side rolls back in well under a second by hand, and a two-phase upload-then-flip should make a part-way failure safe, but no failing run has demonstrated it. +- **The `hugo` type is not ground truth yet.** Section 6 is anticipatory until the hub promotes it to `main`, at which point this repository's registry entry, its own `spec/secrets.json` note, and the type row above all become measurable rather than predicted. -[fidelity]: https://github.com/ptr727/ProjectTemplate/blob/main/spec/fidelity-model.md -[standup]: https://github.com/ptr727/ProjectTemplate/blob/main/STANDUP.md +[issue-29]: https://github.com/ptr727/Blog/issues/29 +[issue-33]: https://github.com/ptr727/Blog/issues/33 [hub-issue]: https://github.com/ptr727/ProjectTemplate/issues/456 +[hub-spec-issue]: https://github.com/ptr727/ProjectTemplate/issues/558 +[issue-552]: https://github.com/ptr727/ProjectTemplate/issues/552 From ea8fa6224025609607416bc2a84f72fbf57091ab Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 5 Aug 2026 07:35:46 -0700 Subject: [PATCH 3/3] Point the hub debt at the issue that now carries it The two registry notes for this repo's hugo checks describe work that was already merged when they were written, and the record that they would be retired mechanically does not hold: the freshness check is gated on a repo having no findings at all, and the one finding here cannot be cleared from this repo. Both are measured in the issue. Co-Authored-By: Claude Opus 5 (1M context) --- TODO.md | 3 ++- reports/Blog/audit.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 8619e19..b75b056 100644 --- a/TODO.md +++ b/TODO.md @@ -43,7 +43,7 @@ Nothing. The spec update this repo owed the hub has landed: [ProjectTemplate#560 Two things become due at that promotion, neither of them work this repo can do first: -- The two `driftNotes` the hub's registry carries for `hugo.vendored.provenance` and `hugo.generator.pinned` describe work [#30][pr-30] already finished, so they are reconciled away rather than carried. +- The two `driftNotes` the hub's registry carries for `hugo.vendored.provenance` and `hugo.generator.pinned` describe work [#30][pr-30] already finished, so they are reconciled away rather than carried. Filed as [ProjectTemplate#563][issue-563], with the measurement that nothing retires them mechanically despite the intent to: the freshness check is gated on a repo having no findings at all, and this repo has one it cannot clear. - This repo's [`spec/secrets.json`](./spec/secrets.json) note states `types: ["source-only"]` in prose and needs the second type once the registry declares it. The reference leaf the hub now ships carries one step this repo's deploy does not, a prune of the remote release tree. That is the corrected form of the check rather than a gap here: this repo's credential cannot observe the destination, so the leaf's own comments say to delete the step and record the ownership on the host side, which is what the entry above tracks. @@ -151,4 +151,5 @@ The deploy root is deliberately absent from this table. The rsync destination is [issue-550]: https://github.com/ptr727/ProjectTemplate/issues/550 [issue-552]: https://github.com/ptr727/ProjectTemplate/issues/552 [issue-554]: https://github.com/ptr727/ProjectTemplate/issues/554 +[issue-563]: https://github.com/ptr727/ProjectTemplate/issues/563 [pr-553]: https://github.com/ptr727/ProjectTemplate/pull/553 diff --git a/reports/Blog/audit.md b/reports/Blog/audit.md index 4fd4f33..0ee5a71 100644 --- a/reports/Blog/audit.md +++ b/reports/Blog/audit.md @@ -108,7 +108,7 @@ Both remaining findings are blocked outside this change: The hub authored a nine-check `hugo` type from this repository's measured deploy shape. It is on the hub's `develop` and **not** on `main`, so it is not ground truth yet and this section is anticipatory: it records what a promoted type would find, so the promotion is not the first time anyone looks. -**Nothing mechanizes these checks.** `spec/audit.py` does not read `spec/project-types.json` at all, so a clean run of it says nothing about any of the nine, and reading one as evidence would be the empty-query trap this repository has been caught by before. Every row below was evaluated by hand against the file it cites. +**Nothing mechanizes these checks.** `spec/audit.py` does not read `spec/project-types.json` at all, so a clean run of it says nothing about any of the nine, and reading one as evidence would be the empty-query trap this repository has been caught by before. Every row below was evaluated by hand against the file it cites. That, and the two registry notes it leaves stranded, are reported to the hub as [ProjectTemplate#563][issue-563]. | Check | Verdict | Evidence | | --- | --- | --- | @@ -158,3 +158,4 @@ Carried forward rather than closed: [hub-issue]: https://github.com/ptr727/ProjectTemplate/issues/456 [hub-spec-issue]: https://github.com/ptr727/ProjectTemplate/issues/558 [issue-552]: https://github.com/ptr727/ProjectTemplate/issues/552 +[issue-563]: https://github.com/ptr727/ProjectTemplate/issues/563