Merge pull request #19 from AGIBuild/dependabot/github_actions/github… #97
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: CI and Release | |
| on: | |
| push: | |
| branches: [main] | |
| paths-ignore: | |
| - 'docs/**' | |
| - '*.md' | |
| - '.github/ISSUE_TEMPLATE/**' | |
| - '.github/PULL_REQUEST_TEMPLATE.md' | |
| - '.github/FUNDING.yml' | |
| - 'LICENSE' | |
| pull_request: | |
| branches: [main] | |
| paths-ignore: | |
| - 'docs/**' | |
| - '*.md' | |
| - '.github/ISSUE_TEMPLATE/**' | |
| - '.github/PULL_REQUEST_TEMPLATE.md' | |
| - '.github/FUNDING.yml' | |
| - 'LICENSE' | |
| workflow_dispatch: | |
| inputs: | |
| prerelease_suffix: | |
| description: 'Pre-release channel suffix (e.g. rc.1, beta.2). Leave empty for standard CI build.' | |
| required: false | |
| default: '' | |
| permissions: | |
| contents: read | |
| checks: write | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| DOTNET_NOLOGO: true | |
| DOTNET_CLI_TELEMETRY_OPTOUT: true | |
| NUKE_TELEMETRY_OPTOUT: true | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| ENABLE_NUGET: 'true' | |
| NUGET_USE_OIDC: 'false' | |
| ENABLE_INSTALLERS: 'true' | |
| ENABLE_ANDROID: 'false' | |
| ENABLE_IOS: 'false' | |
| jobs: | |
| resolve-version: | |
| name: Resolve Version | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| version: ${{ steps.resolve.outputs.version }} | |
| full_version: ${{ steps.resolve.outputs.full_version }} | |
| version_suffix: ${{ steps.resolve.outputs.version_suffix }} | |
| tag: ${{ steps.resolve.outputs.tag }} | |
| sha: ${{ steps.resolve.outputs.sha }} | |
| is_release: ${{ steps.resolve.outputs.is_release }} | |
| build_matrix: ${{ steps.matrix.outputs.build_matrix }} | |
| enable_nuget: ${{ steps.matrix.outputs.enable_nuget }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-tags: true | |
| - name: Resolve version and context | |
| id: resolve | |
| shell: bash | |
| env: | |
| PRERELEASE_SUFFIX: ${{ inputs.prerelease_suffix }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| GIT_REF: ${{ github.ref }} | |
| RUN_NUMBER: ${{ github.run_number }} | |
| run: | | |
| SEMVER_REGEX='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' | |
| VERSION=$(perl -0777 -ne 'if (m{<VersionPrefix>\s*([^<]+?)\s*</VersionPrefix>}s) { print $1 }' Directory.Build.props | head -n1 | xargs) | |
| if [ -z "$VERSION" ]; then | |
| echo "::error::VersionPrefix not found in Directory.Build.props" | |
| exit 1 | |
| fi | |
| if ! [[ "$VERSION" =~ $SEMVER_REGEX ]]; then | |
| echo "::error::Invalid VersionPrefix: $VERSION" | |
| exit 1 | |
| fi | |
| SHA="$(git rev-parse HEAD)" | |
| TAG="v$VERSION" | |
| IS_RELEASE="false" | |
| if [ "$EVENT_NAME" != "pull_request" ] && [ "$GIT_REF" = "refs/heads/main" ]; then | |
| git fetch --tags --force | |
| if git ls-remote --tags origin "refs/tags/$TAG" | grep -q "$TAG"; then | |
| echo "::notice::Tag $TAG already exists. Version not bumped — skipping release." | |
| else | |
| IS_RELEASE="true" | |
| fi | |
| fi | |
| VERSION_SUFFIX="" | |
| if [ -n "$PRERELEASE_SUFFIX" ]; then | |
| if ! [[ "$PRERELEASE_SUFFIX" =~ ^[0-9A-Za-z.-]+$ ]]; then | |
| echo "::error::Invalid prerelease_suffix: must match [0-9A-Za-z.-]+" | |
| exit 1 | |
| fi | |
| VERSION_SUFFIX="$PRERELEASE_SUFFIX" | |
| elif [ "$IS_RELEASE" != "true" ]; then | |
| VERSION_SUFFIX="ci.${RUN_NUMBER}" | |
| fi | |
| if [ -n "$VERSION_SUFFIX" ]; then | |
| FULL_VERSION="${VERSION}-${VERSION_SUFFIX}" | |
| else | |
| FULL_VERSION="${VERSION}" | |
| fi | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "full_version=$FULL_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "version_suffix=$VERSION_SUFFIX" >> "$GITHUB_OUTPUT" | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| echo "sha=$SHA" >> "$GITHUB_OUTPUT" | |
| echo "is_release=$IS_RELEASE" >> "$GITHUB_OUTPUT" | |
| echo "Resolved: version=$VERSION full_version=$FULL_VERSION tag=$TAG sha=$SHA is_release=$IS_RELEASE" | |
| - name: Version summary | |
| shell: bash | |
| run: | | |
| VERSION="${{ steps.resolve.outputs.version }}" | |
| FULL_VERSION="${{ steps.resolve.outputs.full_version }}" | |
| VERSION_SUFFIX="${{ steps.resolve.outputs.version_suffix }}" | |
| IS_RELEASE="${{ steps.resolve.outputs.is_release }}" | |
| SHA="${{ steps.resolve.outputs.sha }}" | |
| TAG="${{ steps.resolve.outputs.tag }}" | |
| IFS='.' read -r CUR_MAJOR CUR_MINOR CUR_PATCH <<< "$VERSION" | |
| PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") | |
| if [ -n "$PREV_TAG" ]; then | |
| PREV_VER="${PREV_TAG#v}" | |
| IFS='.' read -r PRE_MAJOR PRE_MINOR PRE_PATCH <<< "$PREV_VER" | |
| if [ "$CUR_MAJOR" != "$PRE_MAJOR" ]; then | |
| BUMP="MAJOR" | |
| elif [ "$CUR_MINOR" != "$PRE_MINOR" ]; then | |
| BUMP="MINOR" | |
| elif [ "$CUR_PATCH" != "$PRE_PATCH" ]; then | |
| BUMP="PATCH" | |
| else | |
| BUMP="NONE (same as $PREV_TAG)" | |
| fi | |
| else | |
| PREV_TAG="(none)" | |
| BUMP="INITIAL" | |
| fi | |
| MODE=$( [ "$IS_RELEASE" = "true" ] && echo "Release" || echo "CI" ) | |
| CHANNEL="stable" | |
| if [ -n "$VERSION_SUFFIX" ]; then | |
| CHANNEL="$VERSION_SUFFIX" | |
| fi | |
| REPO="${{ github.repository }}" | |
| OWNER="${REPO%%/*}" | |
| REPO_NAME="${REPO##*/}" | |
| DOCS_URL="https://${OWNER,,}.github.io/${REPO_NAME}/" | |
| { | |
| echo "## Version Info" | |
| echo "" | |
| echo "| Property | Value |" | |
| echo "|----------|-------|" | |
| echo "| **Version** | \`$VERSION\` |" | |
| echo "| **Full Version** | \`$FULL_VERSION\` |" | |
| echo "| **Channel** | \`$CHANNEL\` |" | |
| echo "| **Tag** | \`$TAG\` |" | |
| echo "| **Previous Tag** | \`$PREV_TAG\` |" | |
| echo "| **Bump Type** | **$BUMP** |" | |
| echo "| **Mode** | $MODE |" | |
| echo "| **Commit** | \`${SHA::8}\` |" | |
| echo "| **Expected Docs URL** | $DOCS_URL |" | |
| echo "| **Docs Auto-Deploy** | Enable Pages (GitHub Actions) + add \`docs/package.json\` |" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Compute build matrix | |
| id: matrix | |
| shell: bash | |
| run: | | |
| IS_RELEASE="${{ steps.resolve.outputs.is_release }}" | |
| PACK=$( [ "$ENABLE_NUGET" = "true" ] && echo "true" || echo "false" ) | |
| PUBLISH=$( [ "$ENABLE_INSTALLERS" = "true" ] && echo "true" || echo "false" ) | |
| echo "enable_nuget=$PACK" >> "$GITHUB_OUTPUT" | |
| if [ "$IS_RELEASE" = "true" ]; then | |
| ENTRIES="{\"os\":\"ubuntu-latest\",\"runtime\":\"linux-x64\",\"pack\":$PACK,\"publish\":$PUBLISH,\"workloads\":\"\"}" | |
| ENTRIES+=",{\"os\":\"windows-latest\",\"runtime\":\"win-x64\",\"pack\":false,\"publish\":$PUBLISH,\"workloads\":\"\"}" | |
| ENTRIES+=",{\"os\":\"macos-latest\",\"runtime\":\"osx-arm64\",\"pack\":false,\"publish\":$PUBLISH,\"workloads\":\"\"}" | |
| if [ "$ENABLE_ANDROID" = "true" ]; then | |
| ENTRIES+=',{"os":"ubuntu-latest","runtime":"android","pack":false,"publish":false,"workloads":"android"}' | |
| fi | |
| if [ "$ENABLE_IOS" = "true" ]; then | |
| ENTRIES+=',{"os":"macos-latest","runtime":"ios","pack":false,"publish":false,"workloads":"ios"}' | |
| fi | |
| else | |
| ENTRIES="{\"os\":\"ubuntu-latest\",\"runtime\":\"linux-x64\",\"pack\":$PACK,\"publish\":false,\"workloads\":\"\"}" | |
| ENTRIES+=",{\"os\":\"windows-latest\",\"runtime\":\"win-x64\",\"pack\":false,\"publish\":false,\"workloads\":\"\"}" | |
| ENTRIES+=",{\"os\":\"macos-latest\",\"runtime\":\"osx-arm64\",\"pack\":false,\"publish\":false,\"workloads\":\"\"}" | |
| if [ "$ENABLE_ANDROID" = "true" ]; then | |
| ENTRIES+=',{"os":"ubuntu-latest","runtime":"android","pack":false,"publish":false,"workloads":"android"}' | |
| fi | |
| if [ "$ENABLE_IOS" = "true" ]; then | |
| ENTRIES+=',{"os":"macos-latest","runtime":"ios","pack":false,"publish":false,"workloads":"ios"}' | |
| fi | |
| fi | |
| MATRIX="{\"include\":[$ENTRIES]}" | |
| echo "build_matrix=$MATRIX" >> "$GITHUB_OUTPUT" | |
| echo "Matrix: $MATRIX" | |
| build-and-test: | |
| name: Build and Test (${{ matrix.runtime }}) | |
| needs: resolve-version | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 30 | |
| env: | |
| BuildNumber: ${{ github.event_name != 'pull_request' && github.run_number || '' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJSON(needs.resolve-version.outputs.build_matrix) }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ needs.resolve-version.outputs.sha }} | |
| - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 | |
| with: | |
| global-json-file: global.json | |
| - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 | |
| with: | |
| path: ~/.nuget/packages | |
| key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json', '.config/dotnet-tools.json') }} | |
| restore-keys: ${{ runner.os }}-nuget- | |
| - name: Install .NET workloads | |
| if: matrix.workloads != '' | |
| run: dotnet workload install ${{ matrix.workloads }} | |
| - name: Mark build script executable | |
| if: runner.os != 'Windows' | |
| run: chmod +x build.sh | |
| - name: Check code format | |
| if: matrix.runtime == 'linux-x64' | |
| shell: bash | |
| run: ./build.sh Format | |
| - name: Build and test | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: ./build.sh Test --VersionSuffix "${{ needs.resolve-version.outputs.version_suffix }}" | |
| - name: Build and test (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: ./build.ps1 Test --VersionSuffix "${{ needs.resolve-version.outputs.version_suffix }}" | |
| - name: Generate coverage report | |
| if: matrix.runtime == 'linux-x64' | |
| shell: bash | |
| run: ./build.sh CoverageReport --skip Test | |
| - name: Upload coverage report | |
| if: matrix.runtime == 'linux-x64' | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: coverage-report | |
| path: artifacts/test-results/coverage-report/** | |
| if-no-files-found: warn | |
| - name: Pack | |
| if: matrix.pack | |
| shell: bash | |
| run: ./build.sh Pack --skip Build Test | |
| - name: Generate release manifest | |
| if: needs.resolve-version.outputs.is_release == 'true' && matrix.pack | |
| shell: bash | |
| run: ./build.sh GenerateReleaseManifest --skip Build Test Pack | |
| - name: Validate package versions | |
| if: needs.resolve-version.outputs.is_release == 'true' && matrix.pack | |
| shell: bash | |
| run: ./build.sh ValidatePackageVersions --skip Build Test Pack | |
| - name: Publish app | |
| if: needs.resolve-version.outputs.is_release == 'true' && matrix.publish && runner.os != 'Windows' | |
| shell: bash | |
| run: ./build.sh Publish --Runtime "${{ matrix.runtime }}" | |
| - name: Publish app (Windows) | |
| if: needs.resolve-version.outputs.is_release == 'true' && matrix.publish && runner.os == 'Windows' | |
| shell: pwsh | |
| run: ./build.ps1 Publish --Runtime "${{ matrix.runtime }}" | |
| - name: Package app | |
| if: needs.resolve-version.outputs.is_release == 'true' && matrix.publish && runner.os != 'Windows' | |
| shell: bash | |
| run: ./build.sh PackageApp --Runtime "${{ matrix.runtime }}" --skip Publish | |
| - name: Package app (Windows) | |
| if: needs.resolve-version.outputs.is_release == 'true' && matrix.publish && runner.os == 'Windows' | |
| shell: pwsh | |
| run: ./build.ps1 PackageApp --Runtime "${{ matrix.runtime }}" --skip Publish | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: test-results-${{ matrix.runtime }} | |
| path: artifacts/test-results/** | |
| if-no-files-found: warn | |
| - name: Report test results | |
| if: always() && !cancelled() | |
| uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0 | |
| with: | |
| name: Tests (${{ matrix.runtime }}) | |
| path: artifacts/test-results/**/*.trx | |
| reporter: dotnet-trx | |
| - name: Upload packages | |
| if: needs.resolve-version.outputs.is_release == 'true' && matrix.pack | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: packages | |
| path: artifacts/packages/** | |
| if-no-files-found: error | |
| - name: Upload installer artifacts | |
| if: needs.resolve-version.outputs.is_release == 'true' && matrix.publish | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: installer-${{ matrix.runtime }} | |
| path: artifacts/installers/** | |
| if-no-files-found: error | |
| build-docs: | |
| name: Build Documentation | |
| needs: resolve-version | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| outputs: | |
| has_docs: ${{ steps.check.outputs.has_docs }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ needs.resolve-version.outputs.sha }} | |
| - name: Check for documentation source | |
| id: check | |
| shell: bash | |
| run: | | |
| if [ -f "docs/package.json" ]; then | |
| if [ ! -f "docs/package-lock.json" ]; then | |
| echo "::error::docs/package.json exists but docs/package-lock.json is missing. Run 'npm install' in docs/ and commit the lock file." | |
| exit 1 | |
| fi | |
| echo "has_docs=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::notice::No docs/package.json found. Skipping documentation build." | |
| echo "has_docs=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 | |
| if: steps.check.outputs.has_docs == 'true' | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: docs/package-lock.json | |
| - name: Install dependencies | |
| if: steps.check.outputs.has_docs == 'true' | |
| run: npm ci | |
| working-directory: docs | |
| - name: Build documentation | |
| if: steps.check.outputs.has_docs == 'true' | |
| run: npx vitepress build | |
| working-directory: docs | |
| - name: Upload docs artifact | |
| if: steps.check.outputs.has_docs == 'true' | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: docs-site | |
| path: docs/.vitepress/dist | |
| if-no-files-found: error | |
| release-gate: | |
| name: Release Gate | |
| needs: [resolve-version, build-and-test] | |
| if: needs.resolve-version.outputs.is_release == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| environment: release | |
| permissions: | |
| actions: read | |
| contents: read | |
| steps: | |
| - name: Log environment and reviewer status | |
| id: release-status | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| REPO="${{ github.repository }}" | |
| RUN_ID="${{ github.run_id }}" | |
| ENVIRONMENT_NAME="release" | |
| RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| APPROVALS_JSON="$(gh api "repos/$REPO/actions/runs/$RUN_ID/approvals" 2>/dev/null || echo "[]")" | |
| PENDING_JSON="$(gh api "repos/$REPO/actions/runs/$RUN_ID/pending_deployments" 2>/dev/null || echo "[]")" | |
| APPROVAL_JSON="$(echo "$APPROVALS_JSON" | jq --arg env "$ENVIRONMENT_NAME" '[.[] | select(any(.environments[]?; .name == $env))] | first // {}')" | |
| APPROVER="$(echo "$APPROVAL_JSON" | jq -r '.user.login // "N/A"')" | |
| APPROVAL_STATE="$(echo "$APPROVAL_JSON" | jq -r '.state // "N/A"')" | |
| APPROVED_AT="$(echo "$APPROVAL_JSON" | jq -r '.updated_at // "N/A"')" | |
| APPROVAL_COMMENT="$(echo "$APPROVAL_JSON" | jq -r '.comment // "N/A"')" | |
| APPROVAL_COMMENT="${APPROVAL_COMMENT//$'\n'/ }" | |
| APPROVAL_COMMENT="${APPROVAL_COMMENT//|/\\|}" | |
| PENDING_COUNT="$(echo "$PENDING_JSON" | jq 'length')" | |
| mkdir -p artifacts/release | |
| jq -n \ | |
| --arg event "release_environment_status" \ | |
| --arg timestamp "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ | |
| --arg run_id "$RUN_ID" \ | |
| --arg run_url "$RUN_URL" \ | |
| --arg env "$ENVIRONMENT_NAME" \ | |
| --arg approval_state "$APPROVAL_STATE" \ | |
| --arg approver "$APPROVER" \ | |
| --arg approval_comment "$APPROVAL_COMMENT" \ | |
| --arg approved_at "$APPROVED_AT" \ | |
| --arg tag "${{ needs.resolve-version.outputs.tag }}" \ | |
| --arg sha "${{ needs.resolve-version.outputs.sha }}" \ | |
| --argjson pending_count "$PENDING_COUNT" \ | |
| '{ | |
| event: $event, | |
| timestamp: $timestamp, | |
| run_id: $run_id, | |
| run_url: $run_url, | |
| environment: $env, | |
| approval: { | |
| state: $approval_state, | |
| reviewer: $approver, | |
| comment: $approval_comment, | |
| approved_at: $approved_at | |
| }, | |
| pending_deployments_count: $pending_count, | |
| context: { | |
| tag: $tag, | |
| sha: $sha | |
| } | |
| }' > artifacts/release/release-status.json | |
| echo "::group::Release Environment Status" | |
| cat artifacts/release/release-status.json | |
| echo "::endgroup::" | |
| { | |
| echo "## Environment and Reviewer Status" | |
| echo "" | |
| echo "| Property | Value |" | |
| echo "|----------|-------|" | |
| echo "| **Environment** | \`$ENVIRONMENT_NAME\` |" | |
| echo "| **Approval State** | $APPROVAL_STATE |" | |
| echo "| **Approved By** | @$APPROVER |" | |
| echo "| **Approved At** | $APPROVED_AT |" | |
| echo "| **Approval Comment** | $APPROVAL_COMMENT |" | |
| echo "| **Pending Deployments** | $PENDING_COUNT |" | |
| echo "| **Run** | [#$RUN_ID]($RUN_URL) |" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload release status artifact | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: release-status | |
| path: artifacts/release/release-status.json | |
| if-no-files-found: error | |
| release-nuget: | |
| name: Release NuGet | |
| needs: [resolve-version, build-and-test, release-gate] | |
| if: needs.resolve-version.outputs.is_release == 'true' && needs.resolve-version.outputs.enable_nuget == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ needs.resolve-version.outputs.sha }} | |
| - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 | |
| with: | |
| global-json-file: global.json | |
| - name: Download packages | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: packages | |
| path: artifacts/packages | |
| - name: Mark build script executable | |
| run: chmod +x build.sh | |
| - name: Verify release manifest | |
| shell: bash | |
| run: ./build.sh ValidateReleaseManifest | |
| - name: NuGet login (Trusted Publishing / OIDC) | |
| if: env.NUGET_USE_OIDC == 'true' | |
| id: nuget-login | |
| uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544 # v1.1.0 | |
| with: | |
| user: ${{ vars.NUGET_USER || github.repository_owner }} | |
| - name: Resolve NuGet API key | |
| shell: bash | |
| env: | |
| OIDC_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }} | |
| SECRET_API_KEY: ${{ secrets.NUGET_API_KEY }} | |
| run: | | |
| set -euo pipefail | |
| API_KEY="$SECRET_API_KEY" | |
| if [ "$NUGET_USE_OIDC" = "true" ]; then | |
| API_KEY="$OIDC_API_KEY" | |
| fi | |
| if [ -z "$API_KEY" ]; then | |
| echo "::error::NUGET_API_KEY is not available. Set the secret or enable OIDC." | |
| exit 1 | |
| fi | |
| echo "::add-mask::$API_KEY" | |
| echo "NUGET_API_KEY=$API_KEY" >> "$GITHUB_ENV" | |
| - name: Push packages to NuGet.org | |
| shell: bash | |
| run: ./build.sh PushNuGetPackages | |
| create-release: | |
| name: Create Release | |
| needs: [resolve-version, release-gate, release-nuget] | |
| if: needs.resolve-version.outputs.is_release == 'true' && (needs.release-nuget.result == 'success' || needs.release-nuget.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: release-tag-${{ needs.resolve-version.outputs.tag }} | |
| cancel-in-progress: false | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ needs.resolve-version.outputs.sha }} | |
| fetch-depth: 0 | |
| - name: Download installers | |
| if: env.ENABLE_INSTALLERS == 'true' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: artifacts/installers | |
| pattern: installer-* | |
| merge-multiple: true | |
| - name: Create and push tag (idempotent) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TAG="${{ needs.resolve-version.outputs.tag }}" | |
| SHA="${{ needs.resolve-version.outputs.sha }}" | |
| git fetch --tags --force | |
| LOCAL_SHA="$(git rev-parse -q --verify "refs/tags/$TAG^{commit}" 2>/dev/null || true)" | |
| if [ -n "$LOCAL_SHA" ]; then | |
| if [ "$LOCAL_SHA" = "$SHA" ]; then | |
| echo "::notice::Tag $TAG already exists locally at expected SHA $SHA." | |
| exit 0 | |
| fi | |
| echo "::error::Tag $TAG exists locally at $LOCAL_SHA but expected $SHA." | |
| exit 1 | |
| fi | |
| REMOTE_SHA="$(git ls-remote --tags origin "refs/tags/$TAG^{}" | awk '{print $1}' | head -n1)" | |
| if [ -z "$REMOTE_SHA" ]; then | |
| REMOTE_SHA="$(git ls-remote --tags origin "refs/tags/$TAG" | awk '{print $1}' | head -n1)" | |
| fi | |
| if [ -n "$REMOTE_SHA" ]; then | |
| if [ "$REMOTE_SHA" = "$SHA" ]; then | |
| echo "::notice::Tag $TAG already exists on remote at expected SHA $SHA." | |
| exit 0 | |
| fi | |
| echo "::error::Tag $TAG exists on remote at $REMOTE_SHA but expected $SHA." | |
| exit 1 | |
| fi | |
| git tag "$TAG" "$SHA" | |
| git push origin "$TAG" | |
| echo "Created and pushed tag $TAG at $SHA" | |
| - name: Create or update GitHub Release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| TAG="${{ needs.resolve-version.outputs.tag }}" | |
| RELEASE_NAME="dotnet.CI.template $TAG" | |
| shopt -s nullglob | |
| RELEASE_ASSETS=() | |
| if [ "$ENABLE_INSTALLERS" = "true" ]; then | |
| RELEASE_ASSETS+=(artifacts/installers/*.zip) | |
| fi | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| echo "::notice::Release already exists for $TAG. Uploading missing assets." | |
| if [ ${#RELEASE_ASSETS[@]} -gt 0 ]; then | |
| gh release upload "$TAG" "${RELEASE_ASSETS[@]}" --clobber | |
| fi | |
| exit 0 | |
| fi | |
| if [ ${#RELEASE_ASSETS[@]} -gt 0 ]; then | |
| gh release create "$TAG" "${RELEASE_ASSETS[@]}" \ | |
| --title "$RELEASE_NAME" \ | |
| --generate-notes | |
| else | |
| gh release create "$TAG" \ | |
| --title "$RELEASE_NAME" \ | |
| --generate-notes | |
| fi | |
| echo "Created release $TAG with ${#RELEASE_ASSETS[@]} assets" | |
| deploy-docs: | |
| name: Deploy Documentation | |
| needs: [resolve-version, create-release, build-docs] | |
| if: needs.resolve-version.outputs.is_release == 'true' && needs.build-docs.outputs.has_docs == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| concurrency: | |
| group: pages | |
| cancel-in-progress: false | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| steps: | |
| - name: Check if deployment is possible | |
| id: check | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| HAS_PAGES="$(gh api repos/${{ github.repository }} --jq '.has_pages' 2>/dev/null || echo "false")" | |
| if [ "$HAS_PAGES" != "true" ]; then | |
| echo "::notice::GitHub Pages is not enabled. Enable Settings > Pages > Source: GitHub Actions." | |
| echo "should_deploy=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "should_deploy=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Download docs artifact | |
| if: steps.check.outputs.should_deploy == 'true' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: docs-site | |
| path: docs-site | |
| - name: Upload pages artifact | |
| if: steps.check.outputs.should_deploy == 'true' | |
| uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 | |
| with: | |
| path: docs-site | |
| - name: Deploy to GitHub Pages | |
| if: steps.check.outputs.should_deploy == 'true' | |
| id: deployment | |
| uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 |