@hyperdrive.bot/cli 1.0.13 → 1.0.16
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/README.md +1495 -474
- package/dist/commands/deploy.d.ts +18 -0
- package/dist/commands/deploy.js +239 -0
- package/dist/commands/deployment/create.js +10 -2
- package/dist/commands/domain/{switch.d.ts → set-production.d.ts} +1 -1
- package/dist/commands/domain/set-production.js +27 -0
- package/dist/commands/git/list-open-prs.d.ts +12 -0
- package/dist/commands/git/list-open-prs.js +87 -0
- package/dist/commands/hook/add.d.ts +22 -0
- package/dist/commands/hook/add.js +299 -0
- package/dist/commands/hook/list.d.ts +11 -0
- package/dist/commands/hook/list.js +111 -0
- package/dist/commands/hook/logs.d.ts +13 -0
- package/dist/commands/hook/logs.js +124 -0
- package/dist/commands/hook/remove.d.ts +12 -0
- package/dist/commands/hook/remove.js +115 -0
- package/dist/commands/hook/toggle.d.ts +12 -0
- package/dist/commands/hook/toggle.js +125 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +49 -9
- package/dist/commands/module/bindings.d.ts +14 -0
- package/dist/commands/module/bindings.js +125 -0
- package/dist/commands/module/create.d.ts +3 -0
- package/dist/commands/module/create.js +156 -78
- package/dist/commands/module/list.d.ts +1 -0
- package/dist/commands/module/list.js +22 -1
- package/dist/commands/module/sync.d.ts +29 -0
- package/dist/commands/module/sync.js +409 -0
- package/dist/commands/module/unlink.d.ts +11 -0
- package/dist/commands/module/unlink.js +77 -0
- package/dist/commands/module/update.d.ts +10 -0
- package/dist/commands/module/update.js +168 -5
- package/dist/commands/network/discover.d.ts +12 -0
- package/dist/commands/network/discover.js +210 -0
- package/dist/commands/network/get.d.ts +13 -0
- package/dist/commands/network/get.js +90 -0
- package/dist/commands/{auth/logout.d.ts → network/list.d.ts} +2 -9
- package/dist/commands/network/list.js +71 -0
- package/dist/commands/network/register.d.ts +16 -0
- package/dist/commands/network/register.js +144 -0
- package/dist/commands/parameter/sync.d.ts +13 -0
- package/dist/commands/parameter/sync.js +69 -1
- package/dist/commands/project/sync.d.ts +5 -11
- package/dist/commands/project/sync.js +12 -381
- package/dist/commands/seed.d.ts +93 -0
- package/dist/commands/seed.js +324 -0
- package/dist/commands/service/backup.d.ts +17 -0
- package/dist/commands/service/backup.js +156 -0
- package/dist/commands/service/backups.d.ts +14 -0
- package/dist/commands/service/backups.js +110 -0
- package/dist/commands/service/bind.d.ts +16 -0
- package/dist/commands/service/bind.js +106 -0
- package/dist/commands/service/bindings.d.ts +13 -0
- package/dist/commands/service/bindings.js +78 -0
- package/dist/commands/service/clone.d.ts +19 -0
- package/dist/commands/service/clone.js +153 -0
- package/dist/commands/service/create.d.ts +16 -0
- package/dist/commands/service/create.js +212 -0
- package/dist/commands/service/get.d.ts +13 -0
- package/dist/commands/service/get.js +97 -0
- package/dist/commands/service/list.d.ts +12 -0
- package/dist/commands/service/list.js +86 -0
- package/dist/commands/service/register.d.ts +21 -0
- package/dist/commands/service/register.js +215 -0
- package/dist/commands/service/restore.d.ts +19 -0
- package/dist/commands/service/restore.js +158 -0
- package/dist/commands/service/seed.d.ts +17 -0
- package/dist/commands/service/seed.js +173 -0
- package/dist/commands/service/templates.d.ts +10 -0
- package/dist/commands/service/templates.js +66 -0
- package/dist/commands/service/unbind.d.ts +15 -0
- package/dist/commands/service/unbind.js +74 -0
- package/dist/commands/stage/create.d.ts +23 -0
- package/dist/commands/stage/create.js +145 -6
- package/dist/commands/stage/delete.d.ts +11 -0
- package/dist/commands/stage/delete.js +85 -0
- package/dist/commands/stage/deploy.d.ts +34 -0
- package/dist/commands/stage/deploy.js +294 -0
- package/dist/commands/stage/ensure-branches.d.ts +23 -0
- package/dist/commands/stage/ensure-branches.js +101 -0
- package/dist/commands/stage/list.js +4 -0
- package/dist/commands/stage/status.d.ts +14 -0
- package/dist/commands/stage/status.js +100 -0
- package/dist/commands/{jira → tracker}/connect.js +32 -23
- package/dist/commands/tracker/hook/add.d.ts +25 -0
- package/dist/commands/tracker/hook/add.js +284 -0
- package/dist/commands/{jira → tracker}/hook/list.js +20 -11
- package/dist/commands/{jira/hook/add.d.ts → tracker/hook/logs.d.ts} +2 -3
- package/dist/commands/tracker/hook/logs.js +126 -0
- package/dist/commands/{jira → tracker}/hook/remove.js +9 -8
- package/dist/commands/{jira → tracker}/hook/toggle.js +14 -12
- package/dist/commands/tracker/project/init.d.ts +17 -0
- package/dist/commands/tracker/project/init.js +178 -0
- package/dist/commands/tracker/project/link-module.d.ts +17 -0
- package/dist/commands/tracker/project/link-module.js +287 -0
- package/dist/commands/tracker/project/list-modules.d.ts +11 -0
- package/dist/commands/tracker/project/list-modules.js +117 -0
- package/dist/commands/tracker/project/list.d.ts +10 -0
- package/dist/commands/tracker/project/list.js +90 -0
- package/dist/commands/tracker/project/status.d.ts +13 -0
- package/dist/commands/tracker/project/status.js +168 -0
- package/dist/commands/tracker/project/unlink-module.d.ts +13 -0
- package/dist/commands/tracker/project/unlink-module.js +251 -0
- package/dist/commands/{jira → tracker}/status.js +3 -3
- package/dist/lib/ensure-branches.d.ts +53 -0
- package/dist/lib/ensure-branches.js +149 -0
- package/dist/lib/git-providers/github.d.ts +16 -0
- package/dist/lib/git-providers/github.js +157 -0
- package/dist/lib/git-providers/gitlab.d.ts +16 -0
- package/dist/lib/git-providers/gitlab.js +148 -0
- package/dist/lib/git-providers/index.d.ts +67 -0
- package/dist/lib/git-providers/index.js +39 -0
- package/dist/lib/lambda-warmer.d.ts +106 -0
- package/dist/lib/lambda-warmer.js +189 -0
- package/dist/services/hyperdrive-sigv4.d.ts +359 -5
- package/dist/services/hyperdrive-sigv4.js +177 -12
- package/dist/utils/hook-flow.d.ts +60 -3
- package/dist/utils/hook-flow.js +437 -2
- package/dist/utils/hook-normalize.d.ts +6 -0
- package/dist/utils/hook-normalize.js +33 -0
- package/dist/utils/lifecycle-poller.d.ts +32 -0
- package/dist/utils/lifecycle-poller.js +72 -0
- package/dist/utils/retry.d.ts +43 -0
- package/dist/utils/retry.js +88 -0
- package/dist/utils/summary-display.js +1 -1
- package/dist/utils/tracker-project-flow.d.ts +84 -0
- package/dist/utils/tracker-project-flow.js +564 -0
- package/package.json +35 -7
- package/dist/commands/auth/login.d.ts +0 -16
- package/dist/commands/auth/login.js +0 -179
- package/dist/commands/auth/logout.js +0 -116
- package/dist/commands/auth/refresh.d.ts +0 -6
- package/dist/commands/auth/refresh.js +0 -66
- package/dist/commands/auth/status.d.ts +0 -6
- package/dist/commands/auth/status.js +0 -63
- package/dist/commands/config/get.d.ts +0 -9
- package/dist/commands/config/get.js +0 -37
- package/dist/commands/config/set.d.ts +0 -10
- package/dist/commands/config/set.js +0 -48
- package/dist/commands/config/show.d.ts +0 -6
- package/dist/commands/config/show.js +0 -10
- package/dist/commands/domain/current.d.ts +0 -6
- package/dist/commands/domain/current.js +0 -18
- package/dist/commands/domain/list.d.ts +0 -6
- package/dist/commands/domain/list.js +0 -42
- package/dist/commands/domain/switch.js +0 -40
- package/dist/commands/jira/hook/add.js +0 -147
- package/dist/services/tenant-service.d.ts +0 -127
- package/dist/services/tenant-service.js +0 -396
- package/dist/utils/auth-flow.d.ts +0 -147
- package/dist/utils/auth-flow.js +0 -479
- package/oclif.manifest.json +0 -3519
- /package/dist/commands/{jira → tracker}/connect.d.ts +0 -0
- /package/dist/commands/{jira → tracker}/hook/list.d.ts +0 -0
- /package/dist/commands/{jira → tracker}/hook/remove.d.ts +0 -0
- /package/dist/commands/{jira → tracker}/hook/toggle.d.ts +0 -0
- /package/dist/commands/{jira → tracker}/status.d.ts +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
|
|
4
|
+
export default class StageDelete extends Command {
|
|
5
|
+
static description = 'Delete a stage (cleanup runs asynchronously via SQS)';
|
|
6
|
+
static examples = [
|
|
7
|
+
'<%= config.bin %> <%= command.id %> --name stress-test-19 -d ds.hyperdrivebot.dev',
|
|
8
|
+
];
|
|
9
|
+
static flags = {
|
|
10
|
+
domain: Flags.string({
|
|
11
|
+
char: 'd',
|
|
12
|
+
description: 'Tenant domain',
|
|
13
|
+
}),
|
|
14
|
+
force: Flags.boolean({
|
|
15
|
+
char: 'f',
|
|
16
|
+
description: 'Ignored (kept for backward compatibility — cleanup is always async now)',
|
|
17
|
+
default: false,
|
|
18
|
+
hidden: true,
|
|
19
|
+
}),
|
|
20
|
+
name: Flags.string({
|
|
21
|
+
char: 'n',
|
|
22
|
+
description: 'Stage name to delete',
|
|
23
|
+
required: true,
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
async run() {
|
|
27
|
+
const { flags } = await this.parse(StageDelete);
|
|
28
|
+
const service = new HyperdriveSigV4Service(flags.domain);
|
|
29
|
+
this.log(chalk.red(`\u{1F5D1}\u{FE0F} Deleting stage "${flags.name}"...`));
|
|
30
|
+
this.log(chalk.yellow(' This will destroy all CloudFormation stacks for this stage.'));
|
|
31
|
+
if (flags.force) {
|
|
32
|
+
this.log(chalk.yellow(' --force: Skipping domain teardown.'));
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const result = await service.stageDelete(flags.name, flags.force);
|
|
36
|
+
this.log(chalk.green('\n\u2705 Stage deleted from DynamoDB'));
|
|
37
|
+
if ('cleanup' in result && result.cleanup === 'queued') {
|
|
38
|
+
// New async path — cleanup runs via SQS worker
|
|
39
|
+
this.log(chalk.blue('\nCleanup queued \u2014 resources will be cleaned up asynchronously.'));
|
|
40
|
+
this.log(chalk.dim(' CF stacks, log groups, domain resources, and access tuples will be removed by the cleanup worker.'));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// Old sync path (backward compat during rollout)
|
|
44
|
+
const syncResult = result;
|
|
45
|
+
if (syncResult.domainTeardown === 'skipped') {
|
|
46
|
+
this.log(chalk.yellow('\n\u26A0\uFE0F Domain teardown was skipped (--force). CloudFront/Route53 resources may remain.'));
|
|
47
|
+
}
|
|
48
|
+
if (syncResult.stacks.stacksDeleted.length > 0) {
|
|
49
|
+
this.log(chalk.blue(`\n\u{1F4E6} ${syncResult.stacks.stacksDeleted.length} stacks queued for deletion:`));
|
|
50
|
+
for (const stack of syncResult.stacks.stacksDeleted) {
|
|
51
|
+
this.log(chalk.dim(` - ${stack}`));
|
|
52
|
+
}
|
|
53
|
+
this.log(chalk.yellow('\n\u23F3 Stack deletion is async \u2014 may take several minutes to complete.'));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
this.log(chalk.dim('\n No matching CloudFormation stacks found.'));
|
|
57
|
+
}
|
|
58
|
+
if (syncResult.stacks.errors.length > 0) {
|
|
59
|
+
this.log(chalk.red(`\n\u26A0\uFE0F ${syncResult.stacks.errors.length} stack error(s):`));
|
|
60
|
+
for (const err of syncResult.stacks.errors) {
|
|
61
|
+
this.log(chalk.red(` - ${err}`));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Log group cleanup results
|
|
65
|
+
if (syncResult.logGroups) {
|
|
66
|
+
if (syncResult.logGroups.deleted > 0) {
|
|
67
|
+
this.log(chalk.blue(`\n\u{1F9F9} ${syncResult.logGroups.deleted} CloudWatch log groups deleted`));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.log(chalk.dim('\n No CloudWatch log groups found to clean up.'));
|
|
71
|
+
}
|
|
72
|
+
if (syncResult.logGroups.errors.length > 0) {
|
|
73
|
+
this.log(chalk.red(`\n\u26A0\uFE0F ${syncResult.logGroups.errors.length} log group error(s):`));
|
|
74
|
+
for (const err of syncResult.logGroups.errors) {
|
|
75
|
+
this.log(chalk.red(` - ${err}`));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
this.error(`Failed to delete stage: ${error.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class StageDeploy extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
stageName: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static flags: {
|
|
8
|
+
module: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
retry: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Single-module surgical deploy
|
|
16
|
+
*/
|
|
17
|
+
private runSingleModuleDeploy;
|
|
18
|
+
/**
|
|
19
|
+
* Retry all failed/pending modules
|
|
20
|
+
*/
|
|
21
|
+
private runRetryDeploy;
|
|
22
|
+
/**
|
|
23
|
+
* Handle API error responses
|
|
24
|
+
*/
|
|
25
|
+
private handleApiError;
|
|
26
|
+
/**
|
|
27
|
+
* Build CloudWatch console URL
|
|
28
|
+
*/
|
|
29
|
+
private buildConsoleUrl;
|
|
30
|
+
/**
|
|
31
|
+
* Stream mission logs in real-time (shared by --retry and --module)
|
|
32
|
+
*/
|
|
33
|
+
private streamMissionLogs;
|
|
34
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
|
|
5
|
+
import { CloudWatchLogTailer } from '../../services/log-tailer.js';
|
|
6
|
+
export default class StageDeploy extends Command {
|
|
7
|
+
static args = {
|
|
8
|
+
stageName: Args.string({ description: 'Stage slug (e.g., ss-11)', required: true }),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Retry or surgically redeploy modules on a per-branch stage';
|
|
11
|
+
static flags = {
|
|
12
|
+
module: Flags.string({
|
|
13
|
+
char: 'm',
|
|
14
|
+
description: 'Surgically redeploy a single module after validating prior-wave dependencies',
|
|
15
|
+
exclusive: ['retry'],
|
|
16
|
+
}),
|
|
17
|
+
retry: Flags.boolean({
|
|
18
|
+
char: 'r',
|
|
19
|
+
default: false,
|
|
20
|
+
description: 'Retry failed and pending modules from the last mission',
|
|
21
|
+
exclusive: ['module'],
|
|
22
|
+
}),
|
|
23
|
+
verbose: Flags.boolean({
|
|
24
|
+
char: 'v',
|
|
25
|
+
default: false,
|
|
26
|
+
description: 'Show verbose log output',
|
|
27
|
+
}),
|
|
28
|
+
yes: Flags.boolean({
|
|
29
|
+
char: 'y',
|
|
30
|
+
default: false,
|
|
31
|
+
description: 'Skip confirmation prompt',
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
async run() {
|
|
35
|
+
const { args, flags } = await this.parse(StageDeploy);
|
|
36
|
+
// Mutual exclusion belt-and-suspenders (oclif handles it, but exit cleanly)
|
|
37
|
+
if (flags.retry && flags.module) {
|
|
38
|
+
this.error('--retry and --module are mutually exclusive', { exit: 2 });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!flags.retry && !flags.module) {
|
|
42
|
+
this.log(chalk.yellow('Please specify a deploy mode: --retry or --module <slug>'));
|
|
43
|
+
this.log(chalk.gray(' Examples:'));
|
|
44
|
+
this.log(chalk.gray(' hd stage deploy ss-11 --retry'));
|
|
45
|
+
this.log(chalk.gray(' hd stage deploy ss-11 --module sign'));
|
|
46
|
+
this.exit(1);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const service = new HyperdriveSigV4Service();
|
|
51
|
+
if (flags.module) {
|
|
52
|
+
await this.runSingleModuleDeploy(service, args.stageName, flags.module, flags.verbose, flags.yes);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
await this.runRetryDeploy(service, args.stageName, flags.verbose, flags.yes);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const err = error;
|
|
60
|
+
// Ignore EEXIT with code 0
|
|
61
|
+
if (err.message?.includes('EEXIT') && err.oclif?.exit === 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Preserve explicit non-zero oclif exits
|
|
65
|
+
if (err.oclif?.exit !== undefined && err.oclif.exit !== 0) {
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
this.handleApiError(error, args.stageName);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Single-module surgical deploy
|
|
73
|
+
*/
|
|
74
|
+
async runSingleModuleDeploy(service, stageName, moduleSlug, verbose, skipPrompt) {
|
|
75
|
+
// 1. Fetch manifest for confirmation
|
|
76
|
+
this.log(chalk.gray('Fetching deploy manifest...'));
|
|
77
|
+
const manifestResponse = await service.stageGetDeployManifest(stageName);
|
|
78
|
+
if (!manifestResponse.manifest) {
|
|
79
|
+
this.log(chalk.yellow(`No deploy manifest found for stage '${stageName}'. Run \`hd stage create\` first.`));
|
|
80
|
+
this.exit(0);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const manifest = manifestResponse.manifest;
|
|
84
|
+
// 2. Find the module and its wave
|
|
85
|
+
let targetWave = -1;
|
|
86
|
+
let found = false;
|
|
87
|
+
for (const wave of manifest.waves) {
|
|
88
|
+
for (const mod of wave.modules) {
|
|
89
|
+
if (mod.moduleSlug === moduleSlug) {
|
|
90
|
+
targetWave = wave.waveIndex;
|
|
91
|
+
found = true;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (found)
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
if (!found) {
|
|
99
|
+
this.log(chalk.red(`Module '${moduleSlug}' not found in manifest for stage ${stageName}`));
|
|
100
|
+
this.exit(2);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// 3. Count deployed modules in prior waves
|
|
104
|
+
const priorDeployedCount = manifest.waves
|
|
105
|
+
.filter(w => w.waveIndex < targetWave)
|
|
106
|
+
.reduce((sum, w) => sum + w.modules.length, 0);
|
|
107
|
+
// 4. Confirmation prompt
|
|
108
|
+
if (!skipPrompt) {
|
|
109
|
+
this.log('');
|
|
110
|
+
this.log(chalk.blue(`Deploying 1 module: ${moduleSlug} (W${targetWave}). Prior-wave deps OK (${priorDeployedCount} modules deployed).`));
|
|
111
|
+
this.log('');
|
|
112
|
+
const { confirm } = await inquirer.prompt([{
|
|
113
|
+
default: true,
|
|
114
|
+
message: 'Continue? [Y/n]',
|
|
115
|
+
name: 'confirm',
|
|
116
|
+
type: 'confirm',
|
|
117
|
+
}]);
|
|
118
|
+
if (!confirm) {
|
|
119
|
+
this.log('Deploy cancelled.');
|
|
120
|
+
this.exit(0);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// 5. Call the deploy API
|
|
125
|
+
this.log(chalk.gray(`Starting single-module deploy for ${moduleSlug}...`));
|
|
126
|
+
const response = await service.stageDeploy({
|
|
127
|
+
mode: 'module',
|
|
128
|
+
moduleSlug,
|
|
129
|
+
stageName,
|
|
130
|
+
});
|
|
131
|
+
// 6. Stream mission logs (reuse same helper as --retry and create --autoLaunch)
|
|
132
|
+
await this.streamMissionLogs(response.logging, response.missionId, verbose, 'module', moduleSlug);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Retry all failed/pending modules
|
|
136
|
+
*/
|
|
137
|
+
async runRetryDeploy(service, stageName, verbose, skipPrompt) {
|
|
138
|
+
// 1. Fetch manifest to show confirmation prompt
|
|
139
|
+
this.log(chalk.gray('Fetching deploy manifest...'));
|
|
140
|
+
const manifestResponse = await service.stageGetDeployManifest(stageName);
|
|
141
|
+
if (!manifestResponse.manifest) {
|
|
142
|
+
this.log(chalk.yellow(`No deploy manifest found for stage '${stageName}'. Run \`hd stage create\` first.`));
|
|
143
|
+
this.exit(0);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const manifest = manifestResponse.manifest;
|
|
147
|
+
// 2. Compute non-deployed modules
|
|
148
|
+
const retryableModules = [];
|
|
149
|
+
for (const wave of manifest.waves) {
|
|
150
|
+
for (const mod of wave.modules) {
|
|
151
|
+
if (mod.status !== 'deployed') {
|
|
152
|
+
retryableModules.push({
|
|
153
|
+
moduleSlug: mod.moduleSlug,
|
|
154
|
+
status: mod.status,
|
|
155
|
+
waveIndex: wave.waveIndex,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (retryableModules.length === 0) {
|
|
161
|
+
this.log(chalk.green('All modules are already deployed. Nothing to retry.'));
|
|
162
|
+
this.exit(0);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// 3. Print retry summary and confirm
|
|
166
|
+
this.log('');
|
|
167
|
+
this.log(chalk.blue(`Retrying ${retryableModules.length} modules:`));
|
|
168
|
+
for (const mod of retryableModules) {
|
|
169
|
+
this.log(chalk.gray(` - ${mod.moduleSlug} (W${mod.waveIndex}, ${mod.status})`));
|
|
170
|
+
}
|
|
171
|
+
if (!skipPrompt) {
|
|
172
|
+
this.log('');
|
|
173
|
+
const { confirm } = await inquirer.prompt([{
|
|
174
|
+
default: true,
|
|
175
|
+
message: 'Continue?',
|
|
176
|
+
name: 'confirm',
|
|
177
|
+
type: 'confirm',
|
|
178
|
+
}]);
|
|
179
|
+
if (!confirm) {
|
|
180
|
+
this.log('Retry cancelled.');
|
|
181
|
+
this.exit(0);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// 4. Call the deploy API
|
|
186
|
+
this.log(chalk.gray('Starting retry...'));
|
|
187
|
+
const response = await service.stageDeploy({
|
|
188
|
+
mode: 'retry',
|
|
189
|
+
stageName,
|
|
190
|
+
});
|
|
191
|
+
// 5. Stream mission logs
|
|
192
|
+
await this.streamMissionLogs(response.logging, response.missionId, verbose, 'retry');
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Handle API error responses
|
|
196
|
+
*/
|
|
197
|
+
handleApiError(error, stageName) {
|
|
198
|
+
const axiosError = error;
|
|
199
|
+
const responseData = axiosError.response?.data;
|
|
200
|
+
// The API returns structured errors as JSON-stringified message
|
|
201
|
+
let errorCode;
|
|
202
|
+
let errorMessage;
|
|
203
|
+
let parsedBody;
|
|
204
|
+
if (responseData?.message) {
|
|
205
|
+
try {
|
|
206
|
+
parsedBody = JSON.parse(responseData.message);
|
|
207
|
+
errorCode = parsedBody?.code;
|
|
208
|
+
errorMessage = parsedBody?.error ?? responseData.message;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
errorMessage = responseData.message;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
errorMessage = responseData?.error ?? axiosError.message ?? 'Unknown error';
|
|
216
|
+
errorCode = responseData?.code;
|
|
217
|
+
parsedBody = responseData;
|
|
218
|
+
}
|
|
219
|
+
// NOTHING_TO_RETRY is not an error — clean exit
|
|
220
|
+
if (errorCode === 'NOTHING_TO_RETRY') {
|
|
221
|
+
this.log(chalk.green(errorMessage));
|
|
222
|
+
this.exit(0);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
// WAVE_DEPS_NOT_MET — format blocking modules
|
|
226
|
+
if (errorCode === 'WAVE_DEPS_NOT_MET' || (parsedBody?.error === 'WAVE_DEPS_NOT_MET')) {
|
|
227
|
+
const targetModule = parsedBody?.targetModule || 'unknown';
|
|
228
|
+
const targetWave = parsedBody?.targetWave ?? '?';
|
|
229
|
+
const blockers = parsedBody?.blockingModules || [];
|
|
230
|
+
const blockerList = blockers.map(b => `${b.slug} (W${b.wave}, ${b.status})`).join(', ');
|
|
231
|
+
this.log(chalk.red(`Cannot deploy ${targetModule} (W${targetWave}) — blocking modules: ${blockerList}. Run 'hd stage deploy ${stageName} --retry' first.`));
|
|
232
|
+
this.exit(2);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
// PREVIOUS_MISSION_NOT_TERMINAL
|
|
236
|
+
if (errorCode === 'PREVIOUS_MISSION_NOT_TERMINAL' || errorCode === 'MISSION_IN_FLIGHT') {
|
|
237
|
+
const missionId = parsedBody?.missionId || 'unknown';
|
|
238
|
+
const state = parsedBody?.state || 'RUNNING';
|
|
239
|
+
this.log(chalk.red(`Previous mission ${missionId} is still ${state}. Wait for it to complete before retrying.`));
|
|
240
|
+
this.exit(2);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// MODULE_NOT_FOUND
|
|
244
|
+
if (errorCode === 'MODULE_NOT_FOUND') {
|
|
245
|
+
this.log(chalk.red(errorMessage));
|
|
246
|
+
this.exit(2);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
this.log(chalk.red(`❌ ${errorMessage}`));
|
|
250
|
+
this.exit(1);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Build CloudWatch console URL
|
|
254
|
+
*/
|
|
255
|
+
buildConsoleUrl(config) {
|
|
256
|
+
const encodedGroup = encodeURIComponent(encodeURIComponent(config.logGroup));
|
|
257
|
+
const encodedStream = encodeURIComponent(encodeURIComponent(config.logStream));
|
|
258
|
+
return `https://${config.region}.console.aws.amazon.com/cloudwatch/home?region=${config.region}#logsV2:log-groups/log-group/${encodedGroup}/log-events/${encodedStream}`;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Stream mission logs in real-time (shared by --retry and --module)
|
|
262
|
+
*/
|
|
263
|
+
async streamMissionLogs(loggingConfig, missionId, verbose, mode, moduleSlug) {
|
|
264
|
+
if (mode === 'module') {
|
|
265
|
+
this.log(chalk.blue(`\n🎯 Single-module deploy started: ${missionId}`));
|
|
266
|
+
this.log(chalk.gray(` Deploying: ${moduleSlug}`));
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
this.log(chalk.blue(`\n🚀 Retry mission started: ${missionId}`));
|
|
270
|
+
this.log(chalk.gray(' Resuming deployment of failed/pending modules...'));
|
|
271
|
+
}
|
|
272
|
+
this.log('');
|
|
273
|
+
const tailer = new CloudWatchLogTailer(loggingConfig, {
|
|
274
|
+
pollInterval: 1000,
|
|
275
|
+
showDebug: false,
|
|
276
|
+
verbose,
|
|
277
|
+
});
|
|
278
|
+
const outcome = await tailer.tail();
|
|
279
|
+
this.log('');
|
|
280
|
+
if (outcome.success) {
|
|
281
|
+
this.log(chalk.green(`✅ ${outcome.message}`));
|
|
282
|
+
this.log(chalk.cyan(` Mission: ${missionId}`));
|
|
283
|
+
const consoleUrl = this.buildConsoleUrl(loggingConfig);
|
|
284
|
+
this.log(chalk.gray(` Logs: ${consoleUrl}`));
|
|
285
|
+
this.exit(0);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
this.log(chalk.red(`❌ ${outcome.message}`));
|
|
289
|
+
const consoleUrl = this.buildConsoleUrl(loggingConfig);
|
|
290
|
+
this.log(chalk.gray(` Full logs: ${consoleUrl}`));
|
|
291
|
+
this.exit(1);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hd stage ensure-branches <stageName>` — ensure the stage branch exists on
|
|
3
|
+
* all linked project remotes, creating it when missing.
|
|
4
|
+
*
|
|
5
|
+
* This is a CLI-side command: by the time `hd stage create --autoLaunch`
|
|
6
|
+
* kicks off the builder pods, branches must already exist on linked frontend
|
|
7
|
+
* remotes. Running this command beforehand is the primary fix for the
|
|
8
|
+
* "sign + tenant-seed blocked because the SS-N branch doesn't exist on the
|
|
9
|
+
* Sign remote" class of mission failures.
|
|
10
|
+
*/
|
|
11
|
+
import { Command } from '@oclif/core';
|
|
12
|
+
export default class StageEnsureBranches extends Command {
|
|
13
|
+
static description: string;
|
|
14
|
+
static args: {
|
|
15
|
+
stageName: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
16
|
+
};
|
|
17
|
+
static flags: {
|
|
18
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
dryRun: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
|
+
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
+
};
|
|
22
|
+
run(): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hd stage ensure-branches <stageName>` — ensure the stage branch exists on
|
|
3
|
+
* all linked project remotes, creating it when missing.
|
|
4
|
+
*
|
|
5
|
+
* This is a CLI-side command: by the time `hd stage create --autoLaunch`
|
|
6
|
+
* kicks off the builder pods, branches must already exist on linked frontend
|
|
7
|
+
* remotes. Running this command beforehand is the primary fix for the
|
|
8
|
+
* "sign + tenant-seed blocked because the SS-N branch doesn't exist on the
|
|
9
|
+
* Sign remote" class of mission failures.
|
|
10
|
+
*/
|
|
11
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { ensureBranches } from '../../lib/ensure-branches.js';
|
|
14
|
+
import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
|
|
15
|
+
export default class StageEnsureBranches extends Command {
|
|
16
|
+
static description = 'Ensure the stage branch exists on all linked project remotes (creates if missing)';
|
|
17
|
+
static args = {
|
|
18
|
+
stageName: Args.string({ description: 'Name of the stage', required: true }),
|
|
19
|
+
};
|
|
20
|
+
static flags = {
|
|
21
|
+
verbose: Flags.boolean({
|
|
22
|
+
char: 'v',
|
|
23
|
+
default: false,
|
|
24
|
+
description: 'Print per-project status',
|
|
25
|
+
}),
|
|
26
|
+
dryRun: Flags.boolean({
|
|
27
|
+
default: false,
|
|
28
|
+
description: 'Detect missing branches without creating them',
|
|
29
|
+
}),
|
|
30
|
+
domain: Flags.string({
|
|
31
|
+
char: 'd',
|
|
32
|
+
description: 'Tenant domain (for multi-domain setups)',
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
async run() {
|
|
36
|
+
const { args, flags } = await this.parse(StageEnsureBranches);
|
|
37
|
+
try {
|
|
38
|
+
const service = new HyperdriveSigV4Service(flags.domain);
|
|
39
|
+
this.log(chalk.blue(`🌿 Ensuring branches for stage: ${args.stageName}`));
|
|
40
|
+
const stage = await service.stageGet({ name: args.stageName });
|
|
41
|
+
if (!stage.branchName) {
|
|
42
|
+
this.error('Stage has no branchName — cannot ensure branches');
|
|
43
|
+
}
|
|
44
|
+
// Prefer `projectSlugs` (creation-time intent list) — fall back to the
|
|
45
|
+
// keys of `projects` (runtime tracking map) if the backend only
|
|
46
|
+
// exposes the deployment state.
|
|
47
|
+
const projectSlugs = (stage.projectSlugs && stage.projectSlugs.length > 0
|
|
48
|
+
? stage.projectSlugs
|
|
49
|
+
: stage.projects
|
|
50
|
+
? Object.keys(stage.projects)
|
|
51
|
+
: []);
|
|
52
|
+
if (projectSlugs.length === 0) {
|
|
53
|
+
this.log(chalk.gray(`ℹ️ No linked projects for stage ${args.stageName} — nothing to do`));
|
|
54
|
+
this.exit(0);
|
|
55
|
+
}
|
|
56
|
+
const allModules = await service.moduleList();
|
|
57
|
+
const linkedProjects = allModules
|
|
58
|
+
.filter((m) => m.slug && projectSlugs.includes(m.slug))
|
|
59
|
+
.map((m) => ({
|
|
60
|
+
slug: m.slug,
|
|
61
|
+
gitProvider: m.gitProvider,
|
|
62
|
+
gitFullRepoPath: m.gitFullRepoPath,
|
|
63
|
+
}));
|
|
64
|
+
if (flags.dryRun) {
|
|
65
|
+
this.log(chalk.yellow('[dry-run] detection only — no branches will be created'));
|
|
66
|
+
}
|
|
67
|
+
const result = await ensureBranches({
|
|
68
|
+
stageName: args.stageName,
|
|
69
|
+
branchName: stage.branchName,
|
|
70
|
+
linkedProjects,
|
|
71
|
+
verbose: flags.verbose,
|
|
72
|
+
dryRun: flags.dryRun,
|
|
73
|
+
logger: (msg) => this.log(msg),
|
|
74
|
+
});
|
|
75
|
+
// Always print a one-line summary (even in --verbose mode).
|
|
76
|
+
const summary = `${result.created + result.alreadyExisted}/${result.total} branches ensured (${result.created} created, ${result.alreadyExisted} already existed, ${result.failed} failed)`;
|
|
77
|
+
if (result.failed > 0) {
|
|
78
|
+
this.log(chalk.red(`❌ ${summary}`));
|
|
79
|
+
this.exit(1);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
this.log(chalk.green(`✅ ${summary}`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const err = error;
|
|
87
|
+
// Ignore EEXIT with code 0 — oclif's clean-exit pattern.
|
|
88
|
+
if (err.message?.includes('EEXIT') && err.oclif?.exit === 0) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Re-throw oclif exits with non-zero code so the process bubbles up.
|
|
92
|
+
if (err.oclif?.exit !== undefined && err.oclif.exit !== 0) {
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
const axiosError = error;
|
|
96
|
+
const errorMessage = axiosError.response?.data?.message ?? axiosError.message ?? 'Unknown error';
|
|
97
|
+
this.log(chalk.red(`❌ Error ensuring branches: ${errorMessage}`));
|
|
98
|
+
this.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -46,6 +46,10 @@ export default class StageList extends Command {
|
|
|
46
46
|
header: 'Regions',
|
|
47
47
|
get: (row) => (row.regions || []).join(', '),
|
|
48
48
|
},
|
|
49
|
+
customDomain: {
|
|
50
|
+
header: 'Domain',
|
|
51
|
+
get: (row) => row.customDomain || '',
|
|
52
|
+
},
|
|
49
53
|
defaultStage: {
|
|
50
54
|
header: 'Default',
|
|
51
55
|
get: (row) => row.defaultStage ? chalk.green('✓') : '',
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class StageStatus extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
slug: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
private renderManifest;
|
|
14
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
|
|
4
|
+
const STATUS_ICONS = {
|
|
5
|
+
deployed: '✅',
|
|
6
|
+
failed: '❌',
|
|
7
|
+
skipped: '⏸️',
|
|
8
|
+
pending: '⏳',
|
|
9
|
+
timeout: '⏱️',
|
|
10
|
+
};
|
|
11
|
+
const colorizeStatus = (status) => {
|
|
12
|
+
switch (status) {
|
|
13
|
+
case 'success':
|
|
14
|
+
return chalk.green(status);
|
|
15
|
+
case 'running':
|
|
16
|
+
case 'partial-failure':
|
|
17
|
+
return chalk.yellow(status);
|
|
18
|
+
case 'failed':
|
|
19
|
+
return chalk.red(status);
|
|
20
|
+
default:
|
|
21
|
+
return status;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
export default class StageStatus extends Command {
|
|
25
|
+
static args = {
|
|
26
|
+
slug: Args.string({ description: 'Stage slug (e.g. ss-11)', required: true }),
|
|
27
|
+
};
|
|
28
|
+
static description = 'Show deployment status for a stage';
|
|
29
|
+
static examples = [
|
|
30
|
+
'<%= config.bin %> <%= command.id %> ss-11',
|
|
31
|
+
'<%= config.bin %> <%= command.id %> ss-11 --verbose',
|
|
32
|
+
];
|
|
33
|
+
static flags = {
|
|
34
|
+
domain: Flags.string({
|
|
35
|
+
char: 'd',
|
|
36
|
+
description: 'Tenant domain (for multi-domain setups)',
|
|
37
|
+
}),
|
|
38
|
+
verbose: Flags.boolean({
|
|
39
|
+
char: 'v',
|
|
40
|
+
description: 'Show deploymentId and errorType per module',
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
async run() {
|
|
44
|
+
const { args, flags } = await this.parse(StageStatus);
|
|
45
|
+
try {
|
|
46
|
+
const service = new HyperdriveSigV4Service(flags.domain);
|
|
47
|
+
const response = await service.stageGetDeployManifest(args.slug);
|
|
48
|
+
if (!response.manifest) {
|
|
49
|
+
this.log(chalk.yellow(`⚠️ No deployment data for stage ${args.slug}`));
|
|
50
|
+
this.log(chalk.gray(`The stage exists but has no deploy manifest. Run hd stage create ${args.slug} --autoLaunch to deploy it.`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.renderManifest(response.manifest, args.slug, flags.verbose ?? false);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const statusCode = error?.response?.status ?? error?.statusCode;
|
|
57
|
+
if (statusCode === 404) {
|
|
58
|
+
this.error(chalk.red(`❌ Stage ${args.slug} not found`));
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
renderManifest(manifest, slug, verbose) {
|
|
64
|
+
// Compute totals
|
|
65
|
+
const allModules = manifest.waves.flatMap(w => w.modules);
|
|
66
|
+
const total = allModules.length;
|
|
67
|
+
const counts = { deployed: 0, failed: 0, pending: 0, skipped: 0, timeout: 0 };
|
|
68
|
+
for (const m of allModules) {
|
|
69
|
+
counts[m.status] = (counts[m.status] || 0) + 1;
|
|
70
|
+
}
|
|
71
|
+
// Header
|
|
72
|
+
this.log(`Stage: ${chalk.cyan(slug)} | Mission: ${chalk.cyan(manifest.missionId)} | Status: ${colorizeStatus(manifest.status)}`);
|
|
73
|
+
this.log(`Started: ${manifest.startedAt} Updated: ${manifest.updatedAt} ` +
|
|
74
|
+
`Modules: ${counts.deployed}/${total} ✅ ${counts.failed} ❌ ${counts.skipped} ⏸️ ${counts.pending} ⏳ ${counts.timeout} ⏱️`);
|
|
75
|
+
this.log('');
|
|
76
|
+
// Waves
|
|
77
|
+
for (const wave of manifest.waves) {
|
|
78
|
+
const waveDeployed = wave.modules.filter(m => m.status === 'deployed').length;
|
|
79
|
+
if (verbose) {
|
|
80
|
+
this.log(`W${wave.waveIndex}: (${waveDeployed}/${wave.modules.length})`);
|
|
81
|
+
for (const m of wave.modules) {
|
|
82
|
+
const icon = STATUS_ICONS[m.status] || '?';
|
|
83
|
+
let line = ` ${m.moduleSlug} ${icon} ${m.status}`;
|
|
84
|
+
if (m.deploymentId)
|
|
85
|
+
line += ` deploymentId=${m.deploymentId}`;
|
|
86
|
+
if (m.errorType)
|
|
87
|
+
line += ` errorType=${m.errorType}`;
|
|
88
|
+
this.log(line);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const moduleSegments = wave.modules.map(m => {
|
|
93
|
+
const icon = STATUS_ICONS[m.status] || '?';
|
|
94
|
+
return `[${m.moduleSlug} ${icon}]`;
|
|
95
|
+
});
|
|
96
|
+
this.log(`W${wave.waveIndex}: ${moduleSegments.join(' ')} (${waveDeployed}/${wave.modules.length})`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|