Public: anyone can read this; the crew can change it.

An issue never has two build runs at once: a second dispatch while one runs, or within 10 minutes of its pull opening, is refused #6582

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6574-an-issue-never-has-two-build-runs-at-onc → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6574-an-issue-never-has-two-build-runs-at-onc was merged into main as 105f0d7.

Review

Sign in to review this change.
2 files changed +90 −0
src/crew/dispatch.ts +36
@@ -314,6 +314,29 @@ function unstickDue(app: App, repo: Repo, issue: Issue): boolean {
314314 return unstickOwed(app.db, repo, last?.at ?? "");
315315 }
316316
317 /** How long after a crew pull opens its issue takes no second build run. */
318 export const BUILD_SETTLE_MS = 10 * 60_000;
319
320 /** The crew pull opened for the issue within `BUILD_SETTLE_MS`, if any. */
321 function freshPullFor(
322 app: App,
323 issue: Issue,
324 agent: Agent,
325 ): { number: number } | undefined {
326 const since = new Date(Date.now() - BUILD_SETTLE_MS).toISOString();
327 const name = agent.user.username.replace(/[\\%_]/g, "\\$&");
328 return (
329 app.db
330 .query<{ number: number }, [string, string, string, string]>(
331 `SELECT i.number FROM pull p JOIN issue i ON i.id = p.issue_id

Add AND p.state = 'open' (or the column the pull uses) so a closed pull does not block a re-assign; today the refusal is silent.

332 WHERE i.repo_id = ?1 AND i.created_at > ?2
333 AND (p.head_branch = ?3 OR p.head_branch LIKE ?4 ESCAPE '\\')
334 LIMIT 1`,
335 )
336 .get(issue.repoId, since, `${agent.user.username}/${issue.number}`, `${name}/${issue.number}-%`) ?? undefined
337 );
338 }
339
317340 /** Queue a run about an issue for an agent, unless one is already waiting. */
318341 export function queueAgentRun(
319342 app: App,
@@ -406,6 +429,19 @@ export function queueAgentRun(
406429 hasQueuedRun(app.db, "agent", issue.id, mine))
407430 )
408431 return undefined;
432 // One build run per issue: a pull opened a moment ago is that run's
433 // answer, and a second build beside it is the same work twice.
434 if (reason === "assigned") {
435 const fresh = freshPullFor(app, issue, agent);
436 if (fresh !== undefined) {
437 log.info("build run refused: a pull opened in the last 10 minutes", {
438 repo: `${repo.ownerName}/${repo.name}`,
439 issue: issue.number,
440 pull: fresh.number,
441 });
442 return undefined;
443 }
444 }
409445 const group = variantGroup ?? variantGroupOf(app.db, issue.id);
410446 return createRun(app.db, {
411447 repoId: repo.id,
test/build-once.test.ts added +54
@@ -0,0 +1,54 @@
1 /**
2 * An issue never has two build runs at once: a second assigned dispatch
3 * while one runs, or within 10 minutes of its pull opening, is refused (#6574).
4 */
5
6 import { afterAll, beforeAll, expect, test } from "bun:test";
7 import type { App } from "../src/app.ts";
8 import { assignIssue, openIssue } from "../src/actions/issues.ts";
9 import { getIssue, type Issue } from "../src/core/issues.ts";
10 import { listPulls } from "../src/core/pulls.ts";
11 import type { Repo } from "../src/core/repos.ts";
12 import { getAgent } from "../src/core/agents.ts";
13 import type { User } from "../src/core/users.ts";
14 import { queueAgentRun } from "../src/crew/dispatch.ts";
15 import {
16 agentRunSettled,
17 startStandInCrew,
18 stopStandInCrew,
19 type StandInCrew,
20 } from "./helpers/crew-standin.ts";
21
22 let standIn: StandInCrew;
23 let app: App;
24 let alice: User;
25 let bot: User;
26 let repo: Repo;
27 let issue: Issue;
28
29 beforeAll(async () => {
30 standIn = await startStandInCrew();
31 ({ app, alice, bot, repo } = standIn);
32 const opened = openIssue(app, alice, repo, { title: "Plant tulips", body: "Along the fence." });
33 if (!opened.ok) throw new Error("issue not opened");
34 issue = opened.value;
35 assignIssue(app, alice, repo, issue, bot);
36 const run = await agentRunSettled(app, repo, (r) => r.reason === "assigned");
37 expect(run.state).toBe("ok");
38 }, 60_000);
39
40 afterAll(() => stopStandInCrew(standIn));
41
42 test("a second build dispatch within 10 minutes of the pull opening is refused; later it starts", () => {
43 const agent = getAgent(app.db, bot.id)!;
44 const fresh = getIssue(app.db, repo.id, issue.number)!;
45 const pull = listPulls(app.db, repo.id, { state: "open" })[0]!;
46 expect(queueAgentRun(app, repo, fresh, agent, "assigned")).toBeUndefined();
47 // Other runs are unaffected by the pull's age.
48 const old = new Date(Date.now() - 11 * 60_000).toISOString();
49 app.db.query("UPDATE issue SET created_at = ?1 WHERE id = ?2").run(old, pull.id);
50 const run = queueAgentRun(app, repo, fresh, agent, "assigned");
51 expect(run?.state).toBe("queued");
52 // And now that one is queued, a third is refused.
53 expect(queueAgentRun(app, repo, fresh, agent, "assigned")).toBeUndefined();
54 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs