Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion apps/dispatcher/src/check-name.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// `checkLabel` produces a distinct, separately-requirable name.

import { describe, expect, it } from "vitest";
import { checkRunNameFor } from "./check-name";
import { checkRunNameFor, checkRunTitleFor } from "./check-name";

describe("checkRunNameFor", () => {
it("names a labelless dispatch exactly as before — `flare-dispatch/<run>`", () => {
Expand Down Expand Up @@ -74,3 +74,15 @@ describe("checkRunNameFor", () => {
expect(checkRunNameFor("check", { checkLabel: label })).toBe(`flare-dispatch/check:${label}`);
});
});

describe("checkRunTitleFor", () => {
it("is the bare check name on attempt 1", () => {
expect(checkRunTitleFor("flare-dispatch/check", 1)).toBe("flare-dispatch/check");
});

it("names the attempt on a re-run", () => {
expect(checkRunTitleFor("flare-dispatch/check:codegen", 3)).toBe(
"flare-dispatch/check:codegen (attempt 3)",
);
});
});
9 changes: 9 additions & 0 deletions apps/dispatcher/src/check-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,12 @@ export const checkRunNameFor = (run: string, inputs: unknown): string => {
const label = readCheckLabel(inputs);
return label === undefined ? `flare-dispatch/${run}` : `flare-dispatch/${run}:${label}`;
};

/**
* The check-run output TITLE for an execution: the check name, plus
* ` (attempt N)` on a re-run. The NAME stays fixed across attempts — branch
* protection requires checks by name, and GitHub counts the latest check-run
* of a name — so the attempt lives in the title a reviewer reads instead.
*/
export const checkRunTitleFor = (checkRunName: string, attempt: number): string =>
attempt > 1 ? `${checkRunName} (attempt ${attempt})` : checkRunName;
49 changes: 49 additions & 0 deletions apps/dispatcher/src/executions-read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export type ExecutionRow = {
readonly input_json: string;
readonly summary_json: string | null;
readonly check_run_id: number | null;
// --- Attempt lineage (infra/migrations/0007) --------------------------------
/** 1 for a dispatched execution, N for its (N-1)th check-run re-run. */
readonly attempt: number;
/** The id of attempt 1 of this row's family; NULL on attempt 1 itself. */
readonly retry_of: string | null;
// --- Cost rollup (infra/migrations/0005; written at finishExecution) --------
// All nullable: pre-0005 rows, still-running executions, and deploys without
// the cost path leave these NULL.
Expand Down Expand Up @@ -121,6 +126,50 @@ export const listExecutions = async (
export const getExecution = async (db: D1Database, id: string): Promise<ExecutionRow | null> =>
db.prepare("SELECT * FROM executions WHERE id = ?").bind(id).first<ExecutionRow>();

/**
* The execution that posted a check-run, by the GitHub check-run id — or
* `null` when no execution on this deploy posted it. Scoped to `repo` as well:
* the repo named by the webhook is the one the re-run is authorized against,
* so a row from any other repo must not answer.
*/
export const getExecutionByCheckRun = async (
db: D1Database,
repo: string,
checkRunId: number,
): Promise<ExecutionRow | null> =>
db
.prepare("SELECT * FROM executions WHERE check_run_id = ? AND repo = ?")
.bind(checkRunId, repo)
.first<ExecutionRow>();

/**
* Every attempt of one execution family: the root (attempt 1) plus each
* re-run pointing at it through `retry_of`. Unordered; empty when the root id
* names no row.
*/
export const getAttemptFamily = async (db: D1Database, rootId: string): Promise<ExecutionRow[]> => {
const root = await getExecution(db, rootId);
if (root === null) return [];
const { results } = await db
.prepare("SELECT * FROM executions WHERE retry_of = ?")
.bind(rootId)
.all<ExecutionRow>();
return [root, ...(results ?? [])];
};

/** Every execution recorded against one commit of one repo. */
export const listExecutionsAtSha = async (
db: D1Database,
repo: string,
sha: string,
): Promise<ExecutionRow[]> => {
const { results } = await db
.prepare("SELECT * FROM executions WHERE repo = ? AND sha = ?")
.bind(repo, sha)
.all<ExecutionRow>();
return results ?? [];
};

/** Fetch an execution's steps, ordered by start time then name. */
export const getSteps = async (db: D1Database, executionId: string): Promise<StepRow[]> => {
const { results } = await db
Expand Down
9 changes: 8 additions & 1 deletion apps/dispatcher/src/instantiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
//
// Extracted from `routes/dispatch.ts` so EVERY trigger surface that ends in a
// `RUNS_WORKFLOW.create` — Action mode (`routes/dispatch.ts`), Schedule mode
// (`routes/scheduled.ts`), and signal ingress (`routes/signals-webhook.ts`) —
// (`routes/scheduled.ts`), signal ingress (`routes/signals-webhook.ts`), and
// check-run re-runs (`rerequest.ts`) —
// drives the exact same execution path:
//
// 1. receiver-level dedup short-circuit on `IDEMPOTENCY_KV` (when bound),
Expand Down Expand Up @@ -43,6 +44,10 @@ export interface InstantiateArgs {
readonly notify?: { readonly emails: readonly string[] };
/** Absolute origin so the run's artifact URLs come back absolute. */
readonly origin: string;
/** Attempt number of a check-run re-run (rerequest.ts); absent → 1. */
readonly attempt?: number;
/** The id of attempt 1 of the family a re-run retries. */
readonly retryOf?: string;
}

/** The outcome of an instantiation — the bits the 202 response carries. */
Expand Down Expand Up @@ -99,6 +104,8 @@ export const instantiateRun = async (
? { notify: { emails: args.notify.emails } }
: {}),
origin: args.origin,
...(args.attempt !== undefined ? { attempt: args.attempt } : {}),
...(args.retryOf !== undefined ? { retryOf: args.retryOf } : {}),
};

// CF Workflows rejects a `create({id})` whose id was seen before with
Expand Down
Loading
Loading