@starci/hfs 4.12.2 → 4.13.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 (152) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +8 -9
  3. package/cache/app-families.d.mts +30 -0
  4. package/cache/app-families.mjs +94 -0
  5. package/cache/content-index.d.mts +13 -0
  6. package/cache/content-index.mjs +37 -0
  7. package/cache/db-family.d.mts +13 -0
  8. package/cache/db-family.mjs +37 -0
  9. package/cache/fake-session.d.mts +42 -0
  10. package/cache/fake-session.mjs +57 -0
  11. package/cache/fingerprint.d.mts +28 -0
  12. package/cache/fingerprint.mjs +120 -0
  13. package/cache/format-family.d.mts +13 -0
  14. package/cache/format-family.mjs +49 -0
  15. package/cache/git.d.mts +11 -0
  16. package/cache/git.mjs +13 -0
  17. package/cache/hash.d.mts +15 -0
  18. package/cache/hash.mjs +58 -0
  19. package/cache/hfs-family.d.mts +35 -0
  20. package/cache/hfs-family.mjs +164 -0
  21. package/cache/import-graph.d.mts +75 -0
  22. package/cache/import-graph.mjs +204 -0
  23. package/cache/jsonl.d.mts +31 -0
  24. package/cache/jsonl.mjs +115 -0
  25. package/cache/lint-family.d.mts +162 -0
  26. package/cache/lint-family.mjs +433 -0
  27. package/cache/order.d.mts +1 -0
  28. package/cache/order.mjs +2 -0
  29. package/cache/progress.d.mts +19 -0
  30. package/cache/progress.mjs +68 -0
  31. package/cache/quality-baselines.d.mts +109 -0
  32. package/cache/quality-baselines.mjs +235 -0
  33. package/cache/records.d.mts +33 -0
  34. package/cache/records.mjs +54 -0
  35. package/cache/report.d.mts +19 -0
  36. package/cache/report.mjs +32 -0
  37. package/cache/run-log.d.mts +17 -0
  38. package/cache/run-log.mjs +37 -0
  39. package/cache/run.d.mts +33 -0
  40. package/cache/run.mjs +65 -0
  41. package/cache/session.d.mts +29 -0
  42. package/cache/session.mjs +197 -0
  43. package/cache/side-key.d.mts +10 -0
  44. package/cache/side-key.mjs +47 -0
  45. package/cache/test-family.d.mts +130 -0
  46. package/cache/test-family.mjs +258 -0
  47. package/cache/trust.d.mts +53 -0
  48. package/cache/trust.mjs +157 -0
  49. package/cache/types-family.d.mts +74 -0
  50. package/cache/types-family.mjs +154 -0
  51. package/cache/types.d.mts +99 -0
  52. package/cache/types.mjs +8 -0
  53. package/cache/wiring-family.d.mts +14 -0
  54. package/cache/wiring-family.mjs +43 -0
  55. package/coverage/cli.d.mts +4 -2
  56. package/coverage/cli.mjs +44 -5
  57. package/db/migrate.d.mts +16 -0
  58. package/db/migrate.mjs +21 -4
  59. package/db/postgres.d.mts +23 -0
  60. package/db/postgres.mjs +32 -1
  61. package/lint/run.d.mts +47 -1
  62. package/lint/run.mjs +157 -5
  63. package/package.json +2 -1
  64. package/release/gate.d.mts +2 -0
  65. package/release/gate.mjs +4 -2
  66. package/release/records.d.mts +15 -0
  67. package/release/records.mjs +25 -0
  68. package/release/urgent.mjs +23 -4
  69. package/runtime/knowledge/grammars/core/playbook.yaml +12 -18
  70. package/runtime/knowledge/hfs/canon-pins.yaml +8 -8
  71. package/runtime/knowledge/hfs/ci-standard.yaml +29 -6
  72. package/runtime/knowledge/hfs/rules.yaml +74 -7
  73. package/runtime/knowledge/hfs/slots.yaml +19 -7
  74. package/runtime/modules/kernel/failure-codes.yaml +46 -4
  75. package/runtime/modules/schemas/application-stacks.schema.yaml +26 -0
  76. package/runtime/scripts/hfs/architecture/hfs.d.mts +6 -6
  77. package/runtime/scripts/hfs/check.mjs +1 -1
  78. package/runtime/scripts/hfs/gate-threshold.mjs +2 -1
  79. package/runtime/scripts/hfs/rules/boot-proof.d.mts +5 -0
  80. package/runtime/scripts/hfs/rules/boot-proof.mjs +15 -1
  81. package/runtime/scripts/hfs/rules/ci-custody.mjs +3 -1
  82. package/runtime/scripts/hfs/rules/ci-standard.mjs +32 -3
  83. package/runtime/scripts/hfs/rules/database-migrations.d.mts +14 -0
  84. package/runtime/scripts/hfs/rules/database-migrations.mjs +55 -0
  85. package/runtime/scripts/hfs/rules/database.mjs +2 -1
  86. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +53 -6
  87. package/runtime/scripts/hfs/rules/release-provenance.mjs +32 -4
  88. package/runtime/scripts/hfs/rules/stacks.mjs +5 -1
  89. package/runtime/scripts/hfs/rules/supply-chain.mjs +4 -1
  90. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +98 -7
  91. package/runtime/scripts/hfs/rules/unused-symbol.d.mts +9 -2
  92. package/runtime/scripts/hfs/rules/unused-symbol.mjs +15 -2
  93. package/runtime/scripts/hfs/runtime-rules/git-triggers.mjs +21 -5
  94. package/runtime/scripts/hfs/stack-deploy-config.d.mts +6 -0
  95. package/runtime/scripts/hfs/stack-deploy-config.mjs +32 -1
  96. package/runtime/scripts/hfs/stacks-gate.mjs +2 -0
  97. package/runtime/scripts/hfs/tree-findings.mjs +3 -1
  98. package/scaffold/add-inbox-lite.mjs +2 -4
  99. package/scaffold/add-queue-lite.mjs +2 -3
  100. package/scaffold/add-table.mjs +2 -4
  101. package/scaffold/migration-stamp.d.mts +11 -0
  102. package/scaffold/migration-stamp.mjs +32 -0
  103. package/src/helpers/hfs-arch-fixture.d.mts +1 -1
  104. package/src/main.d.mts +4 -1
  105. package/src/main.mjs +189 -74
  106. package/src/stack-records-double.d.mts +31 -0
  107. package/stack/cli.d.mts +3 -1
  108. package/stack/cli.mjs +17 -11
  109. package/stack/config.d.mts +6 -0
  110. package/stack/converge.d.mts +2 -0
  111. package/stack/converge.mjs +1 -1
  112. package/stack/db-phases.d.mts +185 -0
  113. package/stack/db-phases.mjs +346 -0
  114. package/stack/deploy.d.mts +18 -4
  115. package/stack/deploy.mjs +235 -70
  116. package/stack/journal.d.mts +127 -0
  117. package/stack/journal.mjs +182 -0
  118. package/stack/manager-backup.d.mts +54 -0
  119. package/stack/manager-backup.mjs +136 -0
  120. package/stack/release-store.d.mts +44 -0
  121. package/stack/release-store.mjs +89 -6
  122. package/stack/remote.d.mts +2 -0
  123. package/stack/remote.mjs +31 -11
  124. package/stack/server-key.d.mts +2 -0
  125. package/stack/server-key.mjs +1 -1
  126. package/stack/status.d.mts +49 -0
  127. package/stack/status.mjs +114 -0
  128. package/stack/urgent-floor.d.mts +10 -61
  129. package/stack/urgent-floor.mjs +28 -123
  130. package/stack/verify.mjs +26 -0
  131. package/sync/ci-commands.mjs +10 -5
  132. package/sync/format-worker.d.mts +1 -0
  133. package/sync/format-worker.mjs +17 -0
  134. package/sync/format.d.mts +21 -3
  135. package/sync/format.mjs +99 -7
  136. package/sync/index.d.mts +3 -6
  137. package/sync/index.mjs +12 -12
  138. package/sync/stack-images.d.mts +25 -10
  139. package/sync/stack-images.mjs +215 -54
  140. package/templates/app/ci-workflows/github/workflows/ci.yml +114 -7
  141. package/templates/app/ci-workflows/github/workflows/images.yml +21 -65
  142. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +130 -37
  143. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +114 -7
  144. package/templates/app/ci-workflows-lite/github/workflows/images.yml +21 -65
  145. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +130 -37
  146. package/templates/app/gitignore +1 -0
  147. package/templates/app/gitignore-lite +1 -0
  148. package/templates/app/package-scripts/db.json +1 -1
  149. package/templates/app/skeleton/.gitattributes +2 -0
  150. package/templates/app/skeleton-lite/.gitattributes +2 -0
  151. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +2 -1
  152. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +0 -63
