@ultimat3/cli 20.2.1 → 22.0.0
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/CLAUDE.md +84 -1582
- package/README.md +40 -2
- package/package.json +31 -31
- package/src/api-registration.ts +124 -0
- package/src/app-artifacts.ts +39 -0
- package/src/app-boundaries.ts +5 -10
- package/src/app-env.ts +2 -2
- package/src/app-load.ts +37 -5
- package/src/app-openapi.ts +27 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-root.ts +1 -1
- package/src/async-pages.ts +32 -0
- package/src/bin.ts +7 -1
- package/src/boundary-findings.ts +36 -0
- package/src/browser-launcher-fake-html.ts +80 -0
- package/src/browser-launcher-fake.ts +165 -0
- package/src/browser-launcher-port.ts +159 -0
- package/src/browser-launcher.ts +26 -133
- package/src/budgets.ts +56 -13
- package/src/build-errors.ts +54 -0
- package/src/cdp-shot-a11y.ts +77 -0
- package/src/cdp-shot-clock.ts +14 -0
- package/src/cdp-shot-driver.ts +150 -0
- package/src/cdp-shot-element.ts +147 -0
- package/src/cdp-shot-errors.ts +62 -0
- package/src/cdp-shot-keys.ts +152 -0
- package/src/cdp-shot-page.ts +230 -0
- package/src/cdp-shot-watch.ts +241 -0
- package/src/cmd-affected-spec.ts +24 -0
- package/src/cmd-affected.ts +3 -19
- package/src/cmd-build-spec.ts +16 -0
- package/src/cmd-build.ts +46 -20
- package/src/cmd-ci-spec.ts +24 -0
- package/src/cmd-ci.ts +17 -21
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db-spec.ts +99 -0
- package/src/cmd-db.ts +15 -94
- package/src/cmd-deploy-helm.ts +136 -0
- package/src/cmd-deploy-spec.ts +40 -0
- package/src/cmd-deploy.ts +117 -42
- package/src/cmd-dev-spec.ts +25 -0
- package/src/cmd-dev.ts +78 -122
- package/src/cmd-docs-spec.ts +16 -0
- package/src/cmd-docs.ts +2 -11
- package/src/cmd-doctor-spec.ts +21 -0
- package/src/cmd-doctor.ts +4 -18
- package/src/cmd-env-spec.ts +18 -0
- package/src/cmd-env.ts +4 -13
- package/src/cmd-errors-spec.ts +23 -0
- package/src/cmd-errors.ts +3 -17
- package/src/cmd-fix-spec.ts +18 -0
- package/src/cmd-fix.ts +4 -13
- package/src/cmd-generate-spec.ts +31 -0
- package/src/cmd-generate.ts +46 -42
- package/src/cmd-i18n-spec.ts +16 -0
- package/src/cmd-i18n.ts +7 -14
- package/src/cmd-jobs-spec.ts +56 -0
- package/src/cmd-jobs.ts +3 -50
- package/src/cmd-manifest-spec.ts +15 -0
- package/src/cmd-manifest.ts +19 -20
- package/src/cmd-mcp-spec.ts +21 -0
- package/src/cmd-mcp.ts +2 -15
- package/src/cmd-new-spec.ts +33 -0
- package/src/cmd-new.ts +38 -33
- package/src/cmd-policy-spec.ts +13 -0
- package/src/cmd-policy.ts +2 -9
- package/src/cmd-pr-spec.ts +39 -0
- package/src/cmd-pr.ts +5 -65
- package/src/cmd-registries-spec.ts +32 -0
- package/src/cmd-registries.ts +4 -25
- package/src/cmd-routes-spec.ts +12 -0
- package/src/cmd-routes.ts +2 -7
- package/src/cmd-secrets-spec.ts +19 -0
- package/src/cmd-secrets.ts +22 -34
- package/src/cmd-shot-island.ts +12 -13
- package/src/cmd-shot-spec.ts +53 -0
- package/src/cmd-shot.ts +11 -60
- package/src/cmd-tasks-spec.ts +21 -0
- package/src/cmd-tasks.ts +2 -16
- package/src/cmd-test-spec.ts +54 -0
- package/src/cmd-test.ts +4 -49
- package/src/cmd-verify-spec.ts +28 -0
- package/src/cmd-verify.ts +3 -23
- package/src/db-seed.ts +4 -2
- package/src/dev-dashboard.ts +4 -4
- package/src/dev-lock.ts +18 -2
- package/src/dev-port.ts +21 -0
- package/src/dev-route-table.ts +119 -0
- package/src/dispatch.ts +12 -5
- package/src/document-styles.ts +1 -1
- package/src/drift.ts +4 -18
- package/src/error-catalog.ts +16 -9
- package/src/error-codes.ts +33 -28
- package/src/error-pages.ts +19 -7
- package/src/error-unthrown.ts +130 -0
- package/src/errors.ts +22 -29
- package/src/favicon.ts +2 -2
- package/src/fix-imports.ts +1 -1
- package/src/fix-scan.ts +2 -9
- package/src/flag-reads.ts +1 -2
- package/src/foreign-text.ts +36 -0
- package/src/framework-schema.ts +5 -6
- package/src/generate-feature.ts +42 -0
- package/src/generate-files.ts +7 -3
- package/src/generate-grants.ts +83 -0
- package/src/generate-kinds.ts +56 -4
- package/src/i18n-index.ts +59 -10
- package/src/icon-assets.ts +1 -1
- package/src/image-prepare.ts +14 -0
- package/src/import-scan.ts +63 -0
- package/src/index.ts +67 -334
- package/src/invocation-flags.ts +26 -0
- package/src/island-bundle.ts +38 -11
- package/src/island-capture.ts +6 -6
- package/src/island-harness-script.ts +6 -2
- package/src/island-realtime.ts +98 -0
- package/src/island-shot.ts +5 -0
- package/src/island-store.ts +131 -0
- package/src/island-verdict.ts +2 -2
- package/src/job-registration.ts +42 -0
- package/src/jobs-driver.ts +2 -2
- package/src/live-routes.ts +82 -42
- package/src/load-findings.ts +51 -0
- package/src/mcp-db-target.ts +1 -1
- package/src/mcp-errors.ts +30 -26
- package/src/mcp-host.ts +27 -12
- package/src/mcp-ui-diff.ts +27 -0
- package/src/mcp-ui-inspect.ts +4 -4
- package/src/mcp-ui-interact.ts +21 -15
- package/src/mcp-ui.ts +19 -15
- package/src/measure-database.ts +73 -0
- package/src/measure-paths.ts +74 -0
- package/src/measure-scope.ts +74 -0
- package/src/messages.ts +1 -3
- package/src/metrics-endpoint.ts +1 -1
- package/src/otlp-export.ts +1 -1
- package/src/output.ts +6 -0
- package/src/page-sync.ts +54 -0
- package/src/permission-grants.ts +86 -0
- package/src/prerender-out.ts +25 -0
- package/src/prerender.ts +121 -102
- package/src/pwa-artifacts.ts +3 -3
- package/src/realtime-browser-probe-fixture.ts +2 -2
- package/src/reexport-manifest.ts +2 -1
- package/src/registry.ts +80 -56
- package/src/role-realtime.ts +36 -0
- package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
- package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
- package/src/role-start-types.ts +112 -0
- package/src/{dev-roles.ts → role-start.ts} +42 -115
- package/src/{dev-sync.ts → role-sync.ts} +23 -11
- package/src/root-env.ts +67 -0
- package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
- package/src/{dev-services.ts → runtime-bindings.ts} +43 -17
- package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
- package/src/runtime-jobs.ts +87 -0
- package/src/{dev-live-feed.ts → runtime-live-feed.ts} +20 -5
- package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
- package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
- package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
- package/src/runtime-realtime.ts +55 -0
- package/src/{dev-render.ts → runtime-render.ts} +119 -20
- package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
- package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
- package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
- package/src/scaffold-fixture.ts +28 -6
- package/src/scaffold-typecheck.ts +6 -3
- package/src/schema-drift.ts +7 -1
- package/src/script-csp.ts +5 -2
- package/src/secrets-rotation.ts +59 -0
- package/src/serve-boot.ts +192 -0
- package/src/serve-drain.ts +24 -0
- package/src/serve-entry.ts +6 -0
- package/src/serve-env.ts +116 -0
- package/src/serve-types.ts +55 -0
- package/src/serve.ts +44 -338
- package/src/shot-server.ts +2 -2
- package/src/shot-settle.ts +10 -1
- package/src/shot-theme.ts +3 -3
- package/src/shot-verdict.ts +16 -7
- package/src/signal-shred.ts +27 -0
- package/src/solid-loader.ts +26 -2
- package/src/static-report.ts +8 -1
- package/src/sw-artifacts.ts +13 -3
- package/src/sync-url.ts +31 -0
- package/src/templates/action.ts +30 -16
- package/src/templates/entity.ts +12 -7
- package/src/templates/index.ts +1 -1
- package/src/templates/job.ts +10 -7
- package/src/templates/policy.ts +20 -2
- package/src/templates/resource-create.ts +127 -0
- package/src/templates/resource-form-island.ts +76 -30
- package/src/templates/resource.ts +10 -4
- package/src/templates/route.ts +3 -0
- package/src/templates/scaffold-app.ts +4 -1
- package/src/templates/scaffold-auth.ts +3 -1
- package/src/templates/scaffold-container-compose.ts +184 -0
- package/src/templates/scaffold-container.ts +25 -145
- package/src/templates/scaffold-dashboard-example.ts +2 -2
- package/src/templates/scaffold-db-package.ts +16 -3
- package/src/templates/scaffold-demo-org.ts +41 -0
- package/src/templates/scaffold-entries.ts +1 -1
- package/src/templates/scaffold-env.ts +6 -0
- package/src/templates/scaffold-helm-templates.ts +66 -7
- package/src/templates/scaffold-helm.ts +27 -5
- package/src/templates/scaffold-i18n.ts +15 -10
- package/src/templates/scaffold-repo.ts +13 -10
- package/src/templates/scaffold-roles.ts +38 -10
- package/src/templates/slice-foundation.ts +1 -1
- package/src/templates/wrap.ts +4 -1
- package/src/test-passes.ts +2 -1
- package/src/test-workers.ts +26 -0
- package/src/ts-scan.ts +3 -6
- package/src/tsconfig-references.ts +1 -2
- package/src/verify-checks.ts +41 -39
- package/src/verify-e2e.ts +41 -0
- package/src/verify-run.ts +115 -50
- package/src/verify-step.ts +3 -3
- package/src/verify-tests.ts +22 -30
- package/src/verify-typecheck.ts +28 -0
- package/src/web-binding.ts +2 -2
- package/src/worker-bundle.ts +192 -0
- package/src/workspace-graph.ts +10 -33
- package/src/cdp-browser.ts +0 -100
- package/src/cdp-connection.ts +0 -211
- package/src/cdp-e2e-page.ts +0 -209
- package/src/cdp-errors.ts +0 -56
- package/src/cdp-launch.ts +0 -139
- package/src/e2e-dom-fixture.ts +0 -117
- package/src/e2e-driver.ts +0 -97
- package/src/e2e-errors.ts +0 -103
- package/src/e2e-evaluate.ts +0 -156
- package/src/e2e-locator.ts +0 -86
- package/src/e2e-page.ts +0 -150
- package/src/e2e-selection.ts +0 -182
- package/src/measurement-actor.ts +0 -26
- /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// `x build`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-build.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
export const buildSpec: CommandSpec = {
|
|
7
|
+
name: 'build',
|
|
8
|
+
summary: 'build a container image, a single binary, or a prerendered static site',
|
|
9
|
+
usage: 'x build --target docker|binary|static [--tag name] [--out path] [--json]',
|
|
10
|
+
requiresApp: true,
|
|
11
|
+
flags: [
|
|
12
|
+
{ name: 'target', type: 'string', summary: 'docker | binary | static', default: 'docker' },
|
|
13
|
+
{ name: 'tag', type: 'string', summary: 'image tag (docker target)' },
|
|
14
|
+
{ name: 'out', type: 'string', summary: 'output path (binary and static targets)' },
|
|
15
|
+
],
|
|
16
|
+
};
|
package/src/cmd-build.ts
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
// means "anywhere that runs a container or a binary"; nothing here knows the name of a cloud.
|
|
3
3
|
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
|
-
import { join } from 'node:path';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
6
|
import { ERROR_DOCS_URL, frameworkVersion, VERSION_DEFINE } from '@ultimat3/core';
|
|
7
7
|
import { requireAppRoot } from './app-root';
|
|
8
|
+
import { buildSpec } from './cmd-build-spec';
|
|
8
9
|
import { runVerify } from './cmd-verify';
|
|
9
10
|
import type { CliCommand, CommandContext } from './command';
|
|
10
11
|
import { externalArgs } from './compile-externals';
|
|
11
12
|
import { BuildEntryMissingError, UnknownCommandError } from './errors';
|
|
12
13
|
import type { ExecResult } from './exec';
|
|
13
14
|
import { execOutput } from './exec';
|
|
15
|
+
import { prepareImage } from './image-prepare';
|
|
14
16
|
import { msg } from './messages';
|
|
15
17
|
import type { CommandResult } from './output';
|
|
16
18
|
import { flagString } from './parse';
|
|
@@ -56,9 +58,24 @@ export function requireEntry(root: string, target: BuildTarget): string {
|
|
|
56
58
|
return absolute;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* One image for every role; ROLE selects behaviour at start, so there is one artifact to promote.
|
|
63
|
+
* `BUILD_ID` is the manifest's own build id, passed as the build arg the scaffolded Dockerfile
|
|
64
|
+
* declares: without it the image's `BUILD_ID` was empty and every role computed one at boot, so
|
|
65
|
+
* two replicas of one image could disagree about the id their clients are served against.
|
|
66
|
+
*/
|
|
67
|
+
export function dockerArgs(root: string, tag: string, buildId: string): readonly string[] {
|
|
68
|
+
return [
|
|
69
|
+
'docker',
|
|
70
|
+
'build',
|
|
71
|
+
'-f',
|
|
72
|
+
join(root, BUILD_ENTRY.docker),
|
|
73
|
+
'--build-arg',
|
|
74
|
+
`BUILD_ID=${buildId}`,
|
|
75
|
+
'-t',
|
|
76
|
+
tag,
|
|
77
|
+
root,
|
|
78
|
+
];
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
/**
|
|
@@ -93,9 +110,15 @@ export function staticArgs(root: string, out: string): readonly string[] {
|
|
|
93
110
|
|
|
94
111
|
export function argsFor(
|
|
95
112
|
target: BuildTarget,
|
|
96
|
-
paths: {
|
|
113
|
+
paths: {
|
|
114
|
+
readonly root: string;
|
|
115
|
+
readonly tag: string;
|
|
116
|
+
readonly out: string;
|
|
117
|
+
/** The docker target's `BUILD_ID` build arg — `appManifest(root)`'s own. */
|
|
118
|
+
readonly buildId?: string;
|
|
119
|
+
},
|
|
97
120
|
): readonly string[] {
|
|
98
|
-
if (target === 'docker') return dockerArgs(paths.root, paths.tag);
|
|
121
|
+
if (target === 'docker') return dockerArgs(paths.root, paths.tag, paths.buildId ?? 'dev');
|
|
99
122
|
if (target === 'binary') return binaryArgs(paths.root, paths.out);
|
|
100
123
|
return staticArgs(paths.root, paths.out);
|
|
101
124
|
}
|
|
@@ -162,17 +185,7 @@ export function buildResult(input: {
|
|
|
162
185
|
}
|
|
163
186
|
|
|
164
187
|
export const buildCommand: CliCommand = {
|
|
165
|
-
spec:
|
|
166
|
-
name: 'build',
|
|
167
|
-
summary: 'build a container image, a single binary, or a prerendered static site',
|
|
168
|
-
usage: 'x build --target docker|binary|static [--tag name] [--out path] [--json]',
|
|
169
|
-
requiresApp: true,
|
|
170
|
-
flags: [
|
|
171
|
-
{ name: 'target', type: 'string', summary: 'docker | binary | static', default: 'docker' },
|
|
172
|
-
{ name: 'tag', type: 'string', summary: 'image tag (docker target)' },
|
|
173
|
-
{ name: 'out', type: 'string', summary: 'output path (binary and static targets)' },
|
|
174
|
-
],
|
|
175
|
-
},
|
|
188
|
+
spec: buildSpec,
|
|
176
189
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
177
190
|
const root = requireAppRoot('build', ctx.cwd).dir;
|
|
178
191
|
const target = readTarget(flagString(ctx.args, 'target'));
|
|
@@ -186,15 +199,28 @@ export const buildCommand: CliCommand = {
|
|
|
186
199
|
const verifySteps = (await import('./cmd-verify')).VERIFY_STEPS.filter((step) =>
|
|
187
200
|
staticSteps.includes(step.name),
|
|
188
201
|
);
|
|
189
|
-
const verifyResult = await runVerify(verifySteps, { root, runner: ctx.runner });
|
|
202
|
+
const verifyResult = await runVerify(verifySteps, { root, runner: ctx.runner, env: ctx.env });
|
|
190
203
|
if (!verifyResult.ok) {
|
|
191
204
|
return preflightResult(verifyResult);
|
|
192
205
|
}
|
|
193
206
|
|
|
207
|
+
// A relative `--out` is a path the caller typed from where they stand, so it resolves against
|
|
208
|
+
// the cwd — against the root it landed somewhere else whenever `x build` ran from `apps/web`.
|
|
209
|
+
const outFlag = flagString(ctx.args, 'out');
|
|
194
210
|
const out =
|
|
195
|
-
|
|
211
|
+
outFlag === undefined
|
|
212
|
+
? join(root, '.x', target === 'static' ? 'static' : 'app')
|
|
213
|
+
: resolve(ctx.cwd, outFlag);
|
|
196
214
|
const tag = flagString(ctx.args, 'tag') ?? 'ultimate-app:dev';
|
|
197
|
-
|
|
215
|
+
// The docker target stamps the manifest's build id into the image, and writes the island
|
|
216
|
+
// chunks the image serves, so a container boot neither re-derives the one nor rebuilds the other.
|
|
217
|
+
const buildId = target === 'docker' ? await prepareImage(root) : undefined;
|
|
218
|
+
const command = argsFor(target, {
|
|
219
|
+
root,
|
|
220
|
+
tag,
|
|
221
|
+
out,
|
|
222
|
+
...(buildId === undefined ? {} : { buildId }),
|
|
223
|
+
});
|
|
198
224
|
// Removed BEFORE the builder runs, so a build that writes no inventory can never be reported
|
|
199
225
|
// with the last one's: a stale emitted list is worse than none, because it reads as this run's.
|
|
200
226
|
if (target === 'static') await removeStaticReport(root);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// `x ci`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-ci.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
/** Log lines kept per failed job. Enough to hold a findings block, short enough to read. */
|
|
7
|
+
export const TAIL_LINES = 40;
|
|
8
|
+
|
|
9
|
+
export const ciSpec: CommandSpec = {
|
|
10
|
+
name: 'ci',
|
|
11
|
+
summary: 'the workflow runs for this branch, and the findings inside the failed steps log',
|
|
12
|
+
usage: 'x ci [--branch <name>] [--run <id>] [--repo owner/name] [--tail <n>] [--full] [--json]',
|
|
13
|
+
flags: [
|
|
14
|
+
{ name: 'repo', type: 'string', summary: 'owner/name; the checkout own remote by default' },
|
|
15
|
+
{ name: 'branch', type: 'string', summary: 'branch to read runs for; this one by default' },
|
|
16
|
+
{ name: 'run', type: 'string', summary: 'one run id, instead of this branch latest' },
|
|
17
|
+
{
|
|
18
|
+
name: 'tail',
|
|
19
|
+
type: 'string',
|
|
20
|
+
summary: `log lines kept per failed job (default ${TAIL_LINES})`,
|
|
21
|
+
},
|
|
22
|
+
{ name: 'full', type: 'boolean', summary: 'the whole failed-step log, not the tail' },
|
|
23
|
+
],
|
|
24
|
+
};
|
package/src/cmd-ci.ts
CHANGED
|
@@ -9,14 +9,18 @@ import type { CiLogLine } from './ci-log';
|
|
|
9
9
|
import { findingsFrom, jobsInLog, parseLogLines, tailOf } from './ci-log';
|
|
10
10
|
import type { CiJob, CiRun } from './ci-runs';
|
|
11
11
|
import { failedLog, isFailed, isRunning, latestPerWorkflow, listRuns, viewRun } from './ci-runs';
|
|
12
|
+
import { ciSpec, TAIL_LINES } from './cmd-ci-spec';
|
|
12
13
|
import type { CliCommand, CommandContext } from './command';
|
|
13
14
|
import { parseIntFlag } from './flag-number';
|
|
15
|
+
import { commentBlock } from './foreign-text';
|
|
14
16
|
import type { GhRepo } from './gh-target';
|
|
15
17
|
import { currentBranch, resolveRepo } from './gh-target';
|
|
16
18
|
import { msg } from './messages';
|
|
17
19
|
import type { CommandResult, Finding, JsonValue } from './output';
|
|
18
20
|
import { flagBool, flagString } from './parse';
|
|
19
21
|
|
|
22
|
+
export { TAIL_LINES } from './cmd-ci-spec';
|
|
23
|
+
|
|
20
24
|
/**
|
|
21
25
|
* Every catalog key this command renders, declared — `msg()` answers `⟦key⟧` for a key nobody
|
|
22
26
|
* added, which is loud in a terminal and SILENT to a build. `cmd-ci.test.ts` holds this list
|
|
@@ -34,9 +38,6 @@ export const CI_MESSAGE_KEYS = [
|
|
|
34
38
|
'cli.ci.logs.empty',
|
|
35
39
|
] as const;
|
|
36
40
|
|
|
37
|
-
/** Log lines kept per failed job. Enough to hold a findings block, short enough to read. */
|
|
38
|
-
export const TAIL_LINES = 40;
|
|
39
|
-
|
|
40
41
|
/** How far back a branch's run history is read before "the latest run of each workflow". */
|
|
41
42
|
export const RUN_LOOKBACK = 20;
|
|
42
43
|
|
|
@@ -55,22 +56,7 @@ const RUN_FLAG = { name: 'run', command: 'ci', min: 1, example: 'x ci --run 3248
|
|
|
55
56
|
const TAIL_FLAG = { name: 'tail', command: 'ci', min: 1, example: 'x ci --tail 80 --json' };
|
|
56
57
|
|
|
57
58
|
export const ciCommand: CliCommand = {
|
|
58
|
-
spec:
|
|
59
|
-
name: 'ci',
|
|
60
|
-
summary: 'the workflow runs for this branch, and the findings inside the failed steps log',
|
|
61
|
-
usage: 'x ci [--branch <name>] [--run <id>] [--repo owner/name] [--tail <n>] [--full] [--json]',
|
|
62
|
-
flags: [
|
|
63
|
-
{ name: 'repo', type: 'string', summary: 'owner/name; the checkout own remote by default' },
|
|
64
|
-
{ name: 'branch', type: 'string', summary: 'branch to read runs for; this one by default' },
|
|
65
|
-
{ name: 'run', type: 'string', summary: 'one run id, instead of this branch latest' },
|
|
66
|
-
{
|
|
67
|
-
name: 'tail',
|
|
68
|
-
type: 'string',
|
|
69
|
-
summary: `log lines kept per failed job (default ${TAIL_LINES})`,
|
|
70
|
-
},
|
|
71
|
-
{ name: 'full', type: 'boolean', summary: 'the whole failed-step log, not the tail' },
|
|
72
|
-
],
|
|
73
|
-
},
|
|
59
|
+
spec: ciSpec,
|
|
74
60
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
75
61
|
const repo = await resolveRepo(ctx, 'ci', flagString(ctx.args, 'repo'));
|
|
76
62
|
const rawRun = flagString(ctx.args, 'run');
|
|
@@ -142,7 +128,14 @@ async function inspect(
|
|
|
142
128
|
const pool: readonly CiLogLine[] = own.length > 0 ? own : lines;
|
|
143
129
|
const job = failed.find((candidate) => candidate.name === name);
|
|
144
130
|
for (const finding of findingsFrom(pool)) {
|
|
145
|
-
findings.push(
|
|
131
|
+
findings.push({
|
|
132
|
+
...finding,
|
|
133
|
+
at: finding.at ?? name,
|
|
134
|
+
// Fenced and tagged: the log is foreign text, and a `fix:` reconstructed from it is the
|
|
135
|
+
// log author's instruction, not this CLI's. `commentBlock` is `x pr review`'s fence.
|
|
136
|
+
fix: commentBlock(`ci-log ${name}`, [finding.fix]).join('\n'),
|
|
137
|
+
source: 'ci-log',
|
|
138
|
+
});
|
|
146
139
|
}
|
|
147
140
|
failures.push({
|
|
148
141
|
job: name,
|
|
@@ -229,7 +222,8 @@ function runLines(entry: RunReport): readonly string[] {
|
|
|
229
222
|
continue;
|
|
230
223
|
}
|
|
231
224
|
out.push(msg('cli.ci.tail', { job: failure.job }));
|
|
232
|
-
for (const line of
|
|
225
|
+
for (const line of commentBlock(`ci-log ${failure.job}`, failure.tail))
|
|
226
|
+
out.push(` | ${line}`);
|
|
233
227
|
}
|
|
234
228
|
const jobs = entry.jobs;
|
|
235
229
|
if (jobs !== undefined && jobs.length > failures.length) {
|
|
@@ -266,6 +260,8 @@ function runJson(entry: RunReport): JsonValue {
|
|
|
266
260
|
job: failure.job,
|
|
267
261
|
url: failure.url,
|
|
268
262
|
failedSteps: [...failure.failedSteps],
|
|
263
|
+
// Raw lines for a machine, labelled as foreign: the log's author wrote every one.
|
|
264
|
+
tailSource: 'ci-log',
|
|
269
265
|
tail: [...failure.tail],
|
|
270
266
|
})),
|
|
271
267
|
}),
|
package/src/cmd-db-branch.ts
CHANGED
|
@@ -23,12 +23,12 @@ import {
|
|
|
23
23
|
previewUrl,
|
|
24
24
|
} from './db-branch';
|
|
25
25
|
import { stepFinding } from './db-finding';
|
|
26
|
-
import type { DevServices } from './dev-services';
|
|
27
|
-
import { resolveServices } from './dev-services';
|
|
28
26
|
import { MissingPositionalError, UnknownCommandError } from './errors';
|
|
29
27
|
import { msg } from './messages';
|
|
30
28
|
import type { CommandResult, Finding } from './output';
|
|
31
29
|
import { flagString } from './parse';
|
|
30
|
+
import type { DevServices } from './runtime-bindings';
|
|
31
|
+
import { resolveServices } from './runtime-bindings';
|
|
32
32
|
import { portFromEnv } from './serve';
|
|
33
33
|
import { renderTable } from './table';
|
|
34
34
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// `x db`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-db.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import { BRANCH_SUBCOMMANDS } from './db-branch';
|
|
5
|
+
import type { CommandSpec } from './parse';
|
|
6
|
+
|
|
7
|
+
export const DB_SUBCOMMANDS = [
|
|
8
|
+
'gen',
|
|
9
|
+
'migrate',
|
|
10
|
+
'reset',
|
|
11
|
+
'seed',
|
|
12
|
+
'studio',
|
|
13
|
+
'branch',
|
|
14
|
+
'backfill',
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export const dbSpec: CommandSpec = {
|
|
18
|
+
name: 'db',
|
|
19
|
+
summary: 'gen, migrate, reset, seed, studio, branch, backfill',
|
|
20
|
+
usage:
|
|
21
|
+
'x db gen "add publish_at" | migrate | reset | seed [<name>] [--tier reference|dev] [--dry-run] | studio | branch ls | branch create <name> | branch drop <name> | backfill [<name>|--all] [--write] [--force] | backfill --pending | backfill --list [--name n] [--status s] [--limit n]',
|
|
22
|
+
requiresApp: true,
|
|
23
|
+
subcommands: DB_SUBCOMMANDS,
|
|
24
|
+
// Declared from the constant `runBranchCommand` validates against, never a second literal: it
|
|
25
|
+
// is what lets the `errors` step resolve `x db branch ls` — a fix line three shipped errors
|
|
26
|
+
// hand out, which read `ls` as a branch name and cloned a database until 1.2.x.
|
|
27
|
+
subcommandPositionals: { branch: BRANCH_SUBCOMMANDS },
|
|
28
|
+
// Each flag whose summary begins `<subcommand>:` declares that scope, and the parser refuses
|
|
29
|
+
// it anywhere else: `x db gen --dry-run` used to parse, reach `runGen` and WRITE the
|
|
30
|
+
// migration. `cmd-db.test.ts` pins summary and scope to the same fact.
|
|
31
|
+
flags: [
|
|
32
|
+
{
|
|
33
|
+
name: 'name',
|
|
34
|
+
type: 'string',
|
|
35
|
+
summary: 'migration, branch or seed name, or backfill to filter',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'tier',
|
|
39
|
+
type: 'string',
|
|
40
|
+
summary: 'seed: which tier to run — reference or dev; also ULTIMATE_SEED_TIER',
|
|
41
|
+
subcommands: ['seed'],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'dry-run',
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
summary: 'seed: report what each seed would write, and write nothing',
|
|
47
|
+
subcommands: ['seed'],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'list',
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
summary: 'backfill: print the x_backfills ledger',
|
|
53
|
+
subcommands: ['backfill'],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'pending',
|
|
57
|
+
type: 'boolean',
|
|
58
|
+
summary: 'backfill: declared minus completed; non-zero exit when anything is unswept',
|
|
59
|
+
subcommands: ['backfill'],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'all',
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
summary: 'backfill: every pending sweep, isolated per name',
|
|
65
|
+
subcommands: ['backfill'],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'write',
|
|
69
|
+
type: 'boolean',
|
|
70
|
+
summary: 'backfill: enqueue the pass; dry run without it',
|
|
71
|
+
subcommands: ['backfill'],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'force',
|
|
75
|
+
type: 'boolean',
|
|
76
|
+
summary: 'backfill: sweep a name the ledger records as completed, as a NEW ledger row',
|
|
77
|
+
subcommands: ['backfill'],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'status',
|
|
81
|
+
type: 'string',
|
|
82
|
+
summary: 'backfill: filter by running, completed or failed',
|
|
83
|
+
subcommands: ['backfill'],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'limit',
|
|
87
|
+
type: 'string',
|
|
88
|
+
summary: 'backfill: max ledger rows to return',
|
|
89
|
+
subcommands: ['backfill'],
|
|
90
|
+
},
|
|
91
|
+
// Declared because `X_MIGRATION_IRREVERSIBLE`'s own fix line names it. A `fix:` is copied
|
|
92
|
+
// and run verbatim, so a flag the parser refuses would make the error unfollowable.
|
|
93
|
+
{
|
|
94
|
+
name: 'allow-destructive',
|
|
95
|
+
type: 'boolean',
|
|
96
|
+
summary: 'let x db gen emit a drop whose down cannot restore the rows',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
package/src/cmd-db.ts
CHANGED
|
@@ -17,9 +17,9 @@ import { postgresDriver } from '@ultimat3/entity';
|
|
|
17
17
|
import { requireAppRoot } from './app-root';
|
|
18
18
|
import { runBackfillCommand } from './cmd-db-backfill';
|
|
19
19
|
import { runBranchCommand } from './cmd-db-branch';
|
|
20
|
+
import { DB_SUBCOMMANDS, dbSpec } from './cmd-db-spec';
|
|
20
21
|
import { plannedSubcommand } from './cmd-planned';
|
|
21
22
|
import type { CliCommand, CommandContext } from './command';
|
|
22
|
-
import { BRANCH_SUBCOMMANDS } from './db-branch';
|
|
23
23
|
import { stepFinding } from './db-finding';
|
|
24
24
|
import { generateAppMigration, unrenderedJson, unrenderedLines } from './db-generate';
|
|
25
25
|
import type { SeedPassRow } from './db-seed';
|
|
@@ -32,109 +32,20 @@ import {
|
|
|
32
32
|
seedTotals,
|
|
33
33
|
selectSeeds,
|
|
34
34
|
} from './db-seed';
|
|
35
|
-
import {
|
|
35
|
+
import { DevAlreadyRunningError, liveDevLock } from './dev-lock';
|
|
36
36
|
import { CliNotImplementedError, MissingSubcommandError, UnknownCommandError } from './errors';
|
|
37
37
|
import { withJobDriver } from './jobs-driver';
|
|
38
38
|
import { msg } from './messages';
|
|
39
39
|
import type { CommandResult, Finding } from './output';
|
|
40
40
|
import { findingFrom } from './output';
|
|
41
41
|
import { flagBool, flagString } from './parse';
|
|
42
|
+
import { resolveServices } from './runtime-bindings';
|
|
42
43
|
import { runMigrations } from './serve';
|
|
43
44
|
|
|
44
|
-
export
|
|
45
|
-
'gen',
|
|
46
|
-
'migrate',
|
|
47
|
-
'reset',
|
|
48
|
-
'seed',
|
|
49
|
-
'studio',
|
|
50
|
-
'branch',
|
|
51
|
-
'backfill',
|
|
52
|
-
] as const;
|
|
45
|
+
export { DB_SUBCOMMANDS } from './cmd-db-spec';
|
|
53
46
|
|
|
54
47
|
export const dbCommand: CliCommand = {
|
|
55
|
-
spec:
|
|
56
|
-
name: 'db',
|
|
57
|
-
summary: 'gen, migrate, reset, seed, studio, branch, backfill',
|
|
58
|
-
usage:
|
|
59
|
-
'x db gen "add publish_at" | migrate | reset | seed [<name>] [--tier reference|dev] [--dry-run] | studio | branch ls | branch create <name> | branch drop <name> | backfill [<name>|--all] [--write] [--force] | backfill --pending | backfill --list [--name n] [--status s] [--limit n]',
|
|
60
|
-
requiresApp: true,
|
|
61
|
-
subcommands: DB_SUBCOMMANDS,
|
|
62
|
-
// Declared from the constant `runBranchCommand` validates against, never a second literal: it
|
|
63
|
-
// is what lets the `errors` step resolve `x db branch ls` — a fix line three shipped errors
|
|
64
|
-
// hand out, which read `ls` as a branch name and cloned a database until 1.2.x.
|
|
65
|
-
subcommandPositionals: { branch: BRANCH_SUBCOMMANDS },
|
|
66
|
-
// Each flag whose summary begins `<subcommand>:` declares that scope, and the parser refuses
|
|
67
|
-
// it anywhere else: `x db gen --dry-run` used to parse, reach `runGen` and WRITE the
|
|
68
|
-
// migration. `cmd-db.test.ts` pins summary and scope to the same fact.
|
|
69
|
-
flags: [
|
|
70
|
-
{
|
|
71
|
-
name: 'name',
|
|
72
|
-
type: 'string',
|
|
73
|
-
summary: 'migration, branch or seed name, or backfill to filter',
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
name: 'tier',
|
|
77
|
-
type: 'string',
|
|
78
|
-
summary: 'seed: which tier to run — reference or dev; also ULTIMATE_SEED_TIER',
|
|
79
|
-
subcommands: ['seed'],
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
name: 'dry-run',
|
|
83
|
-
type: 'boolean',
|
|
84
|
-
summary: 'seed: report what each seed would write, and write nothing',
|
|
85
|
-
subcommands: ['seed'],
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
name: 'list',
|
|
89
|
-
type: 'boolean',
|
|
90
|
-
summary: 'backfill: print the x_backfills ledger',
|
|
91
|
-
subcommands: ['backfill'],
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
name: 'pending',
|
|
95
|
-
type: 'boolean',
|
|
96
|
-
summary: 'backfill: declared minus completed; non-zero exit when anything is unswept',
|
|
97
|
-
subcommands: ['backfill'],
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
name: 'all',
|
|
101
|
-
type: 'boolean',
|
|
102
|
-
summary: 'backfill: every pending sweep, isolated per name',
|
|
103
|
-
subcommands: ['backfill'],
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
name: 'write',
|
|
107
|
-
type: 'boolean',
|
|
108
|
-
summary: 'backfill: enqueue the pass; dry run without it',
|
|
109
|
-
subcommands: ['backfill'],
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
name: 'force',
|
|
113
|
-
type: 'boolean',
|
|
114
|
-
summary: 'backfill: sweep a name the ledger records as completed, as a NEW ledger row',
|
|
115
|
-
subcommands: ['backfill'],
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
name: 'status',
|
|
119
|
-
type: 'string',
|
|
120
|
-
summary: 'backfill: filter by running, completed or failed',
|
|
121
|
-
subcommands: ['backfill'],
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
name: 'limit',
|
|
125
|
-
type: 'string',
|
|
126
|
-
summary: 'backfill: max ledger rows to return',
|
|
127
|
-
subcommands: ['backfill'],
|
|
128
|
-
},
|
|
129
|
-
// Declared because `X_MIGRATION_IRREVERSIBLE`'s own fix line names it. A `fix:` is copied
|
|
130
|
-
// and run verbatim, so a flag the parser refuses would make the error unfollowable.
|
|
131
|
-
{
|
|
132
|
-
name: 'allow-destructive',
|
|
133
|
-
type: 'boolean',
|
|
134
|
-
summary: 'let x db gen emit a drop whose down cannot restore the rows',
|
|
135
|
-
},
|
|
136
|
-
],
|
|
137
|
-
},
|
|
48
|
+
spec: dbSpec,
|
|
138
49
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
139
50
|
const root = requireAppRoot('db', ctx.cwd).dir;
|
|
140
51
|
// No default, and no `?? 'migrate'` here either: `gen` writes a migration file and `reset`
|
|
@@ -299,6 +210,16 @@ async function runReset(ctx: CommandContext, root: string): Promise<CommandResul
|
|
|
299
210
|
fix: 'drop and recreate the database yourself, then run: x db migrate',
|
|
300
211
|
});
|
|
301
212
|
}
|
|
213
|
+
// A running `x dev` has this directory open: deleting it under a live embedded Postgres is data
|
|
214
|
+
// loss mid-write, not a reset. The dev lock is the one fact that says someone holds it.
|
|
215
|
+
const holder = liveDevLock(services.stateDir);
|
|
216
|
+
if (holder !== undefined) {
|
|
217
|
+
throw new DevAlreadyRunningError({
|
|
218
|
+
lock: holder,
|
|
219
|
+
stateDir: services.stateDir,
|
|
220
|
+
embeddedDb: true,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
302
223
|
await rm(join(services.stateDir, 'pgdata'), { recursive: true, force: true });
|
|
303
224
|
return runMigrate(ctx, root, msg('cli.db.reset.done'));
|
|
304
225
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// The helm half of `x deploy`: which release, in which namespace, waited on for how long, and what
|
|
2
|
+
// the rollout reported. Split from cmd-deploy.ts, which owns the plan and the compose method.
|
|
3
|
+
//
|
|
4
|
+
// `helm upgrade --install` with no `--wait` exits 0 the moment the API server ACCEPTS the objects,
|
|
5
|
+
// so a deploy whose pods never became ready reported success; and helm's default 5m timeout failed
|
|
6
|
+
// an upgrade whose migrate hook was still applying a long migration, leaving the Job running.
|
|
7
|
+
|
|
8
|
+
// why: Bun exposes no path-join primitive; the chart path is handed to helm as one joined string.
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { UltimateError } from '@ultimat3/core';
|
|
11
|
+
import { APP_CONFIG_EXPORT } from './app-auth';
|
|
12
|
+
import { APP_CONFIG_FILE } from './app-root';
|
|
13
|
+
import { BadFlagError } from './errors';
|
|
14
|
+
|
|
15
|
+
/** Long enough for a migration a pre-upgrade hook must finish; helm's own 5m default is not. */
|
|
16
|
+
export const HELM_DEFAULT_TIMEOUT = '15m';
|
|
17
|
+
|
|
18
|
+
/** What a helm deploy is aimed at, resolved once from the flags and `app.config.ts`. */
|
|
19
|
+
export interface HelmTarget {
|
|
20
|
+
readonly release: string;
|
|
21
|
+
readonly namespace: string | undefined;
|
|
22
|
+
readonly timeout: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Helm's own `time.ParseDuration` grammar, positive units only: `15m`, `900s`, `1h30m`. */
|
|
26
|
+
const DURATION = /^(?:\d+(?:h|m|s|ms))+$/;
|
|
27
|
+
/** A namespace and a release are both DNS-1123 labels; helm caps a release at 53 characters. */
|
|
28
|
+
const LABEL = /^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?$/;
|
|
29
|
+
const RELEASE_MAX = 53;
|
|
30
|
+
|
|
31
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
32
|
+
typeof value === 'object' && value !== null;
|
|
33
|
+
|
|
34
|
+
/** `--timeout`, screened: a value helm would reject is refused before anything is spawned. */
|
|
35
|
+
export function readHelmTimeout(raw: string | undefined): string {
|
|
36
|
+
if (raw === undefined) return HELM_DEFAULT_TIMEOUT;
|
|
37
|
+
if (DURATION.test(raw) && /[1-9]/.test(raw)) return raw;
|
|
38
|
+
throw new BadFlagError({
|
|
39
|
+
flag: 'timeout',
|
|
40
|
+
command: 'deploy',
|
|
41
|
+
reason: 'it is not a positive helm duration such as 15m, 900s or 1h30m',
|
|
42
|
+
fix: 'x deploy --method helm --timeout 15m --json',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** `--namespace` and `--release`, each a DNS-1123 label or a refusal naming the flag. */
|
|
47
|
+
export function readLabel(flag: 'namespace' | 'release', raw: string): string {
|
|
48
|
+
const max = flag === 'release' ? RELEASE_MAX : 63;
|
|
49
|
+
if (raw.length <= max && LABEL.test(raw)) return raw;
|
|
50
|
+
throw new BadFlagError({
|
|
51
|
+
flag,
|
|
52
|
+
command: 'deploy',
|
|
53
|
+
reason: `it must be a lowercase DNS-1123 label of at most ${max} characters`,
|
|
54
|
+
fix:
|
|
55
|
+
flag === 'release'
|
|
56
|
+
? 'x deploy --method helm --release my-app --json'
|
|
57
|
+
: 'x deploy --method helm --namespace my-namespace --json',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The release name: `--release`, else `app.config.ts`'s `name`. It was the literal `app` for every
|
|
63
|
+
* app, so two apps deployed to one namespace were one release and the second upgrade replaced the
|
|
64
|
+
* first. `--release` exists for a release that already carries another name — `app`, for every
|
|
65
|
+
* cluster an earlier version deployed to — because a release name is cluster state the framework
|
|
66
|
+
* does not own and cannot rename.
|
|
67
|
+
*/
|
|
68
|
+
export async function readReleaseName(root: string, flag: string | undefined): Promise<string> {
|
|
69
|
+
if (flag !== undefined) return readLabel('release', flag);
|
|
70
|
+
const module = (await import(join(root, APP_CONFIG_FILE))) as Record<string, unknown>;
|
|
71
|
+
const config = module[APP_CONFIG_EXPORT];
|
|
72
|
+
const name = isRecord(config) ? config['name'] : undefined;
|
|
73
|
+
if (typeof name === 'string' && name.length <= RELEASE_MAX && LABEL.test(name)) return name;
|
|
74
|
+
throw new UltimateError({
|
|
75
|
+
code: 'X_CONFIG_INVALID',
|
|
76
|
+
cause:
|
|
77
|
+
typeof name === 'string'
|
|
78
|
+
? `app.config.ts names the app "${name.slice(0, 80)}", which is not a helm release name (a DNS-1123 label of at most ${RELEASE_MAX} characters)`
|
|
79
|
+
: 'app.config.ts exports no config.name, and x deploy --method helm names the release after it',
|
|
80
|
+
fix: 'name the release explicitly: x deploy --method helm --release my-app --json',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The one `helm upgrade`, waited on: `--wait` for the rollout, `--output json` for the verdict. */
|
|
85
|
+
export function helmUpgradeArgs(
|
|
86
|
+
root: string,
|
|
87
|
+
target: HelmTarget,
|
|
88
|
+
imageOverrides: readonly string[],
|
|
89
|
+
): readonly string[] {
|
|
90
|
+
return [
|
|
91
|
+
'helm',
|
|
92
|
+
'upgrade',
|
|
93
|
+
'--install',
|
|
94
|
+
target.release,
|
|
95
|
+
join(root, 'docker', 'helm'),
|
|
96
|
+
...(target.namespace === undefined ? [] : ['--namespace', target.namespace]),
|
|
97
|
+
'--wait',
|
|
98
|
+
'--timeout',
|
|
99
|
+
target.timeout,
|
|
100
|
+
'--output',
|
|
101
|
+
'json',
|
|
102
|
+
...imageOverrides,
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** What `--json` reports about the rollout helm waited on. */
|
|
107
|
+
export interface Rollout {
|
|
108
|
+
readonly release: string;
|
|
109
|
+
readonly namespace: string | null;
|
|
110
|
+
readonly revision: number | null;
|
|
111
|
+
readonly status: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* helm's `--output json` release record, read structurally. `status` is helm's own word —
|
|
116
|
+
* `deployed`, `failed`, `pending-upgrade` — and `unknown` only when helm printed no record at all,
|
|
117
|
+
* never a guess dressed as one.
|
|
118
|
+
*/
|
|
119
|
+
export function readRollout(target: HelmTarget, stdout: string): Rollout {
|
|
120
|
+
let parsed: unknown;
|
|
121
|
+
try {
|
|
122
|
+
parsed = JSON.parse(stdout);
|
|
123
|
+
} catch {
|
|
124
|
+
parsed = undefined;
|
|
125
|
+
}
|
|
126
|
+
const info = isRecord(parsed) ? parsed['info'] : undefined;
|
|
127
|
+
const status = isRecord(info) ? info['status'] : undefined;
|
|
128
|
+
const revision = isRecord(parsed) ? parsed['version'] : undefined;
|
|
129
|
+
const namespace = isRecord(parsed) ? parsed['namespace'] : undefined;
|
|
130
|
+
return {
|
|
131
|
+
release: target.release,
|
|
132
|
+
namespace: typeof namespace === 'string' ? namespace : (target.namespace ?? null),
|
|
133
|
+
revision: typeof revision === 'number' ? revision : null,
|
|
134
|
+
status: typeof status === 'string' ? status : 'unknown',
|
|
135
|
+
};
|
|
136
|
+
}
|