@juangcarmona/lighthouse 0.9.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/assets/schemas/github-profile.schema.json +499 -0
- package/github-profile.md +91 -0
- package/package.json +5 -3
- package/src/create-dotnet.mjs +2 -27
- package/src/github-profile.mjs +84 -0
- package/src/lighthouse.mjs +32 -39
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ Use `lighthouse reconcile` to preview missing Lighthouse-managed paths in an exi
|
|
|
16
16
|
|
|
17
17
|
Use `lighthouse upgrade` to preview a versioned foundation update. The command is dry-run by default, accepts `--scope harness`, `--scope sdlc` or `--scope all`, preserves customized projections and stops when ownership evidence is missing. Add `--apply` only after reviewing the plan, then run `lighthouse verify`.
|
|
18
18
|
|
|
19
|
+
Agent workflows are opt-in through the SDLC package installer. Initialization does not create an entire worker catalog. Upgrading an older adoption releases workflow ownership while preserving the existing files for explicit review and cleanup.
|
|
20
|
+
|
|
19
21
|
The harness package owns the baseline command and verification contract. The SDLC package owns lifecycle skills, commands, metacommands, routes and gates, and `lighthouse init` records its selected version in `.lighthouse/sdlc.json`. The UI package owns visual tokens, native recipes, `DESIGN.md` and its agent skill. Selecting `visual-theme` projects the guide and skill into the adopter and enables its deterministic guardrail. Stack profiles own their source templates, guidance and checks; the .NET creator is the packaged entry point for the selected backend profile.
|
|
20
22
|
|
|
21
23
|
ProductShape, ArcShape and OpenSpec keep their own files and commands. Add them through their supported integration routes when the project needs them, and keep their versions and managed paths visible in the project configuration. In an existing repository, run `lighthouse authorities --adopt-existing` to validate the current ProductShape and ArcShape surfaces without running either initializer; review the dry-run plan and add `--apply` only to record both exact versions in Lighthouse-owned configuration.
|
|
@@ -72,3 +74,7 @@ Only `passed` and `not-applicable` satisfy a required guardrail; `pending-review
|
|
|
72
74
|
To add a future guardrail, the catalog owner adds a JSON entry under the packaged `assets/guardrails/` surface, validates it against `assets/schemas/guardrails.schema.json`, declares its profile applicability and repair hint, and uses an existing checker or an explicit semantic or human verification method; in this delivery the surface belongs to the CLI package, and the CLI core does not change.
|
|
73
75
|
|
|
74
76
|
A new deterministic checker mechanism or a new engineering practice is outside this catalog and requires a separate Product Change.
|
|
77
|
+
|
|
78
|
+
## Optional GitHub repository profile
|
|
79
|
+
|
|
80
|
+
The recommended `github-profile` capability is opt-in. Adopters declare checks, contribution policy and reasoned exceptions in `.lighthouse/github.json`, then use `lighthouse capabilities plan`, `apply` and `verify --id github-profile` to reconcile live settings. No remote settings change during installation or ordinary upgrades. See the [GitHub profile guide](github-profile.md) for configuration, supported controls, authorization and limitations.
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/juangcarmona/lighthouse/schemas/github-profile.schema.json",
|
|
4
|
+
"title": "Optional Lighthouse GitHub repository profile",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schemaVersion",
|
|
9
|
+
"profileVersion",
|
|
10
|
+
"repository",
|
|
11
|
+
"intake",
|
|
12
|
+
"requiredChecks",
|
|
13
|
+
"requiredApprovals"
|
|
14
|
+
],
|
|
15
|
+
"properties": {
|
|
16
|
+
"schemaVersion": {
|
|
17
|
+
"const": 1
|
|
18
|
+
},
|
|
19
|
+
"profileVersion": {
|
|
20
|
+
"const": 1
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9-]*/[A-Za-z0-9_.-]+$"
|
|
25
|
+
},
|
|
26
|
+
"intake": {
|
|
27
|
+
"enum": [
|
|
28
|
+
"preserve",
|
|
29
|
+
"public",
|
|
30
|
+
"collaborators"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"requiredApprovals": {
|
|
34
|
+
"type": "integer",
|
|
35
|
+
"minimum": 0,
|
|
36
|
+
"maximum": 6
|
|
37
|
+
},
|
|
38
|
+
"requiredChecks": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"minItems": 1,
|
|
41
|
+
"items": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"required": [
|
|
45
|
+
"context",
|
|
46
|
+
"integrationId"
|
|
47
|
+
],
|
|
48
|
+
"properties": {
|
|
49
|
+
"context": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"minLength": 1
|
|
52
|
+
},
|
|
53
|
+
"integrationId": {
|
|
54
|
+
"type": "integer",
|
|
55
|
+
"minimum": 1
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"evidenceBranch": {
|
|
61
|
+
"type": [
|
|
62
|
+
"string",
|
|
63
|
+
"null"
|
|
64
|
+
],
|
|
65
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9_/-]*$"
|
|
66
|
+
},
|
|
67
|
+
"releaseWorkflowReady": {
|
|
68
|
+
"type": "boolean"
|
|
69
|
+
},
|
|
70
|
+
"overrides": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"additionalProperties": false,
|
|
73
|
+
"properties": {
|
|
74
|
+
"repository.has_issues": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"additionalProperties": false,
|
|
77
|
+
"required": [
|
|
78
|
+
"value",
|
|
79
|
+
"reason"
|
|
80
|
+
],
|
|
81
|
+
"properties": {
|
|
82
|
+
"value": {
|
|
83
|
+
"type": [
|
|
84
|
+
"boolean",
|
|
85
|
+
"null"
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
"reason": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"minLength": 1
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"repository.has_discussions": {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"additionalProperties": false,
|
|
97
|
+
"required": [
|
|
98
|
+
"value",
|
|
99
|
+
"reason"
|
|
100
|
+
],
|
|
101
|
+
"properties": {
|
|
102
|
+
"value": {
|
|
103
|
+
"type": [
|
|
104
|
+
"boolean",
|
|
105
|
+
"null"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"reason": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"minLength": 1
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"repository.is_template": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"additionalProperties": false,
|
|
117
|
+
"required": [
|
|
118
|
+
"value",
|
|
119
|
+
"reason"
|
|
120
|
+
],
|
|
121
|
+
"properties": {
|
|
122
|
+
"value": {
|
|
123
|
+
"type": [
|
|
124
|
+
"boolean",
|
|
125
|
+
"null"
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
"reason": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"minLength": 1
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"repository.allow_squash_merge": {
|
|
135
|
+
"type": "object",
|
|
136
|
+
"additionalProperties": false,
|
|
137
|
+
"required": [
|
|
138
|
+
"value",
|
|
139
|
+
"reason"
|
|
140
|
+
],
|
|
141
|
+
"properties": {
|
|
142
|
+
"value": {
|
|
143
|
+
"type": [
|
|
144
|
+
"boolean",
|
|
145
|
+
"null"
|
|
146
|
+
]
|
|
147
|
+
},
|
|
148
|
+
"reason": {
|
|
149
|
+
"type": "string",
|
|
150
|
+
"minLength": 1
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"repository.allow_merge_commit": {
|
|
155
|
+
"type": "object",
|
|
156
|
+
"additionalProperties": false,
|
|
157
|
+
"required": [
|
|
158
|
+
"value",
|
|
159
|
+
"reason"
|
|
160
|
+
],
|
|
161
|
+
"properties": {
|
|
162
|
+
"value": {
|
|
163
|
+
"type": [
|
|
164
|
+
"boolean",
|
|
165
|
+
"null"
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
"reason": {
|
|
169
|
+
"type": "string",
|
|
170
|
+
"minLength": 1
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"repository.allow_rebase_merge": {
|
|
175
|
+
"type": "object",
|
|
176
|
+
"additionalProperties": false,
|
|
177
|
+
"required": [
|
|
178
|
+
"value",
|
|
179
|
+
"reason"
|
|
180
|
+
],
|
|
181
|
+
"properties": {
|
|
182
|
+
"value": {
|
|
183
|
+
"type": [
|
|
184
|
+
"boolean",
|
|
185
|
+
"null"
|
|
186
|
+
]
|
|
187
|
+
},
|
|
188
|
+
"reason": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"minLength": 1
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
"repository.allow_auto_merge": {
|
|
195
|
+
"type": "object",
|
|
196
|
+
"additionalProperties": false,
|
|
197
|
+
"required": [
|
|
198
|
+
"value",
|
|
199
|
+
"reason"
|
|
200
|
+
],
|
|
201
|
+
"properties": {
|
|
202
|
+
"value": {
|
|
203
|
+
"type": [
|
|
204
|
+
"boolean",
|
|
205
|
+
"null"
|
|
206
|
+
]
|
|
207
|
+
},
|
|
208
|
+
"reason": {
|
|
209
|
+
"type": "string",
|
|
210
|
+
"minLength": 1
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
"repository.allow_update_branch": {
|
|
215
|
+
"type": "object",
|
|
216
|
+
"additionalProperties": false,
|
|
217
|
+
"required": [
|
|
218
|
+
"value",
|
|
219
|
+
"reason"
|
|
220
|
+
],
|
|
221
|
+
"properties": {
|
|
222
|
+
"value": {
|
|
223
|
+
"type": [
|
|
224
|
+
"boolean",
|
|
225
|
+
"null"
|
|
226
|
+
]
|
|
227
|
+
},
|
|
228
|
+
"reason": {
|
|
229
|
+
"type": "string",
|
|
230
|
+
"minLength": 1
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
"repository.delete_branch_on_merge": {
|
|
235
|
+
"type": "object",
|
|
236
|
+
"additionalProperties": false,
|
|
237
|
+
"required": [
|
|
238
|
+
"value",
|
|
239
|
+
"reason"
|
|
240
|
+
],
|
|
241
|
+
"properties": {
|
|
242
|
+
"value": {
|
|
243
|
+
"type": [
|
|
244
|
+
"boolean",
|
|
245
|
+
"null"
|
|
246
|
+
]
|
|
247
|
+
},
|
|
248
|
+
"reason": {
|
|
249
|
+
"type": "string",
|
|
250
|
+
"minLength": 1
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
"repository.web_commit_signoff_required": {
|
|
255
|
+
"type": "object",
|
|
256
|
+
"additionalProperties": false,
|
|
257
|
+
"required": [
|
|
258
|
+
"value",
|
|
259
|
+
"reason"
|
|
260
|
+
],
|
|
261
|
+
"properties": {
|
|
262
|
+
"value": {
|
|
263
|
+
"type": [
|
|
264
|
+
"boolean",
|
|
265
|
+
"null"
|
|
266
|
+
]
|
|
267
|
+
},
|
|
268
|
+
"reason": {
|
|
269
|
+
"type": "string",
|
|
270
|
+
"minLength": 1
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
"repository.squash_merge_commit_title": {
|
|
275
|
+
"type": "object",
|
|
276
|
+
"additionalProperties": false,
|
|
277
|
+
"required": [
|
|
278
|
+
"value",
|
|
279
|
+
"reason"
|
|
280
|
+
],
|
|
281
|
+
"properties": {
|
|
282
|
+
"value": {
|
|
283
|
+
"enum": [
|
|
284
|
+
"PR_TITLE",
|
|
285
|
+
"COMMIT_OR_PR_TITLE",
|
|
286
|
+
null
|
|
287
|
+
]
|
|
288
|
+
},
|
|
289
|
+
"reason": {
|
|
290
|
+
"type": "string",
|
|
291
|
+
"minLength": 1
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
"repository.squash_merge_commit_message": {
|
|
296
|
+
"type": "object",
|
|
297
|
+
"additionalProperties": false,
|
|
298
|
+
"required": [
|
|
299
|
+
"value",
|
|
300
|
+
"reason"
|
|
301
|
+
],
|
|
302
|
+
"properties": {
|
|
303
|
+
"value": {
|
|
304
|
+
"enum": [
|
|
305
|
+
"PR_BODY",
|
|
306
|
+
"COMMIT_MESSAGES",
|
|
307
|
+
"BLANK",
|
|
308
|
+
null
|
|
309
|
+
]
|
|
310
|
+
},
|
|
311
|
+
"reason": {
|
|
312
|
+
"type": "string",
|
|
313
|
+
"minLength": 1
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
"actions.workflow": {
|
|
318
|
+
"type": "object",
|
|
319
|
+
"additionalProperties": false,
|
|
320
|
+
"required": [
|
|
321
|
+
"value",
|
|
322
|
+
"reason"
|
|
323
|
+
],
|
|
324
|
+
"properties": {
|
|
325
|
+
"value": {
|
|
326
|
+
"enum": [
|
|
327
|
+
null,
|
|
328
|
+
{
|
|
329
|
+
"default_workflow_permissions": "read",
|
|
330
|
+
"can_approve_pull_request_reviews": false
|
|
331
|
+
}
|
|
332
|
+
]
|
|
333
|
+
},
|
|
334
|
+
"reason": {
|
|
335
|
+
"type": "string",
|
|
336
|
+
"minLength": 1
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
"actions.forkApproval": {
|
|
341
|
+
"type": "object",
|
|
342
|
+
"additionalProperties": false,
|
|
343
|
+
"required": [
|
|
344
|
+
"value",
|
|
345
|
+
"reason"
|
|
346
|
+
],
|
|
347
|
+
"properties": {
|
|
348
|
+
"value": {
|
|
349
|
+
"enum": [
|
|
350
|
+
"all_external_contributors",
|
|
351
|
+
"first_time_contributors",
|
|
352
|
+
"first_time_contributors_new_to_github",
|
|
353
|
+
null
|
|
354
|
+
]
|
|
355
|
+
},
|
|
356
|
+
"reason": {
|
|
357
|
+
"type": "string",
|
|
358
|
+
"minLength": 1
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
"security.alerts": {
|
|
363
|
+
"type": "object",
|
|
364
|
+
"additionalProperties": false,
|
|
365
|
+
"required": [
|
|
366
|
+
"value",
|
|
367
|
+
"reason"
|
|
368
|
+
],
|
|
369
|
+
"properties": {
|
|
370
|
+
"value": {
|
|
371
|
+
"enum": [
|
|
372
|
+
true,
|
|
373
|
+
null
|
|
374
|
+
]
|
|
375
|
+
},
|
|
376
|
+
"reason": {
|
|
377
|
+
"type": "string",
|
|
378
|
+
"minLength": 1
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
"security.updates": {
|
|
383
|
+
"type": "object",
|
|
384
|
+
"additionalProperties": false,
|
|
385
|
+
"required": [
|
|
386
|
+
"value",
|
|
387
|
+
"reason"
|
|
388
|
+
],
|
|
389
|
+
"properties": {
|
|
390
|
+
"value": {
|
|
391
|
+
"enum": [
|
|
392
|
+
true,
|
|
393
|
+
null
|
|
394
|
+
]
|
|
395
|
+
},
|
|
396
|
+
"reason": {
|
|
397
|
+
"type": "string",
|
|
398
|
+
"minLength": 1
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
"security.secret_scanning": {
|
|
403
|
+
"type": "object",
|
|
404
|
+
"additionalProperties": false,
|
|
405
|
+
"required": [
|
|
406
|
+
"value",
|
|
407
|
+
"reason"
|
|
408
|
+
],
|
|
409
|
+
"properties": {
|
|
410
|
+
"value": {
|
|
411
|
+
"enum": [
|
|
412
|
+
"enabled",
|
|
413
|
+
null
|
|
414
|
+
]
|
|
415
|
+
},
|
|
416
|
+
"reason": {
|
|
417
|
+
"type": "string",
|
|
418
|
+
"minLength": 1
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
"security.secret_scanning_push_protection": {
|
|
423
|
+
"type": "object",
|
|
424
|
+
"additionalProperties": false,
|
|
425
|
+
"required": [
|
|
426
|
+
"value",
|
|
427
|
+
"reason"
|
|
428
|
+
],
|
|
429
|
+
"properties": {
|
|
430
|
+
"value": {
|
|
431
|
+
"enum": [
|
|
432
|
+
"enabled",
|
|
433
|
+
null
|
|
434
|
+
]
|
|
435
|
+
},
|
|
436
|
+
"reason": {
|
|
437
|
+
"type": "string",
|
|
438
|
+
"minLength": 1
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
"releases.immutable": {
|
|
443
|
+
"type": "object",
|
|
444
|
+
"additionalProperties": false,
|
|
445
|
+
"required": [
|
|
446
|
+
"value",
|
|
447
|
+
"reason"
|
|
448
|
+
],
|
|
449
|
+
"properties": {
|
|
450
|
+
"value": {
|
|
451
|
+
"enum": [
|
|
452
|
+
true,
|
|
453
|
+
null
|
|
454
|
+
]
|
|
455
|
+
},
|
|
456
|
+
"reason": {
|
|
457
|
+
"type": "string",
|
|
458
|
+
"minLength": 1
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
"rules.main": {
|
|
463
|
+
"type": "object",
|
|
464
|
+
"additionalProperties": false,
|
|
465
|
+
"required": [
|
|
466
|
+
"value",
|
|
467
|
+
"reason"
|
|
468
|
+
],
|
|
469
|
+
"properties": {
|
|
470
|
+
"value": {
|
|
471
|
+
"type": "null"
|
|
472
|
+
},
|
|
473
|
+
"reason": {
|
|
474
|
+
"type": "string",
|
|
475
|
+
"minLength": 1
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
"rules.evidence": {
|
|
480
|
+
"type": "object",
|
|
481
|
+
"additionalProperties": false,
|
|
482
|
+
"required": [
|
|
483
|
+
"value",
|
|
484
|
+
"reason"
|
|
485
|
+
],
|
|
486
|
+
"properties": {
|
|
487
|
+
"value": {
|
|
488
|
+
"type": "null"
|
|
489
|
+
},
|
|
490
|
+
"reason": {
|
|
491
|
+
"type": "string",
|
|
492
|
+
"minLength": 1
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Recommended GitHub repository profile
|
|
2
|
+
|
|
3
|
+
The `github-profile` capability is an optional recommendation for GitHub.com repositories. Any Lighthouse adopter can select it, supply its own policy, inspect changes, apply an authorized plan and verify live settings. Leaving it unselected or declining it does not fail the general foundation check. It is independent of application profiles and does not require a factory runtime.
|
|
4
|
+
|
|
5
|
+
The profile is an implementation of [profile adoption](../../../docs/product/model/use-cases/uc-adopt-profile.md). GitHub remains the authority for remote settings; the adopter owns configuration and exceptions. Installation, initialization and upgrades never apply this profile remotely.
|
|
6
|
+
|
|
7
|
+
## Configure adoption
|
|
8
|
+
|
|
9
|
+
Create `.lighthouse/github.json` in the consuming repository using the [configuration schema](assets/schemas/github-profile.schema.json). Keep this file in version control and review it like other repository policy. Select unconditional CI job names and their actual GitHub App IDs; do not make a path-filtered job a universal required check. Approvals and contribution policy are explicit choices rather than assumptions about a solo maintainer or an open-source community.
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"schemaVersion": 1,
|
|
14
|
+
"profileVersion": 1,
|
|
15
|
+
"repository": "example/project",
|
|
16
|
+
"intake": "preserve",
|
|
17
|
+
"requiredChecks": [{ "context": "verify", "integrationId": 15368 }],
|
|
18
|
+
"requiredApprovals": 1,
|
|
19
|
+
"evidenceBranch": null,
|
|
20
|
+
"releaseWorkflowReady": false,
|
|
21
|
+
"overrides": {
|
|
22
|
+
"releases.immutable": {
|
|
23
|
+
"value": null,
|
|
24
|
+
"reason": "No release publisher is configured for this project."
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Choose `collaborators`, `public` or `preserve` for issue and PR creation. A solo maintainer may choose zero required approvals, but that setting does not enforce a human integration decision. Keep that decision in the host's authorization policy and keep publication credentials outside implementation workers.
|
|
31
|
+
|
|
32
|
+
Set `evidenceBranch` to a literal permanent branch name when the project uses one. The adapter protects it without creating it, copying application files or changing its tip. Set `releaseWorkflowReady: true` only after checking that the publisher creates a draft, attaches all assets and then publishes the release. Otherwise preserve that setting with a documented exception.
|
|
33
|
+
|
|
34
|
+
An override has a supported control ID, a typed `value` and a non-empty `reason`. `null` means preserve and exclude that control from compliance, reported as `excepted`. A different supported value records a project-specific policy. A `rules.main` or `rules.evidence` override accepts only `null` with a reason; its result explicitly excludes that protection from compliance and does not claim a protected branch. The adapter never disables secret scanning, alerts, security updates or release immutability. It never removes or weakens an existing ruleset, including when the project asks for fewer approvals or checks.
|
|
35
|
+
|
|
36
|
+
## Plan, apply and verify
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
lighthouse capabilities select --id github-profile --provider github
|
|
40
|
+
profile_plan=$(mktemp)
|
|
41
|
+
lighthouse capabilities plan --id github-profile --input .lighthouse/github.json --plan-file "$profile_plan"
|
|
42
|
+
lighthouse capabilities apply --plan-file "$profile_plan" --authorize <plan-hash> --format json
|
|
43
|
+
lighthouse capabilities verify --id github-profile --input .lighthouse/github.json --format json
|
|
44
|
+
rm -- "$profile_plan"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Selection records a local adoption choice when Lighthouse ownership state exists. Planning and live verification can also run against the explicit configuration before full foundation adoption. The default input path is `.lighthouse/github.json`. An optional `--repo` must match the configured repository; there is no implicit target derived from a Git remote or current login.
|
|
48
|
+
|
|
49
|
+
Planning reads GitHub and writes only the requested local plan. The plan records before/after values, exceptions, repository identity, applicable rules and a SHA-256 authorization hash. Apply validates that exact plan and its permitted operations, rereads the provider before each write, and refuses unexpected drift. Already-satisfied operations are skipped on retry. The adapter fixes its API host to `github.com`; GitHub Enterprise Server is outside this version's supported scope.
|
|
50
|
+
|
|
51
|
+
Apply adds missing protection before changing settings. Compatible rules with no bypass satisfy the minimum; stronger rules, additional checks and unrelated settings remain intact. Missing requirements receive a separate additive ruleset. Existing policies are never replaced, deleted or automatically adopted as Lighthouse-owned. GitHub administration permission is required when changes are planned; narrower credentials may inspect the controls their permissions expose. Missing permissions, unavailable APIs and plan restrictions produce `unavailable` or `blocked`, never a success claim. Private repositories remain private.
|
|
52
|
+
|
|
53
|
+
Apply reports its result on standard output, including completed operations and errors on partial failure; `--format json` makes this machine-readable. It creates no result files or history in the repository. Successful adoption updates only the existing `.lighthouse/upgrade.json` entry. Keep the configuration in Git; use a temporary path for the reviewed plan and remove it when finished. If an operator or CI needs durable evidence, capture the output in its existing logs or artifact storage. Failed or uncertain requests require fresh verification before retry; the adapter does not roll back protections or blindly repeat a write. API operations are not a transaction, and GitHub does not provide a compare-and-swap operation for repository settings. Avoid concurrent administrators applying conflicting policies.
|
|
54
|
+
|
|
55
|
+
Live verification exits nonzero for drift or unavailable selected controls and makes no remote writes. General `lighthouse verify` validates local configuration and points to the live command; it does not make network requests or claim that a previous receipt proves today's remote settings. A result with exceptions covers only the selected automated controls. Manual recommendations are always listed separately.
|
|
56
|
+
|
|
57
|
+
## Automated controls
|
|
58
|
+
|
|
59
|
+
| Control | Recommendation | Application |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `repository.has_issues` | `true` | Enable issues. |
|
|
62
|
+
| `repository.has_discussions` | `false` | Preserve an existing community through a reasoned override. |
|
|
63
|
+
| `repository.is_template` | `false` | Override for an intentional starter repository. |
|
|
64
|
+
| `repository.allow_squash_merge` | `true` | Default integration method. |
|
|
65
|
+
| `repository.allow_merge_commit` | `false` | Override when branch ancestry matters. |
|
|
66
|
+
| `repository.allow_rebase_merge` | `false` | Override for a deliberate rebase workflow. |
|
|
67
|
+
| `repository.squash_merge_commit_title` | `PR_TITLE` | Validate PR titles in the project's CI. |
|
|
68
|
+
| `repository.squash_merge_commit_message` | `PR_BODY` | Preserve the reviewed change description. |
|
|
69
|
+
| `repository.allow_auto_merge` | `true` | Makes the feature available; does not enable it on any PR or authorize integration. |
|
|
70
|
+
| `repository.allow_update_branch` | `true` | Offer branch updates. |
|
|
71
|
+
| `repository.delete_branch_on_merge` | `true` | Clean merged feature branches; protect permanent branches separately. |
|
|
72
|
+
| `repository.web_commit_signoff_required` | `false` | Override if the project adopts DCO. |
|
|
73
|
+
| `intake` | Adopter choice | Apply the chosen issue and PR creation policy together, or preserve it. |
|
|
74
|
+
| `rules.main` | PR, resolved conversations, stale-review dismissal, explicit checks and approvals, strict base, no deletion/force-push/bypass | Resolve the current default branch without renaming it. |
|
|
75
|
+
| `rules.evidence` | No deletion, force-push or bypass | Only when an evidence branch is declared. |
|
|
76
|
+
| `actions.workflow` | Read token, no PR approval | Default token permissions only; workflows can request additional permissions. |
|
|
77
|
+
| `actions.forkApproval` | `all_external_contributors` | Public repositories; private-fork policy needs separate review. |
|
|
78
|
+
| `security.alerts` | `true` | Enable Dependabot alerts before security updates. |
|
|
79
|
+
| `security.updates` | `true` | Enable Dependabot security PRs; does not configure version-update scheduling. |
|
|
80
|
+
| `security.secret_scanning` | `enabled` | Require availability or an explicit exception. |
|
|
81
|
+
| `security.secret_scanning_push_protection` | `enabled` | Require availability or an explicit exception. |
|
|
82
|
+
| `releases.immutable` | `true` | Requires confirmed publisher readiness or a reasoned exception. |
|
|
83
|
+
|
|
84
|
+
The adapter does not manage visibility, repository ownership/name, default-branch renaming, Wiki, Projects, sponsorship, social preview, archive participation, commit comments, linked-issue auto-close, LFS archives, multi-ref push limits, Actions retention, issue templates or workflow contents. These remain product decisions or manual checks; they are not silently treated as compliant. Security availability can depend on GitHub plan and permissions; this capability never purchases a feature or changes visibility to unlock it.
|
|
85
|
+
|
|
86
|
+
## References
|
|
87
|
+
|
|
88
|
+
- [GitHub repository settings API](https://docs.github.com/en/rest/repos/repos)
|
|
89
|
+
- [GitHub rulesets API](https://docs.github.com/en/rest/repos/rules)
|
|
90
|
+
- [GitHub Actions permissions API](https://docs.github.com/en/rest/actions/permissions)
|
|
91
|
+
- [Immutable release publication](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juangcarmona/lighthouse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lighthouse adoption and verification CLI",
|
|
6
6
|
"repository": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@arcshape/cli": "0.1.6",
|
|
24
24
|
"@juangcarmona/lighthouse-ui": "0.2.0",
|
|
25
25
|
"@prodshape/cli": "0.23.0",
|
|
26
|
-
"@juangcarmona/lighthouse-sdlc": "0.
|
|
26
|
+
"@juangcarmona/lighthouse-sdlc": "0.12.0"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"src/lighthouse.mjs",
|
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
"src/profiles.mjs",
|
|
36
36
|
"src/create-dotnet.mjs",
|
|
37
37
|
"src/verify-dotnet.mjs",
|
|
38
|
-
"upgrades"
|
|
38
|
+
"upgrades",
|
|
39
|
+
"src/github-profile.mjs",
|
|
40
|
+
"github-profile.md"
|
|
39
41
|
],
|
|
40
42
|
"bin": {
|
|
41
43
|
"lighthouse": "./src/lighthouse.mjs"
|
package/src/create-dotnet.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
4
3
|
import { dirname, join, resolve } from 'node:path';
|
|
5
4
|
import { fileURLToPath } from 'node:url';
|
|
6
5
|
import { createRequire } from 'node:module';
|
|
@@ -10,23 +9,6 @@ const templateRoot = resolve(packageRoot, 'assets/dotnet-template');
|
|
|
10
9
|
const cliPackage = JSON.parse(await readFile(resolve(packageRoot, 'package.json'), 'utf8'));
|
|
11
10
|
const require = createRequire(import.meta.url);
|
|
12
11
|
const sdlcContract = JSON.parse(await readFile(require.resolve('@juangcarmona/lighthouse-sdlc/contract.json'), 'utf8'));
|
|
13
|
-
const sdlcPackageRoot = dirname(require.resolve('@juangcarmona/lighthouse-sdlc/contract.json'));
|
|
14
|
-
const sdlcWorkflowPaths = [
|
|
15
|
-
'.github/workflows/lighthouse-sdlc-router.yml',
|
|
16
|
-
'.github/workflows/lighthouse-sdlc-triage.md',
|
|
17
|
-
'.github/workflows/lighthouse-sdlc-triage.lock.yml',
|
|
18
|
-
'.github/workflows/lighthouse-sdlc-refine.md',
|
|
19
|
-
'.github/workflows/lighthouse-sdlc-refine.lock.yml',
|
|
20
|
-
'.github/workflows/lighthouse-sdlc-propose.md',
|
|
21
|
-
'.github/workflows/lighthouse-sdlc-propose.lock.yml',
|
|
22
|
-
'.github/workflows/lighthouse-sdlc-implement.md',
|
|
23
|
-
'.github/workflows/lighthouse-sdlc-implement.lock.yml',
|
|
24
|
-
'.github/workflows/lighthouse-sdlc-integrate.md',
|
|
25
|
-
'.github/workflows/lighthouse-sdlc-integrate.lock.yml',
|
|
26
|
-
'.github/workflows/lighthouse-sdlc-review.md',
|
|
27
|
-
'.github/workflows/lighthouse-sdlc-review.lock.yml',
|
|
28
|
-
'.github/workflows/shared/lighthouse-sdlc-stage.md',
|
|
29
|
-
];
|
|
30
12
|
const args = process.argv.slice(2);
|
|
31
13
|
const target = args[0] ? resolve(args[0]) : null;
|
|
32
14
|
const name = args.includes('--name') ? args[args.indexOf('--name') + 1] : null;
|
|
@@ -93,6 +75,7 @@ try {
|
|
|
93
75
|
|
|
94
76
|
await copy(resolve(packageRoot, 'src/lighthouse.mjs'), resolve(stage, 'scripts/lighthouse.mjs'));
|
|
95
77
|
await copy(resolve(packageRoot, 'src/aliases.mjs'), resolve(stage, 'scripts/aliases.mjs'));
|
|
78
|
+
await copy(resolve(packageRoot, 'src/github-profile.mjs'), resolve(stage, 'scripts/github-profile.mjs'));
|
|
96
79
|
await copy(resolve(packageRoot, 'src/init-plan.mjs'), resolve(stage, 'scripts/init-plan.mjs'));
|
|
97
80
|
await copy(resolve(packageRoot, 'src/guardrails.mjs'), resolve(stage, 'scripts/guardrails.mjs'));
|
|
98
81
|
await copy(resolve(packageRoot, 'src/profiles.mjs'), resolve(stage, 'scripts/profiles.mjs'));
|
|
@@ -108,7 +91,7 @@ try {
|
|
|
108
91
|
product: name, application: { shape: 'backend', stacks: ['dotnet'] }, profiles: ['dotnet-backend'],
|
|
109
92
|
integrations: { productShape: null, arcShape: null },
|
|
110
93
|
packages: { sdlc: { name: '@juangcarmona/lighthouse-sdlc', version: sdlcContract.version } },
|
|
111
|
-
managed: ['LIGHTHOUSE.md', 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.lighthouse/sdlc.json',
|
|
94
|
+
managed: ['LIGHTHOUSE.md', 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.lighthouse/sdlc.json', '.agents/skills/lighthouse-init/SKILL.md', '.lighthouse/upgrade.json', '.lighthouse/commands.json', 'docs/profiles/dotnet/README.md', 'docs/profiles/dotnet/adoption.md', 'docs/profiles/dotnet/architecture.md', 'docs/profiles/dotnet/guardrails.md', 'docs/profiles/dotnet/practices.md', 'docs/profiles/dotnet/testing-and-development.md', 'docs/profiles/dotnet/use-cases.md', '.agents/skills/lighthouse-dotnet/SKILL.md'],
|
|
112
95
|
repository: {
|
|
113
96
|
layout: ['src', 'docs', 'infra', 'scripts'], publicSite: { enabled: true },
|
|
114
97
|
conventions: { commits: 'conventional', branchPattern: '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$' },
|
|
@@ -125,14 +108,6 @@ try {
|
|
|
125
108
|
|
|
126
109
|
const initialized = (await import('node:child_process')).spawnSync(process.execPath, [resolve(stage, 'scripts/lighthouse.mjs'), 'init'], { cwd: stage, encoding: 'utf8' });
|
|
127
110
|
if (initialized.status !== 0) throw new Error(initialized.stderr || 'Projection generation failed.');
|
|
128
|
-
await copy(resolve(sdlcPackageRoot, 'workflows'), resolve(stage, '.github/workflows'));
|
|
129
|
-
const upgradePath = resolve(stage, '.lighthouse/upgrade.json');
|
|
130
|
-
const upgrade = JSON.parse(await readFile(upgradePath, 'utf8'));
|
|
131
|
-
for (const path of sdlcWorkflowPaths) {
|
|
132
|
-
const content = await readFile(resolve(stage, path), 'utf8');
|
|
133
|
-
upgrade.files[path] = { hash: createHash('sha256').update(content).digest('hex'), scope: 'sdlc' };
|
|
134
|
-
}
|
|
135
|
-
await writeFile(upgradePath, `${JSON.stringify(upgrade, null, 2)}\n`);
|
|
136
111
|
if (existing) await rmdir(target);
|
|
137
112
|
await rename(stage, target);
|
|
138
113
|
process.stdout.write(`Created ${name} in ${target}. Run dotnet restore ${name}.slnx, then npm run verify.\n`);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
const option = (args, name) => args.includes(name) ? args[args.indexOf(name) + 1] : args.find((arg) => arg.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
6
|
+
|
|
7
|
+
async function writeJson(path, value) {
|
|
8
|
+
await mkdir(dirname(path), { recursive: true });
|
|
9
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
10
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
11
|
+
await rename(temporary, path);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function githubProfileCommand(args, { root, catalog }) {
|
|
15
|
+
const adapter = await import('@juangcarmona/lighthouse-sdlc/github-profile');
|
|
16
|
+
const action = args[0];
|
|
17
|
+
const format = option(args, '--format') ?? 'text';
|
|
18
|
+
const configPath = resolve(root, option(args, '--input') ?? '.lighthouse/github.json');
|
|
19
|
+
const planPath = option(args, '--plan-file');
|
|
20
|
+
const repo = option(args, '--repo');
|
|
21
|
+
const output = (value) => {
|
|
22
|
+
if (format === 'json') console.log(JSON.stringify(value, null, 2));
|
|
23
|
+
else {
|
|
24
|
+
console.log(`GitHub profile: ${value.status} (${value.repository.repo})`);
|
|
25
|
+
if (value.completed) console.log(`Completed operations: ${value.completed.join(', ') || 'none'}`);
|
|
26
|
+
if (value.error) console.log(`Error: ${value.error}`);
|
|
27
|
+
for (const control of value.controls ?? []) console.log(` ${control.status} ${control.id}${control.reason ? `: ${control.reason}` : ''}`);
|
|
28
|
+
if (value.authorization) console.log(`Plan hash: ${value.authorization.planHash}\nRemote writes: none`);
|
|
29
|
+
if (value.status === 'drift' && !value.canApply) console.log('Apply requires repository administration permission.');
|
|
30
|
+
for (const item of value.manualRecommendations ?? []) console.log(` manual: ${item}`);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
if (['plan', 'verify'].includes(action)) {
|
|
34
|
+
if (action === 'plan' && !planPath) throw new Error('GitHub profile planning requires --plan-file <path>.');
|
|
35
|
+
const config = JSON.parse(await readFile(configPath, 'utf8'));
|
|
36
|
+
if (repo && repo.toLowerCase() !== config.repository?.toLowerCase()) throw new Error('--repo does not match the repository declared in the GitHub profile.');
|
|
37
|
+
const plan = adapter.buildGithubProfilePlan(config, await adapter.readGithubProfile(config));
|
|
38
|
+
if (action === 'plan') await writeJson(resolve(root, planPath), plan);
|
|
39
|
+
output({ ...plan, checkedAt: new Date().toISOString(), manualRecommendations: adapter.githubProfileManualRecommendations });
|
|
40
|
+
if ((action === 'verify' && plan.status !== 'passed') || (action === 'plan' && !plan.canApply)) process.exitCode = 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (action !== 'apply' || !planPath || !option(args, '--authorize')) throw new Error('Use capabilities plan|verify --id github-profile, or capabilities apply --plan-file <path> --authorize <hash>.');
|
|
44
|
+
const plan = JSON.parse(await readFile(resolve(root, planPath), 'utf8'));
|
|
45
|
+
if (repo && repo.toLowerCase() !== plan.configuration?.repository?.toLowerCase()) throw new Error('--repo does not match the authorized plan.');
|
|
46
|
+
let result;
|
|
47
|
+
try { result = await adapter.applyGithubProfile(plan, option(args, '--authorize')); }
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (!error.result) throw error;
|
|
50
|
+
output(error.result);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const upgradePath = resolve(root, '.lighthouse/upgrade.json');
|
|
55
|
+
let state;
|
|
56
|
+
try { state = JSON.parse(await readFile(upgradePath, 'utf8')); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
57
|
+
if (state) {
|
|
58
|
+
const capability = catalog.capabilities.find((item) => item.id === 'github-profile');
|
|
59
|
+
state.adoption ??= {};
|
|
60
|
+
state.adoption['github-profile'] = { id: capability.id, status: 'adopted', selection: capability.selection, provider: capability.provider, version: capability.version, catalogVersion: catalog.package.version, nextAction: null };
|
|
61
|
+
await writeJson(upgradePath, state);
|
|
62
|
+
}
|
|
63
|
+
output({ ...result, applied: true, manualRecommendations: adapter.githubProfileManualRecommendations });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function localGithubProfileStatus(root) {
|
|
67
|
+
const result = { id: 'foundation.github-profile', owner: '@juangcarmona/lighthouse-sdlc', level: 'recommended', kind: 'hard', verification: 'deterministic', status: 'not-applicable', evidence: [], problems: [], nextAction: null };
|
|
68
|
+
let config;
|
|
69
|
+
try { config = JSON.parse(await readFile(resolve(root, '.lighthouse/github.json'), 'utf8')); }
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error.code === 'ENOENT') return result;
|
|
72
|
+
return { ...result, status: 'failed', problems: ['Cannot parse .lighthouse/github.json.'] };
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const adapter = await import('@juangcarmona/lighthouse-sdlc/github-profile');
|
|
76
|
+
const errors = adapter.validateGithubProfile(config);
|
|
77
|
+
result.status = errors.length ? 'failed' : 'unconfigured';
|
|
78
|
+
result.problems = errors;
|
|
79
|
+
result.nextAction = 'lighthouse capabilities verify --id github-profile --format json';
|
|
80
|
+
// Local verification must not turn an old receipt into a claim about live GitHub.
|
|
81
|
+
result.evidence = [{ path: '.lighthouse/github.json', scope: 'local-configuration', sha256: createHash('sha256').update(JSON.stringify(config)).digest('hex') }];
|
|
82
|
+
} catch { result.status = 'unavailable'; result.problems = ['Install the SDLC GitHub profile adapter to validate this configuration.']; }
|
|
83
|
+
return result;
|
|
84
|
+
}
|
package/src/lighthouse.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { evaluateGuardrails, readGuardrailCatalog } from './guardrails.mjs';
|
|
|
9
9
|
import { composeProfiles, normalizeApplication, profileCompositionResult, readProfileCatalog } from './profiles.mjs';
|
|
10
10
|
import { hasUnresolvedQuestions, manifestFromInitPlan, validateInitPlan } from './init-plan.mjs';
|
|
11
11
|
import { readCommandAliases, verifyCommandAliases } from './aliases.mjs';
|
|
12
|
+
import { githubProfileCommand, localGithubProfileStatus } from './github-profile.mjs';
|
|
12
13
|
let sdlcCapabilities;
|
|
13
14
|
let sdlcReconcile;
|
|
14
15
|
let sdlcGithub;
|
|
@@ -40,22 +41,6 @@ const upgradeStatePath = resolve(root, upgradeStateRelative);
|
|
|
40
41
|
const layout = ['src', 'docs', 'infra', 'scripts'];
|
|
41
42
|
const sdlcPackage = { name: '@juangcarmona/lighthouse-sdlc', version: installedPackageVersion('@juangcarmona/lighthouse-sdlc', 'contract.json') ?? '0.1.0' };
|
|
42
43
|
const uiPackage = { name: '@juangcarmona/lighthouse-ui', version: installedPackageVersion('@juangcarmona/lighthouse-ui', 'DESIGN.md') ?? '0.1.0' };
|
|
43
|
-
const sdlcWorkflowPaths = Object.freeze([
|
|
44
|
-
'.github/workflows/lighthouse-sdlc-router.yml',
|
|
45
|
-
'.github/workflows/lighthouse-sdlc-triage.md',
|
|
46
|
-
'.github/workflows/lighthouse-sdlc-triage.lock.yml',
|
|
47
|
-
'.github/workflows/lighthouse-sdlc-refine.md',
|
|
48
|
-
'.github/workflows/lighthouse-sdlc-refine.lock.yml',
|
|
49
|
-
'.github/workflows/lighthouse-sdlc-propose.md',
|
|
50
|
-
'.github/workflows/lighthouse-sdlc-propose.lock.yml',
|
|
51
|
-
'.github/workflows/lighthouse-sdlc-implement.md',
|
|
52
|
-
'.github/workflows/lighthouse-sdlc-implement.lock.yml',
|
|
53
|
-
'.github/workflows/lighthouse-sdlc-integrate.md',
|
|
54
|
-
'.github/workflows/lighthouse-sdlc-integrate.lock.yml',
|
|
55
|
-
'.github/workflows/lighthouse-sdlc-review.md',
|
|
56
|
-
'.github/workflows/lighthouse-sdlc-review.lock.yml',
|
|
57
|
-
'.github/workflows/shared/lighthouse-sdlc-stage.md',
|
|
58
|
-
]);
|
|
59
44
|
const managedPaths = [
|
|
60
45
|
projection,
|
|
61
46
|
'scripts/lighthouse-guard.mjs',
|
|
@@ -63,7 +48,6 @@ const managedPaths = [
|
|
|
63
48
|
'.husky/pre-push',
|
|
64
49
|
'.github/workflows/lighthouse.yml',
|
|
65
50
|
'.lighthouse/sdlc.json',
|
|
66
|
-
...sdlcWorkflowPaths,
|
|
67
51
|
'.agents/skills/lighthouse-init/SKILL.md',
|
|
68
52
|
upgradeStateRelative,
|
|
69
53
|
'.lighthouse/commands.json',
|
|
@@ -101,7 +85,7 @@ function managedPathsFor(manifest) {
|
|
|
101
85
|
function upgradeScopesFor(manifest) {
|
|
102
86
|
return {
|
|
103
87
|
harness: [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.agents/skills/lighthouse-init/SKILL.md', ...((manifest?.profiles ?? []).flatMap((profile) => profileManagedPaths[profile] ?? []))],
|
|
104
|
-
sdlc: ['.lighthouse/sdlc.json', '.lighthouse/commands.json'
|
|
88
|
+
sdlc: ['.lighthouse/sdlc.json', '.lighthouse/commands.json'],
|
|
105
89
|
};
|
|
106
90
|
}
|
|
107
91
|
|
|
@@ -110,18 +94,6 @@ function packageAsset(packageName, entry) {
|
|
|
110
94
|
return readFileSync(path, 'utf8');
|
|
111
95
|
}
|
|
112
96
|
|
|
113
|
-
function sdlcWorkflowFiles() {
|
|
114
|
-
const files = {};
|
|
115
|
-
for (const target of sdlcWorkflowPaths) {
|
|
116
|
-
try {
|
|
117
|
-
files[target] = packageAsset('@juangcarmona/lighthouse-sdlc', `workflows/${target.slice('.github/workflows/'.length)}`);
|
|
118
|
-
} catch {
|
|
119
|
-
return {};
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return files;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
97
|
function profileAssets(manifest) {
|
|
126
98
|
const assets = {};
|
|
127
99
|
if ((manifest?.profiles ?? []).includes('visual-theme')) {
|
|
@@ -271,7 +243,7 @@ function capabilityAdoption(previous = null) {
|
|
|
271
243
|
? `lighthouse capabilities select --id ${capability.id} --provider ${capability.provider}`
|
|
272
244
|
: ['adopted', 'declined'].includes(status)
|
|
273
245
|
? null
|
|
274
|
-
: `lighthouse capabilities plan --id ${capability.id} --provider ${capability.provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${capability.id}.plan.json`;
|
|
246
|
+
: `lighthouse capabilities plan --id ${capability.id} --provider ${capability.provider} --repo <owner/repo> --plan-file ${capability.id === 'github-profile' ? '<temporary-plan-path>' : `.lighthouse/reconciliation/${capability.id}.plan.json`}`;
|
|
275
247
|
return [capability.id, {
|
|
276
248
|
id: capability.id,
|
|
277
249
|
status,
|
|
@@ -456,7 +428,6 @@ function managedFiles(manifest) {
|
|
|
456
428
|
'.agents/skills/lighthouse-init/SKILL.md': readFileSync(new URL('../assets/skills/init/SKILL.md', import.meta.url), 'utf8'),
|
|
457
429
|
};
|
|
458
430
|
Object.assign(files, profileAssets(manifest));
|
|
459
|
-
Object.assign(files, sdlcWorkflowFiles());
|
|
460
431
|
const controlPlane = sdlcControlPlane(manifest);
|
|
461
432
|
if (controlPlane) files['.lighthouse/sdlc.json'] = controlPlane;
|
|
462
433
|
files['.lighthouse/commands.json'] = commandAliasProjectionText(commandAliasesProjection());
|
|
@@ -541,6 +512,10 @@ function classifyUpgradeFile(path, current, expected, recordedHash) {
|
|
|
541
512
|
return { classification: 'conflicting', action: 'review', reason: 'ownership is not recorded for this path' };
|
|
542
513
|
}
|
|
543
514
|
|
|
515
|
+
function isOptionalWorkflow(path) {
|
|
516
|
+
return /^\.github\/workflows\/lighthouse-sdlc-(router\.yml|(?:triage|refine|propose|implement|integrate|review)\.(?:md|lock\.yml))$/.test(path) || path === '.github/workflows/shared/lighthouse-sdlc-stage.md';
|
|
517
|
+
}
|
|
518
|
+
|
|
544
519
|
async function planUpgrade(manifest, scopes) {
|
|
545
520
|
const state = await readUpgradeState();
|
|
546
521
|
const problems = stateProblems(state);
|
|
@@ -556,6 +531,9 @@ async function planUpgrade(manifest, scopes) {
|
|
|
556
531
|
const result = classifyUpgradeFile(path, current, expected, recordedHash);
|
|
557
532
|
changes.push({ path, scope: Object.entries(upgradeScopes).find(([, values]) => values.includes(path))?.[0], ...result });
|
|
558
533
|
}
|
|
534
|
+
if (scopes.has('sdlc') && manifest.managed.some(isOptionalWorkflow)) {
|
|
535
|
+
changes.push({ path: 'lighthouse.json', scope: 'sdlc', classification: 'adoptable', action: 'update', reason: 'release optional workflow ownership; preserve all workflow files' });
|
|
536
|
+
}
|
|
559
537
|
if (state?.baseline && state.baseline !== manifest.baseline) {
|
|
560
538
|
changes.unshift({ path: 'baseline', scope: 'harness', classification: 'breaking', action: 'review', reason: `baseline ${state.baseline} is not compatible with ${manifest.baseline}` });
|
|
561
539
|
}
|
|
@@ -687,7 +665,11 @@ function validate(manifest) {
|
|
|
687
665
|
const preSemanticWithoutUpgrade = Array.isArray(manifest.managed)
|
|
688
666
|
&& manifest.managed.length === preSemanticWithoutUpgradePaths.length
|
|
689
667
|
&& manifest.managed.every((entry, index) => entry === preSemanticWithoutUpgradePaths[index]);
|
|
690
|
-
|
|
668
|
+
// Earlier foundations installed every workflow. Accept that ownership inventory during upgrade;
|
|
669
|
+
// the foundation now leaves those files to explicit workflow adoption and never deletes them.
|
|
670
|
+
const withoutOldWorkflows = Array.isArray(manifest.managed) ? manifest.managed.filter(path => !isOptionalWorkflow(path)) : [];
|
|
671
|
+
const previousWorkflowManaged = withoutOldWorkflows.length === expectedManagedPaths.length && withoutOldWorkflows.every((path, index) => path === expectedManagedPaths[index]);
|
|
672
|
+
if (!previousWorkflowManaged && !legacyManaged && !profilelessManaged && !previousCompleteManaged && !completeManaged && !preSemanticManaged && !preSemanticWithoutUpgrade) {
|
|
691
673
|
problems.push(`managed must be ${legacyManagedPaths.join(', ')}, the previous Lighthouse guardrail set or the selected profile set`);
|
|
692
674
|
}
|
|
693
675
|
if ('repository' in manifest) {
|
|
@@ -989,7 +971,10 @@ async function applyUpgrade(manifest, plan) {
|
|
|
989
971
|
for (const change of updates) {
|
|
990
972
|
const target = resolve(root, change.path);
|
|
991
973
|
await mkdir(dirname(target), { recursive: true });
|
|
992
|
-
|
|
974
|
+
const content = change.path === 'lighthouse.json'
|
|
975
|
+
? `${JSON.stringify({ ...manifest, managed: manifest.managed.filter(path => !isOptionalWorkflow(path)) }, null, 2)}\n`
|
|
976
|
+
: files[change.path];
|
|
977
|
+
await writeTextAtomically(target, content);
|
|
993
978
|
}
|
|
994
979
|
const next = upgradeState(manifest, files, { ...previous, lastAppliedMigration: `upgrade-${cliPackage.version}` });
|
|
995
980
|
for (const path of Object.keys(next.files)) {
|
|
@@ -1408,7 +1393,7 @@ async function verify(manifest, format = 'text') {
|
|
|
1408
1393
|
for (const item of adoption) {
|
|
1409
1394
|
if (item.status !== 'adopted' || !item.recordPath || await exists(resolve(root, item.recordPath))) continue;
|
|
1410
1395
|
item.status = 'failed';
|
|
1411
|
-
item.nextAction = `lighthouse capabilities plan --id ${item.id} --provider ${item.provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${item.id}.plan.json`;
|
|
1396
|
+
item.nextAction = `lighthouse capabilities plan --id ${item.id} --provider ${item.provider} --repo <owner/repo> --plan-file ${item.id === 'github-profile' ? '<temporary-plan-path>' : `.lighthouse/reconciliation/${item.id}.plan.json`}`;
|
|
1412
1397
|
}
|
|
1413
1398
|
if (adoption.length) {
|
|
1414
1399
|
const pending = adoption.filter((item) => !['adopted', 'declined', 'available'].includes(item.status));
|
|
@@ -1421,13 +1406,16 @@ async function verify(manifest, format = 'text') {
|
|
|
1421
1406
|
kind: 'hard',
|
|
1422
1407
|
verification: 'deterministic',
|
|
1423
1408
|
status: pending.length ? 'unconfigured' : available || declined ? 'not-applicable' : 'passed',
|
|
1424
|
-
adoptionStatus: pending.length ? pending[0].status :
|
|
1409
|
+
adoptionStatus: pending.length ? pending[0].status : declined ? 'declined' : available ? 'available' : 'adopted',
|
|
1425
1410
|
evidence: adoption.map((item) => ({ capability: item.id, catalogVersion: item.catalogVersion })),
|
|
1426
1411
|
problems: pending.map((item) => `${item.id} is ${item.status}; run ${item.nextAction}`),
|
|
1427
1412
|
nextAction: pending[0]?.nextAction ?? null,
|
|
1428
1413
|
});
|
|
1429
1414
|
report.summary.unconfigured = (report.summary.unconfigured ?? 0) + (pending.length ? 1 : 0);
|
|
1430
1415
|
}
|
|
1416
|
+
const githubProfileResult = await localGithubProfileStatus(root);
|
|
1417
|
+
report.results.push(githubProfileResult);
|
|
1418
|
+
report.summary[githubProfileResult.status] = (report.summary[githubProfileResult.status] ?? 0) + 1;
|
|
1431
1419
|
const evidenceResult = await verifyEvidenceIndex();
|
|
1432
1420
|
report.results.push(evidenceResult);
|
|
1433
1421
|
report.summary[evidenceResult.status] = (report.summary[evidenceResult.status] ?? 0) + 1;
|
|
@@ -1574,6 +1562,11 @@ async function capabilities(args) {
|
|
|
1574
1562
|
const inputPath = args.includes('--input') ? args[args.indexOf('--input') + 1] : args.find((arg) => arg.startsWith('--input='))?.slice(8);
|
|
1575
1563
|
const format = formatFromArgs(args);
|
|
1576
1564
|
const planFile = args.includes('--plan-file') ? args[args.indexOf('--plan-file') + 1] : args.find((arg) => arg.startsWith('--plan-file='))?.slice(12);
|
|
1565
|
+
const isGithubPlan = action === 'apply' && planFile && JSON.parse(await readFile(resolve(root, planFile), 'utf8')).kind === 'github-profile';
|
|
1566
|
+
if ((id === 'github-profile' && ['plan', 'apply', 'verify'].includes(action)) || isGithubPlan) {
|
|
1567
|
+
if (provider !== 'github') throw new Error('The GitHub profile requires provider github.');
|
|
1568
|
+
return githubProfileCommand(args, { root, catalog: sdlcCapabilityCatalog() });
|
|
1569
|
+
}
|
|
1577
1570
|
if (['select', 'decline'].includes(action)) {
|
|
1578
1571
|
const catalog = sdlcCapabilityCatalog();
|
|
1579
1572
|
if (!catalog) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before selecting capabilities.');
|
|
@@ -1590,7 +1583,7 @@ async function capabilities(args) {
|
|
|
1590
1583
|
provider: capability.provider,
|
|
1591
1584
|
version: capability.version,
|
|
1592
1585
|
catalogVersion: catalog.package.version,
|
|
1593
|
-
nextAction: status === 'declined' ? null : `lighthouse capabilities plan --id ${id} --provider ${provider} --repo <owner/repo> --plan-file
|
|
1586
|
+
nextAction: status === 'declined' ? null : `lighthouse capabilities plan --id ${id} --provider ${provider} --repo <owner/repo> --plan-file ${id === 'github-profile' ? '<temporary-plan-path>' : `.lighthouse/reconciliation/${id}.plan.json`}`,
|
|
1594
1587
|
};
|
|
1595
1588
|
await writeTextAtomically(upgradeStatePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
1596
1589
|
const result = { schemaVersion: 1, capability: id, provider, status, applied: true };
|
|
@@ -1669,7 +1662,7 @@ async function capabilities(args) {
|
|
|
1669
1662
|
if (format === 'json') console.log(JSON.stringify(result, null, 2)); else process.stdout.write(capabilityPlanText(result));
|
|
1670
1663
|
return;
|
|
1671
1664
|
}
|
|
1672
|
-
throw new Error('Usage: lighthouse capabilities <select|decline|plan|apply> ...');
|
|
1665
|
+
throw new Error('Usage: lighthouse capabilities <select|decline|plan|apply|verify> ...');
|
|
1673
1666
|
}
|
|
1674
1667
|
|
|
1675
1668
|
function formatFromArgs(args) {
|
|
@@ -1689,7 +1682,7 @@ if (command === 'create-dotnet') {
|
|
|
1689
1682
|
let invalidArgument = false;
|
|
1690
1683
|
for (let index = 0; index < commandArgs.length; index += 1) {
|
|
1691
1684
|
const arg = commandArgs[index];
|
|
1692
|
-
if (command === 'capabilities' && index === 0 && ['select', 'decline', 'plan', 'apply'].includes(arg)) continue;
|
|
1685
|
+
if (command === 'capabilities' && index === 0 && ['select', 'decline', 'plan', 'apply', 'verify'].includes(arg)) continue;
|
|
1693
1686
|
if (arg === '--dry-run') continue;
|
|
1694
1687
|
if (arg === '--apply') { if (!['init', 'authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
|
|
1695
1688
|
if (arg === '--adopt-existing') { if (command !== 'authorities') invalidArgument = true; continue; }
|