forge-workflow 0.1.0-beta.7 → 0.1.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/bin/forge-gh-proxy.js +15 -0
- package/bin/forge-github-credential.js +21 -0
- package/bin/forge.js +16 -1
- package/docs/guides/MIGRATION.md +4 -3
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/VALIDATION.md +14 -0
- package/docs/reference/github-accounts.md +114 -38
- package/lib/bun-workflow-pins.js +129 -3
- package/lib/commands/github.js +234 -49
- package/lib/commands/push.js +298 -46
- package/lib/commands/release.js +18 -1
- package/lib/commands/test.js +8 -0
- package/lib/commands/validate.js +209 -69
- package/lib/gh-proxy.js +309 -0
- package/lib/github-context.js +160 -25
- package/lib/github-credential.js +47 -0
- package/lib/github-router.js +774 -0
- package/lib/native-gh.js +177 -0
- package/lib/npm-publish-workflow.js +5 -5
- package/lib/pr-monitor/flow-monitor.js +125 -26
- package/lib/protected-state-authority.js +214 -13
- package/lib/release-readiness.js +57 -41
- package/lib/shell-utils.js +41 -15
- package/lib/test-workflow.js +389 -0
- package/lib/validation-receipt.js +127 -12
- package/lib/workflow-templates/test.yml +654 -0
- package/package.json +7 -5
- package/scripts/check-forge-token.js +85 -67
- package/scripts/gen-embedded-assets.mjs +31 -5
- package/scripts/test-full-suite.js +69 -15
- package/scripts/test.js +10 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
paths:
|
|
7
|
+
- 'bin/**'
|
|
8
|
+
- 'lib/**'
|
|
9
|
+
- 'scripts/**'
|
|
10
|
+
- 'test/**'
|
|
11
|
+
- 'test-env/**'
|
|
12
|
+
- 'skills/**'
|
|
13
|
+
- 'packages/**'
|
|
14
|
+
- 'package.json'
|
|
15
|
+
- 'bunfig.toml'
|
|
16
|
+
- '.github/workflows/**'
|
|
17
|
+
- '.github/agentic-workflows/**'
|
|
18
|
+
pull_request:
|
|
19
|
+
paths:
|
|
20
|
+
- 'bin/**'
|
|
21
|
+
- 'lib/**'
|
|
22
|
+
- 'scripts/**'
|
|
23
|
+
- 'test/**'
|
|
24
|
+
- 'test-env/**'
|
|
25
|
+
- 'skills/**'
|
|
26
|
+
- 'packages/**'
|
|
27
|
+
- 'package.json'
|
|
28
|
+
- 'bunfig.toml'
|
|
29
|
+
- '.github/workflows/**'
|
|
30
|
+
- '.github/agentic-workflows/**'
|
|
31
|
+
merge_group:
|
|
32
|
+
workflow_dispatch:
|
|
33
|
+
schedule:
|
|
34
|
+
- cron: '0 3 * * 0'
|
|
35
|
+
|
|
36
|
+
permissions:
|
|
37
|
+
contents: read
|
|
38
|
+
|
|
39
|
+
concurrency:
|
|
40
|
+
group: tests-${{ github.event.pull_request.number || github.ref }}
|
|
41
|
+
cancel-in-progress: true
|
|
42
|
+
|
|
43
|
+
jobs:
|
|
44
|
+
changes:
|
|
45
|
+
# Classifies the PR diff so the expensive 3-OS x 2-Node Full Matrix only runs
|
|
46
|
+
# when the change can plausibly behave differently per operating system.
|
|
47
|
+
# Anything that is not a pull_request (push to master, merge_group, schedule,
|
|
48
|
+
# workflow_dispatch) is always treated as OS-sensitive.
|
|
49
|
+
name: Classify Changed Paths
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
timeout-minutes: 10
|
|
52
|
+
outputs:
|
|
53
|
+
os_sensitive: ${{ steps.filter.outputs.os_sensitive }}
|
|
54
|
+
steps:
|
|
55
|
+
- name: Checkout code
|
|
56
|
+
uses: actions/checkout@v7
|
|
57
|
+
with:
|
|
58
|
+
fetch-depth: 0
|
|
59
|
+
- name: Classify diff
|
|
60
|
+
id: filter
|
|
61
|
+
shell: bash
|
|
62
|
+
env:
|
|
63
|
+
EVENT_NAME: ${{ github.event_name }}
|
|
64
|
+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
65
|
+
HEAD_SHA: ${{ github.sha }}
|
|
66
|
+
run: |
|
|
67
|
+
set -uo pipefail
|
|
68
|
+
if [ "$EVENT_NAME" != "pull_request" ]; then
|
|
69
|
+
echo "Non-PR event ($EVENT_NAME): full matrix always runs."
|
|
70
|
+
echo "os_sensitive=true" >> "$GITHUB_OUTPUT"
|
|
71
|
+
exit 0
|
|
72
|
+
fi
|
|
73
|
+
if ! changed="$(git diff --name-only "$BASE_SHA...$HEAD_SHA")"; then
|
|
74
|
+
echo "::warning::Could not resolve the PR diff; failing safe to the full matrix."
|
|
75
|
+
echo "os_sensitive=true" >> "$GITHUB_OUTPUT"
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
# Surfaces where Windows/macOS genuinely diverge from Linux:
|
|
79
|
+
# process/CLI entry points, native + packaged deps, filesystem/path/symlink
|
|
80
|
+
# helpers, git-hook + installer plumbing, the SQLite kernel (Windows file
|
|
81
|
+
# handle/EBUSY history), and this workflow itself.
|
|
82
|
+
os_sensitive_pattern='^(bin/|packages/|test-env/|plugin/hooks/|lib/core/|lib/kernel/|lib/activation/|lib/preflight/|lib/commands/(worktree|setup|init|install)|lib/(shell-utils|symlink-utils|file-utils|file-hash|package-root|forge-lock|detect-worktree|protected-path-manifest|hook-global-installer|hook-renderer|lefthook-check|lefthook-wiring|husky-migration|setup|setup-utils|reset)\.js$|install\.sh$|scripts/install\.|lefthook\.yml$|bunfig\.toml$|package\.json$|bun\.lock$|\.github/workflows/test\.yml$)'
|
|
83
|
+
echo "Changed files:"
|
|
84
|
+
printf '%s\n' "$changed"
|
|
85
|
+
if printf '%s\n' "$changed" | grep -Eq "$os_sensitive_pattern"; then
|
|
86
|
+
echo "OS-sensitive paths changed: running the full matrix."
|
|
87
|
+
echo "os_sensitive=true" >> "$GITHUB_OUTPUT"
|
|
88
|
+
else
|
|
89
|
+
echo "No OS-sensitive paths changed: full matrix skipped on this PR."
|
|
90
|
+
echo "Cross-OS coverage on this PR still comes from Windows Smoke, macOS Smoke"
|
|
91
|
+
echo "and the windows-node22 Targeted PR Tests lane; the full matrix re-runs on"
|
|
92
|
+
echo "merge_group, on every push to master, and on the weekly schedule."
|
|
93
|
+
echo "os_sensitive=false" >> "$GITHUB_OUTPUT"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
full-matrix:
|
|
97
|
+
name: Full Matrix (${{ matrix.os }} / Node ${{ matrix.node-version }})
|
|
98
|
+
needs: [changes]
|
|
99
|
+
if: ${{ needs.changes.outputs.os_sensitive == 'true' }}
|
|
100
|
+
runs-on: ${{ matrix.os }}
|
|
101
|
+
timeout-minutes: 30
|
|
102
|
+
strategy:
|
|
103
|
+
fail-fast: false
|
|
104
|
+
matrix:
|
|
105
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
106
|
+
node-version: [22, 24]
|
|
107
|
+
steps:
|
|
108
|
+
- name: Checkout code
|
|
109
|
+
uses: actions/checkout@v7
|
|
110
|
+
- name: Setup Node.js ${{ matrix.node-version }}
|
|
111
|
+
uses: actions/setup-node@v7
|
|
112
|
+
with:
|
|
113
|
+
node-version: ${{ matrix.node-version }}
|
|
114
|
+
- name: Setup Bun
|
|
115
|
+
uses: oven-sh/setup-bun@v2
|
|
116
|
+
with:
|
|
117
|
+
bun-version: __BUN_VERSION__
|
|
118
|
+
- name: Install dependencies
|
|
119
|
+
run: bun install
|
|
120
|
+
- name: Ensure test-results directory
|
|
121
|
+
run: |
|
|
122
|
+
node -e "require('node:fs').mkdirSync('test-results', { recursive: true })"
|
|
123
|
+
- name: Setup test fixtures (Unix)
|
|
124
|
+
if: runner.os != 'Windows'
|
|
125
|
+
run: bash test-env/automation/setup-fixtures.sh --force
|
|
126
|
+
- name: Setup test fixtures (Windows)
|
|
127
|
+
if: runner.os == 'Windows'
|
|
128
|
+
shell: bash
|
|
129
|
+
run: bash test-env/automation/setup-fixtures.sh --force
|
|
130
|
+
- name: Run matrix full suite
|
|
131
|
+
run: node scripts/test-full-suite.js --timeout 15000 --label-prefix full-matrix-${{ matrix.os }}-node${{ matrix.node-version }}
|
|
132
|
+
- name: Upload matrix artifacts
|
|
133
|
+
uses: actions/upload-artifact@v7
|
|
134
|
+
if: always()
|
|
135
|
+
continue-on-error: true
|
|
136
|
+
with:
|
|
137
|
+
name: test-artifacts-full-matrix-${{ matrix.os }}-node${{ matrix.node-version }}
|
|
138
|
+
path: test-results/
|
|
139
|
+
retention-days: 7
|
|
140
|
+
|
|
141
|
+
unit-shard:
|
|
142
|
+
name: Unit Shard ${{ matrix.shard-index }}
|
|
143
|
+
if: github.event_name == 'pull_request'
|
|
144
|
+
runs-on: ubuntu-latest
|
|
145
|
+
timeout-minutes: 20
|
|
146
|
+
strategy:
|
|
147
|
+
fail-fast: false
|
|
148
|
+
max-parallel: 4
|
|
149
|
+
matrix:
|
|
150
|
+
shard-index: [0, 1, 2, 3]
|
|
151
|
+
steps:
|
|
152
|
+
- name: Checkout code
|
|
153
|
+
uses: actions/checkout@v7
|
|
154
|
+
- name: Setup Node.js 24
|
|
155
|
+
uses: actions/setup-node@v7
|
|
156
|
+
with:
|
|
157
|
+
node-version: 24
|
|
158
|
+
- name: Setup Bun
|
|
159
|
+
uses: oven-sh/setup-bun@v2
|
|
160
|
+
with:
|
|
161
|
+
bun-version: __BUN_VERSION__
|
|
162
|
+
- name: Install dependencies
|
|
163
|
+
run: bun install
|
|
164
|
+
- name: Ensure test-results directory
|
|
165
|
+
run: |
|
|
166
|
+
node -e "require('node:fs').mkdirSync('test-results', { recursive: true })"
|
|
167
|
+
- name: Setup test fixtures
|
|
168
|
+
run: bash test-env/automation/setup-fixtures.sh --force
|
|
169
|
+
- name: Run unit shard
|
|
170
|
+
run: bun run test:ci:shard -- --mode shard --shard-index ${{ matrix.shard-index }} --shard-total 4 --label unit-shard-${{ matrix.shard-index }}
|
|
171
|
+
- name: Build unit shard profile
|
|
172
|
+
run: node scripts/test-profile.js --input-dir test-results --output test-results/unit-shard-${{ matrix.shard-index }}.profile.json --label unit-shard-${{ matrix.shard-index }} --integration-skipped true
|
|
173
|
+
- name: Publish unit shard summary
|
|
174
|
+
if: always()
|
|
175
|
+
shell: bash
|
|
176
|
+
run: |
|
|
177
|
+
dashboard_json="$(node scripts/test-dashboard.js --json --profiles-dir test-results)"
|
|
178
|
+
node -e "const d = JSON.parse(process.argv[1]); const lines = ['## Unit Shard Summary', '', 'Suite duration: ' + d.suiteDurationMs + 'ms', 'Integration skipped: ' + (d.integrationSkipped ? 'yes' : 'no')]; if (d.slowestFiles.length) { lines.push('', 'Slowest files:'); for (const entry of d.slowestFiles.slice(0, 5)) lines.push('- ' + entry.file + ': ' + entry.durationMs + 'ms'); } console.log(lines.join('\n'));" "$dashboard_json" >> "$GITHUB_STEP_SUMMARY"
|
|
179
|
+
- name: Upload unit shard artifacts
|
|
180
|
+
uses: actions/upload-artifact@v7
|
|
181
|
+
if: always()
|
|
182
|
+
continue-on-error: true
|
|
183
|
+
with:
|
|
184
|
+
name: test-artifacts-unit-shard-${{ matrix.shard-index }}
|
|
185
|
+
path: test-results/
|
|
186
|
+
retention-days: 7
|
|
187
|
+
|
|
188
|
+
windows-smoke:
|
|
189
|
+
name: Windows Smoke
|
|
190
|
+
if: github.event_name == 'pull_request'
|
|
191
|
+
runs-on: windows-latest
|
|
192
|
+
timeout-minutes: 25
|
|
193
|
+
steps:
|
|
194
|
+
- name: Checkout code
|
|
195
|
+
uses: actions/checkout@v7
|
|
196
|
+
- name: Setup Node.js 24
|
|
197
|
+
uses: actions/setup-node@v7
|
|
198
|
+
with:
|
|
199
|
+
node-version: 24
|
|
200
|
+
- name: Setup Bun
|
|
201
|
+
uses: oven-sh/setup-bun@v2
|
|
202
|
+
with:
|
|
203
|
+
bun-version: __BUN_VERSION__
|
|
204
|
+
- name: Install dependencies
|
|
205
|
+
run: bun install
|
|
206
|
+
- name: Ensure test-results directory
|
|
207
|
+
run: |
|
|
208
|
+
node -e "require('node:fs').mkdirSync('test-results', { recursive: true })"
|
|
209
|
+
- name: Run Windows smoke tests
|
|
210
|
+
run: bun run test:ci:shard -- --mode smoke --label windows-smoke
|
|
211
|
+
- name: Build Windows smoke profile
|
|
212
|
+
run: node scripts/test-profile.js --input-dir test-results --output test-results/windows-smoke.profile.json --label windows-smoke --integration-skipped true
|
|
213
|
+
- name: Publish Windows smoke summary
|
|
214
|
+
if: always()
|
|
215
|
+
shell: bash
|
|
216
|
+
run: |
|
|
217
|
+
dashboard_json="$(node scripts/test-dashboard.js --json --profiles-dir test-results)"
|
|
218
|
+
node -e "const d = JSON.parse(process.argv[1]); console.log(['## Windows Smoke Summary', '', 'Suite duration: ' + d.suiteDurationMs + 'ms'].join('\n'));" "$dashboard_json" >> "$GITHUB_STEP_SUMMARY"
|
|
219
|
+
- name: Upload Windows smoke artifacts
|
|
220
|
+
uses: actions/upload-artifact@v7
|
|
221
|
+
if: always()
|
|
222
|
+
continue-on-error: true
|
|
223
|
+
with:
|
|
224
|
+
name: test-artifacts-windows-smoke
|
|
225
|
+
path: test-results/
|
|
226
|
+
retention-days: 7
|
|
227
|
+
|
|
228
|
+
macos-smoke:
|
|
229
|
+
name: macOS Smoke
|
|
230
|
+
if: github.event_name == 'pull_request'
|
|
231
|
+
runs-on: macos-latest
|
|
232
|
+
timeout-minutes: 25
|
|
233
|
+
steps:
|
|
234
|
+
- name: Checkout code
|
|
235
|
+
uses: actions/checkout@v7
|
|
236
|
+
- name: Setup Node.js 24
|
|
237
|
+
uses: actions/setup-node@v7
|
|
238
|
+
with:
|
|
239
|
+
node-version: 24
|
|
240
|
+
- name: Setup Bun
|
|
241
|
+
uses: oven-sh/setup-bun@v2
|
|
242
|
+
with:
|
|
243
|
+
bun-version: __BUN_VERSION__
|
|
244
|
+
- name: Install dependencies
|
|
245
|
+
run: bun install
|
|
246
|
+
- name: Ensure test-results directory
|
|
247
|
+
run: |
|
|
248
|
+
node -e "require('node:fs').mkdirSync('test-results', { recursive: true })"
|
|
249
|
+
- name: Run macOS smoke tests
|
|
250
|
+
run: bun run test:ci:shard -- --mode smoke --label macos-smoke
|
|
251
|
+
- name: Build macOS smoke profile
|
|
252
|
+
run: node scripts/test-profile.js --input-dir test-results --output test-results/macos-smoke.profile.json --label macos-smoke --integration-skipped true
|
|
253
|
+
- name: Publish macOS smoke summary
|
|
254
|
+
if: always()
|
|
255
|
+
shell: bash
|
|
256
|
+
run: |
|
|
257
|
+
dashboard_json="$(node scripts/test-dashboard.js --json --profiles-dir test-results)"
|
|
258
|
+
node -e "const d = JSON.parse(process.argv[1]); console.log(['## macOS Smoke Summary', '', 'Suite duration: ' + d.suiteDurationMs + 'ms'].join('\n'));" "$dashboard_json" >> "$GITHUB_STEP_SUMMARY"
|
|
259
|
+
- name: Upload macOS smoke artifacts
|
|
260
|
+
uses: actions/upload-artifact@v7
|
|
261
|
+
if: always()
|
|
262
|
+
continue-on-error: true
|
|
263
|
+
with:
|
|
264
|
+
name: test-artifacts-macos-smoke
|
|
265
|
+
path: test-results/
|
|
266
|
+
retention-days: 7
|
|
267
|
+
|
|
268
|
+
cross-os-gate:
|
|
269
|
+
# Stable, single required status check that enforces the cross-OS smoke lanes.
|
|
270
|
+
# Runs even when a smoke lane fails (if: always) and fails the PR unless BOTH
|
|
271
|
+
# Windows and macOS smoke succeeded (or were legitimately skipped). This is the
|
|
272
|
+
# context to mark Required in branch protection so a red Windows/macOS run
|
|
273
|
+
# cannot merge. For doc-only PRs the real smoke lanes are path-filtered out and
|
|
274
|
+
# the Cross-OS Gate no-op in required-checks-bypass.yml satisfies the check.
|
|
275
|
+
name: Cross-OS Gate
|
|
276
|
+
if: ${{ always() && github.event_name == 'pull_request' }}
|
|
277
|
+
needs: [windows-smoke, macos-smoke]
|
|
278
|
+
runs-on: ubuntu-latest
|
|
279
|
+
timeout-minutes: 10
|
|
280
|
+
steps:
|
|
281
|
+
- name: Require Windows + macOS smoke to pass
|
|
282
|
+
run: |
|
|
283
|
+
w='${{ needs.windows-smoke.result }}'
|
|
284
|
+
m='${{ needs.macos-smoke.result }}'
|
|
285
|
+
echo "windows-smoke=$w macos-smoke=$m"
|
|
286
|
+
for r in "$w" "$m"; do
|
|
287
|
+
if [ "$r" = "failure" ] || [ "$r" = "cancelled" ]; then
|
|
288
|
+
echo "::error::Cross-OS smoke did not pass (windows=$w macos=$m)"
|
|
289
|
+
exit 1
|
|
290
|
+
fi
|
|
291
|
+
done
|
|
292
|
+
echo "Cross-OS smoke OK (success or skipped on both)."
|
|
293
|
+
|
|
294
|
+
followup-tests:
|
|
295
|
+
name: Targeted PR Tests (${{ matrix.label }})
|
|
296
|
+
if: github.event_name == 'pull_request'
|
|
297
|
+
runs-on: ${{ matrix.os }}
|
|
298
|
+
timeout-minutes: 30
|
|
299
|
+
strategy:
|
|
300
|
+
fail-fast: false
|
|
301
|
+
matrix:
|
|
302
|
+
include:
|
|
303
|
+
- os: ubuntu-latest
|
|
304
|
+
node-version: 24
|
|
305
|
+
label: ubuntu-node24
|
|
306
|
+
- os: windows-latest
|
|
307
|
+
node-version: 22
|
|
308
|
+
label: windows-node22
|
|
309
|
+
steps:
|
|
310
|
+
- name: Checkout code
|
|
311
|
+
uses: actions/checkout@v7
|
|
312
|
+
with:
|
|
313
|
+
fetch-depth: 0
|
|
314
|
+
- name: Setup Node.js ${{ matrix.node-version }}
|
|
315
|
+
uses: actions/setup-node@v7
|
|
316
|
+
with:
|
|
317
|
+
node-version: ${{ matrix.node-version }}
|
|
318
|
+
- name: Setup Bun
|
|
319
|
+
uses: oven-sh/setup-bun@v2
|
|
320
|
+
with:
|
|
321
|
+
bun-version: __BUN_VERSION__
|
|
322
|
+
- name: Install dependencies
|
|
323
|
+
run: bun install
|
|
324
|
+
- name: Ensure test-results directory
|
|
325
|
+
run: |
|
|
326
|
+
node -e "require('node:fs').mkdirSync('test-results', { recursive: true })"
|
|
327
|
+
- name: Setup test fixtures
|
|
328
|
+
run: bash test-env/automation/setup-fixtures.sh --force
|
|
329
|
+
- name: Resolve affected test targets
|
|
330
|
+
id: affected
|
|
331
|
+
shell: bash
|
|
332
|
+
env:
|
|
333
|
+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
334
|
+
HEAD_SHA: ${{ github.sha }}
|
|
335
|
+
run: |
|
|
336
|
+
node <<'EOF'
|
|
337
|
+
const { execFileSync } = require('node:child_process');
|
|
338
|
+
const { buildTestExecutionPlan } = require('./scripts/test');
|
|
339
|
+
|
|
340
|
+
const baseSha = process.env.BASE_SHA;
|
|
341
|
+
const headSha = process.env.HEAD_SHA;
|
|
342
|
+
const outputPath = process.env.GITHUB_OUTPUT;
|
|
343
|
+
const diffOutput = execFileSync('git', ['diff', '--name-only', `${baseSha}...${headSha}`], {
|
|
344
|
+
encoding: 'utf8',
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const execStub = (cmd, args, options = {}) => {
|
|
348
|
+
if (cmd === 'git' && args[0] === 'rev-parse' && args.includes('@{upstream}')) {
|
|
349
|
+
return 'origin/pull-request';
|
|
350
|
+
}
|
|
351
|
+
if (cmd === 'git' && args[0] === 'diff') {
|
|
352
|
+
return diffOutput;
|
|
353
|
+
}
|
|
354
|
+
return execFileSync(cmd, args, { ...options, encoding: 'utf8' });
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const plan = buildTestExecutionPlan(process.cwd(), execStub, { sinceUpstream: true });
|
|
358
|
+
const effectiveMode = plan.mode === 'targeted' && plan.testTargets.length === 0
|
|
359
|
+
? 'full'
|
|
360
|
+
: plan.mode;
|
|
361
|
+
const lines = [
|
|
362
|
+
`mode=${effectiveMode}`,
|
|
363
|
+
`reason=${plan.reason}`,
|
|
364
|
+
`run_test_env=${plan.runTestEnv}`,
|
|
365
|
+
`run_e2e=${plan.runE2E}`,
|
|
366
|
+
`run_workflow_tests=${plan.runWorkflowTests}`,
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
if (plan.testTargets.length > 0) {
|
|
370
|
+
lines.push('targets<<EOF');
|
|
371
|
+
lines.push(...plan.testTargets);
|
|
372
|
+
lines.push('EOF');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
require('node:fs').appendFileSync(outputPath, `${lines.join('\n')}\n`);
|
|
376
|
+
EOF
|
|
377
|
+
- name: Run targeted unit tests
|
|
378
|
+
if: steps.affected.outputs.mode == 'targeted'
|
|
379
|
+
shell: bash
|
|
380
|
+
run: |
|
|
381
|
+
mapfile -t test_targets <<'EOF'
|
|
382
|
+
${{ steps.affected.outputs.targets }}
|
|
383
|
+
EOF
|
|
384
|
+
bun test --timeout 15000 "${test_targets[@]}" --reporter=junit --reporter-outfile test-results/followup-targeted.xml
|
|
385
|
+
# Route the full-suite case through the same lane-aware, worker-budgeted
|
|
386
|
+
# runner the full matrix uses. A raw `bun test test/` runs every
|
|
387
|
+
# subprocess-heavy file inside one long-lived bun process with no
|
|
388
|
+
# resource-lane separation, which is where the Windows lane's spawns start
|
|
389
|
+
# failing outright.
|
|
390
|
+
- name: Run single-platform unit suite fallback
|
|
391
|
+
if: steps.affected.outputs.mode == 'full'
|
|
392
|
+
run: node scripts/test-full-suite.js --timeout 15000 --label-prefix followup-full-${{ matrix.label }}
|
|
393
|
+
- name: Run affected e2e tests
|
|
394
|
+
if: steps.affected.outputs.run_e2e == 'true'
|
|
395
|
+
run: bun test --timeout 15000 test/e2e/ --reporter=junit --reporter-outfile test-results/followup-e2e.xml
|
|
396
|
+
- name: Run affected edge-case tests
|
|
397
|
+
if: steps.affected.outputs.run_test_env == 'true'
|
|
398
|
+
run: bun test --timeout 15000 test-env/
|
|
399
|
+
- name: Build followup profile
|
|
400
|
+
run: node scripts/test-profile.js --input-dir test-results --output test-results/followup-${{ matrix.label }}.profile.json --label followup-${{ matrix.label }} --integration-skipped true
|
|
401
|
+
- name: Publish followup summary
|
|
402
|
+
if: always()
|
|
403
|
+
shell: bash
|
|
404
|
+
run: |
|
|
405
|
+
dashboard_json="$(node scripts/test-dashboard.js --json --profiles-dir test-results)"
|
|
406
|
+
node -e "const d = JSON.parse(process.argv[1]); const lines = ['## Follow-up Test Summary', '', 'Suite duration: ' + d.suiteDurationMs + 'ms']; if (d.slowestFiles.length) { lines.push('', 'Slowest files:'); for (const entry of d.slowestFiles.slice(0, 5)) lines.push('- ' + entry.file + ': ' + entry.durationMs + 'ms'); } console.log(lines.join('\n'));" "$dashboard_json" >> "$GITHUB_STEP_SUMMARY"
|
|
407
|
+
- name: Upload followup artifacts
|
|
408
|
+
uses: actions/upload-artifact@v7
|
|
409
|
+
if: always()
|
|
410
|
+
continue-on-error: true
|
|
411
|
+
with:
|
|
412
|
+
name: test-artifacts-followup-${{ matrix.label }}
|
|
413
|
+
path: test-results/
|
|
414
|
+
retention-days: 7
|
|
415
|
+
|
|
416
|
+
coverage:
|
|
417
|
+
name: Code Coverage
|
|
418
|
+
runs-on: ubuntu-latest
|
|
419
|
+
timeout-minutes: 30
|
|
420
|
+
steps:
|
|
421
|
+
- name: Checkout code
|
|
422
|
+
uses: actions/checkout@v7
|
|
423
|
+
- name: Setup Node.js 24
|
|
424
|
+
uses: actions/setup-node@v7
|
|
425
|
+
with:
|
|
426
|
+
node-version: 24
|
|
427
|
+
- name: Setup Bun
|
|
428
|
+
uses: oven-sh/setup-bun@v2
|
|
429
|
+
with:
|
|
430
|
+
bun-version: __BUN_VERSION__
|
|
431
|
+
- name: Install dependencies
|
|
432
|
+
run: bun install
|
|
433
|
+
- name: Ensure test-results directory
|
|
434
|
+
run: |
|
|
435
|
+
node -e "require('node:fs').mkdirSync('test-results', { recursive: true })"
|
|
436
|
+
- name: Setup test fixtures
|
|
437
|
+
run: bash test-env/automation/setup-fixtures.sh --force
|
|
438
|
+
- name: Run tests with coverage
|
|
439
|
+
run: bun test --timeout 15000 --coverage test/ --reporter=junit --reporter-outfile test-results/coverage.xml
|
|
440
|
+
- name: Build coverage profile
|
|
441
|
+
run: node scripts/test-profile.js --input-dir test-results --output test-results/coverage.profile.json --label coverage --integration-skipped true
|
|
442
|
+
- name: Publish coverage runtime summary
|
|
443
|
+
if: always()
|
|
444
|
+
shell: bash
|
|
445
|
+
run: |
|
|
446
|
+
dashboard_json="$(node scripts/test-dashboard.js --json --profiles-dir test-results)"
|
|
447
|
+
node -e "const d = JSON.parse(process.argv[1]); const lines = ['## Coverage Runtime Summary', '', 'Suite duration: ' + d.suiteDurationMs + 'ms']; if (d.slowestFiles.length) { lines.push('', 'Slowest files:'); for (const entry of d.slowestFiles.slice(0, 5)) lines.push('- ' + entry.file + ': ' + entry.durationMs + 'ms'); } console.log(lines.join('\n'));" "$dashboard_json" >> "$GITHUB_STEP_SUMMARY"
|
|
448
|
+
- name: Upload coverage artifacts
|
|
449
|
+
uses: actions/upload-artifact@v7
|
|
450
|
+
if: always()
|
|
451
|
+
continue-on-error: true
|
|
452
|
+
with:
|
|
453
|
+
name: test-artifacts-coverage
|
|
454
|
+
path: |
|
|
455
|
+
coverage/
|
|
456
|
+
test-results/
|
|
457
|
+
retention-days: 7
|
|
458
|
+
- name: Coverage summary
|
|
459
|
+
if: always()
|
|
460
|
+
run: |
|
|
461
|
+
{
|
|
462
|
+
echo "## Coverage Report"
|
|
463
|
+
echo ""
|
|
464
|
+
echo "Coverage thresholds: 80% (lines, branches, functions, statements)"
|
|
465
|
+
if [ -f "coverage/coverage-summary.json" ]; then
|
|
466
|
+
echo "Coverage report generated"
|
|
467
|
+
else
|
|
468
|
+
echo "Coverage report not found"
|
|
469
|
+
fi
|
|
470
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
471
|
+
|
|
472
|
+
e2e:
|
|
473
|
+
name: E2E Tests
|
|
474
|
+
runs-on: ubuntu-latest
|
|
475
|
+
timeout-minutes: 20
|
|
476
|
+
steps:
|
|
477
|
+
- name: Checkout code
|
|
478
|
+
uses: actions/checkout@v7
|
|
479
|
+
- name: Setup Node.js 24
|
|
480
|
+
uses: actions/setup-node@v7
|
|
481
|
+
with:
|
|
482
|
+
node-version: 24
|
|
483
|
+
- name: Setup Bun
|
|
484
|
+
uses: oven-sh/setup-bun@v2
|
|
485
|
+
with:
|
|
486
|
+
bun-version: __BUN_VERSION__
|
|
487
|
+
- name: Install dependencies
|
|
488
|
+
run: bun install
|
|
489
|
+
- name: Ensure test-results directory
|
|
490
|
+
run: |
|
|
491
|
+
node -e "require('node:fs').mkdirSync('test-results', { recursive: true })"
|
|
492
|
+
- name: Run E2E tests
|
|
493
|
+
run: bun test --timeout 15000 test/e2e/ --reporter=junit --reporter-outfile test-results/e2e.xml
|
|
494
|
+
- name: Build E2E profile
|
|
495
|
+
run: node scripts/test-profile.js --input-dir test-results --output test-results/e2e.profile.json --label e2e --integration-skipped true
|
|
496
|
+
- name: Publish E2E runtime summary
|
|
497
|
+
if: always()
|
|
498
|
+
shell: bash
|
|
499
|
+
run: |
|
|
500
|
+
dashboard_json="$(node scripts/test-dashboard.js --json --profiles-dir test-results)"
|
|
501
|
+
node -e "const d = JSON.parse(process.argv[1]); const lines = ['## E2E Runtime Summary', '', 'Suite duration: ' + d.suiteDurationMs + 'ms']; if (d.slowestFiles.length) { lines.push('', 'Slowest files:'); for (const entry of d.slowestFiles.slice(0, 5)) lines.push('- ' + entry.file + ': ' + entry.durationMs + 'ms'); } console.log(lines.join('\n'));" "$dashboard_json" >> "$GITHUB_STEP_SUMMARY"
|
|
502
|
+
- name: Upload E2E artifacts
|
|
503
|
+
uses: actions/upload-artifact@v7
|
|
504
|
+
if: always()
|
|
505
|
+
continue-on-error: true
|
|
506
|
+
with:
|
|
507
|
+
name: test-artifacts-e2e
|
|
508
|
+
path: test-results/
|
|
509
|
+
retention-days: 7
|
|
510
|
+
|
|
511
|
+
mutation:
|
|
512
|
+
name: Mutation Testing
|
|
513
|
+
runs-on: ubuntu-latest
|
|
514
|
+
timeout-minutes: 60
|
|
515
|
+
if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
|
|
516
|
+
steps:
|
|
517
|
+
- name: Checkout code
|
|
518
|
+
uses: actions/checkout@v7
|
|
519
|
+
- name: Setup Node.js 24
|
|
520
|
+
uses: actions/setup-node@v7
|
|
521
|
+
with:
|
|
522
|
+
node-version: 24
|
|
523
|
+
- name: Setup Bun
|
|
524
|
+
uses: oven-sh/setup-bun@v2
|
|
525
|
+
with:
|
|
526
|
+
bun-version: __BUN_VERSION__
|
|
527
|
+
- name: Install dependencies
|
|
528
|
+
run: bun install
|
|
529
|
+
- name: Setup test fixtures
|
|
530
|
+
run: bash test-env/automation/setup-fixtures.sh --force
|
|
531
|
+
- name: Run mutation testing
|
|
532
|
+
run: npx stryker run
|
|
533
|
+
- name: Upload mutation report
|
|
534
|
+
uses: actions/upload-artifact@v7
|
|
535
|
+
if: always()
|
|
536
|
+
continue-on-error: true
|
|
537
|
+
with:
|
|
538
|
+
name: mutation-report
|
|
539
|
+
path: stryker-report/
|
|
540
|
+
retention-days: 30
|
|
541
|
+
|
|
542
|
+
dashboard-pr:
|
|
543
|
+
name: Test Dashboard (PR)
|
|
544
|
+
if: github.event_name == 'pull_request'
|
|
545
|
+
runs-on: ubuntu-latest
|
|
546
|
+
needs: [unit-shard, windows-smoke, macos-smoke, coverage, e2e]
|
|
547
|
+
timeout-minutes: 15
|
|
548
|
+
steps:
|
|
549
|
+
- name: Checkout code
|
|
550
|
+
uses: actions/checkout@v7
|
|
551
|
+
- name: Download test artifacts
|
|
552
|
+
uses: actions/download-artifact@v8
|
|
553
|
+
with:
|
|
554
|
+
pattern: test-artifacts-*
|
|
555
|
+
path: test-results/downloaded
|
|
556
|
+
merge-multiple: true
|
|
557
|
+
- name: Generate test dashboard
|
|
558
|
+
run: node scripts/test-dashboard.js --profiles-dir test-results/downloaded
|
|
559
|
+
- name: Upload dashboard
|
|
560
|
+
uses: actions/upload-artifact@v7
|
|
561
|
+
continue-on-error: true
|
|
562
|
+
with:
|
|
563
|
+
name: test-dashboard
|
|
564
|
+
path: test-dashboard.json
|
|
565
|
+
retention-days: 7
|
|
566
|
+
- name: Dashboard summary
|
|
567
|
+
if: always()
|
|
568
|
+
run: |
|
|
569
|
+
echo "## Test Quality Dashboard" >> $GITHUB_STEP_SUMMARY
|
|
570
|
+
if [ -f test-dashboard.json ]; then
|
|
571
|
+
cat test-dashboard.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Tests: {d[\"testCount\"]}'); print(f'Suite duration: {d[\"suiteDurationMs\"]}ms'); print(f'Integration skipped: {d[\"integrationSkipped\"]}'); print('Slowest files:'); [print(f'- {item[\"file\"]}: {item[\"durationMs\"]}ms') for item in d.get(\"slowestFiles\", [])[:5]]" >> $GITHUB_STEP_SUMMARY 2>/dev/null || echo "Dashboard generated" >> $GITHUB_STEP_SUMMARY
|
|
572
|
+
fi
|
|
573
|
+
|
|
574
|
+
ci-gate:
|
|
575
|
+
# Single aggregate status for the whole Tests workflow. Mark THIS Required in
|
|
576
|
+
# branch protection instead of the individual lanes: it stays green when a lane
|
|
577
|
+
# is legitimately skipped by the path gate above, and red the moment any lane
|
|
578
|
+
# fails or is cancelled. Adding a lane to `needs` is all that is required to
|
|
579
|
+
# bring it under the gate.
|
|
580
|
+
name: CI Gate
|
|
581
|
+
if: ${{ always() }}
|
|
582
|
+
needs:
|
|
583
|
+
- changes
|
|
584
|
+
- full-matrix
|
|
585
|
+
- unit-shard
|
|
586
|
+
- windows-smoke
|
|
587
|
+
- macos-smoke
|
|
588
|
+
- cross-os-gate
|
|
589
|
+
- followup-tests
|
|
590
|
+
- coverage
|
|
591
|
+
- e2e
|
|
592
|
+
runs-on: ubuntu-latest
|
|
593
|
+
timeout-minutes: 10
|
|
594
|
+
steps:
|
|
595
|
+
- name: Aggregate lane results
|
|
596
|
+
shell: bash
|
|
597
|
+
env:
|
|
598
|
+
RESULTS: |
|
|
599
|
+
changes=${{ needs.changes.result }}
|
|
600
|
+
full-matrix=${{ needs.full-matrix.result }}
|
|
601
|
+
unit-shard=${{ needs.unit-shard.result }}
|
|
602
|
+
windows-smoke=${{ needs.windows-smoke.result }}
|
|
603
|
+
macos-smoke=${{ needs.macos-smoke.result }}
|
|
604
|
+
cross-os-gate=${{ needs.cross-os-gate.result }}
|
|
605
|
+
followup-tests=${{ needs.followup-tests.result }}
|
|
606
|
+
coverage=${{ needs.coverage.result }}
|
|
607
|
+
e2e=${{ needs.e2e.result }}
|
|
608
|
+
run: |
|
|
609
|
+
set -uo pipefail
|
|
610
|
+
echo "$RESULTS"
|
|
611
|
+
failed=0
|
|
612
|
+
while IFS='=' read -r lane result; do
|
|
613
|
+
[ -n "$lane" ] || continue
|
|
614
|
+
case "$result" in
|
|
615
|
+
success|skipped) ;;
|
|
616
|
+
*) echo "::error::Lane '$lane' finished as '$result'"; failed=1 ;;
|
|
617
|
+
esac
|
|
618
|
+
done <<< "$RESULTS"
|
|
619
|
+
if [ "$failed" -ne 0 ]; then
|
|
620
|
+
exit 1
|
|
621
|
+
fi
|
|
622
|
+
echo "All test lanes passed or were skipped by the path gate."
|
|
623
|
+
|
|
624
|
+
dashboard-confidence:
|
|
625
|
+
name: Test Dashboard (Confidence Lane)
|
|
626
|
+
if: github.event_name != 'pull_request'
|
|
627
|
+
runs-on: ubuntu-latest
|
|
628
|
+
needs: [full-matrix, coverage, e2e]
|
|
629
|
+
timeout-minutes: 15
|
|
630
|
+
steps:
|
|
631
|
+
- name: Checkout code
|
|
632
|
+
uses: actions/checkout@v7
|
|
633
|
+
- name: Download test artifacts
|
|
634
|
+
uses: actions/download-artifact@v8
|
|
635
|
+
with:
|
|
636
|
+
pattern: test-artifacts-*
|
|
637
|
+
path: test-results/downloaded
|
|
638
|
+
merge-multiple: true
|
|
639
|
+
- name: Generate test dashboard
|
|
640
|
+
run: node scripts/test-dashboard.js --profiles-dir test-results/downloaded
|
|
641
|
+
- name: Upload dashboard
|
|
642
|
+
uses: actions/upload-artifact@v7
|
|
643
|
+
continue-on-error: true
|
|
644
|
+
with:
|
|
645
|
+
name: test-dashboard
|
|
646
|
+
path: test-dashboard.json
|
|
647
|
+
retention-days: 7
|
|
648
|
+
- name: Dashboard summary
|
|
649
|
+
if: always()
|
|
650
|
+
run: |
|
|
651
|
+
echo "## Test Quality Dashboard" >> $GITHUB_STEP_SUMMARY
|
|
652
|
+
if [ -f test-dashboard.json ]; then
|
|
653
|
+
cat test-dashboard.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Tests: {d[\"testCount\"]}'); print(f'Suite duration: {d[\"suiteDurationMs\"]}ms'); print(f'Integration skipped: {d[\"integrationSkipped\"]}'); print('Slowest files:'); [print(f'- {item[\"file\"]}: {item[\"durationMs\"]}ms') for item in d.get(\"slowestFiles\", [])[:5]]" >> $GITHUB_STEP_SUMMARY 2>/dev/null || echo "Dashboard generated" >> $GITHUB_STEP_SUMMARY
|
|
654
|
+
fi
|