@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.
Files changed (176) hide show
  1. package/.github/CODEOWNERS +5 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +68 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +1 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.yml +39 -0
  5. package/.github/ISSUE_TEMPLATE/support.yml +56 -0
  6. package/.github/ISSUE_TEMPLATE/task.yml +89 -0
  7. package/.github/agents/implementer.agent.md +17 -0
  8. package/.github/agents/reviewer.agent.md +18 -0
  9. package/.github/agents/triager.agent.md +13 -0
  10. package/.github/aw/actions-lock.json +9 -0
  11. package/.github/copilot-instructions.md +35 -0
  12. package/.github/hooks/hooks.json +12 -0
  13. package/.github/instructions/core.instructions.md +11 -0
  14. package/.github/instructions/profiles/go.instructions.md +10 -0
  15. package/.github/instructions/profiles/php.instructions.md +11 -0
  16. package/.github/instructions/profiles/python.instructions.md +11 -0
  17. package/.github/instructions/profiles/ruby.instructions.md +11 -0
  18. package/.github/instructions/profiles/typescript.instructions.md +11 -0
  19. package/.github/labels.yml +55 -0
  20. package/.github/pull_request_template.md +33 -0
  21. package/.github/ruleset.example.json +33 -0
  22. package/.github/ruleset.harness-eval.example.json +29 -0
  23. package/.github/skills/quality-loop/SKILL.md +23 -0
  24. package/.github/workflows/agent-retry-orchestrator.yml +161 -0
  25. package/.github/workflows/copilot-setup-steps.yml +64 -0
  26. package/.github/workflows/eval-ci.yml +169 -0
  27. package/.github/workflows/eval-drift.yml +75 -0
  28. package/.github/workflows/gh-aw-dogfood-ci.yml +73 -0
  29. package/.github/workflows/harness-ci.yml +244 -0
  30. package/.github/workflows/harness-sync.yml +28 -0
  31. package/.github/workflows/l1-readiness-check.yml +45 -0
  32. package/.github/workflows/labels-sync.yml +24 -0
  33. package/.github/workflows/nightly-harness-review.lock.yml +1643 -0
  34. package/.github/workflows/nightly-harness-review.md +87 -0
  35. package/.github/workflows/nightly-harness-review.yml +63 -0
  36. package/.github/workflows/npm-publish.yml +49 -0
  37. package/.github/workflows/pr-context-comment.yml +138 -0
  38. package/.github/workflows/product-ci-go.yml +33 -0
  39. package/.github/workflows/product-ci-php.yml +39 -0
  40. package/.github/workflows/product-ci-python.yml +34 -0
  41. package/.github/workflows/product-ci-ruby.yml +35 -0
  42. package/.github/workflows/product-ci-ts.yml +37 -0
  43. package/.github/workflows/task-issue-label-sync.yml +50 -0
  44. package/.github/workflows/weekly-redteam.lock.yml +1571 -0
  45. package/.github/workflows/weekly-redteam.md +76 -0
  46. package/.github/zizmor.yml +11 -0
  47. package/AGENTS.md +54 -0
  48. package/LICENSE +21 -0
  49. package/README.md +366 -0
  50. package/config/stacks.json +55 -0
  51. package/docs/adoption.md +126 -0
  52. package/docs/arch.md +535 -0
  53. package/docs/auth-boundaries.md +16 -0
  54. package/docs/coding-agent-l1.md +152 -0
  55. package/docs/exceptions/README.md +25 -0
  56. package/docs/exceptions/TEMPLATE.md +8 -0
  57. package/docs/failure-taxonomy.md +23 -0
  58. package/docs/gh-aw-dogfood.md +109 -0
  59. package/docs/kpi-baseline.md +9 -0
  60. package/docs/nightly-harness-review.md +94 -0
  61. package/docs/operations.md +108 -0
  62. package/docs/publishing.md +79 -0
  63. package/docs/revert-playbook.md +44 -0
  64. package/docs/shared-config.md +30 -0
  65. package/docs/telemetry-artifacts.md +78 -0
  66. package/docs/telemetry-schema.md +60 -0
  67. package/evals/.score-baseline.json +6 -0
  68. package/evals/e2e-bench/README.md +28 -0
  69. package/evals/e2e-bench/manifest.json +16 -0
  70. package/evals/e2e-bench/tasks/e2e-001.yml +10 -0
  71. package/evals/e2e-bench/tasks/e2e-002.yml +11 -0
  72. package/evals/e2e-bench/tasks/e2e-003.yml +10 -0
  73. package/evals/e2e-bench/tasks/e2e-004.yml +14 -0
  74. package/evals/e2e-bench/tasks/e2e-005.yml +11 -0
  75. package/evals/e2e-bench/tasks/e2e-006.yml +10 -0
  76. package/evals/e2e-bench/tasks/e2e-007.yml +10 -0
  77. package/evals/e2e-bench/tasks/e2e-008.yml +10 -0
  78. package/evals/e2e-bench/tasks/e2e-009.yml +10 -0
  79. package/evals/trajectories/rubric.md +12 -0
  80. package/evals/trajectories/test_harness_conventions.py +271 -0
  81. package/infra/README.md +49 -0
  82. package/infra/langfuse/docker-compose.yml +25 -0
  83. package/infra/otel/collector-config.yml +24 -0
  84. package/infra/samples/gh-aw-dogfood-report.json +44 -0
  85. package/infra/samples/harness-review-routing-plan.json +19 -0
  86. package/infra/samples/harness-review-summary.json +61 -0
  87. package/infra/samples/telemetry-artifact.json +29 -0
  88. package/infra/samples/telemetry-payload.json +19 -0
  89. package/package.json +85 -0
  90. package/prompts/triager-classify.prompt.yml +10 -0
  91. package/sample/go/add.go +5 -0
  92. package/sample/go/add_test.go +9 -0
  93. package/sample/go/go.mod +3 -0
  94. package/sample/php/composer.json +26 -0
  95. package/sample/php/composer.lock +1881 -0
  96. package/sample/php/phpunit.xml +8 -0
  97. package/sample/php/src/Add.php +13 -0
  98. package/sample/php/tests/AddTest.php +16 -0
  99. package/sample/python/requirements-dev.txt +2 -0
  100. package/sample/python/src/__init__.py +0 -0
  101. package/sample/python/src/greet.py +3 -0
  102. package/sample/python/tests/conftest.py +4 -0
  103. package/sample/python/tests/test_greet.py +5 -0
  104. package/sample/ruby/.rubocop.yml +10 -0
  105. package/sample/ruby/Gemfile +6 -0
  106. package/sample/ruby/Gemfile.lock +58 -0
  107. package/sample/ruby/lib/add.rb +9 -0
  108. package/sample/ruby/spec/add_spec.rb +11 -0
  109. package/sample/ts/biome.json +6 -0
  110. package/sample/ts/package-lock.json +1763 -0
  111. package/sample/ts/package.json +15 -0
  112. package/sample/ts/src/add.ts +3 -0
  113. package/sample/ts/tests/add.test.ts +8 -0
  114. package/sample/ts/tsconfig.json +12 -0
  115. package/scripts/aggregate-harness-review.mjs +48 -0
  116. package/scripts/bootstrap-harness.sh +411 -0
  117. package/scripts/check-diff-size.mjs +46 -0
  118. package/scripts/check-e2e-manifest.mjs +35 -0
  119. package/scripts/check-eval-score-drift.mjs +31 -0
  120. package/scripts/check-gh-aw-dogfood-scope.mjs +51 -0
  121. package/scripts/check-issue-spec.mjs +215 -0
  122. package/scripts/check-l1-readiness.mjs +82 -0
  123. package/scripts/check-open-pr-limit.mjs +34 -0
  124. package/scripts/doctor.mjs +177 -0
  125. package/scripts/emit-gh-aw-dogfood-report.mjs +112 -0
  126. package/scripts/emit-telemetry-artifact.mjs +99 -0
  127. package/scripts/fetch-telemetry-artifacts.mjs +176 -0
  128. package/scripts/harness-drift-report.mjs +99 -0
  129. package/scripts/lib/bootstrap-copy.mjs +123 -0
  130. package/scripts/lib/ccsd-contract.mjs +212 -0
  131. package/scripts/lib/diff-size.mjs +103 -0
  132. package/scripts/lib/doctor-local.mjs +179 -0
  133. package/scripts/lib/e2e-manifest.mjs +76 -0
  134. package/scripts/lib/gh-aw-dogfood.mjs +293 -0
  135. package/scripts/lib/github-config.mjs +94 -0
  136. package/scripts/lib/harness-ci-fragments.mjs +98 -0
  137. package/scripts/lib/harness-review-routing.mjs +244 -0
  138. package/scripts/lib/harness-review.mjs +388 -0
  139. package/scripts/lib/issue-form-label-sync.mjs +56 -0
  140. package/scripts/lib/l1-readiness.mjs +258 -0
  141. package/scripts/lib/merge-harness-package.mjs +36 -0
  142. package/scripts/lib/npm-package.mjs +129 -0
  143. package/scripts/lib/setup-wizard.mjs +224 -0
  144. package/scripts/lib/stacks.mjs +138 -0
  145. package/scripts/lib/telemetry-artifact.mjs +253 -0
  146. package/scripts/lib/template-root.mjs +39 -0
  147. package/scripts/merge-harness-package.mjs +14 -0
  148. package/scripts/route-harness-review.mjs +168 -0
  149. package/scripts/run-e2e-bench.mjs +216 -0
  150. package/scripts/sdlc-gh-cli.mjs +91 -0
  151. package/scripts/select-eval-jobs.mjs +41 -0
  152. package/scripts/setup-github.mjs +242 -0
  153. package/scripts/setup-github.sh +4 -0
  154. package/scripts/setup-wizard.mjs +426 -0
  155. package/scripts/test-bootstrap-guidance-scenarios.mjs +94 -0
  156. package/scripts/test-diff-size-scenarios.mjs +88 -0
  157. package/scripts/test-doctor-scenarios.mjs +70 -0
  158. package/scripts/test-e2e-manifest-scenarios.mjs +65 -0
  159. package/scripts/test-gh-aw-dogfood-scenarios.mjs +74 -0
  160. package/scripts/test-harness-review-routing-scenarios.mjs +130 -0
  161. package/scripts/test-harness-review-scenarios.mjs +92 -0
  162. package/scripts/test-hooks-scenarios.mjs +44 -0
  163. package/scripts/test-issue-form-label-sync-scenarios.mjs +48 -0
  164. package/scripts/test-issue-spec-scenarios.mjs +258 -0
  165. package/scripts/test-l1-readiness-scenarios.mjs +204 -0
  166. package/scripts/test-merge-harness-package-scenarios.mjs +53 -0
  167. package/scripts/test-npm-package-scenarios.mjs +31 -0
  168. package/scripts/test-sdlc-gh-cli-scenarios.mjs +54 -0
  169. package/scripts/test-setup-github-scenarios.mjs +103 -0
  170. package/scripts/test-setup-wizard-scenarios.mjs +114 -0
  171. package/scripts/test-telemetry-artifact-scenarios.mjs +69 -0
  172. package/scripts/trim-harness-ci.mjs +18 -0
  173. package/scripts/validate-gh-aw-compile.mjs +64 -0
  174. package/scripts/validate-harness.mjs +199 -0
  175. package/scripts/validate-telemetry.mjs +21 -0
  176. package/scripts/verify-bootstrap-stacks.sh +192 -0
