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
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,8 +61,8 @@ and project-path inputs its targets need.

Pick by where each artifact *goes*, not by language:

- **Files attached to the GitHub Release** (zips, binaries, packaged libraries): a build-executable
or build-nuget hook per output, each uploading `release-asset-<branch>-<name>`. This is where the
- **Files attached to the GitHub Release** (zips, binaries, packaged libraries): a dotnet-publish
hook or a build-nuget hook per output, each uploading `release-asset-<branch>-<name>`. This is where the
.NET `dotnet publish` or `dotnet build` and package push lives. The hub default takes an explicit
project path, and a project needing different build behavior replaces the hook. A data-only
repo's own output (e.g. a symbol library) is not yet
Expand All@@ -72,7 +72,7 @@ Pick by where each artifact *goes*, not by language:
registry. NuGet pushes from inside the build-nuget hook (OIDC trusted publishing through
`NuGet/login`, no stored API key) *and* also uploads a `release-asset-*` (.7z) for the GitHub
release. PyPI is split: the build-pypi hook only builds and uploads the
`pypilibrary-build-<branch>` artifact, and the separate `publish-pypi` job in the caller's own
`pypi-build-<branch>` artifact, and the separate `publish-pypi` job in the caller's own
`publish-release.yml` does the OIDC Trusted-Publishing upload (`id-token: write` is granted only
at that one entry point), and PyPI contributes **no** `release-asset-*`.
- **Image-registry pushes** (Docker Hub): `build-docker-task.yml`, hub-hosted like
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/fleet-skills/.source-digest
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
79a65cdb22c0eb39
f0f6d839705b9d33
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,8 +61,8 @@ and project-path inputs its targets need.

Pick by where each artifact *goes*, not by language:

