merge-steward 0.35.2 → 0.35.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/github/pr-client.d.ts +1 -0
- package/dist/github/pr-client.js +7 -2
- package/dist/interfaces.d.ts +1 -0
- package/dist/open-pr-ancestry.d.ts +10 -0
- package/dist/open-pr-ancestry.js +14 -0
- package/dist/reconciler-merge.js +1 -0
- package/dist/reconciler-prepare.js +1 -0
- package/dist/service-queue.d.ts +15 -2
- package/dist/service-queue.js +37 -6
- package/package.json +1 -1
package/dist/github/pr-client.js
CHANGED
|
@@ -98,7 +98,7 @@ export class GitHubPRClient {
|
|
|
98
98
|
"pr", "list",
|
|
99
99
|
"--repo", this.repoFullName,
|
|
100
100
|
"--state", "open",
|
|
101
|
-
"--json", "number,headRefName,headRefOid",
|
|
101
|
+
"--json", "number,headRefName,headRefOid,baseRefName",
|
|
102
102
|
"--limit", "1000",
|
|
103
103
|
], { allowNonZero: true, githubRepoFullName: this.repoFullName });
|
|
104
104
|
if (result.exitCode !== 0) {
|
|
@@ -106,7 +106,12 @@ export class GitHubPRClient {
|
|
|
106
106
|
}
|
|
107
107
|
try {
|
|
108
108
|
const data = JSON.parse(result.stdout);
|
|
109
|
-
return data.map((pr) => ({
|
|
109
|
+
return data.map((pr) => ({
|
|
110
|
+
number: pr.number,
|
|
111
|
+
branch: pr.headRefName,
|
|
112
|
+
headSha: pr.headRefOid,
|
|
113
|
+
baseBranch: pr.baseRefName,
|
|
114
|
+
}));
|
|
110
115
|
}
|
|
111
116
|
catch (error) {
|
|
112
117
|
throw new Error("GitHub returned malformed open PR data", { cause: error });
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -13,11 +13,21 @@ export interface OpenPrAncestor {
|
|
|
13
13
|
* captures that shared history. If it is absent from current main, landing the
|
|
14
14
|
* candidate can bypass the other PR's review gates. Explicit stacks become
|
|
15
15
|
* valid only after their shared parent history has landed in main.
|
|
16
|
+
*
|
|
17
|
+
* The danger is one-directional. A stack child's candidate carries its
|
|
18
|
+
* parent's commits, which GitHub reviewed against the parent's branch rather
|
|
19
|
+
* than against main, so the child must wait. A parent's candidate carries only
|
|
20
|
+
* its own commits: the history it shares with its child is the parent's own,
|
|
21
|
+
* it is in the parent's diff, and it was reviewed there. So a child never
|
|
22
|
+
* blocks its parent, and saying so takes the declared base — after the parent
|
|
23
|
+
* advances past the commit the child sits on, ancestry alone can no longer
|
|
24
|
+
* tell a stack child from an unrelated PR holding the same unlanded history.
|
|
16
25
|
*/
|
|
17
26
|
export declare function findUnlandedOpenPrAncestors(params: {
|
|
18
27
|
github: GitHubPRApi;
|
|
19
28
|
git: GitOperations;
|
|
20
29
|
currentPrNumber: number;
|
|
30
|
+
currentBranch: string;
|
|
21
31
|
prHeadSha: string;
|
|
22
32
|
candidateSha: string;
|
|
23
33
|
baseSha: string;
|
package/dist/open-pr-ancestry.js
CHANGED
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
* captures that shared history. If it is absent from current main, landing the
|
|
7
7
|
* candidate can bypass the other PR's review gates. Explicit stacks become
|
|
8
8
|
* valid only after their shared parent history has landed in main.
|
|
9
|
+
*
|
|
10
|
+
* The danger is one-directional. A stack child's candidate carries its
|
|
11
|
+
* parent's commits, which GitHub reviewed against the parent's branch rather
|
|
12
|
+
* than against main, so the child must wait. A parent's candidate carries only
|
|
13
|
+
* its own commits: the history it shares with its child is the parent's own,
|
|
14
|
+
* it is in the parent's diff, and it was reviewed there. So a child never
|
|
15
|
+
* blocks its parent, and saying so takes the declared base — after the parent
|
|
16
|
+
* advances past the commit the child sits on, ancestry alone can no longer
|
|
17
|
+
* tell a stack child from an unrelated PR holding the same unlanded history.
|
|
9
18
|
*/
|
|
10
19
|
export async function findUnlandedOpenPrAncestors(params) {
|
|
11
20
|
const openPrs = await params.github.listOpenPRs();
|
|
@@ -13,6 +22,11 @@ export async function findUnlandedOpenPrAncestors(params) {
|
|
|
13
22
|
for (const openPr of openPrs) {
|
|
14
23
|
if (openPr.number === params.currentPrNumber)
|
|
15
24
|
continue;
|
|
25
|
+
// Declared onto this PR's branch: a child, whatever its head has done since.
|
|
26
|
+
// Both names must be present: a missing base must never read as a match,
|
|
27
|
+
// or an API that stopped returning one would disable this check in silence.
|
|
28
|
+
if (params.currentBranch !== "" && openPr.baseBranch === params.currentBranch)
|
|
29
|
+
continue;
|
|
16
30
|
if (openPr.headSha !== params.prHeadSha
|
|
17
31
|
&& await params.git.isAncestor(params.prHeadSha, openPr.headSha))
|
|
18
32
|
continue;
|
package/dist/reconciler-merge.js
CHANGED
package/dist/service-queue.d.ts
CHANGED
|
@@ -23,12 +23,25 @@ export declare class MergeStewardQueueCommands {
|
|
|
23
23
|
baseRefName?: string;
|
|
24
24
|
}): QueueEntry | undefined;
|
|
25
25
|
scanStartupAdmissions(): Promise<void>;
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* A full re-read of what is admissible.
|
|
28
|
+
*
|
|
29
|
+
* This runs both periodically and on startup. Only the startup caller asks
|
|
30
|
+
* to re-read environmental evictions, because that is when the admission
|
|
31
|
+
* policy can have changed under an entry that a previous version evicted.
|
|
32
|
+
*/
|
|
33
|
+
scanEligibleOpenPrs(options?: {
|
|
34
|
+
rereadEnvironmentalEvictions?: boolean;
|
|
35
|
+
}): Promise<{
|
|
27
36
|
scanned: number;
|
|
28
37
|
admitted: number;
|
|
29
38
|
}>;
|
|
30
39
|
updatePriorityByPR(prNumber: number, priority: number): boolean;
|
|
31
|
-
|
|
40
|
+
/** Whether this entry's eviction was about the world rather than the head. */
|
|
41
|
+
private evictedOnEnvironment;
|
|
42
|
+
tryAdmit(prNumber: number, branch: string, headSha: string, options?: {
|
|
43
|
+
rereadEnvironmentalEvictions?: boolean;
|
|
44
|
+
}): Promise<boolean>;
|
|
32
45
|
dequeueByPR(prNumber: number): void;
|
|
33
46
|
updateHeadByPR(prNumber: number, headSha: string): void;
|
|
34
47
|
acknowledgeExternalMerge(prNumber: number): Promise<void>;
|
package/dist/service-queue.js
CHANGED
|
@@ -5,6 +5,18 @@ function matchGlob(pattern, value) {
|
|
|
5
5
|
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
|
|
6
6
|
return regex.test(value);
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Evictions the same head can recover from, because what blocked it was the
|
|
10
|
+
* state around the PR rather than anything about the PR.
|
|
11
|
+
*
|
|
12
|
+
* Every other class is a fact about this head — it conflicts, its checks fail,
|
|
13
|
+
* its branch is broken — and re-admitting it unchanged would only reproduce
|
|
14
|
+
* the eviction. `policy_blocked` is the exception: it says another open PR's
|
|
15
|
+
* unlanded history is in the way, which the head cannot fix and which stops
|
|
16
|
+
* being true when that PR lands, closes, retargets, or when the admission
|
|
17
|
+
* policy itself changes. Those are re-read on the next startup scan.
|
|
18
|
+
*/
|
|
19
|
+
const ENVIRONMENTAL_FAILURES = new Set(["policy_blocked"]);
|
|
8
20
|
function getLatestEvictedEntry(entries, prNumber) {
|
|
9
21
|
const evicted = entries.filter((entry) => entry.prNumber === prNumber && entry.status === "evicted");
|
|
10
22
|
evicted.sort((left, right) => {
|
|
@@ -89,7 +101,7 @@ export class MergeStewardQueueCommands {
|
|
|
89
101
|
async scanStartupAdmissions() {
|
|
90
102
|
this.logger.info({ repoId: this.config.repoId }, "Scanning startup admissions");
|
|
91
103
|
try {
|
|
92
|
-
const { scanned, admitted } = await this.scanEligibleOpenPrs();
|
|
104
|
+
const { scanned, admitted } = await this.scanEligibleOpenPrs({ rereadEnvironmentalEvictions: true });
|
|
93
105
|
if (scanned > 0) {
|
|
94
106
|
this.logger.info({ scanned, admitted }, "Startup scan for eligible open PRs complete");
|
|
95
107
|
}
|
|
@@ -98,12 +110,20 @@ export class MergeStewardQueueCommands {
|
|
|
98
110
|
this.logger.warn({ err: error }, "Startup scan for eligible open PRs failed");
|
|
99
111
|
}
|
|
100
112
|
}
|
|
101
|
-
|
|
113
|
+
/**
|
|
114
|
+
* A full re-read of what is admissible.
|
|
115
|
+
*
|
|
116
|
+
* This runs both periodically and on startup. Only the startup caller asks
|
|
117
|
+
* to re-read environmental evictions, because that is when the admission
|
|
118
|
+
* policy can have changed under an entry that a previous version evicted.
|
|
119
|
+
*/
|
|
120
|
+
async scanEligibleOpenPrs(options) {
|
|
102
121
|
const open = await this.github.listOpenPRs();
|
|
103
122
|
let admitted = 0;
|
|
104
123
|
for (const pr of open) {
|
|
105
|
-
if (await this.tryAdmit(pr.number, pr.branch, pr.headSha))
|
|
124
|
+
if (await this.tryAdmit(pr.number, pr.branch, pr.headSha, options)) {
|
|
106
125
|
admitted += 1;
|
|
126
|
+
}
|
|
107
127
|
}
|
|
108
128
|
return { scanned: open.length, admitted };
|
|
109
129
|
}
|
|
@@ -123,7 +143,13 @@ export class MergeStewardQueueCommands {
|
|
|
123
143
|
this.logger.info({ prNumber, entryId: entry.id, priority }, "Updated queued PR priority");
|
|
124
144
|
return true;
|
|
125
145
|
}
|
|
126
|
-
|
|
146
|
+
/** Whether this entry's eviction was about the world rather than the head. */
|
|
147
|
+
evictedOnEnvironment(entry) {
|
|
148
|
+
const incidents = this.store.listIncidents(entry.id);
|
|
149
|
+
const latest = incidents[incidents.length - 1];
|
|
150
|
+
return latest !== undefined && ENVIRONMENTAL_FAILURES.has(latest.failureClass);
|
|
151
|
+
}
|
|
152
|
+
async tryAdmit(prNumber, branch, headSha, options) {
|
|
127
153
|
if (this.config.excludeBranches.some((pattern) => matchGlob(pattern, branch))) {
|
|
128
154
|
this.logger.debug({ prNumber, branch }, "Branch excluded from admission");
|
|
129
155
|
return false;
|
|
@@ -135,8 +161,13 @@ export class MergeStewardQueueCommands {
|
|
|
135
161
|
}
|
|
136
162
|
const latestEvicted = getLatestEvictedEntry(this.store.listAll(this.config.repoId), prNumber);
|
|
137
163
|
if (latestEvicted?.headSha === headSha) {
|
|
138
|
-
|
|
139
|
-
|
|
164
|
+
const rereadable = options?.rereadEnvironmentalEvictions === true
|
|
165
|
+
&& this.evictedOnEnvironment(latestEvicted);
|
|
166
|
+
if (!rereadable) {
|
|
167
|
+
this.logger.debug({ prNumber, headSha, evictedEntryId: latestEvicted.id }, "PR head matches latest evicted entry, skipping admission until a new push");
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
this.logger.info({ prNumber, headSha, evictedEntryId: latestEvicted.id }, "Re-admitting a head evicted on environment; the blocking condition is re-read from scratch");
|
|
140
171
|
}
|
|
141
172
|
try {
|
|
142
173
|
const status = await this.github.getStatus(prNumber);
|