Skip to content

Pin the toolchain and add a nightly .NET 11 preview lane #979

Pin the toolchain and add a nightly .NET 11 preview lane

Pin the toolchain and add a nightly .NET 11 preview lane #979

Workflow file for this run

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