Repository navigation
219 lines (205 loc) · 10 KB
/
Copy pathnode.yml
File metadata and controls
219 lines (205 loc) · 10 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
name: Node projects
# Nothing in CI exercised the repo's npm projects, so breakage in them only
# surfaced when someone ran them by hand. Two examples reached main: the
# HeadTrax sample server crashed on startup for weeks because Apollo Server 5
# removed the `@apollo/server/express4` subpath (#1175), and all three React
# Native harnesses failed their own `lint` script (#1176).
#
# This workflow holds no list of projects and no per-project commands. It
# discovers every tracked package.json and runs whichever of the check scripts
# below that project declares, so a new npm project is covered the moment it
# lands and nothing here needs editing.
on:
# paths-ignore rather than a paths allowlist. An allowlist of extensions is
# a closed set: a future project using .vue, .svelte, .css or .graphql would
# be discovered from its package.json, then silently skip this workflow on
# every later source-only change. Discovery is meant to be the single thing
# that decides coverage, so the trigger only excludes files that cannot
# affect an npm project.
pull_request:
paths-ignore:
- '**/*.md'
- 'docs/**'
push:
branches: [main]
paths-ignore:
- '**/*.md'
- 'docs/**'
workflow_dispatch:
permissions:
contents: read
# One in-flight run per PR; a newer commit supersedes an older run.
concurrency:
group: node-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
discover:
name: Discover projects
runs-on: ubuntu-latest
outputs:
projects: ${{ steps.scan.outputs.projects }}
any: ${{ steps.scan.outputs.any }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Find npm projects and their declared checks
id: scan
env:
# Scripts treated as checks. `test` is deliberately absent: the React
# Native harnesses declare it but ship no test files, so jest exits 1
# with "No tests found", and --passWithNoTests would only manufacture
# a green check that asserts nothing. Add a name here and every
# project declaring it is picked up automatically.
CHECK_SCRIPTS: 'lint typecheck build compile smoke'
run: |
set -euo pipefail
# `git ls-files` rather than a filesystem glob: it lists only tracked
# files, so a vendored or stray node_modules can never be mistaken
# for a project in this repo.
projects=$(git ls-files '*package.json' \
| grep -v '/node_modules/' \
| node -e '
const fs = require("fs");
const path = require("path");
const checks = process.env.CHECK_SCRIPTS.split(" ").filter(Boolean);
const manifests = fs.readFileSync(0, "utf8").split("\n").map(s => s.trim()).filter(Boolean);
const out = [];
const uncovered = [];
for (const manifest of manifests) {
const pkg = JSON.parse(fs.readFileSync(manifest, "utf8"));
const declared = checks.filter(c => pkg.scripts && pkg.scripts[c]);
if (declared.length === 0) { uncovered.push(manifest); continue; }
out.push({
dir: path.dirname(manifest),
checks: declared.join(" "),
});
}
// A project with none of the recognized scripts would otherwise
// be skipped in silence, which is the exact failure this
// workflow exists to prevent: it would look green while
// covering nothing. Make it a hard, named error instead.
if (uncovered.length > 0) {
console.error("\nnpm projects with no recognized check script:");
for (const m of uncovered) console.error(` ${m}`);
console.error(`\nEvery tracked npm project must declare at least one of: ${checks.join(", ")}.`);
console.error("Add one, or add the script name to CHECK_SCRIPTS in this workflow.");
process.exit(1);
}
process.stdout.write(JSON.stringify(out));
')
node -e '
const p = JSON.parse(process.argv[1]);
for (const x of p) console.log(` ${x.dir} -> ${x.checks}`);
console.log(`discovered ${p.length} project(s) with checks`);
' "$projects"
count=$(node -e 'process.stdout.write(String(JSON.parse(process.argv[1]).length))' "$projects")
echo "projects=$projects" >> "$GITHUB_OUTPUT"
if [ "$count" -gt 0 ]; then echo "any=true" >> "$GITHUB_OUTPUT"; else echo "any=false" >> "$GITHUB_OUTPUT"; fi
check:
name: ${{ matrix.project.dir }} (${{ matrix.os }})
needs: discover
# An empty matrix is an error rather than a skip, so gate the whole job.
if: needs.discover.outputs.any == 'true'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
project: ${{ fromJSON(needs.discover.outputs.projects) }}
# Linux alone is structurally blind to a whole class of breakage here,
# and this repo is Windows-first. This matrix earned itself
# immediately: the better-sqlite3 11 -> 13 bump passed every
# ubuntu-latest job and failed windows-latest outright, because v13
# drops the prebuild-install script and npm's implicit `node-gyp
# rebuild` then has to resolve a Visual Studio toolset that node-gyp
# 11.5.0 cannot identify on that image (see the better-sqlite3 ignore
# entry in .github/dependabot.yml). An ubuntu-only matrix would have
# reported green while shipping a sample no Windows contributor could
# install.
os: [ubuntu-latest, windows-latest]
steps:
# persist-credentials: false — `npm ci` runs lifecycle scripts from
# PR-controlled dependencies, and the project's own scripts run right
# after, so no git credential should survive the checkout for that code
# to recover.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Node 22.13.0 exactly, not '22'. Four of the six projects here declare a
# Node engine -- the three React Native harnesses require
# `^22.13.0 || ^24.3.0 || >= 26.0.0` via @react-native/eslint-config, and
# the HeadTrax service requires `^20.19.0 || ^22.13.0 || ^23.5.0`.
# 22.13.0 is the lowest version satisfying both, so pinning it exercises
# the floor those projects actually claim to support; `node-version: '22'`
# resolves to the newest 22.x and would happily run code relying on APIs
# added well after that floor. (src/vscode-reactor declares only
# engines.vscode and tools/figma-plugin declares no engine at all, so
# neither constrains this pin; they just run on whatever it selects.)
#
# (This previously read "better-sqlite3 ^11.7.0 cannot build on Node 26".
# The real constraint is narrower: v11 declares
# `install: prebuild-install || node-gyp rebuild`, so it fetches a
# prebuilt binary for the host ABI -- it publishes ABIs 108/115/127/131,
# i.e. Node 18/20/22/23 -- and only compiles when none matches.)
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22.13.0'
cache: npm
cache-dependency-path: ${{ matrix.project.dir }}/package-lock.json
# `npm ci`, not `install`: a lockfile that has drifted from package.json
# should itself be a failure.
- name: Install
working-directory: ${{ matrix.project.dir }}
run: npm ci --no-audit --no-fund
- name: Run declared checks
working-directory: ${{ matrix.project.dir }}
# Explicit bash: the Windows runner defaults to pwsh, where
# `set -euo pipefail` and the `for` loop below are not valid syntax.
shell: bash
env:
CHECKS: ${{ matrix.project.checks }}
run: |
set -euo pipefail
for script in $CHECKS; do
echo "::group::npm run $script"
npm run "$script"
echo "::endgroup::"
done
# Several projects commit their build output and ship it directly —
# src/vscode-reactor/package.json points `main` at out/extension.js, and
# tools/figma-plugin/manifest.json points at dist/code.js and
# dist/ui.html. The checks above regenerate those files inside the
# runner, so without this a source-only change compiles green here while
# merging the previous build. Measured: both builds are deterministic
# (rebuilding twice is byte-identical), so a difference means the
# committed artifact is genuinely out of date.
#
# --ignore-cr-at-eol because the repo has no .gitattributes, so a
# contributor on Windows sees whole-file CRLF churn that says nothing
# about staleness.
- name: Check committed build output is current
working-directory: ${{ matrix.project.dir }}
# Explicit bash, as above: this script is bash, not pwsh.
shell: bash
run: |
set -euo pipefail
stale=0
if ! git diff --ignore-cr-at-eol --exit-code -- .; then
stale=1
fi
# `git diff` only sees tracked files. A build that starts emitting a
# new artifact, or a PR that deletes a committed one and lets the
# build recreate it, leaves an untracked file and would otherwise
# look clean.
untracked=$(git ls-files --others --exclude-standard -- .)
if [ -n "$untracked" ]; then
echo "Untracked files produced by the checks:"
echo "$untracked" | sed 's/^/ /'
stale=1
fi
if [ "$stale" -ne 0 ]; then
echo
echo "Committed build output is stale: running the checks above changed the working tree."
echo "Rebuild this project and commit the result."
exit 1
fi