diff --git a/.github/workflows/scheduled-qa.yml b/.github/workflows/scheduled-qa.yml index 665bca35..e04aec52 100644 --- a/.github/workflows/scheduled-qa.yml +++ b/.github/workflows/scheduled-qa.yml @@ -23,6 +23,7 @@ concurrency: env: NODE_VERSION: '22' + K6_IMAGE: grafana/k6:1.7.1 LOG_LEVEL: warn VERITAS_ADMIN_KEY: scheduled-qa-admin-key-000000000000 VERITAS_AUTH_LOCALHOST_BYPASS: 'true' @@ -34,8 +35,6 @@ jobs: name: Playwright E2E runs-on: ubuntu-latest timeout-minutes: 25 - env: - VERITAS_DATA_DIR: ${{ runner.temp }}/veritas-playwright-data steps: - uses: actions/checkout@v6 @@ -49,6 +48,14 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Configure isolated Playwright data directory + run: | + echo "VERITAS_DATA_DIR=$RUNNER_TEMP/veritas-playwright-data" >> "$GITHUB_ENV" + mkdir -p "$RUNNER_TEMP/veritas-playwright-data" + + - name: Build shared package + run: pnpm --filter @veritas-kanban/shared build + - name: Install Chromium run: pnpm exec playwright install --with-deps chromium @@ -75,7 +82,6 @@ jobs: timeout-minutes: 30 env: K6_PROFILE: ${{ github.event_name == 'workflow_dispatch' && inputs.load_profile || 'smoke' }} - VERITAS_DATA_DIR: ${{ runner.temp }}/veritas-k6-data steps: - uses: actions/checkout@v6 @@ -89,6 +95,11 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Configure isolated k6 data directory + run: | + echo "VERITAS_DATA_DIR=$RUNNER_TEMP/veritas-k6-data" >> "$GITHUB_ENV" + mkdir -p "$RUNNER_TEMP/veritas-k6-data" + - name: Build runtime packages run: pnpm build @@ -116,6 +127,7 @@ jobs: set -euo pipefail mkdir -p k6-results + chmod 0777 k6-results if [ "$K6_PROFILE" = "full" ]; then scripts="smoke read-load write-load mixed-load ws-stress v5-remote-mix" @@ -136,7 +148,7 @@ jobs: -e V5_WS_HOLD_MS=40000 \ -v "$PWD:/work" \ -w /work \ - grafana/k6:latest run \ + "$K6_IMAGE" run \ --summary-export "k6-results/${script}.json" \ "load-tests/k6/${script}.js" 2>&1 | tee "k6-results/${script}.log" done diff --git a/docs/testing/scheduled-qa-gates.md b/docs/testing/scheduled-qa-gates.md index 8b124677..971b45ab 100644 --- a/docs/testing/scheduled-qa-gates.md +++ b/docs/testing/scheduled-qa-gates.md @@ -7,6 +7,19 @@ the fast pull-request path. Pull requests stay limited to lint, typecheck, workspace unit tests, build, production dependency audit, and the desktop artifact gate when relevant. +The 2026-06-04 audit found the workflow failing before job creation because +job-level `env` used the `runner.temp` context. GitHub does not expose the +`runner` context until a job is running, so manual dispatch returned a parse +error instead of producing logs. The workflow now writes `VERITAS_DATA_DIR` +from `$RUNNER_TEMP` during job setup. + +Playwright and `pnpm qa:mantine` remain scheduled/manual gates while #568 and +#569 are open. Adding them to PR CI before those gates are stable would create +red PR checks with known non-PR-specific failures. Once both gates pass on +`main`, either add a small PR smoke job for `pnpm qa:mantine` and +`pnpm test:e2e -- e2e/mantine-qa-gate.spec.ts`, or record the release decision +to keep them scheduled-only here. + ## Workflow Workflow file: @@ -54,7 +67,11 @@ Retention: 7 days. ## k6 Gate The k6 job starts the built API server, waits for `/api/health`, then runs the -selected profile through the official k6 Docker image. +selected profile through the pinned official k6 Docker image: + +```text +grafana/k6:1.7.1 +``` Default scheduled profile: diff --git a/load-tests/k6/mixed-load.js b/load-tests/k6/mixed-load.js index 34c91232..db5b6b51 100644 --- a/load-tests/k6/mixed-load.js +++ b/load-tests/k6/mixed-load.js @@ -36,7 +36,9 @@ function readScenario() { // Read a random task from the list try { const body = JSON.parse(listRes.body); - const tasks = Array.isArray(body) ? body : body.tasks || []; + const tasks = Array.isArray(body) + ? body + : body.tasks || (Array.isArray(body.data) ? body.data : body.data?.tasks) || []; if (tasks.length > 0) { const id = tasks[Math.floor(Math.random() * tasks.length)].id; const detailRes = http.get(`${API_BASE}/tasks/${id}`, { @@ -56,14 +58,10 @@ function readScenario() { // ── Write scenario (30 %) ──────────────────────────────────── function writeScenario() { const payload = makeTask('mixed'); - const createRes = http.post( - `${API_BASE}/tasks`, - JSON.stringify(payload), - { - headers: defaultHeaders, - tags: { name: 'POST /tasks' }, - } - ); + const createRes = http.post(`${API_BASE}/tasks`, JSON.stringify(payload), { + headers: defaultHeaders, + tags: { name: 'POST /tasks' }, + }); const createOk = check(createRes, { 'create → 201': (r) => r.status === 201 }); errorRate.add(!createOk); @@ -74,7 +72,7 @@ function writeScenario() { } const created = JSON.parse(createRes.body); - const taskId = created.id || created.task?.id; + const taskId = created.id || created.task?.id || created.data?.id || created.data?.task?.id; sleep(0.2); diff --git a/load-tests/k6/read-load.js b/load-tests/k6/read-load.js index f84c3d45..5f29d353 100644 --- a/load-tests/k6/read-load.js +++ b/load-tests/k6/read-load.js @@ -44,7 +44,9 @@ export default function () { let taskId = null; try { const body = JSON.parse(listRes.body); - const tasks = Array.isArray(body) ? body : body.tasks || []; + const tasks = Array.isArray(body) + ? body + : body.tasks || (Array.isArray(body.data) ? body.data : body.data?.tasks) || []; if (tasks.length > 0) { // Pick a random task taskId = tasks[Math.floor(Math.random() * tasks.length)].id; diff --git a/load-tests/k6/smoke.js b/load-tests/k6/smoke.js index 12711836..0729fd83 100644 --- a/load-tests/k6/smoke.js +++ b/load-tests/k6/smoke.js @@ -21,18 +21,16 @@ export const options = { export default function () { // ── CREATE ───────────────────────────────────────────────── const payload = makeTask('smoke'); - const createRes = http.post( - `${API_BASE}/tasks`, - JSON.stringify(payload), - { headers: defaultHeaders } - ); + const createRes = http.post(`${API_BASE}/tasks`, JSON.stringify(payload), { + headers: defaultHeaders, + }); const createOk = check(createRes, { 'POST /tasks → 201': (r) => r.status === 201, 'POST /tasks → has id': (r) => { try { const body = JSON.parse(r.body); - return !!(body.id || (body.task && body.task.id)); + return !!(body.id || body.task?.id || body.data?.id || body.data?.task?.id); } catch { return false; } @@ -45,7 +43,7 @@ export default function () { } const created = JSON.parse(createRes.body); - const taskId = created.id || created.task?.id; + const taskId = created.id || created.task?.id || created.data?.id || created.data?.task?.id; sleep(0.3); @@ -83,7 +81,12 @@ export default function () { 'GET /tasks → is array or has tasks': (r) => { try { const body = JSON.parse(r.body); - return Array.isArray(body) || Array.isArray(body.tasks); + return ( + Array.isArray(body) || + Array.isArray(body.tasks) || + Array.isArray(body.data) || + Array.isArray(body.data?.tasks) + ); } catch { return false; } @@ -98,7 +101,6 @@ export default function () { }); check(delRes, { - 'DELETE /tasks/:id → 200 or 204': (r) => - r.status === 200 || r.status === 204, + 'DELETE /tasks/:id → 200 or 204': (r) => r.status === 200 || r.status === 204, }); } diff --git a/load-tests/k6/v5-remote-mix.js b/load-tests/k6/v5-remote-mix.js index 34c3d783..13d3be3c 100644 --- a/load-tests/k6/v5-remote-mix.js +++ b/load-tests/k6/v5-remote-mix.js @@ -87,7 +87,9 @@ function createTask(prefix) { tags: { name: 'POST /tasks' }, }); const body = parseJson(response); - return response.status === 201 ? body?.id || body?.task?.id || null : null; + return response.status === 201 + ? body?.id || body?.task?.id || body?.data?.id || body?.data?.task?.id || null + : null; } export function setup() { @@ -117,8 +119,9 @@ export function setup() { } ); const body = parseJson(response); - if (response.status === 200 && body?.sessionId) { - sessionIds.push(body.sessionId); + const sessionId = body?.sessionId || body?.data?.sessionId; + if (response.status === 200 && sessionId) { + sessionIds.push(sessionId); } } diff --git a/load-tests/k6/write-load.js b/load-tests/k6/write-load.js index 363c0074..1fb09f09 100644 --- a/load-tests/k6/write-load.js +++ b/load-tests/k6/write-load.js @@ -27,14 +27,10 @@ export const options = { export default function () { // ── CREATE ───────────────────────────────────────────────── const payload = makeTask('write-load'); - const createRes = http.post( - `${API_BASE}/tasks`, - JSON.stringify(payload), - { - headers: defaultHeaders, - tags: { name: 'POST /tasks' }, - } - ); + const createRes = http.post(`${API_BASE}/tasks`, JSON.stringify(payload), { + headers: defaultHeaders, + tags: { name: 'POST /tasks' }, + }); const createOk = check(createRes, { 'create → 201': (r) => r.status === 201, @@ -47,7 +43,7 @@ export default function () { } const created = JSON.parse(createRes.body); - const taskId = created.id || created.task?.id; + const taskId = created.id || created.task?.id || created.data?.id || created.data?.task?.id; sleep(0.2);