@akash-chowdhury-24/deployhub 2.0.16 → 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/commands/env.js +22 -22
- package/src/commands/init.js +14 -0
- package/src/commands/sync-workflows.js +4 -1
- package/src/core/config.js +2 -2
- package/src/core/environments.js +6 -1
- package/src/core/stages.js +4 -1
- package/src/deployment/init-prompts.js +51 -0
- package/src/storage/providers/local.js +5 -1
- package/src/utils/github-actions.js +134 -49
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/commands/env.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
saveConfig,
|
|
7
7
|
} from '../core/config.js';
|
|
8
8
|
import {
|
|
9
|
+
getEnabledEnvironmentNames,
|
|
9
10
|
getEnvMethod,
|
|
10
11
|
getEnvTrigger,
|
|
11
12
|
isEnvEnabled,
|
|
@@ -25,6 +26,21 @@ import {
|
|
|
25
26
|
envLatestArtifactRemoteKey,
|
|
26
27
|
} from '../utils/build-id.js';
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Regenerate GitHub Actions workflows after env enable/disable/add/remove.
|
|
31
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
32
|
+
* @param {string} cwd
|
|
33
|
+
*/
|
|
34
|
+
async function regenerateWorkflows(config, cwd) {
|
|
35
|
+
await writeWorkflowFile(
|
|
36
|
+
config.storage || [],
|
|
37
|
+
getEnabledEnvironmentNames(config),
|
|
38
|
+
config.environments || {},
|
|
39
|
+
cwd,
|
|
40
|
+
config.cli?.source,
|
|
41
|
+
config
|
|
42
|
+
);
|
|
43
|
+
}
|
|
28
44
|
/**
|
|
29
45
|
* Best-effort last buildId for an env from per-env deploy history.
|
|
30
46
|
* @param {import('../core/config.js').DeployHubConfig} config
|
|
@@ -172,17 +188,7 @@ export function registerEnvCommand(program) {
|
|
|
172
188
|
|
|
173
189
|
await saveConfig(config, cwd);
|
|
174
190
|
|
|
175
|
-
|
|
176
|
-
isEnvEnabled(config.environments[n])
|
|
177
|
-
);
|
|
178
|
-
await writeWorkflowFile(
|
|
179
|
-
config.storage || [],
|
|
180
|
-
enabledNames,
|
|
181
|
-
config.environments,
|
|
182
|
-
cwd,
|
|
183
|
-
config.cli?.source,
|
|
184
|
-
config
|
|
185
|
-
);
|
|
191
|
+
await regenerateWorkflows(config, cwd);
|
|
186
192
|
|
|
187
193
|
log.success(`Added environment "${name}" (${deployAnswers.deployType})`);
|
|
188
194
|
console.log(chalk.gray(` defaultEnvironment: ${config.defaultEnvironment}`));
|
|
@@ -234,7 +240,9 @@ export function registerEnvCommand(program) {
|
|
|
234
240
|
}
|
|
235
241
|
config.environments[name].enabled = true;
|
|
236
242
|
await saveConfig(config, cwd);
|
|
243
|
+
await regenerateWorkflows(config, cwd);
|
|
237
244
|
console.log(chalk.green(`✓ Enabled environment "${name}"`));
|
|
245
|
+
console.log(chalk.gray(' Workflows regenerated — commit .github/workflows if using CI.'));
|
|
238
246
|
});
|
|
239
247
|
|
|
240
248
|
env
|
|
@@ -250,7 +258,9 @@ export function registerEnvCommand(program) {
|
|
|
250
258
|
}
|
|
251
259
|
config.environments[name].enabled = false;
|
|
252
260
|
await saveConfig(config, cwd);
|
|
261
|
+
await regenerateWorkflows(config, cwd);
|
|
253
262
|
console.log(chalk.yellow(`Disabled environment "${name}"`));
|
|
263
|
+
console.log(chalk.gray(' Workflows regenerated — commit .github/workflows if using CI.'));
|
|
254
264
|
});
|
|
255
265
|
|
|
256
266
|
env
|
|
@@ -301,17 +311,7 @@ export function registerEnvCommand(program) {
|
|
|
301
311
|
}
|
|
302
312
|
await saveConfig(config, cwd);
|
|
303
313
|
|
|
304
|
-
|
|
305
|
-
isEnvEnabled(config.environments[n])
|
|
306
|
-
);
|
|
307
|
-
await writeWorkflowFile(
|
|
308
|
-
config.storage || [],
|
|
309
|
-
enabledNames,
|
|
310
|
-
config.environments,
|
|
311
|
-
cwd,
|
|
312
|
-
config.cli?.source,
|
|
313
|
-
config
|
|
314
|
-
);
|
|
314
|
+
await regenerateWorkflows(config, cwd);
|
|
315
315
|
|
|
316
316
|
console.log(chalk.green(`✓ Removed environment "${name}"`));
|
|
317
317
|
console.log(
|
package/src/commands/init.js
CHANGED
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
promptServerDeployment,
|
|
29
29
|
buildServerEnvEntry,
|
|
30
30
|
getDockerEnvSecrets,
|
|
31
|
+
applyInitTriggerDefaults,
|
|
32
|
+
formatMultiEnvTriggerReminder,
|
|
31
33
|
SSH_BASED,
|
|
32
34
|
} from '../deployment/init-prompts.js';
|
|
33
35
|
import {
|
|
@@ -452,6 +454,8 @@ export function registerInitCommand(program) {
|
|
|
452
454
|
defaultEnvironment = picked.defaultEnvironment;
|
|
453
455
|
}
|
|
454
456
|
|
|
457
|
+
applyInitTriggerDefaults(environments, deploy, defaultEnvironment);
|
|
458
|
+
|
|
455
459
|
const version = await getProjectVersion(cwd);
|
|
456
460
|
let hasDocker =
|
|
457
461
|
(detectedFrontend?.hasDocker || detectedBackend?.hasDocker) ?? false;
|
|
@@ -571,6 +575,16 @@ export function registerInitCommand(program) {
|
|
|
571
575
|
console.log(' • .github/workflows/deployhub-rollback.yml');
|
|
572
576
|
console.log(' • .env.example');
|
|
573
577
|
console.log('');
|
|
578
|
+
|
|
579
|
+
if (deploy.length >= 2 && defaultEnvironment) {
|
|
580
|
+
console.log(
|
|
581
|
+
chalk.yellow(
|
|
582
|
+
formatMultiEnvTriggerReminder(String(defaultEnvironment), deploy)
|
|
583
|
+
)
|
|
584
|
+
);
|
|
585
|
+
console.log('');
|
|
586
|
+
}
|
|
587
|
+
|
|
574
588
|
printAuthorFooter();
|
|
575
589
|
});
|
|
576
590
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadConfig, loadEnv } from '../core/config.js';
|
|
3
|
+
import { getEnabledEnvironmentNames } from '../core/environments.js';
|
|
3
4
|
import {
|
|
4
5
|
writeWorkflowFile,
|
|
5
6
|
DEPLOY_WORKFLOW_FILENAME,
|
|
@@ -22,8 +23,10 @@ export function registerSyncWorkflowsCommand(program) {
|
|
|
22
23
|
const config = await loadConfig(cwd);
|
|
23
24
|
|
|
24
25
|
const storage = config.storage || [];
|
|
25
|
-
const deploy = config.deploy || [];
|
|
26
26
|
const environments = config.environments || {};
|
|
27
|
+
// Prefer enabled environments over legacy deploy[] so every reachable env
|
|
28
|
+
// gets its secrets into the regenerated workflow env blocks.
|
|
29
|
+
const deploy = getEnabledEnvironmentNames(config);
|
|
27
30
|
const cliSource = config.cli?.source;
|
|
28
31
|
|
|
29
32
|
await writeWorkflowFile(storage, deploy, environments, cwd, cliSource, config);
|
package/src/core/config.js
CHANGED
|
@@ -290,7 +290,7 @@ export function migrateConfigToEnvironments(raw) {
|
|
|
290
290
|
newEnvironments.default = {
|
|
291
291
|
enabled: true,
|
|
292
292
|
method,
|
|
293
|
-
trigger: '
|
|
293
|
+
trigger: 'push',
|
|
294
294
|
config: flatConfig,
|
|
295
295
|
};
|
|
296
296
|
for (const key of FLAT_DEPLOY_KEYS) {
|
|
@@ -323,7 +323,7 @@ export function migrateConfigToEnvironments(raw) {
|
|
|
323
323
|
newEnvironments[defaultEnvironment] = {
|
|
324
324
|
enabled: true,
|
|
325
325
|
method: 'ssh',
|
|
326
|
-
trigger: '
|
|
326
|
+
trigger: 'push',
|
|
327
327
|
config: {},
|
|
328
328
|
};
|
|
329
329
|
}
|
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
|
}
|
|
@@ -539,6 +539,57 @@ export function buildServerEnvEntry(
|
|
|
539
539
|
};
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
+
/**
|
|
543
|
+
* Apply init-time trigger defaults after environments are collected.
|
|
544
|
+
* - Exactly one env → always `push` (git push = auto-deploy).
|
|
545
|
+
* - Two or more → grandfathered/default gets `push`; all others stay `manual`.
|
|
546
|
+
*
|
|
547
|
+
* @param {Record<string, { trigger?: string }>} environments
|
|
548
|
+
* @param {string[]} deployNames
|
|
549
|
+
* @param {string|undefined|null} defaultEnvironment
|
|
550
|
+
*/
|
|
551
|
+
export function applyInitTriggerDefaults(environments, deployNames, defaultEnvironment) {
|
|
552
|
+
if (deployNames.length === 1 && environments[deployNames[0]]) {
|
|
553
|
+
environments[deployNames[0]].trigger = 'push';
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (deployNames.length >= 2 && defaultEnvironment) {
|
|
557
|
+
for (const name of deployNames) {
|
|
558
|
+
if (!environments[name]) continue;
|
|
559
|
+
environments[name].trigger =
|
|
560
|
+
name === defaultEnvironment ? 'push' : 'manual';
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* End-of-init reminder for multi-env setups (grandfathered = push, others = manual).
|
|
567
|
+
* @param {string} grandfathered
|
|
568
|
+
* @param {string[]} allEnvNames
|
|
569
|
+
* @returns {string}
|
|
570
|
+
*/
|
|
571
|
+
export function formatMultiEnvTriggerReminder(grandfathered, allEnvNames) {
|
|
572
|
+
const others = allEnvNames.filter((n) => n !== grandfathered);
|
|
573
|
+
const otherList =
|
|
574
|
+
others.length > 0 ? others.map((n) => `"${n}"`).join(', ') : '(none)';
|
|
575
|
+
const exampleEnv = others[0] || '<env-name>';
|
|
576
|
+
return [
|
|
577
|
+
'─────────────────────────────────────────────',
|
|
578
|
+
`By default, only your first environment ("${grandfathered}")`,
|
|
579
|
+
`auto-deploys on push. Your other environment(s) — ${otherList} — are set to manual and will only deploy via:`,
|
|
580
|
+
' deployhub deploy --env <name>',
|
|
581
|
+
'or GitHub Actions → Run workflow.',
|
|
582
|
+
'',
|
|
583
|
+
'To make an environment auto-deploy on push instead, open',
|
|
584
|
+
'deployhub.config.json and change its "trigger" to "push":',
|
|
585
|
+
' "environments": {',
|
|
586
|
+
` "${exampleEnv}": { "trigger": "push", ... }`,
|
|
587
|
+
' }',
|
|
588
|
+
'Then run: deployhub sync-workflows',
|
|
589
|
+
'─────────────────────────────────────────────',
|
|
590
|
+
].join('\n');
|
|
591
|
+
}
|
|
592
|
+
|
|
542
593
|
/**
|
|
543
594
|
* @param {Awaited<ReturnType<typeof promptServerDeployment>>} deployAnswers
|
|
544
595
|
* @returns {Record<string, string>|null}
|
|
@@ -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
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
4
5
|
import { getWorkflowHeaderComment } from './author.js';
|
|
5
6
|
import {
|
|
6
7
|
generateDeploymentEnvSection,
|
|
@@ -13,7 +14,11 @@ import {
|
|
|
13
14
|
envUsesPrefixedSecrets,
|
|
14
15
|
DEPLOYMENT_ENV_KEYS,
|
|
15
16
|
} from '../deployment/deployment-env.js';
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
getEnvMethod,
|
|
19
|
+
getEnabledEnvironmentNames,
|
|
20
|
+
isEnvEnabled,
|
|
21
|
+
} from '../core/environments.js';
|
|
17
22
|
|
|
18
23
|
/** @typedef {'aws'|'azure'|'gcp'|'gdrive'|'dropbox'|'local'|'ftp'|'ssh'} ProviderEnvKey */
|
|
19
24
|
|
|
@@ -135,6 +140,34 @@ export function getCliInstallSpec(cliSource) {
|
|
|
135
140
|
return normalized;
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Version/range suitable as a package.json dependency VALUE for this CLI
|
|
145
|
+
* (key is already NPM_PACKAGE — do not embed the package name again).
|
|
146
|
+
*
|
|
147
|
+
* @param {string} [cliSource]
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
export function getCliPackageJsonDependencyVersion(cliSource) {
|
|
151
|
+
const normalized = normalizeCliSource(cliSource);
|
|
152
|
+
if (normalized === DEFAULT_NPM_CLI_SOURCE) {
|
|
153
|
+
try {
|
|
154
|
+
const pkgPath = path.join(
|
|
155
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
156
|
+
'../../package.json'
|
|
157
|
+
);
|
|
158
|
+
const pkg = fs.readJsonSync(pkgPath);
|
|
159
|
+
if (typeof pkg.version === 'string' && pkg.version.trim()) {
|
|
160
|
+
return `^${pkg.version.trim()}`;
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// fall through
|
|
164
|
+
}
|
|
165
|
+
return 'latest';
|
|
166
|
+
}
|
|
167
|
+
// github: / file: specs are valid package.json dependency values as-is
|
|
168
|
+
return getCliInstallSpec(cliSource);
|
|
169
|
+
}
|
|
170
|
+
|
|
138
171
|
/**
|
|
139
172
|
* Shell command to run deployhub build from the installed scoped package.
|
|
140
173
|
* Uses node directly so it works reliably when installed from a private GitHub repo.
|
|
@@ -230,9 +263,41 @@ function hasKubernetesDeploy(deployEnvironments, environments) {
|
|
|
230
263
|
}
|
|
231
264
|
|
|
232
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
|
|
233
280
|
* @returns {string}
|
|
234
281
|
*/
|
|
235
|
-
function
|
|
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]
|
|
297
|
+
* @returns {string}
|
|
298
|
+
*/
|
|
299
|
+
function getKubernetesSetupSteps(kubeconfigSecretName = 'KUBECONFIG') {
|
|
300
|
+
// One kubeconfig file per job — see resolveKubeconfigWorkflowSecretName LIMITATION.
|
|
236
301
|
return ` - name: Setup kubectl
|
|
237
302
|
uses: azure/setup-kubectl@v4
|
|
238
303
|
with:
|
|
@@ -240,7 +305,7 @@ function getKubernetesSetupSteps() {
|
|
|
240
305
|
|
|
241
306
|
- name: Configure kubeconfig
|
|
242
307
|
env:
|
|
243
|
-
KUBECONFIG_SECRET: \${{ secrets
|
|
308
|
+
KUBECONFIG_SECRET: \${{ secrets.${kubeconfigSecretName} }}
|
|
244
309
|
run: |
|
|
245
310
|
mkdir -p "$GITHUB_WORKSPACE/.kube"
|
|
246
311
|
if echo "$KUBECONFIG_SECRET" | base64 -d > "$GITHUB_WORKSPACE/.kube/config" 2>/dev/null; then
|
|
@@ -289,29 +354,28 @@ function upsertWorkflowEnvLine(envVars, lhs, valueExpr) {
|
|
|
289
354
|
|
|
290
355
|
/**
|
|
291
356
|
* Shared env entries for deploy and rollback workflows (same secret resolution).
|
|
292
|
-
* Multi-env:
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
357
|
+
* Multi-env: each environment contributes its own CI secret names (grandfathered
|
|
358
|
+
* unprefixed SSH_HOST, or PRODUCTION_SSH_HOST, etc.). Prefixed envs are NOT
|
|
359
|
+
* last-wins-mapped onto unprefixed keys — that would overwrite the grandfathered
|
|
360
|
+
* binding and leave production looking fine in YAML while development silently
|
|
361
|
+
* inherits the wrong credentials. `applyEnvSecretOverlay` remaps PREFIX_* →
|
|
362
|
+
* unprefixed names at deploy time for non-grandfathered targets.
|
|
297
363
|
*
|
|
298
364
|
* @param {string[]} storageProviders
|
|
299
|
-
* @param {string[]} deployEnvironments
|
|
365
|
+
* @param {string[]} deployEnvironments — environments whose secrets to inject
|
|
300
366
|
* @param {Record<string, { type?: string, method?: string }>} environments
|
|
301
367
|
* @param {import('../core/config.js').DeployHubConfig} [config]
|
|
302
|
-
* @param {{ mapForEnv?: string|null }} [options] — when set, only map that env's secrets to unprefixed keys
|
|
303
368
|
* @returns {Set<string>}
|
|
304
369
|
*/
|
|
305
370
|
export function buildWorkflowEnvEntries(
|
|
306
371
|
storageProviders,
|
|
307
372
|
deployEnvironments,
|
|
308
373
|
environments,
|
|
309
|
-
config = null
|
|
310
|
-
options = {}
|
|
374
|
+
config = null
|
|
311
375
|
) {
|
|
312
376
|
/** @type {Set<string>} */
|
|
313
377
|
const envVars = new Set([
|
|
314
|
-
`DEPLOYHUB_ENV: ${
|
|
378
|
+
`DEPLOYHUB_ENV: ${deployEnvironments[0] || 'production'}`,
|
|
315
379
|
]);
|
|
316
380
|
|
|
317
381
|
for (const provider of storageProviders) {
|
|
@@ -321,13 +385,9 @@ export function buildWorkflowEnvEntries(
|
|
|
321
385
|
}
|
|
322
386
|
}
|
|
323
387
|
|
|
324
|
-
const targets = options.mapForEnv
|
|
325
|
-
? [options.mapForEnv]
|
|
326
|
-
: deployEnvironments;
|
|
327
|
-
|
|
328
388
|
const cfg = { ...(config || {}), environments };
|
|
329
389
|
|
|
330
|
-
for (const envName of
|
|
390
|
+
for (const envName of deployEnvironments) {
|
|
331
391
|
const env = environments[envName];
|
|
332
392
|
if (!env) continue;
|
|
333
393
|
|
|
@@ -338,26 +398,25 @@ export function buildWorkflowEnvEntries(
|
|
|
338
398
|
const secretName = envUsesPrefixedSecrets(envName, cfg)
|
|
339
399
|
? prefixSecretKey(envName, key)
|
|
340
400
|
: key;
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
upsertWorkflowEnvLine(envVars, secretName, `\${{ secrets.${secretName} }}`);
|
|
344
|
-
}
|
|
345
|
-
// Unprefixed mapping for CLI defaults (last target wins in static YAML;
|
|
346
|
-
// applyEnvSecretOverlay restores the correct env's values at deploy time).
|
|
347
|
-
upsertWorkflowEnvLine(envVars, key, `\${{ secrets.${secretName} }}`);
|
|
401
|
+
// Bind process.env[secretName] for this environment (prefixed or grandfathered).
|
|
402
|
+
upsertWorkflowEnvLine(envVars, secretName, `\${{ secrets.${secretName} }}`);
|
|
348
403
|
}
|
|
349
404
|
}
|
|
350
405
|
|
|
351
|
-
applyKubernetesWorkflowEnv(
|
|
406
|
+
applyKubernetesWorkflowEnv(deployEnvironments, environments, envVars);
|
|
352
407
|
return envVars;
|
|
353
408
|
}
|
|
354
409
|
|
|
355
410
|
/**
|
|
411
|
+
* Choice options for workflow_dispatch — enabled environments only, plus `all`.
|
|
412
|
+
* Disabled envs are omitted so selecting them cannot waste a CI run.
|
|
413
|
+
*
|
|
356
414
|
* @param {Record<string, unknown>} environments
|
|
357
415
|
* @returns {string}
|
|
358
416
|
*/
|
|
359
417
|
function formatEnvironmentChoiceOptions(environments) {
|
|
360
|
-
|
|
418
|
+
// Single source of truth: getEnabledEnvironmentNames (do not re-filter here).
|
|
419
|
+
const names = getEnabledEnvironmentNames({ environments });
|
|
361
420
|
const options = [...names, 'all'];
|
|
362
421
|
return options.map((n) => ` - ${n}`).join('\n');
|
|
363
422
|
}
|
|
@@ -412,21 +471,28 @@ export function generateWorkflowYaml(
|
|
|
412
471
|
config = null
|
|
413
472
|
) {
|
|
414
473
|
const envNames = Object.keys(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 });
|
|
415
478
|
const allDeployNames =
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
479
|
+
enabledEnvs.length > 0
|
|
480
|
+
? enabledEnvs
|
|
481
|
+
: deployEnvironments.length > 0
|
|
482
|
+
? deployEnvironments
|
|
483
|
+
: envNames;
|
|
484
|
+
|
|
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);
|
|
426
492
|
|
|
427
493
|
const envVars = buildWorkflowEnvEntries(
|
|
428
494
|
storageProviders,
|
|
429
|
-
|
|
495
|
+
secretEnvs,
|
|
430
496
|
environments,
|
|
431
497
|
config
|
|
432
498
|
);
|
|
@@ -438,8 +504,13 @@ export function generateWorkflowYaml(
|
|
|
438
504
|
const githubGitConfigStep = isGithubCliSource(cliSource)
|
|
439
505
|
? `${getGithubGitConfigStep()}\n`
|
|
440
506
|
: '';
|
|
507
|
+
const kubeconfigSecret = resolveKubeconfigWorkflowSecretName(
|
|
508
|
+
allDeployNames,
|
|
509
|
+
environments,
|
|
510
|
+
config
|
|
511
|
+
);
|
|
441
512
|
const kubernetesSteps = hasKubernetesDeploy(allDeployNames, environments)
|
|
442
|
-
? `${getKubernetesSetupSteps()}\n`
|
|
513
|
+
? `${getKubernetesSetupSteps(kubeconfigSecret)}\n`
|
|
443
514
|
: '';
|
|
444
515
|
|
|
445
516
|
const projectType = config?.projectType || 'frontend';
|
|
@@ -529,10 +600,13 @@ export function generateRollbackWorkflowYaml(
|
|
|
529
600
|
config = null
|
|
530
601
|
) {
|
|
531
602
|
const envNames = Object.keys(environments || {});
|
|
603
|
+
const enabledEnvs = getEnabledEnvironmentNames({ ...(config || {}), environments });
|
|
532
604
|
const allDeployNames =
|
|
533
|
-
|
|
534
|
-
?
|
|
535
|
-
:
|
|
605
|
+
enabledEnvs.length > 0
|
|
606
|
+
? enabledEnvs
|
|
607
|
+
: deployEnvironments.length > 0
|
|
608
|
+
? deployEnvironments
|
|
609
|
+
: envNames.filter((n) => isEnvEnabled(environments[n]));
|
|
536
610
|
|
|
537
611
|
const envVars = buildWorkflowEnvEntries(
|
|
538
612
|
storageProviders,
|
|
@@ -546,8 +620,13 @@ export function generateRollbackWorkflowYaml(
|
|
|
546
620
|
const githubGitConfigStep = isGithubCliSource(cliSource)
|
|
547
621
|
? `${getGithubGitConfigStep()}\n`
|
|
548
622
|
: '';
|
|
623
|
+
const kubeconfigSecret = resolveKubeconfigWorkflowSecretName(
|
|
624
|
+
allDeployNames,
|
|
625
|
+
environments,
|
|
626
|
+
config
|
|
627
|
+
);
|
|
549
628
|
const kubernetesSteps = hasKubernetesDeploy(allDeployNames, environments)
|
|
550
|
-
? `${getKubernetesSetupSteps()}\n`
|
|
629
|
+
? `${getKubernetesSetupSteps(kubeconfigSecret)}\n`
|
|
551
630
|
: '';
|
|
552
631
|
|
|
553
632
|
const rollbackCmd = getCliRollbackCommand();
|
|
@@ -727,11 +806,15 @@ export function expectedWorkflowSecretKeysFromConfig(config, kind = 'rollback')
|
|
|
727
806
|
/** @type {string[]} */
|
|
728
807
|
let targets;
|
|
729
808
|
if (kind === 'deploy') {
|
|
730
|
-
const enabled =
|
|
731
|
-
|
|
732
|
-
targets =
|
|
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);
|
|
733
812
|
} else {
|
|
734
|
-
targets =
|
|
813
|
+
targets =
|
|
814
|
+
allNames.length > 0
|
|
815
|
+
? getEnabledEnvironmentNames({ ...config, environments })
|
|
816
|
+
: allNames;
|
|
817
|
+
if (targets.length === 0) targets = allNames;
|
|
735
818
|
}
|
|
736
819
|
|
|
737
820
|
const entries = buildWorkflowEnvEntries(
|
|
@@ -785,7 +868,7 @@ export function detectWorkflowConfigDrift(yamlText, config, filename = DEPLOY_WO
|
|
|
785
868
|
return { drifted: false, missingEnvs, missingSecrets, summary: '' };
|
|
786
869
|
}
|
|
787
870
|
|
|
788
|
-
const envNames =
|
|
871
|
+
const envNames = getEnabledEnvironmentNames(config);
|
|
789
872
|
const root = /** @type {Record<string, any>} */ (parsed);
|
|
790
873
|
const options = root?.on?.workflow_dispatch?.inputs?.environment?.options;
|
|
791
874
|
|
|
@@ -883,7 +966,9 @@ export async function addDeployhubToPackageJson(cliSource, cwd = process.cwd())
|
|
|
883
966
|
|
|
884
967
|
const pkg = await fs.readJson(pkgPath);
|
|
885
968
|
pkg.devDependencies = pkg.devDependencies || {};
|
|
886
|
-
|
|
969
|
+
// package.json value must be a semver range / "latest" / git URL — never "name@version"
|
|
970
|
+
// (that form is only for `npm install <spec>` via getCliInstallSpec).
|
|
971
|
+
pkg.devDependencies[NPM_PACKAGE] = getCliPackageJsonDependencyVersion(cliSource);
|
|
887
972
|
delete pkg.devDependencies.deployhub;
|
|
888
973
|
pkg.scripts = pkg.scripts || {};
|
|
889
974
|
pkg.scripts['deployhub:build'] = 'deployhub build';
|