Skip to content

Commit 7a91c63

Browse files
committed
ci: run the extension suites on pull requests and develop — the release gate's own script
The suites ran in CI at one moment only: when a tag was pushed. A pull request got CodeQL and nothing else, so the first Linux run of any suite was the release gate, after the merge. That is how v1.3.0 was tagged on a suite that could not pass there (see the previous commit). The gate moves, unchanged, into scripts/test-extensions.sh: discover every extensions/*/test/*.test.js, stop at the first failing suite, and fail if the glob matches nothing. release.yml calls it where the inline loop was, and a new workflow, test.yml, calls it on every pull request and on pushes to develop — same runner image, same Node. One script, so the two cannot drift apart. test.yml asks for read access only and needs no secrets. A newer push to the same branch cancels the run before it. Checked: the script passes all 48 suites on a Mac and in a Linux container under the runner's shell flags; it exits with a failing suite's own code without running the next; it exits 1 when nothing matches. actionlint is clean on both workflow files. release.yml's gate step is the one thing here not exercised until a tag is pushed — but it is now a single line that test.yml runs on this very pull request.
1 parent eab435f commit 7a91c63

3 files changed

Lines changed: 66 additions & 24 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,30 +40,9 @@ jobs:
4040
with:
4141
node-version: "24"
4242
- name: Extension unit tests
43-
# `shopt` is a bash builtin, so pin the shell instead of relying on the runner default (bash on
44-
# Linux/macOS, but pwsh on Windows — where this step would break if the job were ever copied).
45-
# Explicit `shell: bash` also upgrades the default `bash -e` to
46-
# `bash --noprofile --norc -eo pipefail`, so a stray profile file can't perturb the gate either.
47-
shell: bash
48-
# DISCOVERS suites — it used to `cd extensions/levelcode-ai`, so levelcode-updater's tests never
49-
# ran here, including the one guarding the updater's Download button against serving a raw
50-
# .app.zip. Globbing every extension means a new suite is gated the moment it is added, with no
51-
# list here to keep in sync. Requires are file-relative, so running from the repo root is fine.
52-
run: |
53-
shopt -s nullglob
54-
count=0
55-
for t in extensions/*/test/*.test.js; do
56-
echo "── $t"
57-
node "$t" # `-e` (from `shell: bash` above) aborts the job on the first failure
58-
count=$((count + 1))
59-
done
60-
# A zero-match glob would otherwise report success and gate nothing — the exact failure this
61-
# step is fixing. Fail loudly instead.
62-
if [ "$count" -eq 0 ]; then
63-
echo "::error::No suites matched extensions/*/test/*.test.js — the gate would pass vacuously."
64-
exit 1
65-
fi
66-
echo "──────── $count test files passed ────────"
43+
# The gate itself lives in a script, so the pull-request workflow (test.yml) runs exactly what
44+
# this does. It discovers every extensions/*/test/*.test.js and fails if it finds none.
45+
run: ./scripts/test-extensions.sh
6746

6847
build:
6948
name: Build ${{ matrix.arch }}

‎.github/workflows/test.yml‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Extension tests
2+
3+
# The release gate (scripts/test-extensions.sh), on every pull request and on develop.
4+
#
5+
# release.yml runs the same script, but only when a tag is pushed — which made a tag the first time
6+
# the suites ran on Linux. v1.3.0's gate failed there on a suite that had only ever run on a Mac,
7+
# after the pull requests that wrote it had merged. Here a suite that passes on one machine and not
8+
# on the runner is found on the pull request that adds it.
9+
10+
on:
11+
pull_request:
12+
push:
13+
branches: [develop]
14+
15+
permissions:
16+
contents: read
17+
18+
# A newer push to the same branch or pull request supersedes the run before it.
19+
concurrency:
20+
group: extension-tests-${{ github.ref }}
21+
cancel-in-progress: true
22+
23+
jobs:
24+
test:
25+
name: Extension unit tests
26+
runs-on: ubuntu-latest # the runner the release gate uses
27+
steps:
28+
- uses: actions/checkout@v7
29+
- uses: actions/setup-node@v7
30+
with:
31+
node-version: "24" # as release.yml
32+
- name: Extension unit tests
33+
run: ./scripts/test-extensions.sh

‎scripts/test-extensions.sh‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Run every extension's unit suites. This is the gate: the release workflow runs it before spending
4+
# an hour of macOS build minutes, and every pull request runs it too (.github/workflows/test.yml).
5+
#
6+
# It DISCOVERS suites — extensions/*/test/*.test.js — so a new one is gated the moment it is added,
7+
# with no list to keep in sync. (The gate used to `cd extensions/levelcode-ai`; levelcode-updater's
8+
# suites never ran, including the one guarding the Download button against serving a raw .app.zip.)
9+
# Requires are file-relative, so the suites run from the repo root.
10+
#
11+
# Usage:
12+
# ./scripts/test-extensions.sh
13+
#
14+
set -euo pipefail
15+
shopt -s nullglob
16+
17+
cd "$(dirname "$0")/.."
18+
19+
count=0
20+
for t in extensions/*/test/*.test.js; do
21+
echo "── $t"
22+
node "$t" # `set -e` stops at the first suite that fails
23+
count=$((count + 1))
24+
done
25+
# A glob that matches nothing would otherwise report success and gate nothing. Fail loudly instead.
26+
if [ "$count" -eq 0 ]; then
27+
echo "::error::No suites matched extensions/*/test/*.test.js — the gate would pass vacuously."
28+
exit 1
29+
fi
30+
echo "──────── $count test files passed ────────"

0 commit comments

Comments
 (0)