Host the Release Chain and the Docker Core in the Hub - #762
Conversation
Records #762 as the evidence for the Rollout tracker release-chain hub pull request checkbox.
There was a problem hiding this comment.
Pull request overview
Moves the fleet’s release-chain orchestration and Docker “core” implementation into hub-hosted reusable workflows, backed by hub-default hook actions, and updates the spec/docs/audit machinery to reflect the new stub-based adoption model.
Changes:
- Adds hub workflows for
build-release-task.yml(release chain + duplicated Docker core) andbuild-docker-task.yml(Docker core only), plus hub-default composite actions for build hooks and docker-prepare. - Updates the hub’s
publish-release.ymlto exercise the new hub release task (with all targets disabled) and updates spec/docs/audit artifacts for the “caller stub” model. - Retires the prior catalog workflow snippets that the new hub tasks replace and records the retirement in divergences/prose-lint.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
TODO.md |
Updates Stage 4 backlog item with settled decisions for the hub-hosted release chain + Docker core. |
spec/files.json |
Changes publish-release.yml to an interface-checked stub contract. |
spec/fidelity-model.md |
Documents the contract shift from carried job graphs to caller stubs. |
spec/divergences.json |
Adds a retire disposition for downstream build-release-task.yml copies after adoption. |
spec/audit.py |
Adds selftests for the new publish-release.yml interface contract shape. |
scripts/prose_lint.py |
Adds build-release-task.yml to the hub-hosted path allowlist for prose linting. |
docs/reusable-workflows.md |
Documents “Adopting the Release Chain” and updates Stage 4 scope and rollout/proof items. |
catalog/snippets/workflows/README.md |
Updates catalog description to reflect hub-hosted tasks and moves hooks to composite actions. |
catalog/snippets/workflows/build-release-task.yml |
Removes retired snippet (replaced by hub workflow). |
catalog/snippets/workflows/build-pypilibrary-task.yml |
Removes retired snippet (replaced by hook default / hub task). |
catalog/snippets/workflows/build-nugetlibrary-task.yml |
Removes retired snippet (replaced by hook default / hub task). |
catalog/snippets/workflows/build-executable-task.yml |
Removes retired snippet (replaced by hook default / hub task). |
catalog/snippets/workflows/build-docker-task.yml |
Removes retired snippet (replaced by hub Docker task). |
.github/workflows/validate-task.yml |
Adds an actionlint ignore for github.job_workflow_sha. |
.github/workflows/publish-release.yml |
Switches hub publishing to call the new build-release-task.yml with all targets disabled. |
.github/workflows/build-release-task.yml |
Adds the hub-hosted release chain workflow with hook-based build targets and embedded Docker core + GitHub release job. |
.github/workflows/build-docker-task.yml |
Adds the hub-hosted Docker-only workflow (Docker core). |
.github/actions/docker-prepare-default/action.yml |
Adds the hub-default docker-prepare hook implementation. |
.github/actions/build-pypi-default/action.yml |
Adds the hub-default build-pypi hook implementation. |
.github/actions/build-nuget-default/action.yml |
Adds the hub-default build-nuget hook implementation. |
.github/actions/build-executable-default/action.yml |
Adds the hub-default build-executable hook implementation. |
.claude-plugin/fleet-skills/skills/operational-vs-release-workflow/references/release-publish-mechanics.md |
Regenerated/updated skill reference to match the new hub-hosted model. |
.claude-plugin/fleet-skills/.source-digest |
Updates plugin digest for the regenerated content. |
.agents/skills/operational-vs-release-workflow/references/release-publish-mechanics.md |
Updates the source skill reference to match the new hub-hosted model. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 24 changed files in this pull request and generated no new comments.
Suppressed comments (3)
.github/workflows/build-release-task.yml:401
build-dockeris intended to run even when optional dependencies are skipped (e.g. docker_matrix passed => docker-prepare skipped; or other targets disabled on smoke). Withoutalways(), GitHub Actions will skip this job when anyneedsjob is skipped, so Docker builds can be silently skipped on smoke/partial target sets.
build-docker:
name: Build Docker image job
needs: [get-version, validate-release, build-executable, build-nuget, build-pypi, docker-prepare, docker-build-base]
if: >-
${{ inputs.enable_docker && !failure() && !cancelled() &&
(needs.docker-prepare.result == 'success' || needs.docker-prepare.result == 'skipped') &&
(needs.docker-build-base.result == 'success' || needs.docker-build-base.result == 'skipped') }}
spec/files.json:33
- This interface contract requires
publish-release.ymlto have aplanjob and to referencepublish-plan-task.yml, but the hub currently does not ship.github/workflows/publish-plan-task.ymland the hub's own.github/workflows/publish-release.ymlhas onlyvalidate+publish. As written, the hub will fail its own audit and downstream stubs cannot reference the missing hub task.
{ "path": ".github/workflows/validate-task.yml", "fidelity": "intent", "intentRef": "WORKFLOW.md#d1---pr-fast-feedback-smoke", "appliesTo": "*" },
{ "path": ".github/workflows/publish-release.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["plan", "publish"], "requireTokensInJob": { "plan": ["publish-plan-task.yml"], "publish": ["build-release-task.yml"] } }, "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": "*" },
.github/actions/docker-prepare-default/action.yml:36
- The hub default
docker-prepareemits tags/cache refs frominputs.imagewithout validating it is non-empty. If a caller forgets to setimage, this produces invalid tags likedocker.io/:latestand fails later with a confusing Docker error.
set -Eeuo pipefail
moving=develop
[[ "$BRANCH" == "main" ]] && moving=latest
matrix=$(jq -cn \
|
Answering the three suppressed (collapsed, low-confidence) findings from the round-2 review, since they carry no thread to reply on directly. Suppressed finding, Suppressed finding, Suppressed finding, |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.
Suppressed comments (7)
.github/workflows/build-release-task.yml:248
- Same issue here:
github.job_workflow_shamay be missing at runtime for reusable workflows, causing the hub checkout for defaults to fail. Prefer${{ job.workflow_sha }}inside the reusable workflow.
ref: ${{ github.job_workflow_sha }}
.github/workflows/build-release-task.yml:298
- Same issue here: using
github.job_workflow_sharisks an empty ref and a failed hub checkout. Use${{ job.workflow_sha }}for the called-workflow SHA.
ref: ${{ github.job_workflow_sha }}
.github/workflows/build-release-task.yml:341
- Same issue here:
github.job_workflow_shais not consistently populated; if it’s blank, the.hubcheckout will fail and Docker matrix resolution defaults won’t work. Use${{ job.workflow_sha }}.
ref: ${{ github.job_workflow_sha }}
spec/files.json:32
- This new
publish-release.ymlinterface contract requires aplanjob that namespublish-plan-task.yml, but the hub’s.github/workflows/publish-release.ymlin this PR has onlyvalidate+publish(and there is no.github/workflows/publish-plan-task.ymlin-tree). As a result,spec/audit.pywill report the hub as nonconformant for any selector set that includestwo-phase/dispatch-only/publish-on-merge(which includes ProjectTemplate itself).
{ "path": ".github/workflows/publish-release.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["plan", "publish"], "requireTokensInJob": { "plan": ["publish-plan-task.yml"], "publish": ["build-release-task.yml"] } }, "intentRef": "WORKFLOW.md#d4---release--publish", "appliesTo": ["two-phase", "dispatch-only", "publish-on-merge"] },
docs/reusable-workflows.md:310
- The example
publish-pypijob depends onbuild, but the stub shown above definespublish(the hubbuild-release-task.ymlcall) as the job that produces thepypilibrary-build-*artifact. As written, this example won’t run becausebuilddoesn’t exist.
needs: [validate, build]
.github/workflows/validate-task.yml:48
- These comments refer to a
job_workflow_shaproperty, but the reusable-workflow SHA is exposed via thejobcontext (job.workflow_sha). The docs and code should describe the actual context used so future edits don’t perpetuate the wrong property name.
# The job_workflow_sha context property resolves the exact commit a caller pinned a hub task to.
# It is the mechanism docs/reusable-workflows.md "Target Model" uses to check out the hub for a hook default.
# The pinned actionlint 1.7.12 does not yet recognize this real GitHub context property.
# The one finding it produces for that field is ignored here rather than silenced by weakening the workflow.
.github/workflows/validate-task.yml:52
- If the workflows switch to
${{ job.workflow_sha }}(instead ofgithub.job_workflow_sha), this ignore string won’t match the linter message anymore. Update (or remove) the ignore so it continues to be scoped to the one intended warning.
with:
flags: -ignore 'property "job_workflow_sha" is not defined'
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.
Suppressed comments (4)
.github/workflows/build-docker-task.yml:101
- This uses
ref: ${{ job.workflow_sha }}when checking out the hub defaults.job.workflow_shais not a documented context property; the documented name isgithub.job_workflow_sha. If this resolves empty, the hub checkout will default to the hub’s default branch instead of the pinned commit.
ref: ${{ job.workflow_sha }}
.github/workflows/validate-task.yml:53
- The actionlint ignore regex is overly tied to actionlint’s current rendering of the
jobcontext type (the{check_run_id...field list). That makes the gate brittle across actionlint upgrades and also won’t match if the intended context isgithub.job_workflow_sha(the error would mentionjob_workflow_sha, notworkflow_sha).
uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0
with:
flags: -ignore 'property "workflow_sha" is not defined in object type \{check_run_id'
.github/workflows/build-release-task.yml:201
job.workflow_shais not a documented GitHub Actions context; the documented name for the reusable-workflow SHA isgithub.job_workflow_sha. As written, this expression is likely to resolve to an empty string at runtime, causing the hub checkout to fall back to the default branch and breaking the “pinned hub defaults” guarantee.
ref: ${{ job.workflow_sha }}
.github/actions/build-nuget-default/action.yml:70
-property:Versionis set to$SEMVER2, butSemVer2can include+buildmetadata(the workflow’s own validate-release logic explicitly strips+...). Passing a+...version throughVersioncan break MSBuild/NuGet version handling or produce inconsistent assembly/package metadata. The prior workflow shape used the assembly version forVersionand reserved SemVer2 forPackageVersion.
dotnet build "$PROJECT_FILE" \
--configuration "$configuration" \
-property:OutputPath="$RUNNER_TEMP/publish/" \
-property:PackageOutputPath="$RUNNER_TEMP/publish/" \
-property:Version="$SEMVER2" \
-property:FileVersion="$ASSEMBLY_FILE_VERSION" \
-property:AssemblyVersion="$ASSEMBLY_VERSION" \
-property:InformationalVersion="$ASSEMBLY_INFORMATIONAL_VERSION" \
-property:PackageVersion="$SEMVER2"
|
Answering the four suppressed findings raised on head 4f6ad84 (round 4), since they carry no thread to reply on directly.
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.
Suppressed comments (3)
scripts/prose_lint.py:345
- HUB_HOSTED is sourced from the retire dispositions in spec/divergences.json, but it was only extended for build-release-task.yml. Since this PR also introduces hub-hosted build-docker-task.yml, leaving it out risks prose_lint false-flagging references to that hub-hosted path as missing in downstream contexts (and will diverge from spec/divergences.json once it is updated).
HUB_HOSTED = frozenset({"repo-config/configure.sh", ".github/workflows/build-release-task.yml"})
docs/reusable-workflows.md:296
- In the "Adopting the Release Chain" publish-release.yml stub example, the publish job calls build-release-task.yml but doesn’t pass the ref input. Elsewhere in this PR (the hub’s own publish-release.yml) the ref is explicitly pinned to github.sha to avoid releasing an unvalidated later commit after a dispatch; the example should reflect that same invariant.
with:
branch: ${{ github.ref_name }}
smoke: false
spec/divergences.json:11
- spec/divergences.json adds a retire gap for build-release-task.yml but not for build-docker-task.yml, even though this PR also hosts the Docker core in the hub and retires the downstream build-docker-task copies. This will leave downstream build-docker-task.yml copies showing up as UNTRIAGED in divergences reporting instead of as an intentional retire gap.
{ "path": ".github/workflows/build-release-task.yml", "disposition": "retire", "reason": "The release chain is hub-hosted as a workflow_call task, per docs/reusable-workflows.md \"Stage 4: The Release Chain and the Docker Core\", so a downstream copy of this filename is retired rather than re-vendored: the caller stub a repo carries after adoption is publish-release.yml and test-pull-request.yml calling the hub task by pin, and no adopting repo carries a same-named local file. The ten carriers measured on develop at hub 7c67328 are PhotoCleaner, PlexCleaner, LanguageTags, MediaTools, Utilities, aiopurpleair, ESPHome-NonRoot, VSCode-Server-DotNetCore, KiCadLibrary, and homeassistant-purpleair. Delete the copy as each repo adopts the hub task; adoption is a separate, later change per repo (docs/reusable-workflows.md \"Rollout\" Stage 4).", "tracking": null },
{ "path": "LICENSE", "disposition": "accepted", "reason": "Each repo owns its license file. The hub does not standardize license text, so it is intentionally outside the manifest.", "tracking": null },
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Suppressed comments (1)
.github/actions/build-nuget-default/action.yml:66
- The NuGet build sets the MSBuild
Versionproperty toSemVer2, but this property is typically expected to be the numeric assembly/package base version (and this action already takesassembly-versionexplicitly). UsingSemVer2can introduce a prerelease suffix (e.g.-alpha) intoVersion, which can cause inconsistent assembly/package metadata compared to the inputs and the prior workflow snippet behavior.
-property:Version="$SEMVER2" \
|
Answering the round-6 suppressed finding, a repeat of the round-5 one already answered above.
|
Records #762 as the evidence for the Rollout tracker release-chain hub pull request checkbox.
212e42d to
2e423a5
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Suppressed comments (3)
.github/workflows/publish-release.yml:30
- WORKFLOW.md defines S8 (“dispatch from a ref other than
mainordevelop”) as fails fast, but this workflow currently turns a feature-branch dispatch into a silent no-op by skipping thepublishjob viaif:. That can make an accidental dispatch look successful while producing no release.
# Pin to the dispatch-time commit so a push landing after dispatch does not release unvalidated.
publish:
name: Publish project release job
needs: [ validate ]
if: ${{ github.ref_name == 'main' || github.ref_name == 'develop' }}
.github/actions/build-executable-default/action.yml:86
- This default action produces
release-asset-<branch>-executable, but it names the archiveexecutable.7z. WORKFLOW.md’s console target walkthrough specifies the asset archive name asConsole.7z(WORKFLOW.md:284), so this diverges from the documented contract/output.
- name: Zip build output step
if: ${{ inputs.smoke != 'true' }}
shell: bash
run: 7z a -t7z "$RUNNER_TEMP/executable.7z" "$RUNNER_TEMP"/publish/*
.github/actions/build-nuget-default/action.yml:80
- When
inputs.pushis true,NuGet/loginis invoked withuser: ${{ inputs.nuget-username }}, but there’s no explicit guard thatnuget-usernameis non-empty. If the caller forgets to passsecrets.NUGET_USERNAME, the job will fail with a less actionable error from the action instead of a clear, targeted message.
if: ${{ inputs.push == 'true' && inputs.smoke != 'true' }}
id: nuget-login
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0
with:
user: ${{ inputs.nuget-username }}
|
Answering the three suppressed findings raised on head 2e423a5 (round 7).
|
|
Answering the three suppressed findings raised on head b0671fd (round 21), each a direct consequence of that commit's own contract change.
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Suppressed comments (6)
spec/files.json:31
- The publish-release.yml interface contract doesn’t assert that the
validatejob actually callsvalidate-task.yml. Without that token requirement, a caller stub could satisfyrequiredJobKeyswith a no-op validate job and still pass the interface audit, undermining the “publish is gated by the reusable validation task” guarantee.
{ "path": ".github/workflows/publish-release.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["plan", "validate", "publish"], "requireTokensInJob": { "plan": ["publish-plan-task.yml"], "publish": ["build-release-task.yml"] } }, "intentRef": "WORKFLOW.md#d4---release--publish", "appliesTo": ["two-phase", "dispatch-only", "publish-on-merge"] },
spec/audit.py:2159
- The selftest fixture contract for a publish-release.yml caller stub should mirror the spec/files.json interface contract. If the interface contract requires
validateto callvalidate-task.yml, the selftest’spublish_contractshould require that token as well so the fixture tests the intended rule.
"requiredJobKeys": ["plan", "validate", "publish"],
"requireTokensInJob": {
"plan": ["publish-plan-task.yml"],
"publish": ["build-release-task.yml"],
},
spec/audit.py:2133
- This comment block says the
validatejob has no token requirement, but the release model relies on publish being gated by the reusable validation task. To avoid ambiguity (and keep the selftest prose aligned with the interface contract), it’s better to state thatvalidateis expected to callvalidate-task.ymland the contract asserts it.
# The publish-release.yml caller stub once the release chain is hub-hosted, plan, validate and publish job keys.
# Each names its hub task by token, per docs/reusable-workflows.md "Adopting the Release Chain".
# Validate carries no token requirement of its own.
# Its presence as a job key is what stops a stub from skipping the gate, per WORKFLOW.md's source-only shape.
docs/reusable-workflows.md:450
- The example caller stub uses
secrets: inheritwhen callingvalidate-task.yml, but this document’s “Secrets and Permissions” section states callers should map secrets explicitly (and thatsecrets: inheritisn’t used for hub tasks). Prefer explicitly mapping the single declared secret (CODECOV_TOKEN) or omitting secrets entirely, so the example matches the documented contract.
name: Validate job
needs: [plan]
if: ${{ needs.plan.outputs.publish == 'true' }}
uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@<sha> # <tag>
secrets: inherit
.github/workflows/build-release-task.yml:120
- This workflow inherits whatever
GITHUB_TOKENpermissions the caller grants to the job that invokes it, so jobs like get-version/build-executable/build-pypi/docker-* will run with broader scopes (e.g. contents: write, actions: write) than they need. Setting a workflow-level defaultpermissions: { contents: read }keeps those jobs least-privilege while preserving job-level overrides for build-nuget (id-token) and github-release (contents/actions write).
secrets:
# The nuget.org profile name for OIDC trusted publishing.
# Not needed when enable_nuget is false.
NUGET_USERNAME:
required: false
# Not needed when enable_docker is false.
DOCKER_HUB_USERNAME:
required: false
DOCKER_HUB_ACCESS_TOKEN:
required: false
jobs:
get-version:
.github/workflows/build-docker-task.yml:86
- Like build-release-task.yml, this workflow will run with the caller job’s
GITHUB_TOKENscopes unless it sets its own default. Adding a workflow-levelpermissions: { contents: read }keeps docker-prepare/build-docker least-privilege even when the caller grants broader permissions for other jobs.
secrets:
# Not needed on a smoke build, but declared required since a real caller always logs in for cache reads.
DOCKER_HUB_USERNAME:
required: true
DOCKER_HUB_ACCESS_TOKEN:
required: true
jobs:
Round-22's on-head suppressed findings, on head a1df3a7: - spec/files.json: the publish-release.yml interface contract required a validate job key but never checked it actually calls validate-task.yml, so a no-op validate job would still pass. Added requireTokensInJob for validate, mirroring test-pull-request.yml's own existing contract shape. - spec/audit.py: updated the check_interface selftest's publish contract and its explanatory comment to match, and added a case for a validate job that never names validate-task.yml. - docs/reusable-workflows.md: the new publish-release.yml stub example called validate-task.yml with secrets: inherit, contradicting this same doc's own "Secrets and Permissions" section (secrets: inherit is not used on the fleet's personal account). Mapped the one secret validate-task.yml declares, CODECOV_TOKEN, explicitly instead. Declined two suppressed findings from the same round, no change: build-release-task.yml and build-docker-task.yml adding a workflow-level default permissions: contents: read. No existing hub task (get-version-task.yml, publish-plan-task.yml, validate-task.yml, merge-bot-task.yml) sets a workflow-level permissions default, relying instead on the documented per-job elevation only where a job genuinely writes. Adding one here would be new precedent, not a fix, so it is answered in the PR conversation rather than applied.
|
Answering the six suppressed findings raised on head a1df3a7 (round 22).
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Suppressed comments (2)
.github/workflows/build-docker-task.yml:79
- The comment above DOCKER_HUB_* secrets says they are “Not needed on a smoke build”, but the workflow always runs the Docker Hub login step (including smoke) so these secrets are still required. This is misleading for callers and conflicts with the job behavior.
# Not needed on a smoke build, but declared required since a real caller always logs in for cache reads.
spec/files.json:31
- PR description says the publish-release.yml interface contract is
requiredJobKeys: ["plan", "publish"], but the code enforces["plan", "validate", "publish"]. Please update the PR description to match the implemented contract (or adjust the contract if the description is the intended requirement).
{ "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"] } }, "intentRef": "WORKFLOW.md#d4---release--publish", "appliesTo": ["two-phase", "dispatch-only", "publish-on-merge"] },
…ain-tasks # Conflicts: # .agents/skills/operational-vs-release-workflow/references/release-publish-mechanics.md # .claude-plugin/fleet-skills/.source-digest # .claude-plugin/fleet-skills/skills/operational-vs-release-workflow/references/release-publish-mechanics.md # .github/actionlint.yaml # WORKFLOW.md # catalog/snippets/workflows/README.md # docs/reusable-workflows.md # spec/files.json
Round-23's suppressed finding on head 003c444, still present after the develop merge: .github/workflows/build-docker-task.yml declared DOCKER_HUB_USERNAME and DOCKER_HUB_ACCESS_TOKEN required with a comment saying they are "not needed on a smoke build," but the login step a few lines down runs unconditionally, even on smoke, for the higher Docker Hub rate limit on cache reads, and a fork PR without both secrets cannot run the Docker smoke build at all. Reworded to state what is actually true. Also updated the PR description, whose text still quoted spec/files.json's publish-release.yml contract as requiredJobKeys: [plan, publish], stale after b0671fd and 003c444 added validate.
|
Answering the suppressed finding raised on head 003c444 (round 23), which the develop merge (e7f03e2) did not touch and remained real on the new head even though the round-24 full-coverage review did not re-flag it.
Also fixed in the same commit, from the same round: the PR description still quoted Current head a4c5ff9: |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Suppressed comments (2)
.github/actions/build-nuget-default/action.yml:70
- In the NuGet default hook,
dotnet buildsets-property:VersiontoSemVer2. That value may include prerelease/build metadata, and it also differs from the established pattern in this repo (e.g., executable builds useAssemblyVersionforVersionwhile reservingSemVer2forPackageVersion). SettingVersiontoAssemblyVersionavoids accidentally threading prerelease metadata into the assembly/package version surface.
dotnet build "$PROJECT_FILE" \
--configuration "$configuration" \
-property:OutputPath="$RUNNER_TEMP/publish/" \
-property:PackageOutputPath="$RUNNER_TEMP/publish/" \
-property:Version="$SEMVER2" \
-property:FileVersion="$ASSEMBLY_FILE_VERSION" \
-property:AssemblyVersion="$ASSEMBLY_VERSION" \
-property:InformationalVersion="$ASSEMBLY_INFORMATIONAL_VERSION" \
-property:PackageVersion="$SEMVER2"
.github/workflows/build-release-task.yml:238
build-nugetrequestsid-token: writeat the job level even when it’s only doing a smoke/validation build (the actual push is gated later viapush: ${{ inputs.nuget && !inputs.smoke }}). This means any PR smoke build that enables NuGet compilation must grant OIDC (id-token: write) to the reusable workflow call even though no publish occurs, which expands the OIDC attack surface for non-publishing runs.
If the intent is least-privilege for smoke builds, consider splitting NuGet into two phases: a build job (no id-token) and a publish job (only runs when inputs.nuget && !inputs.smoke, and is the only job that requests id-token: write). That likely requires adjusting the hook/default so the build phase never performs the push itself.
# OIDC trusted publishing through NuGet/login needs id-token: write, granted by the caller on its call to this task.
# It is declared alongside contents: read, since a job-level permissions: block zeroes every unlisted scope, and this job's checkout steps need it.
permissions:
contents: read
id-token: write
|
Answering the two suppressed findings raised on head a4c5ff9 (round 25).
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Suppressed comments (1)
.github/workflows/build-release-task.yml:149
- The inlined NBGV step doesn’t set
IGNORE_GITHUB_REF: "true"like.github/workflows/get-version-task.ymldoes. Without it, NBGV can classify versions based on the caller run’sGITHUB_REFinstead of the commit checked out viainputs.ref, which breaks the intended “caller may build another branch/commit” behavior (and can tripvalidate-releaseon a develop leg built from a main-triggered run). Align this with get-version-task.yml by setting the env var on thedotnet/nbgv@masterstep.
# The nbgv action is floated on @master, because its tag stream lags master.
# Dependabot tag-tracking would otherwise propose a downgrade.
- name: Run Nerdbank.GitVersioning tool step
id: nbgv
uses: dotnet/nbgv@master
… Default (#768) Promote `develop` to `main`, carrying the remaining stages of the hub-hosted reusable-workflow rollout and the changes that landed beside them: - #759 Host Get-Version and Publish-Plan as Hub Reusable Tasks - #760 Host the Validate Task and Reshape the Test Pull Request Stub (settles #729 by design: the hub's validate task runs `uvx <tool>@latest`, since Dependabot tracks the action pins and not a uvx version) - #761 Host the Type-Specific Tasks and Retire the Date Badge - #762 Host the Release Chain and the Docker Core in the Hub - #748 and #752, the staged rollout tracker and the PhotoCleaner merge-bot pilot record - #758 Flip the Fleet Line-Ending Default from CRLF to LF - #753, #755, #756, #764, host-setup and test-collection changes The release that follows this promotion is the first tag carrying every hub task, so it is the pin the stage 2 to 5 adoptions and their catalog snippets use. It is also the first run of the hub's own `publish-release.yml` through `build-release-task.yml` with every target disabled, which is the live proof that `github-release` runs when its build needs are skipped. Closes #729. Refs #521 (hub half shipped, the merge-bot adoption sweep is what remains).
## What `build-release-task.yml` no longer declares job-level `permissions:` on `build-nuget` (`id-token: write`) or `github-release` (`contents: write`, `actions: write`). The caller grants what the targets it enables need, and `docs/reusable-workflows.md` "Adopting the Release Chain" says so. ## Why The hub's first release through the task, dispatched on `main` at `82fecef` after #768, ended in `startup_failure` (run 31972504539). A called job's `permissions:` block is validated against the caller's grant before its `if:` runs (GOVERNANCE.md "Workflow YAML Conventions", the reusable-workflows bullet), so `build-nuget` asking for `id-token: write` fails any caller that does not grant it, the hub's own `publish` job included, even though the job is disabled. `github-release`'s block has the same shape and would fail every smoke build, since a Dependabot pull request holds a read-only token that cannot grant `contents: write`. This is the finding #762 declined on the ground that only `NuGet/login` consumes the scope: the consumption was never the problem, the declaration was. ## Verification actionlint, prose gate, audit self-test, 686 unit tests, markdownlint. The live proof is the re-dispatched hub release after this promotes, which is the next step.
…nippets (#775) Release `2.0.352` (`main` `0b07a59d7c65d07d8df275a96deaf2e06cbefd51`) is the first tag carrying every hub-hosted reusable workflow task, produced by [the publish-release run](https://github.com/ptr727/ProjectTemplate/actions/runs/31974067182) after promotion #774. In that run `build-release-task.yml` ran with every target disabled and its `github-release` job succeeded while all five build jobs skipped, proving the `github-release` skip fix (#762) and the caller-granted scopes (#772). The earlier release attempt on `82fecef` ([run](https://github.com/ptr727/ProjectTemplate/actions/runs/31972504539)) ended in `startup_failure` because `build-nuget`/`github-release` declared job-level permissions; #772 fixed it. ## docs/reusable-workflows.md "Rollout" - Ticks the two stage-0 merge-bot proof items: Dependabot PR #771 merged to `develop` with `--squash` through `merge-bot-task.yml` ([run](https://github.com/ptr727/ProjectTemplate/actions/runs/31972622149)), and Dependabot PR #770 merged to `main` with `--merge` ([run](https://github.com/ptr727/ProjectTemplate/actions/runs/31972611554)). - Ticks "Promoted and released" in stages 2, 3, 4, and 5 with `2.0.352` on `0b07a59d` and #774, and stage 4 also notes the first attempt's `startup_failure` and its #772 fix. - Leaves every adoption and pilot checkbox untouched. - Does **not** tick either stage's "Catalog snippets ... pinned to that release" item, since neither stage's full set of snippets is complete this time: stage 2's release-with-smoke shape still calls its own repo's `build-release-task.yml` by `./` path rather than the hub's, and stage 5's `deploy-site.yml`, `publish-docker-readme-task.yml`, and `check-upstream-version-task.yml` each stay open for reasons the doc itself already states (no manifest-wide snippet by design, or a job embedded in a larger stub rather than a standalone caller). Both items gain an inline note identifying which of their snippets this PR adds. ## New catalog snippets Three of the five/six candidate files, each pinned `0b07a59d7c65d07d8df275a96deaf2e06cbefd51 # 2.0.352` and byte-identical to its doc stub block apart from the pin (and, where the doc's own comment wrapping does not survive an actual YAML file's comment-wrap rule, a reworded comment kept in sync on both sides): - `catalog/snippets/workflows/test-pull-request.yml` (the no-build gate shape from "Adopting the Gates") - `catalog/snippets/workflows/publish-release.yml` (the release-repo shape from "Adopting the Release Chain") - `catalog/snippets/workflows/run-periodic-codegen-pull-request.yml` (the codegen stub, which the doc already says keeps today's per-repo shape) Skipped, with the doc updated to say why: - `test-pull-request-smoke.yml` - the doc's smoke stub still calls the caller's own `./.github/workflows/build-release-task.yml` by local path, never the hub's `build-release-task.yml` directly, so it does not meet the "calls the hub task" condition. - `deploy-site.yml` - the doc's own "Deploy-site" paragraph states it "has no manifest-wide catalog snippet either," the same reason `get-version-task.yml`/`publish-plan-task.yml` have none: each site's own shape varies too much around the shared `deploy` job. - `check-upstream-version.yml` - the doc gives only the `check-upstream-version:` job body, not a complete top-level trigger/concurrency shape, and unlike the codegen stub there is no doc statement that a uniform shape exists to copy. `catalog/snippets/workflows/README.md` gained rows for the three new snippets and an updated intro paragraph explaining the partial rollout. ## TODO.md Added one **Settled** line under "Host the type-specific tasks" naming `2.0.352` as the release that makes stages 2-5 adoptable and the run that proved the release chain, and noting #769 as the one open follow-up from the promotion review. ## reports/workflow-reuse.md Regenerated with `python3 spec/workflow_reuse.py --report` against the live fleet at hub `63d244b` (this branch's base). The numbers move against the last committed report only because the live fleet moved since then, not because of anything in this PR. ## Gates run locally `spec/validate.py`, `python3 -m unittest discover -s scripts/tests` (686 tests), `spec/audit.py --selftest`, `spec/workflow_reuse.py --selftest`, `scripts/repo_gate.py` (sha-pin resolved against GitHub), `scripts/build_dist.py --check`, `scripts/prose_lint.py` (all nine checks), `ruff check`/`ruff format --check`, `mypy`, and the Docker `actionlint`, `markdownlint-cli2`, and `editorconfig-checker` linters (only `.ruff_cache` noise there).
What
Hosts the fleet's release chain and Docker core in the hub, per
docs/reusable-workflows.md"Stage 4: The Release Chain and the Docker Core".
.github/workflows/build-release-task.yml(new hub task): inlinesget-versionandvalidate-release, per-target jobs forbuild-executable,build-nuget,build-pypi(each ahook resolved to the caller's own composite action or a hub default), a duplicated Docker core
as
build-docker(docker-prepare / docker-build-base hooks), andgithub-releaseverbatim inshape (pattern download, exists gate,
target_commitish, consume-then-delete,expect_release_assets)..github/workflows/build-docker-task.yml(new hub task): the same Docker core, publishedseparately for a caller that wants only the Docker leg. The two files carry the same job body by
hand, since a hub task cannot reach a sibling hub task by a
./path.build-executable-default,build-nuget-default,build-pypi-default,docker-prepare-default, authored from today's leaf catalog snippets.publish-release.ymlnow callsbuild-release-task.ymlwith every targetdisabled and
expect_release_assets: false, exercising the task'sget-version,validate-release, andgithub-releasejobs on the hub's own release.(
build-release-task.yml,build-executable-task.yml,build-nugetlibrary-task.yml,build-pypilibrary-task.yml,build-docker-task.yml), updates the catalog README, and adds aretiregap disposition tospec/divergences.jsonnaming the ten carriers.publish-release.ymlaninterfacecontract inspec/files.json(
requiredJobKeys: ["plan", "validate", "publish"]) with new_selftestcases inspec/audit.py, andupdates
spec/fidelity-model.md's "The Workflow Override Seam Contract" paragraph for thestub-era shape.
the Rollout tracker and
TODO.mdwith the settled design decisions and proof items.#758's LF-default flip. Every file this PR touches is LF now.Design choices where the brief left room
build-release-task.yml'sbuild-dockerjob andbuild-docker-task.ymlcarry the same job body rather than one calling the other, since a hubtask cannot nest a sibling hub task by a
./path.build-docker-task.ymlstays published fora caller that wants the Docker leg alone.
publish-release-task.yml. A caller stub'splan/validate/publish/publish-pypijobs are each a thin call to one hub task or a verbatim OIDC upload, and the trigger policy
tying them together differs enough across the fleet's shapes (dispatch-only Docker schedule,
push-gated NuGet/PyPI) that hosting it would only move the same
with:block, not remove it.ref/branch/smokeset, the defaults takeproject-file/project-dir, mirroring theDocker hook's own
imageinput, since no live repo's project folder matches the vanillaConsole/NuGetLibrary/PyPiLibraryconvention literally.release-assetshook deferred. No cataloged repo needs an "extra files" hook today, so itis not shipped. The Hook Catalog table row for
build-release-task.ymlreflects only thehooks actually implemented.
job_workflow_shagap.github.job_workflow_sha(used to check out the hub atthe caller's pinned commit) is a real GitHub Actions context property actionlint 1.7.12 does not
yet recognize. Added a scoped
-ignoreflag to the hub's ownvalidate-task.ymlactionlintstep rather than working around the design.
What this cannot prove from the hub
The hub has no build targets, so every behavioral claim (the hook fallback path, OIDC NuGet
publishing, the Docker matrix/build-base hooks, a real release through the new task) is a proof
item on the PhotoCleaner/PlexCleaner pilot adoption, tracked in
docs/reusable-workflows.mdRollout Stage 4. Adoption itself is out of scope for this PR.
Verification
spec/validate.py,python3 -m unittest discover -s scripts/tests,spec/audit.py --selftest,spec/workflow_reuse.py --selftest,scripts/repo_gate.py,scripts/build_dist.py --check,scripts/prose_lint.py(full check set),ruff check/format --check,mypy,actionlint(with the one documented
-ignore),markdownlint-cli2, andeditorconfig-checkerall pass.