mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-08-28 02:44:59 +00:00
Repair scheduled QA workflow dispatch (#588)
This commit is contained in:
parent
1dc4407c69
commit
5b4948f34f
7 changed files with 68 additions and 38 deletions
20
.github/workflows/scheduled-qa.yml
vendored
20
.github/workflows/scheduled-qa.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue