Files
multica/server/migrations/222_github_pr_api_snapshot.up.sql
Bohan-J 09fc98c70c fix(github): concurrent check-run index migration + singleflight token mint
Address Elon's third-round review on the MUL-5265 PR snapshot pipeline.

Must-fix — migration built a non-concurrent index. The
github_pull_request_check_run table declared PRIMARY KEY (pr_id, ordinal)
inside CREATE TABLE, which builds a unique index synchronously and violates
the repo rule that every migration-created index (including on a new table)
use CREATE UNIQUE INDEX CONCURRENTLY in its own single-statement file. Split:
222 now creates the table without a primary key; new 223 adds the
(pr_id, ordinal) unique index CONCURRENTLY. The atomic delete-all/insert
write path already guarantees ordinal uniqueness, so a plain unique index is
sufficient; the index also serves the pr_id-prefix list aggregation and the
workspace/PR cleanup deletes.

Nit — token mint now singleflights per installation. installationToken
released the lock before minting, so the N workers of one installation could
mint N tokens on a cold cache or a simultaneous renew. Concurrent callers for
the same installation are now collapsed via singleflight into one HTTP mint;
added a -race concurrent-mint test asserting a single mint under 16 callers.

Verified: fresh DB migrates through 223 (table has no PK, concurrent unique
index present); ghsnapshot suite + new test pass under -race; migration lint
and handler github/workspace-delete tests pass; sqlc produced no diff;
go build / vet / gofmt / git diff --check clean.

Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 17:56:53 +08:00

59 lines
3.2 KiB
SQL

-- MUL-5265: GitHub API snapshot for PR cards (Plan C).
--
-- The PR card's CI status and mergeability are now sourced from an
-- authenticated GitHub API snapshot (GraphQL pullRequest query) rather than
-- inferred from webhook check_suite events. Webhooks and page visits only
-- trigger a refresh; the API response is the single source of truth and is
-- written as one atomic batch replace per PR.
--
-- All new columns are nullable / defaulted so rows that pre-date the snapshot
-- (or deployments without a GitHub App private key, where the feature degrades
-- off) keep working: the card simply hides the CI / merge region until a
-- snapshot lands.
ALTER TABLE github_pull_request
-- GraphQL `mergeable`: MERGEABLE / CONFLICTING / UNKNOWN. Answers only
-- "is there a merge conflict". NULL until first snapshot.
ADD COLUMN api_mergeable TEXT,
-- GraphQL `mergeStateStatus`: CLEAN / DIRTY / BLOCKED / BEHIND / UNSTABLE /
-- DRAFT / HAS_HOOKS / UNKNOWN. "Ready to merge" is derived ONLY from CLEAN.
ADD COLUMN api_merge_state_status TEXT,
-- GraphQL statusCheckRollup.state: SUCCESS / FAILURE / PENDING / ERROR /
-- EXPECTED. NULL means statusCheckRollup was null → "no checks yet", which
-- must never be rendered as passed.
ADD COLUMN checks_rollup_state TEXT,
-- The head SHA the snapshot was fetched for. Pinned so a slow response for
-- an old head cannot overwrite a newer head's snapshot (head-SHA anti-stale
-- write). Empty until first snapshot.
ADD COLUMN snapshot_head_sha TEXT NOT NULL DEFAULT '',
-- When the snapshot was fetched. Drives the TTL / page-visit refresh and the
-- stale visual marker. NULL until first snapshot.
ADD COLUMN snapshot_fetched_at TIMESTAMPTZ;
-- Per-check snapshot rows for a PR's current head. Replaced atomically (delete
-- all + insert) on every successful API fetch — no incremental inference. Both
-- GraphQL CheckRun and StatusContext contexts are normalized into this shape at
-- write time (see ghsnapshot.normalizeContext). Rows are addressed by
-- (pr_id, ordinal): two checks can share a name (matrix jobs, re-runs), so name
-- is not unique. The (pr_id, ordinal) UNIQUE index is created CONCURRENTLY in
-- the next migration (223) — no index (including a PRIMARY KEY's) may be built
-- non-concurrently in a migration, even on a new table (see CLAUDE.md), so the
-- table is created without a primary key and the unique index is added in its
-- own single-statement migration.
CREATE TABLE github_pull_request_check_run (
pr_id UUID NOT NULL,
head_sha TEXT NOT NULL,
ordinal INTEGER NOT NULL,
name TEXT NOT NULL,
-- Normalized lifecycle: 'queued' / 'in_progress' / 'completed'.
status TEXT NOT NULL,
-- Normalized conclusion: 'success' / 'failure' / 'neutral' / 'cancelled' /
-- 'skipped' / 'timed_out' / 'action_required' / 'error' / ... ; NULL while
-- the check is still running.
conclusion TEXT,
details_url TEXT,
-- TRUE for legacy commit-status contexts (GraphQL StatusContext), FALSE for
-- Checks API runs (GraphQL CheckRun). Kept for display / debugging.
is_status_context BOOLEAN NOT NULL DEFAULT FALSE
);