From d0dc5f6b2b362c83a060c6501cf059e557210fcd Mon Sep 17 00:00:00 2001 From: Postil Maintainer Date: Tue, 14 Jul 2026 02:32:55 +0000 Subject: [PATCH 1/2] Record immutable review trigger provenance --- ARCHITECTURE.md | 13 +- drizzle/0029_review_trigger_provenance.sql | 20 +++ drizzle/meta/_journal.json | 7 + src/app/orgs/[slug]/reviews-table.tsx | 12 +- src/app/orgs/[slug]/runs/[publicId]/page.tsx | 25 ++++ src/app/reports/page.tsx | 7 + src/components/review-trigger-badge.tsx | 16 ++ src/lib/db/schema.ts | 16 ++ src/lib/github/webhook-handler.ts | 67 +++++++++ src/lib/hosted-review-pause.ts | 6 + src/lib/hosted-usage-reservations.ts | 3 + src/lib/org-reviews.ts | 4 + src/lib/queue.ts | 13 ++ src/lib/review-completion.ts | 7 +- src/lib/review-trigger.ts | 107 ++++++++++++++ src/worker/respond.ts | 2 + src/worker/review.ts | 4 + tests/hosted-review-pause.test.ts | 2 + tests/hosted-usage-reservations.test.ts | 24 ++- tests/review-completion.test.ts | 12 +- ...eview-trigger-provenance-migration.test.ts | 138 ++++++++++++++++++ tests/review-trigger.test.ts | 58 ++++++++ tests/webhook-handlers.test.ts | 132 ++++++++++++++++- 23 files changed, 674 insertions(+), 21 deletions(-) create mode 100644 drizzle/0029_review_trigger_provenance.sql create mode 100644 src/components/review-trigger-badge.tsx create mode 100644 src/lib/review-trigger.ts create mode 100644 tests/review-trigger-provenance-migration.test.ts create mode 100644 tests/review-trigger.test.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6fb6818b..6ec33cfe 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -148,6 +148,15 @@ author GitHub ID and login supplied by the reviewable pull-request webhook. Billing counts distinct GitHub author IDs on private pull requests within the entitlement period; bot and service identities count by the same ID rule. +Each review snapshots a closed trigger class and its signed-webhook context at +creation: automatic pull-request event, explicit review command, GitHub check +rerun, or unknown. Historical rows and rolling-deploy jobs without evidence use +unknown; the system does not infer an origin. A database trigger makes review +provenance immutable. Usage events copy the trigger class so billing attribution +survives removal of the related repository or review row. Conversational replies +remain `respond` jobs, record their GitHub mention context in the job payload, +and use the separate `github_mention` usage class. + ## Dashboard The signed-in product surface is three pages, all server-rendered and @@ -168,7 +177,9 @@ noindexed: summary, findings (severity, kind, confidence, sha-pinned GitHub file links), resolved findings, retained policy-suppressed findings in collapsed detail, suppressed/ungrounded counts, gate verdict, - model, token usage, timing, and kind-blocking override state. + model, token usage, timing, immutable trigger provenance, and kind-blocking + override state. The recent-review table exposes the same trigger class and + includes it in text filtering. Authorization is uniform: every page and server action re-derives access from the session. A sealed GitHub OAuth credential refreshes the complete active diff --git a/drizzle/0029_review_trigger_provenance.sql b/drizzle/0029_review_trigger_provenance.sql new file mode 100644 index 00000000..377a3232 --- /dev/null +++ b/drizzle/0029_review_trigger_provenance.sql @@ -0,0 +1,20 @@ +ALTER TABLE "reviews" ADD COLUMN "trigger_source" text DEFAULT 'unknown' NOT NULL;--> statement-breakpoint +ALTER TABLE "reviews" ADD COLUMN "trigger_context" jsonb;--> statement-breakpoint +ALTER TABLE "usage_events" ADD COLUMN "trigger_source" text DEFAULT 'unknown' NOT NULL;--> statement-breakpoint +ALTER TABLE "reviews" ADD CONSTRAINT "reviews_trigger_source_check" CHECK ("reviews"."trigger_source" IN ('unknown', 'automatic_pull_request', 'requested_review', 'github_check_rerun'));--> statement-breakpoint +ALTER TABLE "reviews" ADD CONSTRAINT "reviews_trigger_context_check" CHECK (("reviews"."trigger_source" = 'unknown' AND ("reviews"."trigger_context" IS NULL OR "reviews"."trigger_context"->>'source' = 'unknown')) OR ("reviews"."trigger_source" <> 'unknown' AND "reviews"."trigger_context" IS NOT NULL AND jsonb_typeof("reviews"."trigger_context") = 'object' AND "reviews"."trigger_context"->>'source' = "reviews"."trigger_source" AND COALESCE(length(btrim("reviews"."trigger_context"->>'webhookDeliveryId')), 0) > 0));--> statement-breakpoint +ALTER TABLE "usage_events" ADD CONSTRAINT "usage_events_trigger_source_check" CHECK ("usage_events"."trigger_source" IN ('unknown', 'automatic_pull_request', 'requested_review', 'github_check_rerun', 'github_mention'));--> statement-breakpoint +CREATE OR REPLACE FUNCTION reject_review_trigger_provenance_update() RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + IF NEW.trigger_source IS DISTINCT FROM OLD.trigger_source + OR NEW.trigger_context IS DISTINCT FROM OLD.trigger_context THEN + RAISE EXCEPTION 'review trigger provenance is immutable'; + END IF; + RETURN NEW; +END; +$$;--> statement-breakpoint +CREATE TRIGGER reviews_trigger_provenance_immutable +BEFORE UPDATE OF trigger_source, trigger_context ON reviews +FOR EACH ROW EXECUTE FUNCTION reject_review_trigger_provenance_update(); diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 575a11b4..f1772485 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -204,6 +204,13 @@ "when": 1784286292646, "tag": "0028_github_webhook_redelivery", "breakpoints": true + }, + { + "idx": 29, + "version": "7", + "when": 1784297400000, + "tag": "0029_review_trigger_provenance", + "breakpoints": true } ] } diff --git a/src/app/orgs/[slug]/reviews-table.tsx b/src/app/orgs/[slug]/reviews-table.tsx index e75d1486..57065818 100644 --- a/src/app/orgs/[slug]/reviews-table.tsx +++ b/src/app/orgs/[slug]/reviews-table.tsx @@ -4,8 +4,10 @@ import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { GateBadge, formatMs, ReviewStatusBadge } from "@/components/review-status"; +import { ReviewTriggerBadge } from "@/components/review-trigger-badge"; import { githubPrUrl } from "@/lib/github-links"; import type { OrgReviewRow } from "@/lib/org-reviews"; +import { reviewTriggerLabel, reviewTriggerSearchTerms } from "@/lib/review-trigger"; import { formatAbsoluteTimestamp, formatRelativeTime } from "@/lib/time"; const POLL_INTERVAL_MS = 5_000; @@ -46,6 +48,8 @@ function matchesTextFilter(review: OrgReviewRow, query: string): boolean { review.status, gate, review.modelUsed ?? "", + reviewTriggerLabel(review.triggerSource), + reviewTriggerSearchTerms(review.triggerSource), ] .join(" ") .toLocaleLowerCase(); @@ -161,7 +165,7 @@ export function ReviewsTable({ type="search" value={query} onChange={(event) => setQuery(event.target.value)} - placeholder="Filter repository, PR, status, gate, or model" + placeholder="Filter repository, PR, trigger, status, gate, or model" className="w-full rounded-card border border-stone bg-ivory px-3 py-2 font-mono text-xs text-charcoal placeholder:text-charcoal/40 focus:border-gate focus:outline-none" /> @@ -192,6 +196,7 @@ export function ReviewsTable({ repository PR + trigger status gate findings @@ -223,6 +228,9 @@ export function ReviewsTable({ #{review.prNumber} + + + - + {reviews.length === 0 ? "No reviews yet. Open a pull request on an enabled repository." : "No reviews match these filters."} diff --git a/src/app/orgs/[slug]/runs/[publicId]/page.tsx b/src/app/orgs/[slug]/runs/[publicId]/page.tsx index c1d4c983..86330286 100644 --- a/src/app/orgs/[slug]/runs/[publicId]/page.tsx +++ b/src/app/orgs/[slug]/runs/[publicId]/page.tsx @@ -32,6 +32,11 @@ import { isHostedReviewUnavailable, reviewDisplayStatus, } from "@/lib/review-outcome"; +import { + normalizeReviewTriggerContext, + reviewTriggerLabel, + type ReviewTriggerSource, +} from "@/lib/review-trigger"; import { approveFinding, revokeFinding } from "../../actions"; import { @@ -411,6 +416,8 @@ export default async function RunDetailPage({ queuedAt: schema.reviews.queuedAt, startedAt: schema.reviews.startedAt, finishedAt: schema.reviews.finishedAt, + triggerSource: schema.reviews.triggerSource, + triggerContext: schema.reviews.triggerContext, repoFullName: schema.repositories.fullName, orgId: schema.installations.orgId, githubInstallationId: schema.installations.githubInstallationId, @@ -488,6 +495,8 @@ export default async function RunDetailPage({ review.status, review.errorMessage, ); + const triggerSource = review.triggerSource as ReviewTriggerSource; + const triggerContext = normalizeReviewTriggerContext(review.triggerContext); return ( + + {triggerContext?.source === "requested_review" && triggerContext.sourceUrl ? ( + + {reviewTriggerLabel(triggerSource)} + {triggerContext.requestedByLogin + ? ` by @${triggerContext.requestedByLogin}` + : ""} + + ) : ( + reviewTriggerLabel(triggerSource) + )} + {envelope?.modelUsed ?? "Not recorded"} {(envelope?.scorerModel || envelope?.scorerError) && ( diff --git a/src/app/reports/page.tsx b/src/app/reports/page.tsx index 6cee313a..1df2400a 100644 --- a/src/app/reports/page.tsx +++ b/src/app/reports/page.tsx @@ -10,11 +10,13 @@ import { ReviewStatusBadge, } from "@/components/review-status"; import { ReportsHeader } from "@/components/reports-header"; +import { ReviewTriggerBadge } from "@/components/review-trigger-badge"; import { getDb, schema } from "@/lib/db"; import { githubAppInstallUrl } from "@/lib/github-app"; import { githubPrUrl } from "@/lib/github-links"; import { reviewDisplayStatus } from "@/lib/review-outcome"; import { getVerifiedSessionUser } from "@/lib/session"; +import type { ReviewTriggerSource } from "@/lib/review-trigger"; export const metadata: Metadata = { title: "Reports", @@ -90,6 +92,7 @@ export default async function ReportsPage() { queuedAt: schema.reviews.queuedAt, repoFullName: schema.repositories.fullName, orgSlug: schema.organizations.slug, + triggerSource: schema.reviews.triggerSource, }) .from(schema.reviews) .innerJoin( @@ -171,6 +174,7 @@ export default async function ReportsPage() { repository PR + trigger status gate findings @@ -192,6 +196,9 @@ export default async function ReportsPage() { #{r.prNumber} + + + + {reviewTriggerLabel(source)} + + ); +} diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 02793646..1ec4440a 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -17,6 +17,7 @@ import { import type { Envelope } from "@/lib/envelope"; import type { ReviewConfigProvenance } from "@/lib/github/contents"; +import type { ReviewTriggerContext } from "@/lib/review-trigger"; /** Raw bytes column for AES-256-GCM sealed secrets. */ const bytea = customType<{ data: Buffer; driverData: Buffer }>({ @@ -194,6 +195,8 @@ export const reviews = pgTable( headSha: text("head_sha").notNull(), baseSha: text("base_sha").notNull(), sinceSha: text("since_sha"), + triggerSource: text("trigger_source").notNull().default("unknown"), + triggerContext: jsonb("trigger_context").$type(), status: reviewStatus("status").notNull().default("queued"), envelope: jsonb("envelope").$type(), configFiles: text("config_files").array(), @@ -215,6 +218,14 @@ export const reviews = pgTable( index("reviews_running_started_at_idx") .on(t.startedAt) .where(sql`${t.status} = 'running'`), + check( + "reviews_trigger_source_check", + sql`${t.triggerSource} IN ('unknown', 'automatic_pull_request', 'requested_review', 'github_check_rerun')`, + ), + check( + "reviews_trigger_context_check", + sql`(${t.triggerSource} = 'unknown' AND (${t.triggerContext} IS NULL OR ${t.triggerContext}->>'source' = 'unknown')) OR (${t.triggerSource} <> 'unknown' AND ${t.triggerContext} IS NOT NULL AND jsonb_typeof(${t.triggerContext}) = 'object' AND ${t.triggerContext}->>'source' = ${t.triggerSource} AND COALESCE(length(btrim(${t.triggerContext}->>'webhookDeliveryId')), 0) > 0)`, + ), ], ); @@ -280,6 +291,7 @@ export const usageEvents = pgTable( reviewId: bigint("review_id", { mode: "number" }).references(() => reviews.id, { onDelete: "set null", }), + triggerSource: text("trigger_source").notNull().default("unknown"), promptTokens: integer("prompt_tokens").notNull().default(0), completionTokens: integer("completion_tokens").notNull().default(0), modelUsed: text("model_used"), @@ -305,6 +317,10 @@ export const usageEvents = pgTable( "usage_events_billing_scope_check", sql`${t.billingScope} IN ('analytics', 'private_hosted')`, ), + check( + "usage_events_trigger_source_check", + sql`${t.triggerSource} IN ('unknown', 'automatic_pull_request', 'requested_review', 'github_check_rerun', 'github_mention')`, + ), ], ); diff --git a/src/lib/github/webhook-handler.ts b/src/lib/github/webhook-handler.ts index 335c4868..5a3f3b3f 100644 --- a/src/lib/github/webhook-handler.ts +++ b/src/lib/github/webhook-handler.ts @@ -559,6 +559,12 @@ async function handlePullRequest( headSha, baseSha, sourceDeliveryId, + trigger: { + source: "automatic_pull_request", + webhookDeliveryId: sourceDeliveryId, + webhookEvent: "pull_request", + webhookAction: action, + }, }, triggerFollowupDrain); } @@ -671,6 +677,13 @@ async function handleCheckRerequest( headSha, baseSha, sourceDeliveryId, + trigger: { + source: "github_check_rerun", + webhookDeliveryId: sourceDeliveryId, + webhookEvent: label, + webhookAction: "rerequested", + ...(checkName ? { checkName } : {}), + }, }, triggerFollowupDrain); } @@ -841,6 +854,7 @@ async function handleIssueComment( payload, payload.issue.number, sourceDeliveryId, + "issue_comment", triggerFollowupDrain, ); } else { @@ -863,6 +877,7 @@ async function handleIssueComment( isPr: payload.issue.pull_request != null, comment: body!, sourceDeliveryId, + trigger: respondTrigger(payload, sourceDeliveryId, "issue_comment"), }, triggerFollowupDrain); } @@ -883,6 +898,7 @@ async function handleReviewComment( payload, payload.pull_request.number, sourceDeliveryId, + "pull_request_review_comment", triggerFollowupDrain, ); return; @@ -902,13 +918,38 @@ async function handleReviewComment( comment: body!, commentAnchor: anchor, sourceDeliveryId, + trigger: respondTrigger(payload, sourceDeliveryId, "pull_request_review_comment"), }, triggerFollowupDrain); } +function respondTrigger( + payload: CommentEventPayload, + sourceDeliveryId: string, + webhookEvent: "issue_comment" | "pull_request_review_comment", +): NonNullable { + return { + source: "github_mention", + webhookDeliveryId: sourceDeliveryId, + webhookEvent, + webhookAction: "created", + ...(typeof payload.comment?.id === "number" + ? { sourceCommentId: payload.comment.id } + : {}), + ...(payload.comment?.html_url ? { sourceUrl: payload.comment.html_url } : {}), + ...(typeof payload.comment?.user?.id === "number" + ? { requestedByGithubId: payload.comment.user.id } + : {}), + ...(payload.comment?.user?.login + ? { requestedByLogin: payload.comment.user.login } + : {}), + }; +} + async function enqueueMentionReview( payload: CommentEventPayload, prNumber: number, sourceDeliveryId: string, + webhookEvent: "issue_comment" | "pull_request_review_comment", triggerFollowupDrain: boolean, ): Promise { const installationId = payload.installation?.id; @@ -929,6 +970,22 @@ async function enqueueMentionReview( headSha: context.headSha, baseSha: context.baseSha, sourceDeliveryId, + trigger: { + source: "requested_review", + webhookDeliveryId: sourceDeliveryId, + webhookEvent, + webhookAction: "created", + ...(typeof payload.comment?.id === "number" + ? { sourceCommentId: payload.comment.id } + : {}), + ...(payload.comment?.html_url ? { sourceUrl: payload.comment.html_url } : {}), + ...(typeof payload.comment?.user?.id === "number" + ? { requestedByGithubId: payload.comment.user.id } + : {}), + ...(payload.comment?.user?.login + ? { requestedByLogin: payload.comment.user.login } + : {}), + }, }, triggerFollowupDrain); } @@ -1158,5 +1215,15 @@ async function handleIssues( isPr: false, comment: body!, sourceDeliveryId, + trigger: { + source: "github_mention", + webhookDeliveryId: sourceDeliveryId, + webhookEvent: "issues", + webhookAction: "opened", + ...(typeof payload.sender?.id === "number" + ? { requestedByGithubId: payload.sender.id } + : {}), + ...(payload.sender?.login ? { requestedByLogin: payload.sender.login } : {}), + }, }, triggerFollowupDrain); } diff --git a/src/lib/hosted-review-pause.ts b/src/lib/hosted-review-pause.ts index 503b413c..18d9e0da 100644 --- a/src/lib/hosted-review-pause.ts +++ b/src/lib/hosted-review-pause.ts @@ -4,6 +4,10 @@ import type { Database } from "@/lib/db"; import { schema } from "@/lib/db"; import { checkRunExternalId } from "@/lib/github/checks"; import { HOSTED_REVIEW_UNAVAILABLE_MESSAGE } from "@/lib/review-outcome"; +import type { + ReviewTriggerContext, + ReviewTriggerSource, +} from "@/lib/review-trigger"; export interface HostedReviewPauseClaim { repositoryId: number; @@ -13,6 +17,8 @@ export interface HostedReviewPauseClaim { headSha: string; baseSha: string; sinceSha: string | null; + triggerSource: ReviewTriggerSource; + triggerContext: ReviewTriggerContext; queuedAt: Date; startedAt: Date; } diff --git a/src/lib/hosted-usage-reservations.ts b/src/lib/hosted-usage-reservations.ts index e2f649a9..c05fd3b8 100644 --- a/src/lib/hosted-usage-reservations.ts +++ b/src/lib/hosted-usage-reservations.ts @@ -248,6 +248,7 @@ export async function reconcileHostedRespondSpend( modelUsed: string; actualMicros: number | null; usageAccountingComplete: boolean; + triggerSource?: "github_mention" | "unknown"; delivery?: { jobId: number; repoFullName: string; @@ -303,6 +304,7 @@ export async function reconcileHostedRespondSpend( modelUsed: input.modelUsed, costMicros: input.actualMicros ?? 0, billingScope: "private_hosted", + triggerSource: input.triggerSource ?? "unknown", createdAt: now, }); const unattributedMicros = chargedMicros - (input.actualMicros ?? 0); @@ -316,6 +318,7 @@ export async function reconcileHostedRespondSpend( modelUsed: "unattributed provider usage", costMicros: unattributedMicros, billingScope: "private_hosted", + triggerSource: input.triggerSource ?? "unknown", createdAt: now, }); } diff --git a/src/lib/org-reviews.ts b/src/lib/org-reviews.ts index ec27b7f0..e96320ce 100644 --- a/src/lib/org-reviews.ts +++ b/src/lib/org-reviews.ts @@ -8,6 +8,7 @@ import { reviewDisplayStatus, type ReviewDisplayStatus, } from "@/lib/review-outcome"; +import type { ReviewTriggerSource } from "@/lib/review-trigger"; export type OrgReviewStatus = ReviewDisplayStatus; @@ -23,6 +24,7 @@ export interface OrgReviewRow { startedAt: string | null; finishedAt: string | null; repoFullName: string; + triggerSource: ReviewTriggerSource; } /** @@ -49,6 +51,7 @@ export async function getOrgReviewRows( startedAt: schema.reviews.startedAt, finishedAt: schema.reviews.finishedAt, repoFullName: schema.repositories.fullName, + triggerSource: schema.reviews.triggerSource, }) .from(schema.reviews) .innerJoin(schema.repositories, eq(schema.repositories.id, schema.reviews.repositoryId)) @@ -70,6 +73,7 @@ export async function getOrgReviewRows( return { ...row, status: reviewDisplayStatus(row.status, errorMessage), + triggerSource: row.triggerSource as ReviewTriggerSource, gateFailing: envelope ? computeEffectiveGate( envelope, diff --git a/src/lib/queue.ts b/src/lib/queue.ts index 8b060403..0e1e6383 100644 --- a/src/lib/queue.ts +++ b/src/lib/queue.ts @@ -1,6 +1,7 @@ import type { Pool } from "pg"; import { redactAndTruncate } from "@/lib/redact"; +import type { ReviewTriggerContext } from "@/lib/review-trigger"; /** * Postgres-native job queue. @@ -41,6 +42,8 @@ export interface ReviewJobPayload extends Record { headSha: string; baseSha: string; sourceDeliveryId?: string; + /** Optional only for jobs queued by an older release during a rolling deploy. */ + trigger?: ReviewTriggerContext; } /** An @postil mention on a PR or issue the bot should reply to. */ @@ -55,6 +58,16 @@ export interface RespondJobPayload extends Record { // knows which code the question is about. commentAnchor?: string; sourceDeliveryId?: string; + trigger?: { + source: "github_mention"; + webhookDeliveryId: string; + webhookEvent: "issue_comment" | "pull_request_review_comment" | "issues"; + webhookAction: "created" | "opened"; + sourceCommentId?: number; + sourceUrl?: string; + requestedByGithubId?: number; + requestedByLogin?: string; + }; } /** A fixed webhook reply delivered through the marker-reconciled comment path. */ diff --git a/src/lib/review-completion.ts b/src/lib/review-completion.ts index 1b312013..ce6a2678 100644 --- a/src/lib/review-completion.ts +++ b/src/lib/review-completion.ts @@ -51,12 +51,16 @@ export async function persistReviewCompletion( eq(schema.reviews.status, "running"), ), ) - .returning({ id: schema.reviews.id }); + .returning({ + id: schema.reviews.id, + triggerSource: schema.reviews.triggerSource, + }); if (rows.length === 0) return false; const persistedUsageRows = input.usage.map((usage) => ({ ...usage, reviewId: input.reviewId, + triggerSource: rows[0]!.triggerSource, })); if (input.hostedUsageReservationId) { const reservation = ( @@ -94,6 +98,7 @@ export async function persistReviewCompletion( modelUsed: "unattributed provider usage", costMicros: unattributedMicros, reviewId: input.reviewId, + triggerSource: rows[0]!.triggerSource, }); } const reconciled = await tx diff --git a/src/lib/review-trigger.ts b/src/lib/review-trigger.ts new file mode 100644 index 00000000..514bb11c --- /dev/null +++ b/src/lib/review-trigger.ts @@ -0,0 +1,107 @@ +export const REVIEW_TRIGGER_SOURCES = [ + "unknown", + "automatic_pull_request", + "requested_review", + "github_check_rerun", +] as const; + +export type ReviewTriggerSource = (typeof REVIEW_TRIGGER_SOURCES)[number]; + +export interface ReviewTriggerContext { + source: ReviewTriggerSource; + webhookDeliveryId?: string; + webhookEvent?: "pull_request" | "issue_comment" | "pull_request_review_comment" | "check_run" | "check_suite"; + webhookAction?: string; + sourceCommentId?: number; + sourceUrl?: string; + requestedByGithubId?: number; + requestedByLogin?: string; + checkName?: string; +} + +const TRIGGER_LABELS: Record = { + unknown: "Unknown", + automatic_pull_request: "Automatic", + requested_review: "Requested", + github_check_rerun: "Check rerun", +}; + +export function reviewTriggerLabel(source: ReviewTriggerSource): string { + return TRIGGER_LABELS[source]; +} + +export function reviewTriggerSearchTerms(source: ReviewTriggerSource): string { + if (source === "automatic_pull_request") return "automatic pull request webhook"; + if (source === "requested_review") return "requested tagged mention command"; + if (source === "github_check_rerun") return "github check rerun rerequested"; + return "unknown legacy"; +} + +export function reviewTriggerDescription(source: ReviewTriggerSource): string { + if (source === "automatic_pull_request") return "Started by a pull request event"; + if (source === "requested_review") return "Requested in a GitHub comment"; + if (source === "github_check_rerun") return "Started by rerunning a GitHub check"; + return "Origin not recorded"; +} + +export function normalizeReviewTriggerContext(value: unknown): ReviewTriggerContext { + if (typeof value !== "object" || value === null) return { source: "unknown" }; + const candidate = value as Partial; + if ( + typeof candidate.source !== "string" || + !REVIEW_TRIGGER_SOURCES.includes(candidate.source as ReviewTriggerSource) + ) { + return { source: "unknown" }; + } + if (candidate.source === "unknown") return { source: "unknown" }; + if ( + typeof candidate.webhookDeliveryId !== "string" || + candidate.webhookDeliveryId.length === 0 || + candidate.webhookDeliveryId.length > 200 + ) { + return { source: "unknown" }; + } + + const allowedEvents = + candidate.source === "automatic_pull_request" + ? ["pull_request"] + : candidate.source === "requested_review" + ? ["issue_comment", "pull_request_review_comment"] + : ["check_run", "check_suite"]; + if (!candidate.webhookEvent || !allowedEvents.includes(candidate.webhookEvent)) { + return { source: "unknown" }; + } + + const normalized: ReviewTriggerContext = { + source: candidate.source, + webhookDeliveryId: candidate.webhookDeliveryId, + webhookEvent: candidate.webhookEvent, + ...(typeof candidate.webhookAction === "string" && candidate.webhookAction.length <= 100 + ? { webhookAction: candidate.webhookAction } + : {}), + ...(Number.isSafeInteger(candidate.sourceCommentId) && candidate.sourceCommentId! > 0 + ? { sourceCommentId: candidate.sourceCommentId } + : {}), + ...(safeGithubUrl(candidate.sourceUrl) ? { sourceUrl: candidate.sourceUrl } : {}), + ...(Number.isSafeInteger(candidate.requestedByGithubId) && candidate.requestedByGithubId! > 0 + ? { requestedByGithubId: candidate.requestedByGithubId } + : {}), + ...(typeof candidate.requestedByLogin === "string" && candidate.requestedByLogin.length <= 100 + ? { requestedByLogin: candidate.requestedByLogin } + : {}), + ...(typeof candidate.checkName === "string" && candidate.checkName.length <= 200 + ? { checkName: candidate.checkName } + : {}), + }; + return normalized; +} + +function safeGithubUrl(value: unknown): value is string { + if (typeof value !== "string" || value.length > 2_048) return false; + try { + const parsed = new URL(value); + return parsed.protocol === "https:" && parsed.hostname === "github.com"; + } catch { + return false; + } +} diff --git a/src/worker/respond.ts b/src/worker/respond.ts index 477fba8b..51886c71 100644 --- a/src/worker/respond.ts +++ b/src/worker/respond.ts @@ -217,6 +217,7 @@ export async function runRespondJob(payload: RespondJobPayload, jobId: number): modelUsed: usage?.modelUsed ?? "respond (conservative reservation)", actualMicros: usage?.actualMicros ?? null, usageAccountingComplete: usage?.usageAccountingComplete ?? false, + triggerSource: payload.trigger?.source === "github_mention" ? "github_mention" : "unknown", delivery: { jobId, repoFullName: currentRepository.full_name, @@ -248,6 +249,7 @@ export async function runRespondJob(payload: RespondJobPayload, jobId: number): modelUsed: "respond (conservative reservation)", actualMicros: null, usageAccountingComplete: false, + triggerSource: payload.trigger?.source === "github_mention" ? "github_mention" : "unknown", }).catch((reconcileError) => { console.error( `failed to conservatively reconcile hosted respond usage: ${redactSecrets(reconcileError)}`, diff --git a/src/worker/review.ts b/src/worker/review.ts index f0111323..33f712a8 100644 --- a/src/worker/review.ts +++ b/src/worker/review.ts @@ -55,6 +55,7 @@ import { import { claimPausedHostedReview } from "@/lib/hosted-review-pause"; import { withoutOrgModelConfig } from "@/lib/org-review-config"; import type { CheckRunCleanupJobPayload, ReviewJobPayload } from "@/lib/queue"; +import { normalizeReviewTriggerContext } from "@/lib/review-trigger"; import { redactAndTruncate, redactSecrets } from "@/lib/redact"; import { persistReviewCompletion } from "@/lib/review-completion"; import { discoverPreventionCommands } from "@/lib/review-guidance"; @@ -631,6 +632,7 @@ export async function runReviewJob( // deployment setting must not create a row that only the watchdog can close. const publicOrigin = configuredPublicOrigin(); + const trigger = normalizeReviewTriggerContext(payload.trigger); const reviewValues = { repositoryId: repository.id, prNumber: payload.prNumber, @@ -639,6 +641,8 @@ export async function runReviewJob( headSha: payload.headSha, baseSha: payload.baseSha, sinceSha: baseline?.headSha ?? null, + triggerSource: trigger.source, + triggerContext: trigger, queuedAt: timing.queuedAt, startedAt: timing.startedAt, }; diff --git a/tests/hosted-review-pause.test.ts b/tests/hosted-review-pause.test.ts index a6f31eaa..8649e932 100644 --- a/tests/hosted-review-pause.test.ts +++ b/tests/hosted-review-pause.test.ts @@ -72,6 +72,8 @@ describeDb("paused hosted review claims", () => { headSha: "head-one", baseSha: "base", sinceSha: null, + triggerSource: "unknown" as const, + triggerContext: { source: "unknown" as const }, queuedAt: new Date("2026-07-17T14:00:00Z"), startedAt: new Date("2026-07-17T14:00:01Z"), }; diff --git a/tests/hosted-usage-reservations.test.ts b/tests/hosted-usage-reservations.test.ts index bb7fbbea..828b3368 100644 --- a/tests/hosted-usage-reservations.test.ts +++ b/tests/hosted-usage-reservations.test.ts @@ -88,10 +88,12 @@ describeDb("hosted usage reservations on PostgreSQL", () => { `); repositoryId = Number(repository.rows[0]?.id); const reviews = await migrationClient.query<{ id: string }>(` - INSERT INTO reviews (repository_id, pr_number, head_sha, base_sha, status) + INSERT INTO reviews + (repository_id, pr_number, head_sha, base_sha, status, trigger_source, trigger_context) VALUES - (${repositoryId}, 1, 'head-1', 'base', 'running'), - (${repositoryId}, 2, 'head-2', 'base', 'running') + (${repositoryId}, 1, 'head-1', 'base', 'running', 'unknown', NULL), + (${repositoryId}, 2, 'head-2', 'base', 'running', 'requested_review', + '{"source":"requested_review","webhookDeliveryId":"billing-test"}'::jsonb) RETURNING id; `); reviewIds = reviews.rows.map((row) => Number(row.id)); @@ -166,11 +168,16 @@ describeDb("hosted usage reservations on PostgreSQL", () => { [recovered.reservationId], ); expect(reconciled.rows[0]).toEqual({ status: "reconciled", actual_micros: "1000000" }); - const usage = await pool!.query<{ cost_micros: string }>( - `SELECT sum(cost_micros)::bigint AS cost_micros FROM usage_events WHERE review_id = $1`, + const usage = await pool!.query<{ cost_micros: string; trigger_source: string }>( + `SELECT sum(cost_micros)::bigint AS cost_micros, max(trigger_source) AS trigger_source + FROM usage_events WHERE review_id = $1`, [rejectedReviewId], ); - expect(usage.rows[0]).toEqual({ cost_micros: "1000000" }); + expect(usage.rows[0]).toEqual({ + cost_micros: "1000000", + trigger_source: + rejectedReviewId === reviewIds[1] ? "requested_review" : "unknown", + }); }); test("classifies omitted legacy usage scope without undercounting private hosted work", async () => { @@ -267,6 +274,7 @@ describeDb("hosted usage reservations on PostgreSQL", () => { modelUsed: "z-ai/glm-5.2", actualMicros: 987, usageAccountingComplete: true, + triggerSource: "github_mention", }); const reconciled = await pool!.query<{ operation: string; @@ -289,8 +297,9 @@ describeDb("hosted usage reservations on PostgreSQL", () => { prompt_tokens: number; completion_tokens: number; cost_micros: string; + trigger_source: string; }>( - `SELECT review_id, prompt_tokens, completion_tokens, cost_micros + `SELECT review_id, prompt_tokens, completion_tokens, cost_micros, trigger_source FROM usage_events WHERE repository_id = $1 AND review_id IS NULL`, [respondRepositoryId], ); @@ -299,6 +308,7 @@ describeDb("hosted usage reservations on PostgreSQL", () => { prompt_tokens: 120, completion_tokens: 20, cost_micros: "987", + trigger_source: "github_mention", }); await pool!.query( diff --git a/tests/review-completion.test.ts b/tests/review-completion.test.ts index f9e6bb67..f7a2459a 100644 --- a/tests/review-completion.test.ts +++ b/tests/review-completion.test.ts @@ -22,7 +22,9 @@ function fakeDb(reviewUpdated = true): { return chain; }, returning() { - return Promise.resolve(reviewUpdated ? [{ id: 7 }] : []); + return Promise.resolve( + reviewUpdated ? [{ id: 7, triggerSource: "requested_review" }] : [], + ); }, }; return chain; @@ -92,8 +94,8 @@ describe("review completion transaction", () => { expect(state.inserted.filter((row) => row.table === schema.usageEvents)).toHaveLength(1); const usageInsert = state.inserted.find((row) => row.table === schema.usageEvents); expect(usageInsert?.values).toEqual([ - { ...base.usage[0], reviewId: 7 }, - { ...base.usage[1], reviewId: 7 }, + { ...base.usage[0], reviewId: 7, triggerSource: "requested_review" }, + { ...base.usage[1], reviewId: 7, triggerSource: "requested_review" }, ]); expect(state.inserted.filter((row) => row.table === schema.jobs)).toHaveLength(0); }); @@ -110,8 +112,8 @@ describe("review completion transaction", () => { ).toBe(true); const usageInsert = state.inserted.find((row) => row.table === schema.usageEvents); expect(usageInsert?.values).toEqual([ - { ...sameModelUsage[0], reviewId: 7 }, - { ...sameModelUsage[1], reviewId: 7 }, + { ...sameModelUsage[0], reviewId: 7, triggerSource: "requested_review" }, + { ...sameModelUsage[1], reviewId: 7, triggerSource: "requested_review" }, ]); }); diff --git a/tests/review-trigger-provenance-migration.test.ts b/tests/review-trigger-provenance-migration.test.ts new file mode 100644 index 00000000..b887144b --- /dev/null +++ b/tests/review-trigger-provenance-migration.test.ts @@ -0,0 +1,138 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { readdir, readFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { Pool } from "pg"; + +const TEST_URL = process.env.POSTIL_TEST_DATABASE_URL; +const describeDb = TEST_URL ? describe : describe.skip; + +describeDb("review trigger provenance migration", () => { + const pool = new Pool({ connectionString: TEST_URL, max: 1 }); + let repositoryId: number; + + beforeAll(async () => { + await pool.query("DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public"); + const migrationDirectory = join(import.meta.dir, "..", "drizzle"); + const migrations = (await readdir(migrationDirectory)) + .filter((file) => file.endsWith(".sql") && file < "0029_") + .sort(); + for (const migration of migrations) { + const sql = await readFile(join(migrationDirectory, migration), "utf8"); + for (const statement of sql.split("--> statement-breakpoint")) { + if (statement.trim()) await pool.query(statement); + } + } + const organization = await pool.query<{ id: string }>( + "INSERT INTO organizations (slug, name) VALUES ('octo', 'Octo') RETURNING id", + ); + const installation = await pool.query<{ id: string }>( + `INSERT INTO installations + (github_installation_id, org_id, account_login, account_type) + VALUES (42, $1, 'octo', 'Organization') RETURNING id`, + [organization.rows[0]!.id], + ); + const repository = await pool.query<{ id: string }>( + `INSERT INTO repositories + (installation_id, github_repo_id, full_name, private, enabled) + VALUES ($1, 99, 'octo/repo', false, true) RETURNING id`, + [installation.rows[0]!.id], + ); + repositoryId = Number(repository.rows[0]!.id); + const legacyReview = await pool.query<{ id: string }>( + `INSERT INTO reviews (repository_id, pr_number, head_sha, base_sha) + VALUES ($1, 1, 'legacy-head', 'base') RETURNING id`, + [repositoryId], + ); + await pool.query( + `INSERT INTO usage_events + (org_id, repository_id, review_id, model_used, billing_scope) + VALUES ($1, $2, $3, 'legacy-model', 'analytics')`, + [organization.rows[0]!.id, repositoryId, legacyReview.rows[0]!.id], + ); + const migration = await readFile( + join(migrationDirectory, "0029_review_trigger_provenance.sql"), + "utf8", + ); + for (const statement of migration.split("--> statement-breakpoint")) { + if (statement.trim()) await pool.query(statement); + } + }); + + afterAll(async () => { + await pool.end(); + }); + + test("marks historical reviews unknown without inventing context", async () => { + const legacy = await pool.query<{ + trigger_source: string; + trigger_context: Record | null; + }>("SELECT trigger_source, trigger_context FROM reviews WHERE head_sha = 'legacy-head'"); + expect(legacy.rows).toEqual([{ trigger_source: "unknown", trigger_context: null }]); + const legacyUsage = await pool.query<{ trigger_source: string }>( + "SELECT trigger_source FROM usage_events WHERE model_used = 'legacy-model'", + ); + expect(legacyUsage.rows).toEqual([{ trigger_source: "unknown" }]); + }); + + test("stores valid provenance and rejects later mutation", async () => { + const context = { + source: "requested_review", + webhookDeliveryId: "delivery-1", + sourceCommentId: 123, + }; + const inserted = await pool.query<{ id: string }>( + `INSERT INTO reviews + (repository_id, pr_number, head_sha, base_sha, trigger_source, trigger_context) + VALUES ($1, 2, 'requested-head', 'base', 'requested_review', $2) + RETURNING id`, + [repositoryId, JSON.stringify(context)], + ); + await pool.query("UPDATE reviews SET status = 'running' WHERE id = $1", [ + inserted.rows[0]!.id, + ]); + await expect( + pool.query("UPDATE reviews SET trigger_source = 'unknown' WHERE id = $1", [ + inserted.rows[0]!.id, + ]), + ).rejects.toMatchObject({ code: "P0001" }); + await expect( + pool.query("UPDATE reviews SET trigger_context = '{}'::jsonb WHERE id = $1", [ + inserted.rows[0]!.id, + ]), + ).rejects.toMatchObject({ code: "P0001" }); + }); + + test("rejects provenance values outside the closed vocabulary", async () => { + await expect( + pool.query( + `INSERT INTO reviews + (repository_id, pr_number, head_sha, base_sha, trigger_source) + VALUES ($1, 3, 'invalid-head', 'base', 'guessed')`, + [repositoryId], + ), + ).rejects.toMatchObject({ code: "23514" }); + await expect( + pool.query( + `INSERT INTO reviews + (repository_id, pr_number, head_sha, base_sha, trigger_source, trigger_context) + VALUES ($1, 4, 'mismatched-head', 'base', 'requested_review', $2)`, + [ + repositoryId, + JSON.stringify({ + source: "automatic_pull_request", + webhookDeliveryId: "delivery-2", + }), + ], + ), + ).rejects.toMatchObject({ code: "23514" }); + await expect( + pool.query( + `INSERT INTO reviews + (repository_id, pr_number, head_sha, base_sha, trigger_source, trigger_context) + VALUES ($1, 5, 'missing-evidence-head', 'base', 'requested_review', $2)`, + [repositoryId, JSON.stringify({ source: "requested_review" })], + ), + ).rejects.toMatchObject({ code: "23514" }); + }); +}); diff --git a/tests/review-trigger.test.ts b/tests/review-trigger.test.ts new file mode 100644 index 00000000..1bccf8ec --- /dev/null +++ b/tests/review-trigger.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from "bun:test"; + +import { normalizeReviewTriggerContext } from "@/lib/review-trigger"; + +describe("review trigger context", () => { + test("keeps auditable fields from a valid requested review", () => { + expect( + normalizeReviewTriggerContext({ + source: "requested_review", + webhookDeliveryId: "delivery-1", + webhookEvent: "issue_comment", + webhookAction: "created", + sourceCommentId: 123, + sourceUrl: "https://github.com/octo/repo/pull/1#issuecomment-123", + requestedByGithubId: 456, + requestedByLogin: "octocat", + }), + ).toEqual({ + source: "requested_review", + webhookDeliveryId: "delivery-1", + webhookEvent: "issue_comment", + webhookAction: "created", + sourceCommentId: 123, + sourceUrl: "https://github.com/octo/repo/pull/1#issuecomment-123", + requestedByGithubId: 456, + requestedByLogin: "octocat", + }); + }); + + test("fails closed when source evidence is absent or inconsistent", () => { + expect(normalizeReviewTriggerContext({ source: "requested_review" })).toEqual({ + source: "unknown", + }); + expect( + normalizeReviewTriggerContext({ + source: "requested_review", + webhookDeliveryId: "delivery-2", + webhookEvent: "check_run", + }), + ).toEqual({ source: "unknown" }); + }); + + test("drops unsafe source links and unrelated payload fields", () => { + expect( + normalizeReviewTriggerContext({ + source: "requested_review", + webhookDeliveryId: "delivery-3", + webhookEvent: "pull_request_review_comment", + sourceUrl: "javascript:alert(1)", + commentBody: "not part of provenance", + }), + ).toEqual({ + source: "requested_review", + webhookDeliveryId: "delivery-3", + webhookEvent: "pull_request_review_comment", + }); + }); +}); diff --git a/tests/webhook-handlers.test.ts b/tests/webhook-handlers.test.ts index 8ba31749..4dc685fe 100644 --- a/tests/webhook-handlers.test.ts +++ b/tests/webhook-handlers.test.ts @@ -394,7 +394,11 @@ describeDb("webhook handler behaviour", () => { // And the review job was enqueued (installation resolved, repo enabled). const jobs = await pool.query<{ - payload: { authorGithubId: number; authorLogin: string }; + payload: { + authorGithubId: number; + authorLogin: string; + trigger: Record; + }; }>( "SELECT payload FROM jobs WHERE kind = 'review'", ); @@ -402,6 +406,12 @@ describeDb("webhook handler behaviour", () => { expect(jobs.rows[0]!.payload).toMatchObject({ authorGithubId: 4242, authorLogin: "dependabot[bot]", + trigger: { + source: "automatic_pull_request", + webhookDeliveryId: "delivery-transfer-1", + webhookEvent: "pull_request", + webhookAction: "opened", + }, }); }); @@ -813,11 +823,21 @@ describeDb("webhook handler behaviour", () => { const approvals = await pool.query<{ c: number }>( "SELECT count(*)::int AS c FROM finding_approvals", ); - const jobs = await pool.query<{ c: number }>( - "SELECT count(*)::int AS c FROM jobs WHERE kind = 'respond'", + const jobs = await pool.query<{ payload: { trigger: Record } }>( + "SELECT payload FROM jobs WHERE kind = 'respond'", ); expect(approvals.rows[0]!.c).toBe(0); - expect(jobs.rows[0]!.c).toBe(1); + expect(jobs.rows).toHaveLength(1); + expect(jobs.rows[0]!.payload.trigger).toEqual({ + source: "github_mention", + webhookDeliveryId: "approval-free-form", + webhookEvent: "issue_comment", + webhookAction: "created", + sourceCommentId: 123456, + sourceUrl: "https://github.com/octo/approvals/pull/9#issuecomment-123456", + requestedByGithubId: 501, + requestedByLogin: "admin", + }); }); test("exact PR review mentions enqueue the structured reviewer", async () => { @@ -845,11 +865,63 @@ describeDb("webhook handler behaviour", () => { baseSha: "base-sha", authorGithubId: 501, authorLogin: "admin", + trigger: { + source: "requested_review", + webhookDeliveryId: "mention-review-current-head", + webhookEvent: "issue_comment", + webhookAction: "created", + sourceCommentId: 123456, + sourceUrl: "https://github.com/octo/approvals/pull/9#issuecomment-123456", + requestedByGithubId: 501, + requestedByLogin: "admin", + }, }), }, ]); }); + test("review-thread commands retain their comment origin", async () => { + const orgId = await seedOrg(); + const inst = await seedInstallation(orgId, 702); + await seedRepo(inst, 7002, "octo/threaded"); + + const res = await post( + "pull_request_review_comment", + { + action: "created", + installation: { id: 702 }, + repository: { id: 7002, full_name: "octo/threaded", private: false }, + sender: { id: 502, login: "reviewer", type: "User" }, + comment: { + id: 654321, + html_url: "https://github.com/octo/threaded/pull/5#discussion_r654321", + body: "@postil review the current head", + user: { id: 502, login: "reviewer", type: "User" }, + author_association: "MEMBER", + path: "src/app.ts", + line: 10, + }, + pull_request: { number: 5 }, + }, + "review-thread-command", + ); + + expect(res.status).toBe(200); + const jobs = await pool.query<{ payload: { trigger: Record } }>( + "SELECT payload FROM jobs WHERE kind = 'review'", + ); + expect(jobs.rows[0]!.payload.trigger).toEqual({ + source: "requested_review", + webhookDeliveryId: "review-thread-command", + webhookEvent: "pull_request_review_comment", + webhookAction: "created", + sourceCommentId: 654321, + sourceUrl: "https://github.com/octo/threaded/pull/5#discussion_r654321", + requestedByGithubId: 502, + requestedByLogin: "reviewer", + }); + }); + test("issue review mentions cannot invoke the pull-request reviewer", async () => { const orgId = await seedOrg(); const inst = await seedInstallation(orgId, 701); @@ -1269,12 +1341,62 @@ describeDb("webhook handler behaviour", () => { const res = await checkRunRerequestedEvent("delivery-rerequest-1"); expect(res.status).toBe(200); - const jobs = await pool.query<{ payload: { prNumber: number; headSha: string } }>( + const jobs = await pool.query<{ + payload: { + prNumber: number; + headSha: string; + trigger: Record; + }; + }>( "SELECT payload FROM jobs WHERE kind = 'review'", ); expect(jobs.rows).toHaveLength(1); expect(jobs.rows[0]!.payload.prNumber).toBe(42); expect(jobs.rows[0]!.payload.headSha).toBe("deadbeef"); + expect(jobs.rows[0]!.payload.trigger).toEqual({ + source: "github_check_rerun", + webhookDeliveryId: "delivery-rerequest-1", + webhookEvent: "check_run", + webhookAction: "rerequested", + checkName: "postil/gate", + }); + }); + + test("check_suite rerequested records a check rerun without guessing a check name", async () => { + const orgId = await seedOrg(); + const inst = await seedInstallation(orgId, 501); + await seedRepo(inst, 5556, "octo/suite"); + + const res = await post( + "check_suite", + { + action: "rerequested", + installation: { id: 501 }, + repository: { id: 5556, full_name: "octo/suite", private: false }, + check_suite: { + head_sha: "suite-head", + pull_requests: [ + { + number: 43, + head: { sha: "suite-head" }, + base: { sha: "suite-base" }, + }, + ], + }, + }, + "delivery-suite-rerequest", + ); + + expect(res.status).toBe(200); + const jobs = await pool.query<{ payload: { trigger: Record } }>( + "SELECT payload FROM jobs WHERE kind = 'review'", + ); + expect(jobs.rows[0]!.payload.trigger).toEqual({ + source: "github_check_rerun", + webhookDeliveryId: "delivery-suite-rerequest", + webhookEvent: "check_suite", + webhookAction: "rerequested", + }); }); test("check_run rerequested for an unknown/disabled repo is skipped", async () => { From a8c9e1f07b3e692e546a3dfccebc56cb7f539434 Mon Sep 17 00:00:00 2001 From: Postil Maintainer Date: Sat, 18 Jul 2026 16:05:33 +0000 Subject: [PATCH 2/2] Harden review provenance integrity --- drizzle/0029_review_trigger_provenance.sql | 29 +- drizzle/meta/0029_snapshot.json | 3051 +++++++++++++++++ src/lib/db/schema.ts | 2 +- src/lib/review-trigger.ts | 6 +- tests/hosted-review-pause.test.ts | 31 +- tests/hosted-usage-reservations.test.ts | 2 +- ...eview-trigger-provenance-migration.test.ts | 67 + tests/review-trigger.test.ts | 19 + 8 files changed, 3193 insertions(+), 14 deletions(-) create mode 100644 drizzle/meta/0029_snapshot.json diff --git a/drizzle/0029_review_trigger_provenance.sql b/drizzle/0029_review_trigger_provenance.sql index 377a3232..1b1a6f31 100644 --- a/drizzle/0029_review_trigger_provenance.sql +++ b/drizzle/0029_review_trigger_provenance.sql @@ -1,9 +1,32 @@ ALTER TABLE "reviews" ADD COLUMN "trigger_source" text DEFAULT 'unknown' NOT NULL;--> statement-breakpoint ALTER TABLE "reviews" ADD COLUMN "trigger_context" jsonb;--> statement-breakpoint ALTER TABLE "usage_events" ADD COLUMN "trigger_source" text DEFAULT 'unknown' NOT NULL;--> statement-breakpoint -ALTER TABLE "reviews" ADD CONSTRAINT "reviews_trigger_source_check" CHECK ("reviews"."trigger_source" IN ('unknown', 'automatic_pull_request', 'requested_review', 'github_check_rerun'));--> statement-breakpoint -ALTER TABLE "reviews" ADD CONSTRAINT "reviews_trigger_context_check" CHECK (("reviews"."trigger_source" = 'unknown' AND ("reviews"."trigger_context" IS NULL OR "reviews"."trigger_context"->>'source' = 'unknown')) OR ("reviews"."trigger_source" <> 'unknown' AND "reviews"."trigger_context" IS NOT NULL AND jsonb_typeof("reviews"."trigger_context") = 'object' AND "reviews"."trigger_context"->>'source' = "reviews"."trigger_source" AND COALESCE(length(btrim("reviews"."trigger_context"->>'webhookDeliveryId')), 0) > 0));--> statement-breakpoint -ALTER TABLE "usage_events" ADD CONSTRAINT "usage_events_trigger_source_check" CHECK ("usage_events"."trigger_source" IN ('unknown', 'automatic_pull_request', 'requested_review', 'github_check_rerun', 'github_mention'));--> statement-breakpoint +ALTER TABLE "reviews" ADD CONSTRAINT "reviews_trigger_source_check" CHECK ("reviews"."trigger_source" IN ('unknown', 'automatic_pull_request', 'requested_review', 'github_check_rerun')) NOT VALID;--> statement-breakpoint +ALTER TABLE "reviews" ADD CONSTRAINT "reviews_trigger_context_check" CHECK ( + ("reviews"."trigger_source" = 'unknown' AND ("reviews"."trigger_context" IS NULL OR "reviews"."trigger_context" = '{"source":"unknown"}'::jsonb)) + OR ( + "reviews"."trigger_source" <> 'unknown' + AND "reviews"."trigger_context" IS NOT NULL + AND jsonb_typeof("reviews"."trigger_context") = 'object' + AND "reviews"."trigger_context" - ARRAY['source', 'webhookDeliveryId', 'webhookEvent', 'webhookAction', 'sourceCommentId', 'sourceUrl', 'requestedByGithubId', 'requestedByLogin', 'checkName']::text[] = '{}'::jsonb + AND "reviews"."trigger_context"->>'source' = "reviews"."trigger_source" + AND jsonb_typeof("reviews"."trigger_context"->'webhookDeliveryId') = 'string' + AND COALESCE(length(btrim("reviews"."trigger_context"->>'webhookDeliveryId')), 0) > 0 + AND length("reviews"."trigger_context"->>'webhookDeliveryId') <= 200 + AND ( + ("reviews"."trigger_source" = 'automatic_pull_request' AND "reviews"."trigger_context"->>'webhookEvent' = 'pull_request') + OR ("reviews"."trigger_source" = 'requested_review' AND "reviews"."trigger_context"->>'webhookEvent' IN ('issue_comment', 'pull_request_review_comment')) + OR ("reviews"."trigger_source" = 'github_check_rerun' AND "reviews"."trigger_context"->>'webhookEvent' IN ('check_run', 'check_suite')) + ) + AND (NOT "reviews"."trigger_context" ? 'webhookAction' OR (jsonb_typeof("reviews"."trigger_context"->'webhookAction') = 'string' AND length("reviews"."trigger_context"->>'webhookAction') <= 100)) + AND (NOT "reviews"."trigger_context" ? 'sourceCommentId' OR (jsonb_typeof("reviews"."trigger_context"->'sourceCommentId') = 'number' AND ("reviews"."trigger_context"->>'sourceCommentId')::numeric = trunc(("reviews"."trigger_context"->>'sourceCommentId')::numeric) AND ("reviews"."trigger_context"->>'sourceCommentId')::numeric BETWEEN 1 AND 9007199254740991)) + AND (NOT "reviews"."trigger_context" ? 'sourceUrl' OR (jsonb_typeof("reviews"."trigger_context"->'sourceUrl') = 'string' AND length("reviews"."trigger_context"->>'sourceUrl') <= 2048 AND "reviews"."trigger_context"->>'sourceUrl' ~* '^https://github[.]com([/?#]|$)')) + AND (NOT "reviews"."trigger_context" ? 'requestedByGithubId' OR (jsonb_typeof("reviews"."trigger_context"->'requestedByGithubId') = 'number' AND ("reviews"."trigger_context"->>'requestedByGithubId')::numeric = trunc(("reviews"."trigger_context"->>'requestedByGithubId')::numeric) AND ("reviews"."trigger_context"->>'requestedByGithubId')::numeric BETWEEN 1 AND 9007199254740991)) + AND (NOT "reviews"."trigger_context" ? 'requestedByLogin' OR (jsonb_typeof("reviews"."trigger_context"->'requestedByLogin') = 'string' AND length("reviews"."trigger_context"->>'requestedByLogin') <= 100)) + AND (NOT "reviews"."trigger_context" ? 'checkName' OR (jsonb_typeof("reviews"."trigger_context"->'checkName') = 'string' AND length("reviews"."trigger_context"->>'checkName') <= 200)) + ) +) NOT VALID;--> statement-breakpoint +ALTER TABLE "usage_events" ADD CONSTRAINT "usage_events_trigger_source_check" CHECK ("usage_events"."trigger_source" IN ('unknown', 'automatic_pull_request', 'requested_review', 'github_check_rerun', 'github_mention')) NOT VALID;--> statement-breakpoint CREATE OR REPLACE FUNCTION reject_review_trigger_provenance_update() RETURNS trigger LANGUAGE plpgsql AS $$ diff --git a/drizzle/meta/0029_snapshot.json b/drizzle/meta/0029_snapshot.json new file mode 100644 index 00000000..dc14a2ef --- /dev/null +++ b/drizzle/meta/0029_snapshot.json @@ -0,0 +1,3051 @@ +{ + "id": "d87a64e7-a695-415c-8ec3-0deff04461ea", + "prevId": "6e1a362c-7c3e-4bdc-883b-5ab35195582f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.billing_credit_grants": { + "name": "billing_credit_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "billing_credit_grants_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor": { + "name": "actor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin_script'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applies_at": { + "name": "applies_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_credit_grants_org_created_idx": { + "name": "billing_credit_grants_org_created_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_credit_grants_org_idempotency_idx": { + "name": "billing_credit_grants_org_idempotency_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_credit_grants_org_id_organizations_id_fk": { + "name": "billing_credit_grants_org_id_organizations_id_fk", + "tableFrom": "billing_credit_grants", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_credit_grants_amount_cents_positive": { + "name": "billing_credit_grants_amount_cents_positive", + "value": "\"billing_credit_grants\".\"amount_cents\" > 0" + }, + "billing_credit_grants_reason_nonempty": { + "name": "billing_credit_grants_reason_nonempty", + "value": "length(btrim(\"billing_credit_grants\".\"reason\")) > 0" + }, + "billing_credit_grants_actor_nonempty": { + "name": "billing_credit_grants_actor_nonempty", + "value": "length(btrim(\"billing_credit_grants\".\"actor\")) > 0" + }, + "billing_credit_grants_source_nonempty": { + "name": "billing_credit_grants_source_nonempty", + "value": "length(btrim(\"billing_credit_grants\".\"source\")) > 0" + }, + "billing_credit_grants_idempotency_key_nonempty": { + "name": "billing_credit_grants_idempotency_key_nonempty", + "value": "length(btrim(\"billing_credit_grants\".\"idempotency_key\")) > 0" + } + }, + "isRLSEnabled": false + }, + "public.finding_approvals": { + "name": "finding_approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "actor_github_id": { + "name": "actor_github_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_login_snapshot": { + "name": "actor_login_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_role_snapshot": { + "name": "actor_role_snapshot", + "type": "finding_approval_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "rationale": { + "name": "rationale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "finding_approval_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_comment_id": { + "name": "source_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_by_user_id": { + "name": "revoked_by_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "finding_approvals_active_idx": { + "name": "finding_approvals_active_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"finding_approvals\".\"revoked_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "finding_approvals_review_idx": { + "name": "finding_approvals_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "finding_approvals_review_id_reviews_id_fk": { + "name": "finding_approvals_review_id_reviews_id_fk", + "tableFrom": "finding_approvals", + "tableTo": "reviews", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "finding_approvals_actor_user_id_users_id_fk": { + "name": "finding_approvals_actor_user_id_users_id_fk", + "tableFrom": "finding_approvals", + "tableTo": "users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "finding_approvals_revoked_by_user_id_users_id_fk": { + "name": "finding_approvals_revoked_by_user_id_users_id_fk", + "tableFrom": "finding_approvals", + "tableTo": "users", + "columnsFrom": [ + "revoked_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "finding_approvals_rationale_nonempty": { + "name": "finding_approvals_rationale_nonempty", + "value": "length(btrim(\"finding_approvals\".\"rationale\")) > 0" + } + }, + "isRLSEnabled": false + }, + "public.github_webhook_delivery_recoveries": { + "name": "github_webhook_delivery_recoveries", + "schema": "", + "columns": { + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "delivery_guid": { + "name": "delivery_guid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redelivery": { + "name": "redelivery", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "request_state": { + "name": "request_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_attempts": { + "name": "request_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_requested_at": { + "name": "last_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "request_status_code": { + "name": "request_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "recovery_delivery_id": { + "name": "recovery_delivery_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_category": { + "name": "last_error_category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_webhook_delivery_recoveries_guid_idx": { + "name": "github_webhook_delivery_recoveries_guid_idx", + "columns": [ + { + "expression": "delivery_guid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivered_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_webhook_delivery_recoveries_retry_idx": { + "name": "github_webhook_delivery_recoveries_retry_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivered_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"github_webhook_delivery_recoveries\".\"outcome\" = 'failure' AND \"github_webhook_delivery_recoveries\".\"recovery_delivery_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_webhook_delivery_recoveries_outcome_check": { + "name": "github_webhook_delivery_recoveries_outcome_check", + "value": "\"github_webhook_delivery_recoveries\".\"outcome\" IN ('success', 'failure', 'pending')" + }, + "github_webhook_delivery_recoveries_request_state_check": { + "name": "github_webhook_delivery_recoveries_request_state_check", + "value": "\"github_webhook_delivery_recoveries\".\"request_state\" IS NULL OR \"github_webhook_delivery_recoveries\".\"request_state\" IN ('requesting', 'retryable', 'accepted', 'terminal', 'exhausted', 'recovered')" + }, + "github_webhook_delivery_recoveries_attempts_check": { + "name": "github_webhook_delivery_recoveries_attempts_check", + "value": "\"github_webhook_delivery_recoveries\".\"request_attempts\" >= 0 AND \"github_webhook_delivery_recoveries\".\"request_attempts\" <= 2" + } + }, + "isRLSEnabled": false + }, + "public.github_webhook_redelivery_state": { + "name": "github_webhook_redelivery_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_owner": { + "name": "lease_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sweep_started_at": { + "name": "sweep_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_page_at": { + "name": "last_page_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_sweep_completed_at": { + "name": "last_sweep_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rate_limited_until": { + "name": "rate_limited_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_category": { + "name": "last_error_category", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_webhook_redelivery_state_singleton_check": { + "name": "github_webhook_redelivery_state_singleton_check", + "value": "\"github_webhook_redelivery_state\".\"id\" = 1" + } + }, + "isRLSEnabled": false + }, + "public.hosted_usage_reservations": { + "name": "hosted_usage_reservations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "review_id": { + "name": "review_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'review'" + }, + "reserved_micros": { + "name": "reserved_micros", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "actual_micros": { + "name": "actual_micros", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hosted_usage_reservations_review_idx": { + "name": "hosted_usage_reservations_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hosted_usage_reservations_active_org_expiry_idx": { + "name": "hosted_usage_reservations_active_org_expiry_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"hosted_usage_reservations\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hosted_usage_reservations_org_id_organizations_id_fk": { + "name": "hosted_usage_reservations_org_id_organizations_id_fk", + "tableFrom": "hosted_usage_reservations", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hosted_usage_reservations_review_id_reviews_id_fk": { + "name": "hosted_usage_reservations_review_id_reviews_id_fk", + "tableFrom": "hosted_usage_reservations", + "tableTo": "reviews", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "hosted_usage_reservations_status_check": { + "name": "hosted_usage_reservations_status_check", + "value": "\"hosted_usage_reservations\".\"status\" IN ('active', 'reconciled', 'released')" + }, + "hosted_usage_reservations_operation_check": { + "name": "hosted_usage_reservations_operation_check", + "value": "\"hosted_usage_reservations\".\"operation\" IN ('review', 'respond')" + }, + "hosted_usage_reservations_operation_reference_check": { + "name": "hosted_usage_reservations_operation_reference_check", + "value": "(\"hosted_usage_reservations\".\"operation\" = 'review' AND \"hosted_usage_reservations\".\"review_id\" IS NOT NULL) OR (\"hosted_usage_reservations\".\"operation\" = 'respond' AND \"hosted_usage_reservations\".\"review_id\" IS NULL)" + }, + "hosted_usage_reservations_reserved_positive": { + "name": "hosted_usage_reservations_reserved_positive", + "value": "\"hosted_usage_reservations\".\"reserved_micros\" > 0" + }, + "hosted_usage_reservations_actual_nonnegative": { + "name": "hosted_usage_reservations_actual_nonnegative", + "value": "\"hosted_usage_reservations\".\"actual_micros\" IS NULL OR \"hosted_usage_reservations\".\"actual_micros\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.installations": { + "name": "installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "installations_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "github_installation_id": { + "name": "github_installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suspended": { + "name": "suspended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "installations_org_idx": { + "name": "installations_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "installations_org_id_organizations_id_fk": { + "name": "installations_org_id_organizations_id_fk", + "tableFrom": "installations", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "installations_github_installation_id_unique": { + "name": "installations_github_installation_id_unique", + "nullsNotDistinct": false, + "columns": [ + "github_installation_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jobs": { + "name": "jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "jobs_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "run_after": { + "name": "run_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "locked_by": { + "name": "locked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "jobs_claim_idx": { + "name": "jobs_claim_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_after", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "jobs_running_locked_at_idx": { + "name": "jobs_running_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"jobs\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_config_probe_refreshes": { + "name": "org_config_probe_refreshes", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "refreshed_at": { + "name": "refreshed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "org_config_probe_refreshes_org_id_organizations_id_fk": { + "name": "org_config_probe_refreshes_org_id_organizations_id_fk", + "tableFrom": "org_config_probe_refreshes", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_config_snapshots": { + "name": "org_config_snapshots", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "source_repository_id": { + "name": "source_repository_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_github_repo_id": { + "name": "source_github_repo_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "source_full_name": { + "name": "source_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_yaml": { + "name": "config_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "guardrails_md": { + "name": "guardrails_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_policy_md": { + "name": "content_policy_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "loaded_files": { + "name": "loaded_files", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "stale": { + "name": "stale", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_config_snapshots_org_id_organizations_id_fk": { + "name": "org_config_snapshots_org_id_organizations_id_fk", + "tableFrom": "org_config_snapshots", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_config_snapshots_source_repository_id_repositories_id_fk": { + "name": "org_config_snapshots_source_repository_id_repositories_id_fk", + "tableFrom": "org_config_snapshots", + "tableTo": "repositories", + "columnsFrom": [ + "source_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_members": { + "name": "org_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "org_members_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + } + }, + "indexes": { + "org_members_org_user_idx": { + "name": "org_members_org_user_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_members_user_idx": { + "name": "org_members_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_members_org_id_organizations_id_fk": { + "name": "org_members_org_id_organizations_id_fk", + "tableFrom": "org_members", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_members_user_id_users_id_fk": { + "name": "org_members_user_id_users_id_fk", + "tableFrom": "org_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_settings": { + "name": "org_settings", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "api_base": { + "name": "api_base", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_ciphertext": { + "name": "api_key_ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "api_format": { + "name": "api_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openai-compatible'" + }, + "api_auth_header_ciphertext": { + "name": "api_auth_header_ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "api_auth_value_ciphertext": { + "name": "api_auth_value_ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_cascade": { + "name": "model_cascade", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config_yaml": { + "name": "config_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "guardrails_md": { + "name": "guardrails_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_policy_md": { + "name": "content_policy_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_config_enabled": { + "name": "shared_config_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "escalation_email": { + "name": "escalation_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "escalation_email_pending": { + "name": "escalation_email_pending", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "escalation_email_verified_at": { + "name": "escalation_email_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "escalation_email_verification_token_digest": { + "name": "escalation_email_verification_token_digest", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "escalation_email_verification_token_ciphertext": { + "name": "escalation_email_verification_token_ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "escalation_email_verification_expires_at": { + "name": "escalation_email_verification_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "escalation_email_verification_requested_at": { + "name": "escalation_email_verification_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "escalation_email_verification_sent_at": { + "name": "escalation_email_verification_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "escalation_email_verification_message_id": { + "name": "escalation_email_verification_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_settings_org_id_organizations_id_fk": { + "name": "org_settings_org_id_organizations_id_fk", + "tableFrom": "org_settings", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_entitlements": { + "name": "organization_entitlements", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "subscription_mode": { + "name": "subscription_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "past_due_grace_ends_at": { + "name": "past_due_grace_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "period_starts_at": { + "name": "period_starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "period_ends_at": { + "name": "period_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "included_usage_micros": { + "name": "included_usage_micros", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "overage_hard_cap_micros": { + "name": "overage_hard_cap_micros", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "included_usage_cents": { + "name": "included_usage_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "overage_hard_cap_cents": { + "name": "overage_hard_cap_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "billing_contact_email": { + "name": "billing_contact_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_contact_verified_at": { + "name": "billing_contact_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "billing_contact_pending": { + "name": "billing_contact_pending", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_contact_verification_token_digest": { + "name": "billing_contact_verification_token_digest", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "billing_contact_verification_token_ciphertext": { + "name": "billing_contact_verification_token_ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "billing_contact_verification_expires_at": { + "name": "billing_contact_verification_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "billing_contact_verification_requested_at": { + "name": "billing_contact_verification_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "billing_contact_verification_sent_at": { + "name": "billing_contact_verification_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "billing_contact_verification_message_id": { + "name": "billing_contact_verification_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "promotional_eligible": { + "name": "promotional_eligible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "promotional_ends_at": { + "name": "promotional_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_entitlements_org_id_organizations_id_fk": { + "name": "organization_entitlements_org_id_organizations_id_fk", + "tableFrom": "organization_entitlements", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_entitlements_subscription_mode_check": { + "name": "organization_entitlements_subscription_mode_check", + "value": "\"organization_entitlements\".\"subscription_mode\" IN ('hosted', 'byok')" + }, + "organization_entitlements_status_check": { + "name": "organization_entitlements_status_check", + "value": "\"organization_entitlements\".\"status\" IN ('active', 'trialing', 'past_due', 'suspended')" + }, + "organization_entitlements_included_usage_micros_nonnegative": { + "name": "organization_entitlements_included_usage_micros_nonnegative", + "value": "\"organization_entitlements\".\"included_usage_micros\" >= 0" + }, + "organization_entitlements_overage_cap_micros_nonnegative": { + "name": "organization_entitlements_overage_cap_micros_nonnegative", + "value": "\"organization_entitlements\".\"overage_hard_cap_micros\" IS NULL OR \"organization_entitlements\".\"overage_hard_cap_micros\" >= 0" + }, + "organization_entitlements_included_usage_nonnegative": { + "name": "organization_entitlements_included_usage_nonnegative", + "value": "\"organization_entitlements\".\"included_usage_cents\" >= 0" + }, + "organization_entitlements_overage_cap_nonnegative": { + "name": "organization_entitlements_overage_cap_nonnegative", + "value": "\"organization_entitlements\".\"overage_hard_cap_cents\" IS NULL OR \"organization_entitlements\".\"overage_hard_cap_cents\" >= 0" + }, + "organization_entitlements_updated_by_nonempty": { + "name": "organization_entitlements_updated_by_nonempty", + "value": "length(btrim(\"organization_entitlements\".\"updated_by\")) > 0" + } + }, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "organizations_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_org_id": { + "name": "github_org_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'beta'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_github_org_id_idx": { + "name": "organizations_github_org_id_idx", + "columns": [ + { + "expression": "github_org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.release_steps": { + "name": "release_steps", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repo_config_probes": { + "name": "repo_config_probes", + "schema": "", + "columns": { + "repository_id": { + "name": "repository_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "probed_at": { + "name": "probed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "files": { + "name": "files", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "repo_config_probes_repository_id_repositories_id_fk": { + "name": "repo_config_probes_repository_id_repositories_id_fk", + "tableFrom": "repo_config_probes", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repositories": { + "name": "repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "repositories_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "installation_id": { + "name": "installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "github_repo_id": { + "name": "github_repo_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private": { + "name": "private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "repositories_installation_id_installations_id_fk": { + "name": "repositories_installation_id_installations_id_fk", + "tableFrom": "repositories", + "tableTo": "installations", + "columnsFrom": [ + "installation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "repositories_github_repo_id_unique": { + "name": "repositories_github_repo_id_unique", + "nullsNotDistinct": false, + "columns": [ + "github_repo_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repository_enablement_events": { + "name": "repository_enablement_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "repository_enablement_events_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "github_repo_id": { + "name": "github_repo_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "repository_full_name": { + "name": "repository_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_private": { + "name": "repository_private", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'dashboard'" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "repository_enablement_events_org_time_idx": { + "name": "repository_enablement_events_org_time_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "repository_enablement_events_repo_time_idx": { + "name": "repository_enablement_events_repo_time_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "repository_enablement_events_org_id_organizations_id_fk": { + "name": "repository_enablement_events_org_id_organizations_id_fk", + "tableFrom": "repository_enablement_events", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "repository_enablement_events_repository_id_repositories_id_fk": { + "name": "repository_enablement_events_repository_id_repositories_id_fk", + "tableFrom": "repository_enablement_events", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "repository_enablement_events_actor_user_id_users_id_fk": { + "name": "repository_enablement_events_actor_user_id_users_id_fk", + "tableFrom": "repository_enablement_events", + "tableTo": "users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "repository_enablement_events_action_check": { + "name": "repository_enablement_events_action_check", + "value": "\"repository_enablement_events\".\"action\" IN ('enable', 'disable')" + }, + "repository_enablement_events_source_check": { + "name": "repository_enablement_events_source_check", + "value": "\"repository_enablement_events\".\"source\" IN ('dashboard', 'github_installation', 'github_pull_request', 'github_transfer', 'github_uninstall', 'migration_baseline')" + } + }, + "isRLSEnabled": false + }, + "public.respond_deliveries": { + "name": "respond_deliveries", + "schema": "", + "columns": { + "job_id": { + "name": "job_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "reservation_id": { + "name": "reservation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'prepared'" + }, + "delivery_lease_expires_at": { + "name": "delivery_lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "github_comment_id": { + "name": "github_comment_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "respond_deliveries_pending_idx": { + "name": "respond_deliveries_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "respond_deliveries_job_id_jobs_id_fk": { + "name": "respond_deliveries_job_id_jobs_id_fk", + "tableFrom": "respond_deliveries", + "tableTo": "jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "respond_deliveries_repository_id_repositories_id_fk": { + "name": "respond_deliveries_repository_id_repositories_id_fk", + "tableFrom": "respond_deliveries", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "respond_deliveries_reservation_id_hosted_usage_reservations_id_fk": { + "name": "respond_deliveries_reservation_id_hosted_usage_reservations_id_fk", + "tableFrom": "respond_deliveries", + "tableTo": "hosted_usage_reservations", + "columnsFrom": [ + "reservation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "respond_deliveries_state_check": { + "name": "respond_deliveries_state_check", + "value": "\"respond_deliveries\".\"state\" IN ('prepared', 'delivering', 'delivered')" + }, + "respond_deliveries_issue_number_positive": { + "name": "respond_deliveries_issue_number_positive", + "value": "\"respond_deliveries\".\"issue_number\" > 0" + }, + "respond_deliveries_body_nonempty": { + "name": "respond_deliveries_body_nonempty", + "value": "length(btrim(\"respond_deliveries\".\"body\")) > 0" + } + }, + "isRLSEnabled": false + }, + "public.review_logs": { + "name": "review_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "review_logs_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "review_id": { + "name": "review_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "at": { + "name": "at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "review_logs_review_seq_idx": { + "name": "review_logs_review_seq_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_logs_review_id_reviews_id_fk": { + "name": "review_logs_review_id_reviews_id_fk", + "tableFrom": "review_logs", + "tableTo": "reviews", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reviews": { + "name": "reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "reviews_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "public_id": { + "name": "public_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "repository_id": { + "name": "repository_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "author_github_id": { + "name": "author_github_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "author_login": { + "name": "author_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "since_sha": { + "name": "since_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_source": { + "name": "trigger_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "trigger_context": { + "name": "trigger_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "review_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "envelope": { + "name": "envelope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config_files": { + "name": "config_files", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "config_provenance": { + "name": "config_provenance", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "silent": { + "name": "silent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "engine_gate_failing": { + "name": "engine_gate_failing", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gate_failing": { + "name": "gate_failing", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "advisory_check_run_id": { + "name": "advisory_check_run_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "gate_check_run_id": { + "name": "gate_check_run_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "reviews_public_id_idx": { + "name": "reviews_public_id_idx", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reviews_repo_pr_idx": { + "name": "reviews_repo_pr_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reviews_status_idx": { + "name": "reviews_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reviews_running_started_at_idx": { + "name": "reviews_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"reviews\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reviews_repository_id_repositories_id_fk": { + "name": "reviews_repository_id_repositories_id_fk", + "tableFrom": "reviews", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "reviews_trigger_source_check": { + "name": "reviews_trigger_source_check", + "value": "\"reviews\".\"trigger_source\" IN ('unknown', 'automatic_pull_request', 'requested_review', 'github_check_rerun')" + }, + "reviews_trigger_context_check": { + "name": "reviews_trigger_context_check", + "value": "(\"reviews\".\"trigger_source\" = 'unknown' AND (\"reviews\".\"trigger_context\" IS NULL OR \"reviews\".\"trigger_context\" = '{\"source\":\"unknown\"}'::jsonb)) OR (\"reviews\".\"trigger_source\" <> 'unknown' AND \"reviews\".\"trigger_context\" IS NOT NULL AND jsonb_typeof(\"reviews\".\"trigger_context\") = 'object' AND \"reviews\".\"trigger_context\" - ARRAY['source', 'webhookDeliveryId', 'webhookEvent', 'webhookAction', 'sourceCommentId', 'sourceUrl', 'requestedByGithubId', 'requestedByLogin', 'checkName']::text[] = '{}'::jsonb AND \"reviews\".\"trigger_context\"->>'source' = \"reviews\".\"trigger_source\" AND jsonb_typeof(\"reviews\".\"trigger_context\"->'webhookDeliveryId') = 'string' AND COALESCE(length(btrim(\"reviews\".\"trigger_context\"->>'webhookDeliveryId')), 0) > 0 AND length(\"reviews\".\"trigger_context\"->>'webhookDeliveryId') <= 200 AND ((\"reviews\".\"trigger_source\" = 'automatic_pull_request' AND \"reviews\".\"trigger_context\"->>'webhookEvent' = 'pull_request') OR (\"reviews\".\"trigger_source\" = 'requested_review' AND \"reviews\".\"trigger_context\"->>'webhookEvent' IN ('issue_comment', 'pull_request_review_comment')) OR (\"reviews\".\"trigger_source\" = 'github_check_rerun' AND \"reviews\".\"trigger_context\"->>'webhookEvent' IN ('check_run', 'check_suite'))) AND (NOT \"reviews\".\"trigger_context\" ? 'webhookAction' OR (jsonb_typeof(\"reviews\".\"trigger_context\"->'webhookAction') = 'string' AND length(\"reviews\".\"trigger_context\"->>'webhookAction') <= 100)) AND (NOT \"reviews\".\"trigger_context\" ? 'sourceCommentId' OR (jsonb_typeof(\"reviews\".\"trigger_context\"->'sourceCommentId') = 'number' AND (\"reviews\".\"trigger_context\"->>'sourceCommentId')::numeric = trunc((\"reviews\".\"trigger_context\"->>'sourceCommentId')::numeric) AND (\"reviews\".\"trigger_context\"->>'sourceCommentId')::numeric BETWEEN 1 AND 9007199254740991)) AND (NOT \"reviews\".\"trigger_context\" ? 'sourceUrl' OR (jsonb_typeof(\"reviews\".\"trigger_context\"->'sourceUrl') = 'string' AND length(\"reviews\".\"trigger_context\"->>'sourceUrl') <= 2048 AND \"reviews\".\"trigger_context\"->>'sourceUrl' ~* '^https://github[.]com([/?#]|$)')) AND (NOT \"reviews\".\"trigger_context\" ? 'requestedByGithubId' OR (jsonb_typeof(\"reviews\".\"trigger_context\"->'requestedByGithubId') = 'number' AND (\"reviews\".\"trigger_context\"->>'requestedByGithubId')::numeric = trunc((\"reviews\".\"trigger_context\"->>'requestedByGithubId')::numeric) AND (\"reviews\".\"trigger_context\"->>'requestedByGithubId')::numeric BETWEEN 1 AND 9007199254740991)) AND (NOT \"reviews\".\"trigger_context\" ? 'requestedByLogin' OR (jsonb_typeof(\"reviews\".\"trigger_context\"->'requestedByLogin') = 'string' AND length(\"reviews\".\"trigger_context\"->>'requestedByLogin') <= 100)) AND (NOT \"reviews\".\"trigger_context\" ? 'checkName' OR (jsonb_typeof(\"reviews\".\"trigger_context\"->'checkName') = 'string' AND length(\"reviews\".\"trigger_context\"->>'checkName') <= 200)))" + } + }, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "github_access_token_ciphertext": { + "name": "github_access_token_ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "membership_checked_at": { + "name": "membership_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "membership_check_available_at": { + "name": "membership_check_available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_events": { + "name": "usage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "usage_events_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "review_id": { + "name": "review_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "trigger_source": { + "name": "trigger_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "prompt_tokens": { + "name": "prompt_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "completion_tokens": { + "name": "completion_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_micros": { + "name": "cost_micros", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_scope": { + "name": "billing_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "usage_events_org_id_organizations_id_fk": { + "name": "usage_events_org_id_organizations_id_fk", + "tableFrom": "usage_events", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_events_repository_id_repositories_id_fk": { + "name": "usage_events_repository_id_repositories_id_fk", + "tableFrom": "usage_events", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_events_review_id_reviews_id_fk": { + "name": "usage_events_review_id_reviews_id_fk", + "tableFrom": "usage_events", + "tableTo": "reviews", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_events_cost_micros_nonnegative": { + "name": "usage_events_cost_micros_nonnegative", + "value": "\"usage_events\".\"cost_micros\" IS NULL OR \"usage_events\".\"cost_micros\" >= 0" + }, + "usage_events_cost_cents_nonnegative": { + "name": "usage_events_cost_cents_nonnegative", + "value": "\"usage_events\".\"cost_cents\" IS NULL OR \"usage_events\".\"cost_cents\" >= 0" + }, + "usage_events_billing_scope_check": { + "name": "usage_events_billing_scope_check", + "value": "\"usage_events\".\"billing_scope\" IN ('analytics', 'private_hosted')" + }, + "usage_events_trigger_source_check": { + "name": "usage_events_trigger_source_check", + "value": "\"usage_events\".\"trigger_source\" IN ('unknown', 'automatic_pull_request', 'requested_review', 'github_check_rerun', 'github_mention')" + } + }, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "users_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "github_id": { + "name": "github_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "login": { + "name": "login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_github_id_unique": { + "name": "users_github_id_unique", + "nullsNotDistinct": false, + "columns": [ + "github_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_deliveries": { + "name": "webhook_deliveries", + "schema": "", + "columns": { + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webhook_deliveries_completed_at_idx": { + "name": "webhook_deliveries_completed_at_idx", + "columns": [ + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook_deliveries\".\"completed_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_deliveries_payload_completion_check": { + "name": "webhook_deliveries_payload_completion_check", + "value": "(\"webhook_deliveries\".\"payload\" IS NULL) = (\"webhook_deliveries\".\"completed_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + } + }, + "enums": { + "public.finding_approval_role": { + "name": "finding_approval_role", + "schema": "public", + "values": [ + "member", + "admin" + ] + }, + "public.finding_approval_source": { + "name": "finding_approval_source", + "schema": "public", + "values": [ + "github", + "dashboard" + ] + }, + "public.job_status": { + "name": "job_status", + "schema": "public", + "values": [ + "queued", + "running", + "done", + "failed" + ] + }, + "public.review_status": { + "name": "review_status", + "schema": "public", + "values": [ + "queued", + "running", + "completed", + "failed", + "stale" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 1ec4440a..db795226 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -224,7 +224,7 @@ export const reviews = pgTable( ), check( "reviews_trigger_context_check", - sql`(${t.triggerSource} = 'unknown' AND (${t.triggerContext} IS NULL OR ${t.triggerContext}->>'source' = 'unknown')) OR (${t.triggerSource} <> 'unknown' AND ${t.triggerContext} IS NOT NULL AND jsonb_typeof(${t.triggerContext}) = 'object' AND ${t.triggerContext}->>'source' = ${t.triggerSource} AND COALESCE(length(btrim(${t.triggerContext}->>'webhookDeliveryId')), 0) > 0)`, + sql`(${t.triggerSource} = 'unknown' AND (${t.triggerContext} IS NULL OR ${t.triggerContext} = '{"source":"unknown"}'::jsonb)) OR (${t.triggerSource} <> 'unknown' AND ${t.triggerContext} IS NOT NULL AND jsonb_typeof(${t.triggerContext}) = 'object' AND ${t.triggerContext} - ARRAY['source', 'webhookDeliveryId', 'webhookEvent', 'webhookAction', 'sourceCommentId', 'sourceUrl', 'requestedByGithubId', 'requestedByLogin', 'checkName']::text[] = '{}'::jsonb AND ${t.triggerContext}->>'source' = ${t.triggerSource} AND jsonb_typeof(${t.triggerContext}->'webhookDeliveryId') = 'string' AND COALESCE(length(btrim(${t.triggerContext}->>'webhookDeliveryId')), 0) > 0 AND length(${t.triggerContext}->>'webhookDeliveryId') <= 200 AND ((${t.triggerSource} = 'automatic_pull_request' AND ${t.triggerContext}->>'webhookEvent' = 'pull_request') OR (${t.triggerSource} = 'requested_review' AND ${t.triggerContext}->>'webhookEvent' IN ('issue_comment', 'pull_request_review_comment')) OR (${t.triggerSource} = 'github_check_rerun' AND ${t.triggerContext}->>'webhookEvent' IN ('check_run', 'check_suite'))) AND (NOT ${t.triggerContext} ? 'webhookAction' OR (jsonb_typeof(${t.triggerContext}->'webhookAction') = 'string' AND length(${t.triggerContext}->>'webhookAction') <= 100)) AND (NOT ${t.triggerContext} ? 'sourceCommentId' OR (jsonb_typeof(${t.triggerContext}->'sourceCommentId') = 'number' AND (${t.triggerContext}->>'sourceCommentId')::numeric = trunc((${t.triggerContext}->>'sourceCommentId')::numeric) AND (${t.triggerContext}->>'sourceCommentId')::numeric BETWEEN 1 AND 9007199254740991)) AND (NOT ${t.triggerContext} ? 'sourceUrl' OR (jsonb_typeof(${t.triggerContext}->'sourceUrl') = 'string' AND length(${t.triggerContext}->>'sourceUrl') <= 2048 AND ${t.triggerContext}->>'sourceUrl' ~* '^https://github[.]com([/?#]|$)')) AND (NOT ${t.triggerContext} ? 'requestedByGithubId' OR (jsonb_typeof(${t.triggerContext}->'requestedByGithubId') = 'number' AND (${t.triggerContext}->>'requestedByGithubId')::numeric = trunc((${t.triggerContext}->>'requestedByGithubId')::numeric) AND (${t.triggerContext}->>'requestedByGithubId')::numeric BETWEEN 1 AND 9007199254740991)) AND (NOT ${t.triggerContext} ? 'requestedByLogin' OR (jsonb_typeof(${t.triggerContext}->'requestedByLogin') = 'string' AND length(${t.triggerContext}->>'requestedByLogin') <= 100)) AND (NOT ${t.triggerContext} ? 'checkName' OR (jsonb_typeof(${t.triggerContext}->'checkName') = 'string' AND length(${t.triggerContext}->>'checkName') <= 200)))`, ), ], ); diff --git a/src/lib/review-trigger.ts b/src/lib/review-trigger.ts index 514bb11c..0e9bea65 100644 --- a/src/lib/review-trigger.ts +++ b/src/lib/review-trigger.ts @@ -56,7 +56,7 @@ export function normalizeReviewTriggerContext(value: unknown): ReviewTriggerCont if (candidate.source === "unknown") return { source: "unknown" }; if ( typeof candidate.webhookDeliveryId !== "string" || - candidate.webhookDeliveryId.length === 0 || + candidate.webhookDeliveryId.trim().length === 0 || candidate.webhookDeliveryId.length > 200 ) { return { source: "unknown" }; @@ -97,10 +97,10 @@ export function normalizeReviewTriggerContext(value: unknown): ReviewTriggerCont } function safeGithubUrl(value: unknown): value is string { - if (typeof value !== "string" || value.length > 2_048) return false; + if (typeof value !== "string" || value.length > 2_048 || value !== value.trim()) return false; try { const parsed = new URL(value); - return parsed.protocol === "https:" && parsed.hostname === "github.com"; + return parsed.origin === "https://github.com"; } catch { return false; } diff --git a/tests/hosted-review-pause.test.ts b/tests/hosted-review-pause.test.ts index 8649e932..8acd9cb2 100644 --- a/tests/hosted-review-pause.test.ts +++ b/tests/hosted-review-pause.test.ts @@ -72,8 +72,12 @@ describeDb("paused hosted review claims", () => { headSha: "head-one", baseSha: "base", sinceSha: null, - triggerSource: "unknown" as const, - triggerContext: { source: "unknown" as const }, + triggerSource: "requested_review" as const, + triggerContext: { + source: "requested_review" as const, + webhookDeliveryId: "pause-request", + webhookEvent: "issue_comment" as const, + }, queuedAt: new Date("2026-07-17T14:00:00Z"), startedAt: new Date("2026-07-17T14:00:01Z"), }; @@ -100,16 +104,31 @@ describeDb("paused hosted review claims", () => { ); expect(secondHead).not.toBeNull(); - const rows = await pool!.query<{ head_sha: string; status: string }>( - `SELECT head_sha, status + const rows = await pool!.query<{ + head_sha: string; + status: string; + trigger_source: string; + trigger_context: Record; + }>( + `SELECT head_sha, status, trigger_source, trigger_context FROM reviews WHERE repository_id = $1 AND pr_number = $2 ORDER BY head_sha`, [repositoryId, baseClaim.prNumber], ); expect(rows.rows).toEqual([ - { head_sha: "head-one", status: "failed" }, - { head_sha: "head-two", status: "failed" }, + { + head_sha: "head-one", + status: "failed", + trigger_source: "requested_review", + trigger_context: baseClaim.triggerContext, + }, + { + head_sha: "head-two", + status: "failed", + trigger_source: "requested_review", + trigger_context: baseClaim.triggerContext, + }, ]); const cleanupJobs = await pool!.query<{ payload: Record }>( "SELECT payload FROM jobs WHERE kind = 'check-run-cleanup' ORDER BY id", diff --git a/tests/hosted-usage-reservations.test.ts b/tests/hosted-usage-reservations.test.ts index 828b3368..cdc0602b 100644 --- a/tests/hosted-usage-reservations.test.ts +++ b/tests/hosted-usage-reservations.test.ts @@ -93,7 +93,7 @@ describeDb("hosted usage reservations on PostgreSQL", () => { VALUES (${repositoryId}, 1, 'head-1', 'base', 'running', 'unknown', NULL), (${repositoryId}, 2, 'head-2', 'base', 'running', 'requested_review', - '{"source":"requested_review","webhookDeliveryId":"billing-test"}'::jsonb) + '{"source":"requested_review","webhookDeliveryId":"billing-test","webhookEvent":"issue_comment"}'::jsonb) RETURNING id; `); reviewIds = reviews.rows.map((row) => Number(row.id)); diff --git a/tests/review-trigger-provenance-migration.test.ts b/tests/review-trigger-provenance-migration.test.ts index b887144b..a9a0bdc4 100644 --- a/tests/review-trigger-provenance-migration.test.ts +++ b/tests/review-trigger-provenance-migration.test.ts @@ -79,6 +79,7 @@ describeDb("review trigger provenance migration", () => { const context = { source: "requested_review", webhookDeliveryId: "delivery-1", + webhookEvent: "issue_comment", sourceCommentId: 123, }; const inserted = await pool.query<{ id: string }>( @@ -134,5 +135,71 @@ describeDb("review trigger provenance migration", () => { [repositoryId, JSON.stringify({ source: "requested_review" })], ), ).rejects.toMatchObject({ code: "23514" }); + await expect( + pool.query( + `INSERT INTO reviews + (repository_id, pr_number, head_sha, base_sha, trigger_source, trigger_context) + VALUES ($1, 6, 'wrong-event-head', 'base', 'requested_review', $2)`, + [ + repositoryId, + JSON.stringify({ + source: "requested_review", + webhookDeliveryId: "delivery-3", + webhookEvent: "pull_request", + }), + ], + ), + ).rejects.toMatchObject({ code: "23514" }); + await expect( + pool.query( + `INSERT INTO reviews + (repository_id, pr_number, head_sha, base_sha, trigger_source, trigger_context) + VALUES ($1, 7, 'malformed-evidence-head', 'base', 'requested_review', $2)`, + [ + repositoryId, + JSON.stringify({ + source: "requested_review", + webhookDeliveryId: "delivery-4", + webhookEvent: "issue_comment", + sourceCommentId: "not-a-number", + sourceUrl: "https://example.com/not-github", + }), + ], + ), + ).rejects.toMatchObject({ code: "23514" }); + await expect( + pool.query( + `INSERT INTO reviews + (repository_id, pr_number, head_sha, base_sha, trigger_source, trigger_context) + VALUES ($1, 8, 'extra-evidence-head', 'base', 'requested_review', $2)`, + [ + repositoryId, + JSON.stringify({ + source: "requested_review", + webhookDeliveryId: "delivery-5", + webhookEvent: "issue_comment", + unrelated: "not-provenance", + }), + ], + ), + ).rejects.toMatchObject({ code: "23514" }); + }); + + test("defers historical validation while enforcing constraints on new rows", async () => { + const constraints = await pool.query<{ conname: string; convalidated: boolean }>( + `SELECT conname, convalidated + FROM pg_constraint + WHERE conname IN ( + 'reviews_trigger_source_check', + 'reviews_trigger_context_check', + 'usage_events_trigger_source_check' + ) + ORDER BY conname`, + ); + expect(constraints.rows).toEqual([ + { conname: "reviews_trigger_context_check", convalidated: false }, + { conname: "reviews_trigger_source_check", convalidated: false }, + { conname: "usage_events_trigger_source_check", convalidated: false }, + ]); }); }); diff --git a/tests/review-trigger.test.ts b/tests/review-trigger.test.ts index 1bccf8ec..8420156b 100644 --- a/tests/review-trigger.test.ts +++ b/tests/review-trigger.test.ts @@ -31,6 +31,13 @@ describe("review trigger context", () => { expect(normalizeReviewTriggerContext({ source: "requested_review" })).toEqual({ source: "unknown", }); + expect( + normalizeReviewTriggerContext({ + source: "requested_review", + webhookDeliveryId: " ", + webhookEvent: "issue_comment", + }), + ).toEqual({ source: "unknown" }); expect( normalizeReviewTriggerContext({ source: "requested_review", @@ -54,5 +61,17 @@ describe("review trigger context", () => { webhookDeliveryId: "delivery-3", webhookEvent: "pull_request_review_comment", }); + expect( + normalizeReviewTriggerContext({ + source: "requested_review", + webhookDeliveryId: "delivery-4", + webhookEvent: "issue_comment", + sourceUrl: " https://github.com/octo/repo/issues/1 ", + }), + ).toEqual({ + source: "requested_review", + webhookDeliveryId: "delivery-4", + webhookEvent: "issue_comment", + }); }); });