@@ -1,33 +1,70 @@
1
1
  {{header}}
2
- # Deploys the app's Docker Swarm stack. This file only assembles arguments: the logic lives in the verbs connect, build-args, deploy,
3
- # rollback and disconnect of "starci app stack". It runs by hand only (Actions > deploy stack > Run workflow) from a release tag v*
4
- # whose version is the package.json version; it has no push or schedule trigger.
5
- # operation deploy builds and pushes one image per environments.<env>.deploy.images entry of .starcistacks/application-stacks.yaml,
6
- # then deploys their immutable digests (render, provision, converge, post-deploy tasks, verify, accept; a failure redeploys the previous release)
2
+ # Deploys the app's Docker Swarm stack. This file only assembles arguments: the logic lives in the verbs connect, deploy, rollback and disconnect
3
+ # of "starci app stack". It has two callers:
4
+ # ci.yml calls it (workflow_call) once per environment with deploy.auto, after the verdict job, with the digests of the images that run
5
+ # pushed and the CI run that proves the commit. A green release tag deploys by itself.
6
+ # by hand Actions > deploy stack > Run workflow, from a release tag v* whose version is the package.json version.
7
+ # operation deploy deploys the release of the tag: the immutable digests of its images (the run's, or the registry's sha-<sha> tags when
8
+ # dispatched), the CI evidence of the commit, then render, store, pre-pull, migrate (expand), converge, post-deploy, verify, accept
9
+ # and the contract migrations; a failure redeploys the previous accepted release
10
+ # operation redeploy the same, although the accepted release already runs this commit
7
11
  # operation rollback redeploys a stored release; "release" is 16 lowercase hex characters, empty means the previous accepted release
