@starci/hfs 4.7.12 → 4.7.13
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/package.json +1 -1
- package/release/gate.mjs +388 -378
- package/release/lint-report.mjs +52 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
- package/runtime/knowledge/hfs/ci-standard.yaml +1 -1
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +1 -1
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +6 -4
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +1 -1
- package/runtime/scripts/hfs/ci-declaration.mjs +5 -2
- package/runtime/scripts/hfs/db-baseline-verb.mjs +5 -2
- package/runtime/scripts/hfs/doc-commands.mjs +2 -1
- package/runtime/scripts/hfs/rule-catalog.mjs +5 -2
- package/runtime/scripts/hfs/rules/coverage-below.mjs +5 -3
- package/runtime/scripts/hfs/rules/database-access.mjs +1 -1
- package/runtime/scripts/hfs/rules/database-migrations.mjs +13 -3
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +1 -1
- package/runtime/scripts/hfs/rules/database-queues.mjs +2 -2
- package/runtime/scripts/hfs/rules/deploy-scan.mjs +50 -6
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +11 -1
- package/runtime/scripts/hfs/rules/doc-fresh.mjs +18 -3
- package/runtime/scripts/hfs/rules/docker-health.mjs +4 -1
- package/runtime/scripts/hfs/rules/env-parity.mjs +8 -7
- package/runtime/scripts/hfs/rules/env-value-shape.mjs +2 -4
- package/runtime/scripts/hfs/rules/fe-build-args.mjs +1 -1
- package/runtime/scripts/hfs/rules/release-provenance.mjs +8 -5
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +23 -7
- package/runtime/scripts/hfs/rules/stack-tooling.mjs +5 -2
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +5 -5
- package/runtime/scripts/hfs/rules/supply-chain.mjs +3 -3
- package/runtime/scripts/hfs/rules/unused-symbol-properties.mjs +1 -1
- package/runtime/scripts/hfs/rules/unused-symbol.mjs +8 -4
- package/runtime/scripts/hfs/sealed-members.mjs +1 -1
- package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +1 -1
- package/runtime/scripts/hfs/secret-files.mjs +1 -1
- package/runtime/scripts/hfs/secret-sops.mjs +1 -1
- package/runtime/scripts/hfs/stack-deploy-config.mjs +5 -2
- package/runtime/scripts/lib/env.mjs +2 -1
- package/runtime/scripts/lib/path-key.mjs +1 -1
- package/runtime/scripts/lib/roots.mjs +3 -3
- package/runtime/scripts/lib/secret-patterns.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.mjs +4 -2
- package/sync/ci-commands.mjs +275 -275
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +2 -0
|
@@ -7,16 +7,32 @@ const PLAINTEXT_LOAD = 'HFS_SCRIPT_PLAINTEXT_LOAD';
|
|
|
7
7
|
const XTRACE = 'HFS_SCRIPT_XTRACE';
|
|
8
8
|
const SERVICE_ROLE = 'HFS_SERVICE_ROLE_SCOPE';
|
|
9
9
|
const SUPABASE_OWNER = 'be.integrations.supabase';
|
|
10
|
-
const
|
|
10
|
+
const SECRET_ENV_FILE = /(?:^|\/)(?:secrets?\.env|\.env)(?:\.[^/]*)?/iu;
|
|
11
|
+
const SECRET_NIVO_FILE = /(?:^|\/)\.nivo-lite(?:\/|$)/iu;
|
|
12
|
+
const SECRET_ENC_FILE = /\.enc$/iu;
|
|
13
|
+
const isSecretFile = (file) => SECRET_ENV_FILE.test(file) || SECRET_NIVO_FILE.test(file) || SECRET_ENC_FILE.test(file);
|
|
11
14
|
const EXCLUDED_TREE = /(?:^|\/)(?:node_modules|\.git|\.claude|\.secrets|secrets|tests?|__tests__|docs|coverage|dist|build)(?:\/|$)/u;
|
|
12
15
|
const TEST_FILE = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$/u;
|
|
13
16
|
const SOURCE = /\.(?:[cm]?[jt]sx?|sh|bash|zsh|ksh|ps1|psm1|bat|cmd|py|rb|pl)$/iu;
|
|
14
17
|
const WORKFLOW = /(?:^|\/)\.?(?:github\/)?workflows\/[^/]+\.ya?ml$/u;
|
|
15
18
|
const DELIVERY = /(?:^|\/)(?:deploy|scripts|bin)\//u;
|
|
16
19
|
const DOCKER = /(?:^|\/)(?:Dockerfile(?:\.[^/]*)?|(?:docker-)?compose(?:[.-][^/]*)?\.ya?ml)$/iu;
|
|
17
|
-
const
|
|
20
|
+
const SECRET_REFERENCE_NAME = /(?:secrets?\.env|\.env)(?:\.[\w.-]+)?/;
|
|
21
|
+
const SECRET_REFERENCE_NIVO = /\.nivo-lite(?:[/\\][\w./\\-]*)?/;
|
|
22
|
+
const SECRET_REFERENCE_BEFORE = /(?:^|[\s/\\'"`(=])/;
|
|
23
|
+
const SECRET_REFERENCE_AFTER = /(?=$|[\s/\\'"`),;])/;
|
|
24
|
+
const SECRET_REFERENCE_SOURCE = `${SECRET_REFERENCE_BEFORE.source}(?:${SECRET_REFERENCE_NAME.source}|${SECRET_REFERENCE_NIVO.source})${SECRET_REFERENCE_AFTER.source}`;
|
|
25
|
+
const SECRET_REFERENCE = new RegExp(SECRET_REFERENCE_SOURCE, 'iu');
|
|
18
26
|
const EXAMPLE_REFERENCE = /(?:secrets?\.env|\.env)\.(?:example|sample|template)(?=$|[^\w.-])/giu;
|
|
19
|
-
const
|
|
27
|
+
const LOAD_SHELL_COMMAND = /(?:^|[\s;&|('"`])(?:source|cat|tac|less|more|head|tail|grep|egrep|fgrep|awk|gawk|sed|base64)\b/iu;
|
|
28
|
+
const LOAD_WINDOWS_COMMAND = /(?:^|[\s;&|('"`])(?:Get-Content|gc|Import-DotEnv|env-cmd|load_dotenv|loadEnvFile)\b/iu;
|
|
29
|
+
const LOAD_READ_CALL = /(?:^|[\s;&|('"`])(?:readFile(?:Sync)?|readTextFile(?:Sync)?|ReadAllText|ReadAllLines|dotenv\s*\.\s*(?:config|parse))\b/iu;
|
|
30
|
+
const LOAD_FILE_READ = /\b(?:File|IO)\s*\.\s*(?:read|readlines)\b/iu;
|
|
31
|
+
const LOAD_PYTHON_READ = /\.\s*read_(?:text|bytes)\s*\(/iu;
|
|
32
|
+
const LOAD_DOT_SOURCE = /(?:^|[\s;&|])\.[\t ]+/iu;
|
|
33
|
+
const LOAD_REDIRECT = /(?<!<)<(?!<)/iu;
|
|
34
|
+
const LOAD_PATTERNS = [LOAD_SHELL_COMMAND, LOAD_WINDOWS_COMMAND, LOAD_READ_CALL, LOAD_FILE_READ, LOAD_PYTHON_READ, LOAD_DOT_SOURCE, LOAD_REDIRECT];
|
|
35
|
+
const isLoad = (line) => LOAD_PATTERNS.some((pattern) => pattern.test(line));
|
|
20
36
|
const READ_CALL = /\b(?:readFile(?:Sync)?|readTextFile(?:Sync)?|ReadAllText|ReadAllLines|load_dotenv|loadEnvFile|dotenv\s*\.\s*(?:config|parse))\s*\(/giu;
|
|
21
37
|
const TRACE_COMMAND = /(?:^|[\s;&|('"`]|#!)(?:set|\/?(?:[\w.-]+\/)*(?:bash|sh|dash|zsh|ksh))\s+/giu;
|
|
22
38
|
const SHELL_OPTIONS = /\bSHELLOPTS\s*=\s*['"]?([^\s;'"]*)/giu;
|
|
@@ -26,7 +42,7 @@ const SERVICE_ROLE_NAME = /(?:^|[^a-z0-9])(?:[a-z0-9]+[_-])*service[_-]?role(?:[
|
|
|
26
42
|
function executableText(text) {
|
|
27
43
|
return text.replace(/\/\*[\s\S]*?\*\//gu, '').split(/\r?\n/u)
|
|
28
44
|
.filter((line) => !/^\s*(?:#(?!!)|\/\/)/u.test(line)).join('\n')
|
|
29
|
-
.replace(/
|
|
45
|
+
.replace(/[\\`]\r?\n/gu, ' ');
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
const hasSecretReference = (text) => SECRET_REFERENCE.test(text.replace(EXAMPLE_REFERENCE, 'example-file'));
|
|
@@ -37,7 +53,7 @@ function callArguments(text, from) {
|
|
|
37
53
|
for (let at = from; at < Math.min(text.length, from + 1024); at += 1) {
|
|
38
54
|
const character = text[at];
|
|
39
55
|
if (character === '\\') { at += 1; continue; }
|
|
40
|
-
if (quote) { if (character === quote) quote = ''; continue; }
|
|
56
|
+
if (quote) { if (character === quote) { quote = ''; } continue; }
|
|
41
57
|
if (['\'', '"', '`'].includes(character)) { quote = character; continue; }
|
|
42
58
|
if (character === '(') depth += 1;
|
|
43
59
|
if (character === ')' && --depth === 0) return text.slice(from, at);
|
|
@@ -81,7 +97,7 @@ function plaintextLoad(text) {
|
|
|
81
97
|
const lines = text.split(/\r?\n/u);
|
|
82
98
|
const aliases = secretAliases(lines);
|
|
83
99
|
const secretReference = (value) => hasSecretReference(value) || aliases.some((alias) => alias.test(value));
|
|
84
|
-
return lines.some((line) =>
|
|
100
|
+
return lines.some((line) => isLoad(line) && secretReference(line)) || multilineLoad(text, secretReference) || pythonLoad(text);
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
function tracingOptions(source) {
|
|
@@ -120,7 +136,7 @@ function integrationOwns(file, resolver) {
|
|
|
120
136
|
}
|
|
121
137
|
|
|
122
138
|
function isSource(file) {
|
|
123
|
-
if (
|
|
139
|
+
if (isSecretFile(file) || EXCLUDED_TREE.test(file) || TEST_FILE.test(file)) return false;
|
|
124
140
|
return SOURCE.test(file) || WORKFLOW.test(file) || DOCKER.test(file)
|
|
125
141
|
|| file.endsWith('/package.json') || file === 'package.json'
|
|
126
142
|
|| (DELIVERY.test(file) && !/\.[a-z0-9]+$/iu.test(file));
|
|
@@ -13,7 +13,10 @@ const SCRIPTS_DIR = /(?:^|\/)scripts\//;
|
|
|
13
13
|
const PACKAGE_JSON = /(?:^|\/)package\.json$/;
|
|
14
14
|
const SOURCE_FILE = /(?:^|\/)[^/.]+$|\.(?:sh|bash|zsh|mjs|cjs|js|ts|mts|py|ya?ml)$/;
|
|
15
15
|
const TEST_FILE = /\.(?:spec|test)\.[cm]?[jt]s$/;
|
|
16
|
-
const
|
|
16
|
+
const CREDENTIAL_ENV_FILE = /(?:^|\/)(?:secrets?\.env|\.env)(?:\.[^/]*)?/i;
|
|
17
|
+
const CREDENTIAL_SECRETS_DIR = /(?:^|\/)secrets\//i;
|
|
18
|
+
const CREDENTIAL_ENC_FILE = /\.enc$/i;
|
|
19
|
+
const isCredentialFile = (file) => CREDENTIAL_ENV_FILE.test(file) || CREDENTIAL_SECRETS_DIR.test(file) || CREDENTIAL_ENC_FILE.test(file);
|
|
17
20
|
const VERB_FILE = /^(render|provision|converge|verify|prune|rollback|release|backup|prepare|doctor|jobs|public)\.(?:mjs|js|cjs|sh)$/;
|
|
18
21
|
const COMMENT_LINE = /^[ \t]*(?:#|\/\/).*$/gm;
|
|
19
22
|
const LINE_CONTINUATION = /\\\r?\n\s*/g;
|
|
@@ -57,7 +60,7 @@ function sourceText(file, raw) {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
function deploySource(file) {
|
|
60
|
-
if (MANAGED_WORKFLOW.test(file) ||
|
|
63
|
+
if (MANAGED_WORKFLOW.test(file) || isCredentialFile(file) || TEST_FILE.test(file)) return false;
|
|
61
64
|
if (PACKAGE_JSON.test(file) || WORKFLOW.test(file)) return true;
|
|
62
65
|
return (DEPLOY_DIR.test(file) || SCRIPTS_DIR.test(file)) && SOURCE_FILE.test(file);
|
|
63
66
|
}
|
|
@@ -14,8 +14,8 @@ import { stacksDeploymentFindings } from './stacks-runtime.mjs';
|
|
|
14
14
|
|
|
15
15
|
const mapping = value => plain(value) ? value : {};
|
|
16
16
|
const present = value => typeof value === 'string' && Boolean(value.trim());
|
|
17
|
-
const VERSION = /^[1-9]
|
|
18
|
-
const VERSIONED_SECRET = /-v[1-9]
|
|
17
|
+
const VERSION = /^[1-9]\d*$/;
|
|
18
|
+
const VERSIONED_SECRET = /-v[1-9]\d*$/;
|
|
19
19
|
const SENSITIVE_KEY = /password|passwd|secret|token|credential|private[_-]?key|api[_-]?key|service[_-]?role|(?:(?:^|_)key$)/i;
|
|
20
20
|
const SOURCE = /^be\/(?:apps|src)\/.*\.[cm]?[jt]sx?$/;
|
|
21
21
|
const TEST_SOURCE = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$|\/tests\//;
|
|
@@ -124,7 +124,7 @@ function traefikHosts(rule) {
|
|
|
124
124
|
function positiveHostCall(prefix) {
|
|
125
125
|
const scopes = [false];
|
|
126
126
|
let pending = false;
|
|
127
|
-
for (const token of prefix.match(/"[^"]*"|'[^']*'|`[^`]*`|[()!]|[A-Za-z_]
|
|
127
|
+
for (const token of prefix.match(/"[^"]*"|'[^']*'|`[^`]*`|[()!]|[A-Za-z_]\w*|\S/g) ?? []) {
|
|
128
128
|
if (token === '!') { pending = !pending; continue; }
|
|
129
129
|
if (token === '(') scopes.push(scopes.at(-1) !== pending);
|
|
130
130
|
if (token === ')' && scopes.length > 1) scopes.pop();
|
|
@@ -139,7 +139,7 @@ function labelHosts(service, family) {
|
|
|
139
139
|
if (family === 'traefik' && labels.some(([key, value]) => key === 'traefik.enable' && String(value) === 'false')) return hosts;
|
|
140
140
|
for (const [key, value] of labels) {
|
|
141
141
|
if (family === 'traefik' && key.startsWith('traefik.http.routers.') && key.endsWith('.rule')) hosts.push(...traefikHosts(value));
|
|
142
|
-
if (family === 'caddy-docker-proxy' && /^caddy(?:_
|
|
142
|
+
if (family === 'caddy-docker-proxy' && /^caddy(?:_\d+)?$/.test(key)) hosts.push(...String(value).split(/[\s,]+/).map(hostOf).filter(Boolean));
|
|
143
143
|
}
|
|
144
144
|
return hosts;
|
|
145
145
|
}
|
|
@@ -282,7 +282,7 @@ function positiveMonitor(value) {
|
|
|
282
282
|
const units = value.match(/[a-z]+/g) ?? [], quantities = value.split(/[a-z]+/);
|
|
283
283
|
if (quantities.pop() !== '' || !units.length) return false;
|
|
284
284
|
return units.every(unit => ['ns', 'us', 'ms', 's', 'm', 'h'].includes(unit))
|
|
285
|
-
&& quantities.every(quantity =>
|
|
285
|
+
&& quantities.every(quantity => /^\d+(?:\.\d+)?$/.test(quantity)) && /[1-9]/.test(value);
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
function updatePolicyIssues(id, service, add) {
|
|
@@ -141,12 +141,12 @@ const provenanceEnabled = (value) => asserted(value) || typeof value === 'string
|
|
|
141
141
|
|
|
142
142
|
function imageActionEvidence(step) {
|
|
143
143
|
const action = step.uses ?? '';
|
|
144
|
-
if (
|
|
144
|
+
if (action.startsWith('docker/build-push-action@')) {
|
|
145
145
|
return { sbom: asserted(step.with?.sbom), provenance: provenanceEnabled(step.with?.provenance) };
|
|
146
146
|
}
|
|
147
147
|
return {
|
|
148
|
-
sbom:
|
|
149
|
-
provenance:
|
|
148
|
+
sbom: action.startsWith('actions/attest-sbom@') && Boolean(step.with?.['sbom-path']),
|
|
149
|
+
provenance: action.startsWith('actions/attest-build-provenance@') && Boolean(step.with?.['subject-path'] || step.with?.['subject-digest']),
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -14,7 +14,7 @@ function mergeProperty(before, after) {
|
|
|
14
14
|
/** Every possible spread owner must be known before its property provenance is admitted. */
|
|
15
15
|
function spreadProperty(state, expression, name, seen, resolve) {
|
|
16
16
|
const choices = resolve(state, expression, seen).map(owner => ownerProperty(state, owner, name, seen, resolve));
|
|
17
|
-
if (!choices.length || choices.
|
|
17
|
+
if (!choices.length || choices.includes(null)) return null;
|
|
18
18
|
return { values: choices.flatMap(choice => choice.values), missing: choices.some(choice => choice.missing) };
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -12,7 +12,10 @@ import { returnedExpressions, valuesOf } from './unused-symbol-flow.mjs';
|
|
|
12
12
|
|
|
13
13
|
export const HFS_UNUSED_SYMBOL = 'HFS_UNUSED_SYMBOL';
|
|
14
14
|
const SOURCE = /\.[cm]?[jt]sx?$/u;
|
|
15
|
-
const
|
|
15
|
+
const EXCLUDED_DIR = /(?:^|\/)(?:node_modules|__generated__|_generated|dist|tests?|__tests__|test-support)\//u;
|
|
16
|
+
const EXCLUDED_TEST_FILE = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$/u;
|
|
17
|
+
const EXCLUDED_DECLARATION_FILE = /\.d\.[cm]?ts$/u;
|
|
18
|
+
const isExcluded = (file) => EXCLUDED_DIR.test(file) || EXCLUDED_TEST_FILE.test(file) || EXCLUDED_DECLARATION_FILE.test(file);
|
|
16
19
|
const CATALOG = /(?:\.messages|\.catalog)\.[cm]?[jt]s$/u;
|
|
17
20
|
const JSON_CATALOG = /(?:^|\/)(?:messages|locales)\/[^/]+\.json$|\.catalog\.json$/u;
|
|
18
21
|
const QUEUE_NAME = /(?:^QUEUE(?:_NAME)?$|_QUEUE(?:_NAME)?$)/u;
|
|
@@ -29,7 +32,7 @@ function compilerAt(repoRoot, supplied) {
|
|
|
29
32
|
function sourceFiles(ts, repoRoot, files) {
|
|
30
33
|
const sources = new Map();
|
|
31
34
|
for (const file of files) {
|
|
32
|
-
if (!SOURCE.test(file) ||
|
|
35
|
+
if (!SOURCE.test(file) || isExcluded(file) || !/(?:^|\/)src\//u.test(file)) continue;
|
|
33
36
|
const text = readText(repoRoot, file);
|
|
34
37
|
if (text !== null) sources.set(slash(path.resolve(repoRoot, file)), ts.createSourceFile(path.resolve(repoRoot, file), text, ts.ScriptTarget.Latest, true));
|
|
35
38
|
}
|
|
@@ -259,7 +262,7 @@ function sourceReads(state, sources) {
|
|
|
259
262
|
}
|
|
260
263
|
|
|
261
264
|
function jsonCandidates(state, files) {
|
|
262
|
-
for (const file of files.filter(file => JSON_CATALOG.test(file) && !
|
|
265
|
+
for (const file of files.filter(file => JSON_CATALOG.test(file) && !isExcluded(file))) {
|
|
263
266
|
let json;
|
|
264
267
|
try { json = JSON.parse(readText(state.repoRoot, file)); } catch { continue; }
|
|
265
268
|
const leaves = (value, prefix = '') => {
|
|
@@ -316,7 +319,8 @@ export function unusedSymbolFindings({ repoRoot, files, repo = {}, isRoot = fals
|
|
|
316
319
|
const findings = new Map();
|
|
317
320
|
for (const candidate of state.candidates) {
|
|
318
321
|
if (candidate.used || candidate.kind === 'catalog key' && catalogRead(state, candidate, liveCodes) || !inScope(candidate.path)) continue;
|
|
319
|
-
const
|
|
322
|
+
const extra = { ...candidate };
|
|
323
|
+
delete extra.used;
|
|
320
324
|
findings.set(`${candidate.path}:${candidate.name}`, found(HFS_UNUSED_SYMBOL, candidate.path, `${candidate.path} declares unused ${candidate.kind} ${candidate.name}; remove it or read it in production source (declarations, metadata maps and tests do not count).`, extra));
|
|
321
325
|
}
|
|
322
326
|
return [...findings.values(), ...ledgerFindings.filter(finding => inScope(finding.path))].sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.name.localeCompare(b.name));
|
|
@@ -75,7 +75,7 @@ export function declaredSealedMembers(repoRoot) {
|
|
|
75
75
|
|
|
76
76
|
function placeholderValue(key) {
|
|
77
77
|
const generated = randomUUID();
|
|
78
|
-
if (
|
|
78
|
+
if (key.endsWith('_URL') && key.slice(0, -4).endsWith('DB')) return `postgresql://app_be:${generated}@db.invalid:5432/app`;
|
|
79
79
|
if (key.endsWith('_URL')) return `https://example.invalid/${generated}`;
|
|
80
80
|
return generated;
|
|
81
81
|
}
|
|
@@ -163,7 +163,7 @@ function bootstrapEntries(values) {
|
|
|
163
163
|
const entries = Object.entries(values);
|
|
164
164
|
if (!entries.length) throw new Error('Bootstrap requires at least one value');
|
|
165
165
|
for (const [name, value] of entries) {
|
|
166
|
-
if (!/^[A-Za-z_]
|
|
166
|
+
if (!/^[A-Za-z_]\w*$/.test(name) || name.startsWith('sops_') || name.endsWith('_unencrypted')) {
|
|
167
167
|
throw new Error('Bootstrap contains an invalid or reserved dotenv key');
|
|
168
168
|
}
|
|
169
169
|
if (typeof value !== 'string' || !value.length) throw new Error('Bootstrap values must be nonempty strings');
|
|
@@ -61,7 +61,7 @@ export function privateDirectory(repoRoot, directory) {
|
|
|
61
61
|
const missing = [];
|
|
62
62
|
for (let at = directory; !fs.existsSync(at); at = path.dirname(at))
|
|
63
63
|
missing.push(at);
|
|
64
|
-
for (const at of missing.
|
|
64
|
+
for (const at of missing.toReversed()) fs.mkdirSync(at, { mode: 0o700 });
|
|
65
65
|
for (let at = directory; ; at = path.dirname(at)) {
|
|
66
66
|
const stat = fs.lstatSync(at);
|
|
67
67
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
@@ -19,7 +19,7 @@ export function defaultSops(
|
|
|
19
19
|
env = process.env,
|
|
20
20
|
repoRoot = invocationDir(),
|
|
21
21
|
platform = process.platform,
|
|
22
|
-
tools,
|
|
22
|
+
tools = undefined,
|
|
23
23
|
) {
|
|
24
24
|
const { sops: binary } = publicStorage(() => secretTools(env, tools));
|
|
25
25
|
const selectedEnv = ageIdentityEnv(repoRoot, env, tools);
|
|
@@ -176,7 +176,10 @@ function configs(c, block, probe) {
|
|
|
176
176
|
out.push({ name: entry.name, file: entry.file, env: entry.env });
|
|
177
177
|
});
|
|
178
178
|
const seen = new Set();
|
|
179
|
-
for (const entry of out) {
|
|
179
|
+
for (const entry of out) {
|
|
180
|
+
if (seen.has(entry.name)) c.problem('deploy.configs', `config name ${entry.name} is declared twice`);
|
|
181
|
+
seen.add(entry.name);
|
|
182
|
+
}
|
|
180
183
|
return out;
|
|
181
184
|
}
|
|
182
185
|
|
|
@@ -242,7 +245,7 @@ function tasks(c, block, services) {
|
|
|
242
245
|
return out;
|
|
243
246
|
}
|
|
244
247
|
|
|
245
|
-
const REGISTRY_HOST = /^[a-z0-9][a-z0-9.-]*(
|
|
248
|
+
const REGISTRY_HOST = /^[a-z0-9][a-z0-9.-]*(?::\d{1,5})?$/u;
|
|
246
249
|
const VISIBILITY = ['public', 'private'];
|
|
247
250
|
|
|
248
251
|
/** The image registry: host (default ghcr.io) and the visibility the app's packages are meant to have (default public, the canon scaffold writes it and the app owns it afterwards). */
|
|
@@ -15,6 +15,7 @@ export const isSpecRun = (env = process.env) => Boolean(env.NODE_TEST_CONTEXT);
|
|
|
15
15
|
* would pass red specs from inside a test process. The runner marks the spec files it starts itself, so isSpecRun stays true inside them.
|
|
16
16
|
*/
|
|
17
17
|
export function withoutTestRunner(env = process.env) {
|
|
18
|
-
const
|
|
18
|
+
const rest = { ...env };
|
|
19
|
+
delete rest.NODE_TEST_CONTEXT;
|
|
19
20
|
return rest;
|
|
20
21
|
}
|
|
@@ -44,7 +44,7 @@ export const pathsOverlap = (a, b) => sameOrUnder(a, b) || sameOrUnder(b, a);
|
|
|
44
44
|
* @param {{trim?: boolean, collapse?: boolean, dot?: boolean, glob?: string|null, fold?: boolean}} [options]
|
|
45
45
|
*/
|
|
46
46
|
export const normPath = (p, { trim = false, collapse = false, dot = false, glob = null, fold = false } = {}) => {
|
|
47
|
-
let s =
|
|
47
|
+
let s = p == null ? '' : String(p);
|
|
48
48
|
if (trim) s = s.trim();
|
|
49
49
|
s = slash(s);
|
|
50
50
|
if (collapse) s = s.replace(/\/{2,}/g, '/');
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
// directory or from the Work directory name outside this module.
|
|
18
18
|
import fs from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
20
|
-
import { skillRoot
|
|
21
|
-
import { tempRoot } from '../../engine/temp-root.mjs';
|
|
20
|
+
import { skillRoot } from '../../engine/runtime-root.mjs';
|
|
22
21
|
|
|
23
|
-
export { tempRoot
|
|
22
|
+
export { tempRoot } from '../../engine/temp-root.mjs';
|
|
23
|
+
export { starciLocalRoot, starciSourceRoot } from '../../engine/runtime-root.mjs';
|
|
24
24
|
|
|
25
25
|
/** The runtime tree this code runs from. */
|
|
26
26
|
const runtimeTree = skillRoot;
|
|
@@ -161,7 +161,7 @@ function readmeBadgeText(text, manifestText) {
|
|
|
161
161
|
let url;
|
|
162
162
|
try { url = new URL(address); } catch { return image; }
|
|
163
163
|
if (!hosts.has(url.host) || url.username || url.password) return image;
|
|
164
|
-
const badgePath = url.host === 'codecov.io' ?
|
|
164
|
+
const badgePath = url.host === 'codecov.io' ? url.pathname.endsWith('/graph/badge.svg') : url.pathname === '/api/project_badges/measure';
|
|
165
165
|
if (!badgePath) return image;
|
|
166
166
|
const fragment = address.indexOf('#');
|
|
167
167
|
const queryEnd = fragment < 0 ? address.length : fragment;
|
|
@@ -4,8 +4,10 @@ import { composeEntries } from './compose-values.mjs';
|
|
|
4
4
|
import { repositoryOf } from './stack-services.mjs';
|
|
5
5
|
|
|
6
6
|
const PINNED_IMAGE = /@sha256:[a-f0-9]{64}$/i;
|
|
7
|
-
const VERSIONED_SECRET = /-v[1-9]
|
|
8
|
-
const
|
|
7
|
+
const VERSIONED_SECRET = /-v[1-9]\d*$/;
|
|
8
|
+
const BACKING_IMAGE_SQL_AND_CACHE = 'postgres(?:ql)?|postgresql-repmgr|redis(?:-stack(?:-server)?)?|mysql(?:-server)?|mongo(?:db)?|mongodb-community-server';
|
|
9
|
+
const BACKING_IMAGE_BROKERS_AND_OTHERS = '(?:cp-)?kafka(?:-native)?|keycloak|minio|rabbitmq';
|
|
10
|
+
const BACKING_IMAGE = new RegExp(`^(?:${BACKING_IMAGE_SQL_AND_CACHE}|${BACKING_IMAGE_BROKERS_AND_OTHERS})$`, 'i');
|
|
9
11
|
const mapping = value => plain(value) ? value : {};
|
|
10
12
|
const present = value => typeof value === 'string' && Boolean(value.trim());
|
|
11
13
|
const configured = value => Object.keys(mapping(value)).length > 0;
|