@starci/hfs 4.12.0 → 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.
- package/CHANGELOG.md +43 -0
- package/README.md +8 -9
- package/cache/app-families.d.mts +30 -0
- package/cache/app-families.mjs +94 -0
- package/cache/content-index.d.mts +13 -0
- package/cache/content-index.mjs +37 -0
- package/cache/db-family.d.mts +13 -0
- package/cache/db-family.mjs +37 -0
- package/cache/fake-session.d.mts +42 -0
- package/cache/fake-session.mjs +57 -0
- package/cache/fingerprint.d.mts +28 -0
- package/cache/fingerprint.mjs +120 -0
- package/cache/format-family.d.mts +13 -0
- package/cache/format-family.mjs +49 -0
- package/cache/git.d.mts +11 -0
- package/cache/git.mjs +13 -0
- package/cache/hash.d.mts +15 -0
- package/cache/hash.mjs +58 -0
- package/cache/hfs-family.d.mts +35 -0
- package/cache/hfs-family.mjs +164 -0
- package/cache/import-graph.d.mts +75 -0
- package/cache/import-graph.mjs +204 -0
- package/cache/jsonl.d.mts +31 -0
- package/cache/jsonl.mjs +115 -0
- package/cache/lint-family.d.mts +162 -0
- package/cache/lint-family.mjs +433 -0
- package/cache/order.d.mts +1 -0
- package/cache/order.mjs +2 -0
- package/cache/progress.d.mts +19 -0
- package/cache/progress.mjs +68 -0
- package/cache/quality-baselines.d.mts +109 -0
- package/cache/quality-baselines.mjs +235 -0
- package/cache/records.d.mts +33 -0
- package/cache/records.mjs +54 -0
- package/cache/report.d.mts +19 -0
- package/cache/report.mjs +32 -0
- package/cache/run-log.d.mts +17 -0
- package/cache/run-log.mjs +37 -0
- package/cache/run.d.mts +33 -0
- package/cache/run.mjs +65 -0
- package/cache/session.d.mts +29 -0
- package/cache/session.mjs +197 -0
- package/cache/side-key.d.mts +10 -0
- package/cache/side-key.mjs +47 -0
- package/cache/test-family.d.mts +130 -0
- package/cache/test-family.mjs +258 -0
- package/cache/trust.d.mts +53 -0
- package/cache/trust.mjs +157 -0
- package/cache/types-family.d.mts +74 -0
- package/cache/types-family.mjs +154 -0
- package/cache/types.d.mts +99 -0
- package/cache/types.mjs +8 -0
- package/cache/wiring-family.d.mts +14 -0
- package/cache/wiring-family.mjs +43 -0
- package/coverage/cli.d.mts +4 -2
- package/coverage/cli.mjs +44 -5
- package/db/migrate.d.mts +16 -0
- package/db/migrate.mjs +21 -4
- package/db/postgres.d.mts +23 -0
- package/db/postgres.mjs +32 -1
- package/lint/run.d.mts +47 -1
- package/lint/run.mjs +157 -5
- package/package.json +2 -1
- package/release/gate.d.mts +2 -0
- package/release/gate.mjs +4 -2
- package/release/records.d.mts +15 -0
- package/release/records.mjs +25 -0
- package/release/urgent.mjs +23 -4
- package/runtime/knowledge/grammars/core/playbook.yaml +12 -18
- package/runtime/knowledge/hfs/canon-pins.yaml +8 -8
- package/runtime/knowledge/hfs/ci-standard.yaml +29 -6
- package/runtime/knowledge/hfs/rules.yaml +97 -7
- package/runtime/knowledge/hfs/slots.yaml +19 -7
- package/runtime/modules/kernel/failure-codes.yaml +46 -4
- package/runtime/modules/schemas/application-stacks.schema.yaml +26 -0
- package/runtime/scripts/hfs/architecture/hfs.d.mts +6 -6
- package/runtime/scripts/hfs/check.mjs +1 -1
- package/runtime/scripts/hfs/gate-threshold.mjs +2 -1
- package/runtime/scripts/hfs/rules/boot-proof.d.mts +5 -0
- package/runtime/scripts/hfs/rules/boot-proof.mjs +15 -1
- package/runtime/scripts/hfs/rules/ci-custody.mjs +3 -1
- package/runtime/scripts/hfs/rules/ci-standard.mjs +32 -3
- package/runtime/scripts/hfs/rules/database-migrations.d.mts +14 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +55 -0
- package/runtime/scripts/hfs/rules/database.mjs +2 -1
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +53 -6
- package/runtime/scripts/hfs/rules/release-provenance.mjs +32 -4
- package/runtime/scripts/hfs/rules/stacks.mjs +5 -1
- package/runtime/scripts/hfs/rules/supply-chain.mjs +4 -1
- package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +98 -7
- package/runtime/scripts/hfs/rules/unused-symbol.d.mts +9 -2
- package/runtime/scripts/hfs/rules/unused-symbol.mjs +15 -2
- package/runtime/scripts/hfs/runtime-rules/git-triggers.mjs +21 -5
- package/runtime/scripts/hfs/stack-deploy-config.d.mts +6 -0
- package/runtime/scripts/hfs/stack-deploy-config.mjs +32 -1
- package/runtime/scripts/hfs/stacks-gate.mjs +2 -0
- package/runtime/scripts/hfs/tree-findings.mjs +3 -1
- package/scaffold/add-inbox-lite.mjs +2 -4
- package/scaffold/add-queue-lite.mjs +2 -3
- package/scaffold/add-table.mjs +2 -4
- package/scaffold/migration-stamp.d.mts +11 -0
- package/scaffold/migration-stamp.mjs +32 -0
- package/src/helpers/hfs-arch-fixture.d.mts +1 -1
- package/src/main.d.mts +4 -1
- package/src/main.mjs +189 -74
- package/src/stack-records-double.d.mts +31 -0
- package/stack/cli.d.mts +3 -1
- package/stack/cli.mjs +17 -11
- package/stack/config.d.mts +6 -0
- package/stack/converge.d.mts +2 -0
- package/stack/converge.mjs +1 -1
- package/stack/db-phases.d.mts +185 -0
- package/stack/db-phases.mjs +346 -0
- package/stack/deploy.d.mts +18 -4
- package/stack/deploy.mjs +235 -70
- package/stack/journal.d.mts +127 -0
- package/stack/journal.mjs +182 -0
- package/stack/manager-backup.d.mts +54 -0
- package/stack/manager-backup.mjs +136 -0
- package/stack/release-store.d.mts +44 -0
- package/stack/release-store.mjs +89 -6
- package/stack/remote.d.mts +2 -0
- package/stack/remote.mjs +31 -11
- package/stack/server-key.d.mts +2 -0
- package/stack/server-key.mjs +1 -1
- package/stack/status.d.mts +49 -0
- package/stack/status.mjs +114 -0
- package/stack/urgent-floor.d.mts +10 -61
- package/stack/urgent-floor.mjs +28 -123
- package/stack/verify.mjs +26 -0
- package/sync/ci-commands.mjs +10 -5
- package/sync/format-worker.d.mts +1 -0
- package/sync/format-worker.mjs +17 -0
- package/sync/format.d.mts +21 -3
- package/sync/format.mjs +99 -7
- package/sync/index.d.mts +3 -6
- package/sync/index.mjs +12 -12
- package/sync/stack-images.d.mts +25 -10
- package/sync/stack-images.mjs +215 -54
- package/templates/app/ci-workflows/github/workflows/ci.yml +114 -7
- package/templates/app/ci-workflows/github/workflows/images.yml +21 -65
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +130 -37
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +114 -7
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +21 -65
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +130 -37
- package/templates/app/gitignore +1 -0
- package/templates/app/gitignore-lite +1 -0
- package/templates/app/package-scripts/db.json +1 -1
- package/templates/app/skeleton/.gitattributes +2 -0
- package/templates/app/skeleton-lite/.gitattributes +2 -0
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +2 -1
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +0 -63
package/sync/stack-images.mjs
CHANGED
|
@@ -17,13 +17,15 @@ const CONTINUE = '\\';
|
|
|
17
17
|
const NO_IMAGES_MESSAGE = 'no image is declared: declare environments.<env>.deploy (with images) in .starcistacks/application-stacks.yaml before running this workflow';
|
|
18
18
|
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value); // reason: a YAML mapping
|
|
19
19
|
const hasBuildArgs = (args) => record(args) && (Object.keys(record(args.values) ? args.values : {}).length > 0 || record(args.sealed));
|
|
20
|
+
const PLATFORM = /^linux\/(?:amd64|arm64)$/u;
|
|
21
|
+
const DEFAULT_PLATFORMS = ['linux/amd64'];
|
|
20
22
|
function environmentImages(environment) {
|
|
21
23
|
const images = environment?.deploy?.images;
|
|
22
24
|
if (!record(images))
|
|
23
25
|
return [];
|
|
24
26
|
return Object.entries(images)
|
|
25
27
|
.filter(([service, entry]) => NAME.test(service) && record(entry) && NAME.test(String(entry.app)))
|
|
26
|
-
.map(([service, entry]) => ({ service, app: entry.app, buildArgs: hasBuildArgs(entry.buildArgs) }));
|
|
28
|
+
.map(([service, entry]) => ({ service, app: entry.app, buildArgs: hasBuildArgs(entry.buildArgs), sealed: record(entry.buildArgs) && record(entry.buildArgs.sealed) }));
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
31
|
* The images of a parsed declaration, [{ service, app, buildArgs }] sorted by service: the union over its environments (the first
|
|
@@ -34,7 +36,7 @@ function declaredStackImages(declaration) {
|
|
|
34
36
|
for (const environment of Object.values(record(declaration?.environments) ? declaration.environments : {})) {
|
|
35
37
|
for (const image of environmentImages(environment)) {
|
|
36
38
|
const known = byService.get(image.service);
|
|
37
|
-
byService.set(image.service, known ? { ...known, buildArgs: known.buildArgs || image.buildArgs } : image);
|
|
39
|
+
byService.set(image.service, known ? { ...known, buildArgs: known.buildArgs || image.buildArgs, sealed: Boolean(known.sealed || image.sealed) } : image);
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
return [...byService.values()].sort((a, b) => byCodeUnit(a.service, b.service));
|
|
@@ -52,6 +54,16 @@ function imageEnvironments(declaration) {
|
|
|
52
54
|
.filter(([name, environment]) => NAME.test(name) && environmentImages(environment).length > 0)
|
|
53
55
|
.map(([name, environment]) => [name, environmentImages(environment).map(image => image.app)]));
|
|
54
56
|
}
|
|
57
|
+
/** The environments that carry a deploy block, in declared order, with the auto, approval and platforms keys (defaults: manual, no reviewers, linux/amd64). */
|
|
58
|
+
function deployEnvironments(declaration) {
|
|
59
|
+
return Object.entries(record(declaration?.environments) ? declaration.environments : {})
|
|
60
|
+
.filter(([name, environment]) => NAME.test(name) && record(environment?.deploy))
|
|
61
|
+
.map(([name, environment]) => {
|
|
62
|
+
const deploy = environment.deploy;
|
|
63
|
+
const platforms = Array.isArray(deploy.platforms) ? deploy.platforms.filter((value) => typeof value === 'string' && PLATFORM.test(value)) : [];
|
|
64
|
+
return { name, auto: deploy.auto === true, approval: deploy.approval === 'reviewers' ? 'reviewers' : 'none', platforms: platforms.length ? platforms : DEFAULT_PLATFORMS };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
55
67
|
/**
|
|
56
68
|
* What the managed workflows render from the declaration at `root`: { images, passEnv, imageEnvironments } (empty when the app has no
|
|
57
69
|
* declaration, no deploy block or a declaration that does not parse; `starci app check` judges those). `parseYaml(text)` is injected.
|
|
@@ -71,78 +83,227 @@ export function readDeclaredStack(root, { parseYaml = bundledParseYaml } = {}) {
|
|
|
71
83
|
}
|
|
72
84
|
/** Parsed declarations share the exact image, environment and pass-env projection used by sync. */
|
|
73
85
|
export function declaredStack(declaration) {
|
|
74
|
-
|
|
86
|
+
const cache = declaration?.ci?.imageCache;
|
|
87
|
+
return { images: declaredStackImages(declaration), passEnv: declaredPassEnv(declaration), imageEnvironments: imageEnvironments(declaration), environments: deployEnvironments(declaration), imageCache: cache === 'registry' ? 'registry' : 'gha' };
|
|
75
88
|
}
|
|
76
89
|
const envName = (service) => `IMAGE_${service.toUpperCase().replaceAll('-', '_')}`;
|
|
77
90
|
/** Lines placed at the end of a template line: each one starts with a newline, so nothing declared leaves nothing, not even a blank line. */
|
|
78
91
|
const block = (items, indent = INDENT) => items.map(item => `\n${indent}${item}`).join('');
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
/** Lines of a block placed on a line of their own, each indented (an empty list is the empty string). */
|
|
93
|
+
const lines = (items, indent) => items.map(item => (item === '' ? '' : `${indent}${item}`)).join('\n');
|
|
94
|
+
const CHECKOUT = 'actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0';
|
|
95
|
+
const SETUP_NODE = 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0';
|
|
96
|
+
const SETUP_BUILDX = 'docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0';
|
|
97
|
+
const SETUP_QEMU = 'docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0';
|
|
98
|
+
const DOCKER_LOGIN = 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0';
|
|
99
|
+
const BUILD_PUSH = 'docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2';
|
|
100
|
+
const INSTALL_SOPS = 'mdgreenwald/mozilla-sops-action@bb2d96a13ef438ad122e5827a57ebd75d6ff118d # v2.1.0';
|
|
101
|
+
const TAG_MATCHES_VERSION = 'node -e "const version=require(\'./package.json\').version;if(process.env.GITHUB_REF_NAME!==\'v\'+version)process.exit(1)"';
|
|
102
|
+
/**
|
|
103
|
+
* Which of the app's images need build values, and from where. The values of the release environment (the sole image environment, or production
|
|
104
|
+
* when several deploy images) are the ones an image is built with; an app that environment does not deploy keeps its Dockerfile defaults.
|
|
105
|
+
*/
|
|
106
|
+
function imagePlans(apps, { images, imageEnvironments: environments = {} }) {
|
|
107
|
+
const selected = new Set(environments[releaseEnvironment(environments)] ?? []);
|
|
108
|
+
return apps.map((app) => {
|
|
109
|
+
const own = images.filter(image => image.app === app.name);
|
|
110
|
+
const args = selected.has(app.name) && own.some(image => image.buildArgs);
|
|
111
|
+
return { ...app, deployed: own.length > 0, args, sealed: args && own.some(image => image.sealed === true), services: own.map(image => image.service) };
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const matrixEntries = (plans) => plans.flatMap(plan => [`- name: ${plan.name}`, ` file: ${plan.file}`, ` deployed: ${plan.deployed}`, ` args: ${plan.args}`]);
|
|
115
|
+
/** The platforms every deploying environment needs, the union in a stable order (default linux/amd64: the VPS's architecture). */
|
|
116
|
+
function platformsOf(environments) {
|
|
117
|
+
const all = new Set(environments.flatMap(environment => environment.platforms));
|
|
118
|
+
return all.size ? [...all].sort(byCodeUnit) : DEFAULT_PLATFORMS;
|
|
119
|
+
}
|
|
120
|
+
function imageCacheLines(cache) {
|
|
121
|
+
if (cache === 'registry')
|
|
122
|
+
return [
|
|
123
|
+
'cache-from: type=registry,ref=${{ steps.names.outputs.repo }}:buildcache',
|
|
124
|
+
"cache-to: ${{ inputs.push && format('type=registry,ref={0}:buildcache,mode=max', steps.names.outputs.repo) || '' }}",
|
|
125
|
+
];
|
|
126
|
+
return ['cache-from: type=gha,scope=${{ matrix.name }}', 'cache-to: type=gha,scope=${{ matrix.name }},mode=max'];
|
|
87
127
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
128
|
+
/** One build job of images.yml: `image` builds with public values and no environment, `image-sealed` opens the sealed build values of the release environment. */
|
|
129
|
+
function imageJob(id, plans, { project, nodeMajor, environment, platforms, cache }) {
|
|
130
|
+
const sealed = id === 'image-sealed';
|
|
131
|
+
const qemu = platforms.some(platform => platform !== 'linux/amd64');
|
|
132
|
+
const head = [
|
|
133
|
+
`${id}:`,
|
|
134
|
+
' timeout-minutes: 30',
|
|
135
|
+
' runs-on: ubuntu-latest',
|
|
136
|
+
...(sealed ? [' if: ${{ inputs.release }}', ` environment: ${environment}`] : []),
|
|
137
|
+
' permissions:', ' contents: read', ' packages: write',
|
|
138
|
+
' strategy:', ' fail-fast: false', ' matrix:', ' include:',
|
|
139
|
+
...matrixEntries(plans).map(line => ` ${line}`),
|
|
140
|
+
' steps:',
|
|
141
|
+
];
|
|
142
|
+
const steps = [
|
|
143
|
+
`- uses: ${CHECKOUT}`,
|
|
144
|
+
'- name: release tag matches package version',
|
|
145
|
+
' if: ${{ inputs.release }}',
|
|
146
|
+
' shell: bash',
|
|
147
|
+
' run: |',
|
|
148
|
+
' [[ "$GITHUB_REF" == refs/tags/v* ]] || exit 1',
|
|
149
|
+
` ${TAG_MATCHES_VERSION}`,
|
|
150
|
+
'- name: image repository',
|
|
151
|
+
' id: names',
|
|
152
|
+
' shell: bash',
|
|
153
|
+
' run: |',
|
|
154
|
+
' owner="${GITHUB_REPOSITORY_OWNER,,}"',
|
|
155
|
+
` echo "repo=ghcr.io/\${owner}/${project}-\${{ matrix.name }}" >> "$GITHUB_OUTPUT"`,
|
|
156
|
+
...(qemu ? [`- uses: ${SETUP_QEMU}`] : []),
|
|
157
|
+
`- uses: ${SETUP_BUILDX}`,
|
|
158
|
+
`- uses: ${DOCKER_LOGIN}`,
|
|
159
|
+
' if: ${{ inputs.push }}',
|
|
160
|
+
' with:', ' registry: ghcr.io', ' username: ${{ github.actor }}', ' password: ${{ github.token }}',
|
|
161
|
+
`- uses: ${SETUP_NODE}`,
|
|
162
|
+
' if: ${{ matrix.args }}',
|
|
163
|
+
' with:', ` node-version: ${nodeMajor}`, ' cache: npm',
|
|
164
|
+
'- run: npm ci --ignore-scripts',
|
|
165
|
+
' if: ${{ matrix.args }}',
|
|
166
|
+
...(sealed ? ['- name: install SOPS', ' if: ${{ matrix.args }}', ` uses: ${INSTALL_SOPS}`, ' with:', ' version: v3.13.2'] : []),
|
|
167
|
+
'- name: public build values',
|
|
168
|
+
' id: args',
|
|
169
|
+
' if: ${{ matrix.args }}',
|
|
170
|
+
' env:',
|
|
171
|
+
...(sealed ? [' SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}'] : []),
|
|
172
|
+
' IMAGE_APP: ${{ matrix.name }}',
|
|
173
|
+
` run: npx --no-install starci app stack build-args --env ${environment} --app "$IMAGE_APP"`,
|
|
174
|
+
`- uses: ${BUILD_PUSH}`,
|
|
93
175
|
' with:',
|
|
94
176
|
' context: .',
|
|
95
|
-
|
|
96
|
-
'
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
177
|
+
' file: ${{ matrix.file }}',
|
|
178
|
+
' build-args: ${{ steps.args.outputs.args }}',
|
|
179
|
+
` platforms: ${platforms.join(',')}`,
|
|
180
|
+
' push: ${{ inputs.push && matrix.deployed }}',
|
|
181
|
+
// Immutable and per commit; the release name v<version> is added below without a rebuild, so the digest the checks proved is the one that ships.
|
|
182
|
+
' tags: ${{ steps.names.outputs.repo }}:sha-${{ github.sha }}',
|
|
183
|
+
// The SBOM and the provenance are attached to the pushed digest (OCI attestations); a build that is not pushed is only a proof that the Dockerfile builds.
|
|
184
|
+
' sbom: true',
|
|
185
|
+
' provenance: mode=max',
|
|
186
|
+
...imageCacheLines(cache).map(line => ` ${line}`),
|
|
187
|
+
'- name: tag the release',
|
|
188
|
+
' if: ${{ inputs.release && inputs.push && matrix.deployed }}',
|
|
189
|
+
' shell: bash',
|
|
190
|
+
' env:',
|
|
191
|
+
' REPO: ${{ steps.names.outputs.repo }}',
|
|
192
|
+
' run: docker buildx imagetools create --tag "$REPO:$GITHUB_REF_NAME" "$REPO:sha-$GITHUB_SHA"',
|
|
102
193
|
];
|
|
194
|
+
return lines([...head, ...steps.map(step => ` ${step}`)], ' ');
|
|
195
|
+
}
|
|
196
|
+
/** The collect job: the digests this run pushed under sha-<sha>, as the JSON the deploy takes. The images of sealed build values exist only on a release run. */
|
|
197
|
+
function collectJob(plans, jobs, project) {
|
|
198
|
+
const waits = jobs.map(job => (job === 'image' ? "needs.image.result == 'success'" : "(needs.image-sealed.result == 'success' || needs.image-sealed.result == 'skipped')"));
|
|
199
|
+
const list = plans.filter(plan => plan.deployed).flatMap(plan => plan.services.map(service => `${service} ${plan.name} ${plan.sealed}`));
|
|
200
|
+
return lines([
|
|
201
|
+
'collect:',
|
|
202
|
+
` needs: [${jobs.join(', ')}]`,
|
|
203
|
+
` if: \${{ !cancelled() && inputs.push && ${waits.join(' && ')} }}`,
|
|
204
|
+
' timeout-minutes: 10',
|
|
205
|
+
' runs-on: ubuntu-latest',
|
|
206
|
+
' permissions:', ' contents: read', ' packages: read',
|
|
207
|
+
' outputs:', ' refs: ${{ steps.refs.outputs.refs }}',
|
|
208
|
+
' steps:',
|
|
209
|
+
` - uses: ${SETUP_BUILDX}`,
|
|
210
|
+
` - uses: ${DOCKER_LOGIN}`,
|
|
211
|
+
' with:', ' registry: ghcr.io', ' username: ${{ github.actor }}', ' password: ${{ github.token }}',
|
|
212
|
+
' - name: pushed digests',
|
|
213
|
+
' id: refs',
|
|
214
|
+
' shell: bash',
|
|
215
|
+
' env:', ' RELEASE: ${{ inputs.release }}',
|
|
216
|
+
' run: |',
|
|
217
|
+
' set -euo pipefail',
|
|
218
|
+
' owner="${GITHUB_REPOSITORY_OWNER,,}"',
|
|
219
|
+
" refs='{}'",
|
|
220
|
+
' while read -r service app sealed; do',
|
|
221
|
+
' if [[ "$sealed" == true && "$RELEASE" != true ]]; then continue; fi',
|
|
222
|
+
` repo="ghcr.io/\${owner}/${project}-\${app}"`,
|
|
223
|
+
" digest=$(docker buildx imagetools inspect \"$repo:sha-$GITHUB_SHA\" --format '{{json .Manifest}}' | jq -r .digest)",
|
|
224
|
+
' [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "::error::no pushed digest for the image of ${service}"; exit 1; }',
|
|
225
|
+
" refs=$(jq -c --arg service \"$service\" --arg ref \"$repo@$digest\" '. + {($service): $ref}' <<<\"$refs\")",
|
|
226
|
+
" done <<'LIST'",
|
|
227
|
+
...list.map(entry => ` ${entry}`),
|
|
228
|
+
' LIST',
|
|
229
|
+
' echo "refs=$refs" >> "$GITHUB_OUTPUT"',
|
|
230
|
+
], ' ');
|
|
103
231
|
}
|
|
104
232
|
/**
|
|
105
|
-
* The text blocks
|
|
106
|
-
*
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
233
|
+
* The text blocks of images.yml, the reusable workflow ci.yml calls. imageJobs holds the build jobs (public values; sealed values bound to the
|
|
234
|
+
* release environment, release runs only) and the collect job when an image is deployed; imageWorkflowOutputs the `refs` output of the call.
|
|
235
|
+
*/
|
|
236
|
+
function imageWorkflowVariables(stack, project, apps, nodeMajor, environment) {
|
|
237
|
+
const plans = imagePlans(apps, stack);
|
|
238
|
+
const publicPlans = plans.filter(plan => !plan.sealed);
|
|
239
|
+
const sealedPlans = plans.filter(plan => plan.sealed);
|
|
240
|
+
const options = { project, nodeMajor, environment, platforms: platformsOf(stack.environments ?? []), cache: stack.imageCache ?? 'gha' };
|
|
241
|
+
const jobs = [...(publicPlans.length ? ['image'] : []), ...(sealedPlans.length ? ['image-sealed'] : [])];
|
|
242
|
+
const collect = plans.some(plan => plan.deployed) && jobs.length > 0;
|
|
243
|
+
return {
|
|
244
|
+
imageJobs: [
|
|
245
|
+
...(publicPlans.length ? [imageJob('image', publicPlans, options)] : []),
|
|
246
|
+
...(sealedPlans.length ? [imageJob('image-sealed', sealedPlans, options)] : []),
|
|
247
|
+
...(collect ? [collectJob(plans, jobs, project)] : []),
|
|
248
|
+
].join('\n\n'),
|
|
249
|
+
imageWorkflowOutputs: collect ? lines(['outputs:', ' refs:', ' description: JSON object, service -> repository@sha256:digest of the images this run pushed', ' value: ${{ jobs.collect.outputs.refs }}'], ' ') : '',
|
|
250
|
+
imagesCollected: collect ? 'true' : '',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/** The deploy jobs of ci.yml: one call of the reusable stack-deploy.yml per environment with deploy.auto, in declared order, each after the one before it. */
|
|
254
|
+
function ciDeployVariables(stack, collected) {
|
|
255
|
+
const auto = (stack.environments ?? []).filter(environment => environment.auto);
|
|
256
|
+
const jobs = auto.map((environment, index) => {
|
|
257
|
+
const previous = index > 0 ? `, deploy-${auto[index - 1].name}` : '';
|
|
258
|
+
return lines([
|
|
259
|
+
`deploy-${environment.name}:`,
|
|
260
|
+
` needs: [plan, verdict${collected ? ', images' : ''}${previous}]`,
|
|
261
|
+
" if: ${{ needs.plan.outputs.release == 'true' }}",
|
|
262
|
+
' permissions:', ' contents: read', ' packages: read', ' actions: read',
|
|
263
|
+
' concurrency:', ` group: deploy-${environment.name}`, ' cancel-in-progress: false',
|
|
264
|
+
' uses: ./.github/workflows/stack-deploy.yml',
|
|
265
|
+
' with:',
|
|
266
|
+
` environment: ${environment.name}`,
|
|
267
|
+
' operation: deploy',
|
|
268
|
+
' auto: true',
|
|
269
|
+
...(collected ? [' images: ${{ needs.images.outputs.refs }}'] : []),
|
|
270
|
+
' ci_run: ${{ needs.plan.outputs.ci_run }}',
|
|
271
|
+
], ' ');
|
|
272
|
+
});
|
|
273
|
+
return { ciDeployJobs: jobs.length ? `\n${jobs.join('\n\n')}\n` : '' };
|
|
274
|
+
}
|
|
275
|
+
/** The release environment: the sole image environment, or production when several deploy images. */
|
|
276
|
+
function releaseEnvironment(environments) {
|
|
277
|
+
const names = Object.keys(environments);
|
|
278
|
+
return names.length > 1 ? 'production' : names[0] ?? 'production';
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The text blocks the managed workflows place at the end of a line or on a line of their own (empty when there is nothing to place):
|
|
282
|
+
* stackGuard the first step of the deploy job when the app declares no image (it fails with the reason)
|
|
283
|
+
* stackImageList the `<service> <app>` lines the image step of stack-deploy.yml reads (the registry holds sha-<sha> of each)
|
|
284
|
+
* stackImageEnv the env entries of the deploy step: IMAGE_<SERVICE> = repository@digest, from the image step
|
|
111
285
|
* stackImageArgs the `--image "<service>=$IMAGE_<SERVICE>" \` continuation lines of the deploy command
|
|
112
286
|
* stackPassEnv the env entries of the deploy step for the declared host variables (vars.<NAME>)
|
|
113
|
-
*
|
|
287
|
+
* imageJobs, imageWorkflowOutputs the build and collect jobs and the output of images.yml
|
|
288
|
+
* ciDeployJobs the deploy jobs of ci.yml (one per environment with deploy.auto)
|
|
289
|
+
* `apps` are the apps images.yml builds, each with its Dockerfile (the sync module knows the layout).
|
|
114
290
|
*/
|
|
115
|
-
export function stackWorkflowVariables({ images = [], passEnv = [], imageEnvironments: environments = {} }, project,
|
|
291
|
+
export function stackWorkflowVariables({ images = [], passEnv = [], imageEnvironments: environments = {}, environments: deploying = [], imageCache = 'gha' }, project, apps = [], nodeMajor = '22') {
|
|
116
292
|
const guard = images.length ? '' : block([
|
|
117
293
|
'- name: require a declared image',
|
|
118
294
|
' shell: bash',
|
|
119
295
|
` run: 'echo "::error::${NO_IMAGES_MESSAGE}"; exit 1'`,
|
|
120
296
|
]);
|
|
121
|
-
const
|
|
297
|
+
const stack = { images, passEnv, imageEnvironments: environments, environments: deploying, imageCache };
|
|
298
|
+
const image = imageWorkflowVariables(stack, project, apps, nodeMajor, releaseEnvironment(environments));
|
|
122
299
|
return {
|
|
123
|
-
|
|
300
|
+
imageEnvironment: releaseEnvironment(environments),
|
|
301
|
+
...image,
|
|
302
|
+
...ciDeployVariables(stack, image.imagesCollected === 'true'),
|
|
124
303
|
stackGuard: guard,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
stackImageArgs: block(images.map(image => `--image "${image.service}=$${envName(image.service)}" ${CONTINUE}`), ' '.repeat(12)),
|
|
304
|
+
stackImageList: images.map(entry => `${entry.service} ${entry.app}`).join('\n '),
|
|
305
|
+
stackImageEnv: block(images.map(entry => `${envName(entry.service)}: \${{ steps.images.outputs.${entry.service} }}`), ' '.repeat(10)),
|
|
306
|
+
stackImageArgs: block(images.map(entry => `--image "${entry.service}=$${envName(entry.service)}" ${CONTINUE}`), ' '.repeat(12)),
|
|
129
307
|
stackPassEnv: block(passEnv.map(name => `${name}: \${{ vars.${name} }}`), ' '.repeat(10)),
|
|
130
308
|
};
|
|
131
309
|
}
|
|
132
|
-
/** The matrix uses the deploy verb only for apps the selected environment deploys; undeployed images retain Dockerfile defaults. */
|
|
133
|
-
function releaseImageVariables(environments) {
|
|
134
|
-
const names = Object.keys(environments);
|
|
135
|
-
const selectedApp = names.map(name => `(env.STACK_ENVIRONMENT == '${name}' && contains(fromJSON('${JSON.stringify(environments[name])}'), matrix.name))`).join(' || ') || 'false';
|
|
136
|
-
const multiple = names.length > 1;
|
|
137
|
-
const environment = multiple ? 'production' : names[0] ?? 'production';
|
|
138
|
-
return {
|
|
139
|
-
imageEnvironment: environment,
|
|
140
|
-
imageJobEnvironment: names.length ? `\n environment: ${environment}` : '',
|
|
141
|
-
imageEnvironmentGuard: multiple ? block([
|
|
142
|
-
'- name: require a declared image environment',
|
|
143
|
-
` if: \${{ !contains(fromJSON('${JSON.stringify(names)}'), env.STACK_ENVIRONMENT) }}`,
|
|
144
|
-
' run: \'echo "::error::STACKS_DEPLOY_ENV_UNKNOWN: selected environment declares no deploy images"; exit 1\'',
|
|
145
|
-
]) : '',
|
|
146
|
-
imageBuildArgsIf: `\${{ ${selectedApp} }}`,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
@@ -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
|
-
#
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
@@ -106,3 +172,44 @@ jobs:
|
|
|
106
172
|
{{> app/ci-workflows/build-cache-save.yml}}
|
|
107
173
|
|
|
108
174
|
{{sonarJob}}
|
|
175
|
+
images:
|
|
176
|
+
needs: plan
|
|
177
|
+
if: ${{ needs.plan.outputs.images == 'true' }}
|
|
178
|
+
permissions:
|
|
179
|
+
contents: read
|
|
180
|
+
packages: write
|
|
181
|
+
uses: ./.github/workflows/images.yml
|
|
182
|
+
with:
|
|
183
|
+
push: ${{ needs.plan.outputs.push == 'true' }}
|
|
184
|
+
release: ${{ needs.plan.outputs.release == 'true' }}
|
|
185
|
+
|
|
186
|
+
verdict:
|
|
187
|
+
needs: [plan, fast, lint, test, coverage, build, images{{verdictSonarNeed}}]
|
|
188
|
+
if: ${{ always() }}
|
|
189
|
+
timeout-minutes: 5
|
|
190
|
+
runs-on: ubuntu-latest
|
|
191
|
+
permissions: {}
|
|
192
|
+
steps:
|
|
193
|
+
- name: verdict
|
|
194
|
+
shell: bash
|
|
195
|
+
env:
|
|
196
|
+
NEEDS: ${{ toJSON(needs) }}
|
|
197
|
+
CHECKS: ${{ needs.plan.outputs.checks }}
|
|
198
|
+
IMAGES: ${{ needs.plan.outputs.images }}
|
|
199
|
+
FULL: ${{ needs.plan.outputs.full }}
|
|
200
|
+
run: |
|
|
201
|
+
set -euo pipefail
|
|
202
|
+
fail=0
|
|
203
|
+
expect() {
|
|
204
|
+
got=$(jq -r --arg job "$1" '.[$job].result' <<<"$NEEDS")
|
|
205
|
+
case "$2" in
|
|
206
|
+
success) [[ "$got" == success ]] || { echo "::error::job $1 is $got, expected success"; fail=1; } ;;
|
|
207
|
+
idle) [[ "$got" != success && "$got" != failure && "$got" != cancelled ]] || { echo "::error::job $1 ran ($got) where the plan left it out"; fail=1; } ;;
|
|
208
|
+
esac
|
|
209
|
+
}
|
|
210
|
+
expect plan success
|
|
211
|
+
if [[ "$CHECKS" == true ]]; then want=success; else want=idle; fi
|
|
212
|
+
for job in fast lint test coverage build; do expect "$job" "$want"; done
|
|
213
|
+
if [[ "$IMAGES" == true ]]; then expect images success; else expect images idle; fi{{verdictSonarCheck}}
|
|
214
|
+
exit "$fail"
|
|
215
|
+
{{ciDeployJobs}}
|
|
@@ -1,74 +1,30 @@
|
|
|
1
1
|
{{header}}
|
|
2
|
-
# Builds
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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}}
|