@starci/hfs 4.4.1 → 4.5.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 +5 -0
- package/ci/cli.mjs +64 -0
- package/ci/github.mjs +41 -0
- package/ci/secret.mjs +28 -0
- package/ci/vars.mjs +33 -0
- package/ci/verify.mjs +36 -0
- package/db/backup-receipt.mjs +129 -0
- package/db/dump.mjs +66 -0
- package/db/migrate.mjs +4 -1
- package/db/reset.mjs +125 -0
- package/emit/db-types.mjs +98 -5
- package/lint/run.mjs +28 -6
- package/package.json +2 -1
- package/release/ci-red.mjs +70 -0
- package/release/gate.mjs +50 -10
- package/release/index.mjs +6 -4
- package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
- package/runtime/knowledge/hfs/rules.yaml +95 -4
- package/runtime/modules/kernel/failure-codes.yaml +60 -0
- package/runtime/modules/schemas/application-stacks.schema.yaml +131 -1
- package/runtime/packages/cli/src/catalog.generated.mjs +243 -20
- package/runtime/scripts/hfs/ci-declaration.mjs +102 -0
- package/runtime/scripts/hfs/rules/a15-findings.mjs +9 -0
- package/runtime/scripts/hfs/rules/app-workflow-findings.mjs +10 -0
- package/runtime/scripts/hfs/rules/ci-custody.mjs +104 -0
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +6 -19
- package/runtime/scripts/hfs/rules/fe-build-args.mjs +142 -0
- package/runtime/scripts/hfs/rules/supply-chain.mjs +40 -3
- package/runtime/scripts/hfs/rules/workflow-contexts.mjs +103 -0
- package/runtime/scripts/lib/secret-patterns.mjs +2 -2
- package/src/main.mjs +17 -6
- package/stack/context.mjs +13 -1
- package/stack/docker-transport.mjs +93 -15
- package/stack/release-store.mjs +12 -7
- package/stack/remote.mjs +14 -6
- package/stack/ssh-session.mjs +178 -0
- package/sync/ci-commands.mjs +103 -1
- package/sync/index.mjs +41 -16
- package/sync/quality-services.mjs +61 -0
- package/sync/readme-badges.mjs +44 -0
- package/sync/stack-images.mjs +29 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +26 -24
- package/templates/app/ci-workflows/github/workflows/images.yml +24 -2
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +8 -2
- package/templates/app/ci-workflows/install-steps.yml +21 -0
- package/templates/app/ci-workflows/sonar-steps.yml +3 -9
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +28 -35
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +2 -1
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +24 -2
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +8 -2
- package/templates/app/quality-config/codecov.yml +6 -6
- package/templates/app/quality-config/sonar-project.properties +1 -0
- package/templates/app/readme-badges/README.md +1 -0
- package/templates/app/skeleton/.github/dependabot.yml +1 -14
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +61 -3
- package/templates/app/skeleton/README.md +6 -0
- package/templates/app/skeleton-lite/.github/dependabot.yml +1 -14
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +57 -3
- package/templates/app/skeleton-lite/README.md +6 -0
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +52 -1
package/sync/index.mjs
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
// templates/<scope or common>/<managedBy>/<file path with each leading dot dropped>, with dotted skeleton paths tried when no
|
|
8
8
|
// undotted template exists. The scope is app for a root slot and the
|
|
9
9
|
// side (be, fe) for a side slot, whose files land under that side's folder. Two targets are not whole files of a managedBy slot
|
|
10
|
-
// and are listed in this module: the marked
|
|
10
|
+
// and are listed in this module: the marked blocks of the root .gitignore and README.md, and .starciwork/.gitignore, which lives inside the
|
|
11
11
|
// .starciwork directory slot. Every file is rendered with the app's hfs.json (both sides and their apps) and, for the Sonar
|
|
12
12
|
// exclusions, the jest preset the app installs for its be side and the one coverage scope derived from the slot manifest
|
|
13
13
|
// (scripts/hfs/coverage-scope.mjs: the be/jest.config.js scope, sonar.coverage.exclusions and the codecov.yml paths and components). `--check` compares the sha256 of the rendered content with the
|
|
14
|
-
// file on disk and fails on any drift; `--write` rewrites the drifted files. `.gitignore`
|
|
14
|
+
// file on disk and fails on any drift; `--write` rewrites the drifted files. `.gitignore` and README.md are shared files: only the marked
|
|
15
15
|
// block is managed and the app's own lines around it are left alone. The root package.json is managed by its `scripts` block
|
|
16
16
|
// only (mode scripts, compared as parsed JSON): the rest of the file (dependencies, the npm workspaces fe/apps/* and fe/packages/*,
|
|
17
17
|
// HFS_MONO_WORKSPACES) is the app's. The fe side runs through turbo (turbo.json, app.task-graph): each fe app and package is a workspace.
|
|
@@ -31,6 +31,8 @@ import { readDeclaredStack, stackWorkflowVariables } from './stack-images.mjs';
|
|
|
31
31
|
import { byCodeUnit } from '../report/order.mjs';
|
|
32
32
|
import { isRuntimeExampleApp } from '../runtime/scripts/lib/runtime-example.mjs';
|
|
33
33
|
import { ciWorkflowCommands } from './ci-commands.mjs';
|
|
34
|
+
import { dependabotUpdates, hasDeclaredServices, readDeclaredQuality, readDeclaredBadges, sonarHostLine } from './quality-services.mjs';
|
|
35
|
+
import { qualityServices } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
34
36
|
|
|
35
37
|
export const TEMPLATES_DIR = path.join(import.meta.dirname, '..', 'templates');
|
|
36
38
|
const NODE_MAJOR = 22;
|
|
@@ -167,7 +169,7 @@ const SOURCE_SKIPPED = new Set(['node_modules', 'dist', 'coverage']);
|
|
|
167
169
|
* lite brand tree exists, so sync can preserve its absence. `hasImage` identifies authored optional Dockerfiles without reading
|
|
168
170
|
* their content. An app with no be folder has no source files.
|
|
169
171
|
*/
|
|
170
|
-
export function appSource(root) {
|
|
172
|
+
export function appSource(root, { parseYaml } = {}) {
|
|
171
173
|
const files = [];
|
|
172
174
|
const walk = dir => {
|
|
173
175
|
let entries;
|
|
@@ -180,7 +182,9 @@ export function appSource(root) {
|
|
|
180
182
|
for (const dir of ['apps', 'src']) walk(dir);
|
|
181
183
|
return { runtimeExample: isRuntimeExampleApp(root), files: files.sort(byCodeUnit), read: file => fs.readFileSync(path.join(root, 'be', file), 'utf8'),
|
|
182
184
|
workPresent: fs.existsSync(path.join(root, '.starciwork')), hasImage: file => isFile(path.join(root, file), { followLinks: false }),
|
|
183
|
-
stack: readDeclaredStack(root) }
|
|
185
|
+
stack: readDeclaredStack(root, { parseYaml }), quality: readDeclaredQuality(root, { parseYaml }),
|
|
186
|
+
badges: readDeclaredBadges(root, { parseYaml }),
|
|
187
|
+
qualityDeclared: hasDeclaredServices(root, { parseYaml }) };
|
|
184
188
|
}
|
|
185
189
|
|
|
186
190
|
/**
|
|
@@ -299,8 +303,10 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
|
|
|
299
303
|
const packages = opensPackages(fe);
|
|
300
304
|
const lite = app.edition === 'lite';
|
|
301
305
|
const providers = (app.sides.be.connections ?? []).map(connection => connection.provider);
|
|
306
|
+
const quality = source.quality ?? qualityServices({});
|
|
302
307
|
return {
|
|
303
308
|
header: HEADER(scope),
|
|
309
|
+
readmeBadges: source.badges ?? '',
|
|
304
310
|
project: app.project,
|
|
305
311
|
appBinary: APP_BINARY,
|
|
306
312
|
appScripts: appScripts(app, source),
|
|
@@ -313,15 +319,18 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
|
|
|
313
319
|
lintPrefix: 'npm run codegen --silent && ',
|
|
314
320
|
nodeMajor: String(NODE_MAJOR),
|
|
315
321
|
sonarKey: sonarKey ?? app.project,
|
|
322
|
+
sonarHostLine: sonarHostLine(quality.sonar),
|
|
316
323
|
sonarExclusions: [presets?.sonarExclusions, '**/.next/**', '**/node_modules/**', '**/src/messages/**', ...(lite ? ['supabase/types/**'] : [])].filter(Boolean).join(','),
|
|
317
324
|
sonarSources: ['be/apps', 'be/src', 'fe/apps', ...(packages ? ['fe/packages'] : [])].join(','),
|
|
318
325
|
lcovReport: LCOV_REPORT,
|
|
319
|
-
...ciWorkflowCommands(source, LCOV_REPORT),
|
|
326
|
+
...ciWorkflowCommands(source, LCOV_REPORT, source.qualityDeclared === false ? undefined : source.quality),
|
|
320
327
|
// The coverage scope is the same in both editions: lite measures exactly what full measures (ruling:R212/case-1, amended by ruling:R212/case-4).
|
|
321
328
|
jestCoverage: scope === 'be' ? jestCoverageSource(jestCoverage(manifest, providers)) : '',
|
|
322
329
|
coverageExclusions: scope === APP_SCOPE ? sonarCoverageExclusions(manifest, providers).join(',') : '',
|
|
323
330
|
codecovPaths: scope === APP_SCOPE ? codecovPaths(manifest, providers).map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
|
|
324
331
|
codecovComponents: scope === APP_SCOPE ? componentsYaml(coverageComponents(manifest, { ...source, apps: app.sides.be.apps, providers })) : '',
|
|
332
|
+
codecovTarget: quality.codecov.target,
|
|
333
|
+
dependabotUpdates: dependabotUpdates(quality.dependabot),
|
|
325
334
|
tsconfigPaths: ['be/tsconfig.json', ...fe.apps.map(entry => `fe/apps/${entry.name}/tsconfig.json`), ...(packages ? ['fe/packages/*/tsconfig.json'] : [])].join(','),
|
|
326
335
|
styleGlob: STYLE_GLOB,
|
|
327
336
|
imageMatrix: imageMatrix(app, source),
|
|
@@ -353,10 +362,15 @@ export function renderTargets(hfs, presets, { sonarKey, readTemplate = readBundl
|
|
|
353
362
|
const hasTemplate = name => { try { return readTemplate(name) !== undefined; } catch { return false; } };
|
|
354
363
|
return SCOPES.flatMap(scope => {
|
|
355
364
|
const vars = variables(app, scope, presets, sonarKey, manifest, source);
|
|
356
|
-
const
|
|
365
|
+
const declaredTargets = targetsOf(scope, { edition: app.edition, manifest, hasTemplate });
|
|
366
|
+
if (scope === APP_SCOPE && source?.badges) declaredTargets.push({ path: 'README.md', mode: 'block', markdown: true, template: 'app/readme-badges/README.md' });
|
|
367
|
+
const targets = declaredTargets
|
|
357
368
|
.filter(target => !(app.edition === 'lite' && source?.workPresent === false && target.path === '.starciwork/.gitignore'));
|
|
358
369
|
return targets.map(target => {
|
|
359
|
-
const
|
|
370
|
+
const template = target.path === 'codecov.yml' && source?.quality?.codecov.enabled === false
|
|
371
|
+
? '{{header}}\n# Codecov is disabled in the manifest.\ncodecov:\n require_ci_to_pass: false\n'
|
|
372
|
+
: readTemplate(target.template);
|
|
373
|
+
const body = render(template, vars, readTemplate);
|
|
360
374
|
const where = onSide(scope, target.path);
|
|
361
375
|
if (target.mode === 'scripts') {
|
|
362
376
|
const scripts = parseScripts(body);
|
|
@@ -364,21 +378,22 @@ export function renderTargets(hfs, presets, { sonarKey, readTemplate = readBundl
|
|
|
364
378
|
const content = scriptsText(scripts);
|
|
365
379
|
return { path: where, scope, mode: 'scripts', content, hash: hashOf(content), scripts };
|
|
366
380
|
}
|
|
367
|
-
const
|
|
368
|
-
|
|
381
|
+
const markers = target.markdown ? { begin: '<!-- >>> starci app sync (managed block; do not edit) >>> -->', end: '<!-- <<< starci app sync <<< -->' } : { begin: BLOCK_BEGIN, end: BLOCK_END };
|
|
382
|
+
const managed = target.mode === 'block' ? `${markers.begin}\n${trailingNewline(body)}${markers.end}\n` : body;
|
|
383
|
+
return { path: where, scope, mode: target.mode, markdown: target.markdown, markers, content: managed, hash: hashOf(managed) };
|
|
369
384
|
});
|
|
370
385
|
});
|
|
371
386
|
}
|
|
372
387
|
|
|
373
388
|
// The part of a file sync manages: all of it, the marked block, or the scripts block as text.
|
|
374
|
-
function managedPart(mode, text) {
|
|
389
|
+
function managedPart(mode, text, markers = { begin: BLOCK_BEGIN, end: BLOCK_END }) {
|
|
375
390
|
if (mode === 'file') return text;
|
|
376
391
|
if (mode === 'scripts') {
|
|
377
392
|
const scripts = parseScripts(text);
|
|
378
393
|
return scripts ? scriptsText(scripts) : null;
|
|
379
394
|
}
|
|
380
|
-
const start = text.indexOf(
|
|
381
|
-
const end = text.indexOf(
|
|
395
|
+
const start = text.indexOf(markers.begin);
|
|
396
|
+
const end = text.indexOf(markers.end);
|
|
382
397
|
if (start < 0 || end < start) return null;
|
|
383
398
|
const stop = text.indexOf('\n', end);
|
|
384
399
|
return text.slice(start, stop < 0 ? text.length : stop + 1);
|
|
@@ -397,7 +412,7 @@ export function checkTargets(root, targets) {
|
|
|
397
412
|
return targets.map(target => {
|
|
398
413
|
const file = path.join(root, target.path);
|
|
399
414
|
if (!fs.existsSync(file)) return { path: target.path, status: 'missing', expectedHash: target.hash };
|
|
400
|
-
const part = managedPart(target.mode, lf(fs.readFileSync(file, 'utf8')));
|
|
415
|
+
const part = managedPart(target.mode, lf(fs.readFileSync(file, 'utf8')), target.markers);
|
|
401
416
|
if (part === null) return { path: target.path, status: 'missing', expectedHash: target.hash };
|
|
402
417
|
const actualHash = hashOf(part);
|
|
403
418
|
if (actualHash === target.hash) return { path: target.path, status: 'ok', expectedHash: target.hash, actualHash };
|
|
@@ -405,6 +420,14 @@ export function checkTargets(root, targets) {
|
|
|
405
420
|
});
|
|
406
421
|
}
|
|
407
422
|
|
|
423
|
+
function insertAfterTitle(existing, content) {
|
|
424
|
+
const title = /^# [^\n]*(?:\n|$)/m.exec(existing);
|
|
425
|
+
if (!title) return `${content}\n${existing}`;
|
|
426
|
+
const end = title.index + title[0].length;
|
|
427
|
+
const heading = trailingNewline(existing.slice(0, end));
|
|
428
|
+
return `${heading}\n${content}${existing.slice(end)}`;
|
|
429
|
+
}
|
|
430
|
+
|
|
408
431
|
/** Rewrite the missing or drifted targets; returns the paths written. */
|
|
409
432
|
function writtenContent(target, existing) {
|
|
410
433
|
if (target.mode === 'scripts') {
|
|
@@ -415,8 +438,10 @@ function writtenContent(target, existing) {
|
|
|
415
438
|
return `${JSON.stringify({ ...pkg, scripts: target.scripts }, null, 2)}\n`;
|
|
416
439
|
}
|
|
417
440
|
if (target.mode === 'block' && existing) {
|
|
418
|
-
const current = managedPart('block', existing);
|
|
419
|
-
|
|
441
|
+
const current = managedPart('block', existing, target.markers);
|
|
442
|
+
if (current !== null) return existing.replace(current, () => target.content);
|
|
443
|
+
if (target.markdown) return insertAfterTitle(existing, target.content);
|
|
444
|
+
return `${target.content}\n${existing}`;
|
|
420
445
|
}
|
|
421
446
|
return target.content;
|
|
422
447
|
}
|
|
@@ -454,7 +479,7 @@ export function loadHfs(root, manifest = loadSlotManifest()) {
|
|
|
454
479
|
export async function renderRepo(root, { presets, parseYaml, manifest = loadSlotManifest() } = {}) {
|
|
455
480
|
const hfs = loadHfs(root, manifest);
|
|
456
481
|
const sonarKey = await readDeclaredSonarKey(root, { parseYaml, fail: message => { throw new SyncError('HFS_SYNC_SONAR_KEY', message); } });
|
|
457
|
-
return { hfs, targets: renderTargets(hfs, presets ?? await loadPresets(root), { sonarKey, manifest, source:
|
|
482
|
+
return { hfs, targets: renderTargets(hfs, presets ?? await loadPresets(root), { sonarKey, manifest, source: appSource(root, { parseYaml }) }) };
|
|
458
483
|
}
|
|
459
484
|
|
|
460
485
|
/** `starci app sync --check | --write [--cwd <dir>]` at the app root; returns 0 clean, 1 drift, 2 usage. */
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseYaml as bundledParseYaml } from '../runtime/engine/yaml.mjs';
|
|
4
|
+
import { qualityServices } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
5
|
+
import { isRuntimeExampleApp } from '../runtime/scripts/lib/runtime-example.mjs';
|
|
6
|
+
import { renderReadmeBadges } from './readme-badges.mjs';
|
|
7
|
+
import { DECLARATION } from './sonar-key.mjs';
|
|
8
|
+
|
|
9
|
+
/** Apps without any service block retain the legacy CI defaults until they declare their services. */
|
|
10
|
+
export function hasDeclaredServices(root, { parseYaml = bundledParseYaml } = {}) {
|
|
11
|
+
try {
|
|
12
|
+
const doc = parseYaml(fs.readFileSync(path.join(root, DECLARATION), 'utf8'));
|
|
13
|
+
return Object.keys(doc?.services ?? {}).length > 0;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Missing or unreadable manifests preserve the shared quality defaults; example apps use the runtime example policy. */
|
|
20
|
+
export function readDeclaredQuality(root, { parseYaml = bundledParseYaml } = {}) {
|
|
21
|
+
try {
|
|
22
|
+
const doc = parseYaml(fs.readFileSync(path.join(root, DECLARATION), 'utf8'));
|
|
23
|
+
return qualityServices(doc, { example: isRuntimeExampleApp(root) });
|
|
24
|
+
} catch {
|
|
25
|
+
return qualityServices({});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** SonarCloud names its organization and public host; SonarQube uses the manifest host. */
|
|
30
|
+
export function sonarHostLine(sonar) {
|
|
31
|
+
if (!sonar) return '';
|
|
32
|
+
if (sonar.provider === 'sonarcloud') return [sonar.organization ? `sonar.organization=${sonar.organization}` : '', 'sonar.host.url=https://sonarcloud.io'].filter(Boolean).join('\n');
|
|
33
|
+
return sonar.host ? `sonar.host.url=${sonar.host}` : '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Dependabot groups minor and patch updates while majors remain per package; security updates have their own immediate group. */
|
|
37
|
+
export function dependabotUpdates(dependabot) {
|
|
38
|
+
if (!dependabot.ecosystems.length) return ' []';
|
|
39
|
+
return dependabot.ecosystems.map(ecosystem => [
|
|
40
|
+
` - package-ecosystem: ${ecosystem}`,
|
|
41
|
+
...(ecosystem === 'docker' ? [' directories:', ' - /be/apps/*', ' - /fe/apps/*'] : [' directory: /']),
|
|
42
|
+
' schedule:',
|
|
43
|
+
` interval: ${dependabot.schedule.interval}`,
|
|
44
|
+
...(dependabot.schedule.day ? [` day: ${dependabot.schedule.day}`] : []),
|
|
45
|
+
...(dependabot.schedule.time ? [` time: ${JSON.stringify(dependabot.schedule.time)}`] : []),
|
|
46
|
+
' groups:',
|
|
47
|
+
...(ecosystem === 'github-actions' ? [' actions:', ' patterns: ["*"]'] : [
|
|
48
|
+
...(dependabot.groups.minorPatch === 'grouped' ? [' minor-and-patch:', ' update-types: [minor, patch]'] : []),
|
|
49
|
+
...(dependabot.security === 'immediate' ? [' security:', ' applies-to: security-updates', ' patterns: ["*"]', ' update-types: [minor, patch]'] : []),
|
|
50
|
+
]),
|
|
51
|
+
...(ecosystem === 'npm' && dependabot.ignore.length ? [' ignore:', ...dependabot.ignore.map(name => ` - dependency-name: ${JSON.stringify(name)}`)] : []),
|
|
52
|
+
].join('\n')).join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** README badges use only public manifest fields and the package repository fallback. */
|
|
56
|
+
export function readDeclaredBadges(root, { parseYaml = bundledParseYaml } = {}) {
|
|
57
|
+
let doc = {}, pkg = {};
|
|
58
|
+
try { doc = parseYaml(fs.readFileSync(path.join(root, DECLARATION), 'utf8')); } catch { /* No declaration. */ }
|
|
59
|
+
try { pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); } catch { /* No repository fallback. */ }
|
|
60
|
+
return renderReadmeBadges(doc, { repository: pkg.repository, example: isRuntimeExampleApp(root) });
|
|
61
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { qualityServices } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
2
|
+
|
|
3
|
+
/** Package repository fallbacks must name a GitHub HTTPS or Git transport URL. */
|
|
4
|
+
function githubRepository(repository) {
|
|
5
|
+
const url = typeof repository === 'string' ? repository : repository?.url;
|
|
6
|
+
const match = /^(?:git\+)?https:\/\/github\.com\/([^/]+\/[^/#?]+)|^git@github\.com:([^/]+\/[^/#?]+)/.exec(url ?? '');
|
|
7
|
+
return (match?.[1] ?? match?.[2] ?? '').replace(/\.git$/, '');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const badge = (name, image, link) => `[](${link})`;
|
|
11
|
+
|
|
12
|
+
/** Render live CI, coverage, quality gate and Dependabot badges only when a GitHub repository is declared or inferable; no network is used. */
|
|
13
|
+
export function renderReadmeBadges(doc, { repository, example = false } = {}) {
|
|
14
|
+
const github = doc?.ci?.github ?? {};
|
|
15
|
+
const repo = github.repository ?? githubRepository(repository);
|
|
16
|
+
if (!repo) return '';
|
|
17
|
+
const branch = encodeURIComponent(github.defaultBranch ?? 'main');
|
|
18
|
+
const workflow = encodeURIComponent(github.workflow ?? 'ci.yml');
|
|
19
|
+
const base = `https://github.com/${repo}`;
|
|
20
|
+
const quality = qualityServices(doc, { example });
|
|
21
|
+
const lines = [badge('CI', `${base}/actions/workflows/${workflow}/badge.svg?branch=${branch}`, `${base}/actions/workflows/${workflow}`)];
|
|
22
|
+
if (quality.codecov.enabled) {
|
|
23
|
+
const flag = quality.codecov.flag ? `?flag=${encodeURIComponent(quality.codecov.flag)}` : '';
|
|
24
|
+
lines.push(badge('Codecov', `https://codecov.io/gh/${repo}/branch/${branch}/graph/badge.svg${flag}`, `https://codecov.io/gh/${repo}`));
|
|
25
|
+
}
|
|
26
|
+
const sonar = sonarBadge(doc, quality.sonar, example);
|
|
27
|
+
if (sonar) lines.push(sonar);
|
|
28
|
+
if (quality.dependabot.ecosystems.length) {
|
|
29
|
+
const updates = `${base}/actions/workflows/dependabot/dependabot-updates`;
|
|
30
|
+
lines.push(badge('Dependabot', `${updates}/badge.svg`, updates));
|
|
31
|
+
}
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Runtime examples use the organization-qualified key passed by examples.yml; products declare their complete project key. */
|
|
36
|
+
function sonarBadge(doc, sonar, example) {
|
|
37
|
+
if (!sonar?.projectKey) return '';
|
|
38
|
+
const host = sonar.provider === 'sonarcloud' ? 'https://sonarcloud.io' : sonar.host?.replace(/\/$/, '');
|
|
39
|
+
if (!host) return '';
|
|
40
|
+
const organization = sonar.organization ?? doc?.ci?.vars?.find(entry => entry.name === sonar.organizationVar)?.value;
|
|
41
|
+
const prefix = example && organization ? `${organization}_` : '';
|
|
42
|
+
const key = encodeURIComponent(prefix + sonar.projectKey);
|
|
43
|
+
return badge('Quality Gate', `${host}/api/project_badges/measure?project=${key}&metric=alert_status`, `${host}/dashboard?id=${key}`);
|
|
44
|
+
}
|
package/sync/stack-images.mjs
CHANGED
|
@@ -51,8 +51,15 @@ function declaredPassEnv(declaration) {
|
|
|
51
51
|
return [...new Set(names)].sort(byCodeUnit);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Release builds select the sole image environment, or production when several environments deploy images. */
|
|
55
|
+
function imageEnvironments(declaration) {
|
|
56
|
+
return Object.fromEntries(Object.entries(declaration?.environments ?? {})
|
|
57
|
+
.filter(([name, environment]) => NAME.test(name) && environmentImages(environment).length > 0)
|
|
58
|
+
.map(([name, environment]) => [name, environmentImages(environment).map(image => image.app)]));
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
/**
|
|
55
|
-
* What the managed
|
|
62
|
+
* What the managed workflows render from the declaration at `root`: { images, passEnv, imageEnvironments } (empty when the app has no
|
|
56
63
|
* declaration, no deploy block or a declaration that does not parse; `starci app check` judges those). `parseYaml(text)` is injected.
|
|
57
64
|
*/
|
|
58
65
|
export function readDeclaredStack(root, { parseYaml = bundledParseYaml } = {}) {
|
|
@@ -64,7 +71,7 @@ export function readDeclaredStack(root, { parseYaml = bundledParseYaml } = {}) {
|
|
|
64
71
|
} catch {
|
|
65
72
|
return { images: [], passEnv: [] };
|
|
66
73
|
}
|
|
67
|
-
return { images: declaredStackImages(declaration), passEnv: declaredPassEnv(declaration) };
|
|
74
|
+
return { images: declaredStackImages(declaration), passEnv: declaredPassEnv(declaration), imageEnvironments: imageEnvironments(declaration) };
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
const envName = service => `IMAGE_${service.toUpperCase().replaceAll('-', '_')}`;
|
|
@@ -109,7 +116,7 @@ function buildPushStep(image, dockerfile) {
|
|
|
109
116
|
* stackPassEnv the env entries of the deploy step for the declared host variables (vars.<NAME>)
|
|
110
117
|
* `dockerfileOf(app)` names the Dockerfile of an app (the sync module knows the layout).
|
|
111
118
|
*/
|
|
112
|
-
export function stackWorkflowVariables({ images = [], passEnv = [] }, project, dockerfileOf) {
|
|
119
|
+
export function stackWorkflowVariables({ images = [], passEnv = [], imageEnvironments: environments = {} }, project, dockerfileOf) {
|
|
113
120
|
const guard = images.length ? '' : block([
|
|
114
121
|
'- name: require a declared image',
|
|
115
122
|
' shell: bash',
|
|
@@ -117,6 +124,7 @@ export function stackWorkflowVariables({ images = [], passEnv = [] }, project, d
|
|
|
117
124
|
]);
|
|
118
125
|
const names = images.map(image => `echo "${image.service}=ghcr.io/\${owner}/${project}-${image.app}" >> "$GITHUB_OUTPUT"`);
|
|
119
126
|
return {
|
|
127
|
+
...releaseImageVariables(environments),
|
|
120
128
|
stackGuard: guard,
|
|
121
129
|
stackNames: block(names, ' '.repeat(10)),
|
|
122
130
|
stackImageSteps: images.map(image => block([...(image.buildArgs ? buildArgsStep(image) : []), ...buildPushStep(image, dockerfileOf(image.app))])).join(''),
|
|
@@ -125,3 +133,21 @@ export function stackWorkflowVariables({ images = [], passEnv = [] }, project, d
|
|
|
125
133
|
stackPassEnv: block(passEnv.map(name => `${name}: \${{ vars.${name} }}`), ' '.repeat(10)),
|
|
126
134
|
};
|
|
127
135
|
}
|
|
136
|
+
|
|
137
|
+
/** The matrix uses the deploy verb only for apps the selected environment deploys; undeployed images retain Dockerfile defaults. */
|
|
138
|
+
function releaseImageVariables(environments) {
|
|
139
|
+
const names = Object.keys(environments);
|
|
140
|
+
const selectedApp = names.map(name => `(env.STACK_ENVIRONMENT == '${name}' && contains(fromJSON('${JSON.stringify(environments[name])}'), matrix.name))`).join(' || ') || 'false';
|
|
141
|
+
const multiple = names.length > 1;
|
|
142
|
+
const environment = multiple ? 'production' : names[0] ?? 'production';
|
|
143
|
+
return {
|
|
144
|
+
imageEnvironment: environment,
|
|
145
|
+
imageJobEnvironment: names.length ? `\n environment: ${environment}` : '',
|
|
146
|
+
imageEnvironmentGuard: multiple ? block([
|
|
147
|
+
'- name: require a declared image environment',
|
|
148
|
+
` if: \${{ !contains(fromJSON('${JSON.stringify(names)}'), env.STACK_ENVIRONMENT) }}`,
|
|
149
|
+
' run: \'echo "::error::STACKS_DEPLOY_ENV_UNKNOWN: selected environment declares no deploy images"; exit 1\'',
|
|
150
|
+
]) : '',
|
|
151
|
+
imageBuildArgsIf: `\${{ ${selectedApp} }}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -15,43 +15,45 @@ concurrency:
|
|
|
15
15
|
cancel-in-progress: false
|
|
16
16
|
|
|
17
17
|
jobs:
|
|
18
|
-
|
|
18
|
+
fast:
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
|
-
|
|
21
|
-
permissions:
|
|
22
|
-
contents: read
|
|
23
|
-
id-token: write
|
|
24
|
-
env:
|
|
25
|
-
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
20
|
+
{{sonarEnv}}
|
|
26
21
|
steps:
|
|
27
|
-
|
|
28
|
-
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
29
|
-
with:
|
|
30
|
-
node-version: {{nodeMajor}}
|
|
31
|
-
cache: npm
|
|
32
|
-
- run: npm ci --ignore-scripts
|
|
22
|
+
{{> app/ci-workflows/install-steps.yml}}
|
|
33
23
|
- name: production dependency audit
|
|
34
|
-
run:
|
|
35
|
-
|
|
36
|
-
run: {{ciLintSonar}}
|
|
24
|
+
run: {{auditCommand}}
|
|
25
|
+
{{ciLintSteps}}
|
|
37
26
|
- name: format
|
|
38
27
|
run: npm run format:check
|
|
39
28
|
- name: typecheck
|
|
40
29
|
run: npm run typecheck
|
|
30
|
+
|
|
31
|
+
test:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
# Codecov OIDC authentication requires a job-scoped identity token.
|
|
34
|
+
permissions:
|
|
35
|
+
contents: read
|
|
36
|
+
id-token: write
|
|
37
|
+
steps:
|
|
38
|
+
{{> app/ci-workflows/install-steps.yml}}
|
|
41
39
|
- name: unit
|
|
42
40
|
run: npm test -- --ci
|
|
43
41
|
- name: coverage proof
|
|
44
42
|
run: {{ciCoverageProof}}
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/v') }}
|
|
43
|
+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
44
|
+
if: always() && hashFiles('{{lcovReport}}') != ''
|
|
48
45
|
with:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
name: {{lcovArtifact}}
|
|
47
|
+
path: {{lcovReport}}
|
|
48
|
+
{{codecovSteps}}
|
|
49
|
+
|
|
50
|
+
build:
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
steps:
|
|
53
|
+
{{> app/ci-workflows/install-steps.yml}}
|
|
53
54
|
- name: build be
|
|
54
55
|
run: npm run build:be
|
|
55
56
|
- name: build fe
|
|
56
57
|
run: npm run build:fe
|
|
57
|
-
|
|
58
|
+
|
|
59
|
+
{{sonarJob}}
|
|
@@ -17,21 +17,43 @@ concurrency:
|
|
|
17
17
|
|
|
18
18
|
jobs:
|
|
19
19
|
image:
|
|
20
|
-
runs-on: ubuntu-latest
|
|
20
|
+
runs-on: ubuntu-latest{{imageJobEnvironment}}
|
|
21
|
+
env:
|
|
22
|
+
STACK_ENVIRONMENT: {{imageEnvironment}}
|
|
21
23
|
strategy:
|
|
22
24
|
fail-fast: false
|
|
23
25
|
matrix:
|
|
24
26
|
include:
|
|
25
27
|
{{imageMatrix}}
|
|
26
|
-
steps:
|
|
28
|
+
steps:{{imageEnvironmentGuard}}
|
|
27
29
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
28
30
|
- name: release tag matches package version
|
|
29
31
|
run: node -e "const version=require('./package.json').version;if(process.env.GITHUB_REF_NAME!=='v'+version)process.exit(1)"
|
|
32
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
33
|
+
if: {{imageBuildArgsIf}}
|
|
34
|
+
with:
|
|
35
|
+
node-version: {{nodeMajor}}
|
|
36
|
+
cache: npm
|
|
37
|
+
- run: npm ci --ignore-scripts
|
|
38
|
+
if: {{imageBuildArgsIf}}
|
|
39
|
+
- name: install SOPS
|
|
40
|
+
if: {{imageBuildArgsIf}}
|
|
41
|
+
uses: mdgreenwald/mozilla-sops-action@bb2d96a13ef438ad122e5827a57ebd75d6ff118d # v2.1.0
|
|
42
|
+
with:
|
|
43
|
+
version: v3.13.2
|
|
44
|
+
- name: public build values
|
|
45
|
+
id: args
|
|
46
|
+
if: {{imageBuildArgsIf}}
|
|
47
|
+
env:
|
|
48
|
+
SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
|
|
49
|
+
IMAGE_APP: ${{ matrix.name }}
|
|
50
|
+
run: npx --no-install starci app stack build-args --env "$STACK_ENVIRONMENT" --app "$IMAGE_APP"
|
|
30
51
|
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
|
31
52
|
- uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
|
32
53
|
with:
|
|
33
54
|
context: .
|
|
34
55
|
file: ${{ matrix.file }}
|
|
56
|
+
build-args: ${{ steps.args.outputs.args }}
|
|
35
57
|
push: false
|
|
36
58
|
tags: {{imageProject}}/${{ matrix.name }}:${{ github.ref_name }}
|
|
37
59
|
# OCI keeps the SBOM and provenance attestations without publishing to a registry.
|
|
@@ -50,7 +50,6 @@ jobs:
|
|
|
50
50
|
packages: write
|
|
51
51
|
env:
|
|
52
52
|
STACK_ENVIRONMENT: ${{ inputs.environment }}
|
|
53
|
-
XDG_DATA_HOME: ${{ runner.temp }}/starci-data
|
|
54
53
|
steps:{{stackGuard}}
|
|
55
54
|
- name: require release tag
|
|
56
55
|
shell: bash
|
|
@@ -80,6 +79,7 @@ jobs:
|
|
|
80
79
|
password: ${{ github.token }}{{stackImageSteps}}
|
|
81
80
|
- name: connect to the swarm manager
|
|
82
81
|
env:
|
|
82
|
+
XDG_DATA_HOME: ${{ runner.temp }}/starci-data
|
|
83
83
|
VPS_SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
|
|
84
84
|
VPS_KNOWN_HOSTS: ${{ secrets.VPS_KNOWN_HOSTS }}
|
|
85
85
|
VPS_HOST: ${{ vars.VPS_HOST }}
|
|
@@ -87,11 +87,14 @@ jobs:
|
|
|
87
87
|
run: npx --no-install starci app stack connect --env "$STACK_ENVIRONMENT" --execute
|
|
88
88
|
- name: deploy the release
|
|
89
89
|
env:
|
|
90
|
+
XDG_DATA_HOME: ${{ runner.temp }}/starci-data
|
|
90
91
|
SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}{{stackImageEnv}}{{stackPassEnv}}
|
|
91
92
|
run: |
|
|
92
93
|
npx --no-install starci app stack deploy --env "$STACK_ENVIRONMENT" \{{stackImageArgs}}
|
|
93
94
|
--source-sha "$GITHUB_SHA" --execute
|
|
94
95
|
- name: disconnect from the swarm manager
|
|
96
|
+
env:
|
|
97
|
+
XDG_DATA_HOME: ${{ runner.temp }}/starci-data
|
|
95
98
|
if: ${{ always() }}
|
|
96
99
|
run: npx --no-install starci app stack disconnect --env "$STACK_ENVIRONMENT" --execute
|
|
97
100
|
|
|
@@ -105,7 +108,6 @@ jobs:
|
|
|
105
108
|
packages: read
|
|
106
109
|
env:
|
|
107
110
|
STACK_ENVIRONMENT: ${{ inputs.environment }}
|
|
108
|
-
XDG_DATA_HOME: ${{ runner.temp }}/starci-data
|
|
109
111
|
steps:
|
|
110
112
|
- name: require release tag
|
|
111
113
|
shell: bash
|
|
@@ -129,6 +131,7 @@ jobs:
|
|
|
129
131
|
password: ${{ github.token }}
|
|
130
132
|
- name: connect to the swarm manager
|
|
131
133
|
env:
|
|
134
|
+
XDG_DATA_HOME: ${{ runner.temp }}/starci-data
|
|
132
135
|
VPS_SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
|
|
133
136
|
VPS_KNOWN_HOSTS: ${{ secrets.VPS_KNOWN_HOSTS }}
|
|
134
137
|
VPS_HOST: ${{ vars.VPS_HOST }}
|
|
@@ -136,6 +139,7 @@ jobs:
|
|
|
136
139
|
run: npx --no-install starci app stack connect --env "$STACK_ENVIRONMENT" --execute
|
|
137
140
|
- name: redeploy the stored release
|
|
138
141
|
env:
|
|
142
|
+
XDG_DATA_HOME: ${{ runner.temp }}/starci-data
|
|
139
143
|
SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
|
|
140
144
|
RELEASE: ${{ inputs.release }}
|
|
141
145
|
shell: bash
|
|
@@ -144,5 +148,7 @@ jobs:
|
|
|
144
148
|
[ -z "$RELEASE" ] || release=(--release "$RELEASE")
|
|
145
149
|
npx --no-install starci app stack rollback --env "$STACK_ENVIRONMENT" "${release[@]}" --execute
|
|
146
150
|
- name: disconnect from the swarm manager
|
|
151
|
+
env:
|
|
152
|
+
XDG_DATA_HOME: ${{ runner.temp }}/starci-data
|
|
147
153
|
if: ${{ always() }}
|
|
148
154
|
run: npx --no-install starci app stack disconnect --env "$STACK_ENVIRONMENT" --execute
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
2
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
3
|
+
with:
|
|
4
|
+
node-version: {{nodeMajor}}
|
|
5
|
+
cache: npm
|
|
6
|
+
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
7
|
+
id: deps
|
|
8
|
+
with:
|
|
9
|
+
path: node_modules
|
|
10
|
+
key: ${{ runner.os }}-node{{nodeMajor}}-modules-${{ hashFiles('package-lock.json') }}
|
|
11
|
+
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
12
|
+
with:
|
|
13
|
+
path: |
|
|
14
|
+
.turbo
|
|
15
|
+
fe/apps/*/.next/cache
|
|
16
|
+
key: ${{ runner.os }}-node{{nodeMajor}}-build-${{ hashFiles('package-lock.json') }}-${{ github.sha }}
|
|
17
|
+
restore-keys: |
|
|
18
|
+
${{ runner.os }}-node{{nodeMajor}}-build-${{ hashFiles('package-lock.json') }}-
|
|
19
|
+
${{ runner.os }}-node{{nodeMajor}}-build-
|
|
20
|
+
- run: npm ci --ignore-scripts
|
|
21
|
+
if: steps.deps.outputs.cache-hit != 'true'
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
|
5
|
-
- uses: SonarSource/sonarqube-quality-gate-action@7a5fffe8e523c40e0c740b6bc2712ab503e52efa # v1.2.1
|
|
6
|
-
if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/v') && env.SONAR_TOKEN != '' }}
|
|
7
|
-
timeout-minutes: 10
|
|
8
|
-
env:
|
|
9
|
-
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
|
1
|
+
{{sonarJobHeader}}
|
|
2
|
+
{{> app/ci-workflows/install-steps.yml}}
|
|
3
|
+
{{sonarJobSteps}}
|
|
@@ -9,68 +9,61 @@ on:
|
|
|
9
9
|
permissions:
|
|
10
10
|
contents: read
|
|
11
11
|
|
|
12
|
-
# One run per ref.
|
|
12
|
+
# One run per ref: a release tag is never cancelled by another run.
|
|
13
13
|
concurrency:
|
|
14
14
|
group: ci-${{ github.ref }}
|
|
15
15
|
cancel-in-progress: false
|
|
16
16
|
|
|
17
17
|
jobs:
|
|
18
|
-
|
|
18
|
+
fast:
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
|
-
|
|
21
|
-
permissions:
|
|
22
|
-
contents: read
|
|
23
|
-
id-token: write
|
|
24
|
-
env:
|
|
25
|
-
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
20
|
+
{{sonarEnv}}
|
|
26
21
|
steps:
|
|
27
|
-
|
|
28
|
-
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
29
|
-
with:
|
|
30
|
-
node-version: {{nodeMajor}}
|
|
31
|
-
cache: npm
|
|
32
|
-
- run: npm ci --ignore-scripts
|
|
22
|
+
{{> app/ci-workflows/install-steps.yml}}
|
|
33
23
|
- name: production dependency audit
|
|
34
|
-
run:
|
|
35
|
-
|
|
36
|
-
if: env.SONAR_TOKEN == ''
|
|
37
|
-
run: {{ciLint}}
|
|
38
|
-
- name: lint with Sonar import
|
|
39
|
-
if: env.SONAR_TOKEN != ''
|
|
40
|
-
run: {{ciLintSonar}}
|
|
24
|
+
run: {{auditCommand}}
|
|
25
|
+
{{ciLintSteps}}
|
|
41
26
|
- name: format
|
|
42
27
|
run: npm run format:check
|
|
43
28
|
- name: typecheck
|
|
44
29
|
run: npm run typecheck
|
|
30
|
+
|
|
31
|
+
test:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
# Codecov OIDC authentication requires a job-scoped identity token.
|
|
34
|
+
permissions:
|
|
35
|
+
contents: read
|
|
36
|
+
id-token: write
|
|
37
|
+
steps:
|
|
38
|
+
{{> app/ci-workflows/install-steps.yml}}
|
|
45
39
|
- name: unit
|
|
46
40
|
run: npm test -- --ci
|
|
47
41
|
- name: coverage proof
|
|
48
42
|
run: {{ciCoverageProof}}
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/v') }}
|
|
43
|
+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
44
|
+
if: always() && hashFiles('{{lcovReport}}') != ''
|
|
52
45
|
with:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
name: {{lcovArtifact}}
|
|
47
|
+
path: {{lcovReport}}
|
|
48
|
+
{{codecovSteps}}
|
|
49
|
+
|
|
50
|
+
build:
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
steps:
|
|
53
|
+
{{> app/ci-workflows/install-steps.yml}}
|
|
57
54
|
- name: build be
|
|
58
55
|
run: npm run build:be
|
|
59
56
|
- name: build fe
|
|
60
57
|
run: npm run build:fe
|
|
61
58
|
- name: database migration checks
|
|
62
59
|
run: {{ciDatabaseLint}}
|
|
63
|
-
|
|
60
|
+
|
|
61
|
+
{{sonarJob}}
|
|
64
62
|
|
|
65
63
|
db-types:
|
|
66
64
|
runs-on: ubuntu-latest
|
|
67
65
|
steps:
|
|
68
|
-
|
|
69
|
-
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
70
|
-
with:
|
|
71
|
-
node-version: {{nodeMajor}}
|
|
72
|
-
cache: npm
|
|
73
|
-
- run: npm ci --ignore-scripts
|
|
66
|
+
{{> app/ci-workflows/install-steps.yml}}
|
|
74
67
|
- name: start Supabase
|
|
75
68
|
run: npm run db:start
|
|
76
69
|
- name: apply migrations
|