From 27676bb63f472043c534bceaecf45745e8bfee16 Mon Sep 17 00:00:00 2001 From: Oliver Stenbom Date: Wed, 7 Oct 2026 18:04:45 -0700 Subject: [PATCH 1/9] ci: run product Playwright coverage on Endform with isolated fixtures --- .github/workflows/ci.yml | 35 +++++++-------- e2e/endform-server.ts | 56 ++++++++++++++++++++++++ e2e/fixtures/index.ts | 25 ++++++++++- e2e/fixtures/refresh-server-pat.ts | 8 ++++ e2e/fixtures/warm-up-admin.js | 22 ++++++++++ e2e/global-setup.ts | 37 +++++----------- e2e/tests/content-crud.spec.ts | 1 + endform.config.ts | 7 +++ package.json | 2 + playwright.config.ts | 24 +++++++---- pnpm-lock.yaml | 68 ++++++++++++++++++++++++++++++ 11 files changed, 231 insertions(+), 54 deletions(-) create mode 100644 e2e/endform-server.ts create mode 100644 e2e/fixtures/warm-up-admin.js create mode 100644 endform.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6230ee4996..b75685ebd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -412,16 +412,21 @@ jobs: - run: echo "E2E results are aggregated by the Tests job" test-e2e: - name: E2E shard (${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) + name: E2E Endform (${{ matrix.target }}) needs: change-plan if: needs.change-plan.outputs.e2e_node == 'true' || needs.change-plan.outputs.e2e_cloudflare == 'true' runs-on: ubuntu-latest - timeout-minutes: 40 + timeout-minutes: 120 + permissions: + contents: read + id-token: write + env: + ENDFORM_GIT_BRANCH: ${{ github.head_ref || github.ref_name }} + ENDFORM_GIT_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }} strategy: - fail-fast: true + fail-fast: false matrix: - shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] - shardTotal: [8] + target: [node, cloudflare] steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -434,28 +439,20 @@ jobs: - run: pnpm install --frozen-lockfile - name: Build Node fixture id: node-build - if: ${{ !cancelled() && needs.change-plan.outputs.e2e_node == 'true' }} + if: ${{ !cancelled() && needs.change-plan.outputs.e2e_node == 'true' && matrix.target == 'node' }} run: pnpm run --filter emdash... build - name: Build Cloudflare fixture id: cloudflare-build - if: ${{ !cancelled() && needs.change-plan.outputs.e2e_cloudflare == 'true' }} + if: ${{ !cancelled() && needs.change-plan.outputs.e2e_cloudflare == 'true' && matrix.target == 'cloudflare' }} run: pnpm run --filter "emdash-e2e-fixture-cloudflare..." build - - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - if: ${{ !cancelled() && (steps.node-build.outcome == 'success' || steps.cloudflare-build.outcome == 'success') }} - id: playwright-cache - with: - path: ~/.cache/ms-playwright - key: playwright-${{ hashFiles('pnpm-lock.yaml') }} - - run: pnpm exec playwright install --with-deps chromium - if: ${{ !cancelled() && (steps.node-build.outcome == 'success' || steps.cloudflare-build.outcome == 'success') && steps.playwright-cache.outputs.cache-hit != 'true' }} - name: Run Node E2E id: node-e2e if: ${{ !cancelled() && steps.node-build.outcome == 'success' }} - run: pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + run: npx endform@latest test --organization-id 2G1ZCj7X - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() && steps.node-e2e.outcome == 'failure' with: - name: playwright-report-node-${{ matrix.shardIndex }} + name: playwright-report-node-endform path: | playwright-report/ test-results/ @@ -463,13 +460,13 @@ jobs: - name: Run Cloudflare E2E id: cloudflare-e2e if: ${{ !cancelled() && steps.cloudflare-build.outcome == 'success' }} - run: pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + run: npx endform@latest test --organization-id 2G1ZCj7X env: EMDASH_E2E_TARGET: cloudflare - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() && steps.cloudflare-e2e.outcome == 'failure' with: - name: playwright-report-cloudflare-${{ matrix.shardIndex }} + name: playwright-report-cloudflare-endform path: | playwright-report/ test-results/ diff --git a/e2e/endform-server.ts b/e2e/endform-server.ts new file mode 100644 index 0000000000..231552e8fe --- /dev/null +++ b/e2e/endform-server.ts @@ -0,0 +1,56 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { createServer } from "node:http"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import globalSetup from "./global-setup.js"; +import globalTeardown from "./global-teardown.js"; + +const serverInfoPath = join(tmpdir(), "emdash-pw-server.json"); + +await globalSetup(); + +const server = createServer(async (request, response) => { + if (request.url === "/ready" && request.method === "GET") { + response.end("ready"); + return; + } + if (request.url !== "/server-info") { + response.writeHead(404).end(); + return; + } + if (request.method === "GET") { + response.setHeader("Content-Type", "application/json"); + response.end(readFileSync(serverInfoPath)); + return; + } + if (request.method === "POST") { + let body = ""; + for await (const chunk of request) body += chunk; + const update: { token?: unknown } = JSON.parse(body); + if (typeof update.token !== "string") { + response.writeHead(400).end(); + return; + } + const info = JSON.parse(readFileSync(serverInfoPath, "utf8")); + info.token = update.token; + writeFileSync(serverInfoPath, JSON.stringify(info)); + response.end(); + return; + } + response.writeHead(405).end(); +}); + +server.listen(4446, "127.0.0.1"); + +let stopping = false; +async function stop() { + if (stopping) return; + stopping = true; + server.close(); + await globalTeardown(); + process.exit(0); +} + +process.on("SIGINT", stop); +process.on("SIGTERM", stop); diff --git a/e2e/fixtures/index.ts b/e2e/fixtures/index.ts index a17c004a49..d84e458a5b 100644 --- a/e2e/fixtures/index.ts +++ b/e2e/fixtures/index.ts @@ -6,13 +6,14 @@ * the AdminPage helper and server context to each test. */ -import { readFileSync } from "node:fs"; +import { readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test as base } from "@playwright/test"; import { AdminPage } from "./admin"; +import { warmUpAdmin } from "./warm-up-admin.js"; export { AdminPage } from "./admin"; @@ -40,7 +41,29 @@ function getServerInfo(): ServerInfo { export const test = base.extend<{ admin: AdminPage; serverInfo: ServerInfo; + _endformServerState: void; }>({ + page: [ + async ({ page }, use) => { + if (process.env.ENDFORM === "true") { + await warmUpAdmin(page, process.env.BASE_URL || "http://localhost:4444"); + } + await use(page); + }, + { timeout: 240_000 }, + ], + _endformServerState: [ + // oxlint-disable-next-line no-empty-pattern -- Playwright requires destructured fixture dependencies + async ({}, use) => { + if (process.env.ENDFORM === "true") { + const response = await fetch("http://localhost:4446/server-info"); + if (!response.ok) throw new Error("Unable to load fixture server state"); + writeFileSync(SERVER_INFO_PATH, await response.text()); + } + await use(); + }, + { auto: true }, + ], // eslint-disable-next-line no-empty-pattern serverInfo: async ({}, use) => { await use(getServerInfo()); diff --git a/e2e/fixtures/refresh-server-pat.ts b/e2e/fixtures/refresh-server-pat.ts index 8fb56d026d..914eb535c8 100644 --- a/e2e/fixtures/refresh-server-pat.ts +++ b/e2e/fixtures/refresh-server-pat.ts @@ -21,4 +21,12 @@ export async function refreshServerPatAfterDevBypass(baseUrl: string): Promise { - const { chromium } = await import("@playwright/test"); - const browser = await chromium.launch(); - try { - const page = await browser.newPage(); - // The second load starts after any optimizer reload from the first, so - // its hydration means the dependency set has settled. - for (let load = 0; load < 2; load++) { - try { - await page.goto(`${baseUrl}/_emdash/admin/login`, { - waitUntil: "commit", - timeout: 120_000, - }); - await page.waitForSelector("astro-island:not([ssr])", { timeout: 120_000 }); - await page.waitForLoadState("networkidle", { timeout: 60_000 }); - } catch (error) { - // Non-fatal: a cold first admin test can still pass on retry. - console.warn(`[pw] Admin warm-up load ${load + 1} failed:`, error); - } - } - } finally { - await browser.close(); - } -} - // --------------------------------------------------------------------------- // Global setup // --------------------------------------------------------------------------- @@ -429,7 +406,15 @@ export default async function globalSetup(): Promise { // discovered when a browser requests them, and the optimizer then forces // a full reload that would otherwise land inside the first admin test. console.log("[pw] Warming up admin..."); - await warmUpAdmin(baseUrl); + if (process.env.ENDFORM !== "true") { + const { chromium } = await import("@playwright/test"); + const browser = await chromium.launch(); + try { + await warmUpAdmin(await browser.newPage(), baseUrl); + } finally { + await browser.close(); + } + } // 6. Write server info const info = { diff --git a/e2e/tests/content-crud.spec.ts b/e2e/tests/content-crud.spec.ts index cec6e45120..7e7fcb598e 100644 --- a/e2e/tests/content-crud.spec.ts +++ b/e2e/tests/content-crud.spec.ts @@ -19,6 +19,7 @@ const CONTENT_ID_PATTERN = /\/content\/posts\/[A-Z0-9]+(?:\?.*)?$/; const NEW_CONTENT_URL_PATTERN = /\/content\/posts\/new(?:[?#].*)?$/; test.describe("Content CRUD", () => { + test.describe.configure({ mode: "serial" }); test.beforeEach(async ({ admin }) => { await admin.devBypassAuth(); }); diff --git a/endform.config.ts b/endform.config.ts new file mode 100644 index 0000000000..564778f1a5 --- /dev/null +++ b/endform.config.ts @@ -0,0 +1,7 @@ +import { defineEndformConfig } from "endform"; + +export default defineEndformConfig({ + additionalFiles: ["e2e/fixtures/assets/test-image.png"], + proxyNetworkHosts: [""], + concurrentTestLimits: [{ scope: "within-suite-run", limit: 1 }], +}); diff --git a/package.json b/package.json index d44c2e9d82..3933a9fe90 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@types/node": "catalog:", "@typescript/native-preview": "7.0.0-dev.20260421.2", "emdash": "workspace:*", + "endform": "0.81.3", "knip": "^5.84.1", "oxfmt": "^0.59.0", "oxlint": "^1.74.0", @@ -57,6 +58,7 @@ "pkg-pr-new": "^0.0.78", "prettier": "^3.9.5", "prettier-plugin-astro": "^0.14.1", + "tsx": "4.21.0", "typescript": "6.0.0-beta" }, "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b", diff --git a/playwright.config.ts b/playwright.config.ts index 62ba752804..eb2cd0df9f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,7 @@ import { defineConfig, devices } from "@playwright/test"; +const isEndform = process.env.ENDFORM === "true"; + /** * Playwright E2E test configuration for EmDash CMS * @@ -11,8 +13,8 @@ import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ testDir: "./e2e/tests", testIgnore: "portable-text-table.spec.ts", - // Disable parallel to avoid shared database state issues - fullyParallel: false, + // Endform limits concurrency around the shared fixture in endform.config.ts. + fullyParallel: isEndform, workers: 1, forbidOnly: !!process.env.CI, retries: process.env.CI ? 1 : 0, @@ -23,12 +25,20 @@ export default defineConfig({ // first hit; give it headroom so cold compilation doesn't time out specs. timeout: process.env.EMDASH_E2E_TARGET === "cloudflare" ? 90_000 : 30000, - globalSetup: "./e2e/global-setup.ts", - globalTeardown: "./e2e/global-teardown.ts", + globalSetup: isEndform ? undefined : "./e2e/global-setup.ts", + globalTeardown: isEndform ? undefined : "./e2e/global-teardown.ts", + webServer: isEndform + ? { + command: "pnpm exec tsx e2e/endform-server.ts", + url: "http://127.0.0.1:4446/ready", + reuseExistingServer: false, + timeout: 240_000, + } + : undefined, use: { - baseURL: "http://localhost:4444", - trace: "on-first-retry", + baseURL: process.env.BASE_URL || "http://localhost:4444", + trace: "retain-on-failure", screenshot: "only-on-failure", }, @@ -38,6 +48,4 @@ export default defineConfig({ use: { ...devices["Desktop Chrome"] }, }, ], - - // No webServer — global-setup.ts handles server lifecycle }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e03384404..b3c03097af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -374,6 +374,9 @@ importers: emdash: specifier: workspace:* version: link:packages/core + endform: + specifier: 0.81.3 + version: 0.81.3 knip: specifier: ^5.84.1 version: 5.84.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.10.13)(typescript@6.0.0-beta) @@ -395,6 +398,9 @@ importers: prettier-plugin-astro: specifier: ^0.14.1 version: 0.14.1 + tsx: + specifier: 4.21.0 + version: 4.21.0 typescript: specifier: 6.0.0-beta version: 6.0.0-beta @@ -13265,6 +13271,41 @@ packages: zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + endform-darwin-arm64@0.81.3: + resolution: {integrity: sha512-BV3Ji195mR8GnxTbz+FDBS2wPKymW0o/xzxGz4TbWU0IdD4FLLsx4TZBm/VVEvkrnHnxXeJQhAaN2L+m4YTHVw==} + cpu: [arm64] + os: [darwin] + + endform-darwin-x86@0.81.3: + resolution: {integrity: sha512-w9/t/7X09tzGN5CQMISWXPYr5axRbqbAwjSTq0MWwZCF+sq25SjbSppllqzfCGAgieHJdmiMft6/RffRzAlmcg==} + cpu: [x64] + os: [darwin] + + endform-linux-arm64@0.81.3: + resolution: {integrity: sha512-YR0FMjgx66Y3IwX/8ZvBtooqTKt05GwHjKKuc14raYK+0c3qxArjP/jU/eyjhAc3ybd8YQ9jcTQjszvc8cf9OA==} + cpu: [arm64] + os: [linux] + + endform-linux-x86@0.81.3: + resolution: {integrity: sha512-TVahPFdCvq2dbyO7WtUjcbmHLNT+dXKB1YImEQ7gDXTLpv9AOigpkN8jYmK1SXgNHiwpibM7dGQYULOLw+oEDg==} + cpu: [x64] + os: [linux] + + endform-win32-arm64@0.81.3: + resolution: {integrity: sha512-etfYsOPqesPE1RIxzHKBPy/aICoFcCd1rCkPUw3xRBMZsCwYDHMFC/awrRN8d7Bav98T6ht4jyI8k07tsbVruQ==} + cpu: [arm64] + os: [win32] + + endform-win32-x86@0.81.3: + resolution: {integrity: sha512-2Oz2ys88AS5GbuyUvpUq5KdtPIv6I0DxRiwf+mWd/Onh2diPoTGD/cfZyU3GYF3GYtWFXmc7OKbEOeZiVeDRDw==} + cpu: [x64] + os: [win32] + + endform@0.81.3: + resolution: {integrity: sha512-VnlmxakSs1u7N1CjOp1hRtCDcBnmNwUH9UALAoxG2LRU3fWpUJ5lNw0s7cARqZTn5OM9F0iJYBvnlaRrOICApw==} + hasBin: true + + snapshots: '@adraffy/ens-normalize@1.11.1': {} @@ -24507,3 +24548,30 @@ snapshots: tslib: 2.3.0 zwitch@2.0.4: {} + + endform-darwin-arm64@0.81.3: + optional: true + + endform-darwin-x86@0.81.3: + optional: true + + endform-linux-arm64@0.81.3: + optional: true + + endform-linux-x86@0.81.3: + optional: true + + endform-win32-arm64@0.81.3: + optional: true + + endform-win32-x86@0.81.3: + optional: true + + endform@0.81.3: + optionalDependencies: + endform-darwin-arm64: 0.81.3 + endform-darwin-x86: 0.81.3 + endform-linux-arm64: 0.81.3 + endform-linux-x86: 0.81.3 + endform-win32-arm64: 0.81.3 + endform-win32-x86: 0.81.3 From 32992da58a1624b773a5ff3fe7c16f9b0bcb89b8 Mon Sep 17 00:00:00 2001 From: Oliver Stenbom Date: Wed, 7 Oct 2026 18:18:13 -0700 Subject: [PATCH 2/9] ci: preserve native fixture partitions in Endform comparison --- .github/workflows/ci.yml | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b75685ebd8..87530cfd7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -412,11 +412,11 @@ jobs: - run: echo "E2E results are aggregated by the Tests job" test-e2e: - name: E2E Endform (${{ matrix.target }}) + name: E2E Endform shard (${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) needs: change-plan if: needs.change-plan.outputs.e2e_node == 'true' || needs.change-plan.outputs.e2e_cloudflare == 'true' runs-on: ubuntu-latest - timeout-minutes: 120 + timeout-minutes: 90 permissions: contents: read id-token: write @@ -426,7 +426,8 @@ jobs: strategy: fail-fast: false matrix: - target: [node, cloudflare] + shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] + shardTotal: [8] steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -439,20 +440,22 @@ jobs: - run: pnpm install --frozen-lockfile - name: Build Node fixture id: node-build - if: ${{ !cancelled() && needs.change-plan.outputs.e2e_node == 'true' && matrix.target == 'node' }} + if: ${{ !cancelled() && needs.change-plan.outputs.e2e_node == 'true' }} run: pnpm run --filter emdash... build - name: Build Cloudflare fixture id: cloudflare-build - if: ${{ !cancelled() && needs.change-plan.outputs.e2e_cloudflare == 'true' && matrix.target == 'cloudflare' }} + if: ${{ !cancelled() && needs.change-plan.outputs.e2e_cloudflare == 'true' }} run: pnpm run --filter "emdash-e2e-fixture-cloudflare..." build - name: Run Node E2E id: node-e2e if: ${{ !cancelled() && steps.node-build.outcome == 'success' }} - run: npx endform@latest test --organization-id 2G1ZCj7X + run: | + ENDFORM=false pnpm exec playwright test --list --reporter=list --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} > "$RUNNER_TEMP/emdash-test-list.txt" + npx endform@latest test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() && steps.node-e2e.outcome == 'failure' with: - name: playwright-report-node-endform + name: playwright-report-node-endform-${{ matrix.shardIndex }} path: | playwright-report/ test-results/ @@ -460,13 +463,15 @@ jobs: - name: Run Cloudflare E2E id: cloudflare-e2e if: ${{ !cancelled() && steps.cloudflare-build.outcome == 'success' }} - run: npx endform@latest test --organization-id 2G1ZCj7X + run: | + ENDFORM=false pnpm exec playwright test --list --reporter=list --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} > "$RUNNER_TEMP/emdash-test-list.txt" + npx endform@latest test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" env: EMDASH_E2E_TARGET: cloudflare - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() && steps.cloudflare-e2e.outcome == 'failure' with: - name: playwright-report-cloudflare-endform + name: playwright-report-cloudflare-endform-${{ matrix.shardIndex }} path: | playwright-report/ test-results/ From 6bb9b5f1cac28800148face7df7b3236b1d380ac Mon Sep 17 00:00:00 2001 From: Oliver Stenbom Date: Wed, 7 Oct 2026 18:24:46 -0700 Subject: [PATCH 3/9] ci: capture first-shard Endform failure diagnostics --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87530cfd7b..1de4393895 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -451,7 +451,9 @@ jobs: if: ${{ !cancelled() && steps.node-build.outcome == 'success' }} run: | ENDFORM=false pnpm exec playwright test --list --reporter=list --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} > "$RUNNER_TEMP/emdash-test-list.txt" - npx endform@latest test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" + options=() + if [ "${{ matrix.shardIndex }}" = "1" ]; then options+=(--max-failures 1); fi + npx endform@latest test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" "${options[@]}" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() && steps.node-e2e.outcome == 'failure' with: @@ -465,7 +467,9 @@ jobs: if: ${{ !cancelled() && steps.cloudflare-build.outcome == 'success' }} run: | ENDFORM=false pnpm exec playwright test --list --reporter=list --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} > "$RUNNER_TEMP/emdash-test-list.txt" - npx endform@latest test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" + options=() + if [ "${{ matrix.shardIndex }}" = "1" ]; then options+=(--max-failures 1); fi + npx endform@latest test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" "${options[@]}" env: EMDASH_E2E_TARGET: cloudflare - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 From 4446afbc5f2e483d439960e7bc899851f8b073b1 Mon Sep 17 00:00:00 2001 From: Oliver Stenbom Date: Tue, 13 Oct 2026 09:06:51 -0700 Subject: [PATCH 4/9] ci: retry Endform diagnostics with CLI 0.81.4 --- .github/workflows/ci.yml | 6 ++-- package.json | 2 +- pnpm-lock.yaml | 59 ++++++++++++++++++++-------------------- pnpm-workspace.yaml | 7 +++++ 4 files changed, 41 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1de4393895..143f5fea9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -412,7 +412,7 @@ jobs: - run: echo "E2E results are aggregated by the Tests job" test-e2e: - name: E2E Endform shard (${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) + name: E2E Endform diagnostic shard (${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) needs: change-plan if: needs.change-plan.outputs.e2e_node == 'true' || needs.change-plan.outputs.e2e_cloudflare == 'true' runs-on: ubuntu-latest @@ -426,7 +426,7 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] + shardIndex: [1] shardTotal: [8] steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -450,6 +450,7 @@ jobs: id: node-e2e if: ${{ !cancelled() && steps.node-build.outcome == 'success' }} run: | + npx endform@latest --version ENDFORM=false pnpm exec playwright test --list --reporter=list --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} > "$RUNNER_TEMP/emdash-test-list.txt" options=() if [ "${{ matrix.shardIndex }}" = "1" ]; then options+=(--max-failures 1); fi @@ -466,6 +467,7 @@ jobs: id: cloudflare-e2e if: ${{ !cancelled() && steps.cloudflare-build.outcome == 'success' }} run: | + npx endform@latest --version ENDFORM=false pnpm exec playwright test --list --reporter=list --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} > "$RUNNER_TEMP/emdash-test-list.txt" options=() if [ "${{ matrix.shardIndex }}" = "1" ]; then options+=(--max-failures 1); fi diff --git a/package.json b/package.json index 3933a9fe90..fcf2c8d988 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@types/node": "catalog:", "@typescript/native-preview": "7.0.0-dev.20260421.2", "emdash": "workspace:*", - "endform": "0.81.3", + "endform": "0.81.4", "knip": "^5.84.1", "oxfmt": "^0.59.0", "oxlint": "^1.74.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3c03097af..f40305fb56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -375,8 +375,8 @@ importers: specifier: workspace:* version: link:packages/core endform: - specifier: 0.81.3 - version: 0.81.3 + specifier: 0.81.4 + version: 0.81.4 knip: specifier: ^5.84.1 version: 5.84.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.10.13)(typescript@6.0.0-beta) @@ -13271,41 +13271,40 @@ packages: zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - endform-darwin-arm64@0.81.3: - resolution: {integrity: sha512-BV3Ji195mR8GnxTbz+FDBS2wPKymW0o/xzxGz4TbWU0IdD4FLLsx4TZBm/VVEvkrnHnxXeJQhAaN2L+m4YTHVw==} + endform-darwin-arm64@0.81.4: + resolution: {integrity: sha512-KZNlY+Wfhe+t4J+QDLwgI9fNSUzILflOLLZRoHyQ0CAyUpEuFPsjkTjMWr/uvlxcGQgxmfk+UGxakcST3qfj5g==} cpu: [arm64] os: [darwin] - endform-darwin-x86@0.81.3: - resolution: {integrity: sha512-w9/t/7X09tzGN5CQMISWXPYr5axRbqbAwjSTq0MWwZCF+sq25SjbSppllqzfCGAgieHJdmiMft6/RffRzAlmcg==} + endform-darwin-x86@0.81.4: + resolution: {integrity: sha512-i1Nm9R6ykJuTpGbPa6eC/KyKTyyx0Tlhhvom9cX467JyQQ4weornnWQ9agywcNgYgOCd8QsslkNOnp30mj7Q9w==} cpu: [x64] os: [darwin] - endform-linux-arm64@0.81.3: - resolution: {integrity: sha512-YR0FMjgx66Y3IwX/8ZvBtooqTKt05GwHjKKuc14raYK+0c3qxArjP/jU/eyjhAc3ybd8YQ9jcTQjszvc8cf9OA==} + endform-linux-arm64@0.81.4: + resolution: {integrity: sha512-nYauV4GAxmH6S5qIlFcrdhPl2aUfpGgbS0O7BJ9MXGQ+3Rjhgg7g2S9ZK6tHblezoZYHeHg6TL3r2v9XbIzbIg==} cpu: [arm64] os: [linux] - endform-linux-x86@0.81.3: - resolution: {integrity: sha512-TVahPFdCvq2dbyO7WtUjcbmHLNT+dXKB1YImEQ7gDXTLpv9AOigpkN8jYmK1SXgNHiwpibM7dGQYULOLw+oEDg==} + endform-linux-x86@0.81.4: + resolution: {integrity: sha512-HOz59BQhVKANp4pCIAlgqxyOTz7wgwUcnM1prQACULq/ixUVNPqrIkXzYYz0xtXC1CsgdW+/32YSoTJfoCUsTw==} cpu: [x64] os: [linux] - endform-win32-arm64@0.81.3: - resolution: {integrity: sha512-etfYsOPqesPE1RIxzHKBPy/aICoFcCd1rCkPUw3xRBMZsCwYDHMFC/awrRN8d7Bav98T6ht4jyI8k07tsbVruQ==} + endform-win32-arm64@0.81.4: + resolution: {integrity: sha512-zkiG2ctCXSdTs9VAbOQM7xrbLE0jjTf8pZhc8XlBlYmiaGzqL2+pqWMutuf964Tk7NOQoFs3F6SEyZJe5TBgMg==} cpu: [arm64] os: [win32] - endform-win32-x86@0.81.3: - resolution: {integrity: sha512-2Oz2ys88AS5GbuyUvpUq5KdtPIv6I0DxRiwf+mWd/Onh2diPoTGD/cfZyU3GYF3GYtWFXmc7OKbEOeZiVeDRDw==} + endform-win32-x86@0.81.4: + resolution: {integrity: sha512-0EZZd6RolVGmLpaa2299VIzXHocTECzsjw6qydbgov6F+w7h6ebzvGdLNc+0wWnz+G3jdoqjdpf7HhAAJA6Arw==} cpu: [x64] os: [win32] - endform@0.81.3: - resolution: {integrity: sha512-VnlmxakSs1u7N1CjOp1hRtCDcBnmNwUH9UALAoxG2LRU3fWpUJ5lNw0s7cARqZTn5OM9F0iJYBvnlaRrOICApw==} + endform@0.81.4: + resolution: {integrity: sha512-PWi2OWQtGVs91u4b3FmB8RkWKekQU8Q523SmowhOFrkbmz6KC+703Ql+LHaXtGsfEmJcG71I/kPQXfhtNBcTVg==} hasBin: true - snapshots: '@adraffy/ens-normalize@1.11.1': {} @@ -24549,29 +24548,29 @@ snapshots: zwitch@2.0.4: {} - endform-darwin-arm64@0.81.3: + endform-darwin-arm64@0.81.4: optional: true - endform-darwin-x86@0.81.3: + endform-darwin-x86@0.81.4: optional: true - endform-linux-arm64@0.81.3: + endform-linux-arm64@0.81.4: optional: true - endform-linux-x86@0.81.3: + endform-linux-x86@0.81.4: optional: true - endform-win32-arm64@0.81.3: + endform-win32-arm64@0.81.4: optional: true - endform-win32-x86@0.81.3: + endform-win32-x86@0.81.4: optional: true - endform@0.81.3: + endform@0.81.4: optionalDependencies: - endform-darwin-arm64: 0.81.3 - endform-darwin-x86: 0.81.3 - endform-linux-arm64: 0.81.3 - endform-linux-x86: 0.81.3 - endform-win32-arm64: 0.81.3 - endform-win32-x86: 0.81.3 + endform-darwin-arm64: 0.81.4 + endform-darwin-x86: 0.81.4 + endform-linux-arm64: 0.81.4 + endform-linux-x86: 0.81.4 + endform-win32-arm64: 0.81.4 + endform-win32-x86: 0.81.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1169de4722..3ae80a20f3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,6 +5,13 @@ minimumReleaseAge: 1440 # Scoped to the binaries/runtime only -- @rolldown/pluginutils etc. stay # under the cooldown. minimumReleaseAgeExclude: + - "endform@0.81.4" + - "endform-darwin-arm64@0.81.4" + - "endform-darwin-x86@0.81.4" + - "endform-linux-arm64@0.81.4" + - "endform-linux-x86@0.81.4" + - "endform-win32-arm64@0.81.4" + - "endform-win32-x86@0.81.4" - "@rolldown/binding-*" - "@emnapi/core" - "@emnapi/runtime" From 49c250391197380d0eda9c6332a348c99875e617 Mon Sep 17 00:00:00 2001 From: Oliver Stenbom Date: Thu, 15 Oct 2026 16:23:12 -0700 Subject: [PATCH 5/9] test: run native and Endform suites against built fixtures --- .github/workflows/ci.yml | 33 ++++++----- e2e/fixture-cloudflare/astro.config.mjs | 23 +++++++- e2e/fixture-cloudflare/src/middleware.ts | 1 + e2e/fixture/astro.config.mjs | 30 ++++++++-- e2e/fixture/package.json | 3 +- e2e/fixture/src/middleware.ts | 1 + e2e/fixture/src/outer-middleware.ts | 7 +++ e2e/fixtures/built-test-helpers.mjs | 60 ++++++++++++++++++++ e2e/fixtures/email-capture-plugin.mjs | 20 +++++++ e2e/fixtures/index.ts | 2 +- e2e/fixtures/locale-middleware.ts | 9 +++ e2e/global-setup.ts | 71 +++++++++++++++++------- e2e/global-teardown.ts | 6 +- e2e/tests/editor-image-panel.spec.ts | 2 +- e2e/tests/editor-image-toolbar.spec.ts | 2 +- playwright.config.ts | 4 +- pnpm-lock.yaml | 3 + 17 files changed, 227 insertions(+), 50 deletions(-) create mode 100644 e2e/fixture-cloudflare/src/middleware.ts create mode 100644 e2e/fixture/src/middleware.ts create mode 100644 e2e/fixtures/built-test-helpers.mjs create mode 100644 e2e/fixtures/email-capture-plugin.mjs create mode 100644 e2e/fixtures/locale-middleware.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 143f5fea9e..75fadf7175 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -412,7 +412,7 @@ jobs: - run: echo "E2E results are aggregated by the Tests job" test-e2e: - name: E2E Endform diagnostic shard (${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) + name: E2E built ${{ matrix.runner }} (${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) needs: change-plan if: needs.change-plan.outputs.e2e_node == 'true' || needs.change-plan.outputs.e2e_cloudflare == 'true' runs-on: ubuntu-latest @@ -426,7 +426,8 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1] + runner: [native, endform] + shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] shardTotal: [8] steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -450,15 +451,17 @@ jobs: id: node-e2e if: ${{ !cancelled() && steps.node-build.outcome == 'success' }} run: | - npx endform@latest --version - ENDFORM=false pnpm exec playwright test --list --reporter=list --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} > "$RUNNER_TEMP/emdash-test-list.txt" - options=() - if [ "${{ matrix.shardIndex }}" = "1" ]; then options+=(--max-failures 1); fi - npx endform@latest test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" "${options[@]}" + if [ "${{ matrix.runner }}" = native ]; then + ENDFORM=false pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + else + pnpm exec endform --version + ENDFORM=false pnpm exec playwright test --list --reporter=list --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} > "$RUNNER_TEMP/emdash-test-list.txt" + pnpm exec endform test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" + fi - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() && steps.node-e2e.outcome == 'failure' with: - name: playwright-report-node-endform-${{ matrix.shardIndex }} + name: playwright-report-node-${{ matrix.runner }}-${{ matrix.shardIndex }} path: | playwright-report/ test-results/ @@ -467,17 +470,19 @@ jobs: id: cloudflare-e2e if: ${{ !cancelled() && steps.cloudflare-build.outcome == 'success' }} run: | - npx endform@latest --version - ENDFORM=false pnpm exec playwright test --list --reporter=list --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} > "$RUNNER_TEMP/emdash-test-list.txt" - options=() - if [ "${{ matrix.shardIndex }}" = "1" ]; then options+=(--max-failures 1); fi - npx endform@latest test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" "${options[@]}" + if [ "${{ matrix.runner }}" = native ]; then + ENDFORM=false pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + else + pnpm exec endform --version + ENDFORM=false pnpm exec playwright test --list --reporter=list --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} > "$RUNNER_TEMP/emdash-test-list.txt" + pnpm exec endform test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" + fi env: EMDASH_E2E_TARGET: cloudflare - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() && steps.cloudflare-e2e.outcome == 'failure' with: - name: playwright-report-cloudflare-endform-${{ matrix.shardIndex }} + name: playwright-report-cloudflare-${{ matrix.runner }}-${{ matrix.shardIndex }} path: | playwright-report/ test-results/ diff --git a/e2e/fixture-cloudflare/astro.config.mjs b/e2e/fixture-cloudflare/astro.config.mjs index 29425265bb..2942a17f22 100644 --- a/e2e/fixture-cloudflare/astro.config.mjs +++ b/e2e/fixture-cloudflare/astro.config.mjs @@ -16,6 +16,8 @@ import registryTestPlugin from "@emdash-cms/plugin-marketplace-test"; import { defineConfig } from "astro/config"; import emdash from "emdash/astro"; +import { builtTestHelpers } from "../fixtures/built-test-helpers.mjs"; + const marketplaceUrl = process.env.EMDASH_MARKETPLACE_URL || undefined; const registryUrl = process.env.EMDASH_REGISTRY_URL || undefined; const registryFixturePath = process.env.EMDASH_REGISTRY_FIXTURE; @@ -80,17 +82,32 @@ export default defineConfig({ integrations: [ react(), emdash({ + siteUrl: "http://localhost:4444", database: d1({ binding: "DB" }), middleware: { outer: "./src/outer-middleware.ts" }, storage: r2({ binding: "MEDIA" }), - plugins: [colorPlugin()], + plugins: [ + colorPlugin(), + ...(process.env.EMDASH_E2E_SERVER === "dev" + ? [] + : [ + { + id: "emdash-console-email", + version: "0.0.0", + entrypoint: fileURLToPath( + new URL("../fixtures/email-capture-plugin.mjs", import.meta.url), + ), + }, + ]), + ], sandboxed: [{ ...registryTestPlugin, hooks: e2eHooks }, deniedPlugin], marketplace: marketplaceUrl, - registry: registryUrl, + registry: process.env.EMDASH_E2E_SERVER === "dev" ? registryUrl : false, sandboxRunner: sandbox(), }), ], i18n: { + routing: "manual", defaultLocale: "en", locales: ["en", "fr", "es"], fallback: { fr: "en", es: "en" }, @@ -100,7 +117,7 @@ export default defineConfig({ define: { __EMDASH_REGISTRY_FIXTURE__: JSON.stringify(registryFixture), }, - plugins: [lateManifestImport], + plugins: [builtTestHelpers(), lateManifestImport], server: { fs: { strict: false }, }, diff --git a/e2e/fixture-cloudflare/src/middleware.ts b/e2e/fixture-cloudflare/src/middleware.ts new file mode 100644 index 0000000000..a8f9e3bee1 --- /dev/null +++ b/e2e/fixture-cloudflare/src/middleware.ts @@ -0,0 +1 @@ +export { onRequest } from "../../fixtures/locale-middleware.js"; diff --git a/e2e/fixture/astro.config.mjs b/e2e/fixture/astro.config.mjs index 047ae3d8b6..5e7fafa5f2 100644 --- a/e2e/fixture/astro.config.mjs +++ b/e2e/fixture/astro.config.mjs @@ -16,13 +16,16 @@ import emdash from "emdash/astro"; import { sqlite } from "emdash/db"; import { installRegistryAuthoritativeFixture } from "emdash/internal/testing/registry"; +import { builtTestHelpers } from "../fixtures/built-test-helpers.mjs"; + const dbUrl = process.env.EMDASH_TEST_DB || "file:./test.db"; const marketplaceUrl = process.env.EMDASH_MARKETPLACE_URL || undefined; const registryUrl = process.env.EMDASH_REGISTRY_URL || undefined; const registryFixturePath = process.env.EMDASH_REGISTRY_FIXTURE; -if (registryFixturePath) { - installRegistryAuthoritativeFixture(JSON.parse(readFileSync(registryFixturePath, "utf8"))); -} +const registryFixture = registryFixturePath + ? JSON.parse(readFileSync(registryFixturePath, "utf8")) + : null; +if (registryFixture) installRegistryAuthoritativeFixture(registryFixture); const e2eHookNames = new Set([ "content:afterSave", "content:beforeDelete", @@ -108,22 +111,39 @@ export default defineConfig({ integrations: [ react(), emdash({ + siteUrl: "http://localhost:4444", database: sqlite({ url: dbUrl }), middleware: { outer: "./src/outer-middleware.ts" }, - plugins: [colorPlugin()], + plugins: [ + colorPlugin(), + ...(process.env.EMDASH_E2E_SERVER === "dev" + ? [] + : [ + { + id: "emdash-console-email", + version: "0.0.0", + entrypoint: fileURLToPath( + new URL("../fixtures/email-capture-plugin.mjs", import.meta.url), + ), + }, + ]), + ], sandboxed: [editorExtensionsPlugin, { ...registryTestPlugin, hooks: e2eHooks }, deniedPlugin], marketplace: marketplaceUrl, - registry: registryUrl, + registry: process.env.EMDASH_E2E_SERVER === "dev" ? registryUrl : false, sandboxRunner: "@emdash-cms/sandbox-workerd", }), ], i18n: { + routing: "manual", defaultLocale: "en", locales: ["en", "fr", "es"], fallback: { fr: "en", es: "en" }, }, devToolbar: { enabled: false }, vite: { + define: { __EMDASH_REGISTRY_FIXTURE__: JSON.stringify(registryFixture) }, + plugins: [builtTestHelpers()], server: { fs: { strict: false }, }, diff --git a/e2e/fixture/package.json b/e2e/fixture/package.json index 66e9718242..4fd3c30f3d 100644 --- a/e2e/fixture/package.json +++ b/e2e/fixture/package.json @@ -12,6 +12,7 @@ "astro": "catalog:", "emdash": "workspace:*", "react": "catalog:", - "react-dom": "catalog:" + "react-dom": "catalog:", + "sharp": "0.35.4" } } diff --git a/e2e/fixture/src/middleware.ts b/e2e/fixture/src/middleware.ts new file mode 100644 index 0000000000..a8f9e3bee1 --- /dev/null +++ b/e2e/fixture/src/middleware.ts @@ -0,0 +1 @@ +export { onRequest } from "../../fixtures/locale-middleware.js"; diff --git a/e2e/fixture/src/outer-middleware.ts b/e2e/fixture/src/outer-middleware.ts index 327310a38c..9bfa4f5174 100644 --- a/e2e/fixture/src/outer-middleware.ts +++ b/e2e/fixture/src/outer-middleware.ts @@ -1,6 +1,13 @@ import { defineMiddleware } from "astro:middleware"; +import { installRegistryAuthoritativeFixture } from "emdash/internal/testing/registry"; import { getRequestContext } from "emdash/request-context"; +declare const __EMDASH_REGISTRY_FIXTURE__: unknown; + +if (__EMDASH_REGISTRY_FIXTURE__ !== null) { + installRegistryAuthoritativeFixture(__EMDASH_REGISTRY_FIXTURE__); +} + const FINALIZER_NONCE = "outer-finalizer"; function preNextState(locals: App.Locals): string { diff --git a/e2e/fixtures/built-test-helpers.mjs b/e2e/fixtures/built-test-helpers.mjs new file mode 100644 index 0000000000..cc2f0f270f --- /dev/null +++ b/e2e/fixtures/built-test-helpers.mjs @@ -0,0 +1,60 @@ +const HELPER_ROUTE = + /\/packages\/core\/(?:src|dist)\/astro\/routes\/api\/(?:setup\/dev-(?:bypass|reset)|auth\/dev-bypass|dev\/emails)\.(?:ts|mjs)$/; +const ARTIFACT_ROUTE = + /\/packages\/core\/(?:src|dist)\/astro\/routes\/api\/admin\/plugins\/registry\/artifact\.(?:ts|mjs)$/; +const TRAILING_SEMICOLON = /;\s*$/; + +// Private fixture builds retain setup helpers and the localhost mock registry. +// The admin and application middleware still compile in production mode. +export function builtTestHelpers() { + return { + name: "emdash-e2e-built-helpers", + apply: "build", + enforce: "pre", + transform(code, id) { + if (id === "\0virtual:emdash/config") { + const config = JSON.parse( + code.slice("export default ".length).replace(TRAILING_SEMICOLON, ""), + ); + config.registry = process.env.EMDASH_REGISTRY_URL ?? false; + return { code: `export default ${JSON.stringify(config)};`, map: null }; + } + if (id.includes("/packages/core/") && code.includes("function validateAggregatorUrl(")) { + return { + code: code.replace( + "options.allowLocalhost ?? import.meta.env.DEV", + '(aggregatorUrl === "http://127.0.0.1:4445" || (options.allowLocalhost ?? import.meta.env.DEV))', + ), + map: null, + }; + } + if ( + id.includes("/packages/core/") && + code.includes("async function resolveSafeArtifactTarget(") + ) { + return { + code: code.replace( + "if (!import.meta.env.DEV)", + 'if (!import.meta.env.DEV && url.origin !== "http://127.0.0.1:4445")', + ), + map: null, + }; + } + if ( + ARTIFACT_ROUTE.test(id) || + (id.includes("/packages/core/") && + code.includes("Registry artifact fetch requires an abort signal")) + ) { + return { + code: code.replace("allowHttpLocalhost: import.meta.env.DEV", "allowHttpLocalhost: true"), + map: null, + }; + } + if (!HELPER_ROUTE.test(id.split("?")[0])) return undefined; + if (!code.includes("import.meta.env.DEV")) { + throw new Error(`Missing development guard in fixture helper: ${id}`); + } + return { code: code.replaceAll("import.meta.env.DEV", "true"), map: null }; + }, + }; +} diff --git a/e2e/fixtures/email-capture-plugin.mjs b/e2e/fixtures/email-capture-plugin.mjs new file mode 100644 index 0000000000..d17467993b --- /dev/null +++ b/e2e/fixtures/email-capture-plugin.mjs @@ -0,0 +1,20 @@ +import { definePlugin } from "emdash"; + +export function createPlugin() { + return definePlugin({ + id: "emdash-console-email", + version: "0.0.0", + capabilities: ["hooks.email-transport:register"], + hooks: { + "email:deliver": { + exclusive: true, + handler: async ({ message, source }) => { + const key = Symbol.for("emdash:dev-emails"); + const emails = (globalThis[key] ??= []); + emails.push({ message, source, sentAt: new Date().toISOString() }); + if (emails.length > 100) emails.shift(); + }, + }, + }, + }); +} diff --git a/e2e/fixtures/index.ts b/e2e/fixtures/index.ts index d84e458a5b..cbe9398d0b 100644 --- a/e2e/fixtures/index.ts +++ b/e2e/fixtures/index.ts @@ -45,7 +45,7 @@ export const test = base.extend<{ }>({ page: [ async ({ page }, use) => { - if (process.env.ENDFORM === "true") { + if (process.env.ENDFORM === "true" && process.env.EMDASH_E2E_SERVER === "dev") { await warmUpAdmin(page, process.env.BASE_URL || "http://localhost:4444"); } await use(page); diff --git a/e2e/fixtures/locale-middleware.ts b/e2e/fixtures/locale-middleware.ts new file mode 100644 index 0000000000..bfe76b07b9 --- /dev/null +++ b/e2e/fixtures/locale-middleware.ts @@ -0,0 +1,9 @@ +import { defineMiddleware } from "astro:middleware"; + +const LOCALES = new Set(["en", "fr", "es"]); + +export const onRequest = defineMiddleware((context, next) => { + const locale = context.params.locale; + if (locale && !LOCALES.has(locale)) return new Response("Not found", { status: 404 }); + return next(); +}); diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts index 5bfc4178fe..6ba69f05bb 100644 --- a/e2e/global-setup.ts +++ b/e2e/global-setup.ts @@ -1,13 +1,14 @@ /** * Playwright global setup. * - * Starts an isolated Astro dev server from the minimal e2e fixture, - * runs dev-bypass setup, and seeds test data. Writes server info + * Builds and serves the isolated e2e fixture in production mode, + * runs fixture-only setup helpers, and seeds test data. Writes server info * to a temp file so tests and teardown can find it. */ import { execFile, spawn } from "node:child_process"; import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { createServer } from "node:net"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -49,6 +50,7 @@ const TARGETS: Record = { const TARGET = TARGETS[process.env.EMDASH_E2E_TARGET ?? "node"] ?? TARGETS.node!; const FIXTURE_DIR = TARGET.fixtureDir; +const BUILT_SERVER = process.env.EMDASH_E2E_SERVER !== "dev"; const CLI_BINARY = resolve(ROOT, "packages/core/dist/cli/index.mjs"); const PORT = 4444; const MARKETPLACE_PORT = 4445; @@ -298,6 +300,11 @@ async function seedTestData( // --------------------------------------------------------------------------- export default async function globalSetup(): Promise { + await new Promise((resolvePort, reject) => { + const probe = createServer(); + probe.once("error", reject); + probe.listen(PORT, "127.0.0.1", () => probe.close(() => resolvePort())); + }); await ensureBuilt(); await ensureFixtureDepsBuilt(); @@ -329,22 +336,48 @@ export default async function globalSetup(): Promise { rmSync(join(FIXTURE_DIR, ".wrangler"), { recursive: true, force: true }); } - // 2. Start dev server (with marketplace URL injected via env) + // 2. Build and serve the fixture with the same isolated data for both runners. const astroBin = join(fixtureNodeModules, ".bin", "astro"); - const server = spawn(astroBin, ["dev", "--port", String(PORT)], { - cwd: workDir, - env: { - ...process.env, - // Keep Astro's agent-mode server in this process so teardown owns it. - ASTRO_DEV_BACKGROUND: "1", - EMDASH_TEST_DB: `file:${dbPath}`, - EMDASH_MARKETPLACE_URL: marketplaceUrl, - EMDASH_REGISTRY_URL: marketplaceUrl, - EMDASH_REGISTRY_FIXTURE: registryFixturePath, - EMDASH_ENCRYPTION_KEY: "emdash_enc_v1_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + const serverEnv = { + ...process.env, + HOST: "127.0.0.1", + PORT: String(PORT), + ASTRO_DEV_BACKGROUND: "1", + ASTRO_PREVIEW_BACKGROUND: "1", + EMDASH_TEST_DB: `file:${dbPath}`, + EMDASH_MARKETPLACE_URL: marketplaceUrl, + EMDASH_REGISTRY_URL: marketplaceUrl, + EMDASH_REGISTRY_FIXTURE: registryFixturePath, + EMDASH_ENCRYPTION_KEY: "emdash_enc_v1_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + }; + if (BUILT_SERVER) { + console.log("[pw] Building production fixture..."); + try { + await execAsync(astroBin, ["build"], { + cwd: workDir, + env: serverEnv, + timeout: 240_000, + maxBuffer: 10 * 1024 * 1024, + }); + } catch (error) { + marketplaceServer.close(); + rmSync(tempDataDir, { recursive: true, force: true }); + throw error; + } + } + const standaloneNode = BUILT_SERVER && TARGET.usesTempDb; + const server = spawn( + standaloneNode ? process.execPath : astroBin, + standaloneNode + ? [join(workDir, "dist/server/entry.mjs")] + : [BUILT_SERVER ? "preview" : "dev", "--port", String(PORT)], + { + cwd: workDir, + env: serverEnv, + detached: true, + stdio: "pipe", }, - stdio: "pipe", - }); + ); let serverOutput = ""; const appendServerOutput = (data: Buffer) => { @@ -405,8 +438,8 @@ export default async function globalSetup(): Promise { // 5c. Load the admin in a browser. Its client dependencies are only // discovered when a browser requests them, and the optimizer then forces // a full reload that would otherwise land inside the first admin test. - console.log("[pw] Warming up admin..."); - if (process.env.ENDFORM !== "true") { + if (!BUILT_SERVER && process.env.ENDFORM !== "true") { + console.log("[pw] Warming up admin..."); const { chromium } = await import("@playwright/test"); const browser = await chromium.launch(); try { @@ -433,7 +466,7 @@ export default async function globalSetup(): Promise { console.log(`[pw] Server ready at ${baseUrl} (pid ${server.pid})`); } catch (error) { - server.kill("SIGTERM"); + process.kill(-server.pid!, "SIGTERM"); marketplaceServer.close(); throw new Error( `${error instanceof Error ? error.message : String(error)}\n\nServer output:\n${serverOutput}`, diff --git a/e2e/global-teardown.ts b/e2e/global-teardown.ts index 8d865973d9..a7331c535d 100644 --- a/e2e/global-teardown.ts +++ b/e2e/global-teardown.ts @@ -23,11 +23,11 @@ export default async function globalTeardown(): Promise { // Kill the server process try { - process.kill(info.pid, "SIGTERM"); + process.kill(-info.pid, "SIGTERM"); await new Promise((r) => setTimeout(r, 1000)); try { - process.kill(info.pid, 0); // Check if alive - process.kill(info.pid, "SIGKILL"); + process.kill(-info.pid, 0); // Check if alive + process.kill(-info.pid, "SIGKILL"); } catch { // Already dead } diff --git a/e2e/tests/editor-image-panel.spec.ts b/e2e/tests/editor-image-panel.spec.ts index 5f8a509500..a4d7b35394 100644 --- a/e2e/tests/editor-image-panel.spec.ts +++ b/e2e/tests/editor-image-panel.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from "../fixtures"; -const EXTERNAL_IMAGE_URL = "http://media.example.test/editor-panel.png"; +const EXTERNAL_IMAGE_URL = "https://media.example.test/editor-panel.png"; test.describe("Editor image panel", () => { test.beforeEach(async ({ admin }) => { diff --git a/e2e/tests/editor-image-toolbar.spec.ts b/e2e/tests/editor-image-toolbar.spec.ts index 1e5fac5ab6..979b0c43e4 100644 --- a/e2e/tests/editor-image-toolbar.spec.ts +++ b/e2e/tests/editor-image-toolbar.spec.ts @@ -3,7 +3,7 @@ import type { APIRequestContext } from "@playwright/test"; import { test, expect } from "../fixtures"; const EDITOR = "#field-body .ProseMirror"; -const IMAGE_URL = "http://media.example.test/editor-toolbar.png"; +const IMAGE_URL = "https://media.example.test/editor-toolbar.png"; const csrfHeaders = (token: string) => ({ Authorization: `Bearer ${token}`, "X-EmDash-Request": "1", diff --git a/playwright.config.ts b/playwright.config.ts index eb2cd0df9f..965ebdd54f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,8 +6,8 @@ const isEndform = process.env.ENDFORM === "true"; * Playwright E2E test configuration for EmDash CMS * * Tests run against an isolated fixture (not a demo app). - * Global setup creates a temp directory, starts a fresh dev server, - * runs setup via dev-bypass, and seeds collections with test data. + * Global setup builds the fixture, starts its production server, + * and seeds an isolated database through fixture-only setup helpers. * Port 4444 is used to avoid conflicts with development servers. */ export default defineConfig({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f40305fb56..48b17f320d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1151,6 +1151,9 @@ importers: react-dom: specifier: 'catalog:' version: 19.2.4(react@19.2.4) + sharp: + specifier: 0.35.4 + version: 0.35.4(@types/node@26.1.1) e2e/fixture-cloudflare: dependencies: From 5d59ad8138daa7e25d81fb8cd5cb323121379d6d Mon Sep 17 00:00:00 2001 From: Oliver Stenbom Date: Thu, 15 Oct 2026 16:29:18 -0700 Subject: [PATCH 6/9] test: clean up built servers after Endform exits --- .github/workflows/ci.yml | 5 +++-- e2e/global-setup.ts | 11 +++++++++-- e2e/global-teardown.ts | 7 ++++--- e2e/run-endform.ts | 14 ++++++++++++++ endform.config.ts | 1 + 5 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 e2e/run-endform.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75fadf7175..c836bef3c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: [main] pull_request: + workflow_dispatch: merge_group: permissions: @@ -456,7 +457,7 @@ jobs: else pnpm exec endform --version ENDFORM=false pnpm exec playwright test --list --reporter=list --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} > "$RUNNER_TEMP/emdash-test-list.txt" - pnpm exec endform test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" + pnpm exec tsx e2e/run-endform.ts test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" fi - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() && steps.node-e2e.outcome == 'failure' @@ -475,7 +476,7 @@ jobs: else pnpm exec endform --version ENDFORM=false pnpm exec playwright test --list --reporter=list --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} > "$RUNNER_TEMP/emdash-test-list.txt" - pnpm exec endform test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" + pnpm exec tsx e2e/run-endform.ts test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" fi env: EMDASH_E2E_TARGET: cloudflare diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts index 6ba69f05bb..0f07170828 100644 --- a/e2e/global-setup.ts +++ b/e2e/global-setup.ts @@ -15,6 +15,7 @@ import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { warmUpAdmin } from "./fixtures/warm-up-admin.js"; +import globalTeardown from "./global-teardown.js"; const execAsync = promisify(execFile); @@ -366,6 +367,7 @@ export default async function globalSetup(): Promise { } } const standaloneNode = BUILT_SERVER && TARGET.usesTempDb; + const processGroup = true; const server = spawn( standaloneNode ? process.execPath : astroBin, standaloneNode @@ -374,10 +376,14 @@ export default async function globalSetup(): Promise { { cwd: workDir, env: serverEnv, - detached: true, + detached: processGroup, stdio: "pipe", }, ); + writeFileSync( + SERVER_INFO_PATH, + JSON.stringify({ pid: server.pid, processGroup, workDir, tempDataDir }), + ); let serverOutput = ""; const appendServerOutput = (data: Buffer) => { @@ -452,6 +458,7 @@ export default async function globalSetup(): Promise { // 6. Write server info const info = { pid: server.pid!, + processGroup, workDir, tempDataDir, baseUrl, @@ -466,7 +473,7 @@ export default async function globalSetup(): Promise { console.log(`[pw] Server ready at ${baseUrl} (pid ${server.pid})`); } catch (error) { - process.kill(-server.pid!, "SIGTERM"); + await globalTeardown(); marketplaceServer.close(); throw new Error( `${error instanceof Error ? error.message : String(error)}\n\nServer output:\n${serverOutput}`, diff --git a/e2e/global-teardown.ts b/e2e/global-teardown.ts index a7331c535d..330f588e37 100644 --- a/e2e/global-teardown.ts +++ b/e2e/global-teardown.ts @@ -23,11 +23,12 @@ export default async function globalTeardown(): Promise { // Kill the server process try { - process.kill(-info.pid, "SIGTERM"); + const serverPid = info.processGroup ? -info.pid : info.pid; + process.kill(serverPid, "SIGTERM"); await new Promise((r) => setTimeout(r, 1000)); try { - process.kill(-info.pid, 0); // Check if alive - process.kill(-info.pid, "SIGKILL"); + process.kill(serverPid, 0); // Check if alive + process.kill(serverPid, "SIGKILL"); } catch { // Already dead } diff --git a/e2e/run-endform.ts b/e2e/run-endform.ts new file mode 100644 index 0000000000..7422c53397 --- /dev/null +++ b/e2e/run-endform.ts @@ -0,0 +1,14 @@ +import { spawn } from "node:child_process"; + +import globalTeardown from "./global-teardown.js"; + +try { + const child = spawn("endform", process.argv.slice(2), { stdio: "inherit" }); + process.exitCode = await new Promise((resolve, reject) => { + child.once("error", reject); + child.once("exit", (code) => resolve(code ?? 1)); + }); +} finally { + // Endform can terminate its webServer before the server's signal handler runs. + await globalTeardown(); +} diff --git a/endform.config.ts b/endform.config.ts index 564778f1a5..4f255a80c1 100644 --- a/endform.config.ts +++ b/endform.config.ts @@ -2,6 +2,7 @@ import { defineEndformConfig } from "endform"; export default defineEndformConfig({ additionalFiles: ["e2e/fixtures/assets/test-image.png"], + environmentVariables: ["^EMDASH_E2E_TARGET$", "^EMDASH_E2E_SERVER$"], proxyNetworkHosts: [""], concurrentTestLimits: [{ scope: "within-suite-run", limit: 1 }], }); From 64d51b0a3d110d01675e33db8d84d9ceaf36bb1c Mon Sep 17 00:00:00 2001 From: Oliver Stenbom Date: Thu, 15 Oct 2026 16:35:48 -0700 Subject: [PATCH 7/9] ci: install Chromium for native benchmark jobs --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c836bef3c4..05f3d0ff1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -448,6 +448,14 @@ jobs: id: cloudflare-build if: ${{ !cancelled() && needs.change-plan.outputs.e2e_cloudflare == 'true' }} run: pnpm run --filter "emdash-e2e-fixture-cloudflare..." build + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + if: ${{ !cancelled() && matrix.runner == 'native' && (steps.node-build.outcome == 'success' || steps.cloudflare-build.outcome == 'success') }} + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: playwright-${{ hashFiles('pnpm-lock.yaml') }} + - run: pnpm exec playwright install --with-deps chromium + if: ${{ !cancelled() && matrix.runner == 'native' && (steps.node-build.outcome == 'success' || steps.cloudflare-build.outcome == 'success') && steps.playwright-cache.outputs.cache-hit != 'true' }} - name: Run Node E2E id: node-e2e if: ${{ !cancelled() && steps.node-build.outcome == 'success' }} From 61c5c3bd034d22c614f6c01380d97727abb5c402 Mon Sep 17 00:00:00 2001 From: Oliver Stenbom Date: Thu, 15 Oct 2026 18:21:28 -0700 Subject: [PATCH 8/9] ci: run full Endform suites without sharding or concurrency caps --- .github/workflows/ci.yml | 19 ++++++++++++------- endform.config.ts | 1 - playwright.config.ts | 1 - 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05f3d0ff1c..71022ffa29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -427,9 +427,16 @@ jobs: strategy: fail-fast: false matrix: - runner: [native, endform] - shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] - shardTotal: [8] + include: + - { runner: native, shardIndex: 1, shardTotal: 8 } + - { runner: native, shardIndex: 2, shardTotal: 8 } + - { runner: native, shardIndex: 3, shardTotal: 8 } + - { runner: native, shardIndex: 4, shardTotal: 8 } + - { runner: native, shardIndex: 5, shardTotal: 8 } + - { runner: native, shardIndex: 6, shardTotal: 8 } + - { runner: native, shardIndex: 7, shardTotal: 8 } + - { runner: native, shardIndex: 8, shardTotal: 8 } + - { runner: endform, shardIndex: 1, shardTotal: 1 } steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -464,8 +471,7 @@ jobs: ENDFORM=false pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} else pnpm exec endform --version - ENDFORM=false pnpm exec playwright test --list --reporter=list --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} > "$RUNNER_TEMP/emdash-test-list.txt" - pnpm exec tsx e2e/run-endform.ts test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" + pnpm exec tsx e2e/run-endform.ts test --organization-id 2G1ZCj7X --timeout 240000 fi - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() && steps.node-e2e.outcome == 'failure' @@ -483,8 +489,7 @@ jobs: ENDFORM=false pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} else pnpm exec endform --version - ENDFORM=false pnpm exec playwright test --list --reporter=list --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} > "$RUNNER_TEMP/emdash-test-list.txt" - pnpm exec tsx e2e/run-endform.ts test --organization-id 2G1ZCj7X --test-list "$RUNNER_TEMP/emdash-test-list.txt" + pnpm exec tsx e2e/run-endform.ts test --organization-id 2G1ZCj7X --timeout 240000 fi env: EMDASH_E2E_TARGET: cloudflare diff --git a/endform.config.ts b/endform.config.ts index 4f255a80c1..65e17d0062 100644 --- a/endform.config.ts +++ b/endform.config.ts @@ -4,5 +4,4 @@ export default defineEndformConfig({ additionalFiles: ["e2e/fixtures/assets/test-image.png"], environmentVariables: ["^EMDASH_E2E_TARGET$", "^EMDASH_E2E_SERVER$"], proxyNetworkHosts: [""], - concurrentTestLimits: [{ scope: "within-suite-run", limit: 1 }], }); diff --git a/playwright.config.ts b/playwright.config.ts index 965ebdd54f..21d9fd7297 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -13,7 +13,6 @@ const isEndform = process.env.ENDFORM === "true"; export default defineConfig({ testDir: "./e2e/tests", testIgnore: "portable-text-table.spec.ts", - // Endform limits concurrency around the shared fixture in endform.config.ts. fullyParallel: isEndform, workers: 1, forbidOnly: !!process.env.CI, From 36723f59e9f52b8cf4426e7e8453f783598bf042 Mon Sep 17 00:00:00 2001 From: Oliver Stenbom Date: Thu, 15 Oct 2026 18:49:29 -0700 Subject: [PATCH 9/9] ci: dedicate one full-suite Endform job and update CLI to 0.81.5 --- .github/workflows/ci.yml | 102 +++++++++++++++++++++---------- package.json | 2 +- pnpm-lock.yaml | 126 +++++++++++++++++++-------------------- 3 files changed, 135 insertions(+), 95 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71022ffa29..3507479c6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -226,6 +226,7 @@ jobs: test-backend, test-browser, test-e2e, + test-e2e-endform, test-e2e-table, ] runs-on: ubuntu-latest @@ -247,6 +248,7 @@ jobs: BROWSER_RESULT: ${{ needs.test-browser.result }} E2E_SELECTED: ${{ needs.change-plan.outputs.e2e_node == 'true' || needs.change-plan.outputs.e2e_cloudflare == 'true' }} E2E_RESULT: ${{ needs.test-e2e.result }} + ENDFORM_RESULT: ${{ needs.test-e2e-endform.result }} TABLE_SELECTED: ${{ needs.change-plan.outputs.e2e_table }} TABLE_RESULT: ${{ needs.test-e2e-table.result }} run: | @@ -274,6 +276,7 @@ jobs: check_lane "Backend tests" "$BACKEND_SELECTED" "$BACKEND_RESULT" check_lane "Browser and playground tests" "$BROWSER_SELECTED" "$BROWSER_RESULT" check_lane "Node/Cloudflare E2E" "$E2E_SELECTED" "$E2E_RESULT" + check_lane "Endform E2E tests" "$E2E_SELECTED" "$ENDFORM_RESULT" check_lane "Table E2E" "$TABLE_SELECTED" "$TABLE_RESULT" exit "$failed" @@ -413,30 +416,18 @@ jobs: - run: echo "E2E results are aggregated by the Tests job" test-e2e: - name: E2E built ${{ matrix.runner }} (${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) + name: E2E built native (${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) needs: change-plan if: needs.change-plan.outputs.e2e_node == 'true' || needs.change-plan.outputs.e2e_cloudflare == 'true' runs-on: ubuntu-latest timeout-minutes: 90 permissions: contents: read - id-token: write - env: - ENDFORM_GIT_BRANCH: ${{ github.head_ref || github.ref_name }} - ENDFORM_GIT_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }} strategy: fail-fast: false matrix: - include: - - { runner: native, shardIndex: 1, shardTotal: 8 } - - { runner: native, shardIndex: 2, shardTotal: 8 } - - { runner: native, shardIndex: 3, shardTotal: 8 } - - { runner: native, shardIndex: 4, shardTotal: 8 } - - { runner: native, shardIndex: 5, shardTotal: 8 } - - { runner: native, shardIndex: 6, shardTotal: 8 } - - { runner: native, shardIndex: 7, shardTotal: 8 } - - { runner: native, shardIndex: 8, shardTotal: 8 } - - { runner: endform, shardIndex: 1, shardTotal: 1 } + shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] + shardTotal: [8] steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -456,27 +447,80 @@ jobs: if: ${{ !cancelled() && needs.change-plan.outputs.e2e_cloudflare == 'true' }} run: pnpm run --filter "emdash-e2e-fixture-cloudflare..." build - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - if: ${{ !cancelled() && matrix.runner == 'native' && (steps.node-build.outcome == 'success' || steps.cloudflare-build.outcome == 'success') }} + if: ${{ !cancelled() && (steps.node-build.outcome == 'success' || steps.cloudflare-build.outcome == 'success') }} id: playwright-cache with: path: ~/.cache/ms-playwright key: playwright-${{ hashFiles('pnpm-lock.yaml') }} - run: pnpm exec playwright install --with-deps chromium - if: ${{ !cancelled() && matrix.runner == 'native' && (steps.node-build.outcome == 'success' || steps.cloudflare-build.outcome == 'success') && steps.playwright-cache.outputs.cache-hit != 'true' }} + if: ${{ !cancelled() && (steps.node-build.outcome == 'success' || steps.cloudflare-build.outcome == 'success') && steps.playwright-cache.outputs.cache-hit != 'true' }} + - name: Run Node E2E + id: node-e2e + if: ${{ !cancelled() && steps.node-build.outcome == 'success' }} + run: ENDFORM=false pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: failure() && steps.node-e2e.outcome == 'failure' + with: + name: playwright-report-node-native-${{ matrix.shardIndex }} + path: | + playwright-report/ + test-results/ + retention-days: 7 + - name: Run Cloudflare E2E + id: cloudflare-e2e + if: ${{ !cancelled() && steps.cloudflare-build.outcome == 'success' }} + run: ENDFORM=false pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + env: + EMDASH_E2E_TARGET: cloudflare + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: failure() && steps.cloudflare-e2e.outcome == 'failure' + with: + name: playwright-report-cloudflare-native-${{ matrix.shardIndex }} + path: | + playwright-report/ + test-results/ + retention-days: 7 + + test-e2e-endform: + name: E2E built Endform (full suite) + needs: change-plan + if: needs.change-plan.outputs.e2e_node == 'true' || needs.change-plan.outputs.e2e_cloudflare == 'true' + runs-on: ubuntu-latest + timeout-minutes: 90 + permissions: + contents: read + id-token: write + env: + ENDFORM_GIT_BRANCH: ${{ github.head_ref || github.ref_name }} + ENDFORM_GIT_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Build Node fixture + id: node-build + if: ${{ !cancelled() && needs.change-plan.outputs.e2e_node == 'true' }} + run: pnpm run --filter emdash... build + - name: Build Cloudflare fixture + id: cloudflare-build + if: ${{ !cancelled() && needs.change-plan.outputs.e2e_cloudflare == 'true' }} + run: pnpm run --filter "emdash-e2e-fixture-cloudflare..." build - name: Run Node E2E id: node-e2e if: ${{ !cancelled() && steps.node-build.outcome == 'success' }} run: | - if [ "${{ matrix.runner }}" = native ]; then - ENDFORM=false pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - else - pnpm exec endform --version - pnpm exec tsx e2e/run-endform.ts test --organization-id 2G1ZCj7X --timeout 240000 - fi + pnpm exec endform --version + pnpm exec tsx e2e/run-endform.ts test --organization-id 2G1ZCj7X --timeout 240000 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() && steps.node-e2e.outcome == 'failure' with: - name: playwright-report-node-${{ matrix.runner }}-${{ matrix.shardIndex }} + name: playwright-report-node-endform path: | playwright-report/ test-results/ @@ -485,18 +529,14 @@ jobs: id: cloudflare-e2e if: ${{ !cancelled() && steps.cloudflare-build.outcome == 'success' }} run: | - if [ "${{ matrix.runner }}" = native ]; then - ENDFORM=false pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - else - pnpm exec endform --version - pnpm exec tsx e2e/run-endform.ts test --organization-id 2G1ZCj7X --timeout 240000 - fi + pnpm exec endform --version + pnpm exec tsx e2e/run-endform.ts test --organization-id 2G1ZCj7X --timeout 240000 env: EMDASH_E2E_TARGET: cloudflare - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() && steps.cloudflare-e2e.outcome == 'failure' with: - name: playwright-report-cloudflare-${{ matrix.runner }}-${{ matrix.shardIndex }} + name: playwright-report-cloudflare-endform path: | playwright-report/ test-results/ diff --git a/package.json b/package.json index fcf2c8d988..f2322bed44 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@types/node": "catalog:", "@typescript/native-preview": "7.0.0-dev.20260421.2", "emdash": "workspace:*", - "endform": "0.81.4", + "endform": "0.81.5", "knip": "^5.84.1", "oxfmt": "^0.59.0", "oxlint": "^1.74.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48b17f320d..11d6dc67c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -375,8 +375,8 @@ importers: specifier: workspace:* version: link:packages/core endform: - specifier: 0.81.4 - version: 0.81.4 + specifier: 0.81.5 + version: 0.81.5 knip: specifier: ^5.84.1 version: 5.84.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.10.13)(typescript@6.0.0-beta) @@ -9787,6 +9787,40 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + endform-darwin-arm64@0.81.5: + resolution: {integrity: sha512-+lODLjRXR2mgh9djn4hGlRrOAs6FZWl6Uy5VvnqmqhWvj7EZZ0TFDaO4PDavqkTu4azexn8t+wwFQmbprbYVxw==} + cpu: [arm64] + os: [darwin] + + endform-darwin-x86@0.81.5: + resolution: {integrity: sha512-bIdlYSQ6owQhb093fH9aadTPwZl/Jt2NtRFUgNVwNtnNmnCJAhoMMtdw2h+Xqfp+vvQ2unIyNMDO9rbTwdIqjA==} + cpu: [x64] + os: [darwin] + + endform-linux-arm64@0.81.5: + resolution: {integrity: sha512-r3aqkLWohnT01Cmc1zjmgSI80rTRjWJzxmJGaKBQAL45h08VluKvV/LGBpc6FkIeGqhpTECu7MjUv9/Km24fAg==} + cpu: [arm64] + os: [linux] + + endform-linux-x86@0.81.5: + resolution: {integrity: sha512-l2ehMvjLIE/m0skqvRV0pXNZW0Vqhs4vswOxVbIt5W6KsX7+uoqd3/D6c49CIjMQeadOHIFihAWu35vgsRykXg==} + cpu: [x64] + os: [linux] + + endform-win32-arm64@0.81.5: + resolution: {integrity: sha512-HI7WqWGFhEIJbOKYS17ISxKjb6PjosiOKcxxGXFt2yY8B4dUjSvYdKNQKaHU0ZmvB26z0kQ6eWsrhyPrO+H0DA==} + cpu: [arm64] + os: [win32] + + endform-win32-x86@0.81.5: + resolution: {integrity: sha512-CSsiG4m7C62qCVw/UoizPC0mbPZmN0qydKW/6auW66gglB9VO51+daBPbSsR8g4d3CrTFwFv6saQ51AVsj02Tg==} + cpu: [x64] + os: [win32] + + endform@0.81.5: + resolution: {integrity: sha512-8oOSOy8AzXnAO6Wwt1qMLdTbbCU/lWgorAVkCBKf2o5zbdVMJmkkkhv+5Ri99jhfojulu2lpjD04sWw/Pm5Lzg==} + hasBin: true + enhanced-resolve@5.24.2: resolution: {integrity: sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==} engines: {node: '>=10.13.0'} @@ -13274,40 +13308,6 @@ packages: zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - endform-darwin-arm64@0.81.4: - resolution: {integrity: sha512-KZNlY+Wfhe+t4J+QDLwgI9fNSUzILflOLLZRoHyQ0CAyUpEuFPsjkTjMWr/uvlxcGQgxmfk+UGxakcST3qfj5g==} - cpu: [arm64] - os: [darwin] - - endform-darwin-x86@0.81.4: - resolution: {integrity: sha512-i1Nm9R6ykJuTpGbPa6eC/KyKTyyx0Tlhhvom9cX467JyQQ4weornnWQ9agywcNgYgOCd8QsslkNOnp30mj7Q9w==} - cpu: [x64] - os: [darwin] - - endform-linux-arm64@0.81.4: - resolution: {integrity: sha512-nYauV4GAxmH6S5qIlFcrdhPl2aUfpGgbS0O7BJ9MXGQ+3Rjhgg7g2S9ZK6tHblezoZYHeHg6TL3r2v9XbIzbIg==} - cpu: [arm64] - os: [linux] - - endform-linux-x86@0.81.4: - resolution: {integrity: sha512-HOz59BQhVKANp4pCIAlgqxyOTz7wgwUcnM1prQACULq/ixUVNPqrIkXzYYz0xtXC1CsgdW+/32YSoTJfoCUsTw==} - cpu: [x64] - os: [linux] - - endform-win32-arm64@0.81.4: - resolution: {integrity: sha512-zkiG2ctCXSdTs9VAbOQM7xrbLE0jjTf8pZhc8XlBlYmiaGzqL2+pqWMutuf964Tk7NOQoFs3F6SEyZJe5TBgMg==} - cpu: [arm64] - os: [win32] - - endform-win32-x86@0.81.4: - resolution: {integrity: sha512-0EZZd6RolVGmLpaa2299VIzXHocTECzsjw6qydbgov6F+w7h6ebzvGdLNc+0wWnz+G3jdoqjdpf7HhAAJA6Arw==} - cpu: [x64] - os: [win32] - - endform@0.81.4: - resolution: {integrity: sha512-PWi2OWQtGVs91u4b3FmB8RkWKekQU8Q523SmowhOFrkbmz6KC+703Ql+LHaXtGsfEmJcG71I/kPQXfhtNBcTVg==} - hasBin: true - snapshots: '@adraffy/ens-normalize@1.11.1': {} @@ -20453,6 +20453,33 @@ snapshots: once: 1.4.0 optional: true + endform-darwin-arm64@0.81.5: + optional: true + + endform-darwin-x86@0.81.5: + optional: true + + endform-linux-arm64@0.81.5: + optional: true + + endform-linux-x86@0.81.5: + optional: true + + endform-win32-arm64@0.81.5: + optional: true + + endform-win32-x86@0.81.5: + optional: true + + endform@0.81.5: + optionalDependencies: + endform-darwin-arm64: 0.81.5 + endform-darwin-x86: 0.81.5 + endform-linux-arm64: 0.81.5 + endform-linux-x86: 0.81.5 + endform-win32-arm64: 0.81.5 + endform-win32-x86: 0.81.5 + enhanced-resolve@5.24.2: dependencies: graceful-fs: 4.2.11 @@ -24550,30 +24577,3 @@ snapshots: tslib: 2.3.0 zwitch@2.0.4: {} - - endform-darwin-arm64@0.81.4: - optional: true - - endform-darwin-x86@0.81.4: - optional: true - - endform-linux-arm64@0.81.4: - optional: true - - endform-linux-x86@0.81.4: - optional: true - - endform-win32-arm64@0.81.4: - optional: true - - endform-win32-x86@0.81.4: - optional: true - - endform@0.81.4: - optionalDependencies: - endform-darwin-arm64: 0.81.4 - endform-darwin-x86: 0.81.4 - endform-linux-arm64: 0.81.4 - endform-linux-x86: 0.81.4 - endform-win32-arm64: 0.81.4 - endform-win32-x86: 0.81.4