Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/reusable-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. `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.

Expand Down
2 changes: 1 addition & 1 deletion spec/project-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 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" },
Expand Down
4 changes: 2 additions & 2 deletions spec/secrets.json
Original file line number Diff line number Diff line change
@@ -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, 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"],
Expand Down Expand Up @@ -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": {
Expand Down
Loading