Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 .github/workflows/deploy-site-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:
esac

# Every host-specific value comes from the environment, so this file names no host, path, or address.
# The caller's own job binds the same environment, which is what makes the secret mapped into DEPLOY_SSH_PRIVATE_KEY below resolve from the environment store rather than from the repository store.
# This job's own binding resolves DEPLOY_SSH_PRIVATE_KEY cross-repository from the caller's environments, per docs/reusable-workflows.md "Deploy-site." (the caller's own job cannot carry environment: at all).
Comment thread
ptr727 marked this conversation as resolved.
Outdated
deploy:
name: Deploy site job
runs-on: ubuntu-latest
Expand Down
8 changes: 5 additions & 3 deletions docs/reusable-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ Hub: `publish-docker-readme-task.yml` with a `docker-readme-transform` hook, `ch
- [ ] KiCadLibrary (deletes `build-datebadge-task.yml` and its caller job outright, per TODO.md's retired-badge cleanup, adopting no new stub)
- [ ] 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 environment-secret handoff in the deploy-site adoption, the task's own `environment:` binding (not the caller's, which cannot carry one, [issue #942][issue-942]) resolving `DEPLOY_SSH_PRIVATE_KEY` from the caller's environment store across a cross-repository `uses:`. [Confirmed against Blog's own `staging` environment][run-cross-repo-secret-probe]: `DEPLOY_SSH_PRIVATE_KEY` resolved (masked, non-empty), and only the separately-tracked `SITE_BASE_URL` naming mismatch was missing.
- [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.
- [ ] 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,7 +547,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` 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.
**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 exists only to put the name in the task's reach, evaluates empty at the caller (no `environment:` binding there to read it from), and the task's own environment binding overrides that empty value with the real one. 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, since it does not carry environment-scoped secrets across a cross-repository call. 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
qodo-code-review[bot] marked this conversation as resolved.
Outdated
Comment thread
ptr727 marked this conversation as resolved.
Outdated

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 All @@ -561,7 +561,6 @@ A hook, not a fixed path convention, is still the better contract even at two sc
deploy:
name: Deploy job
needs: [validate]
environment: ${{ inputs.environment }}
permissions:
contents: read
uses: ptr727/ProjectTemplate/.github/workflows/deploy-site-task.yml@<sha> # <tag>
Expand Down Expand Up @@ -608,16 +607,19 @@ Four things the hub cannot prove fall to the first downstream adopter. They are

<!-- Repo -->

