Pin the toolchain and add a nightly .NET 11 preview lane #979
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: Pull Request | |
| on: | |
| pull_request_target: | |
| branches: [ master ] | |
| paths-ignore: | |
| - '**/*.md' | |
| - '.github/ISSUE_TEMPLATE/**' | |
| - '.github/FUNDING.yml' | |
| - 'LICENSE' | |
| - '.claude/**' | |
| permissions: | |
| contents: read | |
| id-token: write # Required for OIDC | |
| pull-requests: write # Required for the api-compat sticky comment | |
| jobs: | |
| approve: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Approve | |
| run: echo For security reasons, all pull requests need to be approved first before running any automated CI. | |
| build: | |
| runs-on: ubuntu-latest | |
| needs: [approve] | |
| environment: | |
| name: ${{ github.event.pull_request.head.repo.full_name != github.repository && 'Integrate Pull Request' || '' }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| fetch-depth: 2 | |
| - name: Setup .NET 10.0 | |
| uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | |
| with: | |
| dotnet-version: '10.0.x' | |
| - name: Set Cobalt packages token | |
| run: echo "COBALT_PACKAGES_TOKEN=${{ secrets.COBALT_PACKAGES_TOKEN }}" >> $GITHUB_ENV | |
| - name: Restore dependencies | |
| run: dotnet restore | |
| - name: Build | |
| run: dotnet build --no-restore /p:ContinuousIntegrationBuild=true | |
| - name: Install Playwright browsers (for sample-app smoke tests) | |
| # The build emits playwright.ps1 next to the SmokeTests assembly; run it with --with-deps | |
| # so Linux runners get the apt packages Chromium needs. Idempotent — caches in ~/.cache. | |
| run: pwsh artifacts/bin/WopiHost.SmokeTests/debug/playwright.ps1 install --with-deps chromium | |
| - name: Test | |
| # Emit JUnit XML alongside lcov coverage so Codecov Test Analytics can ingest run | |
| # times / failures. {assembly} keeps each test project's results in a distinct file. | |
| run: dotnet test --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=lcov --logger "junit;LogFilePath={assembly}.junit.xml" --results-directory ${{ github.workspace }}/test-results | |
| - name: Upload test results to Codecov | |
| # Runs even when the Test step failed — reporting the failed tests is the whole point. | |
| if: ${{ !cancelled() }} | |
| uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ${{ github.workspace }}/test-results/*.junit.xml | |
| - name: Upload coverage to Qlty | |
| uses: qltysh/qlty-action/coverage@08a0a862c159eae9b9003081da6663d96efef637 # v2 | |
| with: | |
| oidc: true | |
| files: "test/**/coverage.info" | |
| # See integrate.yml for the rationale. | |
| skip-missing-files: true | |
| - name: Codecov | |
| uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| - name: FOSSA scan + policy gate | |
| uses: fossa-contrib/fossa-action@d28f0c947e7406706981a62a91ecc009bcc39fd7 # v4 | |
| with: | |
| fossa-api-key: ${{ secrets.FOSSA_API_KEY }} | |
| skip-test: false | |
| filesystem-owner-cross-platform: | |
| # The main `build` job only runs on Linux, so the Windows (ACL) and macOS (stat) | |
| # branches of WopiFile.Owner — and the arch-dependent macOS `stat`/`stat$INODE64` | |
| # symbol selection — would otherwise never be exercised. This job runs the | |
| # dependency-free FileSystemProvider tests on the other OSes/architectures. | |
| name: FS owner lookup (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| needs: [approve] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # windows-latest: ACL owner path. | |
| # macos-latest: Apple Silicon (arm64) -> plain `stat` symbol. | |
| os: [windows-latest, macos-latest] | |
| environment: | |
| name: ${{ github.event.pull_request.head.repo.full_name != github.repository && 'Integrate Pull Request' || '' }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| fetch-depth: 1 | |
| - name: Setup .NET 10.0 | |
| uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | |
| with: | |
| dotnet-version: '10.0.x' | |
| - name: Set Cobalt packages token | |
| # Not needed by this test project (package-source mapping keeps the private feed | |
| # scoped to Microsoft.CobaltCore), but set it so any incidental restore stays quiet. | |
| shell: bash | |
| run: echo "COBALT_PACKAGES_TOKEN=${{ secrets.COBALT_PACKAGES_TOKEN }}" >> $GITHUB_ENV | |
| - name: Test (FileSystemProvider — exercises Owner lookup) | |
| run: dotnet test test/WopiHost.FileSystemProvider.Tests --verbosity normal | |
| api-compat: | |
| name: API Compatibility | |
| runs-on: ubuntu-latest | |
| needs: [approve] | |
| environment: | |
| name: ${{ github.event.pull_request.head.repo.full_name != github.repository && 'Integrate Pull Request' || '' }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| fetch-depth: 1 | |
| - name: Setup .NET 10.0 | |
| uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | |
| with: | |
| dotnet-version: '10.0.x' | |
| - name: Set Cobalt packages token | |
| run: echo "COBALT_PACKAGES_TOKEN=${{ secrets.COBALT_PACKAGES_TOKEN }}" >> $GITHUB_ENV | |
| - name: Pack PR nupkgs | |
| # Use a high version so AssemblyVersion sorts above any baseline release (avoids CP0003 noise). | |
| # PackageValidation is disabled here because ApiCompat runs directly below. | |
| run: | | |
| dotnet pack --configuration Release \ | |
| -p:ContinuousIntegrationBuild=true \ | |
| -p:Version=99.99.99 \ | |
| -p:EnablePackageValidation=false \ | |
| --output ./artifacts/new | |
| - name: Install Microsoft.DotNet.ApiCompat.Tool | |
| run: dotnet tool install --global Microsoft.DotNet.ApiCompat.Tool | |
| - name: Run ApiCompat against latest released packages | |
| id: apicompat | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p ./artifacts/baseline | |
| PACKAGES=( | |
| "WopiHost.Abstractions" | |
| "WopiHost.Core" | |
| "WopiHost.Discovery" | |
| "WopiHost.Url" | |
| "WopiHost.FileSystemProvider" | |
| "WopiHost.MemoryLockProvider" | |
| "WopiHost.AzureStorageProvider" | |
| "WopiHost.AzureLockProvider" | |
| ) | |
| REPORT_FILE="$GITHUB_WORKSPACE/apicompat-report.md" | |
| { | |
| echo "<!-- apicompat-report -->" | |
| echo "## API Compatibility Report" | |
| echo "" | |
| echo "Compared this PR's packed assemblies against the latest stable release on [NuGet.org](https://www.nuget.org/packages?q=WopiHost) for each library, honoring each project's \`CompatibilitySuppressions.xml\`. **This check fails on any public-API break not recorded as intentional** — already-suppressed breaks are masked, so what surfaces below is what *this PR* changes. If a break is deliberate, regenerate and commit the suppressions:" | |
| echo "" | |
| echo '```' | |
| echo "dotnet pack -c Release -p:ApiCompatGenerateSuppressionFile=true" | |
| echo '```' | |
| echo "" | |
| } > "$REPORT_FILE" | |
| ANY_DIFFS=0 | |
| for pkg in "${PACKAGES[@]}"; do | |
| pkg_lower="$(echo "$pkg" | tr '[:upper:]' '[:lower:]')" | |
| # Find the freshly-packed nupkg. | |
| new_nupkg="$(ls ./artifacts/new/${pkg}.99.99.99.nupkg 2>/dev/null || true)" | |
| if [[ -z "$new_nupkg" ]]; then | |
| echo "### :grey_question: ${pkg}" >> "$REPORT_FILE" | |
| echo "_No nupkg produced for this package — skipped._" >> "$REPORT_FILE" | |
| echo "" >> "$REPORT_FILE" | |
| continue | |
| fi | |
| # Resolve latest stable on NuGet.org (filter out prereleases by '-'). | |
| versions_json="$(curl -fsSL "https://api.nuget.org/v3-flatcontainer/${pkg_lower}/index.json" || echo '{"versions":[]}')" | |
| baseline_version="$(echo "$versions_json" | grep -oE '"[0-9][^"]+"' | tr -d '"' | grep -v -- '-' | tail -n 1 || true)" | |
| if [[ -z "$baseline_version" ]]; then | |
| echo "### :grey_question: ${pkg}" >> "$REPORT_FILE" | |
| echo "_No prior stable release on NuGet.org — skipped._" >> "$REPORT_FILE" | |
| echo "" >> "$REPORT_FILE" | |
| continue | |
| fi | |
| # Download baseline nupkg. | |
| baseline_nupkg="./artifacts/baseline/${pkg}.${baseline_version}.nupkg" | |
| curl -fsSL \ | |
| "https://api.nuget.org/v3-flatcontainer/${pkg_lower}/${baseline_version}/${pkg_lower}.${baseline_version}.nupkg" \ | |
| -o "$baseline_nupkg" | |
| # Feed the project's suppression file (default path; the same one the SDK's pack-time | |
| # validator reads at release) so acknowledged breaks are masked and only new ones show. | |
| # --permit-unnecessary-suppressions keeps a stale entry (one whose break is already in | |
| # the baseline, before the post-release cleanup runs) from failing the check. | |
| supp_args=() | |
| supp_file="src/${pkg}/CompatibilitySuppressions.xml" | |
| [[ -f "$supp_file" ]] && supp_args+=(--suppression-file "$supp_file") | |
| # Run apicompat. Note: <package> is positional, --baseline-package is named, --run-api-compat is required. | |
| # The tool installs as 'apicompat' (not 'dotnet apicompat') and exits 0 on both clean and break | |
| # cases; detect findings via output text. | |
| set +e | |
| output="$(apicompat package "$new_nupkg" \ | |
| --baseline-package "$baseline_nupkg" \ | |
| --run-api-compat \ | |
| --permit-unnecessary-suppressions \ | |
| "${supp_args[@]}" \ | |
| --verbosity Normal 2>&1)" | |
| set -e | |
| if grep -Eq '^(CP|PKV)[0-9]+:' <<< "$output"; then | |
| ANY_DIFFS=1 | |
| { | |
| echo "### :x: ${pkg} vs \`${baseline_version}\`" | |
| echo "<details open><summary>Unsuppressed public-API break(s) — revert or suppress</summary>" | |
| echo "" | |
| echo '```' | |
| echo "${output}" | |
| echo '```' | |
| echo "" | |
| echo "</details>" | |
| echo "" | |
| } >> "$REPORT_FILE" | |
| else | |
| { | |
| echo "### :white_check_mark: ${pkg} vs \`${baseline_version}\`" | |
| echo "No public API breaking changes detected." | |
| echo "" | |
| } >> "$REPORT_FILE" | |
| fi | |
| done | |
| { | |
| echo "---" | |
| if [[ $ANY_DIFFS -eq 0 ]]; then | |
| echo "_No unsuppressed public-API breaks across any package._" | |
| else | |
| echo "_If these breaks are intentional, run \`dotnet pack -c Release -p:ApiCompatGenerateSuppressionFile=true\` and commit the regenerated \`CompatibilitySuppressions.xml\`. \`CP\` IDs are assembly-level diffs; \`PKV\` are package-shape diffs ([docs](https://learn.microsoft.com/dotnet/fundamentals/apicompat/diagnostic-ids))._" | |
| fi | |
| } >> "$REPORT_FILE" | |
| # Surface in the job log too. | |
| cat "$REPORT_FILE" | |
| # Fail the check on any unsuppressed break (the report is posted by the next step, | |
| # which runs on always()). A deliberate break is acknowledged via its suppression file. | |
| if [[ $ANY_DIFFS -ne 0 ]]; then | |
| echo "::error::Unsuppressed public-API break(s) detected — see the API Compatibility Report comment." | |
| exit 1 | |
| fi | |
| - name: Post sticky PR comment | |
| if: always() | |
| uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3 | |
| with: | |
| header: apicompat-report | |
| path: apicompat-report.md |