8
- # The environment input names a protected GitHub environment (required reviewers) that holds:
9
- # vars VPS_HOST, VPS_USER (the swarm manager); a variable named in deploy.passEnv, when the stack interpolates one
12
+ # The environment names a GitHub environment whose deployment refs are restricted to the tags v* and that holds:
13
+ # vars STARCI_DEPLOY_PAUSED (true stops the automatic deploy of that environment without a code change; a manual run still works), and a
14
+ # variable named in deploy.passEnv when the stack interpolates one
10
15
  # secrets SOPS_AGE_KEY (that environment's server identity, stored there by starci app secret server-key --env <env> --backup github --execute;
11
- # both jobs are bound to the environment, so they read it and never a repository secret of that name), VPS_SSH_KEY
12
- # (CI-only deploy key), VPS_KNOWN_HOSTS (pinned host keys)
13
- # Nothing else is a secret: images are pushed with the workflow token, which also reads the CI verdict of the tag's commit a production deploy
14
- # waits for (requireCi deploy.prod). Dispatch inputs reach scripts only through env.
16
+ # the jobs are bound to the environment, so they read it and never a repository secret of that name)
17
+ # The transport is sealed: the CI deploy key, the pinned host keys and the host are the member vps-ci under .starcistacks/<env>/secrets, opened with
18
+ # the server identity (a key of its own, never the owner's, authorised on the manager with a forced command). Nothing else is a secret: the images
19
+ # are read with the workflow token. Inputs reach scripts only through env. The log of a public repository is public: no step prints a host or a path.
15
20
  name: deploy stack
16
21
 
17
22
  on:
23
+ workflow_call:
24
+ inputs:
25
+ environment:
26
+ description: GitHub environment holding the server identity and the sealed custody under .starcistacks
27
+ required: true
28
+ type: string
29
+ operation:
30
+ description: deploy, redeploy or rollback
31
+ required: false
32
+ type: string
33
+ default: deploy
34
+ release:
35
+ description: "Rollback only: release id (16 lowercase hex) to deploy again; empty means the previous accepted release"
36
+ required: false
37
+ type: string
38
+ default: ""
39
+ images:
40
+ description: "JSON object, service -> repository@sha256:digest of the images the calling run pushed; empty reads the registry's sha-<sha> tags"
41
+ required: false
42
+ type: string
43
+ default: ""
44
+ ci_run:
45
+ description: Id of the CI run that proves the commit; empty finds the green CI run of the commit
46
+ required: false
47
+ type: string
48
+ default: ""
49
+ auto:
50
+ description: The automatic deploy of a green release tag (it stops when STARCI_DEPLOY_PAUSED is true)
51
+ required: false
52
+ type: boolean
53
+ default: false
18
54
  workflow_dispatch:
19
55
  inputs:
20
56
  environment:
21
- description: Protected environment holding the VPS variables and the sealed custody under .starcistacks
57
+ description: GitHub environment holding the server identity and the sealed custody under .starcistacks
22
58
  required: true
23
59
  type: environment
24
60
  operation:
25
- description: Deploy a new release or roll back to a stored one
61
+ description: Deploy the release of this tag, deploy it again, or roll back to a stored release
26
62
  required: true
27
63
  type: choice
28
64
  default: deploy
29
65
  options:
30
66
  - deploy
