@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.
@@ -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 workerByRoute = new Map(workflowRoutes.map((route) => [route.name, route.worker]));
289
+ const routeByName = new Map(workflowRoutes.map((route) => [route.name, route]));
280
290
  const removed = [];
281
- for (const route of routes) {
282
- const worker = workerByRoute.get(route);
283
- if (worker === undefined)
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 });
@@ -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
- .map((route) => route.worker));
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 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", "scripts/compile-agent-workflows.mjs", "opencode.ci.json", "docs/diagrams.md"];
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;
@@ -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 bumps the version, tags, and publishes a GitHub Release. Manual dispatch only (operation=release).", defaultEnabled: true },
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