@plainconceptsplatform/workflows 0.5.1 → 0.16.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/README.md +105 -88
- package/dist/catalog-installation.d.ts +67 -0
- package/dist/catalog-installation.js +485 -0
- package/dist/catalog-listing.d.ts +13 -0
- package/dist/catalog-listing.js +70 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +273 -0
- package/dist/package-baseline.d.ts +25 -0
- package/dist/package-baseline.js +138 -0
- package/dist/repository-inspection.d.ts +23 -0
- package/dist/repository-inspection.js +113 -0
- package/dist/route-processing.d.ts +4 -0
- package/dist/route-processing.js +80 -0
- package/dist/stack-defaults.d.ts +11 -0
- package/dist/stack-defaults.js +110 -0
- package/dist/tui.d.ts +25 -0
- package/dist/tui.js +271 -0
- package/dist/worker-env.d.ts +46 -0
- package/dist/worker-env.js +179 -0
- package/dist/workflow-catalog.d.ts +27 -0
- package/dist/workflow-catalog.js +57 -0
- package/loops/actions/add-issue-labels/action.yml +22 -2
- package/loops/actions/apply-agent-bundle/apply-bundle.sh +14 -1
- package/loops/actions/audit-close/action.yml +180 -128
- package/loops/actions/classify-route/action.yml +7 -0
- package/loops/actions/classify-route/classify-route.sh +28 -7
- package/loops/actions/cleanup-artifacts/action.yml +38 -12
- package/loops/actions/housekeeping/action.yml +251 -0
- package/loops/actions/identify-gate-subject/action.yml +3 -1
- package/loops/actions/merge-agent-pr/action.yml +13 -0
- package/loops/actions/remove-issue-labels/action.yml +4 -2
- package/loops/actions/report-workflow-errors/action.yml +385 -0
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +13 -1
- package/loops/actions/validate-refine-output/validate-refine-output.sh +18 -2
- package/loops/actions/validate-triage-output/action.yml +1 -1
- package/loops/actions/validate-triage-output/validate-triage-output.sh +9 -5
- package/loops/actions/verify-composite-actions/verify-composite-actions.sh +53 -0
- package/loops/actions/verify-refine-output/verify-refine-output.sh +20 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +1017 -56
- package/loops/scripts/compile-agent-workflows.mjs +96 -1
- package/loops/scripts/merge-changelog.mjs +76 -0
- package/loops/templates/agentics/actionlint.yaml +13 -0
- package/loops/templates/agentics/agentics-checks.yml +203 -86
- package/loops/templates/agentics/agentics-error-report.yml +97 -0
- package/loops/templates/ci/app-ci-dotnet-next.yml +64 -2
- package/loops/templates/ci/app-ci-node-monorepo.yml +52 -0
- package/loops/templates/opencode/opencode.ci.json +3 -3
- package/loops/templates/opencode/opencode.ci.json.md +4 -1
- package/loops/workflows/agent-apply-review.md +452 -465
- package/loops/workflows/agent-audit.md +201 -213
- package/loops/workflows/agent-implement.md +616 -538
- package/loops/workflows/agent-merge-gate.md +830 -730
- package/loops/workflows/agent-refine.md +599 -609
- package/loops/workflows/agent-release.md +244 -258
- package/loops/workflows/agent-triage.md +476 -447
- package/loops/workflows/authorize-bot-work.yml +26 -6
- package/loops/workflows/work-router.yml +1185 -862
- package/package.json +9 -8
- package/loops/actions/stale-recovery/action.yml +0 -288
- package/loops/actions/update-changelog/action.yml +0 -113
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/housekeeping/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Housekeeping
|
|
3
|
+
description: >-
|
|
4
|
+
Retry work the machine stalled on, tidy the state it strands behind, and report what genuinely
|
|
5
|
+
still needs a person. Deterministic: no model runs here.
|
|
6
|
+
|
|
7
|
+
inputs:
|
|
8
|
+
token:
|
|
9
|
+
description: Token with issues, pull-requests and contents write access.
|
|
10
|
+
required: true
|
|
11
|
+
retry-after-hours:
|
|
12
|
+
description: How long an issue may sit stalled before it is retried.
|
|
13
|
+
required: false
|
|
14
|
+
default: '6'
|
|
15
|
+
max-retries:
|
|
16
|
+
description: How many times the janitor retries one issue before leaving it to a person.
|
|
17
|
+
required: false
|
|
18
|
+
default: '3'
|
|
19
|
+
stale-pr-days:
|
|
20
|
+
description: A bot pull request older than this with no CI verdict is reported, never closed.
|
|
21
|
+
required: false
|
|
22
|
+
default: '3'
|
|
23
|
+
digest-title:
|
|
24
|
+
description: Title of the single issue that reports what needs a human. Empty disables it.
|
|
25
|
+
required: false
|
|
26
|
+
default: 'Needs a human'
|
|
27
|
+
dry-run:
|
|
28
|
+
description: Compute everything and write nothing. Every intended change is printed.
|
|
29
|
+
required: false
|
|
30
|
+
default: 'false'
|
|
31
|
+
|
|
32
|
+
runs:
|
|
33
|
+
using: composite
|
|
34
|
+
steps:
|
|
35
|
+
- name: Sweep
|
|
36
|
+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
37
|
+
env:
|
|
38
|
+
RETRY_AFTER_HOURS: ${{ inputs.retry-after-hours }}
|
|
39
|
+
MAX_RETRIES: ${{ inputs.max-retries }}
|
|
40
|
+
STALE_PR_DAYS: ${{ inputs.stale-pr-days }}
|
|
41
|
+
DIGEST_TITLE: ${{ inputs.digest-title }}
|
|
42
|
+
DRY_RUN: ${{ inputs.dry-run }}
|
|
43
|
+
with:
|
|
44
|
+
github-token: ${{ inputs.token }}
|
|
45
|
+
script: |
|
|
46
|
+
const dryRun = process.env.DRY_RUN === 'true';
|
|
47
|
+
const retryAfterMs = Number(process.env.RETRY_AFTER_HOURS) * 3600 * 1000;
|
|
48
|
+
const maxRetries = Number(process.env.MAX_RETRIES);
|
|
49
|
+
const stalePrMs = Number(process.env.STALE_PR_DAYS) * 86400 * 1000;
|
|
50
|
+
const digestTitle = process.env.DIGEST_TITLE;
|
|
51
|
+
|
|
52
|
+
// Every write goes through this, so --dry-run is honest rather than hopeful: there is
|
|
53
|
+
// no second path that forgets to check it.
|
|
54
|
+
const planned = [];
|
|
55
|
+
const act = async (what, run) => {
|
|
56
|
+
planned.push(what);
|
|
57
|
+
if (dryRun) { core.info(`[dry-run] ${what}`); return; }
|
|
58
|
+
core.info(what);
|
|
59
|
+
await run();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const RETRY_MARKER = '<!-- housekeeping-retry -->';
|
|
63
|
+
const DIGEST_MARKER = '<!-- housekeeping-digest -->';
|
|
64
|
+
const WORK_LABELS = ['implement', 'refine', 'triage'];
|
|
65
|
+
const isBot = (login) => login.endsWith('[bot]') || login.startsWith('app/');
|
|
66
|
+
const age = (iso) => Date.now() - new Date(iso).getTime();
|
|
67
|
+
const days = (ms) => Math.floor(ms / 86400000);
|
|
68
|
+
|
|
69
|
+
const issues = await github.paginate(github.rest.issues.listForRepo, {
|
|
70
|
+
...context.repo, state: 'open', per_page: 100,
|
|
71
|
+
});
|
|
72
|
+
const pulls = await github.paginate(github.rest.pulls.list, {
|
|
73
|
+
...context.repo, state: 'open', per_page: 100,
|
|
74
|
+
});
|
|
75
|
+
const names = (issue) => issue.labels.map((l) => (typeof l === 'string' ? l : l.name));
|
|
76
|
+
|
|
77
|
+
const needsHuman = [];
|
|
78
|
+
let retried = 0;
|
|
79
|
+
|
|
80
|
+
// 1. Retry what the machine stalled on. `stalled` marks a park caused by a crash, a
|
|
81
|
+
// timeout or an empty output; `review` alone is a decision the agent made, and
|
|
82
|
+
// re-running a decision only reproduces it, so those are reported and never retried.
|
|
83
|
+
for (const issue of issues) {
|
|
84
|
+
if (issue.pull_request) continue;
|
|
85
|
+
const labels = names(issue);
|
|
86
|
+
if (!labels.includes('review')) continue;
|
|
87
|
+
|
|
88
|
+
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
89
|
+
...context.repo, issue_number: issue.number, per_page: 100,
|
|
90
|
+
});
|
|
91
|
+
const attempts = comments.filter((c) => (c.body ?? '').includes(RETRY_MARKER)).length;
|
|
92
|
+
const work = WORK_LABELS.find((l) => labels.includes(l));
|
|
93
|
+
const waited = age(issue.updated_at);
|
|
94
|
+
|
|
95
|
+
if (!labels.includes('stalled')) {
|
|
96
|
+
needsHuman.push({ kind: 'decided', issue, labels, waited });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (waited < retryAfterMs) continue;
|
|
100
|
+
if (attempts >= maxRetries || !work) {
|
|
101
|
+
needsHuman.push({ kind: attempts >= maxRetries ? 'retries-exhausted' : 'no-work-label', issue, labels, waited, attempts });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await act(`#${issue.number}: retrying ${work} (attempt ${attempts + 1} of ${maxRetries}, stalled ${Math.floor(waited / 3600000)}h)`, async () => {
|
|
106
|
+
await github.rest.issues.createComment({
|
|
107
|
+
...context.repo, issue_number: issue.number,
|
|
108
|
+
body: `${RETRY_MARKER}\nThis issue stalled rather than reaching a decision, so it is being retried automatically: attempt ${attempts + 1} of ${maxRetries}. If it stalls ${maxRetries} times it stays parked for a person.`,
|
|
109
|
+
});
|
|
110
|
+
// Removing the labels is what lets it route again; the work label is already there.
|
|
111
|
+
for (const label of ['review', 'stalled']) {
|
|
112
|
+
await github.rest.issues.removeLabel({ ...context.repo, issue_number: issue.number, name: label })
|
|
113
|
+
.catch((error) => { if (error.status !== 404) throw error; });
|
|
114
|
+
}
|
|
115
|
+
await github.rest.actions.createWorkflowDispatch({
|
|
116
|
+
...context.repo, workflow_id: 'work-router.yml',
|
|
117
|
+
ref: context.payload.repository.default_branch,
|
|
118
|
+
inputs: { operation: work, 'issue-number': String(issue.number) },
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
retried += 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 2. Strands. pr-pending says a pull request is open and waiting; when that pull request
|
|
125
|
+
// closed without merging, nothing ever took the label off.
|
|
126
|
+
for (const issue of issues) {
|
|
127
|
+
if (issue.pull_request || !names(issue).includes('pr-pending')) continue;
|
|
128
|
+
const linked = pulls.some((p) => new RegExp(`(clos(e|es|ed)|fix(es|ed)?|resolves?)\\s+#${issue.number}\\b`, 'i').test(p.body ?? ''));
|
|
129
|
+
if (linked) continue;
|
|
130
|
+
await act(`#${issue.number}: removing pr-pending, no open pull request closes it`, () =>
|
|
131
|
+
github.rest.issues.removeLabel({ ...context.repo, issue_number: issue.number, name: 'pr-pending' })
|
|
132
|
+
.catch((error) => { if (error.status !== 404) throw error; }));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 3. Split parents. Refine leaves the parent open as a tracker and nothing has ever
|
|
136
|
+
// closed one: once every child is done the parent sits there for ever with an unticked
|
|
137
|
+
// checklist. Children carry `<!-- split-parent: N -->`, which is the only machine-
|
|
138
|
+
// readable link between them.
|
|
139
|
+
const childrenOf = new Map();
|
|
140
|
+
for (const issue of await github.paginate(github.rest.issues.listForRepo, { ...context.repo, state: 'all', per_page: 100 })) {
|
|
141
|
+
const match = /<!-- split-parent: (\d+) -->/.exec(issue.body ?? '');
|
|
142
|
+
if (!match || issue.pull_request) continue;
|
|
143
|
+
const parent = Number(match[1]);
|
|
144
|
+
if (!childrenOf.has(parent)) childrenOf.set(parent, []);
|
|
145
|
+
childrenOf.get(parent).push(issue);
|
|
146
|
+
}
|
|
147
|
+
for (const [parent, children] of childrenOf) {
|
|
148
|
+
const open = children.filter((c) => c.state === 'open');
|
|
149
|
+
if (open.length > 0 || !issues.some((i) => i.number === parent)) continue;
|
|
150
|
+
await act(`#${parent}: closing split parent, all ${children.length} children are done`, async () => {
|
|
151
|
+
await github.rest.issues.createComment({
|
|
152
|
+
...context.repo, issue_number: parent,
|
|
153
|
+
body: `${DIGEST_MARKER}\nClosed automatically: every child of this split is closed (${children.map((c) => `#${c.number}`).join(', ')}).`,
|
|
154
|
+
});
|
|
155
|
+
await github.rest.issues.update({ ...context.repo, issue_number: parent, state: 'closed', state_reason: 'completed' });
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 4. Branches. Nothing in the fleet has ever deleted one, so they accumulate for ever
|
|
160
|
+
// after a merge and after a close alike. Only ever touch a branch whose pull request is
|
|
161
|
+
// finished; never the default branch, and never one with a pull request still open.
|
|
162
|
+
//
|
|
163
|
+
// Deleting the branch of a pull request that closed WITHOUT merging does not throw the
|
|
164
|
+
// work away: GitHub keeps `refs/pull/<n>/head` for the life of the pull request, which
|
|
165
|
+
// is what its own "Restore branch" button restores from. Verified on this fleet --
|
|
166
|
+
// after the first sweep removed six branches, every one of their pull requests still
|
|
167
|
+
// resolved through that ref. So `git fetch origin pull/<n>/head` brings any of this
|
|
168
|
+
// back, and the pull request page still shows the diff.
|
|
169
|
+
const branches = await github.paginate(github.rest.repos.listBranches, { ...context.repo, per_page: 100 });
|
|
170
|
+
const defaultBranch = context.payload.repository.default_branch;
|
|
171
|
+
const allPulls = await github.paginate(github.rest.pulls.list, { ...context.repo, state: 'all', per_page: 100 });
|
|
172
|
+
const byHead = new Map();
|
|
173
|
+
for (const pull of allPulls) {
|
|
174
|
+
if (!byHead.has(pull.head.ref)) byHead.set(pull.head.ref, []);
|
|
175
|
+
byHead.get(pull.head.ref).push(pull);
|
|
176
|
+
}
|
|
177
|
+
for (const branch of branches) {
|
|
178
|
+
if (branch.name === defaultBranch) continue;
|
|
179
|
+
const forBranch = byHead.get(branch.name) ?? [];
|
|
180
|
+
if (forBranch.length === 0) continue; // never opened a PR: not ours to judge
|
|
181
|
+
if (forBranch.some((p) => p.state === 'open')) continue; // still in use
|
|
182
|
+
if (!forBranch.every((p) => isBot(p.user?.login ?? ''))) continue;
|
|
183
|
+
const merged = forBranch.some((p) => p.merged_at);
|
|
184
|
+
await act(`branch ${branch.name}: deleting (${merged ? 'merged' : 'closed unmerged'})`, () =>
|
|
185
|
+
github.rest.git.deleteRef({ ...context.repo, ref: `heads/${branch.name}` })
|
|
186
|
+
.catch((error) => { if (error.status !== 404 && error.status !== 422) throw error; }));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 5. Pull requests that need a person: parked by a gate verdict, or old with no CI.
|
|
190
|
+
for (const pull of pulls) {
|
|
191
|
+
if (!isBot(pull.user?.login ?? '')) continue;
|
|
192
|
+
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
193
|
+
...context.repo, issue_number: pull.number, per_page: 100,
|
|
194
|
+
});
|
|
195
|
+
const verdict = [...comments].reverse().find((c) => (c.body ?? '').includes('<!-- agent-merge-gate -->') && (c.body ?? '').includes('**Verdict:**'));
|
|
196
|
+
const parsed = verdict ? /\*\*Verdict:\*\*\s*(\w+)/.exec(verdict.body)?.[1] : undefined;
|
|
197
|
+
if (parsed && parsed !== 'merge') {
|
|
198
|
+
needsHuman.push({ kind: 'pr-parked', pull, verdict: parsed, waited: age(verdict.created_at) });
|
|
199
|
+
} else if (!verdict && age(pull.created_at) > stalePrMs) {
|
|
200
|
+
needsHuman.push({ kind: 'pr-stale', pull, waited: age(pull.created_at) });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 6. One digest. Everything above either fixed itself or is listed here, so there is a
|
|
205
|
+
// single place to look instead of four repositories and three label queries.
|
|
206
|
+
const line = (entry) => {
|
|
207
|
+
if (entry.kind === 'pr-parked') return `- PR #${entry.pull.number} · parked ${days(entry.waited)}d · gate verdict \`${entry.verdict}\` · ${entry.pull.title}`;
|
|
208
|
+
if (entry.kind === 'pr-stale') return `- PR #${entry.pull.number} · ${days(entry.waited)}d old · no gate verdict yet · ${entry.pull.title}`;
|
|
209
|
+
if (entry.kind === 'retries-exhausted') return `- #${entry.issue.number} · stalled ${days(entry.waited)}d · retried ${entry.attempts}× and still stalling · ${entry.issue.title}`;
|
|
210
|
+
if (entry.kind === 'no-work-label') return `- #${entry.issue.number} · stalled ${days(entry.waited)}d · no work label to retry with · ${entry.issue.title}`;
|
|
211
|
+
return `- #${entry.issue.number} · waiting ${days(entry.waited)}d · ${entry.labels.filter((l) => l !== 'review').join(', ') || 'no other labels'} · ${entry.issue.title}`;
|
|
212
|
+
};
|
|
213
|
+
const section = (heading, kinds) => {
|
|
214
|
+
const rows = needsHuman.filter((e) => kinds.includes(e.kind));
|
|
215
|
+
return rows.length === 0 ? '' : `\n### ${heading} (${rows.length})\n\n${rows.map(line).join('\n')}\n`;
|
|
216
|
+
};
|
|
217
|
+
const body = [
|
|
218
|
+
DIGEST_MARKER,
|
|
219
|
+
`_Updated ${new Date().toISOString().slice(0, 16).replace('T', ' ')} UTC. This issue is rewritten by the housekeeping job; edits to it are lost._`,
|
|
220
|
+
section('Waiting on a decision', ['decided']),
|
|
221
|
+
section('Pull requests parked by the gate', ['pr-parked', 'pr-stale']),
|
|
222
|
+
section('Stalled beyond the retry budget', ['retries-exhausted', 'no-work-label']),
|
|
223
|
+
needsHuman.length === 0 ? '\nNothing is waiting on a person right now.\n' : '',
|
|
224
|
+
`\n---\n\nThis run retried ${retried} stalled issue(s) and made ${planned.length} change(s) in total.`,
|
|
225
|
+
].filter(Boolean).join('\n');
|
|
226
|
+
|
|
227
|
+
if (digestTitle) {
|
|
228
|
+
const existing = issues.find((i) => !i.pull_request && i.title === digestTitle && (i.body ?? '').includes(DIGEST_MARKER));
|
|
229
|
+
if (existing) {
|
|
230
|
+
if (needsHuman.length === 0) {
|
|
231
|
+
await act(`closing the digest #${existing.number}: nothing is waiting`, () =>
|
|
232
|
+
github.rest.issues.update({ ...context.repo, issue_number: existing.number, body, state: 'closed', state_reason: 'completed' }));
|
|
233
|
+
} else {
|
|
234
|
+
await act(`updating the digest #${existing.number}: ${needsHuman.length} item(s)`, () =>
|
|
235
|
+
github.rest.issues.update({ ...context.repo, issue_number: existing.number, body }));
|
|
236
|
+
}
|
|
237
|
+
} else if (needsHuman.length > 0) {
|
|
238
|
+
await act(`opening the digest: ${needsHuman.length} item(s)`, () =>
|
|
239
|
+
github.rest.issues.create({ ...context.repo, title: digestTitle, body }));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await core.summary
|
|
244
|
+
.addHeading('Housekeeping', 2)
|
|
245
|
+
.addList([
|
|
246
|
+
`Retried: ${retried}`,
|
|
247
|
+
`Waiting on a person: ${needsHuman.length}`,
|
|
248
|
+
`Changes ${dryRun ? 'planned' : 'made'}: ${planned.length}`,
|
|
249
|
+
])
|
|
250
|
+
.addRaw(planned.length ? `\n\n${planned.map((p) => `- ${p}`).join('\n')}` : '')
|
|
251
|
+
.write();
|
|
@@ -96,8 +96,10 @@ runs:
|
|
|
96
96
|
ci_run_id=""
|
|
97
97
|
if [ -z "$ci_conclusion" ]; then
|
|
98
98
|
head_sha="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid')"
|
|
99
|
+
# A run keeps the name it had when it ran, so a repository whose CI was renamed still
|
|
100
|
+
# carries runs under the old casing. Fold the name before comparing.
|
|
99
101
|
ci_run="$(gh api "repos/$REPO/actions/runs?head_sha=$head_sha&per_page=100" \
|
|
100
|
-
--jq '[.workflow_runs[] | select(.name == "
|
|
102
|
+
--jq '[.workflow_runs[] | select((.name | ascii_downcase) == "app: ci" or (.name | ascii_downcase) == "ci")] | sort_by(.created_at) | last')"
|
|
101
103
|
|
|
102
104
|
if [ -n "$ci_run" ] && [ "$ci_run" != "null" ]; then
|
|
103
105
|
ci_conclusion="$(jq -r '.conclusion // "pending"' <<<"$ci_run")"
|
|
@@ -32,5 +32,18 @@ runs:
|
|
|
32
32
|
HEAD_SHA=$(gh pr view "$PR_NUM" --repo "$REPO" --json headRefOid --jq '.headRefOid')
|
|
33
33
|
[[ "$HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || { echo "::error::Invalid pull request head SHA for #$PR_NUM"; exit 1; }
|
|
34
34
|
|
|
35
|
+
HEAD_REF=$(gh pr view "$PR_NUM" --repo "$REPO" --json headRefName --jq '.headRefName')
|
|
36
|
+
|
|
35
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
|
|
36
49
|
done
|
|
@@ -9,7 +9,7 @@ inputs:
|
|
|
9
9
|
description: Issue or pull request number.
|
|
10
10
|
required: true
|
|
11
11
|
labels:
|
|
12
|
-
description:
|
|
12
|
+
description: Labels to remove, one per line. A comma-separated list is accepted as well.
|
|
13
13
|
required: true
|
|
14
14
|
runs:
|
|
15
15
|
using: composite
|
|
@@ -22,7 +22,9 @@ runs:
|
|
|
22
22
|
with:
|
|
23
23
|
github-token: ${{ inputs.token }}
|
|
24
24
|
script: |
|
|
25
|
-
|
|
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)) {
|
|
26
28
|
try {
|
|
27
29
|
await github.rest.issues.removeLabel({
|
|
28
30
|
...context.repo,
|