67
+ - redeploy
31
68
  - rollback
32
69
  release:
33
70
  description: "Rollback only: release id (16 lowercase hex) to deploy again; empty means the previous accepted release"
@@ -38,69 +75,125 @@ on:
38
75
  permissions:
39
76
  contents: read
40
77
 
41
- concurrency:
42
- group: stack-${{ inputs.environment }}
43
- cancel-in-progress: false
44
-
45
78
  jobs:
46
- release:
47
- if: ${{ inputs.operation == 'deploy' }}
79
+ deploy:
80
+ if: ${{ inputs.operation != 'rollback' }}
48
81
  runs-on: ubuntu-latest
49
82
  environment: ${{ inputs.environment }}
50
83
  timeout-minutes: 60
51
84
  permissions:
52
85
  contents: read
53
- packages: write
86
+ packages: read
54
87
  actions: read
88
+ concurrency:
89
+ group: stack-${{ inputs.environment }}
90
+ cancel-in-progress: false
55
91
  env:
56
92
  STACK_ENVIRONMENT: ${{ inputs.environment }}
57
93
  steps:{{stackGuard}}
58
94
  - name: require release tag
59
95
  shell: bash
60
96
  run: '[[ "$GITHUB_REF" == refs/tags/v* ]] || exit 1'
97
+ - name: deploy pause
98
+ id: pause
99
+ shell: bash
100
+ env:
101
+ AUTO: ${{ inputs.auto }}
102
+ PAUSED: ${{ vars.STARCI_DEPLOY_PAUSED }}
103
+ run: |
104
+ if [[ "$AUTO" == true && "$PAUSED" == true ]]; then
105
+ echo "paused=true" >> "$GITHUB_OUTPUT"
106
+ echo "::notice::STARCI_DEPLOY_PAUSED is true for this environment: this release was not deployed"
107
+ echo "### Deploy paused" >> "$GITHUB_STEP_SUMMARY"
108
+ echo "STARCI_DEPLOY_PAUSED is true for this environment, so the automatic deploy was skipped. Run this workflow by hand to deploy the release." >> "$GITHUB_STEP_SUMMARY"
109
+ else
110
+ echo "paused=false" >> "$GITHUB_OUTPUT"
111
+ fi
61
112
  - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
113
+ if: ${{ steps.pause.outputs.paused != 'true' }}
62
114
  - name: release tag matches package version
115
+ if: ${{ steps.pause.outputs.paused != 'true' }}
63
116
  run: node -e "const version=require('./package.json').version;if(process.env.GITHUB_REF_NAME!=='v'+version)process.exit(1)"
64
117
  - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
118
+ if: ${{ steps.pause.outputs.paused != 'true' }}
65
119
  with:
66
120
  node-version: {{nodeMajor}}
67
121
  cache: npm
68
122
  - run: npm ci --ignore-scripts
123
+ if: ${{ steps.pause.outputs.paused != 'true' }}
69
124
  - name: install SOPS
125
+ if: ${{ steps.pause.outputs.paused != 'true' }}
70
126
  uses: mdgreenwald/mozilla-sops-action@bb2d96a13ef438ad122e5827a57ebd75d6ff118d # v2.1.0
71
127
  with:
72
128
  version: v3.13.2
73
- - name: image repositories
74
- id: names
75
- shell: bash
76
- run: |
77
- owner="${GITHUB_REPOSITORY_OWNER,,}"{{stackNames}}
78
129
  - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
130
+ if: ${{ steps.pause.outputs.paused != 'true' }}
79
131
  - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
132
+ if: ${{ steps.pause.outputs.paused != 'true' }}
80
133
  with:
81
134
  registry: ghcr.io
82
135
  username: ${{ github.actor }}
