@starci/hfs 4.7.0 → 4.7.1
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 +3 -0
- package/ci/local.mjs +8 -6
- package/coverage/cli.mjs +49 -0
- package/package.json +2 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
- package/runtime/packages/cli/src/catalog.generated.mjs +11 -1
- package/runtime/scripts/hfs/lcov-merge.mjs +64 -19
- package/sync/ci-commands.mjs +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.7.1 - 2026-10-18
|
|
4
|
+
- Fixed: the package files list carries coverage/ (4.7.0 shipped `app coverage merge` without coverage/cli.mjs); the lint upload condition of the managed CI names the per-side report with format() instead of a nested ${{ }} that actionlint reads as always true.
|
|
5
|
+
|
|
3
6
|
## 4.7.0 - 2026-10-18
|
|
4
7
|
- Added: `app release push --execute` pushes the release sha to `release/candidate-<version>` and moves the branch and the tag only when its ci.yml run is green (RELEASE_CANDIDATE_CI_RED / _TIMEOUT, RELEASE_CI_UNKNOWN; the owner's sha-bound override record is the emergency path); the managed ci.yml and pre-push hook carry the candidate ref; R301 CI_STANDARD in the app check.
|
|
5
8
|
- Added: the managed CI shards the unit run (`ci.jobs.test.shards`, default 6), lints each side in its own job and merges coverage in a coverage job (`starci app coverage merge`); `ci.jobs.<job>.timeoutMinutes`; jest/build caches keyed by the lockfile and saved always; `starci app ci local` runs the rendered jobs locally in parallel.
|
package/ci/local.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// - the unit shards write their lcov to be/coverage/shards/shard-<i> (on GitHub each shard has its own runner and uploads it), which is where the
|
|
8
8
|
// coverage job's `starci app coverage merge` reads them;
|
|
9
9
|
// - sonar and db-types run only when --jobs names them.
|
|
10
|
-
// Each instance logs to <
|
|
10
|
+
// Each instance logs to <job>[-<matrix>].log in --logs <dir>, else a new <tmp>/starci-ci-local-* directory. Exit 0 every instance green, 1 one red, 2 bad usage or no ci.yml. Run steps through bash.
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import os from 'node:os';
|
|
13
13
|
import path from 'node:path';
|
|
@@ -19,15 +19,16 @@ const GITHUB_ONLY_IF = /SONAR_TOKEN\s*!=\s*''|refs\/tags|startsWith\(github\.ref
|
|
|
19
19
|
const LOCAL_IF = /^(?:\$\{\{\s*)?(?:always\(\)|!cancelled\(\)|env\.SONAR_TOKEN\s*==\s*'')(?:\s*\}\})?$/;
|
|
20
20
|
|
|
21
21
|
function parseArgs(argv) {
|
|
22
|
-
const opts = { jobs: null, parallel: Math.max(1, Math.min(4, Math.floor(os.cpus().length / 2))), dryRun: false, json: false };
|
|
22
|
+
const opts = { logs: null, jobs: null, parallel: Math.max(1, Math.min(4, Math.floor(os.cpus().length / 2))), dryRun: false, json: false };
|
|
23
23
|
for (let index = 0; index < argv.length; index += 1) {
|
|
24
24
|
const arg = argv[index];
|
|
25
25
|
if (arg === '--dry-run') opts.dryRun = true;
|
|
26
26
|
else if (arg === '--json') opts.json = true;
|
|
27
|
-
else if (arg === '--jobs' || arg === '--parallel') {
|
|
27
|
+
else if (arg === '--jobs' || arg === '--parallel' || arg === '--logs') {
|
|
28
28
|
const value = argv[index + 1];
|
|
29
29
|
if (!value || value.startsWith('--')) throw new Error(`${arg} needs a value`);
|
|
30
|
-
if (arg === '--
|
|
30
|
+
if (arg === '--logs') opts.logs = value;
|
|
31
|
+
else if (arg === '--jobs') opts.jobs = value.split(',').map((job) => job.trim()).filter(Boolean);
|
|
31
32
|
else if (!/^[1-9]\d*$/.test(value)) throw new Error('--parallel takes a positive integer');
|
|
32
33
|
else opts.parallel = Number(value);
|
|
33
34
|
index += 1;
|
|
@@ -124,7 +125,7 @@ async function runPlan(plan, { parallel, root, env, logs, report }) {
|
|
|
124
125
|
/** `starci app ci local`. */
|
|
125
126
|
export async function ciLocalMain(argv, { cwd = process.cwd(), stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), env = process.env } = {}) {
|
|
126
127
|
let opts;
|
|
127
|
-
try { opts = parseArgs(argv); } catch (error) { stderr(`starci app ci local: ${error.message}; usage: starci app ci local [--jobs <id,...>] [--parallel <n>] [--dry-run] [--json]\n`); return 2; }
|
|
128
|
+
try { opts = parseArgs(argv); } catch (error) { stderr(`starci app ci local: ${error.message}; usage: starci app ci local [--jobs <id,...>] [--parallel <n>] [--logs <dir>] [--dry-run] [--json]\n`); return 2; }
|
|
128
129
|
const file = path.join(cwd, '.github', 'workflows', 'ci.yml');
|
|
129
130
|
if (!fs.existsSync(file)) { stderr('starci app ci local: no .github/workflows/ci.yml (run starci app sync --write)\n'); return 2; }
|
|
130
131
|
const plan = localPlan(parseYaml(fs.readFileSync(file, 'utf8')), opts.jobs);
|
|
@@ -134,7 +135,8 @@ export async function ciLocalMain(argv, { cwd = process.cwd(), stdout = (s) => p
|
|
|
134
135
|
else for (const instance of plan) stdout(`${instance.label}${instance.needs.length ? ` (after ${instance.needs.join(', ')})` : ''}\n${instance.steps.map((step) => ` ${step.run !== undefined ? 'run ' : 'skip'} ${step.name}${step.skip ? ` - ${step.skip}` : ''}`).join('\n')}\n`);
|
|
135
136
|
return 0;
|
|
136
137
|
}
|
|
137
|
-
const logs = fs.mkdtempSync(path.join(os.tmpdir(), 'starci-ci-local-'));
|
|
138
|
+
const logs = opts.logs ? path.resolve(cwd, opts.logs) : fs.mkdtempSync(path.join(os.tmpdir(), 'starci-ci-local-'));
|
|
139
|
+
fs.mkdirSync(logs, { recursive: true });
|
|
138
140
|
const defaultBranch = 'main';
|
|
139
141
|
const runEnv = { ...env, CI: 'true', STARCI_BASE_REF: env.STARCI_BASE_REF ?? `origin/${defaultBranch}`, SONAR_TOKEN: '' };
|
|
140
142
|
stdout(`starci app ci local: ${plan.length} job instance(s), ${opts.parallel} at a time; logs in ${logs}\n`);
|
package/coverage/cli.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// cli.mjs - `starci app coverage merge --out <lcov> <path>...`: the lcov reports of the sharded unit run (one per `npm test -- --shard=i/N`, downloaded into a
|
|
2
|
+
// directory by the coverage job of the managed CI) merged into the one report the coverage proof, the Codecov upload and the Sonar import read
|
|
3
|
+
// (runtime/scripts/hfs/lcov-merge.mjs: hits summed by line, function and branch identity, one record per source file). A path is an lcov file or a
|
|
4
|
+
// directory searched for every lcov.info below it; no report found is a refusal, so a merge never writes an empty report that would read as "nothing to measure".
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { lcovFilesUnder, mergeLcov } from '../runtime/scripts/hfs/lcov-merge.mjs';
|
|
8
|
+
|
|
9
|
+
const USAGE = 'usage: starci app coverage merge --out <lcov> <lcov-or-dir>... [--cwd <dir>]';
|
|
10
|
+
|
|
11
|
+
function parse(argv) {
|
|
12
|
+
const [action, ...rest] = argv;
|
|
13
|
+
if (action !== 'merge') throw new Error(USAGE);
|
|
14
|
+
const opts = { inputs: [] };
|
|
15
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
16
|
+
const arg = rest[index];
|
|
17
|
+
if (arg === '--out' || arg === '--cwd') {
|
|
18
|
+
const value = rest[index + 1];
|
|
19
|
+
if (!value || value.startsWith('--')) throw new Error(`${arg} needs a value; ${USAGE}`);
|
|
20
|
+
opts[arg.slice(2)] = value;
|
|
21
|
+
index += 1;
|
|
22
|
+
} else if (arg.startsWith('--')) throw new Error(`unknown flag ${arg}; ${USAGE}`);
|
|
23
|
+
else opts.inputs.push(arg);
|
|
24
|
+
}
|
|
25
|
+
if (!opts.out || !opts.inputs.length) throw new Error(USAGE);
|
|
26
|
+
return opts;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Exit 0 merged, 2 bad usage or no report found. */
|
|
30
|
+
export async function coverageMain(argv, { cwd: baseCwd = process.cwd(), stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s) } = {}) {
|
|
31
|
+
let opts;
|
|
32
|
+
try { opts = parse(argv); } catch (error) { stderr(`starci app coverage: ${error.message}\n`); return 2; }
|
|
33
|
+
const cwd = path.resolve(baseCwd, opts.cwd ?? '.');
|
|
34
|
+
const files = [];
|
|
35
|
+
for (const input of opts.inputs) {
|
|
36
|
+
const target = path.resolve(cwd, input);
|
|
37
|
+
if (!fs.existsSync(target)) { stderr(`starci app coverage: ${input} does not exist\n`); return 2; }
|
|
38
|
+
files.push(...lcovFilesUnder(target));
|
|
39
|
+
}
|
|
40
|
+
const out = path.resolve(cwd, opts.out);
|
|
41
|
+
const sources = files.filter((file) => path.resolve(file) !== out);
|
|
42
|
+
if (!sources.length) { stderr(`starci app coverage: no lcov report under ${opts.inputs.join(', ')}; the unit shards wrote none\n`); return 2; }
|
|
43
|
+
const merged = mergeLcov(sources.map((file) => fs.readFileSync(file, 'utf8')));
|
|
44
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
45
|
+
fs.writeFileSync(out, merged);
|
|
46
|
+
const records = merged.split('\n').filter((line) => line.startsWith('SF:')).length;
|
|
47
|
+
stdout(`starci app coverage merge: ${sources.length} report(s), ${records} source file(s) -> ${path.relative(cwd, out).replaceAll('\\', '/')}\n`);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/hfs",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.1",
|
|
4
4
|
"description": "The StarCi app command implementation for scaffolding, linting, checking and synchronizing one product repository.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"release/**",
|
|
23
23
|
"db/**",
|
|
24
24
|
"ci/**",
|
|
25
|
+
"coverage/**",
|
|
25
26
|
"lint/**",
|
|
26
27
|
"scaffold/**",
|
|
27
28
|
"report/**",
|
|
@@ -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.6
|
|
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.6
|
|
40
40
|
group: starci
|
|
41
41
|
install: registry
|
|
42
42
|
side: fe
|
|
@@ -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.7.
|
|
79
|
+
version: 1.7.1
|
|
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.7.
|
|
86
|
+
version: 4.7.1
|
|
87
87
|
group: starci
|
|
88
88
|
install: registry
|
|
89
89
|
side: both
|
|
@@ -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": "7cf3ce3c9e0abb27fa139c93abbbf15a2f8cc8aec47ba95f022d946b53d8998f",
|
|
5
5
|
"global": [
|
|
6
6
|
{
|
|
7
7
|
"name": "json",
|
|
@@ -247,6 +247,11 @@ export const CATALOG = {
|
|
|
247
247
|
"type": "string",
|
|
248
248
|
"summary": "local: job instances run at once (default half the CPUs, at most 4)"
|
|
249
249
|
},
|
|
250
|
+
{
|
|
251
|
+
"name": "logs",
|
|
252
|
+
"type": "string",
|
|
253
|
+
"summary": "local: the directory the job logs go to (default a new temp directory)"
|
|
254
|
+
},
|
|
250
255
|
{
|
|
251
256
|
"name": "dry-run",
|
|
252
257
|
"type": "boolean",
|
|
@@ -293,6 +298,11 @@ export const CATALOG = {
|
|
|
293
298
|
],
|
|
294
299
|
"required": true,
|
|
295
300
|
"variadic": false
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"name": "paths",
|
|
304
|
+
"required": true,
|
|
305
|
+
"variadic": true
|
|
296
306
|
}
|
|
297
307
|
],
|
|
298
308
|
"flags": [
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
// lcov-merge.mjs - the lcov reports of the sharded unit run merged into one (`starci app coverage merge`, the coverage job of the managed CI). Each shard of
|
|
2
|
-
// `npm test -- --shard=i/N` writes its own lcov over the whole collectCoverageFrom scope
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// `npm test -- --shard=i/N` writes its own lcov over the whole collectCoverageFrom scope, so the merge sums every line, function and branch hit by its
|
|
3
|
+
// identity across the shards and writes ONE record per source file, with the summaries recomputed.
|
|
4
|
+
// A record of zeros (no line, function or branch hit) is the placeholder a shard writes for a file none of its tests loaded: jest instruments it on its own
|
|
5
|
+
// (babel-istanbul), so its function names and branch ids are not the ones of the shard that ran it (v8), and summing them would add phantom uncovered
|
|
6
|
+
// entries. Such a placeholder is dropped whenever another report holds a record of the file with a hit; a file that is a placeholder everywhere keeps one,
|
|
7
|
+
// so it stays uncovered and the proof refuses it. Branch ids are not stable either: the v8 provider numbers a file's blocks in the order it met them, and a
|
|
8
|
+
// shard that ran fewer functions numbers them differently. A branch is therefore keyed by its line and its ordinal among the blocks of that line (BRDA
|
|
9
|
+
// <line>,<ordinal>,<branch>), and a record that loaded the file without calling any of its functions brings its lines and functions but not its branches
|
|
10
|
+
// when another report called them. The coverage proof (`starci app check --coverage-from`), the Codecov upload and the Sonar import then read
|
|
11
|
+
// the one be/coverage/lcov.info they always read. Pure over the texts; the file walk is lcovFilesUnder.
|
|
6
12
|
import fs from 'node:fs';
|
|
7
13
|
import path from 'node:path';
|
|
8
14
|
import { byCodeUnit } from '../lib/list.mjs';
|
|
9
15
|
import { addHits as add } from './rules/coverage-below.mjs';
|
|
10
16
|
|
|
17
|
+
const BOM = String.fromCodePoint(0xfeff);
|
|
11
18
|
const count = (value) => (/^\d+$/.test(value) ? Number(value) : 0);
|
|
12
19
|
|
|
13
20
|
function recordOf(records, source) {
|
|
14
|
-
if (!records.has(source)) records.set(source, { lines: new Map(), fnLines: new Map(), fnHits: new Map(), branches: new Map() });
|
|
21
|
+
if (!records.has(source)) records.set(source, { lines: new Map(), fnLines: new Map(), fnHits: new Map(), branches: new Map(), rawBranches: [] });
|
|
15
22
|
return records.get(source);
|
|
16
23
|
}
|
|
17
24
|
|
|
@@ -35,25 +42,63 @@ function dataLine(record, tag, value) {
|
|
|
35
42
|
} else if (tag === 'FN' || tag === 'FNDA') functionLine(record, tag, value);
|
|
36
43
|
else if (tag === 'BRDA') {
|
|
37
44
|
const parts = value.split(',');
|
|
38
|
-
if (parts.length === 4)
|
|
45
|
+
if (parts.length === 4) record.rawBranches.push({ line: Number(parts[0]), block: Number(parts[1]), branch: Number(parts[2]), hits: parts[3] === '-' ? 0 : count(parts[3]) });
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
/**
|
|
49
|
+
/** The branches of a record keyed by line and the ordinal of their block on that line: stable across shards that numbered blocks differently. */
|
|
50
|
+
function keyBranches(record) {
|
|
51
|
+
const blocksByLine = new Map();
|
|
52
|
+
for (const entry of [...record.rawBranches].sort((a, b) => a.block - b.block || a.branch - b.branch)) {
|
|
53
|
+
const blocks = blocksByLine.get(entry.line) ?? [];
|
|
54
|
+
if (!blocks.includes(entry.block)) blocks.push(entry.block);
|
|
55
|
+
blocksByLine.set(entry.line, blocks);
|
|
56
|
+
add(record.branches, `${entry.line},${blocks.indexOf(entry.block)},${entry.branch}`, entry.hits);
|
|
57
|
+
}
|
|
58
|
+
return record;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The records of one text by source file (a source repeated inside one text merges into one record). */
|
|
62
|
+
function recordsOfText(text) {
|
|
63
|
+
const records = new Map();
|
|
64
|
+
let current = null;
|
|
65
|
+
const body = String(text).startsWith(BOM) ? String(text).slice(1) : String(text);
|
|
66
|
+
for (const raw of body.split(/\r?\n/)) {
|
|
67
|
+
const line = raw.trim();
|
|
68
|
+
if (line === 'end_of_record') { current = null; continue; }
|
|
69
|
+
const colon = line.indexOf(':');
|
|
70
|
+
if (colon < 0) continue;
|
|
71
|
+
const tag = line.slice(0, colon);
|
|
72
|
+
const value = line.slice(colon + 1);
|
|
73
|
+
if (tag === 'SF') current = recordOf(records, value);
|
|
74
|
+
else if (current) dataLine(current, tag, value);
|
|
75
|
+
}
|
|
76
|
+
for (const record of records.values()) keyBranches(record);
|
|
77
|
+
return records;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const anyHit = (map) => [...map.values()].some((hits) => hits > 0);
|
|
81
|
+
/** A record no test touched: no line, function or branch hit (the placeholder of a shard that never loaded the file). */
|
|
82
|
+
const placeholder = (record) => !anyHit(record.lines) && !anyHit(record.fnHits) && !anyHit(record.branches);
|
|
83
|
+
|
|
84
|
+
function mergeInto(target, record, { branches = true } = {}) {
|
|
85
|
+
for (const [line, hits] of record.lines) add(target.lines, line, hits);
|
|
86
|
+
for (const [name, line] of record.fnLines) if (!target.fnLines.has(name)) target.fnLines.set(name, line);
|
|
87
|
+
for (const [name, hits] of record.fnHits) add(target.fnHits, name, hits);
|
|
88
|
+
if (branches) for (const [branch, hits] of record.branches) add(target.branches, branch, hits);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Every record of every text merged by source file (SF) and identity; a placeholder counts only when no report touched the file. */
|
|
43
92
|
function parseAll(texts) {
|
|
93
|
+
const bySource = new Map();
|
|
94
|
+
for (const text of texts) for (const [source, record] of recordsOfText(text)) bySource.set(source, [...(bySource.get(source) ?? []), record]);
|
|
44
95
|
const records = new Map();
|
|
45
|
-
for (const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (colon < 0) continue;
|
|
52
|
-
const tag = line.slice(0, colon);
|
|
53
|
-
const value = line.slice(colon + 1);
|
|
54
|
-
if (tag === 'SF') current = recordOf(records, value);
|
|
55
|
-
else if (current) dataLine(current, tag, value);
|
|
56
|
-
}
|
|
96
|
+
for (const [source, list] of bySource) {
|
|
97
|
+
const touched = list.filter((record) => !placeholder(record));
|
|
98
|
+
const target = recordOf(records, source);
|
|
99
|
+
const called = touched.some((record) => anyHit(record.fnHits));
|
|
100
|
+
// A record that loaded the file but called none of its functions keeps only its lines and functions when another record called them.
|
|
101
|
+
for (const record of touched.length ? touched : list.slice(0, 1)) mergeInto(target, record, { branches: !called || anyHit(record.fnHits) });
|
|
57
102
|
}
|
|
58
103
|
return records;
|
|
59
104
|
}
|
package/sync/ci-commands.mjs
CHANGED
|
@@ -258,7 +258,8 @@ export function ciWorkflowCommands(source, lcovReport = 'be/coverage/lcov.info',
|
|
|
258
258
|
' env:',
|
|
259
259
|
' NODE_OPTIONS: --max-old-space-size=6144',
|
|
260
260
|
` - uses: ${UPLOAD_ARTIFACT}`,
|
|
261
|
-
|
|
261
|
+
// Inside an if: expression the matrix value is an argument of format(), never a nested ${{ }} (actionlint: a nested expression makes the condition always true).
|
|
262
|
+
" if: always() && env.SONAR_TOKEN != '' && hashFiles(format('reports/lint.{0}.sonar.json', matrix.side)) != ''",
|
|
262
263
|
' with:',
|
|
263
264
|
` name: sonar-lint-${side}`,
|
|
264
265
|
` path: reports/lint.${side}.sonar.json`,
|