Repair scheduled QA workflow dispatch (#588)

This commit is contained in:
Brad Groux 2026-06-04 14:54:55 -07:00 committed by GitHub
parent 1dc4407c69
commit 5b4948f34f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 68 additions and 38 deletions

View file

@ -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

View file

@ -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:

View file

@ -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);

View file

@ -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;

View file

@ -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,
});
}

View file

@ -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);
}
}

View file

@ -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);