@akash-chowdhury-24/deployhub 2.0.17 → 2.0.18
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 +2 -0
- package/package.json +1 -1
- package/src/core/environments.js +6 -1
- package/src/core/stages.js +4 -1
- package/src/storage/providers/local.js +5 -1
- package/src/utils/github-actions.js +78 -61
package/README.md
CHANGED
|
@@ -909,6 +909,8 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
909
909
|
- Cluster connectivity test during `init`
|
|
910
910
|
- On deploy: registry login → reuse or build image → push (unique tag unless `DOCKER_IMAGE_TAG` is set) → ensure namespace exists (prompt locally / auto-create in CI) → `kubectl apply` → `kubectl set image` with the full resolved image ref → `kubectl rollout restart` when that ref is unchanged so pods pick up a new digest
|
|
911
911
|
|
|
912
|
+
> **Limitation — multiple Kubernetes clusters:** A generated workflow writes **one** kubeconfig file per job. Multiple Kubernetes environments that target **different clusters** in the same workflow run are not yet fully supported (follow-up). Same-cluster multi-namespace / multi-env is fine.
|
|
913
|
+
|
|
912
914
|
**After `init`:**
|
|
913
915
|
1. Verify context: `kubectl config get-contexts`
|
|
914
916
|
2. Copy `.env.example` → `.env`; set `DOCKER_IMAGE_NAME`, registry username/token, and (for local deploys) `KUBECONFIG` / `KUBE_CONTEXT` / `KUBE_NAMESPACE` as needed
|
package/package.json
CHANGED
package/src/core/environments.js
CHANGED
|
@@ -161,8 +161,13 @@ export function resolveDefaultEnvironmentName(config) {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
|
-
* Enabled environment names
|
|
164
|
+
* Enabled environment names — **the single source of truth** for “which envs
|
|
165
|
+
* apply” across CLI, pipeline gating, workflow secret injection, and dropdowns.
|
|
166
|
+
* Do not add a parallel helper that re-filters `environments` by `enabled`
|
|
167
|
+
* (that shape caused Build/Dispatch secret asymmetry when lists drifted).
|
|
168
|
+
*
|
|
165
169
|
* Falls back to legacy `deploy[]` when present and environments lack `enabled`.
|
|
170
|
+
* Used for `--env all` and pipeline defaults.
|
|
166
171
|
*
|
|
167
172
|
* @param {Record<string, unknown>} config
|
|
168
173
|
* @returns {string[]}
|
package/src/core/stages.js
CHANGED
|
@@ -24,11 +24,14 @@ import {
|
|
|
24
24
|
* - GitHub Actions push: every enabled env with trigger "push".
|
|
25
25
|
* - workflow_dispatch: none here (explicit deploy step handles --env).
|
|
26
26
|
*
|
|
27
|
+
* Separate from workflow secret injection: CI may inject secrets for all
|
|
28
|
+
* enabled envs, but only `trigger: "push"` envs are deployed on push.
|
|
29
|
+
*
|
|
27
30
|
* @param {import('./config.js').DeployHubConfig} config
|
|
28
31
|
* @param {Record<string, string|undefined>} [env]
|
|
29
32
|
* @returns {string[]}
|
|
30
33
|
*/
|
|
31
|
-
function pipelineDeployTargets(config, env = process.env) {
|
|
34
|
+
export function pipelineDeployTargets(config, env = process.env) {
|
|
32
35
|
if (env.GITHUB_EVENT_NAME === 'workflow_dispatch') {
|
|
33
36
|
return [];
|
|
34
37
|
}
|
|
@@ -5,7 +5,11 @@ import path from 'path';
|
|
|
5
5
|
* @param {Record<string, string>} [_env]
|
|
6
6
|
*/
|
|
7
7
|
export function createLocalProvider(_env = process.env) {
|
|
8
|
-
|
|
8
|
+
// Tests / isolated runs may pin storage under a temp dir without process.chdir
|
|
9
|
+
// (chdir races when Jest runs suites in parallel).
|
|
10
|
+
const baseDir = _env.DEPLOYHUB_LOCAL_STORAGE_DIR
|
|
11
|
+
? path.resolve(_env.DEPLOYHUB_LOCAL_STORAGE_DIR)
|
|
12
|
+
: path.join(process.cwd(), '.deployhub-storage');
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* @param {string} localPath
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
envUsesPrefixedSecrets,
|
|
15
15
|
DEPLOYMENT_ENV_KEYS,
|
|
16
16
|
} from '../deployment/deployment-env.js';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
getEnvMethod,
|
|
19
|
+
getEnabledEnvironmentNames,
|
|
20
|
+
isEnvEnabled,
|
|
21
|
+
} from '../core/environments.js';
|
|
18
22
|
|
|
19
23
|
/** @typedef {'aws'|'azure'|'gcp'|'gdrive'|'dropbox'|'local'|'ftp'|'ssh'} ProviderEnvKey */
|
|
20
24
|
|
|
@@ -259,9 +263,41 @@ function hasKubernetesDeploy(deployEnvironments, environments) {
|
|
|
259
263
|
}
|
|
260
264
|
|
|
261
265
|
/**
|
|
266
|
+
* Secret name for the Configure-kubeconfig setup step.
|
|
267
|
+
* Must match the env that actually uses kubernetes — when the only k8s env is
|
|
268
|
+
* non-grandfathered (e.g. production), that is PRODUCTION_KUBECONFIG, not the
|
|
269
|
+
* unprefixed KUBECONFIG (which would never be injected and leave the file empty).
|
|
270
|
+
*
|
|
271
|
+
* LIMITATION (follow-up): GitHub Actions writes exactly ONE kubeconfig file per
|
|
272
|
+
* job. Multiple Kubernetes environments that target DIFFERENT clusters in the
|
|
273
|
+
* same workflow run are not yet fully supported — only the first/grandfathered
|
|
274
|
+
* k8s env's secret is used for the setup step. Track as a product follow-up
|
|
275
|
+
* before advertising multi-cluster multi-env CI.
|
|
276
|
+
*
|
|
277
|
+
* @param {string[]} deployEnvironments
|
|
278
|
+
* @param {Record<string, { type?: string, method?: string }>} environments
|
|
279
|
+
* @param {import('../core/config.js').DeployHubConfig|null} config
|
|
280
|
+
* @returns {string}
|
|
281
|
+
*/
|
|
282
|
+
function resolveKubeconfigWorkflowSecretName(deployEnvironments, environments, config) {
|
|
283
|
+
const cfg = { ...(config || {}), environments };
|
|
284
|
+
const k8sNames = deployEnvironments.filter(
|
|
285
|
+
(n) => getEnvMethod(environments[n]) === 'kubernetes'
|
|
286
|
+
);
|
|
287
|
+
if (k8sNames.length === 0) return 'KUBECONFIG';
|
|
288
|
+
const unprefixed = k8sNames.find((n) => !envUsesPrefixedSecrets(n, cfg));
|
|
289
|
+
const chosen = unprefixed || k8sNames[0];
|
|
290
|
+
return envUsesPrefixedSecrets(chosen, cfg)
|
|
291
|
+
? prefixSecretKey(chosen, 'KUBECONFIG')
|
|
292
|
+
: 'KUBECONFIG';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* @param {string} [kubeconfigSecretName]
|
|
262
297
|
* @returns {string}
|
|
263
298
|
*/
|
|
264
|
-
function getKubernetesSetupSteps() {
|
|
299
|
+
function getKubernetesSetupSteps(kubeconfigSecretName = 'KUBECONFIG') {
|
|
300
|
+
// One kubeconfig file per job — see resolveKubeconfigWorkflowSecretName LIMITATION.
|
|
265
301
|
return ` - name: Setup kubectl
|
|
266
302
|
uses: azure/setup-kubectl@v4
|
|
267
303
|
with:
|
|
@@ -269,7 +305,7 @@ function getKubernetesSetupSteps() {
|
|
|
269
305
|
|
|
270
306
|
- name: Configure kubeconfig
|
|
271
307
|
env:
|
|
272
|
-
KUBECONFIG_SECRET: \${{ secrets
|
|
308
|
+
KUBECONFIG_SECRET: \${{ secrets.${kubeconfigSecretName} }}
|
|
273
309
|
run: |
|
|
274
310
|
mkdir -p "$GITHUB_WORKSPACE/.kube"
|
|
275
311
|
if echo "$KUBECONFIG_SECRET" | base64 -d > "$GITHUB_WORKSPACE/.kube/config" 2>/dev/null; then
|
|
@@ -316,27 +352,6 @@ function upsertWorkflowEnvLine(envVars, lhs, valueExpr) {
|
|
|
316
352
|
envVars.add(`${linePrefix}${valueExpr}`);
|
|
317
353
|
}
|
|
318
354
|
|
|
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
|
-
|
|
340
355
|
/**
|
|
341
356
|
* Shared env entries for deploy and rollback workflows (same secret resolution).
|
|
342
357
|
* Multi-env: each environment contributes its own CI secret names (grandfathered
|
|
@@ -400,9 +415,8 @@ export function buildWorkflowEnvEntries(
|
|
|
400
415
|
* @returns {string}
|
|
401
416
|
*/
|
|
402
417
|
function formatEnvironmentChoiceOptions(environments) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
);
|
|
418
|
+
// Single source of truth: getEnabledEnvironmentNames (do not re-filter here).
|
|
419
|
+
const names = getEnabledEnvironmentNames({ environments });
|
|
406
420
|
const options = [...names, 'all'];
|
|
407
421
|
return options.map((n) => ` - ${n}`).join('\n');
|
|
408
422
|
}
|
|
@@ -457,11 +471,10 @@ export function generateWorkflowYaml(
|
|
|
457
471
|
config = null
|
|
458
472
|
) {
|
|
459
473
|
const envNames = Object.keys(environments || {});
|
|
460
|
-
// Secret injection
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
const enabledEnvs =
|
|
464
|
-
const pushEnvs = listPushEnvironmentNames(environments);
|
|
474
|
+
// Secret injection uses ALL enabled environments (not push-only / pipelineDeployTargets).
|
|
475
|
+
// Prefer live enabled names over a possibly-stale deploy[] argument so no enabled
|
|
476
|
+
// env is missing from the job env block. Canonical helper: getEnabledEnvironmentNames.
|
|
477
|
+
const enabledEnvs = getEnabledEnvironmentNames({ ...(config || {}), environments });
|
|
465
478
|
const allDeployNames =
|
|
466
479
|
enabledEnvs.length > 0
|
|
467
480
|
? enabledEnvs
|
|
@@ -469,28 +482,21 @@ export function generateWorkflowYaml(
|
|
|
469
482
|
? deployEnvironments
|
|
470
483
|
: envNames;
|
|
471
484
|
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
//
|
|
477
|
-
const
|
|
485
|
+
// CRITICAL: Build and Deploy (workflow_dispatch) MUST share the same secret union —
|
|
486
|
+
// every enabled environment. Filtering Build to push-only caused a live regression:
|
|
487
|
+
// Dispatch correctly got PRODUCTION_* while Build did not, so `deployhub build`'s
|
|
488
|
+
// push deploy stage failed on production with a missing SSH key. Trigger only
|
|
489
|
+
// controls which envs the CLI deploys; it must not control which secrets are injected.
|
|
490
|
+
const secretEnvs =
|
|
491
|
+
allDeployNames.length > 0 ? allDeployNames : envNames.slice(0, 1);
|
|
478
492
|
|
|
479
|
-
const
|
|
480
|
-
storageProviders,
|
|
481
|
-
buildSecretEnvs,
|
|
482
|
-
environments,
|
|
483
|
-
config
|
|
484
|
-
);
|
|
485
|
-
const buildEnvBlock = formatWorkflowEnvBlock(buildEnvVars);
|
|
486
|
-
|
|
487
|
-
const dispatchEnvVars = buildWorkflowEnvEntries(
|
|
493
|
+
const envVars = buildWorkflowEnvEntries(
|
|
488
494
|
storageProviders,
|
|
489
|
-
|
|
495
|
+
secretEnvs,
|
|
490
496
|
environments,
|
|
491
497
|
config
|
|
492
498
|
);
|
|
493
|
-
const
|
|
499
|
+
const envBlock = formatWorkflowEnvBlock(envVars);
|
|
494
500
|
|
|
495
501
|
const installSpec = getCliInstallSpec(cliSource);
|
|
496
502
|
const backendSteps = getBackendSetupSteps(config);
|
|
@@ -498,8 +504,13 @@ export function generateWorkflowYaml(
|
|
|
498
504
|
const githubGitConfigStep = isGithubCliSource(cliSource)
|
|
499
505
|
? `${getGithubGitConfigStep()}\n`
|
|
500
506
|
: '';
|
|
507
|
+
const kubeconfigSecret = resolveKubeconfigWorkflowSecretName(
|
|
508
|
+
allDeployNames,
|
|
509
|
+
environments,
|
|
510
|
+
config
|
|
511
|
+
);
|
|
501
512
|
const kubernetesSteps = hasKubernetesDeploy(allDeployNames, environments)
|
|
502
|
-
? `${getKubernetesSetupSteps()}\n`
|
|
513
|
+
? `${getKubernetesSetupSteps(kubeconfigSecret)}\n`
|
|
503
514
|
: '';
|
|
504
515
|
|
|
505
516
|
const projectType = config?.projectType || 'frontend';
|
|
@@ -527,7 +538,7 @@ ${envChoiceOptions}
|
|
|
527
538
|
? ` - name: Deploy (workflow_dispatch)
|
|
528
539
|
if: github.event_name == 'workflow_dispatch'
|
|
529
540
|
env:
|
|
530
|
-
${
|
|
541
|
+
${envBlock}
|
|
531
542
|
run: |
|
|
532
543
|
ENV_INPUT="\${{ inputs.environment }}"
|
|
533
544
|
if [ -z "$ENV_INPUT" ] || [ "$ENV_INPUT" = "all" ]; then
|
|
@@ -557,7 +568,7 @@ ${kubernetesSteps}${githubGitConfigStep} - name: Install project dependenci
|
|
|
557
568
|
- name: Build (and auto-deploy push-triggered envs)
|
|
558
569
|
run: ${getCliBuildCommand()}
|
|
559
570
|
env:
|
|
560
|
-
${
|
|
571
|
+
${envBlock}
|
|
561
572
|
${manualDeployStep}`;
|
|
562
573
|
|
|
563
574
|
return workflow;
|
|
@@ -589,7 +600,7 @@ export function generateRollbackWorkflowYaml(
|
|
|
589
600
|
config = null
|
|
590
601
|
) {
|
|
591
602
|
const envNames = Object.keys(environments || {});
|
|
592
|
-
const enabledEnvs =
|
|
603
|
+
const enabledEnvs = getEnabledEnvironmentNames({ ...(config || {}), environments });
|
|
593
604
|
const allDeployNames =
|
|
594
605
|
enabledEnvs.length > 0
|
|
595
606
|
? enabledEnvs
|
|
@@ -609,8 +620,13 @@ export function generateRollbackWorkflowYaml(
|
|
|
609
620
|
const githubGitConfigStep = isGithubCliSource(cliSource)
|
|
610
621
|
? `${getGithubGitConfigStep()}\n`
|
|
611
622
|
: '';
|
|
623
|
+
const kubeconfigSecret = resolveKubeconfigWorkflowSecretName(
|
|
624
|
+
allDeployNames,
|
|
625
|
+
environments,
|
|
626
|
+
config
|
|
627
|
+
);
|
|
612
628
|
const kubernetesSteps = hasKubernetesDeploy(allDeployNames, environments)
|
|
613
|
-
? `${getKubernetesSetupSteps()}\n`
|
|
629
|
+
? `${getKubernetesSetupSteps(kubeconfigSecret)}\n`
|
|
614
630
|
: '';
|
|
615
631
|
|
|
616
632
|
const rollbackCmd = getCliRollbackCommand();
|
|
@@ -790,13 +806,14 @@ export function expectedWorkflowSecretKeysFromConfig(config, kind = 'rollback')
|
|
|
790
806
|
/** @type {string[]} */
|
|
791
807
|
let targets;
|
|
792
808
|
if (kind === 'deploy') {
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
// secrets by using the broader enabled set via kind === 'rollback' / checklist.
|
|
797
|
-
targets = pushEnvs.length > 0 ? pushEnvs : enabled.slice(0, 1);
|
|
809
|
+
const enabled = getEnabledEnvironmentNames({ ...config, environments });
|
|
810
|
+
// Same union as generateWorkflowYaml Build + dispatch steps (all enabled).
|
|
811
|
+
targets = enabled.length > 0 ? enabled : allNames.slice(0, 1);
|
|
798
812
|
} else {
|
|
799
|
-
targets =
|
|
813
|
+
targets =
|
|
814
|
+
allNames.length > 0
|
|
815
|
+
? getEnabledEnvironmentNames({ ...config, environments })
|
|
816
|
+
: allNames;
|
|
800
817
|
if (targets.length === 0) targets = allNames;
|
|
801
818
|
}
|
|
802
819
|
|
|
@@ -851,7 +868,7 @@ export function detectWorkflowConfigDrift(yamlText, config, filename = DEPLOY_WO
|
|
|
851
868
|
return { drifted: false, missingEnvs, missingSecrets, summary: '' };
|
|
852
869
|
}
|
|
853
870
|
|
|
854
|
-
const envNames =
|
|
871
|
+
const envNames = getEnabledEnvironmentNames(config);
|
|
855
872
|
const root = /** @type {Record<string, any>} */ (parsed);
|
|
856
873
|
const options = root?.on?.workflow_dispatch?.inputs?.environment?.options;
|
|
857
874
|
|