@starci/hfs 4.5.1 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/ci/cli.mjs +8 -4
  3. package/ci/local.mjs +146 -0
  4. package/ci/vars.mjs +22 -5
  5. package/ci/verify.mjs +16 -2
  6. package/lint/run.mjs +3 -4
  7. package/package.json +1 -1
  8. package/release/candidate-ci.mjs +111 -0
  9. package/release/gate.mjs +4 -5
  10. package/release/push.mjs +10 -1
  11. package/runtime/knowledge/hfs/canon-pins.yaml +9 -4
  12. package/runtime/knowledge/hfs/ci-standard.yaml +103 -0
  13. package/runtime/knowledge/hfs/rules.yaml +19 -2
  14. package/runtime/knowledge/sonar-gate.yaml +2 -2
  15. package/runtime/modules/kernel/failure-codes.yaml +10 -0
  16. package/runtime/modules/schemas/application-stacks.schema.yaml +56 -0
  17. package/runtime/packages/cli/src/catalog.generated.mjs +121 -36
  18. package/runtime/scripts/hfs/ci-declaration.mjs +10 -1
  19. package/runtime/scripts/hfs/lcov-merge.mjs +89 -0
  20. package/runtime/scripts/hfs/rules/a15-findings.mjs +3 -0
  21. package/runtime/scripts/hfs/rules/ci-standard.mjs +202 -0
  22. package/runtime/scripts/hfs/rules/coverage-below.mjs +1 -1
  23. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +14 -5
  24. package/runtime/scripts/hfs/rules/secrets.mjs +8 -10
  25. package/runtime/scripts/hfs/rules/workflow-contexts.mjs +1 -1
  26. package/runtime/scripts/lib/secret-patterns.mjs +77 -0
  27. package/scaffold/app.mjs +5 -4
  28. package/src/main.mjs +4 -2
  29. package/sync/ci-commands.mjs +81 -27
  30. package/sync/hygiene.mjs +4 -1
  31. package/sync/index.mjs +14 -11
  32. package/sync/quality-services.mjs +7 -5
  33. package/sync/readme-badges.mjs +18 -2
  34. package/sync/stack-images.mjs +5 -0
  35. package/templates/app/ci-workflows/build-cache-save.yml +8 -0
  36. package/templates/app/ci-workflows/github/workflows/ci.yml +52 -12
  37. package/templates/app/ci-workflows/install-steps.yml +3 -2
  38. package/templates/app/ci-workflows/jest-cache-save.yml +6 -0
  39. package/templates/app/ci-workflows/jest-cache.yml +4 -4
  40. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +53 -14
  41. package/templates/app/hooks/husky/pre-push +4 -2
  42. package/templates/app/skeleton/.github/dependabot.yml +1 -2
  43. package/templates/app/skeleton-lite/.github/dependabot.yml +1 -2
  44. package/release/ci-red.mjs +0 -70
@@ -4,6 +4,8 @@
4
4
  # refs/backup/* allowed: the coordinator backup namespace
5
5
  # refs/heads/main, refs/tags/v* allowed only when HEAD carries an annotated tag v[0-9]* and the release cut recorded the
6
6
  # full suite of HEAD: <git common dir>/starci-release/<HEAD sha>.l4.json names that head and tag
7
+ # refs/heads/release/candidate-* the release candidate (starci app release push sends the release sha there first and moves main
8
+ # and the tag only when its ci.yml run is green): allowed under the same release gate, never deleted
7
9
  # any other ref, a delete refused
8
10
  common=$(git rev-parse --git-common-dir)
