@plainconceptsplatform/workflows 0.19.2 → 0.20.1
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.js +12 -1
- package/dist/index.js +0 -0
- package/dist/stack-defaults.js +16 -16
- package/dist/worker-env.js +14 -0
- 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 +108 -108
- package/loops/actions/assess-blast-radius/action.yml +148 -0
- package/loops/actions/assess-blast-radius/assess-blast-radius.sh +138 -0
- package/loops/actions/classify-route/action.yml +100 -100
- 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 +52 -52
- 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 +55 -2
- 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 +62 -40
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +147 -31
- package/loops/actions/validate-refine-output/action.yml +48 -44
- package/loops/actions/validate-refine-output/validate-refine-output.sh +15 -4
- 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 +6 -1
- package/loops/actions/verify-route-matrix/action.yml +9 -9
- package/loops/actions/verify-route-matrix/verify-gate-metrics.mjs +51 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +329 -29
- package/loops/scripts/compile-agent-workflows.mjs +331 -331
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/ci/app-ci-dotnet-next.yml +330 -330
- package/loops/templates/ci/app-ci-node-monorepo.yml +260 -260
- package/loops/templates/issues/bug_report.yml +109 -109
- package/loops/templates/issues/feature_request.yml +75 -75
- package/loops/templates/opencode/opencode.ci.json +55 -49
- package/loops/templates/opencode/opencode.ci.json.md +59 -49
- package/loops/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-merge-gate.md +367 -148
- package/loops/workflows/agent-refine.md +60 -16
- package/loops/workflows/authorize-bot-work.yml +105 -105
- package/loops/workflows/shared/opencode-ci.md +206 -206
- package/loops/workflows/shared/platform-defaults.md +19 -19
- package/package.json +12 -11
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/download-agent-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Download agent output
|
|
3
|
-
description: Download the agent artifact and expose the output JSON and bundle paths.
|
|
4
|
-
inputs:
|
|
5
|
-
artifact-name:
|
|
6
|
-
description: >-
|
|
7
|
-
Artifact name to download. gh-aw prefixes it with the value it derives from the
|
|
8
|
-
workflow's inputs, so a workflow_call worker must pass the activation job's
|
|
9
|
-
artifact_prefix output followed by "agent", not the bare name.
|
|
10
|
-
required: true
|
|
11
|
-
outputs:
|
|
12
|
-
output-file:
|
|
13
|
-
description: Path to agent_output.json, or empty if not found.
|
|
14
|
-
value: ${{ steps.download.outputs.output-file }}
|
|
15
|
-
bundle-file:
|
|
16
|
-
description: Path to the git bundle, or empty if not found.
|
|
17
|
-
value: ${{ steps.download.outputs.bundle-file }}
|
|
18
|
-
item-count:
|
|
19
|
-
description: Number of items in the agent output.
|
|
20
|
-
value: ${{ steps.download.outputs.item-count }}
|
|
21
|
-
runs:
|
|
22
|
-
using: composite
|
|
23
|
-
steps:
|
|
24
|
-
- name: Download agent artifact
|
|
25
|
-
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
26
|
-
with:
|
|
27
|
-
name: ${{ inputs.artifact-name }}
|
|
28
|
-
path: agent-artifact
|
|
29
|
-
- name: Locate output and bundle
|
|
30
|
-
id: download
|
|
31
|
-
shell: bash
|
|
32
|
-
env:
|
|
33
|
-
ARTIFACT_NAME: ${{ inputs.artifact-name }}
|
|
34
|
-
run: |
|
|
35
|
-
set -euo pipefail
|
|
36
|
-
OUTPUT_FILE=""
|
|
37
|
-
BUNDLE_FILE=""
|
|
38
|
-
ITEM_COUNT="0"
|
|
39
|
-
|
|
40
|
-
if [ -f "agent-artifact/agent_output.json" ]; then
|
|
41
|
-
OUTPUT_FILE="agent-artifact/agent_output.json"
|
|
42
|
-
ITEM_COUNT=$(jq '.items | length' "$OUTPUT_FILE")
|
|
43
|
-
BUNDLE_FILE=$(ls agent-artifact/aw-*.bundle 2>/dev/null | head -1 || true)
|
|
44
|
-
else
|
|
45
|
-
echo "::error::Artifact '$ARTIFACT_NAME' contained no agent_output.json. Nothing the agent proposed will be applied."
|
|
46
|
-
exit 1
|
|
47
|
-
fi
|
|
48
|
-
|
|
49
|
-
echo "Artifact '$ARTIFACT_NAME' carries $ITEM_COUNT item(s)."
|
|
50
|
-
|
|
51
|
-
echo "output-file=$OUTPUT_FILE" >> "$GITHUB_OUTPUT"
|
|
52
|
-
echo "bundle-file=$BUNDLE_FILE" >> "$GITHUB_OUTPUT"
|
|
53
|
-
echo "item-count=$ITEM_COUNT" >> "$GITHUB_OUTPUT"
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/download-agent-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Download agent output
|
|
3
|
+
description: Download the agent artifact and expose the output JSON and bundle paths.
|
|
4
|
+
inputs:
|
|
5
|
+
artifact-name:
|
|
6
|
+
description: >-
|
|
7
|
+
Artifact name to download. gh-aw prefixes it with the value it derives from the
|
|
8
|
+
workflow's inputs, so a workflow_call worker must pass the activation job's
|
|
9
|
+
artifact_prefix output followed by "agent", not the bare name.
|
|
10
|
+
required: true
|
|
11
|
+
outputs:
|
|
12
|
+
output-file:
|
|
13
|
+
description: Path to agent_output.json, or empty if not found.
|
|
14
|
+
value: ${{ steps.download.outputs.output-file }}
|
|
15
|
+
bundle-file:
|
|
16
|
+
description: Path to the git bundle, or empty if not found.
|
|
17
|
+
value: ${{ steps.download.outputs.bundle-file }}
|
|
18
|
+
item-count:
|
|
19
|
+
description: Number of items in the agent output.
|
|
20
|
+
value: ${{ steps.download.outputs.item-count }}
|
|
21
|
+
runs:
|
|
22
|
+
using: composite
|
|
23
|
+
steps:
|
|
24
|
+
- name: Download agent artifact
|
|
25
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
26
|
+
with:
|
|
27
|
+
name: ${{ inputs.artifact-name }}
|
|
28
|
+
path: agent-artifact
|
|
29
|
+
- name: Locate output and bundle
|
|
30
|
+
id: download
|
|
31
|
+
shell: bash
|
|
32
|
+
env:
|
|
33
|
+
ARTIFACT_NAME: ${{ inputs.artifact-name }}
|
|
34
|
+
run: |
|
|
35
|
+
set -euo pipefail
|
|
36
|
+
OUTPUT_FILE=""
|
|
37
|
+
BUNDLE_FILE=""
|
|
38
|
+
ITEM_COUNT="0"
|
|
39
|
+
|
|
40
|
+
if [ -f "agent-artifact/agent_output.json" ]; then
|
|
41
|
+
OUTPUT_FILE="agent-artifact/agent_output.json"
|
|
42
|
+
ITEM_COUNT=$(jq '.items | length' "$OUTPUT_FILE")
|
|
43
|
+
BUNDLE_FILE=$(ls agent-artifact/aw-*.bundle 2>/dev/null | head -1 || true)
|
|
44
|
+
else
|
|
45
|
+
echo "::error::Artifact '$ARTIFACT_NAME' contained no agent_output.json. Nothing the agent proposed will be applied."
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
echo "Artifact '$ARTIFACT_NAME' carries $ITEM_COUNT item(s)."
|
|
50
|
+
|
|
51
|
+
echo "output-file=$OUTPUT_FILE" >> "$GITHUB_OUTPUT"
|
|
52
|
+
echo "bundle-file=$BUNDLE_FILE" >> "$GITHUB_OUTPUT"
|
|
53
|
+
echo "item-count=$ITEM_COUNT" >> "$GITHUB_OUTPUT"
|
|
@@ -75,6 +75,12 @@ runs:
|
|
|
75
75
|
const names = (issue) => issue.labels.map((l) => (typeof l === 'string' ? l : l.name));
|
|
76
76
|
|
|
77
77
|
const needsHuman = [];
|
|
78
|
+
// Gate telemetry. Every disposition the gate has ever posted is already on a pull
|
|
79
|
+
// request, marked, so the rate costs one extra listing rather than a store.
|
|
80
|
+
const dispositions = {};
|
|
81
|
+
const autoMerged = [];
|
|
82
|
+
const reverts = [];
|
|
83
|
+
const metricsWindowMs = 14 * 24 * 3600000;
|
|
78
84
|
// Stalled work still inside its retry window. Separate from needsHuman because nobody
|
|
79
85
|
// needs to act on it yet, and reported anyway because the alternative is silence: the
|
|
80
86
|
// window is 6 hours and the budget is 3 attempts, so an issue that keeps stalling was
|
|
@@ -203,14 +209,45 @@ runs:
|
|
|
203
209
|
...context.repo, issue_number: pull.number, per_page: 100,
|
|
204
210
|
});
|
|
205
211
|
const verdict = [...comments].reverse().find((c) => (c.body ?? '').includes('<!-- agent-merge-gate -->') && (c.body ?? '').includes('**Verdict:**'));
|
|
206
|
-
const parsed = verdict ? /\*\*Verdict:\*\*\s*(\w+)/.exec(verdict.body)?.[1] : undefined;
|
|
207
|
-
|
|
212
|
+
const parsed = verdict ? /\*\*Verdict:\*\*\s*([\w-]+)/.exec(verdict.body)?.[1] : undefined;
|
|
213
|
+
// The gate's dispositions are auto-merge, human-review, owner-review and blocked.
|
|
214
|
+
// Only the first needs nobody. The character class has to allow the hyphen or every
|
|
215
|
+
// disposition reads as the bare word "auto" or "human" and nothing matches.
|
|
216
|
+
if (parsed) dispositions[parsed] = (dispositions[parsed] ?? 0) + 1;
|
|
217
|
+
if (parsed && parsed !== 'auto-merge') {
|
|
208
218
|
needsHuman.push({ kind: 'pr-parked', pull, verdict: parsed, waited: age(verdict.created_at) });
|
|
209
219
|
} else if (!verdict && age(pull.created_at) > stalePrMs) {
|
|
210
220
|
needsHuman.push({ kind: 'pr-stale', pull, waited: age(pull.created_at) });
|
|
211
221
|
}
|
|
212
222
|
}
|
|
213
223
|
|
|
224
|
+
// 5b. The one number this whole gate exists to move. Open pull requests alone would
|
|
225
|
+
// only ever count the parked ones, because an auto-merged pull request closes within
|
|
226
|
+
// seconds of its verdict, so the rate would read as zero forever. Closed pull requests
|
|
227
|
+
// from the digest window are where the merges are.
|
|
228
|
+
for (const pull of await github.paginate(github.rest.pulls.list, {
|
|
229
|
+
...context.repo, state: 'closed', sort: 'updated', direction: 'desc', per_page: 100,
|
|
230
|
+
})) {
|
|
231
|
+
if (age(pull.updated_at) > metricsWindowMs) break;
|
|
232
|
+
// A revert names the pull request it undoes, which is how an escaped defect is
|
|
233
|
+
// attributed back to the disposition that let it through. Reverts are frequently
|
|
234
|
+
// opened by a person, so this one line sits above the bot filter.
|
|
235
|
+
if (pull.merged_at) {
|
|
236
|
+
const undone = /revert .*#(\d+)/i.exec(pull.title ?? '');
|
|
237
|
+
if (undone) reverts.push(Number(undone[1]));
|
|
238
|
+
}
|
|
239
|
+
if (!isBot(pull.user?.login ?? '')) continue;
|
|
240
|
+
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
241
|
+
...context.repo, issue_number: pull.number, per_page: 100,
|
|
242
|
+
});
|
|
243
|
+
const verdict = [...comments].reverse().find((c) => (c.body ?? '').includes('<!-- agent-merge-gate -->') && (c.body ?? '').includes('**Verdict:**'));
|
|
244
|
+
const parsed = verdict ? /\*\*Verdict:\*\*\s*([\w-]+)/.exec(verdict.body)?.[1] : undefined;
|
|
245
|
+
if (parsed) dispositions[parsed] = (dispositions[parsed] ?? 0) + 1;
|
|
246
|
+
// An auto-merge followed by a revert is the number that has to stay at zero while
|
|
247
|
+
// the rate climbs. Anything else is a rate bought with escaped defects.
|
|
248
|
+
if (parsed === 'auto-merge' && pull.merged_at) autoMerged.push(pull.number);
|
|
249
|
+
}
|
|
250
|
+
|
|
214
251
|
// 6. One digest. Everything above either fixed itself or is listed here, so there is a
|
|
215
252
|
// single place to look instead of four repositories and three label queries.
|
|
216
253
|
// Audit reports nobody actioned. audit-close labels these `stale-audit` once they are
|
|
@@ -243,6 +280,21 @@ runs:
|
|
|
243
280
|
`- #${entry.issue.number} · stalled ${hours(entry.waited)}h ago · attempt ${entry.attempts + 1} of ${maxRetries} due in ~${hours(entry.due)}h · ${entry.issue.title}`);
|
|
244
281
|
return `\n### Retrying by itself, no action needed (${retrying.length})\n\n${rows.join('\n')}\n`;
|
|
245
282
|
};
|
|
283
|
+
// The gate's own scoreboard. A digest that lists only what went wrong tells you
|
|
284
|
+
// nothing about whether the thing is getting better.
|
|
285
|
+
const gateSection = () => {
|
|
286
|
+
const total = Object.values(dispositions).reduce((a, b) => a + b, 0);
|
|
287
|
+
if (total === 0) return '';
|
|
288
|
+
const auto = dispositions['auto-merge'] ?? 0;
|
|
289
|
+
const rate = Math.round((auto / total) * 100);
|
|
290
|
+
const rows = Object.entries(dispositions)
|
|
291
|
+
.sort((a, b) => b[1] - a[1])
|
|
292
|
+
.map(([name, count]) => `- \`${name}\` · ${count}`);
|
|
293
|
+
const reverted = autoMerged.filter((number) => reverts.includes(number)).length;
|
|
294
|
+
return `\n### Merge gate, last ${metricsWindowMs / (24 * 3600000)} days\n\n` +
|
|
295
|
+
`**${rate}% auto-merged** (${auto} of ${total} dispositions).\n\n${rows.join('\n')}\n\n` +
|
|
296
|
+
`Auto-merged pull requests later reverted: ${reverted}.\n`;
|
|
297
|
+
};
|
|
246
298
|
const body = [
|
|
247
299
|
DIGEST_MARKER,
|
|
248
300
|
`_Updated ${new Date().toISOString().slice(0, 16).replace('T', ' ')} UTC. This issue is rewritten by the housekeeping job; edits to it are lost._`,
|
|
@@ -251,6 +303,7 @@ runs:
|
|
|
251
303
|
section('Stalled beyond the retry budget', ['retries-exhausted', 'no-work-label']),
|
|
252
304
|
section('Audit reports nobody actioned', ['stale-audit']),
|
|
253
305
|
retryingSection(),
|
|
306
|
+
gateSection(),
|
|
254
307
|
needsHuman.length === 0 ? '\nNothing is waiting on a person right now.\n' : '',
|
|
255
308
|
`\n---\n\nThis run retried ${retried} stalled issue(s) and made ${planned.length} change(s) in total.`,
|
|
256
309
|
].filter(Boolean).join('\n');
|
|
@@ -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 }}
|