diff --git a/.github/workflows/publish-selfhost-docker.yml b/.github/workflows/publish-selfhost-docker.yml index 6540ac7cc..51715ff86 100644 --- a/.github/workflows/publish-selfhost-docker.yml +++ b/.github/workflows/publish-selfhost-docker.yml @@ -163,10 +163,121 @@ jobs: if-no-files-found: error retention-days: 1 + # Release gate: run the full selfhost-docker e2e project against the exact + # by-digest image each build leg pushed, before any user-visible tag moves. + # Build-only smoke misses startup crashes and feature-path breaks (a package + # resolved at runtime but missing from the packaged runtime shipped broken in + # v1.5.30 exactly this way); per-arch matters too — the libsql native-addon + # bug was arm64-only. + e2e: + needs: + - metadata + - build + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: blacksmith-4vcpu-ubuntu-2404 + - arch: arm64 + runner: blacksmith-4vcpu-ubuntu-2404-arm + runs-on: ${{ matrix.runner }} + timeout-minutes: 40 + permissions: + contents: read + packages: read + + steps: + - name: Checkout release tag + uses: actions/checkout@v4 + with: + ref: ${{ needs.metadata.outputs.release_tag }} + persist-credentials: false + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.11 + + - name: Cache Bun package cache + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-${{ runner.arch }}-bun-1.3.11-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-bun-1.3.11- + + # Scenario helpers spawn Node sidecars; pin the same Node 24 runtime the + # other workflows use. + - uses: actions/setup-node@v4 + with: + node-version: 24 + + - run: bun install --frozen-lockfile + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-${{ runner.arch }}-playwright-1.60.0 + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-playwright- + + # Install from e2e so bunx resolves ITS pinned playwright (the version + # the tests run against) rather than floating to the latest. + - name: Install Playwright Chromium + run: bunx playwright install --with-deps chromium chromium-headless-shell + working-directory: e2e + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Download image digest + uses: actions/download-artifact@v4 + with: + name: digests-${{ matrix.arch }} + path: ${{ runner.temp }}/digests + + - name: Resolve image reference + id: image_ref + shell: bash + env: + IMAGE: ${{ needs.metadata.outputs.image }} + run: | + set -euo pipefail + digest_file="$(find "$RUNNER_TEMP/digests" -maxdepth 1 -type f | head -n 1)" + if [ -z "$digest_file" ]; then + echo "No image digest was downloaded." + exit 1 + fi + ref="${IMAGE}@sha256:$(basename "$digest_file")" + echo "ref=$ref" >> "$GITHUB_OUTPUT" + docker pull "$ref" + + - name: Run selfhost-docker scenarios against the published digest + env: + E2E_SELFHOST_DOCKER_IMAGE: ${{ steps.image_ref.outputs.ref }} + run: bunx vitest run --project selfhost-docker --retry=2 + working-directory: e2e + + - name: Upload run artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-runs-selfhost-docker-${{ matrix.arch }} + path: | + e2e/runs/ + e2e/selfhost-docker.boot.log + retention-days: 7 + merge: needs: - metadata - build + - e2e runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: packages: write diff --git a/e2e/targets/selfhost-docker.ts b/e2e/targets/selfhost-docker.ts index 1c3d9dc30..147b6272d 100644 --- a/e2e/targets/selfhost-docker.ts +++ b/e2e/targets/selfhost-docker.ts @@ -6,12 +6,10 @@ // lives in setup/selfhost-docker.globalsetup.ts. import { Effect } from "effect"; -import { cookieConsentStrategy } from "@executor-js/mcporter"; - import { e2ePort } from "../src/ports"; import type { Identity, Target } from "../src/target"; import { runSelfhostContainer, stopSelfhostContainer } from "../setup/selfhost-docker.boot"; -import { SELFHOST_ADMIN, signInSession } from "./selfhost"; +import { forcedMcpConsent, SELFHOST_ADMIN, signInSession } from "./selfhost"; export const SELFHOST_DOCKER_PORT = e2ePort("E2E_SELFHOST_DOCKER_PORT", 5); export const SELFHOST_DOCKER_BASE_URL = @@ -35,11 +33,14 @@ export const selfhostDockerTarget = (): Target => ({ cookies, }; }), + // Same forced-consent completion as the selfhost dev target: the app forces + // `prompt=consent` on every MCP authorize, so authorize redirects to the + // relative `/mcp-consent?consent_code=...` screen instead of straight to the + // callback — mcporter's cookieConsentStrategy can't parse that redirect. mcpConsent: (identity: Identity) => - cookieConsentStrategy({ - appBaseUrl: SELFHOST_DOCKER_BASE_URL, + forcedMcpConsent(SELFHOST_DOCKER_BASE_URL, { email: identity.credentials?.email ?? SELFHOST_ADMIN.email, - password: identity.credentials?.password ?? SELFHOST_ADMIN.password, + password: identity.credentials?.password || SELFHOST_ADMIN.password, }), // A real deployment cycle, not a warm `docker restart`: graceful stop // (SIGTERM + docker's grace), remove the container, start a NEW one from diff --git a/e2e/targets/selfhost.ts b/e2e/targets/selfhost.ts index f88fd8763..008142cca 100644 --- a/e2e/targets/selfhost.ts +++ b/e2e/targets/selfhost.ts @@ -50,7 +50,7 @@ export const signInSession = async ( // mcporter's `cookieConsentStrategy` only handles the old direct-code redirect, // so this completes the screen the way the page does: sign in, drive authorize, // then POST the same `/api/auth/oauth2/consent` grant the Allow button fires. -const forcedMcpConsent = +export const forcedMcpConsent = (baseUrl: string, credentials: { readonly email: string; readonly password: string }) => async ({ authorizationUrl }: { authorizationUrl: string }): Promise<{ code: string }> => { const origin = new URL(baseUrl).origin;