Skip to content

Release

Release #74

Workflow file for this run

name: 'Release'
on:
workflow_dispatch:
push:
branches:
- 'preview/**'
- 'release/**'
paths:
- 'src/**'
permissions:
actions: read
pages: write
id-token: write
contents: write
concurrency:
group: release-${{ github.head_ref || github.ref }}
cancel-in-progress: false
env:
dotnet-sdk-version: '10.x'
build-configuration: 'Release'
build-platform: 'Any CPU'
git-version: '6.0.x'
test-result-directory: 'test-results'
nuget-packages-directory: 'nuget-packages'
jobs:
workflow-variables:
name: 'Set workflow variables'
runs-on: ubuntu-latest
outputs:
is-release: ${{ startsWith(github.ref_name, 'release') }}
is-preview: ${{ startsWith(github.ref_name, 'preview') }}
steps:
- name: 'Set workflow variables'
id: github
run: |
echo "is-release:${{ startsWith(github.ref_name, 'release') }}"
echo "is-preview:${{ startsWith(github.ref_name, 'preview') }}"
validate-release:
name: 'Validate release'
needs: [workflow-variables]
runs-on: ubuntu-latest
steps:
- name: 'Validate release branch'
if: ${{ needs.workflow-variables.outputs.is-release != 'true' && needs.workflow-variables.outputs.is-preview != 'true' }}
run: |
echo "This workflow can only be run on 'release/**' or 'preview/**' branches."
exit 1
versioning:
name: 'Extract version from branch'
runs-on: ubuntu-latest
needs: [workflow-variables, validate-release]
outputs:
friendly-version: ${{ steps.format-version.outputs.friendly-version }}
assembly-version: ${{ steps.format-version.outputs.assembly-version }}
assembly-informational-version: ${{ steps.format-version.outputs.assembly-informational-version }}
file-version: ${{ steps.format-version.outputs.file-version }}
release-version: ${{ steps.format-version.outputs.release-version }}
steps:
- name: 'Checkout ${{ github.head_ref || github.ref }}'
uses: actions/checkout@v6
- name: 'Setup .NET ${{ env.dotnet-sdk-version }}'
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ env.dotnet-sdk-version }}
- name: 'Extract version from branch name'
id: extract-version
uses: './.github/actions/versioning/extract-version'
with:
branch-name: ${{ github.ref_name }}
- name: 'Create build number'
shell: bash
id: create-build-number
run: |
git fetch --unshallow --filter=tree:0
build_number=$(git rev-list --count origin/${{ github.ref_name }} ^origin/main)
echo "build-number=$build_number" >> $GITHUB_OUTPUT
- name: 'Create pre-release tag'
shell: bash
id: create-pre-release-tag
env:
build-number: ${{ steps.create-build-number.outputs.build-number }}
run: |
if [[ '${{ needs.workflow-variables.outputs.is-release }}' == 'true' ]]; then
echo "pre-release-tag=" >> $GITHUB_OUTPUT
elif [[ '${{ needs.workflow-variables.outputs.is-preview }}' == 'true' ]]; then
pre_release_tag='preview'
echo "pre-release-tag=$pre_release_tag" >> $GITHUB_OUTPUT
else
pre_release_tag=$(echo ${{ github.ref_name }} | tr '/' '-' | tr '.' '-'| tr '_' '-')
echo "pre-release-tag=$pre_release_tag" >> $GITHUB_OUTPUT
fi
- name: 'Format version'
id: format-version
uses: ./.github/actions/versioning/format-version
with:
version: ${{ steps.extract-version.outputs.version }}
patch: ${{ github.run_number }}
build-number: ${{ steps.create-build-number.outputs.build-number }}
sha: ${{ github.sha }}
pre-release-tag: ${{ steps.create-pre-release-tag.outputs.pre-release-tag }}
build:
name: 'Compile source code'
needs: [workflow-variables, versioning, validate-release]
runs-on: ubuntu-latest
env:
assembly-version: ${{ needs.versioning.outputs.assembly-version }}
assembly-informational-version: ${{ needs.versioning.outputs.assembly-informational-version }}
file-version: ${{ needs.versioning.outputs.file-version }}
steps:
- name: 'Checkout ${{ github.head_ref || github.ref }}'
uses: actions/checkout@v6
- name: 'Compile source code'
uses: ./.github/actions/source/compile
with:
project-path: '**/PolylineAlgorithm.csproj'
assembly-version: ${{ env.assembly-version }}
assembly-informational-version: ${{ env.assembly-informational-version }}
file-version: ${{ env.file-version }}
treat-warnings-as-error: ${{ needs.workflow-variables.outputs.is-release }}
test:
name: 'Run tests'
needs: [build, validate-release]
runs-on: ubuntu-latest
steps:
- name: 'Checkout ${{ github.head_ref || github.ref }}'
uses: actions/checkout@v6
- name: 'Setup .NET'
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ env.dotnet-sdk-version }}
- name: 'Run tests'
uses: ./.github/actions/testing/test
with:
project-path: './tests/PolylineAlgorithm.Tests/PolylineAlgorithm.Tests.csproj'
test-results-directory: '${{ runner.temp }}/${{ env.test-result-directory }}/'
code-coverage-settings-file: '${{ github.workspace}}/code-coverage-settings.xml'
- name: 'Generate test report'
uses: ./.github/actions/testing/test-report
id: test-report
with:
test-result-folder: '${{ runner.temp }}/${{ env.test-result-directory }}/'
- name: Write test report summary
run: cat ${{ steps.test-report.outputs.test-report-file }} >> $GITHUB_STEP_SUMMARY
- name: 'Generate code coverage'
uses: ./.github/actions/testing/code-coverage
id: code-coverage-report
with:
test-result-folder: '${{ runner.temp }}/${{ env.test-result-directory }}/'
- name: Write code coverage report summary
run: cat ${{ steps.code-coverage-report.outputs.code-coverage-report-file }} >> $GITHUB_STEP_SUMMARY
update-api-unshipped:
name: 'Update PublicAPI.Unshipped.txt'
needs: [workflow-variables, validate-release]
# Run even when build fails — the build may fail solely because a dropped preview API is still
# listed in Unshipped.txt (RS0017). This job fixes exactly that, so it must not be gated on build.
if: ${{ needs.workflow-variables.outputs.is-preview == 'true' && needs.validate-release.result == 'success' }}
runs-on: ubuntu-latest
steps:
- name: 'Checkout ${{ github.ref }}'
uses: actions/checkout@v6
- name: 'Setup .NET'
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ env.dotnet-sdk-version }}
- name: 'Snapshot PublicAPI.Shipped.txt'
shell: bash
run: cp src/PolylineAlgorithm/PublicAPI.Shipped.txt src/PolylineAlgorithm/PublicAPI.Shipped.txt.bak
- name: 'Sync PublicAPI.Unshipped.txt (add new + remove dropped preview APIs)'
shell: bash
run: dotnet format analyzers src/PolylineAlgorithm/PolylineAlgorithm.csproj --diagnostics RS0016 RS0017
- name: 'Guard against accidental shipped API removal'
shell: bash
run: |
SHIPPED="src/PolylineAlgorithm/PublicAPI.Shipped.txt"
if ! diff -q "$SHIPPED" "$SHIPPED.bak" > /dev/null 2>&1; then
echo "::error::Breaking change detected — a shipped API was removed from PublicAPI.Shipped.txt."
echo "::error::This requires a major version bump. Reverting the unintended change."
cp "$SHIPPED.bak" "$SHIPPED"
exit 1
fi
rm -f "$SHIPPED.bak"
- name: 'Configure git identity'
uses: './.github/actions/git/configure-identity'
- name: 'Commit and push updated API files'
shell: bash
run: |
git add src/PolylineAlgorithm/PublicAPI.Unshipped.txt
git diff --staged --quiet || (
git commit -m "Sync PublicAPI.Unshipped.txt (add new, remove dropped preview APIs)" &&
git pull --rebase origin ${{ github.ref_name }} &&
git push
)
promote-api-files:
name: 'Promote PublicAPI files (Unshipped -> Shipped)'
needs: [workflow-variables, test, validate-release]
if: ${{ needs.workflow-variables.outputs.is-release == 'true' }}
runs-on: ubuntu-latest
steps:
- name: 'Checkout ${{ github.ref }}'
uses: actions/checkout@v6
- name: 'Promote Unshipped.txt into Shipped.txt'
shell: bash
run: |
UNSHIPPED="src/PolylineAlgorithm/PublicAPI.Unshipped.txt"
SHIPPED="src/PolylineAlgorithm/PublicAPI.Shipped.txt"
# Append every non-blank, non-header line from Unshipped into Shipped
tail -n +2 "$UNSHIPPED" | grep -v '^[[:space:]]*$' >> "$SHIPPED" || true
# Reset Unshipped to just the nullable-enable header (with BOM to match convention)
printf '\xef\xbb\xbf#nullable enable\n' > "$UNSHIPPED"
- name: 'Configure git identity'
uses: './.github/actions/git/configure-identity'
- name: 'Commit and push promoted API files'
shell: bash
run: |
git add src/PolylineAlgorithm/PublicAPI.Shipped.txt src/PolylineAlgorithm/PublicAPI.Unshipped.txt
git diff --staged --quiet || (
git commit -m "Promote PublicAPI.Unshipped.txt into PublicAPI.Shipped.txt for release" &&
git pull --rebase origin ${{ github.ref_name }} &&
git push
)
pack:
name: 'Package binaries'
needs: [versioning, build, test, validate-release]
runs-on: ubuntu-latest
env:
assembly-version: ${{ needs.versioning.outputs.assembly-version }}
assembly-informational-version: ${{ needs.versioning.outputs.assembly-informational-version }}
file-version: ${{ needs.versioning.outputs.file-version }}
release-version: ${{ needs.versioning.outputs.release-version }}
package-artifact-name: package
outputs:
package-artifact-name: ${{ env.package-artifact-name }}
steps:
- name: 'Checkout ${{ github.base_ref }}'
uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ env.dotnet-sdk-version }}
- name: Download Build
uses: actions/download-artifact@v8
with:
name: build
- name: Pack with .NET
run: |
dotnet pack ${{ vars.SRC_DEFAULT_GLOB_PATTERN }} --configuration ${{ env.build-configuration }} /p:Platform="${{ env.build-platform }}" /p:PackageVersion=${{ env.release-version }} /p:Version=${{ env.assembly-version }} /p:AssemblyInformationalVersion=${{ env.assembly-informational-version }} /p:FileVersion=${{ env.file-version }} --output ${{ runner.temp }}/${{ env.nuget-packages-directory }}
- name: Upload Package
uses: actions/upload-artifact@v7
with:
name: ${{ env.package-artifact-name }}
path: |
${{ runner.temp }}/${{ env.nuget-packages-directory }}/**/*.nupkg
${{ runner.temp }}/${{ env.nuget-packages-directory }}/**/*.snupkg
benchmark:
name: Benchmark with .NET CLI on ${{ matrix.os }}
needs: [workflow-variables, build, validate-release]
if: ${{ needs.workflow-variables.outputs.is-release == 'true' || vars.BENCHMARKDOTNET_RUN_OVERRIDE == 'true' }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: 'Checkout ${{ github.head_ref || github.ref }}'
uses: actions/checkout@v6
- name: Install .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.x
10.x
- name: Download Build
uses: actions/download-artifact@v8
with:
name: build
- name: Benchmark
working-directory: ${{ vars.BENCHMARKDOTNET_WORKING_DIRECTORY }}
run: dotnet run --configuration ${{ env.build-configuration }} /p:Platform=${{ env.build-platform }} --framework ${{ vars.DEFAULT_BUILD_FRAMEWORK }} --runtimes ${{ vars.BENCHMARKDOTNET_RUNTIMES }} --filter ${{ vars.BENCHMARKDOTNET_FILTER }} --artifacts ${{ runner.temp }}/benchmarks/ --exporters GitHub --memory --iterationTime 100 --join
- name: Upload Benchmark Results
uses: actions/upload-artifact@v7
with:
name: benchmark-${{ matrix.os }}
path: |
${{ runner.temp }}/benchmarks/**/*-report-github.md
- name: Write Benchmark Summary
shell: bash
run: cat **/*-report-github.md > $GITHUB_STEP_SUMMARY
working-directory: ${{ runner.temp }}/benchmarks/
publish-documentation:
name: 'Publish documentation'
needs: [pack, benchmark, validate-release, workflow-variables]
if: ${{ needs.workflow-variables.outputs.is-release == 'true' }}
uses: ./.github/workflows/publish-documentation.yml
permissions:
actions: read
pages: write
id-token: write
publish-package:
name: 'Publish package'
needs: [pack, validate-release, publish-documentation, update-api-unshipped, promote-api-files]
if: |
always() &&
needs.pack.result == 'success' &&
needs.validate-release.result == 'success' &&
(needs.publish-documentation.result == 'success' || needs.publish-documentation.result == 'skipped') &&
(needs.promote-api-files.result == 'success' || needs.promote-api-files.result == 'skipped') &&
(needs.update-api-unshipped.result == 'success' || needs.update-api-unshipped.result == 'skipped')
env:
package-artifact-name: ${{ needs.pack.outputs.package-artifact-name }}
runs-on: ubuntu-latest
environment: 'NuGet'
steps:
- name: 'Checkout ${{ github.head_ref || github.ref }}'
uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ env.dotnet-sdk-version }}
- name: 'Publish package to Azure Artifact feed'
uses: ./.github/actions/nuget/publish-package
with:
package-artifact-name: ${{ env.package-artifact-name }}
nuget-feed-url: ${{ vars.NUGET_PACKAGE_FEED_URL }}
nuget-feed-api-key: ${{ secrets.NUGET_PACKAGE_FEED_API_KEY }}
nuget-feed-server: 'NuGet'
working-directory: ${{ runner.temp }}/${{ env.nuget-packages-directory }}
dotnet-sdk-version: ${{ env.dotnet-sdk-version }}'
release:
name: 'Create release'
needs: [workflow-variables, publish-package, validate-release, versioning]
runs-on: ubuntu-latest
env:
release-version: ${{ needs.versioning.outputs.release-version }}
is-preview: ${{ needs.workflow-variables.outputs.is-preview }}
notes-start-tag: ${{ needs.workflow-variables.outputs.notes-start-tag }}
steps:
- name: 'Checkout ${{ github.head_ref || github.ref }}'
uses: actions/checkout@v6
with:
fetch-tags: true
- name: 'Determine notes start tag'
id: determine-notes-start-tag
run: |
notes_start_tag=$(git describe --abbrev=0 --tags 2>/dev/null || echo "")
echo "notes-start-tag=$notes_start_tag" >> $GITHUB_OUTPUT
shell: bash
- name: 'Create GitHub Release'
uses: ./.github/actions/github/create-release
with:
release-version: ${{ env.release-version }}
is-preview: ${{ env.is-preview }}
notes-start-tag: ${{ steps.determine-notes-start-tag.outputs.notes-start-tag }}
update-changelog:
name: 'Update CHANGELOG.md'
needs: [workflow-variables, release, versioning]
if: ${{ needs.workflow-variables.outputs.is-release == 'true' }}
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
release-version: ${{ needs.versioning.outputs.release-version }}
steps:
- name: 'Checkout ${{ github.ref }}'
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
- name: 'Fetch release notes'
shell: bash
run: |
gh release view ${{ env.release-version }} --json body --jq '.body' > /tmp/release-notes.txt
- name: 'Prepend entry to CHANGELOG.md'
shell: bash
run: |
release_date=$(date -u +%Y-%m-%d)
{
echo "## ${{ env.release-version }} — ${release_date}"
echo ""
cat /tmp/release-notes.txt
echo ""
} > /tmp/new-entry.txt
awk '
/<!-- New entries are prepended automatically by the release workflow. -->/ {
print
print ""
while ((getline line < "/tmp/new-entry.txt") > 0) print line
close("/tmp/new-entry.txt")
next
}
{ print }
' CHANGELOG.md > /tmp/changelog-new.md
mv /tmp/changelog-new.md CHANGELOG.md
- name: 'Configure git identity'
uses: './.github/actions/git/configure-identity'
- name: 'Commit and push CHANGELOG.md'
shell: bash
run: |
git add CHANGELOG.md
git diff --staged --quiet || (
git commit -m "Update CHANGELOG.md for ${{ env.release-version }}" &&
git pull --rebase origin ${{ github.ref_name }} &&
git push
)
- name: 'Write changelog summary'
shell: bash
run: |
echo "✅ CHANGELOG.md updated for **${{ env.release-version }}**." >> $GITHUB_STEP_SUMMARY
merge-to-main:
name: 'Merge ${{ github.ref_name }} into main'
needs: [workflow-variables, release, versioning, update-changelog]
if: ${{ needs.workflow-variables.outputs.is-release == 'true' }}
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
env:
GH_TOKEN: ${{ github.token }}
current-branch: ${{ github.ref_name }}
current-version: ${{ needs.versioning.outputs.friendly-version }}
steps:
- name: 'Checkout ${{ github.head_ref || github.ref }}'
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: 'Detect if current branch is the latest release'
id: detect-latest
run: |
git fetch origin
latest_version=$(git ls-remote --heads origin 'release/*' | grep -oP 'release/\K\d+\.\d+' | sort -V | tail -1)
current_version=$(echo "${{ env.current-version }}" | grep -oP '^\d+\.\d+')
echo "Latest release branch version: $latest_version"
echo "Current version (normalized): $current_version"
if [[ "$latest_version" == "$current_version" ]]; then
echo "is-latest=true" >> $GITHUB_OUTPUT
else
echo "is-latest=false" >> $GITHUB_OUTPUT
fi
- name: 'Check if PR to main already exists'
id: check-pr
if: ${{ steps.detect-latest.outputs.is-latest == 'true' }}
run: |
pr_count=$(gh pr list --head "${{ env.current-branch }}" --base main --state open --limit 1 --json id --jq '. | length')
if [[ $pr_count -gt 0 ]]; then
echo "pr-exists=true" >> $GITHUB_OUTPUT
else
echo "pr-exists=false" >> $GITHUB_OUTPUT
fi
- name: 'Create PR: Merge ${{ env.current-branch }} into main'
if: ${{ steps.detect-latest.outputs.is-latest == 'true' && steps.check-pr.outputs.pr-exists == 'false' }}
run: |
gh pr create \
--title "Merge ${{ env.current-branch }} into main" \
--fill \
--base main \
--head "${{ env.current-branch }}"
- name: 'Write merge summary'
run: |
if [[ "${{ steps.detect-latest.outputs.is-latest }}" == "true" ]]; then
echo "✅ PR created to merge **${{ env.current-branch }}** into **main**." >> $GITHUB_STEP_SUMMARY
else
echo "⏭️ Skipped merge to main: **${{ env.current-branch }}** is not the highest release branch." >> $GITHUB_STEP_SUMMARY
fi
create-support-branch:
name: 'Create support branch for ${{ github.ref_name }}'
needs: [workflow-variables, release, versioning]
if: ${{ needs.workflow-variables.outputs.is-release == 'true' }}
runs-on: ubuntu-latest
env:
current-version: ${{ needs.versioning.outputs.friendly-version }}
steps:
- name: 'Checkout ${{ github.head_ref || github.ref }}'
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: 'Resolve support branch name'
id: resolve-support-branch
run: |
major_minor=$(echo "${{ env.current-version }}" | grep -oP '^\d+\.\d+')
echo "support-branch=support/$major_minor" >> $GITHUB_OUTPUT
- name: 'Check if support branch already exists'
id: check-support-branch
run: |
git fetch origin
if git ls-remote --exit-code --heads origin "${{ steps.resolve-support-branch.outputs.support-branch }}" > /dev/null 2>&1; then
echo "support-branch-exists=true" >> $GITHUB_OUTPUT
else
echo "support-branch-exists=false" >> $GITHUB_OUTPUT
fi
- name: 'Configure git identity'
if: ${{ steps.check-support-branch.outputs.support-branch-exists == 'false' }}
uses: './.github/actions/git/configure-identity'
- name: 'Create support branch'
if: ${{ steps.check-support-branch.outputs.support-branch-exists == 'false' }}
run: |
git checkout -b "${{ steps.resolve-support-branch.outputs.support-branch }}"
git push --set-upstream origin "${{ steps.resolve-support-branch.outputs.support-branch }}"
- name: 'Write support branch summary'
run: |
if [[ "${{ steps.check-support-branch.outputs.support-branch-exists }}" == "false" ]]; then
echo "✅ Created support branch **${{ steps.resolve-support-branch.outputs.support-branch }}**." >> $GITHUB_STEP_SUMMARY
else
echo "⏭️ Support branch **${{ steps.resolve-support-branch.outputs.support-branch }}** already exists." >> $GITHUB_STEP_SUMMARY
fi