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
13 changes: 12 additions & 1 deletion .github/actions/purview-build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ inputs:
description: .NET SDK used by the consuming repository.
required: false
default: 10.0.x
config-path:
description: >-
Explicit purview-build.json location (PURVIEW_BUILD_CONFIG). When omitted, the tool uses its
documented probe order starting at the repository root.
required: false

runs:
using: composite
Expand All @@ -38,6 +43,12 @@ runs:

- name: Run shared build
shell: bash
run: "${{ runner.temp }}/purview-build/purview-build"
run: |
# Only forward config-path when the caller provided it: an empty PURVIEW_BUILD_CONFIG
# would be an explicit-but-missing path, which the tool treats as an error.
if [ -n "${{ inputs.config-path }}" ]; then
export PURVIEW_BUILD_CONFIG="${{ inputs.config-path }}"
fi
"${{ runner.temp }}/purview-build/purview-build"
env:
GITHUB_TOKEN: ${{ github.token }}
14 changes: 14 additions & 0 deletions .github/workflows/purview-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,20 @@ on:
description: Comma-separated test project names/globs to run (Build__TestProjects).
required: false
type: string
config-path:
description: >-
Explicit purview-build.json location (PURVIEW_BUILD_CONFIG). When omitted, the tool uses
its documented probe order starting at the repository root.
required: false
type: string
secrets:
NUGET_APIKEY:
required: false
# Declared because consuming repositories historically store the key under this name and the
# job forwards it as NUGET_API_KEY. A secret referenced but not declared here resolves only
# under `secrets: inherit`; declaring it makes the contract explicit either way.
NUGET__APIKEY:
required: false

permissions:
contents: read
Expand Down Expand Up @@ -103,4 +114,7 @@ jobs:
if [ -n "${{ inputs.test-projects }}" ]; then
export Build__TestProjects="${{ inputs.test-projects }}"
fi
if [ -n "${{ inputs.config-path }}" ]; then
export PURVIEW_BUILD_CONFIG="${{ inputs.config-path }}"
fi
"${{ runner.temp }}/purview-build/purview-build"
125 changes: 93 additions & 32 deletions .github/workflows/purview-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ on:
required: false
default: main
type: string
config-path:
description: >-
Explicit purview-build.json location (PURVIEW_BUILD_CONFIG). When omitted, the tool uses
its documented probe order starting at the repository root.
required: false
type: string
eligibility-policy:
description: >-
Release eligibility policy to evaluate (Release__Eligibility__Policy), e.g.
TrunkReservesMinor. When omitted, the repository's own configuration decides.
required: false
type: string
release-channel:
description: Named release channel (Release__Channel), e.g. stable or preview.
required: false
type: string
version-source:
description: Where the version comes from (Version__Source). Currently PackageJson.
required: false
type: string
trusted-publishing:
description: Use NuGet Trusted Publishing (no API key) instead of an API key.
required: false
Expand Down Expand Up @@ -63,6 +83,11 @@ on:
secrets:
NUGET_APIKEY:
required: false
# Declared because consuming repositories historically store the key under this name and the
# job forwards it as NUGET_API_KEY. A secret referenced but not declared here resolves only
# under `secrets: inherit`; declaring it makes the contract explicit either way.
NUGET__APIKEY:
required: false

permissions:
contents: write
Expand All @@ -73,9 +98,24 @@ jobs:
name: Release
runs-on: ubuntu-latest
timeout-minutes: 30
env:
GITHUB_TOKEN: ${{ github.token }}
# Consumer repos historically store the key under NUGET__APIKEY; some use NUGET_APIKEY.
# The tool reads the plain process env vars NUGET_APIKEY / NUGET_API_KEY.
NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }}
NUGET_API_KEY: ${{ secrets.NUGET__APIKEY }}
Release__Mode: ${{ inputs.release-mode }}
Release__UploadArtifacts: ${{ inputs.upload-artifacts }}
NuGet__TrustedPublishing: ${{ inputs.trusted-publishing }}
Build__RunTests: ${{ inputs.run-tests }}
Build__RunLint: ${{ inputs.run-lint }}
Build__RunPack: ${{ inputs.run-pack }}
Build__ValidatePack: ${{ inputs.validate-pack }}
steps:
- uses: actions/checkout@v7
with:
# Eligibility is judged against the tags that already exist, so the full tag list
# has to be present before the tool evaluates anything.
fetch-depth: 0
fetch-tags: true

