diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 79b2c50bc..a4add73d3 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -10,18 +10,21 @@ permissions: contents: read jobs: - integration: + # The suite outgrew a single runner, and then a half-suite outgrew a slow + # one: two shards cleared in ~14 minutes on a quiet day, but with six pull + # requests running side by side the larger shard hit the same 25-minute + # guard the split was meant to escape. Three shards keep the biggest slice + # around 60 files, which clears the guard with real headroom even on the + # slowest runner observed. Each shard boots its own Postgres testcontainer; + # the timeout stays a hang detector, not a performance budget. + shard: runs-on: ubuntu-latest - # This guard exists to kill a hung testcontainer or a deadlocked query, not - # to hold the suite to a performance budget. At 15 it had stopped telling - # those apart: the suite itself takes around 13 minutes of job time (128 - # files against a real Postgres), so two consecutive runs of the SAME - # commit range came in at 12m50s and then over 15m, and the second was - # killed mid-run with every test to that point green. A cap that a healthy - # run clears only on a fast runner reports runner weather, not health. - # 25 leaves real headroom and still catches a hang inside one CI cycle. - # If the suite ever approaches this number, split it rather than raise it. timeout-minutes: 25 + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3] + name: integration shard ${{ matrix.shard }}/3 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -36,7 +39,12 @@ jobs: - run: pnpm db:generate - - run: pnpm test:integration + # vitest is invoked directly: pnpm passes the `--` separator through to + # the script literally, so `pnpm test:integration -- --shard=…` hands + # vitest a stray positional argument and the shard flag never applies — + # both "shards" then run the full suite. Proven by a paired local run + # before this workflow shipped. + - run: pnpm exec vitest run --config vitest.integration.config.mts --shard=${{ matrix.shard }}/3 env: # The integration suite boots a Postgres testcontainer and points # Prisma at it via DATABASE_URL set inside `tests/integration/setup.ts`. @@ -50,3 +58,19 @@ jobs: API_TOKEN_HMAC_KEY: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" SESSION_SECRET: "integration-tests" + + # Branch protection requires a check named exactly "integration". This job + # carries that name and answers for both shards: it fails when any shard + # failed or was cancelled, so a half-green matrix cannot satisfy the gate. + integration: + runs-on: ubuntu-latest + needs: shard + if: always() + timeout-minutes: 5 + steps: + - name: Every shard finished green + run: | + if [ "${{ needs.shard.result }}" != "success" ]; then + echo "shard result: ${{ needs.shard.result }}" + exit 1 + fi diff --git a/tests/integration/cycle-flow-opens-cycle.test.ts b/tests/integration/cycle-flow-opens-cycle.test.ts index 5e81fda56..7a9fe7d84 100644 --- a/tests/integration/cycle-flow-opens-cycle.test.ts +++ b/tests/integration/cycle-flow-opens-cycle.test.ts @@ -70,6 +70,14 @@ beforeEach(async () => { username: "cycle-flow", email: "cycle-flow@example.test", gender: "FEMALE", + // The suite runs under TZ=UTC and computes its relative day keys with + // local Date arithmetic, i.e. in UTC. The route resolves "today" in the + // USER's zone and falls back to Europe/Berlin when none is set - one + // hour past 22:00 UTC the two clocks disagree about the date and every + // day-of-cycle assertion is off by one. Pinning the fixture zone to UTC + // makes the test's clock and the route's clock the same clock at any + // hour of the day. + timezone: "UTC", }, }); await loginAs(USER_ID); diff --git a/tests/integration/cycle-routes.test.ts b/tests/integration/cycle-routes.test.ts index 8dc05e92b..d75c8822f 100644 --- a/tests/integration/cycle-routes.test.ts +++ b/tests/integration/cycle-routes.test.ts @@ -69,6 +69,14 @@ beforeEach(async () => { username: "cycle-female", email: "cycle-female@example.test", gender: "FEMALE", + // The suite runs under TZ=UTC and computes its relative day keys with + // local Date arithmetic, i.e. in UTC. The route resolves "today" in the + // USER's zone and falls back to Europe/Berlin when none is set - one + // hour past 22:00 UTC the two clocks disagree about the date and every + // day-of-cycle assertion is off by one. Pinning the fixture zone to UTC + // makes the test's clock and the route's clock the same clock at any + // hour of the day. + timezone: "UTC", }, }); await prisma.user.create({ @@ -77,6 +85,7 @@ beforeEach(async () => { username: "cycle-male", email: "cycle-male@example.test", gender: "MALE", + timezone: "UTC", }, }); });