Public: anyone can read this; the crew can change it.
A red that main passes is the pulls' own: no sentinel issue a builder can only decline #8793
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/8786-a-red-that-main-passes-is-the-pulls-own → main 1 commit fork
5 files
changed +353
−104
src/crew/sentinel.ts +53 −13
| @@ -41,6 +41,7 @@ import { | ||
| 41 | 41 | assertionLines, |
| 42 | 42 | mainVerdict, |
| 43 | 43 | pullsFailedSince, |
| 44 | pullsOwn, | |
| 44 | 45 | SHARED_PULLS, |
| 45 | 46 | SHARED_WINDOW_MS, |
| 46 | 47 | sharedRuns, |
| @@ -804,9 +805,35 @@ function settled(db: Database, cause: string, test: string | undefined, now: num | ||
| 804 | 805 | }; |
| 805 | 806 | } |
| 806 | 807 | |
| 808 | /** A finding's cause, read back from its evidence, once its span has left the day. */ | |
| 809 | const causeOf = (evidence: readonly string[]): string | undefined => | |
| 810 | evidence.find((e) => e.startsWith("cause: "))?.slice(7); | |
| 811 | ||
| 812 | /** | |
| 813 | * Why a shared finding's test is each pull's own (#8786): main's newest | |
| 814 | * check passes it and no re-run since `since` passed it. Undefined while | |
| 815 | * the test is the lane's, or unknown. | |
| 816 | */ | |
| 817 | function ownedByPulls(db: Database, cause: string, test: string | undefined, since: number): string[] | undefined { | |
| 818 | if (!cause.startsWith("shared:") || test === undefined) return undefined; | |
| 819 | const repoId = cause.split(":")[1] ?? ""; | |
| 820 | if (!pullsOwn(db, repoId, test, since)) return undefined; | |
| 821 | const main = mainVerdict(db, repoId, test); | |
| 822 | if (main === undefined) return undefined; | |
| 823 | return [ | |
| 824 | `cause: ${cause}`, | |
| 825 | `test: ${test}`, | |
| 826 | `main: passes on main's newest check, run ${main.runId} at ${main.commitOid}`, | |
| 827 | `re-runs that passed it since ${new Date(since).toISOString()}: none`, | |
| 828 | ]; | |
| 829 | } | |
| 830 | ||
| 807 | 831 | /** |
| 808 | 832 | * A shared red that passed again on its own closes its own open issue as |
| 809 | * dropped, before a builder is handed it and finds nothing to fix (#8287). | |
| 833 | * dropped, before a builder is handed it and finds nothing to fix (#8287); | |
| 834 | * so does one the rule now calls each pull's own (#8786: main passes it and | |
| 835 | * no re-run did), saying so — its cause read from the issue's evidence when | |
| 836 | * its span has left the day. | |
| 810 | 837 | * Not one a person has spoken on: they decide. If the cause opens again, |
| 811 | 838 | * the next hunt files it fresh and quotes this one. A sentinel that may not |
| 812 | 839 | * close issues here closes nothing and says so: no access check is skipped. |
| @@ -817,8 +844,8 @@ function settled(db: Database, cause: string, test: string | undefined, now: num | ||
| 817 | 844 | */ |
| 818 | 845 | function settleShared(app: App, repo: Repo, actor: User, now: number): void { |
| 819 | 846 | const open = app.db |
| 820 | .query<{ key: string; issue_id: string; evidence: string }, []>( | |
| 821 | `SELECT t.key, t.issue_id, t.evidence FROM sentinel_finding t JOIN issue i ON i.id = t.issue_id | |
| 847 | .query<{ key: string; issue_id: string; evidence: string; first_seen: string }, []>( | |
| 848 | `SELECT t.key, t.issue_id, t.evidence, t.first_seen FROM sentinel_finding t JOIN issue i ON i.id = t.issue_id | |
| 822 | 849 | WHERE i.state = 'open' AND t.key LIKE 'degraded:shared-%'`, |
| 823 | 850 | ) |
| 824 | 851 | .all(); |
| @@ -830,10 +857,14 @@ function settleShared(app: App, repo: Repo, actor: User, now: number): void { | ||
| 830 | 857 | const causes = new Map(spans(app.db, now - DAY_MS, now).map((s) => [`degraded:${keyPart(s.cause)}`, s.cause])); |
| 831 | 858 | for (const t of open) { |
| 832 | 859 | try { |
| 833 | const cause = causes.get(t.key); | |
| 834 | if (cause === undefined) continue; | |
| 835 | const done = settled(app.db, cause, testOf(t.evidence.split("\n")), now); | |
| 836 | if (done === undefined || personSpoke(app.db, t.issue_id)) continue; | |
| 860 | const evidence = t.evidence.split("\n"); | |
| 861 | const cause = causes.get(t.key) ?? causeOf(evidence); | |
| 862 | if (cause === undefined || `degraded:${keyPart(cause)}` !== t.key) continue; | |
| 863 | const test = testOf(evidence); | |
| 864 | const firstSeen = Date.parse(t.first_seen); | |
| 865 | const owned = ownedByPulls(app.db, cause, test, (Number.isNaN(firstSeen) ? now : firstSeen) - SHARED_WINDOW_MS); | |
| 866 | const done = owned === undefined ? settled(app.db, cause, test, now) : undefined; | |
| 867 | if ((owned === undefined && done === undefined) || personSpoke(app.db, t.issue_id)) continue; | |
| 837 | 868 | const issue = getIssueById(app.db, t.issue_id); |
| 838 | 869 | if (issue === undefined || issue.repoId !== repo.id) continue; |
| 839 | 870 | const said = addComment( |
| @@ -841,12 +872,19 @@ function settleShared(app: App, repo: Repo, actor: User, now: number): void { | ||
| 841 | 872 | actor, |
| 842 | 873 | repo, |
| 843 | 874 | issue, |
| 844 | [ | |
| 845 | `Passed again on its own at ${new Date(done.at).toISOString()}; evidence:`, | |
| 846 | ...done.evidence.map((e) => `- ${e}`), | |
| 847 | "", | |
| 848 | "Closing as dropped: there is nothing to fix. If it opens again it is filed fresh, quoting this.", | |
| 849 | ].join("\n"), | |
| 875 | owned !== undefined | |
| 876 | ? [ | |
| 877 | "Each pull's own red, not the lane's (#8786): main's newest check passes this test and no re-run of a failing pull passed it, so each pull failed it for what it added. Evidence:", | |
| 878 | ...owned.map((e) => `- ${e}`), | |
| 879 | "", | |
| 880 | "Closing as dropped: there is nothing for the lane to fix. Each pull's builder gets its red as its own follow-up, with the test named.", | |
| 881 | ].join("\n") | |
| 882 | : [ | |
| 883 | `Passed again on its own at ${new Date(done!.at).toISOString()}; evidence:`, | |
| 884 | ...done!.evidence.map((e) => `- ${e}`), | |
| 885 | "", | |
| 886 | "Closing as dropped: there is nothing to fix. If it opens again it is filed fresh, quoting this.", | |
| 887 | ].join("\n"), | |
| 850 | 888 | ); |
| 851 | 889 | if (!said.ok) { |
| 852 | 890 | log.warn("sentinel could not comment", { key: t.key, error: String(said.error) }); |
| @@ -994,6 +1032,8 @@ export function hunt( | ||
| 994 | 1032 | } |
| 995 | 1033 | // A shared red that passed again on its own is no work: the day's total does not file it again. |
| 996 | 1034 | if (settled(app.db, f.cause ?? "", testFor(app.db, f), now) !== undefined) continue; |
| 1035 | // Nor is each pull's own red (#8786): its builders hold it, not the lane. | |
| 1036 | if (ownedByPulls(app.db, f.cause ?? "", testFor(app.db, f), now - SHARED_WINDOW_MS) !== undefined) continue; | |
| 997 | 1037 | const told = |
| 998 | 1038 | sinceFix(app, repo, actor, f, evidence, at, now) ?? |
| 999 | 1039 | (f.incident === undefined ? undefined : continuesDone(app, repo, actor, f, evidence, at, now)); |
new above
offline