@h-rig/standard-plugin 0.0.6-alpha.14 → 0.0.6-alpha.141
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/bundle.d.ts +6 -0
- package/dist/src/bundle.js +1602 -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/plugin.d.ts +63 -0
- package/dist/src/drift/plugin.js +508 -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 +436 -50
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +1102 -62
- package/dist/src/plugin.d.ts +26 -0
- package/dist/src/plugin.js +1685 -0
- package/package.json +28 -5
|
@@ -11,9 +11,9 @@ function createEnvGitHubCredentialProvider() {
|
|
|
11
11
|
return {
|
|
12
12
|
async resolveGitHubToken(input) {
|
|
13
13
|
if (input.purpose === "selected-repo") {
|
|
14
|
-
return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
14
|
+
return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_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,40 @@ function createEnvGitHubCredentialProvider() {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
function createStateGitHubCredentialProvider(options = {}) {
|
|
25
|
-
const
|
|
25
|
+
const stateFileCandidates = () => {
|
|
26
|
+
const candidates = [];
|
|
26
27
|
const explicitFile = options.stateFile ?? process.env.RIG_GITHUB_AUTH_STATE_FILE;
|
|
27
28
|
if (explicitFile?.trim())
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
29
|
+
candidates.push(resolve(explicitFile.trim()));
|
|
30
|
+
for (const dir of [options.stateDir, process.env.RIG_STATE_DIR]) {
|
|
31
|
+
if (dir?.trim())
|
|
32
|
+
candidates.push(resolve(dir.trim(), "github-auth.json"));
|
|
33
|
+
}
|
|
34
|
+
return candidates;
|
|
31
35
|
};
|
|
32
36
|
const readToken = () => {
|
|
33
|
-
const stateFile
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
for (const stateFile of stateFileCandidates()) {
|
|
38
|
+
if (!existsSync(stateFile))
|
|
39
|
+
continue;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
42
|
+
const token = typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
|
|
43
|
+
if (token)
|
|
44
|
+
return token;
|
|
45
|
+
} catch {}
|
|
41
46
|
}
|
|
47
|
+
return null;
|
|
42
48
|
};
|
|
43
49
|
return {
|
|
44
50
|
async resolveGitHubToken(input) {
|
|
45
51
|
const token = readToken();
|
|
46
52
|
if (input.purpose === "selected-repo") {
|
|
47
|
-
return { token: token ?? "", source: "signed-in-user" };
|
|
53
|
+
return { token: token ?? cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
48
54
|
}
|
|
49
55
|
if (token) {
|
|
50
56
|
return { token, source: "signed-in-user" };
|
|
51
57
|
}
|
|
52
|
-
const fallback = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
58
|
+
const fallback = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
53
59
|
if (!fallback) {
|
|
54
60
|
throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
|
|
55
61
|
}
|
|
@@ -83,17 +89,42 @@ function statusFor(issue) {
|
|
|
83
89
|
return "cancelled";
|
|
84
90
|
return "open";
|
|
85
91
|
}
|
|
92
|
+
function parseIssueRefs(raw) {
|
|
93
|
+
return raw.split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
|
|
94
|
+
}
|
|
95
|
+
function parseMetadataList(body, key) {
|
|
96
|
+
const block = body.match(/<!-- rig:metadata:start -->\s*([\s\S]*?)\s*<!-- rig:metadata:end -->/);
|
|
97
|
+
if (!block)
|
|
98
|
+
return [];
|
|
99
|
+
const lines = block[1].split(/\r?\n/);
|
|
100
|
+
const values = [];
|
|
101
|
+
for (let index = 0;index < lines.length; index += 1) {
|
|
102
|
+
const line = lines[index];
|
|
103
|
+
const sameLine = line.match(new RegExp(`^${key}:\\s*(.+)$`, "i"));
|
|
104
|
+
if (sameLine) {
|
|
105
|
+
values.push(...parseIssueRefs(sameLine[1]));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!new RegExp(`^${key}:\\s*$`, "i").test(line))
|
|
109
|
+
continue;
|
|
110
|
+
for (let cursor = index + 1;cursor < lines.length; cursor += 1) {
|
|
111
|
+
const item = lines[cursor].match(/^\s*-\s*(.+)$/);
|
|
112
|
+
if (!item)
|
|
113
|
+
break;
|
|
114
|
+
values.push(...parseIssueRefs(item[1]));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return [...new Set(values)];
|
|
118
|
+
}
|
|
86
119
|
function parseDeps(body) {
|
|
87
120
|
const match = body.match(/^depends-on:\s*([^\n]+)/im);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return match[1].split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
|
|
121
|
+
const bodyRefs = match ? parseIssueRefs(match[1]) : [];
|
|
122
|
+
return [...new Set([...bodyRefs, ...parseMetadataList(body, "depends-on")])];
|
|
91
123
|
}
|
|
92
124
|
function parseParents(body) {
|
|
93
125
|
const match = body.match(/^parents?:\s*([^\n]+)/im);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return match[1].split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
|
|
126
|
+
const bodyRefs = match ? parseIssueRefs(match[1]) : [];
|
|
127
|
+
return [...new Set([...bodyRefs, ...parseMetadataList(body, "parents")])];
|
|
97
128
|
}
|
|
98
129
|
function issueTypeFor(issue) {
|
|
99
130
|
const labels = labelNamesFor(issue);
|
|
@@ -104,7 +135,7 @@ function issueTypeFor(issue) {
|
|
|
104
135
|
return "epic";
|
|
105
136
|
return "task";
|
|
106
137
|
}
|
|
107
|
-
function issueToTask(issue, repo) {
|
|
138
|
+
function issueToTask(issue, repo, nativeDependencies) {
|
|
108
139
|
const labelNames = labelNamesFor(issue);
|
|
109
140
|
const scope = labelNames.filter((l) => l.startsWith("scope:")).map((l) => l.slice("scope:".length));
|
|
110
141
|
const roleLabel = labelNames.find((l) => l.startsWith("role:"));
|
|
@@ -112,10 +143,12 @@ function issueToTask(issue, repo) {
|
|
|
112
143
|
const validators = labelNames.filter((l) => l.startsWith("validator:")).map((l) => l.slice("validator:".length));
|
|
113
144
|
const body = issue.body ?? "";
|
|
114
145
|
const issueNodeId = issue.id ?? issue.nodeId ?? issue.node_id;
|
|
146
|
+
const parsedDeps = parseDeps(body);
|
|
147
|
+
const deps = nativeDependencies?.deps ? [...new Set([...parsedDeps, ...nativeDependencies.deps])] : parsedDeps;
|
|
115
148
|
return {
|
|
116
149
|
id: String(issue.number),
|
|
117
150
|
...typeof issueNodeId === "string" && issueNodeId.trim() ? { issueNodeId: issueNodeId.trim() } : {},
|
|
118
|
-
deps
|
|
151
|
+
deps,
|
|
119
152
|
status: statusFor(issue),
|
|
120
153
|
title: issue.title,
|
|
121
154
|
body,
|
|
@@ -127,6 +160,7 @@ function issueToTask(issue, repo) {
|
|
|
127
160
|
sourceIssueId: `${repo}#${issue.number}`,
|
|
128
161
|
parentChildDeps: parseParents(body),
|
|
129
162
|
labels: labelNames,
|
|
163
|
+
...nativeDependencies?.degraded ? { nativeDependenciesDegraded: true, nativeDependenciesError: nativeDependencies.degraded } : {},
|
|
130
164
|
raw: issue
|
|
131
165
|
};
|
|
132
166
|
}
|
|
@@ -183,13 +217,21 @@ function isRigStickyStatusComment(body) {
|
|
|
183
217
|
return body.includes(RIG_STATUS_COMMENT_MARKER);
|
|
184
218
|
}
|
|
185
219
|
function ghSpawnOptions(extraEnv, timeoutMs) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
220
|
+
return {
|
|
221
|
+
encoding: "utf-8",
|
|
222
|
+
timeout: timeoutMs,
|
|
223
|
+
env: {
|
|
224
|
+
...process.env,
|
|
225
|
+
...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
|
|
226
|
+
...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
|
|
227
|
+
...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {},
|
|
228
|
+
...extraEnv ?? {}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
189
231
|
}
|
|
190
232
|
function credentialEnv(token) {
|
|
191
233
|
const clean = token?.trim() ?? "";
|
|
192
|
-
return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
|
|
234
|
+
return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
|
|
193
235
|
}
|
|
194
236
|
async function resolveCredentialEnv(opts, purpose) {
|
|
195
237
|
if (!opts.credentialProvider)
|
|
@@ -204,28 +246,319 @@ async function resolveCredentialEnv(opts, purpose) {
|
|
|
204
246
|
const resolved = await opts.credentialProvider.resolveGitHubToken(input);
|
|
205
247
|
return credentialEnv(resolved.token);
|
|
206
248
|
}
|
|
249
|
+
function tokenDiagnostic(value) {
|
|
250
|
+
const clean = value?.trim() ?? "";
|
|
251
|
+
return clean ? `present(len=${clean.length})` : "missing";
|
|
252
|
+
}
|
|
207
253
|
function runGh(bin, args, spawn, extraEnv, timeoutMs) {
|
|
208
|
-
const
|
|
209
|
-
|
|
254
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
255
|
+
const res = spawn(bin, [...args], options);
|
|
256
|
+
assertGhSuccess(args, res, options.env);
|
|
210
257
|
if (!res.stdout || res.stdout.trim() === "")
|
|
211
258
|
return [];
|
|
212
259
|
return JSON.parse(res.stdout);
|
|
213
260
|
}
|
|
214
261
|
function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
|
|
215
|
-
const
|
|
216
|
-
|
|
262
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
263
|
+
const res = spawn(bin, [...args], options);
|
|
264
|
+
assertGhSuccess(args, res, options.env);
|
|
217
265
|
}
|
|
218
|
-
function assertGhSuccess(args, res) {
|
|
266
|
+
function assertGhSuccess(args, res, env) {
|
|
219
267
|
if (res.error) {
|
|
220
268
|
const msg = res.error.message ?? String(res.error);
|
|
221
269
|
throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
|
|
222
270
|
}
|
|
223
271
|
if (res.status !== 0) {
|
|
224
|
-
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
272
|
+
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
273
|
+
[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)}`);
|
|
225
274
|
}
|
|
226
275
|
}
|
|
276
|
+
var DEFAULT_PROJECT_STATUSES = {
|
|
277
|
+
todo: "Todo",
|
|
278
|
+
running: "In Progress",
|
|
279
|
+
prOpen: "In Review",
|
|
280
|
+
ciFixing: "In Review",
|
|
281
|
+
merging: "In Review",
|
|
282
|
+
done: "Done",
|
|
283
|
+
needsAttention: "Needs Attention"
|
|
284
|
+
};
|
|
285
|
+
function asProjectRecord(value) {
|
|
286
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
287
|
+
}
|
|
288
|
+
function projectString(value) {
|
|
289
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
290
|
+
}
|
|
291
|
+
function projectLifecycleStatusForTaskStatus(status) {
|
|
292
|
+
const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
|
|
293
|
+
switch (normalized) {
|
|
294
|
+
case "draft":
|
|
295
|
+
case "open":
|
|
296
|
+
case "queued":
|
|
297
|
+
case "ready":
|
|
298
|
+
return "todo";
|
|
299
|
+
case "running":
|
|
300
|
+
case "in_progress":
|
|
301
|
+
return "running";
|
|
302
|
+
case "under_review":
|
|
303
|
+
case "review":
|
|
304
|
+
case "pr_open":
|
|
305
|
+
return "prOpen";
|
|
306
|
+
case "ci_fixing":
|
|
307
|
+
case "fixing":
|
|
308
|
+
return "ciFixing";
|
|
309
|
+
case "merging":
|
|
310
|
+
case "merge":
|
|
311
|
+
return "merging";
|
|
312
|
+
case "closed":
|
|
313
|
+
case "completed":
|
|
314
|
+
case "done":
|
|
315
|
+
return "done";
|
|
316
|
+
case "blocked":
|
|
317
|
+
case "cancelled":
|
|
318
|
+
case "failed":
|
|
319
|
+
case "needs_attention":
|
|
320
|
+
return "needsAttention";
|
|
321
|
+
default:
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
|
|
326
|
+
return async (query, variables) => {
|
|
327
|
+
const args = ["api", "graphql", "-f", `query=${query}`];
|
|
328
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
329
|
+
if (value === undefined || value === null)
|
|
330
|
+
continue;
|
|
331
|
+
args.push("-f", `${key}=${String(value)}`);
|
|
332
|
+
}
|
|
333
|
+
const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
|
|
334
|
+
return asProjectRecord(response)?.data ?? response;
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function issueNodeIdFor(issue) {
|
|
338
|
+
const id = issue.id ?? issue.nodeId ?? issue.node_id;
|
|
339
|
+
return typeof id === "string" && id.trim().length > 0 ? id.trim() : null;
|
|
340
|
+
}
|
|
341
|
+
function nativeIssueDependencyRef(value, currentRepo) {
|
|
342
|
+
const record = asProjectRecord(value);
|
|
343
|
+
const number = typeof record?.number === "number" ? String(record.number) : projectString(record?.number);
|
|
344
|
+
if (!number)
|
|
345
|
+
return null;
|
|
346
|
+
const repository = asProjectRecord(record?.repository);
|
|
347
|
+
const owner = projectString(asProjectRecord(repository?.owner)?.login);
|
|
348
|
+
const name = projectString(repository?.name);
|
|
349
|
+
if (!owner || !name || `${owner}/${name}` === currentRepo)
|
|
350
|
+
return number;
|
|
351
|
+
return `${owner}/${name}#${number}`;
|
|
352
|
+
}
|
|
353
|
+
function nativeDependencyRefsFrom(data, currentRepo) {
|
|
354
|
+
const issue = asProjectRecord(asProjectRecord(data)?.node);
|
|
355
|
+
const blockedBy = asProjectRecord(issue?.blockedBy);
|
|
356
|
+
const nodes = Array.isArray(blockedBy?.nodes) ? blockedBy.nodes : [];
|
|
357
|
+
return [...new Set(nodes.flatMap((node) => {
|
|
358
|
+
const ref = nativeIssueDependencyRef(node, currentRepo);
|
|
359
|
+
return ref ? [ref] : [];
|
|
360
|
+
}))];
|
|
361
|
+
}
|
|
362
|
+
async function readNativeDependenciesForIssue(input) {
|
|
363
|
+
const issueId = issueNodeIdFor(input.issue);
|
|
364
|
+
if (!issueId)
|
|
365
|
+
return { deps: [], degraded: "GitHub issue node id is unavailable." };
|
|
366
|
+
const query = `
|
|
367
|
+
query RigIssueNativeDependencies($issueId: ID!) {
|
|
368
|
+
node(id: $issueId) {
|
|
369
|
+
... on Issue {
|
|
370
|
+
blockedBy(first: 100) {
|
|
371
|
+
nodes {
|
|
372
|
+
number
|
|
373
|
+
repository { name owner { login } }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
`;
|
|
380
|
+
try {
|
|
381
|
+
return {
|
|
382
|
+
deps: nativeDependencyRefsFrom(await input.fetchGraphQL(query, { issueId }, "gh-cli"), input.repo)
|
|
383
|
+
};
|
|
384
|
+
} catch (error) {
|
|
385
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
386
|
+
return { deps: [], degraded: detail };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function formatIssueReference(ref) {
|
|
390
|
+
const clean = ref.trim().replace(/^#/, "");
|
|
391
|
+
return /^\d+$/.test(clean) ? `#${clean}` : clean;
|
|
392
|
+
}
|
|
393
|
+
function appendReferenceLines(body, deps, parents) {
|
|
394
|
+
const lines = [];
|
|
395
|
+
const cleanDeps = (deps ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
|
|
396
|
+
const cleanParents = (parents ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
|
|
397
|
+
if (cleanDeps.length > 0)
|
|
398
|
+
lines.push(`depends-on: ${cleanDeps.join(", ")}`);
|
|
399
|
+
if (cleanParents.length > 0)
|
|
400
|
+
lines.push(`parents: ${cleanParents.join(", ")}`);
|
|
401
|
+
if (lines.length === 0)
|
|
402
|
+
return body;
|
|
403
|
+
return body.trim().length > 0 ? `${body.trimEnd()}
|
|
404
|
+
|
|
405
|
+
${lines.join(`
|
|
406
|
+
`)}` : lines.join(`
|
|
407
|
+
`);
|
|
408
|
+
}
|
|
409
|
+
function bodyForCreatedTask(input) {
|
|
410
|
+
const metadata = { ...input.metadata ?? {} };
|
|
411
|
+
if (input.deps && input.deps.length > 0)
|
|
412
|
+
metadata["depends-on"] = input.deps.map(formatIssueReference);
|
|
413
|
+
if (input.parents && input.parents.length > 0)
|
|
414
|
+
metadata.parents = input.parents.map(formatIssueReference);
|
|
415
|
+
return updateRigOwnedMetadataBlock(appendReferenceLines(input.body, input.deps, input.parents), metadata);
|
|
416
|
+
}
|
|
417
|
+
function projectStatusFieldFrom(data, projectId) {
|
|
418
|
+
const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
|
|
419
|
+
for (const node of Array.isArray(fields) ? fields : []) {
|
|
420
|
+
const record = asProjectRecord(node);
|
|
421
|
+
if (projectString(record?.name)?.toLowerCase() !== "status")
|
|
422
|
+
continue;
|
|
423
|
+
const id = projectString(record?.id);
|
|
424
|
+
if (!id)
|
|
425
|
+
continue;
|
|
426
|
+
const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
|
|
427
|
+
const optionRecord = asProjectRecord(option);
|
|
428
|
+
const optionId = projectString(optionRecord?.id);
|
|
429
|
+
const name = projectString(optionRecord?.name);
|
|
430
|
+
return optionId && name ? [{ id: optionId, name }] : [];
|
|
431
|
+
}) : [];
|
|
432
|
+
return { id, name: "Status", options };
|
|
433
|
+
}
|
|
434
|
+
throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
|
|
435
|
+
}
|
|
436
|
+
async function resolveProjectStatusField(input) {
|
|
437
|
+
const query = `
|
|
438
|
+
query RigProjectStatusField($projectId: ID!) {
|
|
439
|
+
node(id: $projectId) {
|
|
440
|
+
... on ProjectV2 {
|
|
441
|
+
fields(first: 50) {
|
|
442
|
+
nodes {
|
|
443
|
+
... on ProjectV2FieldCommon { id name }
|
|
444
|
+
... on ProjectV2SingleSelectField { id name options { id name } }
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
`;
|
|
451
|
+
return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
|
|
452
|
+
}
|
|
453
|
+
async function ensureIssueProjectItem(input) {
|
|
454
|
+
const query = `
|
|
455
|
+
query RigFindProjectIssueItem($projectId: ID!) {
|
|
456
|
+
node(id: $projectId) {
|
|
457
|
+
... on ProjectV2 {
|
|
458
|
+
items(first: 100) { nodes { id content { ... on Issue { id } } } }
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
`;
|
|
463
|
+
const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
|
|
464
|
+
const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
|
|
465
|
+
for (const node of Array.isArray(nodes) ? nodes : []) {
|
|
466
|
+
const record = asProjectRecord(node);
|
|
467
|
+
const content = asProjectRecord(record?.content);
|
|
468
|
+
if (projectString(content?.id) === input.issueNodeId) {
|
|
469
|
+
const id2 = projectString(record?.id);
|
|
470
|
+
if (id2)
|
|
471
|
+
return { id: id2, created: false };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const mutation = `
|
|
475
|
+
mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
|
|
476
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
|
|
477
|
+
}
|
|
478
|
+
`;
|
|
479
|
+
const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
|
|
480
|
+
const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
|
|
481
|
+
const id = projectString(asProjectRecord(addResult?.item)?.id);
|
|
482
|
+
if (!id)
|
|
483
|
+
throw new Error("GitHub Project item creation did not return an item id.");
|
|
484
|
+
return { id, created: true };
|
|
485
|
+
}
|
|
486
|
+
async function updateIssueProjectStatus(input) {
|
|
487
|
+
const mutation = `
|
|
488
|
+
mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
489
|
+
updateProjectV2ItemFieldValue(input: {
|
|
490
|
+
projectId: $projectId,
|
|
491
|
+
itemId: $itemId,
|
|
492
|
+
fieldId: $fieldId,
|
|
493
|
+
value: { singleSelectOptionId: $optionId }
|
|
494
|
+
}) { projectV2Item { id } }
|
|
495
|
+
}
|
|
496
|
+
`;
|
|
497
|
+
await input.fetchGraphQL(mutation, {
|
|
498
|
+
projectId: input.projectId,
|
|
499
|
+
itemId: input.itemId,
|
|
500
|
+
fieldId: input.fieldId,
|
|
501
|
+
optionId: input.optionId
|
|
502
|
+
}, input.token);
|
|
503
|
+
}
|
|
504
|
+
function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
|
|
505
|
+
const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
|
|
506
|
+
return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
|
|
507
|
+
}
|
|
508
|
+
async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
|
|
509
|
+
if (!projects?.enabled)
|
|
510
|
+
return;
|
|
511
|
+
const projectId = projectString(projects.projectId);
|
|
512
|
+
if (!projectId)
|
|
513
|
+
throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
|
|
514
|
+
const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
|
|
515
|
+
if (!lifecycleStatus)
|
|
516
|
+
return;
|
|
517
|
+
const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
|
|
518
|
+
if (!issueNodeId)
|
|
519
|
+
throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
|
|
520
|
+
const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
|
|
521
|
+
const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
|
|
522
|
+
const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
|
|
523
|
+
const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
|
|
524
|
+
if (!option)
|
|
525
|
+
throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
|
|
526
|
+
const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
|
|
527
|
+
await updateIssueProjectStatus({
|
|
528
|
+
projectId,
|
|
529
|
+
itemId: item.id,
|
|
530
|
+
fieldId: projectString(projects.statusFieldId) ?? field.id,
|
|
531
|
+
optionId: option.id,
|
|
532
|
+
token: "gh-cli",
|
|
533
|
+
fetchGraphQL
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
|
|
537
|
+
function normalizeTaskStatusToken(status) {
|
|
538
|
+
return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
|
|
539
|
+
}
|
|
540
|
+
function issueUpdatesMode(value) {
|
|
541
|
+
return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
|
|
542
|
+
}
|
|
543
|
+
function isTerminalTaskStatus(status) {
|
|
544
|
+
return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
|
|
545
|
+
}
|
|
546
|
+
function shouldWriteIssueUpdate(mode, status) {
|
|
547
|
+
if (mode === "off")
|
|
548
|
+
return false;
|
|
549
|
+
if (mode === "lifecycle")
|
|
550
|
+
return true;
|
|
551
|
+
return isTerminalTaskStatus(status);
|
|
552
|
+
}
|
|
553
|
+
function isRunningStatus(status) {
|
|
554
|
+
return normalizeTaskStatusToken(status) === "running";
|
|
555
|
+
}
|
|
556
|
+
function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
|
|
557
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
|
|
558
|
+
}
|
|
227
559
|
function statusLabelFor(status) {
|
|
228
560
|
switch (status) {
|
|
561
|
+
case "running":
|
|
229
562
|
case "in_progress":
|
|
230
563
|
return "in-progress";
|
|
231
564
|
case "blocked":
|
|
@@ -244,6 +577,8 @@ function statusLabelFor(status) {
|
|
|
244
577
|
return "under-review";
|
|
245
578
|
case "needs_attention":
|
|
246
579
|
return "blocked";
|
|
580
|
+
case "closed":
|
|
581
|
+
case "completed":
|
|
247
582
|
case "open":
|
|
248
583
|
return null;
|
|
249
584
|
default:
|
|
@@ -252,11 +587,13 @@ function statusLabelFor(status) {
|
|
|
252
587
|
}
|
|
253
588
|
function rigStatusLabelFor(status) {
|
|
254
589
|
switch (status) {
|
|
590
|
+
case "running":
|
|
255
591
|
case "in_progress":
|
|
256
592
|
return "rig:running";
|
|
257
593
|
case "under_review":
|
|
258
594
|
return "rig:pr-open";
|
|
259
595
|
case "closed":
|
|
596
|
+
case "completed":
|
|
260
597
|
return "rig:done";
|
|
261
598
|
case "ci_fixing":
|
|
262
599
|
return "rig:ci-fixing";
|
|
@@ -274,9 +611,10 @@ function rigStatusLabelFor(status) {
|
|
|
274
611
|
return null;
|
|
275
612
|
}
|
|
276
613
|
}
|
|
277
|
-
function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
278
|
-
const targetLabel = status === "closed" ? null : statusLabelFor(status);
|
|
614
|
+
async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
615
|
+
const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
|
|
279
616
|
const targetRigLabel = rigStatusLabelFor(status);
|
|
617
|
+
const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
|
|
280
618
|
for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
|
|
281
619
|
if (targetLabel !== null && l === targetLabel)
|
|
282
620
|
continue;
|
|
@@ -298,7 +636,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
|
298
636
|
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
|
|
299
637
|
}
|
|
300
638
|
}
|
|
301
|
-
if (status
|
|
639
|
+
if (isRunningStatus(status)) {
|
|
640
|
+
assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
|
|
641
|
+
if (shouldSyncLifecycle) {
|
|
642
|
+
upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
|
|
643
|
+
status: "running",
|
|
644
|
+
summary: "Rig run started."
|
|
645
|
+
}), extraEnv, timeoutMs);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (shouldSyncLifecycle) {
|
|
649
|
+
await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
|
|
650
|
+
}
|
|
651
|
+
if (status === "closed" || status === "completed") {
|
|
302
652
|
runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
|
|
303
653
|
}
|
|
304
654
|
}
|
|
@@ -368,11 +718,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
|
|
|
368
718
|
} catch {}
|
|
369
719
|
}
|
|
370
720
|
}
|
|
371
|
-
function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
|
|
721
|
+
async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
372
722
|
if (update.status) {
|
|
373
|
-
applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
|
|
723
|
+
await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
|
|
374
724
|
}
|
|
375
|
-
if (update.comment?.trim()) {
|
|
725
|
+
if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
|
|
376
726
|
if (isRigStickyStatusComment(update.comment)) {
|
|
377
727
|
upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
|
|
378
728
|
} else {
|
|
@@ -398,6 +748,17 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
398
748
|
const spawnFn = opts.spawn ?? spawnSync;
|
|
399
749
|
const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
|
|
400
750
|
const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
|
|
751
|
+
const issueUpdates = issueUpdatesMode(opts.issueUpdates);
|
|
752
|
+
async function issueToTaskWithOptionalNativeDependencies(issue, env) {
|
|
753
|
+
if (!opts.useNativeDependencies)
|
|
754
|
+
return issueToTask(issue, repo);
|
|
755
|
+
const nativeDependencies = await readNativeDependenciesForIssue({
|
|
756
|
+
issue,
|
|
757
|
+
repo,
|
|
758
|
+
fetchGraphQL: ghGraphQLFetch(bin, spawnFn, env, timeoutMs)
|
|
759
|
+
});
|
|
760
|
+
return issueToTask(issue, repo, nativeDependencies);
|
|
761
|
+
}
|
|
401
762
|
return {
|
|
402
763
|
id: "std:github-issues",
|
|
403
764
|
kind: "github-issues",
|
|
@@ -424,12 +785,13 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
424
785
|
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
786
|
}
|
|
426
787
|
const issues = rawIssues.filter((issue) => !issue.pull_request);
|
|
427
|
-
return issues.map((
|
|
788
|
+
return Promise.all(issues.map((issue) => issueToTaskWithOptionalNativeDependencies(issue, env)));
|
|
428
789
|
},
|
|
429
790
|
async get(id) {
|
|
791
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
792
|
+
let issue;
|
|
430
793
|
try {
|
|
431
|
-
|
|
432
|
-
const issue = runGh(bin, [
|
|
794
|
+
issue = runGh(bin, [
|
|
433
795
|
"issue",
|
|
434
796
|
"view",
|
|
435
797
|
String(id),
|
|
@@ -438,19 +800,23 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
438
800
|
"--json",
|
|
439
801
|
"number,title,body,labels,state,url,assignees,id"
|
|
440
802
|
], spawnFn, env, timeoutMs);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
803
|
+
} catch (error) {
|
|
804
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
805
|
+
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)) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
throw new Error(`Failed to read task ${id} from GitHub repo ${repo}: ${detail}`);
|
|
444
809
|
}
|
|
810
|
+
return issueToTaskWithOptionalNativeDependencies(issue, env);
|
|
445
811
|
},
|
|
446
812
|
async updateStatus(id, status) {
|
|
447
813
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
448
|
-
applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
|
|
814
|
+
await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
449
815
|
notifyTaskChanged(opts.onTaskChanged, repo, id, status);
|
|
450
816
|
},
|
|
451
817
|
async updateTask(id, update) {
|
|
452
818
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
453
|
-
applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
|
|
819
|
+
await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
454
820
|
notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
|
|
455
821
|
},
|
|
456
822
|
async addLabels(id, labels) {
|
|
@@ -465,6 +831,7 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
465
831
|
},
|
|
466
832
|
async createIssue(input) {
|
|
467
833
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
834
|
+
const body = input.body ?? "";
|
|
468
835
|
const args = [
|
|
469
836
|
"api",
|
|
470
837
|
"-X",
|
|
@@ -473,12 +840,31 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
473
840
|
"-f",
|
|
474
841
|
`title=${input.title}`,
|
|
475
842
|
"-f",
|
|
476
|
-
`body=${
|
|
843
|
+
`body=${body}`,
|
|
477
844
|
...(input.labels ?? []).flatMap((label) => ["-f", `labels[]=${label}`])
|
|
478
845
|
];
|
|
479
846
|
const issue = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
480
847
|
notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
|
|
481
|
-
return issueToTask(issue, repo);
|
|
848
|
+
return issueToTask({ ...issue, body: issue.body ?? body }, repo);
|
|
849
|
+
},
|
|
850
|
+
async create(input) {
|
|
851
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
852
|
+
const body = bodyForCreatedTask(input);
|
|
853
|
+
const args = [
|
|
854
|
+
"api",
|
|
855
|
+
"-X",
|
|
856
|
+
"POST",
|
|
857
|
+
`repos/${repo}/issues`,
|
|
858
|
+
"-f",
|
|
859
|
+
`title=${input.title}`,
|
|
860
|
+
"-f",
|
|
861
|
+
`body=${body}`,
|
|
862
|
+
"-f",
|
|
863
|
+
"labels[]=rig:generated"
|
|
864
|
+
];
|
|
865
|
+
const issue = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
866
|
+
notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
|
|
867
|
+
return issueToTask({ ...issue, body: issue.body ?? body }, repo);
|
|
482
868
|
},
|
|
483
869
|
async getIssueBody(id) {
|
|
484
870
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|