Release Workflow #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release Workflow | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Release version (e.g., 1.0.0 or v1.0.0)' | |
| required: true | |
| type: string | |
| publish-to-nuget: | |
| description: 'Publish to NuGet.org' | |
| required: false | |
| type: boolean | |
| default: true | |
| permissions: | |
| contents: write | |
| packages: write | |
| env: | |
| DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true | |
| DOTNET_NOLOGO: true | |
| DOTNET_CLI_TELEMETRY_OPTOUT: true | |
| jobs: | |
| validate-release: | |
| name: "Validate Release" | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should-publish: ${{ steps.validate.outputs.should-publish }} | |
| tag-name: ${{ steps.validate.outputs.tag-name }} | |
| version: ${{ steps.validate.outputs.version }} | |
| is-manual: ${{ steps.validate.outputs.is-manual }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Validate Release | |
| id: validate | |
| run: | | |
| if [ "${{ github.event_name }}" == "push" ] && [[ "${{ github.ref }}" == refs/tags/* ]]; then | |
| # Triggered by tag push | |
| TAG_NAME="${{ github.ref_name }}" | |
| VERSION="${TAG_NAME#v}" # Remove 'v' prefix if present | |
| SHOULD_PUBLISH="true" | |
| IS_MANUAL="false" | |
| echo "🏷️ Triggered by tag: $TAG_NAME" | |
| elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| # Triggered manually | |
| INPUT_VERSION="${{ inputs.version }}" | |
| # Ensure version has 'v' prefix for tag | |
| if [[ "$INPUT_VERSION" == v* ]]; then | |
| TAG_NAME="$INPUT_VERSION" | |
| VERSION="${INPUT_VERSION#v}" | |
| else | |
| TAG_NAME="v$INPUT_VERSION" | |
| VERSION="$INPUT_VERSION" | |
| fi | |
| SHOULD_PUBLISH="${{ inputs.publish-to-nuget }}" | |
| IS_MANUAL="true" | |
| echo "🖱️ Triggered manually with version: $VERSION" | |
| else | |
| echo "❌ Invalid trigger event: ${{ github.event_name }}" | |
| exit 1 | |
| fi | |
| # Validate version format (basic semver validation) | |
| if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then | |
| echo "❌ Invalid version format: $VERSION" | |
| echo "Expected format: X.Y.Z[-prerelease][+build]" | |
| exit 1 | |
| fi | |
| echo "🏷️ Release tag: $TAG_NAME" | |
| echo "🔢 Version: $VERSION" | |
| echo "🚀 Should publish to NuGet: $SHOULD_PUBLISH" | |
| echo "🖱️ Is manual trigger: $IS_MANUAL" | |
| echo "tag-name=$TAG_NAME" >> $GITHUB_OUTPUT | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "should-publish=$SHOULD_PUBLISH" >> $GITHUB_OUTPUT | |
| echo "is-manual=$IS_MANUAL" >> $GITHUB_OUTPUT | |
| check-tag-exists: | |
| needs: validate-release | |
| name: "Check Tag Exists" | |
| runs-on: ubuntu-latest | |
| if: needs.validate-release.outputs.is-manual == 'true' | |
| outputs: | |
| tag-exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check if tag exists | |
| id: check | |
| run: | | |
| TAG_NAME="${{ needs.validate-release.outputs.tag-name }}" | |
| if git rev-parse --verify "refs/tags/$TAG_NAME" >/dev/null 2>&1; then | |
| echo "⚠️ Tag $TAG_NAME already exists" | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "✅ Tag $TAG_NAME does not exist" | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| fi | |
| build: | |
| needs: [validate-release, check-tag-exists] | |
| name: "Build and Test (Release)" | |
| # Always build and test when validation succeeded, regardless of tag existence | |
| if: always() && needs.validate-release.result == 'success' | |
| strategy: | |
| fail-fast: true | |
| matrix: | |
| include: | |
| - dotnet: '8.0.x' | |
| dotnet-framework: 'net8.0' | |
| os: ubuntu-latest | |
| - dotnet: '8.0.x' | |
| dotnet-framework: 'net8.0' | |
| os: windows-latest | |
| - dotnet: '9.0.x' | |
| dotnet-framework: 'net9.0' | |
| os: ubuntu-latest | |
| - dotnet: '9.0.x' | |
| dotnet-framework: 'net9.0' | |
| os: windows-latest | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install GitVersion | |
| uses: gittools/actions/gitversion/setup@v4.0.1 | |
| with: | |
| versionSpec: '6.x' | |
| - name: Determine Version | |
| id: gitversion | |
| uses: gittools/actions/gitversion/execute@v4.0.1 | |
| with: | |
| overrideConfig: | | |
| next-version=${{ needs.validate-release.outputs.version }} | |
| - name: Setup .NET ${{ matrix.dotnet }} | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: ${{ matrix.dotnet }} | |
| - name: Restore dependencies | |
| run: dotnet restore -p:TargetFramework=${{ matrix.dotnet-framework }} | |
| - name: Build | |
| run: dotnet build --no-restore -c Release -f ${{ matrix.dotnet-framework }} | |
| env: | |
| GitVersion_SemVer: ${{ steps.gitversion.outputs.SemVer }} | |
| GitVersion_AssemblySemVer: ${{ steps.gitversion.outputs.AssemblySemVer }} | |
| GitVersion_AssemblySemFileVer: ${{ steps.gitversion.outputs.AssemblySemFileVer }} | |
| GitVersion_InformationalVersion: ${{ steps.gitversion.outputs.InformationalVersion }} | |
| GitVersion_NuGetVersion: ${{ steps.gitversion.outputs.NuGetVersion }} | |
| - name: Test | |
| run: dotnet test --no-build --verbosity normal -c Release -f ${{ matrix.dotnet-framework }} /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[*.XUnit]*" | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| files: | | |
| **/coverage.opencover.xml | |
| flags: ${{ matrix.dotnet-framework }},${{ matrix.os }} | |
| name: ${{ matrix.os }}-${{ matrix.dotnet-framework }} | |
| fail_ci_if_error: false | |
| verbose: true | |
| env: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| create-tag: | |
| needs: [validate-release, check-tag-exists, build] | |
| name: "Create Tag" | |
| runs-on: ubuntu-latest | |
| if: needs.validate-release.outputs.is-manual == 'true' && needs.check-tag-exists.outputs.tag-exists == 'false' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Create and push tag | |
| run: | | |
| TAG_NAME="${{ needs.validate-release.outputs.tag-name }}" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| echo "📝 Creating tag: $TAG_NAME" | |
| git tag -a "$TAG_NAME" -m "Release $TAG_NAME" | |
| echo "📤 Pushing tag: $TAG_NAME" | |
| git push origin "$TAG_NAME" | |
| echo "✅ Tag $TAG_NAME created and pushed successfully" | |
| publish: | |
| needs: [validate-release, build] | |
| name: "Publish Packages (Release)" | |
| if: always() && needs.build.result == 'success' | |
| uses: ./.github/workflows/publish.yml | |
| with: | |
| publish-to-nuget: ${{ needs.validate-release.outputs.should-publish == 'true' }} | |
| build-configuration: 'Release' | |
| summary: | |
| needs: [validate-release, publish, create-tag] | |
| runs-on: ubuntu-latest | |
| if: always() | |
| steps: | |
| - name: Release Summary | |
| run: | | |
| echo "## 🚀 Release Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY | |
| echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Tag | \`${{ needs.validate-release.outputs.tag-name }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Version | \`${{ needs.validate-release.outputs.version }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Trigger | ${{ needs.validate-release.outputs.is-manual == 'true' && '🖱️ Manual' || '🏷️ Tag Push' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| NuGet.org | ${{ needs.validate-release.outputs.should-publish == 'true' && '✅ Published' || '❌ Skipped' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| GitHub Packages | ✅ Published |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Tag Created | ${{ needs.create-tag.result == 'success' && '✅ Yes' || (needs.create-tag.result == 'skipped' && '⏭️ Skipped' || '❌ Failed') }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "📋 **Test Execution Notes**:" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Ubuntu**: All tests tests executed" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ needs.validate-release.outputs.is-manual }}" == "true" ]; then | |
| echo "🖱️ **Manual Release**: This release was triggered manually with version \`${{ needs.validate-release.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "🏷️ **Tag Release**: This release was triggered by pushing tag \`${{ needs.validate-release.outputs.tag-name }}\`" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "📦 **Published Packages**: Check the [packages page](https://github.com/${{ github.repository }}/pkgs) for the published packages." >> $GITHUB_STEP_SUMMARY |