Expand All @@ -85,24 +125,29 @@ jobs:

- uses: oven-sh/setup-bun@v2

- name: Check for version bump
id: version
- name: Forward optional inputs
shell: bash
run: |
VERSION=$(bun -p "require('./package.json').version")
TAG="v$VERSION"
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Version $VERSION is already tagged as $TAG. Skipping release."
echo "should_release=false" >> "$GITHUB_OUTPUT"
else
echo "New version $VERSION detected. Releasing $TAG."
echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
# Only forward optional settings the caller actually provided. Setting any of these to
# the empty string would override the consuming repo's purview-build.json, because
# environment variables take precedence over the JSON config (see commit 4d72bf7).
if [ -n "${{ inputs.config-path }}" ]; then
echo "PURVIEW_BUILD_CONFIG=${{ inputs.config-path }}" >> "$GITHUB_ENV"
fi
if [ -n "${{ inputs.eligibility-policy }}" ]; then
echo "Release__Eligibility__Policy=${{ inputs.eligibility-policy }}" >> "$GITHUB_ENV"
fi
if [ -n "${{ inputs.release-channel }}" ]; then
echo "Release__Channel=${{ inputs.release-channel }}" >> "$GITHUB_ENV"
fi
if [ -n "${{ inputs.version-source }}" ]; then
echo "Version__Source=${{ inputs.version-source }}" >> "$GITHUB_ENV"
fi
if [ -n "${{ inputs.test-filter }}" ]; then
echo "Build__TestFilter=${{ inputs.test-filter }}" >> "$GITHUB_ENV"
fi

- name: Install shared build
if: steps.version.outputs.should_release == 'true'
shell: bash
run: |
VERSION_ARGS=""
Expand All @@ -111,26 +156,42 @@ jobs:
fi
dotnet tool install Purview.Build --tool-path "${{ runner.temp }}/purview-build" $VERSION_ARGS

- name: Evaluate release eligibility
id: eligibility
shell: bash
run: |
# The tool evaluates; this workflow decides. release-explain runs no module, mutates
# nothing, and always exits 0 — the decision is in its JSON.
"${{ runner.temp }}/purview-build/purview-build" release-explain --format=json > explain.json
cat explain.json

{
echo "verdict=$(jq -r '.verdict' explain.json)"
echo "release_mode=$(jq -r '.releaseMode' explain.json)"
echo "decided_by=$(jq -r '.decidedByRule // ""' explain.json)"
} >> "$GITHUB_OUTPUT"

jq -r '.message' explain.json > eligibility-message.txt

- name: Report ineligible release
if: steps.eligibility.outputs.verdict == 'Fail'
shell: bash
run: |
echo "::error title=Release not eligible::$(cat eligibility-message.txt)"
exit 1

- name: Skip release
if: steps.eligibility.outputs.verdict == 'Skip'
shell: bash
run: |
echo "::notice title=Release skipped::$(cat eligibility-message.txt)"