9
11
  case "$common" in /* | [A-Za-z]:*) ;; *) common="$(pwd)/$common" ;; esac
@@ -12,7 +14,7 @@ record="$common/starci-release/$head.l4.json"
12
14
  status=0
13
15
  gate=""
14
16
  refuse() {
15
- echo "RIGHTS_PUSH_NOT_RELEASE: $1 - the allowed path is the release cut (main and v* tags) or refs/backup/* (backups)" >&2
17
+ echo "RIGHTS_PUSH_NOT_RELEASE: $1 - the allowed path is the release cut (main, v* tags and the release/candidate-* ref) or refs/backup/* (backups)" >&2
16
18
  status=1
17
19
  }
18
20
  # gate: the verdict of the release gate for HEAD, computed once: "ok" or the reason it is closed.
@@ -33,7 +35,7 @@ release_gate() {
33
35
  while read -r local_ref local_sha remote_ref remote_sha; do
34
36
  case "$remote_ref" in
35
37
  refs/backup/*) ;;
36
- refs/heads/main | refs/tags/v*)
38
+ refs/heads/main | refs/tags/v* | refs/heads/release/candidate-*)
37
39
  case "$local_sha" in
38
40
  *[!0]*) ;;
39
41
  *) refuse "refusing to delete $remote_ref"; continue ;;
@@ -1,4 +1,3 @@
1
1
  {{header}}
2
2
  version: 2
3
- updates:
4
- {{dependabotUpdates}}
3
+ updates:{{dependabotUpdates}}
@@ -1,4 +1,3 @@
1
1
  {{header}}
2
2
  version: 2
3
- updates:
4
- {{dependabotUpdates}}
3
+ updates:{{dependabotUpdates}}
@@ -1,70 +0,0 @@
1
- // ci-red.mjs - the latest completed default-branch CI is a release precondition; unreadable GitHub warns, a red run needs the owner's SHA-bound override.
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { refusal } from '../stack/errors.mjs';
5
-
6
- const FIELDS = 'databaseId,headSha,status,conclusion,url,event,createdAt';
7
- const SHA = /^[a-f0-9]{40}$/u;
8
- const RED = new Set(['failure', 'cancelled', 'timed_out', 'startup_failure', 'action_required']);
9
-
10
- /** Every process uses the supplied seam; an unavailable executable is an unreadable result. */
11
- function run(spawn, cwd, command, args) {
12
- try { return spawn(command, args, { cwd }); } catch { return { status: null }; }
13
- }
14
-
15
- function runsOf(result) {
16
- if (result.error || result.status !== 0) return null;
17
- try {
18
- const rows = JSON.parse(result.stdout);
19
- return Array.isArray(rows) ? rows : null;
20
- } catch { return null; }
21
- }
22
-
23
- /** Reads branch runs plus push/tag runs whose commits are reachable from the default branch; the newest completed run decides, even if a newer run is pending. */
24
- export function latestDefaultBranchCi({ spawn, cwd, defaultBranch = 'main' }) {
25
- const args = ['run', 'list', '--workflow', 'ci.yml'];
26
- const tail = ['--limit', '20', '--json', FIELDS];
27
- const branch = runsOf(run(spawn, cwd, 'gh', [...args, '--branch', defaultBranch, ...tail]));
28
- if (!branch) return { state: 'unknown', why: 'gh branch runs unavailable (missing gh, authentication, or unreadable response)' };
29
- const pushes = runsOf(run(spawn, cwd, 'gh', [...args, '--event', 'push', ...tail]));
30
- if (!pushes) return { state: 'unknown', why: 'gh push runs unavailable (missing gh, authentication, or unreadable response)' };
31
- const candidates = branch.filter((row) => row && SHA.test(row.headSha));
32
- let ancestryUnknown = false;
33
- for (const row of pushes) {
34
- if (!row || !SHA.test(row.headSha) || row.event !== 'push') continue;
35
- const ancestor = run(spawn, cwd, 'git', ['merge-base', '--is-ancestor', row.headSha, defaultBranch]);
36
- if (!ancestor.error && ancestor.status === 0) candidates.push(row);
37
- else if (ancestor.error || ancestor.status !== 1) ancestryUnknown = true;
38
- }
39
- const completed = candidates.filter((row) => row.status === 'completed' && Number.isFinite(Date.parse(row.createdAt)));
40
- completed.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt) || Number(b.databaseId) - Number(a.databaseId));
41
- const latest = completed[0];
42
- if (!latest) return { state: 'unknown', why: ancestryUnknown ? 'cannot establish push run ancestry on the default branch' : 'no completed CI run on the default branch' };
43
- if (latest.conclusion === 'success') return { state: 'green', run: latest };
44
- if (RED.has(latest.conclusion)) return { state: 'red', run: latest };
45
- return { state: 'unknown', why: 'latest completed CI run has no success or red conclusion', run: latest };
46
- }
47
-
48
- /** Only a complete starci/ci-override@1 record for this exact commit can excuse a red run. */
49
- function overrideOf(ctx, sha) {
50
- const common = run(ctx.spawn, ctx.cwd, 'git', ['rev-parse', '--git-common-dir']);
51
- if (common.error || common.status !== 0 || !String(common.stdout ?? '').trim()) return null;
52
- const file = path.join(path.resolve(ctx.cwd, String(common.stdout).trim()), 'starci-release', `${sha}.ci-override.json`);
53
- try {
54
- const record = JSON.parse(fs.readFileSync(file, 'utf8'));
55
- if (record.schema !== 'starci/ci-override@1' || record.sha !== sha || typeof record.reason !== 'string' || !record.reason.trim() || typeof record.by !== 'string' || !record.by.trim() || typeof record.at !== 'string' || !Number.isFinite(Date.parse(record.at))) return null;
56
- return record;
57
- } catch { return null; }
58
- }
59
-
60
- /** Warns on unknown CI and refuses red CI unless the owner left a valid override beside the release records. */
61
- export function requireGreenCi(ctx, ci) {
62
- if (ci.state === 'unknown') {
63
- ctx.stderr(`RELEASE_CI_UNKNOWN: ${ci.why}\n`);
64
- return [];
65
- }
66
- if (ci.state !== 'red') return [];
67
- const record = overrideOf(ctx, ci.run.headSha);
68
- if (record) return [`RELEASE_CI_RED overridden by the owner: ${record.reason}`];
69
- throw refusal('RELEASE_CI_RED', `CI run ${ci.run.url} for ${ci.run.headSha} concluded ${ci.run.conclusion}; the owner must use starci release ci-override --sha ${ci.run.headSha} --reason <text>`);
70
- }