Skip to content

Commit f1ba3c9

Browse files
committed
ci: pick the runner image per Xcode line, commit results directly
The matrix reads the macos-26, macos-15 and macos-14 image readmes and sends each Xcode line to the newest image that ships it, including beta lines a runner already has. The collect job commits result files to the default branch instead of opening a pull request.
1 parent ab3aaff commit f1ba3c9

3 files changed

Lines changed: 61 additions & 52 deletions

File tree

‎.github/workflows/compat-check.yml‎

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ jobs:
8686
name: iOS ${{ matrix.runtime }} · Xcode ${{ matrix.xcode }} · CLI ${{ matrix.cli }} · Node ${{ matrix.node }}
8787
needs: matrix
8888
if: needs.matrix.outputs.has_ios == 'true'
89-
runs-on: macos-15
89+
# Chosen per Xcode line by the matrix job from the runner images' readmes.
90+
runs-on: ${{ matrix.runner }}
9091
strategy:
9192
fail-fast: false
9293
matrix:
@@ -100,7 +101,7 @@ jobs:
100101
run: |
101102
app=$(ls -d /Applications/Xcode_${{ matrix.xcode }}*.app 2>/dev/null | sort -V | tail -1)
102103
if [ -z "$app" ]; then
103-
echo "::warning::Xcode ${{ matrix.xcode }} is not installed on this runner image"
104+
echo "::warning::Xcode ${{ matrix.xcode }} is not installed on ${{ matrix.runner }}"
104105
exit 1
105106
fi
106107
sudo xcode-select -s "$app/Contents/Developer"
@@ -179,49 +180,39 @@ jobs:
179180
path: data/verified/
180181

181182
collect:
182-
name: Merge results
183+
name: Record results
183184
needs: [matrix, ios, android]
184-
# Runs even when some builds failed: every success is a verified cell.
185+
# Runs even when some builds failed: every outcome is a recorded cell.
185186
if: always() && needs.matrix.result == 'success'
186187
runs-on: ubuntu-latest
187188
permissions:
188189
contents: write
189-
pull-requests: write
190190
steps:
191-
# Base the results branch on the current default branch, not the commit
192-
# this run started from, so the PR can only ever add result files.
193191
- uses: actions/checkout@v4
194192
with:
195193
ref: ${{ github.event.repository.default_branch }}
196-
- uses: actions/setup-node@v4
197-
with:
198-
node-version: 22
199-
# Each artifact is a data/verified/ tree holding only that job's new
200-
# files, so dropping them all onto the checkout is the whole merge.
194+
# Each artifact is a data/verified/ tree holding that job's result file,
195+
# so dropping them all onto the checkout is the whole merge.
201196
- uses: actions/download-artifact@v4
202197
with:
203198
pattern: result-*
204199
path: data/verified
205200
merge-multiple: true
206-
- id: merge
201+
- name: Commit results
207202
run: |
208-
git add -N data/verified
209-
if git diff --quiet data/verified; then
203+
git add data/verified
204+
if git diff --cached --quiet; then
210205
echo "no new results"
211-
echo "changed=false" >> "$GITHUB_OUTPUT"
212-
else
213-
git diff --stat data/verified
214-
echo "changed=true" >> "$GITHUB_OUTPUT"
206+
exit 0
215207
fi
216-
- uses: peter-evans/create-pull-request@v7
217-
if: steps.merge.outputs.changed == 'true'
218-
with:
219-
branch: ci/verified-results
220-
title: "data: new compatibility results"
221-
commit-message: "data: record compatibility results"
222-
body: |
223-
Successful builds from the compat-check run
224-
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}.
225-
Merging redeploys the site with these cells marked as verified by CI.
226-
add-paths: data/verified
227-
delete-branch: true
208+
git config user.name "github-actions[bot]"
209+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
210+
count=$(git diff --cached --name-only | wc -l | tr -d ' ')
211+
git commit -m "data: record $count compatibility results" \
212+
-m "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
213+
# Another run or a maintainer may have pushed meanwhile; results only add files, so a rebase never conflicts.
214+
for attempt in 1 2 3; do
215+
git push && exit 0
216+
git pull --rebase
217+
done
218+
exit 1

‎README.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ as a browser-support-style matrix (Vue + Vite) for people.
2929

3030
1. **Matrix**: `scripts/build-matrix.mjs` pulls the newest CLI and runtime
3131
releases from npm, Node.js majors from nodejs.org, Xcode lines from
32-
xcodereleases.com (intersected with what the GitHub macOS runner image
33-
ships), Android API levels from the SDK repository and JDK LTS releases
32+
xcodereleases.com (each mapped to the newest GitHub macOS runner image
33+
that ships it, including beta lines a runner already has), Android API levels from the SDK repository and JDK LTS releases
3434
from Adoptium, forms every pinned combination, and drops the ones that
3535
already have a file under `data/verified/`. The first run is large (GitHub
3636
allows 256 jobs per matrix; the rest wait for the next run); afterwards a
@@ -42,9 +42,9 @@ as a browser-support-style matrix (Vue + Vite) for people.
4242
passed or failed. A success proves the runtime's toolchain cells and the
4343
CLI's Node.js cell; a failure marks the toolchain cells unsupported. Either
4444
way the combination is never rebuilt.
45-
3. **Collect**: the artifacts are dropped onto the checkout and proposed as a
46-
pull request that only adds files. Merging it redeploys the site with those
47-
cells marked as verified by CI.
45+
3. **Collect**: the artifacts are dropped onto the checkout and committed to
46+
the default branch. Every file names the run that produced it, so the
47+
commit is the audit trail; nothing needs a manual merge.
4848

4949
Running the workflow by hand with its inputs filled in verifies one specific
5050
combination instead of the feed-driven set; `force` rebuilds a combination

