@juangcarmona/lighthouse 0.9.2 → 0.10.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 +4 -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 +1 -0
- package/src/github-profile.mjs +84 -0
- package/src/lighthouse.mjs +15 -6
package/README.md
CHANGED
|
@@ -72,3 +72,7 @@ Only `passed` and `not-applicable` satisfy a required guardrail; `pending-review
|
|
|
72
72
|
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
73
|
|
|
74
74
|
A new deterministic checker mechanism or a new engineering practice is outside this catalog and requires a separate Product Change.
|
|
75
|
+
|
|
76
|
+
## Optional GitHub repository profile
|
|
77
|
+
|
|
78
|
+
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.10.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.11.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
|
@@ -93,6 +93,7 @@ try {
|
|
|
93
93
|
|
|
94
94
|
await copy(resolve(packageRoot, 'src/lighthouse.mjs'), resolve(stage, 'scripts/lighthouse.mjs'));
|
|
95
95
|
await copy(resolve(packageRoot, 'src/aliases.mjs'), resolve(stage, 'scripts/aliases.mjs'));
|
|
96
|
+
await copy(resolve(packageRoot, 'src/github-profile.mjs'), resolve(stage, 'scripts/github-profile.mjs'));
|
|
96
97
|
await copy(resolve(packageRoot, 'src/init-plan.mjs'), resolve(stage, 'scripts/init-plan.mjs'));
|
|
97
98
|
await copy(resolve(packageRoot, 'src/guardrails.mjs'), resolve(stage, 'scripts/guardrails.mjs'));
|
|
98
99
|
await copy(resolve(packageRoot, 'src/profiles.mjs'), resolve(stage, 'scripts/profiles.mjs'));
|
|
@@ -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;
|
|
@@ -271,7 +272,7 @@ function capabilityAdoption(previous = null) {
|
|
|
271
272
|
? `lighthouse capabilities select --id ${capability.id} --provider ${capability.provider}`
|
|
272
273
|
: ['adopted', 'declined'].includes(status)
|
|
273
274
|
? null
|
|
274
|
-
: `lighthouse capabilities plan --id ${capability.id} --provider ${capability.provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${capability.id}.plan.json`;
|
|
275
|
+
: `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
276
|
return [capability.id, {
|
|
276
277
|
id: capability.id,
|
|
277
278
|
status,
|
|
@@ -1408,7 +1409,7 @@ async function verify(manifest, format = 'text') {
|
|
|
1408
1409
|
for (const item of adoption) {
|
|
1409
1410
|
if (item.status !== 'adopted' || !item.recordPath || await exists(resolve(root, item.recordPath))) continue;
|
|
1410
1411
|
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`;
|
|
1412
|
+
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
1413
|
}
|
|
1413
1414
|
if (adoption.length) {
|
|
1414
1415
|
const pending = adoption.filter((item) => !['adopted', 'declined', 'available'].includes(item.status));
|
|
@@ -1421,13 +1422,16 @@ async function verify(manifest, format = 'text') {
|
|
|
1421
1422
|
kind: 'hard',
|
|
1422
1423
|
verification: 'deterministic',
|
|
1423
1424
|
status: pending.length ? 'unconfigured' : available || declined ? 'not-applicable' : 'passed',
|
|
1424
|
-
adoptionStatus: pending.length ? pending[0].status :
|
|
1425
|
+
adoptionStatus: pending.length ? pending[0].status : declined ? 'declined' : available ? 'available' : 'adopted',
|
|
1425
1426
|
evidence: adoption.map((item) => ({ capability: item.id, catalogVersion: item.catalogVersion })),
|
|
1426
1427
|
problems: pending.map((item) => `${item.id} is ${item.status}; run ${item.nextAction}`),
|
|
1427
1428
|
nextAction: pending[0]?.nextAction ?? null,
|
|
1428
1429
|
});
|
|
1429
1430
|
report.summary.unconfigured = (report.summary.unconfigured ?? 0) + (pending.length ? 1 : 0);
|
|
1430
1431
|
}
|
|
1432
|
+
const githubProfileResult = await localGithubProfileStatus(root);
|
|
1433
|
+
report.results.push(githubProfileResult);
|
|
1434
|
+
report.summary[githubProfileResult.status] = (report.summary[githubProfileResult.status] ?? 0) + 1;
|
|
1431
1435
|
const evidenceResult = await verifyEvidenceIndex();
|
|
1432
1436
|
report.results.push(evidenceResult);
|
|
1433
1437
|
report.summary[evidenceResult.status] = (report.summary[evidenceResult.status] ?? 0) + 1;
|
|
@@ -1574,6 +1578,11 @@ async function capabilities(args) {
|
|
|
1574
1578
|
const inputPath = args.includes('--input') ? args[args.indexOf('--input') + 1] : args.find((arg) => arg.startsWith('--input='))?.slice(8);
|
|
1575
1579
|
const format = formatFromArgs(args);
|
|
1576
1580
|
const planFile = args.includes('--plan-file') ? args[args.indexOf('--plan-file') + 1] : args.find((arg) => arg.startsWith('--plan-file='))?.slice(12);
|
|
1581
|
+
const isGithubPlan = action === 'apply' && planFile && JSON.parse(await readFile(resolve(root, planFile), 'utf8')).kind === 'github-profile';
|
|
1582
|
+
if ((id === 'github-profile' && ['plan', 'apply', 'verify'].includes(action)) || isGithubPlan) {
|
|
1583
|
+
if (provider !== 'github') throw new Error('The GitHub profile requires provider github.');
|
|
1584
|
+
return githubProfileCommand(args, { root, catalog: sdlcCapabilityCatalog() });
|
|
1585
|
+
}
|
|
1577
1586
|
if (['select', 'decline'].includes(action)) {
|
|
1578
1587
|
const catalog = sdlcCapabilityCatalog();
|
|
1579
1588
|
if (!catalog) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before selecting capabilities.');
|
|
@@ -1590,7 +1599,7 @@ async function capabilities(args) {
|
|
|
1590
1599
|
provider: capability.provider,
|
|
1591
1600
|
version: capability.version,
|
|
1592
1601
|
catalogVersion: catalog.package.version,
|
|
1593
|
-
nextAction: status === 'declined' ? null : `lighthouse capabilities plan --id ${id} --provider ${provider} --repo <owner/repo> --plan-file
|
|
1602
|
+
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
1603
|
};
|
|
1595
1604
|
await writeTextAtomically(upgradeStatePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
1596
1605
|
const result = { schemaVersion: 1, capability: id, provider, status, applied: true };
|
|
@@ -1669,7 +1678,7 @@ async function capabilities(args) {
|
|
|
1669
1678
|
if (format === 'json') console.log(JSON.stringify(result, null, 2)); else process.stdout.write(capabilityPlanText(result));
|
|
1670
1679
|
return;
|
|
1671
1680
|
}
|
|
1672
|
-
throw new Error('Usage: lighthouse capabilities <select|decline|plan|apply> ...');
|
|
1681
|
+
throw new Error('Usage: lighthouse capabilities <select|decline|plan|apply|verify> ...');
|
|
1673
1682
|
}
|
|
1674
1683
|
|
|
1675
1684
|
function formatFromArgs(args) {
|
|
@@ -1689,7 +1698,7 @@ if (command === 'create-dotnet') {
|
|
|
1689
1698
|
let invalidArgument = false;
|
|
1690
1699
|
for (let index = 0; index < commandArgs.length; index += 1) {
|
|
1691
1700
|
const arg = commandArgs[index];
|
|
1692
|
-
if (command === 'capabilities' && index === 0 && ['select', 'decline', 'plan', 'apply'].includes(arg)) continue;
|
|
1701
|
+
if (command === 'capabilities' && index === 0 && ['select', 'decline', 'plan', 'apply', 'verify'].includes(arg)) continue;
|
|
1693
1702
|
if (arg === '--dry-run') continue;
|
|
1694
1703
|
if (arg === '--apply') { if (!['init', 'authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
|
|
1695
1704
|
if (arg === '--adopt-existing') { if (command !== 'authorities') invalidArgument = true; continue; }
|