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 docs/pre-commit-hooks-rollout.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ clean, stays operational, per the existing carve-out in GOVERNANCE.md. Two catal
the canonical shape: Husky.Net
(`catalog/snippets/husky/`, for a repo already keeping a .NET tool manifest declaring it) and the
`pre-commit` framework (`catalog/snippets/pre-commit/`, for a repo without one and for any repo
that prefers it). Both now carry the
that prefers it). Both carry the
same shared doc gates, the diff-scoped prose/comment-style gate and the whole-tree line-ending
check, fetched fresh from the hub's `main` branch at run time via `hub-fetch-run.py` rather than
vendored or pinned.
Expand Down
4 changes: 2 additions & 2 deletions 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 sit side by side in PlexCleaner's `test-pull-request.yml`. Its `validate` job calls `./.github/workflows/validate-task.yml` with `secrets: inherit`, to thread `CODECOV_TOKEN` through. Its `smoke-build` job names `DOCKER_HUB_USERNAME` and `DOCKER_HUB_ACCESS_TOKEN` explicitly for the hub's `build-release-task.yml`. The [Adopting the Gates][adopting-the-gates] smoke-build stub carries the local-path shape for the same reason. NxWitness proves an inherited value actually arrives. Its `publish-release.yml` calls its own `build-docker-task.yml` with `secrets: inherit` and `push: true`. That task authenticates to Docker Hub from the two inherited names. 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. The [Adopting the Gates][adopting-the-gates] smoke-build stub carries the local-path shape for the same reason. 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.

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 Expand Up @@ -611,7 +611,7 @@ The caller grants `contents: read` explicitly, since the task's own jobs declare

ESPHome-NonRoot carries two trackers today. `check-upstream-version.yml` adopts the stub above as-is. `check-upstream-dependency.yml`, whose bump waits for a human because its head deliberately does not match a merge-bot rule, adopts a second instance of the same stub with `with: { branches: '["develop"]', bump-branch-prefix: upstream-dependency, auto-merge: false }` and a `resolve-upstream` hook shaped around its apt-package snapshot, setting `versions` to `{"docker_base_packages": "<sorted, comma-joined package list>"}` rather than a name-to-version map. The generic title and body this produces read less specifically than today's bespoke "packages added/removed" wording, which is the cost of folding a bespoke tracker into the shared task.

