History

Gate self-host docker publish on the e2e suite

The publish workflow pushed user-visible tags after only building the image, so a startup crash (v1.5.30's missing worker-bundler) or any feature-path break could ship. Add an e2e job between build and merge that pulls each arch's by-digest image and runs the full selfhost-docker vitest project against it on a matching runner; merge (the step that moves latest/version tags) now requires it. Also switch the selfhost-docker target to the same forcedMcpConsent completion the selfhost dev target uses: the app forces prompt=consent on every MCP authorize, so authorize redirects to the relative /mcp-consent screen and mcporter's cookieConsentStrategy throws on the relative URL. The oauth-client-handoff scenarios failed against the production image because of this.

Rhys Sullivan committed 4 months ago

Commit 42829b71 parent 724600c3 files · +119 −7Browse files

Unified · Split

3 files
  1. .github/workflows/publish-selfhost-docker.yml +111 −0
  2. e2e/targets/selfhost-docker.ts +7 −6
  3. e2e/targets/selfhost.ts +1 −1
.github/workflows/publish-selfhost-docker.yml+111−0
@@ -163,10 +163,121 @@ jobs: · 162 unmodified lines
163163 if-no-files-found: error
164164 retention-days: 1
165165
166+ # Release gate: run the full selfhost-docker e2e project against the exact
167+ # by-digest image each build leg pushed, before any user-visible tag moves.
168+ # Build-only smoke misses startup crashes and feature-path breaks (a package
169+ # resolved at runtime but missing from the packaged runtime shipped broken in
170+ # v1.5.30 exactly this way); per-arch matters too — the libsql native-addon
171+ # bug was arm64-only.
172+ e2e:
173+ needs:
174+ - metadata
175+ - build
176+ strategy:
177+ fail-fast: false
178+ matrix:
179+ include:
180+ - arch: amd64
181+ runner: blacksmith-4vcpu-ubuntu-2404
182+ - arch: arm64
183+ runner: blacksmith-4vcpu-ubuntu-2404-arm
184+ runs-on: ${{ matrix.runner }}
185+ timeout-minutes: 40
186+ permissions:
187+ contents: read
188+ packages: read
189+
190+ steps:
191+ - name: Checkout release tag
192+ uses: actions/checkout@v4
193+ with:
194+ ref: ${{ needs.metadata.outputs.release_tag }}
195+ persist-credentials: false
196+
197+ - uses: oven-sh/setup-bun@v2
198+ with:
199+ bun-version: 1.3.11
200+
201+ - name: Cache Bun package cache
202+ uses: actions/cache@v4
203+ with:
204+ path: ~/.bun/install/cache
205+ key: ${{ runner.os }}-${{ runner.arch }}-bun-1.3.11-${{ hashFiles('bun.lock') }}
206+ restore-keys: |
207+ ${{ runner.os }}-${{ runner.arch }}-bun-1.3.11-
208+
209+ # Scenario helpers spawn Node sidecars; pin the same Node 24 runtime the
210+ # other workflows use.
211+ - uses: actions/setup-node@v4
212+ with:
213+ node-version: 24
214+
215+ - run: bun install --frozen-lockfile
216+
217+ - name: Cache Playwright browsers
218+ uses: actions/cache@v4
219+ with:
220+ path: ~/.cache/ms-playwright
221+ key: ${{ runner.os }}-${{ runner.arch }}-playwright-1.60.0
222+ restore-keys: |
223+ ${{ runner.os }}-${{ runner.arch }}-playwright-
224+
225+ # Install from e2e so bunx resolves ITS pinned playwright (the version
226+ # the tests run against) rather than floating to the latest.
227+ - name: Install Playwright Chromium
228+ run: bunx playwright install --with-deps chromium chromium-headless-shell
229+ working-directory: e2e
230+
231+ - name: Log in to GHCR
232+ uses: docker/login-action@v3
233+ with:
234+ registry: ghcr.io
235+ username: ${{ github.actor }}
236+ password: ${{ secrets.GITHUB_TOKEN }}
237+
238+ - name: Download image digest
239+ uses: actions/download-artifact@v4
240+ with:
241+ name: digests-${{ matrix.arch }}
242+ path: ${{ runner.temp }}/digests
243+
244+ - name: Resolve image reference
245+ id: image_ref
246+ shell: bash
247+ env:
248+ IMAGE: ${{ needs.metadata.outputs.image }}
249+ run: |
250+ set -euo pipefail
251+ digest_file="$(find "$RUNNER_TEMP/digests" -maxdepth 1 -type f | head -n 1)"
252+ if [ -z "$digest_file" ]; then
253+ echo "No image digest was downloaded."
254+ exit 1
255+ fi
256+ ref="${IMAGE}@sha256:$(basename "$digest_file")"
257+ echo "ref=$ref" >> "$GITHUB_OUTPUT"
258+ docker pull "$ref"
259+
260+ - name: Run selfhost-docker scenarios against the published digest
261+ env:
262+ E2E_SELFHOST_DOCKER_IMAGE: ${{ steps.image_ref.outputs.ref }}
263+ run: bunx vitest run --project selfhost-docker --retry=2
264+ working-directory: e2e
265+
266+ - name: Upload run artifacts
267+ if: failure()
268+ uses: actions/upload-artifact@v4
269+ with:
270+ name: e2e-runs-selfhost-docker-${{ matrix.arch }}
271+ path: |
272+ e2e/runs/
273+ e2e/selfhost-docker.boot.log
274+ retention-days: 7
275+
166276 merge:
167277 needs:
168278 - metadata
169279 - build
280+ - e2e
170281 runs-on: blacksmith-4vcpu-ubuntu-2404
171282 permissions:
172283 packages: write
e2e/targets/selfhost-docker.ts+7−6
@@ -6,12 +6,10 @@ · 5 unmodified lines
66 // lives in setup/selfhost-docker.globalsetup.ts.
77 import { Effect } from "effect";
88
9−import { cookieConsentStrategy } from "@executor-js/mcporter";
10−
119 import { e2ePort } from "../src/ports";
1210 import type { Identity, Target } from "../src/target";
1311 import { runSelfhostContainer, stopSelfhostContainer } from "../setup/selfhost-docker.boot";
14−import { SELFHOST_ADMIN, signInSession } from "./selfhost";
12+import { forcedMcpConsent, SELFHOST_ADMIN, signInSession } from "./selfhost";
1513
1614 export const SELFHOST_DOCKER_PORT = e2ePort("E2E_SELFHOST_DOCKER_PORT", 5);
1715 export const SELFHOST_DOCKER_BASE_URL =
@@ -35,11 +33,14 @@ export const selfhostDockerTarget = (): Target => ({ · 17 unmodified lines
3533 cookies,
3634 };
3735 }),
36+ // Same forced-consent completion as the selfhost dev target: the app forces
37+ // `prompt=consent` on every MCP authorize, so authorize redirects to the
38+ // relative `/mcp-consent?consent_code=...` screen instead of straight to the
39+ // callback — mcporter's cookieConsentStrategy can't parse that redirect.
3840 mcpConsent: (identity: Identity) =>
39− cookieConsentStrategy({
40− appBaseUrl: SELFHOST_DOCKER_BASE_URL,
41+ forcedMcpConsent(SELFHOST_DOCKER_BASE_URL, {
4142 email: identity.credentials?.email ?? SELFHOST_ADMIN.email,
42− password: identity.credentials?.password ?? SELFHOST_ADMIN.password,
43+ password: identity.credentials?.password || SELFHOST_ADMIN.password,
4344 }),
4445 // A real deployment cycle, not a warm `docker restart`: graceful stop
4546 // (SIGTERM + docker's grace), remove the container, start a NEW one from
e2e/targets/selfhost.ts+1−1
@@ -50,7 +50,7 @@ export const signInSession = async ( · 49 unmodified lines
5050 // mcporter's `cookieConsentStrategy` only handles the old direct-code redirect,
5151 // so this completes the screen the way the page does: sign in, drive authorize,
5252 // then POST the same `/api/auth/oauth2/consent` grant the Allow button fires.
53−const forcedMcpConsent =
53+export const forcedMcpConsent =
5454 (baseUrl: string, credentials: { readonly email: string; readonly password: string }) =>
5555 async ({ authorizationUrl }: { authorizationUrl: string }): Promise<{ code: string }> => {
5656 const origin = new URL(baseUrl).origin;