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
1 change: 1 addition & 0 deletions .github/workflows/build-docker-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
secrets: inherit
with:
ref: ${{ inputs.ref }}
branch: ${{ inputs.branch }}

build-docker:
name: Build Docker image job
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/build-executable-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
secrets: inherit
with:
ref: ${{ inputs.ref }}
branch: ${{ inputs.branch }}

build-executable-matrix:
name: Build executable project matrix job
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/build-nugetlibrary-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
uses: ./.github/workflows/get-version-task.yml
with:
ref: ${{ inputs.ref }}
branch: ${{ inputs.branch }}

build-nugetlibrary:
name: Build NuGet library project job
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/build-pypilibrary-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
uses: ./.github/workflows/get-version-task.yml
with:
ref: ${{ inputs.ref }}
branch: ${{ inputs.branch }}

build-pypilibrary:
name: Build PyPI library project job
Expand Down
52 changes: 32 additions & 20 deletions .github/workflows/build-release-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,38 @@ jobs:
secrets: inherit
with:
ref: ${{ inputs.ref }}
branch: ${{ inputs.branch }}

# Entry gate: validate branch<->version consistency once, before the build jobs, so an NBGV mis-classification fails
# fast instead of after building and publishing. main must be a public release (no prerelease '-'); every other branch
# must carry a prerelease '-' (guards a develop leg being classified public and published as stable). Strip
# '+buildmetadata' first; a '-' there is legitimate, only a '-' in the core/prerelease segment marks a prerelease.
validate-release:
name: Validate release version job
needs: [get-version]
runs-on: ubuntu-latest
steps:
- name: Validate branch and version consistency step
env:
SEMVER2: ${{ needs.get-version.outputs.SemVer2 }}
BRANCH: ${{ inputs.branch }}
run: |
set -euo pipefail
CORE_AND_PRE="${SEMVER2%%+*}"
if [[ "$BRANCH" == "main" ]]; then
if [[ "$CORE_AND_PRE" == *-* ]]; then
echo "::error::Public (main) release version '$SEMVER2' carries a prerelease suffix; refusing to publish."
exit 1
fi
elif [[ "$CORE_AND_PRE" != *-* ]]; then
echo "::error::Prerelease ($BRANCH) version '$SEMVER2' has no prerelease suffix (NBGV classified it public); refusing to publish."
exit 1
fi

build-nugetlibrary:
name: Build NuGet library job
if: ${{ inputs.enable_nuget }}
needs: [get-version]
needs: [get-version, validate-release]
uses: ./.github/workflows/build-nugetlibrary-task.yml
secrets: inherit
with:
Expand All @@ -88,7 +115,7 @@ jobs:
build-pypilibrary:
name: Build PyPI library job
if: ${{ inputs.enable_pypi }}
needs: [get-version]
needs: [get-version, validate-release]
uses: ./.github/workflows/build-pypilibrary-task.yml
secrets: inherit
with:
Expand All @@ -101,7 +128,7 @@ jobs:
build-executable:
name: Build executable job
if: ${{ inputs.enable_executable }}
needs: [get-version]
needs: [get-version, validate-release]
uses: ./.github/workflows/build-executable-task.yml
secrets: inherit
with:
Expand All @@ -113,7 +140,7 @@ jobs:
build-docker:
name: Build Docker job
if: ${{ inputs.enable_docker }}
needs: [get-version]
needs: [get-version, validate-release]
uses: ./.github/workflows/build-docker-task.yml
secrets: inherit
with:
Expand All @@ -130,7 +157,7 @@ jobs:
# `github: true` still can't create a release.
if: ${{ inputs.github && !inputs.smoke }}
runs-on: ubuntu-latest
needs: [get-version, build-nugetlibrary, build-pypilibrary, build-executable, build-docker]
needs: [get-version, validate-release, build-nugetlibrary, build-pypilibrary, build-executable, build-docker]

steps:

Expand All @@ -140,21 +167,6 @@ jobs:
with:
ref: ${{ needs.get-version.outputs.GitCommitId }}

# Backstop (main only): a public release must not carry a prerelease '-', guarding against NBGV mis-versioning the
# public ref (e.g. a dispatch on a non-default ref) into a malformed "Latest" release. Strip '+buildmetadata'
# first - a '-' there is legitimate; only a '-' in the core/prerelease segment marks a prerelease.
- name: Verify public release version step
if: ${{ inputs.branch == 'main' }}
env:
SEMVER2: ${{ needs.get-version.outputs.SemVer2 }}
run: |
set -euo pipefail
CORE_AND_PRE="${SEMVER2%%+*}" # drop +buildmetadata; a '-' here is the genuine prerelease separator
if [[ "$CORE_AND_PRE" == *-* ]]; then
echo "::error::Public (main) release version '$SEMVER2' carries a prerelease suffix; refusing to publish."
exit 1
fi

