Repository navigation
709 lines (641 loc) · 30.3 KB
/
Copy pathrelease-qa-gate.yml
File metadata and controls
709 lines (641 loc) · 30.3 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
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
name: Release QA Gate
# Spec 081 T1 — Release Qualification Gate.
#
# One reusable workflow that assembles the existing suites (test-api-e2e.sh,
# go race tests, scan-eval --gate) with the new server-type matrix
# (stdio/http/sse/docker/oauth: connect, list, call, reconnect) and the US2
# invariant checks (activity-log request-id correlation, counter deltas,
# quarantine end-to-end, upgrade-in-place). It produces ONE machine-readable
# verdict (gate-report.json) via internal/gatereport; the `verdict` job exits
# per that verdict so a caller (`release.yml` / `prerelease.yml`) that lists
# this workflow in a publish job's `needs:` publishes nothing on a red gate
# (FR-001, FR-002, FR-004).
#
# Triggered as a reusable workflow (workflow_call) by the publishers, or
# manually against any ref (workflow_dispatch — a dry run that publishes
# nothing and never counts as qualification for a later tag, FR-001a).
#
# T2 landed as an ADVISORY entry — it runs on every tag build and is reported,
# but cannot block publication (`continue-on-error` + a non-blocking manifest
# entry):
# advisory/web-ui-sweep — T2 Playwright sweep against the candidate
#
# T4 landed as a BLOCKING entry:
# invariant/surface-consistency — CLI / MCP / Web UI agree with the core's
# /api/v1/servers on every server's state
#
# T3 landed as an ADVISORY entry on a macOS runner:
# advisory/macos-smoke — builds the Swift tray + runs its unit tests,
# boots a core with an isolated data dir, runs
# the tray against it and asserts the socket
# handshake + a clean exit (FR-019/020).
# No Accessibility grant on hosted runners, so
# no mcpproxy-ui-test menu assertions yet.
# See docs/development/release-gate.md.
on:
workflow_call:
inputs:
ref:
description: "Ref to qualify (defaults to the caller's triggering commit)"
required: false
type: string
workflow_dispatch:
inputs:
ref:
description: "Ref to qualify (dry run — publishes nothing, FR-001a)"
required: false
type: string
seed_consistency_mismatch:
description: "Dry-run only: prove T4 fails — override surface:server:field=value (e.g. cli:gate-cons-live:health.level=degraded)"
required: false
type: string
permissions:
contents: read
env:
GO_VERSION: "1.26"
NODE_VERSION: "22"
# Shared report directory: every job writes its fragment here and uploads it;
# the verdict job merges them against the hardcoded gatereport manifest.
GATE_REPORT_DIR: gate-report
jobs:
# ---------------------------------------------------------------------------
# Build the candidate binaries ONCE (frontend build + embed + go build) and
# share them with the suite/matrix jobs (reuses e2e-tests.yml's build-once,
# download-everywhere artifact pattern). The candidate `mcpproxy` is the
# release-representative headless core (embedded frontend, version stamped so
# it never self-reports "development").
# ---------------------------------------------------------------------------
build-candidate:
name: Build candidate binaries
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.sha }}
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Set up Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Build frontend
run: cd frontend && npm ci && npm run build
- name: Copy frontend dist to embed location
shell: bash
run: |
rm -rf web/frontend
mkdir -p web/frontend/dist
cp -r frontend/dist/. web/frontend/dist/
# Recreate the tracked .gitkeep so //go:embed all:frontend/dist always
# has something to embed (matches release.yml / the Makefile target).
touch web/frontend/dist/.gitkeep
- name: Build candidate + fixtures + gate driver
shell: bash
env:
CGO_ENABLED: "0"
run: |
set -euo pipefail
VERSION="${{ inputs.ref || github.ref_name }}"
# -X paths must use the full module path or the ldflag is silently
# ignored and the binary self-reports "development" (see v0.47.0 QA).
LDFLAGS="-s -w -X main.version=${VERSION} -X github.com/smart-mcp-proxy/mcpproxy-go/internal/httpapi.buildVersion=${VERSION}"
mkdir -p dist-bin
# Candidate headless core (nogui matches the E2E build; no tray deps).
go build -tags nogui -ldflags "${LDFLAGS}" -o dist-bin/mcpproxy ./cmd/mcpproxy
# Server edition (Spec 107 round-3 cross-review finding, PR-D):
# test-api-e2e.sh's audit_log sub-test requires ./mcpproxy-server
# and hard-FAILS ("Audit log: server-edition binary present") when
# it is missing/non-executable rather than skipping — this job
# never built it, so every run of suite/api-e2e deterministically
# failed that test.
go build -tags server,nogui -ldflags "${LDFLAGS}" -o dist-bin/mcpproxy-server ./cmd/mcpproxy
go build -o dist-bin/mcpfixture ./cmd/mcpfixture
go build -o dist-bin/oauthserver ./tests/oauthserver/cmd/server
go build -o dist-bin/release-gate ./cmd/release-gate
./dist-bin/mcpproxy --version || true
ls -la dist-bin
- name: Upload candidate artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: gate-candidate
path: dist-bin/
retention-days: 1
# ---------------------------------------------------------------------------
# FR-003 suite: REST API E2E via the UNMODIFIED scripts/test-api-e2e.sh.
# ---------------------------------------------------------------------------
suite-api-e2e:
name: Suite — API E2E
needs: build-candidate
runs-on: ubuntu-latest
# 15m covered the API E2E suite alone; the cleanup-trap safety check
# below runs that same suite again as a subprocess (by design — it is
# proving the trap against a real invocation), so this budget doubled
# accordingly (review round 8, finding 5).
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.sha }}
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Set up Node.js # test-api-e2e.sh launches npx-based fixtures
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Ensure jq is present
run: jq --version
- name: Download candidate binaries
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: gate-candidate
path: dist-bin
- name: Stage binaries
run: |
chmod +x dist-bin/*
# test-api-e2e.sh expects the built core at ./mcpproxy (unmodified).
cp dist-bin/mcpproxy ./mcpproxy
# ...and its audit_log sub-test (Spec 107 PR-D) expects the
# server-edition binary at ./mcpproxy-server.
cp dist-bin/mcpproxy-server ./mcpproxy-server
- name: Run API E2E suite
run: |
./dist-bin/release-gate run-suite \
--name suite/api-e2e --report-dir "${GATE_REPORT_DIR}" \
-- bash scripts/test-api-e2e.sh
# Review round 8, finding 5: proves the E2E cleanup trap (the fix this
# PR ships) actually holds against a real invocation, not just a code
# read — starts a decoy `mcpproxy serve` on its own port/data dir, runs
# the full E2E script above's cleanup trap via test-api-e2e.sh, and
# asserts the decoy is still alive afterward. Needs the same built
# ./mcpproxy this job already staged, so it runs here rather than
# nowhere: before this, neither this script nor the hermetic
# descendant-pids.test.sh (see unit-tests.yml) was wired into any CI
# workflow, so a regression reintroducing the blanket
# `pkill -f "mcpproxy.*serve"` this PR removes would pass CI
# undetected.
- name: Run E2E cleanup-trap safety check
run: |
./dist-bin/release-gate run-suite \
--name suite/api-e2e-cleanup-check --report-dir "${GATE_REPORT_DIR}" \
-- bash scripts/test-api-e2e-cleanup-check.sh
- name: Upload report fragment
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: gate-fragment-api-e2e
path: ${{ env.GATE_REPORT_DIR }}/
retention-days: 1
if-no-files-found: warn
# ---------------------------------------------------------------------------
# FR-003 suite: race tests — personal (internal/...) + server edition.
# NOTE: FR-003 pins the full non-short entry points. If the full race suite
# empirically exceeds this job's budget on standard runners, fall back to
# `go test -short -race ...` HERE and note the FR-003 deviation loudly — the
# heavy property/timing tests still run unguarded in e2e-tests.yml's
# stress-tests job, so coverage is preserved.
# ---------------------------------------------------------------------------
suite-race:
name: Suite — race (personal + server)
needs: build-candidate
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.sha }}
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Download gate driver
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: gate-candidate
path: dist-bin
- name: Stage gate driver
run: chmod +x dist-bin/*
- name: Race — personal (internal/...)
# FR-003 deviation (documented above): the full non-short entry point
# `go test -race ./internal/...` pulls in the binary/protocol/E2E suites
# (TestBinary*, TestMCP*, TestE2E_*) which hard-Fatal without a staged
# `mcpproxy` binary and blow the 25m budget when run in one job. We fall
# back to the exact fast unit-race command proven green in e2e-tests.yml:
# `-short -race` + `-skip` for the binary-dependent entry points. Those
# skipped suites are NOT lost coverage — the candidate binary is exercised
# end-to-end by the server-type matrix job (matrix-invariants) here, and
# the heavy property/timing variants still run unguarded in e2e-tests.yml's
# stress-tests job.
run: |
./dist-bin/release-gate run-suite \
--name suite/unit-race --report-dir "${GATE_REPORT_DIR}" \
-- go test -short -race -timeout 30m \
-skip "Binary|MCP|E2E|TestInfoEndpoint|TestGracefulShutdownNoPanic|TestSocketInfoEndpoint" \
./internal/...
- name: Race — server edition
if: always() # always run so its fragment lands even if the personal suite failed
run: |
./dist-bin/release-gate run-suite \
--name suite/server-race --report-dir "${GATE_REPORT_DIR}" \
-- go test -tags server -race -timeout 20m ./internal/serveredition/... ./internal/oauth/...
- name: Upload report fragment
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: gate-fragment-race
path: ${{ env.GATE_REPORT_DIR }}/
retention-days: 1
if-no-files-found: warn
# ---------------------------------------------------------------------------
# FR-003 + FR-015 suite: scanner eval gate — runs UNCONDITIONALLY on the tag
# commit (independent of which paths the release changed). The eval.yml D2 job
# is path-filtered; this re-runs the exact same offline gate so the
# recall >= 0.90 / FP <= 0.05 guarantee holds for every shipped release.
# (The bare entry point in FR-003 omits the required --corpus flag; the full
# invocation below matches eval.yml so the gate actually runs.)
# ---------------------------------------------------------------------------
suite-scan-eval:
name: Suite — scan-eval gate
needs: build-candidate
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.sha }}
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Download gate driver
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: gate-candidate
path: dist-bin
- name: Stage gate driver
run: chmod +x dist-bin/*
- name: Run scan-eval gate
run: |
./dist-bin/release-gate run-suite \
--name suite/scan-eval --report-dir "${GATE_REPORT_DIR}" \
-- go run ./cmd/scan-eval \
--corpus specs/065-evaluation-foundation/datasets/detect_corpus_v1.json \
--gate --min-recall 0.90 --max-fp 0.05
- name: Upload report fragment
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: gate-fragment-scan-eval
path: ${{ env.GATE_REPORT_DIR }}/
retention-days: 1
if-no-files-found: warn
# ---------------------------------------------------------------------------
# US1 + US2: server-type matrix (stdio/http/sse/docker/oauth) + invariants.
# matrix leaves the candidate core running (state-file) so invariants can
# attach to the SAME live instance and assert over the matrix traffic; both
# driver invocations therefore run in ONE shell step so the backgrounded core
# survives between them. The upgrade-in-place check downloads the previous
# released binary from GitHub (needs network + GITHUB_TOKEN for API limits).
# ---------------------------------------------------------------------------
matrix-invariants:
name: Matrix + invariants
needs: build-candidate
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.sha }}
- name: Set up Go # build-fixture-image.sh builds the static fixture binary
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Download candidate binaries
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: gate-candidate
path: dist-bin
- name: Stage binaries
run: chmod +x dist-bin/*
- name: Docker preflight # FR-009: the docker cell must fail (not skip) without Docker
run: |
docker version
docker info
- name: Build fixture Docker image (FR-006/FR-009)
run: scripts/gate/build-fixture-image.sh
- name: Run matrix + invariants
env:
# Auth the upgrade-in-place check's GitHub API lookup (FR-014).
GITHUB_TOKEN: ${{ github.token }}
run: |
set -uo pipefail
WORK="${RUNNER_TEMP}/gate-work"
mkdir -p "$WORK" "${GATE_REPORT_DIR}"
# Matrix leaves the core running; --state-file lives OUTSIDE the report
# dir so the merger never mistakes it for a report fragment.
./dist-bin/release-gate matrix \
--binary ./dist-bin/mcpproxy \
--fixture ./dist-bin/mcpfixture \
--oauth-server ./dist-bin/oauthserver \
--report-dir "${GATE_REPORT_DIR}" \
--state-file "${WORK}/gate-state.json" \
--work-dir "$WORK"
MATRIX_RC=$?
echo "matrix exit=${MATRIX_RC}"
# Attach to the live core; run invariants + upgrade-in-place.
./dist-bin/release-gate invariants \
--state-file "${WORK}/gate-state.json" \
--report-dir "${GATE_REPORT_DIR}"
INV_RC=$?
echo "invariants exit=${INV_RC}"
# Surface a red job when either failed; fragments (written regardless)
# feed the verdict job, which is the source of truth.
if [ "${MATRIX_RC}" -ne 0 ] || [ "${INV_RC}" -ne 0 ]; then
exit 1
fi
- name: Upload report fragment
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: gate-fragment-matrix-invariants
path: ${{ env.GATE_REPORT_DIR }}/
retention-days: 1
if-no-files-found: warn
# ---------------------------------------------------------------------------
# T4 (US5, FR-018): surface-state consistency. Boots the same five-type
# server matrix (its matrix fragments go to a scratch dir — the
# matrix-invariants job reports those cells), then `release-gate
# consistency` adds quarantined/disabled/unhealthy/live servers and asserts
# that the CLI (`upstream list -o json`), the MCP `upstream_servers list` and
# the RENDERED Web UI (one long-lived Chromium page) agree with the core's
# /api/v1/servers on enabled, quarantined, connection state, health.level,
# health.admin_state, health.status and tool counts — settled, after a live
# disable with no page reload, and after a reload. BLOCKING: a post-retry
# divergence fails the gate with a per-field diff. The macOS tray is T3.
#
# Negative proof: dispatch this workflow with seed_consistency_mismatch set;
# the knob is read only on workflow_dispatch, never on a tag's workflow_call.
# ---------------------------------------------------------------------------
surface-consistency:
name: Surface consistency (CLI / MCP / Web UI vs core)
needs: build-candidate
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.sha }}
- name: Set up Go # build-fixture-image.sh builds the static fixture binary
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Set up Node.js # the Web UI reader drives Chromium via @playwright/test
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Download candidate binaries
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: gate-candidate
path: dist-bin
- name: Stage binaries
run: chmod +x dist-bin/*
- name: Install the locked Playwright + Chromium
run: |
cd e2e/web-ui-sweep
npm ci
npx playwright install --with-deps chromium
- name: Build fixture Docker image (docker cell)
run: scripts/gate/build-fixture-image.sh
- name: Run surface consistency check
env:
# Read only on a manual dry run; empty on every tag (workflow_call).
SEED_MISMATCH: ${{ github.event_name == 'workflow_dispatch' && inputs.seed_consistency_mismatch || '' }}
run: |
set -uo pipefail
WORK="${RUNNER_TEMP}/consistency-work"
mkdir -p "$WORK" "${GATE_REPORT_DIR}"
./dist-bin/release-gate matrix \
--binary ./dist-bin/mcpproxy \
--fixture ./dist-bin/mcpfixture \
--oauth-server ./dist-bin/oauthserver \
--report-dir "${WORK}/matrix-report" \
--state-file "${WORK}/gate-state.json" \
--work-dir "$WORK"
echo "matrix exit=$? (cells are reported by matrix-invariants)"
SEED_ARGS=()
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${SEED_MISMATCH}" ]; then
SEED_ARGS=(--seed-mismatch "${SEED_MISMATCH}")
fi
./dist-bin/release-gate consistency \
--state-file "${WORK}/gate-state.json" \
--report-dir "${GATE_REPORT_DIR}" \
--webui-script "${GITHUB_WORKSPACE}/e2e/web-ui-sweep/surface-snapshot.mjs" \
${SEED_ARGS[@]+"${SEED_ARGS[@]}"}
- name: Upload report fragment
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: gate-fragment-surface-consistency
path: ${{ env.GATE_REPORT_DIR }}/
retention-days: 1
if-no-files-found: warn
# ---------------------------------------------------------------------------
# T2 (US3, FR-016): Playwright Web UI sweep against the Web UI served by the
# CANDIDATE binary (embedded frontend, not a dev server), with a live stdio
# mcpfixture upstream so the servers/tools screens have real data.
#
# ADVISORY, deliberately: `continue-on-error: true` keeps a red sweep out of
# this reusable workflow's conclusion, so a publisher whose job `needs:` the
# gate still publishes. The sweep is nonetheless REPORTED — the driver runs it
# with --advisory, which records `advisory/web-ui-sweep` as `advisory-fail`,
# and the verdict job lists it under advisory_failures. Promote to blocking by
# flipping that manifest entry to Blocking:true and dropping continue-on-error.
#
# Setup is not reinvented here: scripts/run-web-smoke.sh is the same launcher
# used by hand (docs/development/web-ui-verification.md) — it boots a
# throwaway instance, installs Chromium, and runs e2e/web-ui-sweep.
# ---------------------------------------------------------------------------
web-ui-sweep:
name: Web UI sweep (advisory)
needs: build-candidate
runs-on: ubuntu-latest
timeout-minutes: 20
continue-on-error: true # advisory: never blocks the tag (FR-019 pattern)
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.sha }}
- name: Set up Node.js # the sweep installs @playwright/test + Chromium
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Download candidate binaries
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: gate-candidate
path: dist-bin
- name: Stage binaries
run: chmod +x dist-bin/*
- name: Run Web UI sweep
env:
MCPPROXY_BINARY_PATH: ${{ github.workspace }}/dist-bin/mcpproxy
MCPPROXY_FIXTURE_PATH: ${{ github.workspace }}/dist-bin/mcpfixture
ARTIFACT_DIR: ${{ github.workspace }}/tmp/web-smoke-artifacts
run: |
./dist-bin/release-gate run-suite \
--name advisory/web-ui-sweep --advisory --report-dir "${GATE_REPORT_DIR}" \
-- bash scripts/run-web-smoke.sh
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: web-ui-sweep-playwright-report
path: tmp/web-smoke-artifacts/
retention-days: 14
if-no-files-found: warn
- name: Upload report fragment
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: gate-fragment-web-ui-sweep
path: ${{ env.GATE_REPORT_DIR }}/
retention-days: 1
if-no-files-found: warn
# ---------------------------------------------------------------------------
# T3 (US4, FR-019/FR-020): macOS tray smoke on a hosted macOS runner.
#
# ADVISORY, deliberately, exactly like web-ui-sweep: `continue-on-error: true`
# keeps a red smoke out of this reusable workflow's conclusion (so a publisher
# whose job `needs:` the gate still publishes), and `release-gate run-suite
# --advisory` records `advisory/macos-smoke` as `advisory-fail`, which the
# verdict job lists under advisory_failures.
#
# PROMOTION CRITERION (FR-021): promote to blocking once this job has passed on
# 3 consecutive release tags with no flaky or infrastructure failure. Promotion
# is a one-line change per place: Blocking:true on the advisory/macos-smoke
# manifest entry (internal/gatereport) and drop `continue-on-error` here.
#
# It builds its own core and the Swift tray on the macOS runner (the shared
# build-candidate artifact is a linux binary), then runs the same script used
# by hand: scripts/test-macos-smoke.sh. No signing secrets, no notarization —
# the scratch .app is ad-hoc signed.
# ---------------------------------------------------------------------------
macos-smoke:
name: macOS tray smoke (advisory)
# macos-latest, like native-tests.yml: the native suite needs the newer Swift
# toolchain (macos-15 ships Swift 6.1, which cannot type-check
# ReviewPresentationTests.swift:132 in reasonable time).
runs-on: macos-latest
timeout-minutes: 30
continue-on-error: true # advisory: never blocks the tag (FR-020)
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.sha }}
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Show Swift toolchain
run: swift --version
- name: Build gate driver (macOS)
run: |
mkdir -p dist-bin
go build -o dist-bin/release-gate ./cmd/release-gate
- name: Run macOS tray smoke
env:
SMOKE_ARTIFACT_DIR: ${{ github.workspace }}/tmp/macos-smoke-artifacts
run: |
./dist-bin/release-gate run-suite \
--name advisory/macos-smoke --advisory --report-dir "${GATE_REPORT_DIR}" \
-- bash scripts/test-macos-smoke.sh
- name: Upload smoke logs
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: macos-smoke-logs
path: tmp/macos-smoke-artifacts/
retention-days: 14
if-no-files-found: warn
- name: Upload report fragment
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: gate-fragment-macos-smoke
path: ${{ env.GATE_REPORT_DIR }}/
retention-days: 1
if-no-files-found: warn
# ---------------------------------------------------------------------------
# Merge every fragment against the hardcoded gatereport manifest → one
# verdict. Runs even if upstream jobs failed (if: always()) so a missing
# blocking fragment is recorded as a FAIL (FR-004, fail-closed) rather than
# letting the reusable workflow conclude green. This job's exit code IS the
# gate verdict the publishers depend on.
# ---------------------------------------------------------------------------
verdict:
name: Gate verdict
needs: [build-candidate, suite-api-e2e, suite-race, suite-scan-eval, matrix-invariants, surface-consistency, web-ui-sweep, macos-smoke]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Download gate driver
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: gate-candidate
path: dist-bin
continue-on-error: true # if build-candidate failed there is no driver
- name: Download all report fragments
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: gate-fragment-*
path: ${{ env.GATE_REPORT_DIR }}
merge-multiple: true
continue-on-error: true # no fragments → merger fails every blocking entry
- name: Merge fragments and compute verdict
run: |
set -euo pipefail
mkdir -p "${GATE_REPORT_DIR}"
# Presence check, NOT an -x check: actions/upload-artifact strips the
# executable bit, so the downloaded driver is present but non-executable
# until the chmod below. A build-candidate failure instead yields NO file
# (download step is continue-on-error), which -f correctly detects.
if [ ! -f dist-bin/release-gate ]; then
echo "::error::candidate build failed — no gate driver; the gate cannot qualify this ref"
exit 1
fi
chmod +x dist-bin/release-gate
./dist-bin/release-gate report \
--report-dir "${GATE_REPORT_DIR}" \
--out "${GATE_REPORT_DIR}/gate-report.json"
- name: Upload gate report
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: gate-report
path: ${{ env.GATE_REPORT_DIR }}/gate-report.json
retention-days: 14
if-no-files-found: warn