@akash-chowdhury-24/deployhub 2.0.11 → 2.0.14
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 +33 -9
- package/package.json +1 -1
- package/src/artifact/engine.js +9 -2
- package/src/cli/index.js +2 -0
- package/src/commands/artifact.js +77 -22
- package/src/commands/doctor.js +19 -2
- package/src/commands/init.js +1 -0
- package/src/commands/rollback.js +31 -28
- package/src/commands/sync-workflows.js +43 -0
- package/src/core/config.js +1 -0
- package/src/core/stages.js +8 -1
- package/src/deployment/index.js +3 -2
- package/src/deployment/providers/azure-vm.js +39 -18
- package/src/deployment/providers/docker.js +29 -7
- package/src/deployment/providers/ec2.js +39 -18
- package/src/deployment/providers/gcp-vm.js +39 -18
- package/src/deployment/providers/kubernetes.js +67 -12
- package/src/deployment/providers/ssh.js +1 -1
- package/src/storage/index.js +165 -9
- package/src/storage/providers/aws.js +6 -2
- package/src/storage/providers/dropbox.js +8 -2
- package/src/storage/providers/ftp.js +8 -2
- package/src/storage/storage-errors.js +106 -0
- package/src/utils/artifact-history.js +155 -0
- package/src/utils/build-id.js +149 -0
- package/src/utils/docker-image-deploy.js +44 -21
- package/src/utils/docker-image.js +59 -54
- package/src/utils/github-actions.js +175 -8
- package/src/utils/rollback/engine.js +44 -21
package/README.md
CHANGED
|
@@ -225,10 +225,12 @@ Or push to `main` / `master` — the generated workflow runs the same command.
|
|
|
225
225
|
**Useful follow-up commands:**
|
|
226
226
|
|
|
227
227
|
```bash
|
|
228
|
-
deployhub artifact list #
|
|
229
|
-
deployhub artifact
|
|
228
|
+
deployhub artifact list # local artifacts
|
|
229
|
+
deployhub artifact list --remote # include storage history.json
|
|
230
|
+
deployhub artifact restore <buildId> # download a past build
|
|
230
231
|
deployhub deploy # deploy latest artifact without rebuilding
|
|
231
|
-
deployhub rollback
|
|
232
|
+
deployhub rollback # previous build from history
|
|
233
|
+
deployhub rollback <buildId> # exact build (required if semver is ambiguous)
|
|
232
234
|
deployhub logs # last deployment logs
|
|
233
235
|
```
|
|
234
236
|
|
|
@@ -269,7 +271,17 @@ deployhub doctor
|
|
|
269
271
|
deployhub build
|
|
270
272
|
```
|
|
271
273
|
|
|
272
|
-
Artifacts appear under `artifact/{projectName}/{date}/v{
|
|
274
|
+
Artifacts appear under `artifact/{projectName}/{date}/v{buildId}/` locally.
|
|
275
|
+
|
|
276
|
+
**Remote storage** (S3 and other providers) uses a different layout:
|
|
277
|
+
|
|
278
|
+
```text
|
|
279
|
+
{project}/builds/{buildId}/artifact.zip # immutable per CI/build
|
|
280
|
+
{project}/history.json # newest-first index for rollback / list --remote
|
|
281
|
+
{project}/latest/artifact.zip # mutable pointer overwritten every upload (NOT a backup)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`buildId` is unique per pipeline run (e.g. `1.0.6-a1b2c3d`) even if `package.json` semver is unchanged. Legacy keys `{project}/v{semver}/artifact.zip` are no longer written; they remain readable for older uploads only.
|
|
273
285
|
|
|
274
286
|
### Same steps for other languages
|
|
275
287
|
|
|
@@ -943,12 +955,12 @@ Run `deployhub doctor` after any config change.
|
|
|
943
955
|
| `deployhub init` | Interactive project setup |
|
|
944
956
|
| `deployhub build` | Full pipeline: detect → install → test → build → artifact → storage → deploy |
|
|
945
957
|
| `deployhub artifact create` | Create artifact from current build |
|
|
946
|
-
| `deployhub artifact list` | List
|
|
947
|
-
| `deployhub artifact restore <
|
|
958
|
+
| `deployhub artifact list [--remote]` | List local artifacts; `--remote` merges storage `history.json` |
|
|
959
|
+
| `deployhub artifact restore <buildId\|semver>` | Download and extract an artifact |
|
|
948
960
|
| `deployhub storage add <provider>` | Add storage provider credentials |
|
|
949
961
|
| `deployhub storage list` | List storage providers and connection status |
|
|
950
962
|
| `deployhub deploy` | Deploy latest artifact |
|
|
951
|
-
| `deployhub rollback [
|
|
963
|
+
| `deployhub rollback [buildId\|semver]` | Previous build, or exact buildId (ambiguous semver lists matches and exits) |
|
|
952
964
|
| `deployhub logs` | Show logs from last deployment |
|
|
953
965
|
| `deployhub doctor` | Pre-flight checks |
|
|
954
966
|
| `deployhub verify` | Health check on configured endpoint |
|
|
@@ -1029,13 +1041,13 @@ If checks fail:
|
|
|
1029
1041
|
|
|
1030
1042
|
## Artifact Structure
|
|
1031
1043
|
|
|
1032
|
-
Each build creates:
|
|
1044
|
+
Each build creates a **local** directory:
|
|
1033
1045
|
|
|
1034
1046
|
```
|
|
1035
1047
|
artifact/
|
|
1036
1048
|
{projectName}/
|
|
1037
1049
|
{YYYY-MM-DD}/
|
|
1038
|
-
v{
|
|
1050
|
+
v{buildId}/
|
|
1039
1051
|
artifact.zip
|
|
1040
1052
|
metadata.json
|
|
1041
1053
|
logs.txt
|
|
@@ -1045,6 +1057,18 @@ artifact/
|
|
|
1045
1057
|
README.md
|
|
1046
1058
|
```
|
|
1047
1059
|
|
|
1060
|
+
`buildId` looks like `{semver}-{gitSha|ciId|timestamp}` (unique every pipeline run).
|
|
1061
|
+
|
|
1062
|
+
**Remote keys** (all storage providers):
|
|
1063
|
+
|
|
1064
|
+
```
|
|
1065
|
+
{project}/builds/{buildId}/artifact.zip
|
|
1066
|
+
{project}/history.json
|
|
1067
|
+
{project}/latest/artifact.zip # overwritten every build — convenience pointer only, not version history
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
Legacy (read-only fallback, no longer written): `{project}/v{semver}/artifact.zip`
|
|
1071
|
+
|
|
1048
1072
|
`deployment.json` records server deployment metadata per environment:
|
|
1049
1073
|
|
|
1050
1074
|
```json
|
package/package.json
CHANGED
package/src/artifact/engine.js
CHANGED
|
@@ -5,6 +5,7 @@ import { execa } from 'execa';
|
|
|
5
5
|
import { createLogger } from '../logger/index.js';
|
|
6
6
|
import { generateChecksums, formatChecksums } from '../utils/checksums.js';
|
|
7
7
|
import { getProjectVersion } from '../utils/version.js';
|
|
8
|
+
import { resolveBuildId } from '../utils/build-id.js';
|
|
8
9
|
import { generateNginxConfig } from '../utils/nginx.js';
|
|
9
10
|
import {
|
|
10
11
|
ensureDeployScaffold,
|
|
@@ -238,12 +239,13 @@ async function stageBackendArtifact(cwd, stagingDir, config) {
|
|
|
238
239
|
*/
|
|
239
240
|
export function getArtifactDir(config, cwd = process.cwd()) {
|
|
240
241
|
const date = new Date().toISOString().slice(0, 10);
|
|
242
|
+
const buildId = config.buildId || config.version || '0.0.0';
|
|
241
243
|
return path.join(
|
|
242
244
|
cwd,
|
|
243
245
|
'artifact',
|
|
244
246
|
config.project,
|
|
245
247
|
date,
|
|
246
|
-
`v${
|
|
248
|
+
`v${buildId}`
|
|
247
249
|
);
|
|
248
250
|
}
|
|
249
251
|
|
|
@@ -257,6 +259,10 @@ export async function createArtifact(config, deployedTargets = [], cwd = process
|
|
|
257
259
|
const log = createLogger('artifact');
|
|
258
260
|
const version = config.version || (await getProjectVersion(cwd));
|
|
259
261
|
config.version = version;
|
|
262
|
+
if (!config.buildId) {
|
|
263
|
+
const { buildId } = resolveBuildId({ semver: version });
|
|
264
|
+
config.buildId = buildId;
|
|
265
|
+
}
|
|
260
266
|
|
|
261
267
|
const artifactDir = getArtifactDir(config, cwd);
|
|
262
268
|
const stagingDir = path.join(artifactDir, '_staging');
|
|
@@ -266,7 +272,7 @@ export async function createArtifact(config, deployedTargets = [], cwd = process
|
|
|
266
272
|
const artifactType = resolveArtifactType(config);
|
|
267
273
|
const settings = resolveBuildSettings(config);
|
|
268
274
|
|
|
269
|
-
log.info(`Staging ${projectType} artifact...`);
|
|
275
|
+
log.info(`Staging ${projectType} artifact (buildId=${config.buildId})...`);
|
|
270
276
|
|
|
271
277
|
await ensureDeployScaffold(cwd, config, config.environments || {}, { silent: false });
|
|
272
278
|
|
|
@@ -288,6 +294,7 @@ export async function createArtifact(config, deployedTargets = [], cwd = process
|
|
|
288
294
|
const metadata = {
|
|
289
295
|
project: config.project,
|
|
290
296
|
version,
|
|
297
|
+
buildId: config.buildId,
|
|
291
298
|
timestamp,
|
|
292
299
|
gitCommit: git.commit,
|
|
293
300
|
branch: git.branch,
|
package/src/cli/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { registerDoctorCommand } from '../commands/doctor.js';
|
|
|
13
13
|
import { registerVerifyCommand } from '../commands/verify.js';
|
|
14
14
|
import { registerCleanCommand } from '../commands/clean.js';
|
|
15
15
|
import { registerUpdateCommand } from '../commands/update.js';
|
|
16
|
+
import { registerSyncWorkflowsCommand } from '../commands/sync-workflows.js';
|
|
16
17
|
import { formatVersionOutput, printBanner, shouldShowBanner } from '../utils/author.js';
|
|
17
18
|
|
|
18
19
|
loadEnv();
|
|
@@ -41,5 +42,6 @@ registerDoctorCommand(program);
|
|
|
41
42
|
registerVerifyCommand(program);
|
|
42
43
|
registerCleanCommand(program);
|
|
43
44
|
registerUpdateCommand(program);
|
|
45
|
+
registerSyncWorkflowsCommand(program);
|
|
44
46
|
|
|
45
47
|
program.parse();
|
package/src/commands/artifact.js
CHANGED
|
@@ -5,7 +5,10 @@ import {
|
|
|
5
5
|
listLocalArtifacts,
|
|
6
6
|
extractArtifact,
|
|
7
7
|
} from '../artifact/engine.js';
|
|
8
|
-
import { downloadFromFirst } from '../storage/index.js';
|
|
8
|
+
import { downloadArtifactEntry, loadArtifactHistory, downloadFromFirst } from '../storage/index.js';
|
|
9
|
+
import {
|
|
10
|
+
legacyArtifactRemoteKey,
|
|
11
|
+
} from '../utils/build-id.js';
|
|
9
12
|
import fs from 'fs-extra';
|
|
10
13
|
import path from 'path';
|
|
11
14
|
|
|
@@ -29,53 +32,105 @@ export function registerArtifactCommand(program) {
|
|
|
29
32
|
|
|
30
33
|
artifact
|
|
31
34
|
.command('list')
|
|
32
|
-
.description('List
|
|
33
|
-
.
|
|
35
|
+
.description('List local artifacts (add --remote to include storage history.json)')
|
|
36
|
+
.option('--remote', 'Also list builds from remote history.json')
|
|
37
|
+
.action(async (opts) => {
|
|
34
38
|
loadEnv();
|
|
39
|
+
const config = await loadConfig();
|
|
35
40
|
const artifacts = await listLocalArtifacts();
|
|
36
41
|
|
|
42
|
+
console.log(chalk.bold('\nLocal artifacts:\n'));
|
|
37
43
|
if (artifacts.length === 0) {
|
|
38
|
-
console.log(chalk.yellow('
|
|
39
|
-
|
|
44
|
+
console.log(chalk.yellow(' (none)'));
|
|
45
|
+
} else {
|
|
46
|
+
for (const a of artifacts) {
|
|
47
|
+
const sizeMb = (a.size / 1024 / 1024).toFixed(2);
|
|
48
|
+
console.log(
|
|
49
|
+
` ${chalk.cyan(a.version)} ${a.date} ${a.project} ${sizeMb} MB`
|
|
50
|
+
);
|
|
51
|
+
console.log(chalk.gray(` ${a.path}`));
|
|
52
|
+
}
|
|
40
53
|
}
|
|
41
54
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
if (opts.remote) {
|
|
56
|
+
console.log(chalk.bold('\nRemote history (storage):\n'));
|
|
57
|
+
try {
|
|
58
|
+
const { entries: history, source } = await loadArtifactHistory(
|
|
59
|
+
config.storage || [],
|
|
60
|
+
config.project
|
|
61
|
+
);
|
|
62
|
+
if (history.length === 0) {
|
|
63
|
+
console.log(
|
|
64
|
+
chalk.yellow(
|
|
65
|
+
' No artifact history found for this project — you may not have deployed any builds yet.'
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
if (source) {
|
|
70
|
+
console.log(chalk.gray(` Source: ${source}`));
|
|
71
|
+
console.log('');
|
|
72
|
+
}
|
|
73
|
+
for (const e of history) {
|
|
74
|
+
console.log(
|
|
75
|
+
` ${chalk.cyan(e.buildId)} semver=${e.semver} ${e.uploadedAt || ''}`
|
|
76
|
+
);
|
|
77
|
+
console.log(chalk.gray(` ${e.remoteKey}`));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
82
|
+
console.log(chalk.red(` ${detail}`));
|
|
83
|
+
}
|
|
49
84
|
}
|
|
85
|
+
|
|
50
86
|
console.log('');
|
|
51
87
|
});
|
|
52
88
|
|
|
53
89
|
artifact
|
|
54
|
-
.command('restore <
|
|
55
|
-
.description('Download and extract an artifact by
|
|
56
|
-
.action(async (
|
|
90
|
+
.command('restore <versionOrBuildId>')
|
|
91
|
+
.description('Download and extract an artifact by buildId or legacy semver')
|
|
92
|
+
.action(async (versionOrBuildId) => {
|
|
57
93
|
loadEnv();
|
|
58
94
|
const config = await loadConfig();
|
|
59
95
|
const cwd = process.cwd();
|
|
96
|
+
const needle = String(versionOrBuildId).replace(/^v/i, '');
|
|
60
97
|
|
|
61
98
|
const local = await listLocalArtifacts(cwd);
|
|
62
|
-
const localMatch = local.find(
|
|
99
|
+
const localMatch = local.find(
|
|
100
|
+
(a) => a.version === needle || a.version === versionOrBuildId
|
|
101
|
+
);
|
|
63
102
|
|
|
64
103
|
if (localMatch) {
|
|
65
|
-
const extractTo = path.join(cwd, '.deployhub-restore', `v${version}`);
|
|
104
|
+
const extractTo = path.join(cwd, '.deployhub-restore', `v${localMatch.version}`);
|
|
66
105
|
await extractArtifact(localMatch.path, extractTo);
|
|
67
106
|
console.log(chalk.green(`Restored to ${extractTo}`));
|
|
68
107
|
return;
|
|
69
108
|
}
|
|
70
109
|
|
|
71
|
-
const
|
|
72
|
-
const
|
|
110
|
+
const history = await loadArtifactHistory(config.storage || [], config.project);
|
|
111
|
+
const histMatch =
|
|
112
|
+
history.entries.find((e) => e.buildId === needle || e.buildId === versionOrBuildId) ||
|
|
113
|
+
history.entries.find((e) => e.semver === needle);
|
|
114
|
+
|
|
115
|
+
const restoreDir = path.join(cwd, '.deployhub-restore', `v${needle}`);
|
|
73
116
|
await fs.ensureDir(restoreDir);
|
|
74
117
|
const zipPath = path.join(restoreDir, 'artifact.zip');
|
|
75
118
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
119
|
+
if (histMatch) {
|
|
120
|
+
console.log(`Downloading ${histMatch.buildId} from storage...`);
|
|
121
|
+
const provider = await downloadArtifactEntry(
|
|
122
|
+
config.storage,
|
|
123
|
+
config,
|
|
124
|
+
histMatch,
|
|
125
|
+
zipPath
|
|
126
|
+
);
|
|
127
|
+
console.log(chalk.gray(`Downloaded from ${provider}`));
|
|
128
|
+
} else {
|
|
129
|
+
const remoteKey = legacyArtifactRemoteKey(config.project, needle);
|
|
130
|
+
console.log(`Downloading legacy key ${remoteKey} from storage...`);
|
|
131
|
+
const provider = await downloadFromFirst(config.storage, remoteKey, zipPath);
|
|
132
|
+
console.log(chalk.gray(`Downloaded from ${provider}`));
|
|
133
|
+
}
|
|
79
134
|
|
|
80
135
|
const versionDir = path.join(restoreDir, 'artifact');
|
|
81
136
|
await fs.ensureDir(versionDir);
|
package/src/commands/doctor.js
CHANGED
|
@@ -7,7 +7,7 @@ import axios from 'axios';
|
|
|
7
7
|
import { loadConfig, loadEnv } from '../core/config.js';
|
|
8
8
|
import { testProvider } from '../storage/index.js';
|
|
9
9
|
import { getDeploymentProvider } from '../deployment/index.js';
|
|
10
|
-
import { PROVIDER_ENV_MAP } from '../utils/github-actions.js';
|
|
10
|
+
import { PROVIDER_ENV_MAP, getRollbackWorkflowDoctorCheck } from '../utils/github-actions.js';
|
|
11
11
|
import { printDoctorFooter } from '../utils/author.js';
|
|
12
12
|
import { createLocalProvider } from '../storage/providers/local.js';
|
|
13
13
|
import {
|
|
@@ -888,11 +888,28 @@ export function registerDoctorCommand(program) {
|
|
|
888
888
|
return {
|
|
889
889
|
name: 'GitHub Actions',
|
|
890
890
|
pass: false,
|
|
891
|
-
message: 'Workflow file missing — run deployhub init',
|
|
891
|
+
message: 'Workflow file missing — run deployhub init or deployhub sync-workflows',
|
|
892
892
|
};
|
|
893
893
|
})
|
|
894
894
|
);
|
|
895
895
|
|
|
896
|
+
const hasStorage = (config.storage || []).length > 0;
|
|
897
|
+
const hasDeploy = (config.deploy || []).length > 0;
|
|
898
|
+
if (hasStorage && hasDeploy) {
|
|
899
|
+
results.push(
|
|
900
|
+
await runCheck('Rollback workflow', async () => {
|
|
901
|
+
const check = await getRollbackWorkflowDoctorCheck(cwd, config);
|
|
902
|
+
return (
|
|
903
|
+
check || {
|
|
904
|
+
name: 'Rollback workflow',
|
|
905
|
+
pass: true,
|
|
906
|
+
message: 'Skipped',
|
|
907
|
+
}
|
|
908
|
+
);
|
|
909
|
+
})
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
896
913
|
results.push(
|
|
897
914
|
await runCheck('Storage write', async () => {
|
|
898
915
|
const provider = createLocalProvider();
|
package/src/commands/init.js
CHANGED
|
@@ -535,6 +535,7 @@ export function registerInitCommand(program) {
|
|
|
535
535
|
console.log(chalk.bold('Generated files:'));
|
|
536
536
|
console.log(' • deployhub.config.json');
|
|
537
537
|
console.log(' • .github/workflows/deployhub.yml');
|
|
538
|
+
console.log(' • .github/workflows/deployhub-rollback.yml');
|
|
538
539
|
console.log(' • .env.example');
|
|
539
540
|
console.log('');
|
|
540
541
|
printAuthorFooter();
|
package/src/commands/rollback.js
CHANGED
|
@@ -8,42 +8,45 @@ import axios from 'axios';
|
|
|
8
8
|
*/
|
|
9
9
|
export function registerRollbackCommand(program) {
|
|
10
10
|
program
|
|
11
|
-
.command('rollback [
|
|
12
|
-
.description(
|
|
13
|
-
|
|
11
|
+
.command('rollback [versionOrBuildId]')
|
|
12
|
+
.description(
|
|
13
|
+
'Rollback to a previous artifact build (omit arg = previous build; use exact buildId if semver is ambiguous)'
|
|
14
|
+
)
|
|
15
|
+
.action(async (versionOrBuildId) => {
|
|
14
16
|
loadEnv();
|
|
15
17
|
const config = await loadConfig();
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
const {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
console.error(chalk.red('No previous version available for rollback'));
|
|
22
|
-
process.exit(1);
|
|
19
|
+
try {
|
|
20
|
+
const { entry } = await rollbackToVersion(config, versionOrBuildId);
|
|
21
|
+
if (!versionOrBuildId) {
|
|
22
|
+
console.log(chalk.gray(`Rolled back to previous build: ${entry.buildId}`));
|
|
23
23
|
}
|
|
24
|
-
version = artifacts[1].version;
|
|
25
|
-
console.log(chalk.gray(`Rolling back to previous version: v${version}`));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
await rollbackToVersion(config, version);
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
25
|
+
if (config.healthCheck?.url) {
|
|
26
|
+
try {
|
|
27
|
+
const response = await axios.get(config.healthCheck.url, {
|
|
28
|
+
timeout: (config.healthCheck.timeout || 30) * 1000,
|
|
29
|
+
validateStatus: () => true,
|
|
30
|
+
});
|
|
31
|
+
if (response.status >= 200 && response.status < 400) {
|
|
32
|
+
console.log(chalk.green(`Health check passed: HTTP ${response.status}`));
|
|
33
|
+
} else {
|
|
34
|
+
console.log(chalk.yellow(`Health check returned HTTP ${response.status}`));
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.log(
|
|
38
|
+
chalk.yellow(
|
|
39
|
+
`Health check failed: ${err instanceof Error ? err.message : String(err)}`
|
|
40
|
+
)
|
|
41
|
+
);
|
|
40
42
|
}
|
|
41
|
-
} catch (err) {
|
|
42
|
-
console.log(chalk.yellow(`Health check failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
43
43
|
}
|
|
44
|
-
}
|
|
45
44
|
|
|
46
|
-
|
|
45
|
+
console.log(chalk.green(`✓ Rolled back to ${entry.buildId}`));
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
47
50
|
});
|
|
48
51
|
}
|
|
49
52
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig, loadEnv } from '../core/config.js';
|
|
3
|
+
import {
|
|
4
|
+
writeWorkflowFile,
|
|
5
|
+
DEPLOY_WORKFLOW_FILENAME,
|
|
6
|
+
ROLLBACK_WORKFLOW_FILENAME,
|
|
7
|
+
} from '../utils/github-actions.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Regenerate GitHub Actions workflows from deployhub.config.json (no interactive init).
|
|
11
|
+
* @param {import('commander').Command} program
|
|
12
|
+
*/
|
|
13
|
+
export function registerSyncWorkflowsCommand(program) {
|
|
14
|
+
program
|
|
15
|
+
.command('sync-workflows')
|
|
16
|
+
.description(
|
|
17
|
+
'Regenerate .github/workflows/deployhub.yml and deployhub-rollback.yml from deployhub.config.json'
|
|
18
|
+
)
|
|
19
|
+
.action(async () => {
|
|
20
|
+
loadEnv();
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
const config = await loadConfig(cwd);
|
|
23
|
+
|
|
24
|
+
const storage = config.storage || [];
|
|
25
|
+
const deploy = config.deploy || [];
|
|
26
|
+
const environments = config.environments || {};
|
|
27
|
+
const cliSource = config.cli?.source;
|
|
28
|
+
|
|
29
|
+
await writeWorkflowFile(storage, deploy, environments, cwd, cliSource, config);
|
|
30
|
+
|
|
31
|
+
console.log(chalk.green('✓ Regenerated GitHub Actions workflows:'));
|
|
32
|
+
console.log(` • .github/workflows/${DEPLOY_WORKFLOW_FILENAME}`);
|
|
33
|
+
console.log(` • .github/workflows/${ROLLBACK_WORKFLOW_FILENAME}`);
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(
|
|
36
|
+
chalk.gray(
|
|
37
|
+
'Commit and push these files, then use Actions → DeployHub Rollback (workflow_dispatch) to roll back.'
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default { registerSyncWorkflowsCommand };
|
package/src/core/config.js
CHANGED
|
@@ -47,6 +47,7 @@ const EnvironmentSchema = z.object({
|
|
|
47
47
|
const ConfigSchema = z.object({
|
|
48
48
|
project: z.string(),
|
|
49
49
|
version: z.string().optional(),
|
|
50
|
+
buildId: z.string().optional(),
|
|
50
51
|
projectType: z.enum(['frontend', 'backend', 'both']).default('frontend'),
|
|
51
52
|
framework: z.string().optional(),
|
|
52
53
|
language: z.string().optional(),
|
package/src/core/stages.js
CHANGED
|
@@ -6,6 +6,7 @@ import { deployToAll } from '../deployment/index.js';
|
|
|
6
6
|
import { sendNotifications } from '../notifications/index.js';
|
|
7
7
|
import axios from 'axios';
|
|
8
8
|
import { getProjectVersion } from '../utils/version.js';
|
|
9
|
+
import { resolveBuildId } from '../utils/build-id.js';
|
|
9
10
|
import { ensureDeployScaffold } from '../utils/scaffold.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -41,8 +42,10 @@ export function buildPipelineStages(config, cwd, state) {
|
|
|
41
42
|
ctx.config.port = detected.port;
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
|
-
//
|
|
45
|
+
// Semver label (package.json) + unique buildId (shared with image tag when DOCKER_IMAGE_TAG unset)
|
|
45
46
|
ctx.config.version = await getProjectVersion(ctx.cwd);
|
|
47
|
+
const { buildId } = resolveBuildId({ semver: ctx.config.version });
|
|
48
|
+
ctx.config.buildId = buildId;
|
|
46
49
|
const scaffold = await ensureDeployScaffold(
|
|
47
50
|
ctx.cwd,
|
|
48
51
|
ctx.config,
|
|
@@ -123,6 +126,10 @@ export function buildPipelineStages(config, cwd, state) {
|
|
|
123
126
|
if (!ctx.config.version) {
|
|
124
127
|
ctx.config.version = await getProjectVersion(ctx.cwd);
|
|
125
128
|
}
|
|
129
|
+
if (!ctx.config.buildId) {
|
|
130
|
+
const { buildId } = resolveBuildId({ semver: ctx.config.version });
|
|
131
|
+
ctx.config.buildId = buildId;
|
|
132
|
+
}
|
|
126
133
|
const result = await createArtifact(
|
|
127
134
|
ctx.config,
|
|
128
135
|
/** @type {string[]} */ (ctx.state.deployedTargets || []),
|
package/src/deployment/index.js
CHANGED
|
@@ -71,13 +71,14 @@ export async function deployToAll(config, artifactDir, envNames) {
|
|
|
71
71
|
* @param {import('../core/config.js').DeployHubConfig} config
|
|
72
72
|
* @param {string} artifactDir
|
|
73
73
|
* @param {string[]} [envNames]
|
|
74
|
+
* @param {{ buildId?: string, semver?: string, remoteKey?: string }} [meta]
|
|
74
75
|
*/
|
|
75
|
-
export async function rollbackAll(config, artifactDir, envNames) {
|
|
76
|
+
export async function rollbackAll(config, artifactDir, envNames, meta) {
|
|
76
77
|
const targets = envNames || config.deploy || [];
|
|
77
78
|
for (const envName of targets) {
|
|
78
79
|
const envConfig = config.environments[envName];
|
|
79
80
|
const provider = getDeploymentProvider(envConfig.type, config, envName);
|
|
80
|
-
await provider.rollback(artifactDir);
|
|
81
|
+
await provider.rollback(artifactDir, meta);
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -21,7 +21,8 @@ export function createAzureVmProvider(config, envName, env = process.env) {
|
|
|
21
21
|
|
|
22
22
|
if (!subscriptionId || !resourceGroup || !vmName) {
|
|
23
23
|
throw new Error(
|
|
24
|
-
'
|
|
24
|
+
'Could not resolve host via Azure VM lookup, and no SSH_HOST was set — ' +
|
|
25
|
+
'provide SSH_HOST (VM public IP/DNS) or set AZURE_SUBSCRIPTION_ID, AZURE_RESOURCE_GROUP, and AZURE_VM_NAME for auto lookup.'
|
|
25
26
|
);
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -55,34 +56,54 @@ export function createAzureVmProvider(config, envName, env = process.env) {
|
|
|
55
56
|
} catch (err) {
|
|
56
57
|
const msg = err instanceof Error ? err.message : String(err);
|
|
57
58
|
throw new Error(
|
|
58
|
-
`Could not resolve
|
|
59
|
+
`Could not resolve host via Azure VM lookup (${vmName}): ${msg}. ` +
|
|
60
|
+
'Set SSH_HOST to the VM public IP/DNS, or run az login and verify resource group/VM name.'
|
|
59
61
|
);
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Resolve host (skipping cloud lookup when SSH_HOST/environment.host is set),
|
|
67
|
+
* then create an SSH provider that closes over the resolved host.
|
|
68
|
+
*/
|
|
69
|
+
async function getSshProvider() {
|
|
66
70
|
const host = await resolveHost();
|
|
71
|
+
if (!host) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'Could not resolve host via Azure VM lookup, and no SSH_HOST was set — provide one or the other.'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
67
76
|
const environment = config.environments[envName];
|
|
68
|
-
if (environment
|
|
77
|
+
if (environment) {
|
|
69
78
|
environment.host = host;
|
|
70
79
|
}
|
|
71
|
-
|
|
72
|
-
env.SSH_HOST = host;
|
|
73
|
-
}
|
|
74
|
-
return sshProvider.connect();
|
|
80
|
+
return createSshProvider(config, envName, { ...env, SSH_HOST: host });
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
return {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
async connect() {
|
|
85
|
+
const ssh = await getSshProvider();
|
|
86
|
+
return ssh.connect();
|
|
87
|
+
},
|
|
88
|
+
async deploy(artifactDir, options) {
|
|
89
|
+
const ssh = await getSshProvider();
|
|
90
|
+
return ssh.deploy(artifactDir, options);
|
|
91
|
+
},
|
|
92
|
+
async rollback(artifactDir, meta) {
|
|
93
|
+
const ssh = await getSshProvider();
|
|
94
|
+
return ssh.rollback(artifactDir, meta);
|
|
95
|
+
},
|
|
96
|
+
async healthCheck() {
|
|
97
|
+
const ssh = await getSshProvider();
|
|
98
|
+
return ssh.healthCheck();
|
|
99
|
+
},
|
|
100
|
+
async testConnection() {
|
|
101
|
+
const ssh = await getSshProvider();
|
|
102
|
+
return ssh.testConnection();
|
|
103
|
+
},
|
|
104
|
+
async runRemoteCheck(command) {
|
|
105
|
+
const ssh = await getSshProvider();
|
|
106
|
+
return ssh.runRemoteCheck(command);
|
|
86
107
|
},
|
|
87
108
|
};
|
|
88
109
|
}
|