@plainconceptsplatform/workflows 0.17.0 → 0.20.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.js +25 -13
- package/dist/stack-defaults.js +16 -16
- package/dist/worker-env.js +24 -21
- 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/identify-gate-subject/action.yml +134 -122
- 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/require-open-issue/action.yml +59 -0
- 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 +630 -8
- 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-apply-review.md +33 -3
- package/loops/workflows/agent-audit.md +36 -0
- package/loops/workflows/agent-implement.md +77 -5
- package/loops/workflows/agent-merge-gate.md +368 -148
- package/loops/workflows/agent-refine.md +93 -17
- package/loops/workflows/agent-release.md +4 -4
- package/loops/workflows/agent-triage.md +33 -1
- 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/loops/workflows/work-router.yml +25 -11
- package/package.json +2 -2
|
@@ -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,122 +1,134 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/identify-gate-subject/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Identify gate subject
|
|
3
|
-
description: Confirm the pull request is one we authored, resolve the issue it closes, and check that issue carries the required label.
|
|
4
|
-
inputs:
|
|
5
|
-
token:
|
|
6
|
-
description: GitHub token with issues and pull-requests read access.
|
|
7
|
-
required: true
|
|
8
|
-
pr-number:
|
|
9
|
-
description: Pull request number to gate.
|
|
10
|
-
required: true
|
|
11
|
-
ci-conclusion:
|
|
12
|
-
description: Conclusion reported by the CI run that triggered this gate. If empty, fetches from the PR's latest CI run.
|
|
13
|
-
required: false
|
|
14
|
-
default: ''
|
|
15
|
-
|
|
16
|
-
description:
|
|
17
|
-
required: false
|
|
18
|
-
default: ''
|
|
19
|
-
|
|
20
|
-
description:
|
|
21
|
-
required: false
|
|
22
|
-
default: ''
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/identify-gate-subject/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Identify gate subject
|
|
3
|
+
description: Confirm the pull request is one we authored, resolve the issue it closes, and check that issue carries the required label.
|
|
4
|
+
inputs:
|
|
5
|
+
token:
|
|
6
|
+
description: GitHub token with issues and pull-requests read access.
|
|
7
|
+
required: true
|
|
8
|
+
pr-number:
|
|
9
|
+
description: Pull request number to gate.
|
|
10
|
+
required: true
|
|
11
|
+
ci-conclusion:
|
|
12
|
+
description: Conclusion reported by the CI run that triggered this gate. If empty, fetches from the PR's latest CI run.
|
|
13
|
+
required: false
|
|
14
|
+
default: ''
|
|
15
|
+
ci-run-id:
|
|
16
|
+
description: CI run ID reported by the run that triggered this gate. If empty, resolved from the PR's latest CI run.
|
|
17
|
+
required: false
|
|
18
|
+
default: ''
|
|
19
|
+
linked-issue:
|
|
20
|
+
description: Issue the pull request closes, when the router already resolved it.
|
|
21
|
+
required: false
|
|
22
|
+
default: ''
|
|
23
|
+
require-label:
|
|
24
|
+
description: Label the closing issue must carry. Empty disables the check.
|
|
25
|
+
required: false
|
|
26
|
+
default: ''
|
|
27
|
+
outputs:
|
|
28
|
+
found:
|
|
29
|
+
description: Whether a valid gate subject was found.
|
|
30
|
+
value: ${{ steps.identify.outputs.found }}
|
|
31
|
+
pr:
|
|
32
|
+
description: Pull request number.
|
|
33
|
+
value: ${{ steps.identify.outputs.pr }}
|
|
34
|
+
issue:
|
|
35
|
+
description: Issue number the pull request closes.
|
|
36
|
+
value: ${{ steps.identify.outputs.issue }}
|
|
37
|
+
conclusion:
|
|
38
|
+
description: CI conclusion, passed through unchanged or fetched from latest CI run.
|
|
39
|
+
value: ${{ steps.identify.outputs.conclusion }}
|
|
40
|
+
run-id:
|
|
41
|
+
description: CI run ID when fetched.
|
|
42
|
+
value: ${{ steps.identify.outputs.run-id }}
|
|
43
|
+
runs:
|
|
44
|
+
using: composite
|
|
45
|
+
steps:
|
|
46
|
+
- name: Identify pull request and closing issue
|
|
47
|
+
id: identify
|
|
48
|
+
shell: bash
|
|
49
|
+
env:
|
|
50
|
+
GH_TOKEN: ${{ inputs.token }}
|
|
51
|
+
REPO: ${{ github.repository }}
|
|
52
|
+
PR_NUMBER: ${{ inputs.pr-number }}
|
|
53
|
+
CONCLUSION: ${{ inputs.ci-conclusion }}
|
|
54
|
+
CI_RUN_ID: ${{ inputs.ci-run-id }}
|
|
55
|
+
LINKED_ISSUE: ${{ inputs.linked-issue }}
|
|
56
|
+
REQUIRE_LABEL: ${{ inputs.require-label }}
|
|
57
|
+
run: |
|
|
58
|
+
set -euo pipefail
|
|
59
|
+
|
|
60
|
+
none() {
|
|
61
|
+
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
62
|
+
echo "$1"
|
|
63
|
+
exit 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pr="$(gh pr view "$PR_NUMBER" --repo "$REPO" \
|
|
67
|
+
--json state,author,closingIssuesReferences,body)"
|
|
68
|
+
|
|
69
|
+
[ "$(jq -r '.state' <<<"$pr")" = "OPEN" ] || none "PR #$PR_NUMBER is not open"
|
|
70
|
+
|
|
71
|
+
# Use GitHub's bot flag because App login formats differ across APIs.
|
|
72
|
+
if [ "$(jq -r '.author.is_bot' <<<"$pr")" != "true" ]; then
|
|
73
|
+
none "PR #$PR_NUMBER was authored by $(jq -r '.author.login' <<<"$pr"), a human."
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
issue="$LINKED_ISSUE"
|
|
77
|
+
|
|
78
|
+
if [ -z "$issue" ]; then
|
|
79
|
+
issue="$(jq -r '.closingIssuesReferences[0].number // empty' <<<"$pr")"
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
if [ -z "$issue" ]; then
|
|
83
|
+
issue="$(jq -r '.body // ""' <<<"$pr" |
|
|
84
|
+
grep -oiE '(close[sd]?|fixe?[sd]?|resolve[sd]?) +#[0-9]+' |
|
|
85
|
+
grep -oE '[0-9]+' | head -n 1 || true)"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
[ -n "$issue" ] || none "PR #$PR_NUMBER does not close a numbered issue"
|
|
89
|
+
|
|
90
|
+
if [ -n "$REQUIRE_LABEL" ]; then
|
|
91
|
+
labels="$(gh issue view "$issue" --repo "$REPO" --json labels \
|
|
92
|
+
--jq '[.labels[].name]')"
|
|
93
|
+
|
|
94
|
+
if ! jq -e --arg name "$REQUIRE_LABEL" 'index($name)' >/dev/null <<<"$labels"; then
|
|
95
|
+
none "issue #$issue does not carry $REQUIRE_LABEL (has: $(jq -r 'join(", ")' <<<"$labels"))"
|
|
96
|
+
fi
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# The run ID is what lets the gate read the failing job and its logs, so it has to be
|
|
100
|
+
# resolved on every path -- not only the label-triggered one. It used to be set inside
|
|
101
|
+
# `if [ -z "$ci_conclusion" ]`, which meant the caller that *knew* the ID got an empty
|
|
102
|
+
# one: the router passes ci-conclusion and ci-run-id together, so the branch was skipped
|
|
103
|
+
# and the value discarded. The gate then wrote `failed-jobs.json` as `[]`, and the agent,
|
|
104
|
+
# with no failing job name and no logs, escalated to `review`. Pliny-Bot #129 and #131
|
|
105
|
+
# both stalled on a human that way while CI was legitimately red and remediable.
|
|
106
|
+
ci_conclusion="$CONCLUSION"
|
|
107
|
+
ci_run_id="$CI_RUN_ID"
|
|
108
|
+
if [ -z "$ci_conclusion" ] || [ -z "$ci_run_id" ]; then
|
|
109
|
+
head_sha="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid')"
|
|
110
|
+
# A run keeps the name it had when it ran, so a repository whose CI was renamed still
|
|
111
|
+
# carries runs under the old casing. Fold the name before comparing.
|
|
112
|
+
ci_run="$(gh api "repos/$REPO/actions/runs?head_sha=$head_sha&per_page=100" --jq '[.workflow_runs[] | select((.name | ascii_downcase) == "app: ci" or (.name | ascii_downcase) == "ci")] | sort_by(.created_at) | last')"
|
|
113
|
+
|
|
114
|
+
if [ -n "$ci_run" ] && [ "$ci_run" != "null" ]; then
|
|
115
|
+
[ -n "$ci_conclusion" ] || ci_conclusion="$(jq -r '.conclusion // "pending"' <<<"$ci_run")"
|
|
116
|
+
[ -n "$ci_run_id" ] || ci_run_id="$(jq -r '.id' <<<"$ci_run")"
|
|
117
|
+
echo "::notice::Resolved CI status: $ci_conclusion (run $ci_run_id)"
|
|
118
|
+
elif [ -z "$ci_conclusion" ]; then
|
|
119
|
+
ci_conclusion="unknown"
|
|
120
|
+
echo "::warning::No CI run found for PR #$PR_NUMBER"
|
|
121
|
+
else
|
|
122
|
+
echo "::warning::CI concluded $ci_conclusion but no run was found for PR #$PR_NUMBER; the gate has no failure evidence"
|
|
123
|
+
fi
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
echo "found=true"
|
|
128
|
+
echo "pr=$PR_NUMBER"
|
|
129
|
+
echo "issue=$issue"
|
|
130
|
+
echo "conclusion=$ci_conclusion"
|
|
131
|
+
echo "run-id=$ci_run_id"
|
|
132
|
+
} >> "$GITHUB_OUTPUT"
|
|
133
|
+
|
|
134
|
+
echo "PR #$PR_NUMBER closes #$issue; CI concluded $ci_conclusion"
|
|
@@ -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"
|