**Deploy-site.** A site repo keeps `deploy-site.yml` as a per-repo caller (it has no manifest-wide catalog snippet either, since its `uses:` now names the hub, and it still carries the dispatch, the ref gate, and the shared validation call), but its `deploy` job reaches the hub-hosted `deploy-site-task.yml`. A job calling a reusable workflow cannot itself carry an `environment:` key ([GitHub's supported-keywords list][gh-reusing-workflows] omits it, and GitHub rejects the combination outright), so the caller's `deploy` job carries none. `DEPLOY_SSH_PRIVATE_KEY` still crosses correctly: the task's own `deploy` job binds `environment: ${{ inputs.environment }}` on itself, and per GitHub's own cross-repository behavior (the `github` context, and an OIDC token's `sub` claim, always attribute the environment to the *caller*), that resolves against the caller's own GitHub Environment store, not the hub's. The caller's `secrets: DEPLOY_SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}` line is still required (the task declares this a required `workflow_call.secrets` input, and GitHub rejects a call omitting a required one), but what value the task's own job actually sees for it is governed by the task's own `environment:` binding, not by anything the caller's own (environment-less) job context could resolve. Confirmed both against GitHub's documented reusable-workflow secrets mechanics and with a live cross-repository run against Blog's own `staging` environment ([issue #942][issue-942]). `secrets: inherit` was never the alternative here regardless. It is not used on a cross-repository call at all, per [Secrets and Permissions][secrets-and-permissions], and it would not carry an environment-scoped secret across one either. A required `deploy` hook, `.github/actions/deploy/action.yml`, is invoked three times (`build`, `prune`, `verify`) so the site keeps its own generator, precompression, and URL contract while the upload-then-flip sequence stays hub-owned. The hook declares all four inputs the three invocations use between them, `mode`, `bundle-path`, `release-id`, and `environment`, since a composite action rejects an invocation that supplies an input it does not declare, even one a different mode leaves unset. Each invocation also passes the GitHub Environment variables that mode needs (`SITE_BASE_URL` to `build` and `verify`, `DEPLOY_SSH_USER` and `DEPLOY_SSH_HOST` to `prune`) as plain `env:` vars, since a composite action's own steps are not guaranteed to read the caller's `vars` context directly. `verify` additionally receives an optional `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` secret pair the same way, forwarded whenever the caller maps it. A site whose live check sits behind its own token-gated auth is the reason it exists.
**Deploy-site.** A site repo keeps `deploy-site.yml` as a per-repo caller (it has no manifest-wide catalog snippet either, since its `uses:` names the hub, and it still carries the dispatch, the ref gate, and the shared validation call), but its `deploy` job reaches the hub-hosted `deploy-site-task.yml`. A job calling a reusable workflow cannot itself carry an `environment:` key ([GitHub's supported-keywords list][gh-reusing-workflows] omits it, and GitHub rejects the combination outright), so the caller's `deploy` job carries none. `DEPLOY_SSH_PRIVATE_KEY` still crosses correctly: the task's own `deploy` job binds `environment: ${{ inputs.environment }}` on itself, and per GitHub's own cross-repository behavior (the `github` context, and an OIDC token's `sub` claim, always attribute the environment to the *caller*), that resolves against the caller's own GitHub Environment store, not the hub's. The caller's `secrets: DEPLOY_SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}` line is still required (the task declares this a required `workflow_call.secrets` input, and GitHub rejects a call omitting a required one), but what value the task's own job actually sees for it is governed by the task's own `environment:` binding, not by anything the caller's own (environment-less) job context could resolve. Confirmed both against GitHub's documented reusable-workflow secrets mechanics and with a live cross-repository run against Blog's own `staging` environment ([issue #942][issue-942]). `secrets: inherit` was never the alternative here regardless. It is not used on a cross-repository call at all, per [Secrets and Permissions][secrets-and-permissions], and it would not carry an environment-scoped secret across one either. A required `deploy` hook, `.github/actions/deploy/action.yml`, is invoked three times (`build`, `prune`, `verify`) so the site keeps its own generator, precompression, and URL contract while the upload-then-flip sequence stays hub-owned. The hook declares all four inputs the three invocations use between them, `mode`, `bundle-path`, `release-id`, and `environment`, since a composite action rejects an invocation that supplies an input it does not declare, even one a different mode leaves unset. Each invocation also passes the GitHub Environment variables that mode needs (`SITE_BASE_URL` to `build` and `verify`, `DEPLOY_SSH_USER` and `DEPLOY_SSH_HOST` to `prune`) as plain `env:` vars, since a composite action's own steps are not guaranteed to read the caller's `vars` context directly. `verify` additionally receives an optional `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` secret pair the same way, forwarded whenever the caller maps it. A site whose live check sits behind its own token-gated auth is the reason it exists.

Blog is the reference adoption, and its real inventory is two scripts, not three: `deploy/make-release.sh` assembles, hard-links, stamps, and installs a release into whatever root it is pointed at, and `checks/check-live-urls.sh` verifies one against a running server. There is no `deploy/prune-releases.sh`. `build` mode wraps `make-release.sh` pointed at the hub-passed `bundle-path` rather than a live root, alongside whatever generator setup the hook itself needs, Hugo and brotli in Blog's case, that `make-release.sh` assumes are already on `PATH`. The script's own tail, a swap of a local `current` symlink to the release it just wrote and a check that the swap hard-linked something against whatever `current` pointed at before, runs entirely against that ephemeral `bundle-path`, so it is local bookkeeping rather than a second real deploy. It is also what leaves `bundle-path/current` in place for the hub task's own build-mode assertion to find. Because `bundle-path` is empty at the start of every run, that local `current` never resolves to anything and the hard-link check never has a previous release to compare against, so it is inert in CI. The `build` hook's own `current` is never the live one either way: only the hub-owned Upload release and Flip current steps that follow touch the real `/<environment>/` root, so the boundary the upload-then-flip sequence draws is between `bundle-path` and the environment, not a seam inside `make-release.sh` itself.

Expand Down
3 changes: 2 additions & 1 deletion spec/fidelity-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ A downstream repo editing carried content is a different question from a copy tr

The unit decides, never the file. `GOVERNANCE.md` is `intent` as a file while 18 of its 20 declared sections are `verbatim`. An edit inside one of those is caught, and an edit to the prose around them is not.

- **verbatim and verbatim-tree** - detected. The content hash is compared against the canonical and every past revision, and "Stale Versus Modified" below reads the result. An edit matching no revision reports as modified fixed content, and one restoring an earlier revision reports as stale, since the hash cannot say how the bytes got there. A `verbatim-tree` declaration also fails structurally, on a missing included path or an extra one under a pruned root, before any hash is read.
- **verbatim** - detected. The content hash is compared against the canonical and every past revision, and "Stale Versus Modified" below reads the result. An edit matching no revision reports as modified fixed content, and one restoring an earlier revision reports as stale, since the hash cannot say how the bytes got there.
- **verbatim-tree** - detected, by git blob identity rather than by that content hash. A tree entry is compared against the canonical blob and, on a mismatch, against that path's blob history, which gives the same stale-or-modified split. It also fails structurally, on a missing included path or an extra one under a pruned root.
- **interface** - detected only where the edit moves the contract. A changed job key, check name, or hub task token is a finding. An edit to the owned body is the override this tier exists for.
- **intent** - not detected. Content is never judged, and the staleness advisory never identifies a local edit. It compares last-change dates, so the edit first suppresses the advisory by making the copy newer. After the hub's next substantive change to that file, meaning one the line-ending, action-pin, and job-needs normalization does not explain away, the advisory fires and reports the copy as trailing, which routes to a re-vendor that overwrites the edit rather than to the edit itself.
- **presence** - a content edit is not detected. The unit is asserted to exist and is never read, so deleting it is still a finding and editing it is not.
Expand Down