@plainconceptsplatform/workflows 1.3.1 → 1.5.0
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/catalog-installation.d.ts +2 -0
- package/dist/catalog-installation.js +44 -2
- package/dist/index.js +3 -0
- package/dist/stack-defaults.d.ts +1 -0
- package/dist/stack-defaults.js +13 -1
- package/dist/worker-env.js +2 -3
- package/loops/actions/add-issue-labels/action.yml +50 -50
- package/loops/actions/agent-output.cjs +17 -17
- package/loops/actions/apply-agent-bundle/action.yml +24 -24
- package/loops/actions/apply-agent-comments/action.yml +42 -42
- package/loops/actions/apply-agent-labels/action.yml +55 -55
- package/loops/actions/apply-agent-output/action.yml +5 -0
- package/loops/actions/cleanup-artifacts/action.yml +91 -91
- package/loops/actions/close-agent-issues/action.yml +43 -43
- package/loops/actions/create-agent-issues/action.yml +64 -3
- package/loops/actions/create-issue-comment/action.yml +29 -29
- package/loops/actions/download-agent-output/action.yml +53 -53
- package/loops/actions/housekeeping/action.yml +95 -1
- package/loops/actions/link-pr-to-issue/action.yml +40 -40
- package/loops/actions/list-open-issues/action.yml +33 -33
- package/loops/actions/load-issue-context/action.yml +45 -45
- package/loops/actions/merge-agent-pr/action.yml +49 -49
- package/loops/actions/push-agent-branch/action.yml +45 -45
- package/loops/actions/remove-issue-labels/action.yml +37 -37
- package/loops/actions/update-agent-issues/action.yml +58 -58
- package/loops/actions/validate-merge-gate-output/action.yml +5 -0
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +8 -2
- package/loops/actions/validate-refine-output/validate-refine-output.sh +30 -1
- package/loops/actions/validate-review-output/action.yml +35 -35
- package/loops/actions/validate-triage-output/action.yml +36 -36
- package/loops/actions/verify-composite-actions/action.yml +9 -9
- package/loops/actions/verify-refine-output/action.yml +9 -9
- package/loops/actions/verify-refine-output/verify-refine-output.sh +17 -0
- package/loops/actions/verify-route-matrix/action.yml +9 -9
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +221 -7
- package/loops/docs/diagrams.md +90 -12
- package/loops/scripts/compile-agent-workflows.mjs +76 -2
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/issues/bug_report.yml +109 -109
- package/loops/templates/issues/feature_request.yml +75 -75
- package/loops/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-implement.md +43 -8
- package/loops/workflows/agent-merge-gate.md +102 -6
- package/loops/workflows/agent-refine.md +152 -5
- package/loops/workflows/authorize-bot-work.yml +105 -105
- package/loops/workflows/shared/platform-defaults.md +23 -23
- package/loops/workflows/work-router.yml +345 -2
- package/package.json +1 -1
|
@@ -220,6 +220,14 @@ runs:
|
|
|
220
220
|
for (const [parent, children] of childrenOf) {
|
|
221
221
|
const open = children.filter((c) => c.state === 'open');
|
|
222
222
|
if (open.length > 0 || !issues.some((i) => i.number === parent)) continue;
|
|
223
|
+
// A stack feature issue. Its root pull request closing the issue is what ends it, not
|
|
224
|
+
// this sweep: closing the tracker here would close a feature whose root pull request
|
|
225
|
+
// is still open and holding a branch with real work on it. Refine stamps the root
|
|
226
|
+
// markers on the parent before the first child is ever handed over, so the marker
|
|
227
|
+
// is there from birth; finding none means flat mode, and the original rule applies.
|
|
228
|
+
// Everything stack-specific about these parents is decided in section 3b below,
|
|
229
|
+
// which sees the whole stack, not just the child count.
|
|
230
|
+
if (issues.some((i) => i.number === parent && /<!-- stack-root-branch: \S+ -->/.test(i.body ?? ''))) continue;
|
|
223
231
|
await act(`#${parent}: closing split parent, all ${children.length} children are done`, async () => {
|
|
224
232
|
await github.rest.issues.createComment({
|
|
225
233
|
...context.repo, issue_number: parent,
|
|
@@ -229,6 +237,87 @@ runs:
|
|
|
229
237
|
});
|
|
230
238
|
}
|
|
231
239
|
|
|
240
|
+
// The stack and branch sections both need every pull request the repository has ever
|
|
241
|
+
// seen, closed included, so it is paginated once here rather than once per section.
|
|
242
|
+
const allPulls = await github.paginate(github.rest.pulls.list, { ...context.repo, state: 'all', per_page: 100 });
|
|
243
|
+
|
|
244
|
+
// 3b. Stacks. Section 3 refused to close stack feature issues; this is what happens to
|
|
245
|
+
// them instead. The whole stack state is on the feature issue (root pull request) plus
|
|
246
|
+
// the children of the split, and both are already in memory, so a repository without
|
|
247
|
+
// stacks pays nothing: the marker test below fails first and the loop body never runs.
|
|
248
|
+
// No polling is added -- this runs on the same cadence the janitor already has, and it
|
|
249
|
+
// is the backstop for the one event the router can miss: a child issue that closes
|
|
250
|
+
// seconds after a router run already counted it as open.
|
|
251
|
+
for (const issue of issues) {
|
|
252
|
+
if (issue.pull_request) continue;
|
|
253
|
+
const marker = /<!-- stack-root-pr: (\d+) -->/.exec(issue.body ?? '');
|
|
254
|
+
if (!marker) continue;
|
|
255
|
+
const rootPr = Number(marker[1]);
|
|
256
|
+
const root = allPulls.find((p) => p.number === rootPr);
|
|
257
|
+
const children = childrenOf.get(issue.number) ?? [];
|
|
258
|
+
// A child only means work in progress while its pull request is still open.
|
|
259
|
+
// merge-gate closes the child issue the moment its pull request lands on the
|
|
260
|
+
// feature branch; if that close was lost -- an API blip, a run that died between
|
|
261
|
+
// the merge and the close -- the child stays open here for ever and the stack
|
|
262
|
+
// never reads as finished, which is the stall this sweep exists to prevent. The
|
|
263
|
+
// child's own `implement-pr` marker names its pull request, so the test is exact:
|
|
264
|
+
// an open child whose named pull request has merged has landed, whatever its issue
|
|
265
|
+
// state still claims. Closing it here is the same idempotent backstop section 3 is
|
|
266
|
+
// for flat splits, and the App token this sweep runs on is what raises the event.
|
|
267
|
+
let openChildren = 0;
|
|
268
|
+
for (const child of children) {
|
|
269
|
+
if (child.state !== 'open') continue;
|
|
270
|
+
const prMarker = /<!-- implement-pr: (\d+) -->/.exec(child.body ?? '');
|
|
271
|
+
const landed = prMarker && allPulls.some((p) => p.number === Number(prMarker[1]) && p.merged_at);
|
|
272
|
+
if (!landed) { openChildren += 1; continue; }
|
|
273
|
+
await act(`#${child.number}: closing child, its pull request #${prMarker[1]} merged into the stack`, async () => {
|
|
274
|
+
await github.rest.issues.createComment({
|
|
275
|
+
...context.repo, issue_number: child.number,
|
|
276
|
+
body: `${DIGEST_MARKER}\nClosed automatically: its pull request #${prMarker[1]} merged into the feature branch, so this child is done.`,
|
|
277
|
+
});
|
|
278
|
+
await github.rest.issues.update({ ...context.repo, issue_number: child.number, state: 'closed', state_reason: 'completed' });
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (!root) {
|
|
282
|
+
needsHuman.push({ kind: 'stack-root-missing', issue, rootPr, waited: age(issue.updated_at) });
|
|
283
|
+
} else if (root.state === 'open') {
|
|
284
|
+
if (openChildren > 0) continue; // mid-stack: the normal state, not worth a line
|
|
285
|
+
// Every child is done, so the completion announcement should exist on the root
|
|
286
|
+
// pull request. Its absence is the lost race above; a person is told here rather
|
|
287
|
+
// than the stack sitting finished and silent. When the announcement exists but
|
|
288
|
+
// nobody has merged for days, that is staleness worth one line too.
|
|
289
|
+
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
290
|
+
...context.repo, issue_number: rootPr, per_page: 100,
|
|
291
|
+
});
|
|
292
|
+
const told = comments.find((c) => (c.body ?? '').includes('<!-- stack-complete -->'));
|
|
293
|
+
if (!told) {
|
|
294
|
+
needsHuman.push({ kind: 'stack-ready', issue, pull: root, waited: age(issue.updated_at) });
|
|
295
|
+
} else if (age(told.created_at) > stalePrMs) {
|
|
296
|
+
needsHuman.push({ kind: 'stack-held', issue, pull: root, waited: age(told.created_at) });
|
|
297
|
+
}
|
|
298
|
+
} else if (root.merged_at) {
|
|
299
|
+
// The root merged to the default branch and its closing keyword should have
|
|
300
|
+
// closed this issue with it. If something raced that, the same rule section 3
|
|
301
|
+
// applies to flat splits closes it here: every child done, root landed, done.
|
|
302
|
+
if (openChildren === 0) {
|
|
303
|
+
await act(`#${issue.number}: closing stack feature, its root PR #${rootPr} merged`, async () => {
|
|
304
|
+
await github.rest.issues.createComment({
|
|
305
|
+
...context.repo, issue_number: issue.number,
|
|
306
|
+
body: `${DIGEST_MARKER}\nClosed automatically: the stack's root pull request #${rootPr} merged with every child landed (${children.map((c) => `#${c.number}`).join(', ')}).`,
|
|
307
|
+
});
|
|
308
|
+
await github.rest.issues.update({ ...context.repo, issue_number: issue.number, state: 'closed', state_reason: 'completed' });
|
|
309
|
+
});
|
|
310
|
+
} else {
|
|
311
|
+
needsHuman.push({ kind: 'stack-orphaned', issue, pull: root, waited: age(root.merged_at), openChildren });
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
// Closed without merging. That is a person's decision or an accident, and the
|
|
315
|
+
// landed child work only exists on the feature branch, which section 4 may have
|
|
316
|
+
// already deleted -- restorable through refs/pull/<n>/head, but a person decides.
|
|
317
|
+
needsHuman.push({ kind: 'stack-root-closed', issue, pull: root, waited: age(root.closed_at), openChildren });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
232
321
|
// 4. Branches. Nothing in the fleet has ever deleted one, so they accumulate for ever
|
|
233
322
|
// after a merge and after a close alike. Only ever touch a branch whose pull request is
|
|
234
323
|
// finished; never the default branch, and never one with a pull request still open.
|
|
@@ -241,7 +330,6 @@ runs:
|
|
|
241
330
|
// back, and the pull request page still shows the diff.
|
|
242
331
|
const branches = await github.paginate(github.rest.repos.listBranches, { ...context.repo, per_page: 100 });
|
|
243
332
|
const defaultBranch = context.payload.repository.default_branch;
|
|
244
|
-
const allPulls = await github.paginate(github.rest.pulls.list, { ...context.repo, state: 'all', per_page: 100 });
|
|
245
333
|
const byHead = new Map();
|
|
246
334
|
for (const pull of allPulls) {
|
|
247
335
|
if (!byHead.has(pull.head.ref)) byHead.set(pull.head.ref, []);
|
|
@@ -319,6 +407,11 @@ runs:
|
|
|
319
407
|
const line = (entry) => {
|
|
320
408
|
if (entry.kind === 'pr-parked') return `- PR #${entry.pull.number} · parked ${days(entry.waited)}d · gate verdict \`${entry.verdict}\` · ${entry.pull.title}`;
|
|
321
409
|
if (entry.kind === 'pr-stale') return `- PR #${entry.pull.number} · ${days(entry.waited)}d old · no gate verdict yet · ${entry.pull.title}`;
|
|
410
|
+
if (entry.kind === 'stack-ready') return `- PR #${entry.pull.number} · stack finished ${days(entry.waited)}d ago, never announced · human merge needed · ${entry.pull.title}`;
|
|
411
|
+
if (entry.kind === 'stack-held') return `- PR #${entry.pull.number} · stack ready and waiting on a human merge for ${days(entry.waited)}d · ${entry.pull.title}`;
|
|
412
|
+
if (entry.kind === 'stack-root-missing') return `- #${entry.issue.number} · names root PR #${entry.rootPr}, which does not exist · ${entry.issue.title}`;
|
|
413
|
+
if (entry.kind === 'stack-root-closed') return `- PR #${entry.pull.number} · stack root closed unmerged, ${entry.openChildren} child(ren) still open · ${entry.issue.title} · restore the branch from refs/pull/${entry.pull.number}/head if accidental`;
|
|
414
|
+
if (entry.kind === 'stack-orphaned') return `- #${entry.issue.number} · root merged but ${entry.openChildren} child(ren) still open · ${entry.issue.title}`;
|
|
322
415
|
if (entry.kind === 'retries-exhausted') return `- #${entry.issue.number} · stalled ${days(entry.waited)}d · retried ${entry.attempts}× and still stalling · ${entry.issue.title}`;
|
|
323
416
|
if (entry.kind === 'no-work-label') return `- #${entry.issue.number} · stalled ${days(entry.waited)}d · no work label to retry with · ${entry.issue.title}`;
|
|
324
417
|
if (entry.kind === 'stale-audit') return `- #${entry.issue.number} · open ${days(entry.waited)}d · audit report nobody actioned · ${entry.issue.title}`;
|
|
@@ -356,6 +449,7 @@ runs:
|
|
|
356
449
|
DIGEST_MARKER,
|
|
357
450
|
`_Updated ${new Date().toISOString().slice(0, 16).replace('T', ' ')} UTC. This issue is rewritten by the housekeeping job; edits to it are lost._`,
|
|
358
451
|
section('Waiting on a decision', ['decided']),
|
|
452
|
+
section('Stacked features waiting on a human', ['stack-ready', 'stack-held', 'stack-root-missing', 'stack-root-closed', 'stack-orphaned']),
|
|
359
453
|
section('Pull requests parked by the gate', ['pr-parked', 'pr-stale']),
|
|
360
454
|
section('Stalled beyond the retry budget', ['retries-exhausted', 'no-work-label']),
|
|
361
455
|
section('Audit reports nobody actioned', ['stale-audit']),
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/link-pr-to-issue/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Link pull request to issue
|
|
3
|
-
description: Ensure the pull request body carries a closing reference to its source issue.
|
|
4
|
-
inputs:
|
|
5
|
-
token:
|
|
6
|
-
description: GitHub token with pull-requests write access.
|
|
7
|
-
required: true
|
|
8
|
-
pr-number:
|
|
9
|
-
description: Pull request number.
|
|
10
|
-
required: true
|
|
11
|
-
issue-number:
|
|
12
|
-
description: Issue number the pull request should close.
|
|
13
|
-
required: true
|
|
14
|
-
runs:
|
|
15
|
-
using: composite
|
|
16
|
-
steps:
|
|
17
|
-
- name: Ensure a closing reference
|
|
18
|
-
shell: bash
|
|
19
|
-
env:
|
|
20
|
-
GH_TOKEN: ${{ inputs.token }}
|
|
21
|
-
REPO: ${{ github.repository }}
|
|
22
|
-
PR_NUMBER: ${{ inputs.pr-number }}
|
|
23
|
-
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
24
|
-
run: |
|
|
25
|
-
set -euo pipefail
|
|
26
|
-
|
|
27
|
-
pr="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json closingIssuesReferences,body)"
|
|
28
|
-
|
|
29
|
-
if [ "$(jq '.closingIssuesReferences | length' <<<"$pr")" -gt 0 ]; then
|
|
30
|
-
echo "PR #$PR_NUMBER already closes an issue."
|
|
31
|
-
exit 0
|
|
32
|
-
fi
|
|
33
|
-
|
|
34
|
-
# A closing reference can only be created from the pull request body; there is no
|
|
35
|
-
# REST or GraphQL mutation for it.
|
|
36
|
-
body="$(jq -r '.body // ""' <<<"$pr")"
|
|
37
|
-
printf '%s\n\nCloses #%s\n' "$body" "$ISSUE_NUMBER" > /tmp/pr-body.md
|
|
38
|
-
|
|
39
|
-
gh pr edit "$PR_NUMBER" --repo "$REPO" --body-file /tmp/pr-body.md
|
|
40
|
-
echo "Added 'Closes #$ISSUE_NUMBER' to PR #$PR_NUMBER."
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/link-pr-to-issue/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Link pull request to issue
|
|
3
|
+
description: Ensure the pull request body carries a closing reference to its source issue.
|
|
4
|
+
inputs:
|
|
5
|
+
token:
|
|
6
|
+
description: GitHub token with pull-requests write access.
|
|
7
|
+
required: true
|
|
8
|
+
pr-number:
|
|
9
|
+
description: Pull request number.
|
|
10
|
+
required: true
|
|
11
|
+
issue-number:
|
|
12
|
+
description: Issue number the pull request should close.
|
|
13
|
+
required: true
|
|
14
|
+
runs:
|
|
15
|
+
using: composite
|
|
16
|
+
steps:
|
|
17
|
+
- name: Ensure a closing reference
|
|
18
|
+
shell: bash
|
|
19
|
+
env:
|
|
20
|
+
GH_TOKEN: ${{ inputs.token }}
|
|
21
|
+
REPO: ${{ github.repository }}
|
|
22
|
+
PR_NUMBER: ${{ inputs.pr-number }}
|
|
23
|
+
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
24
|
+
run: |
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
pr="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json closingIssuesReferences,body)"
|
|
28
|
+
|
|
29
|
+
if [ "$(jq '.closingIssuesReferences | length' <<<"$pr")" -gt 0 ]; then
|
|
30
|
+
echo "PR #$PR_NUMBER already closes an issue."
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# A closing reference can only be created from the pull request body; there is no
|
|
35
|
+
# REST or GraphQL mutation for it.
|
|
36
|
+
body="$(jq -r '.body // ""' <<<"$pr")"
|
|
37
|
+
printf '%s\n\nCloses #%s\n' "$body" "$ISSUE_NUMBER" > /tmp/pr-body.md
|
|
38
|
+
|
|
39
|
+
gh pr edit "$PR_NUMBER" --repo "$REPO" --body-file /tmp/pr-body.md
|
|
40
|
+
echo "Added 'Closes #$ISSUE_NUMBER' to PR #$PR_NUMBER."
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/list-open-issues/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: List open issues
|
|
3
|
-
description: |
|
|
4
|
-
Writes all open issues (number, title, labels) to `/tmp/gh-aw/agent/open-issues.json`
|
|
5
|
-
so the agent can check for semantic duplicates without spending turns discovering them.
|
|
6
|
-
inputs:
|
|
7
|
-
token:
|
|
8
|
-
description: GitHub token with issues:read permission.
|
|
9
|
-
required: true
|
|
10
|
-
repo:
|
|
11
|
-
description: Repository in `owner/name` format.
|
|
12
|
-
required: true
|
|
13
|
-
output-path:
|
|
14
|
-
description: Path to write the JSON output.
|
|
15
|
-
required: false
|
|
16
|
-
default: /tmp/gh-aw/agent/open-issues.json
|
|
17
|
-
runs:
|
|
18
|
-
using: composite
|
|
19
|
-
steps:
|
|
20
|
-
- name: List open issues
|
|
21
|
-
shell: bash
|
|
22
|
-
env:
|
|
23
|
-
GH_TOKEN: ${{ inputs.token }}
|
|
24
|
-
REPO: ${{ inputs.repo }}
|
|
25
|
-
OUTPUT_PATH: ${{ inputs.output-path }}
|
|
26
|
-
run: |
|
|
27
|
-
set -euo pipefail
|
|
28
|
-
mkdir -p "$(dirname "$OUTPUT_PATH")"
|
|
29
|
-
gh issue list --repo "$REPO" --state open --limit 500 \
|
|
30
|
-
--json number,title,labels \
|
|
31
|
-
--jq '[.[] | {number, title, labels: [.labels[].name]}]' \
|
|
32
|
-
> "$OUTPUT_PATH"
|
|
33
|
-
echo "Wrote $(jq 'length' "$OUTPUT_PATH") open issues to $OUTPUT_PATH"
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/list-open-issues/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: List open issues
|
|
3
|
+
description: |
|
|
4
|
+
Writes all open issues (number, title, labels) to `/tmp/gh-aw/agent/open-issues.json`
|
|
5
|
+
so the agent can check for semantic duplicates without spending turns discovering them.
|
|
6
|
+
inputs:
|
|
7
|
+
token:
|
|
8
|
+
description: GitHub token with issues:read permission.
|
|
9
|
+
required: true
|
|
10
|
+
repo:
|
|
11
|
+
description: Repository in `owner/name` format.
|
|
12
|
+
required: true
|
|
13
|
+
output-path:
|
|
14
|
+
description: Path to write the JSON output.
|
|
15
|
+
required: false
|
|
16
|
+
default: /tmp/gh-aw/agent/open-issues.json
|
|
17
|
+
runs:
|
|
18
|
+
using: composite
|
|
19
|
+
steps:
|
|
20
|
+
- name: List open issues
|
|
21
|
+
shell: bash
|
|
22
|
+
env:
|
|
23
|
+
GH_TOKEN: ${{ inputs.token }}
|
|
24
|
+
REPO: ${{ inputs.repo }}
|
|
25
|
+
OUTPUT_PATH: ${{ inputs.output-path }}
|
|
26
|
+
run: |
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
mkdir -p "$(dirname "$OUTPUT_PATH")"
|
|
29
|
+
gh issue list --repo "$REPO" --state open --limit 500 \
|
|
30
|
+
--json number,title,labels \
|
|
31
|
+
--jq '[.[] | {number, title, labels: [.labels[].name]}]' \
|
|
32
|
+
> "$OUTPUT_PATH"
|
|
33
|
+
echo "Wrote $(jq 'length' "$OUTPUT_PATH") open issues to $OUTPUT_PATH"
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/load-issue-context/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Load issue context
|
|
3
|
-
description: Write issue metadata and comment context to a JSON file.
|
|
4
|
-
inputs:
|
|
5
|
-
token:
|
|
6
|
-
description: GitHub token used by github-script.
|
|
7
|
-
required: true
|
|
8
|
-
issue-number:
|
|
9
|
-
description: Issue number to load.
|
|
10
|
-
required: true
|
|
11
|
-
output-path:
|
|
12
|
-
description: Absolute path where context JSON is written.
|
|
13
|
-
required: true
|
|
14
|
-
runs:
|
|
15
|
-
using: composite
|
|
16
|
-
steps:
|
|
17
|
-
- name: Load issue and comments
|
|
18
|
-
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
19
|
-
env:
|
|
20
|
-
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
21
|
-
OUTPUT_PATH: ${{ inputs.output-path }}
|
|
22
|
-
with:
|
|
23
|
-
github-token: ${{ inputs.token }}
|
|
24
|
-
script: |
|
|
25
|
-
const fs = require('fs');
|
|
26
|
-
const issueNumber = Number(process.env.ISSUE_NUMBER);
|
|
27
|
-
const issue = await github.rest.issues.get({ ...context.repo, issue_number: issueNumber });
|
|
28
|
-
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
29
|
-
...context.repo, issue_number: issueNumber, per_page: 100,
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
fs.mkdirSync(require('path').dirname(process.env.OUTPUT_PATH), { recursive: true });
|
|
33
|
-
fs.writeFileSync(process.env.OUTPUT_PATH, JSON.stringify({
|
|
34
|
-
number: issue.data.number,
|
|
35
|
-
title: issue.data.title,
|
|
36
|
-
body: issue.data.body,
|
|
37
|
-
author: issue.data.user.login,
|
|
38
|
-
authorAssociation: issue.data.author_association,
|
|
39
|
-
labels: issue.data.labels.map(({ name }) => name),
|
|
40
|
-
comments: comments.map(({ user, author_association, body }) => ({
|
|
41
|
-
author: user.login,
|
|
42
|
-
association: author_association,
|
|
43
|
-
body,
|
|
44
|
-
})),
|
|
45
|
-
}, null, 2));
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/load-issue-context/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Load issue context
|
|
3
|
+
description: Write issue metadata and comment context to a JSON file.
|
|
4
|
+
inputs:
|
|
5
|
+
token:
|
|
6
|
+
description: GitHub token used by github-script.
|
|
7
|
+
required: true
|
|
8
|
+
issue-number:
|
|
9
|
+
description: Issue number to load.
|
|
10
|
+
required: true
|
|
11
|
+
output-path:
|
|
12
|
+
description: Absolute path where context JSON is written.
|
|
13
|
+
required: true
|
|
14
|
+
runs:
|
|
15
|
+
using: composite
|
|
16
|
+
steps:
|
|
17
|
+
- name: Load issue and comments
|
|
18
|
+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
19
|
+
env:
|
|
20
|
+
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
21
|
+
OUTPUT_PATH: ${{ inputs.output-path }}
|
|
22
|
+
with:
|
|
23
|
+
github-token: ${{ inputs.token }}
|
|
24
|
+
script: |
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const issueNumber = Number(process.env.ISSUE_NUMBER);
|
|
27
|
+
const issue = await github.rest.issues.get({ ...context.repo, issue_number: issueNumber });
|
|
28
|
+
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
29
|
+
...context.repo, issue_number: issueNumber, per_page: 100,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
fs.mkdirSync(require('path').dirname(process.env.OUTPUT_PATH), { recursive: true });
|
|
33
|
+
fs.writeFileSync(process.env.OUTPUT_PATH, JSON.stringify({
|
|
34
|
+
number: issue.data.number,
|
|
35
|
+
title: issue.data.title,
|
|
36
|
+
body: issue.data.body,
|
|
37
|
+
author: issue.data.user.login,
|
|
38
|
+
authorAssociation: issue.data.author_association,
|
|
39
|
+
labels: issue.data.labels.map(({ name }) => name),
|
|
40
|
+
comments: comments.map(({ user, author_association, body }) => ({
|
|
41
|
+
author: user.login,
|
|
42
|
+
association: author_association,
|
|
43
|
+
body,
|
|
44
|
+
})),
|
|
45
|
+
}, null, 2));
|
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/merge-agent-pr/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Merge agent pull request
|
|
3
|
-
description: Read merge_pull_request from agent_output.json and merge the PR via gh.
|
|
4
|
-
inputs:
|
|
5
|
-
output-file:
|
|
6
|
-
description: Path to agent_output.json.
|
|
7
|
-
required: true
|
|
8
|
-
token:
|
|
9
|
-
description: GitHub token with pull-requests:write and contents:write.
|
|
10
|
-
required: true
|
|
11
|
-
repo:
|
|
12
|
-
description: Target repository in owner/name format.
|
|
13
|
-
required: false
|
|
14
|
-
default: ${{ github.repository }}
|
|
15
|
-
runs:
|
|
16
|
-
using: composite
|
|
17
|
-
steps:
|
|
18
|
-
- name: Merge pull request
|
|
19
|
-
shell: bash
|
|
20
|
-
env:
|
|
21
|
-
GH_TOKEN: ${{ inputs.token }}
|
|
22
|
-
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
23
|
-
REPO: ${{ inputs.repo }}
|
|
24
|
-
run: |
|
|
25
|
-
set -euo pipefail
|
|
26
|
-
[ -f "$OUTPUT_FILE" ] || exit 0
|
|
27
|
-
|
|
28
|
-
jq -c '.items[] | select(.type == "merge_pull_request")' "$OUTPUT_FILE" | while read -r item; do
|
|
29
|
-
PR_NUM=$(echo "$item" | jq -er '.pr_number // .item_number // empty')
|
|
30
|
-
[[ "$PR_NUM" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Invalid pull request number: $PR_NUM"; exit 1; }
|
|
31
|
-
|
|
32
|
-
HEAD_SHA=$(gh pr view "$PR_NUM" --repo "$REPO" --json headRefOid --jq '.headRefOid')
|
|
33
|
-
[[ "$HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || { echo "::error::Invalid pull request head SHA for #$PR_NUM"; exit 1; }
|
|
34
|
-
|
|
35
|
-
HEAD_REF=$(gh pr view "$PR_NUM" --repo "$REPO" --json headRefName --jq '.headRefName')
|
|
36
|
-
|
|
37
|
-
gh pr merge "$PR_NUM" --repo "$REPO" --squash --match-head-commit "$HEAD_SHA"
|
|
38
|
-
|
|
39
|
-
# Delete the branch, because nothing else ever has: four repositories had accumulated
|
|
40
|
-
# 47 non-main branches, every one the head of a pull request merged or closed months
|
|
41
|
-
# ago. Done after the merge and with its failure swallowed rather than through
|
|
42
|
-
# `gh pr merge --delete-branch`, which also tries to delete the local branch and can
|
|
43
|
-
# turn a completed merge into a red step. `delete_branch_on_merge` on the repository
|
|
44
|
-
# does the same job, but only where someone remembered to switch it on.
|
|
45
|
-
if [ -n "$HEAD_REF" ] && [ "$HEAD_REF" != "null" ]; then
|
|
46
|
-
gh api -X DELETE "repos/$REPO/git/refs/heads/$HEAD_REF" >/dev/null 2>&1 ||
|
|
47
|
-
echo "::notice::branch $HEAD_REF was already gone or is protected"
|
|
48
|
-
fi
|
|
49
|
-
done
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/merge-agent-pr/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Merge agent pull request
|
|
3
|
+
description: Read merge_pull_request from agent_output.json and merge the PR via gh.
|
|
4
|
+
inputs:
|
|
5
|
+
output-file:
|
|
6
|
+
description: Path to agent_output.json.
|
|
7
|
+
required: true
|
|
8
|
+
token:
|
|
9
|
+
description: GitHub token with pull-requests:write and contents:write.
|
|
10
|
+
required: true
|
|
11
|
+
repo:
|
|
12
|
+
description: Target repository in owner/name format.
|
|
13
|
+
required: false
|
|
14
|
+
default: ${{ github.repository }}
|
|
15
|
+
runs:
|
|
16
|
+
using: composite
|
|
17
|
+
steps:
|
|
18
|
+
- name: Merge pull request
|
|
19
|
+
shell: bash
|
|
20
|
+
env:
|
|
21
|
+
GH_TOKEN: ${{ inputs.token }}
|
|
22
|
+
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
23
|
+
REPO: ${{ inputs.repo }}
|
|
24
|
+
run: |
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
[ -f "$OUTPUT_FILE" ] || exit 0
|
|
27
|
+
|
|
28
|
+
jq -c '.items[] | select(.type == "merge_pull_request")' "$OUTPUT_FILE" | while read -r item; do
|
|
29
|
+
PR_NUM=$(echo "$item" | jq -er '.pr_number // .item_number // empty')
|
|
30
|
+
[[ "$PR_NUM" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Invalid pull request number: $PR_NUM"; exit 1; }
|
|
31
|
+
|
|
32
|
+
HEAD_SHA=$(gh pr view "$PR_NUM" --repo "$REPO" --json headRefOid --jq '.headRefOid')
|
|
33
|
+
[[ "$HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || { echo "::error::Invalid pull request head SHA for #$PR_NUM"; exit 1; }
|
|
34
|
+
|
|
35
|
+
HEAD_REF=$(gh pr view "$PR_NUM" --repo "$REPO" --json headRefName --jq '.headRefName')
|
|
36
|
+
|
|
37
|
+
gh pr merge "$PR_NUM" --repo "$REPO" --squash --match-head-commit "$HEAD_SHA"
|
|
38
|
+
|
|
39
|
+
# Delete the branch, because nothing else ever has: four repositories had accumulated
|
|
40
|
+
# 47 non-main branches, every one the head of a pull request merged or closed months
|
|
41
|
+
# ago. Done after the merge and with its failure swallowed rather than through
|
|
42
|
+
# `gh pr merge --delete-branch`, which also tries to delete the local branch and can
|
|
43
|
+
# turn a completed merge into a red step. `delete_branch_on_merge` on the repository
|
|
44
|
+
# does the same job, but only where someone remembered to switch it on.
|
|
45
|
+
if [ -n "$HEAD_REF" ] && [ "$HEAD_REF" != "null" ]; then
|
|
46
|
+
gh api -X DELETE "repos/$REPO/git/refs/heads/$HEAD_REF" >/dev/null 2>&1 ||
|
|
47
|
+
echo "::notice::branch $HEAD_REF was already gone or is protected"
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/push-agent-branch/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Push agent branch
|
|
3
|
-
description: Read push_to_pull_request_branch from agent_output.json and fast-forward from its git bundle.
|
|
4
|
-
inputs:
|
|
5
|
-
output-file:
|
|
6
|
-
description: Path to agent_output.json.
|
|
7
|
-
required: true
|
|
8
|
-
bundle-file:
|
|
9
|
-
description: Path to the git bundle from the agent artifact.
|
|
10
|
-
required: false
|
|
11
|
-
default: ''
|
|
12
|
-
token:
|
|
13
|
-
description: GitHub token with contents:write.
|
|
14
|
-
required: true
|
|
15
|
-
runs:
|
|
16
|
-
using: composite
|
|
17
|
-
steps:
|
|
18
|
-
- name: Configure git credentials
|
|
19
|
-
shell: bash
|
|
20
|
-
env:
|
|
21
|
-
GH_TOKEN: ${{ inputs.token }}
|
|
22
|
-
run: |
|
|
23
|
-
git config --global url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf "https://github.com/"
|
|
24
|
-
- name: Read pull request branch
|
|
25
|
-
id: request
|
|
26
|
-
shell: bash
|
|
27
|
-
env:
|
|
28
|
-
GH_TOKEN: ${{ inputs.token }}
|
|
29
|
-
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
30
|
-
BUNDLE_FILE: ${{ inputs.bundle-file }}
|
|
31
|
-
run: |
|
|
32
|
-
set -euo pipefail
|
|
33
|
-
[ -f "$OUTPUT_FILE" ] || exit 0
|
|
34
|
-
|
|
35
|
-
ITEM=$(jq -c '.items[] | select(.type == "push_to_pull_request_branch")' "$OUTPUT_FILE")
|
|
36
|
-
[ -n "$ITEM" ] || exit 0
|
|
37
|
-
|
|
38
|
-
BRANCH=$(echo "$ITEM" | jq -r '.branch')
|
|
39
|
-
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
|
40
|
-
- name: Apply bundle to pull request branch
|
|
41
|
-
if: steps.request.outputs.branch != '' && inputs.bundle-file != ''
|
|
42
|
-
uses: ./.github/actions/apply-agent-bundle
|
|
43
|
-
with:
|
|
44
|
-
bundle-file: ${{ inputs.bundle-file }}
|
|
45
|
-
target-branch: ${{ steps.request.outputs.branch }}
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/push-agent-branch/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Push agent branch
|
|
3
|
+
description: Read push_to_pull_request_branch from agent_output.json and fast-forward from its git bundle.
|
|
4
|
+
inputs:
|
|
5
|
+
output-file:
|
|
6
|
+
description: Path to agent_output.json.
|
|
7
|
+
required: true
|
|
8
|
+
bundle-file:
|
|
9
|
+
description: Path to the git bundle from the agent artifact.
|
|
10
|
+
required: false
|
|
11
|
+
default: ''
|
|
12
|
+
token:
|
|
13
|
+
description: GitHub token with contents:write.
|
|
14
|
+
required: true
|
|
15
|
+
runs:
|
|
16
|
+
using: composite
|
|
17
|
+
steps:
|
|
18
|
+
- name: Configure git credentials
|
|
19
|
+
shell: bash
|
|
20
|
+
env:
|
|
21
|
+
GH_TOKEN: ${{ inputs.token }}
|
|
22
|
+
run: |
|
|
23
|
+
git config --global url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf "https://github.com/"
|
|
24
|
+
- name: Read pull request branch
|
|
25
|
+
id: request
|
|
26
|
+
shell: bash
|
|
27
|
+
env:
|
|
28
|
+
GH_TOKEN: ${{ inputs.token }}
|
|
29
|
+
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
30
|
+
BUNDLE_FILE: ${{ inputs.bundle-file }}
|
|
31
|
+
run: |
|
|
32
|
+
set -euo pipefail
|
|
33
|
+
[ -f "$OUTPUT_FILE" ] || exit 0
|
|
34
|
+
|
|
35
|
+
ITEM=$(jq -c '.items[] | select(.type == "push_to_pull_request_branch")' "$OUTPUT_FILE")
|
|
36
|
+
[ -n "$ITEM" ] || exit 0
|
|
37
|
+
|
|
38
|
+
BRANCH=$(echo "$ITEM" | jq -r '.branch')
|
|
39
|
+
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
|
40
|
+
- name: Apply bundle to pull request branch
|
|
41
|
+
if: steps.request.outputs.branch != '' && inputs.bundle-file != ''
|
|
42
|
+
uses: ./.github/actions/apply-agent-bundle
|
|
43
|
+
with:
|
|
44
|
+
bundle-file: ${{ inputs.bundle-file }}
|
|
45
|
+
target-branch: ${{ steps.request.outputs.branch }}
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/remove-issue-labels/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Remove issue labels
|
|
3
|
-
description: Remove one or more labels from an issue or pull request.
|
|
4
|
-
inputs:
|
|
5
|
-
token:
|
|
6
|
-
description: GitHub token used by github-script.
|
|
7
|
-
required: true
|
|
8
|
-
issue-number:
|
|
9
|
-
description: Issue or pull request number.
|
|
10
|
-
required: true
|
|
11
|
-
labels:
|
|
12
|
-
description: Labels to remove, one per line. A comma-separated list is accepted as well.
|
|
13
|
-
required: true
|
|
14
|
-
runs:
|
|
15
|
-
using: composite
|
|
16
|
-
steps:
|
|
17
|
-
- name: Remove labels
|
|
18
|
-
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
19
|
-
env:
|
|
20
|
-
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
21
|
-
LABELS: ${{ inputs.labels }}
|
|
22
|
-
with:
|
|
23
|
-
github-token: ${{ inputs.token }}
|
|
24
|
-
script: |
|
|
25
|
-
// Callers wrote `labels: a,b` while this split on newlines, so one label named "a,b"
|
|
26
|
-
// was removed: a 404, swallowed below, and neither label ever came off.
|
|
27
|
-
for (const label of process.env.LABELS.split(/\r?\n|,/).map((label) => label.trim()).filter(Boolean)) {
|
|
28
|
-
try {
|
|
29
|
-
await github.rest.issues.removeLabel({
|
|
30
|
-
...context.repo,
|
|
31
|
-
issue_number: Number(process.env.ISSUE_NUMBER),
|
|
32
|
-
name: label,
|
|
33
|
-
});
|
|
34
|
-
} catch (error) {
|
|
35
|
-
if (error.status !== 404) throw error;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/remove-issue-labels/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Remove issue labels
|
|
3
|
+
description: Remove one or more labels from an issue or pull request.
|
|
4
|
+
inputs:
|
|
5
|
+
token:
|
|
6
|
+
description: GitHub token used by github-script.
|
|
7
|
+
required: true
|
|
8
|
+
issue-number:
|
|
9
|
+
description: Issue or pull request number.
|
|
10
|
+
required: true
|
|
11
|
+
labels:
|
|
12
|
+
description: Labels to remove, one per line. A comma-separated list is accepted as well.
|
|
13
|
+
required: true
|
|
14
|
+
runs:
|
|
15
|
+
using: composite
|
|
16
|
+
steps:
|
|
17
|
+
- name: Remove labels
|
|
18
|
+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
19
|
+
env:
|
|
20
|
+
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
21
|
+
LABELS: ${{ inputs.labels }}
|
|
22
|
+
with:
|
|
23
|
+
github-token: ${{ inputs.token }}
|
|
24
|
+
script: |
|
|
25
|
+
// Callers wrote `labels: a,b` while this split on newlines, so one label named "a,b"
|
|
26
|
+
// was removed: a 404, swallowed below, and neither label ever came off.
|
|
27
|
+
for (const label of process.env.LABELS.split(/\r?\n|,/).map((label) => label.trim()).filter(Boolean)) {
|
|
28
|
+
try {
|
|
29
|
+
await github.rest.issues.removeLabel({
|
|
30
|
+
...context.repo,
|
|
31
|
+
issue_number: Number(process.env.ISSUE_NUMBER),
|
|
32
|
+
name: label,
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.status !== 404) throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|