@akash-chowdhury-24/deployhub 2.0.15 → 2.0.17
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 +47 -16
- package/package.json +1 -1
- package/src/artifact/engine.js +11 -4
- package/src/cli/index.js +2 -0
- package/src/commands/deploy.js +61 -16
- package/src/commands/doctor.js +212 -26
- package/src/commands/env.js +326 -0
- package/src/commands/init.js +111 -64
- package/src/commands/rollback.js +84 -24
- package/src/commands/sync-workflows.js +4 -1
- package/src/commands/verify.js +97 -33
- package/src/core/config.js +371 -33
- package/src/core/environments.js +300 -0
- package/src/core/stages.js +55 -19
- package/src/deployment/deployment-env.js +210 -0
- package/src/deployment/index.js +58 -7
- package/src/deployment/init-prompts.js +202 -46
- package/src/deployment/providers/azure-vm.js +8 -10
- package/src/deployment/providers/docker.js +10 -2
- package/src/deployment/providers/ec2.js +7 -9
- package/src/deployment/providers/gcp-vm.js +8 -10
- package/src/deployment/providers/kubernetes.js +15 -6
- package/src/deployment/providers/ssh.js +18 -13
- package/src/storage/index.js +230 -3
- package/src/utils/build-id.js +23 -0
- package/src/utils/github-actions.js +414 -33
- package/src/utils/health-check.js +158 -0
- package/src/utils/kubernetes-manifests.js +3 -1
- package/src/utils/nginx.js +66 -15
- package/src/utils/rollback/engine.js +147 -40
- package/src/utils/scaffold.js +2 -1
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
3
5
|
import { getWorkflowHeaderComment } from './author.js';
|
|
4
6
|
import {
|
|
5
7
|
generateDeploymentEnvSection,
|
|
6
8
|
getDeploymentWorkflowSecretKeys,
|
|
7
9
|
getDeploymentSecretChecklistItems,
|
|
10
|
+
getDeploymentWorkflowSecretKeysForEnv,
|
|
11
|
+
getDeploymentSecretChecklistItemsForEnv,
|
|
12
|
+
shouldPrefixEnvSecrets,
|
|
13
|
+
prefixSecretKey,
|
|
14
|
+
envUsesPrefixedSecrets,
|
|
8
15
|
DEPLOYMENT_ENV_KEYS,
|
|
9
16
|
} from '../deployment/deployment-env.js';
|
|
17
|
+
import { getEnvMethod, getEnvTrigger, isEnvEnabled } from '../core/environments.js';
|
|
10
18
|
|
|
11
19
|
/** @typedef {'aws'|'azure'|'gcp'|'gdrive'|'dropbox'|'local'|'ftp'|'ssh'} ProviderEnvKey */
|
|
12
20
|
|
|
@@ -128,6 +136,34 @@ export function getCliInstallSpec(cliSource) {
|
|
|
128
136
|
return normalized;
|
|
129
137
|
}
|
|
130
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Version/range suitable as a package.json dependency VALUE for this CLI
|
|
141
|
+
* (key is already NPM_PACKAGE — do not embed the package name again).
|
|
142
|
+
*
|
|
143
|
+
* @param {string} [cliSource]
|
|
144
|
+
* @returns {string}
|
|
145
|
+
*/
|
|
146
|
+
export function getCliPackageJsonDependencyVersion(cliSource) {
|
|
147
|
+
const normalized = normalizeCliSource(cliSource);
|
|
148
|
+
if (normalized === DEFAULT_NPM_CLI_SOURCE) {
|
|
149
|
+
try {
|
|
150
|
+
const pkgPath = path.join(
|
|
151
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
152
|
+
'../../package.json'
|
|
153
|
+
);
|
|
154
|
+
const pkg = fs.readJsonSync(pkgPath);
|
|
155
|
+
if (typeof pkg.version === 'string' && pkg.version.trim()) {
|
|
156
|
+
return `^${pkg.version.trim()}`;
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// fall through
|
|
160
|
+
}
|
|
161
|
+
return 'latest';
|
|
162
|
+
}
|
|
163
|
+
// github: / file: specs are valid package.json dependency values as-is
|
|
164
|
+
return getCliInstallSpec(cliSource);
|
|
165
|
+
}
|
|
166
|
+
|
|
131
167
|
/**
|
|
132
168
|
* Shell command to run deployhub build from the installed scoped package.
|
|
133
169
|
* Uses node directly so it works reliably when installed from a private GitHub repo.
|
|
@@ -217,7 +253,9 @@ const KUBECTL_VERSION = 'v1.30.4';
|
|
|
217
253
|
* @returns {boolean}
|
|
218
254
|
*/
|
|
219
255
|
function hasKubernetesDeploy(deployEnvironments, environments) {
|
|
220
|
-
return deployEnvironments.some(
|
|
256
|
+
return deployEnvironments.some(
|
|
257
|
+
(envName) => getEnvMethod(environments[envName]) === 'kubernetes'
|
|
258
|
+
);
|
|
221
259
|
}
|
|
222
260
|
|
|
223
261
|
/**
|
|
@@ -262,13 +300,55 @@ function applyKubernetesWorkflowEnv(deployEnvironments, environments, envVars) {
|
|
|
262
300
|
export const DEPLOY_WORKFLOW_FILENAME = 'deployhub.yml';
|
|
263
301
|
export const ROLLBACK_WORKFLOW_FILENAME = 'deployhub-rollback.yml';
|
|
264
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Upsert a `KEY: value` line into the workflow env set (LHS must be unique for valid YAML).
|
|
305
|
+
* @param {Set<string>} envVars
|
|
306
|
+
* @param {string} lhs
|
|
307
|
+
* @param {string} valueExpr
|
|
308
|
+
*/
|
|
309
|
+
function upsertWorkflowEnvLine(envVars, lhs, valueExpr) {
|
|
310
|
+
const linePrefix = `${lhs}: `;
|
|
311
|
+
for (const entry of [...envVars]) {
|
|
312
|
+
if (entry.startsWith(linePrefix)) {
|
|
313
|
+
envVars.delete(entry);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
envVars.add(`${linePrefix}${valueExpr}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Enabled environment names from the live `environments` map (preferred over
|
|
321
|
+
* a possibly-stale `deploy[]` argument passed into workflow generators).
|
|
322
|
+
* @param {Record<string, { type?: string, method?: string }>} environments
|
|
323
|
+
* @returns {string[]}
|
|
324
|
+
*/
|
|
325
|
+
function listEnabledEnvironmentNames(environments) {
|
|
326
|
+
return Object.keys(environments || {}).filter((n) => isEnvEnabled(environments[n]));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Push-triggered enabled envs — same set `pipelineDeployTargets` deploys in GHA.
|
|
331
|
+
* @param {Record<string, { type?: string, method?: string }>} environments
|
|
332
|
+
* @returns {string[]}
|
|
333
|
+
*/
|
|
334
|
+
function listPushEnvironmentNames(environments) {
|
|
335
|
+
return listEnabledEnvironmentNames(environments).filter(
|
|
336
|
+
(n) => getEnvTrigger(environments[n]) === 'push'
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
265
340
|
/**
|
|
266
341
|
* Shared env entries for deploy and rollback workflows (same secret resolution).
|
|
267
|
-
*
|
|
342
|
+
* Multi-env: each environment contributes its own CI secret names (grandfathered
|
|
343
|
+
* unprefixed SSH_HOST, or PRODUCTION_SSH_HOST, etc.). Prefixed envs are NOT
|
|
344
|
+
* last-wins-mapped onto unprefixed keys — that would overwrite the grandfathered
|
|
345
|
+
* binding and leave production looking fine in YAML while development silently
|
|
346
|
+
* inherits the wrong credentials. `applyEnvSecretOverlay` remaps PREFIX_* →
|
|
347
|
+
* unprefixed names at deploy time for non-grandfathered targets.
|
|
268
348
|
*
|
|
269
349
|
* @param {string[]} storageProviders
|
|
270
|
-
* @param {string[]} deployEnvironments
|
|
271
|
-
* @param {Record<string, { type
|
|
350
|
+
* @param {string[]} deployEnvironments — environments whose secrets to inject
|
|
351
|
+
* @param {Record<string, { type?: string, method?: string }>} environments
|
|
272
352
|
* @param {import('../core/config.js').DeployHubConfig} [config]
|
|
273
353
|
* @returns {Set<string>}
|
|
274
354
|
*/
|
|
@@ -279,22 +359,32 @@ export function buildWorkflowEnvEntries(
|
|
|
279
359
|
config = null
|
|
280
360
|
) {
|
|
281
361
|
/** @type {Set<string>} */
|
|
282
|
-
const envVars = new Set([
|
|
362
|
+
const envVars = new Set([
|
|
363
|
+
`DEPLOYHUB_ENV: ${deployEnvironments[0] || 'production'}`,
|
|
364
|
+
]);
|
|
283
365
|
|
|
284
366
|
for (const provider of storageProviders) {
|
|
285
367
|
const keys = PROVIDER_ENV_MAP[provider] || [];
|
|
286
368
|
for (const key of keys) {
|
|
287
|
-
envVars
|
|
369
|
+
upsertWorkflowEnvLine(envVars, key, `\${{ secrets.${key} }}`);
|
|
288
370
|
}
|
|
289
371
|
}
|
|
290
372
|
|
|
373
|
+
const cfg = { ...(config || {}), environments };
|
|
374
|
+
|
|
291
375
|
for (const envName of deployEnvironments) {
|
|
292
376
|
const env = environments[envName];
|
|
293
377
|
if (!env) continue;
|
|
294
378
|
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
379
|
+
const method = getEnvMethod(env);
|
|
380
|
+
if (!method) continue;
|
|
381
|
+
const unprefixedKeys = getDeploymentWorkflowSecretKeys(method, config);
|
|
382
|
+
for (const key of unprefixedKeys) {
|
|
383
|
+
const secretName = envUsesPrefixedSecrets(envName, cfg)
|
|
384
|
+
? prefixSecretKey(envName, key)
|
|
385
|
+
: key;
|
|
386
|
+
// Bind process.env[secretName] for this environment (prefixed or grandfathered).
|
|
387
|
+
upsertWorkflowEnvLine(envVars, secretName, `\${{ secrets.${secretName} }}`);
|
|
298
388
|
}
|
|
299
389
|
}
|
|
300
390
|
|
|
@@ -302,6 +392,21 @@ export function buildWorkflowEnvEntries(
|
|
|
302
392
|
return envVars;
|
|
303
393
|
}
|
|
304
394
|
|
|
395
|
+
/**
|
|
396
|
+
* Choice options for workflow_dispatch — enabled environments only, plus `all`.
|
|
397
|
+
* Disabled envs are omitted so selecting them cannot waste a CI run.
|
|
398
|
+
*
|
|
399
|
+
* @param {Record<string, unknown>} environments
|
|
400
|
+
* @returns {string}
|
|
401
|
+
*/
|
|
402
|
+
function formatEnvironmentChoiceOptions(environments) {
|
|
403
|
+
const names = Object.keys(environments || {}).filter((n) =>
|
|
404
|
+
isEnvEnabled(environments[n])
|
|
405
|
+
);
|
|
406
|
+
const options = [...names, 'all'];
|
|
407
|
+
return options.map((n) => ` - ${n}`).join('\n');
|
|
408
|
+
}
|
|
409
|
+
|
|
305
410
|
/**
|
|
306
411
|
* @param {Set<string>} envVars
|
|
307
412
|
* @param {string} [indent]
|
|
@@ -339,7 +444,7 @@ export function extractWorkflowSecretKeys(yaml) {
|
|
|
339
444
|
/**
|
|
340
445
|
* @param {string[]} storageProviders
|
|
341
446
|
* @param {string[]} deployEnvironments
|
|
342
|
-
* @param {Record<string, { type
|
|
447
|
+
* @param {Record<string, { type?: string, method?: string }>} environments
|
|
343
448
|
* @param {string} [cliSource]
|
|
344
449
|
* @param {import('../core/config.js').DeployHubConfig} [config]
|
|
345
450
|
* @returns {string}
|
|
@@ -351,13 +456,41 @@ export function generateWorkflowYaml(
|
|
|
351
456
|
cliSource = DEFAULT_NPM_CLI_SOURCE,
|
|
352
457
|
config = null
|
|
353
458
|
) {
|
|
354
|
-
const
|
|
459
|
+
const envNames = Object.keys(environments || {});
|
|
460
|
+
// Secret injection must follow live environments (same as pipelineDeployTargets),
|
|
461
|
+
// not a possibly-stale deploy[] argument — otherwise push-deployed envs missing
|
|
462
|
+
// from deploy[] get no secrets in the job env block.
|
|
463
|
+
const enabledEnvs = listEnabledEnvironmentNames(environments);
|
|
464
|
+
const pushEnvs = listPushEnvironmentNames(environments);
|
|
465
|
+
const allDeployNames =
|
|
466
|
+
enabledEnvs.length > 0
|
|
467
|
+
? enabledEnvs
|
|
468
|
+
: deployEnvironments.length > 0
|
|
469
|
+
? deployEnvironments
|
|
470
|
+
: envNames;
|
|
471
|
+
|
|
472
|
+
// Build step: union of secrets for every push-triggered env (build deploys all of them).
|
|
473
|
+
const buildSecretEnvs =
|
|
474
|
+
pushEnvs.length > 0 ? pushEnvs : allDeployNames.slice(0, 1);
|
|
475
|
+
|
|
476
|
+
// workflow_dispatch step: union for ALL enabled envs (dropdown can pick any / all).
|
|
477
|
+
const dispatchSecretEnvs = allDeployNames;
|
|
478
|
+
|
|
479
|
+
const buildEnvVars = buildWorkflowEnvEntries(
|
|
355
480
|
storageProviders,
|
|
356
|
-
|
|
481
|
+
buildSecretEnvs,
|
|
357
482
|
environments,
|
|
358
483
|
config
|
|
359
484
|
);
|
|
360
|
-
const
|
|
485
|
+
const buildEnvBlock = formatWorkflowEnvBlock(buildEnvVars);
|
|
486
|
+
|
|
487
|
+
const dispatchEnvVars = buildWorkflowEnvEntries(
|
|
488
|
+
storageProviders,
|
|
489
|
+
dispatchSecretEnvs,
|
|
490
|
+
environments,
|
|
491
|
+
config
|
|
492
|
+
);
|
|
493
|
+
const dispatchEnvBlock = formatWorkflowEnvBlock(dispatchEnvVars);
|
|
361
494
|
|
|
362
495
|
const installSpec = getCliInstallSpec(cliSource);
|
|
363
496
|
const backendSteps = getBackendSetupSteps(config);
|
|
@@ -365,7 +498,7 @@ export function generateWorkflowYaml(
|
|
|
365
498
|
const githubGitConfigStep = isGithubCliSource(cliSource)
|
|
366
499
|
? `${getGithubGitConfigStep()}\n`
|
|
367
500
|
: '';
|
|
368
|
-
const kubernetesSteps = hasKubernetesDeploy(
|
|
501
|
+
const kubernetesSteps = hasKubernetesDeploy(allDeployNames, environments)
|
|
369
502
|
? `${getKubernetesSetupSteps()}\n`
|
|
370
503
|
: '';
|
|
371
504
|
|
|
@@ -375,11 +508,41 @@ export function generateWorkflowYaml(
|
|
|
375
508
|
? getInstallDepsCommand(config)
|
|
376
509
|
: 'npm install';
|
|
377
510
|
|
|
511
|
+
const envChoiceOptions = formatEnvironmentChoiceOptions(environments);
|
|
512
|
+
const hasEnvs = envNames.length > 0;
|
|
513
|
+
const dispatchInputs = hasEnvs
|
|
514
|
+
? ` workflow_dispatch:
|
|
515
|
+
inputs:
|
|
516
|
+
environment:
|
|
517
|
+
description: 'Environment to deploy (leave blank on push; use "all" for every enabled env)'
|
|
518
|
+
required: false
|
|
519
|
+
type: choice
|
|
520
|
+
options:
|
|
521
|
+
${envChoiceOptions}
|
|
522
|
+
`
|
|
523
|
+
: ` workflow_dispatch:
|
|
524
|
+
`;
|
|
525
|
+
|
|
526
|
+
const manualDeployStep = hasEnvs
|
|
527
|
+
? ` - name: Deploy (workflow_dispatch)
|
|
528
|
+
if: github.event_name == 'workflow_dispatch'
|
|
529
|
+
env:
|
|
530
|
+
${dispatchEnvBlock}
|
|
531
|
+
run: |
|
|
532
|
+
ENV_INPUT="\${{ inputs.environment }}"
|
|
533
|
+
if [ -z "$ENV_INPUT" ] || [ "$ENV_INPUT" = "all" ]; then
|
|
534
|
+
${getCliDeployCommand()} --env all
|
|
535
|
+
else
|
|
536
|
+
${getCliDeployCommand()} --env "$ENV_INPUT"
|
|
537
|
+
fi
|
|
538
|
+
`
|
|
539
|
+
: '';
|
|
540
|
+
|
|
378
541
|
const workflow = `${getWorkflowHeaderComment()}name: DeployHub
|
|
379
542
|
on:
|
|
380
543
|
push:
|
|
381
544
|
branches: [main]
|
|
382
|
-
jobs:
|
|
545
|
+
${dispatchInputs}jobs:
|
|
383
546
|
deploy:
|
|
384
547
|
runs-on: ubuntu-latest
|
|
385
548
|
steps:
|
|
@@ -391,20 +554,29 @@ ${kubernetesSteps}${githubGitConfigStep} - name: Install project dependenci
|
|
|
391
554
|
run: ${installDepsCommand}
|
|
392
555
|
- name: Install DeployHub CLI
|
|
393
556
|
run: npm install ${installSpec} --no-save
|
|
394
|
-
-
|
|
557
|
+
- name: Build (and auto-deploy push-triggered envs)
|
|
558
|
+
run: ${getCliBuildCommand()}
|
|
395
559
|
env:
|
|
396
|
-
${
|
|
397
|
-
`;
|
|
560
|
+
${buildEnvBlock}
|
|
561
|
+
${manualDeployStep}`;
|
|
398
562
|
|
|
399
563
|
return workflow;
|
|
400
564
|
}
|
|
401
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Shell command to run deployhub deploy from the installed scoped package.
|
|
568
|
+
* @returns {string}
|
|
569
|
+
*/
|
|
570
|
+
export function getCliDeployCommand() {
|
|
571
|
+
return `node ./node_modules/${NPM_PACKAGE}/src/cli/index.js deploy`;
|
|
572
|
+
}
|
|
573
|
+
|
|
402
574
|
/**
|
|
403
575
|
* Manual rollback via Actions tab / gh workflow run (workflow_dispatch only).
|
|
404
576
|
*
|
|
405
577
|
* @param {string[]} storageProviders
|
|
406
578
|
* @param {string[]} deployEnvironments
|
|
407
|
-
* @param {Record<string, { type
|
|
579
|
+
* @param {Record<string, { type?: string, method?: string }>} environments
|
|
408
580
|
* @param {string} [cliSource]
|
|
409
581
|
* @param {import('../core/config.js').DeployHubConfig} [config]
|
|
410
582
|
* @returns {string}
|
|
@@ -416,9 +588,18 @@ export function generateRollbackWorkflowYaml(
|
|
|
416
588
|
cliSource = DEFAULT_NPM_CLI_SOURCE,
|
|
417
589
|
config = null
|
|
418
590
|
) {
|
|
591
|
+
const envNames = Object.keys(environments || {});
|
|
592
|
+
const enabledEnvs = listEnabledEnvironmentNames(environments);
|
|
593
|
+
const allDeployNames =
|
|
594
|
+
enabledEnvs.length > 0
|
|
595
|
+
? enabledEnvs
|
|
596
|
+
: deployEnvironments.length > 0
|
|
597
|
+
? deployEnvironments
|
|
598
|
+
: envNames.filter((n) => isEnvEnabled(environments[n]));
|
|
599
|
+
|
|
419
600
|
const envVars = buildWorkflowEnvEntries(
|
|
420
601
|
storageProviders,
|
|
421
|
-
|
|
602
|
+
allDeployNames,
|
|
422
603
|
environments,
|
|
423
604
|
config
|
|
424
605
|
);
|
|
@@ -428,11 +609,43 @@ export function generateRollbackWorkflowYaml(
|
|
|
428
609
|
const githubGitConfigStep = isGithubCliSource(cliSource)
|
|
429
610
|
? `${getGithubGitConfigStep()}\n`
|
|
430
611
|
: '';
|
|
431
|
-
const kubernetesSteps = hasKubernetesDeploy(
|
|
612
|
+
const kubernetesSteps = hasKubernetesDeploy(allDeployNames, environments)
|
|
432
613
|
? `${getKubernetesSetupSteps()}\n`
|
|
433
614
|
: '';
|
|
434
615
|
|
|
435
616
|
const rollbackCmd = getCliRollbackCommand();
|
|
617
|
+
const envChoiceOptions = formatEnvironmentChoiceOptions(environments);
|
|
618
|
+
const hasEnvs = envNames.length > 0;
|
|
619
|
+
|
|
620
|
+
const environmentInput = hasEnvs
|
|
621
|
+
? ` environment:
|
|
622
|
+
description: 'Environment to roll back (or "all")'
|
|
623
|
+
required: false
|
|
624
|
+
type: choice
|
|
625
|
+
options:
|
|
626
|
+
${envChoiceOptions}
|
|
627
|
+
`
|
|
628
|
+
: '';
|
|
629
|
+
|
|
630
|
+
const rollbackRun = hasEnvs
|
|
631
|
+
? ` ENV_INPUT="\${{ inputs.environment }}"
|
|
632
|
+
BUILD_INPUT="\${{ inputs.buildId }}"
|
|
633
|
+
ENV_FLAG=""
|
|
634
|
+
if [ -z "$ENV_INPUT" ] || [ "$ENV_INPUT" = "all" ]; then
|
|
635
|
+
ENV_FLAG="--env all"
|
|
636
|
+
else
|
|
637
|
+
ENV_FLAG="--env $ENV_INPUT"
|
|
638
|
+
fi
|
|
639
|
+
if [ -n "$BUILD_INPUT" ]; then
|
|
640
|
+
${rollbackCmd} "$BUILD_INPUT" $ENV_FLAG
|
|
641
|
+
else
|
|
642
|
+
${rollbackCmd} $ENV_FLAG
|
|
643
|
+
fi`
|
|
644
|
+
: ` if [ -n "\${{ inputs.buildId }}" ]; then
|
|
645
|
+
${rollbackCmd} "\${{ inputs.buildId }}"
|
|
646
|
+
else
|
|
647
|
+
${rollbackCmd}
|
|
648
|
+
fi`;
|
|
436
649
|
|
|
437
650
|
return `${getWorkflowHeaderComment()}name: DeployHub Rollback
|
|
438
651
|
on:
|
|
@@ -442,7 +655,7 @@ on:
|
|
|
442
655
|
description: 'Exact buildId to restore (leave blank = previous build)'
|
|
443
656
|
required: false
|
|
444
657
|
type: string
|
|
445
|
-
jobs:
|
|
658
|
+
${environmentInput}jobs:
|
|
446
659
|
rollback:
|
|
447
660
|
runs-on: ubuntu-latest
|
|
448
661
|
steps:
|
|
@@ -456,11 +669,7 @@ ${kubernetesSteps}${githubGitConfigStep} - name: Install DeployHub CLI
|
|
|
456
669
|
env:
|
|
457
670
|
${envBlock}
|
|
458
671
|
run: |
|
|
459
|
-
|
|
460
|
-
${rollbackCmd} "\${{ inputs.buildId }}"
|
|
461
|
-
else
|
|
462
|
-
${rollbackCmd}
|
|
463
|
-
fi
|
|
672
|
+
${rollbackRun}
|
|
464
673
|
`;
|
|
465
674
|
}
|
|
466
675
|
|
|
@@ -567,6 +776,169 @@ export async function getRollbackWorkflowDoctorCheck(cwd, config) {
|
|
|
567
776
|
};
|
|
568
777
|
}
|
|
569
778
|
|
|
779
|
+
/**
|
|
780
|
+
* Expected CI secret names for the current config — same set a fresh
|
|
781
|
+
* `sync-workflows` would reference (via buildWorkflowEnvEntries).
|
|
782
|
+
*
|
|
783
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
784
|
+
* @param {'deploy'|'rollback'} [kind]
|
|
785
|
+
* @returns {Set<string>}
|
|
786
|
+
*/
|
|
787
|
+
export function expectedWorkflowSecretKeysFromConfig(config, kind = 'rollback') {
|
|
788
|
+
const environments = config.environments || {};
|
|
789
|
+
const allNames = Object.keys(environments);
|
|
790
|
+
/** @type {string[]} */
|
|
791
|
+
let targets;
|
|
792
|
+
if (kind === 'deploy') {
|
|
793
|
+
const pushEnvs = listPushEnvironmentNames(environments);
|
|
794
|
+
const enabled = listEnabledEnvironmentNames(environments);
|
|
795
|
+
// Deploy workflow Build step uses push envs; doctor also flags dispatch-needed
|
|
796
|
+
// secrets by using the broader enabled set via kind === 'rollback' / checklist.
|
|
797
|
+
targets = pushEnvs.length > 0 ? pushEnvs : enabled.slice(0, 1);
|
|
798
|
+
} else {
|
|
799
|
+
targets = allNames.length > 0 ? listEnabledEnvironmentNames(environments) : allNames;
|
|
800
|
+
if (targets.length === 0) targets = allNames;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const entries = buildWorkflowEnvEntries(
|
|
804
|
+
config.storage || [],
|
|
805
|
+
targets,
|
|
806
|
+
environments,
|
|
807
|
+
config
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
/** @type {Set<string>} */
|
|
811
|
+
const keys = new Set();
|
|
812
|
+
const re = /secrets\.([A-Z0-9_]+)/g;
|
|
813
|
+
for (const line of entries) {
|
|
814
|
+
let match;
|
|
815
|
+
re.lastIndex = 0;
|
|
816
|
+
while ((match = re.exec(line)) !== null) {
|
|
817
|
+
keys.add(match[1]);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return keys;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Lightweight drift check: checked-in workflow vs current config.
|
|
825
|
+
* Catches missing dispatch env options and missing required secret references.
|
|
826
|
+
*
|
|
827
|
+
* @param {string} yamlText
|
|
828
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
829
|
+
* @param {string} [filename]
|
|
830
|
+
* @returns {{ drifted: boolean, missingEnvs: string[], missingSecrets: string[], summary: string }}
|
|
831
|
+
*/
|
|
832
|
+
export function detectWorkflowConfigDrift(yamlText, config, filename = DEPLOY_WORKFLOW_FILENAME) {
|
|
833
|
+
/** @type {string[]} */
|
|
834
|
+
const missingEnvs = [];
|
|
835
|
+
/** @type {string[]} */
|
|
836
|
+
const missingSecrets = [];
|
|
837
|
+
|
|
838
|
+
let parsed;
|
|
839
|
+
try {
|
|
840
|
+
parsed = yaml.load(yamlText);
|
|
841
|
+
} catch {
|
|
842
|
+
return {
|
|
843
|
+
drifted: false,
|
|
844
|
+
missingEnvs,
|
|
845
|
+
missingSecrets,
|
|
846
|
+
summary: '',
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
851
|
+
return { drifted: false, missingEnvs, missingSecrets, summary: '' };
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const envNames = listEnabledEnvironmentNames(config.environments || {});
|
|
855
|
+
const root = /** @type {Record<string, any>} */ (parsed);
|
|
856
|
+
const options = root?.on?.workflow_dispatch?.inputs?.environment?.options;
|
|
857
|
+
|
|
858
|
+
if (Array.isArray(options) && envNames.length > 0) {
|
|
859
|
+
const optionSet = new Set(options.map(String));
|
|
860
|
+
for (const name of envNames) {
|
|
861
|
+
if (!optionSet.has(name)) missingEnvs.push(name);
|
|
862
|
+
}
|
|
863
|
+
} else if (envNames.length > 1) {
|
|
864
|
+
// Multi-env config but no dispatch dropdown — stale single-env workflow.
|
|
865
|
+
missingEnvs.push(...envNames);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const kind = filename.includes('rollback') ? 'rollback' : 'deploy';
|
|
869
|
+
const fileSecrets = new Set(extractWorkflowSecretKeys(yamlText));
|
|
870
|
+
const expected = expectedWorkflowSecretKeysFromConfig(config, kind);
|
|
871
|
+
for (const key of expected) {
|
|
872
|
+
if (!fileSecrets.has(key)) missingSecrets.push(key);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const drifted = missingEnvs.length > 0 || missingSecrets.length > 0;
|
|
876
|
+
/** @type {string[]} */
|
|
877
|
+
const parts = [];
|
|
878
|
+
if (missingEnvs.length > 0) {
|
|
879
|
+
parts.push(
|
|
880
|
+
`missing: ${missingEnvs.map((n) => `"${n}"`).join(', ')} in the dispatch dropdown`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
if (missingSecrets.length > 0) {
|
|
884
|
+
const shown = missingSecrets.slice(0, 4);
|
|
885
|
+
parts.push(
|
|
886
|
+
`missing secret(s): ${shown.join(', ')}${missingSecrets.length > 4 ? ', …' : ''}`
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return {
|
|
891
|
+
drifted,
|
|
892
|
+
missingEnvs,
|
|
893
|
+
missingSecrets,
|
|
894
|
+
summary: parts.join('; '),
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Doctor helper: informational checks when checked-in workflows drift from config.
|
|
900
|
+
* Skips missing/unparseable files (other checks cover "file missing").
|
|
901
|
+
* Returns an empty array when everything is in sync or not applicable.
|
|
902
|
+
*
|
|
903
|
+
* @param {string} cwd
|
|
904
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
905
|
+
* @returns {Promise<{ name: string, pass: boolean, message: string }[]>}
|
|
906
|
+
*/
|
|
907
|
+
export async function getWorkflowDriftDoctorChecks(cwd, config) {
|
|
908
|
+
const envCount = Object.keys(config.environments || {}).length;
|
|
909
|
+
if (envCount === 0) return [];
|
|
910
|
+
|
|
911
|
+
/** @type {{ name: string, pass: boolean, message: string }[]} */
|
|
912
|
+
const checks = [];
|
|
913
|
+
const files = [DEPLOY_WORKFLOW_FILENAME, ROLLBACK_WORKFLOW_FILENAME];
|
|
914
|
+
|
|
915
|
+
for (const filename of files) {
|
|
916
|
+
const filePath = path.join(cwd, '.github', 'workflows', filename);
|
|
917
|
+
if (!(await fs.pathExists(filePath))) continue;
|
|
918
|
+
|
|
919
|
+
let text;
|
|
920
|
+
try {
|
|
921
|
+
text = await fs.readFile(filePath, 'utf8');
|
|
922
|
+
} catch {
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const drift = detectWorkflowConfigDrift(text, config, filename);
|
|
927
|
+
if (!drift.drifted) continue;
|
|
928
|
+
|
|
929
|
+
checks.push({
|
|
930
|
+
name: `Workflow sync (${filename})`,
|
|
931
|
+
// Informational nudge — same pattern as missing rollback workflow (pass: true).
|
|
932
|
+
pass: true,
|
|
933
|
+
message:
|
|
934
|
+
`.github/workflows/${filename} looks out of date with your current environments config ` +
|
|
935
|
+
`(${drift.summary}). Run: deployhub sync-workflows`,
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return checks;
|
|
940
|
+
}
|
|
941
|
+
|
|
570
942
|
/**
|
|
571
943
|
* @param {string} cliSource
|
|
572
944
|
* @param {string} [cwd]
|
|
@@ -577,7 +949,9 @@ export async function addDeployhubToPackageJson(cliSource, cwd = process.cwd())
|
|
|
577
949
|
|
|
578
950
|
const pkg = await fs.readJson(pkgPath);
|
|
579
951
|
pkg.devDependencies = pkg.devDependencies || {};
|
|
580
|
-
|
|
952
|
+
// package.json value must be a semver range / "latest" / git URL — never "name@version"
|
|
953
|
+
// (that form is only for `npm install <spec>` via getCliInstallSpec).
|
|
954
|
+
pkg.devDependencies[NPM_PACKAGE] = getCliPackageJsonDependencyVersion(cliSource);
|
|
581
955
|
delete pkg.devDependencies.deployhub;
|
|
582
956
|
pkg.scripts = pkg.scripts || {};
|
|
583
957
|
pkg.scripts['deployhub:build'] = 'deployhub build';
|
|
@@ -669,9 +1043,15 @@ export function getGithubSecretsChecklist(
|
|
|
669
1043
|
|
|
670
1044
|
for (const envName of deployEnvironments) {
|
|
671
1045
|
const env = environments[envName];
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1046
|
+
const method = getEnvMethod(env);
|
|
1047
|
+
if (!method) continue;
|
|
1048
|
+
|
|
1049
|
+
for (const item of getDeploymentSecretChecklistItemsForEnv(
|
|
1050
|
+
envName,
|
|
1051
|
+
method,
|
|
1052
|
+
config,
|
|
1053
|
+
environments
|
|
1054
|
+
)) {
|
|
675
1055
|
const existing = byKey.get(item.key);
|
|
676
1056
|
if (existing) {
|
|
677
1057
|
if (item.required && !existing.required) {
|
|
@@ -741,9 +1121,10 @@ export function generateEnvExampleContent(
|
|
|
741
1121
|
|
|
742
1122
|
for (const envName of deployEnvironments) {
|
|
743
1123
|
const env = environments[envName];
|
|
744
|
-
|
|
1124
|
+
const method = getEnvMethod(env);
|
|
1125
|
+
if (!method) continue;
|
|
745
1126
|
|
|
746
|
-
const deploySection = generateDeploymentEnvSection(
|
|
1127
|
+
const deploySection = generateDeploymentEnvSection(method, config, environments);
|
|
747
1128
|
if (deploySection) {
|
|
748
1129
|
sections.push(`${deploySection}\n`);
|
|
749
1130
|
}
|