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
3 changes: 2 additions & 1 deletion .github/workflows/deploy-site-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ jobs:
echo "::error::Missing or empty GitHub Environment value(s) on '${{ inputs.environment }}': ${missing[*]}"
exit 1
fi
# SITE_AUTH_TOKEN_ID and SITE_AUTH_TOKEN are each optional, but only together: a caller that maps one without the other reaches the verify hook as a silent partial credential rather than a caught mistake.
# SITE_AUTH_TOKEN_ID and SITE_AUTH_TOKEN are each optional, but only together.
# A caller that maps one without the other fails here instead of reaching the verify hook with a partial credential.
if { [ -n "$SITE_AUTH_TOKEN_ID" ] && [ -z "$SITE_AUTH_TOKEN" ]; } || { [ -z "$SITE_AUTH_TOKEN_ID" ] && [ -n "$SITE_AUTH_TOKEN" ]; }; then
echo "::error::SITE_AUTH_TOKEN_ID and SITE_AUTH_TOKEN must both be mapped or neither; got only one."
exit 1
Expand Down
8 changes: 4 additions & 4 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 `NUGET_USERNAME` and `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. 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`. `secrets: inherit` is not used, since it is documented for a caller in the same organization or enterprise as the called workflow and the fleet is a personal account. 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 `NUGET_USERNAME` and `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. 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`. `secrets: inherit` is not used, since it is documented for a caller in the same organization or enterprise as the called workflow and the fleet is a personal account. 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.
Comment thread
ptr727 marked this conversation as resolved.

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 @@ -214,7 +214,7 @@ Hub: `publish-docker-readme-task.yml` with a `docker-readme-transform` hook, `ch
- [ ] Catalog snippets for `publish-docker-readme-task.yml`, `check-upstream-version-task.yml`, `deploy-site.yml`, `deploy-site-task.yml`, and `run-codegen-pull-request-task.yml` pinned to the release that first carries each task. `catalog/snippets/workflows/run-periodic-codegen-pull-request.yml` now exists, since [Codegen](#adopting-the-type-specific-tasks) already states it keeps the same per-repo shape as today. The other four stay open: `publish-docker-readme-task.yml` and `check-upstream-version-task.yml` are each a job embedded in a repo's own workflow rather than a standalone top-level caller with a snippet of its own, and `deploy-site.yml` carries no manifest-wide snippet by design, since each site's own shape varies around the shared `deploy` job.
- [ ] `reports/workflow-reuse.md` regenerated, and the fleet total's callers equal to the sum of the stubs the fleet needs.
- [ ] The environment-secret handoff in the deploy-site adoption, the caller job's own `environment:` binding resolving `DEPLOY_SSH_PRIVATE_KEY` for an explicit `secrets:` map across a cross-repository `uses:`, observed on Blog's first live deploy run. Tick with the run URL.
- [x] The `deploy` hook's `verify` mode gained a same-shaped secret handoff as `DEPLOY_SSH_PRIVATE_KEY` above it, the case Blog's `checks/check-live-urls.sh` raised against its own staging environment's token-gated auth. `deploy-site-task.yml` declares an optional, generic `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair and forwards it into the `verify` invocation as `env:`, the named-pair shape decided in [issue #929][issue-929]. Which product gates a given environment, and how a caller maps that product's own secrets to these two names, stays that repo's concern rather than the hub's.
- [x] The `deploy` hook's `verify` mode gained a same-shaped secret handoff as `DEPLOY_SSH_PRIVATE_KEY` above it. Blog's `checks/check-live-urls.sh` raised the case, against its own staging environment's token-gated auth. `deploy-site-task.yml` declares an optional, generic `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair and forwards it into the `verify` invocation as `env:`, the named-pair shape decided in [issue #929][issue-929]. Which product gates a given environment, and how a caller maps its secrets to these two names, stays that repo's concern, not the hub's.
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
- [ ] The default `docker-readme-transform` action resolving through `$/` at the caller's pinned hub commit, observed on a caller with no override hook. Tick with the run URL.
- [ ] `$/` recognized by a released actionlint, so the scoped `.github/actionlint.yaml` ignores can drop.

Expand Down Expand Up @@ -547,13 +547,13 @@ 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` and binds the same `environment:` the task binds, which is what lets the one crossing secret, `DEPLOY_SSH_PRIVATE_KEY`, resolve from the GitHub Environment store at the call site rather than through `secrets: inherit`, unusable across repositories. 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, for a site whose live check sits behind its own token-gated auth.
**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` and binds the same `environment:` the task binds, which is what lets the one crossing secret, `DEPLOY_SSH_PRIVATE_KEY`, resolve from the GitHub Environment store at the call site rather than through `secrets: inherit`, unusable across repositories. 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.
Comment thread
ptr727 marked this conversation as resolved.

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.

`prune` mode is a no-op for Blog. Its deploy credential is a forced `rsync` command confined write-only, so it can neither list nor delete the remote destination, and retention there is owned by the host's own daily timer instead, which Blog's own `OPERATIONS.md` records by name next to that ownership line. A site whose credential can observe its own destination prunes for real in this mode instead, the case `deploy-site-task.yml`'s own comment on that step already anticipates.

`verify` mode wraps `checks/check-live-urls.sh`, reading the `SITE_BASE_URL` `env:` var the same way `build` mode does, plus the optional `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair, since Blog's staging environment sits behind its own token-gated auth proxy. Which product gates that environment, and how Blog's own caller maps its secrets to those two generic names, is Blog's concern to document in its own repository, not the hub's: a further adopter behind a different product's token gate needs no hub change and no new sentence here, only its own caller mapping its own secret names to `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN`.
`verify` mode wraps `checks/check-live-urls.sh`, reading the `SITE_BASE_URL` `env:` var the same way `build` mode does. It also gets the optional `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair, since Blog's staging environment sits behind its own token-gated auth proxy. Which product gates that environment is Blog's concern to document in its own repository, not the hub's. So is how Blog's own caller maps its secrets to those two generic names. A further adopter behind a different product's token gate needs no hub change. Its own caller just maps its own secret names to `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN`.

A hook, not a fixed path convention, is still the better contract even at two scripts: Blog's own generator setup (`install-hugo`, the mtime restore) and its staging auth check are exactly the per-site variation a hardcoded script name could not absorb.

Expand Down