@@ -0,0 +1,293 @@
1
+ /**
2
+ * gh-aw dogfood validation track for sdlc-gh itself.
3
+ * See docs/gh-aw-dogfood.md.
4
+ */
5
+
6
+ export const DOGFOOD_TASK_LABEL = "task:gh-aw-dogfood";
7
+
8
+ /** Markdown sources and compiled lock files under validation */
9
+ export const GH_AW_SOURCE_WORKFLOWS = [
10
+ {
11
+ id: "nightly-harness-review",
12
+ md: ".github/workflows/nightly-harness-review.md",
13
+ lock: ".github/workflows/nightly-harness-review.lock.yml",
14
+ },
15
+ {
16
+ id: "weekly-redteam",
17
+ md: ".github/workflows/weekly-redteam.md",
18
+ lock: ".github/workflows/weekly-redteam.lock.yml",
19
+ },
20
+ ];
21
+
22
+ /**
23
+ * Paths that may change during a dogfood task (narrow, reviewable scope).
24
+ * @type {readonly string[]}
25
+ */
26
+ export const DOGFOOD_ALLOWED_PATH_PREFIXES = [
27
+ ".github/workflows/nightly-harness-review.md",
28
+ ".github/workflows/nightly-harness-review.lock.yml",
29
+ ".github/workflows/weekly-redteam.md",
30
+ ".github/workflows/weekly-redteam.lock.yml",
31
+ ".github/workflows/gh-aw-dogfood-ci.yml",
32
+ ".github/labels.yml",
33
+ ".github/aw/",
34
+ "scripts/lib/gh-aw-dogfood.mjs",
35
+ "scripts/check-gh-aw-dogfood-scope.mjs",
36
+ "scripts/validate-gh-aw-compile.mjs",
37
+ "scripts/emit-gh-aw-dogfood-report.mjs",
38
+ "scripts/test-gh-aw-dogfood-scenarios.mjs",
39
+ "docs/gh-aw-dogfood.md",
40
+ "docs/nightly-harness-review.md",
41
+ "infra/samples/gh-aw-dogfood-report.json",
42
+ ];
43
+
44
+ export const DOGFOOD_EVALUATION_CRITERIA = [
45
+ "scope",
46
+ "safe_outputs",
47
+ "compile",
48
+ "lock_drift",
49
+ "reviewability",
50
+ ];
51
+
52
+ export const GH_AW_SOURCE_REQUIRED_SECTIONS = {
53
+ "nightly-harness-review": [
54
+ "## Required inputs",
55
+ "## Forbidden operations",
56
+ "## Expected outputs",
57
+ "## Fallback when gh-aw regresses",
58
+ "## Promotion criteria",
59
+ ],
60
+ "weekly-redteam": [
61
+ "## Required inputs",
62
+ "## Forbidden operations",
63
+ "## Expected outputs",
64
+ "## Fallback when gh-aw or garak regresses",
65
+ "## Promotion criteria",
66
+ ],
67
+ };
68
+
69
+ /**
70
+ * @param {string} content
71
+ * @param {string} workflowId
72
+ * @returns {{ ok: boolean, missing: string[] }}
73
+ */
74
+ export function validateGhAwSourceSections(content, workflowId) {
75
+ const required = GH_AW_SOURCE_REQUIRED_SECTIONS[workflowId] ?? [];
76
+ const missing = required.filter((section) => !String(content).includes(section));
77
+ return { ok: missing.length === 0, missing };
78
+ }
79
+
80
+ /**
81
+ * @param {string} path
82
+ * @returns {boolean}
83
+ */
84
+ export function isDogfoodAllowedPath(path) {
85
+ const normalized = String(path).replace(/^\.\//, "");
86
+ return DOGFOOD_ALLOWED_PATH_PREFIXES.some(
87
+ (prefix) => normalized === prefix || normalized.startsWith(prefix),
88
+ );
89
+ }
90
+
91
+ /**
92
+ * @param {string[]} changedFiles
93
+ * @returns {string[]}
94
+ */
95
+ export function findOutOfScopePaths(changedFiles) {
96
+ return changedFiles.filter((file) => !isDogfoodAllowedPath(file));
97
+ }
98
+
99
+ /**
100
+ * Parse comma-separated PR label names (GITHUB event payload style).
101
+ * @param {string | undefined} raw
102
+ * @returns {string[]}
103
+ */
104
+ export function parseDogfoodLabels(raw) {
105
+ return String(raw || "")
106
+ .split(",")
107
+ .map((s) => s.trim())
108
+ .filter(Boolean);
109
+ }
110
+
111
+ /**
112
+ * Scope is enforced only when `task:gh-aw-dogfood` is set (matches check-gh-aw-dogfood-scope).
113
+ * @param {string[]} changedFiles
114
+ * @param {string[]} labels
115
+ * @returns {{ ok: boolean, issues: string[], enforced: boolean }}
116
+ */
117
+ export function evaluateDogfoodScope(changedFiles, labels = []) {
118
+ const enforced = labels.includes(DOGFOOD_TASK_LABEL);
119
+ if (!enforced) {
120
+ return { ok: true, issues: [], enforced: false };
121
+ }
122
+ const outOfScope = findOutOfScopePaths(changedFiles);
123
+ return {
124
+ ok: outOfScope.length === 0,
125
+ issues: outOfScope.map((file) => `out of scope: ${file}`),
126
+ enforced: true,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * @param {string} block
132
+ * @returns {Record<string, unknown>}
133
+ */
134
+ function parseSafeOutputsBlock(block) {
135
+ const result = {};
136
+ let current = null;
137
+ for (const line of String(block).split("\n")) {
138
+ const level1 = line.match(/^ ([\w-]+):\s*(.*)$/);
139
+ if (level1) {
140
+ current = level1[1];
141
+ const value = level1[2].trim();
142
+ result[current] = value ? (/^\d+$/.test(value) ? Number(value) : value) : {};
143
+ continue;
144
+ }
145
+ const level2 = line.match(/^ ([\w-]+):\s*(.*)$/);
146
+ if (level2 && current) {
147
+ if (typeof result[current] !== "object" || result[current] === null) {
148
+ result[current] = {};
149
+ }
150
+ const value = level2[2].trim();
151
+ result[current][level2[1]] = /^\d+$/.test(value) ? Number(value) : value;
152
+ }
153
+ }
154
+ return result;
155
+ }
156
+
157
+ /**
158
+ * @param {string} content
159
+ * @returns {{ raw: string, fields: Record<string, unknown>, body: string } | null}
160
+ */
161
+ export function parseGhAwWorkflowMarkdown(content) {
162
+ const match = String(content).match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
163
+ if (!match) return null;
164
+
165
+ const fmText = match[1];
166
+ const fields = {};
167
+ for (const line of fmText.split("\n")) {
168
+ const top = line.match(/^(\w[\w-]*):\s*(.*)$/);
169
+ if (!top || line.startsWith(" ")) continue;
170
+ const value = top[2].trim().replace(/^['"]|['"]$/g, "");
171
+ fields[top[1]] = value || true;
172
+ }
173
+
174
+ const safeMatch = fmText.match(/safe-outputs:\s*\n([\s\S]*?)(?=\n[a-z].*:|$)/i);
175
+ if (safeMatch) {
176
+ fields["safe-outputs"] = parseSafeOutputsBlock(safeMatch[1]);
177
+ }
178
+
179
+ return { raw: fmText, fields, body: match[2] };
180
+ }
181
+
182
+ /**
183
+ * @param {Record<string, unknown>} frontmatterFields
184
+ * @returns {{ ok: boolean, issues: string[] }}
185
+ */
186
+ export function validateSafeOutputs(frontmatterFields) {
187
+ const issues = [];
188
+ const safeOutputs = frontmatterFields["safe-outputs"];
189
+ if (!safeOutputs || typeof safeOutputs !== "object") {
190
+ issues.push("safe-outputs block missing");
191
+ return { ok: false, issues };
192
+ }
193
+
194
+ const pr = safeOutputs["create-pull-request"];
195
+ if (pr && typeof pr === "object") {
196
+ const max = Number(pr.max ?? 0);
197
+ if (max > 1) issues.push("create-pull-request.max must be <= 1 for dogfood");
198
+ }
199
+
200
+ const forbidden = ["auto-merge", "merge-pull-request"];
201
+ for (const key of forbidden) {
202
+ if (safeOutputs[key]) issues.push(`forbidden safe-output: ${key}`);
203
+ }
204
+
205
+ return { ok: issues.length === 0, issues };
206
+ }
207
+
208
+ /**
209
+ * @param {string} lockContent
210
+ * @returns {Record<string, unknown> | null}
211
+ */
212
+ export function parseGhAwLockMetadata(lockContent) {
213
+ const line = String(lockContent).split("\n").find((l) => l.startsWith("# gh-aw-metadata:"));
214
+ if (!line) return null;
215
+ const json = line.slice("# gh-aw-metadata:".length).trim();
216
+ try {
217
+ return JSON.parse(json);
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * @param {string} root
225
+ * @param {typeof GH_AW_SOURCE_WORKFLOWS} workflows
226
+ * @returns {Record<string, { ok: boolean, issues: string[] }>}
227
+ */
228
+ export function evaluateSafeOutputsForWorkflows(readFile, workflows = GH_AW_SOURCE_WORKFLOWS) {
229
+ const results = {};
230
+ for (const wf of workflows) {
231
+ const content = readFile(wf.md);
232
+ const parsed = parseGhAwWorkflowMarkdown(content);
233
+ if (!parsed) {
234
+ results[wf.id] = { ok: false, issues: ["missing YAML frontmatter"] };
235
+ continue;
236
+ }
237
+ results[wf.id] = validateSafeOutputs(parsed.fields);
238
+ }
239
+ return results;
240
+ }
241
+
242
+ /**
243
+ * @param {object} input
244
+ * @returns {Record<string, unknown>}
245
+ */
246
+ export function buildDogfoodReport({
247
+ scope = { ok: true, issues: [] },
248
+ safeOutputs = {},
249
+ compile = { ok: true, skipped: false, issues: [] },
250
+ lockDrift = { ok: true, issues: [] },
251
+ repo = "unknown/unknown",
252
+ }) {
253
+ const safeOk = Object.values(safeOutputs).every((r) => r.ok);
254
+ const criteria = {
255
+ scope: { pass: scope.ok, issues: scope.issues },
256
+ safe_outputs: {
257
+ pass: safeOk,
258
+ workflows: safeOutputs,
259
+ },
260
+ compile: {
261
+ pass: compile.ok,
262
+ skipped: compile.skipped ?? false,
263
+ issues: compile.issues ?? [],
264
+ },
265
+ lock_drift: {
266
+ pass: lockDrift.ok,
267
+ issues: lockDrift.issues ?? [],
268
+ },
269
+ reviewability: {
270
+ pass: scope.ok && safeOk && lockDrift.ok && (compile.ok || compile.skipped),
271
+ note: "Outputs limited to PRs, summaries, compile results, or issues — no auto-merge",
272
+ },
273
+ };
274
+
275
+ const pass = Object.entries(criteria)
276
+ .filter(([key]) => key !== "reviewability")
277
+ .every(([key, value]) => value.pass || (key === "compile" && value.skipped));
278
+
279
+ return {
280
+ schema_version: "1",
281
+ generated_at: new Date().toISOString(),
282
+ repo,
283
+ track: "gh-aw-dogfood",
284
+ pass,
285
+ criteria,
286
+ rollback: {
287
+ trigger:
288
+ "gh-aw preview regression, compile failure, or safe-output policy breach on dogfood track",
289
+ action: "Revert .md/.lock.yml pair and disable gh-aw-dogfood-ci until upstream fix; keep GHA outer loop",
290
+ doc: "docs/gh-aw-dogfood.md#rollback",
291
+ },
292
+ };
293
+ }
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { getStack } from "./stacks.mjs";
4
+
5
+ function mergeRequiredStatusChecks(existingChecks, requiredContexts) {
6
+ const checks = Array.isArray(existingChecks) ? existingChecks : [];
7
+ const deduped = new Map(checks.map((check) => [check.context, check]));
8
+ for (const context of requiredContexts) {
9
+ deduped.set(context, { context });
10
+ }
11
+ return [...deduped.values()].sort((a, b) => a.context.localeCompare(b.context));
12
+ }
13
+
14
+ function applyRequiredContexts(payload, requiredContexts) {
15
+ const rules = Array.isArray(payload.rules) ? payload.rules : [];
16
+ let statusRule = rules.find((rule) => rule.type === "required_status_checks");
17
+ if (!statusRule) {
18
+ statusRule = {
19
+ type: "required_status_checks",
20
+ parameters: {
21
+ strict_required_status_checks_policy: true,
22
+ required_status_checks: [],
23
+ },
24
+ };
25
+ rules.push(statusRule);
26
+ payload.rules = rules;
27
+ }
28
+
29
+ statusRule.parameters = {
30
+ ...statusRule.parameters,
31
+ strict_required_status_checks_policy: true,
32
+ required_status_checks: mergeRequiredStatusChecks(
33
+ statusRule.parameters?.required_status_checks,
34
+ requiredContexts,
35
+ ),
36
+ };
37
+
38
+ delete payload._comment;
39
+ return payload;
40
+ }
41
+
42
+ export function buildMainRequiredContexts(stackId) {
43
+ getStack(stackId);
44
+ return ["harness-static", "diff-size", "issue-spec-check", `product-ci-${stackId}`];
45
+ }
46
+
47
+ export function buildEvalRequiredContexts() {
48
+ return ["harness-static", "select", "trajectory-conventions"];
49
+ }
50
+
51
+ export function buildRulesetPayload(templatePath, stackId) {
52
+ const payload = JSON.parse(readFileSync(templatePath, "utf8"));
53
+ return applyRequiredContexts(payload, buildMainRequiredContexts(stackId));
54
+ }
55
+
56
+ export function buildEvalRulesetPayload(templatePath) {
57
+ const payload = JSON.parse(readFileSync(templatePath, "utf8"));
58
+ return applyRequiredContexts(payload, buildEvalRequiredContexts());
59
+ }
60
+
61
+ export function parseLabels(text) {
62
+ const labels = [];
63
+ let current = null;
64
+
65
+ for (const rawLine of text.split(/\r?\n/)) {
66
+ const line = rawLine.trimEnd();
67
+ if (!line.trim()) continue;
68
+
69
+ if (line.startsWith("- name:")) {
70
+ if (current) labels.push(current);
71
+ current = {
72
+ name: line.slice("- name:".length).trim(),
73
+ color: "",
74
+ description: "",
75
+ };
76
+ continue;
77
+ }
78
+
79
+ if (!current) continue;
80
+ const trimmed = line.trim();
81
+ if (trimmed.startsWith("color:")) {
82
+ current.color = trimmed.slice("color:".length).trim().replace(/^"|"$/g, "");
83
+ } else if (trimmed.startsWith("description:")) {
84
+ current.description = trimmed.slice("description:".length).trim().replace(/^"|"$/g, "");
85
+ }
86
+ }
87
+
88
+ if (current) labels.push(current);
89
+ return labels;
90
+ }
91
+
92
+ export function loadLabels(filePath) {
93
+ return parseLabels(readFileSync(filePath, "utf8"));
94
+ }
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate harness-ci detect/product fragments from stack catalog.
4
+ */
5
+ import { loadStacks, getStack } from "./stacks.mjs";
6
+
7
+ export function stacksForHarness(stackId = null) {
8
+ const stacks = loadStacks();
9
+ if (stackId) {
10
+ return [getStack(stackId)];
11
+ }
12
+ return stacks;
13
+ }
14
+
15
+ export function buildDetectOutputs(stacks) {
16
+ return stacks.map((s) => ` ${s.id}: \${{ steps.detect.outputs.${s.id} }}`).join("\n");
17
+ }
18
+
19
+ export function buildDetectRun(stacks) {
20
+ return stacks
21
+ .map((s) => {
22
+ const samplePath = `sample/${s.sampleDir}/${s.marker}`;
23
+ return ` if [[ -f ${samplePath} || -f ${s.marker} ]]; then
24
+ echo "${s.id}=true" >> "$GITHUB_OUTPUT"
25
+ else
26
+ echo "${s.id}=false" >> "$GITHUB_OUTPUT"
27
+ fi`;
28
+ })
29
+ .join("\n");
30
+ }
31
+
32
+ export function buildProductJobs(stacks) {
33
+ return stacks
34
+ .map(
35
+ (s) => ` product-${s.id}:
36
+ name: product-ci-${s.id}
37
+ needs: detect-projects
38
+ if: needs.detect-projects.outputs.${s.id} == 'true'
39
+ uses: ./.github/workflows/${s.workflow}`,
40
+ )
41
+ .join("\n\n");
42
+ }
43
+
44
+ const TELEMETRY_BASE_NEEDS = [
45
+ "harness-static",
46
+ "issue-spec-check",
47
+ "open-pr-limit",
48
+ "diff-size",
49
+ "detect-projects",
50
+ ];
51
+
52
+ export function buildTelemetryNeeds(stacks) {
53
+ return [
54
+ ...TELEMETRY_BASE_NEEDS.map((job) => ` - ${job}`),
55
+ ...stacks.map((s) => ` - product-${s.id}`),
56
+ ].join("\n");
57
+ }
58
+
59
+ export function patchTelemetryNeeds(content, stacks) {
60
+ const needs = buildTelemetryNeeds(stacks);
61
+ const telemetryNeedsRe = /( telemetry:\n[\s\S]*? needs:\n)([\s\S]*?)( steps:)/;
62
+ if (!telemetryNeedsRe.test(content)) {
63
+ return content;
64
+ }
65
+ return content.replace(telemetryNeedsRe, `$1${needs}\n$3`);
66
+ }
67
+
68
+ export function patchHarnessCi(content, stacks) {
69
+ const outputs = buildDetectOutputs(stacks);
70
+ const detectRun = buildDetectRun(stacks);
71
+ const productJobs = buildProductJobs(stacks);
72
+
73
+ const outputsRe = /( outputs:\n)([\s\S]*?)( steps:)/;
74
+ if (!outputsRe.test(content)) {
75
+ throw new Error("harness-ci.yml: could not find detect-projects outputs block");
76
+ }
77
+ let patched = content.replace(outputsRe, `$1${outputs}\n$3`);
78
+
79
+ const runRe = /( - id: detect\n shell: bash\n run: \|\n)([\s\S]*?)(\n product-)/;
80
+ if (!runRe.test(patched)) {
81
+ throw new Error("harness-ci.yml: could not find detect-projects run block");
82
+ }
83
+ patched = patched.replace(runRe, `$1${detectRun}\n$3`);
84
+
85
+ const productWithTelemetry = /( product-ts:[\s\S]*?)(\n\n telemetry:[\s\S]*)$/;
86
+ if (productWithTelemetry.test(patched)) {
87
+ patched = patched.replace(productWithTelemetry, `${productJobs}$2`);
88
+ return patchTelemetryNeeds(patched, stacks);
89
+ }
90
+
91
+ const productRe = /( product-ts:[\s\S]*)$/;
92
+ if (!productRe.test(patched)) {
93
+ throw new Error("harness-ci.yml: could not find product-* jobs block");
94
+ }
95
+ patched = patched.replace(productRe, `${productJobs}\n`);
96
+
97
+ return patchTelemetryNeeds(patched, stacks);
98
+ }