Repository navigation
306 lines (277 loc) · 12.7 KB
/
Copy pathtypescript.yml
File metadata and controls
306 lines (277 loc) · 12.7 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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
name: TypeScript
on:
# Push only, as in the Inspector: with pull_request as well, every commit on a
# PR branch ran each workflow twice (once per event). PR checks still report
# from the push run of the head commit.
push:
# A push made with the workflow token starts no workflow, so the version PRs
# that automation opens (Version Packages, Prepare Python Release) get no
# push run. Their workflows dispatch this one on the PR's branch instead
# (#5110): `workflow_dispatch` is the one event that token can start, and its
# run's checks attach to the branch's head commit. The rollup a PR reads
# leaves dispatched checks out, so the `dispatch-status` jobs below report
# the run as a commit status instead.
workflow_dispatch:
# Superseded runs on the same ref are cancelled (e.g. rapid amend-pushes),
# except on main, where every merge keeps its own CI run
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
# Every job here only reads the repository (checkout, caches, artifacts), so
# the token is scoped in the file rather than inherited from the repository's
# default workflow permission, which could be widened in the UI with no commit
# (#5091).
permissions:
contents: read
jobs:
# Root-level guards: the checks over repo-wide tooling that no per-package
# matrix leg covers. Created by #4863 for the skills gates; #4864 extended it
# to run the root's whole `validate:guards` — the root tooling's format and
# lint, `verify:format-coverage`, `verify:skills`, `verify:typecheck-coverage`,
# the shared-devDependency guard `verify:dep-lockstep`, `verify:no-test-retries`,
# and `test:scripts` (the guard scripts' own unit tests, which include the
# workflow guard: no `local:*` script in a workflow, a timeout on every job).
# `scripts/verify-skills.mjs` asserts this job keeps running
# `verify:skills:cli` unconditionally, so do not gate these steps with `if:`.
root-guards:
timeout-minutes: 10
name: Root guards
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- name: Install dependencies
run: npm ci
- name: Run the root guards
run: npm run validate:guards
# The authoritative `claude plugin validate`, at the pinned CLI version
# (fetched with npx when not installed). Needs no authentication.
- name: Validate skills with the pinned Claude Code CLI
run: npm run verify:skills:cli
detect-packages:
timeout-minutes: 5
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.find-packages.outputs.packages }}
steps:
- uses: actions/checkout@v6
- name: Find JS packages
id: find-packages
working-directory: src
run: |
PACKAGES=$(find . -name package.json -not -path "*/node_modules/*" -exec dirname {} \; | sed 's/^\.\///' | jq -R -s -c 'split("\n")[:-1]')
echo "packages=$PACKAGES" >> $GITHUB_OUTPUT
# One leg per TypeScript server, each running ONLY its own package's
# `validate` (format:check -> lint -> typecheck -> build -> test). What no
# leg covers — the root tooling and the cross-workspace guards — is the
# root-guards job above; together they are the root `npm run validate`.
#
# Each package is also validated on every Node line it supports (#5068):
# 24, the oldest supported (#5168), through 26. The `node` axis multiplies
# legs, not steps, so the versions run side by side and the job takes no
# longer than its slowest leg. The other jobs here stay on one Node, 24: the
# oldest supported, so an API only newer Node has cannot slip in unnoticed.
validate:
timeout-minutes: 15
needs: [detect-packages]
strategy:
fail-fast: false
matrix:
package: ${{ fromJson(needs.detect-packages.outputs.packages) }}
node: [24, 26]
name: Validate ${{ matrix.package }} (Node ${{ matrix.node }})
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: npm
# This package plus the root: run from a workspace directory, `npm ci`
# installs only that workspace's dependencies, and the gate's tools
# (Prettier, ESLint) are root devDependencies. A bare root `npm ci` would
# instead run every workspace's `prepare` (a build), coupling the legs.
- name: Install dependencies
run: npm ci --workspace src/${{ matrix.package }} --include-workspace-root
- name: Validate package
working-directory: src/${{ matrix.package }}
run: npm run validate
# The per-file coverage gate (#4854): one leg per TypeScript server, beside
# `validate` rather than inside it, as the Inspector's CI does (inspector#2159).
# Each server's `coverage` (`vitest run --coverage`) fails when any file is
# below 90% on lines, statements, functions or branches; the thresholds live
# in the server's `vitest.config.ts`. It re-runs the suite instrumented, so it
# is kept out of the fast `test`/`validate` loop and given its own runner.
# `npm run local:gate` runs the same check through the root `npm run coverage`.
coverage:
timeout-minutes: 15
needs: [detect-packages]
strategy:
fail-fast: false
matrix:
package: ${{ fromJson(needs.detect-packages.outputs.packages) }}
name: Coverage ${{ matrix.package }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
# The same install as the `validate` leg: this package plus the root. Its
# `prepare` builds the package, so a test that launches `dist/` finds it.
- name: Install dependencies
run: npm ci --workspace src/${{ matrix.package }} --include-workspace-root
- name: Enforce the per-file coverage gate
working-directory: src/${{ matrix.package }}
run: npm run coverage
# The test suites on Windows (#1149). Every other job runs on Ubuntu, so
# Windows-only code paths (drive letters, UNC shares, no O_NOFOLLOW, rename
# over a locked file) were only ever exercised by mocking `path.win32`. This
# leg runs each server's own `npm test` on a real Windows runner. It is tests
# only: format, lint, typecheck and coverage do not depend on the OS and stay
# in the Ubuntu legs above.
test-windows:
timeout-minutes: 20
needs: [detect-packages]
strategy:
fail-fast: false
matrix:
package: ${{ fromJson(needs.detect-packages.outputs.packages) }}
name: Test ${{ matrix.package }} (Windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
# The same install as the `validate` leg; its `prepare` builds `dist/`.
- name: Install dependencies
run: npm ci --workspace src/${{ matrix.package }} --include-workspace-root
- name: Run tests
working-directory: src/${{ matrix.package }}
run: npm test
# The boot smoke (#4871): every server started the way a client starts it —
# the built `dist/index.js` for the TypeScript servers, the console script
# through `uv run` for the Python ones — over each transport it implements,
# then one tool call. It is one job rather than a step in each matrix leg
# because it is a single client driving all seven servers, and it lives in
# this workflow because that client is Node. `npm run local:gate` runs the
# same `npm run smoke` as its last stage.
smoke:
name: Boot smoke
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- name: Install uv
uses: astral-sh/setup-uv@v7
# A root `npm ci` runs every workspace's `prepare`, which builds it, so
# the four `dist/` trees the smoke launches exist after this step.
- name: Install dependencies and build
run: npm ci
# The smoke launches the Python servers with `uv run --no-sync`: it tests
# an environment, it does not create one. This is the sync `validate:py`
# does before the smoke in the local gate.
- name: Sync the Python servers
run: |
for server in fetch git time; do
(cd "src/$server" && uv sync --locked --all-extras --dev)
done
- name: Boot every server
run: npm run smoke
# A dispatched run's checks attach to the branch's head commit but are left
# out of its status rollup, which is what a pull request reads (verified on
# #5110: 29 successful dispatched check runs, a rollup of null). So on a
# dispatch, which is how a version PR opened by automation gets CI, these two
# jobs mirror the run onto the commit as a status, which the rollup does
# include: pending at the start, then the result of every job above. A push
# run reports through its own checks and skips both.
dispatch-status-pending:
name: Report the dispatched run as pending
if: github.event_name == 'workflow_dispatch'
timeout-minutes: 5
runs-on: ubuntu-latest
permissions:
statuses: write
# To look for a newer dispatched run of this workflow on the commit.
actions: read
steps:
- name: Set the commit status to pending
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
# A re-run keeps its run ID, so a newer run means this one is
# superseded and the status is no longer its to set (#5223).
NEWER=$(gh run list --workflow typescript.yml --event workflow_dispatch \
--commit "$GITHUB_SHA" --json databaseId \
--jq "[.[] | select(.databaseId > $GITHUB_RUN_ID)] | length") || NEWER=0
if [ "${NEWER:-0}" -gt 0 ]; then
echo "Superseded by a newer dispatched run, which reports instead"
exit 0
fi
gh api "repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \
-f state=pending -f context="TypeScript (dispatched)" \
-f description="Dispatched run in progress" \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
# `always()`, so a cancelled run cannot leave the status pending. A run that
# a newer dispatch on the same commit superseded posts nothing, whatever its
# jobs' results, and the newer run reports instead; any other cancellation
# is reported as an error. A job skipped by its own `if:` counts as passed.
dispatch-status:
name: Report the dispatched run's result
if: github.event_name == 'workflow_dispatch' && always()
needs:
[
dispatch-status-pending,
root-guards,
detect-packages,
validate,
coverage,
test-windows,
smoke,
]
timeout-minutes: 5
runs-on: ubuntu-latest
permissions:
statuses: write
# To look for a newer dispatched run of this workflow on the commit.
actions: read
steps:
- name: Set the commit status to the run's result
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NEEDS: ${{ toJSON(needs) }}
run: |
# A run superseded by a newer dispatch on the same commit posts
# nothing, whatever its jobs' results: the newer run owns the status
# (#5178). A failed lookup counts as no newer run, so this run still
# reports: a stale status is better than one left pending.
NEWER=$(gh run list --workflow typescript.yml --event workflow_dispatch \
--commit "$GITHUB_SHA" --json databaseId \
--jq "[.[] | select(.databaseId > $GITHUB_RUN_ID)] | length") || NEWER=0
if [ "${NEWER:-0}" -gt 0 ]; then
echo "Superseded by a newer dispatched run, which reports instead"
exit 0
fi
if jq -e 'all(.[]; .result == "success" or .result == "skipped")' <<<"$NEEDS" >/dev/null; then
STATE=success DESCRIPTION="Every job passed"
elif jq -e 'any(.[]; .result == "failure")' <<<"$NEEDS" >/dev/null; then
STATE=failure DESCRIPTION="A job failed; open the run for details"
else
STATE=error DESCRIPTION="The run was cancelled; dispatch it again"
fi
gh api "repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \
-f state="$STATE" -f context="TypeScript (dispatched)" \
-f description="$DESCRIPTION" \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"