Skip to content

Commit 5737599

Browse files
songoowclaude
andcommitted
ci: qualify the chat bundle in a browser off the critical path
Every heavy lane waited ~5 minutes for `chat-bundle`, of which ~4.5 minutes was the Playwright qualification, before downloading the artifact. Publish the bundle once it is built and verified, and run the same three browser smokes in a parallel `chat-bundle-browser` lane that consumes that exact artifact. `checks` now requires the browser lane, so `merge-gate` still cannot pass on a bundle that failed qualification. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: song <22676124+songoow@users.noreply.github.com>
1 parent 5ebeb55 commit 5737599

3 files changed

Lines changed: 74 additions & 25 deletions

File tree

‎.github/workflows/python-tests.yml‎

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,20 +105,43 @@ jobs:
105105
cache-dependency-path: apps/presentation/dashboard/package-lock.json
106106
- run: python scripts/chat_bundle.py build --install
107107
- run: python scripts/chat_bundle.py verify --source
108-
- name: Qualify the actual compiled UI before saving the artifact
109-
working-directory: apps/presentation/dashboard
110-
run: |
111-
./node_modules/.bin/playwright install --with-deps chromium
112-
npm run smoke:personal-workspace-packaged
113-
npm run smoke:chat-turn-acceptance-retry
114-
npm run smoke:chat-upgrade
108+
# Publish as soon as the bundle is built and verified so consumers start
109+
# in parallel with browser qualification; `checks` still requires it.
115110
- uses: actions/upload-artifact@v7
116111
with:
117112
name: chat-bundle-${{ github.sha }}
118113
path: loopx/web/chat/
119114
if-no-files-found: error
120115
retention-days: 7
121116

117+
chat-bundle-browser:
118+
needs: [changes, chat-bundle]
119+
if: needs.changes.outputs.core_tests == 'true'
120+
runs-on: ubuntu-latest
121+
timeout-minutes: 15
122+
steps:
123+
- uses: actions/checkout@v7
124+
- uses: actions/download-artifact@v7
125+
with:
126+
name: chat-bundle-${{ github.sha }}
127+
path: loopx/web/chat/
128+
- uses: actions/setup-python@v6
129+
with:
130+
python-version: "3.11"
131+
- uses: actions/setup-node@v6
132+
with:
133+
node-version: "24"
134+
cache: npm
135+
cache-dependency-path: apps/presentation/dashboard/package-lock.json
136+
- name: Qualify the actual compiled UI
137+
working-directory: apps/presentation/dashboard
138+
run: |
139+
npm ci --ignore-scripts
140+
./node_modules/.bin/playwright install --with-deps chromium
141+
npm run smoke:personal-workspace-packaged
142+
npm run smoke:chat-turn-acceptance-retry
143+
npm run smoke:chat-upgrade
144+
122145
kernel-static-checks:
123146
needs: [changes, chat-bundle]
124147
if: needs.changes.outputs.core_tests == 'true'
@@ -320,19 +343,21 @@ jobs:
320343
checks:
321344
# Preserve the required check name while exposing independent failure lanes.
322345
if: always() && needs.changes.outputs.core_tests == 'true'
323-
needs: [changes, kernel-static-checks, typescript-coverage, dashboard-acceptance]
346+
needs: [changes, kernel-static-checks, typescript-coverage, dashboard-acceptance, chat-bundle-browser]
324347
runs-on: ubuntu-latest
325348
timeout-minutes: 2
326349
steps:
327350
- name: Require kernel and Dashboard qualification
328351
env:
352+
BROWSER_RESULT: ${{ needs.chat-bundle-browser.result }}
329353
DASHBOARD_RESULT: ${{ needs.dashboard-acceptance.result }}
330354
KERNEL_RESULT: ${{ needs.kernel-static-checks.result }}
331355
TYPESCRIPT_RESULT: ${{ needs.typescript-coverage.result }}
332356
run: |
333357
test "$KERNEL_RESULT" = success
334358
test "$TYPESCRIPT_RESULT" = success
335359
test "$DASHBOARD_RESULT" = success
360+
test "$BROWSER_RESULT" = success
336361
337362
node-minimum-compatibility:
338363
needs: changes
@@ -707,7 +732,7 @@ jobs:
707732
- uses: actions/setup-python@v6
708733
with:
709734
python-version: "3.11"
710-
- name: Verify the source-bound, browser-qualified Dashboard artifact
735+
- name: Verify the source-bound Dashboard artifact
711736
run: python scripts/chat_bundle.py verify --source
712737

