@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
|
@@ -19,6 +19,50 @@ function resolveGhPath() {
|
|
|
19
19
|
return "gh";
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// gh-aw folds a workflow's top-level `if:` into the activation job but computes activation.needs
|
|
23
|
+
// on its own, so a compiled `if:` can read `needs.<job>` for a job activation never waits for.
|
|
24
|
+
// GitHub does not reject that: the reference resolves to '' at runtime and the clause is silently
|
|
25
|
+
// true (agent-merge-gate's protected_changes guard, Pliny-Bot run 34042143350). actionlint would
|
|
26
|
+
// flag it, but it is deliberately not run on generated files (it does not model concurrency.queue
|
|
27
|
+
// or job.workflow_*). This is the one check that reads the locks, and it needs no dependency:
|
|
28
|
+
// gh-aw emits jobs at two spaces, job keys at four, list items at six.
|
|
29
|
+
function undeclaredNeeds(lock) {
|
|
30
|
+
const violations = [];
|
|
31
|
+
let inJobs = false;
|
|
32
|
+
let job = null;
|
|
33
|
+
let inList = false;
|
|
34
|
+
let jobs = 0;
|
|
35
|
+
let needs = new Set();
|
|
36
|
+
let refs = new Set();
|
|
37
|
+
const flush = () => {
|
|
38
|
+
if (!job) return;
|
|
39
|
+
for (const ref of refs) if (!needs.has(ref)) violations.push({ job, ref, needs: [...needs].sort() });
|
|
40
|
+
};
|
|
41
|
+
for (const line of lock.split("\n")) {
|
|
42
|
+
if (/^jobs:\s*$/.test(line)) { inJobs = true; continue; }
|
|
43
|
+
if (inJobs && /^[^\s#]/.test(line)) inJobs = false;
|
|
44
|
+
if (!inJobs) continue;
|
|
45
|
+
const start = line.match(/^ ([a-z_][a-zA-Z0-9_-]*):\s*$/);
|
|
46
|
+
if (start) { flush(); job = start[1]; jobs += 1; needs = new Set(); refs = new Set(); inList = false; continue; }
|
|
47
|
+
if (!job) continue;
|
|
48
|
+
if (/^ needs:\s*$/.test(line)) { inList = true; continue; }
|
|
49
|
+
const inline = line.match(/^ needs:\s*(.+?)\s*$/);
|
|
50
|
+
if (inline) {
|
|
51
|
+
for (const n of inline[1].replace(/[\[\]]/g, "").split(",")) if (n.trim()) needs.add(n.trim());
|
|
52
|
+
inList = false;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (inList) {
|
|
56
|
+
const item = line.match(/^ - (\S+)\s*$/);
|
|
57
|
+
if (item) { needs.add(item[1]); continue; }
|
|
58
|
+
inList = false;
|
|
59
|
+
}
|
|
60
|
+
for (const m of line.matchAll(/needs\.([a-z_][a-zA-Z0-9_-]*)\./g)) refs.add(m[1]);
|
|
61
|
+
}
|
|
62
|
+
flush();
|
|
63
|
+
return { jobs, violations };
|
|
64
|
+
}
|
|
65
|
+
|
|
22
66
|
const compile = spawnSync(resolveGhPath(), ["aw", "compile", "--strict", "--dir", workflowDirectory], {
|
|
23
67
|
stdio: "inherit",
|
|
24
68
|
shell: false,
|
|
@@ -31,12 +75,43 @@ if (compile.error?.code === "ENOENT" || compile.status === null) {
|
|
|
31
75
|
|
|
32
76
|
if (compile.status !== 0) process.exit(compile.status ?? 1);
|
|
33
77
|
|
|
78
|
+
function lintLock(file, text) {
|
|
79
|
+
const lint = undeclaredNeeds(text);
|
|
80
|
+
if (lint.jobs === 0) {
|
|
81
|
+
process.stderr.write(
|
|
82
|
+
`${file}: the needs lint found no jobs; the gh-aw lock layout changed. ` +
|
|
83
|
+
`Re-anchor undeclaredNeeds() in loops/scripts/compile-agent-workflows.mjs.\n`,
|
|
84
|
+
);
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
}
|
|
87
|
+
for (const v of lint.violations) {
|
|
88
|
+
process.stderr.write(
|
|
89
|
+
`${file}: job "${v.job}" reads needs.${v.ref} but its needs are [${v.needs.join(", ")}]; ` +
|
|
90
|
+
`the reference resolves to '' at runtime. For activation, list the job under on.needs in the ` +
|
|
91
|
+
`source frontmatter; for a custom job, add it to that job's needs.\n`,
|
|
92
|
+
);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// gh aw compile leaves a lock alone when its frontmatter and body hashes still match the source.
|
|
98
|
+
// This script then ran over a lock it had already patched: AGENTMEMORY_URL was inserted a second
|
|
99
|
+
// time, the opencode install line was wrapped in itself, and the consumer freshness check could
|
|
100
|
+
// never be clean (Pliny-Bot agentics-checks run 33963600563). The sentinel marks a processed
|
|
101
|
+
// lock; a lock that carries it is linted and otherwise left as it is.
|
|
102
|
+
const SENTINEL = "# post-processed by scripts/compile-agent-workflows.mjs; run it again and nothing changes";
|
|
103
|
+
|
|
34
104
|
for (const file of readdirSync(workflowDirectory)) {
|
|
35
105
|
if (!file.endsWith(".lock.yml")) continue;
|
|
36
106
|
|
|
37
107
|
const path = join(workflowDirectory, file);
|
|
38
108
|
const content = readFileSync(path, "utf8");
|
|
109
|
+
if (content.includes(SENTINEL)) {
|
|
110
|
+
lintLock(file, content);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
39
113
|
const patched = content
|
|
114
|
+
.replace(/^(# This file was automatically generated by gh-aw[^\n]*\n)/m, `$1${SENTINEL}\n`)
|
|
40
115
|
.replaceAll("opencode run --print-logs --log-level DEBUG", "opencode run --log-level ERROR")
|
|
41
116
|
.replaceAll("opencode run --print-logs --log-level ERROR", "opencode run --log-level ERROR")
|
|
42
117
|
.replaceAll("opencode run --log-level ERROR", "opencode run --log-level ERROR")
|
|
@@ -138,6 +213,16 @@ for (const file of readdirSync(workflowDirectory)) {
|
|
|
138
213
|
' /tmp/gh-aw/aw-*.patch\n' +
|
|
139
214
|
' /tmp/gh-aw/aw-*.bundle\n' +
|
|
140
215
|
' /tmp/gh-aw/agent_output.json\n')
|
|
216
|
+
// "A failed run is already a red run": report-failure-as-issue: false and
|
|
217
|
+
// GH_AW_REPORT_FAILED_JOBS: "false" were supposed to silence issue creation, but
|
|
218
|
+
// 0.87.5's engine-failure reporter files an "[aw] ... failed" issue anyway (seen as
|
|
219
|
+
// Pliny-Bot #57: an OOM-killed gate produced a fresh issue nobody needs, while the
|
|
220
|
+
// retry belt is the actual remediation). `if: false` keeps the step compiled but
|
|
221
|
+
// never runs it, so the failure stays visible in the run and in the belt's attempt
|
|
222
|
+
// comment, and out of the issue tracker.
|
|
223
|
+
.replace(
|
|
224
|
+
/^([ \t]+)- name: Report failed jobs\n([ \t]*)id: report_failed_jobs\n[ \t]*if: always\(\)\n/m,
|
|
225
|
+
'$1- name: Report failed jobs\n$2id: report_failed_jobs\n$2if: false # never: failures surface in the run and the retry belt\n')
|
|
141
226
|
|
|
142
227
|
// v0.87.5's arc-dind mode stages the engine CLI to a daemon-visible path but assumes the
|
|
143
228
|
// Copilot engine: command -v copilot is empty under engine: opencode and cp "" fails the
|
|
@@ -222,6 +307,7 @@ for (const file of readdirSync(workflowDirectory)) {
|
|
|
222
307
|
for (const check of [
|
|
223
308
|
{ name: "bundle upload glob", ok: /\n( +)- name: Upload agent artifacts\n\1 if: always\(\)\n\1 continue-on-error: true\n\1 uses: actions\/upload-artifact@[^\n]*\n\1 with:\n\1 name: \$\{\{ needs\.activation\.outputs\.artifact_prefix \}\}agent\n\1 path: \|\n(?:\1 [^\n]*\n)*\1 \/tmp\/gh-aw\/aw-\*\.bundle\n/ },
|
|
224
309
|
{ name: "placeholder step intact", ok: /\n( +)- name: Write agent output placeholder if missing\n\1 if: always\(\)\n\1 run: \|\n\1 if \[ ! -f \/tmp\/gh-aw\/agent_output\.json \]; then\n\1 echo '\{"items":\[\]\}' > \/tmp\/gh-aw\/agent_output\.json\n\1 fi\n/ },
|
|
310
|
+
{ name: "failed-jobs reporter disabled", ok: /- name: Report failed jobs\n[ \t]*id: report_failed_jobs\n[ \t]*if: false[^\n]*\n/ },
|
|
225
311
|
]) {
|
|
226
312
|
if (!check.ok.test(rewritten)) {
|
|
227
313
|
process.stderr.write(
|
|
@@ -230,7 +316,16 @@ for (const file of readdirSync(workflowDirectory)) {
|
|
|
230
316
|
);
|
|
231
317
|
process.exitCode = 1;
|
|
232
318
|
}
|
|
233
|
-
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!rewritten.includes(SENTINEL)) {
|
|
322
|
+
process.stderr.write(
|
|
323
|
+
`${file}: the generated-file header changed and the post-processing sentinel was not placed; ` +
|
|
324
|
+
`re-anchor the SENTINEL insert in loops/scripts/compile-agent-workflows.mjs.\n`,
|
|
325
|
+
);
|
|
326
|
+
process.exitCode = 1;
|
|
327
|
+
}
|
|
328
|
+
lintLock(file, rewritten);
|
|
234
329
|
|
|
235
330
|
if (rewritten !== content) writeFileSync(path, rewritten);
|
|
236
331
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Managed by @plainconceptsplatform/workflows. Source: loops/scripts/merge-changelog.mjs. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
//
|
|
3
|
+
// A git merge driver for a newest-first changelog array.
|
|
4
|
+
//
|
|
5
|
+
// Every merge into the default branch invalidates every other open pull request that added a
|
|
6
|
+
// changelog entry, because they all insert at the top of the same list. The conflict is real
|
|
7
|
+
// but it is never interesting: both entries belong, newest first. Left to the default driver
|
|
8
|
+
// it costs a full model run per sibling pull request, or a person, and it recurs on every
|
|
9
|
+
// merge for as long as there is more than one pull request in flight.
|
|
10
|
+
//
|
|
11
|
+
// Registered from .gitattributes as `merge=changelog`, so git calls this instead of producing
|
|
12
|
+
// conflict markers. Falls back to exit 1 (a normal conflict) whenever it cannot be sure, so a
|
|
13
|
+
// genuine edit to the same entry is still escalated rather than silently mangled.
|
|
14
|
+
//
|
|
15
|
+
// Usage (git's merge driver contract): merge-changelog.mjs %O %A %B
|
|
16
|
+
// %O ancestor, %A ours (written back with the result), %B theirs.
|
|
17
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
|
|
19
|
+
const [ancestorPath, oursPath, theirsPath] = process.argv.slice(2);
|
|
20
|
+
|
|
21
|
+
/** Parse, or return undefined so the caller can bail out to a normal conflict. */
|
|
22
|
+
function read(path) {
|
|
23
|
+
try {
|
|
24
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
25
|
+
return Array.isArray(value?.changes) ? value : undefined;
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ours = read(oursPath);
|
|
32
|
+
const theirs = read(theirsPath);
|
|
33
|
+
const ancestor = read(ancestorPath) ?? { changes: [] };
|
|
34
|
+
|
|
35
|
+
if (!ours || !theirs) {
|
|
36
|
+
process.stderr.write("merge-changelog: not a changelog document on both sides; leaving the conflict\n");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// An entry is identified by its commit when it has one, and by the whole record otherwise.
|
|
41
|
+
const identify = (entry) => entry?.commit ?? JSON.stringify(entry);
|
|
42
|
+
|
|
43
|
+
// Anything either side changed relative to the ancestor, plus everything the ancestor had.
|
|
44
|
+
// Union by identity: an append on both sides is the case this exists for.
|
|
45
|
+
const merged = new Map();
|
|
46
|
+
for (const entry of [...ancestor.changes, ...theirs.changes, ...ours.changes]) {
|
|
47
|
+
merged.set(identify(entry), entry);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// An entry the ancestor had and both sides removed should stay removed.
|
|
51
|
+
const kept = [...merged.values()].filter((entry) => {
|
|
52
|
+
const id = identify(entry);
|
|
53
|
+
const inAncestor = ancestor.changes.some((candidate) => identify(candidate) === id);
|
|
54
|
+
if (!inAncestor) return true;
|
|
55
|
+
return ours.changes.some((candidate) => identify(candidate) === id) ||
|
|
56
|
+
theirs.changes.some((candidate) => identify(candidate) === id);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// If the same identity carries different content on the two sides, somebody edited an entry
|
|
60
|
+
// rather than adding one. That is a real conflict and a person should look at it.
|
|
61
|
+
for (const entry of kept) {
|
|
62
|
+
const id = identify(entry);
|
|
63
|
+
const mine = ours.changes.find((candidate) => identify(candidate) === id);
|
|
64
|
+
const yours = theirs.changes.find((candidate) => identify(candidate) === id);
|
|
65
|
+
if (mine && yours && JSON.stringify(mine) !== JSON.stringify(yours)) {
|
|
66
|
+
process.stderr.write(`merge-changelog: entry ${id} differs on both sides; leaving the conflict\n`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Newest first, which is the order the file is read in and the order the page renders.
|
|
72
|
+
kept.sort((a, b) => String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")));
|
|
73
|
+
|
|
74
|
+
const result = { ...theirs, ...ours, changes: kept };
|
|
75
|
+
writeFileSync(oursPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
76
|
+
process.stderr.write(`merge-changelog: combined ${ours.changes.length} + ${theirs.changes.length} entries into ${kept.length}\n`);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/templates/agentics/actionlint.yaml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
# Runners this repository actually has.
|
|
3
|
+
#
|
|
4
|
+
# actionlint knows GitHub's own labels and rejects everything else, so a workflow that
|
|
5
|
+
# names one of our runner groups was reported as an error on every pull request. Both
|
|
6
|
+
# of these are real: agents-arc runs the agentic workflows, RunnerLandingZone runs the
|
|
7
|
+
# work router. Listing them here is how actionlint is told a label is self-hosted
|
|
8
|
+
# rather than a typo, and it keeps that knowledge in a file the workflow generator
|
|
9
|
+
# does not overwrite.
|
|
10
|
+
self-hosted-runner:
|
|
11
|
+
labels:
|
|
12
|
+
- agents-arc
|
|
13
|
+
- RunnerLandingZone
|
|
@@ -1,86 +1,203 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/templates/agentics/agentics-checks.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
-
name: Agentics checks
|
|
3
|
-
|
|
4
|
-
on:
|
|
5
|
-
pull_request:
|
|
6
|
-
paths:
|
|
7
|
-
- .github/workflows/*.md
|
|
8
|
-
- .github/workflows/shared/*.md
|
|
9
|
-
- .github/workflows/*.yml
|
|
10
|
-
- .github/workflows/aw.json
|
|
11
|
-
- .github/actions/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/templates/agentics/agentics-checks.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
name: Agentics checks
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
pull_request:
|
|
6
|
+
paths:
|
|
7
|
+
- .github/workflows/*.md
|
|
8
|
+
- .github/workflows/shared/*.md
|
|
9
|
+
- .github/workflows/*.yml
|
|
10
|
+
- .github/workflows/aw.json
|
|
11
|
+
- .github/actions/**
|
|
12
|
+
# A push to the default branch that bypassed review is exactly the case that needs the check:
|
|
13
|
+
# seven broken locks reached Pliny-Bot's main in twenty minutes with no pull request and no
|
|
14
|
+
# run of this workflow. A red run here is the detection.
|
|
15
|
+
push:
|
|
16
|
+
branches: [main]
|
|
17
|
+
paths:
|
|
18
|
+
- .github/workflows/*.md
|
|
19
|
+
- .github/workflows/shared/*.md
|
|
20
|
+
- .github/workflows/*.yml
|
|
21
|
+
- .github/workflows/aw.json
|
|
22
|
+
- .github/actions/**
|
|
23
|
+
|
|
24
|
+
env:
|
|
25
|
+
# Must match the version the locks were compiled with (first line of every .lock.yml) and the
|
|
26
|
+
# `github/gh-aw/...@vX` import pin in every worker. A different compiler regenerates every lock
|
|
27
|
+
# and the freshness diff measures the version, not the change under review.
|
|
28
|
+
GH_AW_VERSION: v0.87.5
|
|
29
|
+
ACTIONLINT_VERSION: 1.7.7
|
|
30
|
+
ACTIONLINT_SHA256: 023070a287cd8cccd71515fedc843f1985bf96c436b7effaecce67290e7e0757
|
|
31
|
+
SHELLCHECK_VERSION: 0.10.0
|
|
32
|
+
SHELLCHECK_SHA256: 6c881ab0698e4e6ea235245f22832860544f17ba386442fe7e9d629f8cbedf87
|
|
33
|
+
# The same script a developer runs. It wraps `gh aw compile --strict` and applies the
|
|
34
|
+
# repository's post-processing; a bare `gh aw compile` on this side would regenerate every lock
|
|
35
|
+
# without the transform, and the diff below could never be clean.
|
|
36
|
+
AGENTICS_COMPILE_COMMAND: node scripts/compile-agent-workflows.mjs
|
|
37
|
+
|
|
38
|
+
permissions:
|
|
39
|
+
contents: read
|
|
40
|
+
|
|
41
|
+
concurrency:
|
|
42
|
+
group: agentics-checks-${{ github.event.pull_request.number || github.ref }}
|
|
43
|
+
cancel-in-progress: true
|
|
44
|
+
|
|
45
|
+
jobs:
|
|
46
|
+
verify-lockfiles:
|
|
47
|
+
name: Verify generated agent lockfiles
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
timeout-minutes: 10
|
|
50
|
+
env:
|
|
51
|
+
# gh-aw resolves the `github/gh-aw/...` imports through the GitHub API on every compile,
|
|
52
|
+
# and an unauthenticated gh on a hosted runner refuses API calls.
|
|
53
|
+
GH_TOKEN: ${{ github.token }}
|
|
54
|
+
steps:
|
|
55
|
+
- name: Checkout repository
|
|
56
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
57
|
+
with:
|
|
58
|
+
persist-credentials: false
|
|
59
|
+
- name: Install gh-aw
|
|
60
|
+
shell: bash
|
|
61
|
+
env:
|
|
62
|
+
GH_TOKEN: ${{ github.token }}
|
|
63
|
+
run: |
|
|
64
|
+
set -euo pipefail
|
|
65
|
+
gh extension install github/gh-aw --pin "$GH_AW_VERSION"
|
|
66
|
+
gh aw --version
|
|
67
|
+
# The compile command is a node script. Hosted images have node; a setup step pins the
|
|
68
|
+
# major version so the script sees the same runtime everywhere.
|
|
69
|
+
- name: Set up Node
|
|
70
|
+
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
71
|
+
with:
|
|
72
|
+
node-version: 22
|
|
73
|
+
- name: Verify the locks and the workers agree on the gh-aw version
|
|
74
|
+
shell: bash
|
|
75
|
+
run: |
|
|
76
|
+
set -euo pipefail
|
|
77
|
+
# `gh aw --version` writes to stderr; without 2>&1 the grep saw nothing and set -e
|
|
78
|
+
# ended the step with no message at all.
|
|
79
|
+
want="$(gh aw --version 2>&1 | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)"
|
|
80
|
+
[ "$want" = "$GH_AW_VERSION" ] || { echo "::error::gh aw reports '${want:-nothing}', this workflow pins $GH_AW_VERSION"; exit 1; }
|
|
81
|
+
status=0
|
|
82
|
+
for lock in .github/workflows/*.lock.yml; do
|
|
83
|
+
have="$(sed -n '1s/.*"compiler_version":"\([^"]*\)".*/\1/p' "$lock")"
|
|
84
|
+
if [ "$have" != "$want" ]; then
|
|
85
|
+
echo "::error::$lock was compiled by ${have:-an unknown version}; this job runs $want. Recompile with the pinned version, or bump GH_AW_VERSION and the worker import pins together."
|
|
86
|
+
status=1
|
|
87
|
+
fi
|
|
88
|
+
done
|
|
89
|
+
while read -r file pin; do
|
|
90
|
+
if [ "$pin" != "$want" ]; then
|
|
91
|
+
echo "::error::$file imports gh-aw shared files at $pin; this job runs $want."
|
|
92
|
+
status=1
|
|
93
|
+
fi
|
|
94
|
+
done < <(grep -HoE 'github/gh-aw/[^@]*@v[0-9]+\.[0-9]+\.[0-9]+' .github/workflows/*.md | sed -E 's/^([^:]*):.*@(v[0-9.]+)$/\1 \2/' | sort -u)
|
|
95
|
+
exit "$status"
|
|
96
|
+
- name: Compile agent workflows strictly
|
|
97
|
+
run: ${{ env.AGENTICS_COMPILE_COMMAND }}
|
|
98
|
+
- name: Verify generated lockfiles are current
|
|
99
|
+
shell: bash
|
|
100
|
+
run: |
|
|
101
|
+
set -euo pipefail
|
|
102
|
+
if git diff --exit-code -- .github/workflows/*.lock.yml; then
|
|
103
|
+
exit 0
|
|
104
|
+
fi
|
|
105
|
+
if git diff --exit-code -I '^[[:space:]]*GH_AW_INFO_MODEL_COSTS:' -- .github/workflows/*.lock.yml; then
|
|
106
|
+
echo "::warning::Only volatile GH_AW_INFO_MODEL_COSTS metadata changed."
|
|
107
|
+
exit 0
|
|
108
|
+
fi
|
|
109
|
+
echo "::error::Generated agent lockfiles are stale. Run '${AGENTICS_COMPILE_COMMAND}' and commit the resulting .lock.yml files."
|
|
110
|
+
git diff -- .github/workflows/*.lock.yml
|
|
111
|
+
exit 1
|
|
112
|
+
# gh aw compile leaves a lock alone when its hashes still match the source, so the script
|
|
113
|
+
# must recognise a lock it already processed. If a second run changes anything, the
|
|
114
|
+
# post-processing is not idempotent and every consumer's freshness check is measuring the
|
|
115
|
+
# transform instead of the drift.
|
|
116
|
+
- name: Verify a second compile changes nothing
|
|
117
|
+
shell: bash
|
|
118
|
+
run: |
|
|
119
|
+
set -euo pipefail
|
|
120
|
+
cp -R .github/workflows "$RUNNER_TEMP/first-pass"
|
|
121
|
+
${AGENTICS_COMPILE_COMMAND}
|
|
122
|
+
if ! diff -r -I '^[[:space:]]*GH_AW_INFO_MODEL_COSTS:' "$RUNNER_TEMP/first-pass" .github/workflows; then
|
|
123
|
+
echo "::error::Running '${AGENTICS_COMPILE_COMMAND}' twice produced different locks; the post-processing is not idempotent."
|
|
124
|
+
exit 1
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
lint-workflows:
|
|
128
|
+
name: Lint authored workflows and shell
|
|
129
|
+
runs-on: ubuntu-latest
|
|
130
|
+
timeout-minutes: 10
|
|
131
|
+
steps:
|
|
132
|
+
- name: Checkout repository
|
|
133
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
134
|
+
with:
|
|
135
|
+
persist-credentials: false
|
|
136
|
+
- name: Install actionlint
|
|
137
|
+
shell: bash
|
|
138
|
+
run: |
|
|
139
|
+
set -euo pipefail
|
|
140
|
+
tarball="$RUNNER_TEMP/actionlint.tar.gz"
|
|
141
|
+
curl -fsSL -o "$tarball" "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
|
|
142
|
+
echo "${ACTIONLINT_SHA256} $tarball" | sha256sum --check --strict
|
|
143
|
+
mkdir -p "$RUNNER_TEMP/bin"
|
|
144
|
+
tar -xzf "$tarball" -C "$RUNNER_TEMP/bin" actionlint
|
|
145
|
+
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
|
|
146
|
+
# Pinned rather than taken from the image: a copy of this job that ran on a self-hosted
|
|
147
|
+
# runner died with "shellcheck: No such file or directory".
|
|
148
|
+
- name: Install shellcheck
|
|
149
|
+
shell: bash
|
|
150
|
+
run: |
|
|
151
|
+
set -euo pipefail
|
|
152
|
+
tarball="$RUNNER_TEMP/shellcheck.tar.xz"
|
|
153
|
+
curl -fsSL -o "$tarball" "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz"
|
|
154
|
+
echo "${SHELLCHECK_SHA256} $tarball" | sha256sum --check --strict
|
|
155
|
+
mkdir -p "$RUNNER_TEMP/bin"
|
|
156
|
+
tar -xJf "$tarball" -C "$RUNNER_TEMP/bin" --strip-components=1 "shellcheck-v${SHELLCHECK_VERSION}/shellcheck"
|
|
157
|
+
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
|
|
158
|
+
# actionlint 1.7.7 predates `concurrency.queue`, which GitHub accepts and the router uses;
|
|
159
|
+
# that one message is a known false positive and is ignored by its exact text so anything
|
|
160
|
+
# else under `concurrency` still fails. shellcheck style notes (SC2129 and friends) are
|
|
161
|
+
# opinions about consumer scripts, not defects; warnings and errors still fail the job.
|
|
162
|
+
- name: Lint authored workflows
|
|
163
|
+
shell: bash
|
|
164
|
+
env:
|
|
165
|
+
SHELLCHECK_OPTS: -S warning
|
|
166
|
+
run: |
|
|
167
|
+
set -euo pipefail
|
|
168
|
+
mapfile -t authored < <(git ls-files '.github/workflows/*.yml' | grep -vE '\.lock\.yml$|agentics-maintenance\.yml$')
|
|
169
|
+
if [ "${#authored[@]}" -gt 0 ]; then
|
|
170
|
+
actionlint -ignore 'unexpected key "queue" for "concurrency" section' "${authored[@]}"
|
|
171
|
+
fi
|
|
172
|
+
- name: Lint composite action shell
|
|
173
|
+
shell: bash
|
|
174
|
+
run: find .github/actions -name '*.sh' -print0 | xargs -0 -r shellcheck -x
|
|
175
|
+
# A password written into a workflow is a finding whatever it protects, and one sat in
|
|
176
|
+
# these files for weeks: a throwaway SQL container credential, repeated inline in six
|
|
177
|
+
# to nine places per repository, found by a policy scan rather than by us. Anything a
|
|
178
|
+
# job needs at container-start time belongs in an expression or a secret, and this is
|
|
179
|
+
# the check that says so before a reviewer has to.
|
|
180
|
+
- name: No written-down passwords in workflows
|
|
181
|
+
shell: bash
|
|
182
|
+
run: |
|
|
183
|
+
set -euo pipefail
|
|
184
|
+
mapfile -t authored < <(git ls-files '.github/workflows/*.yml' '.github/actions/**' 2>/dev/null || true)
|
|
185
|
+
[ "${#authored[@]}" -gt 0 ] || exit 0
|
|
186
|
+
# Two passes, because "is this a literal" is the hard half. The first finds every
|
|
187
|
+
# assignment to a password-ish name whose value is quoted; the second keeps only
|
|
188
|
+
# the lines with no dollar sign, so a value built from an expression or read from a
|
|
189
|
+
# shell variable passes and a written-down word does not. A literal that happens to
|
|
190
|
+
# contain a dollar sign slips through; erring that way beats a check people learn to
|
|
191
|
+
# route around.
|
|
192
|
+
# Two shapes: a password-ish name assigned a quoted value, and a command-line
|
|
193
|
+
# flag given one. The literal these repositories carried appeared as both.
|
|
194
|
+
hits=$(grep -nEi \
|
|
195
|
+
-e "(password|passwd|pwd)[\"']?[[:space:]]*[:=][[:space:]]*[\"'][^\"']" \
|
|
196
|
+
-e "(^|[[:space:]])(-P|--password)[[:space:]=]*[\"'][^\"']" \
|
|
197
|
+
"${authored[@]}" | grep -v '[$]' || true)
|
|
198
|
+
if [ -n "$hits" ]; then
|
|
199
|
+
printf '%s\n' "$hits"
|
|
200
|
+
echo "::error::A password is written into a workflow. Use a secret, or derive it per run from an expression."
|
|
201
|
+
exit 1
|
|
202
|
+
fi
|
|
203
|
+
echo "No written-down passwords."
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: templates/agentics/agentics-error-report.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
name: "Agentics: Error Report"
|
|
3
|
+
|
|
4
|
+
# Once a day, look at how the workflows this package ships behaved in this repository and file
|
|
5
|
+
# what broke upstream, so the package gets fixed instead of every repository working around the
|
|
6
|
+
# same bug on its own.
|
|
7
|
+
#
|
|
8
|
+
# THIS REPOSITORY IS PRIVATE AND THE REPORT LEAVES IT. That is the whole design constraint. The
|
|
9
|
+
# job is deterministic — no model runs, because a model summarising a log would paraphrase
|
|
10
|
+
# whatever the log happened to contain — and it sends only:
|
|
11
|
+
#
|
|
12
|
+
# * the names this package gives its own workflows, jobs and steps
|
|
13
|
+
# * a conclusion, a runner label and a count
|
|
14
|
+
# * the id of a matched entry from a fixed catalogue of failure shapes
|
|
15
|
+
#
|
|
16
|
+
# It never sends a log line, a branch name, an issue title, a file path, a commit, a URL or a
|
|
17
|
+
# number that could be looked up. Before anything is filed, the finished text is run through a
|
|
18
|
+
# leak scanner; if the scanner matches, the report is withheld and the run goes red rather than
|
|
19
|
+
# filing it. See loops/actions/report-workflow-errors/action.yml.
|
|
20
|
+
#
|
|
21
|
+
# To turn it off: delete this file, or clear UPSTREAM_NAME below to compute the report into the
|
|
22
|
+
# job summary and file nothing.
|
|
23
|
+
|
|
24
|
+
run-name: "Error report: last ${{ github.event.inputs.lookback-hours || '24' }}h"
|
|
25
|
+
|
|
26
|
+
on:
|
|
27
|
+
schedule:
|
|
28
|
+
# Slot this per repository the same way as the audit cron: the report is cheap, but four
|
|
29
|
+
# repositories waking at the same minute still queue against one another.
|
|
30
|
+
- cron: "11 7 * * *"
|
|
31
|
+
workflow_dispatch:
|
|
32
|
+
inputs:
|
|
33
|
+
lookback-hours:
|
|
34
|
+
description: "How far back to look"
|
|
35
|
+
required: false
|
|
36
|
+
type: string
|
|
37
|
+
default: '24'
|
|
38
|
+
dry-run:
|
|
39
|
+
description: "Compute the report and file nothing"
|
|
40
|
+
required: false
|
|
41
|
+
type: boolean
|
|
42
|
+
default: false
|
|
43
|
+
|
|
44
|
+
permissions:
|
|
45
|
+
contents: read
|
|
46
|
+
|
|
47
|
+
env:
|
|
48
|
+
# Where the report goes. Owner and name are separate because the App token has to be scoped
|
|
49
|
+
# to the one repository, and GitHub expressions have no split(): there is no way to take the
|
|
50
|
+
# halves of "owner/name" apart inside a workflow. Clear UPSTREAM_NAME to keep the report
|
|
51
|
+
# local to the job summary.
|
|
52
|
+
UPSTREAM_OWNER: "PlainConceptsPlatform"
|
|
53
|
+
UPSTREAM_NAME: "Agentic-Workflows"
|
|
54
|
+
# How far back a scheduled run looks. Matches the cron so nothing is counted twice.
|
|
55
|
+
LOOKBACK_HOURS: "24"
|
|
56
|
+
|
|
57
|
+
jobs:
|
|
58
|
+
report:
|
|
59
|
+
# Fork of a consumer: the fleet is not theirs and neither is the report.
|
|
60
|
+
if: "!github.event.repository.fork"
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
timeout-minutes: 20
|
|
63
|
+
concurrency:
|
|
64
|
+
group: agentics-error-report
|
|
65
|
+
cancel-in-progress: false
|
|
66
|
+
permissions:
|
|
67
|
+
# Reading this repository's own runs, jobs and logs. Read-only throughout: the job files
|
|
68
|
+
# upstream with a separate token and writes nothing here.
|
|
69
|
+
contents: read
|
|
70
|
+
actions: read
|
|
71
|
+
steps:
|
|
72
|
+
- name: Checkout workflow actions
|
|
73
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
74
|
+
with:
|
|
75
|
+
persist-credentials: false
|
|
76
|
+
|
|
77
|
+
# A token for the upstream repository only. The App has to be installed there; where it is
|
|
78
|
+
# not, this step fails, the next one is skipped, and the report still lands in the job
|
|
79
|
+
# summary with a warning rather than a red run.
|
|
80
|
+
- name: Generate an upstream token
|
|
81
|
+
id: upstream-token
|
|
82
|
+
if: env.UPSTREAM_NAME != ''
|
|
83
|
+
continue-on-error: true
|
|
84
|
+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
85
|
+
with:
|
|
86
|
+
app-id: ${{ secrets.BOT_APP_ID }}
|
|
87
|
+
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
88
|
+
owner: ${{ env.UPSTREAM_OWNER }}
|
|
89
|
+
repositories: ${{ env.UPSTREAM_NAME }}
|
|
90
|
+
|
|
91
|
+
- uses: ./.github/actions/report-workflow-errors
|
|
92
|
+
with:
|
|
93
|
+
token: ${{ github.token }}
|
|
94
|
+
upstream-token: ${{ steps.upstream-token.outputs.token }}
|
|
95
|
+
upstream-repo: ${{ env.UPSTREAM_OWNER }}/${{ env.UPSTREAM_NAME }}
|
|
96
|
+
lookback-hours: ${{ github.event.inputs.lookback-hours || env.LOOKBACK_HOURS }}
|
|
97
|
+
dry-run: ${{ github.event.inputs.dry-run || 'false' }}
|