[gh-reusing-workflows]: https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations
[governance-hub-hosted-tooling]: ../GOVERNANCE.md#hub-hosted-tooling
[governance-workflow-yaml-conventions]: ../GOVERNANCE.md#workflow-yaml-conventions
[issue-585]: https://github.com/ptr727/ProjectTemplate/issues/585
[issue-929]: https://github.com/ptr727/ProjectTemplate/issues/929
[issue-942]: https://github.com/ptr727/ProjectTemplate/issues/942
[no-build-caller-snippet]: ../catalog/snippets/workflows/test-pull-request.yml
[override-path-run]: https://github.com/ptr727/ProjectTemplate/actions/runs/31950332387/job/95172710046
[pilot-publish-run]: https://github.com/ptr727/PhotoCleaner/actions/runs/31977092102
[pilot-smoke-run]: https://github.com/ptr727/PhotoCleaner/actions/runs/31974932749
[self-reference-smoke-run]: https://github.com/ptr727/PhotoCleaner/actions/runs/32047594855
[pr-760]: https://github.com/ptr727/ProjectTemplate/pull/760
[run-cross-repo-secret-probe]: https://github.com/ptr727/Blog/actions/runs/32618245296
[run-770]: https://github.com/ptr727/ProjectTemplate/actions/runs/31972611554
[run-771]: https://github.com/ptr727/ProjectTemplate/actions/runs/31972622149
[run-startup-failure]: https://github.com/ptr727/ProjectTemplate/actions/runs/31972504539
Expand Down
15 changes: 6 additions & 9 deletions spec/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2677,13 +2677,13 @@ def _selftest():
]
# The deploy-site.yml caller stub once deploy-site-task.yml is hub-hosted: no secrets: inherit
# (a cross-repository reusable workflow cannot use it), the one crossing secret named instead.
# No job-level environment: on the caller, unsupported on a job with uses: (ptr727/ProjectTemplate#942).
deploy_stub = (
"jobs:\n"
" assert-ref:\n runs-on: ubuntu-latest\n steps: []\n"
" validate:\n uses: ./.github/workflows/validate-task.yml\n"
" deploy:\n"
" name: Deploy job\n"
" environment: ${{ inputs.environment }}\n"
" permissions:\n contents: read\n"
" uses: acme/hub/.github/workflows/deploy-site-task.yml@" + "a" * 40 + " # 2.0.1\n"
" with:\n environment: ${{ inputs.environment }}\n"
Expand All @@ -2694,8 +2694,9 @@ def _selftest():
"requireTokensInJob": {
"deploy": [
"deploy-site-task.yml",
Comment thread
ptr727 marked this conversation as resolved.
# Indent-anchored (4 spaces) so a with: input of the same name, indented 6, cannot satisfy this on its own.
"\n environment:",
# Indent-anchored (6 spaces) to the with: block, since the task declares this
# input required and a caller missing it fails at dispatch, not at audit time.
"\n environment:",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
"contents: read",
"DEPLOY_SSH_PRIVATE_KEY",
]
Expand All @@ -2718,12 +2719,8 @@ def _selftest():
1,
),
(
"deploy-site.yml caller stub missing the environment binding the crossing secret needs",
# Removes only the job-level environment: line, leaving the with:-nested environment: input untouched, the exact ambiguity an unanchored token would miss.
deploy_stub.replace(
" name: Deploy job\n environment: ${{ inputs.environment }}\n",
" name: Deploy job\n",
),
"deploy-site.yml caller stub missing the required environment input",
deploy_stub.replace(" with:\n environment: ${{ inputs.environment }}\n", ""),
deploy_contract,
1,
),
Expand Down
2 changes: 1 addition & 1 deletion spec/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
{ "path": ".github/workflows/test-pull-request.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["check-workflow-status", "validate"], "requiredCheckName": "Check pull request workflow status job", "requireTokensInJob": { "validate": ["validate-task.yml"] } }, "intentRef": "GOVERNANCE.md#workflow-yaml-conventions", "appliesTo": "*" },
{ "path": ".github/workflows/publish-release.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["plan", "validate", "publish"], "requireTokensInJob": { "plan": ["publish-plan-task.yml"], "validate": ["validate-task.yml"], "publish": ["build-release-task.yml", "needs.validate.result == 'success'"] } }, "intentRef": "WORKFLOW.md#d4---release--publish", "appliesTo": ["two-phase", "dispatch-only", "publish-on-merge"] },
{ "path": ".github/workflows/merge-bot-pull-request.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["merge-bot"], "requireTokensInJob": { "merge-bot": ["merge-bot-task.yml", "CODEGEN_APP_CLIENT_ID", "CODEGEN_APP_PRIVATE_KEY"] } }, "intentRef": "WORKFLOW.md#d8---bots--automation", "appliesTo": "*" },
{ "path": ".github/workflows/deploy-site.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["assert-ref", "validate", "deploy"], "requireTokensInJob": { "deploy": ["deploy-site-task.yml", "\n environment:", "contents: read", "DEPLOY_SSH_PRIVATE_KEY"] } }, "intentRef": "docs/reusable-workflows.md#adopting-the-type-specific-tasks", "appliesTo": ["hugo"] },
{ "path": ".github/workflows/deploy-site.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["assert-ref", "validate", "deploy"], "requireTokensInJob": { "deploy": ["deploy-site-task.yml", "\n environment:", "contents: read", "DEPLOY_SSH_PRIVATE_KEY"] } }, "intentRef": "docs/reusable-workflows.md#adopting-the-type-specific-tasks", "appliesTo": ["hugo"] },
Comment thread
Copilot marked this conversation as resolved.
Outdated
{ "path": ".vscode/tasks.json", "sections": ["clean-compile task group"], "reference": "catalog/snippets/configs/vscode-tasks.json", "appliesTo": ["csharp"] },
{ "path": ".vscode/tasks.json", "sections": ["clean-compile task group"], "reference": "catalog/snippets/configs/vscode-tasks-python.json", "appliesTo": ["python"] },
{ "path": "codecov.yml", "fidelity": "intent", "reference": "catalog/snippets/configs/codecov.yml", "intentRef": "WORKFLOW.md", "appliesTo": ["csharp", "python"] },
Expand Down