-
-
Notifications
You must be signed in to change notification settings - Fork 145
227 lines (204 loc) · 9.39 KB
/
Copy pathnode-core-subset.yml
File metadata and controls
227 lines (204 loc) · 9.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
name: Node Core Subset Radar
# Advisory nightly Node-compat radar (#800). It publishes a parity number and
# an artifact; it does NOT gate merges or releases, and it deliberately does
# NOT run on pull_request — it is a heavy, cargo-relinking sweep.
#
# #6305: this used to be one serial job that swept all 23 APIs. It never once
# completed — 38/38 scheduled runs died with exit 143, and the runner's own
# system log gave the reason: "The runner has received a shutdown signal", i.e.
# the hosted VM was reclaimed under the job rather than stopped by
# `timeout-minutes`. When that happens no further step runs — not even an
# `if: always()` one — so the report was never uploaded and #4975 lost its
# parity number entirely. The sweep is now split across shards, each shard is
# bounded by a wall-clock budget it enforces itself, and the report is written
# incrementally so a partial sweep still publishes a (clearly-flagged) number.
on:
workflow_dispatch:
inputs:
apis:
description: "Optional space-separated API list, e.g. 'path url buffer'"
required: false
default: ""
max_per_api:
description: "Maximum upstream Node tests to sample per API; 0 means no cap"
required: false
default: "25"
schedule:
- cron: "17 3 * * *"
permissions:
contents: read
concurrency:
group: node-core-subset-${{ github.ref }}
cancel-in-progress: false
env:
# Number of shards. MUST equal the length of `matrix.shard` below — the merge
# job uses it to know which per-shard reports to expect, so that a shard whose
# runner died outright is detected as missing rather than silently dropped.
NODE_CORE_SHARDS: "6"
# Bail out of the sweep with a partial-but-flagged report at this many seconds
# rather than running until something else kills us. Sized to sit well inside
# the job's `timeout-minutes` even after the release build and a cold ext-crate
# prewarm (the budget covers the prewarm too).
NODE_CORE_TIME_BUDGET: "2400"
jobs:
radar:
name: radar (shard ${{ matrix.shard }})
runs-on: ubuntu-latest
timeout-minutes: 75
strategy:
# One shard blowing its budget must not cancel the others: their numbers
# are still worth publishing.
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6]
env:
NODE_CORE_APIS: ${{ github.event_name == 'workflow_dispatch' && inputs.apis || '' }}
NODE_CORE_MAX_PER_API: ${{ github.event_name == 'workflow_dispatch' && inputs.max_per_api || '25' }}
# Without this, Python block-buffers stdout into the Actions pipe and a
# killed step shows *zero* output — which is why #6305 was undiagnosable
# from the job log for six weeks.
PYTHONUNBUFFERED: "1"
steps:
- uses: actions/checkout@v7
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: "${{ runner.os }}-perry"
save-if: ${{ github.ref == 'refs/heads/main' }}
# DELIBERATE EXEMPTION from the repo-wide .node-version pin.
#
# This job runs Node's OWN test/parallel corpus, sparse-checked-out at the
# ref in test-compat/node-core/pinned-version.txt. Those tests are written
# against the internals of that exact Node line, so the Node runtime here
# must match the vendored test suite — not the gap-suite oracle. Deriving
# the version from the same file that pins the corpus makes the coupling
# explicit and impossible to break: bump pinned-version.txt and the runtime
# follows. (A bare inline "22" here looked like drift from .node-version;
# it wasn't, and the next person to "fix" it would have broken the job.)
- name: Resolve Node version from the pinned test-corpus ref
id: node_core_version
run: |
set -euo pipefail
# pinned-version.txt holds a git branch name, e.g. "v22.x".
node_ref="$(tr -d '[:space:]' < test-compat/node-core/pinned-version.txt)"
node_version="${node_ref#v}" # v22.x -> 22.x
echo "Pinned Node core corpus: $node_ref -> node-version $node_version"
echo "version=$node_version" >> "$GITHUB_OUTPUT"
- name: Setup Node.js (matches the pinned test corpus, NOT .node-version)
uses: actions/setup-node@v7
with:
node-version: ${{ steps.node_core_version.outputs.version }}
- name: Sparse checkout pinned Node.js tests
run: |
set -euo pipefail
node_ref="$(cat test-compat/node-core/pinned-version.txt)"
git clone --no-checkout --depth 1 --branch "$node_ref" \
--filter=blob:none https://github.com/nodejs/node vendor/nodejs
git -C vendor/nodejs sparse-checkout set test/parallel test/common test/fixtures
git -C vendor/nodejs checkout
- name: Build Perry release binary
run: cargo build --release -p perry -p perry-runtime -p perry-stdlib -p perry-runtime-static -p perry-stdlib-static
- name: Run Node core subset radar
run: |
set -euo pipefail
args=(
--root vendor/nodejs
--report "test-compat/node-core/report-shard-${{ matrix.shard }}.json"
--max-per-api "$NODE_CORE_MAX_PER_API"
--shard "${{ matrix.shard }}/${{ strategy.job-total }}"
--time-budget "$NODE_CORE_TIME_BUDGET"
)
if [[ -n "$NODE_CORE_APIS" ]]; then
read -r -a requested_apis <<< "$NODE_CORE_APIS"
args+=(--api "${requested_apis[@]}")
fi
scripts/node_core_subset.py "${args[@]}"
- name: Runner headroom
if: always()
run: |
# The nightly's terminal symptom was the VM being reclaimed mid-sweep.
# If that recurs, this is the evidence we did not have (#6305).
df -h / | tail -1
free -h | head -2
- name: Upload shard report
if: always()
uses: actions/upload-artifact@v7
with:
name: node-core-subset-report-shard-${{ matrix.shard }}
path: test-compat/node-core/report-shard-${{ matrix.shard }}.json
if-no-files-found: warn
report:
name: merge + publish
needs: radar
# Publish whatever the shards managed even if some of them failed: a
# clearly-flagged lower bound beats the nothing-at-all that #6305 produced.
if: always()
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- name: Download shard reports
uses: actions/download-artifact@v8
with:
pattern: node-core-subset-report-shard-*
merge-multiple: true
path: shard-reports
- name: Merge shard reports
run: |
set -euo pipefail
# Ask for every shard report BY NAME rather than globbing whatever
# turned up: a shard whose runner was reclaimed uploads nothing, and
# that has to read as a missing shard, not as a smaller sweep. The
# merge exits non-zero if any shard is missing or incomplete — but it
# writes the aggregate report first, so the number still lands.
reports=()
for i in $(seq 1 "$NODE_CORE_SHARDS"); do
reports+=("shard-reports/report-shard-${i}.json")
done
scripts/node_core_subset.py \
--merge "${reports[@]}" \
--report test-compat/node-core/report.json
- name: Write radar summary
if: always()
run: |
python3 - <<'PY' >> "$GITHUB_STEP_SUMMARY"
import json
from pathlib import Path
report_path = Path("test-compat/node-core/report.json")
print("## Node Core Subset Radar")
print("")
if not report_path.exists():
print("No report was generated.")
raise SystemExit(0)
report = json.loads(report_path.read_text())
if not report.get("complete", False):
print("> [!WARNING]")
print("> **The sweep did not complete** — the parity below is a "
"LOWER BOUND over the tests that actually ran.")
for key, label in (("missing_shard_reports", "Missing shard reports"),
("unreached_apis", "Never reached"),
("partial_apis", "Cut off mid-API")):
if report.get(key):
print(f"> - {label}: `{', '.join(report[key])}`")
print("")
print(f"Node corpus: `{report.get('node_pinned', '')}`")
print(f"Node runtime: `{report.get('node_runtime', '')}`")
print(f"Parity: `{report.get('parity_pct', 0)}%` over `{report.get('judged', 0)}` judged tests")
print("")
print("| api | pass | diff | runtime-fail | compile-fail | node-skip |")
print("| --- | ---: | ---: | ---: | ---: | ---: |")
for api, counts in sorted(report.get("per_api", {}).items()):
print(
f"| {api} | {counts.get('pass', 0)} | {counts.get('diff', 0)} | "
f"{counts.get('runtime-fail', 0)} | {counts.get('compile-fail', 0)} | "
f"{counts.get('node-skip', 0)} |"
)
PY
- name: Upload radar report
if: always()
uses: actions/upload-artifact@v7
with:
name: node-core-subset-report
path: test-compat/node-core/report.json
if-no-files-found: warn