@h-rig/standard-plugin 0.0.6-alpha.15 → 0.0.6-alpha.151
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/src/blocker-classifier.d.ts +1 -0
- package/dist/src/blocker-classifier.js +18 -0
- package/dist/src/bundle.d.ts +7 -0
- package/dist/src/bundle.js +1859 -0
- package/dist/src/cli-surface.d.ts +1 -0
- package/dist/src/cli-surface.js +12 -0
- package/dist/src/default-lifecycle.d.ts +2 -0
- package/dist/src/default-lifecycle.js +12 -0
- package/dist/src/dependency-graph.d.ts +1 -0
- package/dist/src/dependency-graph.js +22 -0
- package/dist/src/drift/__fixtures__/temp-repo.d.ts +9 -0
- package/dist/src/drift/__fixtures__/temp-repo.js +41 -0
- package/dist/src/drift/detect.d.ts +11 -0
- package/dist/src/drift/detect.js +299 -0
- package/dist/src/drift/extract-refs.d.ts +7 -0
- package/dist/src/drift/extract-refs.js +60 -0
- package/dist/src/drift/git-adapter.d.ts +7 -0
- package/dist/src/drift/git-adapter.js +63 -0
- package/dist/src/drift/judge.d.ts +19 -0
- package/dist/src/drift/judge.js +16 -0
- package/dist/src/drift/metadata.d.ts +13 -0
- package/dist/src/drift/metadata.js +33 -0
- package/dist/src/drift/plugin.d.ts +53 -0
- package/dist/src/drift/plugin.js +507 -0
- package/dist/src/files-source.d.ts +18 -0
- package/dist/src/files-source.js +4 -3
- package/dist/src/github-issues-source.d.ts +80 -0
- package/dist/src/github-issues-source.js +482 -53
- package/dist/src/index.d.ts +13 -0
- package/dist/src/index.js +1369 -68
- package/dist/src/lifecycle-closeout.d.ts +2 -0
- package/dist/src/lifecycle-closeout.js +6 -0
- package/dist/src/planning.d.ts +1 -0
- package/dist/src/planning.js +14 -0
- package/dist/src/plugin.d.ts +24 -0
- package/dist/src/plugin.js +1814 -0
- package/dist/src/product-plugin.d.ts +3 -0
- package/dist/src/product-plugin.js +18 -0
- package/dist/src/run-worker-panels.d.ts +15 -0
- package/dist/src/run-worker-panels.js +53 -0
- package/dist/src/supervisor.d.ts +1 -0
- package/dist/src/supervisor.js +12 -0
- package/dist/src/task-cli.d.ts +1 -0
- package/dist/src/task-cli.js +14 -0
- package/package.json +67 -5
|
@@ -13,7 +13,7 @@ function createEnvGitHubCredentialProvider() {
|
|
|
13
13
|
if (input.purpose === "selected-repo") {
|
|
14
14
|
return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
15
15
|
}
|
|
16
|
-
const token = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
16
|
+
const token = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
17
17
|
if (!token) {
|
|
18
18
|
throw new Error("No host GitHub token is configured for admin fallback.");
|
|
19
19
|
}
|
|
@@ -22,34 +22,60 @@ function createEnvGitHubCredentialProvider() {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
function createStateGitHubCredentialProvider(options = {}) {
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
return
|
|
29
|
-
const
|
|
30
|
-
|
|
25
|
+
const addCandidate = (candidates, path) => {
|
|
26
|
+
const trimmed = path?.trim();
|
|
27
|
+
if (!trimmed)
|
|
28
|
+
return;
|
|
29
|
+
const resolved = resolve(trimmed);
|
|
30
|
+
if (!candidates.includes(resolved))
|
|
31
|
+
candidates.push(resolved);
|
|
32
|
+
};
|
|
33
|
+
const addStateDir = (candidates, dir) => {
|
|
34
|
+
const trimmed = dir?.trim();
|
|
35
|
+
if (!trimmed)
|
|
36
|
+
return;
|
|
37
|
+
addCandidate(candidates, resolve(trimmed, "github-auth.json"));
|
|
38
|
+
};
|
|
39
|
+
const addProjectStateDir = (candidates, root) => {
|
|
40
|
+
const trimmed = root?.trim();
|
|
41
|
+
if (!trimmed)
|
|
42
|
+
return;
|
|
43
|
+
addStateDir(candidates, resolve(trimmed, ".rig", "state"));
|
|
44
|
+
};
|
|
45
|
+
const stateFileCandidates = () => {
|
|
46
|
+
const candidates = [];
|
|
47
|
+
addCandidate(candidates, options.stateFile ?? process.env.RIG_GITHUB_AUTH_STATE_FILE);
|
|
48
|
+
addStateDir(candidates, options.stateDir);
|
|
49
|
+
addStateDir(candidates, process.env.RIG_STATE_DIR);
|
|
50
|
+
addProjectStateDir(candidates, process.env.PROJECT_RIG_ROOT);
|
|
51
|
+
addProjectStateDir(candidates, process.env.RIG_PROJECT_ROOT);
|
|
52
|
+
addProjectStateDir(candidates, process.env.RIG_HOST_PROJECT_ROOT);
|
|
53
|
+
addProjectStateDir(candidates, process.cwd());
|
|
54
|
+
return candidates;
|
|
31
55
|
};
|
|
32
56
|
const readToken = () => {
|
|
33
|
-
const stateFile
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
for (const stateFile of stateFileCandidates()) {
|
|
58
|
+
if (!existsSync(stateFile))
|
|
59
|
+
continue;
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
62
|
+
const token = typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
|
|
63
|
+
if (token)
|
|
64
|
+
return token;
|
|
65
|
+
} catch {}
|
|
41
66
|
}
|
|
67
|
+
return null;
|
|
42
68
|
};
|
|
43
69
|
return {
|
|
44
70
|
async resolveGitHubToken(input) {
|
|
45
71
|
const token = readToken();
|
|
46
72
|
if (input.purpose === "selected-repo") {
|
|
47
|
-
return { token: token ?? "", source: "signed-in-user" };
|
|
73
|
+
return { token: token ?? cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
48
74
|
}
|
|
49
75
|
if (token) {
|
|
50
76
|
return { token, source: "signed-in-user" };
|
|
51
77
|
}
|
|
52
|
-
const fallback = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
78
|
+
const fallback = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
53
79
|
if (!fallback) {
|
|
54
80
|
throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
|
|
55
81
|
}
|
|
@@ -83,17 +109,57 @@ function statusFor(issue) {
|
|
|
83
109
|
return "cancelled";
|
|
84
110
|
return "open";
|
|
85
111
|
}
|
|
86
|
-
function
|
|
87
|
-
const
|
|
88
|
-
|
|
112
|
+
function parseIssueRefs(raw) {
|
|
113
|
+
const refs = [...raw.matchAll(/(?:^|[^\w/.-])(?:[\w.-]+\/[\w.-]+#|#)?(\d+)\b/g)].map((match) => match[1]).filter((value) => Boolean(value));
|
|
114
|
+
return [...new Set(refs)];
|
|
115
|
+
}
|
|
116
|
+
function metadataKeyPattern(keys) {
|
|
117
|
+
return new RegExp(`^(?:${keys.map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")}):\\s*(.*)$`, "i");
|
|
118
|
+
}
|
|
119
|
+
function parseMetadataList(body, keys) {
|
|
120
|
+
const block = body.match(/<!-- rig:metadata:start -->\s*([\s\S]*?)\s*<!-- rig:metadata:end -->/);
|
|
121
|
+
if (!block)
|
|
89
122
|
return [];
|
|
90
|
-
|
|
123
|
+
const lines = block[1].split(/\r?\n/);
|
|
124
|
+
const values = [];
|
|
125
|
+
const keyPattern = metadataKeyPattern(keys);
|
|
126
|
+
for (let index = 0;index < lines.length; index += 1) {
|
|
127
|
+
const line = lines[index];
|
|
128
|
+
const sameLine = line.match(keyPattern);
|
|
129
|
+
if (!sameLine)
|
|
130
|
+
continue;
|
|
131
|
+
const inlineValue = sameLine[1]?.trim() ?? "";
|
|
132
|
+
if (inlineValue) {
|
|
133
|
+
values.push(...parseIssueRefs(inlineValue));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
for (let cursor = index + 1;cursor < lines.length; cursor += 1) {
|
|
137
|
+
const item = lines[cursor].match(/^\s*-\s*(.+)$/);
|
|
138
|
+
if (!item)
|
|
139
|
+
break;
|
|
140
|
+
values.push(...parseIssueRefs(item[1]));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return [...new Set(values)];
|
|
144
|
+
}
|
|
145
|
+
function bodyWithoutRigMetadataBlock(body) {
|
|
146
|
+
return body.replace(/<!-- rig:metadata:start -->\s*[\s\S]*?\s*<!-- rig:metadata:end -->/g, "");
|
|
147
|
+
}
|
|
148
|
+
function parseBodyKeyRefs(body, keys) {
|
|
149
|
+
const keyPattern = metadataKeyPattern(keys);
|
|
150
|
+
const values = bodyWithoutRigMetadataBlock(body).split(/\r?\n/).flatMap((line) => {
|
|
151
|
+
const match = line.match(keyPattern);
|
|
152
|
+
return match?.[1] ? parseIssueRefs(match[1]) : [];
|
|
153
|
+
});
|
|
154
|
+
return [...new Set(values)];
|
|
155
|
+
}
|
|
156
|
+
function parseDeps(body) {
|
|
157
|
+
const keys = ["depends-on", "deps", "blocked-by", "blocked_by"];
|
|
158
|
+
return [...new Set([...parseBodyKeyRefs(body, keys), ...parseMetadataList(body, keys)])];
|
|
91
159
|
}
|
|
92
160
|
function parseParents(body) {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
return [];
|
|
96
|
-
return match[1].split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
|
|
161
|
+
const keys = ["parents", "parent"];
|
|
162
|
+
return [...new Set([...parseBodyKeyRefs(body, keys), ...parseMetadataList(body, keys)])];
|
|
97
163
|
}
|
|
98
164
|
function issueTypeFor(issue) {
|
|
99
165
|
const labels = labelNamesFor(issue);
|
|
@@ -104,7 +170,7 @@ function issueTypeFor(issue) {
|
|
|
104
170
|
return "epic";
|
|
105
171
|
return "task";
|
|
106
172
|
}
|
|
107
|
-
function issueToTask(issue, repo) {
|
|
173
|
+
function issueToTask(issue, repo, nativeDependencies) {
|
|
108
174
|
const labelNames = labelNamesFor(issue);
|
|
109
175
|
const scope = labelNames.filter((l) => l.startsWith("scope:")).map((l) => l.slice("scope:".length));
|
|
110
176
|
const roleLabel = labelNames.find((l) => l.startsWith("role:"));
|
|
@@ -112,10 +178,12 @@ function issueToTask(issue, repo) {
|
|
|
112
178
|
const validators = labelNames.filter((l) => l.startsWith("validator:")).map((l) => l.slice("validator:".length));
|
|
113
179
|
const body = issue.body ?? "";
|
|
114
180
|
const issueNodeId = issue.id ?? issue.nodeId ?? issue.node_id;
|
|
181
|
+
const parsedDeps = parseDeps(body);
|
|
182
|
+
const deps = nativeDependencies?.deps ? [...new Set([...parsedDeps, ...nativeDependencies.deps])] : parsedDeps;
|
|
115
183
|
return {
|
|
116
184
|
id: String(issue.number),
|
|
117
185
|
...typeof issueNodeId === "string" && issueNodeId.trim() ? { issueNodeId: issueNodeId.trim() } : {},
|
|
118
|
-
deps
|
|
186
|
+
deps,
|
|
119
187
|
status: statusFor(issue),
|
|
120
188
|
title: issue.title,
|
|
121
189
|
body,
|
|
@@ -127,6 +195,7 @@ function issueToTask(issue, repo) {
|
|
|
127
195
|
sourceIssueId: `${repo}#${issue.number}`,
|
|
128
196
|
parentChildDeps: parseParents(body),
|
|
129
197
|
labels: labelNames,
|
|
198
|
+
...nativeDependencies?.degraded ? { nativeDependenciesDegraded: true, nativeDependenciesError: nativeDependencies.degraded } : {},
|
|
130
199
|
raw: issue
|
|
131
200
|
};
|
|
132
201
|
}
|
|
@@ -183,13 +252,29 @@ function isRigStickyStatusComment(body) {
|
|
|
183
252
|
return body.includes(RIG_STATUS_COMMENT_MARKER);
|
|
184
253
|
}
|
|
185
254
|
function ghSpawnOptions(extraEnv, timeoutMs) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
255
|
+
const env = {
|
|
256
|
+
...process.env,
|
|
257
|
+
...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
|
|
258
|
+
...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
|
|
259
|
+
...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {}
|
|
260
|
+
};
|
|
261
|
+
for (const [key, value] of Object.entries(extraEnv ?? {})) {
|
|
262
|
+
if (value === undefined)
|
|
263
|
+
delete env[key];
|
|
264
|
+
else
|
|
265
|
+
env[key] = value;
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
encoding: "utf-8",
|
|
269
|
+
timeout: timeoutMs,
|
|
270
|
+
env
|
|
271
|
+
};
|
|
189
272
|
}
|
|
190
273
|
function credentialEnv(token) {
|
|
191
274
|
const clean = token?.trim() ?? "";
|
|
192
|
-
|
|
275
|
+
if (clean)
|
|
276
|
+
return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
|
|
277
|
+
return { GH_TOKEN: undefined, GITHUB_TOKEN: undefined, RIG_GITHUB_TOKEN: undefined };
|
|
193
278
|
}
|
|
194
279
|
async function resolveCredentialEnv(opts, purpose) {
|
|
195
280
|
if (!opts.credentialProvider)
|
|
@@ -204,28 +289,319 @@ async function resolveCredentialEnv(opts, purpose) {
|
|
|
204
289
|
const resolved = await opts.credentialProvider.resolveGitHubToken(input);
|
|
205
290
|
return credentialEnv(resolved.token);
|
|
206
291
|
}
|
|
292
|
+
function tokenDiagnostic(value) {
|
|
293
|
+
const clean = value?.trim() ?? "";
|
|
294
|
+
return clean ? `present(len=${clean.length})` : "missing";
|
|
295
|
+
}
|
|
207
296
|
function runGh(bin, args, spawn, extraEnv, timeoutMs) {
|
|
208
|
-
const
|
|
209
|
-
|
|
297
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
298
|
+
const res = spawn(bin, [...args], options);
|
|
299
|
+
assertGhSuccess(args, res, options.env);
|
|
210
300
|
if (!res.stdout || res.stdout.trim() === "")
|
|
211
301
|
return [];
|
|
212
302
|
return JSON.parse(res.stdout);
|
|
213
303
|
}
|
|
214
304
|
function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
|
|
215
|
-
const
|
|
216
|
-
|
|
305
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
306
|
+
const res = spawn(bin, [...args], options);
|
|
307
|
+
assertGhSuccess(args, res, options.env);
|
|
217
308
|
}
|
|
218
|
-
function assertGhSuccess(args, res) {
|
|
309
|
+
function assertGhSuccess(args, res, env) {
|
|
219
310
|
if (res.error) {
|
|
220
311
|
const msg = res.error.message ?? String(res.error);
|
|
221
312
|
throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
|
|
222
313
|
}
|
|
223
314
|
if (res.status !== 0) {
|
|
224
|
-
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
315
|
+
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
316
|
+
[rig gh env:standard-plugin] GH_TOKEN=${tokenDiagnostic(env.GH_TOKEN)} GITHUB_TOKEN=${tokenDiagnostic(env.GITHUB_TOKEN)} RIG_GITHUB_TOKEN=${tokenDiagnostic(env.RIG_GITHUB_TOKEN)}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
var DEFAULT_PROJECT_STATUSES = {
|
|
320
|
+
todo: "Todo",
|
|
321
|
+
running: "In Progress",
|
|
322
|
+
prOpen: "In Review",
|
|
323
|
+
ciFixing: "In Review",
|
|
324
|
+
merging: "In Review",
|
|
325
|
+
done: "Done",
|
|
326
|
+
needsAttention: "Needs Attention"
|
|
327
|
+
};
|
|
328
|
+
function asProjectRecord(value) {
|
|
329
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
330
|
+
}
|
|
331
|
+
function projectString(value) {
|
|
332
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
333
|
+
}
|
|
334
|
+
function projectLifecycleStatusForTaskStatus(status) {
|
|
335
|
+
const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
|
|
336
|
+
switch (normalized) {
|
|
337
|
+
case "draft":
|
|
338
|
+
case "open":
|
|
339
|
+
case "queued":
|
|
340
|
+
case "ready":
|
|
341
|
+
return "todo";
|
|
342
|
+
case "running":
|
|
343
|
+
case "in_progress":
|
|
344
|
+
return "running";
|
|
345
|
+
case "under_review":
|
|
346
|
+
case "review":
|
|
347
|
+
case "pr_open":
|
|
348
|
+
return "prOpen";
|
|
349
|
+
case "ci_fixing":
|
|
350
|
+
case "fixing":
|
|
351
|
+
return "ciFixing";
|
|
352
|
+
case "merging":
|
|
353
|
+
case "merge":
|
|
354
|
+
return "merging";
|
|
355
|
+
case "closed":
|
|
356
|
+
case "completed":
|
|
357
|
+
case "done":
|
|
358
|
+
return "done";
|
|
359
|
+
case "blocked":
|
|
360
|
+
case "cancelled":
|
|
361
|
+
case "failed":
|
|
362
|
+
case "needs_attention":
|
|
363
|
+
return "needsAttention";
|
|
364
|
+
default:
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
|
|
369
|
+
return async (query, variables) => {
|
|
370
|
+
const args = ["api", "graphql", "-f", `query=${query}`];
|
|
371
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
372
|
+
if (value === undefined || value === null)
|
|
373
|
+
continue;
|
|
374
|
+
args.push("-f", `${key}=${String(value)}`);
|
|
375
|
+
}
|
|
376
|
+
const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
|
|
377
|
+
return asProjectRecord(response)?.data ?? response;
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function issueNodeIdFor(issue) {
|
|
381
|
+
const id = issue.id ?? issue.nodeId ?? issue.node_id;
|
|
382
|
+
return typeof id === "string" && id.trim().length > 0 ? id.trim() : null;
|
|
383
|
+
}
|
|
384
|
+
function nativeIssueDependencyRef(value, currentRepo) {
|
|
385
|
+
const record = asProjectRecord(value);
|
|
386
|
+
const number = typeof record?.number === "number" ? String(record.number) : projectString(record?.number);
|
|
387
|
+
if (!number)
|
|
388
|
+
return null;
|
|
389
|
+
const repository = asProjectRecord(record?.repository);
|
|
390
|
+
const owner = projectString(asProjectRecord(repository?.owner)?.login);
|
|
391
|
+
const name = projectString(repository?.name);
|
|
392
|
+
if (!owner || !name || `${owner}/${name}` === currentRepo)
|
|
393
|
+
return number;
|
|
394
|
+
return `${owner}/${name}#${number}`;
|
|
395
|
+
}
|
|
396
|
+
function nativeDependencyRefsFrom(data, currentRepo) {
|
|
397
|
+
const issue = asProjectRecord(asProjectRecord(data)?.node);
|
|
398
|
+
const blockedBy = asProjectRecord(issue?.blockedBy);
|
|
399
|
+
const nodes = Array.isArray(blockedBy?.nodes) ? blockedBy.nodes : [];
|
|
400
|
+
return [...new Set(nodes.flatMap((node) => {
|
|
401
|
+
const ref = nativeIssueDependencyRef(node, currentRepo);
|
|
402
|
+
return ref ? [ref] : [];
|
|
403
|
+
}))];
|
|
404
|
+
}
|
|
405
|
+
async function readNativeDependenciesForIssue(input) {
|
|
406
|
+
const issueId = issueNodeIdFor(input.issue);
|
|
407
|
+
if (!issueId)
|
|
408
|
+
return { deps: [], degraded: "GitHub issue node id is unavailable." };
|
|
409
|
+
const query = `
|
|
410
|
+
query RigIssueNativeDependencies($issueId: ID!) {
|
|
411
|
+
node(id: $issueId) {
|
|
412
|
+
... on Issue {
|
|
413
|
+
blockedBy(first: 100) {
|
|
414
|
+
nodes {
|
|
415
|
+
number
|
|
416
|
+
repository { name owner { login } }
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
`;
|
|
423
|
+
try {
|
|
424
|
+
return {
|
|
425
|
+
deps: nativeDependencyRefsFrom(await input.fetchGraphQL(query, { issueId }, "gh-cli"), input.repo)
|
|
426
|
+
};
|
|
427
|
+
} catch (error) {
|
|
428
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
429
|
+
return { deps: [], degraded: detail };
|
|
225
430
|
}
|
|
226
431
|
}
|
|
432
|
+
function formatIssueReference(ref) {
|
|
433
|
+
const clean = ref.trim().replace(/^#/, "");
|
|
434
|
+
return /^\d+$/.test(clean) ? `#${clean}` : clean;
|
|
435
|
+
}
|
|
436
|
+
function appendReferenceLines(body, deps, parents) {
|
|
437
|
+
const lines = [];
|
|
438
|
+
const cleanDeps = (deps ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
|
|
439
|
+
const cleanParents = (parents ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
|
|
440
|
+
if (cleanDeps.length > 0)
|
|
441
|
+
lines.push(`depends-on: ${cleanDeps.join(", ")}`);
|
|
442
|
+
if (cleanParents.length > 0)
|
|
443
|
+
lines.push(`parents: ${cleanParents.join(", ")}`);
|
|
444
|
+
if (lines.length === 0)
|
|
445
|
+
return body;
|
|
446
|
+
return body.trim().length > 0 ? `${body.trimEnd()}
|
|
447
|
+
|
|
448
|
+
${lines.join(`
|
|
449
|
+
`)}` : lines.join(`
|
|
450
|
+
`);
|
|
451
|
+
}
|
|
452
|
+
function bodyForCreatedTask(input) {
|
|
453
|
+
const metadata = { ...input.metadata ?? {} };
|
|
454
|
+
if (input.deps && input.deps.length > 0)
|
|
455
|
+
metadata["depends-on"] = input.deps.map(formatIssueReference);
|
|
456
|
+
if (input.parents && input.parents.length > 0)
|
|
457
|
+
metadata.parents = input.parents.map(formatIssueReference);
|
|
458
|
+
return updateRigOwnedMetadataBlock(appendReferenceLines(input.body, input.deps, input.parents), metadata);
|
|
459
|
+
}
|
|
460
|
+
function projectStatusFieldFrom(data, projectId) {
|
|
461
|
+
const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
|
|
462
|
+
for (const node of Array.isArray(fields) ? fields : []) {
|
|
463
|
+
const record = asProjectRecord(node);
|
|
464
|
+
if (projectString(record?.name)?.toLowerCase() !== "status")
|
|
465
|
+
continue;
|
|
466
|
+
const id = projectString(record?.id);
|
|
467
|
+
if (!id)
|
|
468
|
+
continue;
|
|
469
|
+
const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
|
|
470
|
+
const optionRecord = asProjectRecord(option);
|
|
471
|
+
const optionId = projectString(optionRecord?.id);
|
|
472
|
+
const name = projectString(optionRecord?.name);
|
|
473
|
+
return optionId && name ? [{ id: optionId, name }] : [];
|
|
474
|
+
}) : [];
|
|
475
|
+
return { id, name: "Status", options };
|
|
476
|
+
}
|
|
477
|
+
throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
|
|
478
|
+
}
|
|
479
|
+
async function resolveProjectStatusField(input) {
|
|
480
|
+
const query = `
|
|
481
|
+
query RigProjectStatusField($projectId: ID!) {
|
|
482
|
+
node(id: $projectId) {
|
|
483
|
+
... on ProjectV2 {
|
|
484
|
+
fields(first: 50) {
|
|
485
|
+
nodes {
|
|
486
|
+
... on ProjectV2FieldCommon { id name }
|
|
487
|
+
... on ProjectV2SingleSelectField { id name options { id name } }
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
`;
|
|
494
|
+
return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
|
|
495
|
+
}
|
|
496
|
+
async function ensureIssueProjectItem(input) {
|
|
497
|
+
const query = `
|
|
498
|
+
query RigFindProjectIssueItem($projectId: ID!) {
|
|
499
|
+
node(id: $projectId) {
|
|
500
|
+
... on ProjectV2 {
|
|
501
|
+
items(first: 100) { nodes { id content { ... on Issue { id } } } }
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
`;
|
|
506
|
+
const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
|
|
507
|
+
const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
|
|
508
|
+
for (const node of Array.isArray(nodes) ? nodes : []) {
|
|
509
|
+
const record = asProjectRecord(node);
|
|
510
|
+
const content = asProjectRecord(record?.content);
|
|
511
|
+
if (projectString(content?.id) === input.issueNodeId) {
|
|
512
|
+
const id2 = projectString(record?.id);
|
|
513
|
+
if (id2)
|
|
514
|
+
return { id: id2, created: false };
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const mutation = `
|
|
518
|
+
mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
|
|
519
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
|
|
520
|
+
}
|
|
521
|
+
`;
|
|
522
|
+
const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
|
|
523
|
+
const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
|
|
524
|
+
const id = projectString(asProjectRecord(addResult?.item)?.id);
|
|
525
|
+
if (!id)
|
|
526
|
+
throw new Error("GitHub Project item creation did not return an item id.");
|
|
527
|
+
return { id, created: true };
|
|
528
|
+
}
|
|
529
|
+
async function updateIssueProjectStatus(input) {
|
|
530
|
+
const mutation = `
|
|
531
|
+
mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
532
|
+
updateProjectV2ItemFieldValue(input: {
|
|
533
|
+
projectId: $projectId,
|
|
534
|
+
itemId: $itemId,
|
|
535
|
+
fieldId: $fieldId,
|
|
536
|
+
value: { singleSelectOptionId: $optionId }
|
|
537
|
+
}) { projectV2Item { id } }
|
|
538
|
+
}
|
|
539
|
+
`;
|
|
540
|
+
await input.fetchGraphQL(mutation, {
|
|
541
|
+
projectId: input.projectId,
|
|
542
|
+
itemId: input.itemId,
|
|
543
|
+
fieldId: input.fieldId,
|
|
544
|
+
optionId: input.optionId
|
|
545
|
+
}, input.token);
|
|
546
|
+
}
|
|
547
|
+
function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
|
|
548
|
+
const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
|
|
549
|
+
return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
|
|
550
|
+
}
|
|
551
|
+
async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
|
|
552
|
+
if (!projects?.enabled)
|
|
553
|
+
return;
|
|
554
|
+
const projectId = projectString(projects.projectId);
|
|
555
|
+
if (!projectId)
|
|
556
|
+
throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
|
|
557
|
+
const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
|
|
558
|
+
if (!lifecycleStatus)
|
|
559
|
+
return;
|
|
560
|
+
const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
|
|
561
|
+
if (!issueNodeId)
|
|
562
|
+
throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
|
|
563
|
+
const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
|
|
564
|
+
const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
|
|
565
|
+
const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
|
|
566
|
+
const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
|
|
567
|
+
if (!option)
|
|
568
|
+
throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
|
|
569
|
+
const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
|
|
570
|
+
await updateIssueProjectStatus({
|
|
571
|
+
projectId,
|
|
572
|
+
itemId: item.id,
|
|
573
|
+
fieldId: projectString(projects.statusFieldId) ?? field.id,
|
|
574
|
+
optionId: option.id,
|
|
575
|
+
token: "gh-cli",
|
|
576
|
+
fetchGraphQL
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
|
|
580
|
+
function normalizeTaskStatusToken(status) {
|
|
581
|
+
return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
|
|
582
|
+
}
|
|
583
|
+
function issueUpdatesMode(value) {
|
|
584
|
+
return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
|
|
585
|
+
}
|
|
586
|
+
function isTerminalTaskStatus(status) {
|
|
587
|
+
return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
|
|
588
|
+
}
|
|
589
|
+
function shouldWriteIssueUpdate(mode, status) {
|
|
590
|
+
if (mode === "off")
|
|
591
|
+
return false;
|
|
592
|
+
if (mode === "lifecycle")
|
|
593
|
+
return true;
|
|
594
|
+
return isTerminalTaskStatus(status);
|
|
595
|
+
}
|
|
596
|
+
function isRunningStatus(status) {
|
|
597
|
+
return normalizeTaskStatusToken(status) === "running";
|
|
598
|
+
}
|
|
599
|
+
function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
|
|
600
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
|
|
601
|
+
}
|
|
227
602
|
function statusLabelFor(status) {
|
|
228
603
|
switch (status) {
|
|
604
|
+
case "running":
|
|
229
605
|
case "in_progress":
|
|
230
606
|
return "in-progress";
|
|
231
607
|
case "blocked":
|
|
@@ -244,6 +620,8 @@ function statusLabelFor(status) {
|
|
|
244
620
|
return "under-review";
|
|
245
621
|
case "needs_attention":
|
|
246
622
|
return "blocked";
|
|
623
|
+
case "closed":
|
|
624
|
+
case "completed":
|
|
247
625
|
case "open":
|
|
248
626
|
return null;
|
|
249
627
|
default:
|
|
@@ -252,11 +630,13 @@ function statusLabelFor(status) {
|
|
|
252
630
|
}
|
|
253
631
|
function rigStatusLabelFor(status) {
|
|
254
632
|
switch (status) {
|
|
633
|
+
case "running":
|
|
255
634
|
case "in_progress":
|
|
256
635
|
return "rig:running";
|
|
257
636
|
case "under_review":
|
|
258
637
|
return "rig:pr-open";
|
|
259
638
|
case "closed":
|
|
639
|
+
case "completed":
|
|
260
640
|
return "rig:done";
|
|
261
641
|
case "ci_fixing":
|
|
262
642
|
return "rig:ci-fixing";
|
|
@@ -274,9 +654,10 @@ function rigStatusLabelFor(status) {
|
|
|
274
654
|
return null;
|
|
275
655
|
}
|
|
276
656
|
}
|
|
277
|
-
function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
278
|
-
const targetLabel = status === "closed" ? null : statusLabelFor(status);
|
|
657
|
+
async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
658
|
+
const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
|
|
279
659
|
const targetRigLabel = rigStatusLabelFor(status);
|
|
660
|
+
const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
|
|
280
661
|
for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
|
|
281
662
|
if (targetLabel !== null && l === targetLabel)
|
|
282
663
|
continue;
|
|
@@ -298,7 +679,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
|
298
679
|
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
|
|
299
680
|
}
|
|
300
681
|
}
|
|
301
|
-
if (status
|
|
682
|
+
if (isRunningStatus(status)) {
|
|
683
|
+
assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
|
|
684
|
+
if (shouldSyncLifecycle) {
|
|
685
|
+
upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
|
|
686
|
+
status: "running",
|
|
687
|
+
summary: "Rig run started."
|
|
688
|
+
}), extraEnv, timeoutMs);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (shouldSyncLifecycle) {
|
|
692
|
+
await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
|
|
693
|
+
}
|
|
694
|
+
if (status === "closed" || status === "completed") {
|
|
302
695
|
runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
|
|
303
696
|
}
|
|
304
697
|
}
|
|
@@ -368,11 +761,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
|
|
|
368
761
|
} catch {}
|
|
369
762
|
}
|
|
370
763
|
}
|
|
371
|
-
function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
|
|
764
|
+
async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
372
765
|
if (update.status) {
|
|
373
|
-
applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
|
|
766
|
+
await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
|
|
374
767
|
}
|
|
375
|
-
if (update.comment?.trim()) {
|
|
768
|
+
if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
|
|
376
769
|
if (isRigStickyStatusComment(update.comment)) {
|
|
377
770
|
upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
|
|
378
771
|
} else {
|
|
@@ -398,6 +791,17 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
398
791
|
const spawnFn = opts.spawn ?? spawnSync;
|
|
399
792
|
const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
|
|
400
793
|
const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
|
|
794
|
+
const issueUpdates = issueUpdatesMode(opts.issueUpdates);
|
|
795
|
+
async function issueToTaskWithOptionalNativeDependencies(issue, env) {
|
|
796
|
+
if (!opts.useNativeDependencies)
|
|
797
|
+
return issueToTask(issue, repo);
|
|
798
|
+
const nativeDependencies = await readNativeDependenciesForIssue({
|
|
799
|
+
issue,
|
|
800
|
+
repo,
|
|
801
|
+
fetchGraphQL: ghGraphQLFetch(bin, spawnFn, env, timeoutMs)
|
|
802
|
+
});
|
|
803
|
+
return issueToTask(issue, repo, nativeDependencies);
|
|
804
|
+
}
|
|
401
805
|
return {
|
|
402
806
|
id: "std:github-issues",
|
|
403
807
|
kind: "github-issues",
|
|
@@ -424,12 +828,13 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
424
828
|
throw new Error(`GitHub issue list for ${repo} reached the configured limit (${listLimit}); refusing to silently truncate matching issues. Increase taskSource.options.listLimit or narrow labels/state/assignee.`);
|
|
425
829
|
}
|
|
426
830
|
const issues = rawIssues.filter((issue) => !issue.pull_request);
|
|
427
|
-
return issues.map((
|
|
831
|
+
return Promise.all(issues.map((issue) => issueToTaskWithOptionalNativeDependencies(issue, env)));
|
|
428
832
|
},
|
|
429
833
|
async get(id) {
|
|
834
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
835
|
+
let issue;
|
|
430
836
|
try {
|
|
431
|
-
|
|
432
|
-
const issue = runGh(bin, [
|
|
837
|
+
issue = runGh(bin, [
|
|
433
838
|
"issue",
|
|
434
839
|
"view",
|
|
435
840
|
String(id),
|
|
@@ -438,19 +843,23 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
438
843
|
"--json",
|
|
439
844
|
"number,title,body,labels,state,url,assignees,id"
|
|
440
845
|
], spawnFn, env, timeoutMs);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
846
|
+
} catch (error) {
|
|
847
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
848
|
+
if (/could not resolve to (an? )?(issue|pullrequest)|no issues? (found|matched)|404 not found|gh: not found|gh issue view\b[\s\S]*failed \(exit \d+\): not found\b/i.test(detail)) {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
throw new Error(`Failed to read task ${id} from GitHub repo ${repo}: ${detail}`);
|
|
444
852
|
}
|
|
853
|
+
return issueToTaskWithOptionalNativeDependencies(issue, env);
|
|
445
854
|
},
|
|
446
855
|
async updateStatus(id, status) {
|
|
447
856
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
448
|
-
applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
|
|
857
|
+
await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
449
858
|
notifyTaskChanged(opts.onTaskChanged, repo, id, status);
|
|
450
859
|
},
|
|
451
860
|
async updateTask(id, update) {
|
|
452
861
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
453
|
-
applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
|
|
862
|
+
await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
454
863
|
notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
|
|
455
864
|
},
|
|
456
865
|
async addLabels(id, labels) {
|
|
@@ -465,6 +874,7 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
465
874
|
},
|
|
466
875
|
async createIssue(input) {
|
|
467
876
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
877
|
+
const body = input.body ?? "";
|
|
468
878
|
const args = [
|
|
469
879
|
"api",
|
|
470
880
|
"-X",
|
|
@@ -473,12 +883,31 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
473
883
|
"-f",
|
|
474
884
|
`title=${input.title}`,
|
|
475
885
|
"-f",
|
|
476
|
-
`body=${
|
|
886
|
+
`body=${body}`,
|
|
477
887
|
...(input.labels ?? []).flatMap((label) => ["-f", `labels[]=${label}`])
|
|
478
888
|
];
|
|
479
889
|
const issue = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
480
890
|
notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
|
|
481
|
-
return issueToTask(issue, repo);
|
|
891
|
+
return issueToTask({ ...issue, body: issue.body ?? body }, repo);
|
|
892
|
+
},
|
|
893
|
+
async create(input) {
|
|
894
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
895
|
+
const body = bodyForCreatedTask(input);
|
|
896
|
+
const args = [
|
|
897
|
+
"api",
|
|
898
|
+
"-X",
|
|
899
|
+
"POST",
|
|
900
|
+
`repos/${repo}/issues`,
|
|
901
|
+
"-f",
|
|
902
|
+
`title=${input.title}`,
|
|
903
|
+
"-f",
|
|
904
|
+
`body=${body}`,
|
|
905
|
+
"-f",
|
|
906
|
+
"labels[]=rig:generated"
|
|
907
|
+
];
|
|
908
|
+
const issue = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
909
|
+
notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
|
|
910
|
+
return issueToTask({ ...issue, body: issue.body ?? body }, repo);
|
|
482
911
|
},
|
|
483
912
|
async getIssueBody(id) {
|
|
484
913
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|