@starci/hfs 4.12.2 → 4.13.2
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 +50 -0
- package/README.md +8 -9
- package/cache/app-families.d.mts +30 -0
- package/cache/app-families.mjs +94 -0
- package/cache/content-index.d.mts +13 -0
- package/cache/content-index.mjs +37 -0
- package/cache/db-family.d.mts +13 -0
- package/cache/db-family.mjs +37 -0
- package/cache/fake-session.d.mts +42 -0
- package/cache/fake-session.mjs +57 -0
- package/cache/fingerprint.d.mts +28 -0
- package/cache/fingerprint.mjs +120 -0
- package/cache/format-family.d.mts +13 -0
- package/cache/format-family.mjs +49 -0
- package/cache/git.d.mts +11 -0
- package/cache/git.mjs +13 -0
- package/cache/hash.d.mts +15 -0
- package/cache/hash.mjs +58 -0
- package/cache/hfs-family.d.mts +35 -0
- package/cache/hfs-family.mjs +164 -0
- package/cache/import-graph.d.mts +75 -0
- package/cache/import-graph.mjs +204 -0
- package/cache/jsonl.d.mts +31 -0
- package/cache/jsonl.mjs +115 -0
- package/cache/lint-family.d.mts +162 -0
- package/cache/lint-family.mjs +433 -0
- package/cache/order.d.mts +1 -0
- package/cache/order.mjs +2 -0
- package/cache/progress.d.mts +19 -0
- package/cache/progress.mjs +68 -0
- package/cache/quality-baselines.d.mts +109 -0
- package/cache/quality-baselines.mjs +235 -0
- package/cache/records.d.mts +33 -0
- package/cache/records.mjs +54 -0
- package/cache/report.d.mts +19 -0
- package/cache/report.mjs +32 -0
- package/cache/run-log.d.mts +17 -0
- package/cache/run-log.mjs +37 -0
- package/cache/run.d.mts +33 -0
- package/cache/run.mjs +65 -0
- package/cache/session.d.mts +29 -0
- package/cache/session.mjs +197 -0
- package/cache/side-key.d.mts +10 -0
- package/cache/side-key.mjs +47 -0
- package/cache/test-family.d.mts +130 -0
- package/cache/test-family.mjs +258 -0
- package/cache/trust.d.mts +53 -0
- package/cache/trust.mjs +157 -0
- package/cache/types-family.d.mts +74 -0
- package/cache/types-family.mjs +154 -0
- package/cache/types.d.mts +99 -0
- package/cache/types.mjs +8 -0
- package/cache/wiring-family.d.mts +14 -0
- package/cache/wiring-family.mjs +43 -0
- package/coverage/cli.d.mts +4 -2
- package/coverage/cli.mjs +44 -5
- package/db/migrate.d.mts +16 -0
- package/db/migrate.mjs +21 -4
- package/db/postgres.d.mts +23 -0
- package/db/postgres.mjs +32 -1
- package/lint/run.d.mts +47 -1
- package/lint/run.mjs +157 -5
- package/package.json +2 -1
- package/release/gate.d.mts +2 -0
- package/release/gate.mjs +4 -2
- package/release/records.d.mts +15 -0
- package/release/records.mjs +25 -0
- package/release/urgent.mjs +23 -4
- package/runtime/knowledge/grammars/core/playbook.yaml +12 -18
- package/runtime/knowledge/hfs/canon-pins.yaml +8 -8
- package/runtime/knowledge/hfs/ci-standard.yaml +29 -6
- package/runtime/knowledge/hfs/rules.yaml +97 -7
- package/runtime/knowledge/hfs/slots.yaml +20 -8
- package/runtime/modules/kernel/failure-codes.yaml +46 -4
- package/runtime/modules/schemas/application-stacks.schema.yaml +48 -0
- package/runtime/scripts/hfs/architecture/hfs.d.mts +6 -6
- package/runtime/scripts/hfs/check.mjs +1 -1
- package/runtime/scripts/hfs/gate-threshold.mjs +2 -1
- package/runtime/scripts/hfs/rules/boot-proof.d.mts +5 -0
- package/runtime/scripts/hfs/rules/boot-proof.mjs +15 -1
- package/runtime/scripts/hfs/rules/ci-custody.mjs +3 -1
- package/runtime/scripts/hfs/rules/ci-standard.mjs +32 -3
- package/runtime/scripts/hfs/rules/database-migrations.d.mts +14 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +55 -0
- package/runtime/scripts/hfs/rules/database.mjs +2 -1
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +53 -6
- package/runtime/scripts/hfs/rules/release-provenance.mjs +32 -4
- package/runtime/scripts/hfs/rules/stacks.mjs +5 -1
- package/runtime/scripts/hfs/rules/supply-chain.mjs +4 -1
- package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +98 -7
- package/runtime/scripts/hfs/rules/unused-symbol.d.mts +9 -2
- package/runtime/scripts/hfs/rules/unused-symbol.mjs +15 -2
- package/runtime/scripts/hfs/runtime-rules/git-triggers.mjs +21 -5
- package/runtime/scripts/hfs/stack-deploy-config.d.mts +18 -0
- package/runtime/scripts/hfs/stack-deploy-config.mjs +53 -1
- package/runtime/scripts/hfs/stacks-gate.mjs +2 -0
- package/runtime/scripts/hfs/tree-findings.mjs +3 -1
- package/scaffold/add-inbox-lite.mjs +2 -4
- package/scaffold/add-queue-lite.mjs +2 -3
- package/scaffold/add-table.mjs +2 -4
- package/scaffold/migration-stamp.d.mts +11 -0
- package/scaffold/migration-stamp.mjs +32 -0
- package/src/helpers/hfs-arch-fixture.d.mts +1 -1
- package/src/main.d.mts +4 -1
- package/src/main.mjs +189 -74
- package/src/stack-records-double.d.mts +33 -0
- package/stack/ci-key.d.mts +11 -0
- package/stack/ci-key.mjs +44 -0
- package/stack/cli.d.mts +7 -1
- package/stack/cli.mjs +22 -13
- package/stack/config.d.mts +15 -0
- package/stack/converge.d.mts +2 -0
- package/stack/converge.mjs +1 -1
- package/stack/db-phases.d.mts +185 -0
- package/stack/db-phases.mjs +346 -0
- package/stack/deploy.d.mts +22 -5
- package/stack/deploy.mjs +242 -72
- package/stack/journal.d.mts +146 -0
- package/stack/journal.mjs +191 -0
- package/stack/manager-backup.d.mts +54 -0
- package/stack/manager-backup.mjs +136 -0
- package/stack/notify.d.mts +9 -0
- package/stack/notify.mjs +32 -0
- package/stack/release-store.d.mts +49 -1
- package/stack/release-store.mjs +102 -16
- package/stack/remote.d.mts +2 -0
- package/stack/remote.mjs +32 -11
- package/stack/rollback-guard.d.mts +10 -0
- package/stack/rollback-guard.mjs +41 -0
- package/stack/server-key.d.mts +2 -0
- package/stack/server-key.mjs +1 -1
- package/stack/ssh-gate.d.mts +35 -0
- package/stack/ssh-gate.mjs +145 -0
- package/stack/status.d.mts +51 -0
- package/stack/status.mjs +115 -0
- package/stack/urgent-floor.d.mts +10 -61
- package/stack/urgent-floor.mjs +28 -123
- package/stack/verify.mjs +26 -0
- package/sync/ci-commands.mjs +10 -5
- package/sync/format-worker.d.mts +1 -0
- package/sync/format-worker.mjs +17 -0
- package/sync/format.d.mts +21 -3
- package/sync/format.mjs +99 -7
- package/sync/index.d.mts +3 -6
- package/sync/index.mjs +12 -12
- package/sync/stack-images.d.mts +25 -10
- package/sync/stack-images.mjs +215 -54
- package/templates/app/ci-workflows/github/workflows/ci.yml +114 -7
- package/templates/app/ci-workflows/github/workflows/images.yml +21 -65
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +130 -37
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +114 -7
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +21 -65
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +130 -37
- package/templates/app/gitignore +1 -0
- package/templates/app/gitignore-lite +1 -0
- package/templates/app/package-scripts/db.json +1 -1
- package/templates/app/skeleton/.gitattributes +2 -0
- package/templates/app/skeleton-lite/.gitattributes +2 -0
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +2 -1
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +0 -63
package/src/main.mjs
CHANGED
|
@@ -8,27 +8,31 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
// The @starci/hfs implementation behind `starci app`: one repository, `<app>/`, with one package.json, lockfile and node_modules
|
|
10
10
|
// at its root and two sides, be/ and fe/, declared by the one hfs.json of kind app. Every verb runs at the app root.
|
|
11
|
-
// starci app check [--cwd <dir>] [--json] [--
|
|
11
|
+
// starci app check [--cwd <dir>] [--json] [--full] [--hfs] [--format] [--db] [--secrets] [--wiring] [--types] [--lint] [--test] [--sonar] [--coverage-from <lcov>] | check --staged
|
|
12
12
|
// every tracked path has a slot; required files exist; nothing forbidden or
|
|
13
13
|
// tracked-that-must-be-ignored; pins match; every managed file equals its render
|
|
14
14
|
// (sync/managed.mjs, the .gitignore block and sonar-project.properties included); no empty or ghost
|
|
15
15
|
// directory, no untracked entry outside an ignored slot; plaintext secrets, the .starcistacks shape, CI
|
|
16
16
|
// and pre-push canon steps, dependency version skew, the contract snapshot and the test, wire and i18n
|
|
17
|
-
// trees; prettier over every tracked file through the repository's own install (sync/format.mjs;
|
|
18
|
-
//
|
|
17
|
+
// trees; prettier over every tracked file through the repository's own install (sync/format.mjs; a repository
|
|
18
|
+
// without prettier is a refusal); soft-size backlog (report only); then the whole
|
|
19
19
|
// architecture machine (tiers, owners, clones, dead exports, module registration, the
|
|
20
20
|
// front-end and back-end source rules), each violation a finding with its why.
|
|
21
21
|
// ESLint executes once per side through the installed managed config; the FE config composes
|
|
22
22
|
// the universal canon and the explicitly selected Grammar-family canon.
|
|
23
|
+
// The results are committed in .starcistacks/cache/ (cache/, contexts/project/check-cache.md): with no selector every
|
|
24
|
+
// family runs diff-only against it and writes what it judged back; --full runs the selected families in full; a
|
|
25
|
+
// selector (--hfs, --format, --db, --secrets, --wiring, --types, --lint, --test, --sonar) runs only that family, and
|
|
26
|
+
// selectors combine. Each family prints one diagnosis line (full or diff and why, checked, reused, findings new, known,
|
|
27
|
+
// fixed); progress goes to stderr (--quiet silences it). STARCI_APP_CHECK_CACHE=verify runs diff then full and fails (exit 2)
|
|
28
|
+
// on any difference; =full never reads the cache (the release gate).
|
|
29
|
+
// --coverage-from <lcov> judges a coverage report produced elsewhere (the merge of a sharded unit run) and records nothing.
|
|
23
30
|
// --staged: the pre-commit guard alone (sync/hygiene.mjs: staged .starciwork and .starcistacks paths, the secrets
|
|
24
31
|
// guard and the bypass guard over every staged file read from the index, a new back-end logic file staged
|
|
25
32
|
// without its unit spec); it takes no other flag.
|
|
26
|
-
//
|
|
27
|
-
// overrides it); the machine
|
|
28
|
-
// runs on those owners without clones and dead exports. No merge-base is a refusal
|
|
29
|
-
// (exit 2), never a silent full pass. Exit 1 at 20 error findings or a hard blocker (gate-threshold.mjs). The root checks run once and
|
|
33
|
+
// Exit 1 at 20 error findings or a hard blocker (gate-threshold.mjs). The root checks run once and
|
|
30
34
|
// the side checks and the machine once per side, the side folder as their root; every path is app-relative.
|
|
31
|
-
// starci app lint [--cwd <dir>] [--changed <file>...] [--workspace <dir>] [--side be|fe] [--fix] [--json] [--out <file>]
|
|
35
|
+
// starci app lint [--cwd <dir>] [--changed <file>...] [--workspace <dir>] [--side be|fe] [--fix] [--full] [--json] [--out <file>]
|
|
32
36
|
// the ONE lint entry (npm run lint): ESLint per side with that side's canon config (per-file rules and
|
|
33
37
|
// the project-graph rules), this check's findings, and stylelint over the fe side, as one starci/lint@1
|
|
34
38
|
// report; --out writes the one Sonar import file. Exit 0 under 20 error findings and no hard blocker, 1 otherwise, 2 a tool could not run (lint/run.mjs).
|
|
@@ -81,26 +85,33 @@ import { createRequire } from 'node:module';
|
|
|
81
85
|
import path from 'node:path';
|
|
82
86
|
import { pathToFileURL } from 'node:url';
|
|
83
87
|
import { isMain } from '../runtime/scripts/lib/is-main.mjs';
|
|
84
|
-
import {
|
|
88
|
+
import { explainPath, readWhy, trackedFiles, withCheckProofs } from '../runtime/scripts/hfs/check.mjs';
|
|
89
|
+
import { withWhy } from '../runtime/scripts/hfs/check-results.mjs';
|
|
90
|
+
import { skillRoot } from '../runtime/engine/runtime-root.mjs';
|
|
85
91
|
import { secretMain } from '../runtime/scripts/hfs/secret.mjs';
|
|
86
92
|
import { dbBaselineMain } from '../runtime/scripts/hfs/db-baseline-verb.mjs';
|
|
87
93
|
import { dbRestampMain } from '../runtime/scripts/hfs/db-restamp-verb.mjs';
|
|
88
94
|
import { formatCoverageReport } from '../runtime/scripts/hfs/coverage-from.mjs';
|
|
89
95
|
import { gateVerdict } from '../runtime/scripts/hfs/gate-threshold.mjs';
|
|
90
96
|
import { HFS_DECLARATION_FILE, HfsSlotsError, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
|
|
91
|
-
import { formatFindings } from '../sync/format.mjs';
|
|
92
97
|
import { main as syncMain } from '../sync/cli.mjs';
|
|
93
98
|
import { runWorkHygiene } from '../sync/hygiene.mjs';
|
|
94
99
|
import { SyncError, bundledPresets } from '../sync/index.mjs';
|
|
95
|
-
import { managedFindings } from '../sync/managed.mjs';
|
|
96
100
|
import { emitContracts } from '../emit/contracts.mjs';
|
|
97
101
|
import { ScaffoldError, newService, newSpec } from '../scaffold/service.mjs';
|
|
98
102
|
import { EditionRefusal } from '../scaffold/edition-gate.mjs';
|
|
99
103
|
import { addKind } from '../scaffold/add.mjs';
|
|
100
104
|
import { scaffoldApp } from '../scaffold/app.mjs';
|
|
101
105
|
import { newImages } from '../scaffold/image.mjs';
|
|
102
|
-
import {
|
|
103
|
-
import {
|
|
106
|
+
import { cachedLinters, eslintRepository, lintRepository, parseLintArgs, printLintText } from '../lint/run.mjs';
|
|
107
|
+
import { runSonarForApp, runTestForApp, runTestShard, runTypesForApp } from '../cache/app-families.mjs';
|
|
108
|
+
import { CACHE_ENV, createDiskSession } from '../cache/session.mjs';
|
|
109
|
+
import { DEFAULT_FAMILIES, runAppFamilies } from '../cache/run.mjs';
|
|
110
|
+
import { createProgress, silentProgress } from '../cache/progress.mjs';
|
|
111
|
+
import { familyLine } from '../cache/report.mjs';
|
|
112
|
+
import { writeRunLog } from '../cache/run-log.mjs';
|
|
113
|
+
import { FAMILIES } from '../cache/types.mjs';
|
|
114
|
+
import { SECRET_CODES as SECRET_CODE_LIST } from '../cache/hfs-family.mjs';
|
|
104
115
|
import { writeReport } from '../report/sonar.mjs';
|
|
105
116
|
import { checkEdition } from '../upgrade/check.mjs';
|
|
106
117
|
import { UpgradeError, upgradeMain } from '../upgrade/index.mjs';
|
|
@@ -134,6 +145,8 @@ const DATABASE_VERBS = new Map([['baseline', dbBaselineMain], ['restamp', dbRest
|
|
|
134
145
|
*/
|
|
135
146
|
/** The flags of the add nouns that generate a file tree from a pattern (service, spec and image take none of them, and --inject is only theirs). */
|
|
136
147
|
const ADD_FLAGS = { value: ['--event', '--from', '--service', '--connection', '--owner', '--failed', '--done', '--shapes', '--read', '--app', '--kind', '--blocks', '--tabs'], boolean: ['--fe', '--form'] };
|
|
148
|
+
/** The family selectors of `app check`, one flag per family of the cache (contexts/project/check-cache.md). */
|
|
149
|
+
const FAMILY_FLAGS = FAMILIES.map((family) => `--${family}`);
|
|
137
150
|
const PARSED = {
|
|
138
151
|
scaffold: { run: scaffoldMain, value: ['--registry-visibility'], edition: true, forms: [{ operands: '<name>' }] },
|
|
139
152
|
add: {
|
|
@@ -145,7 +158,7 @@ const PARSED = {
|
|
|
145
158
|
{ operands: 'image', flags: [] },
|
|
146
159
|
],
|
|
147
160
|
},
|
|
148
|
-
check: { run: checkMain, value: ['--
|
|
161
|
+
check: { run: checkMain, value: ['--coverage-from', '--shard'], boolean: ['--json', '--staged', '--full', ...FAMILY_FLAGS], edition: true, forms: [{ only: { boolean: ['--json', '--full', ...FAMILY_FLAGS], value: ['--coverage-from', '--shard'] } }, { only: { boolean: ['--staged'] }, edition: false }] },
|
|
149
162
|
slot: { run: slotMain, boolean: ['--json'], forms: [{ operands: '<path>' }] },
|
|
150
163
|
emit: { run: emitMain, forms: [{}] },
|
|
151
164
|
};
|
|
@@ -155,7 +168,7 @@ const ADD_FLAGS_ALL = flagsOf(ADD_FLAGS);
|
|
|
155
168
|
const PARSED_FLAGS = new Set([...Object.values(PARSED).flatMap(flagsOf), '--edition']);
|
|
156
169
|
/** The placeholder of a value flag in the usage; any other prints as <name>. */
|
|
157
170
|
const PLACEHOLDERS = {
|
|
158
|
-
'--registry-visibility': 'public|private', '--edition': 'full|lite', '--base': '<ref>', '--coverage-from': '<lcov>', '--inject': '<binding>',
|
|
171
|
+
'--registry-visibility': 'public|private', '--edition': 'full|lite', '--base': '<ref>', '--coverage-from': '<lcov>', '--shard': '<i>/<n>', '--inject': '<binding>',
|
|
159
172
|
'--event': '<event>', '--from': '<service>', '--service': '<Class>=<module>', '--owner': '<service>', '--failed': '<event>', '--done': '<event>',
|
|
160
173
|
'--shapes': '<a,b,c>', '--blocks': '<a,b>', '--tabs': '<a,b>', '--kind': 'modal|drawer', '--image': '<service>=<ref>', '--sha': '<sha>',
|
|
161
174
|
'--previous': '<release>', '--release': '<release>', '--out': '<path>', '--since': '<ISO>', '--timeout': '<seconds>', '--retry': '<seconds>',
|
|
@@ -178,7 +191,7 @@ const DATABASE_USAGE = [['new', '<kebab-name>', DB_NEW_FLAGS], ['baseline', '',
|
|
|
178
191
|
/** The usage of each verb, from the tables its parser refuses every other flag with (lint, codemod and upgrade keep hand-parsed arguments, held by main.spec.mjs). */
|
|
179
192
|
const VERB_USAGE = {
|
|
180
193
|
...Object.fromEntries(Object.entries(PARSED).map(([verb, spec]) => [verb, spec.forms.map((form) => line(`starci app ${verb}`, form.operands, specUsage(form.only ?? (form.flags === undefined ? spec : { value: form.flags })), spec.edition && form.edition !== false ? flagUsage('--edition', { value: true }) : ''))])),
|
|
181
|
-
lint: ['starci app lint [--changed <file>...] [--workspace <dir>] [--side be|fe] [--fix] [--json] [--out <file>]'],
|
|
194
|
+
lint: ['starci app lint [--changed <file>...] [--workspace <dir>] [--side be|fe] [--fix] [--full] [--full] [--json] [--out <file>]'],
|
|
182
195
|
codemod: ['starci app codemod button-intent [--execute] [--json] [--format json|markdown] [--out <file.json|file.md>]'],
|
|
183
196
|
upgrade: ['starci app upgrade [--execute]'],
|
|
184
197
|
sync: [SYNC_USAGE.sync],
|
|
@@ -244,22 +257,31 @@ function contractDescription(contracts) {
|
|
|
244
257
|
return `emitted and compared, ${apps}`;
|
|
245
258
|
}
|
|
246
259
|
function printCheckOverview(result, out) {
|
|
247
|
-
out(`starci app check
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
260
|
+
out(`starci app check ${result.repoRoot} (profile ${result.profile ?? 'unknown'}, manifest ${result.manifest}, ${result.tracked} tracked paths)
|
|
261
|
+
`);
|
|
262
|
+
for (const report of (result.families ?? []))
|
|
263
|
+
out(` ${familyLine(report)}
|
|
264
|
+
`);
|
|
265
|
+
for (const warning of (result.cacheWarnings ?? []))
|
|
266
|
+
out(` cache: ${warning}
|
|
267
|
+
`);
|
|
268
|
+
out(` architecture machine: ${machineDescription(result.machine)}
|
|
269
|
+
`);
|
|
251
270
|
if (result.contracts)
|
|
252
|
-
out(` contract snapshots: ${contractDescription(result.contracts)}
|
|
271
|
+
out(` contract snapshots: ${contractDescription(result.contracts)}
|
|
272
|
+
`);
|
|
253
273
|
if (result.eslint) {
|
|
254
274
|
const sides = Object.entries(result.eslint.sides).map(([side, run]) => `${side}: ${run.files} files${run.skipped ? ` (${run.skipped})` : ''}`).join(', ');
|
|
255
|
-
out(` ESLint: ${sides}
|
|
275
|
+
out(` ESLint: ${sides}
|
|
276
|
+
`);
|
|
256
277
|
for (const error of result.eslint.errors)
|
|
257
|
-
out(` ESLint tool error: ${error}
|
|
278
|
+
out(` ESLint tool error: ${error}
|
|
279
|
+
`);
|
|
258
280
|
}
|
|
259
281
|
}
|
|
260
282
|
function printFindingGroup(code, list, out) {
|
|
261
283
|
const first = list[0];
|
|
262
|
-
if (first.engine
|
|
284
|
+
if (first.engine)
|
|
263
285
|
out(`\n[${first.level.toUpperCase()}] ${code} x${list.length}\n`);
|
|
264
286
|
else
|
|
265
287
|
out(`\n[${first.level.toUpperCase()}] ${code} x${list.length}: ${first.titleVi}\n ${first.whyVi}\n -> ${first.nextStepVi}\n`);
|
|
@@ -307,36 +329,37 @@ function printExplain(e, out) {
|
|
|
307
329
|
if (e.code)
|
|
308
330
|
out(` ${e.code}: ${e.titleVi}\n ${e.whyVi}\n`);
|
|
309
331
|
}
|
|
332
|
+
/** The family selectors given, as families; none given selects `fallback`. */
|
|
333
|
+
function selectedFamilies(opts, fallback) {
|
|
334
|
+
const given = FAMILIES.filter((family) => opts[family] === true);
|
|
335
|
+
return new Set(given.length > 0 ? given : fallback);
|
|
336
|
+
}
|
|
337
|
+
/** Opens the session of a verb over the tracked files of the app: progress on stderr, `--quiet` silent, a terminal rewriting running lines in place. */
|
|
338
|
+
function openSession({ repoRoot, tracked, io, full, use, planned }) {
|
|
339
|
+
const progress = io.quiet === true ? silentProgress : createProgress({ write: (text) => io.stderr(text), tty: io.stderrIsTty === true, planned });
|
|
340
|
+
const env = use === 'bypass' ? { ...io.env, [CACHE_ENV]: 'full' } : use === 'read' ? { ...io.env, [CACHE_ENV]: io.env[CACHE_ENV] === 'full' ? 'full' : 'verify' } : io.env;
|
|
341
|
+
return createDiskSession({ root: repoRoot, tracked, env, progress, cliVersion: io.harness?.version, full });
|
|
342
|
+
}
|
|
310
343
|
/**
|
|
311
|
-
* The app pass
|
|
312
|
-
* `only` limits the formatter to those (app-relative) files.
|
|
344
|
+
* The app pass of `starci app check` and `starci app lint`: the families the verb selects, against one cache session, merged into one result.
|
|
345
|
+
* `only` limits the formatter to those (app-relative) files and bypasses the cache (`app lint --changed`).
|
|
313
346
|
*/
|
|
314
|
-
async function
|
|
347
|
+
async function appPass({ repoRoot, harness, only, presets, prettier, io, selected, full, use = 'normal', session: given }) {
|
|
315
348
|
const tracked = trackedFiles(repoRoot);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const files = tracked.filter((file) => file.startsWith('be/')).map((file) => file.slice('be/'.length));
|
|
331
|
-
const emitted = contractEmitFindings({ repoRoot: path.join(repoRoot, 'be'), files, repo: be, emit: emitContracts });
|
|
332
|
-
extraFindings.push(...emitted.findings.map((finding) => ({ ...finding, side: 'be', path: `be/${finding.path}` })));
|
|
333
|
-
contracts = { status: 'checked', apps: emitted.apps };
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
const result = checkRepository({ repoRoot, harness, fast, base, extraFindings });
|
|
337
|
-
if (contracts)
|
|
338
|
-
result.contracts = contracts;
|
|
339
|
-
return result;
|
|
349
|
+
const session = given ?? openSession({ repoRoot, tracked, io, full, use: only === undefined ? use : 'bypass', planned: tracked.length * Math.max(1, selected.size) });
|
|
350
|
+
const result = await runAppFamilies({ repoRoot, tracked, session, selected, harness, presets, prettier, only, edition: undefined });
|
|
351
|
+
if (given !== undefined)
|
|
352
|
+
return result;
|
|
353
|
+
const flushed = session.flush();
|
|
354
|
+
return { ...result, findings: [...result.findings, ...verdictFindings(session)], cacheWarnings: flushed.warnings };
|
|
355
|
+
}
|
|
356
|
+
/** The verdicts of the CI comparison (forged, stale, missing) as findings with their why. */
|
|
357
|
+
function verdictFindings(session) {
|
|
358
|
+
const verdicts = session.verdicts();
|
|
359
|
+
if (verdicts.length === 0)
|
|
360
|
+
return [];
|
|
361
|
+
const why = readWhy(skillRoot, [...new Set(verdicts.map((verdict) => verdict.code))]);
|
|
362
|
+
return withWhy(verdicts.map((verdict) => ({ code: verdict.code, level: verdict.level, path: verdict.path, message: verdict.message })), why);
|
|
340
363
|
}
|
|
341
364
|
/** The nearest folder at or above `dir` that holds hfs.json (the app root of a workspace), or `dir` itself when none does (the lint then refuses it). */
|
|
342
365
|
export function appRootAbove(dir) {
|
|
@@ -348,7 +371,8 @@ export function appRootAbove(dir) {
|
|
|
348
371
|
}
|
|
349
372
|
}
|
|
350
373
|
/** `starci app lint`: ESLint per side, the app checks and stylelint over the fe side as one report; see lint/run.mjs. */
|
|
351
|
-
async function lintMain(argv,
|
|
374
|
+
async function lintMain(argv, io) {
|
|
375
|
+
const { cwd, harness, stdout, presets, prettier, runLinter, checkApp = appPass } = io;
|
|
352
376
|
const opts = parseLintArgs(argv);
|
|
353
377
|
// --workspace names a folder from the working directory (`starci app lint --workspace .` is an fe workspace script); the app
|
|
354
378
|
// root is then the nearest folder above it that holds hfs.json, and the workspace is passed on app-relative.
|
|
@@ -356,10 +380,17 @@ async function lintMain(argv, { cwd, harness, stdout, presets, prettier, runLint
|
|
|
356
380
|
const repoRoot = path.resolve(workspaceDir ? appRootAbove(workspaceDir) : cwd);
|
|
357
381
|
if (workspaceDir)
|
|
358
382
|
opts.workspace = path.relative(repoRoot, workspaceDir).split(path.sep).join('/');
|
|
383
|
+
// The lint shares the cache of `app check`: --workspace reads it and writes none, --changed and --fix neither read nor write, --full judges again.
|
|
384
|
+
const use = opts.changed !== null || opts.fix ? 'bypass' : opts.workspace !== undefined ? 'read' : 'normal';
|
|
385
|
+
const selected = new Set(['hfs', 'secrets', 'db', 'format']);
|
|
386
|
+
// One session for the app findings and the linters, so the two never hold two answers; checkApp is a spec seam that brings its own.
|
|
387
|
+
const session = io.checkApp === undefined ? openSession({ repoRoot, tracked: trackedFiles(repoRoot), io, full: opts.full === true ? new Set([...selected, 'lint']) : undefined, use, planned: 1 }) : undefined;
|
|
359
388
|
const { report, sonar, exit } = await lintRepository({
|
|
360
|
-
repoRoot, opts, trackedFiles, runLinter,
|
|
361
|
-
hfsCheck: (root) => checkApp({ repoRoot: root, harness, only: opts.changed ?? undefined, presets, prettier }),
|
|
389
|
+
repoRoot, opts, trackedFiles, runLinter, ...(session === undefined ? {} : { cache: { session } }),
|
|
390
|
+
hfsCheck: (root) => checkApp({ repoRoot: root, harness, only: opts.changed ?? undefined, presets, prettier, io, selected, use, session }),
|
|
362
391
|
});
|
|
392
|
+
const flushed = session?.flush();
|
|
393
|
+
const verdicts = session === undefined ? [] : verdictFindings(session);
|
|
363
394
|
if (opts.out !== undefined)
|
|
364
395
|
writeReport(path.resolve(cwd, opts.out), sonar);
|
|
365
396
|
if (opts.format === 'json')
|
|
@@ -367,7 +398,11 @@ async function lintMain(argv, { cwd, harness, stdout, presets, prettier, runLint
|
|
|
367
398
|
`);
|
|
368
399
|
else
|
|
369
400
|
printLintText(report, stdout);
|
|
370
|
-
|
|
401
|
+
for (const verdict of verdicts)
|
|
402
|
+
stdout(`${verdict.level.toUpperCase()} ${verdict.code}: ${verdict.message}\n`);
|
|
403
|
+
for (const warning of flushed?.warnings ?? [])
|
|
404
|
+
stdout(`cache: ${warning}\n`);
|
|
405
|
+
return Math.max(exit, verdicts.some((verdict) => verdict.level === 'error') ? 1 : 0);
|
|
371
406
|
}
|
|
372
407
|
/**
|
|
373
408
|
* The jest preset a new app renders its Sonar exclusions from: the copy this @starci/hfs ships (bundledPresets), the same version the
|
|
@@ -448,8 +483,80 @@ function printCheckProofs(result, out) {
|
|
|
448
483
|
out(formatCoverageReport(result.coverage));
|
|
449
484
|
}
|
|
450
485
|
/** The flags `check --staged` refuses: it is the pre-commit guard alone, so none of the whole-app pass's options means anything with it. */
|
|
451
|
-
const STAGED_REFUSES = ['--json', '--
|
|
452
|
-
|
|
486
|
+
const STAGED_REFUSES = ['--json', '--full', '--coverage-from', '--shard', ...FAMILY_FLAGS];
|
|
487
|
+
/** The family a finding belongs to, for naming it when a verified run disagrees with the cache. */
|
|
488
|
+
function familyOf(finding) {
|
|
489
|
+
const code = finding.code ?? '';
|
|
490
|
+
if (finding.engine === 'eslint' || finding.engine === 'stylelint')
|
|
491
|
+
return 'lint';
|
|
492
|
+
if (code === 'HFS_FORMAT')
|
|
493
|
+
return 'format';
|
|
494
|
+
if (/^(DB_|LITE_DB_)/.test(code))
|
|
495
|
+
return 'db';
|
|
496
|
+
if (code === 'BE_APP_BOOT_PROOF_MISSING' || code === 'BOOT_PROOF_TIMEOUT')
|
|
497
|
+
return 'wiring';
|
|
498
|
+
return SECRET_CODE_LIST.has(code) ? 'secrets' : 'hfs';
|
|
499
|
+
}
|
|
500
|
+
/** One run of the selected families against the cache, then the coverage proof on top; returns the result and what it took. */
|
|
501
|
+
async function checkRun({ io, selected, full, use, coverageFrom }) {
|
|
502
|
+
const { cwd: repoRoot, harness, edition, presets, prettier, checkApp = appPass, runLinter } = io;
|
|
503
|
+
const started = Date.now();
|
|
504
|
+
const tracked = io.checkApp === undefined ? trackedFiles(repoRoot) : [];
|
|
505
|
+
const session = io.checkApp === undefined ? openSession({ repoRoot, tracked, io, full, use, planned: tracked.length * Math.max(1, selected.size) }) : undefined;
|
|
506
|
+
const checked = await checkEdition({ repoRoot, edition, normal: () => checkApp({ repoRoot, harness, presets, prettier, io, selected, full, use, session }), presets, prettier });
|
|
507
|
+
const proof = withCheckProofs(checked, { repoRoot, edition, wiring: false, coverageFrom });
|
|
508
|
+
const extra = [];
|
|
509
|
+
const lines = [];
|
|
510
|
+
let eslint = null;
|
|
511
|
+
if (selected.has('lint')) {
|
|
512
|
+
if (session !== undefined) {
|
|
513
|
+
const app = readRepoDeclaration(loadSlotManifest(), repoRoot);
|
|
514
|
+
eslint = (await cachedLinters({ repoRoot, app, opts: { changed: null, fix: false, format: 'text', out: undefined, workspace: undefined }, workspace: null, onFe: null, run: runLinter, cache: { session } })).eslint;
|
|
515
|
+
}
|
|
516
|
+
else
|
|
517
|
+
eslint = await eslintRepository({ repoRoot, changed: null, existing: [], run: runLinter });
|
|
518
|
+
}
|
|
519
|
+
if (session !== undefined && selected.has('types'))
|
|
520
|
+
extra.push(...runTypesForApp(session, { repoRoot, tracked, env: io.env }).findings);
|
|
521
|
+
if (session !== undefined && selected.has('test')) {
|
|
522
|
+
const tests = await runTestForApp(session, { repoRoot, tracked });
|
|
523
|
+
extra.push(...tests.findings);
|
|
524
|
+
}
|
|
525
|
+
if (session !== undefined && selected.has('sonar'))
|
|
526
|
+
lines.push(...(runSonarForApp(session, { repoRoot })?.lines ?? []));
|
|
527
|
+
const flushed = session?.flush();
|
|
528
|
+
const verdicts = session === undefined ? [] : verdictFindings(session);
|
|
529
|
+
const reports = session === undefined ? [...(checked.families ?? [])] : [...session.reports];
|
|
530
|
+
const findings = [...proof.findings, ...extra, ...verdicts, ...(eslint?.findings ?? []).map(finding => ({ ...finding, code: finding.code ?? finding.rule, level: 'error' }))];
|
|
531
|
+
// Green under the threshold (gate-threshold.mjs): every finding is still listed, only the verdict changes.
|
|
532
|
+
const verdict = gateVerdict(findings.filter(finding => finding.level === 'error'));
|
|
533
|
+
const toolErrors = eslint?.errors ?? [];
|
|
534
|
+
const counts = { ...proof.counts, error: findings.filter(finding => finding.level === 'error').length, warning: findings.filter(finding => finding.level === 'warning').length };
|
|
535
|
+
const result = { ...proof, findings, families: reports, predictions: lines, cacheWarnings: [...(checked.cacheWarnings ?? []), ...(flushed?.warnings ?? [])], counts, ...(eslint ? { eslint: { ...eslint.engine, errors: eslint.errors } } : {}), ok: verdict.pass && toolErrors.length === 0, gate: verdict };
|
|
536
|
+
return { result, seconds: (Date.now() - started) / 1000 };
|
|
537
|
+
}
|
|
538
|
+
/** STARCI_APP_CHECK_CACHE=verify: the diff-only run, then the full run, compared finding by finding; any difference names family and file (exit 2). */
|
|
539
|
+
async function verifyCache(io, selected, coverageFrom) {
|
|
540
|
+
const diff = await checkRun({ io, selected, full: undefined, use: 'read', coverageFrom });
|
|
541
|
+
const full = await checkRun({ io, selected, full: new Set(selected), use: 'bypass', coverageFrom });
|
|
542
|
+
const keyed = (result) => new Map(result.findings.map((finding) => [`${finding.code}|${finding.path ?? ''}|${finding.message}`, `${familyOf(finding)}: ${finding.path ?? finding.code}`]));
|
|
543
|
+
const left = keyed(diff.result);
|
|
544
|
+
const right = keyed(full.result);
|
|
545
|
+
const differences = [...left].filter(([key]) => !right.has(key)).map(([, name]) => `diff-only only: ${name}`)
|
|
546
|
+
.concat([...right].filter(([key]) => !left.has(key)).map(([, name]) => `full only: ${name}`)).sort();
|
|
547
|
+
io.stdout(`${CACHE_ENV}=verify: diff-only ${diff.seconds.toFixed(1)}s, full ${full.seconds.toFixed(1)}s, ${right.size} finding(s) compared\n`);
|
|
548
|
+
for (const report of diff.result.families)
|
|
549
|
+
io.stdout(` diff-only ${familyLine(report)}\n`);
|
|
550
|
+
if (differences.length === 0) {
|
|
551
|
+
io.stdout(' identical\n');
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
for (const difference of differences)
|
|
555
|
+
io.stdout(` DIFFERENCE ${difference}\n`);
|
|
556
|
+
return 2;
|
|
557
|
+
}
|
|
558
|
+
async function checkMain(opts, io) {
|
|
559
|
+
const { cwd: repoRoot, edition, stdout: out } = io;
|
|
453
560
|
if (opts.positional.length)
|
|
454
561
|
throw new Error('starci app check takes no path');
|
|
455
562
|
if (opts.staged === true) {
|
|
@@ -458,26 +565,33 @@ async function checkMain(opts, { cwd: repoRoot, harness, edition, stdout: out, p
|
|
|
458
565
|
throw new Error(`starci app check --staged is the pre-commit guard alone and takes no ${given.join(', ')}`);
|
|
459
566
|
return runWorkHygiene({ cwd: repoRoot, out: (text) => out(`${text}\n`) });
|
|
460
567
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
|
|
568
|
+
const coverageFrom = valueFlag(opts, 'coverage-from');
|
|
569
|
+
const shard = valueFlag(opts, 'shard');
|
|
570
|
+
if (shard !== undefined) {
|
|
571
|
+
// The sharded full unit run of CI (--test --full --shard <i>/<n>): the shard's Jest run in the shape the shard merge reads; nothing is recorded.
|
|
572
|
+
const match = /^(\d+)\/(\d+)$/.exec(shard);
|
|
573
|
+
if (match === null || opts.test !== true || opts.full !== true)
|
|
574
|
+
throw new Error('--shard <i>/<n> belongs to `starci app check --test --full`');
|
|
575
|
+
return runTestShard(repoRoot, Number(match[1]), Number(match[2]));
|
|
576
|
+
}
|
|
577
|
+
const explicit = selectedFamilies(opts, []);
|
|
578
|
+
// 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.
|
|
579
|
+
const selected = explicit.size > 0 ? explicit : coverageFrom === undefined ? new Set(DEFAULT_FAMILIES) : new Set();
|
|
580
|
+
const full = opts.full === true ? new Set(selected) : undefined;
|
|
581
|
+
if (io.env[CACHE_ENV] === 'verify')
|
|
582
|
+
return verifyCache(io, selected, coverageFrom);
|
|
583
|
+
const { result, seconds } = await checkRun({ io, selected, full, use: coverageFrom !== undefined && explicit.size === 0 ? 'bypass' : 'normal', coverageFrom });
|
|
472
584
|
if (opts.json)
|
|
473
585
|
out(`${JSON.stringify(result, null, 2)}\n`);
|
|
474
586
|
else {
|
|
475
587
|
printCheck(result, out);
|
|
476
588
|
printCheckProofs(result, out);
|
|
477
|
-
if (
|
|
478
|
-
out(`${
|
|
589
|
+
if (result.gate.line)
|
|
590
|
+
out(`${result.gate.line}\n`);
|
|
479
591
|
}
|
|
480
|
-
|
|
592
|
+
const exit = result.eslint?.errors.length ? 2 : result.ok ? 0 : 1;
|
|
593
|
+
writeRunLog(repoRoot, { command: `check${[...selected].map((family) => ` --${family}`).join('')}${opts.full === true ? ' --full' : ''}`, seconds, exit, families: result.families.map((report) => ({ ...report, seconds: 0 })) }, io.env);
|
|
594
|
+
return exit;
|
|
481
595
|
}
|
|
482
596
|
function emitMain(opts, { cwd: repoRoot, stdout: out }) {
|
|
483
597
|
if (opts.positional.length)
|
|
@@ -607,11 +721,12 @@ function errorMessage(error) {
|
|
|
607
721
|
* the HFS check and `stackIo` Swarm tools; production defaults execute every check. `hostIdentity` ({ label, read }) is the
|
|
608
722
|
* 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.
|
|
609
723
|
*/
|
|
610
|
-
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, } = {}) {
|
|
724
|
+
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, } = {}) {
|
|
611
725
|
try {
|
|
612
726
|
const global = globalOptions(argv, path.resolve(baseCwd));
|
|
613
727
|
const [verb, ...given] = global.argv;
|
|
614
|
-
|
|
728
|
+
// --quiet silences stdout for every verb but check and lint, whose diagnosis it keeps (their progress on stderr is what it mutes).
|
|
729
|
+
const out = global.quiet && !['check', 'lint'].includes(verb) ? () => { } : stdout;
|
|
615
730
|
if (global.help) {
|
|
616
731
|
out(USAGE);
|
|
617
732
|
return 0;
|
|
@@ -621,7 +736,7 @@ export async function main(argv, { cwd: baseCwd = process.cwd(), harness, stdout
|
|
|
621
736
|
return 2;
|
|
622
737
|
}
|
|
623
738
|
const { edition, argv: rest } = editionOption(verb, given);
|
|
624
|
-
return await dispatchMain(verb, rest, { cwd: global.cwd, harness, edition, stdout: out, stderr, presets, prettier, lock, run, stackMain, env, stdin, prompt, hostIdentity, sops, tools, platform, stackIo, runLinter, checkApp });
|
|
739
|
+
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 });
|
|
625
740
|
}
|
|
626
741
|
catch (error) {
|
|
627
742
|
stderr(errorMessage(error));
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReleaseStore, StoredRelease } from '../stack/release-store.mjs';
|
|
2
|
+
/** The records half of a ReleaseStore, plus what it kept. */
|
|
3
|
+
export interface RecordsDouble extends Pick<ReleaseStore, 'read' | 'tryLock' | 'refreshLock' | 'unlock' | 'journal' | 'journals'> {
|
|
4
|
+
/** The latest document of each attempt, as the journal wrote it (a copy at each write). */
|
|
5
|
+
documents: Map<string, Record<string, unknown>>;
|
|
6
|
+
/** Every write of every document, in order: the phase boundaries a cancelled job would have left. */
|
|
7
|
+
writes: Record<string, unknown>[];
|
|
8
|
+
/** The append-only index. */
|
|
9
|
+
index: Record<string, unknown>[];
|
|
10
|
+
/** The holder token and trigger of the lock, null when it is free. */
|
|
11
|
+
lock: {
|
|
12
|
+
token: string;
|
|
13
|
+
trigger: string;
|
|
14
|
+
} | null;
|
|
15
|
+
/** Lock calls in order: `try:<trigger>`, `refresh`, `unlock`. */
|
|
16
|
+
calls: string[];
|
|
17
|
+
}
|
|
18
|
+
/** What a double can be told: the release `read` answers, a lock held by someone else, and journal writes that fail. */
|
|
19
|
+
export interface RecordsOptions {
|
|
20
|
+
read?: (id: string) => StoredRelease | Promise<StoredRelease>;
|
|
21
|
+
/** The lock is held by another deploy for this many seconds: `tryLock` answers busy until `freeAfterTries` tries have passed. */
|
|
22
|
+
busy?: {
|
|
23
|
+
holder: string;
|
|
24
|
+
ageSeconds: number;
|
|
25
|
+
freeAfterTries?: number;
|
|
26
|
+
} | undefined;
|
|
27
|
+
failJournal?: boolean | undefined;
|
|
28
|
+
/** The attempt documents the journal already holds (what journals() answers): the evidence of the manual rollback guard. */
|
|
29
|
+
history?: Record<string, unknown>[] | undefined;
|
|
30
|
+
/** The journal accepts this many document writes and fails every later one (a manager that drops mid-deploy). */
|
|
31
|
+
failJournalAfter?: number | undefined;
|
|
32
|
+
}
|
|
33
|
+
export declare function recordsDouble(options?: RecordsOptions): RecordsDouble;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DeployConfig } from './config.mjs';
|
|
2
|
+
import type { StackContext } from './context.mjs';
|
|
3
|
+
import type { ActionResult } from './remote.mjs';
|
|
4
|
+
/** What the verb takes. */
|
|
5
|
+
export interface CiKeyOptions {
|
|
6
|
+
publicKey?: string | null | undefined;
|
|
7
|
+
printAuthorizedKeys?: boolean | undefined;
|
|
8
|
+
printGate?: boolean | undefined;
|
|
9
|
+
}
|
|
10
|
+
/** Prints the gate, the authorized_keys line or the install steps (see the top of the file). */
|
|
11
|
+
export declare function ciKey(_ctx: StackContext, config: DeployConfig, options?: CiKeyOptions): Promise<ActionResult>;
|
package/stack/ci-key.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// ci-key.mjs - `starci app stack ci-key`: the owner-run side of the forced-command gate of the CI SSH key (ssh-gate.mts).
|
|
2
|
+
// --print-gate prints the text of starci-ssh-gate, to install executable as /usr/local/bin/starci-ssh-gate on the manager
|
|
3
|
+
// --print-authorized-keys prints the authorized_keys line of the CI key from its PUBLIC key file (--public-key <file>)
|
|
4
|
+
// Both print only their text, so they pipe straight into the owner's ssh. Nothing here reaches a server, reads a sealed member or touches a private
|
|
5
|
+
// key: creating, installing and rotating the key stay owner-run steps. Without a flag the verb prints the install steps.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import { refusal } from './errors.mjs';
|
|
8
|
+
import { GATE_PATH, authorizedKeysLine, gateScript } from './ssh-gate.mjs';
|
|
9
|
+
const lineOf = (file) => {
|
|
10
|
+
if (!file)
|
|
11
|
+
throw refusal('STACK_USAGE', '--print-authorized-keys needs --public-key <the .pub file of the CI key>');
|
|
12
|
+
let text;
|
|
13
|
+
try {
|
|
14
|
+
text = fs.readFileSync(file, 'utf8');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw refusal('STACK_CI_KEY_PUBLIC', `cannot read the public key file ${file}`);
|
|
18
|
+
}
|
|
19
|
+
return authorizedKeysLine(text);
|
|
20
|
+
};
|
|
21
|
+
/** Prints the gate, the authorized_keys line or the install steps (see the top of the file). */
|
|
22
|
+
export async function ciKey(_ctx, config, options = {}) {
|
|
23
|
+
if (options.printGate === true && options.printAuthorizedKeys === true)
|
|
24
|
+
throw refusal('STACK_USAGE', 'print the gate or the authorized_keys line, one at a time');
|
|
25
|
+
if (options.printGate === true)
|
|
26
|
+
return { ok: true, lines: [gateScript().trimEnd()], data: { gatePath: GATE_PATH } };
|
|
27
|
+
if (options.printAuthorizedKeys === true) {
|
|
28
|
+
const line = lineOf(options.publicKey);
|
|
29
|
+
return { ok: true, lines: [line], data: { authorizedKeys: line } };
|
|
30
|
+
}
|
|
31
|
+
const env = config.env;
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
lines: [
|
|
35
|
+
'the CI key reaches Docker, which is root on the manager: its authorized_keys line forces starci-ssh-gate, which admits docker system dial-stdio and the release store\'s named operations only.',
|
|
36
|
+
'owner-run steps on the manager (as root; <ops> is your own ssh access):',
|
|
37
|
+
` 1. starci app stack ci-key --env ${env} --print-gate | ssh <ops> 'install -m 0755 -o root -g root /dev/stdin ${GATE_PATH}'`,
|
|
38
|
+
` 2. starci app stack ci-key --env ${env} --public-key <ci-key.pub> --print-authorized-keys | ssh <ops> 'cat >> ~<ci-account>/.ssh/authorized_keys'`,
|
|
39
|
+
' 3. seal VPS_GATE=1 in the vps-ci member of the environment, so `stack connect` tells the deploy to use the gate (STARCI_SSH_GATE=1); remove it to go back.',
|
|
40
|
+
'not yet admitted by the gate: the post-deploy tasks and the manager backup/decrypt of a stack database run free commands; keep those environments on an ungated key until they are named operations.',
|
|
41
|
+
],
|
|
42
|
+
data: { gatePath: GATE_PATH },
|
|
43
|
+
};
|
|
44
|
+
}
|
package/stack/cli.d.mts
CHANGED
|
@@ -14,6 +14,11 @@ export interface Flags {
|
|
|
14
14
|
push?: boolean;
|
|
15
15
|
urgent?: boolean;
|
|
16
16
|
'first-start'?: boolean;
|
|
17
|
+
redeploy?: boolean;
|
|
18
|
+
'data-acknowledged'?: boolean;
|
|
19
|
+
'print-authorized-keys'?: boolean;
|
|
20
|
+
'print-gate'?: boolean;
|
|
21
|
+
'public-key'?: string;
|
|
17
22
|
image?: string[];
|
|
18
23
|
keep?: string[];
|
|
19
24
|
'extra-compose'?: string[];
|
|
@@ -33,8 +38,9 @@ export interface Flags {
|
|
|
33
38
|
'db-service'?: string;
|
|
34
39
|
reason?: string;
|
|
35
40
|
remote?: string;
|
|
41
|
+
'ci-run'?: string;
|
|
36
42
|
}
|
|
37
|
-
export declare const ACTIONS: readonly ["prepare", "connect", "disconnect", "build-args", "render", "deploy", "converge", "verify", "rollback", "prune", "backup", "jobs", "task", "ingress", "retire", "build", "accept"];
|
|
43
|
+
export declare const ACTIONS: readonly ["prepare", "connect", "disconnect", "build-args", "render", "deploy", "converge", "verify", "rollback", "prune", "backup", "jobs", "task", "ingress", "retire", "build", "accept", "status", "ci-key"];
|
|
38
44
|
/** One action of `starci app stack`. */
|
|
39
45
|
export type Action = (typeof ACTIONS)[number];
|
|
40
46
|
/** True when `value` names one action of `starci app stack`. */
|