83
- password: ${{ github.token }}{{stackImageSteps}}
136
+ password: ${{ github.token }}
137
+ - name: image digests
138
+ id: images
139
+ if: ${{ steps.pause.outputs.paused != 'true' }}
140
+ shell: bash
141
+ env:
142
+ IMAGES: ${{ inputs.images }}
143
+ run: |
144
+ set -euo pipefail
145
+ owner="${GITHUB_REPOSITORY_OWNER,,}"
146
+ [[ -n "$IMAGES" ]] || IMAGES='{}'
147
+ while read -r service app; do
148
+ ref=$(jq -r --arg service "$service" '.[$service] // empty' <<<"$IMAGES")
149
+ if [[ -z "$ref" ]]; then
150
+ repo="ghcr.io/${owner}/{{project}}-${app}"
151
+ digest=$(docker buildx imagetools inspect "$repo:sha-$GITHUB_SHA" --format '{{json .Manifest}}' | jq -r .digest)
152
+ ref="${repo}@${digest}"
153
+ fi
154
+ [[ "$ref" =~ ^ghcr\.io/[a-z0-9._/-]+@sha256:[0-9a-f]{64}$ ]] || { echo "::error::no immutable image for ${service}"; exit 1; }
155
+ echo "${service}=${ref}" >> "$GITHUB_OUTPUT"
156
+ done <<'LIST'
157
+ {{stackImageList}}
158
+ LIST
159
+ - name: CI run of the commit
160
+ id: ci
161
+ if: ${{ steps.pause.outputs.paused != 'true' }}
162
+ shell: bash
163
+ env:
164
+ GH_TOKEN: ${{ github.token }}
165
+ CI_RUN: ${{ inputs.ci_run }}
166
+ run: |
167
+ set -euo pipefail
168
+ if [[ -z "$CI_RUN" ]]; then
169
+ CI_RUN=$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs?head_sha=${GITHUB_SHA}&event=push&status=success&per_page=100" --jq '[.workflow_runs[].id] | first // empty')
170
+ fi
171
+ [[ "$CI_RUN" =~ ^[1-9][0-9]*$ ]] || { echo "::error::no green CI run of this commit: a release deploys only a commit the CI proved"; exit 1; }
172
+ echo "run=$CI_RUN" >> "$GITHUB_OUTPUT"
84
173
  - name: connect to the swarm manager
174
+ if: ${{ steps.pause.outputs.paused != 'true' }}
85
175
  env:
86
176
  XDG_DATA_HOME: ${{ runner.temp }}/starci-data
87
- VPS_SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
88
- VPS_KNOWN_HOSTS: ${{ secrets.VPS_KNOWN_HOSTS }}
89
- VPS_HOST: ${{ vars.VPS_HOST }}
90
- VPS_USER: ${{ vars.VPS_USER }}
177
+ SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
91
178
  run: npx --no-install starci app stack connect --env "$STACK_ENVIRONMENT" --execute
92
179
  - name: deploy the release
180
+ if: ${{ steps.pause.outputs.paused != 'true' }}
181
+ shell: bash
93
182
  env:
94
183
  XDG_DATA_HOME: ${{ runner.temp }}/starci-data
95
184
  GH_TOKEN: ${{ github.token }}
96
- SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}{{stackImageEnv}}{{stackPassEnv}}
185
+ SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
186
+ OPERATION: ${{ inputs.operation }}
187
+ CI_RUN: ${{ steps.ci.outputs.run }}{{stackImageEnv}}{{stackPassEnv}}
97
188
  run: |
189
+ again=()
190
+ [[ "$OPERATION" != redeploy ]] || again=(--redeploy)
98
191
  npx --no-install starci app stack deploy --env "$STACK_ENVIRONMENT" \{{stackImageArgs}}
99
- --sha "$GITHUB_SHA" --execute
192
+ --sha "$GITHUB_SHA" --ci-run "$CI_RUN" "${again[@]}" --execute
100
193
  - name: disconnect from the swarm manager
101
194
  env:
102
195
  XDG_DATA_HOME: ${{ runner.temp }}/starci-data
103
- if: ${{ always() }}
196
+ if: ${{ always() && steps.pause.outputs.paused != 'true' }}
104
197
  run: npx --no-install starci app stack disconnect --env "$STACK_ENVIRONMENT" --execute
105
198
 
106
199
  rollback:
@@ -111,6 +204,9 @@ jobs:
111
204
  permissions:
112
205
  contents: read
113
206
  packages: read
207
+ concurrency:
208
+ group: stack-${{ inputs.environment }}
209
+ cancel-in-progress: false
114
210
  env:
115
211
  STACK_ENVIRONMENT: ${{ inputs.environment }}
116
212
  steps:
@@ -137,10 +233,7 @@ jobs:
137
233
  - name: connect to the swarm manager
138
234
  env:
139
235
  XDG_DATA_HOME: ${{ runner.temp }}/starci-data
140
- VPS_SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
141
- VPS_KNOWN_HOSTS: ${{ secrets.VPS_KNOWN_HOSTS }}
142
- VPS_HOST: ${{ vars.VPS_HOST }}
143
- VPS_USER: ${{ vars.VPS_USER }}
236
+ SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
144
237
  run: npx --no-install starci app stack connect --env "$STACK_ENVIRONMENT" --execute
145
238
  - name: redeploy the stored release
146
239
  env:
@@ -1,25 +1,85 @@
1
1
  {{header}}
2
2
  name: ci
3
3
 
