From c150e7ef5d5eabb55d1d0f28850c63cfca7af670 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 1 Oct 2026 18:23:46 -0700 Subject: [PATCH 1/7] Correct the False No-Vocabulary Claims About Environment Secrets spec/secrets.json said it had no vocabulary for per-environment secrets and that a repo declares them in its own environments block, while spec/secrets.schema.json defines an environments block and the file carries none. spec/project-types.json and docs/reusable-workflows.md repeated the first claim. All four now say what holds: the schema allows such a block, this file carries none, and no tool here checks environment-scoped secrets. Refs #2268 Co-Authored-By: Claude Opus 5.5 --- docs/reusable-workflows.md | 2 +- spec/project-types.json | 2 +- spec/secrets.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reusable-workflows.md b/docs/reusable-workflows.md index 6b8d611d..6acef467 100644 --- a/docs/reusable-workflows.md +++ b/docs/reusable-workflows.md @@ -68,7 +68,7 @@ The sequencing consequence is that a hub task lands on `develop`, promotes to `m ### Secrets and Permissions -Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. Whether `secrets: inherit` is used is decided by the call's own boundary, not by the fleet's preference. [GitHub documents the keyword][gh-reusing-workflows] for a caller in the same organization or enterprise as the called workflow, and the fleet is a personal account. So a cross-repository call to a hub task names each secret it passes, and `inherit` is never used on one. A call whose job needs none passes no `secrets:` key, which is what the [Adopting the Gates][adopting-the-gates] `validate` stub does. A call by local path stays inside one repository. There the caller's own secret store is the one the called workflow reads, so `inherit` is available. Availability is not a reason to use it, and this repository's own local-path calls name their secrets or pass none. Both shapes run in the fleet today, one repo carrying a local-path `inherit` call beside a cross-repository call that names its secrets, and another proving an inherited value reaches a publishing task that authenticates from it. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it cross a GitHub Environment boundary `spec/secrets.json` has no vocabulary for, per its `deploy-ssh` mechanism note. +Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. Whether `secrets: inherit` is used is decided by the call's own boundary, not by the fleet's preference. [GitHub documents the keyword][gh-reusing-workflows] for a caller in the same organization or enterprise as the called workflow, and the fleet is a personal account. So a cross-repository call to a hub task names each secret it passes, and `inherit` is never used on one. A call whose job needs none passes no `secrets:` key, which is what the [Adopting the Gates][adopting-the-gates] `validate` stub does. A call by local path stays inside one repository. There the caller's own secret store is the one the called workflow reads, so `inherit` is available. Availability is not a reason to use it, and this repository's own local-path calls name their secrets or pass none. Both shapes run in the fleet today, one repo carrying a local-path `inherit` call beside a cross-repository call that names its secrets, and another proving an inherited value reaches a publishing task that authenticates from it. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it cross a GitHub Environment boundary that no hub tool checks, per the `deploy-ssh` mechanism note in `spec/secrets.json`. A hub task declares no job-level `permissions:` where every write goes through the App token, and the caller sets `permissions: {}`. A called workflow can only keep or reduce the caller's grant. A callee job naming a scope the caller did not grant fails at startup even when its `if:` is false. Declaring nothing in the callee is therefore the shape that cannot fail against any caller, and it gives `GITHUB_TOKEN` no scope. A task whose job genuinely writes with `GITHUB_TOKEN`, such as a release upload, declares that scope in the callee job and documents it in the stub's comment so the caller grants it. diff --git a/spec/project-types.json b/spec/project-types.json index 59031dab..b365cee8 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -112,7 +112,7 @@ "hugo": { "detect": ["hugo.yaml", "hugo.toml", "config/_default/hugo.yaml"], "intentRefs": ["WORKFLOW.md"], - "note": "Named for the generator rather than for the transport, because what a repo builds and where the result lands are separate axes. The destination is publish[] ({ target, mechanism }), so a repo changes transport without changing type. Every assert below is phrased without naming the generator except hugo.build.strict, where a generator-specific flag is the letter, so promoting the generic ones to a shared type when a second generator arrives is a registry edit. Deploy credentials are per-environment GitHub Environment secrets and variables, which spec/secrets.json cannot yet express, so a repo does not list them in its registry requiredSecrets: spec/audit.py resolves that list against the repository actions store and would report an environment-scoped name as missing.", + "note": "Named for the generator rather than for the transport, because what a repo builds and where the result lands are separate axes. The destination is publish[] ({ target, mechanism }), so a repo changes transport without changing type. Every assert below is phrased without naming the generator except hugo.build.strict, where a generator-specific flag is the letter, so promoting the generic ones to a shared type when a second generator arrives is a registry edit. Deploy credentials are per-environment GitHub Environment secrets and variables, which no tool here checks, so a repo does not list them in its registry requiredSecrets: spec/audit.py resolves that list against the repository actions store and would report an environment-scoped name as missing.", "checks": [ { "id": "hugo.build.strict", "verdict": "letter", "assert": "The site build fails on a generator warning rather than rendering around it (hugo --gc --minify --panicOnWarning), and the pull request gate and the deploy run the same build command rather than two variants.", "workflowRef": "WORKFLOW.md#d1---pr-fast-feedback-smoke" }, { "id": "hugo.urls.parity", "verdict": "letter", "assert": "A URL contract gate compares the built tree against a committed list of the URLs that must render and the URLs that must redirect, and asserts a minimum length on each list before comparing it, since a truncated list makes every assertion below it pass vacuously. This is the type's check of record, standing in for the unit tests a site does not have.", "workflowRef": "WORKFLOW.md#6-per-project-type-test-walkthroughs" }, diff --git a/spec/secrets.json b/spec/secrets.json index 02eca3dd..0b85f7fc 100644 --- a/spec/secrets.json +++ b/spec/secrets.json @@ -1,6 +1,6 @@ { "$schema": "./secrets.schema.json", - "note": "Secrets the audit cross-checks. `baseline` applies to every fleet repo (the App-signed merge-bot runs everywhere). `mechanisms` are per-target/per-feature additions: a repo requires the baseline plus the mechanisms its declared publish target (`targetMechanisms`) or declared type (`typeMechanisms`) maps to. All three mappings resolve from the registry entry rather than from workflow content: nothing reads a repo's Actions files to infer a mechanism, and `workflowNeeds` records what a mechanism needs to appear in a workflow for a human or agent reading the audit, rather than being a detector. `featureMechanisms` is shape-validated but claims nothing today, since the one feature it names (codecov) is claimed through `typeMechanisms` wherever the repo carries tests for that type instead. Baseline secrets are implicit and are NOT repeated in a repo's registry `requiredSecrets`, which lists only the domain-specific additions. `typeMechanisms` are per-language requirements: a `csharp` or `python` repo carrying tests for that language must carry the mapped mechanism (codecov) regardless of opt-in, a lint-only language other than Python excepted. A configured secret that no applicable mechanism claims is a stale-secret finding; a present `forbids` secret is a defect. `environments`, where a repo carries it, lists the per-environment GitHub Environment secrets and variables its deploy needs. It is operator documentation rather than part of the mechanism audit: no tool reads it, because neither `spec/validate.py` nor `spec/audit.py` queries an environment-scoped store, so a clean audit is not evidence that an environment is configured. `environmentSecrets` names what one environment carries and another does not, so a name audit does not read a single-environment credential as missing everywhere else.", + "note": "Secrets the audit cross-checks. `baseline` applies to every fleet repo (the App-signed merge-bot runs everywhere). `mechanisms` are per-target/per-feature additions: a repo requires the baseline plus the mechanisms its declared publish target (`targetMechanisms`) or declared type (`typeMechanisms`) maps to. All three mappings resolve from the registry entry rather than from workflow content: nothing reads a repo's Actions files to infer a mechanism, and `workflowNeeds` records what a mechanism needs to appear in a workflow for a human or agent reading the audit, rather than being a detector. `featureMechanisms` is shape-validated but claims nothing today, since the one feature it names (codecov) is claimed through `typeMechanisms` wherever the repo carries tests for that type instead. Baseline secrets are implicit and are NOT repeated in a repo's registry `requiredSecrets`, which lists only the domain-specific additions. `typeMechanisms` are per-language requirements: a `csharp` or `python` repo carrying tests for that language must carry the mapped mechanism (codecov) regardless of opt-in, a lint-only language other than Python excepted. A configured secret that no applicable mechanism claims is a stale-secret finding; a present `forbids` secret is a defect. The schema also allows an `environments` block listing the per-environment GitHub Environment secrets and variables a deploy needs, and this file carries none. Such a block is operator documentation rather than part of the mechanism audit: no tool reads it, because neither `spec/validate.py` nor `spec/audit.py` queries an environment-scoped store, so a clean audit is not evidence that an environment is configured. `environmentSecrets` names what one environment carries and another does not, so a name audit does not read a single-environment credential as missing everywhere else.", "baseline": { "requires": ["CODEGEN_APP_CLIENT_ID", "CODEGEN_APP_PRIVATE_KEY"], "forbids": ["CODEGEN_APP_ID"], @@ -44,7 +44,7 @@ "forbids": [], "workflowNeeds": ["environment:", "IdentitiesOnly=yes"], "stores": [], - "note": "A deploy to a filesystem on a host the project owns, reached over SSH. requires and stores are empty deliberately rather than for want of credentials: the key and the host values are per-environment GitHub Environment secrets and variables, which this file has no vocabulary for and neither validate.py nor audit.py queries. Listing the names would force them into the repo's registry requiredSecrets, which the audit resolves against the repository actions store, so a correctly configured repo would report every one of them as missing. A repo declares them in its own environments block below instead. The key is confined at the far end by an authorized_keys forced command rooted at the deploy tree, so the workflow names no host path." + "note": "A deploy to a filesystem on a host the project owns, reached over SSH. requires and stores are empty deliberately rather than for want of credentials: the key and the host values are per-environment GitHub Environment secrets and variables, which neither validate.py nor audit.py queries. Listing the names would force them into the repo's registry requiredSecrets, which the audit resolves against the repository actions store, so a correctly configured repo would report every one of them as missing. The key is confined at the far end by an authorized_keys forced command rooted at the deploy tree, so the workflow names no host path." } }, "targetMechanisms": { From e92a939d491640aca3afbdefd2734a77a719d280 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 1 Oct 2026 18:27:49 -0700 Subject: [PATCH 2/7] Name the Real Gap: the Environments Block Has No Per-Repo Place The strict review found the first wording hid the gap the old claim pointed at: the schema's environments block has no per-repo dimension, and downstream copies of spec/secrets.json are retired, so no repository can declare its environment names there. The secrets.json note now says so, environmentSecrets is described as a field of that block, project-types.json gives the real reason requiredSecrets leaves the names out, the reusable-workflows paragraph separates the checked environment boundary from its unchecked secrets, TODO.md's settled entry stops saying a repo may declare them, and the note's one semicolon is recast. Refs #2268 Co-Authored-By: Claude Opus 5.5 --- TODO.md | 2 +- docs/reusable-workflows.md | 2 +- spec/project-types.json | 2 +- spec/secrets.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/TODO.md b/TODO.md index d11a9543..0ddf1504 100644 --- a/TODO.md +++ b/TODO.md @@ -155,7 +155,7 @@ One pull request giving a repo a declared way to say what it needs at runtime, t - **Settled** - Blog needs it immediately, since it deploys on the proxmox host through HomeAutomation-Config's Docker Compose stack and carries the copy destinations and the internal URI. - **Settled** - Adopting it in the hub comes first, since the hub carries neither piece. - **Settled** - The GitHub side has the same missing axis, surfaced by the `hugo` type, since a deploy's credentials are per-environment secrets and variables while `stores` is a closed enum of `actions` and `dependabot`, and [`spec/audit.py`][audit] seeds its map with those two keys and indexes it unguarded, so adding an `environments` value raises a key error for every repo whose publish maps to that mechanism. - - **Settled** - An optional `environments` block is legal in [`spec/secrets.schema.json`][secrets-schema] so a repo may declare its per-environment names, and no tool reads one where it exists, which is honest and is not a gate, so a clean audit says nothing about whether an environment is configured. + - **Settled** - An optional `environments` block is legal in [`spec/secrets.schema.json`][secrets-schema], but it has no per-repo dimension and downstream copies of `spec/secrets.json` are retired, so no repo declares its per-environment names there, and no tool would read one, which is honest and is not a gate, so a clean audit says nothing about whether an environment is configured. ### The Docker Image Freshness Rule diff --git a/docs/reusable-workflows.md b/docs/reusable-workflows.md index 6acef467..7cf118f0 100644 --- a/docs/reusable-workflows.md +++ b/docs/reusable-workflows.md @@ -68,7 +68,7 @@ The sequencing consequence is that a hub task lands on `develop`, promotes to `m ### Secrets and Permissions -Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. Whether `secrets: inherit` is used is decided by the call's own boundary, not by the fleet's preference. [GitHub documents the keyword][gh-reusing-workflows] for a caller in the same organization or enterprise as the called workflow, and the fleet is a personal account. So a cross-repository call to a hub task names each secret it passes, and `inherit` is never used on one. A call whose job needs none passes no `secrets:` key, which is what the [Adopting the Gates][adopting-the-gates] `validate` stub does. A call by local path stays inside one repository. There the caller's own secret store is the one the called workflow reads, so `inherit` is available. Availability is not a reason to use it, and this repository's own local-path calls name their secrets or pass none. Both shapes run in the fleet today, one repo carrying a local-path `inherit` call beside a cross-repository call that names its secrets, and another proving an inherited value reaches a publishing task that authenticates from it. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it cross a GitHub Environment boundary that no hub tool checks, per the `deploy-ssh` mechanism note in `spec/secrets.json`. +Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. Whether `secrets: inherit` is used is decided by the call's own boundary, not by the fleet's preference. [GitHub documents the keyword][gh-reusing-workflows] for a caller in the same organization or enterprise as the called workflow, and the fleet is a personal account. So a cross-repository call to a hub task names each secret it passes, and `inherit` is never used on one. A call whose job needs none passes no `secrets:` key, which is what the [Adopting the Gates][adopting-the-gates] `validate` stub does. A call by local path stays inside one repository. There the caller's own secret store is the one the called workflow reads, so `inherit` is available. Availability is not a reason to use it, and this repository's own local-path calls name their secrets or pass none. Both shapes run in the fleet today, one repo carrying a local-path `inherit` call beside a cross-repository call that names its secrets, and another proving an inherited value reaches a publishing task that authenticates from it. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `spec/secrets.json` declares none of `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it, since each lives in a GitHub Environment rather than the repository store, and no hub tool checks the secrets an environment holds, per the `deploy-ssh` mechanism note there. A hub task declares no job-level `permissions:` where every write goes through the App token, and the caller sets `permissions: {}`. A called workflow can only keep or reduce the caller's grant. A callee job naming a scope the caller did not grant fails at startup even when its `if:` is false. Declaring nothing in the callee is therefore the shape that cannot fail against any caller, and it gives `GITHUB_TOKEN` no scope. A task whose job genuinely writes with `GITHUB_TOKEN`, such as a release upload, declares that scope in the callee job and documents it in the stub's comment so the caller grants it. diff --git a/spec/project-types.json b/spec/project-types.json index b365cee8..92ae24d3 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -112,7 +112,7 @@ "hugo": { "detect": ["hugo.yaml", "hugo.toml", "config/_default/hugo.yaml"], "intentRefs": ["WORKFLOW.md"], - "note": "Named for the generator rather than for the transport, because what a repo builds and where the result lands are separate axes. The destination is publish[] ({ target, mechanism }), so a repo changes transport without changing type. Every assert below is phrased without naming the generator except hugo.build.strict, where a generator-specific flag is the letter, so promoting the generic ones to a shared type when a second generator arrives is a registry edit. Deploy credentials are per-environment GitHub Environment secrets and variables, which no tool here checks, so a repo does not list them in its registry requiredSecrets: spec/audit.py resolves that list against the repository actions store and would report an environment-scoped name as missing.", + "note": "Named for the generator rather than for the transport, because what a repo builds and where the result lands are separate axes. The destination is publish[] ({ target, mechanism }), so a repo changes transport without changing type. Every assert below is phrased without naming the generator except hugo.build.strict, where a generator-specific flag is the letter, so promoting the generic ones to a shared type when a second generator arrives is a registry edit. Deploy credentials are per-environment GitHub Environment secrets and variables, which no tool here checks. A repo does not list them in its registry requiredSecrets, because spec/audit.py resolves that list against the repository actions store and would report an environment-scoped name as missing.", "checks": [ { "id": "hugo.build.strict", "verdict": "letter", "assert": "The site build fails on a generator warning rather than rendering around it (hugo --gc --minify --panicOnWarning), and the pull request gate and the deploy run the same build command rather than two variants.", "workflowRef": "WORKFLOW.md#d1---pr-fast-feedback-smoke" }, { "id": "hugo.urls.parity", "verdict": "letter", "assert": "A URL contract gate compares the built tree against a committed list of the URLs that must render and the URLs that must redirect, and asserts a minimum length on each list before comparing it, since a truncated list makes every assertion below it pass vacuously. This is the type's check of record, standing in for the unit tests a site does not have.", "workflowRef": "WORKFLOW.md#6-per-project-type-test-walkthroughs" }, diff --git a/spec/secrets.json b/spec/secrets.json index 0b85f7fc..65088572 100644 --- a/spec/secrets.json +++ b/spec/secrets.json @@ -1,6 +1,6 @@ { "$schema": "./secrets.schema.json", - "note": "Secrets the audit cross-checks. `baseline` applies to every fleet repo (the App-signed merge-bot runs everywhere). `mechanisms` are per-target/per-feature additions: a repo requires the baseline plus the mechanisms its declared publish target (`targetMechanisms`) or declared type (`typeMechanisms`) maps to. All three mappings resolve from the registry entry rather than from workflow content: nothing reads a repo's Actions files to infer a mechanism, and `workflowNeeds` records what a mechanism needs to appear in a workflow for a human or agent reading the audit, rather than being a detector. `featureMechanisms` is shape-validated but claims nothing today, since the one feature it names (codecov) is claimed through `typeMechanisms` wherever the repo carries tests for that type instead. Baseline secrets are implicit and are NOT repeated in a repo's registry `requiredSecrets`, which lists only the domain-specific additions. `typeMechanisms` are per-language requirements: a `csharp` or `python` repo carrying tests for that language must carry the mapped mechanism (codecov) regardless of opt-in, a lint-only language other than Python excepted. A configured secret that no applicable mechanism claims is a stale-secret finding; a present `forbids` secret is a defect. The schema also allows an `environments` block listing the per-environment GitHub Environment secrets and variables a deploy needs, and this file carries none. Such a block is operator documentation rather than part of the mechanism audit: no tool reads it, because neither `spec/validate.py` nor `spec/audit.py` queries an environment-scoped store, so a clean audit is not evidence that an environment is configured. `environmentSecrets` names what one environment carries and another does not, so a name audit does not read a single-environment credential as missing everywhere else.", + "note": "Secrets the audit cross-checks. `baseline` applies to every fleet repo (the App-signed merge-bot runs everywhere). `mechanisms` are per-target/per-feature additions: a repo requires the baseline plus the mechanisms its declared publish target (`targetMechanisms`) or declared type (`typeMechanisms`) maps to. All three mappings resolve from the registry entry rather than from workflow content: nothing reads a repo's Actions files to infer a mechanism, and `workflowNeeds` records what a mechanism needs to appear in a workflow for a human or agent reading the audit, rather than being a detector. `featureMechanisms` is shape-validated but claims nothing today, since the one feature it names (codecov) is claimed through `typeMechanisms` wherever the repo carries tests for that type instead. Baseline secrets are implicit and are NOT repeated in a repo's registry `requiredSecrets`, which lists only the domain-specific additions. `typeMechanisms` are per-language requirements: a `csharp` or `python` repo carrying tests for that language must carry the mapped mechanism (codecov) regardless of opt-in, a lint-only language other than Python excepted. A configured secret that no applicable mechanism claims is a stale-secret finding, and a present `forbids` secret is a defect. The schema still allows an `environments` block, whose `environmentSecrets` map would name what one environment carries and another does not, but the block has no per-repo dimension and downstream copies of this file are retired, so no repository has a place here to declare its environment names, and this file carries none. No tool would read one either: neither `spec/validate.py` nor `spec/audit.py` queries an environment-scoped store, so a clean audit is not evidence that an environment is configured.", "baseline": { "requires": ["CODEGEN_APP_CLIENT_ID", "CODEGEN_APP_PRIVATE_KEY"], "forbids": ["CODEGEN_APP_ID"], From 787a4dfc249d874c88a2b36af65cd297e7e43dc7 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 1 Oct 2026 18:33:10 -0700 Subject: [PATCH 3/7] Point the Stand-Up Checklist at the Registry for Environments STANDUP.md's prerequisite list said spec/secrets.json mechanisms declare a deploy's environments, which none does. It now names the mechanisms for publish credentials, the registry entry's environments for environments, and says no hub file declares the environment secret names, so the maintainer supplies them. Refs #2268 Co-Authored-By: Claude Opus 5.5 --- STANDUP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/STANDUP.md b/STANDUP.md index a28bfd7d..c0d40037 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -77,7 +77,7 @@ After the first commit, confirm it took with `git log -1 --format='%G? author=%a - **The repository**, with its owner, name, and visibility. - **The GitHub App installed on it.** An App that is created but not installed does not work, per [`repo-config/README.md`][repo-config-readme]. - **The App secret values**, in the Actions and Dependabot stores both. -- **Every publish credential and environment the repo's mechanisms declare** in [`spec/secrets.json`][secrets], including any environment a deploy gates on. +- **Every publish credential the repo's mechanisms declare** in [`spec/secrets.json`][secrets], **and every environment its registry entry's `environments` names**, including any environment a deploy gates on, with the secrets and variables the deploy reads there. No hub file declares those environment names, so the maintainer supplies them. **A repo with no remote is not partially stood up. It is not started.** Steps 0 through 3 complete locally and report progress with no repository in existence, so local progress is not evidence of onboarding progress. [`AUDIT.md`][audit] is the check that would catch it, and it reads a live repo, so the one instrument that detects this condition is unavailable exactly while it holds. From 8e8c9118c9999bb706eb5ac14d0a09fccea77b6c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 1 Oct 2026 18:37:19 -0700 Subject: [PATCH 4/7] Say the Hub Declares No Environment Secret Names, Not No Environments The strict review found the checklist item claiming no hub file declares the environment names, which the registry does, and pointing at a registry entry that does not exist yet when the list is handed over. It now asks for every environment a deploy gates on, which step 1 records in the registry, and says no hub file declares the secret and variable names an environment holds. Refs #2268 Co-Authored-By: Claude Opus 5.5 --- STANDUP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/STANDUP.md b/STANDUP.md index c0d40037..f0233421 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -77,7 +77,7 @@ After the first commit, confirm it took with `git log -1 --format='%G? author=%a - **The repository**, with its owner, name, and visibility. - **The GitHub App installed on it.** An App that is created but not installed does not work, per [`repo-config/README.md`][repo-config-readme]. - **The App secret values**, in the Actions and Dependabot stores both. -- **Every publish credential the repo's mechanisms declare** in [`spec/secrets.json`][secrets], **and every environment its registry entry's `environments` names**, including any environment a deploy gates on, with the secrets and variables the deploy reads there. No hub file declares those environment names, so the maintainer supplies them. +- **Every publish credential the repo's mechanisms declare** in [`spec/secrets.json`][secrets], **and every environment a deploy gates on**, which step 1 records in the registry entry's `environments`, with the secrets and variables the deploy reads there. No hub file declares the secret and variable names an environment holds, so the maintainer supplies them. **A repo with no remote is not partially stood up. It is not started.** Steps 0 through 3 complete locally and report progress with no repository in existence, so local progress is not evidence of onboarding progress. [`AUDIT.md`][audit] is the check that would catch it, and it reads a live repo, so the one instrument that detects this condition is unavailable exactly while it holds. From a7e3320185a36ddf031aa0e9c44c67982f93b9e4 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 1 Oct 2026 18:38:15 -0700 Subject: [PATCH 5/7] Take the STANDUP.md Checklist Rewrite Back Out of This Change Two rewrites of the stand-up prerequisite item each drew a new false claim under review, the second about the environment secret and variable names the hub's deploy-site-task.yml fixes. Per the stop rule this change set for itself, the item returns to its develop wording and is filed as its own issue, leaving this change the four statements that verified true. Refs #2268 Co-Authored-By: Claude Opus 5.5 --- STANDUP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/STANDUP.md b/STANDUP.md index f0233421..a28bfd7d 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -77,7 +77,7 @@ After the first commit, confirm it took with `git log -1 --format='%G? author=%a - **The repository**, with its owner, name, and visibility. - **The GitHub App installed on it.** An App that is created but not installed does not work, per [`repo-config/README.md`][repo-config-readme]. - **The App secret values**, in the Actions and Dependabot stores both. -- **Every publish credential the repo's mechanisms declare** in [`spec/secrets.json`][secrets], **and every environment a deploy gates on**, which step 1 records in the registry entry's `environments`, with the secrets and variables the deploy reads there. No hub file declares the secret and variable names an environment holds, so the maintainer supplies them. +- **Every publish credential and environment the repo's mechanisms declare** in [`spec/secrets.json`][secrets], including any environment a deploy gates on. **A repo with no remote is not partially stood up. It is not started.** Steps 0 through 3 complete locally and report progress with no repository in existence, so local progress is not evidence of onboarding progress. [`AUDIT.md`][audit] is the check that would catch it, and it reads a live repo, so the one instrument that detects this condition is unavailable exactly while it holds. From 2b22868db5ca6ee482d753c3682126e9f44d07fd Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 1 Oct 2026 18:43:38 -0700 Subject: [PATCH 6/7] Scope the No-Check Claims to Audit and Configuration Tooling Copilot found "no hub tool checks the secrets an environment holds" false, since deploy-site-task.yml fails its run when a value it needs is missing or empty. The reusable-workflows paragraph and the hugo type note now say no audit or configuration tool inventories those secrets, and name the deploy task's runtime check as the one place an empty value is caught. Refs #2268 Co-Authored-By: Claude Opus 5.5 --- docs/reusable-workflows.md | 2 +- spec/project-types.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reusable-workflows.md b/docs/reusable-workflows.md index 7cf118f0..87ff96e9 100644 --- a/docs/reusable-workflows.md +++ b/docs/reusable-workflows.md @@ -68,7 +68,7 @@ The sequencing consequence is that a hub task lands on `develop`, promotes to `m ### Secrets and Permissions -Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. Whether `secrets: inherit` is used is decided by the call's own boundary, not by the fleet's preference. [GitHub documents the keyword][gh-reusing-workflows] for a caller in the same organization or enterprise as the called workflow, and the fleet is a personal account. So a cross-repository call to a hub task names each secret it passes, and `inherit` is never used on one. A call whose job needs none passes no `secrets:` key, which is what the [Adopting the Gates][adopting-the-gates] `validate` stub does. A call by local path stays inside one repository. There the caller's own secret store is the one the called workflow reads, so `inherit` is available. Availability is not a reason to use it, and this repository's own local-path calls name their secrets or pass none. Both shapes run in the fleet today, one repo carrying a local-path `inherit` call beside a cross-repository call that names its secrets, and another proving an inherited value reaches a publishing task that authenticates from it. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `spec/secrets.json` declares none of `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it, since each lives in a GitHub Environment rather than the repository store, and no hub tool checks the secrets an environment holds, per the `deploy-ssh` mechanism note there. +Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. Whether `secrets: inherit` is used is decided by the call's own boundary, not by the fleet's preference. [GitHub documents the keyword][gh-reusing-workflows] for a caller in the same organization or enterprise as the called workflow, and the fleet is a personal account. So a cross-repository call to a hub task names each secret it passes, and `inherit` is never used on one. A call whose job needs none passes no `secrets:` key, which is what the [Adopting the Gates][adopting-the-gates] `validate` stub does. A call by local path stays inside one repository. There the caller's own secret store is the one the called workflow reads, so `inherit` is available. Availability is not a reason to use it, and this repository's own local-path calls name their secrets or pass none. Both shapes run in the fleet today, one repo carrying a local-path `inherit` call beside a cross-repository call that names its secrets, and another proving an inherited value reaches a publishing task that authenticates from it. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `spec/secrets.json` declares none of `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it, since each lives in a GitHub Environment rather than the repository store, and no hub audit or configuration tool inventories the secrets an environment holds, per the `deploy-ssh` mechanism note there. The deploy task itself fails its run when one it needs is missing or empty, which is a runtime check rather than an audit. A hub task declares no job-level `permissions:` where every write goes through the App token, and the caller sets `permissions: {}`. A called workflow can only keep or reduce the caller's grant. A callee job naming a scope the caller did not grant fails at startup even when its `if:` is false. Declaring nothing in the callee is therefore the shape that cannot fail against any caller, and it gives `GITHUB_TOKEN` no scope. A task whose job genuinely writes with `GITHUB_TOKEN`, such as a release upload, declares that scope in the callee job and documents it in the stub's comment so the caller grants it. diff --git a/spec/project-types.json b/spec/project-types.json index 92ae24d3..801e924f 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -112,7 +112,7 @@ "hugo": { "detect": ["hugo.yaml", "hugo.toml", "config/_default/hugo.yaml"], "intentRefs": ["WORKFLOW.md"], - "note": "Named for the generator rather than for the transport, because what a repo builds and where the result lands are separate axes. The destination is publish[] ({ target, mechanism }), so a repo changes transport without changing type. Every assert below is phrased without naming the generator except hugo.build.strict, where a generator-specific flag is the letter, so promoting the generic ones to a shared type when a second generator arrives is a registry edit. Deploy credentials are per-environment GitHub Environment secrets and variables, which no tool here checks. A repo does not list them in its registry requiredSecrets, because spec/audit.py resolves that list against the repository actions store and would report an environment-scoped name as missing.", + "note": "Named for the generator rather than for the transport, because what a repo builds and where the result lands are separate axes. The destination is publish[] ({ target, mechanism }), so a repo changes transport without changing type. Every assert below is phrased without naming the generator except hugo.build.strict, where a generator-specific flag is the letter, so promoting the generic ones to a shared type when a second generator arrives is a registry edit. Deploy credentials are per-environment GitHub Environment secrets and variables, which no audit or configuration tool here inventories, the hub's deploy-site-task.yml failing its own run on an empty one instead. A repo does not list them in its registry requiredSecrets, because spec/audit.py resolves that list against the repository actions store and would report an environment-scoped name as missing.", "checks": [ { "id": "hugo.build.strict", "verdict": "letter", "assert": "The site build fails on a generator warning rather than rendering around it (hugo --gc --minify --panicOnWarning), and the pull request gate and the deploy run the same build command rather than two variants.", "workflowRef": "WORKFLOW.md#d1---pr-fast-feedback-smoke" }, { "id": "hugo.urls.parity", "verdict": "letter", "assert": "A URL contract gate compares the built tree against a committed list of the URLs that must render and the URLs that must redirect, and asserts a minimum length on each list before comparing it, since a truncated list makes every assertion below it pass vacuously. This is the type's check of record, standing in for the unit tests a site does not have.", "workflowRef": "WORKFLOW.md#6-per-project-type-test-walkthroughs" }, From 90a7b77e44c8415d71dcc152fbb413bf152bfe1d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 1 Oct 2026 18:44:40 -0700 Subject: [PATCH 7/7] Narrow the Hugo Note to the Values the Deploy Task Needs The deploy task fails its run on an empty required value and passes an empty optional one, so the hugo type note now says "an empty one it needs". Refs #2268 Co-Authored-By: Claude Opus 5.5 --- spec/project-types.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/project-types.json b/spec/project-types.json index 801e924f..2dd82d3d 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -112,7 +112,7 @@ "hugo": { "detect": ["hugo.yaml", "hugo.toml", "config/_default/hugo.yaml"], "intentRefs": ["WORKFLOW.md"], - "note": "Named for the generator rather than for the transport, because what a repo builds and where the result lands are separate axes. The destination is publish[] ({ target, mechanism }), so a repo changes transport without changing type. Every assert below is phrased without naming the generator except hugo.build.strict, where a generator-specific flag is the letter, so promoting the generic ones to a shared type when a second generator arrives is a registry edit. Deploy credentials are per-environment GitHub Environment secrets and variables, which no audit or configuration tool here inventories, the hub's deploy-site-task.yml failing its own run on an empty one instead. A repo does not list them in its registry requiredSecrets, because spec/audit.py resolves that list against the repository actions store and would report an environment-scoped name as missing.", + "note": "Named for the generator rather than for the transport, because what a repo builds and where the result lands are separate axes. The destination is publish[] ({ target, mechanism }), so a repo changes transport without changing type. Every assert below is phrased without naming the generator except hugo.build.strict, where a generator-specific flag is the letter, so promoting the generic ones to a shared type when a second generator arrives is a registry edit. Deploy credentials are per-environment GitHub Environment secrets and variables, which no audit or configuration tool here inventories, the hub's deploy-site-task.yml failing its own run on an empty one it needs instead. A repo does not list them in its registry requiredSecrets, because spec/audit.py resolves that list against the repository actions store and would report an environment-scoped name as missing.", "checks": [ { "id": "hugo.build.strict", "verdict": "letter", "assert": "The site build fails on a generator warning rather than rendering around it (hugo --gc --minify --panicOnWarning), and the pull request gate and the deploy run the same build command rather than two variants.", "workflowRef": "WORKFLOW.md#d1---pr-fast-feedback-smoke" }, { "id": "hugo.urls.parity", "verdict": "letter", "assert": "A URL contract gate compares the built tree against a committed list of the URLs that must render and the URLs that must redirect, and asserts a minimum length on each list before comparing it, since a truncated list makes every assertion below it pass vacuously. This is the type's check of record, standing in for the unit tests a site does not have.", "workflowRef": "WORKFLOW.md#6-per-project-type-test-walkthroughs" },