@plainconceptsplatform/workflows 0.16.2 → 0.17.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 +15 -3
- package/loops/actions/housekeeping/action.yml +41 -5
- package/loops/actions/report-workflow-errors/action.yml +11 -4
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +77 -0
- package/loops/templates/ci/app-ci-node-monorepo.yml +12 -12
- package/loops/workflows/agent-implement.md +1 -1
- package/loops/workflows/work-router.yml +2 -2
- package/package.json +1 -1
|
@@ -26,6 +26,12 @@ export function mandatoryFileSpecs(sourcePath) {
|
|
|
26
26
|
export function catalogSourcePath(modulePath = fileURLToPath(import.meta.url)) {
|
|
27
27
|
return resolve(dirname(modulePath), "..", "loops");
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Where `gh aw` writes the pinned-action lock, and the only place it should be named.
|
|
31
|
+
* `generatedConsumerTargets` in the catalog declares the same path; two literals is how the
|
|
32
|
+
* pre-commit hook ended up guarding one that exists nowhere.
|
|
33
|
+
*/
|
|
34
|
+
const ACTIONS_LOCK = ".github/aw/actions-lock.json";
|
|
29
35
|
const isWorker = (target) => target.startsWith(".github/workflows/agent-") && target.endsWith(".md");
|
|
30
36
|
const isRouter = (target) => target === ".github/workflows/work-router.yml";
|
|
31
37
|
const carriesHeader = (target) => /\.(ya?ml|md|sh|mjs|cjs)$/.test(target);
|
|
@@ -287,7 +293,13 @@ async function preCommitHookUpdate(repositoryPath) {
|
|
|
287
293
|
const hookPath = join(repositoryPath, target);
|
|
288
294
|
const compileLine = "node scripts/compile-agent-workflows.mjs";
|
|
289
295
|
const stageLine = "git add -- .github/workflows/*.lock.yml";
|
|
290
|
-
|
|
296
|
+
// gh aw writes this at .github/aw/, which is what every consumer tracks and what
|
|
297
|
+
// `generatedConsumerTargets` in the catalog declares. The line used to name
|
|
298
|
+
// `.github/actions/actions-lock.json`, a path that exists nowhere, so `[ ! -f ... ]` was
|
|
299
|
+
// always true, the `||` short-circuited, and the real lock was never staged: a compile that
|
|
300
|
+
// bumped an action pin left the lock out of the commit and the tree dirty behind it. Nothing
|
|
301
|
+
// failed, which is why it survived. Found by the first audit run in the dogfood repository.
|
|
302
|
+
const actionLockLine = `[ ! -f ${ACTIONS_LOCK} ] || git add -- ${ACTIONS_LOCK}`;
|
|
291
303
|
const managedLines = `if git diff --cached --name-only -- .github | grep -q .; then\n ${compileLine}\n ${stageLine}\n ${actionLockLine}\nfi\n`;
|
|
292
304
|
if (!await exists(hookPath)) {
|
|
293
305
|
return { target, content: managedLines };
|
|
@@ -363,9 +375,9 @@ async function generatedFiles(repositoryPath) {
|
|
|
363
375
|
updates.push({ target: `.github/workflows/${file.replaceAll("\\", "/")}`, content: await readFile(join(workflowPath, file), "utf8") });
|
|
364
376
|
}
|
|
365
377
|
}
|
|
366
|
-
const actionsLock = join(repositoryPath,
|
|
378
|
+
const actionsLock = join(repositoryPath, ...ACTIONS_LOCK.split("/"));
|
|
367
379
|
if (await exists(actionsLock)) {
|
|
368
|
-
updates.push({ target:
|
|
380
|
+
updates.push({ target: ACTIONS_LOCK, content: await readFile(actionsLock, "utf8") });
|
|
369
381
|
}
|
|
370
382
|
return updates.sort((left, right) => left.target.localeCompare(right.target));
|
|
371
383
|
}
|
|
@@ -75,6 +75,13 @@ runs:
|
|
|
75
75
|
const names = (issue) => issue.labels.map((l) => (typeof l === 'string' ? l : l.name));
|
|
76
76
|
|
|
77
77
|
const needsHuman = [];
|
|
78
|
+
// Stalled work still inside its retry window. Separate from needsHuman because nobody
|
|
79
|
+
// needs to act on it yet, and reported anyway because the alternative is silence: the
|
|
80
|
+
// window is 6 hours and the budget is 3 attempts, so an issue that keeps stalling was
|
|
81
|
+
// invisible for eighteen hours before it surfaced as retries-exhausted. Watched that
|
|
82
|
+
// happen to an audit report in the dogfood repository -- parked, and the digest said
|
|
83
|
+
// one item while three issues were sitting there.
|
|
84
|
+
const retrying = [];
|
|
78
85
|
let retried = 0;
|
|
79
86
|
|
|
80
87
|
// 1. Retry what the machine stalled on. `stalled` marks a park caused by a crash, a
|
|
@@ -96,7 +103,10 @@ runs:
|
|
|
96
103
|
needsHuman.push({ kind: 'decided', issue, labels, waited });
|
|
97
104
|
continue;
|
|
98
105
|
}
|
|
99
|
-
if (waited < retryAfterMs)
|
|
106
|
+
if (waited < retryAfterMs) {
|
|
107
|
+
retrying.push({ issue, waited, attempts, work, due: retryAfterMs - waited });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
100
110
|
if (attempts >= maxRetries || !work) {
|
|
101
111
|
needsHuman.push({ kind: attempts >= maxRetries ? 'retries-exhausted' : 'no-work-label', issue, labels, waited, attempts });
|
|
102
112
|
continue;
|
|
@@ -203,39 +213,64 @@ runs:
|
|
|
203
213
|
|
|
204
214
|
// 6. One digest. Everything above either fixed itself or is listed here, so there is a
|
|
205
215
|
// single place to look instead of four repositories and three label queries.
|
|
216
|
+
// Audit reports nobody actioned. audit-close labels these `stale-audit` once they are
|
|
217
|
+
// past their budget, which frees the audit's backpressure slot. Freeing the slot
|
|
218
|
+
// without listing the report anywhere would only move the silence, so they land here.
|
|
219
|
+
for (const issue of issues) {
|
|
220
|
+
if (issue.pull_request) continue;
|
|
221
|
+
if (!names(issue).includes('stale-audit')) continue;
|
|
222
|
+
needsHuman.push({ kind: 'stale-audit', issue, waited: age(issue.created_at) });
|
|
223
|
+
}
|
|
224
|
+
|
|
206
225
|
const line = (entry) => {
|
|
207
226
|
if (entry.kind === 'pr-parked') return `- PR #${entry.pull.number} · parked ${days(entry.waited)}d · gate verdict \`${entry.verdict}\` · ${entry.pull.title}`;
|
|
208
227
|
if (entry.kind === 'pr-stale') return `- PR #${entry.pull.number} · ${days(entry.waited)}d old · no gate verdict yet · ${entry.pull.title}`;
|
|
209
228
|
if (entry.kind === 'retries-exhausted') return `- #${entry.issue.number} · stalled ${days(entry.waited)}d · retried ${entry.attempts}× and still stalling · ${entry.issue.title}`;
|
|
210
229
|
if (entry.kind === 'no-work-label') return `- #${entry.issue.number} · stalled ${days(entry.waited)}d · no work label to retry with · ${entry.issue.title}`;
|
|
230
|
+
if (entry.kind === 'stale-audit') return `- #${entry.issue.number} · open ${days(entry.waited)}d · audit report nobody actioned · ${entry.issue.title}`;
|
|
211
231
|
return `- #${entry.issue.number} · waiting ${days(entry.waited)}d · ${entry.labels.filter((l) => l !== 'review').join(', ') || 'no other labels'} · ${entry.issue.title}`;
|
|
212
232
|
};
|
|
213
233
|
const section = (heading, kinds) => {
|
|
214
234
|
const rows = needsHuman.filter((e) => kinds.includes(e.kind));
|
|
215
235
|
return rows.length === 0 ? '' : `\n### ${heading} (${rows.length})\n\n${rows.map(line).join('\n')}\n`;
|
|
216
236
|
};
|
|
237
|
+
// Listed, but not as something to act on: the machine is going to try again by itself.
|
|
238
|
+
// The heading says so, because a reader who acts on these duplicates the retry.
|
|
239
|
+
const retryingSection = () => {
|
|
240
|
+
if (retrying.length === 0) return '';
|
|
241
|
+
const hours = (ms) => Math.max(1, Math.round(ms / 3600000));
|
|
242
|
+
const rows = retrying.map((entry) =>
|
|
243
|
+
`- #${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
|
+
return `\n### Retrying by itself, no action needed (${retrying.length})\n\n${rows.join('\n')}\n`;
|
|
245
|
+
};
|
|
217
246
|
const body = [
|
|
218
247
|
DIGEST_MARKER,
|
|
219
248
|
`_Updated ${new Date().toISOString().slice(0, 16).replace('T', ' ')} UTC. This issue is rewritten by the housekeeping job; edits to it are lost._`,
|
|
220
249
|
section('Waiting on a decision', ['decided']),
|
|
221
250
|
section('Pull requests parked by the gate', ['pr-parked', 'pr-stale']),
|
|
222
251
|
section('Stalled beyond the retry budget', ['retries-exhausted', 'no-work-label']),
|
|
252
|
+
section('Audit reports nobody actioned', ['stale-audit']),
|
|
253
|
+
retryingSection(),
|
|
223
254
|
needsHuman.length === 0 ? '\nNothing is waiting on a person right now.\n' : '',
|
|
224
255
|
`\n---\n\nThis run retried ${retried} stalled issue(s) and made ${planned.length} change(s) in total.`,
|
|
225
256
|
].filter(Boolean).join('\n');
|
|
226
257
|
|
|
227
258
|
if (digestTitle) {
|
|
259
|
+
// The digest exists whenever there is anything to show, including work the machine is
|
|
260
|
+
// about to retry. Gating it on needsHuman alone is what left three parked issues
|
|
261
|
+
// with a digest that named one.
|
|
262
|
+
const anythingToShow = needsHuman.length + retrying.length;
|
|
228
263
|
const existing = issues.find((i) => !i.pull_request && i.title === digestTitle && (i.body ?? '').includes(DIGEST_MARKER));
|
|
229
264
|
if (existing) {
|
|
230
|
-
if (
|
|
265
|
+
if (anythingToShow === 0) {
|
|
231
266
|
await act(`closing the digest #${existing.number}: nothing is waiting`, () =>
|
|
232
267
|
github.rest.issues.update({ ...context.repo, issue_number: existing.number, body, state: 'closed', state_reason: 'completed' }));
|
|
233
268
|
} else {
|
|
234
|
-
await act(`updating the digest #${existing.number}: ${
|
|
269
|
+
await act(`updating the digest #${existing.number}: ${anythingToShow} item(s)`, () =>
|
|
235
270
|
github.rest.issues.update({ ...context.repo, issue_number: existing.number, body }));
|
|
236
271
|
}
|
|
237
|
-
} else if (
|
|
238
|
-
await act(`opening the digest: ${
|
|
272
|
+
} else if (anythingToShow > 0) {
|
|
273
|
+
await act(`opening the digest: ${anythingToShow} item(s)`, () =>
|
|
239
274
|
github.rest.issues.create({ ...context.repo, title: digestTitle, body }));
|
|
240
275
|
}
|
|
241
276
|
}
|
|
@@ -245,6 +280,7 @@ runs:
|
|
|
245
280
|
.addList([
|
|
246
281
|
`Retried: ${retried}`,
|
|
247
282
|
`Waiting on a person: ${needsHuman.length}`,
|
|
283
|
+
`Stalled, retrying by itself: ${retrying.length}`,
|
|
248
284
|
`Changes ${dryRun ? 'planned' : 'made'}: ${planned.length}`,
|
|
249
285
|
])
|
|
250
286
|
.addRaw(planned.length ? `\n\n${planned.map((p) => `- ${p}`).join('\n')}` : '')
|
|
@@ -62,10 +62,17 @@ runs:
|
|
|
62
62
|
const since = new Date(Date.now() - Number(process.env.LOOKBACK_HOURS) * 3600 * 1000);
|
|
63
63
|
|
|
64
64
|
// Only workflows this package ships. A consumer's own workflow name is theirs and can
|
|
65
|
-
// describe a product, a customer or an environment --
|
|
66
|
-
//
|
|
67
|
-
// boundary, and it is the only thing keeping those names
|
|
68
|
-
// counted and otherwise ignored.
|
|
65
|
+
// describe a product, a customer or an environment -- one consumer really does have a
|
|
66
|
+
// job named "Deploy app to" followed by an interpolated environment input -- so an
|
|
67
|
+
// allowlist by filename is the boundary, and it is the only thing keeping those names
|
|
68
|
+
// out. Anything not matching is counted and otherwise ignored.
|
|
69
|
+
//
|
|
70
|
+
// That example is described rather than quoted on purpose. An Actions expression is
|
|
71
|
+
// interpolated into this script by the runner BEFORE the JavaScript is parsed, comments
|
|
72
|
+
// included, so quoting one here made the comment a live substitution -- flagged by
|
|
73
|
+
// semgrep as github-script-injection, and invisible to a `node --check` pass because
|
|
74
|
+
// the checker has to mask the expression to parse the file at all. Everything this
|
|
75
|
+
// script needs arrives through `env:` above; nothing is interpolated into it.
|
|
69
76
|
//
|
|
70
77
|
// Exactly the filenames the package installs, and no pattern that admits a name it does
|
|
71
78
|
// not. The previous version was written as a shape (`\.(yml|lock\.yml)$` over a group of
|
|
@@ -405,6 +405,83 @@ else
|
|
|
405
405
|
fi
|
|
406
406
|
if [ "$BELT_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
407
407
|
|
|
408
|
+
# The two guards in the stale-reservation sweep, executed rather than read.
|
|
409
|
+
#
|
|
410
|
+
# Both were dead in production for as long as they existed. They matched `#N` with a `\b` word
|
|
411
|
+
# boundary written as `\\b` inside a double-quoted bash string -- and bash halves that to `\b`,
|
|
412
|
+
# which jq's string parser reads as a BACKSPACE (0x08), not as a boundary. So the regex hunted
|
|
413
|
+
# for a character no title or body contains, both guards returned 0 every time, and the sweep
|
|
414
|
+
# cleared `bot-working` off every issue that carried it: live queued runs included. Watched it
|
|
415
|
+
# happen in the dogfood repository twenty minutes after that repository existed.
|
|
416
|
+
#
|
|
417
|
+
# Implement carries the same filter to decide whether a pull request already closes its issue,
|
|
418
|
+
# and it was dead the same way, which is the most likely source of the same-day duplicate pull
|
|
419
|
+
# requests in the backlog.
|
|
420
|
+
#
|
|
421
|
+
# There is no backslash in the fix and no way to reintroduce one by accident: the subject gains a
|
|
422
|
+
# trailing space, so a number at the end of a body is still followed by a non-digit, and the
|
|
423
|
+
# pattern matches `[^0-9]`. Four quoting layers cannot eat a character class.
|
|
424
|
+
SWEEP_OK=1
|
|
425
|
+
if ! grep -qF 'test(\"#${issue}[^0-9]\")' "$ROUTER_YML"; then
|
|
426
|
+
SWEEP_OK=0
|
|
427
|
+
echo "FAIL: the stale-reservation sweep does not match a run title with a character class; a word boundary there is eaten by bash and jq" >&2
|
|
428
|
+
else
|
|
429
|
+
sweep_runs='{"workflow_runs":[
|
|
430
|
+
{"status":"queued","display_title":"Working (Refine): a thing (#1)"},
|
|
431
|
+
{"status":"completed","display_title":"Working (Refine): another (#2)"},
|
|
432
|
+
{"status":"in_progress","display_title":"Working (Implement): more (#10)"}
|
|
433
|
+
]}'
|
|
434
|
+
# The same program the router runs, with the same escaping, expanded the same way.
|
|
435
|
+
live_for() {
|
|
436
|
+
jq "[.workflow_runs[]
|
|
437
|
+
| select(.status == \"queued\" or .status == \"in_progress\" or .status == \"pending\" or .status == \"waiting\")
|
|
438
|
+
| select((.display_title + \" \") | test(\"#${1}[^0-9]\"))] | length" <<<"$sweep_runs"
|
|
439
|
+
}
|
|
440
|
+
for probe in "1:1" "2:0" "10:1" "3:0"; do
|
|
441
|
+
issue="${probe%%:*}"; want="${probe##*:}"
|
|
442
|
+
got=$(live_for "$issue" 2>&1)
|
|
443
|
+
if [ "$got" != "$want" ]; then
|
|
444
|
+
SWEEP_OK=0
|
|
445
|
+
echo "FAIL: the sweep's live-run check said ${got} live run(s) for #${issue}, expected ${want}" >&2
|
|
446
|
+
fi
|
|
447
|
+
done
|
|
448
|
+
|
|
449
|
+
sweep_pulls='[
|
|
450
|
+
{"number":11,"body":"Closes #1 and some detail."},
|
|
451
|
+
{"number":12,"body":"fixes #22"},
|
|
452
|
+
{"number":13,"body":"Mentions #9 but closes nothing."},
|
|
453
|
+
{"number":14,"body":"closes #5"}
|
|
454
|
+
]'
|
|
455
|
+
has_pr_for() {
|
|
456
|
+
jq "[.[] | select(((.body // \"\") + \" \") | ascii_downcase | test(\"clos(e|es|ed) #${1}[^0-9]|fix(es|ed)? #${1}[^0-9]|resolves? #${1}[^0-9]\"))] | length" <<<"$sweep_pulls"
|
|
457
|
+
}
|
|
458
|
+
# The last cases are what the boundary is for: a bare mention is not a close, `#22` must not
|
|
459
|
+
# answer for `#2`, and the trailing space is what makes `closes #5` at the end of a body match.
|
|
460
|
+
for probe in "1:1" "22:1" "9:0" "2:0" "5:1" "999:0"; do
|
|
461
|
+
issue="${probe%%:*}"; want="${probe##*:}"
|
|
462
|
+
got=$(has_pr_for "$issue" 2>&1)
|
|
463
|
+
if [ "$got" != "$want" ]; then
|
|
464
|
+
SWEEP_OK=0
|
|
465
|
+
echo "FAIL: the open-pull-request guard found ${got} for #${issue}, expected ${want}" >&2
|
|
466
|
+
fi
|
|
467
|
+
done
|
|
468
|
+
fi
|
|
469
|
+
if [ "$SWEEP_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
470
|
+
|
|
471
|
+
# And nothing may go back to a word boundary in a double-quoted jq program anywhere, because the
|
|
472
|
+
# failure is silent in both directions: the filter matches nothing and the job stays green.
|
|
473
|
+
BOUNDARY_OK=1
|
|
474
|
+
for candidate in "$ROUTER_YML" "${WORKFLOWS_DIR}"/agent-*.md; do
|
|
475
|
+
[ -f "$candidate" ] || continue
|
|
476
|
+
offenders=$(grep -n -- '--jq "' "$candidate" 2>/dev/null | grep -F '\b' || true)
|
|
477
|
+
if [ -n "$offenders" ]; then
|
|
478
|
+
BOUNDARY_OK=0
|
|
479
|
+
echo "FAIL: $(basename "$candidate") uses a word boundary inside a double-quoted jq program, which bash and jq turn into a backspace:" >&2
|
|
480
|
+
printf ' %s\n' "$offenders" | cut -c1-160 >&2
|
|
481
|
+
fi
|
|
482
|
+
done
|
|
483
|
+
if [ "$BOUNDARY_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
484
|
+
|
|
408
485
|
# `review` must not be a one-way door. It used to be: authorize-bot-work refused to fire on an
|
|
409
486
|
# issue carrying it, and the classifier refuses to route while it is set, so a person adding
|
|
410
487
|
# `refine` to a parked issue got nothing at all — no run, no comment, no error. Triage's own
|
|
@@ -42,9 +42,9 @@ jobs:
|
|
|
42
42
|
steps:
|
|
43
43
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
44
44
|
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
|
|
45
|
-
with: { version: ${{ env.PNPM_VERSION }}, run_install: false }
|
|
45
|
+
with: { version: "${{ env.PNPM_VERSION }}", run_install: false }
|
|
46
46
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
47
|
-
with: { node-version: ${{ env.NODE_VERSION }}, cache: pnpm }
|
|
47
|
+
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
|
|
48
48
|
- run: pnpm install --frozen-lockfile
|
|
49
49
|
- run: pnpm exec biome check .
|
|
50
50
|
|
|
@@ -55,9 +55,9 @@ jobs:
|
|
|
55
55
|
steps:
|
|
56
56
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
57
57
|
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
|
|
58
|
-
with: { version: ${{ env.PNPM_VERSION }}, run_install: false }
|
|
58
|
+
with: { version: "${{ env.PNPM_VERSION }}", run_install: false }
|
|
59
59
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
60
|
-
with: { node-version: ${{ env.NODE_VERSION }}, cache: pnpm }
|
|
60
|
+
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
|
|
61
61
|
- run: pnpm install --frozen-lockfile
|
|
62
62
|
- run: pnpm --filter "$WEB_PACKAGE" run typecheck
|
|
63
63
|
- run: pnpm --filter "$WEB_PACKAGE" run test
|
|
@@ -80,9 +80,9 @@ jobs:
|
|
|
80
80
|
with:
|
|
81
81
|
fetch-depth: 0
|
|
82
82
|
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
|
|
83
|
-
with: { version: ${{ env.PNPM_VERSION }}, run_install: false }
|
|
83
|
+
with: { version: "${{ env.PNPM_VERSION }}", run_install: false }
|
|
84
84
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
85
|
-
with: { node-version: ${{ env.NODE_VERSION }}, cache: pnpm }
|
|
85
|
+
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
|
|
86
86
|
- name: Detect web changes
|
|
87
87
|
id: scope
|
|
88
88
|
run: |
|
|
@@ -107,9 +107,9 @@ jobs:
|
|
|
107
107
|
steps:
|
|
108
108
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
109
109
|
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
|
|
110
|
-
with: { version: ${{ env.PNPM_VERSION }}, run_install: false }
|
|
110
|
+
with: { version: "${{ env.PNPM_VERSION }}", run_install: false }
|
|
111
111
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
112
|
-
with: { node-version: ${{ env.NODE_VERSION }}, cache: pnpm }
|
|
112
|
+
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
|
|
113
113
|
- run: pnpm install --frozen-lockfile
|
|
114
114
|
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
|
115
115
|
with:
|
|
@@ -132,9 +132,9 @@ jobs:
|
|
|
132
132
|
steps:
|
|
133
133
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
134
134
|
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
|
|
135
|
-
with: { version: ${{ env.PNPM_VERSION }}, run_install: false }
|
|
135
|
+
with: { version: "${{ env.PNPM_VERSION }}", run_install: false }
|
|
136
136
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
137
|
-
with: { node-version: ${{ env.NODE_VERSION }}, cache: pnpm }
|
|
137
|
+
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
|
|
138
138
|
- run: pnpm install --frozen-lockfile
|
|
139
139
|
- run: pnpm --filter "$DESKTOP_PACKAGE" run typecheck
|
|
140
140
|
- run: pnpm --filter "$DESKTOP_PACKAGE" run build
|
|
@@ -156,9 +156,9 @@ jobs:
|
|
|
156
156
|
steps:
|
|
157
157
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
158
158
|
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
|
|
159
|
-
with: { version: ${{ env.PNPM_VERSION }}, run_install: false }
|
|
159
|
+
with: { version: "${{ env.PNPM_VERSION }}", run_install: false }
|
|
160
160
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
161
|
-
with: { node-version: ${{ env.NODE_VERSION }}, cache: pnpm }
|
|
161
|
+
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
|
|
162
162
|
- run: pnpm install --frozen-lockfile
|
|
163
163
|
- run: pnpm --filter "$MOBILE_PACKAGE" run typecheck
|
|
164
164
|
- run: |
|
|
@@ -404,7 +404,7 @@ jobs:
|
|
|
404
404
|
| grep -oE '<!-- implement-pr: [0-9]+ -->' | head -1 | grep -oE '[0-9]+' || true)
|
|
405
405
|
if [ -z "$pr" ]; then
|
|
406
406
|
pr=$(gh pr list --repo "$REPO" --state open --json number,body \
|
|
407
|
-
--jq "[.[] | select((.body // \"\") | ascii_downcase | test(\"clos(e|es|ed) #${ISSUE}
|
|
407
|
+
--jq "[.[] | select(((.body // \"\") + \" \") | ascii_downcase | test(\"clos(e|es|ed) #${ISSUE}[^0-9]|fix(es|ed)? #${ISSUE}[^0-9]|resolves? #${ISSUE}[^0-9]\"))][0].number // empty")
|
|
408
408
|
fi
|
|
409
409
|
if [ -z "$pr" ]; then
|
|
410
410
|
echo "::notice::No pull request found for #$ISSUE; nothing to wait for."
|
|
@@ -943,11 +943,11 @@ jobs:
|
|
|
943
943
|
live=$(gh api "repos/$REPO/actions/runs?per_page=100" \
|
|
944
944
|
--jq "[.workflow_runs[]
|
|
945
945
|
| select(.status == \"queued\" or .status == \"in_progress\" or .status == \"pending\" or .status == \"waiting\")
|
|
946
|
-
| select(.display_title | test(\"#${issue}
|
|
946
|
+
| select((.display_title + \" \") | test(\"#${issue}[^0-9]\"))] | length")
|
|
947
947
|
[ "${live:-0}" -eq 0 ] || continue
|
|
948
948
|
|
|
949
949
|
has_pr=$(gh pr list --repo "$REPO" --state open --json body \
|
|
950
|
-
--jq "[.[] | select((.body // \"\") | ascii_downcase | test(\"clos(e|es|ed) #${issue}
|
|
950
|
+
--jq "[.[] | select(((.body // \"\") + \" \") | ascii_downcase | test(\"clos(e|es|ed) #${issue}[^0-9]|fix(es|ed)? #${issue}[^0-9]|resolves? #${issue}[^0-9]\"))] | length")
|
|
951
951
|
[ "${has_pr:-0}" -eq 0 ] || continue
|
|
952
952
|
|
|
953
953
|
echo "Clearing a stale bot-working reservation on #$issue: no run, no pull request."
|