.NET Workflow #425
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: .NET Workflow | |
| on: | |
| push: | |
| branches: [main, develop] | |
| paths-ignore: | |
| ["**.md", ".github/ISSUE_TEMPLATE/**", ".github/pull_request_template.md"] | |
| pull_request: | |
| paths-ignore: | |
| ["**.md", ".github/ISSUE_TEMPLATE/**", ".github/pull_request_template.md"] | |
| schedule: | |
| - cron: "0 23 * * *" # Daily at 11 PM UTC | |
| workflow_dispatch: # Allow manual triggers | |
| inputs: | |
| version-bump: | |
| description: 'Version bump type' | |
| required: false | |
| default: 'auto' | |
| type: choice | |
| options: | |
| - auto | |
| - patch | |
| - minor | |
| - major | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Default permissions | |
| permissions: | |
| contents: read | |
| env: | |
| DOTNET_VERSION: "10.0" # Only needed for actions/setup-dotnet | |
| jobs: | |
| build: | |
| name: Build, Test & Release | |
| runs-on: windows-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: write # For creating releases and committing metadata | |
| packages: write # For publishing packages | |
| outputs: | |
| version: ${{ steps.pipeline.outputs.version }} | |
| release_hash: ${{ steps.pipeline.outputs.release_hash }} | |
| should_release: ${{ steps.pipeline.outputs.should_release }} | |
| steps: | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v5 | |
| with: | |
| java-version: 17 | |
| distribution: "zulu" # Alternative distribution options are available. | |
| - name: Checkout Repository | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 # Full history for versioning | |
| fetch-tags: true | |
| lfs: true | |
| submodules: recursive | |
| persist-credentials: true | |
| - name: Setup .NET SDK ${{ env.DOTNET_VERSION }} | |
| uses: actions/setup-dotnet@v6 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }}.x | |
| # setup-dotnet caches the NuGet global packages folder and keys it on a hash of these | |
| # files. The csproj files alone are not enough: this repository uses central package | |
| # management, so Directory.Packages.props is where versions actually live, and global.json | |
| # pins the ktsu SDK versions, which are NuGet packages too. Without them a version bump in | |
| # either file reuses the previous key, and because a cache hit never re-saves, the newly | |
| # downloaded packages would never make it into the cache. | |
| cache: true | |
| cache-dependency-path: | | |
| **/*.csproj | |
| **/Directory.Packages.props | |
| **/global.json | |
| # Ensure NuGet packages directory exists for caching (prevents error when pipeline exits early) | |
| - name: Ensure NuGet cache directory exists | |
| run: New-Item -Path "$env:USERPROFILE\.nuget\packages" -ItemType Directory -Force | |
| shell: pwsh | |
| - name: Cache SonarQube Cloud packages | |
| if: ${{ env.SONAR_TOKEN != '' }} | |
| uses: actions/cache@v6 | |
| env: | |
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} | |
| with: | |
| path: ~\sonar\cache | |
| key: ${{ runner.os }}-sonar | |
| restore-keys: ${{ runner.os }}-sonar | |
| - name: Cache SonarQube Cloud scanner | |
| if: ${{ env.SONAR_TOKEN != '' }} | |
| id: cache-sonar-scanner | |
| uses: actions/cache@v6 | |
| env: | |
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} | |
| with: | |
| path: .\.sonar\scanner | |
| key: ${{ runner.os }}-sonar-scanner | |
| restore-keys: ${{ runner.os }}-sonar-scanner | |
| - name: Install SonarQube Cloud scanner | |
| if: ${{ env.SONAR_TOKEN != '' && steps.cache-sonar-scanner.outputs.cache-hit != 'true' }} | |
| env: | |
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} | |
| shell: pwsh | |
| run: | | |
| New-Item -Path .\.sonar\scanner -ItemType Directory | |
| dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner | |
| # Installed as a prebuilt package rather than cloned and compiled from source. Nothing is | |
| # compiled here, which matters for SonarQube: the scanner injects its Roslyn analyzers into | |
| # every project compiled between begin and end, so building KtsuBuild inside that window used | |
| # to report KtsuBuild's own code smells against this repository. That is a compiler diagnostic | |
| # rather than a reported issue, so sonar.exclusions could not suppress it, and the build had to | |
| # be ordered ahead of the window. A tool install has no such constraint. | |
| # | |
| # --tool-path rather than -g so the install is self-contained; the directory is added to PATH | |
| # for later steps in this job. Jobs do not share PATH, so each one installs the tool itself. | |
| - name: Install KtsuBuild | |
| shell: pwsh | |
| run: | | |
| dotnet tool install ktsu.KtsuBuild.Tool --tool-path "${{ runner.temp }}/ktsubuild" | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| "${{ runner.temp }}/ktsubuild" >> $env:GITHUB_PATH | |
| - name: Begin SonarQube | |
| if: ${{ env.SONAR_TOKEN != '' }} | |
| env: | |
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} | |
| shell: pwsh | |
| run: | | |
| # sonar.projectBaseDir is pinned to the workspace. Scanner for .NET v8 otherwise derives it | |
| # from the projects it sees, and anything compiled outside the workspace widens it to a | |
| # common ancestor, which pulls the runner's own checkouts under _actions and _temp into the | |
| # file scan. Pinning it means only this repository is ever in scope, so no exclusions are | |
| # needed for those directories. | |
| .\.sonar\scanner\dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" /o:"${{ github.repository_owner }}" /d:sonar.token="$env:SONAR_TOKEN" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.projectBaseDir="${{ github.workspace }}" /d:sonar.cs.vscoveragexml.reportsPaths="coverage/coverage.xml" /d:sonar.coverage.exclusions="**/*Test*.cs,**/*.Tests.cs,**/*.Tests/**/*,**/obj/**/*,**/*.dll,**/NativeExports.cs" /d:sonar.cs.vstest.reportsPaths="coverage/TestResults/**/*.trx" /d:sonar.exclusions="**/NativeExports.cs" | |
| - name: Run KtsuBuild CI Pipeline | |
| id: pipeline | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| NUGET_API_KEY: ${{ secrets.NUGET_KEY }} | |
| KTSU_PACKAGE_KEY: ${{ secrets.KTSU_PACKAGE_KEY }} | |
| EXPECTED_OWNER: ktsu-dev | |
| run: | | |
| # Run the CI pipeline | |
| $versionBump = "${{ github.event.inputs.version-bump }}" | |
| # Build arguments array - only add --version-bump if explicitly set (for backward compatibility during bootstrap) | |
| $args = @("ci", "--workspace", "${{ github.workspace }}", "--verbose") | |
| if (![string]::IsNullOrEmpty($versionBump) -and $versionBump -ne "auto") { | |
| $args += @("--version-bump", $versionBump) | |
| } | |
| & ktsubuild @args | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| # Set outputs for downstream jobs | |
| $version = (Get-Content "${{ github.workspace }}/VERSION.md" -Raw).Trim() | |
| "version=$version" >> $env:GITHUB_OUTPUT | |
| $releaseHash = git rev-parse HEAD | |
| "release_hash=$releaseHash" >> $env:GITHUB_OUTPUT | |
| # Compute should_release (same logic as BuildConfigurationProvider) | |
| $isMain = "${{ github.ref }}" -eq "refs/heads/main" | |
| $isTagged = [bool](git tag --points-at "${{ github.sha }}" 2>$null) | |
| $isFork = "${{ github.event.repository.fork }}" -eq "true" | |
| $isExpectedOwner = "${{ github.repository_owner }}" -eq "ktsu-dev" | |
| $isOfficial = (-not $isFork) -and $isExpectedOwner | |
| $shouldRelease = $isMain -and (-not $isTagged) -and $isOfficial | |
| "should_release=$($shouldRelease.ToString().ToLower())" >> $env:GITHUB_OUTPUT | |
| - name: End SonarQube | |
| if: env.SONAR_TOKEN != '' | |
| env: | |
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} | |
| shell: pwsh | |
| run: | | |
| .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="$env:SONAR_TOKEN" | |
| - name: Upload Coverage Report | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: coverage-report | |
| path: | | |
| ./coverage/* | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| winget: | |
| name: Update Winget Manifests | |
| needs: build | |
| if: needs.build.outputs.should_release == 'true' | |
| runs-on: windows-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout Release Commit | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ needs.build.outputs.release_hash }} | |
| fetch-depth: 0 # Full history for better auto-detection | |
| - name: Setup .NET SDK ${{ env.DOTNET_VERSION }} | |
| uses: actions/setup-dotnet@v6 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }}.x | |
| # PATH is not shared between jobs, so this job installs the tool for itself. | |
| - name: Install KtsuBuild | |
| shell: pwsh | |
| run: | | |
| dotnet tool install ktsu.KtsuBuild.Tool --tool-path "${{ runner.temp }}/ktsubuild" | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| "${{ runner.temp }}/ktsubuild" >> $env:GITHUB_PATH | |
| - name: Update Winget Manifests | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| ktsubuild winget generate --version "${{ needs.build.outputs.version }}" --workspace "${{ github.workspace }}" --verbose | |
| - name: Upload Updated Manifests | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: winget-manifests-${{ needs.build.outputs.version }} | |
| path: winget/*.yaml | |
| retention-days: 30 | |
| security: | |
| name: Security Scanning | |
| needs: build | |
| if: needs.build.outputs.should_release == 'true' | |
| runs-on: windows-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| id-token: write # For dependency submission | |
| contents: write # For dependency submission | |
| steps: | |
| - name: Checkout Release Commit | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ needs.build.outputs.release_hash }} | |
| # Pinned to a full commit SHA rather than a tag. Tags are mutable, so a third party action | |
| # referenced by tag can be repointed at different code after review. The trailing comment is | |
| # the convention dependabot reads, so it still offers upgrades and rewrites both parts. | |
| - name: Detect Dependencies | |
| uses: advanced-security/component-detection-dependency-submission-action@31f25a8de68ae5ce2ca274bc28546a78683c15ce # v0.1.4 |