@plainconceptsplatform/workflows 0.2.1 → 0.3.2
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.d.ts +7 -1
- package/dist/catalog-installation.js +94 -12
- package/dist/catalog-installation.test.js +39 -0
- package/dist/index.js +84 -24
- package/dist/index.test.js +122 -2
- package/dist/repository-inspection.test.js +1 -1
- package/dist/route-processing.d.ts +6 -0
- package/dist/route-processing.js +121 -0
- package/dist/route-processing.test.d.ts +1 -0
- package/dist/route-processing.test.js +340 -0
- package/dist/stack-defaults.d.ts +11 -0
- package/dist/stack-defaults.js +105 -0
- package/dist/stack-defaults.test.d.ts +1 -0
- package/dist/stack-defaults.test.js +257 -0
- package/dist/tui.js +12 -2
- package/loops/templates/ci/app-ci-dotnet-next.yml +5 -1
- package/loops/workflows/shared/platform-defaults.md +0 -2
- package/loops/workflows/work-router.yml +33 -0
- package/package.json +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { generateOpencodeCi, generateOpencodeConfig, generateStackDefaults, injectStackEnv, } from "./stack-defaults.js";
|
|
3
|
+
function makeInspection(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
repositoryPath: "/repo",
|
|
6
|
+
existingAgentWorkflows: [],
|
|
7
|
+
stackHints: {
|
|
8
|
+
packageJson: false,
|
|
9
|
+
pnpmLockfile: false,
|
|
10
|
+
solutionFiles: [],
|
|
11
|
+
openSpec: false,
|
|
12
|
+
...overrides,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
describe("generateStackDefaults", () => {
|
|
17
|
+
it("returns .NET verify commands when .slnx is found", () => {
|
|
18
|
+
const defaults = generateStackDefaults(makeInspection({
|
|
19
|
+
solutionFiles: ["app.slnx"],
|
|
20
|
+
}));
|
|
21
|
+
expect(defaults.verifyCommands).toBe("dotnet restore && dotnet build -c Release --no-restore && dotnet test");
|
|
22
|
+
expect(defaults.hasDotnet).toBe(true);
|
|
23
|
+
expect(defaults.hasNodeOnly).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
it("returns pnpm verify when pnpm-lock.yaml is found without .slnx", () => {
|
|
26
|
+
const defaults = generateStackDefaults(makeInspection({
|
|
27
|
+
pnpmLockfile: true,
|
|
28
|
+
}));
|
|
29
|
+
expect(defaults.verifyCommands).toBe("pnpm verify");
|
|
30
|
+
expect(defaults.hasDotnet).toBe(false);
|
|
31
|
+
expect(defaults.hasNodeOnly).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
it("returns both .NET and Web verify commands for a full-stack repo", () => {
|
|
34
|
+
const defaults = generateStackDefaults(makeInspection({
|
|
35
|
+
solutionFiles: ["app.slnx"],
|
|
36
|
+
pnpmLockfile: true,
|
|
37
|
+
}));
|
|
38
|
+
expect(defaults.verifyCommands).toContain(".NET");
|
|
39
|
+
expect(defaults.verifyCommands).toContain("dotnet restore");
|
|
40
|
+
expect(defaults.verifyCommands).toContain("dotnet build -c Release");
|
|
41
|
+
expect(defaults.verifyCommands).toContain("dotnet test");
|
|
42
|
+
expect(defaults.verifyCommands).toContain("Web");
|
|
43
|
+
expect(defaults.verifyCommands).toContain("pnpm lint");
|
|
44
|
+
expect(defaults.verifyCommands).toContain("pnpm test");
|
|
45
|
+
expect(defaults.verifyCommands).toContain("pnpm build");
|
|
46
|
+
expect(defaults.hasDotnet).toBe(true);
|
|
47
|
+
expect(defaults.hasNodeOnly).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it("returns pnpm verify when neither .slnx nor pnpm-lock.yaml is present", () => {
|
|
50
|
+
const defaults = generateStackDefaults(makeInspection());
|
|
51
|
+
expect(defaults.verifyCommands).toBe("pnpm verify");
|
|
52
|
+
expect(defaults.hasDotnet).toBe(false);
|
|
53
|
+
expect(defaults.hasNodeOnly).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
it("includes a repo rules base string with architecture context", () => {
|
|
56
|
+
const dotnetDefaults = generateStackDefaults(makeInspection({
|
|
57
|
+
solutionFiles: ["app.slnx"],
|
|
58
|
+
}));
|
|
59
|
+
expect(dotnetDefaults.repoRulesBase).toContain("Clean Architecture");
|
|
60
|
+
const nodeDefaults = generateStackDefaults(makeInspection({
|
|
61
|
+
pnpmLockfile: true,
|
|
62
|
+
}));
|
|
63
|
+
expect(nodeDefaults.repoRulesBase).toContain("Node.js");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe("injectStackEnv", () => {
|
|
67
|
+
it("injects VERIFY_COMMANDS into a worker that has an env block", () => {
|
|
68
|
+
const content = `---
|
|
69
|
+
env:
|
|
70
|
+
REPO_RULES: "some rules"
|
|
71
|
+
description: test
|
|
72
|
+
---`;
|
|
73
|
+
const defaults = generateStackDefaults(makeInspection({
|
|
74
|
+
solutionFiles: ["app.slnx"],
|
|
75
|
+
}));
|
|
76
|
+
const result = injectStackEnv(content, defaults);
|
|
77
|
+
expect(result).toContain('VERIFY_COMMANDS: "dotnet restore && dotnet build -c Release --no-restore && dotnet test"');
|
|
78
|
+
});
|
|
79
|
+
it("does not inject when verifyCommands is pnpm verify (the default)", () => {
|
|
80
|
+
const content = `---
|
|
81
|
+
env:
|
|
82
|
+
REPO_RULES: "some rules"
|
|
83
|
+
---`;
|
|
84
|
+
const defaults = generateStackDefaults(makeInspection({
|
|
85
|
+
pnpmLockfile: true,
|
|
86
|
+
}));
|
|
87
|
+
const result = injectStackEnv(content, defaults);
|
|
88
|
+
expect(result).not.toContain("VERIFY_COMMANDS");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
const OPENCODE_CI_MD = `---
|
|
92
|
+
env:
|
|
93
|
+
AGENTMEMORY_VERSION: "0.9.28"
|
|
94
|
+
CODEGRAPH_VERSION: "1.5.0"
|
|
95
|
+
description: Shared CI setup.
|
|
96
|
+
|
|
97
|
+
pre-agent-steps:
|
|
98
|
+
- name: Install RTK
|
|
99
|
+
run: |
|
|
100
|
+
rtk --version
|
|
101
|
+
rtk init -g --opencode --auto-patch
|
|
102
|
+
- name: Install opencode plugin dependencies
|
|
103
|
+
run: |
|
|
104
|
+
set -euo pipefail
|
|
105
|
+
if [ ! -f .opencode/package.json ]; then
|
|
106
|
+
echo "No .opencode/package.json, nothing to install"
|
|
107
|
+
exit 0
|
|
108
|
+
fi
|
|
109
|
+
- name: Install workspace dependencies
|
|
110
|
+
run: pnpm install --frozen-lockfile
|
|
111
|
+
- name: Merge the CI-only OpenCode provider into opencode.jsonc
|
|
112
|
+
run: |
|
|
113
|
+
jq -e . "$FRAGMENT" > /dev/null
|
|
114
|
+
---`;
|
|
115
|
+
describe("generateOpencodeCi", () => {
|
|
116
|
+
it("adds NuGet cache and dotnet restore steps when .slnx is found", () => {
|
|
117
|
+
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
118
|
+
solutionFiles: ["app.slnx"],
|
|
119
|
+
}));
|
|
120
|
+
expect(result).toContain("Cache NuGet packages");
|
|
121
|
+
expect(result).toContain("Restore .NET dependencies");
|
|
122
|
+
expect(result).toContain("dotnet restore");
|
|
123
|
+
});
|
|
124
|
+
it("adds OpenSpec CLI install step when openspec/ directory exists", () => {
|
|
125
|
+
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
126
|
+
openSpec: true,
|
|
127
|
+
}));
|
|
128
|
+
expect(result).toContain("Install OpenSpec CLI");
|
|
129
|
+
expect(result).toContain("@openspec/cli");
|
|
130
|
+
});
|
|
131
|
+
it("adds both NuGet and OpenSpec steps when both are detected", () => {
|
|
132
|
+
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
133
|
+
solutionFiles: ["app.slnx"],
|
|
134
|
+
openSpec: true,
|
|
135
|
+
}));
|
|
136
|
+
expect(result).toContain("Cache NuGet packages");
|
|
137
|
+
expect(result).toContain("Install OpenSpec CLI");
|
|
138
|
+
});
|
|
139
|
+
it("does not add NuGet steps when no .slnx is present", () => {
|
|
140
|
+
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
141
|
+
pnpmLockfile: true,
|
|
142
|
+
}));
|
|
143
|
+
expect(result).not.toContain("Cache NuGet packages");
|
|
144
|
+
expect(result).not.toContain("Restore .NET dependencies");
|
|
145
|
+
});
|
|
146
|
+
it("preserves the original merge step", () => {
|
|
147
|
+
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
148
|
+
solutionFiles: ["app.slnx"],
|
|
149
|
+
openSpec: true,
|
|
150
|
+
}));
|
|
151
|
+
expect(result).toContain("Merge the CI-only OpenCode provider");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
const OPENCODE_CI_JSON = `{
|
|
155
|
+
"$schema": "https://opencode.ai/config.json",
|
|
156
|
+
"model": "plainconcepts/glm-5-2",
|
|
157
|
+
"plugin": [],
|
|
158
|
+
"default_agent": "ci-workflow-agent",
|
|
159
|
+
"agent": {
|
|
160
|
+
"ci-workflow-agent": {
|
|
161
|
+
"description": "Executes GitHub Agentic Workflow tasks in CI.",
|
|
162
|
+
"mode": "primary",
|
|
163
|
+
"prompt": "You execute the GitHub Agentic Workflow task in the user prompt."
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"permission": {
|
|
167
|
+
"read": "allow"
|
|
168
|
+
},
|
|
169
|
+
"lsp": {
|
|
170
|
+
"csharp": {
|
|
171
|
+
"disabled": true
|
|
172
|
+
},
|
|
173
|
+
"fsharp": {
|
|
174
|
+
"disabled": true
|
|
175
|
+
},
|
|
176
|
+
"razor": {
|
|
177
|
+
"disabled": true
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
"provider": {
|
|
181
|
+
"plainconcepts": {
|
|
182
|
+
"api": "http://172.30.0.30:10000",
|
|
183
|
+
"options": {
|
|
184
|
+
"apiKey": "awf-openai-proxy"
|
|
185
|
+
},
|
|
186
|
+
"models": {
|
|
187
|
+
"glm-5-2": {
|
|
188
|
+
"name": "GLM 5.2"
|
|
189
|
+
},
|
|
190
|
+
"glm-5-1": {
|
|
191
|
+
"name": "GLM 5.1"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
`;
|
|
198
|
+
describe("generateOpencodeConfig", () => {
|
|
199
|
+
it("keeps LSP section when .slnx is present", () => {
|
|
200
|
+
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
201
|
+
solutionFiles: ["app.slnx"],
|
|
202
|
+
}));
|
|
203
|
+
const parsed = JSON.parse(result);
|
|
204
|
+
expect(parsed.lsp).toBeDefined();
|
|
205
|
+
expect(parsed.lsp.csharp.disabled).toBe(true);
|
|
206
|
+
expect(parsed.lsp.fsharp.disabled).toBe(true);
|
|
207
|
+
expect(parsed.lsp.razor.disabled).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
it("removes LSP section when no .slnx is present", () => {
|
|
210
|
+
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
211
|
+
pnpmLockfile: true,
|
|
212
|
+
}));
|
|
213
|
+
const parsed = JSON.parse(result);
|
|
214
|
+
expect(parsed.lsp).toBeUndefined();
|
|
215
|
+
});
|
|
216
|
+
it("adds .NET guardrails to agent prompt when .slnx is present and prompt lacks them", () => {
|
|
217
|
+
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
218
|
+
solutionFiles: ["app.slnx"],
|
|
219
|
+
}));
|
|
220
|
+
const parsed = JSON.parse(result);
|
|
221
|
+
expect(parsed.agent["ci-workflow-agent"].prompt).toContain(".NET guardrails");
|
|
222
|
+
expect(parsed.agent["ci-workflow-agent"].prompt).toContain("Clean Architecture");
|
|
223
|
+
});
|
|
224
|
+
it("adds Node/React rules to agent prompt when no .slnx", () => {
|
|
225
|
+
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
226
|
+
pnpmLockfile: true,
|
|
227
|
+
}));
|
|
228
|
+
const parsed = JSON.parse(result);
|
|
229
|
+
expect(parsed.agent["ci-workflow-agent"].prompt).toContain("Node/React rules");
|
|
230
|
+
expect(parsed.agent["ci-workflow-agent"].prompt).toContain("Feature-Sliced Design");
|
|
231
|
+
});
|
|
232
|
+
it("preserves both models in the provider", () => {
|
|
233
|
+
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
234
|
+
solutionFiles: ["app.slnx"],
|
|
235
|
+
}));
|
|
236
|
+
const parsed = JSON.parse(result);
|
|
237
|
+
expect(parsed.provider.plainconcepts.models["glm-5-2"]).toBeDefined();
|
|
238
|
+
expect(parsed.provider.plainconcepts.models["glm-5-1"]).toBeDefined();
|
|
239
|
+
});
|
|
240
|
+
it("preserves the plainconcepts provider and its API URL", () => {
|
|
241
|
+
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
242
|
+
solutionFiles: ["app.slnx"],
|
|
243
|
+
}));
|
|
244
|
+
const parsed = JSON.parse(result);
|
|
245
|
+
expect(parsed.provider.plainconcepts.api).toBe("http://172.30.0.30:10000");
|
|
246
|
+
expect(parsed.provider.plainconcepts.options.apiKey).toBe("awf-openai-proxy");
|
|
247
|
+
});
|
|
248
|
+
it("does not add .NET guardrails twice when already present", () => {
|
|
249
|
+
const withGuardrails = OPENCODE_CI_JSON.replace('"You execute the GitHub Agentic Workflow task in the user prompt."', '"You execute the GitHub Agentic Workflow task in the user prompt.\\n\\n# .NET guardrails\\nFollow Clean Architecture."');
|
|
250
|
+
const result = generateOpencodeConfig(withGuardrails, makeInspection({
|
|
251
|
+
solutionFiles: ["app.slnx"],
|
|
252
|
+
}));
|
|
253
|
+
const parsed = JSON.parse(result);
|
|
254
|
+
const guardrailsCount = (parsed.agent["ci-workflow-agent"].prompt.match(/\.NET guardrails/g) ?? []).length;
|
|
255
|
+
expect(guardrailsCount).toBe(1);
|
|
256
|
+
});
|
|
257
|
+
});
|
package/dist/tui.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as readline from "node:readline";
|
|
2
2
|
import { formatCatalog, listCatalog } from "./catalog-listing.js";
|
|
3
3
|
import { installCatalog, installMandatoryFiles, installTemplate, isTemplateName } from "./catalog-installation.js";
|
|
4
|
+
import { inspectRepository } from "./repository-inspection.js";
|
|
5
|
+
import { routeNames } from "./workflow-catalog.js";
|
|
4
6
|
const ANSI = {
|
|
5
7
|
clear: "\x1b[2J",
|
|
6
8
|
home: "\x1b[H",
|
|
@@ -220,8 +222,16 @@ async function installSelected(state, repositoryPath, force) {
|
|
|
220
222
|
const templates = items.filter((entry) => entry.kind === "template");
|
|
221
223
|
const allConflicts = [];
|
|
222
224
|
const allInstalled = [];
|
|
225
|
+
const selectedRouteNames = routes.length > 0
|
|
226
|
+
? routes.map((entry) => entry.name).filter((name) => routeNames.includes(name))
|
|
227
|
+
: [...routeNames];
|
|
228
|
+
const inspection = await inspectRepository(repositoryPath);
|
|
223
229
|
if (routes.length > 0) {
|
|
224
|
-
const result = await installCatalog(repositoryPath, {
|
|
230
|
+
const result = await installCatalog(repositoryPath, {
|
|
231
|
+
force,
|
|
232
|
+
selectedRoutes: selectedRouteNames,
|
|
233
|
+
inspection,
|
|
234
|
+
});
|
|
225
235
|
allConflicts.push(...result.conflicts);
|
|
226
236
|
allInstalled.push(...result.installed);
|
|
227
237
|
}
|
|
@@ -233,7 +243,7 @@ async function installSelected(state, repositoryPath, force) {
|
|
|
233
243
|
for (const template of templates) {
|
|
234
244
|
if (!isTemplateName(template.name))
|
|
235
245
|
continue;
|
|
236
|
-
const result = await installTemplate(repositoryPath, template.name, { force });
|
|
246
|
+
const result = await installTemplate(repositoryPath, template.name, { force, inspection });
|
|
237
247
|
allConflicts.push(...result.conflicts);
|
|
238
248
|
allInstalled.push(...result.installed);
|
|
239
249
|
}
|
|
@@ -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"
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
---
|
|
2
2
|
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/shared/platform-defaults.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
|
-
env:
|
|
4
|
-
VERIFY_COMMANDS: "pnpm verify"
|
|
5
3
|
description: Shared network and safe-output defaults for catalog agent workflows.
|
|
6
4
|
|
|
7
5
|
network:
|
|
@@ -312,6 +312,39 @@ jobs:
|
|
|
312
312
|
BOT_APP_ID: ${{ secrets.BOT_APP_ID }}
|
|
313
313
|
BOT_PRIVATE_KEY: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
314
314
|
|
|
315
|
+
bot-approve:
|
|
316
|
+
needs: classify
|
|
317
|
+
if: >
|
|
318
|
+
needs.classify.outputs.route == 'bot-approve' &&
|
|
319
|
+
(github.actor == 'app/github-actions' || github.actor == 'platform-devbox[bot]')
|
|
320
|
+
runs-on: ubuntu-latest
|
|
321
|
+
timeout-minutes: 5
|
|
322
|
+
permissions:
|
|
323
|
+
actions: write
|
|
324
|
+
steps:
|
|
325
|
+
- name: Approve pending workflow runs
|
|
326
|
+
env:
|
|
327
|
+
GH_TOKEN: ${{ github.token }}
|
|
328
|
+
REPO: ${{ github.repository }}
|
|
329
|
+
BRANCH: ${{ github.head_ref }}
|
|
330
|
+
run: |
|
|
331
|
+
set -euo pipefail
|
|
332
|
+
|
|
333
|
+
mapfile -t run_ids < <(
|
|
334
|
+
gh api "repos/$REPO/actions/runs?status=action_required&branch=$BRANCH" \
|
|
335
|
+
--jq '.workflow_runs[].id'
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if [ "${#run_ids[@]}" -eq 0 ]; then
|
|
339
|
+
echo "No runs awaiting approval on branch $BRANCH"
|
|
340
|
+
exit 0
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
for run_id in "${run_ids[@]}"; do
|
|
344
|
+
echo "Approving run $run_id"
|
|
345
|
+
gh api "repos/$REPO/actions/runs/$run_id/approve" --method POST
|
|
346
|
+
done
|
|
347
|
+
|
|
315
348
|
audit-close:
|
|
316
349
|
needs: classify
|
|
317
350
|
if: needs.classify.outputs.route == 'audit-close'
|