@akash-chowdhury-24/deployhub 2.0.9 → 2.0.11
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 +46 -18
- package/package.json +1 -1
- package/src/commands/doctor.js +18 -0
- package/src/deployment/deployment-env.js +11 -5
- package/src/deployment/providers/kubernetes.js +10 -16
- package/src/utils/docker-image-deploy.js +9 -1
- package/src/utils/docker-image.js +86 -2
- package/src/utils/kubernetes-deploy-image.js +111 -0
- package/src/utils/kubernetes-manifests.js +1 -0
package/README.md
CHANGED
|
@@ -675,6 +675,8 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
675
675
|
|
|
676
676
|
### Docker
|
|
677
677
|
|
|
678
|
+
**Verification:** Real-world verified (local and CI Docker deploys).
|
|
679
|
+
|
|
678
680
|
**Prerequisites:**
|
|
679
681
|
- [ ] Docker installed (`docker --version` works)
|
|
680
682
|
- [ ] Registry account if pushing private images
|
|
@@ -682,20 +684,25 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
682
684
|
|
|
683
685
|
**What DeployHub automates:**
|
|
684
686
|
- Starter `Dockerfile` at project root when none exists (framework-aware; skipped if you already have one)
|
|
687
|
+
- `.dockerignore` when missing (never overwrites an existing one)
|
|
685
688
|
- `.env.example` for image name, registry, remote `DOCKER_HOST`
|
|
686
689
|
- Docker daemon connectivity test during `init`
|
|
690
|
+
- Reuses the image built in the pipeline `docker` stage when present; otherwise builds from the artifact
|
|
691
|
+
- Registry login + push when `DOCKER_REGISTRY_USERNAME` / `DOCKER_REGISTRY_TOKEN` are set
|
|
692
|
+
- Auto-generates a unique image tag per build when `DOCKER_IMAGE_TAG` is unset (git SHA → CI run id → timestamp)
|
|
687
693
|
- `docker compose up` or build/push/run during deploy
|
|
688
694
|
|
|
689
695
|
**After `init`:**
|
|
690
|
-
1. Set `DOCKER_IMAGE_NAME` in `.env`
|
|
691
|
-
2. For private registries: set `DOCKER_REGISTRY_USERNAME` and `DOCKER_REGISTRY_TOKEN`
|
|
692
|
-
3.
|
|
693
|
-
4.
|
|
696
|
+
1. Set `DOCKER_IMAGE_NAME` in `.env` (e.g. `myuser/myapp` for Docker Hub)
|
|
697
|
+
2. For private registries (or any push): set `DOCKER_REGISTRY_USERNAME` and `DOCKER_REGISTRY_TOKEN`
|
|
698
|
+
3. Leave `DOCKER_IMAGE_TAG` unset for a unique tag each build — set it only if you intentionally want a fixed tag
|
|
699
|
+
4. For remote Docker: set `DOCKER_HOST` (e.g. `ssh://ubuntu@203.0.113.10`)
|
|
700
|
+
5. Run `deployhub doctor`, then `git push origin main`
|
|
694
701
|
|
|
695
702
|
| Variable | Description | Example | Where to get it |
|
|
696
703
|
|----------|-------------|---------|-----------------|
|
|
697
704
|
| `DOCKER_IMAGE_NAME` | Image repository path | `myorg/myapp` | Your registry naming |
|
|
698
|
-
| `DOCKER_IMAGE_TAG` |
|
|
705
|
+
| `DOCKER_IMAGE_TAG` | Optional fixed tag (unset → unique per build) | `latest` | Your choice; prefer unset |
|
|
699
706
|
| `DOCKER_REGISTRY_URL` | Registry URL (optional) | `https://ghcr.io` | Registry docs |
|
|
700
707
|
| `DOCKER_REGISTRY_USERNAME` | Registry user | `myuser` | Registry account |
|
|
701
708
|
| `DOCKER_REGISTRY_TOKEN` | Registry password/token | *(secret)* | Docker Hub / GHCR PAT |
|
|
@@ -813,34 +820,55 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
813
820
|
|
|
814
821
|
### Kubernetes
|
|
815
822
|
|
|
823
|
+
**Verification:** Real-world verified (including k3s and CI deploys).
|
|
824
|
+
|
|
816
825
|
**Prerequisites:**
|
|
817
826
|
- [ ] Existing Kubernetes cluster (DeployHub does not provision clusters)
|
|
818
827
|
- [ ] `kubectl` installed and configured on your **local machine** (for `deployhub doctor` / manual `deployhub deploy`)
|
|
819
|
-
- [ ] Cluster reachable from CI (kubeconfig secret or cloud auth)
|
|
828
|
+
- [ ] Cluster reachable from CI (kubeconfig **secret contents** or cloud auth — see `KUBECONFIG` below)
|
|
829
|
+
- [ ] Container registry credentials so the cluster can pull the image you push
|
|
830
|
+
|
|
831
|
+
**Init prompts (what `deployhub init` asks for Kubernetes):**
|
|
832
|
+
1. Path to kubeconfig file (e.g. `~/.kube/config`)
|
|
833
|
+
2. Kubernetes context
|
|
834
|
+
3. Namespace (defaults to project name)
|
|
835
|
+
4. Container image name
|
|
836
|
+
5. Registry URL (leave empty for Docker Hub)
|
|
837
|
+
6. Registry username (required to push)
|
|
838
|
+
7. Registry token/password (required to push)
|
|
839
|
+
8. Health check URL (optional)
|
|
820
840
|
|
|
821
841
|
**What DeployHub automates:**
|
|
822
842
|
- Starter `k8s/deployment.yaml` and `k8s/service.yaml` when no manifests exist (skipped if you already have a `k8s/` directory or root-level Kubernetes YAML files)
|
|
823
843
|
- GitHub Actions installs `kubectl` on the CI runner and writes kubeconfig from secrets (no local `kubectl` required for the automated push-to-main deploy path)
|
|
824
844
|
- Lists `kubectl` contexts during `init` for easy selection
|
|
825
845
|
- Auto-detects `~/.kube/config`
|
|
826
|
-
- Complete `.env.example` for kubeconfig, context, namespace
|
|
846
|
+
- Complete `.env.example` for kubeconfig, context, namespace, and registry settings
|
|
827
847
|
- Cluster connectivity test during `init`
|
|
848
|
+
- 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
|
|
828
849
|
|
|
829
850
|
**After `init`:**
|
|
830
851
|
1. Verify context: `kubectl config get-contexts`
|
|
831
|
-
2.
|
|
832
|
-
3.
|
|
833
|
-
4.
|
|
834
|
-
5.
|
|
852
|
+
2. Copy `.env.example` → `.env`; set `DOCKER_IMAGE_NAME`, registry username/token, and (for local deploys) `KUBECONFIG` / `KUBE_CONTEXT` / `KUBE_NAMESPACE` as needed
|
|
853
|
+
3. Leave `DOCKER_IMAGE_TAG` unset for a unique tag each build — set it only if you want a fixed tag (DeployHub will still rollout-restart when the full image ref is unchanged)
|
|
854
|
+
4. Namespace is created on first deploy if missing (you will be prompted locally; CI auto-creates). Or create it yourself: `kubectl create namespace my-app`
|
|
855
|
+
5. For private registries: create an `imagePullSecret` and set `KUBE_IMAGE_PULL_SECRET`
|
|
856
|
+
6. Add GitHub Secrets for CI (see table — **`KUBECONFIG` must be the file contents, not a path**)
|
|
857
|
+
7. Run `deployhub doctor`, then `git push origin main`
|
|
858
|
+
|
|
859
|
+
> **Warning — Service `targetPort`:** the default `targetPort` in generated `k8s/service.yaml` may not match your app's actual exposed port (e.g. a static nginx image serves on port **80**, not the config's default like 3000). Verify and adjust `k8s/service.yaml`'s `targetPort` (and the Deployment `containerPort` if needed) before your first deploy.
|
|
835
860
|
|
|
836
861
|
| Variable | Description | Example | Where to get it |
|
|
837
862
|
|----------|-------------|---------|-----------------|
|
|
838
|
-
| `KUBECONFIG` |
|
|
863
|
+
| `KUBECONFIG` | **Local:** path to kubeconfig. **CI (GitHub Secret):** full kubeconfig **file contents** (or base64) — not a path | `~/.kube/config` locally; paste file contents in CI | `~/.kube/config` |
|
|
839
864
|
| `KUBE_CONTEXT` | Context name | `my-cluster` | `kubectl config get-contexts` |
|
|
840
|
-
| `KUBE_NAMESPACE` | Target namespace | `my-app` |
|
|
841
|
-
| `DOCKER_IMAGE_NAME` | Container image | `ghcr.io/org/app` | Your registry |
|
|
842
|
-
| `DOCKER_IMAGE_TAG` |
|
|
843
|
-
| `
|
|
865
|
+
| `KUBE_NAMESPACE` | Target namespace (optional; defaults to project name) | `my-app` | Your choice |
|
|
866
|
+
| `DOCKER_IMAGE_NAME` | Container image repository | `myuser/myapp` or `ghcr.io/org/app` | Your registry |
|
|
867
|
+
| `DOCKER_IMAGE_TAG` | Optional fixed tag (unset → unique per build) | `latest` | Prefer unset |
|
|
868
|
+
| `DOCKER_REGISTRY_URL` | Registry URL (optional; empty = Docker Hub) | `https://ghcr.io` | Registry docs |
|
|
869
|
+
| `DOCKER_REGISTRY_USERNAME` | Registry user (required to push) | `myuser` | Registry account |
|
|
870
|
+
| `DOCKER_REGISTRY_TOKEN` | Registry password/token (required to push) | *(secret)* | Docker Hub token / GHCR PAT |
|
|
871
|
+
| `KUBE_IMAGE_PULL_SECRET` | Pull secret name (optional, private registries) | `regcred` | `kubectl create secret docker-registry` |
|
|
844
872
|
|
|
845
873
|
---
|
|
846
874
|
|
|
@@ -966,8 +994,8 @@ Add these secrets in your repository (Settings → Secrets and variables → Act
|
|
|
966
994
|
| `EC2_INSTANCE_ID`, `AWS_*` | Optional EC2 dynamic IP lookup |
|
|
967
995
|
| `AZURE_SUBSCRIPTION_ID`, `AZURE_RESOURCE_GROUP`, `AZURE_VM_NAME` | Optional Azure VM IP lookup |
|
|
968
996
|
| `GCP_PROJECT_ID`, `GCP_ZONE`, `GCP_INSTANCE_NAME`, `GCP_KEY_FILE` | Optional GCP VM IP lookup |
|
|
969
|
-
| `DOCKER_IMAGE_NAME`, `
|
|
970
|
-
| `KUBECONFIG`, `KUBE_CONTEXT`, `KUBE_NAMESPACE` | Kubernetes
|
|
997
|
+
| `DOCKER_IMAGE_NAME`, `DOCKER_REGISTRY_USERNAME`, `DOCKER_REGISTRY_TOKEN`, `DOCKER_REGISTRY_URL`, `DOCKER_HOST` | Docker deployment (`DOCKER_IMAGE_TAG` optional) |
|
|
998
|
+
| `KUBECONFIG`, `KUBE_CONTEXT`, `KUBE_NAMESPACE`, `DOCKER_IMAGE_NAME`, `DOCKER_REGISTRY_USERNAME`, `DOCKER_REGISTRY_TOKEN`, `DOCKER_REGISTRY_URL`, `DOCKER_IMAGE_TAG`, `KUBE_IMAGE_PULL_SECRET` | Kubernetes — **`KUBECONFIG` in GitHub Secrets must be the kubeconfig file contents (or base64), not a filesystem path**. `DOCKER_IMAGE_TAG`, `KUBE_NAMESPACE`, `DOCKER_REGISTRY_URL`, and `KUBE_IMAGE_PULL_SECRET` are optional |
|
|
971
999
|
|
|
972
1000
|
See [Deployment method guides](#deployment-method-guides) for full per-method variable tables with examples.
|
|
973
1001
|
|
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -450,6 +450,24 @@ async function runDeploymentChecks(config, envName, envConfig) {
|
|
|
450
450
|
})
|
|
451
451
|
);
|
|
452
452
|
|
|
453
|
+
checks.push(
|
|
454
|
+
await runCheck('Image tag strategy', async () => {
|
|
455
|
+
if (process.env.DOCKER_IMAGE_TAG) {
|
|
456
|
+
return {
|
|
457
|
+
name: 'Image tag strategy',
|
|
458
|
+
pass: true,
|
|
459
|
+
message: `DOCKER_IMAGE_TAG='${process.env.DOCKER_IMAGE_TAG}' is set explicitly — deploy will rollout-restart when the full image ref is unchanged; prefer unset for unique tags per build`,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
name: 'Image tag strategy',
|
|
464
|
+
pass: true,
|
|
465
|
+
message:
|
|
466
|
+
'DOCKER_IMAGE_TAG unset — deploy will auto-generate a unique tag (git SHA → CI id → timestamp)',
|
|
467
|
+
};
|
|
468
|
+
})
|
|
469
|
+
);
|
|
470
|
+
|
|
453
471
|
checks.push(
|
|
454
472
|
await runCheck('Container image pullable', async () => {
|
|
455
473
|
const result = await checkImagePullability(config, process.env);
|
|
@@ -96,8 +96,12 @@ export const DEPLOYMENT_ENV_DEFS = {
|
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
key: 'DOCKER_IMAGE_TAG',
|
|
99
|
-
optionalReason:
|
|
100
|
-
|
|
99
|
+
optionalReason:
|
|
100
|
+
'leave unset for a unique tag per build (git SHA → CI run id → timestamp); explicit tags are reused as-is',
|
|
101
|
+
comment: [
|
|
102
|
+
'Optional. Leave unset for a unique tag per build (git SHA, CI run id, or timestamp).',
|
|
103
|
+
'If set explicitly, the same tag is reused — Kubernetes may keep stale pods unless imagePullPolicy is Always or a rollout restart runs.',
|
|
104
|
+
],
|
|
101
105
|
example: 'latest',
|
|
102
106
|
when: 'optional',
|
|
103
107
|
},
|
|
@@ -324,9 +328,11 @@ export const DEPLOYMENT_ENV_DEFS = {
|
|
|
324
328
|
},
|
|
325
329
|
{
|
|
326
330
|
key: 'DOCKER_IMAGE_TAG',
|
|
327
|
-
optionalReason:
|
|
331
|
+
optionalReason:
|
|
332
|
+
'leave unset for a unique tag per build (git SHA → CI run id → timestamp); explicit tags are reused as-is',
|
|
328
333
|
comment: [
|
|
329
|
-
'
|
|
334
|
+
'Optional image tag. Unset → DeployHub auto-generates a unique tag each build.',
|
|
335
|
+
'If set, that exact tag is used — reusing it can leave pods on a stale image (IfNotPresent) unless you rely on deploy-time rollout restart or set imagePullPolicy: Always.',
|
|
330
336
|
],
|
|
331
337
|
example: 'latest',
|
|
332
338
|
when: 'optional',
|
|
@@ -572,7 +578,7 @@ function getDefaultFromConfig(key, config, environments) {
|
|
|
572
578
|
SSH_SSH_PORT: '22',
|
|
573
579
|
KUBE_NAMESPACE: config?.project || 'default',
|
|
574
580
|
DOCKER_IMAGE_NAME: config?.project,
|
|
575
|
-
DOCKER_IMAGE_TAG:
|
|
581
|
+
DOCKER_IMAGE_TAG: '',
|
|
576
582
|
DOCKER_REGISTRY_URL: envEntry.dockerRegistryUrl,
|
|
577
583
|
AWS_REGION: 'us-east-1',
|
|
578
584
|
};
|
|
@@ -6,6 +6,7 @@ import { createLogger } from '../../logger/index.js';
|
|
|
6
6
|
import { sanitizeK8sName } from '../../utils/kubernetes-manifests.js';
|
|
7
7
|
import { createDockerImageDeployContext } from '../../utils/docker-image-deploy.js';
|
|
8
8
|
import { ensureKubernetesNamespace } from '../../utils/kubernetes-namespace.js';
|
|
9
|
+
import { syncKubernetesDeploymentImage } from '../../utils/kubernetes-deploy-image.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @param {import('../../core/config.js').DeployHubConfig} config
|
|
@@ -88,22 +89,15 @@ export function createKubernetesProvider(config, envName, env = process.env) {
|
|
|
88
89
|
env: getKubectlEnv(),
|
|
89
90
|
});
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
`${deploymentName}=${imageName}:${imageTag}`,
|
|
101
|
-
]),
|
|
102
|
-
{ stdio: 'pipe', env: getKubectlEnv() }
|
|
103
|
-
).catch(() => {
|
|
104
|
-
log.warn('kubectl set image skipped (deployment name may differ from project name)');
|
|
105
|
-
});
|
|
106
|
-
}
|
|
92
|
+
// Always set image to the resolved fullImage (includes registry URL prefix when set).
|
|
93
|
+
// If the live ref already equals fullImage, rollout restart so a new digest is pulled.
|
|
94
|
+
await syncKubernetesDeploymentImage({
|
|
95
|
+
deploymentName,
|
|
96
|
+
fullImage: imageOps.fullImage,
|
|
97
|
+
kubectlArgs,
|
|
98
|
+
getKubectlEnv,
|
|
99
|
+
log,
|
|
100
|
+
});
|
|
107
101
|
|
|
108
102
|
log.success('Kubernetes deployment complete');
|
|
109
103
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
isFrontendStaticFramework,
|
|
12
12
|
isInterpretedBackendFramework,
|
|
13
13
|
resolveDockerImageRef,
|
|
14
|
+
EXPLICIT_IMAGE_TAG_WARNING,
|
|
14
15
|
} from './docker-image.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -22,13 +23,19 @@ import {
|
|
|
22
23
|
* @param {{ info: Function, warn: Function, success: Function }} log
|
|
23
24
|
*/
|
|
24
25
|
export function createDockerImageDeployContext(config, env = process.env, log) {
|
|
25
|
-
const { fullImage, latestImage, legacyLatestImage, imageTag } =
|
|
26
|
+
const { fullImage, latestImage, legacyLatestImage, imageTag, tagSource } =
|
|
26
27
|
resolveDockerImageRef(config, env);
|
|
27
28
|
const registryUrl = env.DOCKER_REGISTRY_URL || '';
|
|
28
29
|
const registryUser = env.DOCKER_REGISTRY_USERNAME || '';
|
|
29
30
|
const registryToken = env.DOCKER_REGISTRY_TOKEN || '';
|
|
30
31
|
const dockerHost = env.DOCKER_HOST || '';
|
|
31
32
|
|
|
33
|
+
if (tagSource === 'explicit') {
|
|
34
|
+
log.warn(EXPLICIT_IMAGE_TAG_WARNING);
|
|
35
|
+
} else {
|
|
36
|
+
log.info(`Using auto image tag '${imageTag}' (source: ${tagSource})`);
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
function getDockerEnv() {
|
|
33
40
|
/** @type {Record<string, string>} */
|
|
34
41
|
const dockerEnv = { ...process.env };
|
|
@@ -324,6 +331,7 @@ export function createDockerImageDeployContext(config, env = process.env, log) {
|
|
|
324
331
|
fullImage,
|
|
325
332
|
latestImage,
|
|
326
333
|
imageTag,
|
|
334
|
+
tagSource,
|
|
327
335
|
getDockerEnv,
|
|
328
336
|
hasRegistryCredentials,
|
|
329
337
|
dockerLogin,
|
|
@@ -2,20 +2,100 @@
|
|
|
2
2
|
* Shared Docker image naming for pipeline builds and deploy.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { execFileSync } from 'child_process';
|
|
6
|
+
|
|
7
|
+
/** @typedef {'explicit'|'git'|'ci'|'timestamp'} ImageTagSource */
|
|
8
|
+
|
|
9
|
+
export const EXPLICIT_IMAGE_TAG_WARNING =
|
|
10
|
+
'DOCKER_IMAGE_TAG is set — reusing the same tag across deploys can leave Kubernetes pods on a stale image (imagePullPolicy defaults to IfNotPresent) unless imagePullPolicy is Always or a rollout restart runs.';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* High-resolution timestamp for image tags only (artifact versioning keeps getDateVersion()).
|
|
14
|
+
* Minute prefix matches getDateVersion(); seconds+ms avoid collisions in fast rebuild loops.
|
|
15
|
+
* @param {Date} [now]
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function highResImageTagFallback(now = new Date()) {
|
|
19
|
+
const y = now.getFullYear();
|
|
20
|
+
const m = String(now.getMonth() + 1).padStart(2, '0');
|
|
21
|
+
const d = String(now.getDate()).padStart(2, '0');
|
|
22
|
+
const h = String(now.getHours()).padStart(2, '0');
|
|
23
|
+
const min = String(now.getMinutes()).padStart(2, '0');
|
|
24
|
+
const sec = String(now.getSeconds()).padStart(2, '0');
|
|
25
|
+
const ms = String(now.getMilliseconds()).padStart(3, '0');
|
|
26
|
+
return `${y}.${m}.${d}.${h}${min}-${sec}${ms}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @returns {string|null}
|
|
31
|
+
*/
|
|
32
|
+
function defaultGetGitShortSha() {
|
|
33
|
+
try {
|
|
34
|
+
const sha = execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
37
|
+
}).trim();
|
|
38
|
+
return sha || null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve image tag when DOCKER_IMAGE_TAG is unset: git SHA → CI id → high-res timestamp.
|
|
46
|
+
* Does not use config.version (static package versions would prevent redeploys).
|
|
47
|
+
*
|
|
48
|
+
* @param {Record<string, string|undefined>} [env]
|
|
49
|
+
* @param {{
|
|
50
|
+
* getGitShortSha?: () => string|null,
|
|
51
|
+
* now?: () => Date,
|
|
52
|
+
* }} [options]
|
|
53
|
+
* @returns {{ imageTag: string, tagSource: ImageTagSource }}
|
|
54
|
+
*/
|
|
55
|
+
export function resolveImageTag(env = process.env, options = {}) {
|
|
56
|
+
const explicit = env.DOCKER_IMAGE_TAG;
|
|
57
|
+
if (explicit) {
|
|
58
|
+
return { imageTag: explicit, tagSource: 'explicit' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const getGitShortSha = options.getGitShortSha || defaultGetGitShortSha;
|
|
62
|
+
const gitSha = getGitShortSha();
|
|
63
|
+
if (gitSha) {
|
|
64
|
+
return { imageTag: gitSha, tagSource: 'git' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ciTag =
|
|
68
|
+
(env.GITHUB_SHA && String(env.GITHUB_SHA).slice(0, 7)) ||
|
|
69
|
+
env.GITHUB_RUN_ID ||
|
|
70
|
+
env.CI_COMMIT_SHORT_SHA ||
|
|
71
|
+
env.CI_PIPELINE_ID;
|
|
72
|
+
if (ciTag) {
|
|
73
|
+
return { imageTag: String(ciTag), tagSource: 'ci' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const now = options.now ? options.now() : new Date();
|
|
77
|
+
return { imageTag: highResImageTagFallback(now), tagSource: 'timestamp' };
|
|
78
|
+
}
|
|
79
|
+
|
|
5
80
|
/**
|
|
6
81
|
* @param {import('../core/config.js').DeployHubConfig} config
|
|
7
82
|
* @param {Record<string, string|undefined>} [env]
|
|
83
|
+
* @param {{
|
|
84
|
+
* getGitShortSha?: () => string|null,
|
|
85
|
+
* now?: () => Date,
|
|
86
|
+
* }} [options]
|
|
8
87
|
* @returns {{
|
|
9
88
|
* imageName: string,
|
|
10
89
|
* imageTag: string,
|
|
11
90
|
* fullImage: string,
|
|
12
91
|
* latestImage: string,
|
|
13
92
|
* legacyLatestImage: string,
|
|
93
|
+
* tagSource: ImageTagSource,
|
|
14
94
|
* }}
|
|
15
95
|
*/
|
|
16
|
-
export function resolveDockerImageRef(config, env = process.env) {
|
|
96
|
+
export function resolveDockerImageRef(config, env = process.env, options = {}) {
|
|
17
97
|
const imageName = env.DOCKER_IMAGE_NAME || config.project;
|
|
18
|
-
const imageTag = env
|
|
98
|
+
const { imageTag, tagSource } = resolveImageTag(env, options);
|
|
19
99
|
const registryUrl = env.DOCKER_REGISTRY_URL || '';
|
|
20
100
|
|
|
21
101
|
const repository =
|
|
@@ -29,6 +109,7 @@ export function resolveDockerImageRef(config, env = process.env) {
|
|
|
29
109
|
fullImage: `${repository}:${imageTag}`,
|
|
30
110
|
latestImage: `${repository}:latest`,
|
|
31
111
|
legacyLatestImage: `${config.project}:latest`,
|
|
112
|
+
tagSource,
|
|
32
113
|
};
|
|
33
114
|
}
|
|
34
115
|
|
|
@@ -170,6 +251,9 @@ export function describeInterpretedBackendGap(framework) {
|
|
|
170
251
|
|
|
171
252
|
export default {
|
|
172
253
|
resolveDockerImageRef,
|
|
254
|
+
resolveImageTag,
|
|
255
|
+
highResImageTagFallback,
|
|
256
|
+
EXPLICIT_IMAGE_TAG_WARNING,
|
|
173
257
|
generateFrontendRuntimeDockerfile,
|
|
174
258
|
generateSpringRuntimeDockerfile,
|
|
175
259
|
generateGoRuntimeDockerfile,
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* After kubectl apply, set the Deployment container image to fullImage and
|
|
5
|
+
* rollout-restart only when the live image ref is already identical (same
|
|
6
|
+
* registry + name + tag), so a new digest under a reused tag still redeploys.
|
|
7
|
+
*
|
|
8
|
+
* @param {{
|
|
9
|
+
* deploymentName: string,
|
|
10
|
+
* fullImage: string,
|
|
11
|
+
* kubectlArgs: (args: string[]) => string[],
|
|
12
|
+
* getKubectlEnv: () => NodeJS.ProcessEnv,
|
|
13
|
+
* log: { info: Function, warn: Function, success?: Function },
|
|
14
|
+
* execaFn?: typeof execa,
|
|
15
|
+
* }} options
|
|
16
|
+
* @returns {Promise<{ beforeImage: string, setImage: boolean, restarted: boolean }>}
|
|
17
|
+
*/
|
|
18
|
+
export async function syncKubernetesDeploymentImage(options) {
|
|
19
|
+
const {
|
|
20
|
+
deploymentName,
|
|
21
|
+
fullImage,
|
|
22
|
+
kubectlArgs,
|
|
23
|
+
getKubectlEnv,
|
|
24
|
+
log,
|
|
25
|
+
execaFn = execa,
|
|
26
|
+
} = options;
|
|
27
|
+
|
|
28
|
+
const beforeImage = await readDeploymentContainerImage({
|
|
29
|
+
deploymentName,
|
|
30
|
+
kubectlArgs,
|
|
31
|
+
getKubectlEnv,
|
|
32
|
+
execaFn,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let setImage = false;
|
|
36
|
+
try {
|
|
37
|
+
await execaFn(
|
|
38
|
+
'kubectl',
|
|
39
|
+
kubectlArgs([
|
|
40
|
+
'set',
|
|
41
|
+
'image',
|
|
42
|
+
`deployment/${deploymentName}`,
|
|
43
|
+
`${deploymentName}=${fullImage}`,
|
|
44
|
+
]),
|
|
45
|
+
{ stdio: 'pipe', env: getKubectlEnv() }
|
|
46
|
+
);
|
|
47
|
+
setImage = true;
|
|
48
|
+
} catch {
|
|
49
|
+
log.warn('kubectl set image skipped (deployment name may differ from project name)');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Compare full image refs (registry + name + tag), not tag alone.
|
|
53
|
+
if (beforeImage === fullImage) {
|
|
54
|
+
log.info(
|
|
55
|
+
`Image ref unchanged (${fullImage}) — running rollout restart so pods pick up a new digest`
|
|
56
|
+
);
|
|
57
|
+
await execaFn(
|
|
58
|
+
'kubectl',
|
|
59
|
+
kubectlArgs(['rollout', 'restart', `deployment/${deploymentName}`]),
|
|
60
|
+
{ stdio: 'inherit', env: getKubectlEnv() }
|
|
61
|
+
);
|
|
62
|
+
return { beforeImage, setImage, restarted: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { beforeImage, setImage, restarted: false };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {{
|
|
70
|
+
* deploymentName: string,
|
|
71
|
+
* kubectlArgs: (args: string[]) => string[],
|
|
72
|
+
* getKubectlEnv: () => NodeJS.ProcessEnv,
|
|
73
|
+
* execaFn: typeof execa,
|
|
74
|
+
* }} options
|
|
75
|
+
* @returns {Promise<string>}
|
|
76
|
+
*/
|
|
77
|
+
async function readDeploymentContainerImage(options) {
|
|
78
|
+
const { deploymentName, kubectlArgs, getKubectlEnv, execaFn } = options;
|
|
79
|
+
|
|
80
|
+
const jsonpath = `{.spec.template.spec.containers[?(@.name=="${deploymentName}")].image}`;
|
|
81
|
+
try {
|
|
82
|
+
const { stdout } = await execaFn(
|
|
83
|
+
'kubectl',
|
|
84
|
+
kubectlArgs(['get', 'deployment', deploymentName, '-o', `jsonpath=${jsonpath}`]),
|
|
85
|
+
{ stdio: 'pipe', env: getKubectlEnv() }
|
|
86
|
+
);
|
|
87
|
+
const trimmed = String(stdout || '').trim();
|
|
88
|
+
if (trimmed) return trimmed;
|
|
89
|
+
} catch {
|
|
90
|
+
// fall through to containers[0]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const { stdout } = await execaFn(
|
|
95
|
+
'kubectl',
|
|
96
|
+
kubectlArgs([
|
|
97
|
+
'get',
|
|
98
|
+
'deployment',
|
|
99
|
+
deploymentName,
|
|
100
|
+
'-o',
|
|
101
|
+
'jsonpath={.spec.template.spec.containers[0].image}',
|
|
102
|
+
]),
|
|
103
|
+
{ stdio: 'pipe', env: getKubectlEnv() }
|
|
104
|
+
);
|
|
105
|
+
return String(stdout || '').trim();
|
|
106
|
+
} catch {
|
|
107
|
+
return '';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default { syncKubernetesDeploymentImage };
|