@guilz-dev/sdlc-gh 0.1.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/.github/CODEOWNERS +5 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +68 -0
- package/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +39 -0
- package/.github/ISSUE_TEMPLATE/support.yml +56 -0
- package/.github/ISSUE_TEMPLATE/task.yml +89 -0
- package/.github/agents/implementer.agent.md +17 -0
- package/.github/agents/reviewer.agent.md +18 -0
- package/.github/agents/triager.agent.md +13 -0
- package/.github/aw/actions-lock.json +9 -0
- package/.github/copilot-instructions.md +35 -0
- package/.github/hooks/hooks.json +12 -0
- package/.github/instructions/core.instructions.md +11 -0
- package/.github/instructions/profiles/go.instructions.md +10 -0
- package/.github/instructions/profiles/php.instructions.md +11 -0
- package/.github/instructions/profiles/python.instructions.md +11 -0
- package/.github/instructions/profiles/ruby.instructions.md +11 -0
- package/.github/instructions/profiles/typescript.instructions.md +11 -0
- package/.github/labels.yml +55 -0
- package/.github/pull_request_template.md +33 -0
- package/.github/ruleset.example.json +33 -0
- package/.github/ruleset.harness-eval.example.json +29 -0
- package/.github/skills/quality-loop/SKILL.md +23 -0
- package/.github/workflows/agent-retry-orchestrator.yml +161 -0
- package/.github/workflows/copilot-setup-steps.yml +64 -0
- package/.github/workflows/eval-ci.yml +169 -0
- package/.github/workflows/eval-drift.yml +75 -0
- package/.github/workflows/gh-aw-dogfood-ci.yml +73 -0
- package/.github/workflows/harness-ci.yml +244 -0
- package/.github/workflows/harness-sync.yml +28 -0
- package/.github/workflows/l1-readiness-check.yml +45 -0
- package/.github/workflows/labels-sync.yml +24 -0
- package/.github/workflows/nightly-harness-review.lock.yml +1643 -0
- package/.github/workflows/nightly-harness-review.md +87 -0
- package/.github/workflows/nightly-harness-review.yml +63 -0
- package/.github/workflows/npm-publish.yml +49 -0
- package/.github/workflows/pr-context-comment.yml +138 -0
- package/.github/workflows/product-ci-go.yml +33 -0
- package/.github/workflows/product-ci-php.yml +39 -0
- package/.github/workflows/product-ci-python.yml +34 -0
- package/.github/workflows/product-ci-ruby.yml +35 -0
- package/.github/workflows/product-ci-ts.yml +37 -0
- package/.github/workflows/task-issue-label-sync.yml +50 -0
- package/.github/workflows/weekly-redteam.lock.yml +1571 -0
- package/.github/workflows/weekly-redteam.md +76 -0
- package/.github/zizmor.yml +11 -0
- package/AGENTS.md +54 -0
- package/LICENSE +21 -0
- package/README.md +366 -0
- package/config/stacks.json +55 -0
- package/docs/adoption.md +126 -0
- package/docs/arch.md +535 -0
- package/docs/auth-boundaries.md +16 -0
- package/docs/coding-agent-l1.md +152 -0
- package/docs/exceptions/README.md +25 -0
- package/docs/exceptions/TEMPLATE.md +8 -0
- package/docs/failure-taxonomy.md +23 -0
- package/docs/gh-aw-dogfood.md +109 -0
- package/docs/kpi-baseline.md +9 -0
- package/docs/nightly-harness-review.md +94 -0
- package/docs/operations.md +108 -0
- package/docs/publishing.md +79 -0
- package/docs/revert-playbook.md +44 -0
- package/docs/shared-config.md +30 -0
- package/docs/telemetry-artifacts.md +78 -0
- package/docs/telemetry-schema.md +60 -0
- package/evals/.score-baseline.json +6 -0
- package/evals/e2e-bench/README.md +28 -0
- package/evals/e2e-bench/manifest.json +16 -0
- package/evals/e2e-bench/tasks/e2e-001.yml +10 -0
- package/evals/e2e-bench/tasks/e2e-002.yml +11 -0
- package/evals/e2e-bench/tasks/e2e-003.yml +10 -0
- package/evals/e2e-bench/tasks/e2e-004.yml +14 -0
- package/evals/e2e-bench/tasks/e2e-005.yml +11 -0
- package/evals/e2e-bench/tasks/e2e-006.yml +10 -0
- package/evals/e2e-bench/tasks/e2e-007.yml +10 -0
- package/evals/e2e-bench/tasks/e2e-008.yml +10 -0
- package/evals/e2e-bench/tasks/e2e-009.yml +10 -0
- package/evals/trajectories/rubric.md +12 -0
- package/evals/trajectories/test_harness_conventions.py +271 -0
- package/infra/README.md +49 -0
- package/infra/langfuse/docker-compose.yml +25 -0
- package/infra/otel/collector-config.yml +24 -0
- package/infra/samples/gh-aw-dogfood-report.json +44 -0
- package/infra/samples/harness-review-routing-plan.json +19 -0
- package/infra/samples/harness-review-summary.json +61 -0
- package/infra/samples/telemetry-artifact.json +29 -0
- package/infra/samples/telemetry-payload.json +19 -0
- package/package.json +85 -0
- package/prompts/triager-classify.prompt.yml +10 -0
- package/sample/go/add.go +5 -0
- package/sample/go/add_test.go +9 -0
- package/sample/go/go.mod +3 -0
- package/sample/php/composer.json +26 -0
- package/sample/php/composer.lock +1881 -0
- package/sample/php/phpunit.xml +8 -0
- package/sample/php/src/Add.php +13 -0
- package/sample/php/tests/AddTest.php +16 -0
- package/sample/python/requirements-dev.txt +2 -0
- package/sample/python/src/__init__.py +0 -0
- package/sample/python/src/greet.py +3 -0
- package/sample/python/tests/conftest.py +4 -0
- package/sample/python/tests/test_greet.py +5 -0
- package/sample/ruby/.rubocop.yml +10 -0
- package/sample/ruby/Gemfile +6 -0
- package/sample/ruby/Gemfile.lock +58 -0
- package/sample/ruby/lib/add.rb +9 -0
- package/sample/ruby/spec/add_spec.rb +11 -0
- package/sample/ts/biome.json +6 -0
- package/sample/ts/package-lock.json +1763 -0
- package/sample/ts/package.json +15 -0
- package/sample/ts/src/add.ts +3 -0
- package/sample/ts/tests/add.test.ts +8 -0
- package/sample/ts/tsconfig.json +12 -0
- package/scripts/aggregate-harness-review.mjs +48 -0
- package/scripts/bootstrap-harness.sh +411 -0
- package/scripts/check-diff-size.mjs +46 -0
- package/scripts/check-e2e-manifest.mjs +35 -0
- package/scripts/check-eval-score-drift.mjs +31 -0
- package/scripts/check-gh-aw-dogfood-scope.mjs +51 -0
- package/scripts/check-issue-spec.mjs +215 -0
- package/scripts/check-l1-readiness.mjs +82 -0
- package/scripts/check-open-pr-limit.mjs +34 -0
- package/scripts/doctor.mjs +177 -0
- package/scripts/emit-gh-aw-dogfood-report.mjs +112 -0
- package/scripts/emit-telemetry-artifact.mjs +99 -0
- package/scripts/fetch-telemetry-artifacts.mjs +176 -0
- package/scripts/harness-drift-report.mjs +99 -0
- package/scripts/lib/bootstrap-copy.mjs +123 -0
- package/scripts/lib/ccsd-contract.mjs +212 -0
- package/scripts/lib/diff-size.mjs +103 -0
- package/scripts/lib/doctor-local.mjs +179 -0
- package/scripts/lib/e2e-manifest.mjs +76 -0
- package/scripts/lib/gh-aw-dogfood.mjs +293 -0
- package/scripts/lib/github-config.mjs +94 -0
- package/scripts/lib/harness-ci-fragments.mjs +98 -0
- package/scripts/lib/harness-review-routing.mjs +244 -0
- package/scripts/lib/harness-review.mjs +388 -0
- package/scripts/lib/issue-form-label-sync.mjs +56 -0
- package/scripts/lib/l1-readiness.mjs +258 -0
- package/scripts/lib/merge-harness-package.mjs +36 -0
- package/scripts/lib/npm-package.mjs +129 -0
- package/scripts/lib/setup-wizard.mjs +224 -0
- package/scripts/lib/stacks.mjs +138 -0
- package/scripts/lib/telemetry-artifact.mjs +253 -0
- package/scripts/lib/template-root.mjs +39 -0
- package/scripts/merge-harness-package.mjs +14 -0
- package/scripts/route-harness-review.mjs +168 -0
- package/scripts/run-e2e-bench.mjs +216 -0
- package/scripts/sdlc-gh-cli.mjs +91 -0
- package/scripts/select-eval-jobs.mjs +41 -0
- package/scripts/setup-github.mjs +242 -0
- package/scripts/setup-github.sh +4 -0
- package/scripts/setup-wizard.mjs +426 -0
- package/scripts/test-bootstrap-guidance-scenarios.mjs +94 -0
- package/scripts/test-diff-size-scenarios.mjs +88 -0
- package/scripts/test-doctor-scenarios.mjs +70 -0
- package/scripts/test-e2e-manifest-scenarios.mjs +65 -0
- package/scripts/test-gh-aw-dogfood-scenarios.mjs +74 -0
- package/scripts/test-harness-review-routing-scenarios.mjs +130 -0
- package/scripts/test-harness-review-scenarios.mjs +92 -0
- package/scripts/test-hooks-scenarios.mjs +44 -0
- package/scripts/test-issue-form-label-sync-scenarios.mjs +48 -0
- package/scripts/test-issue-spec-scenarios.mjs +258 -0
- package/scripts/test-l1-readiness-scenarios.mjs +204 -0
- package/scripts/test-merge-harness-package-scenarios.mjs +53 -0
- package/scripts/test-npm-package-scenarios.mjs +31 -0
- package/scripts/test-sdlc-gh-cli-scenarios.mjs +54 -0
- package/scripts/test-setup-github-scenarios.mjs +103 -0
- package/scripts/test-setup-wizard-scenarios.mjs +114 -0
- package/scripts/test-telemetry-artifact-scenarios.mjs +69 -0
- package/scripts/trim-harness-ci.mjs +18 -0
- package/scripts/validate-gh-aw-compile.mjs +64 -0
- package/scripts/validate-harness.mjs +199 -0
- package/scripts/validate-telemetry.mjs +21 -0
- package/scripts/verify-bootstrap-stacks.sh +192 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import readline from "node:readline/promises";
|
|
3
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import {
|
|
6
|
+
applyCodeownersOwner,
|
|
7
|
+
buildWizardPlan,
|
|
8
|
+
codeownersHasPlaceholder,
|
|
9
|
+
detectRepoProfile,
|
|
10
|
+
detectStackCandidates,
|
|
11
|
+
getStack,
|
|
12
|
+
ghReady,
|
|
13
|
+
isValidCodeownersOwner,
|
|
14
|
+
readHarnessStack,
|
|
15
|
+
resolveGithubRepo,
|
|
16
|
+
runBootstrap,
|
|
17
|
+
runDoctor,
|
|
18
|
+
runSetupGithub,
|
|
19
|
+
stackIds,
|
|
20
|
+
suggestStack,
|
|
21
|
+
writeHarnessStack,
|
|
22
|
+
} from "./lib/setup-wizard.mjs";
|
|
23
|
+
import { resolveTemplateRoot } from "./lib/template-root.mjs";
|
|
24
|
+
import { postInstallHint } from "./lib/npm-package.mjs";
|
|
25
|
+
|
|
26
|
+
function fail(message) {
|
|
27
|
+
console.error(message);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveRepoRoot() {
|
|
32
|
+
try {
|
|
33
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
36
|
+
}).trim();
|
|
37
|
+
} catch {
|
|
38
|
+
return process.cwd();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseArgs(argv) {
|
|
43
|
+
const args = {
|
|
44
|
+
repoRoot: "",
|
|
45
|
+
stack: "",
|
|
46
|
+
codeowners: "",
|
|
47
|
+
githubRepo: "",
|
|
48
|
+
mode: "",
|
|
49
|
+
yes: false,
|
|
50
|
+
skipGithub: false,
|
|
51
|
+
withEvalRuleset: false,
|
|
52
|
+
template: false,
|
|
53
|
+
dryRun: false,
|
|
54
|
+
bootstrap: false,
|
|
55
|
+
forceBootstrap: false,
|
|
56
|
+
patchCodeowners: false,
|
|
57
|
+
templateRoot: "",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
61
|
+
const value = argv[i];
|
|
62
|
+
if (value === "--repo") {
|
|
63
|
+
args.repoRoot = argv[++i] ?? "";
|
|
64
|
+
} else if (value === "--stack") {
|
|
65
|
+
args.stack = argv[++i] ?? "";
|
|
66
|
+
} else if (value === "--codeowners" || value === "--codeowners-team") {
|
|
67
|
+
args.codeowners = argv[++i] ?? "";
|
|
68
|
+
} else if (value === "--github-repo") {
|
|
69
|
+
args.githubRepo = argv[++i] ?? "";
|
|
70
|
+
} else if (value === "--mode") {
|
|
71
|
+
args.mode = argv[++i] ?? "";
|
|
72
|
+
} else if (value === "--yes") {
|
|
73
|
+
args.yes = true;
|
|
74
|
+
} else if (value === "--skip-github") {
|
|
75
|
+
args.skipGithub = true;
|
|
76
|
+
} else if (value === "--with-eval-ruleset") {
|
|
77
|
+
args.withEvalRuleset = true;
|
|
78
|
+
} else if (value === "--template") {
|
|
79
|
+
args.template = true;
|
|
80
|
+
} else if (value === "--dry-run") {
|
|
81
|
+
args.dryRun = true;
|
|
82
|
+
} else if (value === "--bootstrap") {
|
|
83
|
+
args.bootstrap = true;
|
|
84
|
+
} else if (value === "--force-bootstrap") {
|
|
85
|
+
args.forceBootstrap = true;
|
|
86
|
+
args.bootstrap = true;
|
|
87
|
+
} else if (value === "--patch-codeowners") {
|
|
88
|
+
args.patchCodeowners = true;
|
|
89
|
+
} else if (value === "--template-root") {
|
|
90
|
+
args.templateRoot = argv[++i] ?? "";
|
|
91
|
+
} else if (value === "--help" || value === "-h") {
|
|
92
|
+
printUsage();
|
|
93
|
+
process.exit(0);
|
|
94
|
+
} else {
|
|
95
|
+
fail(`Unknown argument: ${value}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return args;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function printUsage() {
|
|
102
|
+
console.log(`Usage: setup-wizard.mjs [options]
|
|
103
|
+
|
|
104
|
+
Interactive setup for Phase 0–1: .harness-stack, CODEOWNERS, GitHub labels/rulesets, doctor.
|
|
105
|
+
|
|
106
|
+
Options:
|
|
107
|
+
--repo <path> Repository root (default: git root)
|
|
108
|
+
--stack <id> Primary stack (${stackIds().join("|")})
|
|
109
|
+
--codeowners @org/team CODEOWNERS owner (team or @username)
|
|
110
|
+
--github-repo owner/name GitHub repository for setup-github
|
|
111
|
+
--mode new|existing Bootstrap mode when harness is missing
|
|
112
|
+
--template Template repo mode (multiple product-ci workflows)
|
|
113
|
+
--patch-codeowners Replace CODEOWNERS placeholder (default off in --template mode)
|
|
114
|
+
--with-eval-ruleset Also apply harness-pr-eval-required ruleset
|
|
115
|
+
--skip-github Local files only; skip setup-github
|
|
116
|
+
--bootstrap Force bootstrap when harness is missing
|
|
117
|
+
--force-bootstrap Re-run bootstrap on an existing harness (overwrites assets)
|
|
118
|
+
--dry-run Print plan; skip mutating GitHub and doctor
|
|
119
|
+
--yes Non-interactive (requires stack + codeowners when configuring)
|
|
120
|
+
--template-root <path> Harness template source (auto-detected when omitted)
|
|
121
|
+
|
|
122
|
+
Examples:
|
|
123
|
+
npx @guilz-dev/sdlc-gh
|
|
124
|
+
./scripts/setup-wizard.mjs
|
|
125
|
+
./scripts/setup-wizard.mjs --template --yes --stack ts
|
|
126
|
+
./scripts/setup-wizard.mjs --yes --stack ts --codeowners @acme/platform
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function ask(rl, question, defaultValue = "") {
|
|
131
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
132
|
+
const answer = (await rl.question(`${question}${suffix}: `)).trim();
|
|
133
|
+
return answer || defaultValue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function confirm(rl, summary, yes) {
|
|
137
|
+
if (yes) return true;
|
|
138
|
+
if (summary) console.log(summary);
|
|
139
|
+
const answer = (await rl.question("Proceed? [y/N]: ")).trim();
|
|
140
|
+
return /^(y|yes)$/i.test(answer);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function resolveStack(rl, repoRoot, stackArg, yes) {
|
|
144
|
+
if (stackArg) {
|
|
145
|
+
getStack(stackArg);
|
|
146
|
+
return stackArg;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const existing = readHarnessStack(repoRoot);
|
|
150
|
+
if (existing) {
|
|
151
|
+
getStack(existing);
|
|
152
|
+
console.log(`Existing .harness-stack: ${existing}`);
|
|
153
|
+
if (yes) return existing;
|
|
154
|
+
const keep = await ask(rl, "Keep this stack? (y/n)", "y");
|
|
155
|
+
if (/^(y|yes)$/i.test(keep)) return existing;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const suggested = suggestStack(repoRoot);
|
|
159
|
+
if (suggested && !detectStackCandidates(repoRoot).ambiguous) {
|
|
160
|
+
console.log(`Detected stack: ${suggested}`);
|
|
161
|
+
if (yes) return suggested;
|
|
162
|
+
const answer = await ask(rl, "Use this stack? (y/n)", "y");
|
|
163
|
+
if (/^(y|yes)$/i.test(answer)) return suggested;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (yes) fail("--stack is required with --yes when stack cannot be inferred.");
|
|
167
|
+
|
|
168
|
+
const detected = detectStackCandidates(repoRoot);
|
|
169
|
+
if (detected.rootMatches.length || detected.nestedMatches.length) {
|
|
170
|
+
for (const match of detected.rootMatches) {
|
|
171
|
+
console.log(` root: ${match.stackId} (${match.path})`);
|
|
172
|
+
}
|
|
173
|
+
for (const match of detected.nestedMatches) {
|
|
174
|
+
console.log(` nested: ${match.stackId} (${match.path})`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const stack = await ask(rl, `Choose stack (${stackIds().join(", ")})`, suggested || "ts");
|
|
179
|
+
getStack(stack);
|
|
180
|
+
return stack;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function resolveCodeowners(rl, codeownersArg, yes, required) {
|
|
184
|
+
if (codeownersArg) {
|
|
185
|
+
if (!isValidCodeownersOwner(codeownersArg)) fail(`Invalid CODEOWNERS owner: ${codeownersArg}`);
|
|
186
|
+
return codeownersArg.trim();
|
|
187
|
+
}
|
|
188
|
+
if (!required) return "";
|
|
189
|
+
if (yes) fail("--codeowners is required with --yes.");
|
|
190
|
+
const owner = await ask(rl, "CODEOWNERS owner (@org/team or @username)");
|
|
191
|
+
if (!isValidCodeownersOwner(owner)) fail(`Invalid CODEOWNERS owner: ${owner}`);
|
|
192
|
+
return owner.trim();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function resolveBootstrapMode(rl, repoRoot, modeArg, yes) {
|
|
196
|
+
if (modeArg === "new" || modeArg === "existing") return modeArg;
|
|
197
|
+
if (yes) fail("--mode is required with --yes when bootstrapping.");
|
|
198
|
+
|
|
199
|
+
const answer = await ask(rl, "Bootstrap mode (new=copy sample to root, existing=keep product code)", "existing");
|
|
200
|
+
if (answer !== "new" && answer !== "existing") fail(`Unknown mode: ${answer}`);
|
|
201
|
+
return answer;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function printPlanHeader(plan) {
|
|
205
|
+
console.log("\nSetup wizard plan");
|
|
206
|
+
console.log(` repo: ${plan.repoRoot}`);
|
|
207
|
+
console.log(` profile: ${plan.template ? "template" : "product"}`);
|
|
208
|
+
console.log(` stack: ${plan.stackId}`);
|
|
209
|
+
if (plan.owner) console.log(` CODEOWNERS: ${plan.owner}`);
|
|
210
|
+
if (!plan.skipGithub) console.log(` github: ${plan.githubRepo || "(auto-detect)"}`);
|
|
211
|
+
if (plan.withEvalRuleset) console.log(" eval ruleset: yes");
|
|
212
|
+
console.log(" steps:");
|
|
213
|
+
for (const step of plan.steps) {
|
|
214
|
+
console.log(` - ${step.id}: ${step.detail}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function main() {
|
|
219
|
+
const args = parseArgs(process.argv.slice(2));
|
|
220
|
+
const repoRoot = args.repoRoot || resolveRepoRoot();
|
|
221
|
+
const profile = detectRepoProfile(repoRoot, { template: args.template });
|
|
222
|
+
const template = profile.template || args.template;
|
|
223
|
+
const wantsBootstrap = args.bootstrap || args.forceBootstrap;
|
|
224
|
+
const needsBootstrap = !profile.harnessPresent || wantsBootstrap;
|
|
225
|
+
|
|
226
|
+
if (args.templateRoot.trim()) {
|
|
227
|
+
process.env.SDLCGH_TEMPLATE_ROOT = args.templateRoot.trim();
|
|
228
|
+
}
|
|
229
|
+
let templateRoot = args.templateRoot.trim();
|
|
230
|
+
try {
|
|
231
|
+
templateRoot = resolveTemplateRoot({ fromModule: import.meta.url });
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (needsBootstrap) {
|
|
234
|
+
fail(error.message);
|
|
235
|
+
}
|
|
236
|
+
templateRoot = repoRoot;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (profile.harnessPresent && wantsBootstrap && !args.forceBootstrap) {
|
|
240
|
+
fail(
|
|
241
|
+
"Harness assets already present. Omit --bootstrap or pass --force-bootstrap to overwrite (destructive).",
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (profile.harnessPresent && args.forceBootstrap && args.yes) {
|
|
246
|
+
fail("--force-bootstrap requires interactive confirmation; omit --yes.");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10);
|
|
250
|
+
if (nodeMajor < 22) {
|
|
251
|
+
fail(`Node.js 22+ required (current: ${process.versions.node}).`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const rl = readline.createInterface({ input, output });
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
console.log("SDLC-GH setup wizard (Phase 0–1)\n");
|
|
258
|
+
console.log(`Repository: ${repoRoot}`);
|
|
259
|
+
console.log(`Profile: ${profile.kind}${template ? " (template mode)" : ""}`);
|
|
260
|
+
|
|
261
|
+
let stackId = args.stack;
|
|
262
|
+
let owner = args.codeowners;
|
|
263
|
+
let ranBootstrap = false;
|
|
264
|
+
let bootstrapMode = "";
|
|
265
|
+
|
|
266
|
+
if (needsBootstrap) {
|
|
267
|
+
if (args.forceBootstrap) {
|
|
268
|
+
console.log("\nWARNING: --force-bootstrap will overwrite harness assets in this repository.");
|
|
269
|
+
} else {
|
|
270
|
+
console.log("\nHarness assets not detected — bootstrap required.");
|
|
271
|
+
}
|
|
272
|
+
stackId = await resolveStack(rl, repoRoot, stackId, args.yes);
|
|
273
|
+
bootstrapMode = await resolveBootstrapMode(rl, repoRoot, args.mode, args.yes);
|
|
274
|
+
owner = await resolveCodeowners(rl, owner, args.yes, true);
|
|
275
|
+
|
|
276
|
+
const bootstrapSummary = [
|
|
277
|
+
"Bootstrap summary",
|
|
278
|
+
` repo: ${repoRoot}`,
|
|
279
|
+
` stack: ${stackId}`,
|
|
280
|
+
` mode: ${bootstrapMode}`,
|
|
281
|
+
` CODEOWNERS: ${owner}`,
|
|
282
|
+
].join("\n");
|
|
283
|
+
|
|
284
|
+
if (!(await confirm(rl, bootstrapSummary, args.yes))) {
|
|
285
|
+
console.log("Cancelled.");
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (args.dryRun) {
|
|
290
|
+
console.log("[dry-run] bootstrap-harness.sh would run");
|
|
291
|
+
} else {
|
|
292
|
+
const bootstrapResult = runBootstrap({
|
|
293
|
+
repoRoot,
|
|
294
|
+
stackId,
|
|
295
|
+
mode: bootstrapMode,
|
|
296
|
+
owner,
|
|
297
|
+
yes: true,
|
|
298
|
+
templateRoot,
|
|
299
|
+
});
|
|
300
|
+
if (bootstrapResult.status !== 0) {
|
|
301
|
+
console.error(bootstrapResult.stderr || bootstrapResult.stdout);
|
|
302
|
+
fail("Bootstrap failed.");
|
|
303
|
+
}
|
|
304
|
+
if (bootstrapResult.stdout) console.log(bootstrapResult.stdout);
|
|
305
|
+
ranBootstrap = true;
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
stackId = await resolveStack(rl, repoRoot, stackId, args.yes);
|
|
309
|
+
if (template && !args.patchCodeowners) {
|
|
310
|
+
console.log("Template repo: keeping CODEOWNERS placeholder (pass --patch-codeowners to replace).");
|
|
311
|
+
if (args.codeowners) {
|
|
312
|
+
console.log("::notice::--codeowners ignored in template mode without --patch-codeowners.");
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
const needsOwner = codeownersHasPlaceholder(repoRoot);
|
|
316
|
+
if (needsOwner) {
|
|
317
|
+
owner = await resolveCodeowners(rl, owner, args.yes, true);
|
|
318
|
+
} else if (owner) {
|
|
319
|
+
if (!isValidCodeownersOwner(owner)) fail(`Invalid CODEOWNERS owner: ${owner}`);
|
|
320
|
+
owner = owner.trim();
|
|
321
|
+
} else {
|
|
322
|
+
console.log("CODEOWNERS: placeholder already replaced.");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const githubRepo = args.githubRepo || resolveGithubRepo(repoRoot);
|
|
328
|
+
if (!args.skipGithub && !args.dryRun) {
|
|
329
|
+
const gh = ghReady(repoRoot);
|
|
330
|
+
if (!gh.ok) fail(`${gh.reason}. Use --skip-github to configure local files only.`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const allowCodeownersPatch = !template || args.patchCodeowners;
|
|
334
|
+
const willPatchCodeowners =
|
|
335
|
+
allowCodeownersPatch && Boolean(owner) && codeownersHasPlaceholder(repoRoot);
|
|
336
|
+
const ownerLabel = owner || "(unchanged)";
|
|
337
|
+
|
|
338
|
+
const plan = buildWizardPlan({
|
|
339
|
+
repoRoot,
|
|
340
|
+
stackId,
|
|
341
|
+
owner: ownerLabel,
|
|
342
|
+
githubRepo,
|
|
343
|
+
template,
|
|
344
|
+
withEvalRuleset: args.withEvalRuleset,
|
|
345
|
+
yes: args.yes,
|
|
346
|
+
skipGithub: args.skipGithub,
|
|
347
|
+
dryRun: args.dryRun,
|
|
348
|
+
writeHarnessStack: !ranBootstrap,
|
|
349
|
+
patchCodeowners: !ranBootstrap && willPatchCodeowners,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
if (owner && !willPatchCodeowners && !ranBootstrap) {
|
|
353
|
+
console.log(
|
|
354
|
+
"::notice::CODEOWNERS placeholder already replaced; --codeowners ignored (edit .github/CODEOWNERS manually).",
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (ranBootstrap) {
|
|
359
|
+
plan.steps = plan.steps.filter((step) => step.id !== "harness-stack" && step.id !== "codeowners");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
printPlanHeader({ ...plan, repoRoot, stackId, owner });
|
|
363
|
+
|
|
364
|
+
if (!(await confirm(rl, "", args.yes))) {
|
|
365
|
+
console.log("Cancelled.");
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (args.dryRun) {
|
|
370
|
+
console.log("\nDry run complete.");
|
|
371
|
+
process.exit(0);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (!ranBootstrap) {
|
|
375
|
+
writeHarnessStack(repoRoot, stackId);
|
|
376
|
+
if (willPatchCodeowners) {
|
|
377
|
+
applyCodeownersOwner(repoRoot, owner);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!args.skipGithub) {
|
|
382
|
+
const setupResult = runSetupGithub({
|
|
383
|
+
repoRoot,
|
|
384
|
+
githubRepo,
|
|
385
|
+
withEvalRuleset: args.withEvalRuleset,
|
|
386
|
+
yes: true,
|
|
387
|
+
dryRun: false,
|
|
388
|
+
});
|
|
389
|
+
if (setupResult.status !== 0) {
|
|
390
|
+
console.error(setupResult.stderr || setupResult.stdout);
|
|
391
|
+
fail("setup-github failed.");
|
|
392
|
+
}
|
|
393
|
+
console.log(setupResult.stdout || "GitHub setup complete.");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const doctorResult = runDoctor({ repoRoot, template, strict: true });
|
|
397
|
+
if (doctorResult.stdout) console.log(doctorResult.stdout);
|
|
398
|
+
if (doctorResult.stderr) console.error(doctorResult.stderr);
|
|
399
|
+
|
|
400
|
+
if (doctorResult.status !== 0) {
|
|
401
|
+
fail("doctor --strict reported failures. Fix the items above and re-run.");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
console.log("\nSetup wizard complete.");
|
|
405
|
+
if (template) {
|
|
406
|
+
console.log("Template repo: CODEOWNERS remains placeholder-only; commit .harness-stack is gitignored.");
|
|
407
|
+
} else {
|
|
408
|
+
console.log("Next: commit CODEOWNERS and open a test PR to verify required checks.");
|
|
409
|
+
}
|
|
410
|
+
if (ranBootstrap && bootstrapMode === "new") {
|
|
411
|
+
console.log(postInstallHint(stackId));
|
|
412
|
+
}
|
|
413
|
+
if (profile.harnessPresent && !ranBootstrap && templateRoot !== repoRoot) {
|
|
414
|
+
console.log(
|
|
415
|
+
"Harness already present — bootstrap was skipped. To refresh harness assets from a newer package, re-run with `--force-bootstrap` (destructive).",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
console.log("Then: run `npm run check-l1-readiness` before starting spec-driven L1 delegation.");
|
|
419
|
+
} finally {
|
|
420
|
+
rl.close();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
main().catch((error) => {
|
|
425
|
+
fail(error.message);
|
|
426
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const ROOT = resolve(process.cwd());
|
|
9
|
+
const scriptPath = join(ROOT, "scripts/bootstrap-harness.sh");
|
|
10
|
+
|
|
11
|
+
function runBootstrap({
|
|
12
|
+
repoPath,
|
|
13
|
+
extraEnv = {},
|
|
14
|
+
}) {
|
|
15
|
+
return spawnSync(
|
|
16
|
+
scriptPath,
|
|
17
|
+
[
|
|
18
|
+
"--repo",
|
|
19
|
+
repoPath,
|
|
20
|
+
"--stack",
|
|
21
|
+
"ts",
|
|
22
|
+
"--mode",
|
|
23
|
+
"new",
|
|
24
|
+
"--codeowners-team",
|
|
25
|
+
"@acme/platform",
|
|
26
|
+
"--yes",
|
|
27
|
+
],
|
|
28
|
+
{
|
|
29
|
+
cwd: ROOT,
|
|
30
|
+
encoding: "utf8",
|
|
31
|
+
env: {
|
|
32
|
+
...process.env,
|
|
33
|
+
...extraEnv,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const noGhRepoDir = mkdtempSync(join(tmpdir(), "sdlc-gh-guidance-no-gh-"));
|
|
40
|
+
const noGhRepo = join(noGhRepoDir, "repo");
|
|
41
|
+
mkdirSync(noGhRepo, { recursive: true });
|
|
42
|
+
const noGhResult = runBootstrap({ repoPath: noGhRepo });
|
|
43
|
+
assert.equal(noGhResult.status, 0, noGhResult.stderr);
|
|
44
|
+
assert.match(
|
|
45
|
+
noGhResult.stdout,
|
|
46
|
+
/Next: npx @guilz-dev\/sdlc-gh --yes --stack ts --codeowners @acme\/platform/,
|
|
47
|
+
);
|
|
48
|
+
assert.match(
|
|
49
|
+
noGhResult.stdout,
|
|
50
|
+
/setup-wizard\.mjs --yes --stack ts --codeowners @acme\/platform --github-repo OWNER\/REPO/,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const fakeBinDir = mkdtempSync(join(tmpdir(), "sdlc-gh-guidance-bin-"));
|
|
54
|
+
const fakeGh = join(fakeBinDir, "gh");
|
|
55
|
+
writeFileSync(
|
|
56
|
+
fakeGh,
|
|
57
|
+
`#!/bin/sh
|
|
58
|
+
if [ "$1" = "repo" ] && [ "$2" = "view" ] && [ "$3" = "--json" ] && [ "$4" = "nameWithOwner" ]; then
|
|
59
|
+
printf '{"nameWithOwner":"acme/test-repo"}\\n'
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
exit 1
|
|
63
|
+
`,
|
|
64
|
+
{ mode: 0o755 },
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const ghRepoDir = mkdtempSync(join(tmpdir(), "sdlc-gh-guidance-gh-"));
|
|
68
|
+
const ghRepo = join(ghRepoDir, "repo");
|
|
69
|
+
mkdirSync(ghRepo, { recursive: true });
|
|
70
|
+
const gitInit = spawnSync("git", ["init"], {
|
|
71
|
+
cwd: ghRepo,
|
|
72
|
+
encoding: "utf8",
|
|
73
|
+
});
|
|
74
|
+
assert.equal(gitInit.status, 0, gitInit.stderr);
|
|
75
|
+
const ghResult = runBootstrap({
|
|
76
|
+
repoPath: ghRepo,
|
|
77
|
+
extraEnv: {
|
|
78
|
+
PATH: `${fakeBinDir}:${process.env.PATH}`,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
assert.equal(ghResult.status, 0, ghResult.stderr);
|
|
82
|
+
assert.match(
|
|
83
|
+
ghResult.stdout,
|
|
84
|
+
/Next: npx @guilz-dev\/sdlc-gh --yes --stack ts --codeowners @acme\/platform/,
|
|
85
|
+
);
|
|
86
|
+
assert.doesNotMatch(
|
|
87
|
+
ghResult.stdout,
|
|
88
|
+
/npx @guilz-dev\/sdlc-gh --yes --stack ts --codeowners @acme\/platform --github-repo OWNER\/REPO/,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const copiedTest = readFileSync(join(ghRepo, "scripts/test-bootstrap-guidance-scenarios.mjs"), "utf8");
|
|
92
|
+
assert.ok(copiedTest.includes("setup-wizard.mjs"));
|
|
93
|
+
|
|
94
|
+
console.log("Bootstrap guidance scenario tests passed");
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
aggregateDiffStats,
|
|
5
|
+
evaluateDiffSize,
|
|
6
|
+
resolveAutonomyLevel,
|
|
7
|
+
resolveEnforcementMode,
|
|
8
|
+
resolveLimits,
|
|
9
|
+
} from "./lib/diff-size.mjs";
|
|
10
|
+
|
|
11
|
+
// no label / L1 default warn
|
|
12
|
+
const l1Default = evaluateDiffSize({
|
|
13
|
+
labels: [],
|
|
14
|
+
numstatText: "400\t0\tbig.ts",
|
|
15
|
+
});
|
|
16
|
+
assert.equal(l1Default.level, "L1");
|
|
17
|
+
assert.equal(l1Default.mode, "warn");
|
|
18
|
+
assert.equal(l1Default.overLimit, true);
|
|
19
|
+
|
|
20
|
+
// L1 hard-fail opt-in
|
|
21
|
+
const l1Hard = evaluateDiffSize({
|
|
22
|
+
labels: ["autonomy:L1"],
|
|
23
|
+
numstatText: "400\t0\tbig.ts",
|
|
24
|
+
l1HardFail: true,
|
|
25
|
+
});
|
|
26
|
+
assert.equal(l1Hard.mode, "hard-fail");
|
|
27
|
+
assert.equal(l1Hard.overLimit, true);
|
|
28
|
+
|
|
29
|
+
// L2 over-limit fail
|
|
30
|
+
const l2 = evaluateDiffSize({
|
|
31
|
+
labels: ["autonomy:L2"],
|
|
32
|
+
numstatText: "150\t0\ta.ts\n10\t0\tb.ts\n10\t0\tc.ts\n10\t0\td.ts\n10\t0\te.ts",
|
|
33
|
+
});
|
|
34
|
+
assert.equal(l2.level, "L2");
|
|
35
|
+
assert.equal(l2.mode, "hard-fail");
|
|
36
|
+
assert.equal(l2.overLimit, true);
|
|
37
|
+
assert.equal(resolveLimits("L2").loc, 120);
|
|
38
|
+
|
|
39
|
+
// L3 over-limit fail
|
|
40
|
+
const l3 = evaluateDiffSize({
|
|
41
|
+
labels: ["autonomy:L3"],
|
|
42
|
+
numstatText: "70\t0\ta.ts\n10\t0\tb.ts\n10\t0\tc.ts",
|
|
43
|
+
});
|
|
44
|
+
assert.equal(l3.level, "L3");
|
|
45
|
+
assert.equal(l3.mode, "hard-fail");
|
|
46
|
+
assert.equal(l3.overLimit, true);
|
|
47
|
+
|
|
48
|
+
// infra-sensitive path changed without task:infra warns
|
|
49
|
+
const infra = evaluateDiffSize({
|
|
50
|
+
labels: ["task:docs", "autonomy:L1"],
|
|
51
|
+
numstatText: "5\t0\tREADME.md",
|
|
52
|
+
diffFiles: [".github/workflows/harness-ci.yml"],
|
|
53
|
+
});
|
|
54
|
+
assert.equal(infra.sensitiveWarnings.length, 1);
|
|
55
|
+
assert.match(infra.sensitiveWarnings[0], /task:infra/);
|
|
56
|
+
|
|
57
|
+
const infraOk = evaluateDiffSize({
|
|
58
|
+
labels: ["task:infra", "autonomy:L0"],
|
|
59
|
+
numstatText: "5\t0\t.github/workflows/harness-ci.yml",
|
|
60
|
+
diffFiles: [".github/workflows/harness-ci.yml"],
|
|
61
|
+
});
|
|
62
|
+
assert.equal(infraOk.sensitiveWarnings.length, 0);
|
|
63
|
+
|
|
64
|
+
// L0 proposal-only: no size limits enforced (behavior/spec correction vs legacy L0→L1 fallback)
|
|
65
|
+
const l0Huge = evaluateDiffSize({
|
|
66
|
+
labels: ["autonomy:L0"],
|
|
67
|
+
numstatText: "5000\t0\ta.ts\n5000\t0\tb.ts",
|
|
68
|
+
});
|
|
69
|
+
assert.equal(l0Huge.level, "L0");
|
|
70
|
+
assert.equal(l0Huge.mode, "proposal-only");
|
|
71
|
+
assert.equal(l0Huge.limits, null);
|
|
72
|
+
assert.equal(l0Huge.overLimit, false);
|
|
73
|
+
|
|
74
|
+
// within limits passes
|
|
75
|
+
const ok = evaluateDiffSize({
|
|
76
|
+
labels: ["autonomy:L2"],
|
|
77
|
+
numstatText: "50\t0\ta.ts",
|
|
78
|
+
});
|
|
79
|
+
assert.equal(ok.overLimit, false);
|
|
80
|
+
|
|
81
|
+
assert.equal(resolveAutonomyLevel(["autonomy:L3"]), "L3");
|
|
82
|
+
assert.equal(resolveAutonomyLevel(["autonomy:L2"]), "L2");
|
|
83
|
+
assert.equal(resolveAutonomyLevel(["autonomy:L0"]), "L0");
|
|
84
|
+
assert.equal(resolveEnforcementMode("L1"), "warn");
|
|
85
|
+
assert.equal(resolveEnforcementMode("L1", { l1HardFail: true }), "hard-fail");
|
|
86
|
+
assert.equal(aggregateDiffStats("10\t5\tfile.ts").loc, 15);
|
|
87
|
+
|
|
88
|
+
console.log("Diff-size scenario tests passed");
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { localChecks } from "./lib/doctor-local.mjs";
|
|
7
|
+
|
|
8
|
+
const tempDir = mkdtempSync(join(tmpdir(), "sdlc-gh-doctor-"));
|
|
9
|
+
|
|
10
|
+
mkdirSync(join(tempDir, ".github/workflows"), { recursive: true });
|
|
11
|
+
writeFileSync(join(tempDir, ".harness-stack"), "ts\n");
|
|
12
|
+
writeFileSync(join(tempDir, ".github/workflows/product-ci-ts.yml"), "name: product-ci-ts\n");
|
|
13
|
+
writeFileSync(
|
|
14
|
+
join(tempDir, ".github/CODEOWNERS"),
|
|
15
|
+
"* @my-org/harness-engineers\n",
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const healthy = localChecks(tempDir, { nodeVersion: "22.0.0" });
|
|
19
|
+
assert.ok(healthy.entries.every((entry) => entry.status === "PASS"));
|
|
20
|
+
assert.equal(healthy.stackId, "ts");
|
|
21
|
+
|
|
22
|
+
const missingDir = mkdtempSync(join(tmpdir(), "sdlc-gh-doctor-missing-"));
|
|
23
|
+
mkdirSync(join(missingDir, ".github/workflows"), { recursive: true });
|
|
24
|
+
writeFileSync(join(missingDir, ".github/CODEOWNERS"), "* @my-org/harness-engineers\n");
|
|
25
|
+
const noStack = localChecks(missingDir, { nodeVersion: "22.0.0" });
|
|
26
|
+
assert.ok(noStack.entries.some((e) => e.label === ".harness-stack" && e.status === "FAIL"));
|
|
27
|
+
assert.ok(noStack.entries.some((e) => e.label === "product-ci workflow" && e.status === "FAIL"));
|
|
28
|
+
|
|
29
|
+
const inferredDir = mkdtempSync(join(tmpdir(), "sdlc-gh-doctor-inferred-"));
|
|
30
|
+
mkdirSync(join(inferredDir, ".github/workflows"), { recursive: true });
|
|
31
|
+
writeFileSync(join(inferredDir, ".github/workflows/product-ci-python.yml"), "name: product-ci-python\n");
|
|
32
|
+
writeFileSync(join(inferredDir, ".github/CODEOWNERS"), "* @my-org/harness-engineers\n");
|
|
33
|
+
const inferred = localChecks(inferredDir, { nodeVersion: "22.0.0" });
|
|
34
|
+
assert.equal(inferred.stackId, "python");
|
|
35
|
+
assert.ok(inferred.entries.some((e) => e.label === ".harness-stack" && e.status === "PASS" && e.detail.includes("inferred python")));
|
|
36
|
+
|
|
37
|
+
const placeholderDir = mkdtempSync(join(tmpdir(), "sdlc-gh-doctor-placeholder-"));
|
|
38
|
+
mkdirSync(join(placeholderDir, ".github/workflows"), { recursive: true });
|
|
39
|
+
writeFileSync(join(placeholderDir, ".harness-stack"), "python\n");
|
|
40
|
+
writeFileSync(join(placeholderDir, ".github/workflows/product-ci-python.yml"), "name: product-ci-python\n");
|
|
41
|
+
writeFileSync(
|
|
42
|
+
join(placeholderDir, ".github/CODEOWNERS"),
|
|
43
|
+
"* @your-org/harness-engineers\n",
|
|
44
|
+
);
|
|
45
|
+
const placeholder = localChecks(placeholderDir, { nodeVersion: "22.0.0" });
|
|
46
|
+
assert.ok(placeholder.entries.some((e) => e.label === "CODEOWNERS" && e.status === "FAIL"));
|
|
47
|
+
|
|
48
|
+
const oldNode = localChecks(tempDir, { nodeVersion: "20.0.0" });
|
|
49
|
+
assert.ok(oldNode.entries.some((e) => e.label === "Node.js" && e.status === "FAIL"));
|
|
50
|
+
|
|
51
|
+
const templateMulti = mkdtempSync(join(tmpdir(), "sdlc-gh-doctor-template-"));
|
|
52
|
+
mkdirSync(join(templateMulti, ".github/workflows"), { recursive: true });
|
|
53
|
+
writeFileSync(join(templateMulti, ".harness-stack"), "ts\n");
|
|
54
|
+
writeFileSync(join(templateMulti, ".github/workflows/product-ci-ts.yml"), "name: product-ci-ts\n");
|
|
55
|
+
writeFileSync(join(templateMulti, ".github/workflows/product-ci-go.yml"), "name: product-ci-go\n");
|
|
56
|
+
writeFileSync(join(templateMulti, ".github/CODEOWNERS"), "* @your-org/harness-engineers\n");
|
|
57
|
+
const templateMode = localChecks(templateMulti, { nodeVersion: "22.0.0", templateMode: true });
|
|
58
|
+
assert.ok(templateMode.entries.some((e) => e.label === "product-ci workflow" && e.status === "PASS"));
|
|
59
|
+
assert.ok(templateMode.entries.some((e) => e.label === "CODEOWNERS" && e.status === "PASS"));
|
|
60
|
+
|
|
61
|
+
const templatePersonalized = mkdtempSync(join(tmpdir(), "sdlc-gh-doctor-template-personal-"));
|
|
62
|
+
mkdirSync(join(templatePersonalized, ".github/workflows"), { recursive: true });
|
|
63
|
+
writeFileSync(join(templatePersonalized, ".harness-stack"), "ts\n");
|
|
64
|
+
writeFileSync(join(templatePersonalized, ".github/workflows/product-ci-ts.yml"), "name: product-ci-ts\n");
|
|
65
|
+
writeFileSync(join(templatePersonalized, ".github/workflows/product-ci-go.yml"), "name: product-ci-go\n");
|
|
66
|
+
writeFileSync(join(templatePersonalized, ".github/CODEOWNERS"), "* @acme/platform\n");
|
|
67
|
+
const templatePersonal = localChecks(templatePersonalized, { nodeVersion: "22.0.0", templateMode: true });
|
|
68
|
+
assert.ok(templatePersonal.entries.some((e) => e.label === "CODEOWNERS" && e.status === "FAIL"));
|
|
69
|
+
|
|
70
|
+
console.log("Doctor scenario tests passed");
|