@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,258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Scenario tests for CC-SD issue-spec-check validation.
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
CCSD_ENFORCED_TASK_CLASSES,
|
|
10
|
+
isPlaceholderContent,
|
|
11
|
+
pickLinkedIssue,
|
|
12
|
+
resolveFetchFailureAction,
|
|
13
|
+
shouldEnforceCcsd,
|
|
14
|
+
validateCcsdFields,
|
|
15
|
+
validateLabelShape,
|
|
16
|
+
} from "./lib/ccsd-contract.mjs";
|
|
17
|
+
|
|
18
|
+
const ROOT = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const CHECK_SCRIPT = join(ROOT, "check-issue-spec.mjs");
|
|
20
|
+
|
|
21
|
+
function completeIssueBody({ omitField } = {}) {
|
|
22
|
+
const fields = {
|
|
23
|
+
Goal: "Update the README quick-start section with bootstrap instructions.",
|
|
24
|
+
"Non-goals": "- Do not change CI workflows\n- Do not modify sample code",
|
|
25
|
+
Constraints: "- Markdown only\n- Keep under 60 LOC",
|
|
26
|
+
"Acceptance criteria":
|
|
27
|
+
"- [ ] README lists all three bootstrap options\n- [ ] Links resolve correctly",
|
|
28
|
+
"Rollback hints": "Revert the single README commit.",
|
|
29
|
+
};
|
|
30
|
+
if (omitField) delete fields[omitField];
|
|
31
|
+
return Object.entries(fields)
|
|
32
|
+
.map(([k, v]) => `### ${k}\n\n${v}`)
|
|
33
|
+
.join("\n\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function runCheck(env) {
|
|
37
|
+
return spawnSync("node", [CHECK_SCRIPT], {
|
|
38
|
+
env: { ...process.env, ...env },
|
|
39
|
+
encoding: "utf8",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function assertExit(name, result, expectedCode) {
|
|
44
|
+
if (result.status !== expectedCode) {
|
|
45
|
+
console.error(`::error::${name}: expected exit ${expectedCode}, got ${result.status}`);
|
|
46
|
+
console.error(result.stdout);
|
|
47
|
+
console.error(result.stderr);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Unit-level checks
|
|
53
|
+
const complete = validateCcsdFields(completeIssueBody());
|
|
54
|
+
if (!complete.ok) {
|
|
55
|
+
console.error("::error::Complete issue body should pass validation");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const missingNonGoals = validateCcsdFields(completeIssueBody({ omitField: "Non-goals" }));
|
|
60
|
+
if (missingNonGoals.ok || !missingNonGoals.missing.includes("Non-goals")) {
|
|
61
|
+
console.error("::error::Missing Non-goals should fail validation");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const placeholderRollback = validateCcsdFields(
|
|
66
|
+
completeIssueBody().replace(
|
|
67
|
+
"Revert the single README commit.",
|
|
68
|
+
"How to revert this change immediately if needed.",
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
if (placeholderRollback.ok || !placeholderRollback.placeholder.includes("Rollback hints")) {
|
|
72
|
+
console.error("::error::Placeholder Rollback hints should fail validation");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const goalWithExtra = isPlaceholderContent(
|
|
77
|
+
"One short paragraph describing what this task achieves. Update the README quick-start.",
|
|
78
|
+
);
|
|
79
|
+
if (goalWithExtra) {
|
|
80
|
+
console.error("::error::Goal with placeholder prefix plus real content should pass");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const nonGoalsWithReal = validateCcsdFields(
|
|
85
|
+
completeIssueBody().replace(
|
|
86
|
+
"- Do not change CI workflows\n- Do not modify sample code",
|
|
87
|
+
"- Item the task must not do or change\n- Do not change CI workflows",
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
if (!nonGoalsWithReal.ok) {
|
|
91
|
+
console.error("::error::Non-goals with placeholder line plus real bullets should pass");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const acceptancePlaceholderOnly = validateCcsdFields(
|
|
96
|
+
completeIssueBody().replace(
|
|
97
|
+
"- [ ] README lists all three bootstrap options\n- [ ] Links resolve correctly",
|
|
98
|
+
"- [ ] Criterion 1\n- [ ] Criterion 2",
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
if (acceptancePlaceholderOnly.ok || !acceptancePlaceholderOnly.placeholder.includes("Acceptance criteria")) {
|
|
102
|
+
console.error("::error::Placeholder-only Acceptance criteria should fail validation");
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const acceptanceWithReal = validateCcsdFields(
|
|
107
|
+
completeIssueBody().replace(
|
|
108
|
+
"- [ ] README lists all three bootstrap options\n- [ ] Links resolve correctly",
|
|
109
|
+
"- [ ] Criterion 1\n- [ ] README lists all three bootstrap options",
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
if (!acceptanceWithReal.ok) {
|
|
113
|
+
console.error("::error::Acceptance criteria with placeholder line plus real item should pass");
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const ambiguous = pickLinkedIssue([
|
|
118
|
+
{ body: "a", labels: ["task:docs", "autonomy:L1"], issueNumber: 1 },
|
|
119
|
+
{ body: "b", labels: ["task:test-fix", "autonomy:L1"], issueNumber: 2 },
|
|
120
|
+
]);
|
|
121
|
+
if (ambiguous.kind !== "ambiguous") {
|
|
122
|
+
console.error("::error::Multiple enforced issues should be flagged as ambiguous");
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const preferEnforced = pickLinkedIssue([
|
|
127
|
+
{ body: "skip", labels: ["task:feature-small", "autonomy:L1"], issueNumber: 10 },
|
|
128
|
+
{ body: "enforce", labels: ["task:docs", "autonomy:L1"], issueNumber: 11 },
|
|
129
|
+
]);
|
|
130
|
+
if (preferEnforced.kind !== "issue" || preferEnforced.issueNumber !== 11) {
|
|
131
|
+
console.error("::error::pickLinkedIssue should prefer the enforced L1 docs/test-fix issue");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!shouldEnforceCcsd(["task:docs", "autonomy:L1"])) {
|
|
136
|
+
console.error("::error::task:docs + autonomy:L1 should trigger enforcement");
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (shouldEnforceCcsd(["task:feature-small", "autonomy:L1"])) {
|
|
141
|
+
console.error("::error::task:feature-small should not trigger enforcement in v1");
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const taskClass of ["infra", "security-sensitive"]) {
|
|
146
|
+
if (shouldEnforceCcsd([`task:${taskClass}`, "autonomy:L1"])) {
|
|
147
|
+
console.error(`::error::task:${taskClass} should not trigger enforcement`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (CCSD_ENFORCED_TASK_CLASSES.length !== 2) {
|
|
153
|
+
console.error("::error::v1 should enforce exactly docs and test-fix");
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Integration checks via check-issue-spec.mjs
|
|
158
|
+
const l1Labels = "task:docs,autonomy:L1";
|
|
159
|
+
|
|
160
|
+
assertExit(
|
|
161
|
+
"docs+L1 complete passes",
|
|
162
|
+
runCheck({ ISSUE_BODY: completeIssueBody(), ISSUE_LABELS: l1Labels }),
|
|
163
|
+
0,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
assertExit(
|
|
167
|
+
"docs+L1 missing Non-goals fails",
|
|
168
|
+
runCheck({
|
|
169
|
+
ISSUE_BODY: completeIssueBody({ omitField: "Non-goals" }),
|
|
170
|
+
ISSUE_LABELS: l1Labels,
|
|
171
|
+
}),
|
|
172
|
+
1,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
assertExit(
|
|
176
|
+
"docs+L1 placeholder Rollback hints fails",
|
|
177
|
+
runCheck({
|
|
178
|
+
ISSUE_BODY: completeIssueBody().replace(
|
|
179
|
+
"Revert the single README commit.",
|
|
180
|
+
"How to revert this change immediately if needed.",
|
|
181
|
+
),
|
|
182
|
+
ISSUE_LABELS: l1Labels,
|
|
183
|
+
}),
|
|
184
|
+
1,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
assertExit(
|
|
188
|
+
"test-fix+L1 complete passes",
|
|
189
|
+
runCheck({
|
|
190
|
+
ISSUE_BODY: completeIssueBody().replace("README", "flaky unit test"),
|
|
191
|
+
ISSUE_LABELS: "task:test-fix,autonomy:L1",
|
|
192
|
+
}),
|
|
193
|
+
0,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
assertExit(
|
|
197
|
+
"feature-small+L1 skips (passes)",
|
|
198
|
+
runCheck({
|
|
199
|
+
ISSUE_BODY: completeIssueBody({ omitField: "Non-goals" }),
|
|
200
|
+
ISSUE_LABELS: "task:feature-small,autonomy:L1",
|
|
201
|
+
}),
|
|
202
|
+
0,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
assertExit(
|
|
206
|
+
"docs+L1 without linked issue fails when inferred from PR labels",
|
|
207
|
+
runCheck({
|
|
208
|
+
PR_LABELS: "task:docs,autonomy:L1",
|
|
209
|
+
}),
|
|
210
|
+
1,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
for (const taskClass of ["infra", "security-sensitive"]) {
|
|
214
|
+
assertExit(
|
|
215
|
+
`${taskClass}+L1 skips (passes)`,
|
|
216
|
+
runCheck({
|
|
217
|
+
ISSUE_BODY: completeIssueBody({ omitField: "Non-goals" }),
|
|
218
|
+
ISSUE_LABELS: `task:${taskClass},autonomy:L1`,
|
|
219
|
+
}),
|
|
220
|
+
0,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const multipleTaskLabels = validateLabelShape([
|
|
225
|
+
"task:docs",
|
|
226
|
+
"task:test-fix",
|
|
227
|
+
"autonomy:L1",
|
|
228
|
+
]);
|
|
229
|
+
if (multipleTaskLabels.ok) {
|
|
230
|
+
console.error("::error::Multiple task:* labels should fail label shape validation");
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
assertExit(
|
|
235
|
+
"multiple task labels on L1 docs fails",
|
|
236
|
+
runCheck({
|
|
237
|
+
ISSUE_BODY: completeIssueBody(),
|
|
238
|
+
ISSUE_LABELS: "task:docs,task:test-fix,autonomy:L1",
|
|
239
|
+
}),
|
|
240
|
+
1,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (resolveFetchFailureAction(["task:docs", "autonomy:L1"]) !== "fail") {
|
|
244
|
+
console.error("::error::Fetch failure with L1 docs proxy labels should fail");
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (resolveFetchFailureAction(["task:infra", "autonomy:L1"]) !== "warn_skip") {
|
|
249
|
+
console.error("::error::Fetch failure with infra proxy labels should warn/skip");
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (resolveFetchFailureAction([]) !== "warn_skip") {
|
|
254
|
+
console.error("::error::Fetch failure without proxy labels should warn/skip");
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
console.log("Issue-spec scenario tests passed");
|
|
@@ -0,0 +1,204 @@
|
|
|
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, resolve } from "node:path";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { runReadinessCheck } from "./lib/l1-readiness.mjs";
|
|
8
|
+
|
|
9
|
+
const ROOT = resolve(process.cwd());
|
|
10
|
+
const checker = join(ROOT, "scripts/check-l1-readiness.mjs");
|
|
11
|
+
|
|
12
|
+
function writeMinimalHarness(repoDir, { stack = "ts", includeAgents = true, templateCodeowners = true } = {}) {
|
|
13
|
+
mkdirSync(join(repoDir, ".github/workflows"), { recursive: true });
|
|
14
|
+
mkdirSync(join(repoDir, ".github/ISSUE_TEMPLATE"), { recursive: true });
|
|
15
|
+
if (includeAgents) {
|
|
16
|
+
mkdirSync(join(repoDir, ".github/agents"), { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
writeFileSync(
|
|
19
|
+
join(repoDir, ".github/CODEOWNERS"),
|
|
20
|
+
templateCodeowners
|
|
21
|
+
? "# template\n/.github/ @your-org/harness-engineers\n"
|
|
22
|
+
: "* @acme/platform\n",
|
|
23
|
+
);
|
|
24
|
+
writeFileSync(join(repoDir, ".github/workflows/harness-ci.yml"), "name: harness-ci\n");
|
|
25
|
+
writeFileSync(join(repoDir, `.github/workflows/product-ci-${stack}.yml`), `name: product-ci-${stack}\n`);
|
|
26
|
+
writeFileSync(join(repoDir, ".github/workflows/copilot-setup-steps.yml"), "name: Copilot setup\n");
|
|
27
|
+
writeFileSync(join(repoDir, ".github/ISSUE_TEMPLATE/task.yml"), "name: Task\n");
|
|
28
|
+
if (includeAgents) {
|
|
29
|
+
writeFileSync(join(repoDir, ".github/agents/triager.agent.md"), "---\nname: triager\n---\n");
|
|
30
|
+
writeFileSync(join(repoDir, ".github/agents/implementer.agent.md"), "---\nname: implementer\n---\n");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeFakeGh(binDir, handlers) {
|
|
35
|
+
const fakeGh = join(binDir, "gh");
|
|
36
|
+
const handlerSource = handlers
|
|
37
|
+
.map(
|
|
38
|
+
(handler, index) => `
|
|
39
|
+
function handler${index}(args) {
|
|
40
|
+
${handler.body}
|
|
41
|
+
}
|
|
42
|
+
`,
|
|
43
|
+
)
|
|
44
|
+
.join("\n");
|
|
45
|
+
const dispatch = handlers
|
|
46
|
+
.map((handler, index) => `if (${handler.match}) return handler${index}(args);`)
|
|
47
|
+
.join("\n");
|
|
48
|
+
|
|
49
|
+
writeFileSync(
|
|
50
|
+
fakeGh,
|
|
51
|
+
`#!/usr/bin/env node
|
|
52
|
+
const args = process.argv.slice(2);
|
|
53
|
+
${handlerSource}
|
|
54
|
+
${dispatch}
|
|
55
|
+
process.stderr.write("unexpected gh invocation: " + JSON.stringify(args) + "\\n");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
`,
|
|
58
|
+
{ mode: 0o755 },
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const repoDir = mkdtempSync(join(tmpdir(), "sdlc-gh-l1-readiness-"));
|
|
63
|
+
writeMinimalHarness(repoDir);
|
|
64
|
+
|
|
65
|
+
const noGhBinDir = mkdtempSync(join(tmpdir(), "sdlc-gh-l1-readiness-no-gh-bin-"));
|
|
66
|
+
makeFakeGh(noGhBinDir, [
|
|
67
|
+
{
|
|
68
|
+
match: 'args[0] === "--version"',
|
|
69
|
+
body: 'process.stdout.write("gh version 2.0.0\\n"); process.exit(0);',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
match: 'args[0] === "auth" && args[1] === "status"',
|
|
73
|
+
body: "process.exit(1);",
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
const isolatedEnv = {
|
|
77
|
+
...process.env,
|
|
78
|
+
PATH: `${noGhBinDir}:${process.env.PATH}`,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const normal = spawnSync("node", [checker, "--template"], {
|
|
82
|
+
cwd: repoDir,
|
|
83
|
+
encoding: "utf8",
|
|
84
|
+
env: isolatedEnv,
|
|
85
|
+
});
|
|
86
|
+
assert.equal(normal.status, 0, normal.stderr);
|
|
87
|
+
assert.match(normal.stdout, /PASS doctor:CODEOWNERS: template placeholder preserved/);
|
|
88
|
+
assert.match(normal.stdout, /MANUAL Copilot coding agent entitlement/);
|
|
89
|
+
assert.match(normal.stdout, /SKIP GitHub CLI\/Auth/);
|
|
90
|
+
|
|
91
|
+
const strict = spawnSync("node", [checker, "--template", "--strict"], {
|
|
92
|
+
cwd: repoDir,
|
|
93
|
+
encoding: "utf8",
|
|
94
|
+
env: isolatedEnv,
|
|
95
|
+
});
|
|
96
|
+
assert.equal(strict.status, 1, `expected strict mode to fail on SKIP entries\n${strict.stdout}\n${strict.stderr}`);
|
|
97
|
+
assert.match(strict.stdout, /SKIP GitHub CLI\/Auth/);
|
|
98
|
+
|
|
99
|
+
const json = spawnSync("node", [checker, "--template", "--json"], {
|
|
100
|
+
cwd: repoDir,
|
|
101
|
+
encoding: "utf8",
|
|
102
|
+
env: isolatedEnv,
|
|
103
|
+
});
|
|
104
|
+
assert.equal(json.status, 0, json.stderr);
|
|
105
|
+
const parsed = JSON.parse(json.stdout);
|
|
106
|
+
assert.equal(parsed.profile, "template");
|
|
107
|
+
assert.equal(parsed.stackId, "ts");
|
|
108
|
+
assert.equal(Array.isArray(parsed.entries), true);
|
|
109
|
+
assert.equal(parsed.exitCode, 0);
|
|
110
|
+
|
|
111
|
+
const missingAgentsDir = mkdtempSync(join(tmpdir(), "sdlc-gh-l1-readiness-missing-"));
|
|
112
|
+
writeMinimalHarness(missingAgentsDir, { includeAgents: false });
|
|
113
|
+
const missing = runReadinessCheck({
|
|
114
|
+
template: true,
|
|
115
|
+
strict: false,
|
|
116
|
+
repoRoot: missingAgentsDir,
|
|
117
|
+
});
|
|
118
|
+
assert.equal(missing.exitCode, 1, "missing L1 assets should fail");
|
|
119
|
+
assert.ok(
|
|
120
|
+
missing.entries.some((entry) => entry.label.includes("implementer.agent.md") && entry.status === "FAIL"),
|
|
121
|
+
"expected missing implementer agent to fail",
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const inferredDir = mkdtempSync(join(tmpdir(), "sdlc-gh-l1-readiness-inferred-"));
|
|
125
|
+
writeMinimalHarness(inferredDir, { stack: "python", templateCodeowners: false });
|
|
126
|
+
const inferredReport = runReadinessCheck({
|
|
127
|
+
template: false,
|
|
128
|
+
strict: false,
|
|
129
|
+
repoRoot: inferredDir,
|
|
130
|
+
});
|
|
131
|
+
assert.equal(inferredReport.stackId, "python");
|
|
132
|
+
assert.ok(
|
|
133
|
+
inferredReport.entries.some(
|
|
134
|
+
(entry) => entry.label === "doctor:.harness-stack" && entry.status === "PASS" && entry.detail.includes("inferred python"),
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const ghBinDir = mkdtempSync(join(tmpdir(), "sdlc-gh-l1-readiness-gh-bin-"));
|
|
139
|
+
makeFakeGh(ghBinDir, [
|
|
140
|
+
{
|
|
141
|
+
match: 'args[0] === "--version"',
|
|
142
|
+
body: 'process.stdout.write("gh version 2.0.0\\n"); process.exit(0);',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
match: 'args[0] === "auth" && args[1] === "status"',
|
|
146
|
+
body: "process.exit(0);",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
match: 'args[0] === "repo" && args[1] === "view"',
|
|
150
|
+
body: 'process.stdout.write(JSON.stringify({ nameWithOwner: "acme/product" }) + "\\n"); process.exit(0);',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
match: 'args[0] === "api" && args[1].includes("/labels")',
|
|
154
|
+
body: 'process.stdout.write(JSON.stringify([{ name: "task:docs" }, { name: "task:test-fix" }, { name: "autonomy:L1" }]) + "\\n"); process.exit(0);',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
match: 'args[0] === "api" && args[1].endsWith("/rulesets")',
|
|
158
|
+
body: 'process.stdout.write(JSON.stringify([{ id: 1, name: "main-protection" }]) + "\\n"); process.exit(0);',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
match: 'args[0] === "api" && args[1].includes("/rulesets/1")',
|
|
162
|
+
body: `process.stdout.write(JSON.stringify({
|
|
163
|
+
enforcement: "active",
|
|
164
|
+
rules: [{
|
|
165
|
+
type: "required_status_checks",
|
|
166
|
+
parameters: {
|
|
167
|
+
required_status_checks: [
|
|
168
|
+
{ context: "harness-static" },
|
|
169
|
+
{ context: "diff-size" },
|
|
170
|
+
{ context: "issue-spec-check" },
|
|
171
|
+
{ context: "product-ci-python" },
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
}],
|
|
175
|
+
}) + "\\n"); process.exit(0);`,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
match: 'args[0] === "api" && args[1].includes("copilot-setup-steps.yml/runs")',
|
|
179
|
+
body: `process.stdout.write(JSON.stringify({
|
|
180
|
+
workflow_runs: [{ id: 99, status: "in_progress", conclusion: null }],
|
|
181
|
+
}) + "\\n"); process.exit(0);`,
|
|
182
|
+
},
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
const ghRepoDir = mkdtempSync(join(tmpdir(), "sdlc-gh-l1-readiness-gh-"));
|
|
186
|
+
writeMinimalHarness(ghRepoDir, { stack: "python", templateCodeowners: false });
|
|
187
|
+
const ghReport = spawnSync("node", [checker, "--github-repo", "acme/product", "--json"], {
|
|
188
|
+
cwd: ghRepoDir,
|
|
189
|
+
encoding: "utf8",
|
|
190
|
+
env: {
|
|
191
|
+
...process.env,
|
|
192
|
+
PATH: `${ghBinDir}:${process.env.PATH}`,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
assert.equal(ghReport.status, 0, ghReport.stderr);
|
|
196
|
+
const ghParsed = JSON.parse(ghReport.stdout);
|
|
197
|
+
assert.ok(
|
|
198
|
+
ghParsed.entries.some(
|
|
199
|
+
(entry) => entry.label === "Copilot setup workflow" && entry.status === "WARN" && entry.detail.includes("in_progress"),
|
|
200
|
+
),
|
|
201
|
+
"in-progress copilot setup should warn, not fail",
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
console.log("L1 readiness scenario tests passed");
|
|
@@ -0,0 +1,53 @@
|
|
|
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 { mergeHarnessPackageJson } from "./lib/merge-harness-package.mjs";
|
|
7
|
+
|
|
8
|
+
const ROOT = resolve(process.cwd());
|
|
9
|
+
const templatePath = join(ROOT, "package.json");
|
|
10
|
+
|
|
11
|
+
const mergeDir = mkdtempSync(join(tmpdir(), "sdlc-gh-merge-pkg-"));
|
|
12
|
+
const targetPath = join(mergeDir, "package.json");
|
|
13
|
+
writeFileSync(
|
|
14
|
+
targetPath,
|
|
15
|
+
`${JSON.stringify(
|
|
16
|
+
{
|
|
17
|
+
name: "my-product",
|
|
18
|
+
version: "1.0.0",
|
|
19
|
+
private: true,
|
|
20
|
+
scripts: {
|
|
21
|
+
test: "vitest run",
|
|
22
|
+
build: "tsc -p tsconfig.json",
|
|
23
|
+
},
|
|
24
|
+
dependencies: {
|
|
25
|
+
react: "^19.0.0",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
null,
|
|
29
|
+
2,
|
|
30
|
+
)}\n`,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const merged = mergeHarnessPackageJson(templatePath, targetPath);
|
|
34
|
+
assert.equal(merged.action, "merged");
|
|
35
|
+
const parsed = JSON.parse(readFileSync(targetPath, "utf8"));
|
|
36
|
+
assert.equal(parsed.name, "my-product");
|
|
37
|
+
assert.equal(parsed.version, "1.0.0");
|
|
38
|
+
assert.deepEqual(parsed.dependencies, { react: "^19.0.0" });
|
|
39
|
+
assert.equal(parsed.scripts.test, "vitest run");
|
|
40
|
+
assert.equal(parsed.scripts.build, "tsc -p tsconfig.json");
|
|
41
|
+
assert.equal(typeof parsed.scripts["check-l1-readiness"], "string");
|
|
42
|
+
assert.equal(typeof parsed.scripts.validate, "string");
|
|
43
|
+
|
|
44
|
+
const createDir = mkdtempSync(join(tmpdir(), "sdlc-gh-merge-pkg-create-"));
|
|
45
|
+
const createdPath = join(createDir, "package.json");
|
|
46
|
+
const created = mergeHarnessPackageJson(templatePath, createdPath);
|
|
47
|
+
assert.equal(created.action, "created");
|
|
48
|
+
const createdParsed = JSON.parse(readFileSync(createdPath, "utf8"));
|
|
49
|
+
assert.equal(createdParsed.private, true);
|
|
50
|
+
assert.equal(typeof createdParsed.scripts["check-l1-readiness"], "string");
|
|
51
|
+
assert.equal(createdParsed.name, undefined);
|
|
52
|
+
|
|
53
|
+
console.log("Merge harness package scenario tests passed");
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
NPM_PACKAGE_FILES,
|
|
6
|
+
isPathCoveredByNpmFiles,
|
|
7
|
+
postInstallHint,
|
|
8
|
+
validateNpmSampleCoverage,
|
|
9
|
+
validatePackageJsonFiles,
|
|
10
|
+
} from "./lib/npm-package.mjs";
|
|
11
|
+
|
|
12
|
+
const ROOT = resolve(process.cwd());
|
|
13
|
+
|
|
14
|
+
assert.equal(isPathCoveredByNpmFiles("sample/ts/src/index.ts"), true);
|
|
15
|
+
assert.equal(isPathCoveredByNpmFiles("sample/ts/node_modules/foo.js"), false);
|
|
16
|
+
assert.equal(isPathCoveredByNpmFiles("sample/php/vendor/autoload.php"), false);
|
|
17
|
+
|
|
18
|
+
const pkgCheck = validatePackageJsonFiles(ROOT);
|
|
19
|
+
assert.equal(pkgCheck.ok, true, pkgCheck.reason);
|
|
20
|
+
|
|
21
|
+
const coverage = validateNpmSampleCoverage(ROOT);
|
|
22
|
+
assert.deepEqual(coverage.missingOnDisk, []);
|
|
23
|
+
assert.deepEqual(coverage.notPacked, []);
|
|
24
|
+
|
|
25
|
+
assert.match(postInstallHint("ts"), /npm install/);
|
|
26
|
+
assert.match(postInstallHint("php"), /composer install/);
|
|
27
|
+
|
|
28
|
+
assert.ok(NPM_PACKAGE_FILES.includes("sample/go"));
|
|
29
|
+
assert.ok(NPM_PACKAGE_FILES.includes("scripts"));
|
|
30
|
+
|
|
31
|
+
console.log("npm package scenario tests passed");
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { isTemplateRoot, resolveTemplateRoot } from "./lib/template-root.mjs";
|
|
8
|
+
import { runBootstrap } from "./lib/setup-wizard.mjs";
|
|
9
|
+
|
|
10
|
+
const ROOT = resolve(process.cwd());
|
|
11
|
+
assert.equal(isTemplateRoot(ROOT), true);
|
|
12
|
+
|
|
13
|
+
const resolved = resolveTemplateRoot({ fromModule: import.meta.url });
|
|
14
|
+
assert.equal(resolved, ROOT);
|
|
15
|
+
|
|
16
|
+
const emptyDir = mkdtempSync(join(tmpdir(), "sdlc-gh-cli-empty-"));
|
|
17
|
+
const prevCwd = process.cwd();
|
|
18
|
+
try {
|
|
19
|
+
process.chdir(emptyDir);
|
|
20
|
+
let threw = false;
|
|
21
|
+
try {
|
|
22
|
+
resolveTemplateRoot();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
threw = true;
|
|
25
|
+
assert.match(String(error.message), /Unable to locate harness template root/);
|
|
26
|
+
}
|
|
27
|
+
assert.equal(threw, true);
|
|
28
|
+
} finally {
|
|
29
|
+
process.chdir(prevCwd);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const targetDir = mkdtempSync(join(tmpdir(), "sdlc-gh-cli-target-"));
|
|
33
|
+
mkdirSync(targetDir, { recursive: true });
|
|
34
|
+
const bootstrapResult = runBootstrap({
|
|
35
|
+
repoRoot: targetDir,
|
|
36
|
+
stackId: "ts",
|
|
37
|
+
mode: "new",
|
|
38
|
+
owner: "@acme/platform",
|
|
39
|
+
yes: true,
|
|
40
|
+
templateRoot: ROOT,
|
|
41
|
+
});
|
|
42
|
+
assert.equal(bootstrapResult.status, 0, bootstrapResult.stderr || bootstrapResult.stdout);
|
|
43
|
+
assert.equal(
|
|
44
|
+
spawnSync("test", ["-f", join(targetDir, ".github/workflows/harness-ci.yml")]).status,
|
|
45
|
+
0,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const helpResult = spawnSync(process.execPath, [join(ROOT, "scripts/sdlc-gh-cli.mjs"), "--help"], {
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
});
|
|
51
|
+
assert.equal(helpResult.status, 0);
|
|
52
|
+
assert.match(helpResult.stdout, /@guilz-dev\/sdlc-gh/);
|
|
53
|
+
|
|
54
|
+
console.log("sdlc-gh CLI scenario tests passed");
|