@starci/hfs 5.0.4 → 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 +10 -0
- package/README.md +22 -2
- package/cache/app-families.d.mts +8 -5
- package/cache/app-families.mjs +11 -9
- package/cache/hfs-family.d.mts +2 -0
- package/cache/hfs-family.mjs +4 -4
- package/cache/run.d.mts +2 -0
- package/cache/run.mjs +2 -5
- package/cache/session.mjs +1 -1
- package/cache/test-family.d.mts +6 -4
- package/cache/test-family.mjs +22 -14
- package/cache/types-family.d.mts +5 -3
- package/cache/types-family.mjs +9 -8
- package/cache/wiring-family.d.mts +4 -2
- package/cache/wiring-family.mjs +5 -5
- 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/emit/contracts.d.mts +12 -9
- package/emit/contracts.mjs +29 -11
- package/lint/chunk.mjs +11 -2
- package/lint/run.d.mts +2 -1
- package/lint/run.mjs +26 -29
- 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/grammars/index.yaml +11 -1
- 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/api/node/run-node.d.mts +29 -0
- package/runtime/scripts/api/node/run-node.mjs +19 -0
- 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/app-wiring.d.mts +8 -2
- package/runtime/scripts/hfs/app-wiring.mjs +27 -7
- 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/contract.d.mts +11 -3
- package/runtime/scripts/hfs/rules/contract.mjs +50 -11
- 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.d.mts +3 -1
- package/src/main.mjs +14 -11
- 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
|
@@ -15,12 +15,20 @@ export declare function contractFindings({ repoRoot, files, repo, resolver }: {
|
|
|
15
15
|
* `openapi.json` and `status` one of `fresh`, `stale`, `not-committed`, `none` (nothing emitted, nothing committed; GraphQL only),
|
|
16
16
|
* `left-behind` (committed, nothing emitted) or `emit-failed`.
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
interface ContractEmitInput {
|
|
19
19
|
repoRoot: string;
|
|
20
20
|
files: string[];
|
|
21
21
|
repo: Repo;
|
|
22
22
|
emit: Parsed;
|
|
23
|
-
}
|
|
23
|
+
}
|
|
24
|
+
/** The synchronous source finder and asynchronous app cache share every emission comparison and failure rule. */
|
|
25
|
+
export declare function contractEmitFindings(input: ContractEmitInput): {
|
|
24
26
|
findings: Finding[];
|
|
25
|
-
apps:
|
|
27
|
+
apps: Parsed[];
|
|
26
28
|
};
|
|
29
|
+
/** Awaits actual governed compiler children before the existing drift proof can be recorded. */
|
|
30
|
+
export declare function contractEmitFindingsAsync(input: ContractEmitInput): Promise<{
|
|
31
|
+
findings: Finding[];
|
|
32
|
+
apps: Parsed[];
|
|
33
|
+
}>;
|
|
34
|
+
export {};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// resolvers the app root composes, and the OpenAPI 3.1 document of the typed operation table `apps/<app>/src/operations.ts`.
|
|
7
7
|
// That the committed snapshot equals what the app emits now is contractEmitFindings (full pass only, never --fast): it emits every
|
|
8
8
|
// api app into a temp directory through the injected `emit` (packages/hfs/emit) and compares hashes; an emit that cannot run is a finding.
|
|
9
|
+
import { isNodeExecutionRefusal } from '../../api/node/run-node.mjs';
|
|
9
10
|
import { createHash } from 'node:crypto';
|
|
10
11
|
import fs from 'node:fs';
|
|
11
12
|
import path from 'node:path';
|
|
@@ -145,16 +146,7 @@ function artifactFindings({ repoRoot, tracked, scratch, app, artifact, what, emi
|
|
|
145
146
|
findings.push(found(CONTRACT_SNAPSHOT_DRIFT, snapshot, `${snapshot} (${committed.slice(0, 12)}) differs from what ${app.name} emits now (${String(fresh).slice(0, 12)}); run \`npm run contract:emit\` and commit the result`, { app: app.name, drift: 'stale' }));
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
|
-
|
|
149
|
-
* R23, full pass: each committed `contracts/<app>/schema.graphql` and `contracts/<app>/openapi.json` equals what `emit` writes for
|
|
150
|
-
* the app now. `emit({ repoRoot, declaration, outDir })` is `emitContracts` of packages/hfs/emit and answers `{ written, skipped }`;
|
|
151
|
-
* it is injected because the runtime scripts do not import the hfs package. Every api app is emitted on its own into a temp
|
|
152
|
-
* directory, so one app that cannot emit does not hide the others; an uncommitted emitted snapshot is a finding here too. Answers
|
|
153
|
-
* `{ findings, apps }`, `apps` being what was judged: `[{ app, artifact, status }]`, `artifact` being `schema.graphql` or
|
|
154
|
-
* `openapi.json` and `status` one of `fresh`, `stale`, `not-committed`, `none` (nothing emitted, nothing committed; GraphQL only),
|
|
155
|
-
* `left-behind` (committed, nothing emitted) or `emit-failed`.
|
|
156
|
-
*/
|
|
157
|
-
export function contractEmitFindings({ repoRoot, files, repo, emit }) {
|
|
149
|
+
function* contractProgram({ repoRoot, files, repo }) {
|
|
158
150
|
const findings = [];
|
|
159
151
|
const apps = [];
|
|
160
152
|
if (repo.profile !== 'be')
|
|
@@ -166,9 +158,14 @@ export function contractEmitFindings({ repoRoot, files, repo, emit }) {
|
|
|
166
158
|
const artifacts = [{ artifact: SNAPSHOT_OF.graphql, what: 'serves no GraphQL any more' }, { artifact: SNAPSHOT_OF.json, what: 'declares no operations any more' }];
|
|
167
159
|
let emitted;
|
|
168
160
|
try {
|
|
169
|
-
|
|
161
|
+
const response = yield { repoRoot, declaration: { apps: [app] }, outDir: scratch };
|
|
162
|
+
if ('error' in response)
|
|
163
|
+
throw response.error;
|
|
164
|
+
emitted = response.value;
|
|
170
165
|
}
|
|
171
166
|
catch (error) {
|
|
167
|
+
if (isNodeExecutionRefusal(error))
|
|
168
|
+
throw error;
|
|
172
169
|
emitFailure(app, artifacts, error, apps, findings);
|
|
173
170
|
continue;
|
|
174
171
|
}
|
|
@@ -183,3 +180,45 @@ export function contractEmitFindings({ repoRoot, files, repo, emit }) {
|
|
|
183
180
|
}
|
|
184
181
|
return { findings, apps };
|
|
185
182
|
}
|
|
183
|
+
/** The synchronous source finder and asynchronous app cache share every emission comparison and failure rule. */
|
|
184
|
+
export function contractEmitFindings(input) {
|
|
185
|
+
const program = contractProgram(input);
|
|
186
|
+
let step = program.next();
|
|
187
|
+
try {
|
|
188
|
+
while (!step.done) {
|
|
189
|
+
let response;
|
|
190
|
+
try {
|
|
191
|
+
response = { value: input.emit(step.value) };
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
response = { error };
|
|
195
|
+
}
|
|
196
|
+
step = program.next(response);
|
|
197
|
+
}
|
|
198
|
+
return step.value;
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
program.return({ findings: [], apps: [] });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** Awaits actual governed compiler children before the existing drift proof can be recorded. */
|
|
205
|
+
export async function contractEmitFindingsAsync(input) {
|
|
206
|
+
const program = contractProgram(input);
|
|
207
|
+
let step = program.next();
|
|
208
|
+
try {
|
|
209
|
+
while (!step.done) {
|
|
210
|
+
let response;
|
|
211
|
+
try {
|
|
212
|
+
response = { value: await input.emit(step.value) };
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
response = { error };
|
|
216
|
+
}
|
|
217
|
+
step = program.next(response);
|
|
218
|
+
}
|
|
219
|
+
return step.value;
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
program.return({ findings: [], apps: [] });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -22,7 +22,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
22
22
|
import { loadTypescript } from '../../lib/package-at.mjs';
|
|
23
23
|
import { parseYaml } from '../../../engine/yaml.mjs';
|
|
24
24
|
import { envelopeOf } from '../secret-document.mjs';
|
|
25
|
-
import { SSH_GROUP, sshTransportMembers } from '../secret-standard.mjs';
|
|
25
|
+
import { SSH_GROUP, sshTransportMembers, declaredServiceKeys } from '../secret-standard.mjs';
|
|
26
|
+
import { repositoryName, identityQueryEnvironment } from '../repo-identity.mjs';
|
|
27
|
+
import { serviceCredentialNames } from '../ci-declaration.mjs';
|
|
26
28
|
import { memberOfName } from '../../lib/sealed-member.mjs';
|
|
27
29
|
import { found, readText } from './read.mjs';
|
|
28
30
|
export const ENV_REQUIRED_UNSET = 'ENV_REQUIRED_UNSET';
|
|
@@ -342,12 +344,12 @@ function addRequiredFindings(reads, sealed, declared, findings) {
|
|
|
342
344
|
}
|
|
343
345
|
}
|
|
344
346
|
}
|
|
345
|
-
function sealedMemberFinding(key, member, reads, declared) {
|
|
347
|
+
function sealedMemberFinding(key, member, reads, declared, serviceConsumer) {
|
|
346
348
|
// The specific public-custody code wins over the generic unread one for a NEXT_PUBLIC_* key.
|
|
347
349
|
if (key.startsWith('NEXT_PUBLIC_')) {
|
|
348
350
|
return found(ENV_PUBLIC_SEALED, member, `${member} seals the public key ${key}; NEXT_PUBLIC_* values are public configuration and never enter custody`);
|
|
349
351
|
}
|
|
350
|
-
if (!reads.has(key) && !declared.has(key)) {
|
|
352
|
+
if (!reads.has(key) && !declared.has(key) && !serviceConsumer(member, key)) {
|
|
351
353
|
return found(ENV_SEALED_UNREAD, member, `${member} seals ${key}, which no EnvSource reader and no declared consumer uses; unseal it or declare who consumes it`);
|
|
352
354
|
}
|
|
353
355
|
return null;
|
|
@@ -362,10 +364,10 @@ function transportMembers(repoRoot) {
|
|
|
362
364
|
catch { /* a malformed stacks yaml is the shape rule's finding, not this one's */ }
|
|
363
365
|
return (environment) => sshTransportMembers(stacks.environments?.[environment]);
|
|
364
366
|
}
|
|
365
|
-
function addSealedFindings(sealed, reads, declared, transport, findings) {
|
|
367
|
+
function addSealedFindings(sealed, reads, declared, transport, findings, serviceConsumer) {
|
|
366
368
|
for (const [key, members] of sealed) {
|
|
367
369
|
for (const member of members) {
|
|
368
|
-
const finding = sealedMemberFinding(key, member, reads, declared);
|
|
370
|
+
const finding = sealedMemberFinding(key, member, reads, declared, serviceConsumer);
|
|
369
371
|
if (finding !== null)
|
|
370
372
|
findings.push(finding);
|
|
371
373
|
}
|
|
@@ -410,8 +412,15 @@ export function envParityFindings({ repoRoot, all = [], isRoot }) {
|
|
|
410
412
|
const sealed = sealedKeys(repoRoot, all);
|
|
411
413
|
const declared = declaredKeys(repoRoot, all);
|
|
412
414
|
const findings = [];
|
|
415
|
+
let stacks = {};
|
|
416
|
+
try {
|
|
417
|
+
stacks = parseYaml(readText(repoRoot, '.starcistacks/application-stacks.yaml') ?? '') ?? {};
|
|
418
|
+
}
|
|
419
|
+
catch { /* the shape owner reports invalid YAML */ }
|
|
420
|
+
const repository = repositoryName(repoRoot, { env: identityQueryEnvironment(process.env, serviceCredentialNames(stacks)) });
|
|
421
|
+
const serviceConsumer = (file, key) => declaredServiceKeys(stacks, repository, file.split('/')[1], memberOfName(file.split('/').pop())).includes(key);
|
|
413
422
|
addRequiredFindings(reads, sealed, declared, findings);
|
|
414
|
-
addSealedFindings(sealed, reads, declared, transportMembers(repoRoot), findings);
|
|
423
|
+
addSealedFindings(sealed, reads, declared, transportMembers(repoRoot), findings, serviceConsumer);
|
|
415
424
|
addExampleFindings(repoRoot, all, reads, findings);
|
|
416
425
|
return findings;
|
|
417
426
|
}
|
|
@@ -46,6 +46,7 @@ export interface MoveOptions {
|
|
|
46
46
|
}
|
|
47
47
|
/** The provider a key belongs to: its first word (`SEPAY_IPN_SECRET_KEY` is `sepay`, `OPENROUTER_API_KEY` is `openrouter`). */
|
|
48
48
|
export declare const providerOf: (key: string) => string;
|
|
49
|
+
export declare function declaredServiceKeys(document: StacksDocument, repository: string, environment: string, member: string): string[];
|
|
49
50
|
/** The keys of the one sealed SSH member `deploy.remote.sealed` names (stack/remote.mjs): the deploy transport reads exactly these. */
|
|
50
51
|
export declare const SSH_GROUP: readonly string[];
|
|
51
52
|
/**
|
|
@@ -13,12 +13,26 @@ import { configuredRecipients } from "./secret-init.mjs";
|
|
|
13
13
|
import { envelopeOf, validateMap } from "./secret-document.mjs";
|
|
14
14
|
import { deleteSealed, fileOf, sealedFiles, writeSecret } from "./secret-store.mjs";
|
|
15
15
|
import { SecretError } from "./secret-errors.mjs";
|
|
16
|
+
import { serviceMemberPath, serviceCredentialNames } from "./ci-declaration.mjs";
|
|
17
|
+
import { repositoryName, identityQueryEnvironment } from "./repo-identity.mjs";
|
|
18
|
+
import { list } from "../lib/list.mjs";
|
|
16
19
|
/** The provider a key belongs to: its first word (`SEPAY_IPN_SECRET_KEY` is `sepay`, `OPENROUTER_API_KEY` is `openrouter`). */
|
|
17
20
|
export const providerOf = (key) => key.split("_")[0].toLowerCase();
|
|
18
21
|
const sameSet = (left, right) => left.length === right.length && left.every((item) => right.includes(item));
|
|
19
22
|
const keyList = (keys) => keys.join(", ");
|
|
20
23
|
/** The member a `secrets:` entry names: `member`, else the slug of `where`, else its `name`. */
|
|
21
24
|
const memberOfEntry = (entry) => entry?.member ?? /^secrets\/([a-z0-9-]+)/u.exec(entry?.where ?? "")?.[1] ?? entry?.name;
|
|
25
|
+
/** The declared connection/credential keys of this project's services, including separately kept operator login custody. */
|
|
26
|
+
function declaredServiceReferences(document, repository) {
|
|
27
|
+
const services = document.services;
|
|
28
|
+
return Object.values(services ?? {}).flatMap((service) => list(service?.credentials).flatMap((credential) => {
|
|
29
|
+
const selected = serviceMemberPath(credential?.custody?.path);
|
|
30
|
+
return credential?.custody?.repository === repository && selected && typeof credential.key === "string" ? [{ ...selected, key: credential.key }] : [];
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
export function declaredServiceKeys(document, repository, environment, member) {
|
|
34
|
+
return declaredServiceReferences(document, repository).filter((reference) => reference.environment === environment && reference.member === member).map((reference) => reference.key);
|
|
35
|
+
}
|
|
22
36
|
/** The keys an environment's `secrets:` declares for `member`: an entry naming `key` with that `member`, or a whole-member entry. */
|
|
23
37
|
function declaredKeys(spec, member, keys) {
|
|
24
38
|
const entries = Array.isArray(spec?.secrets) ? spec.secrets : [];
|
|
@@ -80,6 +94,11 @@ export function auditSecrets(repoRoot, environment = null) {
|
|
|
80
94
|
.map((entry) => entry.name)
|
|
81
95
|
.sort();
|
|
82
96
|
const findings = [];
|
|
97
|
+
const repository = repositoryName(repoRoot, { env: identityQueryEnvironment(process.env, serviceCredentialNames(stacks)) });
|
|
98
|
+
for (const reference of declaredServiceReferences(stacks, repository).filter((item) => !environment || item.environment === environment)) {
|
|
99
|
+
if (!Object.hasOwn(stacks.environments ?? {}, reference.environment) || !fs.existsSync(path.join(repoRoot, reference.path)))
|
|
100
|
+
findings.push({ code: "MISSING", env: reference.environment, member: reference.member, detail: "service credential references a missing member or undeclared environment" });
|
|
101
|
+
}
|
|
83
102
|
for (const env of environments) {
|
|
84
103
|
const directory = path.join(root, env, "secrets");
|
|
85
104
|
const spec = stacks.environments?.[env];
|
|
@@ -92,16 +111,17 @@ export function auditSecrets(repoRoot, environment = null) {
|
|
|
92
111
|
const providers = [...new Set(envelope.keys.map(providerOf))];
|
|
93
112
|
if (isLegacyName(name))
|
|
94
113
|
add("LEGACY_MEMBER", `is a dotenv .enc member; move it: starci app secret migrate --env ${env} ${member} [--to <provider>] --execute`);
|
|
95
|
-
|
|
114
|
+
const serviceKeys = declaredServiceKeys(stacks, repository, env, member);
|
|
115
|
+
if (providers.length > 1 && !declaredGroup(spec, member, envelope.keys) && !envelope.keys.every((key) => serviceKeys.includes(key)))
|
|
96
116
|
add("MIXED_PROVIDERS", `holds keys of ${providers.length} providers by their first word (${providers.join(", ")}) that the stack does not group in it; split it (starci app secret split --env ${env} ${member} [--group KEY=member] --execute) or declare its keys in environments.${env}.secrets (an entry naming ${member} with keys: [...])`);
|
|
97
117
|
const wanted = recipientsFor(repoRoot, path.join(directory, `${member}${MEMBER_SUFFIX}`));
|
|
98
118
|
if (!isLegacyName(name) && wanted.length && !sameSet(wanted, envelope.recipients))
|
|
99
119
|
add("RECIPIENT_DRIFT", `is sealed to ${keyList(envelope.recipients)}, its environment's rule names ${keyList(wanted)}; seal it again: starci app secret migrate --env ${env} ${member} --execute`);
|
|
100
120
|
if (spec) {
|
|
101
|
-
const declared = declaredKeys(spec, member, envelope.keys);
|
|
121
|
+
const declared = [...declaredKeys(spec, member, envelope.keys), ...serviceKeys];
|
|
102
122
|
const missing = envelope.keys.filter((key) => !declared.includes(key));
|
|
103
123
|
if (missing.length)
|
|
104
|
-
add("UNDECLARED_KEY", `holds ${keyList(missing)}, not declared
|
|
124
|
+
add("UNDECLARED_KEY", `holds ${keyList(missing)}, not declared as runtime secrets or exact service credential references in application-stacks.yaml`);
|
|
105
125
|
}
|
|
106
126
|
if (tracked && !tracked.has(name))
|
|
107
127
|
add("UNTRACKED", "is not committed; commit it (it is ciphertext)");
|
|
@@ -155,7 +175,8 @@ export function declareKeys(repoRoot, environment, member, keys) {
|
|
|
155
175
|
const spec = document.environments?.[environment];
|
|
156
176
|
if (!spec)
|
|
157
177
|
return { declared: [], byHand: [] };
|
|
158
|
-
const
|
|
178
|
+
const serviceKeys = declaredServiceKeys(document, repositoryName(repoRoot, { env: identityQueryEnvironment(process.env, serviceCredentialNames(document)) }), environment, member);
|
|
179
|
+
const missing = keys.filter((key) => !declaredKeys(spec, member, keys).includes(key) && !serviceKeys.includes(key));
|
|
159
180
|
if (!missing.length)
|
|
160
181
|
return { declared: [], byHand: [] };
|
|
161
182
|
// A swarm environment's secret is a versioned external secret its stack file names (runtimeName, deploy grants): its
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { type SecretSops } from "./secret-sops.mjs";
|
|
2
|
+
import type { ServiceBinding } from "./ci-declaration.mjs";
|
|
2
3
|
/** The directory of the sealed secrets of the app at `repoRoot` for `env`; the one env that has secrets when none is named. */
|
|
3
4
|
export declare function secretsDirectory(repoRoot: string, env?: string | null): string;
|
|
4
5
|
export declare const fileOf: (directory: string, slug: string | undefined) => string;
|
|
6
|
+
/** Judge a selected service member and cleartext key names without decrypting; its real path stays in this product. */
|
|
7
|
+
export declare function serviceCredentialFile(repoRoot: string, binding: ServiceBinding): string;
|
|
8
|
+
/** Native service loader: one exact key, decrypted in memory through the common identity/SOPS owner, never an env fallback. */
|
|
9
|
+
export declare function serviceCredentialValue(repoRoot: string, binding: ServiceBinding, sops: Pick<SecretSops, "decrypt">): string;
|
|
5
10
|
/** Replace one existing sealed member with an already validated envelope: same claim and atomic publish as set. */
|
|
6
11
|
export declare const replaceSealed: (file: string, sealed: string) => void;
|
|
7
12
|
/** Delete one sealed member under the writer claim. */
|
|
@@ -41,6 +41,37 @@ export const fileOf = (directory, slug) => {
|
|
|
41
41
|
throw new SecretError(error.message);
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
|
+
/** Judge a selected service member and cleartext key names without decrypting; its real path stays in this product. */
|
|
45
|
+
export function serviceCredentialFile(repoRoot, binding) {
|
|
46
|
+
const file = fileOf(secretsDirectory(repoRoot, binding.environment), binding.member);
|
|
47
|
+
const declared = path.resolve(repoRoot, binding.path);
|
|
48
|
+
try {
|
|
49
|
+
if (file !== declared || !fs.statSync(file).isFile() || fs.realpathSync(file) !== path.join(fs.realpathSync(repoRoot), binding.path))
|
|
50
|
+
throw new SecretError("service custody must name one regular sealed member inside this product");
|
|
51
|
+
if (!envelopeOf(fs.readFileSync(file, "utf8")).keys.includes(binding.key))
|
|
52
|
+
throw new SecretError("service custody does not hold the declared key");
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new SecretError("service custody is missing, ambiguous or outside this product");
|
|
56
|
+
}
|
|
57
|
+
return file;
|
|
58
|
+
}
|
|
59
|
+
/** Native service loader: one exact key, decrypted in memory through the common identity/SOPS owner, never an env fallback. */
|
|
60
|
+
export function serviceCredentialValue(repoRoot, binding, sops) {
|
|
61
|
+
const file = serviceCredentialFile(repoRoot, binding);
|
|
62
|
+
try {
|
|
63
|
+
const envelope = envelopeOf(fs.readFileSync(file, "utf8"));
|
|
64
|
+
if (!envelope.keys.includes(binding.key))
|
|
65
|
+
throw new SecretError("service credential key is absent from the selected member");
|
|
66
|
+
const value = sops.decrypt(file, envelope.format)[binding.key];
|
|
67
|
+
if (typeof value !== "string" || !value.length)
|
|
68
|
+
throw new SecretError("service credential key is empty or absent");
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new SecretError("service credential custody could not be opened; check its member, key and owner identity");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
44
75
|
/**
|
|
45
76
|
* Build and validate a replacement envelope without publishing it. An existing member is decrypted only when it holds a key
|
|
46
77
|
* `values` does not replace; a member whose every key is replaced (the one-key member a `set` re-seals) needs no age identity.
|
package/src/main.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HeavyNodeRun } from '../runtime/scripts/api/node/run-node.mjs';
|
|
1
2
|
import { type LockResult } from '../scaffold/app.mjs';
|
|
2
3
|
import type { GhSpawn } from '../ci/github.mjs';
|
|
3
4
|
import type { Prompt } from '../runtime/scripts/hfs/secret-prompt.mjs';
|
|
@@ -39,6 +40,7 @@ interface AppIo {
|
|
|
39
40
|
stackIo?: Record<string, unknown> | undefined;
|
|
40
41
|
runLinter?: LinterRunner | undefined;
|
|
41
42
|
checkApp?: ((args: Dynamic) => Dynamic) | undefined;
|
|
43
|
+
runHeavyNode?: HeavyNodeRun | undefined;
|
|
42
44
|
/** --quiet: no progress on stderr (the diagnosis stays). */
|
|
43
45
|
quiet?: boolean | undefined;
|
|
44
46
|
stderrIsTty?: boolean | undefined;
|
|
@@ -57,5 +59,5 @@ export declare function appRootAbove(dir: string): string;
|
|
|
57
59
|
* the HFS check and `stackIo` Swarm tools; production defaults execute every check. `hostIdentity` ({ label, read }) is the
|
|
58
60
|
* harness CLI's own secret.env SOPS_AGE_KEY, which every verb that opens a sealed member (secret, stack, release, db) reads last for the age identity (decryptionIdentity); `prompt` replaces its masked prompt.
|
|
59
61
|
*/
|
|
60
|
-
export declare function main(argv: string[], { cwd: baseCwd, harness, stdout, stderr, presets, prettier, lock, run, stackMain, env, stdin, prompt, hostIdentity, sops, tools, platform, stackIo, runLinter, checkApp, stderrIsTty, }?: MainOptions): Promise<number>;
|
|
62
|
+
export declare function main(argv: string[], { cwd: baseCwd, harness, stdout, stderr, presets, prettier, lock, run, stackMain, env, stdin, prompt, hostIdentity, sops, tools, platform, stackIo, runLinter, runHeavyNode, checkApp, stderrIsTty, }?: MainOptions): Promise<number>;
|
|
61
63
|
export {};
|
package/src/main.mjs
CHANGED
|
@@ -64,7 +64,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
64
64
|
// starci app secret materialize --env <env> | list --env <env> | verify --env <env> | recipients --env <env> [--add|--remove <age1...|owner>] [--execute]
|
|
65
65
|
// dotenv custody in .starcistacks, public age rule initialization, private KEY files outside Git;
|
|
66
66
|
// values reach stdout only when get explicitly receives --reveal.
|
|
67
|
-
// starci app ci secret set <NAME> [--env E | --dependabot] [--repo owner/name] [--execute]
|
|
67
|
+
// starci app ci secret set <NAME> [--from-custody] [--env E | --dependabot] [--repo owner/name] [--execute] stdin or the explicit service custody binding; dry until --execute.
|
|
68
68
|
// starci app ci var set <NAME> [--env E] [--value v] [--execute] | var sync [--execute] | verify [--json] | local [--execute]
|
|
69
69
|
// declared GitHub CI custody; verify compares names, scopes and public values; local prints the CI plan, --execute runs it.
|
|
70
70
|
// starci app db new <name> | baseline | restamp --map <restamp.yaml> | migrate | post-cutover | dump | drift [--db-url-env <NAME>]
|
|
@@ -83,6 +83,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
83
83
|
// Every finding names a why code and carries its Vietnamese text. check, lint and slot read the app, never write to it. Exit codes:
|
|
84
84
|
// 0 clean, 1 error findings, 2 a refusal or bad usage.
|
|
85
85
|
import fs from 'node:fs';
|
|
86
|
+
import { isHostHeld } from '../runtime/scripts/api/node/run-node.mjs';
|
|
86
87
|
import { createRequire } from 'node:module';
|
|
87
88
|
import path from 'node:path';
|
|
88
89
|
import { pathToFileURL } from 'node:url';
|
|
@@ -99,13 +100,13 @@ import { HFS_DECLARATION_FILE, HfsSlotsError, loadSlotManifest, readRepoDeclarat
|
|
|
99
100
|
import { main as syncMain } from '../sync/cli.mjs';
|
|
100
101
|
import { runWorkHygiene } from '../sync/hygiene.mjs';
|
|
101
102
|
import { SyncError, bundledPresets } from '../sync/index.mjs';
|
|
102
|
-
import {
|
|
103
|
+
import { emitContractsAsync } from '../emit/contracts.mjs';
|
|
103
104
|
import { ScaffoldError, newService, newSpec } from '../scaffold/service.mjs';
|
|
104
105
|
import { EditionRefusal } from '../scaffold/edition-gate.mjs';
|
|
105
106
|
import { addKind } from '../scaffold/add.mjs';
|
|
106
107
|
import { scaffoldApp } from '../scaffold/app.mjs';
|
|
107
108
|
import { newImages } from '../scaffold/image.mjs';
|
|
108
|
-
import { cachedLinters, eslintRepository, lintRepository, parseLintArgs, printLintText } from '../lint/run.mjs';
|
|
109
|
+
import { cachedLinters, eslintRepository, lintRepository, parseLintArgs, printLintText, runLinter as nativeLinter } from '../lint/run.mjs';
|
|
109
110
|
import { runSonarForApp, runTestForApp, runTestShard, runTypesForApp } from '../cache/app-families.mjs';
|
|
110
111
|
import { CACHE_ENV, createDiskSession } from '../cache/session.mjs';
|
|
111
112
|
import { DEFAULT_FAMILIES, runAppFamilies } from '../cache/run.mjs';
|
|
@@ -359,7 +360,7 @@ function openSession({ repoRoot, tracked, io, full, use, planned }) {
|
|
|
359
360
|
async function appPass({ repoRoot, harness, only, presets, prettier, io, selected, full, use = 'normal', session: given }) {
|
|
360
361
|
const tracked = trackedFiles(repoRoot);
|
|
361
362
|
const session = given ?? openSession({ repoRoot, tracked, io, full, use: only === undefined ? use : 'bypass', planned: tracked.length * Math.max(1, selected.size) });
|
|
362
|
-
const result = await runAppFamilies({ repoRoot, tracked, session, selected, harness, presets, prettier, only, edition: undefined });
|
|
363
|
+
const result = await runAppFamilies({ repoRoot, tracked, session, selected, harness, presets, prettier, only, edition: undefined, execute: io.runHeavyNode });
|
|
363
364
|
if (given !== undefined)
|
|
364
365
|
return result;
|
|
365
366
|
const flushed = session.flush();
|
|
@@ -529,9 +530,9 @@ async function checkRun({ io, selected, full, use, coverageFrom }) {
|
|
|
529
530
|
eslint = await eslintRepository({ repoRoot, changed: null, existing: [], run: runLinter });
|
|
530
531
|
}
|
|
531
532
|
if (session !== undefined && selected.has('types'))
|
|
532
|
-
extra.push(...runTypesForApp(session, { repoRoot, tracked, env: io.env }).findings);
|
|
533
|
+
extra.push(...(await runTypesForApp(session, { repoRoot, tracked, env: io.env, execute: io.runHeavyNode })).findings);
|
|
533
534
|
if (session !== undefined && selected.has('test')) {
|
|
534
|
-
const tests = await runTestForApp(session, { repoRoot, tracked });
|
|
535
|
+
const tests = await runTestForApp(session, { repoRoot, tracked, execute: io.runHeavyNode });
|
|
535
536
|
extra.push(...tests.findings);
|
|
536
537
|
}
|
|
537
538
|
if (session !== undefined && selected.has('sonar'))
|
|
@@ -589,7 +590,7 @@ async function checkMain(opts, io) {
|
|
|
589
590
|
const total = Number(match?.[2]);
|
|
590
591
|
if (match === null || !Number.isSafeInteger(index) || !Number.isSafeInteger(total) || index < 1 || total < index)
|
|
591
592
|
throw new Error('--shard needs <i>/<n> with 1 <= i <= n');
|
|
592
|
-
return runTestShard(repoRoot, index, total);
|
|
593
|
+
return runTestShard(repoRoot, index, total, io.runHeavyNode);
|
|
593
594
|
}
|
|
594
595
|
// A coverage report judged on its own (the merge job of a sharded run) runs no family; with selectors it comes on top of them.
|
|
595
596
|
const selected = explicit.size > 0 ? explicit : coverageFrom === undefined ? new Set(DEFAULT_FAMILIES) : new Set();
|
|
@@ -609,13 +610,13 @@ async function checkMain(opts, io) {
|
|
|
609
610
|
writeRunLog(repoRoot, { command: `check${[...new Set(listFlag(opts, 'gate') ?? [])].map((gate) => ` --gate ${gate}`).join('')}${opts['no-cache'] === true ? ' --no-cache' : ''}${coverageFrom === undefined ? '' : ` --coverage-from ${JSON.stringify(coverageFrom)}`}`, seconds, exit, families: result.families.map((report) => ({ ...report, seconds: 0 })) }, io.env);
|
|
610
611
|
return exit;
|
|
611
612
|
}
|
|
612
|
-
function emitMain(opts, { cwd: repoRoot, stdout: out }) {
|
|
613
|
+
async function emitMain(opts, { cwd: repoRoot, stdout: out, runHeavyNode }) {
|
|
613
614
|
if (opts.positional.length)
|
|
614
615
|
throw new Error('starci app emit takes no path');
|
|
615
616
|
const be = readRepoDeclaration(loadSlotManifest(), repoRoot).sides?.be;
|
|
616
617
|
if (!be)
|
|
617
618
|
throw new Error('starci app emit runs at the app root (the folder of hfs.json)');
|
|
618
|
-
const { written, skipped, standIns } =
|
|
619
|
+
const { written, skipped, standIns } = await emitContractsAsync({ repoRoot: path.join(repoRoot, 'be'), declaration: be, execute: runHeavyNode });
|
|
619
620
|
for (const file of written)
|
|
620
621
|
out(`wrote be/${file}\n`);
|
|
621
622
|
const skipSummary = skipped.length ? `, ${skipped.join(', ')} serve no GraphQL` : '';
|
|
@@ -737,7 +738,7 @@ function errorMessage(error) {
|
|
|
737
738
|
* the HFS check and `stackIo` Swarm tools; production defaults execute every check. `hostIdentity` ({ label, read }) is the
|
|
738
739
|
* harness CLI's own secret.env SOPS_AGE_KEY, which every verb that opens a sealed member (secret, stack, release, db) reads last for the age identity (decryptionIdentity); `prompt` replaces its masked prompt.
|
|
739
740
|
*/
|
|
740
|
-
export async function main(argv, { cwd: baseCwd = process.cwd(), harness, stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), presets, prettier, lock, run, stackMain, env = process.env, stdin, prompt, hostIdentity, sops, tools, platform, stackIo, runLinter, checkApp, stderrIsTty = process.stderr.isTTY === true, } = {}) {
|
|
741
|
+
export async function main(argv, { cwd: baseCwd = process.cwd(), harness, stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), presets, prettier, lock, run, stackMain, env = process.env, stdin, prompt, hostIdentity, sops, tools, platform, stackIo, runLinter, runHeavyNode, checkApp, stderrIsTty = process.stderr.isTTY === true, } = {}) {
|
|
741
742
|
try {
|
|
742
743
|
const global = globalOptions(argv, path.resolve(baseCwd));
|
|
743
744
|
const [verb, ...given] = global.argv;
|
|
@@ -752,10 +753,12 @@ export async function main(argv, { cwd: baseCwd = process.cwd(), harness, stdout
|
|
|
752
753
|
return 2;
|
|
753
754
|
}
|
|
754
755
|
const { edition, argv: rest } = editionOption(verb, given);
|
|
755
|
-
return await dispatchMain(verb, rest, { cwd: global.cwd, harness, edition, quiet: global.quiet, stderrIsTty, stdout: out, stderr, presets, prettier, lock, run, stackMain, env, stdin, prompt, hostIdentity, sops, tools, platform, stackIo, runLinter, checkApp });
|
|
756
|
+
return await dispatchMain(verb, rest, { cwd: global.cwd, harness, edition, quiet: global.quiet, stderrIsTty, stdout: out, stderr, presets, prettier, lock, run, stackMain, env, stdin, prompt, hostIdentity, sops, tools, platform, stackIo, runHeavyNode, runLinter: runLinter ?? ((spec) => nativeLinter(spec, runHeavyNode)), checkApp });
|
|
756
757
|
}
|
|
757
758
|
catch (error) {
|
|
758
759
|
stderr(errorMessage(error));
|
|
760
|
+
if (isHostHeld(error))
|
|
761
|
+
return 75;
|
|
759
762
|
return 2;
|
|
760
763
|
}
|
|
761
764
|
}
|
package/sync/ci-commands.mjs
CHANGED
|
@@ -35,7 +35,7 @@ function sonarJob(sonar, lcovReport, lcovArtifact, nodeMajor, timeout) {
|
|
|
35
35
|
if (!sonar)
|
|
36
36
|
return '';
|
|
37
37
|
const condition = SONAR_CONDITION;
|
|
38
|
-
const host = sonar.provider === 'sonarcloud' ? 'https://sonarcloud.io' : `\${{ vars.${sonar.hostVar} }}`;
|
|
38
|
+
const host = sonar.hostSecret ? `\${{ secrets.${sonar.hostSecret} }}` : sonar.provider === 'sonarcloud' ? 'https://sonarcloud.io' : `\${{ vars.${sonar.hostVar} }}`;
|
|
39
39
|
const header = [
|
|
40
40
|
' sonar:',
|
|
41
41
|
' needs: [plan, lint, coverage]',
|
|
@@ -52,6 +52,7 @@ function sonarJob(sonar, lcovReport, lcovArtifact, nodeMajor, timeout) {
|
|
|
52
52
|
` if: ${condition}`,
|
|
53
53
|
' run: |',
|
|
54
54
|
` if [ -z "$SONAR_TOKEN" ]; then echo "::error::SONAR_TOKEN is not set: Sonar is required, so a missing token fails this job (secret ${sonar.token})"; exit 1; fi`,
|
|
55
|
+
' if [ -z "$SONAR_HOST_URL" ]; then echo "::error::SONAR_HOST_URL is not set: the declared analysis endpoint is required"; exit 1; fi',
|
|
55
56
|
` - uses: ${DOWNLOAD_ARTIFACT}`,
|
|
56
57
|
` if: ${condition}`,
|
|
57
58
|
' with:',
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseYaml as bundledParseYaml } from '../runtime/engine/yaml.mjs';
|
|
4
|
-
import { qualityServices } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
4
|
+
import { qualityServices, serviceCredentialNames } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
5
|
+
import { repositoryName, identityQueryEnvironment } from '../runtime/scripts/hfs/repo-identity.mjs';
|
|
5
6
|
import { isRuntimeExampleApp } from '../runtime/scripts/lib/runtime-example.mjs';
|
|
6
7
|
import { badgeTokenNotices, renderReadmeBadges } from './readme-badges.mjs';
|
|
7
8
|
import { DECLARATION } from './sonar-key.mjs';
|
|
@@ -19,7 +20,7 @@ export function hasDeclaredServices(root, { parseYaml = bundledParseYaml } = {})
|
|
|
19
20
|
export function readDeclaredQuality(root, { parseYaml = bundledParseYaml } = {}) {
|
|
20
21
|
try {
|
|
21
22
|
const doc = parseYaml(fs.readFileSync(path.join(root, DECLARATION), 'utf8'));
|
|
22
|
-
return qualityServices(doc, { example: isRuntimeExampleApp(root) });
|
|
23
|
+
return qualityServices(doc, { example: isRuntimeExampleApp(root), repository: repositoryName(root, { env: identityQueryEnvironment(process.env, serviceCredentialNames(doc)) }) });
|
|
23
24
|
}
|
|
24
25
|
catch {
|
|
25
26
|
return qualityServices({});
|
package/sync/readme-badges.mjs
CHANGED
|
@@ -17,7 +17,7 @@ export function renderReadmeBadges(doc, { repository, example = false } = {}) {
|
|
|
17
17
|
const branch = encodeURIComponent(github.defaultBranch ?? 'main');
|
|
18
18
|
const workflow = encodeURIComponent(github.workflow ?? 'ci.yml');
|
|
19
19
|
const base = `https://github.com/${repo}`;
|
|
20
|
-
const quality = qualityServices(doc, { example });
|
|
20
|
+
const quality = qualityServices(doc, { example, repository: repo.split('/')[1] });
|
|
21
21
|
const lines = [badge('CI', `${base}/actions/workflows/${workflow}/badge.svg?branch=${branch}`, `${base}/actions/workflows/${workflow}`)];
|
|
22
22
|
if (quality.codecov.enabled) {
|
|
23
23
|
const params = [];
|
package/sync/sonar-key.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { repositoryName } from '../runtime/scripts/hfs/repo-identity.mjs';
|
|
2
|
+
export { repositoryName };
|
|
1
3
|
type Declaration = any;
|
|
2
4
|
type ParseYaml = (text: string) => Declaration;
|
|
3
5
|
/** The stack declaration, app-relative: .starcistacks sits at the app root, beside be/, fe/ and .starciwork. */
|
|
4
6
|
export declare const DECLARATION: string;
|
|
5
|
-
/** The repository name the declaration lists its projects under: the app package.json name, else the folder name. */
|
|
6
|
-
export declare function repositoryName(root: string): string;
|
|
7
7
|
/** The keys services.sonar declares for `repository` in a parsed declaration (empty when it declares none). */
|
|
8
8
|
export declare function declaredSonarKeys(declaration: Declaration, repository: string): string[];
|
|
9
9
|
/**
|
|
@@ -28,4 +28,3 @@ export declare function readDeclaredSonarGate(root: string, { parseYaml }?: {
|
|
|
28
28
|
} | null;
|
|
29
29
|
/** The quality-gate name for an app edition from the one sonar-gate document. */
|
|
30
30
|
export declare function sonarGateName(document: Declaration, edition?: string): string | null;
|
|
31
|
-
export {};
|
package/sync/sonar-key.mjs
CHANGED
|
@@ -4,20 +4,11 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { parseYaml as bundledParseYaml } from '../runtime/engine/yaml.mjs';
|
|
7
|
+
import { repositoryName, identityQueryEnvironment } from '../runtime/scripts/hfs/repo-identity.mjs';
|
|
8
|
+
import { serviceCredentialNames } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
9
|
+
export { repositoryName };
|
|
7
10
|
/** The stack declaration, app-relative: .starcistacks sits at the app root, beside be/, fe/ and .starciwork. */
|
|
8
11
|
export const DECLARATION = path.join('.starcistacks', 'application-stacks.yaml');
|
|
9
|
-
/** The repository name the declaration lists its projects under: the app package.json name, else the folder name. */
|
|
10
|
-
export function repositoryName(root) {
|
|
11
|
-
try {
|
|
12
|
-
const name = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).name;
|
|
13
|
-
if (typeof name === 'string' && name)
|
|
14
|
-
return name;
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
// no readable package.json: the folder name stands in
|
|
18
|
-
}
|
|
19
|
-
return path.basename(path.resolve(root));
|
|
20
|
-
}
|
|
21
12
|
/** The keys services.sonar declares for `repository` in a parsed declaration (empty when it declares none). */
|
|
22
13
|
export function declaredSonarKeys(declaration, repository) {
|
|
23
14
|
const projects = declaration?.services?.sonar?.projects;
|
|
@@ -35,9 +26,11 @@ export async function readDeclaredSonarKey(root, { parseYaml, fail }) {
|
|
|
35
26
|
if (!fs.existsSync(file))
|
|
36
27
|
return null;
|
|
37
28
|
const parse = parseYaml ?? bundledParseYaml;
|
|
38
|
-
const
|
|
29
|
+
const doc = parse(fs.readFileSync(file, 'utf8'));
|
|
30
|
+
const repository = repositoryName(root, { env: identityQueryEnvironment(process.env, serviceCredentialNames(doc)) });
|
|
31
|
+
const keys = declaredSonarKeys(doc, repository);
|
|
39
32
|
if (keys.length > 1)
|
|
40
|
-
fail(`${DECLARATION.split(path.sep).join('/')} declares ${keys.length} Sonar projects for ${
|
|
33
|
+
fail(`${DECLARATION.split(path.sep).join('/')} declares ${keys.length} Sonar projects for ${repository} (${keys.join(', ')}); one app has one key`);
|
|
41
34
|
return keys[0] ?? null;
|
|
42
35
|
}
|
|
43
36
|
/**
|
|
@@ -181,11 +181,15 @@ services:
|
|
|
181
181
|
environment: dev
|
|
182
182
|
compose: compose.yaml
|
|
183
183
|
auth: token
|
|
184
|
-
credentials:
|
|
184
|
+
credentials:
|
|
185
|
+
- id: analysis
|
|
186
|
+
project: "{{project}}"
|
|
187
|
+
key: SONAR_TOKEN
|
|
188
|
+
custody: {repository: "{{project}}", path: .starcistacks/dev/secrets/sonar.enc.yaml}
|
|
185
189
|
projects: [{repository: "{{project}}", key: "{{project}}"}]
|
|
186
|
-
ci: {wiring: required, workflow: .github/workflows/ci.yml, token: SONAR_TOKEN, hostVar: SONAR_HOST_URL}
|
|
190
|
+
ci: {wiring: required, workflow: .github/workflows/ci.yml, token: SONAR_TOKEN, hostVar: SONAR_HOST_URL, secrets: [{name: SONAR_TOKEN, credential: analysis}]}
|
|
187
191
|
ownerAction:
|
|
188
|
-
needed:
|
|
192
|
+
needed: Declare this app's connection profile and project credential with its actual sealed member/key and project reference, seal it through starci app secret, bind ci.secrets to that credential, then set the CI execution copy with starci app ci secret set SONAR_TOKEN --from-custody --execute and sync SONAR_HOST_URL.
|
|
189
193
|
reason: A fresh app declares its quality service without fabricating credentials or a successful analysis.
|
|
190
194
|
codecov:
|
|
191
195
|
provider: codecov
|
|
@@ -270,11 +270,15 @@ services:
|
|
|
270
270
|
environment: dev
|
|
271
271
|
compose: compose.yaml
|
|
272
272
|
auth: token
|
|
273
|
-
credentials:
|
|
273
|
+
credentials:
|
|
274
|
+
- id: analysis
|
|
275
|
+
project: "{{project}}"
|
|
276
|
+
key: SONAR_TOKEN
|
|
277
|
+
custody: {repository: "{{project}}", path: .starcistacks/dev/secrets/sonar.enc.yaml}
|
|
274
278
|
projects: [{repository: "{{project}}", key: "{{project}}"}]
|
|
275
|
-
ci: {wiring: required, workflow: .github/workflows/ci.yml, token: SONAR_TOKEN, hostVar: SONAR_HOST_URL}
|
|
279
|
+
ci: {wiring: required, workflow: .github/workflows/ci.yml, token: SONAR_TOKEN, hostVar: SONAR_HOST_URL, secrets: [{name: SONAR_TOKEN, credential: analysis}]}
|
|
276
280
|
ownerAction:
|
|
277
|
-
needed:
|
|
281
|
+
needed: Declare this app's connection profile and project credential with its actual sealed member/key and project reference, seal it through starci app secret, bind ci.secrets to that credential, then set the CI execution copy with starci app ci secret set SONAR_TOKEN --from-custody --execute and sync SONAR_HOST_URL.
|
|
278
282
|
reason: A fresh app declares its quality service without fabricating credentials or a successful analysis.
|
|
279
283
|
codecov:
|
|
280
284
|
provider: codecov
|