Skip to content

Commit 2c03a17

Browse files
Add CI gates: PR build, nightly published-package tests, validated MAUI bump (#117)
* Add CI gates: PR build, nightly published-package tests, validated MAUI bump The plugin wraps third-party native bindings (Xamarin.GooglePlayServices.Ads.Lite, Jc.GMA.iOS, Jc.UMP.iOS, Xamarin.AndroidX.*.Ktx) that drift independently of this repo, and the package version tracks MAUI. Until now nothing built on PR, and the nightly MAUI bump opened PRs with zero validation — so "the world changed under a shipped release" (issues #68, #82) was caught only when a user hit it. Two tiers, both able to target either the source or the published NuGet: - tests/Plugin.AdMob.PackageConsumer — MAUI library referencing the PUBLISHED package across all 4 TFMs, touching the public API surface so a released version that drops or renames anything fails the build. - tests/Plugin.AdMob.DeviceTests — MAUI app that loads every ad format against Google's test ad units and asserts OnAdLoaded fires, logging results for CI to scrape. Workflows: - build.yml — source build gate on every PR/push, 4-TFM matrix. Hard-fails on NU1605 (downgrade); surfaces NU1608 to the summary rather than failing, since the binding graph already emits 8 such conflicts today. - device-tests.yml — reusable; Android emulator (gating) + iOS simulator (advisory). - nightly.yml — 04:00 UTC, resolves the latest published version and runs both tiers against it. - bump-maui.yml — now validates before publishing: the candidate goes to a throwaway bump-maui-validate branch, is built and device-tested, and only then is the reviewed branch fast-forwarded and the PR opened/updated. Gating model: on a headless software-GPU emulator (all GitHub-hosted runners) only the banner format reliably fills; full-screen and native creatives need -gpu host to pre-render. So SUMMARY_BANNER is the hard gate and SUMMARY_ALL is enforced only when require_all_formats is set, for a GPU-capable self-hosted runner. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Fix CI: install workloads explicitly and narrow restore to one TFM per leg `dotnet workload restore <project>` has to evaluate the project before it can decide what to install, but on a clean runner the platform TFMs aren't known yet — so a MAUI app degrades to plain net10.0 and fails on the ProjectReference to the plugin: error : Project '../../src/Plugin.AdMob/Plugin.AdMob.csproj' targets 'net10.0-android;net10.0-ios;net10.0-maccatalyst'. It cannot be referenced by a project that targets '.NETCoreApp,Version=v10.0'. It only passed locally because the workloads were already installed there. Install each leg's workload explicitly instead, and build with -p:TargetFrameworks=<tfm> rather than -f <tfm>. `-f` leaves restore cross-targeting, which would demand every platform's workload on every leg (and was the NETSDK1147 failure the device-test jobs would otherwise have hit); setting it as a global property narrows restore and propagates through the ProjectReference, so the plugin is restored for that TFM alone. Verified locally that the narrowing reaches the referenced project: restoring the sample with -p:TargetFrameworks=net10.0-android leaves src/Plugin.AdMob with only net10.0-android in project.assets.json. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent cc6604f commit 2c03a17

24 files changed

Lines changed: 1240 additions & 19 deletions

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Shell scripts run on Linux CI runners — CRLF would break the shebang.
2+
*.sh text eol=lf

.github/workflows/build.yml

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
name: Build
2+
3+
# Source-build gate. Restores + compiles the plugin and the sample app across every
4+
# platform it ships, on every PR and push to main. Hard-fails on NU1605 (package
5+
# downgrade) and surfaces NU1608 (native binding constraint drift, the issue #68 / #82
6+
# class) to the run summary. Also callable (workflow_call) so the MAUI auto-bump can
7+
# validate its own change before opening a PR.
8+
9+
on:
10+
pull_request:
11+
push:
12+
branches: [ main ]
13+
workflow_dispatch:
14+
workflow_call:
15+
inputs:
16+
ref:
17+
description: Git ref to check out and build.
18+
required: false
19+
type: string
20+
21+
permissions:
22+
contents: read
23+
24+
jobs:
25+
build:
26+
name: ${{ matrix.tfm }}
27+
runs-on: ${{ matrix.os }}
28+
strategy:
29+
fail-fast: false
30+
matrix:
31+
include:
32+
# Android builds the sample APP, so the manifest merge (issue #68) is exercised,
33+
# not just the library — and an Android app build needs no code signing.
34+
- os: ubuntu-latest
35+
tfm: net10.0-android
36+
workload: maui-android
37+
project: samples/Foo.Bar.SampleApp/Foo.Bar.SampleApp.csproj
38+
# iOS / MacCatalyst / Windows compile the plugin library (same as release.yml). App-level
39+
# iOS link is covered advisorily by device-tests.yml; keeping it out of the hard gate
40+
# avoids iOS app signing/link flaking the gate for non-code reasons.
41+
- os: macos-latest
42+
tfm: net10.0-ios
43+
workload: maui-ios
44+
project: src/Plugin.AdMob/Plugin.AdMob.csproj
45+
- os: macos-latest
46+
tfm: net10.0-maccatalyst
47+
workload: maui-maccatalyst
48+
project: src/Plugin.AdMob/Plugin.AdMob.csproj
49+
- os: windows-latest
50+
tfm: net10.0-windows10.0.19041.0
51+
workload: maui-windows
52+
project: src/Plugin.AdMob/Plugin.AdMob.csproj
53+
54+
steps:
55+
- uses: actions/checkout@v4
56+
with:
57+
ref: ${{ inputs.ref || github.ref }}
58+
59+
- uses: actions/setup-dotnet@v4
60+
with:
61+
dotnet-version: 10.0.x
62+
63+
# Install the leg's workload explicitly rather than `dotnet workload restore <project>`:
64+
# that has to evaluate the project first, and on a clean runner the platform TFMs aren't
65+
# known yet, so a MAUI app degrades to plain net10.0 and can't reference the
66+
# platform-targeted plugin ("It cannot be referenced by a project that targets
67+
# '.NETCoreApp,Version=v10.0'").
68+
- name: Install MAUI workload
69+
run: dotnet workload install ${{ matrix.workload }}
70+
71+
# -p:TargetFrameworks (not -f): `-f` still leaves restore cross-targeting, which would
72+
# demand every platform's workload on every leg. Setting the property as a global
73+
# property narrows restore, and propagates through the ProjectReference so the plugin
74+
# is restored for this TFM only.
75+
- name: Build (${{ matrix.tfm }})
76+
shell: pwsh
77+
run: |
78+
dotnet build ${{ matrix.project }} -p:TargetFrameworks=${{ matrix.tfm }} -warnaserror:NU1605 --nologo | Tee-Object -FilePath build.log
79+
exit $LASTEXITCODE
80+
81+
- name: Surface NU1608 native binding drift
82+
if: always()
83+
shell: pwsh
84+
run: |
85+
if (Test-Path build.log) {
86+
$nu = Select-String -Path build.log -Pattern 'warning NU1608' |
87+
ForEach-Object { ($_.Line -replace '.*warning NU1608', 'NU1608').Trim() } |
88+
Sort-Object -Unique
89+
if ($nu) {
90+
"### NU1608 dependency-constraint conflicts — ${{ matrix.tfm }}" | Out-File $env:GITHUB_STEP_SUMMARY -Append
91+
"These are transitive native-binding version skews. New entries (vs. previous runs) indicate upstream drift worth investigating." | Out-File $env:GITHUB_STEP_SUMMARY -Append
92+
'```' | Out-File $env:GITHUB_STEP_SUMMARY -Append
93+
$nu | Out-File $env:GITHUB_STEP_SUMMARY -Append
94+
'```' | Out-File $env:GITHUB_STEP_SUMMARY -Append
95+
}
96+
}

.github/workflows/bump-maui.yml

Lines changed: 129 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
name: Bump MAUI
22

3+
# Nightly check for a newer MAUI patch. Unlike before, the bump is now VALIDATED before a PR
4+
# is opened: the change is pushed to a branch, built across all target frameworks, and run
5+
# through the device tests. The PR is only opened when the build gate passes, so a MAUI patch
6+
# that fails to restore/compile against the native bindings never reaches review as a green PR.
7+
38
on:
49
schedule:
510
- cron: '0 6 * * *'
@@ -12,13 +17,16 @@ permissions:
1217
concurrency:
1318
group: bump-maui
1419

15-
defaults:
16-
run:
17-
shell: bash
18-
1920
jobs:
20-
bump:
21+
check:
2122
runs-on: ubuntu-latest
23+
outputs:
24+
bump: ${{ steps.check.outputs.bump }}
25+
current: ${{ steps.check.outputs.current }}
26+
latest: ${{ steps.check.outputs.latest }}
27+
defaults:
28+
run:
29+
shell: bash
2230
steps:
2331
- uses: actions/checkout@v4
2432

@@ -44,10 +52,23 @@ jobs:
4452
echo "MAUI $current is up to date"
4553
fi
4654
47-
- name: Apply the version bump
48-
if: steps.check.outputs.bump == 'true'
55+
prepare:
56+
needs: check
57+
if: needs.check.outputs.bump == 'true'
58+
runs-on: ubuntu-latest
59+
defaults:
60+
run:
61+
shell: bash
62+
steps:
63+
- uses: actions/checkout@v4
64+
65+
# The candidate goes to a THROWAWAY branch, never straight to the reviewed branch. The
66+
# reviewed branch is only moved once the gate is green (see open-pr), so a failing bump
67+
# can't silently replace the head of an already-open, already-validated PR.
68+
- name: Apply the version bump and push a validation branch
69+
id: push
4970
env:
50-
LATEST: ${{ steps.check.outputs.latest }}
71+
LATEST: ${{ needs.check.outputs.latest }}
5172
run: |
5273
sed -i -E "s|<MauiPackageVersion>[^<]+</MauiPackageVersion>|<MauiPackageVersion>$LATEST</MauiPackageVersion>|" \
5374
src/Plugin.AdMob/Plugin.AdMob.csproj
@@ -58,18 +79,107 @@ jobs:
5879
samples/Foo.Bar.SampleApp/Foo.Bar.SampleApp.csproj \
5980
samples/Foo.Bar.SampleApp.Hybrid/Foo.Bar.SampleApp.Hybrid.csproj
6081
git diff --stat
82+
git config user.name "github-actions[bot]"
83+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
84+
git checkout -B bump-maui-validate
85+
git commit -am "Bump .NET MAUI to $LATEST"
86+
git push --force origin bump-maui-validate
87+
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
6188
62-
- name: Open pull request
63-
if: steps.check.outputs.bump == 'true'
64-
uses: peter-evans/create-pull-request@v7
89+
# Hard gate: the bumped source must restore + compile on every target framework.
90+
validate-build:
91+
needs: [check, prepare]
92+
if: needs.check.outputs.bump == 'true'
93+
uses: ./.github/workflows/build.yml
94+
with:
95+
ref: bump-maui-validate
96+
97+
# Runtime signal: device tests against the bumped SOURCE. Advisory for opening the PR
98+
# (its result is reported in the PR body) so emulator flakiness can't block a valid bump.
99+
validate-device:
100+
needs: [check, prepare]
101+
if: needs.check.outputs.bump == 'true'
102+
uses: ./.github/workflows/device-tests.yml
103+
with:
104+
ref: bump-maui-validate
105+
use_published: false
106+
# Pin MAUI to the version being bumped to — otherwise the harness's floating
107+
# Microsoft.Maui.Controls reference would test a different MAUI than the PR ships.
108+
maui_version: ${{ needs.check.outputs.latest }}
109+
110+
open-pr:
111+
needs: [check, prepare, validate-build, validate-device]
112+
if: always() && needs.check.outputs.bump == 'true' && needs.validate-build.result == 'success'
113+
runs-on: ubuntu-latest
114+
defaults:
115+
run:
116+
shell: bash
117+
steps:
118+
- uses: actions/checkout@v4
65119
with:
66-
branch: bump-maui-version
67-
delete-branch: true
68-
commit-message: Bump .NET MAUI to ${{ steps.check.outputs.latest }}
69-
title: Bump .NET MAUI to ${{ steps.check.outputs.latest }}
70-
body: |
71-
Bumps .NET MAUI from `${{ steps.check.outputs.current }}` to `${{ steps.check.outputs.latest }}`.
120+
ref: bump-maui-validate
121+
fetch-depth: 0 # need real history to tell bot commits from human ones
122+
123+
# Move the reviewed branch onto the validated commit. Head, title and body therefore
124+
# always change together — the atomicity peter-evans/create-pull-request used to give us.
125+
- name: Promote the validated commit onto the review branch
126+
id: promote
127+
run: |
128+
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
129+
git fetch --no-tags origin +refs/heads/bump-maui-version:refs/remotes/origin/bump-maui-version || true
130+
131+
if ! git rev-parse -q --verify refs/remotes/origin/bump-maui-version >/dev/null; then
132+
git push origin HEAD:refs/heads/bump-maui-version
133+
echo "pushed=true" >> "$GITHUB_OUTPUT"
134+
exit 0
135+
fi
136+
137+
# Already holds exactly this change (open PR, re-run on a later night): leave it alone.
138+
if [ "$(git rev-parse refs/remotes/origin/bump-maui-version^{tree})" = "$(git rev-parse HEAD^{tree})" ]; then
139+
echo "bump-maui-version already holds this exact change; nothing to push."
140+
echo "pushed=false" >> "$GITHUB_OUTPUT"
141+
exit 0
142+
fi
143+
144+
# Only inspect commits unique to the branch, so main's own human history is ignored.
145+
if git log --format=%ae refs/remotes/origin/main..refs/remotes/origin/bump-maui-version |
146+
grep -qv '^41898282+github-actions\[bot\]@users\.noreply\.github\.com$'; then
147+
echo "::warning::bump-maui-version carries manual commits — skipping the bump rather than clobbering them."
148+
echo "pushed=false" >> "$GITHUB_OUTPUT"
149+
exit 0
150+
fi
151+
152+
git push --force-with-lease=bump-maui-version:"$(git rev-parse refs/remotes/origin/bump-maui-version)" \
153+
origin HEAD:refs/heads/bump-maui-version
154+
echo "pushed=true" >> "$GITHUB_OUTPUT"
72155
73-
Updates `MauiPackageVersion` in the plugin (which also becomes the next plugin package version) and the pinned MAUI packages in both sample apps.
156+
- name: Open or update the pull request
157+
if: steps.promote.outputs.pushed == 'true'
158+
env:
159+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
160+
CURRENT: ${{ needs.check.outputs.current }}
161+
LATEST: ${{ needs.check.outputs.latest }}
162+
BUILD_RESULT: ${{ needs.validate-build.result }}
163+
DEVICE_RESULT: ${{ needs.validate-device.result }}
164+
run: |
165+
body=$(printf '%s\n' \
166+
"Bumps .NET MAUI from \`$CURRENT\` to \`$LATEST\`." \
167+
"" \
168+
"Updates \`MauiPackageVersion\` in the plugin (which also becomes the next plugin package version) and the pinned MAUI packages in both sample apps." \
169+
"" \
170+
"**Pre-merge validation on this exact commit:**" \
171+
"- Build gate (all target frameworks): $BUILD_RESULT" \
172+
"- Device tests (Android emulator, banner hard-gate): $DEVICE_RESULT" \
173+
"" \
174+
"Release notes: https://github.com/dotnet/maui/releases")
175+
existing=$(gh pr list --head bump-maui-version --state open --json number -q '.[0].number' || true)
176+
if [ -n "$existing" ]; then
177+
echo "PR #$existing already open; branch push updated it."
178+
gh pr edit "$existing" --title "Bump .NET MAUI to $LATEST" --body "$body"
179+
else
180+
gh pr create --base main --head bump-maui-version --title "Bump .NET MAUI to $LATEST" --body "$body"
181+
fi
74182
75-
Release notes: https://github.com/dotnet/maui/releases
183+
- name: Clean up the validation branch
184+
if: always()
185+
run: git push origin --delete bump-maui-validate || true

0 commit comments

Comments
 (0)