@starci/hfs 5.0.5 → 5.0.6
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 +6 -0
- package/README.md +22 -2
- package/cache/session.mjs +1 -1
- package/cache/test-family.d.mts +1 -1
- package/cache/test-family.mjs +15 -7
- package/ci/cli.d.mts +7 -1
- package/ci/cli.mjs +37 -6
- package/ci/github.d.mts +1 -0
- package/ci/secret.d.mts +10 -1
- package/ci/secret.mjs +26 -6
- package/ci/verify.d.mts +9 -1
- package/ci/verify.mjs +5 -3
- package/package.json +1 -1
- package/release/candidate-ci.mjs +1 -1
- package/release/gate.d.mts +1 -0
- package/release/gate.mjs +76 -18
- package/release/index.d.mts +1 -1
- package/release/index.mjs +7 -3
- package/runtime/knowledge/hfs/canon-pins.yaml +6 -6
- package/runtime/knowledge/hfs/rules.yaml +2 -2
- package/runtime/modules/kernel/failure-codes.yaml +4 -4
- package/runtime/modules/schemas/application-stacks.schema.yaml +10 -7
- package/runtime/scripts/gates/starcistacks-ci.mjs +3 -1
- package/runtime/scripts/gates/starcistacks-form.d.mts +1 -0
- package/runtime/scripts/gates/starcistacks.d.mts +1 -0
- package/runtime/scripts/gates/starcistacks.mjs +22 -10
- package/runtime/scripts/hfs/architecture/contract-bounds.d.mts +3 -0
- package/runtime/scripts/hfs/architecture/contract-bounds.mjs +72 -0
- package/runtime/scripts/hfs/architecture/index.mjs +1 -1
- package/runtime/scripts/hfs/architecture/tiers.d.mts +2 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +45 -4
- package/runtime/scripts/hfs/ci-declaration.d.mts +47 -1
- package/runtime/scripts/hfs/ci-declaration.mjs +115 -3
- package/runtime/scripts/hfs/repo-identity.d.mts +5 -1
- package/runtime/scripts/hfs/repo-identity.mjs +15 -10
- package/runtime/scripts/hfs/rules/ci-custody.mjs +21 -32
- package/runtime/scripts/hfs/rules/env-parity.mjs +15 -6
- package/runtime/scripts/hfs/secret-standard.d.mts +1 -0
- package/runtime/scripts/hfs/secret-standard.mjs +25 -4
- package/runtime/scripts/hfs/secret-store.d.mts +5 -0
- package/runtime/scripts/hfs/secret-store.mjs +31 -0
- package/src/main.mjs +1 -1
- package/sync/ci-commands.mjs +2 -1
- package/sync/quality-services.mjs +3 -2
- package/sync/readme-badges.mjs +1 -1
- package/sync/sonar-key.d.mts +2 -3
- package/sync/sonar-key.mjs +7 -14
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +7 -3
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.0.6
|
|
4
|
+
|
|
5
|
+
- Changed: Support sealed per-project quality-service credentials and preserve valid composite cache entries and reused spec records.
|
|
6
|
+
|
|
7
|
+
- Fixed: Preserve generated-contract provenance in front-end tier and bounds checks.
|
|
8
|
+
|
|
3
9
|
## 5.0.5
|
|
4
10
|
|
|
5
11
|
- Changed: Govern each app-check linter, compiler, test discovery and coverage, contract emitter and boot child through the CLI execution port with actual worker limits and complete output capture. Preserve cache identities, closure, flush behavior and coverage thresholds. Record source-proven native Page body layouts with their contextual constraints.
|
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ starci app slot <path> [--cwd <dir>] [--json]
|
|
|
26
26
|
starci app emit [--cwd <dir>]
|
|
27
27
|
starci app db drift [--db-url-env <NAME>] [--json] [--cwd <dir>]
|
|
28
28
|
starci app secret init | <set|get|unset|rename|remove|materialize|list|verify|recipients|migrate|split|audit|server-key> --env <env> ... [--cwd <dir>]
|
|
29
|
+
starci app ci secret set <NAME> [--from-custody] [--env <env>] [--repo <owner/name>] [--execute]
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
`starci app add` writes every front-end unit, in either edition, already in the shape of the harness's `contexts/code/fe`
|
|
@@ -227,8 +228,9 @@ read until `migrate` moves it (`runtime/scripts/lib/sealed-member.mjs` names bot
|
|
|
227
228
|
seals every named key in one sops write (`runtime/scripts/hfs/secret.mjs`): on a terminal it prompts `<KEY>: ` for each with
|
|
228
229
|
masked input (one `*` per character; a paste, backspace, Enter, Ctrl+C to cancel with nothing written), redirected stdin gives
|
|
229
230
|
one key its whole text or several one `KEY=value` line each, `--from-file KEY=path` reads a key byte for byte
|
|
230
|
-
(`secret-prompt.mjs`), and `--generate <bytes>` makes a random value per key.
|
|
231
|
-
|
|
231
|
+
(`secret-prompt.mjs`), and `--generate <bytes>` makes a random value per key. Keys declared by exact service credential
|
|
232
|
+
references stay with that consumer; other keys are declared in the environment's `secrets:` of a compose environment
|
|
233
|
+
(`declareKeys`, `secret-standard.mjs`), while a swarm environment's are named to wire by hand.
|
|
232
234
|
A member is sealed to its environment's rule of the managed `.sops.yaml`; `recipients --env <env> [--add|--remove <age1...|owner>]`
|
|
233
235
|
rewrites that rule (`environmentRecipients`, `secret-init.mjs`), and `init` takes the owner's identity as the app's recipient.
|
|
234
236
|
Re-sealing a member whose every key the set replaces needs no age identity; adding a key to one does, and the identity is found
|
|
@@ -244,6 +246,24 @@ except `get --reveal`.
|
|
|
244
246
|
|
|
245
247
|
## Sonar
|
|
246
248
|
|
|
249
|
+
The product's `.starcistacks/application-stacks.yaml` holds its connection profile: provider, local/hosted mode, public
|
|
250
|
+
URL and `projects` entries. Each stored username, password, private URL or token has a separate `credentials[]` reference
|
|
251
|
+
to its actual sealed member and `key`; a private endpoint uses `host.fromCredential`. A project analysis reference has
|
|
252
|
+
`project: <projects[].key>`, independently of its reference `id`. Operator/admin login keys live in a separate authority
|
|
253
|
+
member and are never selected implicitly by analysis or CI. Change the declared connection profile when moving to a
|
|
254
|
+
hosted/cloud server; usernames, passwords and tokens remain sealed.
|
|
255
|
+
|
|
256
|
+
The CI execution copy is explicit: `services.sonar.ci.secrets: [{name: <CI_NAME>, credential: <analysis-id>}]`, with
|
|
257
|
+
`ci.token: <CI_NAME>` and the name/scope declared in the top-level `ci.secrets`. Declare the exact product credential
|
|
258
|
+
first, seal it with `starci app secret set --env <declared-env> <provider> <KEY>`, then run
|
|
259
|
+
`starci app ci secret set <CI_NAME> --from-custody` to inspect the plan and add `--execute` to stream its selected value
|
|
260
|
+
to the CI provider. A dry run decrypts no credential, reads no stdin and reaches no remote. Custody input uses terminal or
|
|
261
|
+
closed/null stdin; a competing pipe/file channel, even empty, is refused without reading it. A private endpoint has its
|
|
262
|
+
own explicit secret-copy link selected by `ci.hostVar`; public endpoint selectors read CI variables. The native local release opens
|
|
263
|
+
the selected product token through the common SOPS identity loader in memory and passes it only to the scanner child.
|
|
264
|
+
Missing, ambiguous, unopenable, wrong-project or runtime-granted custody is a failure, with no host/global fallback.
|
|
265
|
+
Analysis never creates a project, replaces its history or rotates a token. OIDC Codecov keeps no synthetic token member.
|
|
266
|
+
|
|
247
267
|
One mechanism for both sides: every finding of the canon is imported into Sonar, and the quality gate
|
|
248
268
|
fails while any is open. Nothing is configured per app; the pieces are managed files (`starci app sync`) and this package.
|
|
249
269
|
|
package/cache/session.mjs
CHANGED
|
@@ -137,7 +137,7 @@ export function createDiskSession(options) {
|
|
|
137
137
|
// A file edited while the run read it gets no record.
|
|
138
138
|
const settled = new Map();
|
|
139
139
|
for (const [folder, { records }] of replaced) {
|
|
140
|
-
settled.set(folder, records.filter((record) => index.hashOf(record.path) === undefined || hashFile(path.join(root, record.path)) === record.
|
|
140
|
+
settled.set(folder, records.filter((record) => index.hashOf(record.path) === undefined || hashFile(path.join(root, record.path)) === index.hashOf(record.path)));
|
|
141
141
|
}
|
|
142
142
|
if (trust.kind === 'base') {
|
|
143
143
|
const committed = new Map();
|
package/cache/test-family.d.mts
CHANGED
|
@@ -68,7 +68,7 @@ export interface TestFamilyResult {
|
|
|
68
68
|
readonly ok: boolean;
|
|
69
69
|
readonly ran: readonly string[];
|
|
70
70
|
readonly reused: readonly string[];
|
|
71
|
-
/** Specs that failed
|
|
71
|
+
/** Specs that failed, did not report, or whose inputs changed: nothing of theirs is stored. */
|
|
72
72
|
readonly failed: readonly string[];
|
|
73
73
|
/** The merged lcov, or undefined when no spec has a record. */
|
|
74
74
|
readonly lcov: string | undefined;
|
package/cache/test-family.mjs
CHANGED
|
@@ -83,9 +83,8 @@ export async function runTestFamily(options) {
|
|
|
83
83
|
const reported = new Map(jest.readCoverage(scratch).map((file) => [file.spec, file]));
|
|
84
84
|
for (const spec of toRun) {
|
|
85
85
|
const file = reported.get(posix(path.relative(path.join(session.root, side), path.join(session.root, spec))));
|
|
86
|
-
const key =
|
|
87
|
-
|
|
88
|
-
if (file?.passed === true && key === keys.get(spec))
|
|
86
|
+
const key = keys.get(spec);
|
|
87
|
+
if (file?.passed === true && key !== undefined)
|
|
89
88
|
fresh.push(recordOf(spec, key, file.sources, side));
|
|
90
89
|
else
|
|
91
90
|
failed.push(spec);
|
|
@@ -96,9 +95,18 @@ export async function runTestFamily(options) {
|
|
|
96
95
|
fs.rmSync(scratch, { recursive: true, force: true });
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
|
-
|
|
98
|
+
// Recheck every candidate against disk, including a reused spec whose inputs another running spec could change.
|
|
99
|
+
const records = [...kept, ...fresh].filter((record) => {
|
|
100
|
+
const key = specKey({ spec: record.path, closure: jest.closureOf(record.path), configHash: jest.configHash, hashOf: (file) => hashFile(path.join(session.root, file)) });
|
|
101
|
+
if (key === keys.get(record.path))
|
|
102
|
+
return true;
|
|
103
|
+
failed.push(record.path);
|
|
104
|
+
return false;
|
|
105
|
+
});
|
|
106
|
+
const settled = new Set(records.map((record) => record.path));
|
|
107
|
+
const reused = kept.filter((record) => settled.has(record.path));
|
|
108
|
+
progress.tick(jest.specs.length, reused.length, toRun.length);
|
|
100
109
|
progress.end();
|
|
101
|
-
const records = [...kept, ...fresh];
|
|
102
110
|
session.replace(TEST_FOLDER, records);
|
|
103
111
|
let lcov;
|
|
104
112
|
let threshold;
|
|
@@ -117,13 +125,13 @@ export async function runTestFamily(options) {
|
|
|
117
125
|
mode: decision.mode,
|
|
118
126
|
reason: decision.reason,
|
|
119
127
|
checked: toRun.length,
|
|
120
|
-
reused:
|
|
128
|
+
reused: reused.length,
|
|
121
129
|
newFindings: failed.length + thresholdFindings,
|
|
122
130
|
knownFindings: 0,
|
|
123
131
|
fixedFindings: 0,
|
|
124
132
|
};
|
|
125
133
|
session.report(report);
|
|
126
|
-
return { ok: failed.length === 0 && (threshold?.ok ?? records.length === 0), ran: toRun, reused:
|
|
134
|
+
return { ok: failed.length === 0 && (threshold?.ok ?? records.length === 0), ran: toRun, reused: reused.map((record) => record.path), failed, lcov, threshold, report, output };
|
|
127
135
|
}
|
|
128
136
|
/** The digest of a project config with host paths turned into `<root>` and the keys that differ per host dropped. */
|
|
129
137
|
export function projectConfigHash(config) {
|
package/ci/cli.d.mts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { type SecretSops } from '../runtime/scripts/hfs/secret-sops.mjs';
|
|
2
|
+
import type { AgeHost } from '../runtime/scripts/hfs/secret-init.mjs';
|
|
1
3
|
import type { GhSpawn, StdinSource } from './github.mjs';
|
|
2
4
|
/** The commands of `starci app ci` (besides `local`) with the options each takes; the parser refuses every other. */
|
|
3
5
|
export declare const COMMANDS: Record<string, string[]>;
|
|
4
6
|
export declare const BOOLEAN: Set<string>;
|
|
5
7
|
/** Manifest and sealed key reads remain offline; only github.mjs may invoke the GitHub CLI. */
|
|
6
|
-
export declare function ciMain(argv: string[], { cwd, stdout, stderr, stdin, env, tools }?: {
|
|
8
|
+
export declare function ciMain(argv: string[], { cwd, stdout, stderr, stdin, env, tools, hostIdentity, sops, platform, inputRedirected }?: {
|
|
7
9
|
cwd?: string | undefined;
|
|
8
10
|
stdout?: ((text: string) => unknown) | undefined;
|
|
9
11
|
stderr?: ((text: string) => unknown) | undefined;
|
|
@@ -12,4 +14,8 @@ export declare function ciMain(argv: string[], { cwd, stdout, stderr, stdin, env
|
|
|
12
14
|
tools?: {
|
|
13
15
|
spawn?: GhSpawn | undefined;
|
|
14
16
|
} | undefined;
|
|
17
|
+
hostIdentity?: AgeHost | null | undefined;
|
|
18
|
+
sops?: Pick<SecretSops, 'decrypt'> | null | undefined;
|
|
19
|
+
platform?: NodeJS.Platform | undefined;
|
|
20
|
+
inputRedirected?: (() => boolean) | undefined;
|
|
15
21
|
}): Promise<number>;
|
package/ci/cli.mjs
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseYaml } from '../runtime/engine/yaml.mjs';
|
|
4
|
-
import { isValidCiDeclaration, readCiDeclaration, sealedKeyNames } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
4
|
+
import { isValidCiDeclaration, readCiDeclaration, sealedKeyNames, serviceBindings, serviceCredentialNames, runtimeGranted } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
5
|
+
import { repositoryName, identityQueryEnvironment } from '../runtime/scripts/hfs/repo-identity.mjs';
|
|
6
|
+
import { serviceCredentialFile, serviceCredentialValue } from '../runtime/scripts/hfs/secret-store.mjs';
|
|
7
|
+
import { defaultSops } from '../runtime/scripts/hfs/secret-sops.mjs';
|
|
5
8
|
import { github, refuse } from './github.mjs';
|
|
6
9
|
import { setSecret } from './secret.mjs';
|
|
7
10
|
import { setVariable, syncVariables } from './vars.mjs';
|
|
8
11
|
import { badgeVisibilityNotices, verifyCi } from './verify.mjs';
|
|
9
12
|
/** The commands of `starci app ci` (besides `local`) with the options each takes; the parser refuses every other. */
|
|
10
13
|
export const COMMANDS = {
|
|
11
|
-
'secret set': ['env', 'dependabot', 'repo', 'execute'],
|
|
14
|
+
'secret set': ['env', 'dependabot', 'repo', 'execute', 'from-custody'],
|
|
12
15
|
'var set': ['env', 'value', 'repo', 'execute'],
|
|
13
16
|
'var sync': ['execute', 'repo'],
|
|
14
17
|
verify: ['json', 'repo'],
|
|
15
18
|
};
|
|
16
|
-
export const BOOLEAN = new Set(['dependabot', 'execute', 'json']);
|
|
19
|
+
export const BOOLEAN = new Set(['dependabot', 'execute', 'json', 'from-custody']);
|
|
17
20
|
/** Invalid options never echo argv, because an extra argument may itself contain a secret. */
|
|
18
21
|
function parse(argv) {
|
|
19
22
|
const command = argv[0] === 'verify' ? 'verify' : argv.slice(0, 2).join(' ');
|
|
@@ -52,7 +55,7 @@ function parse(argv) {
|
|
|
52
55
|
return { command, options: options, name: positional[0] };
|
|
53
56
|
}
|
|
54
57
|
/** Manifest and sealed key reads remain offline; only github.mjs may invoke the GitHub CLI. */
|
|
55
|
-
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 = {} } = {}) {
|
|
58
|
+
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 = {}, hostIdentity, sops, platform = process.platform, inputRedirected } = {}) {
|
|
56
59
|
// `ci local` runs the rendered CI on this machine (local.mjs): no manifest custody and no GitHub.
|
|
57
60
|
if (argv[0] === 'local')
|
|
58
61
|
return (await import('./local.mjs')).ciLocalMain(argv.slice(1), { cwd, stdout, stderr, env });
|
|
@@ -69,8 +72,36 @@ export async function ciMain(argv, { cwd = process.cwd(), stdout = (s) => proces
|
|
|
69
72
|
}
|
|
70
73
|
if (!isValidCiDeclaration(declaration))
|
|
71
74
|
throw refuse('CI_DECLARATION_INVALID', 'correct the CI declaration before using GitHub');
|
|
72
|
-
const
|
|
73
|
-
const
|
|
75
|
+
const childEnv = identityQueryEnvironment(env, serviceCredentialNames(doc));
|
|
76
|
+
const repository = repositoryName(cwd, { env: childEnv }), selected = serviceBindings(doc, repository);
|
|
77
|
+
if (selected.problems.length)
|
|
78
|
+
throw refuse('CI_CUSTODY_INVALID', 'correct the explicit project service credential bindings before using CI');
|
|
79
|
+
const runtime = sealed.filter((entry) => runtimeGranted(doc, entry.env, entry.member, entry.key));
|
|
80
|
+
if (parsed.command.endsWith(' set') && runtime.some((entry) => entry.key === parsed.name))
|
|
81
|
+
throw refuse('APP_SECRET_IN_CI', 'the selected key belongs to app runtime authority');
|
|
82
|
+
if (parsed.options['from-custody']) {
|
|
83
|
+
const binding = selected.bindings.find((item) => item.name === parsed.name);
|
|
84
|
+
if (!binding || parsed.options.dependabot)
|
|
85
|
+
throw refuse('CI_CUSTODY_INVALID', 'one project service binding is required');
|
|
86
|
+
try {
|
|
87
|
+
serviceCredentialFile(cwd, binding);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw refuse('CI_CUSTODY_INVALID', 'the selected project member is missing, ambiguous or outside this product');
|
|
91
|
+
}
|
|
92
|
+
if (parsed.options.repo && (parsed.options.repo.split('/')[1] !== repository || (doc.ci?.github?.repository && parsed.options.repo !== doc.ci.github.repository)))
|
|
93
|
+
throw refuse('CI_CUSTODY_INVALID', 'the destination repository differs from this project');
|
|
94
|
+
}
|
|
95
|
+
const gh = github({ cwd, env: childEnv, spawn: tools.spawn });
|
|
96
|
+
const custody = (binding) => {
|
|
97
|
+
try {
|
|
98
|
+
return serviceCredentialValue(cwd, binding, sops ?? defaultSops(env, cwd, platform, undefined, hostIdentity));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
throw refuse('CI_CUSTODY_UNAVAILABLE', 'the selected service member could not be opened with the owner identity');
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const context = { ...parsed, declaration, sealed, runtime, gh, stdin, stdout, bindings: selected.bindings, custody, inputRedirected };
|
|
74
105
|
if (parsed.command === 'secret set')
|
|
75
106
|
return await setSecret(context);
|
|
76
107
|
if (parsed.command === 'var set')
|
package/ci/github.d.mts
CHANGED
|
@@ -11,6 +11,7 @@ export interface CiOptions extends CiScope {
|
|
|
11
11
|
value?: string | undefined;
|
|
12
12
|
execute?: boolean | undefined;
|
|
13
13
|
json?: boolean | undefined;
|
|
14
|
+
'from-custody'?: boolean | undefined;
|
|
14
15
|
}
|
|
15
16
|
/** One declared CI secret or variable of a declaration without problems (runtime/scripts/hfs/ci-declaration.mjs readCiDeclaration). */
|
|
16
17
|
export type CiEntry = ValidCiEntry;
|
package/ci/secret.d.mts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { CiDeclaration, CiEntry, CiOptions, Gh, StdinSource } from './github.mjs';
|
|
2
|
+
import { type ServiceBinding } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
3
|
+
/** An input pipe or file is an input source even before its bytes arrive; fstat never consumes its value. */
|
|
4
|
+
export declare function redirectedSecretInput(): boolean;
|
|
2
5
|
/** A declared name must belong to the requested store before a secret body is read. */
|
|
3
6
|
export declare function declaredEntry(entries: readonly CiEntry[], name: string, options: CiOptions): CiEntry;
|
|
4
7
|
/** Secret input is read once, stripped of one trailing newline, and passed only to the child stdin; without options.execute nothing is read or written. */
|
|
5
|
-
export declare function setSecret({ declaration, sealed, gh, options, name, stdin, stdout }: {
|
|
8
|
+
export declare function setSecret({ declaration, sealed, gh, options, name, stdin, stdout, bindings, custody, inputRedirected }: {
|
|
6
9
|
declaration: CiDeclaration;
|
|
7
10
|
sealed: readonly {
|
|
11
|
+
env?: string;
|
|
12
|
+
member?: string;
|
|
13
|
+
file?: string;
|
|
8
14
|
key: string;
|
|
9
15
|
}[];
|
|
10
16
|
gh: Gh;
|
|
@@ -12,4 +18,7 @@ export declare function setSecret({ declaration, sealed, gh, options, name, stdi
|
|
|
12
18
|
name: string;
|
|
13
19
|
stdin: StdinSource;
|
|
14
20
|
stdout: (text: string) => unknown;
|
|
21
|
+
bindings?: ServiceBinding[];
|
|
22
|
+
custody?: ((binding: ServiceBinding) => string) | undefined;
|
|
23
|
+
inputRedirected?: (() => boolean) | undefined;
|
|
15
24
|
}): Promise<number>;
|
package/ci/secret.mjs
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { refuse, scopeArgs, scopeName } from './github.mjs';
|
|
3
|
+
import { boundSealedKey } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
4
|
+
/** An input pipe or file is an input source even before its bytes arrive; fstat never consumes its value. */
|
|
5
|
+
export function redirectedSecretInput() {
|
|
6
|
+
try {
|
|
7
|
+
const input = fs.fstatSync(0);
|
|
8
|
+
return !input.isCharacterDevice();
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
3
14
|
/** A declared name must belong to the requested store before a secret body is read. */
|
|
4
15
|
export function declaredEntry(entries, name, options) {
|
|
5
16
|
const entry = entries.find((item) => item.name === name);
|
|
@@ -11,24 +22,33 @@ export function declaredEntry(entries, name, options) {
|
|
|
11
22
|
return entry;
|
|
12
23
|
}
|
|
13
24
|
/** Secret input is read once, stripped of one trailing newline, and passed only to the child stdin; without options.execute nothing is read or written. */
|
|
14
|
-
export async function setSecret({ declaration, sealed, gh, options, name, stdin, stdout }) {
|
|
15
|
-
if (sealed.some((item) => item.key === name))
|
|
16
|
-
throw refuse('CI_SECRET_SEALED', 'the name belongs to a sealed app secret');
|
|
25
|
+
export async function setSecret({ declaration, sealed, gh, options, name, stdin, stdout, bindings = [], custody, inputRedirected = redirectedSecretInput }) {
|
|
17
26
|
declaredEntry(options.dependabot ? declaration.dependabotSecrets : declaration.secrets, name, options);
|
|
27
|
+
const binding = options.dependabot ? undefined : bindings.find((item) => item.name === name);
|
|
28
|
+
if (sealed.some((item) => (item.key === name || (binding && item.key === binding.key)) && !(binding && boundSealedKey(binding, item))))
|
|
29
|
+
throw refuse('CI_SECRET_SEALED', 'the name or selected key belongs to unrelated sealed app custody');
|
|
30
|
+
if (binding && options['from-custody'] !== true)
|
|
31
|
+
throw refuse('CI_CUSTODY_REQUIRED', 'this service secret is an execution copy; use --from-custody');
|
|
32
|
+
if (options['from-custody'] && (!binding || !custody))
|
|
33
|
+
throw refuse('CI_CUSTODY_INVALID', 'one explicit project service credential binding is required');
|
|
34
|
+
if (options['from-custody'] && (stdin !== undefined || inputRedirected()))
|
|
35
|
+
throw refuse('CI_VALUE_AMBIGUOUS', 'choose custody or stdin, never both');
|
|
18
36
|
// Dry until --execute: the name is judged, but no value is read and GitHub is not reached; the line never carries a value.
|
|
19
37
|
if (options.execute !== true) {
|
|
20
|
-
stdout(`dry run: would set secret ${name} (${scopeName(options)}) from the value on stdin; add --execute\n`);
|
|
38
|
+
stdout(`dry run: would set secret ${name} (${scopeName(options)}) from ${binding ? 'its declared service custody' : 'the value on stdin'}; add --execute\n`);
|
|
21
39
|
return 0;
|
|
22
40
|
}
|
|
23
41
|
let value;
|
|
24
|
-
if (
|
|
42
|
+
if (binding && custody)
|
|
43
|
+
value = custody(binding);
|
|
44
|
+
else if (stdin === undefined) {
|
|
25
45
|
if (process.stdin.isTTY)
|
|
26
46
|
throw refuse('CI_VALUE_EMPTY', 'a secret value is required on stdin');
|
|
27
47
|
value = fs.readFileSync(0, 'utf8');
|
|
28
48
|
}
|
|
29
49
|
else
|
|
30
50
|
value = typeof stdin === 'function' ? await stdin() : stdin;
|
|
31
|
-
value = String(value ?? '').replace(/\r?\n$/, '');
|
|
51
|
+
value = binding ? value : String(value ?? '').replace(/\r?\n$/, '');
|
|
32
52
|
if (!value.length)
|
|
33
53
|
throw refuse('CI_VALUE_EMPTY', 'a secret value is required on stdin');
|
|
34
54
|
const repo = gh.repository(options.repo);
|
package/ci/verify.d.mts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import type { CiDeclaration, CiOptions, Gh } from './github.mjs';
|
|
2
|
+
import { type ServiceBinding } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
2
3
|
/** Verification reports names and scopes only; values are compared in memory and excluded from findings. */
|
|
3
|
-
export declare function verifyCi({ declaration, sealed, gh, options, stdout, notices }: {
|
|
4
|
+
export declare function verifyCi({ declaration, sealed, gh, options, stdout, notices, bindings, runtime }: {
|
|
4
5
|
declaration: CiDeclaration;
|
|
5
6
|
sealed: readonly {
|
|
7
|
+
env?: string;
|
|
8
|
+
member?: string;
|
|
9
|
+
file?: string;
|
|
6
10
|
key: string;
|
|
7
11
|
}[];
|
|
8
12
|
gh: Gh;
|
|
9
13
|
options: CiOptions;
|
|
10
14
|
stdout: (text: string) => unknown;
|
|
11
15
|
notices?: string[];
|
|
16
|
+
bindings?: ServiceBinding[];
|
|
17
|
+
runtime?: readonly {
|
|
18
|
+
key: string;
|
|
19
|
+
}[];
|
|
12
20
|
}): number;
|
|
13
21
|
/** Manifest visibility wins; optional GitHub visibility discovery never changes verification findings or errors. */
|
|
14
22
|
export declare function badgeVisibilityNotices({ doc, gh, repo }: {
|
package/ci/verify.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { badgeTokenNotices } from '../sync/readme-badges.mjs';
|
|
2
2
|
import { refuse, scopeArgs, scopeName } from './github.mjs';
|
|
3
|
+
import { boundSealedKey } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
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, notices = [] }) {
|
|
5
|
+
export function verifyCi({ declaration, sealed, gh, options, stdout, notices = [], bindings = [], runtime = [] }) {
|
|
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 }]) {
|
|
@@ -12,7 +13,7 @@ export function verifyCi({ declaration, sealed, gh, options, stdout, notices = [
|
|
|
12
13
|
stores.push({ scope, kind, rows });
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
|
-
const findings = []
|
|
16
|
+
const findings = [];
|
|
16
17
|
const declarations = (store) => store.scope.dependabot ? declaration.dependabotSecrets : store.kind === 'secret' ? declaration.secrets : declaration.vars;
|
|
17
18
|
const matches = (entry, scope) => (entry.scope === 'environment' ? entry.environment : undefined) === scope.env;
|
|
18
19
|
const add = (code, name, scope) => { findings.push({ code, name, scope: scopeName(scope) }); };
|
|
@@ -26,7 +27,8 @@ export function verifyCi({ declaration, sealed, gh, options, stdout, notices = [
|
|
|
26
27
|
add('CI_VAR_DRIFT', entry.name, store.scope);
|
|
27
28
|
}
|
|
28
29
|
for (const row of store.rows) {
|
|
29
|
-
|
|
30
|
+
const binding = store.scope.dependabot ? undefined : bindings.find((item) => item.name === row.name);
|
|
31
|
+
if (runtime.some((item) => item.key === row.name) || (store.kind === 'secret' && sealed.some((item) => (item.key === row.name || (binding && item.key === binding.key)) && !(binding && boundSealedKey(binding, item)))))
|
|
30
32
|
add('APP_SECRET_IN_CI', row.name, store.scope);
|
|
31
33
|
if (entries.some((entry) => entry.name === row.name && matches(entry, store.scope)))
|
|
32
34
|
continue;
|
package/package.json
CHANGED
package/release/candidate-ci.mjs
CHANGED
|
@@ -8,7 +8,7 @@ const PASSING = new Set(['success', 'neutral']);
|
|
|
8
8
|
export const candidateRef = (tag, prefix = 'v') => `${CANDIDATE_PREFIX}${String(tag).startsWith(prefix) ? String(tag).slice(prefix.length) : String(tag)}`;
|
|
9
9
|
function run(ctx, command, args) {
|
|
10
10
|
try {
|
|
11
|
-
return ctx.spawn(command, args, { cwd: ctx.cwd });
|
|
11
|
+
return ctx.spawn(command, args, { cwd: ctx.cwd, env: ctx.env });
|
|
12
12
|
}
|
|
13
13
|
catch {
|
|
14
14
|
return { status: null };
|
package/release/gate.d.mts
CHANGED
|
@@ -72,6 +72,7 @@ export declare const refName: (value: unknown) => value is string;
|
|
|
72
72
|
export declare function normalizeRelease(block: unknown, scripts?: Record<string, unknown> | undefined, services?: Services, options?: {
|
|
73
73
|
example?: boolean;
|
|
74
74
|
entities?: boolean;
|
|
75
|
+
repository?: string;
|
|
75
76
|
}): {
|
|
76
77
|
config: ReleaseConfig;
|
|
77
78
|
problems?: undefined;
|
package/release/gate.mjs
CHANGED
|
@@ -7,7 +7,9 @@ import { createHash } from 'node:crypto';
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { isPlainObject as plain } from '../runtime/engine/plain-object.mjs';
|
|
10
|
-
import { qualityServices } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
10
|
+
import { qualityServices, serviceBindings, projectServiceCredential, serviceCredentialNames } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
11
|
+
import { repositoryName, identityQueryEnvironment } from '../runtime/scripts/hfs/repo-identity.mjs';
|
|
12
|
+
import { serviceCredentialFile, serviceCredentialValue } from '../runtime/scripts/hfs/secret-store.mjs';
|
|
11
13
|
import { openHfs } from '../runtime/scripts/hfs/slots.mjs';
|
|
12
14
|
import { firstLine, lines, tool } from '../stack/context.mjs';
|
|
13
15
|
import { failure, refusal } from '../stack/errors.mjs';
|
|
@@ -74,21 +76,72 @@ function auditGate(audit) {
|
|
|
74
76
|
throw refusal(RELEASE_CODE, 'services.audit.level must be an npm audit level');
|
|
75
77
|
return { name: 'audit', command: ['npm', 'audit', ...(audit.omitDev ? ['--omit=dev'] : []), `--audit-level=${audit.level}`], summary: null, env: {} };
|
|
76
78
|
}
|
|
77
|
-
/**
|
|
78
|
-
function sonarGate(services,
|
|
79
|
+
/** The CI execution copy is job-local; a native owner run opens the selected product credential only when executed. */
|
|
80
|
+
function sonarGate(services, ctx, execute) {
|
|
81
|
+
const env = ctx.env;
|
|
79
82
|
const configured = services?.sonar;
|
|
80
|
-
|
|
83
|
+
let sonar = configured?.provider === 'sonarcloud' ? { ...configured, host: configured.host || 'https://sonarcloud.io' } : configured;
|
|
81
84
|
const tokenName = sonar?.token;
|
|
82
|
-
if (!sonar
|
|
83
|
-
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'declare an enabled services.sonar host and
|
|
84
|
-
|
|
85
|
+
if (!sonar || !ENV_KEY.test(tokenName))
|
|
86
|
+
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'declare an enabled services.sonar host and ci.token selector');
|
|
87
|
+
let token;
|
|
88
|
+
let privateHost;
|
|
89
|
+
if (env.GITHUB_ACTIONS === 'true') {
|
|
90
|
+
token = env[tokenName];
|
|
91
|
+
if (!token)
|
|
92
|
+
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'the declared Sonar CI execution copy is missing');
|
|
93
|
+
if (sonar.hostCredential) {
|
|
94
|
+
privateHost = env[sonar.hostVar || 'SONAR_HOST_URL'];
|
|
95
|
+
sonar = { ...sonar, host: privateHost ?? null, privateHost: true };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const doc = services.declaration;
|
|
100
|
+
const repository = repositoryName(ctx.cwd, { env: identityQueryEnvironment(env, serviceCredentialNames(doc)) });
|
|
101
|
+
const selected = serviceBindings(doc, repository);
|
|
102
|
+
const binding = selected.bindings.find((item) => item.service === 'sonar' && item.name === tokenName);
|
|
103
|
+
if (selected.problems.length || !binding)
|
|
104
|
+
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'declare one explicit Sonar project credential binding in this product');
|
|
105
|
+
try {
|
|
106
|
+
serviceCredentialFile(ctx.cwd, binding);
|
|
107
|
+
if (execute)
|
|
108
|
+
token = serviceCredentialValue(ctx.cwd, binding, ctx.sops);
|
|
109
|
+
if (sonar.hostCredential) {
|
|
110
|
+
const endpoint = projectServiceCredential(doc, 'sonar', sonar.hostCredential, repository);
|
|
111
|
+
if (!endpoint)
|
|
112
|
+
throw new Error('invalid endpoint reference');
|
|
113
|
+
serviceCredentialFile(ctx.cwd, endpoint);
|
|
114
|
+
if (execute)
|
|
115
|
+
privateHost = serviceCredentialValue(ctx.cwd, endpoint, ctx.sops);
|
|
116
|
+
sonar = { ...sonar, host: privateHost ?? null, privateHost: true };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'the selected Sonar project custody is missing or cannot be opened');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!sonar.host && !(sonar.hostCredential && !execute))
|
|
124
|
+
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'the declared Sonar connection URL is missing');
|
|
125
|
+
if (sonar.host) {
|
|
126
|
+
try {
|
|
127
|
+
const url = new URL(sonar.host);
|
|
128
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password)
|
|
129
|
+
throw new Error('invalid URL');
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'the Sonar connection must be HTTP(S), with login kept in separate sealed credential keys');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const command = ['npx', '--yes', '@sonar/scan@4.3.0', ...(sonar.privateHost ? [] : [`-Dsonar.host.url=${sonar.host}`])];
|
|
136
|
+
if (sonar.projectKey)
|
|
137
|
+
command.push(`-Dsonar.projectKey=${sonar.projectKey}`);
|
|
85
138
|
const organization = sonar.organization ?? (sonar.organizationVar ? env[sonar.organizationVar] : null);
|
|
86
139
|
if (organization)
|
|
87
140
|
command.push(`-Dsonar.organization=${organization}`);
|
|
88
141
|
if (!command.every((word) => TOKEN.test(word)))
|
|
89
142
|
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'Sonar host and organization must be plain command arguments');
|
|
90
|
-
return { name: 'sonar', command, summary: null, env: { SONAR_TOKEN:
|
|
91
|
-
credentialNames: [...new Set(['SONAR_TOKEN', tokenName])] };
|
|
143
|
+
return { name: 'sonar', command, summary: null, env: { ...(token === undefined ? {} : { SONAR_TOKEN: token }), ...(privateHost ? { SONAR_HOST_URL: privateHost } : {}) }, provider: sonar,
|
|
144
|
+
credentialNames: [...new Set(['SONAR_TOKEN', tokenName, ...serviceCredentialNames(services.declaration), ...(sonar.hostCredential ? ['SONAR_HOST_URL', sonar.hostVar] : [])].filter((name) => typeof name === 'string'))] };
|
|
92
145
|
}
|
|
93
146
|
function gateProblems(entry, at, seen) {
|
|
94
147
|
const found = [];
|
|
@@ -261,7 +314,7 @@ async function sonarAvailability(ctx, gate) {
|
|
|
261
314
|
}
|
|
262
315
|
if (up)
|
|
263
316
|
return null;
|
|
264
|
-
return { pass: false, detail: `${SONAR_UNREACHABLE}: ${sonar.host} did not report UP within ten seconds` };
|
|
317
|
+
return { pass: false, detail: `${SONAR_UNREACHABLE}: ${sonar.privateHost ? 'the declared private host' : sonar.host} did not report UP within ten seconds` };
|
|
265
318
|
}
|
|
266
319
|
/** An unreachable Sonar host stays a red row with a real log and no scan invocation. */
|
|
267
320
|
export async function runAvailableGate(ctx, gate, options) {
|
|
@@ -292,12 +345,18 @@ async function judgeQualityGate(ctx, gate, row) {
|
|
|
292
345
|
return row;
|
|
293
346
|
const started = ctx.now();
|
|
294
347
|
const verdict = await sonarQualityGate(ctx, { cwd: ctx.cwd, host: gate.provider.host, token: gate.env.SONAR_TOKEN });
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
348
|
+
const detail = redactGateText(gate, verdict.detail);
|
|
349
|
+
const failed = (verdict.conditions ?? []).map((line) => ` failed: ${redactGateText(gate, line)}\n`).join('');
|
|
350
|
+
fs.appendFileSync(row.log, `\nquality gate: ${detail}\n${failed}`);
|
|
351
|
+
return { ...row, pass: verdict.pass, detail, seconds: row.seconds + Math.round((ctx.now() - started) / 1000), logSha256: sha256(row.log) };
|
|
352
|
+
}
|
|
353
|
+
/** Scanner output and the later server verdict share one redaction boundary. */
|
|
354
|
+
function redactGateText(gate, text) {
|
|
355
|
+
const values = gate.provider ? [gate.env.SONAR_TOKEN, ...(gate.provider.privateHost ? [gate.env.SONAR_HOST_URL] : [])].filter((value) => Boolean(value)) : [];
|
|
356
|
+
return values.reduce((redacted, value) => redacted.replaceAll(value, '[REDACTED]'), text);
|
|
298
357
|
}
|
|
299
358
|
export const withoutCredentials = (values, credentialNames) => {
|
|
300
|
-
const selected = new Set(credentialNames);
|
|
359
|
+
const selected = new Set([...credentialNames, 'SOPS_AGE_KEY', 'SOPS_AGE_KEY_FILE'].map((name) => name.toUpperCase()));
|
|
301
360
|
return Object.fromEntries(Object.entries(values ?? {}).filter(([key]) => !selected.has(key.toUpperCase())));
|
|
302
361
|
};
|
|
303
362
|
export function runGate(ctx, gate, { dir, platform, credentialNames, preface = '' }) {
|
|
@@ -308,11 +367,10 @@ export function runGate(ctx, gate, { dir, platform, credentialNames, preface = '
|
|
|
308
367
|
const env = { ...ctx.env, ...(gate.provider ? gate.env : withoutCredentials(gate.env, credentialNames)), ...RELEASE_CACHE_ENV };
|
|
309
368
|
const run = tool(ctx, command, args, { cwd: ctx.cwd, env });
|
|
310
369
|
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`;
|
|
311
|
-
const text =
|
|
370
|
+
const text = redactGateText(gate, output);
|
|
312
371
|
fs.writeFileSync(log, preface + text);
|
|
313
372
|
const verdict = judgeVerdict(run, gate, text);
|
|
314
|
-
|
|
315
|
-
verdict.detail = verdict.detail.replaceAll(gate.env.SONAR_TOKEN, '[REDACTED]');
|
|
373
|
+
verdict.detail = redactGateText(gate, verdict.detail);
|
|
316
374
|
const seconds = Math.round((ctx.now() - started) / 1000);
|
|
317
375
|
return {
|
|
318
376
|
name: gate.name, command: gate.command.join(' '), pass: verdict.pass, detail: verdict.detail, seconds,
|
|
@@ -459,7 +517,7 @@ function backfillRed(ctx, { common, urgent, failed, openTicket }) {
|
|
|
459
517
|
export async function releaseGate(ctx, config, options = {}) {
|
|
460
518
|
// Sonar is a row of the managed CI, so it is a row of the local gate whenever the app declares services.sonar; a missing token is a refusal, never a skip.
|
|
461
519
|
if (config.services?.sonar)
|
|
462
|
-
config = { ...config, gates: [...config.gates.filter((gate) => gate.name !== 'sonar'), sonarGate(config.services, ctx.
|
|
520
|
+
config = { ...config, gates: [...config.gates.filter((gate) => gate.name !== 'sonar'), sonarGate(config.services, ctx, options.execute === true)] };
|
|
463
521
|
const credentialNames = config.gates.find(gate => gate.provider)?.credentialNames ?? ['SONAR_TOKEN', config.services?.sonar?.token].filter(Boolean);
|
|
464
522
|
ctx = Object.create(ctx, { env: { value: withoutCredentials(ctx.env, credentialNames) } });
|
|
465
523
|
// The CI verdict of a release is the candidate run of its own sha, judged by `starci app release push` (candidate-ci.mjs, OWNER RULING candidate-ci 2026-10-18):
|
package/release/index.d.mts
CHANGED
|
@@ -42,13 +42,13 @@ export declare function contextOf(io?: VerbIo): VerbContext;
|
|
|
42
42
|
export declare function readDeclaration(root: string): Record<string, any>;
|
|
43
43
|
/** The normalised `release:` block (tolerant of absence) with the package version: { root, version, gates, tagPrefix, defaultBranch, requireCi }. */
|
|
44
44
|
export declare function loadReleaseConfig(root: string): {
|
|
45
|
+
services: any;
|
|
45
46
|
root: string;
|
|
46
47
|
version: any;
|
|
47
48
|
gates: import("./gate.mjs").Gate[];
|
|
48
49
|
tagPrefix: string;
|
|
49
50
|
defaultBranch: string;
|
|
50
51
|
requireCi: boolean;
|
|
51
|
-
services: any;
|
|
52
52
|
};
|
|
53
53
|
/** Parses the flags, runs `run(ctx, flags, positional)` and prints its result; returns the exit code. Only StackError is mapped, anything else is a bug and propagates. */
|
|
54
54
|
export declare function runVerb({ label, name, io, argv, spec, run, code }: {
|
package/release/index.mjs
CHANGED
|
@@ -10,11 +10,13 @@ import path from 'node:path';
|
|
|
10
10
|
import { isPlainObject as plain } from '../runtime/engine/plain-object.mjs';
|
|
11
11
|
import { parseYaml } from '../runtime/engine/yaml.mjs';
|
|
12
12
|
import { isRuntimeExampleApp } from '../runtime/scripts/lib/runtime-example.mjs';
|
|
13
|
+
import { repositoryName, identityQueryEnvironment } from '../runtime/scripts/hfs/repo-identity.mjs';
|
|
14
|
+
import { serviceCredentialNames } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
13
15
|
import { hasEntityList } from '../db/entity-list.mjs';
|
|
14
16
|
import { STACKS_DIR, STACKS_FILE } from '../stack/config.mjs';
|
|
15
17
|
import { stackContext } from '../stack/context.mjs';
|
|
16
18
|
import { EXIT, StackError, refusal } from '../stack/errors.mjs';
|
|
17
|
-
import { RELEASE_CODE, VERSION, normalizeRelease, releaseGate } from './gate.mjs';
|
|
19
|
+
import { RELEASE_CODE, VERSION, normalizeRelease, releaseGate, withoutCredentials } from './gate.mjs';
|
|
18
20
|
import { releasePush } from './push.mjs';
|
|
19
21
|
import { releaseUrgentPush } from './urgent.mjs';
|
|
20
22
|
const IO_KEYS = ['cwd', 'stdout', 'stderr', 'env', 'sops', 'hostIdentity'];
|
|
@@ -77,10 +79,11 @@ export function loadReleaseConfig(root) {
|
|
|
77
79
|
if (typeof pkg.version !== 'string' || !VERSION.test(pkg.version))
|
|
78
80
|
throw refusal(RELEASE_CODE, 'package.json needs a version like 1.2.3 (the release tag derives from it)');
|
|
79
81
|
const declaration = readDeclaration(root);
|
|
80
|
-
const { config, problems } = normalizeRelease(declaration.release, pkg.scripts, declaration.services, { example: isRuntimeExampleApp(root), entities: hasEntityList(root)
|
|
82
|
+
const { config, problems } = normalizeRelease(declaration.release, pkg.scripts, declaration.services, { example: isRuntimeExampleApp(root), entities: hasEntityList(root),
|
|
83
|
+
repository: repositoryName(root, { env: identityQueryEnvironment(process.env, serviceCredentialNames(declaration)) }) });
|
|
81
84
|
if (!config)
|
|
82
85
|
throw refusal(RELEASE_CODE, `${problems.length} problem(s) in release: ${problems.slice(0, 6).join('; ')}`);
|
|
83
|
-
return { root, version: pkg.version, ...config };
|
|
86
|
+
return { root, version: pkg.version, ...config, services: { ...config.services, declaration } };
|
|
84
87
|
}
|
|
85
88
|
function print(ctx, name, flags, result) {
|
|
86
89
|
if (flags.json)
|
|
@@ -119,6 +122,7 @@ const ACTIONS = {
|
|
|
119
122
|
if (flags.reason !== undefined && flags.urgent !== true)
|
|
120
123
|
throw refusal('RELEASE_USAGE', '--reason belongs to an urgent release (--urgent)');
|
|
121
124
|
const config = loadReleaseConfig(ctx.cwd);
|
|
125
|
+
ctx = Object.create(ctx, { env: { value: withoutCredentials(ctx.env, serviceCredentialNames(config.services?.declaration)) } });
|
|
122
126
|
if (flags.urgent === true)
|
|
123
127
|
return releaseUrgentPush(ctx, config, { execute: flags.execute === true, remote: flags.remote, reason: flags.reason, role: harness?.role ?? ctx.env.STARCI_ROLE });
|
|
124
128
|
return releasePush(ctx, config, { execute: flags.execute === true, remote: flags.remote });
|