next-issue 2.4.0 → 2.4.2
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/README.md +68 -68
- package/dist/agents.mjs +32 -31
- package/dist/args.mjs +17 -17
- package/dist/cli.mjs +16 -14
- package/dist/config.mjs +42 -42
- package/dist/exec.mjs +26 -26
- package/dist/git.mjs +40 -40
- package/dist/github.mjs +110 -79
- package/dist/interrupt.mjs +7 -7
- package/dist/loop.mjs +5 -3
- package/dist/observe.mjs +19 -19
- package/dist/pipeline.mjs +138 -116
- package/dist/progress.mjs +22 -22
- package/dist/prompts.mjs +52 -52
- package/dist/run.mjs +28 -28
- package/dist/serve.mjs +49 -45
- package/dist/setup.mjs +13 -13
- package/dist/state.mjs +20 -20
- package/dist/summary.mjs +7 -7
- package/dist/verdict.mjs +30 -25
- package/dist/version.mjs +3 -3
- package/package.json +7 -1
package/dist/github.mjs
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
import { must, run } from
|
|
1
|
+
import { must, run } from './exec.mjs';
|
|
2
2
|
const CHECK_PENDING_CODE = 8;
|
|
3
3
|
const READ_TRIES = 3;
|
|
4
4
|
const NETWORK_TRIES = 3;
|
|
5
5
|
const NETWORK_FAULT = /unexpected EOF|connection reset|connection refused|i\/o timeout|TLS handshake timeout|HTTP 50[234]/i;
|
|
6
6
|
const KNOWN_LABELS = new Map();
|
|
7
7
|
const NO_CHECKS = /no checks reported/i;
|
|
8
|
-
const FAILED_CONCLUSIONS = new Set([
|
|
8
|
+
const FAILED_CONCLUSIONS = new Set(['failure', 'timed_out', 'startup_failure']);
|
|
9
9
|
function slug(repo) {
|
|
10
10
|
return `${repo.owner}/${repo.name}`;
|
|
11
11
|
}
|
|
12
12
|
async function repeatable(args, options) {
|
|
13
13
|
for (let attempt = 1;; attempt += 1) {
|
|
14
14
|
try {
|
|
15
|
-
return await must(
|
|
15
|
+
return await must('gh', args, options);
|
|
16
16
|
}
|
|
17
17
|
catch (error) {
|
|
18
|
-
if (attempt >= NETWORK_TRIES ||
|
|
18
|
+
if (attempt >= NETWORK_TRIES ||
|
|
19
|
+
!(error instanceof Error) ||
|
|
20
|
+
!NETWORK_FAULT.test(error.message)) {
|
|
19
21
|
throw error;
|
|
20
22
|
}
|
|
21
23
|
await new Promise((resolve) => setTimeout(resolve, attempt * 1000));
|
|
@@ -23,93 +25,95 @@ async function repeatable(args, options) {
|
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
async function gh(repo, args) {
|
|
26
|
-
return repeatable([...args,
|
|
28
|
+
return repeatable([...args, '--repo', slug(repo)], { cwd: repo.root });
|
|
27
29
|
}
|
|
28
30
|
async function ghOnce(repo, args, input) {
|
|
29
|
-
return must(
|
|
31
|
+
return must('gh', [...args, '--repo', slug(repo)], { cwd: repo.root, input });
|
|
30
32
|
}
|
|
31
33
|
export async function defaultBranch(repo) {
|
|
32
|
-
const raw = await repeatable([
|
|
34
|
+
const raw = await repeatable(['repo', 'view', slug(repo), '--json', 'defaultBranchRef'], {
|
|
33
35
|
cwd: repo.root,
|
|
34
36
|
});
|
|
35
37
|
return JSON.parse(raw).defaultBranchRef.name;
|
|
36
38
|
}
|
|
37
|
-
const ISSUE_FIELDS =
|
|
39
|
+
const ISSUE_FIELDS = 'number,title,body,createdAt,labels';
|
|
38
40
|
function toIssue(item) {
|
|
39
41
|
return {
|
|
40
42
|
number: item.number,
|
|
41
43
|
title: item.title,
|
|
42
|
-
body: item.body ??
|
|
44
|
+
body: item.body ?? '',
|
|
43
45
|
createdAt: item.createdAt,
|
|
44
46
|
labels: item.labels.map((label) => label.name),
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
|
-
export async function openIssues(repo, limit) {
|
|
49
|
+
export async function openIssues(repo, limit, labels) {
|
|
50
|
+
const filter = labels.length === 0 ? '' : ` label:${labels.map((label) => `"${label}"`).join(',')}`;
|
|
48
51
|
const raw = await gh(repo, [
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
'issue',
|
|
53
|
+
'list',
|
|
54
|
+
'--state',
|
|
55
|
+
'open',
|
|
56
|
+
'--search',
|
|
57
|
+
`sort:created-asc${filter}`,
|
|
58
|
+
'--limit',
|
|
56
59
|
String(limit),
|
|
57
|
-
|
|
60
|
+
'--json',
|
|
58
61
|
ISSUE_FIELDS,
|
|
59
62
|
]);
|
|
60
63
|
const parsed = JSON.parse(raw);
|
|
61
64
|
return parsed.map(toIssue).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
62
65
|
}
|
|
63
66
|
export async function viewIssue(repo, issue) {
|
|
64
|
-
const raw = await gh(repo, [
|
|
65
|
-
|
|
67
|
+
const raw = await gh(repo, ['issue', 'view', String(issue), '--json', `${ISSUE_FIELDS},state`]);
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
return { ...toIssue(parsed), open: parsed.state === 'OPEN' };
|
|
66
70
|
}
|
|
67
71
|
export async function issueComments(repo, issue) {
|
|
68
|
-
const raw = await gh(repo, [
|
|
72
|
+
const raw = await gh(repo, ['issue', 'view', String(issue), '--json', 'comments']);
|
|
69
73
|
const parsed = JSON.parse(raw);
|
|
70
74
|
return parsed.comments.map((comment) => `@${comment.author.login}: ${comment.body}`);
|
|
71
75
|
}
|
|
72
76
|
export async function openBlockers(repo, issue) {
|
|
73
77
|
const raw = await repeatable([
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
'api',
|
|
79
|
+
'--paginate',
|
|
76
80
|
`repos/${slug(repo)}/issues/${issue}/dependencies/blocked_by`,
|
|
77
|
-
|
|
81
|
+
'--jq',
|
|
78
82
|
'.[] | select(.state == "open") | (.repository_url | split("/") | .[-2:] | join("/")) + "#" + (.number | tostring)',
|
|
79
83
|
], { cwd: repo.root });
|
|
80
84
|
return raw
|
|
81
|
-
.split(
|
|
85
|
+
.split('\n')
|
|
82
86
|
.filter((line) => line.length > 0)
|
|
83
87
|
.map((ref) => (ref.startsWith(`${slug(repo)}#`) ? ref.slice(slug(repo).length) : ref));
|
|
84
88
|
}
|
|
85
89
|
async function labelsOf(repo, kind, number) {
|
|
86
|
-
const raw = await gh(repo, [kind,
|
|
90
|
+
const raw = await gh(repo, [kind, 'view', String(number), '--json', 'labels']);
|
|
87
91
|
const parsed = JSON.parse(raw);
|
|
88
92
|
return parsed.labels.map((label) => label.name);
|
|
89
93
|
}
|
|
90
94
|
export function labelArgs(add, remove, current) {
|
|
91
|
-
const args = [
|
|
95
|
+
const args = ['--add-label', add];
|
|
92
96
|
for (const name of remove.filter((item) => item !== add && current.includes(item))) {
|
|
93
|
-
args.push(
|
|
97
|
+
args.push('--remove-label', name);
|
|
94
98
|
}
|
|
95
99
|
return args;
|
|
96
100
|
}
|
|
97
101
|
export async function setLabel(repo, kind, number, add, remove) {
|
|
98
102
|
await ensureLabel(repo, add);
|
|
99
103
|
const current = await labelsOf(repo, kind, number);
|
|
100
|
-
await gh(repo, [kind,
|
|
104
|
+
await gh(repo, [kind, 'edit', String(number), ...labelArgs(add, remove, current)]);
|
|
101
105
|
}
|
|
102
106
|
async function ensureLabel(repo, label) {
|
|
103
107
|
let known = KNOWN_LABELS.get(slug(repo));
|
|
104
108
|
if (known === undefined) {
|
|
105
|
-
const raw = await gh(repo, [
|
|
109
|
+
const raw = await gh(repo, ['label', 'list', '--limit', '1000', '--json', 'name']);
|
|
106
110
|
known = new Set(JSON.parse(raw).map((item) => item.name));
|
|
107
111
|
KNOWN_LABELS.set(slug(repo), known);
|
|
108
112
|
}
|
|
109
113
|
if (known.has(label)) {
|
|
110
114
|
return;
|
|
111
115
|
}
|
|
112
|
-
const result = await run(
|
|
116
|
+
const result = await run('gh', ['label', 'create', label, '--repo', slug(repo)], {
|
|
113
117
|
cwd: repo.root,
|
|
114
118
|
});
|
|
115
119
|
if (result.code === 0) {
|
|
@@ -117,23 +121,32 @@ async function ensureLabel(repo, label) {
|
|
|
117
121
|
}
|
|
118
122
|
}
|
|
119
123
|
export async function findPr(repo, branch) {
|
|
120
|
-
const raw = await gh(repo, [
|
|
124
|
+
const raw = await gh(repo, [
|
|
125
|
+
'pr',
|
|
126
|
+
'list',
|
|
127
|
+
'--head',
|
|
128
|
+
branch,
|
|
129
|
+
'--state',
|
|
130
|
+
'open',
|
|
131
|
+
'--json',
|
|
132
|
+
'number',
|
|
133
|
+
]);
|
|
121
134
|
const parsed = JSON.parse(raw);
|
|
122
135
|
return parsed[0]?.number;
|
|
123
136
|
}
|
|
124
137
|
export async function createPr(repo, branch, base, title, body, draft) {
|
|
125
138
|
const created = await ghOnce(repo, [
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
'pr',
|
|
140
|
+
'create',
|
|
141
|
+
'--head',
|
|
129
142
|
branch,
|
|
130
|
-
|
|
143
|
+
'--base',
|
|
131
144
|
base,
|
|
132
|
-
|
|
145
|
+
'--title',
|
|
133
146
|
title,
|
|
134
|
-
|
|
147
|
+
'--body',
|
|
135
148
|
body,
|
|
136
|
-
...(draft ? [
|
|
149
|
+
...(draft ? ['--draft'] : []),
|
|
137
150
|
]);
|
|
138
151
|
const number = prNumber(created) ?? (await findPr(repo, branch));
|
|
139
152
|
if (number === undefined) {
|
|
@@ -146,34 +159,44 @@ export function prNumber(output) {
|
|
|
146
159
|
return match === null ? undefined : Number(match[1]);
|
|
147
160
|
}
|
|
148
161
|
async function prHead(repo, pr) {
|
|
149
|
-
return gh(repo, [
|
|
162
|
+
return gh(repo, ['pr', 'view', String(pr), '--json', 'headRefOid', '--jq', '.headRefOid']);
|
|
150
163
|
}
|
|
151
164
|
async function readChecks(repo, pr) {
|
|
152
|
-
const result = await run(
|
|
165
|
+
const result = await run('gh', ['pr', 'checks', String(pr), '--json', 'name,bucket,link,description', '--repo', slug(repo)], { cwd: repo.root });
|
|
153
166
|
const output = result.stdout.trim();
|
|
154
167
|
if (output.length === 0) {
|
|
155
168
|
if (NO_CHECKS.test(result.stderr)) {
|
|
156
|
-
return
|
|
169
|
+
return 'none';
|
|
157
170
|
}
|
|
158
171
|
throw new Error(`gh pr checks ${pr} failed with code ${result.code}: ${result.stderr.trim()}`);
|
|
159
172
|
}
|
|
160
173
|
const checks = JSON.parse(output);
|
|
161
|
-
return checks.length === 0 ?
|
|
174
|
+
return checks.length === 0 ? 'none' : checks;
|
|
162
175
|
}
|
|
163
176
|
async function watchOnce(repo, pr, intervalSeconds, timeoutMs, show) {
|
|
164
|
-
const watch = await run(
|
|
177
|
+
const watch = await run('gh', [
|
|
178
|
+
'pr',
|
|
179
|
+
'checks',
|
|
180
|
+
String(pr),
|
|
181
|
+
'--watch',
|
|
182
|
+
'--fail-fast',
|
|
183
|
+
'--interval',
|
|
184
|
+
String(intervalSeconds),
|
|
185
|
+
'--repo',
|
|
186
|
+
slug(repo),
|
|
187
|
+
], { cwd: repo.root, show, timeoutMs });
|
|
165
188
|
if (watch.timedOut) {
|
|
166
|
-
return
|
|
189
|
+
return 'timeout';
|
|
167
190
|
}
|
|
168
191
|
if (watch.code === 0) {
|
|
169
|
-
return
|
|
192
|
+
return 'pass';
|
|
170
193
|
}
|
|
171
194
|
if (watch.code === CHECK_PENDING_CODE) {
|
|
172
|
-
return
|
|
195
|
+
return 'pending';
|
|
173
196
|
}
|
|
174
197
|
const checks = await readChecks(repo, pr);
|
|
175
|
-
if (checks !==
|
|
176
|
-
return
|
|
198
|
+
if (checks !== 'none' && checks.some((check) => check.bucket === 'fail')) {
|
|
199
|
+
return 'fail';
|
|
177
200
|
}
|
|
178
201
|
throw new Error(`gh pr checks ${pr} --watch failed with code ${watch.code} and no check failed: ${watch.stderr.trim()}`);
|
|
179
202
|
}
|
|
@@ -188,7 +211,7 @@ export async function pollChecks(probe, options, clock = REAL_CLOCK) {
|
|
|
188
211
|
let failures = 0;
|
|
189
212
|
for (;;) {
|
|
190
213
|
if (rest() === 0) {
|
|
191
|
-
return
|
|
214
|
+
return 'timeout';
|
|
192
215
|
}
|
|
193
216
|
const state = await read(probe, options.head, rest(), () => (shown ??= clock.now()));
|
|
194
217
|
if (state instanceof Error) {
|
|
@@ -199,10 +222,10 @@ export async function pollChecks(probe, options, clock = REAL_CLOCK) {
|
|
|
199
222
|
}
|
|
200
223
|
else {
|
|
201
224
|
failures = 0;
|
|
202
|
-
if (state ===
|
|
203
|
-
return
|
|
225
|
+
if (state === 'none' && clock.now() - (shown ?? 0) >= options.graceMs) {
|
|
226
|
+
return 'none';
|
|
204
227
|
}
|
|
205
|
-
if (state !==
|
|
228
|
+
if (state !== 'none' && state !== 'behind' && state !== 'pending') {
|
|
206
229
|
return state;
|
|
207
230
|
}
|
|
208
231
|
}
|
|
@@ -212,11 +235,11 @@ export async function pollChecks(probe, options, clock = REAL_CLOCK) {
|
|
|
212
235
|
async function read(probe, head, timeoutMs, shown) {
|
|
213
236
|
try {
|
|
214
237
|
if (head !== undefined && (await probe.head()) !== head) {
|
|
215
|
-
return
|
|
238
|
+
return 'behind';
|
|
216
239
|
}
|
|
217
240
|
shown();
|
|
218
|
-
if ((await probe.list()) ===
|
|
219
|
-
return
|
|
241
|
+
if ((await probe.list()) === 'none') {
|
|
242
|
+
return 'none';
|
|
220
243
|
}
|
|
221
244
|
return await probe.watch(timeoutMs);
|
|
222
245
|
}
|
|
@@ -227,17 +250,17 @@ async function read(probe, head, timeoutMs, shown) {
|
|
|
227
250
|
export function waitForChecks(repo, pr, options) {
|
|
228
251
|
return pollChecks({
|
|
229
252
|
head: () => prHead(repo, pr),
|
|
230
|
-
list: async () => ((await readChecks(repo, pr)) ===
|
|
253
|
+
list: async () => ((await readChecks(repo, pr)) === 'none' ? 'none' : 'some'),
|
|
231
254
|
watch: (timeoutMs) => watchOnce(repo, pr, options.intervalSeconds, timeoutMs, options.show),
|
|
232
255
|
}, options);
|
|
233
256
|
}
|
|
234
257
|
export async function failedCheckLogs(repo, pr, maxChars) {
|
|
235
258
|
const checks = await readChecks(repo, pr);
|
|
236
|
-
if (checks ===
|
|
237
|
-
return
|
|
259
|
+
if (checks === 'none') {
|
|
260
|
+
return 'The checks failed, but gh reports no check for the pull request.';
|
|
238
261
|
}
|
|
239
262
|
const failed = checks
|
|
240
|
-
.filter((check) => check.bucket ===
|
|
263
|
+
.filter((check) => check.bucket === 'fail')
|
|
241
264
|
.map((check) => ({
|
|
242
265
|
heading: `## ${check.name}\n${check.description}`,
|
|
243
266
|
runId: /\/actions\/runs\/(\d+)/.exec(check.link)?.[1],
|
|
@@ -245,25 +268,32 @@ export async function failedCheckLogs(repo, pr, maxChars) {
|
|
|
245
268
|
return runLogs(repo, failed, maxChars);
|
|
246
269
|
}
|
|
247
270
|
async function baseRuns(repo, sha) {
|
|
248
|
-
const raw = await gh(repo, [
|
|
271
|
+
const raw = await gh(repo, [
|
|
272
|
+
'run',
|
|
273
|
+
'list',
|
|
274
|
+
'--commit',
|
|
275
|
+
sha,
|
|
276
|
+
'--json',
|
|
277
|
+
'databaseId,name,status,conclusion',
|
|
278
|
+
]);
|
|
249
279
|
return JSON.parse(raw);
|
|
250
280
|
}
|
|
251
281
|
function baseState(runs) {
|
|
252
|
-
if (runs.some((entry) => entry.status ===
|
|
253
|
-
return
|
|
282
|
+
if (runs.some((entry) => entry.status === 'completed' && FAILED_CONCLUSIONS.has(entry.conclusion))) {
|
|
283
|
+
return 'fail';
|
|
254
284
|
}
|
|
255
|
-
return runs.every((entry) => entry.status ===
|
|
285
|
+
return runs.every((entry) => entry.status === 'completed') ? 'pass' : 'pending';
|
|
256
286
|
}
|
|
257
287
|
export function waitForBaseChecks(repo, sha, options) {
|
|
258
288
|
return pollChecks({
|
|
259
289
|
head: async () => sha,
|
|
260
|
-
list: async () => ((await baseRuns(repo, sha)).length === 0 ?
|
|
290
|
+
list: async () => ((await baseRuns(repo, sha)).length === 0 ? 'none' : 'some'),
|
|
261
291
|
watch: async () => baseState(await baseRuns(repo, sha)),
|
|
262
292
|
}, options);
|
|
263
293
|
}
|
|
264
294
|
export async function failedBaseLogs(repo, sha, maxChars) {
|
|
265
295
|
const failed = (await baseRuns(repo, sha))
|
|
266
|
-
.filter((entry) => entry.status ===
|
|
296
|
+
.filter((entry) => entry.status === 'completed' && FAILED_CONCLUSIONS.has(entry.conclusion))
|
|
267
297
|
.map((entry) => ({ heading: `## ${entry.name}`, runId: String(entry.databaseId) }));
|
|
268
298
|
return runLogs(repo, failed, maxChars);
|
|
269
299
|
}
|
|
@@ -275,7 +305,7 @@ async function runLogs(repo, failed, maxChars) {
|
|
|
275
305
|
if (runId === undefined) {
|
|
276
306
|
continue;
|
|
277
307
|
}
|
|
278
|
-
const logs = await run(
|
|
308
|
+
const logs = await run('gh', ['run', 'view', runId, '--log-failed', '--repo', slug(repo)], {
|
|
279
309
|
cwd: repo.root,
|
|
280
310
|
tailChars: share,
|
|
281
311
|
});
|
|
@@ -284,43 +314,44 @@ async function runLogs(repo, failed, maxChars) {
|
|
|
284
314
|
continue;
|
|
285
315
|
}
|
|
286
316
|
const reason = [`The log read failed with code ${logs.code}.`, logs.stderr.trim()]
|
|
287
|
-
.filter((line) => line !==
|
|
288
|
-
.join(
|
|
289
|
-
parts.push([logs.stdout.trim(), reason].filter((line) => line !==
|
|
317
|
+
.filter((line) => line !== '')
|
|
318
|
+
.join(' ');
|
|
319
|
+
parts.push([logs.stdout.trim(), reason].filter((line) => line !== '').join('\n'));
|
|
290
320
|
}
|
|
291
|
-
return parts.join(
|
|
321
|
+
return parts.join('\n\n');
|
|
292
322
|
}
|
|
293
323
|
export async function markPrReady(repo, pr) {
|
|
294
|
-
const result = await run(
|
|
324
|
+
const result = await run('gh', ['pr', 'ready', String(pr), '--repo', slug(repo)], {
|
|
295
325
|
cwd: repo.root,
|
|
296
326
|
});
|
|
297
327
|
return result.code === 0;
|
|
298
328
|
}
|
|
299
329
|
export async function mergePr(repo, pr) {
|
|
300
|
-
const result = await run(
|
|
330
|
+
const result = await run('gh', ['pr', 'merge', String(pr), '--squash', '--delete-branch', '--repo', slug(repo)], { cwd: repo.root });
|
|
301
331
|
return result.code === 0;
|
|
302
332
|
}
|
|
303
333
|
export async function prMerged(repo, pr) {
|
|
304
|
-
return (await gh(repo, [
|
|
334
|
+
return ((await gh(repo, ['pr', 'view', String(pr), '--json', 'state', '--jq', '.state'])).trim() ===
|
|
335
|
+
'MERGED');
|
|
305
336
|
}
|
|
306
337
|
export async function mergeCommit(repo, pr, options, clock = REAL_CLOCK) {
|
|
307
338
|
const deadline = clock.now() + options.timeoutMs;
|
|
308
339
|
for (;;) {
|
|
309
|
-
const raw = await gh(repo, [
|
|
340
|
+
const raw = await gh(repo, ['pr', 'view', String(pr), '--json', 'state,mergeCommit']);
|
|
310
341
|
const view = JSON.parse(raw);
|
|
311
|
-
if (view.state ===
|
|
342
|
+
if (view.state === 'MERGED' && view.mergeCommit?.oid) {
|
|
312
343
|
return view.mergeCommit.oid;
|
|
313
344
|
}
|
|
314
345
|
const rest = deadline - clock.now();
|
|
315
|
-
if (view.state ===
|
|
346
|
+
if (view.state === 'CLOSED' || rest <= 0) {
|
|
316
347
|
return undefined;
|
|
317
348
|
}
|
|
318
349
|
await clock.sleep(Math.min(options.intervalSeconds * 1000, rest));
|
|
319
350
|
}
|
|
320
351
|
}
|
|
321
352
|
export async function reopenIssue(repo, issue) {
|
|
322
|
-
await gh(repo, [
|
|
353
|
+
await gh(repo, ['issue', 'reopen', String(issue)]);
|
|
323
354
|
}
|
|
324
355
|
export async function commentOnPr(repo, pr, body) {
|
|
325
|
-
await ghOnce(repo, [
|
|
356
|
+
await ghOnce(repo, ['pr', 'comment', String(pr), '--body-file', '-'], body);
|
|
326
357
|
}
|
package/dist/interrupt.mjs
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { stopChildren } from
|
|
1
|
+
import { stopChildren } from './exec.mjs';
|
|
2
2
|
export function listenForInterrupts(say) {
|
|
3
3
|
let presses = 0;
|
|
4
4
|
let wake;
|
|
5
5
|
const press = () => {
|
|
6
6
|
presses += 1;
|
|
7
7
|
if (presses === 1 && wake !== undefined) {
|
|
8
|
-
say(
|
|
8
|
+
say('Stopping now.');
|
|
9
9
|
wake();
|
|
10
10
|
}
|
|
11
11
|
else if (presses === 1) {
|
|
12
|
-
say(
|
|
12
|
+
say('Stopping after this issue. Press Ctrl-C again to stop after this round.');
|
|
13
13
|
}
|
|
14
14
|
else if (presses === 2) {
|
|
15
|
-
say(
|
|
15
|
+
say('Stopping after this round. Press Ctrl-C again to stop now.');
|
|
16
16
|
}
|
|
17
17
|
else {
|
|
18
|
-
process.off(
|
|
19
|
-
stopChildren(
|
|
18
|
+
process.off('SIGINT', press);
|
|
19
|
+
stopChildren('SIGINT');
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
|
-
process.on(
|
|
22
|
+
process.on('SIGINT', press);
|
|
23
23
|
return {
|
|
24
24
|
afterIssue: () => presses >= 1,
|
|
25
25
|
afterRound: () => presses >= 2,
|
package/dist/loop.mjs
CHANGED
|
@@ -20,7 +20,7 @@ export async function runIssues(args, loop) {
|
|
|
20
20
|
if (loop.stop() || report.halt === true) {
|
|
21
21
|
return { reports, stoppedAt: issue.number };
|
|
22
22
|
}
|
|
23
|
-
if (args.once && report.outcome !==
|
|
23
|
+
if (args.once && report.outcome !== 'skipped') {
|
|
24
24
|
break;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -51,8 +51,10 @@ export async function watchIssues(args, watch) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
export function exitCode(reports) {
|
|
54
|
-
return reports.some((report) => report.outcome ===
|
|
54
|
+
return reports.some((report) => report.outcome === 'needs-human' || report.outcome === 'error')
|
|
55
|
+
? 1
|
|
56
|
+
: 0;
|
|
55
57
|
}
|
|
56
58
|
function handled(reports) {
|
|
57
|
-
return reports.filter((report) => report.outcome !==
|
|
59
|
+
return reports.filter((report) => report.outcome !== 'skipped').length;
|
|
58
60
|
}
|
package/dist/observe.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createWriteStream } from
|
|
2
|
-
import { mkdir, readdir, rm, stat } from
|
|
3
|
-
import { join } from
|
|
4
|
-
import { STATE_DIR } from
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { mkdir, readdir, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { STATE_DIR } from './state.mjs';
|
|
5
5
|
const VISIBILITY = { quiet: 0, normal: 1, verbose: 2 };
|
|
6
6
|
export function emptyUsage() {
|
|
7
7
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
@@ -63,11 +63,11 @@ export class Recorder {
|
|
|
63
63
|
this.runId = runId;
|
|
64
64
|
}
|
|
65
65
|
static async create(root, level, listener) {
|
|
66
|
-
const runId = new Date().toISOString().replace(/[:.]/g,
|
|
67
|
-
const dir = join(root, STATE_DIR,
|
|
66
|
+
const runId = new Date().toISOString().replace(/[:.]/g, '-');
|
|
67
|
+
const dir = join(root, STATE_DIR, 'runs');
|
|
68
68
|
await mkdir(dir, { recursive: true });
|
|
69
69
|
const file = join(dir, `${runId}.jsonl`);
|
|
70
|
-
const sink = new Sink(createWriteStream(file, { flags:
|
|
70
|
+
const sink = new Sink(createWriteStream(file, { flags: 'a' }), level, file, listener);
|
|
71
71
|
return new Recorder(sink, {}, runId);
|
|
72
72
|
}
|
|
73
73
|
get logFile() {
|
|
@@ -79,7 +79,7 @@ export class Recorder {
|
|
|
79
79
|
scope(fields) {
|
|
80
80
|
return new Recorder(this.sink, { ...this.base, ...fields }, this.runId);
|
|
81
81
|
}
|
|
82
|
-
event(kind, fields = {}, visibility =
|
|
82
|
+
event(kind, fields = {}, visibility = 'normal') {
|
|
83
83
|
this.sink.count(kind);
|
|
84
84
|
const record = {
|
|
85
85
|
ts: new Date().toISOString(),
|
|
@@ -95,18 +95,18 @@ export class Recorder {
|
|
|
95
95
|
}
|
|
96
96
|
usage(role, usage, ms, fields = {}) {
|
|
97
97
|
this.sink.add(role, usage, ms);
|
|
98
|
-
this.event(
|
|
98
|
+
this.event('usage', { role, ms, ...usage, ...fields }, 'normal');
|
|
99
99
|
}
|
|
100
100
|
async step(kind, fields, run) {
|
|
101
101
|
const started = Date.now();
|
|
102
|
-
this.event(`${kind}.start`, fields,
|
|
102
|
+
this.event(`${kind}.start`, fields, 'verbose');
|
|
103
103
|
try {
|
|
104
104
|
const result = await run();
|
|
105
105
|
this.event(`${kind}.end`, { ...fields, ms: Date.now() - started });
|
|
106
106
|
return result;
|
|
107
107
|
}
|
|
108
108
|
catch (error) {
|
|
109
|
-
this.event(`${kind}.error`, { ...fields, ms: Date.now() - started, error: message(error) },
|
|
109
|
+
this.event(`${kind}.error`, { ...fields, ms: Date.now() - started, error: message(error) }, 'quiet');
|
|
110
110
|
throw error;
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -140,7 +140,7 @@ export async function pruneRuns(root, keepDays) {
|
|
|
140
140
|
if (keepDays <= 0) {
|
|
141
141
|
return 0;
|
|
142
142
|
}
|
|
143
|
-
const dir = join(root, STATE_DIR,
|
|
143
|
+
const dir = join(root, STATE_DIR, 'runs');
|
|
144
144
|
let names;
|
|
145
145
|
try {
|
|
146
146
|
names = await readdir(dir);
|
|
@@ -170,20 +170,20 @@ export function message(error) {
|
|
|
170
170
|
}
|
|
171
171
|
function render(record) {
|
|
172
172
|
const time = new Date(String(record.ts)).toTimeString().slice(0, 8);
|
|
173
|
-
const issue = record.issue === undefined ?
|
|
173
|
+
const issue = record.issue === undefined ? '' : ` #${record.issue}`;
|
|
174
174
|
const extras = Object.entries(record)
|
|
175
|
-
.filter(([key]) => ![
|
|
175
|
+
.filter(([key]) => !['ts', 'runId', 'kind', 'issue'].includes(key))
|
|
176
176
|
.map(([key, value]) => `${key}=${short(value)}`)
|
|
177
|
-
.join(
|
|
178
|
-
return `${time}${issue} ${String(record.kind)}${extras.length > 0 ? ` ${extras}` :
|
|
177
|
+
.join(' ');
|
|
178
|
+
return `${time}${issue} ${String(record.kind)}${extras.length > 0 ? ` ${extras}` : ''}\n`;
|
|
179
179
|
}
|
|
180
180
|
function short(value) {
|
|
181
181
|
if (value === undefined || value === null) {
|
|
182
|
-
return
|
|
182
|
+
return '-';
|
|
183
183
|
}
|
|
184
|
-
if (typeof value ===
|
|
184
|
+
if (typeof value === 'number') {
|
|
185
185
|
return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(6)));
|
|
186
186
|
}
|
|
187
|
-
const text = String(value).replace(/\s+/g,
|
|
187
|
+
const text = String(value).replace(/\s+/g, ' ');
|
|
188
188
|
return text.length > 120 ? `${text.slice(0, 117)}...` : text;
|
|
189
189
|
}
|