4
- # CI judges main, release candidates and release tags.
4
+ # CI judges pull requests, main, release candidates and release tags, and a green release tag deploys.
5
+ # pull_request the checks and the image builds of a change to main, with no secret and no environment (a Dependabot or fork run has none anyway);
6
+ # and no run of this workflow executes a pull request's code with a token that can write
7
+ # main judged only when no release tag points at the pushed commit (the release push sends main and its tag together; the tag run is the release run)
8
+ # candidate release/candidate-<version>: the full run of a release the tag run then reuses
9
+ # tag v* the release run: plan, checks and images in parallel, the verdict, then the deploy of every environment with deploy.auto
5
10
  on:
6
11
  push:
7
12
  branches: ["main", "release/candidate-*"]
8
13
  tags: ["v*"]
14
+ pull_request:
15
+ branches: ["main"]
9
16
  workflow_dispatch:
10
17
 
11
18
  permissions:
12
19
  contents: read
13
20
 
14
- # One run per ref: a release tag is never cancelled by another run.
21
+ # The committed check cache (.starcistacks/cache, contexts/project/check-cache.md): a run judges only what differs from the cache at its trusted base
22
+ # (the first parent of a push to main). A release candidate and a release tag judge in full and never read the cache.
23
+ env:
24
+ STARCI_APP_CHECK_CACHE: ${{ (startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/release/candidate-')) && 'full' || '' }}
25
+
26
+ # One run per ref. A pull request run is replaced by the next push to it; every other run, a release tag above all, is never cancelled.
15
27
  concurrency:
16
28
  group: ci-${{ github.ref }}
17
- cancel-in-progress: false
29
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
18
30
 
19
- # The job shape of the StarCi CI standard (knowledge/hfs/ci-standard.yaml): fast, lint per side, the unit run in shards, one coverage merge, build,
20
- # and sonar. Timeouts and the shard count come from ci.jobs of .starcistacks/application-stacks.yaml (defaults in the standard).
31
+ # The job shape of the StarCi CI standard (knowledge/hfs/ci-standard.yaml): plan, fast, lint per side, the unit run in shards, one coverage merge, build, images,
32
+ # sonar, the verdict and the deploy jobs. Timeouts and the shard count come from ci.jobs of .starcistacks/application-stacks.yaml (defaults in the standard).
33
+ # A job the plan leaves out is never a green one: the verdict job requires every job it needs to be green or, where the plan left it out on purpose, not run.
21
34
  jobs:
35
+ plan:
36
+ timeout-minutes: 10
37
+ runs-on: ubuntu-latest
38
+ permissions:
39
+ contents: read
40
+ actions: read
41
+ outputs:
42
+ checks: ${{ steps.plan.outputs.checks }}
43
+ images: ${{ steps.plan.outputs.images }}
44
+ push: ${{ steps.plan.outputs.push }}
45
+ full: ${{ steps.plan.outputs.full }}
46
+ release: ${{ steps.plan.outputs.release }}
47
+ ci_run: ${{ steps.plan.outputs.ci_run }}
48
+ steps:
49
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
50
+ with:
51
+ fetch-depth: 0
52
+ - name: plan
53
+ id: plan
54
+ shell: bash
55
+ env:
56
+ GH_TOKEN: ${{ github.token }}
57
+ EVENT: ${{ github.event_name }}
58
+ REF_TYPE: ${{ github.ref_type }}
59
+ run: |
60
+ set -euo pipefail
61
+ checks=true images=true push=false full=false release=false ci_run="$GITHUB_RUN_ID" note="checks and image builds"
62
+ if [[ "$EVENT" == push ]]; then
63
+ full=true push=true
64
+ if [[ "$REF_TYPE" == tag ]]; then
65
+ release=true
66
+ [[ "$GITHUB_REF_NAME" == "v$(node -p "require('./package.json').version")" ]] || { echo "::error::the release tag does not match the package version"; exit 1; }
67
+ reused=$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs?head_sha=${GITHUB_SHA}&event=push&status=success&per_page=100" --jq '[.workflow_runs[] | select(.head_branch | startswith("release/candidate-"))] | first | .id // empty')
68
+ if [[ -n "$reused" ]]; then checks=false ci_run="$reused" note="release run: the green candidate run $reused judged this commit; images are built and pushed here"; else note="release run: full checks and images"; fi
69
+ elif [[ "$GITHUB_REF" == refs/heads/release/candidate-* ]]; then
70
+ note="candidate run: full checks; images are built and pushed under sha-<sha>"
71
+ elif [[ -n "$(git tag --points-at "$GITHUB_SHA" --list 'v*')" ]]; then
72
+ checks=false images=false push=false full=false note="deferred: the release tag run of this commit judges it"
73
+ else
74
+ images=false push=false full=false note="main without a release tag: checks only"
75
+ fi
76
+ fi
77
+ { echo "checks=$checks"; echo "images=$images"; echo "push=$push"; echo "full=$full"; echo "release=$release"; echo "ci_run=$ci_run"; } >> "$GITHUB_OUTPUT"
78
+ echo "plan: $note" >> "$GITHUB_STEP_SUMMARY"
79
+
22
80
  fast:
81
+ needs: plan
82
+ if: ${{ needs.plan.outputs.checks == 'true' }}
23
83
  timeout-minutes: {{timeoutFast}}
24
84
  runs-on: ubuntu-latest
25
85
  steps:
@@ -27,13 +87,15 @@ jobs:
27
87
  - name: production dependency audit
28
88
  run: {{auditCommand}}
29
89
  - name: format
30
- run: npm run format:check
90
+ run: npx --no-install starci app check --format
31
91
  - name: typecheck
32
92
  run: npm run typecheck
33
93
  env:
34
94
  NODE_OPTIONS: {{nodeOptions}}
35
95
 
36
96
  lint:
97
+ needs: plan
98
+ if: ${{ needs.plan.outputs.checks == 'true' }}
37
99
  timeout-minutes: {{timeoutLint}}
38
100
  runs-on: ubuntu-latest
39
101
  strategy:
@@ -46,6 +108,8 @@ jobs:
46
108
  {{ciLintSteps}}
47
109
 
48
110
  test:
111
+ needs: plan
112
+ if: ${{ needs.plan.outputs.checks == 'true' }}
49
113
  timeout-minutes: {{timeoutTest}}
50
114
  runs-on: ubuntu-latest
51
115
  strategy:
@@ -69,7 +133,7 @@ jobs:
69
133
  path: {{shardCoverageReport}}
70
134
 
71
135
  coverage:
72
- needs: test
136
+ needs: [plan, test]
73
137
  timeout-minutes: {{timeoutCoverage}}
74
138
  runs-on: ubuntu-latest
75
139
  # Codecov OIDC authentication requires a job-scoped identity token.
@@ -96,6 +160,8 @@ jobs:
96
160
  {{codecovSteps}}
97
161
 
98
162
  build:
163
+ needs: plan
164
+ if: ${{ needs.plan.outputs.checks == 'true' }}
99
165
  timeout-minutes: {{timeoutBuild}}
100
166
  runs-on: ubuntu-latest
101
167
  steps:
@@ -110,3 +176,44 @@ jobs:
110
176
  NODE_OPTIONS: {{nodeOptions}}
111
177
 
112
178
  {{sonarJob}}
179
+ images:
180
+ needs: plan
181
+ if: ${{ needs.plan.outputs.images == 'true' }}
182
+ permissions:
183
+ contents: read
184
+ packages: write
185
+ uses: ./.github/workflows/images.yml
186
+ with:
187
+ push: ${{ needs.plan.outputs.push == 'true' }}
188
+ release: ${{ needs.plan.outputs.release == 'true' }}
189
+
190
+ verdict:
191
+ needs: [plan, fast, lint, test, coverage, build, images{{verdictSonarNeed}}]
192
+ if: ${{ always() }}
193
+ timeout-minutes: 5
194
+ runs-on: ubuntu-latest
195
+ permissions: {}
196
+ steps:
197
+ - name: verdict
198
+ shell: bash
199
+ env:
200
+ NEEDS: ${{ toJSON(needs) }}
201
+ CHECKS: ${{ needs.plan.outputs.checks }}
202
+ IMAGES: ${{ needs.plan.outputs.images }}
203
+ FULL: ${{ needs.plan.outputs.full }}
204
+ run: |
205
+ set -euo pipefail
206
+ fail=0
207
+ expect() {
208
+ got=$(jq -r --arg job "$1" '.[$job].result' <<<"$NEEDS")
209
+ case "$2" in
210
+ success) [[ "$got" == success ]] || { echo "::error::job $1 is $got, expected success"; fail=1; } ;;
211
+ idle) [[ "$got" != success && "$got" != failure && "$got" != cancelled ]] || { echo "::error::job $1 ran ($got) where the plan left it out"; fail=1; } ;;
212
+ esac
213
+ }
214
+ expect plan success
215
+ if [[ "$CHECKS" == true ]]; then want=success; else want=idle; fi
216
+ for job in fast lint test coverage build; do expect "$job" "$want"; done
217
+ if [[ "$IMAGES" == true ]]; then expect images success; else expect images idle; fi{{verdictSonarCheck}}
218
+ exit "$fail"
219
+ {{ciDeployJobs}}
@@ -1,74 +1,30 @@
1
1
  {{header}}
2
- # Builds each image declared by the lite app. The API image is required; a front-end image is emitted only when declared.
3
- # It runs on the release tag, so every image is built there, and never pushes.
4
- # When an environment declares images, the job is bound to that protected GitHub environment and opens its sealed build
5
- # arguments with that environment's SOPS_AGE_KEY (its server identity, starci app secret server-key --env <env> --backup github --execute).
2
+ # Builds every image the app declares, once per commit, and pushes the deployed ones to GHCR. ci.yml calls it next to the checks, so the images
3
+ # are built in parallel with them and a Dockerfile that no longer builds fails the same run. It has no trigger of its own.
4
+ # tags sha-<sha> on every push; the release tag v<version> is added on a release run by retagging that digest (no rebuild), so the
5
+ # image the checks ran beside is the image that ships. No mutable environment tag is ever written or deployed.
6
+ # evidence an SBOM and provenance (mode=max) are attached to the pushed digest; a build that is not pushed only proves the Dockerfile builds.
7
+ # values build arguments are public values (the browser receives them); a secret never enters an image build.
8
+ # sealed an image whose public build values live in a sealed member is built by the job bound to the release environment, on a release
9
+ # tag run only (that environment is restricted to v* tags); every other image builds without an environment or a secret.
10
+ # The workflow is read by anyone when the repository is public: it runs on GitHub-hosted runners only, and nothing it prints names a host.
6
11
  name: images
7
12
 
8
13
  on:
9
- push:
10
- tags: ["v*"]
14
+ workflow_call:
15
+ inputs:
16
+ push:
17
+ description: Push the deployed images (a push run); a pull request or a manual run only builds them
18
+ required: true
19
+ type: boolean
20
+ release:
21
+ description: A release tag run - also tag the pushed images v<version> and build the images that open sealed build values
22
+ required: true
23
+ type: boolean
24
+ {{imageWorkflowOutputs}}
11
25
 
12
26
  permissions:
13
27
  contents: read
14
28
 
15
- # One run per ref.
16
- concurrency:
17
- group: images-${{ github.ref }}
18
- cancel-in-progress: false
19
-
20
29
  jobs:
21
- image:
22
- timeout-minutes: 30
23
- runs-on: ubuntu-latest{{imageJobEnvironment}}
24
- env:
25
- STACK_ENVIRONMENT: {{imageEnvironment}}
26
- strategy:
27
- fail-fast: false
28
- matrix:
29
- include:
30
- {{imageMatrix}}
31
- steps:{{imageEnvironmentGuard}}
32
- - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
33
- - name: release tag matches package version
34
- run: node -e "const version=require('./package.json').version;if(process.env.GITHUB_REF_NAME!=='v'+version)process.exit(1)"
35
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
36
- if: {{imageBuildArgsIf}}
37
- with:
38
- node-version: {{nodeMajor}}
39
- cache: npm
40
- - run: npm ci --ignore-scripts
41
- if: {{imageBuildArgsIf}}
42
- - name: install SOPS
43
- if: {{imageBuildArgsIf}}
44
- uses: mdgreenwald/mozilla-sops-action@bb2d96a13ef438ad122e5827a57ebd75d6ff118d # v2.1.0
45
- with:
46
- version: v3.13.2
47
- - name: public build values
48
- id: args
49
- if: {{imageBuildArgsIf}}
50
- env:
51
- SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
52
- IMAGE_APP: ${{ matrix.name }}
53
- run: npx --no-install starci app stack build-args --env "$STACK_ENVIRONMENT" --app "$IMAGE_APP"
54
- - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
55
- - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
56
- with:
57
- context: .
58
- file: ${{ matrix.file }}
59
- build-args: ${{ steps.args.outputs.args }}
60
- push: false
61
- tags: {{imageProject}}/${{ matrix.name }}:${{ github.ref_name }}
62
- # OCI keeps the SBOM and provenance attestations without publishing to a registry.
63
- outputs: type=oci,dest=${{ runner.temp }}/${{ matrix.name }}.oci.tar
64
- sbom: true
65
- provenance: mode=max
66
- cache-from: type=gha,scope=${{ matrix.name }}
67
- cache-to: type=gha,scope=${{ matrix.name }},mode=max
68
- - name: retain image with SBOM and provenance
69
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
70
- with:
71
- name: image-${{ matrix.name }}-${{ github.sha }}-${{ github.run_attempt }}
72
- path: ${{ runner.temp }}/${{ matrix.name }}.oci.tar
73
- if-no-files-found: error
74
- compression-level: 0
30
+ {{imageJobs}}