@starci/hfs 4.5.0 → 4.6.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 +8 -0
- package/ci/cli.mjs +6 -4
- package/ci/vars.mjs +22 -5
- package/ci/verify.mjs +16 -2
- package/lint/run.mjs +3 -4
- package/package.json +1 -1
- package/release/ci-red.mjs +44 -12
- package/release/gate.mjs +50 -19
- package/release/push.mjs +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +5 -5
- package/runtime/knowledge/sonar-gate.yaml +2 -2
- package/runtime/modules/schemas/application-stacks.schema.yaml +35 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +9 -7
- package/runtime/scripts/hfs/ci-declaration.mjs +12 -3
- package/runtime/scripts/hfs/rules/secrets.mjs +8 -10
- package/runtime/scripts/lib/secret-patterns.mjs +77 -0
- package/scaffold/app.mjs +2 -2
- package/sync/ci-commands.mjs +154 -14
- package/sync/hygiene.mjs +4 -1
- package/sync/index.mjs +16 -12
- package/sync/quality-services.mjs +7 -5
- package/sync/readme-badges.mjs +18 -2
- package/sync/stack-images.mjs +5 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +9 -3
- package/templates/app/ci-workflows/github/workflows/e2e.yml +1 -0
- package/templates/app/ci-workflows/github/workflows/images.yml +1 -0
- package/templates/app/ci-workflows/install-steps.yml +2 -0
- package/templates/app/ci-workflows/jest-cache.yml +8 -0
- package/templates/app/ci-workflows/lint-base.yml +16 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +10 -4
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +1 -0
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +1 -0
- package/templates/app/gitignore +2 -0
- package/templates/app/gitignore-lite +2 -0
- package/templates/app/skeleton/.github/dependabot.yml +1 -2
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +1 -0
- package/templates/app/skeleton-lite/.github/dependabot.yml +1 -2
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +1 -0
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.6.0 - 2026-10-18
|
|
4
|
+
- Added: `services.codecov|sonar.badgeToken` README badge tokens and `ci.github.visibility` with a private-repository notice (sync, ci verify); `ci.vars` `ownerValue: true` (var sync skips, var set reads stdin, verify checks the name).
|
|
5
|
+
- Fixed: the app release gate counts tag-push CI runs reachable from origin/main, main or HEAD; the secret scans exempt only declared badge tokens.
|
|
6
|
+
- Fixed: empty placeholder lines vanish so every managed file is prettier-stable; CI coverage proof heap, per-side Sonar lint reports merged for the import, per-fe-app build env; `app lint --side` combines with `--sonar`.
|
|
7
|
+
|
|
8
|
+
## 4.5.1 - 2026-10-18
|
|
9
|
+
- Fixed: managed CI base refs, public build values, caching and timeouts; optional quality service handling, README formatting and Supabase migration-history parsing.
|
|
10
|
+
|
|
3
11
|
## 4.5.0 - 2026-10-17
|
|
4
12
|
- Added: consumer-class CI declarations, `app ci` custody verbs and R296-R298; manifest-driven quality files and split cached CI with immutable action pins.
|
|
5
13
|
- Added: release audit/local Sonar and main-CI protection; encrypted outside-repository DB dump and armed linked reset with backup/auth/storage validation.
|
package/ci/cli.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { readCiDeclaration, sealedKeyNames } from '../runtime/scripts/hfs/ci-dec
|
|
|
5
5
|
import { github, refuse } from './github.mjs';
|
|
6
6
|
import { setSecret } from './secret.mjs';
|
|
7
7
|
import { setVariable, syncVariables } from './vars.mjs';
|
|
8
|
-
import { verifyCi } from './verify.mjs';
|
|
8
|
+
import { badgeVisibilityNotices, verifyCi } from './verify.mjs';
|
|
9
9
|
|
|
10
10
|
const COMMANDS = {
|
|
11
11
|
'secret set': ['env', 'dependabot', 'repo'],
|
|
@@ -46,16 +46,18 @@ function parse(argv) {
|
|
|
46
46
|
export async function ciMain(argv, { cwd = process.cwd(), stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), stdin, env = process.env, tools = {} } = {}) {
|
|
47
47
|
try {
|
|
48
48
|
const parsed = parse(argv);
|
|
49
|
-
let declaration, sealed;
|
|
49
|
+
let doc, declaration, sealed;
|
|
50
50
|
try {
|
|
51
|
-
|
|
51
|
+
doc = parseYaml(fs.readFileSync(path.join(cwd, '.starcistacks/application-stacks.yaml'), 'utf8'));
|
|
52
|
+
declaration = readCiDeclaration(doc);
|
|
52
53
|
sealed = sealedKeyNames(cwd, { fs });
|
|
53
54
|
} catch { throw refuse('CI_DECLARATION_INVALID', 'the manifest or sealed key names could not be read'); }
|
|
54
55
|
if (declaration.problems.length) throw refuse('CI_DECLARATION_INVALID', 'correct the CI declaration before using GitHub');
|
|
55
56
|
const context = { ...parsed, declaration, sealed, gh: github({ cwd, env, spawn: tools.spawn }), stdin, stdout };
|
|
56
57
|
if (parsed.command === 'secret set') return await setSecret(context);
|
|
57
|
-
if (parsed.command === 'var set') return setVariable(context);
|
|
58
|
+
if (parsed.command === 'var set') return await setVariable(context);
|
|
58
59
|
if (parsed.command === 'var sync') return syncVariables(context);
|
|
60
|
+
context.notices = badgeVisibilityNotices({ doc, gh: context.gh, repo: context.gh.repository(parsed.options.repo) });
|
|
59
61
|
return verifyCi(context);
|
|
60
62
|
} catch (error) {
|
|
61
63
|
stderr(`${error.code ? error.message : 'CI_USAGE: CI command could not complete'}\n`);
|
package/ci/vars.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import { refuse, scopeArgs, scopeName } from './github.mjs';
|
|
2
3
|
import { declaredEntry } from './secret.mjs';
|
|
3
4
|
|
|
@@ -15,18 +16,34 @@ function writeVariable(gh, repo, entry, value, options, stdout) {
|
|
|
15
16
|
stdout(`set variable ${entry.name} (${scopeName(options)})\n`);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
/** Owner-supplied input is read once from stdin, with one trailing newline removed and no value echoed. */
|
|
20
|
+
async function ownerValueOf(options, stdin) {
|
|
21
|
+
if (Object.hasOwn(options, 'value')) throw refuse('CI_VALUE_ARGV', 'an owner-supplied variable takes its value on stdin');
|
|
22
|
+
let value;
|
|
23
|
+
if (stdin === undefined) {
|
|
24
|
+
if (process.stdin.isTTY) throw refuse('CI_VALUE_EMPTY', 'an owner-supplied variable value is required on stdin');
|
|
25
|
+
value = fs.readFileSync(0, 'utf8');
|
|
26
|
+
} else value = typeof stdin === 'function' ? await stdin() : stdin;
|
|
27
|
+
value = String(value ?? '').replace(/\r?\n$/, '');
|
|
28
|
+
if (!value.length) throw refuse('CI_VALUE_EMPTY', 'an owner-supplied variable value is required on stdin');
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Owner-supplied variables accept stdin only; other public variables retain manifest and argv agreement. */
|
|
33
|
+
export async function setVariable({ declaration, gh, options, name, stdin, stdout }) {
|
|
34
|
+
const entry = declaredEntry(declaration.vars, name, options);
|
|
35
|
+
const value = entry.ownerValue ? await ownerValueOf(options, stdin) : valueOf(entry, options.value);
|
|
20
36
|
writeVariable(gh, gh.repository(options.repo), entry, value, options, stdout);
|
|
21
37
|
return 0;
|
|
22
38
|
}
|
|
23
39
|
|
|
24
40
|
/** Sync validates the whole plan first and performs no GitHub access until execution is requested. */
|
|
25
41
|
export function syncVariables({ declaration, gh, options, stdout }) {
|
|
26
|
-
const plan = declaration.vars.map((entry) => ({ entry, value: valueOf(entry), options: { env: entry.scope === 'environment' ? entry.environment : undefined } }));
|
|
27
|
-
const repo = options.execute && plan.
|
|
42
|
+
const plan = declaration.vars.map((entry) => ({ entry, value: entry.ownerValue ? undefined : valueOf(entry), options: { env: entry.scope === 'environment' ? entry.environment : undefined } }));
|
|
43
|
+
const repo = options.execute && plan.some((item) => !item.entry.ownerValue) ? gh.repository(options.repo) : undefined;
|
|
28
44
|
for (const item of plan) {
|
|
29
|
-
if (
|
|
45
|
+
if (item.entry.ownerValue) stdout(`skip variable ${item.entry.name} (${scopeName(item.options)}): owner-supplied value; the owner runs starci app ci var set ${item.entry.name} [--env ENV] with the value on stdin\n`);
|
|
46
|
+
else if (options.execute) writeVariable(gh, repo, item.entry, item.value, item.options, stdout);
|
|
30
47
|
else stdout(`would set variable ${item.entry.name} (${scopeName(item.options)})\n`);
|
|
31
48
|
}
|
|
32
49
|
return 0;
|
package/ci/verify.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { badgeTokenNotices } from '../sync/readme-badges.mjs';
|
|
1
2
|
import { refuse, scopeArgs, scopeName } from './github.mjs';
|
|
2
3
|
|
|
3
4
|
/** Verification reports names and scopes only; values are compared in memory and excluded from findings. */
|
|
4
|
-
export function verifyCi({ declaration, sealed, gh, options, stdout }) {
|
|
5
|
+
export function verifyCi({ declaration, sealed, gh, options, stdout, notices = [] }) {
|
|
5
6
|
const repo = gh.repository(options.repo), stores = [];
|
|
6
7
|
const environments = [...new Set([...declaration.secrets, ...declaration.vars].filter((item) => item.scope === 'environment').map((item) => item.environment))].sort();
|
|
7
8
|
for (const scope of [{}, ...environments.map((env) => ({ env })), { dependabot: true }]) {
|
|
@@ -29,8 +30,21 @@ export function verifyCi({ declaration, sealed, gh, options, stdout }) {
|
|
|
29
30
|
add(sameKind.some((entry) => entry.name === row.name) ? 'CI_SCOPE_MISMATCH' : store.kind === 'secret' ? 'CI_SECRET_UNDECLARED' : 'CI_VAR_UNDECLARED', row.name, store.scope);
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
|
-
if (options.json) stdout(`${JSON.stringify({ ok: findings.length === 0, findings }, null, 2)}\n`);
|
|
33
|
+
if (options.json) stdout(`${JSON.stringify({ ok: findings.length === 0, findings, notices }, null, 2)}\n`);
|
|
33
34
|
else if (!findings.length) stdout('CI verification passed\n');
|
|
34
35
|
else for (const finding of findings) stdout(`${finding.code}: ${finding.name} (${finding.scope})\n`);
|
|
36
|
+
if (!options.json) for (const notice of notices) stdout(`${notice}\n`);
|
|
35
37
|
return findings.length ? 1 : 0;
|
|
36
38
|
}
|
|
39
|
+
|
|
40
|
+
/** Manifest visibility wins; optional GitHub visibility discovery never changes verification findings or errors. */
|
|
41
|
+
export function badgeVisibilityNotices({ doc, gh, repo }) {
|
|
42
|
+
let visibility = doc?.ci?.github?.visibility;
|
|
43
|
+
if (visibility === undefined) {
|
|
44
|
+
try {
|
|
45
|
+
const remote = gh.json(['repo', 'view', '--json', 'visibility', '--repo', repo]);
|
|
46
|
+
visibility = ['PRIVATE', 'INTERNAL'].includes(remote?.visibility) ? 'private' : 'public';
|
|
47
|
+
} catch { return []; }
|
|
48
|
+
}
|
|
49
|
+
return badgeTokenNotices({ ...doc, ci: { ...doc?.ci, github: { ...doc?.ci?.github, visibility } } }, { repository: `https://github.com/${repo}` });
|
|
50
|
+
}
|
package/lint/run.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import { STYLE_GLOB } from '../sync/index.mjs';
|
|
|
26
26
|
import { byCodeUnit } from '../report/order.mjs';
|
|
27
27
|
import { braceVariants, globExpression } from '../runtime/scripts/lib/glob.mjs';
|
|
28
28
|
|
|
29
|
-
// --side judges only that side; root HFS findings belong to be,
|
|
29
|
+
// --side judges only that side; root HFS findings belong to be, its Sonar report stays on that side.
|
|
30
30
|
export const LINT_SCHEMA = 'starci/lint@1';
|
|
31
31
|
/** The side whose stylesheets stylelint judges. */
|
|
32
32
|
const STYLE_SIDE = 'fe';
|
|
@@ -42,11 +42,10 @@ export function workspaceOf(workspace) {
|
|
|
42
42
|
return rel;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/** Side scopes partition the full lint; Sonar
|
|
45
|
+
/** Side scopes partition the full lint; Sonar reports share that scope and a frontend workspace cannot belong to be. */
|
|
46
46
|
function validateSide(opts) {
|
|
47
47
|
if (opts.side === undefined) return;
|
|
48
48
|
if (!SIDES.includes(opts.side)) throw new Error('LINT_SIDE_INVALID: --side is be or fe');
|
|
49
|
-
if (opts.sonar !== undefined) throw new Error('LINT_SIDE_SONAR_CONFLICT: --side and --sonar do not combine');
|
|
50
49
|
if (opts.side === 'be' && opts.workspace !== undefined) throw new Error('LINT_SIDE_WORKSPACE_CONFLICT: --side be cannot judge a frontend workspace');
|
|
51
50
|
}
|
|
52
51
|
|
|
@@ -312,7 +311,7 @@ export async function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles =
|
|
|
312
311
|
|
|
313
312
|
let properties = '';
|
|
314
313
|
try { properties = fs.readFileSync(path.join(repoRoot, 'sonar-project.properties'), 'utf8'); } catch { /* no properties: every finding keeps its own path */ }
|
|
315
|
-
const sourceRoots = sourceRootsOf(properties);
|
|
314
|
+
const sourceRoots = sourceRootsOf(properties).filter((root) => findingOnSide({ path: root }, opts.side));
|
|
316
315
|
const tracked = sourceRoots.length ? trackedFiles(repoRoot) : [];
|
|
317
316
|
const sonar = mergeReports([
|
|
318
317
|
sonarReport(kept, { sourceRoots, tracked }),
|
package/package.json
CHANGED
package/release/ci-red.mjs
CHANGED
|
@@ -20,29 +20,61 @@ function runsOf(result) {
|
|
|
20
20
|
} catch { return null; }
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Resolve each available default-branch reference once, including HEAD in detached release worktrees. */
|
|
24
|
+
function referenceTips(spawn, cwd, defaultBranch) {
|
|
25
|
+
const refs = [`origin/${defaultBranch}`, defaultBranch, 'HEAD'];
|
|
26
|
+
return refs.flatMap((name) => {
|
|
27
|
+
const result = run(spawn, cwd, 'git', ['rev-parse', '--verify', '--quiet', `${name}^{commit}`]);
|
|
28
|
+
const tip = String(result.stdout ?? '').trim();
|
|
29
|
+
return !result.error && result.status === 0 && SHA.test(tip) ? [{ name, tip }] : [];
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Equality proves reachability directly; ancestry is unknown only when every available probe errors. */
|
|
34
|
+
function pushAncestry(spawn, cwd, row, refs) {
|
|
35
|
+
if (refs.some((ref) => ref.tip === row.headSha)) return { candidate: true, unknown: false };
|
|
36
|
+
let known = false;
|
|
37
|
+
for (const ref of refs) {
|
|
38
|
+
const result = run(spawn, cwd, 'git', ['merge-base', '--is-ancestor', row.headSha, ref.tip]);
|
|
39
|
+
if (!result.error && result.status === 0) return { candidate: true, unknown: false };
|
|
40
|
+
if (!result.error && result.status === 1) known = true;
|
|
41
|
+
}
|
|
42
|
+
return { candidate: false, unknown: !known };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Branch rows remain candidates; push rows must reach a resolved reference, and each databaseId counts once. */
|
|
46
|
+
function candidateRuns(spawn, cwd, branch, pushes, refs) {
|
|
47
|
+
const candidates = branch.filter((row) => row && SHA.test(row.headSha));
|
|
48
|
+
let ancestryUnknown = refs.length === 0;
|
|
49
|
+
for (const row of pushes) {
|
|
50
|
+
if (!row || !SHA.test(row.headSha) || row.event !== 'push') continue;
|
|
51
|
+
const ancestry = pushAncestry(spawn, cwd, row, refs);
|
|
52
|
+
if (ancestry.candidate) candidates.push(row);
|
|
53
|
+
if (ancestry.unknown) ancestryUnknown = true;
|
|
54
|
+
}
|
|
55
|
+
const unique = new Map();
|
|
56
|
+
for (const row of candidates) if (!unique.has(row.databaseId)) unique.set(row.databaseId, row);
|
|
57
|
+
return { candidates: [...unique.values()], ancestryUnknown };
|
|
58
|
+
}
|
|
59
|
+
|
|
23
60
|
/** Reads branch runs plus push/tag runs whose commits are reachable from the default branch; the newest completed run decides, even if a newer run is pending. */
|
|
24
61
|
export function latestDefaultBranchCi({ spawn, cwd, defaultBranch = 'main' }) {
|
|
62
|
+
const refs = referenceTips(spawn, cwd, defaultBranch);
|
|
63
|
+
const unknown = (why) => ({ state: 'unknown', why: `${why} (refs: ${refs.map((ref) => ref.name).join(', ') || 'none'})` });
|
|
25
64
|
const args = ['run', 'list', '--workflow', 'ci.yml'];
|
|
26
65
|
const tail = ['--limit', '20', '--json', FIELDS];
|
|
27
66
|
const branch = runsOf(run(spawn, cwd, 'gh', [...args, '--branch', defaultBranch, ...tail]));
|
|
28
|
-
if (!branch) return
|
|
67
|
+
if (!branch) return unknown('gh branch runs unavailable (missing gh, authentication, or unreadable response)');
|
|
29
68
|
const pushes = runsOf(run(spawn, cwd, 'gh', [...args, '--event', 'push', ...tail]));
|
|
30
|
-
if (!pushes) return
|
|
31
|
-
const candidates =
|
|
32
|
-
let ancestryUnknown = false;
|
|
33
|
-
for (const row of pushes) {
|
|
34
|
-
if (!row || !SHA.test(row.headSha) || row.event !== 'push') continue;
|
|
35
|
-
const ancestor = run(spawn, cwd, 'git', ['merge-base', '--is-ancestor', row.headSha, defaultBranch]);
|
|
36
|
-
if (!ancestor.error && ancestor.status === 0) candidates.push(row);
|
|
37
|
-
else if (ancestor.error || ancestor.status !== 1) ancestryUnknown = true;
|
|
38
|
-
}
|
|
69
|
+
if (!pushes) return unknown('gh push runs unavailable (missing gh, authentication, or unreadable response)');
|
|
70
|
+
const { candidates, ancestryUnknown } = candidateRuns(spawn, cwd, branch, pushes, refs);
|
|
39
71
|
const completed = candidates.filter((row) => row.status === 'completed' && Number.isFinite(Date.parse(row.createdAt)));
|
|
40
72
|
completed.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt) || Number(b.databaseId) - Number(a.databaseId));
|
|
41
73
|
const latest = completed[0];
|
|
42
|
-
if (!latest) return
|
|
74
|
+
if (!latest) return unknown(ancestryUnknown ? 'cannot establish push run ancestry on the default branch' : 'no completed CI run on the default branch');
|
|
43
75
|
if (latest.conclusion === 'success') return { state: 'green', run: latest };
|
|
44
76
|
if (RED.has(latest.conclusion)) return { state: 'red', run: latest };
|
|
45
|
-
return {
|
|
77
|
+
return { ...unknown('latest completed CI run has no success or red conclusion'), run: latest };
|
|
46
78
|
}
|
|
47
79
|
|
|
48
80
|
/** Only a complete starci/ci-override@1 record for this exact commit can excuse a red run. */
|
package/release/gate.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// gate.mjs - `starci app release gate`: the honest release gate. It runs the app's own gates on a clean HEAD and
|
|
1
|
+
// gate.mjs - `starci app release gate`: the honest release gate. It runs the app's own gates on a clean HEAD and when all required gates pass it
|
|
2
2
|
// writes the L4 record <git-common-dir>/starci-release/<HEAD sha>.l4.json (head, tag, per-gate log path and sha256) and creates the annotated tag.
|
|
3
3
|
// The canon pre-push hook lets main and v* tags through only for a HEAD with that tag and that record. Nothing is pushed here (see push.mjs).
|
|
4
4
|
// The gate list comes from the optional `release:` block of .starcistacks/application-stacks.yaml, else from the app's managed package scripts.
|
|
@@ -13,6 +13,7 @@ import { failure, refusal } from '../stack/errors.mjs';
|
|
|
13
13
|
import { latestDefaultBranchCi, requireGreenCi } from './ci-red.mjs';
|
|
14
14
|
|
|
15
15
|
export const RELEASE_CODE = 'RELEASE_CONFIG_INVALID';
|
|
16
|
+
const SONAR_UNREACHABLE = 'SONAR_UNREACHABLE';
|
|
16
17
|
|
|
17
18
|
const FIELDS = new Set(['gates', 'tagPrefix', 'defaultBranch', 'tagMatchesVersion']);
|
|
18
19
|
const GATE_FIELDS = new Set(['name', 'command', 'summary', 'env']);
|
|
@@ -63,14 +64,15 @@ function auditGate(audit) {
|
|
|
63
64
|
|
|
64
65
|
/** A requested local Sonar scan uses only the declared host and token variable; the token never enters argv. */
|
|
65
66
|
function sonarGate(services, env) {
|
|
66
|
-
const
|
|
67
|
+
const configured = services?.sonar;
|
|
68
|
+
const sonar = configured?.provider === 'sonarcloud' ? { ...configured, host: 'https://sonarcloud.io' } : configured;
|
|
67
69
|
const tokenName = sonar?.token;
|
|
68
70
|
if (!sonar?.host || !ENV_KEY.test(tokenName) || !env[tokenName]) throw refusal('RELEASE_SONAR_UNAVAILABLE', 'declare an enabled services.sonar host and set its ci.token environment variable');
|
|
69
71
|
const command = ['npx', '--yes', '@sonar/scan@4.3.0', `-Dsonar.host.url=${sonar.host}`];
|
|
70
72
|
const organization = sonar.organization ?? (sonar.organizationVar ? env[sonar.organizationVar] : null);
|
|
71
73
|
if (organization) command.push(`-Dsonar.organization=${organization}`);
|
|
72
74
|
if (!command.every((word) => TOKEN.test(word))) throw refusal('RELEASE_SONAR_UNAVAILABLE', 'Sonar host and organization must be plain command arguments');
|
|
73
|
-
return { name: 'sonar', command, summary: null, env: { SONAR_TOKEN: env[tokenName] } };
|
|
75
|
+
return { name: 'sonar', command, summary: null, env: { SONAR_TOKEN: env[tokenName] }, provider: sonar };
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
function gateProblems(entry, at, seen) {
|
|
@@ -209,6 +211,32 @@ export function launch(argv, platform) {
|
|
|
209
211
|
return [argv[0], argv.slice(1)];
|
|
210
212
|
}
|
|
211
213
|
|
|
214
|
+
/** Sonar availability is public and bounded; HTTP uses the existing injected fetch seam. */
|
|
215
|
+
async function sonarAvailability(ctx, gate) {
|
|
216
|
+
const sonar = gate.provider;
|
|
217
|
+
if (!sonar) return null;
|
|
218
|
+
let up = false;
|
|
219
|
+
try {
|
|
220
|
+
const response = await ctx.fetch(sonar.host.replace(/\/$/u, '') + '/api/system/status', { signal: AbortSignal.timeout(10000) });
|
|
221
|
+
up = response.ok && (await response.json()).status === 'UP';
|
|
222
|
+
} catch { up = false; }
|
|
223
|
+
if (up) return null;
|
|
224
|
+
const detail = sonar.optional
|
|
225
|
+
? `notice: sonar skipped: ${sonar.host} not reachable (services.sonar.optional)`
|
|
226
|
+
: `${SONAR_UNREACHABLE}: ${sonar.host} did not report UP within ten seconds`;
|
|
227
|
+
return { skipped: sonar.optional === true, pass: false, detail };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Unreachable optional providers remain explicit skipped rows, with a real log and no scan invocation. */
|
|
231
|
+
async function runAvailableGate(ctx, gate, options) {
|
|
232
|
+
const availability = await sonarAvailability(ctx, gate);
|
|
233
|
+
if (!availability) return runGate(ctx, gate, options);
|
|
234
|
+
const log = path.join(options.dir, `${gate.name}.log`);
|
|
235
|
+
fs.writeFileSync(log, availability.detail + '\n');
|
|
236
|
+
return { name: gate.name, command: gate.command.join(' '), ...availability, seconds: 0,
|
|
237
|
+
summary: null, log: path.resolve(log), logSha256: sha256(log), env: Object.keys(gate.env) };
|
|
238
|
+
}
|
|
239
|
+
|
|
212
240
|
function runGate(ctx, gate, { dir, platform }) {
|
|
213
241
|
const log = path.join(dir, `${gate.name}.log`);
|
|
214
242
|
const started = ctx.now();
|
|
@@ -270,7 +298,7 @@ function inspect(ctx, config, options) {
|
|
|
270
298
|
}
|
|
271
299
|
|
|
272
300
|
function planLines(config, facts, dir, recordPath) {
|
|
273
|
-
const out = [`L4 record ${recordPath}`, `logs ${dir}`, '', 'gates (
|
|
301
|
+
const out = [`L4 record ${recordPath}`, `logs ${dir}`, '', 'gates (required gates must pass, optional provider outages are skipped, in this order):'];
|
|
274
302
|
for (const gate of config.gates) {
|
|
275
303
|
const summary = gate.summary ? ` (summary: ${gate.summary})` : '';
|
|
276
304
|
out.push(` ${gate.name.padEnd(17)} ${gate.command.join(' ')}${summary}`);
|
|
@@ -289,28 +317,31 @@ function assertUnmoved(ctx, head) {
|
|
|
289
317
|
function writeRecord(ctx, { head, tag, results, recordPath }) {
|
|
290
318
|
const record = {
|
|
291
319
|
schema: 'starci-app-l4-record/1', head, tag, createdAt: new Date(ctx.now()).toISOString(), producedBy: 'starci app release gate',
|
|
292
|
-
note: '
|
|
320
|
+
note: 'Required gates passed on a clean HEAD; optional provider skips are explicit and every entry links its log and sha256.',
|
|
293
321
|
gates: results,
|
|
294
322
|
};
|
|
295
323
|
fs.mkdirSync(path.dirname(recordPath), { recursive: true });
|
|
296
324
|
fs.writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}\n`);
|
|
297
325
|
}
|
|
298
326
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
results.
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
return results;
|
|
327
|
+
/** Gates run sequentially because each failure prevents subsequent commands and record creation. */
|
|
328
|
+
async function runAll(ctx, config, options) {
|
|
329
|
+
fs.mkdirSync(options.dir, { recursive: true });
|
|
330
|
+
return config.gates.reduce(async (pending, gate) => {
|
|
331
|
+
const results = await pending;
|
|
332
|
+
if (results.some((result) => !result.pass && !result.skipped)) return results;
|
|
333
|
+
return [...results, await runAvailableGate(ctx, gate, options)];
|
|
334
|
+
}, Promise.resolve([]));
|
|
308
335
|
}
|
|
309
336
|
|
|
310
|
-
|
|
337
|
+
function summaryOf(result) {
|
|
338
|
+
let status = result.pass ? 'PASS' : 'FAIL';
|
|
339
|
+
if (result.skipped) status = 'SKIPPED';
|
|
340
|
+
return `${status} ${result.name} (${result.detail}, ${result.seconds}s) log ${result.log}`;
|
|
341
|
+
}
|
|
311
342
|
|
|
312
343
|
/**
|
|
313
|
-
* `starci app release gate`: dry run prints the preconditions, the gate list, the head and the tag; --execute runs every gate and, only if
|
|
344
|
+
* `starci app release gate`: dry run prints the preconditions, the gate list, the head and the tag; --execute runs every gate and, only if required gates pass,
|
|
314
345
|
* writes the L4 record and creates the annotated tag. options: { execute, sonar, source, tagPrefix, platform }.
|
|
315
346
|
*/
|
|
316
347
|
export async function releaseGate(ctx, config, options = {}) {
|
|
@@ -325,14 +356,14 @@ export async function releaseGate(ctx, config, options = {}) {
|
|
|
325
356
|
const base = { head: facts.head, tag: facts.chosen.tag, record: recordPath, gates: config.gates.map((gate) => gate.name), ci: { state: ci.state, notices: ciLines } };
|
|
326
357
|
if (facts.red.length > 0) return { ok: false, lines: [...plan, ...facts.red.map((item) => `RED: ${item}`), 'nothing was run.'], data: { ...base, red: facts.red, executed: false } };
|
|
327
358
|
if (options.execute !== true) return { ok: true, lines: [...plan, '', 'dry run: no gate was run, nothing written. preconditions OK.'], data: { ...base, red: [], executed: false } };
|
|
328
|
-
const results = runAll(ctx, config, { dir, platform: options.platform ?? process.platform });
|
|
359
|
+
const results = await runAll(ctx, config, { dir, platform: options.platform ?? process.platform });
|
|
329
360
|
const report = results.map(summaryOf);
|
|
330
|
-
const failed = results.filter((result) => !result.pass);
|
|
361
|
+
const failed = results.filter((result) => !result.pass && !result.skipped);
|
|
331
362
|
if (failed.length > 0) return { ok: false, lines: [...plan, ...report, `gate(s) red: ${failed.map((item) => item.name).join(', ')}. No L4 record, no tag.`], data: { ...base, executed: true, results } };
|
|
332
363
|
assertUnmoved(ctx, facts.head);
|
|
333
364
|
writeRecord(ctx, { head: facts.head, tag: facts.chosen.tag, results, recordPath });
|
|
334
365
|
const created = facts.chosen.state !== 'on-head';
|
|
335
|
-
if (created) git(ctx, ['tag', '-a', facts.chosen.tag, '-m', `release ${facts.chosen.tag}: gates
|
|
366
|
+
if (created) git(ctx, ['tag', '-a', facts.chosen.tag, '-m', `release ${facts.chosen.tag}: required gates passed on ${facts.head.slice(0, 12)} (L4 record ${path.basename(recordPath)})`, facts.head]);
|
|
336
367
|
const tagged = created ? `annotated tag ${facts.chosen.tag} created on ${facts.head.slice(0, 10)}` : `annotated tag ${facts.chosen.tag} already on ${facts.head.slice(0, 10)}`;
|
|
337
368
|
return { ok: true, lines: [...plan, ...report, `L4 record written: ${recordPath}`, tagged], data: { ...base, executed: true, results, tagCreated: created } };
|
|
338
369
|
}
|
package/release/push.mjs
CHANGED
|
@@ -14,7 +14,7 @@ function readRecord(file) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const greenRecord = (record, head, tag) => record?.head === head && record?.tag === tag
|
|
17
|
-
&& Array.isArray(record.gates) && record.gates.length > 0 && record.gates.every((gate) => gate?.pass === true);
|
|
17
|
+
&& Array.isArray(record.gates) && record.gates.length > 0 && record.gates.every((gate) => gate?.pass === true || (gate?.name === 'sonar' && gate?.skipped === true));
|
|
18
18
|
|
|
19
19
|
/** The release gate as the hook reads it: the annotated tag on HEAD whose L4 record names head and tag. Returns { tag } or { reason }. */
|
|
20
20
|
function gateState(ctx, head, prefix) {
|
|
@@ -29,14 +29,14 @@ pins:
|
|
|
29
29
|
source: packages/grammar/package.json
|
|
30
30
|
why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
|
|
31
31
|
'@starci/eslint-canon-be':
|
|
32
|
-
version: 3.4.
|
|
32
|
+
version: 3.4.4
|
|
33
33
|
group: starci
|
|
34
34
|
install: registry
|
|
35
35
|
side: be
|
|
36
36
|
source: packages/eslint/be/package.json
|
|
37
37
|
why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
|
|
38
38
|
'@starci/eslint-canon-fe':
|
|
39
|
-
version: 8.4.
|
|
39
|
+
version: 8.4.4
|
|
40
40
|
group: starci
|
|
41
41
|
install: registry
|
|
42
42
|
side: fe
|
|
@@ -62,7 +62,7 @@ pins:
|
|
|
62
62
|
side: both
|
|
63
63
|
source: packages/prettier-config/package.json
|
|
64
64
|
'@starci/jest-preset':
|
|
65
|
-
version: 2.3.
|
|
65
|
+
version: 2.3.1
|
|
66
66
|
group: starci
|
|
67
67
|
install: registry
|
|
68
68
|
side: be
|
|
@@ -76,14 +76,14 @@ pins:
|
|
|
76
76
|
source: packages/test-world/package.json
|
|
77
77
|
why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
|
|
78
78
|
'@starci/cli':
|
|
79
|
-
version: 1.
|
|
79
|
+
version: 1.6.0
|
|
80
80
|
group: starci
|
|
81
81
|
install: registry
|
|
82
82
|
side: both
|
|
83
83
|
source: packages/cli/package.json
|
|
84
84
|
why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
|
|
85
85
|
'@starci/hfs':
|
|
86
|
-
version: 4.
|
|
86
|
+
version: 4.6.0
|
|
87
87
|
group: starci
|
|
88
88
|
install: registry
|
|
89
89
|
side: both
|
|
@@ -41,8 +41,8 @@ newCode:
|
|
|
41
41
|
minReviewedPercent: 100
|
|
42
42
|
unreviewedMax: 0
|
|
43
43
|
# The whole code, not only the new code. Every finding of the HFS canon reaches Sonar through ONE entry, `starci app lint`
|
|
44
|
-
# (packages/hfs, contract changes hfs-sonar-import and hfs-lint-entry)
|
|
45
|
-
#
|
|
44
|
+
# (packages/hfs, contract changes hfs-sonar-import and hfs-lint-entry). CI runs it per side with --side and --sonar,
|
|
45
|
+
# then merges rules by id and joins issues into ONE file, reports/lint.sonar.json, named by sonar.externalIssuesReportPaths; the gate holds the open-issue count of
|
|
46
46
|
# the whole project at zero. The file carries three engines:
|
|
47
47
|
# starci-hfs the repository findings of `starci app check` (managed files, tree, pins, CI, contracts; the rule id is the finding code)
|
|
48
48
|
# eslint ESLint's json over the repository (the BE and FE canon plugins alike, per-file and project-graph rules)
|
|
@@ -149,6 +149,16 @@ $defs:
|
|
|
149
149
|
target: {type: number, minimum: 0, maximum: 100}
|
|
150
150
|
components: {enum: [auto]}
|
|
151
151
|
required: {type: boolean}
|
|
152
|
+
optional:
|
|
153
|
+
description: Skip an unreachable quality provider with a notice; SonarQube defaults true, SonarCloud and Codecov false.
|
|
154
|
+
type: boolean
|
|
155
|
+
badgeToken:
|
|
156
|
+
description: >-
|
|
157
|
+
codecov and sonar only: a read-only, public-safe badge token (Codecov graphing token, Sonar project badge token)
|
|
158
|
+
that the README badge of a private repository needs. It is public configuration rendered into README.md, never a
|
|
159
|
+
secret; a CI token or any write credential never goes here.
|
|
160
|
+
type: string
|
|
161
|
+
pattern: '^[A-Za-z0-9_-]{6,128}$'
|
|
152
162
|
ecosystems: {type: array, items: {enum: [npm, docker, github-actions]}}
|
|
153
163
|
schedule:
|
|
154
164
|
type: object
|
|
@@ -272,6 +282,9 @@ $defs:
|
|
|
272
282
|
repository: {type: string, pattern: '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'}
|
|
273
283
|
defaultBranch: {type: string, minLength: 1, default: main}
|
|
274
284
|
workflow: {type: string, pattern: '^[A-Za-z0-9_.-]+\.ya?ml$', default: ci.yml}
|
|
285
|
+
visibility:
|
|
286
|
+
description: The GitHub repository visibility; private READMEs need services.<id>.badgeToken for live badges. Omitted means unknown.
|
|
287
|
+
enum: [public, private]
|
|
275
288
|
secrets: {type: array, items: {$ref: '#/$defs/ciSecret'}}
|
|
276
289
|
vars: {type: array, items: {$ref: '#/$defs/ciVariable'}}
|
|
277
290
|
dependabot:
|
|
@@ -307,6 +320,9 @@ $defs:
|
|
|
307
320
|
purpose: {type: string}
|
|
308
321
|
optional: {type: boolean}
|
|
309
322
|
ciVariable:
|
|
323
|
+
description: >-
|
|
324
|
+
A public CI variable either carries its literal value, or declares ownerValue true (no value) when only the owner
|
|
325
|
+
may supply it; var sync skips an owner-supplied value and ci verify checks that it exists by name.
|
|
310
326
|
anyOf:
|
|
311
327
|
- type: object
|
|
312
328
|
additionalProperties: false
|
|
@@ -327,6 +343,25 @@ $defs:
|
|
|
327
343
|
value: {type: string}
|
|
328
344
|
purpose: {type: string}
|
|
329
345
|
optional: {type: boolean}
|
|
346
|
+
- type: object
|
|
347
|
+
additionalProperties: false
|
|
348
|
+
required: [name, scope, ownerValue]
|
|
349
|
+
properties:
|
|
350
|
+
name: {$ref: '#/$defs/ciName'}
|
|
351
|
+
scope: {const: repository}
|
|
352
|
+
ownerValue: {const: true}
|
|
353
|
+
purpose: {type: string}
|
|
354
|
+
optional: {type: boolean}
|
|
355
|
+
- type: object
|
|
356
|
+
additionalProperties: false
|
|
357
|
+
required: [name, scope, ownerValue, environment]
|
|
358
|
+
properties:
|
|
359
|
+
name: {$ref: '#/$defs/ciName'}
|
|
360
|
+
scope: {const: environment}
|
|
361
|
+
environment: {type: string, pattern: '^[a-z][a-z0-9-]*$'}
|
|
362
|
+
ownerValue: {const: true}
|
|
363
|
+
purpose: {type: string}
|
|
364
|
+
optional: {type: boolean}
|
|
330
365
|
serviceStack:
|
|
331
366
|
description: >-
|
|
332
367
|
Which stack defines a local service - either a project-owned .starcistacks stack
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GENERATED by scripts/cli/gen-catalog.mjs — do not edit
|
|
2
2
|
export const CATALOG = {
|
|
3
3
|
"schema": "starci/cli-catalog@1",
|
|
4
|
-
"hash": "
|
|
4
|
+
"hash": "93fd9007c93fe7430043d8b6682fd46dd0e1d5f9f9b7094fc94ffad2ccdcceeb",
|
|
5
5
|
"global": [
|
|
6
6
|
{
|
|
7
7
|
"name": "json",
|
|
@@ -224,12 +224,12 @@ export const CATALOG = {
|
|
|
224
224
|
{
|
|
225
225
|
"name": "value",
|
|
226
226
|
"type": "string",
|
|
227
|
-
"summary": "public variable value that must agree with the manifest"
|
|
227
|
+
"summary": "public variable value that must agree with the manifest; var set ownerValue requires stdin instead"
|
|
228
228
|
},
|
|
229
229
|
{
|
|
230
230
|
"name": "execute",
|
|
231
231
|
"type": "boolean",
|
|
232
|
-
"summary": "apply the variable sync plan"
|
|
232
|
+
"summary": "apply the variable sync plan; var sync skips ownerValue variables"
|
|
233
233
|
},
|
|
234
234
|
{
|
|
235
235
|
"name": "json",
|
|
@@ -7317,12 +7317,12 @@ export const CATALOG = {
|
|
|
7317
7317
|
{
|
|
7318
7318
|
"name": "dev",
|
|
7319
7319
|
"type": "boolean",
|
|
7320
|
-
"summary": "save the packages as development dependencies"
|
|
7320
|
+
"summary": "save the CLI and explicit packages as development dependencies"
|
|
7321
7321
|
},
|
|
7322
7322
|
{
|
|
7323
7323
|
"name": "canon",
|
|
7324
7324
|
"type": "boolean",
|
|
7325
|
-
"summary": "install the target CLI and
|
|
7325
|
+
"summary": "install the target CLI and move its declared exact StarCi canon pins"
|
|
7326
7326
|
}
|
|
7327
7327
|
],
|
|
7328
7328
|
"exit": {
|
|
@@ -7353,10 +7353,12 @@ export const CATALOG = {
|
|
|
7353
7353
|
"bare package names resolve only from knowledge/hfs/canon-pins.yaml and never through a registry lookup",
|
|
7354
7354
|
"explicit versions use exact x.y.z form and must agree with the canon pin when one exists, except the target CLI version on a canon upgrade",
|
|
7355
7355
|
"naming @starci/cli or passing --canon installs the exact target CLI first,",
|
|
7356
|
-
"then reads its installed HFS runtime canon-pins.yaml and
|
|
7356
|
+
"then reads its installed HFS runtime canon-pins.yaml and, in the same host lock,",
|
|
7357
|
+
"moves each other pinned @starci package only where a root or workspace package.json declares it, in its own section",
|
|
7357
7358
|
"--canon without a CLI positional uses the running runtime's exact CLI pin as the target",
|
|
7358
7359
|
"target pins never fall back to the running runtime when the target pin file is missing or inconsistent",
|
|
7359
|
-
"each target StarCi pin is reported as moved
|
|
7360
|
+
"each target StarCi pin is reported as moved (with its workspace), skipped when no package.json declares it, or refused with a reason",
|
|
7361
|
+
"a failed canon phase exits nonzero even when the first CLI install succeeded",
|
|
7360
7362
|
"installs always use --save-exact and report the package.json and package-lock.json diff stat"
|
|
7361
7363
|
],
|
|
7362
7364
|
"runtimeSide": "host"
|
|
@@ -4,6 +4,14 @@ import { parseYaml } from '../../engine/yaml.mjs';
|
|
|
4
4
|
|
|
5
5
|
const nameOf = (item) => typeof item === 'string' ? item : item?.name;
|
|
6
6
|
|
|
7
|
+
/** A public CI variable declares either a manifest value or an owner-supplied value, never both. */
|
|
8
|
+
function variableProblems(item, at, invalid) {
|
|
9
|
+
const hasValue = item != null && Object.hasOwn(item, 'value');
|
|
10
|
+
const hasOwnerValue = item != null && Object.hasOwn(item, 'ownerValue');
|
|
11
|
+
if (hasValue && hasOwnerValue) invalid(at, 'a variable has either a value or ownerValue');
|
|
12
|
+
else if (!hasValue && item?.ownerValue !== true) invalid(at, 'a variable needs a value or ownerValue: true');
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
/** Each CI name belongs to one class and scope; declarations contain names and public configuration only. */
|
|
8
16
|
export function readCiDeclaration(doc) {
|
|
9
17
|
const problems = [], seen = new Set();
|
|
@@ -17,8 +25,9 @@ export function readCiDeclaration(doc) {
|
|
|
17
25
|
if (!['repository', 'environment'].includes(scope)) invalid(`${where}.scope`, 'scope must be repository or environment');
|
|
18
26
|
if (scope === 'environment' && (typeof item?.environment !== 'string' || !/^[a-z][a-z0-9-]*$/.test(item.environment))) invalid(`${where}.environment`, 'environment scope requires an environment name');
|
|
19
27
|
if ((dependabot || scope !== 'environment') && item && Object.hasOwn(item, 'environment')) invalid(`${where}.environment`, 'only environment scope may name an environment');
|
|
28
|
+
if (variable) variableProblems(item, where, invalid);
|
|
20
29
|
return { name, scope, environment: item?.environment ?? null, purpose: item?.purpose ?? null,
|
|
21
|
-
optional: item?.optional === true, ...(variable ? { value: item?.value } : {}) };
|
|
30
|
+
optional: item?.optional === true, ...(variable ? { value: item?.value, ownerValue: item?.ownerValue === true } : {}) };
|
|
22
31
|
});
|
|
23
32
|
const ci = doc?.ci;
|
|
24
33
|
const secrets = normalize(ci?.secrets, 'ci.secrets');
|
|
@@ -33,13 +42,13 @@ export function qualityServices(doc, { example = false } = {}) {
|
|
|
33
42
|
const dependabot = services.dependabot ?? {}, audit = services.audit ?? {};
|
|
34
43
|
return {
|
|
35
44
|
sonar: !sonar || sonar.mode === 'disabled' ? null : {
|
|
36
|
-
provider: sonar.provider, mode: sonar.mode, host: sonar.host?.public || sonar.host?.local || null,
|
|
45
|
+
provider: sonar.provider, mode: sonar.mode, optional: sonar.optional ?? sonar.provider === 'sonarqube', host: sonar.host?.public || sonar.host?.local || null,
|
|
37
46
|
organization: sonar.organization ?? null,
|
|
38
47
|
organizationVar: sonar.ci?.organizationVar ?? (sonar.provider === 'sonarcloud' && (example || list(sonar.runtimeSecrets).length > 0) && !list(sonar.credentials).length ? 'SONAR_ORGANIZATION' : null), projectKey: sonar.projects?.[0]?.key ?? null,
|
|
39
48
|
qualityGate: sonar.qualityGate ?? (example ? 'starci-quality-lite' : 'starci-quality'), wiring: sonar.ci?.wiring ?? 'required',
|
|
40
49
|
token: sonar.ci?.token || nameOf(sonar.ci?.secrets?.[0]) || 'SONAR_TOKEN', hostVar: sonar.ci?.hostVar || 'SONAR_HOST_URL',
|
|
41
50
|
},
|
|
42
|
-
codecov: { enabled: codecov.mode !== 'disabled', auth: codecov.auth ?? 'oidc', token: codecov.ci?.token ?? null,
|
|
51
|
+
codecov: { optional: codecov.optional ?? false, enabled: codecov.mode !== 'disabled', auth: codecov.auth ?? 'oidc', token: codecov.ci?.token ?? null,
|
|
43
52
|
flag: codecov.flag ?? null, target: codecov.target ?? 100, components: codecov.components ?? 'auto', required: codecov.required ?? true, wiring: codecov.ci?.wiring ?? 'required' },
|
|
44
53
|
dependabot: { ecosystems: [...(dependabot.ecosystems ?? ['npm', 'github-actions', 'docker'])],
|
|
45
54
|
schedule: { interval: 'weekly', ...dependabot.schedule }, groups: { minorPatch: 'grouped', major: 'per-package', ...dependabot.groups },
|