@h-rig/standard-plugin 0.0.6-alpha.90 → 0.0.6-alpha.92
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/github-issues-source.d.ts +11 -0
- package/dist/src/github-issues-source.js +259 -22
- package/dist/src/index.js +316 -25
- package/package.json +4 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import type { RegisteredTaskSource, TaskRecord } from "@rig/contracts";
|
|
3
3
|
export type GitHubCredentialPurpose = "selected-repo" | "admin-fallback";
|
|
4
|
+
export type GitHubIssueUpdatesMode = "lifecycle" | "minimal" | "off";
|
|
4
5
|
export interface GitHubCredentialProvider {
|
|
5
6
|
resolveGitHubToken(input: {
|
|
6
7
|
owner: string;
|
|
@@ -13,6 +14,13 @@ export interface GitHubCredentialProvider {
|
|
|
13
14
|
source: "signed-in-user" | "host-admin-fallback";
|
|
14
15
|
}>;
|
|
15
16
|
}
|
|
17
|
+
export type GitHubProjectLifecycleStatus = "todo" | "running" | "prOpen" | "ciFixing" | "merging" | "done" | "needsAttention";
|
|
18
|
+
export interface GitHubProjectsOptions {
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
projectId?: string;
|
|
21
|
+
statusFieldId?: string;
|
|
22
|
+
statuses?: Partial<Record<GitHubProjectLifecycleStatus, string>>;
|
|
23
|
+
}
|
|
16
24
|
export interface GitHubIssuesOptions {
|
|
17
25
|
owner: string;
|
|
18
26
|
repo: string;
|
|
@@ -23,6 +31,7 @@ export interface GitHubIssuesOptions {
|
|
|
23
31
|
workspaceId?: string;
|
|
24
32
|
userId?: string;
|
|
25
33
|
credentialProvider?: GitHubCredentialProvider;
|
|
34
|
+
issueUpdates?: GitHubIssueUpdatesMode;
|
|
26
35
|
/** Timeout for every gh CLI call. Defaults to 15 seconds. */
|
|
27
36
|
timeoutMs?: number;
|
|
28
37
|
/** Maximum issue-list rows before Rig fails loudly instead of silently truncating. Defaults to 1,000. */
|
|
@@ -36,6 +45,8 @@ export interface GitHubIssuesOptions {
|
|
|
36
45
|
status?: string;
|
|
37
46
|
reason: "github-issue-updated";
|
|
38
47
|
}) => void;
|
|
48
|
+
/** Optional GitHub Projects (v2) status-field sync mapped from Rig task status. */
|
|
49
|
+
projects?: GitHubProjectsOptions;
|
|
39
50
|
}
|
|
40
51
|
export interface GitHubIssueCreateInput {
|
|
41
52
|
title: string;
|
|
@@ -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
|
}
|
|
@@ -44,12 +44,12 @@ function createStateGitHubCredentialProvider(options = {}) {
|
|
|
44
44
|
async resolveGitHubToken(input) {
|
|
45
45
|
const token = readToken();
|
|
46
46
|
if (input.purpose === "selected-repo") {
|
|
47
|
-
return { token: token ?? "", source: "signed-in-user" };
|
|
47
|
+
return { token: token ?? cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
48
48
|
}
|
|
49
49
|
if (token) {
|
|
50
50
|
return { token, source: "signed-in-user" };
|
|
51
51
|
}
|
|
52
|
-
const fallback = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
52
|
+
const fallback = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
53
53
|
if (!fallback) {
|
|
54
54
|
throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
|
|
55
55
|
}
|
|
@@ -183,13 +183,21 @@ function isRigStickyStatusComment(body) {
|
|
|
183
183
|
return body.includes(RIG_STATUS_COMMENT_MARKER);
|
|
184
184
|
}
|
|
185
185
|
function ghSpawnOptions(extraEnv, timeoutMs) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
return {
|
|
187
|
+
encoding: "utf-8",
|
|
188
|
+
timeout: timeoutMs,
|
|
189
|
+
env: {
|
|
190
|
+
...process.env,
|
|
191
|
+
...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
|
|
192
|
+
...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
|
|
193
|
+
...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {},
|
|
194
|
+
...extraEnv ?? {}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
189
197
|
}
|
|
190
198
|
function credentialEnv(token) {
|
|
191
199
|
const clean = token?.trim() ?? "";
|
|
192
|
-
return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
|
|
200
|
+
return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
|
|
193
201
|
}
|
|
194
202
|
async function resolveCredentialEnv(opts, purpose) {
|
|
195
203
|
if (!opts.credentialProvider)
|
|
@@ -204,28 +212,239 @@ async function resolveCredentialEnv(opts, purpose) {
|
|
|
204
212
|
const resolved = await opts.credentialProvider.resolveGitHubToken(input);
|
|
205
213
|
return credentialEnv(resolved.token);
|
|
206
214
|
}
|
|
215
|
+
function tokenDiagnostic(value) {
|
|
216
|
+
const clean = value?.trim() ?? "";
|
|
217
|
+
return clean ? `present(len=${clean.length})` : "missing";
|
|
218
|
+
}
|
|
207
219
|
function runGh(bin, args, spawn, extraEnv, timeoutMs) {
|
|
208
|
-
const
|
|
209
|
-
|
|
220
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
221
|
+
const res = spawn(bin, [...args], options);
|
|
222
|
+
assertGhSuccess(args, res, options.env);
|
|
210
223
|
if (!res.stdout || res.stdout.trim() === "")
|
|
211
224
|
return [];
|
|
212
225
|
return JSON.parse(res.stdout);
|
|
213
226
|
}
|
|
214
227
|
function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
|
|
215
|
-
const
|
|
216
|
-
|
|
228
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
229
|
+
const res = spawn(bin, [...args], options);
|
|
230
|
+
assertGhSuccess(args, res, options.env);
|
|
217
231
|
}
|
|
218
|
-
function assertGhSuccess(args, res) {
|
|
232
|
+
function assertGhSuccess(args, res, env) {
|
|
219
233
|
if (res.error) {
|
|
220
234
|
const msg = res.error.message ?? String(res.error);
|
|
221
235
|
throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
|
|
222
236
|
}
|
|
223
237
|
if (res.status !== 0) {
|
|
224
|
-
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
238
|
+
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
239
|
+
[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)}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
var DEFAULT_PROJECT_STATUSES = {
|
|
243
|
+
todo: "Todo",
|
|
244
|
+
running: "In Progress",
|
|
245
|
+
prOpen: "In Review",
|
|
246
|
+
ciFixing: "In Review",
|
|
247
|
+
merging: "In Review",
|
|
248
|
+
done: "Done",
|
|
249
|
+
needsAttention: "Needs Attention"
|
|
250
|
+
};
|
|
251
|
+
function asProjectRecord(value) {
|
|
252
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
253
|
+
}
|
|
254
|
+
function projectString(value) {
|
|
255
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
256
|
+
}
|
|
257
|
+
function projectLifecycleStatusForTaskStatus(status) {
|
|
258
|
+
const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
|
|
259
|
+
switch (normalized) {
|
|
260
|
+
case "draft":
|
|
261
|
+
case "open":
|
|
262
|
+
case "queued":
|
|
263
|
+
case "ready":
|
|
264
|
+
return "todo";
|
|
265
|
+
case "running":
|
|
266
|
+
case "in_progress":
|
|
267
|
+
return "running";
|
|
268
|
+
case "under_review":
|
|
269
|
+
case "review":
|
|
270
|
+
case "pr_open":
|
|
271
|
+
return "prOpen";
|
|
272
|
+
case "ci_fixing":
|
|
273
|
+
case "fixing":
|
|
274
|
+
return "ciFixing";
|
|
275
|
+
case "merging":
|
|
276
|
+
case "merge":
|
|
277
|
+
return "merging";
|
|
278
|
+
case "closed":
|
|
279
|
+
case "completed":
|
|
280
|
+
case "done":
|
|
281
|
+
return "done";
|
|
282
|
+
case "blocked":
|
|
283
|
+
case "cancelled":
|
|
284
|
+
case "failed":
|
|
285
|
+
case "needs_attention":
|
|
286
|
+
return "needsAttention";
|
|
287
|
+
default:
|
|
288
|
+
return null;
|
|
225
289
|
}
|
|
226
290
|
}
|
|
291
|
+
function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
|
|
292
|
+
return async (query, variables) => {
|
|
293
|
+
const args = ["api", "graphql", "-f", `query=${query}`];
|
|
294
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
295
|
+
if (value === undefined || value === null)
|
|
296
|
+
continue;
|
|
297
|
+
args.push("-f", `${key}=${String(value)}`);
|
|
298
|
+
}
|
|
299
|
+
const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
|
|
300
|
+
return asProjectRecord(response)?.data ?? response;
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function projectStatusFieldFrom(data, projectId) {
|
|
304
|
+
const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
|
|
305
|
+
for (const node of Array.isArray(fields) ? fields : []) {
|
|
306
|
+
const record = asProjectRecord(node);
|
|
307
|
+
if (projectString(record?.name)?.toLowerCase() !== "status")
|
|
308
|
+
continue;
|
|
309
|
+
const id = projectString(record?.id);
|
|
310
|
+
if (!id)
|
|
311
|
+
continue;
|
|
312
|
+
const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
|
|
313
|
+
const optionRecord = asProjectRecord(option);
|
|
314
|
+
const optionId = projectString(optionRecord?.id);
|
|
315
|
+
const name = projectString(optionRecord?.name);
|
|
316
|
+
return optionId && name ? [{ id: optionId, name }] : [];
|
|
317
|
+
}) : [];
|
|
318
|
+
return { id, name: "Status", options };
|
|
319
|
+
}
|
|
320
|
+
throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
|
|
321
|
+
}
|
|
322
|
+
async function resolveProjectStatusField(input) {
|
|
323
|
+
const query = `
|
|
324
|
+
query RigProjectStatusField($projectId: ID!) {
|
|
325
|
+
node(id: $projectId) {
|
|
326
|
+
... on ProjectV2 {
|
|
327
|
+
fields(first: 50) {
|
|
328
|
+
nodes {
|
|
329
|
+
... on ProjectV2FieldCommon { id name }
|
|
330
|
+
... on ProjectV2SingleSelectField { id name options { id name } }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
`;
|
|
337
|
+
return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
|
|
338
|
+
}
|
|
339
|
+
async function ensureIssueProjectItem(input) {
|
|
340
|
+
const query = `
|
|
341
|
+
query RigFindProjectIssueItem($projectId: ID!) {
|
|
342
|
+
node(id: $projectId) {
|
|
343
|
+
... on ProjectV2 {
|
|
344
|
+
items(first: 100) { nodes { id content { ... on Issue { id } } } }
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
`;
|
|
349
|
+
const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
|
|
350
|
+
const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
|
|
351
|
+
for (const node of Array.isArray(nodes) ? nodes : []) {
|
|
352
|
+
const record = asProjectRecord(node);
|
|
353
|
+
const content = asProjectRecord(record?.content);
|
|
354
|
+
if (projectString(content?.id) === input.issueNodeId) {
|
|
355
|
+
const id2 = projectString(record?.id);
|
|
356
|
+
if (id2)
|
|
357
|
+
return { id: id2, created: false };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const mutation = `
|
|
361
|
+
mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
|
|
362
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
|
|
363
|
+
}
|
|
364
|
+
`;
|
|
365
|
+
const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
|
|
366
|
+
const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
|
|
367
|
+
const id = projectString(asProjectRecord(addResult?.item)?.id);
|
|
368
|
+
if (!id)
|
|
369
|
+
throw new Error("GitHub Project item creation did not return an item id.");
|
|
370
|
+
return { id, created: true };
|
|
371
|
+
}
|
|
372
|
+
async function updateIssueProjectStatus(input) {
|
|
373
|
+
const mutation = `
|
|
374
|
+
mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
375
|
+
updateProjectV2ItemFieldValue(input: {
|
|
376
|
+
projectId: $projectId,
|
|
377
|
+
itemId: $itemId,
|
|
378
|
+
fieldId: $fieldId,
|
|
379
|
+
value: { singleSelectOptionId: $optionId }
|
|
380
|
+
}) { projectV2Item { id } }
|
|
381
|
+
}
|
|
382
|
+
`;
|
|
383
|
+
await input.fetchGraphQL(mutation, {
|
|
384
|
+
projectId: input.projectId,
|
|
385
|
+
itemId: input.itemId,
|
|
386
|
+
fieldId: input.fieldId,
|
|
387
|
+
optionId: input.optionId
|
|
388
|
+
}, input.token);
|
|
389
|
+
}
|
|
390
|
+
function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
|
|
391
|
+
const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
|
|
392
|
+
return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
|
|
393
|
+
}
|
|
394
|
+
async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
|
|
395
|
+
if (!projects?.enabled)
|
|
396
|
+
return;
|
|
397
|
+
const projectId = projectString(projects.projectId);
|
|
398
|
+
if (!projectId)
|
|
399
|
+
throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
|
|
400
|
+
const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
|
|
401
|
+
if (!lifecycleStatus)
|
|
402
|
+
return;
|
|
403
|
+
const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
|
|
404
|
+
if (!issueNodeId)
|
|
405
|
+
throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
|
|
406
|
+
const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
|
|
407
|
+
const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
|
|
408
|
+
const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
|
|
409
|
+
const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
|
|
410
|
+
if (!option)
|
|
411
|
+
throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
|
|
412
|
+
const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
|
|
413
|
+
await updateIssueProjectStatus({
|
|
414
|
+
projectId,
|
|
415
|
+
itemId: item.id,
|
|
416
|
+
fieldId: projectString(projects.statusFieldId) ?? field.id,
|
|
417
|
+
optionId: option.id,
|
|
418
|
+
token: "gh-cli",
|
|
419
|
+
fetchGraphQL
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
|
|
423
|
+
function normalizeTaskStatusToken(status) {
|
|
424
|
+
return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
|
|
425
|
+
}
|
|
426
|
+
function issueUpdatesMode(value) {
|
|
427
|
+
return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
|
|
428
|
+
}
|
|
429
|
+
function isTerminalTaskStatus(status) {
|
|
430
|
+
return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
|
|
431
|
+
}
|
|
432
|
+
function shouldWriteIssueUpdate(mode, status) {
|
|
433
|
+
if (mode === "off")
|
|
434
|
+
return false;
|
|
435
|
+
if (mode === "lifecycle")
|
|
436
|
+
return true;
|
|
437
|
+
return isTerminalTaskStatus(status);
|
|
438
|
+
}
|
|
439
|
+
function isRunningStatus(status) {
|
|
440
|
+
return normalizeTaskStatusToken(status) === "running";
|
|
441
|
+
}
|
|
442
|
+
function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
|
|
443
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
|
|
444
|
+
}
|
|
227
445
|
function statusLabelFor(status) {
|
|
228
446
|
switch (status) {
|
|
447
|
+
case "running":
|
|
229
448
|
case "in_progress":
|
|
230
449
|
return "in-progress";
|
|
231
450
|
case "blocked":
|
|
@@ -244,6 +463,8 @@ function statusLabelFor(status) {
|
|
|
244
463
|
return "under-review";
|
|
245
464
|
case "needs_attention":
|
|
246
465
|
return "blocked";
|
|
466
|
+
case "closed":
|
|
467
|
+
case "completed":
|
|
247
468
|
case "open":
|
|
248
469
|
return null;
|
|
249
470
|
default:
|
|
@@ -252,11 +473,13 @@ function statusLabelFor(status) {
|
|
|
252
473
|
}
|
|
253
474
|
function rigStatusLabelFor(status) {
|
|
254
475
|
switch (status) {
|
|
476
|
+
case "running":
|
|
255
477
|
case "in_progress":
|
|
256
478
|
return "rig:running";
|
|
257
479
|
case "under_review":
|
|
258
480
|
return "rig:pr-open";
|
|
259
481
|
case "closed":
|
|
482
|
+
case "completed":
|
|
260
483
|
return "rig:done";
|
|
261
484
|
case "ci_fixing":
|
|
262
485
|
return "rig:ci-fixing";
|
|
@@ -274,9 +497,10 @@ function rigStatusLabelFor(status) {
|
|
|
274
497
|
return null;
|
|
275
498
|
}
|
|
276
499
|
}
|
|
277
|
-
function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
278
|
-
const targetLabel = status === "closed" ? null : statusLabelFor(status);
|
|
500
|
+
async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
501
|
+
const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
|
|
279
502
|
const targetRigLabel = rigStatusLabelFor(status);
|
|
503
|
+
const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
|
|
280
504
|
for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
|
|
281
505
|
if (targetLabel !== null && l === targetLabel)
|
|
282
506
|
continue;
|
|
@@ -298,7 +522,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
|
298
522
|
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
|
|
299
523
|
}
|
|
300
524
|
}
|
|
301
|
-
if (status
|
|
525
|
+
if (isRunningStatus(status)) {
|
|
526
|
+
assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
|
|
527
|
+
if (shouldSyncLifecycle) {
|
|
528
|
+
upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
|
|
529
|
+
status: "running",
|
|
530
|
+
summary: "Rig run started."
|
|
531
|
+
}), extraEnv, timeoutMs);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (shouldSyncLifecycle) {
|
|
535
|
+
await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
|
|
536
|
+
}
|
|
537
|
+
if (status === "closed" || status === "completed") {
|
|
302
538
|
runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
|
|
303
539
|
}
|
|
304
540
|
}
|
|
@@ -368,11 +604,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
|
|
|
368
604
|
} catch {}
|
|
369
605
|
}
|
|
370
606
|
}
|
|
371
|
-
function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
|
|
607
|
+
async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
372
608
|
if (update.status) {
|
|
373
|
-
applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
|
|
609
|
+
await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
|
|
374
610
|
}
|
|
375
|
-
if (update.comment?.trim()) {
|
|
611
|
+
if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
|
|
376
612
|
if (isRigStickyStatusComment(update.comment)) {
|
|
377
613
|
upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
|
|
378
614
|
} else {
|
|
@@ -398,6 +634,7 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
398
634
|
const spawnFn = opts.spawn ?? spawnSync;
|
|
399
635
|
const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
|
|
400
636
|
const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
|
|
637
|
+
const issueUpdates = issueUpdatesMode(opts.issueUpdates);
|
|
401
638
|
return {
|
|
402
639
|
id: "std:github-issues",
|
|
403
640
|
kind: "github-issues",
|
|
@@ -445,12 +682,12 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
445
682
|
},
|
|
446
683
|
async updateStatus(id, status) {
|
|
447
684
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
448
|
-
applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
|
|
685
|
+
await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
449
686
|
notifyTaskChanged(opts.onTaskChanged, repo, id, status);
|
|
450
687
|
},
|
|
451
688
|
async updateTask(id, update) {
|
|
452
689
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
453
|
-
applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
|
|
690
|
+
await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
454
691
|
notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
|
|
455
692
|
},
|
|
456
693
|
async addLabels(id, labels) {
|
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/standard-plugin/src/index.ts
|
|
3
|
+
import { resolve as resolve3 } from "path";
|
|
3
4
|
import { definePlugin } from "@rig/core";
|
|
4
5
|
|
|
5
6
|
// packages/standard-plugin/src/github-issues-source.ts
|
|
@@ -14,9 +15,9 @@ function createEnvGitHubCredentialProvider() {
|
|
|
14
15
|
return {
|
|
15
16
|
async resolveGitHubToken(input) {
|
|
16
17
|
if (input.purpose === "selected-repo") {
|
|
17
|
-
return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
18
|
+
return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
18
19
|
}
|
|
19
|
-
const token = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
20
|
+
const token = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
20
21
|
if (!token) {
|
|
21
22
|
throw new Error("No host GitHub token is configured for admin fallback.");
|
|
22
23
|
}
|
|
@@ -47,12 +48,12 @@ function createStateGitHubCredentialProvider(options = {}) {
|
|
|
47
48
|
async resolveGitHubToken(input) {
|
|
48
49
|
const token = readToken();
|
|
49
50
|
if (input.purpose === "selected-repo") {
|
|
50
|
-
return { token: token ?? "", source: "signed-in-user" };
|
|
51
|
+
return { token: token ?? cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
51
52
|
}
|
|
52
53
|
if (token) {
|
|
53
54
|
return { token, source: "signed-in-user" };
|
|
54
55
|
}
|
|
55
|
-
const fallback = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
56
|
+
const fallback = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
56
57
|
if (!fallback) {
|
|
57
58
|
throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
|
|
58
59
|
}
|
|
@@ -166,17 +167,41 @@ ${rendered}
|
|
|
166
167
|
` : `${rendered}
|
|
167
168
|
`;
|
|
168
169
|
}
|
|
170
|
+
function buildRigStickyStatusComment(input) {
|
|
171
|
+
const lines = [
|
|
172
|
+
RIG_STATUS_COMMENT_MARKER,
|
|
173
|
+
`### Rig status: ${input.status}`,
|
|
174
|
+
"",
|
|
175
|
+
input.summary
|
|
176
|
+
];
|
|
177
|
+
if (input.runId)
|
|
178
|
+
lines.push("", `- Run: ${input.runId}`);
|
|
179
|
+
if (input.prUrl)
|
|
180
|
+
lines.push(`- PR: ${input.prUrl}`);
|
|
181
|
+
for (const detail of input.details ?? [])
|
|
182
|
+
lines.push(`- ${detail}`);
|
|
183
|
+
return lines.join(`
|
|
184
|
+
`);
|
|
185
|
+
}
|
|
169
186
|
function isRigStickyStatusComment(body) {
|
|
170
187
|
return body.includes(RIG_STATUS_COMMENT_MARKER);
|
|
171
188
|
}
|
|
172
189
|
function ghSpawnOptions(extraEnv, timeoutMs) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
190
|
+
return {
|
|
191
|
+
encoding: "utf-8",
|
|
192
|
+
timeout: timeoutMs,
|
|
193
|
+
env: {
|
|
194
|
+
...process.env,
|
|
195
|
+
...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
|
|
196
|
+
...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
|
|
197
|
+
...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {},
|
|
198
|
+
...extraEnv ?? {}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
176
201
|
}
|
|
177
202
|
function credentialEnv(token) {
|
|
178
203
|
const clean = token?.trim() ?? "";
|
|
179
|
-
return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
|
|
204
|
+
return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
|
|
180
205
|
}
|
|
181
206
|
async function resolveCredentialEnv(opts, purpose) {
|
|
182
207
|
if (!opts.credentialProvider)
|
|
@@ -191,28 +216,239 @@ async function resolveCredentialEnv(opts, purpose) {
|
|
|
191
216
|
const resolved = await opts.credentialProvider.resolveGitHubToken(input);
|
|
192
217
|
return credentialEnv(resolved.token);
|
|
193
218
|
}
|
|
219
|
+
function tokenDiagnostic(value) {
|
|
220
|
+
const clean = value?.trim() ?? "";
|
|
221
|
+
return clean ? `present(len=${clean.length})` : "missing";
|
|
222
|
+
}
|
|
194
223
|
function runGh(bin, args, spawn, extraEnv, timeoutMs) {
|
|
195
|
-
const
|
|
196
|
-
|
|
224
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
225
|
+
const res = spawn(bin, [...args], options);
|
|
226
|
+
assertGhSuccess(args, res, options.env);
|
|
197
227
|
if (!res.stdout || res.stdout.trim() === "")
|
|
198
228
|
return [];
|
|
199
229
|
return JSON.parse(res.stdout);
|
|
200
230
|
}
|
|
201
231
|
function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
|
|
202
|
-
const
|
|
203
|
-
|
|
232
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
233
|
+
const res = spawn(bin, [...args], options);
|
|
234
|
+
assertGhSuccess(args, res, options.env);
|
|
204
235
|
}
|
|
205
|
-
function assertGhSuccess(args, res) {
|
|
236
|
+
function assertGhSuccess(args, res, env) {
|
|
206
237
|
if (res.error) {
|
|
207
238
|
const msg = res.error.message ?? String(res.error);
|
|
208
239
|
throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
|
|
209
240
|
}
|
|
210
241
|
if (res.status !== 0) {
|
|
211
|
-
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
242
|
+
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
243
|
+
[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)}`);
|
|
212
244
|
}
|
|
213
245
|
}
|
|
246
|
+
var DEFAULT_PROJECT_STATUSES = {
|
|
247
|
+
todo: "Todo",
|
|
248
|
+
running: "In Progress",
|
|
249
|
+
prOpen: "In Review",
|
|
250
|
+
ciFixing: "In Review",
|
|
251
|
+
merging: "In Review",
|
|
252
|
+
done: "Done",
|
|
253
|
+
needsAttention: "Needs Attention"
|
|
254
|
+
};
|
|
255
|
+
function asProjectRecord(value) {
|
|
256
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
257
|
+
}
|
|
258
|
+
function projectString(value) {
|
|
259
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
260
|
+
}
|
|
261
|
+
function projectLifecycleStatusForTaskStatus(status) {
|
|
262
|
+
const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
|
|
263
|
+
switch (normalized) {
|
|
264
|
+
case "draft":
|
|
265
|
+
case "open":
|
|
266
|
+
case "queued":
|
|
267
|
+
case "ready":
|
|
268
|
+
return "todo";
|
|
269
|
+
case "running":
|
|
270
|
+
case "in_progress":
|
|
271
|
+
return "running";
|
|
272
|
+
case "under_review":
|
|
273
|
+
case "review":
|
|
274
|
+
case "pr_open":
|
|
275
|
+
return "prOpen";
|
|
276
|
+
case "ci_fixing":
|
|
277
|
+
case "fixing":
|
|
278
|
+
return "ciFixing";
|
|
279
|
+
case "merging":
|
|
280
|
+
case "merge":
|
|
281
|
+
return "merging";
|
|
282
|
+
case "closed":
|
|
283
|
+
case "completed":
|
|
284
|
+
case "done":
|
|
285
|
+
return "done";
|
|
286
|
+
case "blocked":
|
|
287
|
+
case "cancelled":
|
|
288
|
+
case "failed":
|
|
289
|
+
case "needs_attention":
|
|
290
|
+
return "needsAttention";
|
|
291
|
+
default:
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
|
|
296
|
+
return async (query, variables) => {
|
|
297
|
+
const args = ["api", "graphql", "-f", `query=${query}`];
|
|
298
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
299
|
+
if (value === undefined || value === null)
|
|
300
|
+
continue;
|
|
301
|
+
args.push("-f", `${key}=${String(value)}`);
|
|
302
|
+
}
|
|
303
|
+
const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
|
|
304
|
+
return asProjectRecord(response)?.data ?? response;
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function projectStatusFieldFrom(data, projectId) {
|
|
308
|
+
const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
|
|
309
|
+
for (const node of Array.isArray(fields) ? fields : []) {
|
|
310
|
+
const record = asProjectRecord(node);
|
|
311
|
+
if (projectString(record?.name)?.toLowerCase() !== "status")
|
|
312
|
+
continue;
|
|
313
|
+
const id = projectString(record?.id);
|
|
314
|
+
if (!id)
|
|
315
|
+
continue;
|
|
316
|
+
const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
|
|
317
|
+
const optionRecord = asProjectRecord(option);
|
|
318
|
+
const optionId = projectString(optionRecord?.id);
|
|
319
|
+
const name = projectString(optionRecord?.name);
|
|
320
|
+
return optionId && name ? [{ id: optionId, name }] : [];
|
|
321
|
+
}) : [];
|
|
322
|
+
return { id, name: "Status", options };
|
|
323
|
+
}
|
|
324
|
+
throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
|
|
325
|
+
}
|
|
326
|
+
async function resolveProjectStatusField(input) {
|
|
327
|
+
const query = `
|
|
328
|
+
query RigProjectStatusField($projectId: ID!) {
|
|
329
|
+
node(id: $projectId) {
|
|
330
|
+
... on ProjectV2 {
|
|
331
|
+
fields(first: 50) {
|
|
332
|
+
nodes {
|
|
333
|
+
... on ProjectV2FieldCommon { id name }
|
|
334
|
+
... on ProjectV2SingleSelectField { id name options { id name } }
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
`;
|
|
341
|
+
return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
|
|
342
|
+
}
|
|
343
|
+
async function ensureIssueProjectItem(input) {
|
|
344
|
+
const query = `
|
|
345
|
+
query RigFindProjectIssueItem($projectId: ID!) {
|
|
346
|
+
node(id: $projectId) {
|
|
347
|
+
... on ProjectV2 {
|
|
348
|
+
items(first: 100) { nodes { id content { ... on Issue { id } } } }
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
`;
|
|
353
|
+
const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
|
|
354
|
+
const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
|
|
355
|
+
for (const node of Array.isArray(nodes) ? nodes : []) {
|
|
356
|
+
const record = asProjectRecord(node);
|
|
357
|
+
const content = asProjectRecord(record?.content);
|
|
358
|
+
if (projectString(content?.id) === input.issueNodeId) {
|
|
359
|
+
const id2 = projectString(record?.id);
|
|
360
|
+
if (id2)
|
|
361
|
+
return { id: id2, created: false };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const mutation = `
|
|
365
|
+
mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
|
|
366
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
|
|
367
|
+
}
|
|
368
|
+
`;
|
|
369
|
+
const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
|
|
370
|
+
const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
|
|
371
|
+
const id = projectString(asProjectRecord(addResult?.item)?.id);
|
|
372
|
+
if (!id)
|
|
373
|
+
throw new Error("GitHub Project item creation did not return an item id.");
|
|
374
|
+
return { id, created: true };
|
|
375
|
+
}
|
|
376
|
+
async function updateIssueProjectStatus(input) {
|
|
377
|
+
const mutation = `
|
|
378
|
+
mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
379
|
+
updateProjectV2ItemFieldValue(input: {
|
|
380
|
+
projectId: $projectId,
|
|
381
|
+
itemId: $itemId,
|
|
382
|
+
fieldId: $fieldId,
|
|
383
|
+
value: { singleSelectOptionId: $optionId }
|
|
384
|
+
}) { projectV2Item { id } }
|
|
385
|
+
}
|
|
386
|
+
`;
|
|
387
|
+
await input.fetchGraphQL(mutation, {
|
|
388
|
+
projectId: input.projectId,
|
|
389
|
+
itemId: input.itemId,
|
|
390
|
+
fieldId: input.fieldId,
|
|
391
|
+
optionId: input.optionId
|
|
392
|
+
}, input.token);
|
|
393
|
+
}
|
|
394
|
+
function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
|
|
395
|
+
const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
|
|
396
|
+
return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
|
|
397
|
+
}
|
|
398
|
+
async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
|
|
399
|
+
if (!projects?.enabled)
|
|
400
|
+
return;
|
|
401
|
+
const projectId = projectString(projects.projectId);
|
|
402
|
+
if (!projectId)
|
|
403
|
+
throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
|
|
404
|
+
const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
|
|
405
|
+
if (!lifecycleStatus)
|
|
406
|
+
return;
|
|
407
|
+
const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
|
|
408
|
+
if (!issueNodeId)
|
|
409
|
+
throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
|
|
410
|
+
const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
|
|
411
|
+
const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
|
|
412
|
+
const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
|
|
413
|
+
const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
|
|
414
|
+
if (!option)
|
|
415
|
+
throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
|
|
416
|
+
const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
|
|
417
|
+
await updateIssueProjectStatus({
|
|
418
|
+
projectId,
|
|
419
|
+
itemId: item.id,
|
|
420
|
+
fieldId: projectString(projects.statusFieldId) ?? field.id,
|
|
421
|
+
optionId: option.id,
|
|
422
|
+
token: "gh-cli",
|
|
423
|
+
fetchGraphQL
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
|
|
427
|
+
function normalizeTaskStatusToken(status) {
|
|
428
|
+
return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
|
|
429
|
+
}
|
|
430
|
+
function issueUpdatesMode(value) {
|
|
431
|
+
return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
|
|
432
|
+
}
|
|
433
|
+
function isTerminalTaskStatus(status) {
|
|
434
|
+
return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
|
|
435
|
+
}
|
|
436
|
+
function shouldWriteIssueUpdate(mode, status) {
|
|
437
|
+
if (mode === "off")
|
|
438
|
+
return false;
|
|
439
|
+
if (mode === "lifecycle")
|
|
440
|
+
return true;
|
|
441
|
+
return isTerminalTaskStatus(status);
|
|
442
|
+
}
|
|
443
|
+
function isRunningStatus(status) {
|
|
444
|
+
return normalizeTaskStatusToken(status) === "running";
|
|
445
|
+
}
|
|
446
|
+
function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
|
|
447
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
|
|
448
|
+
}
|
|
214
449
|
function statusLabelFor(status) {
|
|
215
450
|
switch (status) {
|
|
451
|
+
case "running":
|
|
216
452
|
case "in_progress":
|
|
217
453
|
return "in-progress";
|
|
218
454
|
case "blocked":
|
|
@@ -231,6 +467,8 @@ function statusLabelFor(status) {
|
|
|
231
467
|
return "under-review";
|
|
232
468
|
case "needs_attention":
|
|
233
469
|
return "blocked";
|
|
470
|
+
case "closed":
|
|
471
|
+
case "completed":
|
|
234
472
|
case "open":
|
|
235
473
|
return null;
|
|
236
474
|
default:
|
|
@@ -239,11 +477,13 @@ function statusLabelFor(status) {
|
|
|
239
477
|
}
|
|
240
478
|
function rigStatusLabelFor(status) {
|
|
241
479
|
switch (status) {
|
|
480
|
+
case "running":
|
|
242
481
|
case "in_progress":
|
|
243
482
|
return "rig:running";
|
|
244
483
|
case "under_review":
|
|
245
484
|
return "rig:pr-open";
|
|
246
485
|
case "closed":
|
|
486
|
+
case "completed":
|
|
247
487
|
return "rig:done";
|
|
248
488
|
case "ci_fixing":
|
|
249
489
|
return "rig:ci-fixing";
|
|
@@ -261,9 +501,10 @@ function rigStatusLabelFor(status) {
|
|
|
261
501
|
return null;
|
|
262
502
|
}
|
|
263
503
|
}
|
|
264
|
-
function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
265
|
-
const targetLabel = status === "closed" ? null : statusLabelFor(status);
|
|
504
|
+
async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
505
|
+
const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
|
|
266
506
|
const targetRigLabel = rigStatusLabelFor(status);
|
|
507
|
+
const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
|
|
267
508
|
for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
|
|
268
509
|
if (targetLabel !== null && l === targetLabel)
|
|
269
510
|
continue;
|
|
@@ -285,7 +526,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
|
285
526
|
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
|
|
286
527
|
}
|
|
287
528
|
}
|
|
288
|
-
if (status
|
|
529
|
+
if (isRunningStatus(status)) {
|
|
530
|
+
assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
|
|
531
|
+
if (shouldSyncLifecycle) {
|
|
532
|
+
upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
|
|
533
|
+
status: "running",
|
|
534
|
+
summary: "Rig run started."
|
|
535
|
+
}), extraEnv, timeoutMs);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (shouldSyncLifecycle) {
|
|
539
|
+
await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
|
|
540
|
+
}
|
|
541
|
+
if (status === "closed" || status === "completed") {
|
|
289
542
|
runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
|
|
290
543
|
}
|
|
291
544
|
}
|
|
@@ -355,11 +608,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
|
|
|
355
608
|
} catch {}
|
|
356
609
|
}
|
|
357
610
|
}
|
|
358
|
-
function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
|
|
611
|
+
async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
359
612
|
if (update.status) {
|
|
360
|
-
applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
|
|
613
|
+
await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
|
|
361
614
|
}
|
|
362
|
-
if (update.comment?.trim()) {
|
|
615
|
+
if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
|
|
363
616
|
if (isRigStickyStatusComment(update.comment)) {
|
|
364
617
|
upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
|
|
365
618
|
} else {
|
|
@@ -385,6 +638,7 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
385
638
|
const spawnFn = opts.spawn ?? spawnSync;
|
|
386
639
|
const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
|
|
387
640
|
const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
|
|
641
|
+
const issueUpdates = issueUpdatesMode(opts.issueUpdates);
|
|
388
642
|
return {
|
|
389
643
|
id: "std:github-issues",
|
|
390
644
|
kind: "github-issues",
|
|
@@ -432,12 +686,12 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
432
686
|
},
|
|
433
687
|
async updateStatus(id, status) {
|
|
434
688
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
435
|
-
applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
|
|
689
|
+
await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
436
690
|
notifyTaskChanged(opts.onTaskChanged, repo, id, status);
|
|
437
691
|
},
|
|
438
692
|
async updateTask(id, update) {
|
|
439
693
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
440
|
-
applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
|
|
694
|
+
await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
441
695
|
notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
|
|
442
696
|
},
|
|
443
697
|
async addLabels(id, labels) {
|
|
@@ -586,6 +840,40 @@ function requireStringField(config, field, kind) {
|
|
|
586
840
|
}
|
|
587
841
|
return value;
|
|
588
842
|
}
|
|
843
|
+
function isRecord(value) {
|
|
844
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
845
|
+
}
|
|
846
|
+
function optionalString(value) {
|
|
847
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
848
|
+
}
|
|
849
|
+
function parseGitHubProjectsOptions(value) {
|
|
850
|
+
if (!isRecord(value))
|
|
851
|
+
return;
|
|
852
|
+
const statusesSource = isRecord(value.statuses) ? value.statuses : undefined;
|
|
853
|
+
const statuses = {};
|
|
854
|
+
for (const key of ["todo", "running", "prOpen", "ciFixing", "merging", "done", "needsAttention"]) {
|
|
855
|
+
const status = optionalString(statusesSource?.[key]);
|
|
856
|
+
if (status)
|
|
857
|
+
statuses[key] = status;
|
|
858
|
+
}
|
|
859
|
+
const parsed = {};
|
|
860
|
+
if (typeof value.enabled === "boolean")
|
|
861
|
+
parsed.enabled = value.enabled;
|
|
862
|
+
const projectId = optionalString(value.projectId);
|
|
863
|
+
if (projectId)
|
|
864
|
+
parsed.projectId = projectId;
|
|
865
|
+
const statusFieldId = optionalString(value.statusFieldId);
|
|
866
|
+
if (statusFieldId)
|
|
867
|
+
parsed.statusFieldId = statusFieldId;
|
|
868
|
+
if (Object.keys(statuses).length > 0)
|
|
869
|
+
parsed.statuses = statuses;
|
|
870
|
+
return parsed;
|
|
871
|
+
}
|
|
872
|
+
function githubProjectsOptionsFromConfig(config, context) {
|
|
873
|
+
const rigConfig = isRecord(context?.rigConfig) ? context.rigConfig : undefined;
|
|
874
|
+
const github = isRecord(rigConfig?.github) ? rigConfig.github : undefined;
|
|
875
|
+
return parseGitHubProjectsOptions(config.options?.projects) ?? parseGitHubProjectsOptions(github?.projects);
|
|
876
|
+
}
|
|
589
877
|
function standardPlugin(opts = {}) {
|
|
590
878
|
return definePlugin({
|
|
591
879
|
name: "rig-standard",
|
|
@@ -610,13 +898,13 @@ function standardPlugin(opts = {}) {
|
|
|
610
898
|
id: "std:github-issues",
|
|
611
899
|
kind: "github-issues",
|
|
612
900
|
description: "GitHub Issues via gh CLI",
|
|
613
|
-
factory(config) {
|
|
901
|
+
factory(config, context) {
|
|
614
902
|
const options = {
|
|
615
903
|
owner: requireStringField(config, "owner", "github-issues"),
|
|
616
904
|
repo: requireStringField(config, "repo", "github-issues")
|
|
617
905
|
};
|
|
618
|
-
|
|
619
|
-
|
|
906
|
+
const credentialProviderOptions = context?.projectRoot ? { stateDir: resolve3(context.projectRoot, ".rig", "state") } : {};
|
|
907
|
+
options.credentialProvider = opts.githubCredentialProvider ?? createStateGitHubCredentialProvider(credentialProviderOptions);
|
|
620
908
|
if (opts.githubWorkspaceId)
|
|
621
909
|
options.workspaceId = opts.githubWorkspaceId;
|
|
622
910
|
if (opts.githubUserId)
|
|
@@ -638,6 +926,9 @@ function standardPlugin(opts = {}) {
|
|
|
638
926
|
const listLimit = typeof config.options?.listLimit === "number" ? config.options.listLimit : undefined;
|
|
639
927
|
if (listLimit !== undefined)
|
|
640
928
|
options.listLimit = listLimit;
|
|
929
|
+
const projects = githubProjectsOptionsFromConfig(config, context);
|
|
930
|
+
if (projects)
|
|
931
|
+
options.projects = projects;
|
|
641
932
|
return createGitHubIssuesTaskSource(options);
|
|
642
933
|
}
|
|
643
934
|
},
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-rig/standard-plugin",
|
|
3
|
-
"version": "0.0.6-alpha.
|
|
3
|
+
"version": "0.0.6-alpha.92",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Rig
|
|
5
|
+
"description": "First-party contribution bundle for Rig's OMP extension plugin graph; not a standalone plugin runtime.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"module": "./dist/src/index.js",
|
|
22
22
|
"types": "./dist/src/index.d.ts",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.
|
|
25
|
-
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.
|
|
24
|
+
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.92",
|
|
25
|
+
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.92",
|
|
26
26
|
"effect": "4.0.0-beta.78"
|
|
27
27
|
}
|
|
28
28
|
}
|