‎scripts/build-matrix.mjs‎

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ const XCODE_FEED = "https://xcodereleases.com/data.json";
2020
const NODE_FEED = "https://nodejs.org/dist/index.json";
2121
const ANDROID_REPOSITORY = "https://dl.google.com/android/repository/repository2-3.xml";
2222
const ADOPTIUM_RELEASES = "https://api.adoptium.net/v3/info/available_releases";
23-
const MACOS_RUNNER_README = "https://raw.githubusercontent.com/actions/runner-images/main/images/macos/macos-15-Readme.md";
23+
// Newest image first: an Xcode line shipped by several images runs on the newest one.
24+
const MACOS_RUNNERS = ["macos-26", "macos-15", "macos-14"];
25+
const RUNNER_README = (image) => `https://raw.githubusercontent.com/actions/runner-images/main/images/macos/${image}-Readme.md`;
2426

2527
const CLI_VERSIONS = 2; // newest stable CLI releases
2628
const RUNTIME_VERSIONS = 2; // newest stable releases of each runtime
@@ -91,16 +93,24 @@ async function xcodeLines() {
9193
return [...lines].sort(compareVersions).slice(0, XCODE_LINES);
9294
}
9395

94-
// Xcode versions installed on the GitHub macOS runner image, from its published readme.
96+
// Which GitHub macOS runner image ships each Xcode line, from the images' published readmes.
9597
async function runnerXcodes() {
96-
try {
97-
const readme = await text(MACOS_RUNNER_README);
98-
const section = readme.split(/^#+ .*Xcode.*$/m)[1] ?? readme;
99-
return [...section.matchAll(/^\|\s*(\d+\.\d+(?:\.\d+)?)/gm)].map((m) => m[1]);
100-
} catch (err) {
101-
console.warn(`runner image readme unavailable (${err.message}); every Xcode line will be attempted`);
102-
return null;
98+
const byLine = new Map();
99+
for (const image of MACOS_RUNNERS) {
100+
try {
101+
const readme = await text(RUNNER_README(image));
102+
const section = readme.split(/^#+ .*Xcode.*$/m)[1] ?? readme;
103+
for (const [, version] of section.matchAll(/^\|\s*(\d+\.\d+(?:\.\d+)?)/gm)) {
104+
const line = version.split(".").slice(0, 2).join(".");
105+
if (!byLine.has(line)) {
106+
byLine.set(line, image);
107+
}
108+
}
109+
} catch (err) {
110+
console.warn(`${image} readme unavailable (${err.message})`);
111+
}
103112
}
113+
return byLine;
104114
}
105115

106116
// The SDK repository is the only source of installable package ids: newer API
@@ -183,7 +193,12 @@ async function manualMatrix(env) {
183193
const matrix = { ios: [], android: [], deferred: { ios: 0, android: 0 }, manual: true };
184194

185195
if (platform === "ios") {
186-
const job = { ...base, xcode: required("MANUAL_XCODE") };
196+
const xcode = required("MANUAL_XCODE");
197+
const runners = await runnerXcodes();
198+
if (!runners.has(xcode)) {
199+
throw new Error(`no GitHub macOS runner image ships Xcode ${xcode} (known: ${[...runners.keys()].join(", ")})`);
200+
}
201+
const job = { ...base, xcode, runner: runners.get(xcode) };
187202
const result = { package: "@nativescript/ios", version: job.runtime, toolchains: { xcode: job.xcode }, with: { nativescript: job.cli, node: job.node } };
188203
if (force || !isRecorded(result)) {
189204
matrix.ios.push(job);
@@ -230,17 +245,20 @@ const [clis, nodes, iosRuntimes, androidRuntimes, xcodes, installedXcodes, level
230245
jdkReleases(),
231246
]);
232247

248+
// Lines a runner ships that are newer than the newest stable feed line are
249+
// betas; they join the matrix so a new Xcode is exercised before it ships.
250+
const newestStable = xcodes[0];
251+
const betaLines = [...installedXcodes.keys()].filter((line) => compareVersions(line, newestStable) < 0);
252+
const xcodeLinesToTest = [...new Set([...betaLines, ...xcodes])].filter((line) => installedXcodes.has(line));
253+
233254
const ios = [];
234255
for (const cli of clis) {
235256
for (const node of nodes) {
236257
for (const runtime of iosRuntimes) {
237-
for (const xcode of xcodes) {
238-
if (installedXcodes && !installedXcodes.some((v) => sameLine(v, xcode))) {
239-
continue;
240-
}
258+
for (const xcode of xcodeLinesToTest) {
241259
const result = { package: "@nativescript/ios", version: runtime, toolchains: { xcode }, with: { nativescript: cli, node } };
242260
if (!isRecorded(result)) {
243-
ios.push({ cli, node, runtime, xcode });
261+
ios.push({ cli, node, runtime, xcode, runner: installedXcodes.get(xcode) });
244262
}
245263
}
246264
}
@@ -273,7 +291,7 @@ const matrix = {
273291
ios: ios.slice(0, MAX_JOBS_PER_MATRIX),
274292
android: android.slice(0, MAX_JOBS_PER_MATRIX),
275293
deferred: { ios: Math.max(0, ios.length - MAX_JOBS_PER_MATRIX), android: Math.max(0, android.length - MAX_JOBS_PER_MATRIX) },
276-
feeds: { clis, nodes, iosRuntimes, androidRuntimes, xcodes, installedXcodes, levels, jdks },
294+
feeds: { clis, nodes, iosRuntimes, androidRuntimes, xcodes, runners: Object.fromEntries(installedXcodes), levels, jdks },
277295
};
278296

279297
emit(matrix);

0 commit comments

Comments
 (0)