713738
merge-gate:

‎docs/development/frontend-delivery.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,12 @@ and freshness metadata, not a signature or a replacement for release attestation
4444

4545
## PR qualification and releases
4646

47-
PR CI builds a clean bundle once, exercises its actual pages in a browser, then
48-
uploads `chat-bundle-<checkout SHA>`. Consumers download that qualified artifact;
49-
both frontend-only and mixed/backend PRs pass through this producer. On pull requests the checkout SHA is GitHub's tested merge commit, which
47+
PR CI builds and verifies a clean bundle once, then uploads
48+
`chat-bundle-<checkout SHA>`. Consumers download that one artifact, and
49+
`chat-bundle-browser` exercises its actual pages in a browser in parallel. The
50+
`checks` aggregate requires that browser lane, so `merge-gate` cannot pass on
51+
an artifact that failed browser qualification. Both frontend-only and
52+
mixed/backend PRs pass through this producer. On pull requests the checkout SHA is GitHub's tested merge commit, which
5053
may differ from the branch head. Generated-file Git cleanliness is no longer a
5154
qualification gate. Browser, integrity and workflow gates remain required.
5255

‎tests/test_python_ci_workflow.py‎

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ def test_dashboard_acceptance_and_kernel_checks_run_independently() -> None:
3737
assert "python -m mypy" not in dashboard
3838

3939
assert "if: always() && needs.changes.outputs.core_tests == 'true'" in aggregate
40-
assert "needs: [changes, kernel-static-checks, typescript-coverage, dashboard-acceptance]" in aggregate
40+
assert (
41+
"needs: [changes, kernel-static-checks, typescript-coverage, "
42+
"dashboard-acceptance, chat-bundle-browser]"
43+
) in aggregate
44+
assert "needs.chat-bundle-browser.result" in aggregate
4145
assert "needs.kernel-static-checks.result" in aggregate
4246
assert "needs.typescript-coverage.result" in aggregate
4347
assert "needs.dashboard-acceptance.result" in aggregate
@@ -46,23 +50,27 @@ def test_dashboard_acceptance_and_kernel_checks_run_independently() -> None:
4650
@pytest.mark.parametrize("kernel", ["success", "failure", "cancelled", "skipped"])
4751
@pytest.mark.parametrize("typescript", ["success", "failure", "cancelled", "skipped"])
4852
@pytest.mark.parametrize("dashboard", ["success", "failure", "cancelled", "skipped"])
53+
@pytest.mark.parametrize("browser", ["success", "failure", "cancelled", "skipped"])
4954
def test_checks_aggregate_requires_every_parallel_lane(
50-
kernel: str, typescript: str, dashboard: str,
55+
kernel: str, typescript: str, dashboard: str, browser: str,
5156
) -> None:
5257
gate = WORKFLOW.split("name: Require kernel and Dashboard qualification", 1)[1]
5358
script = gate.split("run: |", 1)[1].split("\n\n node-minimum-compatibility:", 1)[0]
5459
result = subprocess.run(
5560
["bash", "-e", "-c", script],
5661
env={
5762
**os.environ,
63+
"BROWSER_RESULT": browser,
5864
"DASHBOARD_RESULT": dashboard,
5965
"KERNEL_RESULT": kernel,
6066
"TYPESCRIPT_RESULT": typescript,
6167
},
6268
capture_output=True,
6369
check=False,
6470
)
65-
assert (result.returncode == 0) == (kernel == typescript == dashboard == "success")
71+
assert (result.returncode == 0) == (
72+
kernel == typescript == dashboard == browser == "success"
73+
)
6674

6775

