From ab9c36692b0d65908691f0132ef8031a6b300d34 Mon Sep 17 00:00:00 2001 From: Dan Zyto Date: Fri, 14 Aug 2026 13:56:28 +0200 Subject: [PATCH 1/5] =?UTF-8?q?ci:=20wire=20the=20DEV-2203=20suites=20?= =?UTF-8?q?=E2=80=94=20post-deploy=20smoke,=20weekly=20canary,=20monthly?= =?UTF-8?q?=20matrix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit e2e-live.yml grows three ways in: the manual dispatch it always had (now with ai and pkg_pr_new_ref inputs), a weekly Monday prod canary for external drift (the hosted bundler, npm, the broker, docs-bucket rot — deploy breakage is caught at deploy time, so daily would only buy triage), and a workflow_call smoke mode both deploy workflows now invoke after their curl checks: the @smoke subset, one container, minutes. The full deployed run folds in the specs nothing ever ran (row-striping, preview-recovery's live half, import-live) plus the DEV-2203 additions, split by what they hold: the sandpack/API-read step runs parallel, the container step strictly --workers=1 against the global pool of five, the authed share round-trip treats a rotten E2E_BROKER_TOKEN as a notice rather than a red run, and the AI step runs on the canary or by asking. e2e-starter-matrix.yml gains the monthly sweep (1st, 05:00 UTC) — the only coverage of the full starters × majors surface, at a cadence matched to npm-release timescales. Concurrency keeps deployed runs uncancellable so no run ever strands container sessions. --- .github/workflows/deploy-runner-api.yml | 10 + .github/workflows/deploy-runner-authoring.yml | 11 + .github/workflows/e2e-live.yml | 199 ++++++++++++------ .github/workflows/e2e-starter-matrix.yml | 16 +- runner/AGENTS.md | 8 +- 5 files changed, 174 insertions(+), 70 deletions(-) diff --git a/.github/workflows/deploy-runner-api.yml b/.github/workflows/deploy-runner-api.yml index 05c9372f..5b9167a3 100644 --- a/.github/workflows/deploy-runner-api.yml +++ b/.github/workflows/deploy-runner-api.yml @@ -80,3 +80,13 @@ jobs: done echo "::error::prod /api/health did not return 200 after deploy" exit 1 + + # Post-deploy smoke (DEV-2203): the @smoke E2E subset against prod. The + # health curl above proves the worker answers; this boots one container — + # exactly the thing this deploy just rebuilt — and renders a grid in it. + smoke: + needs: deploy + uses: ./.github/workflows/e2e-live.yml + with: + base_url: 'https://demos.handsontable.com' + smoke: true diff --git a/.github/workflows/deploy-runner-authoring.yml b/.github/workflows/deploy-runner-authoring.yml index 3ca277b4..cb2338f3 100644 --- a/.github/workflows/deploy-runner-authoring.yml +++ b/.github/workflows/deploy-runner-authoring.yml @@ -93,3 +93,14 @@ jobs: done echo "::error::prod frontend does not serve the freshly built bundle" exit 1 + + # Post-deploy smoke (DEV-2203): the @smoke E2E subset against the freshly + # deployed frontend — one render per engine, a share fixture, a docs example + # and one Style round-trip. The curl check above proves the bundle shipped; + # this proves the shipped bundle still puts a grid on screen. + smoke: + needs: deploy + uses: ./.github/workflows/e2e-live.yml + with: + base_url: 'https://demos.handsontable.com' + smoke: true diff --git a/.github/workflows/e2e-live.yml b/.github/workflows/e2e-live.yml index b2677605..3e26e77d 100644 --- a/.github/workflows/e2e-live.yml +++ b/.github/workflows/e2e-live.yml @@ -1,38 +1,31 @@ name: E2E — live render (Sandpack bundler) -# Runs the E2E_LIVE=1 checks — the tests that need the external hosted Sandpack -# bundler (or a real Tier-2 container) and therefore stay out of the -# deterministic PR suite (ci.yml): +# Runs everything the deterministic PR suite (ci.yml) cannot: the tests that +# need the external hosted Sandpack bundler, a real Tier-2 container, the +# deployed API worker, or real LLM budget. # -# - docs-examples.spec.ts what actually renders inside a Tier-1 sandbox: -# grid boots, plugins work (context menu opens, -# getPlugin() succeeds), compiled JSX executes. -# Both DEV-2129 regressions (dead plugins, "React is -# not defined") were invisible to PR CI. -# - style-apply.spec.ts whether a generated theme module reaches the grid, -# one case per wiring shape (DEV-2197). The only -# suite that *executes* generated theme source; the -# node tests read it as text, and every defect it -# found was green under those. +# Three ways in (DEV-2203): +# - workflow_dispatch — the manual run. base_url empty (default) builds the +# selected ref and tests it via a local `vite preview`; base_url set tests +# the deployed app directly. `ai: true` adds the LLM answer checks; +# `pkg_pr_new_ref` boots one container at a pkg.pr.new build (DEV-2198 +# validation days). +# - schedule — the weekly prod canary (Mon 05:00 UTC), deployed mode with AI +# on. Deploy-time breakage is caught by the post-deploy smoke below; the +# canary exists for external drift — the hosted bundler, npm, the broker, +# docs-bucket rot — which moves on week-scale. Daily would burn container +# hours and turn every bundler blip into a triage. +# - workflow_call with `smoke: true` — the post-deploy subset both deploy +# workflows invoke: `--grep @smoke`, one container, a few minutes. # -# Manual only (not a merge gate — owner decision): depends on an external -# service, so a bundler outage would block merges. Run it: -# - before merging any change to packages/runtime (sandpack.ts, transpile.ts) -# or pipeline/import-docs.mjs — pick your branch in the dispatch UI; -# - after a deploy / docs-bucket regen, with base_url pointed at prod. +# Not a merge gate — owner decision: all of it depends on external services, +# so an outage would block merges. # -# Two modes: -# - base_url empty (default): builds the workspace from the selected ref and -# tests it via a local `vite preview` (playwright.config.ts webServer) — -# validates YOUR CODE against the live bundler before it ships. -# - base_url set (e.g. https://demos.handsontable.com): tests the deployed -# app directly — post-deploy smoke; no build needed. -# -# Only the deployed mode can cover the whole Style panel suite. Its `astro` and -# `angular` cases are Tier 2: they need the API worker reachable on the *same -# origin*, and local mode serves the app with `vite preview`, whose config has no -# API proxy (the proxy lives under `server:`). Locally those two are skipped -# rather than left to fail on a preview stuck at `booting`. +# Only the deployed mode can cover the container suites. Tier-2 cases need the +# API worker reachable on the *same origin*, and local mode serves the app with +# `vite preview`, whose config has no API proxy (the proxy lives under +# `server:`). Backend-bound specs self-gate on E2E_BASE_URL for the same +# reason — locally they skip instead of failing on a preview stuck at booting. on: workflow_dispatch: @@ -41,34 +34,53 @@ on: description: 'Deployed app URL to test (leave empty to build and test the selected ref locally)' default: '' required: false + ai: + description: 'Also run the live LLM answer checks (spends budget)' + type: boolean + default: false + pkg_pr_new_ref: + description: 'pkg.pr.new build id to verify end to end in a container (DEV-2198)' + default: '' + required: false + schedule: + # Weekly prod canary. Monday 05:00 UTC — before the CET workday, off-peak + # for the container pool. + - cron: '0 5 * * 1' + workflow_call: + inputs: + base_url: + type: string + required: true + smoke: + type: boolean + default: false permissions: contents: read -# Cancelling is safe in local mode only. That premise used to hold for the whole -# workflow — Tier-1 render checks hold nothing server-side — but the deployed -# Style panel step boots Tier-2 containers, and the spec has no teardown: its -# sessions are left to expire. Killing a run mid-flight therefore strands them in -# the global `Sandbox` pool of 5 and starves live previews, which is why -# `e2e-starter-matrix.yml` disables cancellation outright. -# -# The group is keyed by mode as well as by ref. Sharing one group would let a new -# *local* run — which is cancellable — cancel an in-flight *deployed* run, and -# that is precisely the leak this avoids. +# Cancelling is safe in local mode only: deployed runs boot Tier-2 containers +# and a killed run strands its sessions in the global `Sandbox` pool of 5 +# (specs clean up in `finally`, which a cancellation never reaches). The group +# is keyed by mode so a new cancellable *local* run can never cancel an +# in-flight *deployed* one, and smoke calls key by run id so two deploys never +# queue behind each other's smoke. concurrency: - group: e2e-live-${{ github.ref }}-${{ inputs.base_url != '' && 'deployed' || 'local' }} - cancel-in-progress: ${{ inputs.base_url == '' }} + group: e2e-live-${{ github.ref }}-${{ inputs.smoke && format('smoke-{0}', github.run_id) || (inputs.base_url != '' && 'deployed' || (github.event_name == 'schedule' && 'canary' || 'local')) }} + cancel-in-progress: ${{ github.event_name == 'workflow_dispatch' && inputs.base_url == '' && !inputs.smoke }} jobs: live: runs-on: ubuntu-latest - # Local live run takes seconds once built; leave headroom for a cold - # hosted-bundler transpile of the heavier examples. The deployed run also - # boots four real containers for the Style panel suite, one at a time. - timeout-minutes: 60 + # Smoke: minutes. Full deployed run: the sandpack suite plus ~8 container + # boots, strictly one at a time. Local run: seconds once built. + timeout-minutes: 90 defaults: run: working-directory: runner + env: + # One place resolves the target: dispatch/call input, or prod when the + # weekly canary fires (schedule events carry no inputs). + BASE_URL: ${{ inputs.base_url || (github.event_name == 'schedule' && 'https://demos.handsontable.com' || '') }} steps: - uses: actions/checkout@v4 @@ -85,7 +97,7 @@ jobs: - run: pnpm install --frozen-lockfile - name: Build workspace + authoring app (local mode only) - if: inputs.base_url == '' + if: env.BASE_URL == '' run: | pnpm build pnpm --filter @handsontable/demo-authoring build @@ -93,31 +105,94 @@ jobs: - name: Install Playwright browser run: pnpm exec playwright install --with-deps chromium - - name: E2E with live-render checks + # ---- Post-deploy smoke: the @smoke subset and nothing else. ---------- + - name: Smoke — one render per engine, one share, one docs example, one Style round-trip + if: inputs.smoke + env: + E2E_LIVE: '1' + E2E_BASE_URL: ${{ env.BASE_URL }} + run: pnpm e2e --grep "@smoke" --workers=1 + + # ---- The full run (everything below skips when smoke). --------------- + + # Sandpack-only and API-read-only specs: no containers held, parallel-safe. + # Backend-bound tests inside these files self-skip in local mode. + - name: E2E — Sandpack live renders, docs examples, editor, versions, share viewer + if: '!inputs.smoke' env: E2E_LIVE: '1' - E2E_BASE_URL: ${{ inputs.base_url }} - run: pnpm e2e e2e/docs-examples.spec.ts - - # A separate step, and `--workers=1`: the Tier-2 cases each hold a - # live-preview container, prod caps those at 5 concurrently *globally* - # (Sandbox max_instances), and sessions are not torn down between tests. In - # parallel the second one fails with the preview stuck on `booting`, which - # reads as a product failure and is not one. - - name: E2E — Style panel reaches the grid (deployed, all wiring shapes) - if: inputs.base_url != '' + E2E_BASE_URL: ${{ env.BASE_URL }} + run: > + pnpm e2e + e2e/docs-examples.spec.ts + e2e/preview-recovery.spec.ts + e2e/import-live.spec.ts + e2e/row-striping.spec.ts + e2e/preview-downloads.spec.ts + e2e/editor-download.spec.ts + e2e/version-pinning.spec.ts + e2e/share-view.spec.ts + e2e/style-panel.spec.ts + + # Container suites, one worker: each Tier-2 case holds a live-preview + # container, prod caps those at 5 *globally* (Sandbox max_instances), and + # a parallel second boot reads as a product failure without being one. + - name: E2E — container suites (Style shapes, docs frameworks, engine smoke) + if: ${{ !inputs.smoke && env.BASE_URL != '' }} env: E2E_LIVE: '1' - E2E_BASE_URL: ${{ inputs.base_url }} - run: pnpm e2e e2e/style-apply.spec.ts --workers=1 + E2E_BASE_URL: ${{ env.BASE_URL }} + run: > + pnpm e2e + e2e/style-apply.spec.ts + e2e/docs-frameworks.spec.ts + e2e/engine-smoke.spec.ts + --workers=1 - # Local mode: Tier-1 shapes only — see the note at the top of this file. + # DEV-2198 validation: one real container at a pkg.pr.new build. + - name: E2E — pkg.pr.new build end to end + if: ${{ !inputs.smoke && env.BASE_URL != '' && inputs.pkg_pr_new_ref != '' }} + env: + E2E_BASE_URL: ${{ env.BASE_URL }} + E2E_PKG_PR_NEW_REF: ${{ inputs.pkg_pr_new_ref }} + run: pnpm e2e e2e/version-pinning.spec.ts -g "pkg.pr.new build boots" --workers=1 + + # Local mode: Tier-1 Style shapes only — see the note at the top. - name: E2E — Style panel reaches the grid (local build, Tier 1 only) - if: inputs.base_url == '' + if: ${{ !inputs.smoke && env.BASE_URL == '' }} env: E2E_LIVE: '1' run: pnpm e2e e2e/style-apply.spec.ts --workers=1 -g "react|vue|javascript|switching" + # The authed share round-trip needs a hand-refreshed broker token + # (AGENTS.md § E2E). An absent or expired token is a notice, not a red + # run — the secret rots by design and rot is not a product failure. + - name: E2E — authed share round-trip (secret-gated) + if: ${{ !inputs.smoke && env.BASE_URL != '' }} + env: + E2E_BASE_URL: ${{ env.BASE_URL }} + E2E_BROKER_TOKEN: ${{ secrets.E2E_BROKER_TOKEN }} + run: | + if [ -z "$E2E_BROKER_TOKEN" ]; then + echo "::notice::E2E_BROKER_TOKEN is not set — the authed share round-trip was skipped." + exit 0 + fi + code=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $E2E_BROKER_TOKEN" \ + "https://mcp-auth-proxy-j0tb.onrender.com/broker/userinfo" || true) + if [ "$code" != "200" ]; then + echo "::warning::E2E_BROKER_TOKEN no longer validates against the broker ($code) — refresh the secret to run the authed share round-trip." + exit 0 + fi + pnpm e2e e2e/share-create-live.spec.ts --workers=1 + + # Two real LLM calls. On by dispatch input and on the weekly canary. + - name: E2E — AI endpoints answer usably + if: ${{ !inputs.smoke && env.BASE_URL != '' && (inputs.ai || github.event_name == 'schedule') }} + env: + E2E_AI: '1' + E2E_BASE_URL: ${{ env.BASE_URL }} + run: pnpm e2e e2e/ai-live.spec.ts + - name: Upload Playwright report on failure if: failure() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/e2e-starter-matrix.yml b/.github/workflows/e2e-starter-matrix.yml index d1f927a3..5129c9e3 100644 --- a/.github/workflows/e2e-starter-matrix.yml +++ b/.github/workflows/e2e-starter-matrix.yml @@ -10,9 +10,12 @@ name: E2E — starter compatibility matrix # Anonymous session creation on prod works (the app only sends an Authorization # header when a login token exists), so no secrets are required here. # -# Manual only — it spins ~50 real container sessions. A local run at --workers=2 -# --retries=2 took ~36 min (46 passed / 27 skipped); allow headroom for CI + -# flake retries. +# Manual, plus a monthly sweep (1st of the month, 05:00 UTC — off-peak for the +# container pool). It spins ~50 real container sessions; a local run at +# --workers=2 --retries=2 took ~36 min (46 passed / 27 skipped); allow headroom +# for CI + flake retries. The monthly cadence exists because this is the only +# coverage of the full starters × majors surface — weekly would double the +# container-hours for drift that moves on npm-release timescales (DEV-2203). # # SINGLE JOB, --workers=2. Do NOT "optimize" this into a parallel matrix: prod # caps live-preview Sandboxes at 5 CONCURRENT *globally* (not per-job), so N @@ -26,6 +29,10 @@ on: description: 'Deployed app to test against (must have a real API backend)' default: 'https://demos.handsontable.com' required: true + schedule: + # Monthly full sweep; schedule events carry no inputs, so the run step + # falls back to prod below. + - cron: '0 5 1 * *' # Two overlapping runs = 4 workers against the global 5-container cap, leaving # real traffic 1 slot. cancel-in-progress: false (opposite of ci.yml) on purpose: @@ -68,7 +75,8 @@ jobs: - name: Run starter matrix env: - E2E_BASE_URL: ${{ inputs.base_url }} + # The monthly schedule carries no inputs — fall back to prod. + E2E_BASE_URL: ${{ inputs.base_url || 'https://demos.handsontable.com' }} # e2e:matrix already sets E2E_STARTER_MATRIX=1, PLAYWRIGHT_JSON_OUTPUT_NAME # and --workers=2 --retries=1 --reporter=list,json. run: pnpm e2e:matrix diff --git a/runner/AGENTS.md b/runner/AGENTS.md index 92291212..e32f6da8 100644 --- a/runner/AGENTS.md +++ b/runner/AGENTS.md @@ -162,10 +162,10 @@ Seven workflows live in `.github/workflows/` at the repo root: | Workflow | Trigger | What it does | |----------|---------|--------------| | `ci.yml` | every PR + push to `master` | build, typecheck, unit + catalog-smoke tests, authoring build, Playwright e2e. Also `workflow_call`able, so the deploy workflows gate on it. | -| `deploy-runner-api.yml` | push to `master` touching `workers/api`, `containers`, `scripts`, `config`, `packages` (or manual) | deploys `workers/api`. | -| `deploy-runner-authoring.yml` | push to `master` touching `apps/authoring`, `packages`, `config`, **`catalog.json`** (or manual) | builds + deploys `apps/authoring`. | -| `e2e-live.yml` | manual | the `E2E_LIVE=1` specs that mount a real preview. | -| `e2e-starter-matrix.yml` | manual | every starter through a live session; serialized against the global container cap. | +| `deploy-runner-api.yml` | push to `master` touching `workers/api`, `containers`, `scripts`, `config`, `packages` (or manual) | deploys `workers/api`, then calls the `@smoke` E2E subset against prod. | +| `deploy-runner-authoring.yml` | push to `master` touching `apps/authoring`, `packages`, `config`, **`catalog.json`** (or manual) | builds + deploys `apps/authoring`, then calls the `@smoke` E2E subset against prod. | +| `e2e-live.yml` | manual, weekly canary (Mon 05:00 UTC, prod + AI), or `workflow_call` with `smoke: true` from the deploy workflows | everything ci.yml cannot run: live renders, container suites, the share viewer/round-trip, AI answer checks. Dispatch inputs: `base_url`, `ai`, `pkg_pr_new_ref` (DEV-2198). | +| `e2e-starter-matrix.yml` | manual + monthly (1st, 05:00 UTC) | every starter × major through a live session; serialized against the global container cap. | | `import-docs.yml` | manual, or `repository_dispatch: docs-examples-sync` from the docs repo | re-imports the documentation-guide examples. | | `import-starters.yml` | manual, `repository_dispatch: starter-examples-sync`, weekly cron, or push touching `examples/**` | re-imports the versioned starter buckets (each from `prod-examples/` when the branch exists, else `master`), rebuilds the catalog index + container contexts, opens a PR. | From 9e958c1863875cc811a393fb2a7c8aaac3d92174 Mon Sep 17 00:00:00 2001 From: Dan Zyto Date: Mon, 17 Aug 2026 08:37:27 +0200 Subject: [PATCH 2/5] ci: queue authoring deploys instead of cancelling them (Bugbot #189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The post-deploy smoke boots a live-preview container; an unconditional cancel-in-progress killed the whole run — smoke included — on the next push to master, stranding that session in the global Sandbox pool (cleanup lives in a finally a cancellation never reaches). Deploys now queue, matching deploy-runner-api.yml. The other #189 finding — e2e-live listing style-panel.spec.ts before it existed on this branch — is resolved by linearising the stack: the spec now sits in this branch's ancestry (185 → 186 → 187 → here). --- .github/workflows/deploy-runner-authoring.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-runner-authoring.yml b/.github/workflows/deploy-runner-authoring.yml index cb2338f3..c61ffdcf 100644 --- a/.github/workflows/deploy-runner-authoring.yml +++ b/.github/workflows/deploy-runner-authoring.yml @@ -25,9 +25,14 @@ on: - ".github/workflows/ci.yml" workflow_dispatch: {} +# cancel-in-progress was true until the post-deploy smoke arrived (DEV-2203): +# the smoke boots a live-preview container, and cancelling a run mid-smoke +# strands that session in the global Sandbox pool (cleanup lives in a `finally` +# a cancellation never reaches). Deploys now queue instead — same posture as +# deploy-runner-api.yml. concurrency: group: deploy-runner-authoring - cancel-in-progress: true + cancel-in-progress: false jobs: # Gate: run the full CI suite (typecheck, unit/smoke, e2e) before deploying. From b62ed738220fe78703cba23fbe2ba59cbb921767 Mon Sep 17 00:00:00 2001 From: Dan Zyto Date: Mon, 17 Aug 2026 08:46:25 +0200 Subject: [PATCH 3/5] ci: keep the broker JWT out of artifacts, and de-collide the crons (Bugbot #189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The authed-share step puts a live session JWT in sessionStorage and an Authorization header; a Playwright trace records both, GitHub does not redact secrets inside artifact zips, and this repo is public — so a failed canary would have published a valid @handsontable.com session for seven days. That step now runs with --trace off --retries=0 and scrubs test-results/ and playwright-report/ before the on-failure upload can see them (|| status capture: Actions runs bash -e, so a plain $? after a failing command is unreachable). The monthly starter matrix moves to 03:00 on the 1st: at 05:00 a Monday-the-1st would start it alongside the weekly canary, and matrix --workers=2 plus the canary's serialized container suite against the same global pool of five is exactly the contention both workflow headers warn about. --- .github/workflows/e2e-live.yml | 13 +++++++++++-- .github/workflows/e2e-starter-matrix.yml | 6 ++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/e2e-live.yml b/.github/workflows/e2e-live.yml index 3e26e77d..34ed15c8 100644 --- a/.github/workflows/e2e-live.yml +++ b/.github/workflows/e2e-live.yml @@ -44,7 +44,8 @@ on: required: false schedule: # Weekly prod canary. Monday 05:00 UTC — before the CET workday, off-peak - # for the container pool. + # for the container pool. The monthly starter matrix runs at 03:00 on the + # 1st so a Monday-the-1st never starts both against the same five slots. - cron: '0 5 * * 1' workflow_call: inputs: @@ -183,7 +184,15 @@ jobs: echo "::warning::E2E_BROKER_TOKEN no longer validates against the broker ($code) — refresh the secret to run the authed share round-trip." exit 0 fi - pnpm e2e e2e/share-create-live.spec.ts --workers=1 + # Secret hygiene (Bugbot, #189): this run puts a live session JWT in + # sessionStorage and an Authorization header — a Playwright trace + # records both, GitHub does not redact secrets inside artifact zips, + # and this repo is public. No traces, no retries, and every artifact + # of this step is scrubbed before the on-failure upload can see it. + status=0 + pnpm e2e e2e/share-create-live.spec.ts --workers=1 --retries=0 --trace off || status=$? + rm -rf test-results playwright-report + exit $status # Two real LLM calls. On by dispatch input and on the weekly canary. - name: E2E — AI endpoints answer usably diff --git a/.github/workflows/e2e-starter-matrix.yml b/.github/workflows/e2e-starter-matrix.yml index 5129c9e3..5abd4671 100644 --- a/.github/workflows/e2e-starter-matrix.yml +++ b/.github/workflows/e2e-starter-matrix.yml @@ -31,8 +31,10 @@ on: required: true schedule: # Monthly full sweep; schedule events carry no inputs, so the run step - # falls back to prod below. - - cron: '0 5 1 * *' + # falls back to prod below. 03:00, not 05:00: the weekly e2e-live canary + # fires Mondays at 05:00, and a Monday-the-1st must not start both against + # the same global pool of five container slots (Bugbot, #189). + - cron: '0 3 1 * *' # Two overlapping runs = 4 workers against the global 5-container cap, leaving # real traffic 1 slot. cancel-in-progress: false (opposite of ci.yml) on purpose: From b695c50abdd96a23202f5c3d14eaa6d1d86a954b Mon Sep 17 00:00:00 2001 From: Dan Zyto Date: Wed, 19 Aug 2026 09:48:15 +0200 Subject: [PATCH 4/5] ci: master deploys first and verifies production after (Dan, #184 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test gate before the deploys is gone: the full suite runs on every PR, each deploy job runs its own pnpm build (a broken build cannot reach wrangler), and what a master push needs verified is production — which only exists after the deploy. The @smoke e2e subset is that verification, and the weekly canary covers the rest. Re-running unit on every merge bought nothing the PR run had not already proven. The compensating control for merge skew (a PR tested before master moved) is branch protection's 'require branches to be up to date' — a repo setting, recommended in the PR body, not a workflow job. --- .github/workflows/ci.yml | 7 +++--- .github/workflows/master.yml | 41 ++++++++++++++++++------------------ runner/AGENTS.md | 4 ++-- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8ed858c..a01a2695 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,10 @@ name: CI # behind E2E_LIVE (e2e-live.yml). Also callable (workflow_call) so the deploy # workflows can gate on the whole DAG. -# No push trigger of its own: master pushes run this exactly once through -# master.yml's `test` job (workflow_call). Before that split, one push touching -# runner/packages/** ran this suite three times. +# No push trigger of its own, and master.yml does not call it either: the full +# suite runs on every PR, and a master push goes straight to deploy with the +# @smoke subset verifying production afterwards (master.yml). workflow_call +# stays for any future caller; workflow_dispatch for by-hand runs. on: pull_request: {} workflow_call: {} diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 8b4b21fb..75b57bee 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -1,18 +1,22 @@ name: Master -# The single master-push pipeline (the handsontable monorepo's develop.yml -# pattern). Before this existed, one push touching runner/packages/** ran the -# CI suite three times — ci.yml's own push trigger plus a `uses: ci.yml` gate -# inside each deploy workflow. Now: one CI run, and the two deploys hang off -# it as conditional jobs, path-gated by a plain `git diff` (the push-level -# `paths:` filter can't be used once the deploys share a workflow). +# The single master-push pipeline: deploy first, verify production after. # -# changes ─┐ -# ├─► deploy-authoring (if authoring paths changed) -# test ────┤ -# └─► deploy-api (if api paths changed) +# changes ─┬─► deploy-authoring ─┐ +# └─► deploy-api ───────┴─► smoke (@smoke e2e against prod) # -# Manual deploys: workflow_dispatch with the two checkboxes. +# No test gate before the deploys, deliberately (Dan, #184 review): the full +# suite already ran on the PR, each deploy job runs its own `pnpm build` (a +# broken build cannot reach wrangler), and what a master push needs verified +# is PRODUCTION — which only exists after the deploy. The smoke job is that +# verification; the weekly e2e-live canary covers the rest. The compensating +# control for merge skew (a PR tested before master moved) is branch +# protection's "require branches to be up to date" — a repo setting, not a +# workflow. +# +# Deploys are path-gated by a plain `git diff` (the push-level `paths:` filter +# can't be used once the deploys share a workflow). Manual deploys: +# workflow_dispatch with the two checkboxes. on: push: @@ -80,13 +84,8 @@ jobs: echo "authoring=$authoring" >> "$GITHUB_OUTPUT" echo "api=$api" >> "$GITHUB_OUTPUT" - # The one CI run per master push — the deploys gate on it, and it doubles as - # the post-merge canary for pushes that deploy nothing. - test: - uses: ./.github/workflows/ci.yml - deploy-authoring: - needs: [changes, test] + needs: [changes] if: needs.changes.outputs.authoring == 'true' runs-on: ubuntu-latest defaults: @@ -110,9 +109,9 @@ jobs: # Build workspace packages, then the authoring app. VITE_API_BASE comes # from apps/authoring/.env.production (committed) so it targets prod. - run: pnpm build - # SENTRY_* are only set here, on the deploying build. The `test` job - # reuses ci.yml, which gets no token, so PR builds neither emit source - # maps nor upload a release — see apps/authoring/vite.config.ts. + # SENTRY_* are only set here, on the deploying build. PR CI (ci.yml) + # gets no token, so PR builds neither emit source maps nor upload a + # release — see apps/authoring/vite.config.ts. - run: pnpm --filter @handsontable/demo-authoring build env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} @@ -164,7 +163,7 @@ jobs: smoke: true deploy-api: - needs: [changes, test] + needs: [changes] if: needs.changes.outputs.api == 'true' runs-on: ubuntu-latest defaults: diff --git a/runner/AGENTS.md b/runner/AGENTS.md index 46acb4a3..07c4c517 100644 --- a/runner/AGENTS.md +++ b/runner/AGENTS.md @@ -169,8 +169,8 @@ Six workflows live in `.github/workflows/` at the repo root: | Workflow | Trigger | What it does | |----------|---------|--------------| -| `ci.yml` | every PR; `workflow_call` from `master.yml` and manual dispatch | the reusable CI DAG: build → authoring → e2e (in the pinned Playwright container), with unit in parallel. No push trigger of its own — master runs it once through `master.yml`. | -| `master.yml` | every push to `master` (or manual dispatch with per-target checkboxes) | one CI run + path-gated deploys (`deploy-authoring`, `deploy-api`), each followed by the `@smoke` E2E subset against prod. Replaces the two `deploy-runner-*.yml` workflows, whose per-workflow CI gates ran the suite up to three times per push. | +| `ci.yml` | every PR (+ manual dispatch) | the CI DAG: presence + unit → build → authoring → e2e (in the pinned Playwright container). PRs are the only place the full suite runs — master does not repeat it. | +| `master.yml` | every push to `master` (or manual dispatch with per-target checkboxes) | deploy-first: path-gated `deploy-authoring`/`deploy-api` (each self-builds — a broken build never reaches wrangler), then one `@smoke` E2E run against prod. Merge-skew is covered by branch protection ("require branches to be up to date"), not by re-running the suite. | | `e2e-live.yml` | manual, weekly canary (Mon 05:00 UTC, prod + AI), or `workflow_call` with `smoke: true` from `master.yml` | everything ci.yml cannot run: live renders, container suites, the share viewer/round-trip, AI answer checks. Dispatch inputs: `base_url`, `ai`, `pkg_pr_new_ref` (DEV-2198). | | `e2e-starter-matrix.yml` | manual + monthly (1st, 03:00 UTC) | every starter × major through a live session; serialized against the global container cap. | | `import-docs.yml` | manual, or `repository_dispatch: docs-examples-sync` from the docs repo | re-imports the documentation-guide examples. | From 1c5c44192680be9cd192099c7ce272b3d76a9b90 Mon Sep 17 00:00:00 2001 From: Dan Zyto Date: Wed, 19 Aug 2026 12:25:01 +0200 Subject: [PATCH 5/5] fix(runner): afterEach owns the demo id from either source (Bugbot #186) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wire capture is un-awaited and its json read best-effort — if it lost the race, a demo existed (the dialog was showing its link) while afterEach saw null and skipped the revoke. The dialog link's id is now the recovery path: demoId ??= linkId before any assertion can throw, and the two sources are asserted to agree when both answered. --- runner/e2e/share-create-live.spec.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/runner/e2e/share-create-live.spec.ts b/runner/e2e/share-create-live.spec.ts index 3be1a77b..ddd81d85 100644 --- a/runner/e2e/share-create-live.spec.ts +++ b/runner/e2e/share-create-live.spec.ts @@ -94,8 +94,16 @@ test("a demo shared today is a page a client can open — until it is revoked", // The textbox role, not getByLabel: the field's copy button is named // "Copy Public client link" and would collide under strict mode. const clientLink = await dialog.getByRole("textbox", { name: /client link/i }).inputValue(); - expect(demoId, "the mint response carried a demo id").toBeTruthy(); - expect(clientLink, "the dialog's client link names the minted demo").toContain(demoId!); + + // The wire capture is un-awaited and its json read is best-effort — if it + // lost the race, a demo would exist (the dialog is showing its link) while + // afterEach saw null and skipped the revoke (Bugbot, #186). The link is the + // recovery path: whichever source answers, afterEach ends up owning the id + // of any demo that now exists. + const linkId = new URL(clientLink).pathname.split("/").filter(Boolean).pop() ?? null; + demoId ??= linkId; + expect(demoId, "a demo id, from the mint response or the dialog's link").toBeTruthy(); + expect(linkId, "the dialog's client link names the minted demo").toBe(demoId); // The built page renders for an anonymous client (fresh context state not // needed — /d is public and static, cookies play no part).