- name: Run release pipeline
if: steps.version.outputs.should_release == 'true'
if: steps.eligibility.outputs.verdict == 'Release'
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
# Consumer repos historically store the key under NUGET__APIKEY; some use NUGET_APIKEY.
# The tool reads the plain process env vars NUGET_APIKEY / NUGET_API_KEY.
NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }}
NUGET_API_KEY: ${{ secrets.NUGET__APIKEY }}
Release__Mode: ${{ inputs.release-mode }}
Release__UploadArtifacts: ${{ inputs.upload-artifacts }}
NuGet__TrustedPublishing: ${{ inputs.trusted-publishing }}
Build__RunTests: ${{ inputs.run-tests }}
Build__RunLint: ${{ inputs.run-lint }}
Build__RunPack: ${{ inputs.run-pack }}
Build__ValidatePack: ${{ inputs.validate-pack }}
# The evaluated decision, not the raw input: a verdict that is not "Release" resolves to
# None, so the publish and release modules skip even if this step were reached.
Release__Mode: ${{ steps.eligibility.outputs.release_mode }}
run: |
# Only forward the optional test filter when the caller provides it.
# Setting it to empty would override a consuming repo's purview-build.json
# (env vars take precedence over the JSON config) and silently disable the filter.
if [ -n "${{ inputs.test-filter }}" ]; then
export Build__TestFilter="${{ inputs.test-filter }}"
fi
"${{ runner.temp }}/purview-build/purview-build"
50 changes: 30 additions & 20 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,35 +31,22 @@ jobs:

- uses: oven-sh/setup-bun@v2

- name: Read and validate release version
# Irreducible: the tool cannot read its own version for `dotnet pack` before it has been
# packed. Eligibility is NOT decided here — that is the tool's job, below.
- name: Read release version
id: version
shell: bash
run: |
VERSION=$(bun -p "require('./package.json').version")
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then
echo "package.json version '$VERSION' is not SemVer." >&2
exit 1
fi
TAG="v$VERSION"
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Version $VERSION is already released as $TAG."
echo "release=false" >> "$GITHUB_OUTPUT"
else
echo "release=true" >> "$GITHUB_OUTPUT"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"

- name: Restore
if: steps.version.outputs.release == 'true'
run: dotnet restore src/Build.slnx

- name: Build gate
if: steps.version.outputs.release == 'true'
run: dotnet build src/Build.slnx --configuration Release --no-restore --warnaserror

- name: Pack tool from source
if: steps.version.outputs.release == 'true'
run: >-
dotnet pack src/Build.slnx
--configuration Release --no-build --output artifacts
Expand All @@ -68,7 +55,6 @@ jobs:
-p:PackageVersion=${{ steps.version.outputs.version }}

- name: Verify NuGet API key
if: steps.version.outputs.release == 'true'
env:
NUGET_APIKEY: ${{ secrets.NUGET__APIKEY }}
run: |
Expand All @@ -78,19 +64,43 @@ jobs:
fi

- name: Install packed tool
if: steps.version.outputs.release == 'true'
run: >-
dotnet tool install Purview.Build
--tool-path "$RUNNER_TEMP/purview-build"
--add-source artifacts
--version "${{ steps.version.outputs.version }}"

- name: Evaluate release eligibility
id: eligibility
env:
Release__Mode: NuGet
run: |
"$RUNNER_TEMP/purview-build/purview-build" release-explain --format=json > explain.json
cat explain.json

{
echo "verdict=$(jq -r '.verdict' explain.json)"
echo "release_mode=$(jq -r '.releaseMode' explain.json)"
} >> "$GITHUB_OUTPUT"

jq -r '.message' explain.json > eligibility-message.txt

- name: Report ineligible release
if: steps.eligibility.outputs.verdict == 'Fail'
run: |
echo "::error title=Release not eligible::$(cat eligibility-message.txt)"
exit 1

- name: Skip release
if: steps.eligibility.outputs.verdict == 'Skip'
run: echo "::notice title=Release skipped::$(cat eligibility-message.txt)"

- name: Run release pipeline (builds, publishes and tags itself)
if: steps.version.outputs.release == 'true'
if: steps.eligibility.outputs.verdict == 'Release'
env:
GITHUB_TOKEN: ${{ github.token }}
NUGET_APIKEY: ${{ secrets.NUGET__APIKEY }}
Release__Mode: NuGet
Release__Mode: ${{ steps.eligibility.outputs.release_mode }}
Release__UploadArtifacts: "true"
NuGet__FeedUrl: https://api.nuget.org/v3/index.json
Build__RunTests: "false"
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -662,3 +662,8 @@ BenchmarkDotNet.Artifacts/
!build/

.tools/

# Throwaway repositories created by the release/config scenario matrices
.scenario-runs/

!src/src/Build/Release
Loading
Loading