@plainconceptsplatform/workflows 0.3.0 → 0.4.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 +13 -2
- package/dist/catalog-installation.js +8 -3
- package/dist/catalog-installation.test.js +53 -1
- package/dist/index.js +83 -24
- package/dist/index.test.js +122 -2
- package/dist/repository-inspection.js +27 -5
- package/dist/repository-inspection.test.js +2 -2
- package/dist/route-processing.js +33 -4
- package/dist/route-processing.test.js +33 -5
- package/dist/stack-defaults.js +4 -5
- package/dist/stack-defaults.test.js +7 -7
- package/loops/scripts/compile-agent-workflows.mjs +20 -6
- package/loops/templates/ci/app-ci-dotnet-next.yml +5 -1
- package/loops/templates/opencode/opencode.ci.json +2 -1
- package/loops/workflows/shared/platform-defaults.md +2 -0
- package/loops/workflows/work-router.yml +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ The primary entrypoint is the interactive TUI. Run it with no arguments:
|
|
|
10
10
|
npx @plainconceptsplatform/workflows
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
The TUI lists all routes and templates with install status. Arrow keys navigate, space toggles, Enter installs. Selecting
|
|
13
|
+
The TUI lists all routes and templates with install status. Arrow keys navigate, space toggles, Enter installs. Selecting routes installs only those route workers plus mandatory files (opencode.ci.json, compile script, shared imports, actions, router, classifier, route matrix). Selecting only templates installs just those templates.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -35,7 +35,18 @@ pnpm exec workflows add
|
|
|
35
35
|
|
|
36
36
|
`init` inspects the repository and reports its stack and visibility. It does not create or manage repository configuration or a manifest.
|
|
37
37
|
|
|
38
|
-
`add` installs
|
|
38
|
+
`add` installs mandatory files (opencode.ci.json, compile script, shared imports, actions, router, classifier, route matrix) when called with no route arguments. Pass route names as positional arguments to install specific route workers alongside the mandatory files:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
workflows add # mandatory files only, no worker .md files
|
|
42
|
+
workflows add implement refine direct # those route workers plus mandatory files
|
|
43
|
+
workflows add --template agentics-checks # named template only (no mandatory files)
|
|
44
|
+
workflows add refine --template agentics-checks --force # routes + template + mandatory, overwriting conflicts
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Route names: refine, implement, direct, apply-review, merge-gate, audit, propose. Unknown arguments produce an error.
|
|
48
|
+
|
|
49
|
+
Use `workflows update --force` to force-overwrite managed files that differ from the package source.
|
|
39
50
|
|
|
40
51
|
Install optional standalone templates with `add --template`. Available templates are `agentics-checks`, `agentics-maintenance`, `app-ci-dotnet-next`, and `app-ci-node-monorepo`. CI templates are stack-specific copies, not a combined template. Edit their top-level `env:` values for repository paths, package names, and commands.
|
|
41
52
|
|
|
@@ -133,10 +133,13 @@ export function isTemplateName(value) {
|
|
|
133
133
|
}
|
|
134
134
|
export async function ensurePreCommitHook(repositoryPath) {
|
|
135
135
|
const hookPath = join(repositoryPath, ".husky", "pre-commit");
|
|
136
|
-
|
|
136
|
+
const compileLine = "node scripts/compile-agent-workflows.mjs";
|
|
137
|
+
if (!await exists(hookPath)) {
|
|
138
|
+
await mkdir(dirname(hookPath), { recursive: true });
|
|
139
|
+
await writeFile(hookPath, `${compileLine}\n`, "utf8");
|
|
137
140
|
return;
|
|
141
|
+
}
|
|
138
142
|
const content = await readFile(hookPath, "utf8");
|
|
139
|
-
const compileLine = "node scripts/compile-agent-workflows.mjs";
|
|
140
143
|
if (content.includes("compile-agent-workflows"))
|
|
141
144
|
return;
|
|
142
145
|
const newContent = content.endsWith("\n") || content === ""
|
|
@@ -156,7 +159,9 @@ function catalogTemplateMeta(template) {
|
|
|
156
159
|
throw new Error(`Unknown template: ${template}`);
|
|
157
160
|
const directory = template.startsWith("opencode") ? "opencode" : template.startsWith("app-ci-") ? "ci" : "agentics";
|
|
158
161
|
const isWorkflow = entry.file.endsWith(".yml");
|
|
159
|
-
const target =
|
|
162
|
+
const target = template === "app-ci-dotnet-next"
|
|
163
|
+
? ".github/workflows/app-ci.yml"
|
|
164
|
+
: isWorkflow ? `.github/workflows/${entry.file}` : entry.file;
|
|
160
165
|
return { directory, file: entry.file, target };
|
|
161
166
|
}
|
|
162
167
|
async function catalogFiles(sourcePath) {
|
|
@@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { catalogSourcePath, installCatalog, installMandatoryFiles, installTemplate } from "./catalog-installation.js";
|
|
5
|
+
import { catalogSourcePath, ensurePreCommitHook, installCatalog, installMandatoryFiles, installTemplate } from "./catalog-installation.js";
|
|
6
6
|
const temporaryDirectories = [];
|
|
7
7
|
afterEach(async () => {
|
|
8
8
|
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })));
|
|
@@ -15,6 +15,34 @@ describe("catalog installation", () => {
|
|
|
15
15
|
});
|
|
16
16
|
expect(catalogSourcePath(join(packageDirectory, "dist", "catalog-installation.js"))).toBe(join(packageDirectory, "loops"));
|
|
17
17
|
});
|
|
18
|
+
it("installCatalog with empty selectedRoutes installs mandatory and infrastructure files but no worker .md files", async () => {
|
|
19
|
+
const sourcePath = await createDirectory({
|
|
20
|
+
"actions/check/action.yml": "name: Check\n",
|
|
21
|
+
"workflows/agent-refine.md": "# Refine\n",
|
|
22
|
+
"workflows/agent-implement.md": "# Implement\n",
|
|
23
|
+
"workflows/agent-direct.md": "# Direct\n",
|
|
24
|
+
"workflows/agent-audit.md": "# Audit\n",
|
|
25
|
+
"workflows/agent-propose.md": "# Propose\n",
|
|
26
|
+
"workflows/agent-apply-review.md": "# Apply Review\n",
|
|
27
|
+
"workflows/agent-merge-gate.md": "# Merge Gate\n",
|
|
28
|
+
"workflows/shared/defaults.md": "defaults\n",
|
|
29
|
+
"workflows/work-router.yml": "name: Router\n",
|
|
30
|
+
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
31
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
32
|
+
});
|
|
33
|
+
const repositoryPath = await createDirectory({});
|
|
34
|
+
const result = await installCatalog(repositoryPath, { sourcePath, selectedRoutes: [] });
|
|
35
|
+
expect(result.installed).toContain("opencode.ci.json");
|
|
36
|
+
expect(result.installed).toContain("scripts/compile-agent-workflows.mjs");
|
|
37
|
+
expect(result.installed).toContain(".github/actions/check/action.yml");
|
|
38
|
+
expect(result.installed.some((f) => f.endsWith("shared/defaults.md"))).toBe(true);
|
|
39
|
+
expect(result.installed.some((f) => f.endsWith("work-router.yml"))).toBe(true);
|
|
40
|
+
// No worker .md files
|
|
41
|
+
expect(result.installed).not.toContain(".github/workflows/agent-refine.md");
|
|
42
|
+
expect(result.installed).not.toContain(".github/workflows/agent-implement.md");
|
|
43
|
+
expect(result.installed).not.toContain(".github/workflows/agent-audit.md");
|
|
44
|
+
expect(result.installed).not.toContain(".github/workflows/agent-propose.md");
|
|
45
|
+
});
|
|
18
46
|
it("installs package-owned loops files including mandatory opencode.ci.json and compile script", async () => {
|
|
19
47
|
const sourcePath = await createDirectory({
|
|
20
48
|
"actions/check/action.yml": "name: Check\n",
|
|
@@ -215,6 +243,19 @@ describe("catalog installation", () => {
|
|
|
215
243
|
const compileEntries = result.installed.filter((path) => path === "scripts/compile-agent-workflows.mjs");
|
|
216
244
|
expect(compileEntries).toHaveLength(1);
|
|
217
245
|
});
|
|
246
|
+
it("creates a Husky pre-commit hook when the consumer has none", async () => {
|
|
247
|
+
const repositoryPath = await createDirectory({});
|
|
248
|
+
await ensurePreCommitHook(repositoryPath);
|
|
249
|
+
await expect(readFile(join(repositoryPath, ".husky", "pre-commit"), "utf8"))
|
|
250
|
+
.resolves.toBe("node scripts/compile-agent-workflows.mjs\n");
|
|
251
|
+
});
|
|
252
|
+
it("keeps existing pre-commit commands and appends the compiler once", async () => {
|
|
253
|
+
const repositoryPath = await createDirectory({ ".husky/pre-commit": "pnpm lint\n" });
|
|
254
|
+
await ensurePreCommitHook(repositoryPath);
|
|
255
|
+
await ensurePreCommitHook(repositoryPath);
|
|
256
|
+
await expect(readFile(join(repositoryPath, ".husky", "pre-commit"), "utf8"))
|
|
257
|
+
.resolves.toBe("pnpm lint\nnode scripts/compile-agent-workflows.mjs\n");
|
|
258
|
+
});
|
|
218
259
|
it("installs the opencode.ci.json template to the repository root", async () => {
|
|
219
260
|
const sourcePath = await createDirectory({
|
|
220
261
|
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
@@ -226,6 +267,17 @@ describe("catalog installation", () => {
|
|
|
226
267
|
});
|
|
227
268
|
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-2\" }\n");
|
|
228
269
|
});
|
|
270
|
+
it("installs the .NET and Next.js CI template as app-ci.yml", async () => {
|
|
271
|
+
const sourcePath = await createDirectory({
|
|
272
|
+
"templates/ci/app-ci-dotnet-next.yml": "name: App: CI\n",
|
|
273
|
+
});
|
|
274
|
+
const repositoryPath = await createDirectory({});
|
|
275
|
+
await expect(installTemplate(repositoryPath, "app-ci-dotnet-next", { sourcePath })).resolves.toEqual({
|
|
276
|
+
installed: [".github/workflows/app-ci.yml"],
|
|
277
|
+
conflicts: [],
|
|
278
|
+
});
|
|
279
|
+
await expect(readFile(join(repositoryPath, ".github/workflows/app-ci.yml"), "utf8")).resolves.toBe("name: App: CI\n");
|
|
280
|
+
});
|
|
229
281
|
it("requires force to replace the opencode.ci.json template", async () => {
|
|
230
282
|
const sourcePath = await createDirectory({
|
|
231
283
|
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { inspectRepository, parseVisibility, resolveVisibility } from "./repository-inspection.js";
|
|
3
3
|
import { installCatalog, installTemplate, isTemplateName } from "./catalog-installation.js";
|
|
4
4
|
import { formatCatalog, listCatalog, searchCatalog } from "./catalog-listing.js";
|
|
5
|
+
import { routeNames } from "./workflow-catalog.js";
|
|
5
6
|
import { runInteractive } from "./tui.js";
|
|
6
7
|
import { resolve } from "node:path";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -19,15 +20,25 @@ Usage: workflows <command> [options]
|
|
|
19
20
|
Commands:
|
|
20
21
|
(default) Launch the interactive TUI for selecting and installing items.
|
|
21
22
|
init Inspect the repository and report its stack and visibility.
|
|
22
|
-
add
|
|
23
|
-
update Alias for add.
|
|
23
|
+
add [routes] [--template <name>] [--force] Install route workers, a template, or mandatory files.
|
|
24
|
+
update Alias for add.
|
|
24
25
|
status Print repository inspection as JSON.
|
|
25
26
|
list List all available workflows and templates with install status.
|
|
26
27
|
search <query> Filter workflows and templates by name or description.
|
|
27
28
|
|
|
29
|
+
Route names (positional arguments to add):
|
|
30
|
+
refine, implement, direct, apply-review, merge-gate, audit, propose
|
|
31
|
+
|
|
32
|
+
add Mandatory files only (opencode.ci.json, compile script,
|
|
33
|
+
shared imports, actions, router, classifier, route matrix).
|
|
34
|
+
add implement refine direct Installs those route workers plus mandatory files.
|
|
35
|
+
add --template agentics-checks Installs the named template only (no mandatory files).
|
|
36
|
+
add refine --template agentics-checks Installs routes + mandatory + the named template.
|
|
37
|
+
add refine implement --force Forces re-install of routes plus mandatory, overwriting.
|
|
38
|
+
|
|
28
39
|
Options:
|
|
29
40
|
--visibility public|private Override repository visibility (init only).
|
|
30
|
-
--template <name> Install a standalone template instead of
|
|
41
|
+
--template <name> Install a standalone template alongside or instead of routes.
|
|
31
42
|
Templates: agentics-checks, agentics-maintenance,
|
|
32
43
|
app-ci-dotnet-next, app-ci-node-monorepo,
|
|
33
44
|
opencode.ci.json.
|
|
@@ -74,19 +85,35 @@ export async function run(arguments_, repositoryPath = process.cwd()) {
|
|
|
74
85
|
return 0;
|
|
75
86
|
}
|
|
76
87
|
if (command === "add" || command === "update") {
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
79
|
-
return fail(
|
|
80
|
-
const force = options.includes("--force");
|
|
88
|
+
const parsed = parseAddOptions(options);
|
|
89
|
+
if (parsed.kind === "invalid")
|
|
90
|
+
return fail(parsed.message);
|
|
81
91
|
const inspection = await inspectRepository(repositoryPath);
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
const routes = parsed.routes;
|
|
93
|
+
const template = parsed.template;
|
|
94
|
+
const force = parsed.force;
|
|
95
|
+
const allConflicts = [];
|
|
96
|
+
const allInstalled = [];
|
|
97
|
+
if (routes.length > 0) {
|
|
98
|
+
const result = await installCatalog(repositoryPath, { force, selectedRoutes: routes, inspection });
|
|
99
|
+
allConflicts.push(...result.conflicts);
|
|
100
|
+
allInstalled.push(...result.installed);
|
|
101
|
+
}
|
|
102
|
+
else if (template === undefined) {
|
|
103
|
+
const result = await installCatalog(repositoryPath, { force, selectedRoutes: [], inspection });
|
|
104
|
+
allConflicts.push(...result.conflicts);
|
|
105
|
+
allInstalled.push(...result.installed);
|
|
106
|
+
}
|
|
107
|
+
if (template !== undefined) {
|
|
108
|
+
const result = await installTemplate(repositoryPath, template, { force, inspection });
|
|
109
|
+
allConflicts.push(...result.conflicts);
|
|
110
|
+
allInstalled.push(...result.installed);
|
|
111
|
+
}
|
|
112
|
+
if (allConflicts.length > 0 && !force) {
|
|
113
|
+
console.error(`Catalog conflicts found. Re-run with --force to overwrite package-managed files:\n${allConflicts.join("\n")}`);
|
|
87
114
|
return 1;
|
|
88
115
|
}
|
|
89
|
-
console.log(JSON.stringify({ command,
|
|
116
|
+
console.log(JSON.stringify({ command, installed: allInstalled.sort(), conflicts: allConflicts }, null, 2));
|
|
90
117
|
return 0;
|
|
91
118
|
}
|
|
92
119
|
return fail(`Unknown command: ${command}`);
|
|
@@ -98,18 +125,50 @@ function readVisibilityOption(options) {
|
|
|
98
125
|
return "invalid";
|
|
99
126
|
return parseVisibility(options[1]) ?? "invalid";
|
|
100
127
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
128
|
+
const TEMPLATE_NAMES = "agentics-checks|agentics-maintenance|app-ci-dotnet-next|app-ci-node-monorepo|opencode.ci.json";
|
|
129
|
+
function parseAddOptions(options) {
|
|
130
|
+
const routes = [];
|
|
131
|
+
let template;
|
|
132
|
+
let templateSeen = false;
|
|
133
|
+
let force = false;
|
|
134
|
+
let i = 0;
|
|
135
|
+
while (i < options.length) {
|
|
136
|
+
const token = options[i];
|
|
137
|
+
if (token === "--force") {
|
|
138
|
+
force = true;
|
|
139
|
+
i++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (token === "--template") {
|
|
143
|
+
if (templateSeen)
|
|
144
|
+
return invalid("--template can only be specified once.");
|
|
145
|
+
templateSeen = true;
|
|
146
|
+
if (i + 1 >= options.length)
|
|
147
|
+
return invalid("--template requires a name.");
|
|
148
|
+
const name = options[i + 1];
|
|
149
|
+
if (!isTemplateName(name))
|
|
150
|
+
return invalid(`--template must be one of: ${TEMPLATE_NAMES}.`);
|
|
151
|
+
template = name;
|
|
152
|
+
i += 2;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (token.startsWith("--")) {
|
|
156
|
+
return invalid(`Unknown option: ${token}`);
|
|
157
|
+
}
|
|
158
|
+
if (routeNames.includes(token)) {
|
|
159
|
+
const route = token;
|
|
160
|
+
if (routes.includes(route))
|
|
161
|
+
return invalid(`Duplicate route: ${route}.`);
|
|
162
|
+
routes.push(route);
|
|
163
|
+
i++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
return invalid(`Unknown route: ${token}. Valid routes: ${routeNames.join(", ")}.`);
|
|
167
|
+
}
|
|
168
|
+
return { kind: "ok", routes, template, force };
|
|
110
169
|
}
|
|
111
|
-
function
|
|
112
|
-
return
|
|
170
|
+
function invalid(message) {
|
|
171
|
+
return { kind: "invalid", message };
|
|
113
172
|
}
|
|
114
173
|
function fail(message) {
|
|
115
174
|
console.error(message);
|
package/dist/index.test.js
CHANGED
|
@@ -3,10 +3,17 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { run } from "./index.js";
|
|
6
|
+
import * as catalogInstallation from "./catalog-installation.js";
|
|
6
7
|
const temporaryDirectories = [];
|
|
7
8
|
afterEach(async () => {
|
|
8
9
|
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })));
|
|
10
|
+
vi.restoreAllMocks();
|
|
9
11
|
});
|
|
12
|
+
function mockInstallers() {
|
|
13
|
+
const installCatalog = vi.spyOn(catalogInstallation, "installCatalog").mockResolvedValue({ installed: [], conflicts: [] });
|
|
14
|
+
const installTemplate = vi.spyOn(catalogInstallation, "installTemplate").mockResolvedValue({ installed: [], conflicts: [] });
|
|
15
|
+
return { installCatalog, installTemplate };
|
|
16
|
+
}
|
|
10
17
|
describe("workflows CLI", () => {
|
|
11
18
|
it("prints template names in help", async () => {
|
|
12
19
|
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
@@ -16,10 +23,30 @@ describe("workflows CLI", () => {
|
|
|
16
23
|
expect(log).toHaveBeenCalledWith(expect.stringContaining("opencode.ci.json"));
|
|
17
24
|
log.mockRestore();
|
|
18
25
|
});
|
|
19
|
-
it("
|
|
26
|
+
it("prints route names as positional arguments in help", async () => {
|
|
27
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
28
|
+
await expect(run(["--help"])).resolves.toBe(0);
|
|
29
|
+
const output = log.mock.calls[0][0];
|
|
30
|
+
expect(output).toContain("refine, implement, direct, apply-review, merge-gate, audit, propose");
|
|
31
|
+
expect(output).toContain("add [routes]");
|
|
32
|
+
log.mockRestore();
|
|
33
|
+
});
|
|
34
|
+
it("rejects an unsupported template name", async () => {
|
|
20
35
|
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
21
36
|
await expect(run(["add", "--template", "unknown"])).resolves.toBe(1);
|
|
22
|
-
expect(error).toHaveBeenCalledWith("
|
|
37
|
+
expect(error).toHaveBeenCalledWith("--template must be one of: agentics-checks|agentics-maintenance|app-ci-dotnet-next|app-ci-node-monorepo|opencode.ci.json.");
|
|
38
|
+
error.mockRestore();
|
|
39
|
+
});
|
|
40
|
+
it("rejects an unknown route passed to add", async () => {
|
|
41
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
42
|
+
await expect(run(["add", "frobnicate"])).resolves.toBe(1);
|
|
43
|
+
expect(error).toHaveBeenCalledWith("Unknown route: frobnicate. Valid routes: refine, implement, direct, apply-review, merge-gate, audit, propose.");
|
|
44
|
+
error.mockRestore();
|
|
45
|
+
});
|
|
46
|
+
it("rejects a duplicate route", async () => {
|
|
47
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
48
|
+
await expect(run(["add", "refine", "refine"])).resolves.toBe(1);
|
|
49
|
+
expect(error).toHaveBeenCalledWith("Duplicate route: refine.");
|
|
23
50
|
error.mockRestore();
|
|
24
51
|
});
|
|
25
52
|
it("lists all workflows and templates with install status [ ] when none installed", async () => {
|
|
@@ -98,6 +125,99 @@ describe("workflows CLI", () => {
|
|
|
98
125
|
expect(error).toHaveBeenCalledWith("search requires exactly one query argument.");
|
|
99
126
|
error.mockRestore();
|
|
100
127
|
});
|
|
128
|
+
it("add with no routes and no template calls installCatalog with empty selectedRoutes", async () => {
|
|
129
|
+
const { installCatalog, installTemplate } = mockInstallers();
|
|
130
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
131
|
+
await expect(run(["add"])).resolves.toBe(0);
|
|
132
|
+
expect(installCatalog).toHaveBeenCalledOnce();
|
|
133
|
+
expect(installCatalog).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ selectedRoutes: [] }));
|
|
134
|
+
expect(installTemplate).not.toHaveBeenCalled();
|
|
135
|
+
log.mockRestore();
|
|
136
|
+
});
|
|
137
|
+
it("add with explicit routes passes those routes to installCatalog", async () => {
|
|
138
|
+
const { installCatalog, installTemplate } = mockInstallers();
|
|
139
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
140
|
+
await expect(run(["add", "implement", "refine", "direct"])).resolves.toBe(0);
|
|
141
|
+
expect(installCatalog).toHaveBeenCalledOnce();
|
|
142
|
+
expect(installCatalog).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ selectedRoutes: ["implement", "refine", "direct"] }));
|
|
143
|
+
expect(installTemplate).not.toHaveBeenCalled();
|
|
144
|
+
log.mockRestore();
|
|
145
|
+
});
|
|
146
|
+
it("add --force passes force flag to installCatalog", async () => {
|
|
147
|
+
const { installCatalog } = mockInstallers();
|
|
148
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
149
|
+
await expect(run(["add", "--force"])).resolves.toBe(0);
|
|
150
|
+
expect(installCatalog).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ force: true, selectedRoutes: [] }));
|
|
151
|
+
log.mockRestore();
|
|
152
|
+
});
|
|
153
|
+
it("add with routes and --force passes both", async () => {
|
|
154
|
+
const { installCatalog } = mockInstallers();
|
|
155
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
156
|
+
await expect(run(["add", "refine", "implement", "--force"])).resolves.toBe(0);
|
|
157
|
+
expect(installCatalog).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ force: true, selectedRoutes: ["refine", "implement"] }));
|
|
158
|
+
log.mockRestore();
|
|
159
|
+
});
|
|
160
|
+
it("add with routes and template calls both installCatalog and installTemplate", async () => {
|
|
161
|
+
const { installCatalog, installTemplate } = mockInstallers();
|
|
162
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
163
|
+
await expect(run(["add", "refine", "--template", "agentics-checks"])).resolves.toBe(0);
|
|
164
|
+
expect(installCatalog).toHaveBeenCalledOnce();
|
|
165
|
+
expect(installCatalog).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ selectedRoutes: ["refine"] }));
|
|
166
|
+
expect(installTemplate).toHaveBeenCalledOnce();
|
|
167
|
+
expect(installTemplate).toHaveBeenCalledWith(expect.any(String), "agentics-checks", expect.any(Object));
|
|
168
|
+
log.mockRestore();
|
|
169
|
+
});
|
|
170
|
+
it("add with template only calls installTemplate but not installCatalog", async () => {
|
|
171
|
+
const { installCatalog, installTemplate } = mockInstallers();
|
|
172
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
173
|
+
await expect(run(["add", "--template", "agentics-checks"])).resolves.toBe(0);
|
|
174
|
+
expect(installCatalog).not.toHaveBeenCalled();
|
|
175
|
+
expect(installTemplate).toHaveBeenCalledOnce();
|
|
176
|
+
expect(installTemplate).toHaveBeenCalledWith(expect.any(String), "agentics-checks", expect.any(Object));
|
|
177
|
+
log.mockRestore();
|
|
178
|
+
});
|
|
179
|
+
it("add with routes and template and force passes all options", async () => {
|
|
180
|
+
const { installCatalog, installTemplate } = mockInstallers();
|
|
181
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
182
|
+
await expect(run(["add", "refine", "implement", "--template", "agentics-checks", "--force"])).resolves.toBe(0);
|
|
183
|
+
expect(installCatalog).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ force: true, selectedRoutes: ["refine", "implement"] }));
|
|
184
|
+
expect(installTemplate).toHaveBeenCalledWith(expect.any(String), "agentics-checks", expect.objectContaining({ force: true }));
|
|
185
|
+
log.mockRestore();
|
|
186
|
+
});
|
|
187
|
+
it("add with conflict and no force exits 1", async () => {
|
|
188
|
+
const { installCatalog } = mockInstallers();
|
|
189
|
+
installCatalog.mockResolvedValue({ installed: [], conflicts: ["opencode.ci.json"] });
|
|
190
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
191
|
+
await expect(run(["add"])).resolves.toBe(1);
|
|
192
|
+
expect(error).toHaveBeenCalledWith(expect.stringContaining("Catalog conflicts found"));
|
|
193
|
+
error.mockRestore();
|
|
194
|
+
});
|
|
195
|
+
it("add with conflict and --force still succeeds", async () => {
|
|
196
|
+
const { installCatalog } = mockInstallers();
|
|
197
|
+
installCatalog.mockResolvedValue({ installed: ["opencode.ci.json"], conflicts: ["opencode.ci.json"] });
|
|
198
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
199
|
+
await expect(run(["add", "--force"])).resolves.toBe(0);
|
|
200
|
+
expect(log).toHaveBeenCalled();
|
|
201
|
+
log.mockRestore();
|
|
202
|
+
});
|
|
203
|
+
it("add with --template but missing name produces an error", async () => {
|
|
204
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
205
|
+
await expect(run(["add", "--template"])).resolves.toBe(1);
|
|
206
|
+
expect(error).toHaveBeenCalledWith("--template requires a name.");
|
|
207
|
+
error.mockRestore();
|
|
208
|
+
});
|
|
209
|
+
it("add with multiple --template flags produces an error", async () => {
|
|
210
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
211
|
+
await expect(run(["add", "--template", "agentics-checks", "--template", "agentics-maintenance"])).resolves.toBe(1);
|
|
212
|
+
expect(error).toHaveBeenCalledWith("--template can only be specified once.");
|
|
213
|
+
error.mockRestore();
|
|
214
|
+
});
|
|
215
|
+
it("add with unknown flag produces an error", async () => {
|
|
216
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
217
|
+
await expect(run(["add", "--unknown"])).resolves.toBe(1);
|
|
218
|
+
expect(error).toHaveBeenCalledWith("Unknown option: --unknown");
|
|
219
|
+
error.mockRestore();
|
|
220
|
+
});
|
|
101
221
|
});
|
|
102
222
|
async function createRepository(files) {
|
|
103
223
|
const repositoryPath = await mkdtemp(join(tmpdir(), "workflows-"));
|
|
@@ -46,11 +46,33 @@ export function parseVisibility(value) {
|
|
|
46
46
|
return value === "public" || value === "private" ? value : undefined;
|
|
47
47
|
}
|
|
48
48
|
async function findSolutionFiles(repositoryPath) {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
const results = [];
|
|
50
|
+
await scanForSlnx(repositoryPath, "", results, 0);
|
|
51
|
+
return results.sort();
|
|
52
|
+
}
|
|
53
|
+
const MAX_DEPTH = 5;
|
|
54
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", ".next", "dist", "out", "build", ".turbo", ".cache"]);
|
|
55
|
+
async function scanForSlnx(root, relativePath, results, depth) {
|
|
56
|
+
if (depth >= MAX_DEPTH)
|
|
57
|
+
return;
|
|
58
|
+
const currentPath = join(root, relativePath);
|
|
59
|
+
let entries;
|
|
60
|
+
try {
|
|
61
|
+
entries = await readdir(currentPath, { withFileTypes: true });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
if (entry.isDirectory()) {
|
|
68
|
+
if (SKIP_DIRS.has(entry.name))
|
|
69
|
+
continue;
|
|
70
|
+
await scanForSlnx(root, join(relativePath, entry.name), results, depth + 1);
|
|
71
|
+
}
|
|
72
|
+
else if (entry.isFile() && entry.name.endsWith(".slnx")) {
|
|
73
|
+
results.push(relativePath === "" ? entry.name : join(relativePath, entry.name));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
54
76
|
}
|
|
55
77
|
async function pathExists(path) {
|
|
56
78
|
try {
|
|
@@ -24,7 +24,7 @@ describe("repository inspection", () => {
|
|
|
24
24
|
stackHints: {
|
|
25
25
|
packageJson: true,
|
|
26
26
|
pnpmLockfile: true,
|
|
27
|
-
solutionFiles: [join(
|
|
27
|
+
solutionFiles: [join("apps", "api", "Numa.slnx")],
|
|
28
28
|
openSpec: true,
|
|
29
29
|
},
|
|
30
30
|
});
|
|
@@ -54,7 +54,7 @@ describe("CLI commands", () => {
|
|
|
54
54
|
const repositoryPath = await createRepository({});
|
|
55
55
|
const error = captureConsole("error");
|
|
56
56
|
await expect(run(["update", "--invalid"], repositoryPath)).resolves.toBe(1);
|
|
57
|
-
expect(error.calls).toEqual(["
|
|
57
|
+
expect(error.calls).toEqual(["Unknown option: --invalid"]);
|
|
58
58
|
error.restore();
|
|
59
59
|
});
|
|
60
60
|
});
|
package/dist/route-processing.js
CHANGED
|
@@ -72,17 +72,46 @@ export function processRoutes(files, selectedRoutes) {
|
|
|
72
72
|
if (routerKey !== undefined) {
|
|
73
73
|
result.set(routerKey, stripRouteFromRouter(result.get(routerKey), route));
|
|
74
74
|
}
|
|
75
|
-
const classifierKey = findFileKey(result, "classify-route.sh");
|
|
76
|
-
if (classifierKey !== undefined) {
|
|
77
|
-
result.set(classifierKey, stripRouteFromClassifier(result.get(classifierKey), route));
|
|
78
|
-
}
|
|
79
75
|
const matrixKey = findFileKey(result, "verify-route-matrix.sh");
|
|
80
76
|
if (matrixKey !== undefined) {
|
|
81
77
|
result.set(matrixKey, addRouteExclusion(result.get(matrixKey), route));
|
|
82
78
|
}
|
|
83
79
|
}
|
|
80
|
+
const matrixKey = findFileKey(result, "verify-route-matrix.sh");
|
|
81
|
+
if (matrixKey !== undefined) {
|
|
82
|
+
result.set(matrixKey, createRouteMatrix(selectedRoutes));
|
|
83
|
+
}
|
|
84
84
|
return result;
|
|
85
85
|
}
|
|
86
|
+
function createRouteMatrix(selectedRoutes) {
|
|
87
|
+
const selected = selectedRoutes.join(" ");
|
|
88
|
+
const excluded = routeNames.filter((route) => !selectedRoutes.includes(route)).join(" ");
|
|
89
|
+
return `#!/usr/bin/env bash
|
|
90
|
+
set -euo pipefail
|
|
91
|
+
|
|
92
|
+
HERE="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
93
|
+
ROUTER_YML="\${HERE}/../../workflows/work-router.yml"
|
|
94
|
+
CLASSIFIER="\${HERE}/../classify-route/classify-route.sh"
|
|
95
|
+
|
|
96
|
+
bash -n "$CLASSIFIER"
|
|
97
|
+
|
|
98
|
+
for route in ${selected}; do
|
|
99
|
+
grep -q "route == '$route'" "$ROUTER_YML" || {
|
|
100
|
+
echo "FAIL: selected route '$route' has no router job" >&2
|
|
101
|
+
exit 1
|
|
102
|
+
}
|
|
103
|
+
done
|
|
104
|
+
|
|
105
|
+
for route in ${excluded}; do
|
|
106
|
+
if grep -q "route == '$route'" "$ROUTER_YML"; then
|
|
107
|
+
echo "FAIL: excluded route '$route' remains in router" >&2
|
|
108
|
+
exit 1
|
|
109
|
+
fi
|
|
110
|
+
done
|
|
111
|
+
|
|
112
|
+
echo "Route matrix: selected routes valid"
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
86
115
|
export function excludedWorkerFiles(selectedRoutes) {
|
|
87
116
|
return new Set(workflowRoutes
|
|
88
117
|
.filter((route) => !selectedRoutes.includes(route.name))
|
|
@@ -246,6 +246,24 @@ describe("processRoutes", () => {
|
|
|
246
246
|
const result = processRoutes(files, [...routeNames]);
|
|
247
247
|
expect(result).toBe(files);
|
|
248
248
|
});
|
|
249
|
+
it("strips every worker route when no routes are selected", () => {
|
|
250
|
+
const files = new Map([
|
|
251
|
+
[".github/workflows/work-router.yml", ROUTER_YAML],
|
|
252
|
+
[".github/actions/classify-route/classify-route.sh", CLASSIFIER_SH],
|
|
253
|
+
[".github/actions/verify-route-matrix/verify-route-matrix.sh", MATRIX_SH],
|
|
254
|
+
]);
|
|
255
|
+
const result = processRoutes(files, []);
|
|
256
|
+
const router = result.get(".github/workflows/work-router.yml");
|
|
257
|
+
expect(router).not.toContain("call-propose");
|
|
258
|
+
expect(router).not.toContain("call-audit");
|
|
259
|
+
expect(router).not.toContain("- propose");
|
|
260
|
+
expect(router).not.toContain("- refine");
|
|
261
|
+
const classifier = result.get(".github/actions/classify-route/classify-route.sh");
|
|
262
|
+
expect(classifier).toBe(CLASSIFIER_SH);
|
|
263
|
+
const matrix = result.get(".github/actions/verify-route-matrix/verify-route-matrix.sh");
|
|
264
|
+
expect(matrix).toContain("Route matrix: selected routes valid");
|
|
265
|
+
expect(matrix).toContain("for route in refine implement direct apply-review merge-gate audit propose");
|
|
266
|
+
});
|
|
249
267
|
it("strips propose from all three files when unselected", () => {
|
|
250
268
|
const files = new Map([
|
|
251
269
|
[".github/workflows/work-router.yml", ROUTER_YAML],
|
|
@@ -259,10 +277,10 @@ describe("processRoutes", () => {
|
|
|
259
277
|
expect(router).not.toContain('cron: "29 7 * * *"');
|
|
260
278
|
expect(router).not.toMatch(/^\s+- propose$/m);
|
|
261
279
|
const classifier = result.get(".github/actions/classify-route/classify-route.sh");
|
|
262
|
-
expect(classifier).
|
|
263
|
-
expect(classifier).not.toContain('"$PROPOSE_CRON")');
|
|
280
|
+
expect(classifier).toBe(CLASSIFIER_SH);
|
|
264
281
|
const matrix = result.get(".github/actions/verify-route-matrix/verify-route-matrix.sh");
|
|
265
|
-
expect(matrix).toContain("
|
|
282
|
+
expect(matrix).toContain("for route in refine implement direct apply-review merge-gate audit");
|
|
283
|
+
expect(matrix).toContain("for route in propose");
|
|
266
284
|
});
|
|
267
285
|
it("strips audit from all three files when unselected", () => {
|
|
268
286
|
const files = new Map([
|
|
@@ -276,8 +294,7 @@ describe("processRoutes", () => {
|
|
|
276
294
|
expect(router).not.toContain("call-audit");
|
|
277
295
|
expect(router).not.toContain('cron: "17 1 * * 1"');
|
|
278
296
|
const classifier = result.get("classify-route.sh");
|
|
279
|
-
expect(classifier).
|
|
280
|
-
expect(classifier).not.toContain('"$AUDIT_CRON")');
|
|
297
|
+
expect(classifier).toBe(CLASSIFIER_SH);
|
|
281
298
|
});
|
|
282
299
|
it("processes multiple excluded routes at once", () => {
|
|
283
300
|
const files = new Map([
|
|
@@ -307,4 +324,15 @@ describe("excludedWorkerFiles", () => {
|
|
|
307
324
|
const excluded = excludedWorkerFiles([...routeNames]);
|
|
308
325
|
expect(excluded.size).toBe(0);
|
|
309
326
|
});
|
|
327
|
+
it("returns all worker files when no routes are selected", () => {
|
|
328
|
+
const excluded = excludedWorkerFiles([]);
|
|
329
|
+
expect(excluded.size).toBe(7);
|
|
330
|
+
expect(excluded.has("agent-refine.md")).toBe(true);
|
|
331
|
+
expect(excluded.has("agent-implement.md")).toBe(true);
|
|
332
|
+
expect(excluded.has("agent-direct.md")).toBe(true);
|
|
333
|
+
expect(excluded.has("agent-apply-review.md")).toBe(true);
|
|
334
|
+
expect(excluded.has("agent-merge-gate.md")).toBe(true);
|
|
335
|
+
expect(excluded.has("agent-audit.md")).toBe(true);
|
|
336
|
+
expect(excluded.has("agent-propose.md")).toBe(true);
|
|
337
|
+
});
|
|
310
338
|
});
|
package/dist/stack-defaults.js
CHANGED
|
@@ -20,8 +20,6 @@ export function generateStackDefaults(inspection) {
|
|
|
20
20
|
}
|
|
21
21
|
export function injectStackEnv(content, defaults) {
|
|
22
22
|
let result = content;
|
|
23
|
-
if (defaults.verifyCommands === "pnpm verify")
|
|
24
|
-
return result;
|
|
25
23
|
if (result.includes("VERIFY_COMMANDS:")) {
|
|
26
24
|
result = result.replace(/ VERIFY_COMMANDS: ".*"/, ` VERIFY_COMMANDS: "${defaults.verifyCommands}"`);
|
|
27
25
|
}
|
|
@@ -33,6 +31,7 @@ export function injectStackEnv(content, defaults) {
|
|
|
33
31
|
export function generateOpencodeCi(baseContent, inspection) {
|
|
34
32
|
let result = baseContent;
|
|
35
33
|
if (inspection.stackHints.solutionFiles.length > 0) {
|
|
34
|
+
const solutionPath = inspection.stackHints.solutionFiles[0];
|
|
36
35
|
const nugetSteps = ` - name: Cache NuGet packages
|
|
37
36
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
38
37
|
with:
|
|
@@ -40,8 +39,8 @@ export function generateOpencodeCi(baseContent, inspection) {
|
|
|
40
39
|
key: nuget-\${{ runner.os }}-\${{ hashFiles('**/*.slnx', '**/Directory.Packages.props') }}
|
|
41
40
|
restore-keys: nuget-\${{ runner.os }}-
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
run: dotnet restore
|
|
42
|
+
- name: Restore .NET dependencies
|
|
43
|
+
run: dotnet restore ${solutionPath}
|
|
45
44
|
`;
|
|
46
45
|
result = insertBeforeMarker(result, nugetSteps, " - name: Install workspace dependencies");
|
|
47
46
|
}
|
|
@@ -49,7 +48,7 @@ export function generateOpencodeCi(baseContent, inspection) {
|
|
|
49
48
|
const openspecStep = ` - name: Install OpenSpec CLI
|
|
50
49
|
run: |
|
|
51
50
|
set -euo pipefail
|
|
52
|
-
npm install -g @openspec
|
|
51
|
+
npm install -g "@fission-ai/openspec@1.8.0"
|
|
53
52
|
openspec --version
|
|
54
53
|
`;
|
|
55
54
|
result = insertBeforeMarker(result, openspecStep, " - name: Install workspace dependencies");
|
|
@@ -76,7 +76,7 @@ description: test
|
|
|
76
76
|
const result = injectStackEnv(content, defaults);
|
|
77
77
|
expect(result).toContain('VERIFY_COMMANDS: "dotnet restore && dotnet build -c Release --no-restore && dotnet test"');
|
|
78
78
|
});
|
|
79
|
-
it("
|
|
79
|
+
it("injects pnpm verification when no .slnx is present", () => {
|
|
80
80
|
const content = `---
|
|
81
81
|
env:
|
|
82
82
|
REPO_RULES: "some rules"
|
|
@@ -85,7 +85,7 @@ env:
|
|
|
85
85
|
pnpmLockfile: true,
|
|
86
86
|
}));
|
|
87
87
|
const result = injectStackEnv(content, defaults);
|
|
88
|
-
expect(result).
|
|
88
|
+
expect(result).toContain('VERIFY_COMMANDS: "pnpm verify"');
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
91
|
const OPENCODE_CI_MD = `---
|
|
@@ -113,20 +113,20 @@ pre-agent-steps:
|
|
|
113
113
|
jq -e . "$FRAGMENT" > /dev/null
|
|
114
114
|
---`;
|
|
115
115
|
describe("generateOpencodeCi", () => {
|
|
116
|
-
it("adds NuGet cache and
|
|
116
|
+
it("adds NuGet cache and restores detected solution when .slnx is found", () => {
|
|
117
117
|
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
118
|
-
solutionFiles: ["
|
|
118
|
+
solutionFiles: ["apps/api/Numa.slnx"],
|
|
119
119
|
}));
|
|
120
120
|
expect(result).toContain("Cache NuGet packages");
|
|
121
121
|
expect(result).toContain("Restore .NET dependencies");
|
|
122
|
-
expect(result).toContain("dotnet restore");
|
|
122
|
+
expect(result).toContain("dotnet restore apps/api/Numa.slnx");
|
|
123
123
|
});
|
|
124
|
-
it("adds OpenSpec CLI install step when openspec/ directory exists", () => {
|
|
124
|
+
it("adds the pinned OpenSpec CLI install step when openspec/ directory exists", () => {
|
|
125
125
|
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
126
126
|
openSpec: true,
|
|
127
127
|
}));
|
|
128
128
|
expect(result).toContain("Install OpenSpec CLI");
|
|
129
|
-
expect(result).toContain("@openspec
|
|
129
|
+
expect(result).toContain("@fission-ai/openspec@1.8.0");
|
|
130
130
|
});
|
|
131
131
|
it("adds both NuGet and OpenSpec steps when both are detected", () => {
|
|
132
132
|
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Managed by @plainconceptsplatform/workflows. Source: loops/scripts/compile-agent-workflows.mjs. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
4
5
|
|
|
5
6
|
const workflowDirectory = existsSync("loops/workflows") ? "loops/workflows" : ".github/workflows";
|
|
6
7
|
|
|
@@ -21,9 +22,22 @@ const compile = spawnSync(resolveGhPath(), ["aw", "compile", "--strict", "--dir"
|
|
|
21
22
|
shell: false,
|
|
22
23
|
});
|
|
23
24
|
|
|
24
|
-
if (compile.error?.code === "ENOENT" || compile.status === null) {
|
|
25
|
+
if (compile.error?.code === "ENOENT" || compile.status === null) {
|
|
25
26
|
process.stderr.write("Could not run `gh aw compile`. Install githubnext/gh-aw first.\n");
|
|
26
27
|
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
process.exit(compile.status ?? 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (compile.status !== 0) process.exit(compile.status ?? 1);
|
|
31
|
+
|
|
32
|
+
for (const file of readdirSync(workflowDirectory)) {
|
|
33
|
+
if (!file.endsWith(".lock.yml")) continue;
|
|
34
|
+
|
|
35
|
+
const path = join(workflowDirectory, file);
|
|
36
|
+
const content = readFileSync(path, "utf8");
|
|
37
|
+
const patched = content
|
|
38
|
+
.replaceAll("opencode run --print-logs --log-level DEBUG", "opencode run --log-level ERROR")
|
|
39
|
+
.replaceAll("opencode run --print-logs --log-level ERROR", "opencode run --log-level ERROR")
|
|
40
|
+
.replaceAll("--log-level DEBUG", "--log-level ERROR");
|
|
41
|
+
|
|
42
|
+
if (patched !== content) writeFileSync(path, patched);
|
|
43
|
+
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# Managed by @plainconceptsplatform/workflows. Source: loops/templates/ci/app-ci-dotnet-next.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
-
name: "App: CI
|
|
2
|
+
name: "App: CI"
|
|
3
3
|
|
|
4
4
|
on:
|
|
5
5
|
pull_request:
|
|
6
6
|
schedule:
|
|
7
7
|
- cron: "0 6 * * 1"
|
|
8
8
|
workflow_dispatch:
|
|
9
|
+
workflow_call:
|
|
10
|
+
secrets:
|
|
11
|
+
NPM_REGISTRY_TOKEN:
|
|
12
|
+
required: false
|
|
9
13
|
|
|
10
14
|
env:
|
|
11
15
|
DOTNET_VERSION: "10.0.x"
|
|
@@ -39,7 +39,7 @@ on:
|
|
|
39
39
|
pull_request_target:
|
|
40
40
|
types: [opened, synchronize, reopened]
|
|
41
41
|
workflow_run:
|
|
42
|
-
workflows: ["App: CI"]
|
|
42
|
+
workflows: ["App: CI", "CI"]
|
|
43
43
|
types: [completed]
|
|
44
44
|
branches:
|
|
45
45
|
- "fix/*"
|
|
@@ -142,7 +142,10 @@ jobs:
|
|
|
142
142
|
;;
|
|
143
143
|
esac
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
# Platform bot owns Safe Outputs. Its label changes must be able to
|
|
146
|
+
# start the follow-on worker, even though GitHub does not report it
|
|
147
|
+
# as a repository collaborator.
|
|
148
|
+
if [ "$ACTOR" = "github-actions[bot]" ] || [ "$ACTOR" = "platform-devbox[bot]" ]; then
|
|
146
149
|
echo "trusted=true" >> "$GITHUB_OUTPUT"
|
|
147
150
|
exit 0
|
|
148
151
|
fi
|