# Collect assets by the `release-asset-<branch>-*` pattern so this step is target-agnostic: subset releases by
# deleting the target, not `enable_*: false` (a skipped `needs` job would skip this release job too). The release
# step guards `fail_on_unmatched_files: true`, so at least one `release-asset-*` must match; a repo that drops
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/get-version-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ on:
required: false
type: string
default: ''
# Logical branch NBGV classifies against. Pins PublicRelease to this branch instead of the runner's GITHUB_REF,
# which on a publish dispatched from the default branch is that branch for every matrix leg. Empty keeps GITHUB_REF.
branch:
required: false
type: string
default: ''
outputs:
SemVer2:
value: ${{ jobs.get-version.outputs.SemVer2 }}
Expand Down Expand Up @@ -52,3 +58,8 @@ jobs:
- name: Run Nerdbank.GitVersioning tool step
id: nbgv
uses: dotnet/nbgv@master
env:
# NBGV reads the branch from GITHUB_REF; pin it to the leg being versioned so a publish dispatched from the
# default branch doesn't classify every leg as the public ref and strip its prerelease suffix.
GITHUB_REF: ${{ inputs.branch != '' && format('refs/heads/{0}', inputs.branch) || github.ref }}
GITHUB_REF_NAME: ${{ inputs.branch != '' && inputs.branch || github.ref_name }}
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ These conventions describe the target state. New and modified workflows must res
- **Shells**: multi-line `run:` blocks with bash start with `set -euo pipefail` - fail fast, fail on undefined vars, fail on a failed pipe segment.
- **Conditionals**: multi-line `if:` uses folded scalar `if: >-` so YAML preserves whitespace correctly. Literal block (`if: |`) is wrong because it embeds newlines inside the boolean expression.
- **Boolean inputs**: workflows triggered both via `workflow_call` and `workflow_dispatch` must declare each boolean input in *both* trigger blocks - one definition does not propagate to the other. `workflow_call` delivers booleans as actual booleans; `workflow_dispatch` delivers them as the *strings* `"true"`/`"false"`. Any `if:` consuming a boolean input must compare against both forms - `if: ${{ inputs.foo == true || inputs.foo == 'true' }}`.
- **Validate input/state consistency at entry, fail fast**: when a workflow's inputs must satisfy a cross-input or input-versus-derived-state invariant (e.g. the release branch must match the computed version's prerelease status, or two inputs are mutually exclusive), assert it **once** in a dedicated entry validation step/job that the downstream jobs `needs:`, before any expensive build or publish work - not as partial checks scattered deep in later jobs. One gate that fails fast with a clear `::error::` beats a late or one-directional check. Examples: [`build-release-task.yml`](./.github/workflows/build-release-task.yml)'s `validate-release` job (branch-versus-prerelease, both directions) and [`publish-docker-readme-task.yml`](./.github/workflows/publish-docker-readme-task.yml)'s "Validate inputs step".
- **Reusable workflows**: job-level `permissions:` are validated *before* the `if:` evaluates, so even a skipped job needs valid permissions declared. A `release` job with `permissions: contents: write` and `if: ${{ inputs.publish }}` will still cause `startup_failure` on a caller that doesn't grant `contents: write`. Either declare permissions at the call site, or omit the inner block and inherit.
- **Allowlist `success` and `skipped` explicitly** when chaining jobs across optional dependencies - `!= 'failure'` lets `cancelled` through (timeout, runner failure, manual cancel). Use `(needs.X.result == 'success' || needs.X.result == 'skipped')`.
- **Artifact retention**: workflow artifacts are an intra-run handoff only - durable copies live on the GitHub release, not in workflow artifacts - so they must not survive the run and accumulate against the small account-wide artifact-storage quota. **Every workflow that can produce artifacts ends with a terminal `cleanup-artifacts` job** that deletes the run's artifacts via the REST API: `permissions: actions: write`, `needs` the artifact producers, an `if:` that **includes** `always()` (so a failed run still cleans up) plus any workflow-specific gate (e.g. `publish-release.yml` adds `&& needs.setup.outputs.publish == 'true'` to run only on real publishes), independent of any required status check so housekeeping never gates a merge, `continue-on-error: true` on the delete step so even an unexpected failure never reds the run, and tolerant of individual list/delete failures (warn and continue). This covers not just `actions/upload-artifact` but build-records that actions emit automatically (e.g. `docker/build-push-action`'s `.dockerbuild`). Both `publish-release.yml` and `test-pull-request.yml` carry one; add one to any new artifact-producing entry workflow. Set `retention-days: 1` on explicit uploads as a backstop.
Expand Down