@starci/hfs 4.7.45 → 4.7.46
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 +5 -0
- package/lint/run.mjs +1 -1
- package/package.json +1 -1
- package/release/candidate-ci.mjs +7 -7
- package/release/push.mjs +3 -3
- package/runtime/config.example.yaml +9 -10
- package/runtime/engine/config.mjs +3 -3
- package/runtime/engine/release-config.mjs +5 -5
- package/runtime/knowledge/grammars/offset-pop/playbook.yaml +1 -1
- package/runtime/knowledge/grammars/starci/playbook.yaml +16 -2
- package/runtime/knowledge/hfs/canon-pins.yaml +7 -7
- package/runtime/knowledge/hfs/ci-standard.yaml +1 -1
- package/runtime/knowledge/hfs/rules.yaml +36 -5
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -0
- package/runtime/modules/kernel/command-policy.yaml +5 -5
- package/runtime/modules/kernel/failure-codes.yaml +32 -20
- package/runtime/modules/kernel/removed-vocabulary.yaml +33 -7
- package/runtime/packages/cli/src/catalog.generated.mjs +90 -37
- package/runtime/scripts/guards/command-policy.mjs +4 -4
- package/runtime/scripts/guards/push-scan.mjs +128 -0
- package/runtime/scripts/guards/release-push-gate.mjs +31 -29
- package/runtime/scripts/hfs/example-secrets.mjs +4 -4
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +5 -5
- package/runtime/scripts/hfs/runtime-rules/git-triggers.mjs +61 -0
- package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +4 -6
- package/runtime/scripts/lib/release-version.mjs +134 -134
- package/runtime/scripts/machine/home.mjs +2 -2
- package/templates/app/ci-workflows/github/workflows/ci.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +2 -2
- package/templates/app/hooks/husky/pre-push +3 -56
- package/release/supabase-bucket.mjs +0 -8
- package/runtime/scripts/api/fs/write-json-file.mjs +0 -10
- package/runtime/scripts/api/git/merge-base-query.mjs +0 -5
- package/runtime/scripts/gates/commit-proof.mjs +0 -53
- package/runtime/scripts/guards/release-definition.mjs +0 -204
- package/runtime/scripts/guards/release-record.mjs +0 -58
- package/runtime/scripts/guards/release-suite-mode.mjs +0 -15
- package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +0 -44
|
@@ -131,12 +131,6 @@ removed:
|
|
|
131
131
|
use: '`starci debug digest` (read-only) and `starci debug run <probe>`'
|
|
132
132
|
since: 1.0.0-alpha.6
|
|
133
133
|
match: '(?<![\w-])debug pass\b'
|
|
134
|
-
- id: config-supervisor-landgate-push
|
|
135
|
-
kind: config-key
|
|
136
|
-
name: supervisor.landGate.push
|
|
137
|
-
use: 'a land fast-forwards local main only and never pushes; the remote main of the runtime moves once per release, through starci release cut'
|
|
138
|
-
since: 1.0.0-alpha.7
|
|
139
|
-
match: 'landGate\.push\b'
|
|
140
134
|
- id: flag-land-no-push
|
|
141
135
|
kind: flag
|
|
142
136
|
name: --no-push
|
|
@@ -355,7 +349,7 @@ removed:
|
|
|
355
349
|
- id: verb-test-run
|
|
356
350
|
kind: verb
|
|
357
351
|
name: starci test run
|
|
358
|
-
use: 'the L1 to L4 spec ladder is gone with the runtime specs; `starci runtime verify` proves a change,
|
|
352
|
+
use: 'the L1 to L4 spec ladder is gone with the runtime specs; `starci runtime verify` proves a change, CI runs the release rows'
|
|
359
353
|
since: 1.0.0-alpha.19
|
|
360
354
|
match: '\bstarci test run\b'
|
|
361
355
|
- id: verb-gate-runtime-coverage
|
|
@@ -430,3 +424,35 @@ removed:
|
|
|
430
424
|
use: 'DropdownMenuItem.tone uses neutral or danger; destructive commands use danger'
|
|
431
425
|
since: 1.0.0-alpha.31
|
|
432
426
|
match: 'DropdownMenuItem(?:Tone|\.tone)[^\r\n]*["'']negative["'']'
|
|
427
|
+
|
|
428
|
+
- id: code-release-receipt-missing
|
|
429
|
+
kind: code
|
|
430
|
+
name: RELEASE_RECEIPT_MISSING
|
|
431
|
+
use: 'the push of main asks for a fast-forward and a clean secret scan; a release tag asks for no receipt'
|
|
432
|
+
since: 1.0.0-alpha.31.1
|
|
433
|
+
- id: code-release-receipt-incomplete
|
|
434
|
+
kind: code
|
|
435
|
+
name: RELEASE_RECEIPT_INCOMPLETE
|
|
436
|
+
use: 'the push of main asks for a fast-forward and a clean secret scan; CI judges the full suite'
|
|
437
|
+
since: 1.0.0-alpha.31.1
|
|
438
|
+
- id: code-release-suite-mode
|
|
439
|
+
kind: code
|
|
440
|
+
name: RELEASE_SUITE_MODE
|
|
441
|
+
use: 'release.suite chooses whether the cut also runs the L4 rows; no record or gate reads the mode'
|
|
442
|
+
since: 1.0.0-alpha.31.1
|
|
443
|
+
- id: code-release-version-unchanged
|
|
444
|
+
kind: code
|
|
445
|
+
name: RELEASE_VERSION_UNCHANGED
|
|
446
|
+
use: 'main is pushed without a version move; RELEASE_VERSION_NOT_NEXT judges the version of a tag against the v* tags'
|
|
447
|
+
since: 1.0.0-alpha.31.1
|
|
448
|
+
- id: code-release-remote-head-unknown
|
|
449
|
+
kind: code
|
|
450
|
+
name: RELEASE_REMOTE_HEAD_UNKNOWN
|
|
451
|
+
use: 'a release tag is judged against the v* tags, not against the remote main head'
|
|
452
|
+
since: 1.0.0-alpha.31.1
|
|
453
|
+
- id: config-release-candidate-blocking
|
|
454
|
+
kind: config-key
|
|
455
|
+
name: candidate.blocking
|
|
456
|
+
use: 'the runtime cut has no candidate step; the ci, examples and sonar workflows judge each push to main (suite.ci.workflows)'
|
|
457
|
+
since: 1.0.0-alpha.31.1
|
|
458
|
+
match: 'candidate\.blocking\b'
|
|
@@ -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": "adcf9a8643a12ebae13bca08676226a04b3853b7bbaa22f79ead4d053d6353e6",
|
|
5
5
|
"global": [
|
|
6
6
|
{
|
|
7
7
|
"name": "json",
|
|
@@ -1301,8 +1301,8 @@ export const CATALOG = {
|
|
|
1301
1301
|
"release.requireCi (default false, owner ruling release-require-ci-per-project) decides whether push waits for CI; every local gate row runs either way",
|
|
1302
1302
|
"requireCi false: push needs the tag and the green L4 record, runs the dry run, pushes atomically and states CI was not required",
|
|
1303
1303
|
"requireCi true: push --execute sends HEAD to refs/heads/release/candidate-<version> first and waits for its ci.yml run",
|
|
1304
|
-
"green moves the branch and the tag in one atomic push; red refuses
|
|
1305
|
-
"no run,
|
|
1304
|
+
"green moves the branch and the tag in one atomic push; red refuses naming the failing jobs",
|
|
1305
|
+
"no run, no end in time or an unreadable GitHub refuses; nothing reaches the branch",
|
|
1306
1306
|
"a red candidate never passes: there is no override and no --no-ci-check flag",
|
|
1307
1307
|
"the gate tag is always <release.tagPrefix><package.json version>; a taken tag on another commit is red, never another patch number",
|
|
1308
1308
|
"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",
|
|
@@ -3399,7 +3399,7 @@ export const CATALOG = {
|
|
|
3399
3399
|
"land only a tip `starci runtime verify` proved (check, package clean-test, scaffold e2e on that exact commit)",
|
|
3400
3400
|
"the runtime has no specs of its own (owner ruling no-runtime-specs)",
|
|
3401
3401
|
"lands are serial and hold the host land lock through the receipt, gate, full check and local merge",
|
|
3402
|
-
"local main is the only destination; a land never pushes, tags or touches origin"
|
|
3402
|
+
"local main is the only destination; a land never pushes, tags or touches origin (starci release push sends main)"
|
|
3403
3403
|
],
|
|
3404
3404
|
"runtimeSide": "host"
|
|
3405
3405
|
},
|
|
@@ -8551,7 +8551,7 @@ export const CATALOG = {
|
|
|
8551
8551
|
"runtimeSide": "tree"
|
|
8552
8552
|
},
|
|
8553
8553
|
"ci-status": {
|
|
8554
|
-
"summary": "the
|
|
8554
|
+
"summary": "the GitHub CI verdict of a pushed commit; red names the next alpha.N.M to cut",
|
|
8555
8555
|
"owner": "runtime",
|
|
8556
8556
|
"flags": [
|
|
8557
8557
|
{
|
|
@@ -8562,23 +8562,29 @@ export const CATALOG = {
|
|
|
8562
8562
|
{
|
|
8563
8563
|
"name": "tag",
|
|
8564
8564
|
"type": "string",
|
|
8565
|
-
"summary": "the release tag v* whose commit is read
|
|
8565
|
+
"summary": "the release tag v* whose commit is read"
|
|
8566
|
+
},
|
|
8567
|
+
{
|
|
8568
|
+
"name": "sha",
|
|
8569
|
+
"type": "string",
|
|
8570
|
+
"summary": "the commit to read (default is HEAD, the commit starci release push sent to main)"
|
|
8566
8571
|
},
|
|
8567
8572
|
{
|
|
8568
8573
|
"name": "wait",
|
|
8569
8574
|
"type": "boolean",
|
|
8570
|
-
"summary": "poll until the
|
|
8575
|
+
"summary": "poll until the runs end (modules/supervisor/release-cut.yaml suite.ci pollSeconds, waitMinutes)"
|
|
8571
8576
|
}
|
|
8572
8577
|
],
|
|
8573
8578
|
"exit": {
|
|
8574
|
-
"0": "the ci
|
|
8575
|
-
"1": "
|
|
8579
|
+
"0": "the ci, examples and sonar workflows of the commit are green",
|
|
8580
|
+
"1": "one is red (recorded once as runtime-defect:ci-red-<id>, and the next alpha.N.M to cut is printed as nextFix)",
|
|
8576
8581
|
"2": "bad usage",
|
|
8577
8582
|
"3": "no verdict yet - no run, still running, or GitHub could not be read"
|
|
8578
8583
|
},
|
|
8579
8584
|
"json": "flag",
|
|
8580
8585
|
"examples": [
|
|
8581
|
-
"starci release ci-status --
|
|
8586
|
+
"starci release ci-status --wait",
|
|
8587
|
+
"starci release ci-status --tag v1.0.0-alpha.31.1 --wait",
|
|
8582
8588
|
"starci release ci-status --json"
|
|
8583
8589
|
],
|
|
8584
8590
|
"editions": [
|
|
@@ -8588,8 +8594,11 @@ export const CATALOG = {
|
|
|
8588
8594
|
"script": "scripts/supervisor/release-ci-status-cli.mjs"
|
|
8589
8595
|
},
|
|
8590
8596
|
"conventions": [
|
|
8591
|
-
"
|
|
8592
|
-
"
|
|
8597
|
+
"Owner ruling push-main-after-scoped-check: main is pushed after a green scoped check and the full suite runs only in these workflows; this verb is how",
|
|
8598
|
+
"their verdict comes back.",
|
|
8599
|
+
"A red verdict never blocks the next push: it opens the next alpha.N.M (the fix is pushed after its own scoped check, then cut) through the defect",
|
|
8600
|
+
"queue of the Supervisor.",
|
|
8601
|
+
"The state is left beside the other release proofs (starci-release/<sha>.ci.json); starci debug digest and starci reconciler up --check show the last CI."
|
|
8593
8602
|
],
|
|
8594
8603
|
"runtimeSide": "tree"
|
|
8595
8604
|
},
|
|
@@ -8628,7 +8637,7 @@ export const CATALOG = {
|
|
|
8628
8637
|
"runtimeSide": "tree"
|
|
8629
8638
|
},
|
|
8630
8639
|
"cut": {
|
|
8631
|
-
"summary": "
|
|
8640
|
+
"summary": "tag a release on a commit already pushed to main; it runs no suite",
|
|
8632
8641
|
"owner": "runtime",
|
|
8633
8642
|
"flags": [
|
|
8634
8643
|
{
|
|
@@ -8649,36 +8658,35 @@ export const CATALOG = {
|
|
|
8649
8658
|
{
|
|
8650
8659
|
"name": "plan",
|
|
8651
8660
|
"type": "boolean",
|
|
8652
|
-
"summary": "report what the cut would
|
|
8661
|
+
"summary": "report what the cut would do and which rows CI judges instead, and stop; it runs and tags nothing"
|
|
8653
8662
|
},
|
|
8654
8663
|
{
|
|
8655
8664
|
"name": "tag",
|
|
8656
8665
|
"type": "string",
|
|
8657
|
-
"summary": "the release tag v* to
|
|
8666
|
+
"summary": "the release tag v* to cut (default is the one new annotated v* tag on HEAD)"
|
|
8658
8667
|
},
|
|
8659
8668
|
{
|
|
8660
8669
|
"name": "rows",
|
|
8661
8670
|
"type": "string",
|
|
8662
|
-
"summary": "re-run only these rows (comma separated, names as --plan lists them) on the SAME commit
|
|
8671
|
+
"summary": "suite local only: re-run only these rows (comma separated, names as --plan lists them) on the SAME commit; a row that is not green there refuses the cut"
|
|
8663
8672
|
},
|
|
8664
8673
|
{
|
|
8665
8674
|
"name": "no-reuse",
|
|
8666
8675
|
"type": "boolean",
|
|
8667
|
-
"summary": "run every row; stand in for none from an earlier commit
|
|
8676
|
+
"summary": "suite local only: run every row; stand in for none from an earlier commit"
|
|
8668
8677
|
}
|
|
8669
8678
|
],
|
|
8670
8679
|
"exit": {
|
|
8671
|
-
"0": "the
|
|
8672
|
-
"1": "refused (
|
|
8680
|
+
"0": "the annotated tag was created on the pushed head and pushed alone, or --plan found nothing missing",
|
|
8681
|
+
"1": "refused (HEAD is not the head of the remote main, dirty tree, no or wrong tag, release notes incomplete, RELEASE_VERSION_NOT_NEXT, and under suite local a red row) and nothing was pushed",
|
|
8673
8682
|
"2": "bad usage"
|
|
8674
8683
|
},
|
|
8675
8684
|
"json": "flag",
|
|
8676
8685
|
"examples": [
|
|
8677
8686
|
"starci release cut",
|
|
8678
|
-
"starci release cut --plan --tag v1.0.0-alpha.
|
|
8687
|
+
"starci release cut --plan --tag v1.0.0-alpha.31.1",
|
|
8679
8688
|
"starci release cut --tag v1.0.0-alpha.31.1",
|
|
8680
|
-
"starci release cut --tag v1.0.0-alpha.
|
|
8681
|
-
"starci release cut --repo <path> --tag v1.0.0-alpha.9 --json"
|
|
8689
|
+
"starci release cut --repo <path> --tag v1.0.0-alpha.31.1 --json"
|
|
8682
8690
|
],
|
|
8683
8691
|
"editions": [
|
|
8684
8692
|
"full"
|
|
@@ -8687,20 +8695,18 @@ export const CATALOG = {
|
|
|
8687
8695
|
"script": "scripts/supervisor/release-cut-cli.mjs"
|
|
8688
8696
|
},
|
|
8689
8697
|
"conventions": [
|
|
8698
|
+
"Owner ruling push-main-after-scoped-check (2026-10-26): the cut is light. Main was already pushed by starci release push after a green scoped check;",
|
|
8699
|
+
"the cut tags that commit and pushes the tag alone.",
|
|
8700
|
+
"Under config.yaml release.suite: ci (the owner's choice) the plan holds no row: every L4 row is delegated to the ci, examples and sonar workflows on",
|
|
8701
|
+
"the push of main and is listed as delegated, never green.",
|
|
8702
|
+
"Under release.suite: local (the shipped default, an opt-in to a heavy cut) the L4 rows run on this host before the tag, and a red row refuses the cut.",
|
|
8703
|
+
"No push gate depends on it.",
|
|
8690
8704
|
"Naming (owner ruling release-waves): the next release is alpha.N.M+1 of the highest v* tag (a wave or a fix) or the next line alpha.N+1,",
|
|
8691
|
-
"derived from the v* tags, never guessed (RELEASE_VERSION_NOT_NEXT); without --tag the refusal names both.",
|
|
8692
|
-
"The
|
|
8693
|
-
"
|
|
8694
|
-
"
|
|
8695
|
-
"
|
|
8696
|
-
"After the suite and the secret scan the exact HEAD is pushed to refs/heads/release/candidate-<version>;",
|
|
8697
|
-
"a re-cut of the same version force-moves only that ref, never main.",
|
|
8698
|
-
"The cut waits for every required workflow run of that sha (pollSeconds, graceMinutes, waitMinutes in release-cut.yaml).",
|
|
8699
|
-
"Red refuses RELEASE_CANDIDATE_CI_RED with the run URLs and the failing jobs and steps; nothing reaches main.",
|
|
8700
|
-
"No run within the grace, or no end within the wait, refuses RELEASE_CANDIDATE_CI_TIMEOUT.",
|
|
8701
|
-
"A red candidate keeps its ref for diagnosis; after a cut the candidate ref is left on the remote, naming the released sha.",
|
|
8702
|
-
"Only on green is the annotated tag created and main plus the tag pushed atomically; the release record logs the candidate verdict.",
|
|
8703
|
-
"There is no override: a red candidate never passes."
|
|
8705
|
+
"derived from the v* tags, never guessed (RELEASE_VERSION_NOT_NEXT); without --tag the refusal names both. The legacy -fix form is read and never cut.",
|
|
8706
|
+
"The tag message is the CHANGELOG section of the version (R222); the CI record of the commit is left pending and starci release ci-status --tag <tag>",
|
|
8707
|
+
"--wait reads the verdict.",
|
|
8708
|
+
"Publication is a separate verb, starci release publish, run as soon as a package branch is done (owner ruling publish-as-soon-as-done); the cut",
|
|
8709
|
+
"happens on the last publish."
|
|
8704
8710
|
],
|
|
8705
8711
|
"runtimeSide": "tree"
|
|
8706
8712
|
},
|
|
@@ -8945,6 +8951,53 @@ export const CATALOG = {
|
|
|
8945
8951
|
],
|
|
8946
8952
|
"runtimeSide": "tree"
|
|
8947
8953
|
},
|
|
8954
|
+
"push": {
|
|
8955
|
+
"summary": "push the runtime main right after a green scoped check; CI is the critic",
|
|
8956
|
+
"owner": "runtime",
|
|
8957
|
+
"flags": [
|
|
8958
|
+
{
|
|
8959
|
+
"name": "repo",
|
|
8960
|
+
"type": "string",
|
|
8961
|
+
"summary": "the repository to push (default: the runtime this command runs from, not the directory it is started in)"
|
|
8962
|
+
},
|
|
8963
|
+
{
|
|
8964
|
+
"name": "remote",
|
|
8965
|
+
"type": "string",
|
|
8966
|
+
"summary": "the remote to push to (default origin)"
|
|
8967
|
+
},
|
|
8968
|
+
{
|
|
8969
|
+
"name": "branch",
|
|
8970
|
+
"type": "string",
|
|
8971
|
+
"summary": "the branch to push (default main)"
|
|
8972
|
+
}
|
|
8973
|
+
],
|
|
8974
|
+
"exit": {
|
|
8975
|
+
"0": "main was pushed (or the remote already has this commit)",
|
|
8976
|
+
"1": "refused (push closed by supervisor.landGate.push false, not on main, dirty tree, not a fast-forward, secret scan, a red hook) and nothing moved",
|
|
8977
|
+
"2": "bad usage"
|
|
8978
|
+
},
|
|
8979
|
+
"json": "flag",
|
|
8980
|
+
"examples": [
|
|
8981
|
+
"starci release push",
|
|
8982
|
+
"starci release push --json"
|
|
8983
|
+
],
|
|
8984
|
+
"editions": [
|
|
8985
|
+
"full"
|
|
8986
|
+
],
|
|
8987
|
+
"impl": {
|
|
8988
|
+
"script": "scripts/supervisor/release-push-cli.mjs"
|
|
8989
|
+
},
|
|
8990
|
+
"conventions": [
|
|
8991
|
+
"Owner ruling push-main-after-scoped-check: main is pushed right after a green scoped check, even for a small fix; the check is the agent's, written as",
|
|
8992
|
+
"a scope decision first.",
|
|
8993
|
+
"The verb asks the very judgments of the pre-push hook (scripts/guards/release-push-gate.mjs) first, then runs git push WITH the hooks: never",
|
|
8994
|
+
"--no-verify, never a raw push, never a force.",
|
|
8995
|
+
"The gate asks for a fast-forward and a clean secret scan of the pushed range. No release record and no L4 row.",
|
|
8996
|
+
"The full suite is CI's verdict on the push (ci, examples, sonar): starci release ci-status --sha <head> --wait. A red CI opens the next alpha.N.M at",
|
|
8997
|
+
"once and never blocks the next push."
|
|
8998
|
+
],
|
|
8999
|
+
"runtimeSide": "tree"
|
|
9000
|
+
},
|
|
8948
9001
|
"scaffold-e2e": {
|
|
8949
9002
|
"summary": "run the scaffold end-to-end proof (full and lite scaffolds installed, built and run for real)",
|
|
8950
9003
|
"owner": "runtime",
|
|
@@ -11685,7 +11738,7 @@ export const CATALOG = {
|
|
|
11685
11738
|
"runtimeSide": "host"
|
|
11686
11739
|
},
|
|
11687
11740
|
"land": {
|
|
11688
|
-
"summary": "run the in-runtime land gate; it fast-forwards local main
|
|
11741
|
+
"summary": "run the in-runtime land gate; it fast-forwards local main (release push sends it)",
|
|
11689
11742
|
"owner": "runtime",
|
|
11690
11743
|
"flags": [
|
|
11691
11744
|
{
|
|
@@ -15857,7 +15910,7 @@ export const CATALOG = {
|
|
|
15857
15910
|
"starci guard input": "starci guard input is removed (the reference-transaction hook reads prepared ref updates directly in its shell loop)",
|
|
15858
15911
|
"starci debug pass": "debug pass is removed (`starci debug digest` (read-only) and `starci debug run <probe>`)",
|
|
15859
15912
|
"starci test affected": "starci test affected is removed (the runtime has no specs of its own (owner ruling no-runtime-specs): `starci runtime verify` runs the check, `starci release clean-test --base <ref>` and `starci release scaffold-e2e`)",
|
|
15860
|
-
"starci test run": "starci test run is removed (the L1 to L4 spec ladder is gone with the runtime specs; `starci runtime verify` proves a change,
|
|
15913
|
+
"starci test run": "starci test run is removed (the L1 to L4 spec ladder is gone with the runtime specs; `starci runtime verify` proves a change, CI runs the release rows)",
|
|
15861
15914
|
"starci gate runtime-coverage": "starci gate runtime-coverage is removed (the runtime measures no coverage of its own; the example apps keep their Codecov flags)",
|
|
15862
15915
|
"starci release env-test": "starci release env-test is removed (the release rows are the package clean-test (`starci release clean-test`), the scaffold e2e (`starci release scaffold-e2e`) and the runtime check)",
|
|
15863
15916
|
"starci runtime benchmark-snapshot": "starci runtime benchmark-snapshot is removed (routing evidence is read live from the ledgers: `starci machine op-metrics --by op` and the ledger view v_model_scorecard (modules/models/registry.yaml costPolicy))"
|
|
@@ -166,7 +166,7 @@ const npmReadAllowed = (args, words, verb, p) => p.npmRead.has(verb)
|
|
|
166
166
|
|| (verb === 'pack' && args.includes('--dry-run'));
|
|
167
167
|
|
|
168
168
|
const npmPublishVerdict = (role, text, policy) => {
|
|
169
|
-
const denied = policy.npm?.deny?.publish, use = useOf(denied, 'starci release
|
|
169
|
+
const denied = policy.npm?.deny?.publish, use = useOf(denied, 'starci release publish');
|
|
170
170
|
return refusal(codeOf(denied, 'RIGHTS_NPM_PUBLISH'), text, `the ${role} role does not publish a package because packages are published once per release from the release commit`, use);
|
|
171
171
|
};
|
|
172
172
|
|
|
@@ -174,7 +174,7 @@ const npmSuiteVerdict = ({ role, args, p, policy, guard, text, words, verb }) =>
|
|
|
174
174
|
if (!suiteScript(words) || afterDashDash(args).length) return undefined;
|
|
175
175
|
if (p.suiteOps.has(String(guard?.op ?? ''))) return null;
|
|
176
176
|
const use = useOf(policy.npm?.deny?.[verb === 'run' || verb === 'run-script' ? verb : 'test'], 'starci gate unit --root <app>');
|
|
177
|
-
return refusal('RIGHTS_SUITE_RUN', text, `the ${role} role does not run a whole suite because full suites run
|
|
177
|
+
return refusal('RIGHTS_SUITE_RUN', text, `the ${role} role does not run a whole suite because full suites run in CI or the requested verify ops`, use);
|
|
178
178
|
};
|
|
179
179
|
|
|
180
180
|
const npmCleanInstallVerdict = ({ args, policy, handle, lockOwner, text, verb }) => {
|
|
@@ -212,7 +212,7 @@ const nodePolicyVerdict = ({ role, args, p, policy, guard, text }) => {
|
|
|
212
212
|
if (args.includes('--test')) {
|
|
213
213
|
if (nodeWholeSuite(args)) {
|
|
214
214
|
if (p.suiteOps.has(String(guard?.op ?? ''))) return null;
|
|
215
|
-
return refusal('RIGHTS_SUITE_RUN', text, `the ${role} role does not run a whole suite because full suites run
|
|
215
|
+
return refusal('RIGHTS_SUITE_RUN', text, `the ${role} role does not run a whole suite because full suites run in CI or the requested verify ops`, 'starci runtime verify in the runtime (the check, package clean-test and scaffold e2e), or one explicit spec file');
|
|
216
216
|
}
|
|
217
217
|
const targets = nodeOperands(args).filter((value) => value !== 'test');
|
|
218
218
|
if (p.nodeTest.has(role) && targets.length && targets.every((value) => /\.(?:spec|test)\.[cm]?js$/i.test(value))) return null;
|
|
@@ -293,7 +293,7 @@ export function policyVerdict({ role, command, guard = null, handle = null, lock
|
|
|
293
293
|
|
|
294
294
|
if (releaseCall(program, args, policy)) {
|
|
295
295
|
const use = useOf(policy.release, 'starci release cut');
|
|
296
|
-
return refusal('RIGHTS_RELEASE_CUT', text, `the ${role} role does not cut or publish a release because
|
|
296
|
+
return refusal('RIGHTS_RELEASE_CUT', text, `the ${role} role does not cut or publish a release because tagging a release and publishing packages are owner-approved`, use);
|
|
297
297
|
}
|
|
298
298
|
const seat = seatVerdict({ role, guard, handle, policy, program, args, text });
|
|
299
299
|
if (seat !== undefined) return seat;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// push-scan.mjs - the secret scan of an outgoing range, shared by the Supervisor's push of the product repositories (push-mains.mjs) and the push gate of the runtime
|
|
2
|
+
// repository (scripts/guards/release-push-gate.mjs, `starci release push`). Findings name the file, line and pattern, never the value. Its imports stay light on purpose: the
|
|
3
|
+
// runtime push hook is bundled with the hfs package (scripts/hfs/sync-runtime.mjs) and must not drag the supervisor in.
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { safeRemove } from '../api/fs/safe-remove.mjs';
|
|
6
|
+
import { makeTempDir } from '../api/fs/make-temp-dir.mjs';
|
|
7
|
+
import { diff as gitDiff } from '../api/git/diff.mjs';
|
|
8
|
+
import { show as gitShow } from '../api/git/show.mjs';
|
|
9
|
+
import { artifactHoldReason } from '../machine/artifact-hold.mjs';
|
|
10
|
+
import { FORBIDDEN_FILES, secretHits, secretHitsInFile } from '../lib/secret-patterns.mjs';
|
|
11
|
+
import { slash } from '../lib/path-key.mjs';
|
|
12
|
+
import { isSopsEnvelope, setCommand } from '../lib/sops-envelope.mjs';
|
|
13
|
+
import { forEachFileLine } from '../lib/read-text.mjs';
|
|
14
|
+
|
|
15
|
+
const SCAN_GIT = { diff: gitDiff, show: gitShow };
|
|
16
|
+
/** The scan's git runner: `args[0]` names the scripts/api/git call file it runs, in `cwd`: {ok, stdout, stderr, error}. */
|
|
17
|
+
function git([verb, ...args], { cwd }) {
|
|
18
|
+
const r = SCAN_GIT[verb](args, { cwd, timeout: 300_000, maxBuffer: 64 * 1024 * 1024 });
|
|
19
|
+
return { ok: r.status === 0, stdout: String(r.stdout ?? '').trim(), stderr: String(r.stderr ?? '').trim(), error: r.error?.message ?? null };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Scan a unified diff (added lines only) and its file list. Returns [{file, line, pattern}] - never the value.
|
|
24
|
+
* `files` are the changed paths (status A/M/R); deleted paths are not scanned.
|
|
25
|
+
*/
|
|
26
|
+
export function scanDiff({ diff = '', files = [] } = {}) {
|
|
27
|
+
const scanner = diffScanner(files);
|
|
28
|
+
for (const raw of String(diff).split(/\r?\n/)) scanner.line(raw);
|
|
29
|
+
return scanner.findings;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** scanDiff one line at a time: feed each diff line to `line(raw)`; `findings` accumulates.
|
|
33
|
+
* A `*.enc` file is held back until its diff ends and passes only when it is a sops-encrypted file with no
|
|
34
|
+
* plaintext value (scripts/lib/sops-envelope.mjs isSopsEnvelope) - judged on the whole file at the pushed commit
|
|
35
|
+
* when `encText(file)` can read it (a --unified=0 diff of an edited sops YAML holds only its changed lines), else
|
|
36
|
+
* on its added lines. Anything else in a `.enc` file is scanned like any other file, and a `.enc` under
|
|
37
|
+
* `.starcistacks/` that is not a sops envelope refuses the push by itself (sops-not-envelope). */
|
|
38
|
+
function diffScanner(files = [], { encText = null, keepHit = null } = {}) {
|
|
39
|
+
const findings = [];
|
|
40
|
+
for (const file of files) for (const rule of FORBIDDEN_FILES) if (rule.test(slash(file))) findings.push({ file, line: null, pattern: rule.name });
|
|
41
|
+
let file = null, line = 0, enc = null;
|
|
42
|
+
const flushEnc = () => {
|
|
43
|
+
if (enc && !isSopsEnvelope((encText ? encText(enc.file) : null) ?? enc.added.join('\n'))) {
|
|
44
|
+
findings.push(...enc.findings);
|
|
45
|
+
// .starcistacks holds sops twins only: anything else there (a JSON key the value rules cannot see, a
|
|
46
|
+
// plaintext value next to the sops block) refuses the push by itself.
|
|
47
|
+
if (/(^|\/)\.starcistacks\//i.test(enc.file)) findings.push({ file: enc.file, line: enc.line, pattern: 'sops-not-envelope' });
|
|
48
|
+
}
|
|
49
|
+
enc = null;
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
get findings() { flushEnc(); return findings; },
|
|
53
|
+
line(raw) {
|
|
54
|
+
if (raw.startsWith('+++ ')) {
|
|
55
|
+
flushEnc();
|
|
56
|
+
file = raw.slice(4).replace(/^b\//, '');
|
|
57
|
+
if (/\.enc$/i.test(file)) enc = { file, line: null, added: [], findings: [] };
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(raw);
|
|
61
|
+
if (hunk) { line = Number(hunk[1]); return; }
|
|
62
|
+
if (raw.startsWith('+')) {
|
|
63
|
+
const text = raw.slice(1);
|
|
64
|
+
if (enc) { enc.added.push(text); enc.line ??= line; }
|
|
65
|
+
(enc ? enc.findings : findings).push(...addedLineFindings(file, line, text, keepHit));
|
|
66
|
+
line += 1;
|
|
67
|
+
} else if (!raw.startsWith('-')) line += 1;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Every added line is scanned first; an optional whole-file judgement can then retain or drop its hits. */
|
|
73
|
+
function addedLineFindings(file, line, text, keepHit) {
|
|
74
|
+
const patterns = secretHits(file?.replace(/\.tpl$/, ''), text);
|
|
75
|
+
if (!patterns.length || (keepHit && !keepHit(file, line))) return [];
|
|
76
|
+
return patterns.map(pattern => ({ file, line, pattern }));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Only badge-bearing paths use cached post-images; an unreadable post-image keeps every diff hit. */
|
|
80
|
+
function badgeHitFilter(readText) {
|
|
81
|
+
const linesByFile = new Map();
|
|
82
|
+
return (file, line) => {
|
|
83
|
+
if (!/(?:^|\/)(?:README\.md|\.starcistacks\/application-stacks\.yaml)$/.test(file)) return true;
|
|
84
|
+
if (!linesByFile.has(file)) {
|
|
85
|
+
const text = readText(file);
|
|
86
|
+
const manifestText = /(?:^|\/)README\.md$/.test(file) ? readText(path.posix.join(path.posix.dirname(file), '.starcistacks/application-stacks.yaml')) ?? undefined : undefined;
|
|
87
|
+
linesByFile.set(file, text === null ? null : new Set(secretHitsInFile(file, text, { manifestText }).map(hit => hit.line)));
|
|
88
|
+
}
|
|
89
|
+
const lines = linesByFile.get(file);
|
|
90
|
+
return lines === null || lines.has(line);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Read each committed post-image once; a NUL commit prefix protects its leading whitespace from the git runner's trim. */
|
|
95
|
+
function postImageReader(cwd, to) {
|
|
96
|
+
const texts = new Map();
|
|
97
|
+
return file => {
|
|
98
|
+
if (!texts.has(file)) {
|
|
99
|
+
const result = git(['show', '--format=%x00', '--no-patch', to, `${to}:${file}`], { cwd });
|
|
100
|
+
const readable = result.ok && result.stdout.startsWith('\0\n');
|
|
101
|
+
texts.set(file, readable ? result.stdout.slice(2) : null);
|
|
102
|
+
}
|
|
103
|
+
return texts.get(file);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The one fix a refused plaintext test credential gets (owner ruling push-scan-test-secrets-encrypted): the product
|
|
108
|
+
* repository's own .starcistacks + sops convention, or a value generated per run. */
|
|
109
|
+
const TEST_SECRET_HINT = `move it to .starcistacks/<stack>/secrets/test/<name> and encrypt it with the repository's own command (${setCommand('<name>', '<stack>')}; commit only the .enc, read it with testSecret() from scripts/uat/test-secret.mjs) or generate it per run; never a plaintext literal`;
|
|
110
|
+
export const scanHint = (findings = []) => (findings.some((f) => f.pattern === 'assigned-secret') ? TEST_SECRET_HINT : null);
|
|
111
|
+
|
|
112
|
+
/** Scan the range `from..to` of `cwd`: {ok, findings, files}. The diff is written to a temp file and read
|
|
113
|
+
* in chunks, never held whole in a spawn buffer (a 64 MB overflow read as `scan failed: git diff failed`). */
|
|
114
|
+
export function scanRange({ cwd, from, to }) {
|
|
115
|
+
const names = git(['diff', '--name-only', '--diff-filter=ACMR', `${from}..${to}`], { cwd });
|
|
116
|
+
if (!names.ok) return { ok: false, error: names.stderr || names.error || 'git diff failed', findings: [] };
|
|
117
|
+
const files = names.stdout.split(/\r?\n/).filter(Boolean);
|
|
118
|
+
const dir = makeTempDir('starci-push-scan-');
|
|
119
|
+
const out = path.join(dir, 'range.diff');
|
|
120
|
+
try {
|
|
121
|
+
const diff = git(['diff', '--no-color', '--unified=0', '--diff-filter=ACMR', `--output=${out}`, `${from}..${to}`], { cwd });
|
|
122
|
+
if (!diff.ok) return { ok: false, error: diff.stderr || diff.error || 'git diff failed', findings: [] };
|
|
123
|
+
const encText = (f) => { const r = git(['show', `${to}:${f}`], { cwd }); return r.ok ? r.stdout : null; };
|
|
124
|
+
const scanner = diffScanner(files, { encText, keepHit: badgeHitFilter(postImageReader(cwd, to)) });
|
|
125
|
+
forEachFileLine(out, (l) => scanner.line(l));
|
|
126
|
+
return { ok: scanner.findings.length === 0, findings: scanner.findings, files };
|
|
127
|
+
} finally { safeRemove(dir, { hold: artifactHoldReason }); }
|
|
128
|
+
}
|
|
@@ -1,63 +1,65 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// release-push-gate.mjs - the body of the runtime repository's pre-push hook (scripts/guards/git-hooks.mjs). Git hands the hook one line
|
|
3
|
-
// per ref on stdin: <local ref> <local sha> <remote ref> <remote sha>.
|
|
4
|
-
//
|
|
5
|
-
// refs/heads/main not deleted, a fast-forward of the remote main, and the pushed
|
|
6
|
-
// refs/tags/v* annotated
|
|
7
|
-
// refs/heads/release/candidate-* allowed, force and delete included: the release candidate ref `starci release cut` pushes the release sha to and waits on
|
|
8
|
-
// (scripts/supervisor/release-candidate.mjs, OWNER RULING candidate-ci 2026-10-18); its CI is the release verdict, so it must stay open
|
|
3
|
+
// per ref on stdin: <local ref> <local sha> <remote ref> <remote sha>. Owner ruling push-main-after-scoped-check: the push is light and CI is the critic, so a push of main
|
|
4
|
+
// or of a tag passes when the hook runs clean and the secret scan of the pushed range finds nothing:
|
|
5
|
+
// refs/heads/main not deleted, a fast-forward of the remote main, and the pushed range passes the secret scan (scripts/guards/push-scan.mjs)
|
|
6
|
+
// refs/tags/v* not deleted and annotated (its message is the release notes); the release definition is judged by `starci release cut`, which makes the tag
|
|
9
7
|
// any other ref allowed (a lane branch, refs/backup/*, a tag that is not a release tag)
|
|
10
|
-
// A refusal prints `RIGHTS_PUSH_NOT_RELEASE: <ref> is closed: <
|
|
11
|
-
//
|
|
8
|
+
// No release record and no L4 row is asked for. A refusal prints `RIGHTS_PUSH_NOT_RELEASE: <ref> is closed: <why>` per ref and exits 1 (the code keeps its name: the hfs rules and the
|
|
9
|
+
// failure-code catalogue cite it; its meaning is "the pre-push gate refused the push"). There is no switch that opens it and no --no-verify.
|
|
12
10
|
import { catFile } from '../api/git/cat-file.mjs';
|
|
13
11
|
import { isAncestor } from '../api/git/is-ancestor.mjs';
|
|
14
|
-
import { revParseQuery } from '../api/git/rev-parse-query.mjs';
|
|
15
12
|
import { isMain } from '../lib/is-main.mjs';
|
|
16
13
|
import { invocationDir } from '../lib/roots.mjs';
|
|
17
|
-
import {
|
|
14
|
+
import { scanRange } from './push-scan.mjs';
|
|
18
15
|
import { hookErrorMessage, readInput, writeHookOutput } from './hook-io.mjs';
|
|
19
16
|
|
|
20
17
|
export const REFUSAL_CODE = 'RIGHTS_PUSH_NOT_RELEASE';
|
|
21
18
|
const ZERO = /^0+$/;
|
|
22
19
|
const RELEASE_TAG_REF = /^refs\/tags\/v\d/;
|
|
23
|
-
/**
|
|
24
|
-
const
|
|
20
|
+
/** git's empty tree: the range base of a first push, so the whole tree is scanned. */
|
|
21
|
+
const EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
22
|
+
const LISTED = 8;
|
|
25
23
|
|
|
26
24
|
const parseLines = (text) => String(text ?? '').split(/\r?\n/).map((line) => line.trim().split(/\s+/)).filter((parts) => parts.length === 4)
|
|
27
25
|
.map(([localRef, localSha, remoteRef, remoteSha]) => ({ localRef, localSha, remoteRef, remoteSha }));
|
|
28
|
-
const describe = (findings) => findings.map((f) => `${f.missing} (${f.fix})`).join('; ');
|
|
29
26
|
const refuse = (ref, why) => `${REFUSAL_CODE}: ${ref} is closed: ${why}`;
|
|
30
27
|
|
|
31
|
-
|
|
28
|
+
/** The secret-scan refusal of the pushed range, or null when it is clean. A finding names the file, line and pattern, never the value. */
|
|
29
|
+
function scanRefusal(cwd, { localSha, remoteSha }, scan) {
|
|
30
|
+
const result = scan({ cwd, from: ZERO.test(remoteSha) ? EMPTY_TREE : remoteSha, to: localSha });
|
|
31
|
+
if (result.ok) return null;
|
|
32
|
+
if (result.error) return `the secret scan of the pushed range could not run (${result.error}), so it cannot be called clean`;
|
|
33
|
+
const where = (f) => [f.line ? `${f.file}:${f.line}` : f.file, f.pattern].join(' ');
|
|
34
|
+
const found = result.findings.slice(0, LISTED).map(where).join('; ');
|
|
35
|
+
const more = result.findings.length > LISTED ? '; ...' : '';
|
|
36
|
+
return `the secret scan found ${result.findings.length} candidate(s) in the pushed range: ${found}${more} (remove or encrypt them, then commit again)`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function mainRefusal(cwd, { remoteRef, localSha, remoteSha }, scan) {
|
|
32
40
|
if (ZERO.test(localSha)) return refuse(remoteRef, 'a push never deletes the runtime main');
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return findings.length ? refuse(remoteRef, `the pushed head is not a release commit: ${describe(findings)}`) : null;
|
|
41
|
+
if (!ZERO.test(remoteSha) && !isAncestor(cwd, remoteSha, localSha)) return refuse(remoteRef, 'it is not a fast-forward of the remote main (fetch it first; a force push rewrites main)');
|
|
42
|
+
const scanned = scanRefusal(cwd, { localSha, remoteSha }, scan);
|
|
43
|
+
return scanned ? refuse(remoteRef, scanned) : null;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
function tagRefusal(cwd, { remoteRef, localSha }) {
|
|
40
47
|
if (ZERO.test(localSha)) return refuse(remoteRef, 'a push never deletes a release tag');
|
|
41
48
|
const kind = catFile(['-t', localSha], { cwd, timeout: 60_000 }).stdout?.trim();
|
|
42
|
-
|
|
43
|
-
const commit = revParseQuery([`${localSha}^{commit}`], { cwd, timeout: 60_000 }).stdout?.trim();
|
|
44
|
-
const findings = releaseFindings({ cwd, commit, tag: remoteRef.slice('refs/tags/'.length) });
|
|
45
|
-
return findings.length ? refuse(remoteRef, `the tagged commit is not a release commit: ${describe(findings)}`) : null;
|
|
49
|
+
return kind === 'tag' ? null : refuse(remoteRef, 'a release tag is annotated: its message is the release notes');
|
|
46
50
|
}
|
|
47
51
|
|
|
48
|
-
/** The refusal lines for the pushed refs `text` (git's pre-push stdin) of the repository at `cwd`: [] when every ref may move. */
|
|
49
|
-
function pushRefusals({ text, cwd }) {
|
|
52
|
+
/** The refusal lines for the pushed refs `text` (git's pre-push stdin) of the repository at `cwd`: [] when every ref may move. `scan` is the secret scan (a seam). */
|
|
53
|
+
export function pushRefusals({ text, cwd, scan = scanRange }) {
|
|
50
54
|
return parseLines(text).flatMap((ref) => {
|
|
51
55
|
let line = null;
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
else if (RELEASE_TAG_REF.test(ref.remoteRef)) line = tagRefusal(cwd, ref);
|
|
55
|
-
}
|
|
56
|
+
if (ref.remoteRef === 'refs/heads/main') line = mainRefusal(cwd, ref, scan);
|
|
57
|
+
else if (RELEASE_TAG_REF.test(ref.remoteRef)) line = tagRefusal(cwd, ref);
|
|
56
58
|
return line ? [line] : [];
|
|
57
59
|
});
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
/** Read a bounded Git ref list; unavailable input cannot authorize a
|
|
62
|
+
/** Read a bounded Git ref list; unavailable input cannot authorize a push of main. */
|
|
61
63
|
export async function main({ stdin = process.stdin, stderr = process.stderr, cwd = invocationDir() } = {}) {
|
|
62
64
|
try {
|
|
63
65
|
const refusals = pushRefusals({ text: await readInput(stdin), cwd });
|
|
@@ -6,7 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { runProgram } from '../api/process/run-program.mjs';
|
|
7
7
|
import { init as initGit } from '../api/git/init.mjs';
|
|
8
8
|
import { add as addGit } from '../api/git/add.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { revParseQuery } from '../api/git/rev-parse-query.mjs';
|
|
10
10
|
import { mergeBase } from '../api/git/merge-base.mjs';
|
|
11
11
|
import { writeTree } from '../api/git/write-tree.mjs';
|
|
12
12
|
import { commitTree } from '../api/git/commit-tree.mjs';
|
|
@@ -68,9 +68,9 @@ const ok = (r) => !r.error && r.status === 0 && Boolean(String(r.stdout ?? '').t
|
|
|
68
68
|
*/
|
|
69
69
|
function sealedLintBase(repoRoot, source, env) {
|
|
70
70
|
const query = { cwd: source, env };
|
|
71
|
-
const top =
|
|
71
|
+
const top = revParseQuery(['--show-toplevel'], query);
|
|
72
72
|
if (!ok(top)) return null;
|
|
73
|
-
const common =
|
|
73
|
+
const common = revParseQuery(['--git-common-dir'], query);
|
|
74
74
|
if (!ok(common)) return null;
|
|
75
75
|
const candidates = [...(env.STARCI_BASE_REF ? [env.STARCI_BASE_REF] : []), 'origin/main', 'main'];
|
|
76
76
|
let baseSha = null;
|
|
@@ -80,7 +80,7 @@ function sealedLintBase(repoRoot, source, env) {
|
|
|
80
80
|
}
|
|
81
81
|
if (!baseSha) throw new Error('sealed lint baseline unavailable: set STARCI_BASE_REF to a source commit sharing history with HEAD, or fetch origin/main or main');
|
|
82
82
|
const relative = path.relative(top.stdout.trim(), path.resolve(source)).split(path.sep).join('/');
|
|
83
|
-
const tree =
|
|
83
|
+
const tree = revParseQuery(['--verify', '--quiet', relative && !relative.startsWith('..') ? `${baseSha}:${relative}` : `${baseSha}^{tree}`], query);
|
|
84
84
|
const baseTree = ok(tree) ? tree.stdout.trim() : EMPTY_TREE;
|
|
85
85
|
const info = path.join(repoRoot, '.git', 'objects', 'info');
|
|
86
86
|
fs.mkdirSync(info, { recursive: true });
|