From 5fe952b63de6ce9ec4aa14f1929ddb9f12f065be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 21 Jul 2026 02:30:40 +0200 Subject: [PATCH] fix(ci): harden release and e2e verification --- .github/workflows/e2e.yml | 3 ++- .github/workflows/integration.yml | 2 +- .github/workflows/post-publish-verify.yml | 7 +++--- Dockerfile | 8 +++---- e2e/settings-export.spec.ts | 18 +++++++++++++++ e2e/share-documents.spec.ts | 25 +++++++++++++++++---- next.config.ts | 1 - scripts/__tests__/ci-determinism.test.ts | 20 +++++++++++++++-- scripts/__tests__/release-workflows.test.ts | 24 ++++++++++++++------ vitest.integration.config.mts | 3 ++- 10 files changed, 87 insertions(+), 24 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 43e332643..526a6e57b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -37,10 +37,11 @@ jobs: DATABASE_URL: postgresql://healthlog:healthlog@localhost:5432/healthlog?schema=public ENCRYPTION_KEYS: '{"v1":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' ENCRYPTION_ACTIVE_KEY_ID: v1 - API_TOKEN_HMAC_KEY: ci-hmac-key-32-bytes-min-padding-x + API_TOKEN_HMAC_KEY: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb AUTH_RP_NAME: HealthLog AUTH_RP_ID: localhost AUTH_RP_ORIGIN: http://localhost:3000 + SESSION_COOKIE_SECURE: "false" NODE_ENV: production steps: diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 867ce3ac2..d1e16580b 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -38,6 +38,6 @@ jobs: # string "0" — failing crypto's hex/base64 length check and breaking # the backups + integration-status suites for 50+ runs (regression # since v1.4.13). Quoting forces a literal scalar. - API_TOKEN_HMAC_KEY: "integration-tests-do-not-use-in-prod" + API_TOKEN_HMAC_KEY: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" SESSION_SECRET: "integration-tests" diff --git a/.github/workflows/post-publish-verify.yml b/.github/workflows/post-publish-verify.yml index 15900b5d9..6f4347795 100644 --- a/.github/workflows/post-publish-verify.yml +++ b/.github/workflows/post-publish-verify.yml @@ -111,7 +111,7 @@ jobs: -e DATABASE_URL="$DATABASE_URL" \ -e ENCRYPTION_KEYS='{"v1":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' \ -e ENCRYPTION_ACTIVE_KEY_ID=v1 \ - -e API_TOKEN_HMAC_KEY=verify-hmac-key-32-bytes-padding-x \ + -e API_TOKEN_HMAC_KEY=cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc \ -e AUTH_RP_NAME=HealthLog \ -e AUTH_RP_ID=localhost \ -e AUTH_RP_ORIGIN=http://localhost:3000 \ @@ -135,10 +135,11 @@ jobs: exit 1 fi + EXPECTED_VERSION="${IMAGE_TAG#v}" echo "$body" | jq . - if ! jq -e --arg expected "$IMAGE_TAG" '.data.version == $expected' <<< "$body" >/dev/null; then + if ! jq -e --arg expected "$EXPECTED_VERSION" '.data.version == $expected' <<< "$body" >/dev/null; then actual="$(jq -r '.data.version // ""' <<< "$body")" - echo "::error::Version mismatch: exact image reports '$actual', expected '$IMAGE_TAG'." >&2 + echo "::error::Version mismatch: exact image reports '$actual', expected '$EXPECTED_VERSION' (release tag '$IMAGE_TAG')." >&2 exit 1 fi if ! jq -e --arg expected "$BUILD_SHA" '.data.buildSha == $expected' <<< "$body" >/dev/null; then diff --git a/Dockerfile b/Dockerfile index fe0462e13..540a6b0f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -80,10 +80,10 @@ RUN addgroup --system --gid 1001 nodejs && \ COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -# Next's standalone output must carry every externalized worker dependency. -# Fail the image build here if output tracing ever stops shipping one rather -# than compensating with a second, independently versioned npm installation. -RUN node -e "require.resolve('pg-boss'); require.resolve('@prisma/adapter-pg'); require.resolve('pg'); require.resolve('@prisma/client')" +# Next's standalone output must carry the externalized worker dependencies. +# Prisma's pure-JS adapter stays bundled; only modules loaded through native +# Node resolution belong in this runtime assertion. +RUN node -e "require.resolve('pg-boss'); require.resolve('pg')" # @napi-rs/canvas (document PDF rasterization): Next's file tracer copies the # native binary's package into the standalone tree but NOT the pnpm symlinks that diff --git a/e2e/settings-export.spec.ts b/e2e/settings-export.spec.ts index e8a04d1af..b116e6a16 100644 --- a/e2e/settings-export.spec.ts +++ b/e2e/settings-export.spec.ts @@ -94,9 +94,27 @@ test.describe("Settings → Export & Import", () => { }) => { // v1.18.0 (S5) — the panel (and its disclosure) live on the // dedicated Gesundheitsakte section now. + await page.route("**/api/share-links", async (route) => { + if (route.request().method() !== "GET") { + await route.continue(); + return; + } + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ data: [], error: null, meta: null }), + }); + }); + const sharingDataReady = page.waitForResponse( + (response) => + response.status() === 200 && + response.request().method() === "GET" && + new URL(response.url()).pathname === "/api/share-links", + ); await page.goto("/settings/gesundheitsakte", { waitUntil: "domcontentloaded", }); + await sharingDataReady; const toggle = page.getByTestId("health-record-included-data-toggle"); await expect(toggle).toBeVisible({ timeout: 10_000 }); // Collapsed on first render — the checklist panel is absent. diff --git a/e2e/share-documents.spec.ts b/e2e/share-documents.spec.ts index 1163bd826..d33cfe1de 100644 --- a/e2e/share-documents.spec.ts +++ b/e2e/share-documents.spec.ts @@ -57,7 +57,14 @@ interface CreatedShare { async function createShareWithDocs(page: Page): Promise { const label = `${SHARE_DOC_PREFIX} ${Date.now()}`; + const sharingDataReady = page.waitForResponse( + (response) => + response.status() === 200 && + response.request().method() === "GET" && + new URL(response.url()).pathname === "/api/share-links", + ); await page.goto("/settings/sharing"); + await sharingDataReady; await page.locator("#share-label").fill(label); // Open the document picker and isolate the seeded trio. @@ -69,13 +76,18 @@ async function createShareWithDocs(page: Page): Promise { // Exactly the three share-fixture documents match the prefix. const rows = list.getByRole("button"); await expect(rows).toHaveCount(3); - for (let i = 0; i < 3; i++) await rows.nth(i).click(); + for (let i = 0; i < 3; i++) { + const row = rows.nth(i); + await row.click(); + await expect(row).toHaveAttribute("aria-pressed", "true"); + } await page.getByRole("button", { name: "Done" }).click(); // Three chips staged before create. await expect( page.getByTestId("share-attached-chips").getByRole("listitem"), ).toHaveCount(3); + await expect(page.getByRole("button", { name: "Create link" })).toBeEnabled(); await page.getByRole("button", { name: "Create link" }).click(); @@ -150,9 +162,14 @@ test.describe("clinician document sharing", () => { clinician.getByRole("heading", { name: "Documents" }), ).toBeVisible(); // Class A image → inline at the token-scoped serve route. - await expect( - clinician.locator(`img[src*="/d/${SHARE_JPEG_DOC_ID}"]`), - ).toBeVisible(); + const inlineImage = clinician.getByRole("img", { + name: "Share e2e scan", + }); + await expect(inlineImage).toBeVisible(); + await expect(inlineImage).toHaveAttribute( + "src", + new RegExp(`/d/${SHARE_JPEG_DOC_ID}$`), + ); // Class A PDF → inline