- **Files attached to the GitHub Release** (zips, binaries, packaged libraries): a build-executable
or build-nuget hook per output, each uploading `release-asset-<branch>-<name>`. This is where the
- **Files attached to the GitHub Release** (zips, binaries, packaged libraries): a dotnet-publish
hook or a build-nuget hook per output, each uploading `release-asset-<branch>-<name>`. This is where the
.NET `dotnet publish` or `dotnet build` and package push lives. The hub default takes an explicit
project path, and a project needing different build behavior replaces the hook. A data-only
repo's own output (e.g. a symbol library) is not yet
Expand All@@ -72,7 +72,7 @@ Pick by where each artifact *goes*, not by language:
registry. NuGet pushes from inside the build-nuget hook (OIDC trusted publishing through
`NuGet/login`, no stored API key) *and* also uploads a `release-asset-*` (.7z) for the GitHub
release. PyPI is split: the build-pypi hook only builds and uploads the
`pypilibrary-build-<branch>` artifact, and the separate `publish-pypi` job in the caller's own
`pypi-build-<branch>` artifact, and the separate `publish-pypi` job in the caller's own
`publish-release.yml` does the OIDC Trusted-Publishing upload (`id-token: write` is granted only
at that one entry point), and PyPI contributes **no** `release-asset-*`.
- **Image-registry pushes** (Docker Hub): `build-docker-task.yml`, hub-hosted like
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/dotnet-publish-default/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,7 +101,7 @@ runs:
if: ${{ inputs.smoke != 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-asset-${{ inputs.branch }}-executable
name: release-asset-${{ inputs.branch }}-dotnet-publish
path: ${{ steps.zip.outputs.asset-path }}
# Consumed within the run by the github-release job, so minimize artifact storage.
retention-days: 1
6 changes: 3 additions & 3 deletions .github/actions/nuget-push-default/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,14 +103,14 @@ runs:
- name: Zip output step
if: ${{ inputs.smoke != 'true' }}
shell: bash
run: 7z a -t7z "$RUNNER_TEMP/nugetlibrary.7z" "$RUNNER_TEMP"/publish/*
run: 7z a -t7z "$RUNNER_TEMP/nuget.7z" "$RUNNER_TEMP"/publish/*

# GitHub-release asset, uploaded under the release-asset-<branch>-<target> pattern the github-release job collects.
- name: Upload release asset step
if: ${{ inputs.smoke != 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-asset-${{ inputs.branch }}-nugetlibrary
path: ${{ runner.temp }}/nugetlibrary.7z
name: release-asset-${{ inputs.branch }}-nuget
path: ${{ runner.temp }}/nuget.7z
# Consumed within the run by the github-release job, so minimize artifact storage.
retention-days: 1
2 changes: 1 addition & 1 deletion .github/actions/pypi-build-default/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,7 +111,7 @@ runs:
if: ${{ inputs.smoke != 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pypilibrary-build-${{ inputs.branch }}
name: pypi-build-${{ inputs.branch }}
path: ${{ inputs.project-dir }}/dist/*
# Consumed within the run by the caller stub's publish-pypi job, so minimize artifact storage.
retention-days: 1
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,8 +61,8 @@ and project-path inputs its targets need.

Pick by where each artifact *goes*, not by language:

- **Files attached to the GitHub Release** (zips, binaries, packaged libraries): a build-executable
or build-nuget hook per output, each uploading `release-asset-<branch>-<name>`. This is where the
- **Files attached to the GitHub Release** (zips, binaries, packaged libraries): a dotnet-publish
hook or a build-nuget hook per output, each uploading `release-asset-<branch>-<name>`. This is where the
.NET `dotnet publish` or `dotnet build` and package push lives. The hub default takes an explicit
project path, and a project needing different build behavior replaces the hook. A data-only
repo's own output (e.g. a symbol library) is not yet
Expand All@@ -72,7 +72,7 @@ Pick by where each artifact *goes*, not by language:
registry. NuGet pushes from inside the build-nuget hook (OIDC trusted publishing through
`NuGet/login`, no stored API key) *and* also uploads a `release-asset-*` (.7z) for the GitHub
release. PyPI is split: the build-pypi hook only builds and uploads the
`pypilibrary-build-<branch>` artifact, and the separate `publish-pypi` job in the caller's own
`pypi-build-<branch>` artifact, and the separate `publish-pypi` job in the caller's own
`publish-release.yml` does the OIDC Trusted-Publishing upload (`id-token: write` is granted only
at that one entry point), and PyPI contributes **no** `release-asset-*`.
- **Image-registry pushes** (Docker Hub): `build-docker-task.yml`, hub-hosted like
Expand Down
32 changes: 16 additions & 16 deletions .github/workflows/build-release-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ on:
required: false
type: boolean
default: true
enable_executable:
enable_dotnet_publish:
required: false
type: boolean
default: true
Expand All@@ -84,12 +84,12 @@ on:
type: boolean
default: false
# Project paths forwarded to the hub default hooks.
executable_project:
dotnet_publish_project:
required: false
type: string
default: ''
# The release archive's name without .7z, derived from the project file's stem when empty.
executable_asset_name:
dotnet_publish_asset_name:
required: false
type: string
default: ''
Expand DownExpand Up@@ -158,9 +158,9 @@ jobs:
exit 1
fi

build-executable:
name: Build executable job
if: ${{ inputs.enable_executable }}
dotnet-publish:
name: Publish .NET project job
if: ${{ inputs.enable_dotnet_publish }}
needs: [get-version, validate-release]
runs-on: ubuntu-latest
steps:
Expand All@@ -172,20 +172,20 @@ jobs:
ref: ${{ needs.get-version.outputs.GitCommitId }}

# A caller's own hook names its archive itself, so the asset name input reaches only the hub default.
- name: Run caller build-executable hook step
if: ${{ hashFiles('.github/actions/build-executable/action.yml') != '' }}
uses: ./.github/actions/build-executable
- name: Run caller dotnet-publish hook step
if: ${{ hashFiles('.github/actions/dotnet-publish/action.yml') != '' }}
uses: ./.github/actions/dotnet-publish
with:
branch: ${{ inputs.branch }}
smoke: ${{ inputs.smoke }}
semver2: ${{ needs.get-version.outputs.SemVer2 }}
assembly-version: ${{ needs.get-version.outputs.AssemblyVersion }}
assembly-file-version: ${{ needs.get-version.outputs.AssemblyFileVersion }}
assembly-informational-version: ${{ needs.get-version.outputs.AssemblyInformationalVersion }}
project-file: ${{ inputs.executable_project }}
project-file: ${{ inputs.dotnet_publish_project }}

- name: Run hub build-executable default step
if: ${{ hashFiles('.github/actions/build-executable/action.yml') == '' }}
- name: Run hub dotnet-publish default step
if: ${{ hashFiles('.github/actions/dotnet-publish/action.yml') == '' }}
uses: $/.github/actions/dotnet-publish-default
with:
branch: ${{ inputs.branch }}
Expand All@@ -194,8 +194,8 @@ jobs:
assembly-version: ${{ needs.get-version.outputs.AssemblyVersion }}
assembly-file-version: ${{ needs.get-version.outputs.AssemblyFileVersion }}
assembly-informational-version: ${{ needs.get-version.outputs.AssemblyInformationalVersion }}
project-file: ${{ inputs.executable_project }}
asset-name: ${{ inputs.executable_asset_name }}
project-file: ${{ inputs.dotnet_publish_project }}
asset-name: ${{ inputs.dotnet_publish_asset_name }}

build-nuget:
name: Build NuGet library job
Expand DownExpand Up@@ -276,7 +276,7 @@ jobs:

build-docker:
name: Build Docker image job
needs: [get-version, validate-release, build-executable, build-nuget, build-pypi]
needs: [get-version, validate-release, dotnet-publish, build-nuget, build-pypi]
if: ${{ inputs.enable_docker && !failure() && !cancelled() }}
uses: $/.github/workflows/build-docker-task.yml
secrets:
Expand DownExpand Up@@ -304,7 +304,7 @@ jobs:
# The explicit functions replace that implicit success() instead, tolerating a skipped need while still failing closed on a real failure.
if: ${{ inputs.github && !inputs.smoke && !failure() && !cancelled() }}
runs-on: ubuntu-latest
needs: [get-version, validate-release, build-executable, build-nuget, build-pypi, build-docker]
needs: [get-version, validate-release, dotnet-publish, build-nuget, build-pypi, build-docker]
# The release upload and the artifact-delete cleanup both write with GITHUB_TOKEN, and the caller grants contents: write and actions: write when it sets github: true on a non-smoke run.
# A caller publishing only to a registry, or smoke building, leaves this job disabled and grants neither.
# No job-level permissions: block here, for the reason build-nuget gives: a block is validated against the caller's grant before if: runs, and a smoke caller holds a read-only token.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,6 @@ jobs:
enable_docker: false
enable_nuget: false
enable_pypi: false
enable_executable: false
enable_dotnet_publish: false
# This repo is source-only, so the release is just the tag + source zip + README + LICENSE.
expect_release_assets: false
2 changes: 1 addition & 1 deletion GOVERNANCE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -207,7 +207,7 @@ This section and [`WORKFLOW.md`](./WORKFLOW.md) keep the full rules, this sectio
- **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`'s `validate-release` job (branch-versus-prerelease, both directions) and `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, since `!= '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, with durable copies living on the GitHub release rather than in workflow artifacts, so they must not survive the run and accumulate against the small account-wide artifact-storage quota. **Clean up each transfer artifact surgically at its point of consumption**: the job that downloads it deletes it by exact name/pattern right after consuming it (the `github-release` job deletes `release-asset-<branch>-*` after attaching them to the release, and `publish-release.yml`'s `publish-pypi` deletes `pypilibrary-build-<branch>` after publishing). Deletion needs `actions: write` granted on that job, and for a reusable callee (e.g. `github-release` inside `build-release-task.yml`) the **caller** grants it (`publish-release.yml`'s `publish` job does). **Never blanket-delete the run's artifacts** (`gh api .../artifacts --jq '.artifacts[].id'`). That also destroys diagnostic/log artifacts and the build-records actions emit automatically (`docker/build-push-action`'s `.dockerbuild`), which are exactly what you need to debug a failed run. Set `retention-days: 1` on **every** explicit `upload-artifact`: it is the failure-path backstop, since a job that dies before its consumer runs leaves its artifact to be reaped within a day, so no separate terminal cleanup job is needed. A repo customizing these jobs must preserve the consume-then-delete shape.
- **Artifact retention**: workflow artifacts are an intra-run handoff only, with durable copies living on the GitHub release rather than in workflow artifacts, so they must not survive the run and accumulate against the small account-wide artifact-storage quota. **Clean up each transfer artifact surgically at its point of consumption**: the job that downloads it deletes it by exact name/pattern right after consuming it (the `github-release` job deletes `release-asset-<branch>-*` after attaching them to the release, and `publish-release.yml`'s `publish-pypi` deletes `pypi-build-<branch>` after publishing). Deletion needs `actions: write` granted on that job, and for a reusable callee (e.g. `github-release` inside `build-release-task.yml`) the **caller** grants it (`publish-release.yml`'s `publish` job does). **Never blanket-delete the run's artifacts** (`gh api .../artifacts --jq '.artifacts[].id'`). That also destroys diagnostic/log artifacts and the build-records actions emit automatically (`docker/build-push-action`'s `.dockerbuild`), which are exactly what you need to debug a failed run. Set `retention-days: 1` on **every** explicit `upload-artifact`: it is the failure-path backstop, since a job that dies before its consumer runs leaves its artifact to be reaped within a day, so no separate terminal cleanup job is needed. A repo customizing these jobs must preserve the consume-then-delete shape.
- **Docker layer cache**: cache to/from a registry tag (`type=registry`, e.g. `buildcache-<branch>` on Docker Hub), not the GitHub Actions cache (`type=gha`), to keep large image layers off the 10 GB Actions cache. A **multi-image** repo uses a **per-image** buildcache tag (`<repo>:buildcache-<branch>` for each image, plus the base image's own tag and inline cache). It does not fall back to `type=gha` for the extra images.
- **Tag pinning on releases**: when using `softprops/action-gh-release` (or any tag-creating action), pass `target_commitish` explicitly, because without it GitHub's REST API defaults the new tag to the repository's default branch instead of the commit that built the artifact. Pin it to the **exact built commit's SHA** (the publisher uses NBGV's `GitCommitId` output), not `github.sha` (which may differ from the exact commit NBGV versioned) and not a branch name (a moving ref that a mid-run commit could advance past the built tree).

Expand Down
Loading