@plainconceptsplatform/workflows 1.0.14 → 1.2.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 +17 -7
- package/dist/route-processing.js +1 -1
- package/dist/workflow-catalog.d.ts +4 -1
- package/dist/workflow-catalog.js +10 -1
- package/loops/actions/capture-evidence/action.yml +118 -0
- package/loops/actions/capture-evidence/capture.mjs +693 -0
- package/loops/actions/classify-route/action.yml +1 -0
- package/loops/actions/classify-route/classify-route.sh +15 -3
- package/loops/actions/report-workflow-errors/action.yml +1 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +78 -61
- package/loops/docs/diagrams.md +48 -44
- package/loops/workflows/agent-merge-gate.md +0 -45
- package/loops/workflows/agent-release.md +73 -17
- package/loops/workflows/evidence.yml +460 -0
- package/loops/workflows/release-tag.yml +98 -0
- package/loops/workflows/shared/opencode-ci.md +5 -6
- package/loops/workflows/work-router.yml +29 -14
- package/package.json +1 -1
- package/loops/actions/attach-screenshots/action.yml +0 -93
- package/loops/workflows/agent-visual-verify.md +0 -350
|
@@ -5,7 +5,7 @@ import { dirname, join, relative, resolve } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { parse as parseYaml } from "yaml";
|
|
8
|
-
import { catalogTemplates, mandatoryFiles, routeNames, templateNames, workflowRoutes } from "./workflow-catalog.js";
|
|
8
|
+
import { catalogTemplates, mandatoryFiles, retiredWorkflowFiles, routeNames, templateNames, workflowRoutes } from "./workflow-catalog.js";
|
|
9
9
|
import { processRoutes, excludedWorkerFiles } from "./route-processing.js";
|
|
10
10
|
import { generateOpencodeCi, generateOpencodeConfig, generateStackDefaults, injectStackEnv } from "./stack-defaults.js";
|
|
11
11
|
import { mergeRouter, mergeWorker } from "./worker-env.js";
|
|
@@ -202,6 +202,16 @@ async function orphanedManagedFiles(repositoryPath, keep) {
|
|
|
202
202
|
orphans.push(target);
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
|
+
// A retired worker is ours when it still carries the header; its lock was generated from it, so
|
|
206
|
+
// the lock goes with it either way.
|
|
207
|
+
for (const file of retiredWorkflowFiles) {
|
|
208
|
+
const target = `.github/workflows/${file}`;
|
|
209
|
+
const path = join(repositoryPath, target);
|
|
210
|
+
if (!await exists(path))
|
|
211
|
+
continue;
|
|
212
|
+
if (file.endsWith(".lock.yml") || hasOwnershipHeader(await readFile(path, "utf8")))
|
|
213
|
+
orphans.push(target);
|
|
214
|
+
}
|
|
205
215
|
return orphans.sort();
|
|
206
216
|
}
|
|
207
217
|
async function removeFiles(repositoryPath, targets) {
|
|
@@ -276,14 +286,14 @@ export async function installedRoutes(repositoryPath) {
|
|
|
276
286
|
return found.filter((name) => name !== undefined);
|
|
277
287
|
}
|
|
278
288
|
export async function removeRouteFiles(repositoryPath, routes) {
|
|
279
|
-
const
|
|
289
|
+
const routeByName = new Map(workflowRoutes.map((route) => [route.name, route]));
|
|
280
290
|
const removed = [];
|
|
281
|
-
for (const
|
|
282
|
-
const
|
|
283
|
-
if (
|
|
291
|
+
for (const name of routes) {
|
|
292
|
+
const route = routeByName.get(name);
|
|
293
|
+
if (route === undefined)
|
|
284
294
|
continue;
|
|
285
|
-
const lock = worker.replace(/\.md$/, ".lock.yml");
|
|
286
|
-
for (const file of [worker, lock]) {
|
|
295
|
+
const lock = route.worker.replace(/\.md$/, ".lock.yml");
|
|
296
|
+
for (const file of [route.worker, lock, ...(route.companions ?? [])]) {
|
|
287
297
|
const destination = join(repositoryPath, ".github", "workflows", file);
|
|
288
298
|
if (await exists(destination)) {
|
|
289
299
|
await rm(destination, { force: true });
|
package/dist/route-processing.js
CHANGED
|
@@ -41,7 +41,7 @@ export function processRoutes(files, selectedRoutes) {
|
|
|
41
41
|
export function excludedWorkerFiles(selectedRoutes) {
|
|
42
42
|
return new Set(workflowRoutes
|
|
43
43
|
.filter((route) => !selectedRoutes.includes(route.name))
|
|
44
|
-
.
|
|
44
|
+
.flatMap((route) => [route.worker, ...(route.companions ?? [])]));
|
|
45
45
|
}
|
|
46
46
|
// Remove a top-level job (two-space indent) together with the comment lines that introduce it.
|
|
47
47
|
// A job's comment describes that job; left behind it describes nothing.
|
|
@@ -5,9 +5,12 @@ export interface WorkflowRoute {
|
|
|
5
5
|
readonly worker: string;
|
|
6
6
|
readonly description: string;
|
|
7
7
|
readonly defaultEnabled: boolean;
|
|
8
|
+
/** Plain workflows that exist only to serve this route. They are installed and removed with it. */
|
|
9
|
+
readonly companions?: readonly string[];
|
|
8
10
|
}
|
|
9
11
|
export declare const workflowRoutes: readonly WorkflowRoute[];
|
|
10
|
-
export declare const
|
|
12
|
+
export declare const retiredWorkflowFiles: readonly ["agent-visual-verify.md", "agent-visual-verify.lock.yml"];
|
|
13
|
+
export declare const packageOwnedTargets: readonly [".github/actions", ".github/workflows/agent-*.md", ".github/workflows/shared/platform-defaults.md", ".github/workflows/shared/opencode-ci.md", ".github/workflows/work-router.yml", ".github/workflows/evidence.yml", "scripts/compile-agent-workflows.mjs", "opencode.ci.json", "docs/diagrams.md"];
|
|
11
14
|
export interface MandatoryFile {
|
|
12
15
|
readonly source: string;
|
|
13
16
|
readonly target: string;
|
package/dist/workflow-catalog.js
CHANGED
|
@@ -14,7 +14,15 @@ export const workflowRoutes = [
|
|
|
14
14
|
{ name: "apply-review", worker: "agent-apply-review.md", description: "Applies reviewer feedback to an open pull request the bot authored, then pushes the fixes to the same branch.", defaultEnabled: true },
|
|
15
15
|
{ name: "merge-gate", worker: "agent-merge-gate.md", description: "Decides what happens to a bot-authored pull request once CI has reported: merge, hand to a human, or fix CI.", defaultEnabled: true },
|
|
16
16
|
{ name: "audit", worker: "agent-audit.md", description: "Read-only repository audit. Finds 5-7 problems, scores each 1-10, and files a single issue that Refine then sizes and splits.", defaultEnabled: true },
|
|
17
|
-
{ name: "release", worker: "agent-release.md", description: "Writes release notes from the commit log, then a deterministic job
|
|
17
|
+
{ name: "release", worker: "agent-release.md", description: "Writes release notes from the commit log, then a deterministic job opens a version-bump pull request with auto-merge; once it merges, release-tag.yml tags the commit and publishes the GitHub Release. Manual dispatch only (operation=release).", defaultEnabled: true, companions: ["release-tag.yml"] },
|
|
18
|
+
];
|
|
19
|
+
// Workflow files the package used to ship and no longer does. `.github/workflows/` is not pruned
|
|
20
|
+
// wholesale, because a missing worker there means a route the consumer left out, so a retired one
|
|
21
|
+
// is named here or it sits in every consumer forever with a lock that still compiles.
|
|
22
|
+
// agent-visual-verify was replaced by evidence.yml in 1.1.0.
|
|
23
|
+
export const retiredWorkflowFiles = [
|
|
24
|
+
"agent-visual-verify.md",
|
|
25
|
+
"agent-visual-verify.lock.yml",
|
|
18
26
|
];
|
|
19
27
|
export const packageOwnedTargets = [
|
|
20
28
|
".github/actions",
|
|
@@ -22,6 +30,7 @@ export const packageOwnedTargets = [
|
|
|
22
30
|
".github/workflows/shared/platform-defaults.md",
|
|
23
31
|
".github/workflows/shared/opencode-ci.md",
|
|
24
32
|
".github/workflows/work-router.yml",
|
|
33
|
+
".github/workflows/evidence.yml",
|
|
25
34
|
"scripts/compile-agent-workflows.mjs",
|
|
26
35
|
"opencode.ci.json",
|
|
27
36
|
"docs/diagrams.md",
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/capture-evidence/action.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
name: Capture evidence
|
|
3
|
+
description: >
|
|
4
|
+
Reads a pull request's verification plans (written by the harness skill pc-repo-verify) and,
|
|
5
|
+
in capture mode, replays them in headless Chromium against the running app, writing one JPEG
|
|
6
|
+
per waypoint under files/<plan directory>/evidence/, laid out as the repository is, plus
|
|
7
|
+
manifest.json and comment.md. Inspect mode only reports whether any plan is
|
|
8
|
+
replayable, at which address, and with which start command. No model, no network beyond
|
|
9
|
+
installing its two pinned packages.
|
|
10
|
+
inputs:
|
|
11
|
+
mode:
|
|
12
|
+
description: inspect, capture, or unreached (the app never answered; record why and stop).
|
|
13
|
+
required: true
|
|
14
|
+
root:
|
|
15
|
+
description: Checkout of the pull request head the plans are read from.
|
|
16
|
+
required: true
|
|
17
|
+
plans:
|
|
18
|
+
description: Newline-separated plan paths relative to root.
|
|
19
|
+
required: true
|
|
20
|
+
out-dir:
|
|
21
|
+
description: Where capture and unreached write the screenshots, manifest.json and comment.md.
|
|
22
|
+
required: false
|
|
23
|
+
default: ''
|
|
24
|
+
playwright-version:
|
|
25
|
+
description: playwright-core version. Must match the mcr.microsoft.com/playwright image tag the capture runs in.
|
|
26
|
+
required: false
|
|
27
|
+
default: '1.62.1'
|
|
28
|
+
yaml-version:
|
|
29
|
+
description: yaml package version.
|
|
30
|
+
required: false
|
|
31
|
+
default: '2.9.0'
|
|
32
|
+
outputs:
|
|
33
|
+
runnable:
|
|
34
|
+
description: inspect only. true when at least one plan has a journey to replay.
|
|
35
|
+
value: ${{ steps.run.outputs.runnable }}
|
|
36
|
+
url:
|
|
37
|
+
description: inspect only. The address the first replayable plan expects the app at.
|
|
38
|
+
value: ${{ steps.run.outputs.url }}
|
|
39
|
+
start:
|
|
40
|
+
description: inspect only. The start command the first replayable plan names, empty for the default.
|
|
41
|
+
value: ${{ steps.run.outputs.start }}
|
|
42
|
+
runs:
|
|
43
|
+
using: composite
|
|
44
|
+
steps:
|
|
45
|
+
- name: Install the plan runner
|
|
46
|
+
shell: bash
|
|
47
|
+
env:
|
|
48
|
+
MODE: ${{ inputs.mode }}
|
|
49
|
+
PLAYWRIGHT_VERSION: ${{ inputs.playwright-version }}
|
|
50
|
+
YAML_VERSION: ${{ inputs.yaml-version }}
|
|
51
|
+
ACTION_DIR: ${{ github.action_path }}
|
|
52
|
+
run: |
|
|
53
|
+
set -euo pipefail
|
|
54
|
+
runner="$RUNNER_TEMP/capture-evidence"
|
|
55
|
+
mkdir -p "$runner"
|
|
56
|
+
# A copy beside its node_modules: ESM resolves bare imports from the file's own
|
|
57
|
+
# directory, never from NODE_PATH, so the script cannot run from the action folder.
|
|
58
|
+
cp "$ACTION_DIR/capture.mjs" "$runner/capture.mjs"
|
|
59
|
+
if [ ! -d "$runner/node_modules/playwright-core" ] || [ ! -d "$runner/node_modules/yaml" ]; then
|
|
60
|
+
npm install --prefix "$runner" --no-save --no-package-lock --no-audit --no-fund \
|
|
61
|
+
"playwright-core@${PLAYWRIGHT_VERSION}" "yaml@${YAML_VERSION}"
|
|
62
|
+
fi
|
|
63
|
+
# capture runs in the Playwright image, which carries Chromium and its system libraries;
|
|
64
|
+
# the runner has neither, and its user has no sudo to add them (libatk was the first of
|
|
65
|
+
# many missing). Only without Docker does the browser come down here, as a best effort.
|
|
66
|
+
if [ "$MODE" = "capture" ] && ! command -v docker > /dev/null 2>&1; then
|
|
67
|
+
node "$runner/node_modules/playwright-core/cli.js" install --only-shell chromium
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
- name: Run the plans
|
|
71
|
+
id: run
|
|
72
|
+
shell: bash
|
|
73
|
+
env:
|
|
74
|
+
MODE: ${{ inputs.mode }}
|
|
75
|
+
ROOT: ${{ inputs.root }}
|
|
76
|
+
PLANS: ${{ inputs.plans }}
|
|
77
|
+
OUT_DIR: ${{ inputs.out-dir }}
|
|
78
|
+
PLAYWRIGHT_VERSION: ${{ inputs.playwright-version }}
|
|
79
|
+
# Chromium's own stderr. A browser that dies on launch says why only here.
|
|
80
|
+
DEBUG: pw:browser
|
|
81
|
+
run: |
|
|
82
|
+
set -euo pipefail
|
|
83
|
+
runner="$RUNNER_TEMP/capture-evidence"
|
|
84
|
+
mapfile -t plans < <(printf '%s\n' "$PLANS" | sed '/^[[:space:]]*$/d')
|
|
85
|
+
if [ "${#plans[@]}" -eq 0 ]; then
|
|
86
|
+
echo "::error::no plan paths were given"
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
case "$MODE" in
|
|
90
|
+
inspect)
|
|
91
|
+
report="$(node "$runner/capture.mjs" inspect "$ROOT" "${plans[@]}")"
|
|
92
|
+
echo "$report"
|
|
93
|
+
{
|
|
94
|
+
echo "runnable=$(jq -r '.runnable' <<<"$report")"
|
|
95
|
+
echo "url=$(jq -r '.url' <<<"$report")"
|
|
96
|
+
echo "start=$(jq -r '.start' <<<"$report")"
|
|
97
|
+
} >> "$GITHUB_OUTPUT"
|
|
98
|
+
;;
|
|
99
|
+
capture)
|
|
100
|
+
[ -n "$OUT_DIR" ] || { echo "::error::capture needs out-dir"; exit 1; }
|
|
101
|
+
mkdir -p "$OUT_DIR"
|
|
102
|
+
if command -v docker > /dev/null 2>&1; then
|
|
103
|
+
# The host network, so the browser reaches the app on localhost where it started;
|
|
104
|
+
# the runner's own user, so what it writes the publish job can read.
|
|
105
|
+
docker run --rm --network host --ipc host --user "$(id -u):$(id -g)" -e HOME=/tmp -e DEBUG -e EVIDENCE_SHA -e EVIDENCE_RUN_URL -e EVIDENCE_STEP_TIMEOUT_MS -e EVIDENCE_NAVIGATION_TIMEOUT_MS -v "$runner:$runner:ro" -v "$GITHUB_WORKSPACE:$GITHUB_WORKSPACE:ro" -v "$OUT_DIR:$OUT_DIR" -w "$GITHUB_WORKSPACE" "mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-noble" node "$runner/capture.mjs" capture "$ROOT" "$OUT_DIR" "${plans[@]}"
|
|
106
|
+
else
|
|
107
|
+
node "$runner/capture.mjs" capture "$ROOT" "$OUT_DIR" "${plans[@]}"
|
|
108
|
+
fi
|
|
109
|
+
;;
|
|
110
|
+
unreached)
|
|
111
|
+
[ -n "$OUT_DIR" ] || { echo "::error::unreached needs out-dir"; exit 1; }
|
|
112
|
+
node "$runner/capture.mjs" unreached "$ROOT" "$OUT_DIR" "${plans[@]}"
|
|
113
|
+
;;
|
|
114
|
+
*)
|
|
115
|
+
echo "::error::unknown mode '$MODE'"
|
|
116
|
+
exit 1
|
|
117
|
+
;;
|
|
118
|
+
esac
|