6876
def test_minimum_node_lane_exercises_sqlite_without_a_skip_list() -> None:
@@ -197,7 +205,10 @@ def test_merge_gate_runs_on_all_prs_and_checks_every_core_aggregate() -> None:
197205
for name, output in (("checks", "core_tests"), ("test-shard", "python_tests"), ("stage2c-suite", "stage2c_tests"), ("windows-powershell", "python_tests"), ("presentation", "presentation_tests")):
198206
job = WORKFLOW.split(f" {name}:\n", 1)[1].split(" steps:", 1)[0]
199207
if name == "checks":
200-
assert "needs: [changes, kernel-static-checks, typescript-coverage, dashboard-acceptance]" in job
208+
assert (
209+
"needs: [changes, kernel-static-checks, typescript-coverage, "
210+
"dashboard-acceptance, chat-bundle-browser]"
211+
) in job
201212
assert "if: always() && needs.changes.outputs.core_tests == 'true'" in job
202213
else:
203214
assert "needs: [changes, chat-bundle]" in job
@@ -207,15 +218,23 @@ def test_merge_gate_runs_on_all_prs_and_checks_every_core_aggregate() -> None:
207218
def test_presentation_exemption_retains_real_frontend_checks_and_force_full() -> None:
208219
job = WORKFLOW.split(" presentation:\n", 1)[1].split(" merge-gate:\n", 1)[0]
209220
assert "name: chat-bundle-${{ github.sha }}" in job
210-
producer = WORKFLOW.split(" chat-bundle:\n", 1)[1].split(" kernel-static-checks:\n", 1)[0]
211-
assert "npm run smoke:personal-workspace-packaged" in producer
212-
assert "npm run smoke:chat-turn-acceptance-retry" in producer
213-
assert "npm run smoke:chat-upgrade" in producer
221+
producer = WORKFLOW.split(" chat-bundle:\n", 1)[1].split(" chat-bundle-browser:\n", 1)[0]
222+
browser = WORKFLOW.split(" chat-bundle-browser:\n", 1)[1].split(" kernel-static-checks:\n", 1)[0]
223+
# The producer publishes a built, verified bundle; the browser lane
224+
# qualifies that same artifact in parallel and `checks` requires it.
225+
assert producer.index("chat_bundle.py verify --source") < producer.index("actions/upload-artifact")
226+
assert "smoke:" not in producer
227+
assert "needs: [changes, chat-bundle]" in browser
228+
assert "if: needs.changes.outputs.core_tests == 'true'" in browser
229+
assert "name: chat-bundle-${{ github.sha }}" in browser
230+
assert "chat_bundle.py build" not in browser
214231
assert (
215-
producer.index("npm run smoke:personal-workspace-packaged")
216-
< producer.index("npm run smoke:chat-turn-acceptance-retry")
217-
< producer.index("actions/upload-artifact")
232+
browser.index("actions/download-artifact")
233+
< browser.index("npm run smoke:personal-workspace-packaged")
234+
< browser.index("npm run smoke:chat-turn-acceptance-retry")
235+
< browser.index("npm run smoke:chat-upgrade")
218236
)
237+
assert "continue-on-error" not in browser
219238
assert "scripts/chat_bundle.py verify --source" in job
220239
assert "status --short --untracked-files=all -- loopx/web/chat" not in job
221240
assert "continue-on-error" not in job
@@ -348,9 +367,11 @@ def test_four_shards_execute_each_test_once_and_merge_portable_coverage(
348367

349368

350369
def test_backend_and_mixed_prs_require_the_browser_qualified_artifact() -> None:
351-
producer = WORKFLOW.split(" chat-bundle:\n", 1)[1].split(" kernel-static-checks:\n", 1)[0]
370+
producer = WORKFLOW.split(" chat-bundle:\n", 1)[1].split(" chat-bundle-browser:\n", 1)[0]
352371
assert "needs.changes.outputs.core_tests == 'true'" in producer
353-
for name in ("kernel-static-checks", "typescript-core", "dashboard-acceptance", "test-shard", "stage2c-suite", "windows-powershell", "presentation"):
372+
aggregate = WORKFLOW.split(" checks:\n", 1)[1].split(" steps:", 1)[0]
373+
assert "chat-bundle-browser" in aggregate
374+
for name in ("chat-bundle-browser", "kernel-static-checks", "typescript-core", "dashboard-acceptance", "test-shard", "stage2c-suite", "windows-powershell", "presentation"):
354375
job = WORKFLOW.split(f" {name}:\n", 1)[1].split(" - uses: actions/setup-", 1)[0]
355376
assert "needs: [changes, chat-bundle]" in job
356377
assert "name: chat-bundle-${{ github.sha }}" in job

0 commit comments

Comments
 (0)