@starci/hfs 4.4.1 → 4.5.1
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/CHANGELOG.md +8 -0
- package/ci/cli.mjs +64 -0
- package/ci/github.mjs +41 -0
- package/ci/secret.mjs +28 -0
- package/ci/vars.mjs +33 -0
- package/ci/verify.mjs +36 -0
- package/db/backup-receipt.mjs +129 -0
- package/db/dump.mjs +66 -0
- package/db/migrate.mjs +4 -1
- package/db/reset.mjs +125 -0
- package/emit/db-types.mjs +98 -5
- package/lint/run.mjs +28 -6
- package/package.json +2 -1
- package/release/ci-red.mjs +70 -0
- package/release/gate.mjs +98 -27
- package/release/index.mjs +6 -4
- package/release/push.mjs +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +5 -5
- package/runtime/knowledge/hfs/rules.yaml +95 -4
- package/runtime/modules/kernel/failure-codes.yaml +60 -0
- package/runtime/modules/schemas/application-stacks.schema.yaml +134 -1
- package/runtime/packages/cli/src/catalog.generated.mjs +243 -20
- package/runtime/scripts/hfs/ci-declaration.mjs +102 -0
- package/runtime/scripts/hfs/rules/a15-findings.mjs +9 -0
- package/runtime/scripts/hfs/rules/app-workflow-findings.mjs +10 -0
- package/runtime/scripts/hfs/rules/ci-custody.mjs +104 -0
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +6 -19
- package/runtime/scripts/hfs/rules/fe-build-args.mjs +142 -0
- package/runtime/scripts/hfs/rules/supply-chain.mjs +40 -3
- package/runtime/scripts/hfs/rules/workflow-contexts.mjs +103 -0
- package/runtime/scripts/lib/secret-patterns.mjs +2 -2
- package/src/main.mjs +17 -6
- package/stack/context.mjs +13 -1
- package/stack/docker-transport.mjs +93 -15
- package/stack/release-store.mjs +12 -7
- package/stack/remote.mjs +14 -6
- package/stack/ssh-session.mjs +178 -0
- package/sync/ci-commands.mjs +205 -1
- package/sync/index.mjs +42 -16
- package/sync/quality-services.mjs +61 -0
- package/sync/readme-badges.mjs +44 -0
- package/sync/stack-images.mjs +29 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +33 -25
- package/templates/app/ci-workflows/github/workflows/e2e.yml +1 -0
- package/templates/app/ci-workflows/github/workflows/images.yml +25 -2
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +8 -2
- package/templates/app/ci-workflows/install-steps.yml +23 -0
- package/templates/app/ci-workflows/jest-cache.yml +8 -0
- package/templates/app/ci-workflows/lint-base.yml +16 -0
- package/templates/app/ci-workflows/sonar-steps.yml +3 -9
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +36 -36
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -1
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +25 -2
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +8 -2
- package/templates/app/gitignore +2 -0
- package/templates/app/gitignore-lite +2 -0
- package/templates/app/quality-config/codecov.yml +6 -6
- package/templates/app/quality-config/sonar-project.properties +1 -0
- package/templates/app/readme-badges/README.md +1 -0
- package/templates/app/skeleton/.github/dependabot.yml +1 -14
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +62 -3
- package/templates/app/skeleton/README.md +6 -0
- package/templates/app/skeleton-lite/.github/dependabot.yml +1 -14
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +58 -3
- package/templates/app/skeleton-lite/README.md +6 -0
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +53 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GENERATED by scripts/cli/gen-catalog.mjs — do not edit
|
|
2
2
|
export const CATALOG = {
|
|
3
3
|
"schema": "starci/cli-catalog@1",
|
|
4
|
-
"hash": "
|
|
4
|
+
"hash": "d230745d3aa5cfe65f75cb5fb2872924b935f1b4610c0b72db2f729572386404",
|
|
5
5
|
"global": [
|
|
6
6
|
{
|
|
7
7
|
"name": "json",
|
|
@@ -176,8 +176,91 @@ export const CATALOG = {
|
|
|
176
176
|
"impl": null,
|
|
177
177
|
"runtimeSide": "host"
|
|
178
178
|
},
|
|
179
|
+
"ci": {
|
|
180
|
+
"summary": "set declared GitHub CI secrets and variables and verify their custody",
|
|
181
|
+
"owner": "@starci/hfs",
|
|
182
|
+
"positional": [
|
|
183
|
+
{
|
|
184
|
+
"name": "action",
|
|
185
|
+
"enum": [
|
|
186
|
+
"secret",
|
|
187
|
+
"var",
|
|
188
|
+
"verify"
|
|
189
|
+
],
|
|
190
|
+
"required": true,
|
|
191
|
+
"variadic": false
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"name": "operation",
|
|
195
|
+
"enum": [
|
|
196
|
+
"set",
|
|
197
|
+
"sync"
|
|
198
|
+
],
|
|
199
|
+
"required": false,
|
|
200
|
+
"variadic": false
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"name": "name",
|
|
204
|
+
"required": false,
|
|
205
|
+
"variadic": false
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
"flags": [
|
|
209
|
+
{
|
|
210
|
+
"name": "env",
|
|
211
|
+
"type": "string",
|
|
212
|
+
"summary": "select the declared GitHub environment"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"name": "dependabot",
|
|
216
|
+
"type": "boolean",
|
|
217
|
+
"summary": "select the Dependabot secret store"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"name": "repo",
|
|
221
|
+
"type": "string",
|
|
222
|
+
"summary": "override the app repository with owner/name"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"name": "value",
|
|
226
|
+
"type": "string",
|
|
227
|
+
"summary": "public variable value that must agree with the manifest"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"name": "execute",
|
|
231
|
+
"type": "boolean",
|
|
232
|
+
"summary": "apply the variable sync plan"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"name": "json",
|
|
236
|
+
"type": "boolean",
|
|
237
|
+
"summary": "print verification findings as JSON"
|
|
238
|
+
}
|
|
239
|
+
],
|
|
240
|
+
"exit": {
|
|
241
|
+
"0": "action completed or verification clean",
|
|
242
|
+
"1": "verification findings",
|
|
243
|
+
"2": "refusal or bad usage"
|
|
244
|
+
},
|
|
245
|
+
"json": "flag",
|
|
246
|
+
"examples": [
|
|
247
|
+
"starci app ci secret set SONAR_TOKEN",
|
|
248
|
+
"starci app ci secret set VPS_SSH_KEY --env production",
|
|
249
|
+
"starci app ci secret set NPM_TOKEN --dependabot",
|
|
250
|
+
"starci app ci var set SONAR_HOST_URL",
|
|
251
|
+
"starci app ci var sync",
|
|
252
|
+
"starci app ci var sync --execute",
|
|
253
|
+
"starci app ci verify --json"
|
|
254
|
+
],
|
|
255
|
+
"editions": [
|
|
256
|
+
"full",
|
|
257
|
+
"lite"
|
|
258
|
+
],
|
|
259
|
+
"impl": null,
|
|
260
|
+
"runtimeSide": "host"
|
|
261
|
+
},
|
|
179
262
|
"db": {
|
|
180
|
-
"summary": "preview or apply
|
|
263
|
+
"summary": "preview or apply database migrations, encrypted backups and guarded linked resets",
|
|
181
264
|
"owner": "@starci/hfs",
|
|
182
265
|
"positional": [
|
|
183
266
|
{
|
|
@@ -187,13 +270,40 @@ export const CATALOG = {
|
|
|
187
270
|
"restamp",
|
|
188
271
|
"plan",
|
|
189
272
|
"migrate",
|
|
190
|
-
"post-cutover"
|
|
273
|
+
"post-cutover",
|
|
274
|
+
"dump",
|
|
275
|
+
"reset"
|
|
191
276
|
],
|
|
192
277
|
"required": true,
|
|
193
278
|
"variadic": false
|
|
194
279
|
}
|
|
195
280
|
],
|
|
196
281
|
"flags": [
|
|
282
|
+
{
|
|
283
|
+
"name": "out",
|
|
284
|
+
"type": "string",
|
|
285
|
+
"summary": "dump: destination outside the app and every Git worktree"
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"name": "schema-only",
|
|
289
|
+
"type": "boolean",
|
|
290
|
+
"summary": "dump: omit data parts; this backup cannot authorize reset"
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"name": "auth",
|
|
294
|
+
"type": "string",
|
|
295
|
+
"summary": "reset: required keep or drop; drop deletes users and identities"
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"name": "storage",
|
|
299
|
+
"type": "string",
|
|
300
|
+
"summary": "reset: required keep or drop; drop empties objects metadata but does not delete backend files"
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"name": "backup",
|
|
304
|
+
"type": "string",
|
|
305
|
+
"summary": "reset: existing full encrypted backup receipt for this project, younger than database.reset.backupMaxAgeHours (default 24)"
|
|
306
|
+
},
|
|
197
307
|
{
|
|
198
308
|
"name": "stamp",
|
|
199
309
|
"type": "string",
|
|
@@ -217,17 +327,17 @@ export const CATALOG = {
|
|
|
217
327
|
{
|
|
218
328
|
"name": "execute",
|
|
219
329
|
"type": "boolean",
|
|
220
|
-
"summary": "migrate, post-cutover: apply the plan (also needs STARCI_DB_ARMED=1);
|
|
330
|
+
"summary": "migrate, post-cutover, reset: apply the plan (also needs STARCI_DB_ARMED=1); otherwise read-only"
|
|
221
331
|
},
|
|
222
332
|
{
|
|
223
333
|
"name": "linked",
|
|
224
334
|
"type": "boolean",
|
|
225
|
-
"summary": "
|
|
335
|
+
"summary": "use the linked project; required explicitly for reset and one of the dump target choices"
|
|
226
336
|
},
|
|
227
337
|
{
|
|
228
338
|
"name": "url-env",
|
|
229
339
|
"type": "string",
|
|
230
|
-
"summary": "migrate, post-cutover:
|
|
340
|
+
"summary": "migrate, post-cutover, dump: environment variable holding the connection string"
|
|
231
341
|
},
|
|
232
342
|
{
|
|
233
343
|
"name": "green-at",
|
|
@@ -242,16 +352,19 @@ export const CATALOG = {
|
|
|
242
352
|
{
|
|
243
353
|
"name": "json",
|
|
244
354
|
"type": "boolean",
|
|
245
|
-
"summary": "plan, migrate, post-cutover: print the result as JSON"
|
|
355
|
+
"summary": "plan, migrate, post-cutover, dump, reset: print the result as JSON"
|
|
246
356
|
}
|
|
247
357
|
],
|
|
248
358
|
"exit": {
|
|
249
359
|
"0": "preview or action completed with no drift",
|
|
250
|
-
"1": "database plan drift",
|
|
251
|
-
"2": "refusal or bad usage"
|
|
360
|
+
"1": "database plan drift or failed tool/verification",
|
|
361
|
+
"2": "refusal or bad usage",
|
|
362
|
+
"3": "database mutation is not armed"
|
|
252
363
|
},
|
|
253
364
|
"json": "flag",
|
|
254
365
|
"examples": [
|
|
366
|
+
"starci app db dump --linked --out /var/backups/my-app",
|
|
367
|
+
"starci app db reset --linked --auth keep --storage keep --backup /var/backups/my-app/backup.receipt.json",
|
|
255
368
|
"starci app db baseline",
|
|
256
369
|
"starci app db baseline --stamp 20261014000000 --apply",
|
|
257
370
|
"starci app db restamp --map restamp.yaml",
|
|
@@ -267,6 +380,17 @@ export const CATALOG = {
|
|
|
267
380
|
],
|
|
268
381
|
"impl": null,
|
|
269
382
|
"conventions": [
|
|
383
|
+
"Owner ruling 2026-10-17 alpha.20: \"add a canon DB reset/backup verb design ...",
|
|
384
|
+
"at least starci app db dump (encrypted, outside repo) and starci app db reset --linked with explicit auth/storage behaviour flags\"",
|
|
385
|
+
"Owner ruling main 2026-10-17: reset \"must require --backup <receipt> pointing at an existing encrypted dump made in the last N hours,",
|
|
386
|
+
"and must refuse without explicit --auth/--storage choices\"",
|
|
387
|
+
"dump requires exactly one target; archives roles, schema, auth/storage schema, data and rowcounts;",
|
|
388
|
+
"age encrypts to .sops.yaml recipients and deletes plaintext even on failure",
|
|
389
|
+
"dump verifies age archive header, size and sha256; owner identity is required for a separate decryption proof",
|
|
390
|
+
"reset is linked only, dry run by default, and requires STARCI_DB_ARMED=1 with --execute; local reset stays npm run db:reset",
|
|
391
|
+
"reset keep encrypts auth/storage data before reset, proves identity access, restores transactionally and verifies fingerprints and migration head;",
|
|
392
|
+
"requires Supabase db query --file and SOPS_AGE_KEY or SOPS_AGE_KEY_FILE",
|
|
393
|
+
"reset keeps encrypted temporary recovery parts after a reset/restore failure and reports their directory; plaintext is always deleted",
|
|
270
394
|
"baseline accepts --stamp; restamp requires --map; both preview without --apply",
|
|
271
395
|
"plan is always read-only and supports --json; it compares migrations, cron, queues and Vault names",
|
|
272
396
|
"migrate and post-cutover read the optional database block of application-stacks.yaml; both are dry runs without --execute",
|
|
@@ -355,6 +479,15 @@ export const CATALOG = {
|
|
|
355
479
|
"type": "string",
|
|
356
480
|
"summary": "one frontend workspace to judge"
|
|
357
481
|
},
|
|
482
|
+
{
|
|
483
|
+
"name": "side",
|
|
484
|
+
"type": "enum",
|
|
485
|
+
"enum": [
|
|
486
|
+
"be",
|
|
487
|
+
"fe"
|
|
488
|
+
],
|
|
489
|
+
"summary": "judge one side (root findings belong to be; not with --sonar)"
|
|
490
|
+
},
|
|
358
491
|
{
|
|
359
492
|
"name": "fix",
|
|
360
493
|
"type": "boolean",
|
|
@@ -467,6 +600,11 @@ export const CATALOG = {
|
|
|
467
600
|
"type": "string",
|
|
468
601
|
"summary": "gate: release tag prefix (default from release.tagPrefix, v)"
|
|
469
602
|
},
|
|
603
|
+
{
|
|
604
|
+
"name": "sonar",
|
|
605
|
+
"type": "boolean",
|
|
606
|
+
"summary": "gate: append a local Sonar scan using services.sonar host and the token environment variable named by ci.token (default SONAR_TOKEN)"
|
|
607
|
+
},
|
|
470
608
|
{
|
|
471
609
|
"name": "remote",
|
|
472
610
|
"type": "string",
|
|
@@ -481,12 +619,13 @@ export const CATALOG = {
|
|
|
481
619
|
"exit": {
|
|
482
620
|
"0": "gate green or push done or nothing to do",
|
|
483
621
|
"1": "a gate is red or the push was refused by the hook",
|
|
484
|
-
"2": "refusal or bad usage"
|
|
622
|
+
"2": "refusal or bad usage including RELEASE_CI_RED and RELEASE_SONAR_UNAVAILABLE"
|
|
485
623
|
},
|
|
486
624
|
"json": "flag",
|
|
487
625
|
"examples": [
|
|
488
626
|
"starci app release gate",
|
|
489
627
|
"starci app release gate --execute",
|
|
628
|
+
"starci app release gate --sonar --execute",
|
|
490
629
|
"starci app release push --remote origin",
|
|
491
630
|
"starci app release push --execute"
|
|
492
631
|
],
|
|
@@ -496,7 +635,16 @@ export const CATALOG = {
|
|
|
496
635
|
],
|
|
497
636
|
"impl": null,
|
|
498
637
|
"conventions": [
|
|
499
|
-
"the gate list is release.gates of application-stacks.yaml (argument arrays), else
|
|
638
|
+
"the gate list is release.gates of application-stacks.yaml (argument arrays), else app-check, audit, then the managed package scripts",
|
|
639
|
+
"audit runs npm audit --omit=dev --audit-level=high by default; services.audit.level and omitDev configure it,",
|
|
640
|
+
"and an explicit audit.in without release-gate omits it;",
|
|
641
|
+
"declared release.gates gets audit appended if missing",
|
|
642
|
+
"--sonar appends @sonar/scan@4.3.0 against services.sonar.host (host.public for a host mapping), plus organization for sonarcloud;",
|
|
643
|
+
"disabled or unavailable Sonar or an unset token refuses RELEASE_SONAR_UNAVAILABLE",
|
|
644
|
+
"gate checks the newest completed ci.yml run on the default branch, including reachable push/tag runs;",
|
|
645
|
+
"failure, cancelled or timed_out refuses RELEASE_CI_RED with its URL and SHA;",
|
|
646
|
+
"unreadable GitHub or no run warns RELEASE_CI_UNKNOWN and continues",
|
|
647
|
+
"only the owner's starci release ci-override --sha <sha> --reason <text> record can override red CI; there is no --no-ci-check flag",
|
|
500
648
|
"gate refuses a dirty tree, HEAD other than --source, a moved HEAD and a tree dirtied by the gates; record and tag only when all gates are green",
|
|
501
649
|
"a gate with summary jest is red on failed tests and on a coverage threshold miss even when the process exits 0",
|
|
502
650
|
"push needs the annotated tag and L4 record of HEAD, runs git push --dry-run --atomic first and never uses --no-verify"
|
|
@@ -2201,7 +2349,7 @@ export const CATALOG = {
|
|
|
2201
2349
|
{
|
|
2202
2350
|
"name": "public",
|
|
2203
2351
|
"type": "boolean",
|
|
2204
|
-
"summary": "up or stop:
|
|
2352
|
+
"summary": "up or stop: activate profile public; up verifies the tunnel is running (needs CLOUDFLARE_TUNNEL_TOKEN)"
|
|
2205
2353
|
}
|
|
2206
2354
|
],
|
|
2207
2355
|
"exit": {
|
|
@@ -2223,6 +2371,12 @@ export const CATALOG = {
|
|
|
2223
2371
|
"impl": {
|
|
2224
2372
|
"script": "scripts/gates/sonar-local.mjs"
|
|
2225
2373
|
},
|
|
2374
|
+
"conventions": [
|
|
2375
|
+
"up --public retries the tunnel running check for up to 30 seconds; SONAR_TUNNEL_NOT_RUNNING refuses with container state and the last 20 scrubbed log lines",
|
|
2376
|
+
"SONAR_DB_PASSWORD_MISMATCH: the starci-sonarqube-postgres-data volume has another password than SONARQUBE_DB_PASSWORD in secret.env",
|
|
2377
|
+
"(Compose supplies SONAR_JDBC_PASSWORD and POSTGRES_PASSWORD);",
|
|
2378
|
+
"restore the old password or export a backup before recreating the volume"
|
|
2379
|
+
],
|
|
2226
2380
|
"runtimeSide": "tree"
|
|
2227
2381
|
},
|
|
2228
2382
|
"starcistacks": {
|
|
@@ -7150,12 +7304,12 @@ export const CATALOG = {
|
|
|
7150
7304
|
"runtimeSide": "host"
|
|
7151
7305
|
},
|
|
7152
7306
|
"install": {
|
|
7153
|
-
"summary": "
|
|
7307
|
+
"summary": "install exact dependencies and move StarCi pins with the target CLI",
|
|
7154
7308
|
"owner": "runtime",
|
|
7155
7309
|
"positional": [
|
|
7156
7310
|
{
|
|
7157
7311
|
"name": "package",
|
|
7158
|
-
"required":
|
|
7312
|
+
"required": false,
|
|
7159
7313
|
"variadic": true
|
|
7160
7314
|
}
|
|
7161
7315
|
],
|
|
@@ -7164,17 +7318,24 @@ export const CATALOG = {
|
|
|
7164
7318
|
"name": "dev",
|
|
7165
7319
|
"type": "boolean",
|
|
7166
7320
|
"summary": "save the packages as development dependencies"
|
|
7321
|
+
},
|
|
7322
|
+
{
|
|
7323
|
+
"name": "canon",
|
|
7324
|
+
"type": "boolean",
|
|
7325
|
+
"summary": "install the target CLI and all of its exact StarCi canon pins"
|
|
7167
7326
|
}
|
|
7168
7327
|
],
|
|
7169
7328
|
"exit": {
|
|
7170
|
-
"0": "exact dependencies were installed",
|
|
7171
|
-
"1": "npm, Git or the host lock failed",
|
|
7172
|
-
"2": "the package or worktree was refused"
|
|
7329
|
+
"0": "exact dependencies and any requested canon pins were installed",
|
|
7330
|
+
"1": "npm, Git, target canon loading or the host lock failed",
|
|
7331
|
+
"2": "the package, target pin or worktree was refused"
|
|
7173
7332
|
},
|
|
7174
7333
|
"json": "starci/npm-install@1",
|
|
7175
7334
|
"examples": [
|
|
7176
7335
|
"starci npm install typescript",
|
|
7177
|
-
"starci npm install left-pad@1.3.0 --dev"
|
|
7336
|
+
"starci npm install left-pad@1.3.0 --dev",
|
|
7337
|
+
"starci npm install @starci/cli --dev",
|
|
7338
|
+
"starci npm install --canon --dev"
|
|
7178
7339
|
],
|
|
7179
7340
|
"editions": [
|
|
7180
7341
|
"full"
|
|
@@ -7190,7 +7351,12 @@ export const CATALOG = {
|
|
|
7190
7351
|
],
|
|
7191
7352
|
"conventions": [
|
|
7192
7353
|
"bare package names resolve only from knowledge/hfs/canon-pins.yaml and never through a registry lookup",
|
|
7193
|
-
"explicit versions use exact x.y.z form and must agree with the canon pin when one exists",
|
|
7354
|
+
"explicit versions use exact x.y.z form and must agree with the canon pin when one exists, except the target CLI version on a canon upgrade",
|
|
7355
|
+
"naming @starci/cli or passing --canon installs the exact target CLI first,",
|
|
7356
|
+
"then reads its installed HFS runtime canon-pins.yaml and installs every pinned @starci package in the same host lock",
|
|
7357
|
+
"--canon without a CLI positional uses the running runtime's exact CLI pin as the target",
|
|
7358
|
+
"target pins never fall back to the running runtime when the target pin file is missing or inconsistent",
|
|
7359
|
+
"each target StarCi pin is reported as moved or refused with a reason; a failed canon phase exits nonzero even when the first CLI install succeeded",
|
|
7194
7360
|
"installs always use --save-exact and report the package.json and package-lock.json diff stat"
|
|
7195
7361
|
],
|
|
7196
7362
|
"runtimeSide": "host"
|
|
@@ -7609,6 +7775,53 @@ export const CATALOG = {
|
|
|
7609
7775
|
},
|
|
7610
7776
|
"runtimeSide": "tree"
|
|
7611
7777
|
},
|
|
7778
|
+
"ci-override": {
|
|
7779
|
+
"summary": "the owner accepts red CI for one SHA and records who and why",
|
|
7780
|
+
"owner": "runtime",
|
|
7781
|
+
"flags": [
|
|
7782
|
+
{
|
|
7783
|
+
"name": "sha",
|
|
7784
|
+
"type": "string",
|
|
7785
|
+
"summary": "required full 40 lowercase hex SHA of the red CI run"
|
|
7786
|
+
},
|
|
7787
|
+
{
|
|
7788
|
+
"name": "reason",
|
|
7789
|
+
"type": "string",
|
|
7790
|
+
"summary": "required nonempty owner reason, recorded verbatim after trimming"
|
|
7791
|
+
},
|
|
7792
|
+
{
|
|
7793
|
+
"name": "repo",
|
|
7794
|
+
"type": "string",
|
|
7795
|
+
"summary": "repository receiving the override (default current directory)"
|
|
7796
|
+
}
|
|
7797
|
+
],
|
|
7798
|
+
"exit": {
|
|
7799
|
+
"0": "the override record and runtime owner ruling were written",
|
|
7800
|
+
"2": "RIGHTS_OWNER_ONLY (a bound role or non-TTY stdin), bad usage, confirmation mismatch, or write failure"
|
|
7801
|
+
},
|
|
7802
|
+
"json": "none",
|
|
7803
|
+
"examples": [
|
|
7804
|
+
"starci release ci-override --sha 0123456789abcdef0123456789abcdef01234567 --reason \"Owner accepts this CI failure for the release\"",
|
|
7805
|
+
"starci release ci-override --repo ../my-app --sha 0123456789abcdef0123456789abcdef01234567 --reason \"Owner accepts the failed run\""
|
|
7806
|
+
],
|
|
7807
|
+
"editions": [
|
|
7808
|
+
"full"
|
|
7809
|
+
],
|
|
7810
|
+
"impl": {
|
|
7811
|
+
"script": "scripts/supervisor/release-ci-override-cli.mjs"
|
|
7812
|
+
},
|
|
7813
|
+
"conventions": [
|
|
7814
|
+
"OWNER RULING 2026-10-17 (chat to main, relayed): alpha.20 \"cut refuses when the latest CI on main is red, with a documented owner override verb\";",
|
|
7815
|
+
"the override is owner-only, records who and why in the owner rulings, and agents cannot run it.",
|
|
7816
|
+
"Every bound role is refused by the command guard and by this script. The owner must use a terminal and type the first 12 SHA characters exactly.",
|
|
7817
|
+
"Writes <git-common-dir>/starci-release/<sha>.ci-override.json, schema starci/ci-override@1,",
|
|
7818
|
+
"with sha, reason, by (git user.name and OS user), at, and runUrl (empty if GitHub is unavailable).",
|
|
7819
|
+
"For the runtime, appends ci-override-<sha12> under rulings before the top-level supersedes key in modules/kernel/owner-rulings.yaml.",
|
|
7820
|
+
"Commit that entry into the tree being cut. App repositories receive only the record.",
|
|
7821
|
+
"An override applies only to its exact SHA. It does not permit a cut when CI is unknown. Existing ruling IDs with another reason refuse."
|
|
7822
|
+
],
|
|
7823
|
+
"runtimeSide": "tree"
|
|
7824
|
+
},
|
|
7612
7825
|
"ci-status": {
|
|
7613
7826
|
"summary": "the verdict of the GitHub ci workflow, Codecov and Sonar for a release; red is recorded for Debug",
|
|
7614
7827
|
"owner": "runtime",
|
|
@@ -7728,7 +7941,7 @@ export const CATALOG = {
|
|
|
7728
7941
|
],
|
|
7729
7942
|
"exit": {
|
|
7730
7943
|
"0": "the release was cut and main plus its tag were pushed atomically, or --plan found nothing missing",
|
|
7731
|
-
"1": "refused (dirty tree, no or wrong tag, release notes incomplete, version not moved past the remote main, suite red, main moved, secret scan) and nothing was pushed",
|
|
7944
|
+
"1": "refused (RELEASE_CI_RED, RELEASE_CI_UNKNOWN, dirty tree, no or wrong tag, release notes incomplete, version not moved past the remote main, suite red, main moved, secret scan) and nothing was pushed",
|
|
7732
7945
|
"2": "bad usage"
|
|
7733
7946
|
},
|
|
7734
7947
|
"json": "flag",
|
|
@@ -7744,6 +7957,16 @@ export const CATALOG = {
|
|
|
7744
7957
|
"impl": {
|
|
7745
7958
|
"script": "scripts/supervisor/release-cut-cli.mjs"
|
|
7746
7959
|
},
|
|
7960
|
+
"conventions": [
|
|
7961
|
+
"OWNER RULING 2026-10-17 (chat to main, relayed): alpha.20 \"cut refuses when the latest CI on main is red, with a documented owner override verb\";",
|
|
7962
|
+
"the override is owner-only, records who and why in the owner rulings, and agents cannot run it.",
|
|
7963
|
+
"Before the suite, including --plan, read the newest completed CI workflow run on main.",
|
|
7964
|
+
"Only success continues; every other conclusion refuses RELEASE_CI_RED and names the run URL and headSha.",
|
|
7965
|
+
"Unreadable GitHub or no completed run refuses RELEASE_CI_UNKNOWN: a cut must not proceed blind, including under suite ci.",
|
|
7966
|
+
"The owner may run starci release ci-override --sha <headSha> --reason <text>.",
|
|
7967
|
+
"A red run requires both starci-release/<headSha>.ci-override.json in the git common dir and ci-override-<sha12>",
|
|
7968
|
+
"in modules/kernel/owner-rulings.yaml of the tree being cut. Commit the ruling before cutting."
|
|
7969
|
+
],
|
|
7747
7970
|
"runtimeSide": "tree"
|
|
7748
7971
|
},
|
|
7749
7972
|
"images": {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { list } from '../lib/list.mjs';
|
|
3
|
+
import { parseYaml } from '../../engine/yaml.mjs';
|
|
4
|
+
|
|
5
|
+
const nameOf = (item) => typeof item === 'string' ? item : item?.name;
|
|
6
|
+
|
|
7
|
+
/** Each CI name belongs to one class and scope; declarations contain names and public configuration only. */
|
|
8
|
+
export function readCiDeclaration(doc) {
|
|
9
|
+
const problems = [], seen = new Set();
|
|
10
|
+
const invalid = (at, message) => problems.push({ code: 'CI_DECLARATION_INVALID', at, message });
|
|
11
|
+
const normalize = (entries, at, dependabot = false, variable = false) => list(entries).map((item, index) => {
|
|
12
|
+
const where = `${at}[${index}]`, name = item?.name;
|
|
13
|
+
if (seen.has(name)) invalid(`${where}.name`, 'a name must appear once across all CI classes');
|
|
14
|
+
seen.add(name);
|
|
15
|
+
if (typeof name !== 'string' || !/^[A-Z][A-Z0-9_]*$/.test(name) || name.startsWith('GITHUB_')) invalid(`${where}.name`, 'use an uppercase CI name outside the reserved GITHUB_ namespace');
|
|
16
|
+
const scope = dependabot ? 'repository' : item?.scope;
|
|
17
|
+
if (!['repository', 'environment'].includes(scope)) invalid(`${where}.scope`, 'scope must be repository or environment');
|
|
18
|
+
if (scope === 'environment' && (typeof item?.environment !== 'string' || !/^[a-z][a-z0-9-]*$/.test(item.environment))) invalid(`${where}.environment`, 'environment scope requires an environment name');
|
|
19
|
+
if ((dependabot || scope !== 'environment') && item && Object.hasOwn(item, 'environment')) invalid(`${where}.environment`, 'only environment scope may name an environment');
|
|
20
|
+
return { name, scope, environment: item?.environment ?? null, purpose: item?.purpose ?? null,
|
|
21
|
+
optional: item?.optional === true, ...(variable ? { value: item?.value } : {}) };
|
|
22
|
+
});
|
|
23
|
+
const ci = doc?.ci;
|
|
24
|
+
const secrets = normalize(ci?.secrets, 'ci.secrets');
|
|
25
|
+
const vars = normalize(ci?.vars, 'ci.vars', false, true);
|
|
26
|
+
const dependabotSecrets = normalize(ci?.dependabot?.secrets, 'ci.dependabot.secrets', true);
|
|
27
|
+
return { secrets, vars, dependabotSecrets, problems };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Missing quality blocks preserve the existing rendered defaults; disabled services remain explicitly disabled. */
|
|
31
|
+
export function qualityServices(doc, { example = false } = {}) {
|
|
32
|
+
const services = doc?.services ?? {}, sonar = services.sonar, codecov = services.codecov ?? {};
|
|
33
|
+
const dependabot = services.dependabot ?? {}, audit = services.audit ?? {};
|
|
34
|
+
return {
|
|
35
|
+
sonar: !sonar || sonar.mode === 'disabled' ? null : {
|
|
36
|
+
provider: sonar.provider, mode: sonar.mode, optional: sonar.optional ?? sonar.provider === 'sonarqube', host: sonar.host?.public || sonar.host?.local || null,
|
|
37
|
+
organization: sonar.organization ?? null,
|
|
38
|
+
organizationVar: sonar.ci?.organizationVar ?? (sonar.provider === 'sonarcloud' && (example || list(sonar.runtimeSecrets).length > 0) && !list(sonar.credentials).length ? 'SONAR_ORGANIZATION' : null), projectKey: sonar.projects?.[0]?.key ?? null,
|
|
39
|
+
qualityGate: sonar.qualityGate ?? (example ? 'starci-quality-lite' : 'starci-quality'), wiring: sonar.ci?.wiring ?? 'required',
|
|
40
|
+
token: sonar.ci?.token || nameOf(sonar.ci?.secrets?.[0]) || 'SONAR_TOKEN', hostVar: sonar.ci?.hostVar || 'SONAR_HOST_URL',
|
|
41
|
+
},
|
|
42
|
+
codecov: { optional: codecov.optional ?? false, enabled: codecov.mode !== 'disabled', auth: codecov.auth ?? 'oidc', token: codecov.ci?.token ?? null,
|
|
43
|
+
flag: codecov.flag ?? null, target: codecov.target ?? 100, components: codecov.components ?? 'auto', required: codecov.required ?? true, wiring: codecov.ci?.wiring ?? 'required' },
|
|
44
|
+
dependabot: { ecosystems: [...(dependabot.ecosystems ?? ['npm', 'github-actions', 'docker'])],
|
|
45
|
+
schedule: { interval: 'weekly', ...dependabot.schedule }, groups: { minorPatch: 'grouped', major: 'per-package', ...dependabot.groups },
|
|
46
|
+
ignore: [...(dependabot.ignore ?? ['@starci/*'])], security: dependabot.security ?? 'immediate' },
|
|
47
|
+
audit: { level: audit.level ?? 'high', omitDev: audit.omitDev ?? true, in: [...(audit.in ?? ['release-gate', 'ci'])] },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Enumerate a missing directory as empty; all filesystem access uses the caller's injected synchronous seam. */
|
|
52
|
+
function entries(dir, fs) {
|
|
53
|
+
try { return fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)); }
|
|
54
|
+
catch (error) { if (error.code === 'ENOENT') return []; throw error; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Read only encrypted envelope key names, never decrypting or returning any sealed value. */
|
|
58
|
+
export function sealedKeyNames(root, { fs }) {
|
|
59
|
+
const out = [], base = path.join(root, '.starcistacks');
|
|
60
|
+
for (const env of entries(base, fs).filter((entry) => entry.isDirectory())) {
|
|
61
|
+
for (const member of entries(path.join(base, env.name, 'secrets'), fs).filter((entry) => entry.isFile() && entry.name.endsWith('.enc'))) {
|
|
62
|
+
const file = `.starcistacks/${env.name}/secrets/${member.name}`;
|
|
63
|
+
const source = fs.readFileSync(path.join(root, file), 'utf8');
|
|
64
|
+
const dotenv = [...source.matchAll(/^([A-Za-z_][A-Za-z0-9_]*)=ENC\[/gm)].map((match) => match[1]).filter((key) => !key.startsWith('sops_'));
|
|
65
|
+
let keys = dotenv;
|
|
66
|
+
if (!dotenv.length) {
|
|
67
|
+
const doc = parseYaml(source);
|
|
68
|
+
keys = doc && typeof doc === 'object' && !Array.isArray(doc) ? Object.keys(doc).filter((key) => key !== 'sops' && !key.startsWith('sops_')) : [];
|
|
69
|
+
}
|
|
70
|
+
for (const key of keys) out.push({ env: env.name, member: member.name.slice(0, -4), key, file });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Collect every static secrets and vars reference inside workflow expressions, excluding the automatic GitHub token. */
|
|
77
|
+
export function workflowReferences(root, { fs }) {
|
|
78
|
+
const out = [], dir = path.join(root, '.github', 'workflows');
|
|
79
|
+
for (const entry of entries(dir, fs).filter((item) => item.isFile() && /\.ya?ml$/i.test(item.name))) {
|
|
80
|
+
const file = `.github/workflows/${entry.name}`, source = fs.readFileSync(path.join(root, file), 'utf8');
|
|
81
|
+
for (const expression of source.matchAll(/\$\{\{([^}]*)/g)) {
|
|
82
|
+
for (const match of expression[1].matchAll(/\b(secrets|vars)\.([A-Z][A-Z0-9_]*)/g)) {
|
|
83
|
+
if (match[2] !== 'GITHUB_TOKEN') out.push({ file, kind: match[1], name: match[2] });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Service CI bindings name the shared declaration classes, including the readable legacy lists. */
|
|
91
|
+
export function ciNamesOfServices(doc) {
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const [service, entry] of Object.entries(doc?.services ?? {})) {
|
|
94
|
+
const ci = entry?.ci ?? {};
|
|
95
|
+
if (ci.token) out.push({ service, name: ci.token, kind: 'secret' });
|
|
96
|
+
if (ci.hostVar) out.push({ service, name: ci.hostVar, kind: 'var' });
|
|
97
|
+
if (ci.organizationVar) out.push({ service, name: ci.organizationVar, kind: 'var' });
|
|
98
|
+
for (const item of list(ci.secrets)) out.push({ service, name: nameOf(item), kind: 'secret' });
|
|
99
|
+
for (const item of list(ci.vars)) out.push({ service, name: nameOf(item), kind: 'var' });
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
@@ -32,17 +32,25 @@ import { stacksIngressFindings } from './stacks-ingress.mjs';
|
|
|
32
32
|
import { stacksLiteBackingFindings } from './stacks-lite-backing.mjs';
|
|
33
33
|
import { stackToolingFindings } from './stack-tooling.mjs';
|
|
34
34
|
import { stacksDeployFindings } from './stacks-deploy.mjs';
|
|
35
|
+
import { ciCustodyFindings } from './ci-custody.mjs';
|
|
36
|
+
import { workflowContextsFindings } from './workflow-contexts.mjs';
|
|
37
|
+
import { feBuildArgsFindings } from './fe-build-args.mjs';
|
|
35
38
|
// finders-import-end
|
|
36
39
|
|
|
37
40
|
/** The failure codes the finders above emit, so `starci app check` ships their labels (check.mjs adds them to CHECK_CODES). Append-only; the coordinator regenerates it at merge time. */
|
|
38
41
|
export const A15_CODES = [
|
|
39
42
|
// codes-begin
|
|
43
|
+
'FE_PUBLIC_BUILD_ARG_MISSING',
|
|
44
|
+
'HFS_WORKFLOW_CONTEXT',
|
|
45
|
+
'CI_SECRET_SEALED', 'APP_SECRET_IN_CI', 'CI_DECLARATION_DRIFT',
|
|
40
46
|
'BACKUP_PROOF_MISSING', 'BACKUP_UNDECLARED', 'BE_APP_BOOT_PROOF_MISSING', 'BE_APP_ENV_REQUIRED', 'BE_APP_MODULE_LIST_INLINE', 'BE_LITE_PROOF_LANE_MISSING', 'DB_API_EXPOSED', 'DB_ARCHIVE_MUTATED', 'DB_BASELINE_SHAPE', 'DB_CRON_OWNED', 'DB_EXTENSION_ALLOWED', 'DB_MIGRATION_DESTRUCTIVE', 'DB_PGMQ_DECLARED', 'DB_VAULT_WRITE', 'ENV_DUPLICATE', 'ENV_EXAMPLE_DRIFT', 'ENV_PUBLIC_SEALED', 'ENV_REQUIRED_UNSET', 'ENV_SEALED_UNREAD', 'ENV_VALUE_SHAPE', 'FE_STACK_CORE_MISSING', 'FE_STACK_UNLISTED', 'HFS_AGE_KEY_CUSTODY', 'HFS_APP_LEXICON', 'HFS_APP_WORKFLOW_TRIGGER', 'HFS_ASSET_KIND', 'HFS_ASSET_OVERSIZE', 'HFS_AUDIT_STEP_MISSING', 'HFS_CANON_COPY_DRIFT', 'HFS_CI_TOOL_INSTALL', 'HFS_COVERAGE_BELOW', 'HFS_DB_DEPLOY_UNGUARDED', 'HFS_DEPENDABOT_MISSING', 'HFS_DEPLOY_COMPOSE_ON_VPS', 'HFS_DEPLOY_NO_LOGIC', 'HFS_DEPLOY_SCHEDULER', 'HFS_DEPLOY_WORKFLOW', 'HFS_DOC_COMMAND_UNKNOWN', 'HFS_DOC_ENV_UNKNOWN', 'HFS_DOC_PATH_MISSING', 'HFS_DOC_RULE_UNKNOWN', 'HFS_FEATURE_FILE_BUDGET', 'HFS_HOST_CRON', 'HFS_RELEASE_TAG_MISMATCH', 'HFS_SBOM_MISSING', 'HFS_SCHEDULE_UNDECLARED', 'HFS_SCRIPT_PLAINTEXT_LOAD', 'HFS_SCRIPT_XTRACE', 'HFS_SECRET_OUTSIDE_ENC', 'HFS_SECRET_PLAINTEXT_FILE', 'HFS_SEED_PII', 'HFS_SERVICE_ROLE_SCOPE', 'HFS_UNUSED_SYMBOL', 'HFS_WORKFLOW_PIN', 'STACKS_DEPLOY_CONFIG_INVALID', 'STACKS_DNS_UNDECLARED', 'STACKS_HEALTHCHECK_MISSING', 'STACKS_HOST_UNROUTED', 'STACKS_INGRESS_UNDECLARED', 'STACKS_INGRESS_UNKNOWN', 'STACKS_LITE_SELFHOSTED_DB', 'STACKS_MODEL_UNAVAILABLE', 'STACKS_PLACEMENT_MISSING', 'STACKS_PORT_PUBLISHED', 'STACKS_REALTIME_SCALED', 'STACKS_RELEASE_UNDECLARED', 'STACKS_SECOND_INGRESS', 'STACKS_SECRET_ENV_LITERAL', 'STACKS_SECRET_NOT_EXTERNAL', 'STACKS_SECRET_UNVERSIONED', 'STACKS_SIDECAR_DB_UNSUPPORTED', 'STACKS_SIDECAR_VOLUME_UNDECLARED', 'STACKS_TLS_UNDECLARED', 'STACKS_UPDATE_POLICY_MISSING', 'STACK_TOOLING_REIMPLEMENTED',
|
|
41
47
|
// codes-end
|
|
42
48
|
];
|
|
43
49
|
|
|
44
50
|
const FINDERS = [
|
|
45
51
|
// finders-begin
|
|
52
|
+
feBuildArgsFindings,
|
|
53
|
+
workflowContextsFindings,
|
|
46
54
|
featureFileFindings,
|
|
47
55
|
lexiconFindings,
|
|
48
56
|
assetsFindings,
|
|
@@ -72,6 +80,7 @@ const FINDERS = [
|
|
|
72
80
|
stacksLiteBackingFindings,
|
|
73
81
|
stackToolingFindings,
|
|
74
82
|
stacksDeployFindings,
|
|
83
|
+
ciCustodyFindings,
|
|
75
84
|
// finders-end
|
|
76
85
|
];
|
|
77
86
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// app-workflow-findings.mjs - app-root workflow scope shared by deployment and expression-context rules.
|
|
2
|
+
import { workflowFindings } from '../runtime-rules/workflow-source.mjs';
|
|
3
|
+
import { readText } from './read.mjs';
|
|
4
|
+
|
|
5
|
+
/** Judge tracked workflows only at full/lite app roots, preserving each caller's duplicate-file policy. */
|
|
6
|
+
export function appWorkflowFindings({ repoRoot, files, all = files, repo, isRoot, inScope = () => true }, perWorkflow, { dedupe = false } = {}) {
|
|
7
|
+
if (isRoot === false || repo?.kind !== 'app' || !['lite', 'full'].includes(repo.edition)) return [];
|
|
8
|
+
const selected = dedupe ? [...new Set(all)] : all;
|
|
9
|
+
return workflowFindings({ files: selected.filter((file) => inScope(file)), read: (file) => readText(repoRoot, file) }, perWorkflow);
|
|
10
|
+
}
|