fix(ci): parse Postgres array labels so the runner-label risk check fires #551

Merged
gmackie merged 1 commit from fix/runner-label-parsing into main 2026-09-04 18:40:28 +00:00
Owner

Follow-up to #549. The runner-label coverage check that PR shipped has never produced a single result in production.

The bug

It guarded with Array.isArray(row.labels). But ci_runners.labels is a text[] that does not come back as a JavaScript array on the deployed stack — it arrives as the raw Postgres array literal:

{ubuntu-latest,ubuntu-24.04,forgegraph-ci}

So every row yielded an empty label list, coverage was empty, and labelRisks came back [].

Verified against production minutes after #549 merged:

GET /api/fg/ci/queue
  verdict: idle
  runners: 6 / 7
  labelRisks: []        <- should have listed ubuntu-latest

At that moment ubuntu-latest was carried by exactly one online runner (labnuc; vanuc offline since 01:05). That is precisely the condition the check exists to catch, and it reported clean. The same fleet also has images with zero online runners and several other single-runner labels, none of which appeared.

Why it slipped through

apps/web/src/app/api/fg/ci/runners/route.ts already carried a hand-written parser for this exact column — quote handling, escapes, NULL filtering — which is the only reason that endpoint renders labels at all. The knowledge was in the repo; the new code just did not use it.

The fix

Extract that parser to packages/api/src/lib/runner-labels.ts, delete the route's local copy, and have all three readers (buildQueue.queueStats, GET /api/fg/ci/queue, GET /api/fg/ci/runners) share one implementation so they cannot drift again.

Also folds in runnerLabelName. act_runner registers labels with an execution suffix — labnuc's on-disk config has ubuntu-latest:docker://localhost/forgegraph-act:node-24.14.0 — and Forgejo matches on the part before the first colon, so the suffix has to be dropped or one pool counts as two separate ones.

Tests

9 new tests covering the shapes production actually returns, not the shape the code assumed: the Postgres literal, a real JavaScript array, quoted elements, escaped quotes, NULL members, empty/malformed input, and mixed-type arrays. Plus the suffix stripping.

Existing ci/runners route tests still pass after the refactor (2/2). packages/api and apps/web both typecheck clean.

Note

This is the second time on this install that a parser written against an assumed shape silently no-opped against the real one. Sharing the single implementation is the point of the change, not just fixing the guard.

🤖 Generated with Claude Code

Follow-up to #549. The runner-label coverage check that PR shipped has never produced a single result in production. ## The bug It guarded with `Array.isArray(row.labels)`. But `ci_runners.labels` is a `text[]` that does **not** come back as a JavaScript array on the deployed stack — it arrives as the raw Postgres array literal: ``` {ubuntu-latest,ubuntu-24.04,forgegraph-ci} ``` So every row yielded an empty label list, coverage was empty, and `labelRisks` came back `[]`. **Verified against production** minutes after #549 merged: ``` GET /api/fg/ci/queue verdict: idle runners: 6 / 7 labelRisks: [] <- should have listed ubuntu-latest ``` At that moment `ubuntu-latest` was carried by exactly one online runner (labnuc; vanuc offline since 01:05). That is precisely the condition the check exists to catch, and it reported clean. The same fleet also has `images` with zero online runners and several other single-runner labels, none of which appeared. ## Why it slipped through `apps/web/src/app/api/fg/ci/runners/route.ts` already carried a hand-written parser for this exact column — quote handling, escapes, `NULL` filtering — which is the only reason that endpoint renders labels at all. The knowledge was in the repo; the new code just did not use it. ## The fix Extract that parser to `packages/api/src/lib/runner-labels.ts`, delete the route's local copy, and have all three readers (`buildQueue.queueStats`, `GET /api/fg/ci/queue`, `GET /api/fg/ci/runners`) share one implementation so they cannot drift again. Also folds in `runnerLabelName`. act_runner registers labels with an execution suffix — labnuc's on-disk config has `ubuntu-latest:docker://localhost/forgegraph-act:node-24.14.0` — and Forgejo matches on the part before the first colon, so the suffix has to be dropped or one pool counts as two separate ones. ## Tests 9 new tests covering the shapes production actually returns, not the shape the code assumed: the Postgres literal, a real JavaScript array, quoted elements, escaped quotes, `NULL` members, empty/malformed input, and mixed-type arrays. Plus the suffix stripping. Existing `ci/runners` route tests still pass after the refactor (2/2). `packages/api` and `apps/web` both typecheck clean. ## Note This is the second time on this install that a parser written against an assumed shape silently no-opped against the real one. Sharing the single implementation is the point of the change, not just fixing the guard. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(ci): parse Postgres array labels so the runner-label risk check fires
All checks were successful
CI / gitleaks (pull_request) Successful in 6s
CI / storybook (pull_request) Successful in 1m33s
forgegraph/ci CI passed
CI / ci (pull_request) Successful in 9m52s
4c28b8b169
#549 shipped a runner-label coverage check and it has never produced a
single result in production. It guarded with `Array.isArray(row.labels)`,
but `ci_runners.labels` is a text[] that does NOT come back as a
JavaScript array on the deployed stack -- it arrives as the raw Postgres
literal, `{ubuntu-latest,ubuntu-24.04,forgegraph-ci}`. So every row
yielded an empty label list, coverage was empty, and `labelRisks` came
back `[]`.

Verified against production right after the merge: GET /api/fg/ci/queue
returned `labelRisks: []` for a fleet where `ubuntu-latest` was carried
by exactly one online runner (labnuc; vanuc offline since 01:05) -- the
precise condition the check exists to catch, reported as clean.

`apps/web/.../ci/runners/route.ts` already carried a hand parser for this
column, which is the only reason that endpoint renders labels at all. So
the knowledge existed and the new code did not use it. Extract that
parser to packages/api/src/lib/runner-labels.ts, delete the route's local
copy, and have all three readers share it so they cannot drift again.

Also fold in `runnerLabelName`: act_runner registers labels with an
execution suffix (labnuc's config has
`ubuntu-latest:docker://localhost/forgegraph-act:node-24.14.0`) and
Forgejo matches on the part before the first colon, so the suffix has to
be dropped or one pool counts as two.

Tests cover the shapes production actually returns -- the Postgres
literal, a real array, quoted and escaped elements, NULL members, and
non-string input -- rather than only the shape the code assumed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
gmackie/ForgeGraph!551
No description provided.