@nmakarov/cli-toolkit 0.27.0 → 0.32.0
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 +38 -0
- package/dist/aws.cjs +210 -0
- package/dist/aws.cjs.map +1 -0
- package/dist/aws.js +201 -0
- package/dist/aws.js.map +1 -0
- package/dist/cli-runner.cjs +43 -11
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +43 -11
- package/dist/cli-runner.js.map +1 -1
- package/dist/deploy.cjs +1166 -0
- package/dist/deploy.cjs.map +1 -0
- package/dist/deploy.js +1096 -0
- package/dist/deploy.js.map +1 -0
- package/dist/index.cjs +1405 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1354 -29
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +19 -6
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +19 -6
- package/dist/init.js.map +1 -1
- package/dist/params.cjs +8 -4
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +8 -4
- package/dist/params.js.map +1 -1
- package/dist/s3.cjs +1 -0
- package/dist/s3.cjs.map +1 -1
- package/dist/s3.js +1 -0
- package/dist/s3.js.map +1 -1
- package/dist/tasks.cjs +25 -7
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +25 -7
- package/dist/tasks.js.map +1 -1
- package/package.json +15 -2
- package/scripts/deploy/cli.js +181 -0
package/dist/index.cjs
CHANGED
|
@@ -561,7 +561,7 @@ async function showScreen(config2) {
|
|
|
561
561
|
onRender,
|
|
562
562
|
parentData = {}
|
|
563
563
|
} = config2;
|
|
564
|
-
return new Promise((
|
|
564
|
+
return new Promise((resolve3) => {
|
|
565
565
|
let instance2;
|
|
566
566
|
const keyBindings = [];
|
|
567
567
|
const actions = {};
|
|
@@ -736,7 +736,7 @@ async function showScreen(config2) {
|
|
|
736
736
|
};
|
|
737
737
|
const cleanup = (result) => {
|
|
738
738
|
if (instance2) instance2.unmount();
|
|
739
|
-
setTimeout(() =>
|
|
739
|
+
setTimeout(() => resolve3(result), 50);
|
|
740
740
|
};
|
|
741
741
|
instance2 = (0, import_ink3.render)((0, import_react3.createElement)(Screen));
|
|
742
742
|
});
|
|
@@ -1072,6 +1072,7 @@ var src_exports = {};
|
|
|
1072
1072
|
__export(src_exports, {
|
|
1073
1073
|
AbstractTask: () => AbstractTask,
|
|
1074
1074
|
Args: () => Args,
|
|
1075
|
+
Aws: () => Aws,
|
|
1075
1076
|
Box: () => import_ink5.Box,
|
|
1076
1077
|
Db: () => Db,
|
|
1077
1078
|
Divider: () => Divider,
|
|
@@ -1085,6 +1086,7 @@ __export(src_exports, {
|
|
|
1085
1086
|
MultiColumnListComponent: () => MultiColumnListComponent,
|
|
1086
1087
|
MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
|
|
1087
1088
|
Params: () => Params,
|
|
1089
|
+
REMOTE_CLI_REL: () => REMOTE_CLI_REL,
|
|
1088
1090
|
React: () => import_react5.default,
|
|
1089
1091
|
S3: () => S3,
|
|
1090
1092
|
SERVICE_TASK_NAMES: () => SERVICE_TASK_NAMES,
|
|
@@ -1105,51 +1107,94 @@ __export(src_exports, {
|
|
|
1105
1107
|
TasksRegistry: () => TasksRegistry,
|
|
1106
1108
|
Text: () => import_ink5.Text,
|
|
1107
1109
|
TextBlock: () => TextBlock,
|
|
1110
|
+
activateRelease: () => activateRelease,
|
|
1111
|
+
appendDeployLog: () => appendDeployLog,
|
|
1108
1112
|
appendTaskIpcLog: () => appendTaskIpcLog,
|
|
1113
|
+
bootstrapHost: () => bootstrapHost,
|
|
1109
1114
|
buildBreadcrumb: () => buildBreadcrumb,
|
|
1110
1115
|
buildDetailBreadcrumb: () => buildDetailBreadcrumb,
|
|
1111
1116
|
buildFooter: () => buildFooter,
|
|
1117
|
+
bumpPatchVersion: () => bumpPatchVersion,
|
|
1118
|
+
cloneRepo: () => cloneRepo,
|
|
1112
1119
|
convertPattern: () => convertPattern,
|
|
1120
|
+
createRelease: () => createRelease,
|
|
1113
1121
|
defaultFileSynopsisFunction: () => defaultFileSynopsisFunction,
|
|
1114
1122
|
defaultTasksRegistry: () => defaultTasksRegistry,
|
|
1115
1123
|
defaultVersionSynopsisFunction: () => defaultVersionSynopsisFunction,
|
|
1124
|
+
defineService: () => defineService,
|
|
1125
|
+
deployService: () => deployService,
|
|
1126
|
+
deriveRepoDirName: () => deriveRepoDirName,
|
|
1127
|
+
enableNginxUpstream: () => enableNginxUpstream,
|
|
1116
1128
|
enqueueStopTask: () => enqueueStopTask,
|
|
1117
1129
|
enqueueTask: () => enqueueTask,
|
|
1130
|
+
ensureDeployKeyOnRemote: () => ensureDeployKeyOnRemote,
|
|
1131
|
+
ensureEnvOnRemote: () => ensureEnvOnRemote,
|
|
1132
|
+
ensureRemoteRepo: () => ensureRemoteRepo,
|
|
1133
|
+
ensureRepoDependencies: () => ensureRepoDependencies,
|
|
1118
1134
|
ensureTaskTables: () => ensureTaskTables,
|
|
1119
1135
|
flushTaskIpcLogs: () => flushTaskIpcLogs,
|
|
1120
1136
|
getArgsInstance: () => getArgsInstance,
|
|
1121
1137
|
h: () => import_react5.createElement,
|
|
1138
|
+
initServiceStructure: () => initServiceStructure,
|
|
1139
|
+
installDeps: () => installDeps,
|
|
1122
1140
|
ipcFileLogsTableNameForSourceResource: () => ipcFileLogsTableNameForSourceResource,
|
|
1123
1141
|
joiEdateType: () => joiEdateType,
|
|
1124
1142
|
joiStringArrayType: () => joiStringArrayType,
|
|
1125
1143
|
listAliveRunnerHeartbeats: () => listServicesRegistry,
|
|
1144
|
+
listReleases: () => listReleases,
|
|
1126
1145
|
listServicesRegistry: () => listServicesRegistry,
|
|
1127
1146
|
listSources: () => listSources,
|
|
1128
1147
|
listTables: () => listTables,
|
|
1129
1148
|
load: () => load,
|
|
1149
|
+
loadServices: () => loadServices,
|
|
1130
1150
|
matchesParsedPattern: () => matchesParsedPattern,
|
|
1131
1151
|
memo: () => import_react5.memo,
|
|
1132
1152
|
mergeAllowedTasksWithServiceTasks: () => mergeAllowedTasksWithServiceTasks,
|
|
1133
1153
|
nextTimeMatch: () => nextTimeMatch,
|
|
1134
1154
|
normalizeAllowedTasks: () => normalizeAllowedTasks,
|
|
1155
|
+
npmEnv: () => npmEnv,
|
|
1156
|
+
npmInstallEnv: () => npmInstallEnv,
|
|
1135
1157
|
organizeFooterMessages: () => organizeFooterMessages,
|
|
1158
|
+
parseGitHost: () => parseGitHost,
|
|
1159
|
+
prepareGitHost: () => prepareGitHost,
|
|
1160
|
+
provisionService: () => provisionService,
|
|
1161
|
+
pruneReleases: () => pruneReleases,
|
|
1162
|
+
pullRepo: () => pullRepo,
|
|
1136
1163
|
queueToTableNames: () => queueToTableNames,
|
|
1164
|
+
readCurrentRelease: () => readCurrentRelease,
|
|
1165
|
+
readReleaseBuildInfo: () => readReleaseBuildInfo,
|
|
1137
1166
|
readTaskIpcLogsSnapshot: () => readTaskIpcLogsSnapshot,
|
|
1138
1167
|
registerInServicesRegistry: () => registerInServicesRegistry,
|
|
1139
1168
|
registerRunnerHeartbeat: () => registerInServicesRegistry,
|
|
1169
|
+
releaseDir: () => releaseDir,
|
|
1170
|
+
releaseStamp: () => releaseStamp,
|
|
1171
|
+
reloadPm2: () => reloadPm2,
|
|
1140
1172
|
resolveAsterisks: () => resolveAsterisks,
|
|
1141
1173
|
resolveIpcFileLogsDir: () => resolveIpcFileLogsDir,
|
|
1174
|
+
resolveNextVersion: () => resolveNextVersion,
|
|
1142
1175
|
resolveRanges: () => resolveRanges,
|
|
1176
|
+
resolveServiceFrom: () => resolveServiceFrom,
|
|
1143
1177
|
resolveSteps: () => resolveSteps,
|
|
1178
|
+
rollbackService: () => rollbackService,
|
|
1179
|
+
run: () => run,
|
|
1144
1180
|
runNodeTaskScript: () => runNodeTaskScript,
|
|
1181
|
+
runReleaseTests: () => runReleaseTests,
|
|
1182
|
+
runRemoteCli: () => runRemoteCli,
|
|
1183
|
+
runRemoteStatus: () => runRemoteStatus,
|
|
1184
|
+
runShell: () => runShell,
|
|
1145
1185
|
runTasksLoop: () => runTasksLoop,
|
|
1186
|
+
scrubEnvContent: () => scrubEnvContent,
|
|
1187
|
+
servicePaths: () => servicePaths,
|
|
1146
1188
|
setupContext: () => setupContext,
|
|
1189
|
+
shellQuote: () => shellQuote,
|
|
1147
1190
|
showListScreen: () => showListScreen,
|
|
1148
1191
|
showMenuScreen: () => showMenuScreen,
|
|
1149
1192
|
showMultiColumnListScreen: () => showMultiColumnListScreen,
|
|
1150
1193
|
showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
|
|
1151
1194
|
showScreen: () => showScreen,
|
|
1152
1195
|
showWordGridScreen: () => showWordGridScreen,
|
|
1196
|
+
sshRun: () => sshRun,
|
|
1197
|
+
syncEnv: () => syncEnv,
|
|
1153
1198
|
taskHistoryInsertFromQueueRow: () => taskHistoryInsertFromQueueRow,
|
|
1154
1199
|
timeMatcher: () => timeMatcher,
|
|
1155
1200
|
touchRunnerHeartbeat: () => touchServicesRegistry,
|
|
@@ -1165,7 +1210,8 @@ __export(src_exports, {
|
|
|
1165
1210
|
useMemo: () => import_react5.useMemo,
|
|
1166
1211
|
useRef: () => import_react5.useRef,
|
|
1167
1212
|
useState: () => import_react5.useState,
|
|
1168
|
-
waitForTaskResult: () => waitForTaskResult
|
|
1213
|
+
waitForTaskResult: () => waitForTaskResult,
|
|
1214
|
+
writeReleaseBuildInfo: () => writeReleaseBuildInfo
|
|
1169
1215
|
});
|
|
1170
1216
|
module.exports = __toCommonJS(src_exports);
|
|
1171
1217
|
|
|
@@ -1813,7 +1859,8 @@ var Params = class _Params {
|
|
|
1813
1859
|
/**
|
|
1814
1860
|
* Resolved early in constructor so cleanup does not read params lazily.
|
|
1815
1861
|
* One of: false (off) | "end" (print at exit) | "top" (print after init,
|
|
1816
|
-
* via context.showUsedParamsIfNeeded())
|
|
1862
|
+
* via context.showUsedParamsIfNeeded()) | "stop" (print after init, then
|
|
1863
|
+
* exit the process — also via context.showUsedParamsIfNeeded()).
|
|
1817
1864
|
*/
|
|
1818
1865
|
_showUsedParamsMode = false;
|
|
1819
1866
|
/** Guard so the dump prints at most once (top OR end, never both). */
|
|
@@ -1837,8 +1884,9 @@ var Params = class _Params {
|
|
|
1837
1884
|
* (absent) / --no-showUsedParams / =false -> false (off)
|
|
1838
1885
|
* --showUsedParams / =true -> "end" (print at exit)
|
|
1839
1886
|
* --showUsedParams=top -> "top" (print after init)
|
|
1840
|
-
*
|
|
1841
|
-
*
|
|
1887
|
+
* --showUsedParams=stop -> "stop" (print after init, then exit)
|
|
1888
|
+
* Read raw (uncoerced) from args so the string "top"/"stop" isn't forced to
|
|
1889
|
+
* a boolean, then track it under the "script" module for the dump itself.
|
|
1842
1890
|
*/
|
|
1843
1891
|
_resolveShowUsedParams() {
|
|
1844
1892
|
const raw = this.args.get("showUsedParams");
|
|
@@ -1848,6 +1896,8 @@ var Params = class _Params {
|
|
|
1848
1896
|
mode = false;
|
|
1849
1897
|
} else if (typeof raw === "string" && raw.trim().toLowerCase() === "top") {
|
|
1850
1898
|
mode = "top";
|
|
1899
|
+
} else if (typeof raw === "string" && raw.trim().toLowerCase() === "stop") {
|
|
1900
|
+
mode = "stop";
|
|
1851
1901
|
} else {
|
|
1852
1902
|
const s = typeof raw === "string" ? raw.trim().toLowerCase() : raw;
|
|
1853
1903
|
const falsey = s === false || s === "false" || s === "0" || s === "no" || s === "off";
|
|
@@ -1861,7 +1911,7 @@ var Params = class _Params {
|
|
|
1861
1911
|
getShowUsedParams() {
|
|
1862
1912
|
return this._showUsedParamsMode !== false;
|
|
1863
1913
|
}
|
|
1864
|
-
/** Resolved mode: false | "end" | "top". */
|
|
1914
|
+
/** Resolved mode: false | "end" | "top" | "stop". */
|
|
1865
1915
|
getShowUsedParamsMode() {
|
|
1866
1916
|
return this._showUsedParamsMode;
|
|
1867
1917
|
}
|
|
@@ -2555,8 +2605,8 @@ var FileDatabase = class _FileDatabase {
|
|
|
2555
2605
|
const items = await import_fs4.default.promises.readdir(destPath);
|
|
2556
2606
|
const versions = items.filter((item) => {
|
|
2557
2607
|
const itemPath = import_path4.default.join(destPath, item);
|
|
2558
|
-
const
|
|
2559
|
-
return
|
|
2608
|
+
const stat2 = import_fs4.default.statSync(itemPath);
|
|
2609
|
+
return stat2.isDirectory() && isTimestampFolder(item);
|
|
2560
2610
|
});
|
|
2561
2611
|
return versions.sort();
|
|
2562
2612
|
} catch (error) {
|
|
@@ -2621,8 +2671,8 @@ var FileDatabase = class _FileDatabase {
|
|
|
2621
2671
|
}
|
|
2622
2672
|
const versionFolders = items.filter((item) => {
|
|
2623
2673
|
const itemPath = import_path4.default.join(tablePath, item);
|
|
2624
|
-
const
|
|
2625
|
-
return
|
|
2674
|
+
const stat2 = import_fs4.default.statSync(itemPath);
|
|
2675
|
+
return stat2.isDirectory() && isTimestampFolder(item);
|
|
2626
2676
|
});
|
|
2627
2677
|
if (versionFolders.length > 0) {
|
|
2628
2678
|
const latestVersion = versionFolders.sort().pop();
|
|
@@ -3896,6 +3946,7 @@ var S3 = class _S3 {
|
|
|
3896
3946
|
secretAccessKey: config2.secretAccessKey
|
|
3897
3947
|
};
|
|
3898
3948
|
}
|
|
3949
|
+
process.env.AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED ??= "true";
|
|
3899
3950
|
this.client = new import_client_s3.S3Client(clientConfig);
|
|
3900
3951
|
}
|
|
3901
3952
|
// ── info ────────────────────────────────────────────────────────────────
|
|
@@ -4077,6 +4128,189 @@ var S3 = class _S3 {
|
|
|
4077
4128
|
}
|
|
4078
4129
|
};
|
|
4079
4130
|
|
|
4131
|
+
// src/aws/index.js
|
|
4132
|
+
var import_client_sts = require("@aws-sdk/client-sts");
|
|
4133
|
+
var import_client_ec2 = require("@aws-sdk/client-ec2");
|
|
4134
|
+
var import_client_route_53 = require("@aws-sdk/client-route-53");
|
|
4135
|
+
var DEFAULT_REGION = "us-east-1";
|
|
4136
|
+
var CANONICAL_OWNER_ID = "099720109477";
|
|
4137
|
+
var DEFAULT_UBUNTU_PATTERN = "ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*";
|
|
4138
|
+
function nameTag(tags) {
|
|
4139
|
+
const t = (tags ?? []).find((x) => x.Key === "Name");
|
|
4140
|
+
return t?.Value ?? null;
|
|
4141
|
+
}
|
|
4142
|
+
var Aws = class _Aws {
|
|
4143
|
+
/**
|
|
4144
|
+
* Resolve credentials/region from params and return an Aws instance.
|
|
4145
|
+
* Does not call AWS until you invoke a method (clients are lazy).
|
|
4146
|
+
*/
|
|
4147
|
+
static async init(context, options = {}) {
|
|
4148
|
+
const getOpt = async (k) => {
|
|
4149
|
+
try {
|
|
4150
|
+
const v = await context?.params?.get?.(k, "string");
|
|
4151
|
+
return v != null && String(v).trim() !== "" ? v : void 0;
|
|
4152
|
+
} catch {
|
|
4153
|
+
return void 0;
|
|
4154
|
+
}
|
|
4155
|
+
};
|
|
4156
|
+
const config2 = {
|
|
4157
|
+
region: options.region ?? await getOpt("awsRegion") ?? DEFAULT_REGION,
|
|
4158
|
+
accessKeyId: options.accessKeyId ?? await getOpt("awsAccessKeyId"),
|
|
4159
|
+
secretAccessKey: options.secretAccessKey ?? await getOpt("awsSecretAccessKey"),
|
|
4160
|
+
sessionToken: options.sessionToken ?? await getOpt("awsSessionToken")
|
|
4161
|
+
};
|
|
4162
|
+
const aws = new _Aws(context, config2);
|
|
4163
|
+
context?.logger?.debug?.(
|
|
4164
|
+
`[aws] region=${config2.region} credentials=${config2.accessKeyId ? "explicit" : "default-chain"}`
|
|
4165
|
+
);
|
|
4166
|
+
return aws;
|
|
4167
|
+
}
|
|
4168
|
+
constructor(context, config2) {
|
|
4169
|
+
this.logger = context?.logger ?? console;
|
|
4170
|
+
this.region = config2.region || DEFAULT_REGION;
|
|
4171
|
+
this._credentials = config2.accessKeyId && config2.secretAccessKey ? {
|
|
4172
|
+
accessKeyId: config2.accessKeyId,
|
|
4173
|
+
secretAccessKey: config2.secretAccessKey,
|
|
4174
|
+
...config2.sessionToken ? { sessionToken: config2.sessionToken } : {}
|
|
4175
|
+
} : void 0;
|
|
4176
|
+
process.env.AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED ??= "true";
|
|
4177
|
+
this._clients = {};
|
|
4178
|
+
}
|
|
4179
|
+
// ── clients (lazy, optionally region-scoped) ──────────────────────────────
|
|
4180
|
+
_clientConfig(region) {
|
|
4181
|
+
const cfg = { region: region || this.region };
|
|
4182
|
+
if (this._credentials) cfg.credentials = this._credentials;
|
|
4183
|
+
return cfg;
|
|
4184
|
+
}
|
|
4185
|
+
_sts() {
|
|
4186
|
+
return this._clients.sts ??= new import_client_sts.STSClient(this._clientConfig());
|
|
4187
|
+
}
|
|
4188
|
+
_ec2(region) {
|
|
4189
|
+
const key = `ec2:${region || this.region}`;
|
|
4190
|
+
return this._clients[key] ??= new import_client_ec2.EC2Client(this._clientConfig(region));
|
|
4191
|
+
}
|
|
4192
|
+
_route53() {
|
|
4193
|
+
return this._clients.route53 ??= new import_client_route_53.Route53Client(this._clientConfig());
|
|
4194
|
+
}
|
|
4195
|
+
getRegion() {
|
|
4196
|
+
return this.region;
|
|
4197
|
+
}
|
|
4198
|
+
// ── identity ──────────────────────────────────────────────────────────────
|
|
4199
|
+
/** { account, arn, userId } — confirm which account/identity the keys belong to. */
|
|
4200
|
+
async whoAmI() {
|
|
4201
|
+
const out = await this._sts().send(new import_client_sts.GetCallerIdentityCommand({}));
|
|
4202
|
+
return { account: out.Account, arn: out.Arn, userId: out.UserId };
|
|
4203
|
+
}
|
|
4204
|
+
// ── regions / AZs ──────────────────────────────────────────────────────────
|
|
4205
|
+
/** Enabled region names, sorted. */
|
|
4206
|
+
async listRegions({ allRegions = false } = {}) {
|
|
4207
|
+
const out = await this._ec2().send(new import_client_ec2.DescribeRegionsCommand({ AllRegions: allRegions }));
|
|
4208
|
+
return (out.Regions ?? []).map((r) => r.RegionName).sort();
|
|
4209
|
+
}
|
|
4210
|
+
/** Availability zone names for a region (defaults to the instance region). */
|
|
4211
|
+
async listAvailabilityZones(region) {
|
|
4212
|
+
const out = await this._ec2(region).send(new import_client_ec2.DescribeAvailabilityZonesCommand({}));
|
|
4213
|
+
return (out.AvailabilityZones ?? []).filter((z) => z.State === "available").map((z) => z.ZoneName).sort();
|
|
4214
|
+
}
|
|
4215
|
+
// ── VPC / subnets / SGs / key pairs ────────────────────────────────────────
|
|
4216
|
+
/** [{ id, cidr, isDefault, name }] in a region. */
|
|
4217
|
+
async listVpcs(region) {
|
|
4218
|
+
const out = await this._ec2(region).send(new import_client_ec2.DescribeVpcsCommand({}));
|
|
4219
|
+
return (out.Vpcs ?? []).map((v) => ({
|
|
4220
|
+
id: v.VpcId,
|
|
4221
|
+
cidr: v.CidrBlock,
|
|
4222
|
+
isDefault: !!v.IsDefault,
|
|
4223
|
+
name: nameTag(v.Tags)
|
|
4224
|
+
}));
|
|
4225
|
+
}
|
|
4226
|
+
/** [{ id, vpcId, az, cidr, public, name }]; pass vpcId to scope. */
|
|
4227
|
+
async listSubnets({ vpcId, region } = {}) {
|
|
4228
|
+
const Filters = vpcId ? [{ Name: "vpc-id", Values: [vpcId] }] : void 0;
|
|
4229
|
+
const out = await this._ec2(region).send(new import_client_ec2.DescribeSubnetsCommand({ Filters }));
|
|
4230
|
+
return (out.Subnets ?? []).map((s) => ({
|
|
4231
|
+
id: s.SubnetId,
|
|
4232
|
+
vpcId: s.VpcId,
|
|
4233
|
+
az: s.AvailabilityZone,
|
|
4234
|
+
cidr: s.CidrBlock,
|
|
4235
|
+
public: !!s.MapPublicIpOnLaunch,
|
|
4236
|
+
name: nameTag(s.Tags)
|
|
4237
|
+
}));
|
|
4238
|
+
}
|
|
4239
|
+
/** [{ id, name, vpcId, description }]; pass vpcId to scope. */
|
|
4240
|
+
async listSecurityGroups({ vpcId, region } = {}) {
|
|
4241
|
+
const Filters = vpcId ? [{ Name: "vpc-id", Values: [vpcId] }] : void 0;
|
|
4242
|
+
const out = await this._ec2(region).send(new import_client_ec2.DescribeSecurityGroupsCommand({ Filters }));
|
|
4243
|
+
return (out.SecurityGroups ?? []).map((g) => ({
|
|
4244
|
+
id: g.GroupId,
|
|
4245
|
+
name: g.GroupName,
|
|
4246
|
+
vpcId: g.VpcId,
|
|
4247
|
+
description: g.Description
|
|
4248
|
+
}));
|
|
4249
|
+
}
|
|
4250
|
+
/** [{ name, fingerprint }] EC2 key pairs in a region. */
|
|
4251
|
+
async listKeyPairs(region) {
|
|
4252
|
+
const out = await this._ec2(region).send(new import_client_ec2.DescribeKeyPairsCommand({}));
|
|
4253
|
+
return (out.KeyPairs ?? []).map((k) => ({ name: k.KeyName, fingerprint: k.KeyFingerprint }));
|
|
4254
|
+
}
|
|
4255
|
+
// ── Route53 ────────────────────────────────────────────────────────────────
|
|
4256
|
+
/** [{ id, name, private, recordCount }] — id is the bare zone id (no /hostedzone/). */
|
|
4257
|
+
async listHostedZones() {
|
|
4258
|
+
const zones = [];
|
|
4259
|
+
let marker;
|
|
4260
|
+
do {
|
|
4261
|
+
const out = await this._route53().send(new import_client_route_53.ListHostedZonesCommand({ Marker: marker }));
|
|
4262
|
+
for (const z of out.HostedZones ?? []) {
|
|
4263
|
+
zones.push({
|
|
4264
|
+
id: (z.Id ?? "").replace("/hostedzone/", ""),
|
|
4265
|
+
name: (z.Name ?? "").replace(/\.$/, ""),
|
|
4266
|
+
// strip trailing dot
|
|
4267
|
+
private: !!z.Config?.PrivateZone,
|
|
4268
|
+
recordCount: z.ResourceRecordSetCount
|
|
4269
|
+
});
|
|
4270
|
+
}
|
|
4271
|
+
marker = out.IsTruncated ? out.NextMarker : void 0;
|
|
4272
|
+
} while (marker);
|
|
4273
|
+
return zones;
|
|
4274
|
+
}
|
|
4275
|
+
/**
|
|
4276
|
+
* Best hosted zone for a domain: exact match, else the longest zone name that
|
|
4277
|
+
* is a suffix of the domain (so "api.foo.com" matches the "foo.com" zone).
|
|
4278
|
+
* Returns the zone object or null.
|
|
4279
|
+
*/
|
|
4280
|
+
async findHostedZoneForDomain(domain) {
|
|
4281
|
+
const target = String(domain ?? "").replace(/\.$/, "").toLowerCase();
|
|
4282
|
+
if (!target) return null;
|
|
4283
|
+
const zones = await this.listHostedZones();
|
|
4284
|
+
const exact = zones.find((z) => z.name.toLowerCase() === target);
|
|
4285
|
+
if (exact) return exact;
|
|
4286
|
+
return zones.filter((z) => target.endsWith(`.${z.name.toLowerCase()}`)).sort((a, b) => b.name.length - a.name.length)[0] ?? null;
|
|
4287
|
+
}
|
|
4288
|
+
// ── AMI lookup ──────────────────────────────────────────────────────────────
|
|
4289
|
+
/**
|
|
4290
|
+
* Latest Ubuntu AMI matching a name pattern in a region.
|
|
4291
|
+
* Returns { id, name, creationDate, architecture } or null.
|
|
4292
|
+
*/
|
|
4293
|
+
async findLatestUbuntuAmi({ region, pattern = DEFAULT_UBUNTU_PATTERN, architecture = "x86_64" } = {}) {
|
|
4294
|
+
const out = await this._ec2(region).send(new import_client_ec2.DescribeImagesCommand({
|
|
4295
|
+
Owners: [CANONICAL_OWNER_ID],
|
|
4296
|
+
Filters: [
|
|
4297
|
+
{ Name: "name", Values: [pattern] },
|
|
4298
|
+
{ Name: "virtualization-type", Values: ["hvm"] },
|
|
4299
|
+
{ Name: "state", Values: ["available"] },
|
|
4300
|
+
...architecture ? [{ Name: "architecture", Values: [architecture] }] : []
|
|
4301
|
+
]
|
|
4302
|
+
}));
|
|
4303
|
+
const newest = (out.Images ?? []).sort((a, b) => String(b.CreationDate).localeCompare(String(a.CreationDate)))[0];
|
|
4304
|
+
if (!newest) return null;
|
|
4305
|
+
return {
|
|
4306
|
+
id: newest.ImageId,
|
|
4307
|
+
name: newest.Name,
|
|
4308
|
+
creationDate: newest.CreationDate,
|
|
4309
|
+
architecture: newest.Architecture
|
|
4310
|
+
};
|
|
4311
|
+
}
|
|
4312
|
+
};
|
|
4313
|
+
|
|
4080
4314
|
// src/logger/index.js
|
|
4081
4315
|
var import_chalk = __toESM(require("chalk"), 1);
|
|
4082
4316
|
var import_util = __toESM(require("util"), 1);
|
|
@@ -4427,9 +4661,18 @@ function setup(opts = {}) {
|
|
|
4427
4661
|
// the used-params list now (after the script has initialized all its
|
|
4428
4662
|
// own components), instead of at exit. No-op for the default mode,
|
|
4429
4663
|
// which prints at exit via the cleanup registered by Params.
|
|
4664
|
+
//
|
|
4665
|
+
// --showUsedParams=stop behaves like "top" but then exits immediately —
|
|
4666
|
+
// a quick "show me the figured params and quit" that skips the flow's
|
|
4667
|
+
// actual work. Like --stopAfter=init, this is a hard exit(0) (registered
|
|
4668
|
+
// cleanups are skipped); call it once components/params are resolved.
|
|
4430
4669
|
showUsedParamsIfNeeded: () => {
|
|
4431
|
-
|
|
4432
|
-
|
|
4670
|
+
const mode = params.getShowUsedParamsMode?.();
|
|
4671
|
+
if (mode !== "top" && mode !== "stop") return;
|
|
4672
|
+
params.printUsedParams(logger);
|
|
4673
|
+
if (mode === "stop") {
|
|
4674
|
+
logger.debug?.("[showUsedParams=stop] params printed \u2014 exiting");
|
|
4675
|
+
process.exit(0);
|
|
4433
4676
|
}
|
|
4434
4677
|
}
|
|
4435
4678
|
};
|
|
@@ -4440,12 +4683,1061 @@ function setupContext(opts = {}) {
|
|
|
4440
4683
|
return setup(opts);
|
|
4441
4684
|
}
|
|
4442
4685
|
|
|
4686
|
+
// src/deploy/service.js
|
|
4687
|
+
function deriveRepoDirName(repoUrl) {
|
|
4688
|
+
const tail = String(repoUrl ?? "").split("/").pop() ?? "repo";
|
|
4689
|
+
return tail.replace(/\.git$/, "") || "repo";
|
|
4690
|
+
}
|
|
4691
|
+
function defineService(service) {
|
|
4692
|
+
if (!service?.name) throw new Error("service manifest needs a `name`");
|
|
4693
|
+
if (!service.appsRoot) throw new Error(`service "${service.name}" needs an appsRoot`);
|
|
4694
|
+
if (!service.repoUrl) throw new Error(`service "${service.name}" needs a repoUrl`);
|
|
4695
|
+
if (!service.pm2?.script) throw new Error(`service "${service.name}" needs pm2.script`);
|
|
4696
|
+
const pm2 = {
|
|
4697
|
+
appName: service.name,
|
|
4698
|
+
args: "",
|
|
4699
|
+
...service.pm2
|
|
4700
|
+
};
|
|
4701
|
+
const nginx = service.nginx ? { siteName: service.name, ...service.nginx } : null;
|
|
4702
|
+
return {
|
|
4703
|
+
repoDirName: deriveRepoDirName(service.repoUrl),
|
|
4704
|
+
repoSubdir: "",
|
|
4705
|
+
keepReleases: 3,
|
|
4706
|
+
testCommand: null,
|
|
4707
|
+
envScrubPatterns: [],
|
|
4708
|
+
legacyRepoEnv: null,
|
|
4709
|
+
buildInfoPath: "build-info.json",
|
|
4710
|
+
deployKey: null,
|
|
4711
|
+
requireEnv: false,
|
|
4712
|
+
...service,
|
|
4713
|
+
pm2,
|
|
4714
|
+
nginx
|
|
4715
|
+
};
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4718
|
+
// src/deploy/paths.js
|
|
4719
|
+
var import_node_path = require("path");
|
|
4720
|
+
function servicePaths(service) {
|
|
4721
|
+
const root = service.appsRoot;
|
|
4722
|
+
const repoDirName = service.repoDirName ?? "repo";
|
|
4723
|
+
const repoSubdir = service.repoSubdir ?? "";
|
|
4724
|
+
const repo = (0, import_node_path.join)(root, repoDirName);
|
|
4725
|
+
const repoRun = repoSubdir ? (0, import_node_path.join)(repo, repoSubdir) : repo;
|
|
4726
|
+
return {
|
|
4727
|
+
root,
|
|
4728
|
+
repo,
|
|
4729
|
+
repoRun,
|
|
4730
|
+
repoEnv: (0, import_node_path.join)(repoRun, ".env"),
|
|
4731
|
+
releases: (0, import_node_path.join)(root, "releases"),
|
|
4732
|
+
shared: (0, import_node_path.join)(root, "shared"),
|
|
4733
|
+
logs: (0, import_node_path.join)(root, "logs"),
|
|
4734
|
+
current: (0, import_node_path.join)(root, "current"),
|
|
4735
|
+
sharedEnv: (0, import_node_path.join)(root, "shared", ".env"),
|
|
4736
|
+
ecosystem: (0, import_node_path.join)(root, "shared", "ecosystem.config.cjs"),
|
|
4737
|
+
deployLog: (0, import_node_path.join)(root, "logs", "deploy.log"),
|
|
4738
|
+
lockHashFile: (0, import_node_path.join)(root, "shared", ".package-lock.sha256")
|
|
4739
|
+
};
|
|
4740
|
+
}
|
|
4741
|
+
function releaseStamp(date = /* @__PURE__ */ new Date()) {
|
|
4742
|
+
return date.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
4743
|
+
}
|
|
4744
|
+
function releaseDir(releasesRoot, stamp) {
|
|
4745
|
+
return (0, import_node_path.join)(releasesRoot, stamp);
|
|
4746
|
+
}
|
|
4747
|
+
|
|
4748
|
+
// src/deploy/run.js
|
|
4749
|
+
var import_node_child_process = require("child_process");
|
|
4750
|
+
function npmEnv(baseEnv = process.env) {
|
|
4751
|
+
const env = { ...baseEnv };
|
|
4752
|
+
delete env.NODE_ENV;
|
|
4753
|
+
return env;
|
|
4754
|
+
}
|
|
4755
|
+
function npmInstallEnv(baseEnv = process.env) {
|
|
4756
|
+
return {
|
|
4757
|
+
...npmEnv(baseEnv),
|
|
4758
|
+
NPM_CONFIG_PRODUCTION: "false",
|
|
4759
|
+
npm_config_production: "false"
|
|
4760
|
+
};
|
|
4761
|
+
}
|
|
4762
|
+
function run(cmd, args, options = {}) {
|
|
4763
|
+
const { cwd, env, logger } = options;
|
|
4764
|
+
return new Promise((resolve3, reject) => {
|
|
4765
|
+
logger?.info?.(`$ ${cmd} ${args.join(" ")}${cwd ? ` (cwd=${cwd})` : ""}`);
|
|
4766
|
+
const child = (0, import_node_child_process.spawn)(cmd, args, {
|
|
4767
|
+
cwd,
|
|
4768
|
+
env: env ?? process.env,
|
|
4769
|
+
stdio: "inherit"
|
|
4770
|
+
});
|
|
4771
|
+
child.on("error", reject);
|
|
4772
|
+
child.on("close", (code) => {
|
|
4773
|
+
if (code === 0) resolve3();
|
|
4774
|
+
else reject(new Error(`${cmd} exited with code ${code}`));
|
|
4775
|
+
});
|
|
4776
|
+
});
|
|
4777
|
+
}
|
|
4778
|
+
function runShell(command, options = {}) {
|
|
4779
|
+
return run("bash", ["-lc", command], options);
|
|
4780
|
+
}
|
|
4781
|
+
|
|
4782
|
+
// src/deploy/log.js
|
|
4783
|
+
var import_promises = require("fs/promises");
|
|
4784
|
+
async function appendDeployLog(deployLogPath, message) {
|
|
4785
|
+
await (0, import_promises.mkdir)(deployLogPath.replace(/\/[^/]+$/, ""), { recursive: true });
|
|
4786
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
|
|
4787
|
+
`;
|
|
4788
|
+
await (0, import_promises.appendFile)(deployLogPath, line);
|
|
4789
|
+
}
|
|
4790
|
+
|
|
4791
|
+
// src/deploy/git.js
|
|
4792
|
+
var import_promises2 = require("fs/promises");
|
|
4793
|
+
async function pathExists(path5) {
|
|
4794
|
+
try {
|
|
4795
|
+
await (0, import_promises2.access)(path5);
|
|
4796
|
+
return true;
|
|
4797
|
+
} catch {
|
|
4798
|
+
return false;
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
async function cloneRepo(service, options = {}) {
|
|
4802
|
+
const { dryRun = false, logger = console } = options;
|
|
4803
|
+
const paths = servicePaths(service);
|
|
4804
|
+
if (await pathExists(paths.repo)) {
|
|
4805
|
+
throw new Error(`Repo already exists at ${paths.repo} \u2014 use git pull instead`);
|
|
4806
|
+
}
|
|
4807
|
+
if (dryRun) {
|
|
4808
|
+
logger.info(`[dryRun] would git clone ${service.repoUrl} ${paths.repo}`);
|
|
4809
|
+
return;
|
|
4810
|
+
}
|
|
4811
|
+
await run("git", ["clone", service.repoUrl, paths.repo], { logger });
|
|
4812
|
+
logger.info(`cloned ${service.repoUrl} \u2192 ${paths.repo}`);
|
|
4813
|
+
}
|
|
4814
|
+
async function pullRepo(service, options = {}) {
|
|
4815
|
+
const { dryRun = false, logger = console } = options;
|
|
4816
|
+
const paths = servicePaths(service);
|
|
4817
|
+
if (!await pathExists(paths.repo)) {
|
|
4818
|
+
throw new Error(`Repo missing at ${paths.repo} \u2014 run provision first`);
|
|
4819
|
+
}
|
|
4820
|
+
if (dryRun) {
|
|
4821
|
+
logger.info(`[dryRun] would git -C ${paths.repo} pull --ff-only`);
|
|
4822
|
+
return;
|
|
4823
|
+
}
|
|
4824
|
+
await run("git", ["-C", paths.repo, "pull", "--ff-only"], { logger });
|
|
4825
|
+
logger.info(`pulled ${paths.repo}`);
|
|
4826
|
+
}
|
|
4827
|
+
|
|
4828
|
+
// src/deploy/release.js
|
|
4829
|
+
var import_promises3 = require("fs/promises");
|
|
4830
|
+
var import_node_path2 = require("path");
|
|
4831
|
+
var SKIP_TOP = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
4832
|
+
function shouldCopyEntry(srcPath) {
|
|
4833
|
+
const parts = srcPath.split(import_node_path2.sep);
|
|
4834
|
+
if (parts.some((p) => p === "node_modules" || p === ".git")) return false;
|
|
4835
|
+
const top = parts.at(-1);
|
|
4836
|
+
if (parts.length === 1 && SKIP_TOP.has(top)) return false;
|
|
4837
|
+
return true;
|
|
4838
|
+
}
|
|
4839
|
+
async function createRelease(service, options = {}) {
|
|
4840
|
+
const { stamp = releaseStamp(), dryRun = false, logger = console } = options;
|
|
4841
|
+
const paths = servicePaths(service);
|
|
4842
|
+
const dest = releaseDir(paths.releases, stamp);
|
|
4843
|
+
if (dryRun) {
|
|
4844
|
+
logger.info(`[dryRun] would copy ${paths.repoRun} \u2192 ${dest}`);
|
|
4845
|
+
return { stamp, path: dest };
|
|
4846
|
+
}
|
|
4847
|
+
await (0, import_promises3.mkdir)(paths.releases, { recursive: true });
|
|
4848
|
+
await (0, import_promises3.cp)(paths.repoRun, dest, {
|
|
4849
|
+
recursive: true,
|
|
4850
|
+
filter: (src) => shouldCopyEntry(src)
|
|
4851
|
+
});
|
|
4852
|
+
logger.info(`release ${stamp} created at ${dest}`);
|
|
4853
|
+
return { stamp, path: dest };
|
|
4854
|
+
}
|
|
4855
|
+
async function readCurrentRelease(paths) {
|
|
4856
|
+
try {
|
|
4857
|
+
const target = await (0, import_promises3.readlink)(paths.current);
|
|
4858
|
+
return target.startsWith("/") ? target : (0, import_node_path2.join)((0, import_node_path2.dirname)(paths.current), target);
|
|
4859
|
+
} catch {
|
|
4860
|
+
return null;
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
async function listReleases(paths) {
|
|
4864
|
+
let names;
|
|
4865
|
+
try {
|
|
4866
|
+
names = await (0, import_promises3.readdir)(paths.releases);
|
|
4867
|
+
} catch {
|
|
4868
|
+
return [];
|
|
4869
|
+
}
|
|
4870
|
+
const entries = [];
|
|
4871
|
+
for (const name of names) {
|
|
4872
|
+
const full = releaseDir(paths.releases, name);
|
|
4873
|
+
const s = await (0, import_promises3.stat)(full);
|
|
4874
|
+
if (s.isDirectory()) entries.push({ name, path: full, mtime: s.mtime });
|
|
4875
|
+
}
|
|
4876
|
+
entries.sort((a, b) => b.name.localeCompare(a.name));
|
|
4877
|
+
return entries;
|
|
4878
|
+
}
|
|
4879
|
+
|
|
4880
|
+
// src/deploy/activate.js
|
|
4881
|
+
var import_promises4 = require("fs/promises");
|
|
4882
|
+
var import_node_path3 = require("path");
|
|
4883
|
+
async function activateRelease(releasePath, paths, options = {}) {
|
|
4884
|
+
const { dryRun = false, logger = console } = options;
|
|
4885
|
+
if (dryRun) {
|
|
4886
|
+
logger.info(`[dryRun] would activate ${releasePath} \u2192 ${paths.current}`);
|
|
4887
|
+
return;
|
|
4888
|
+
}
|
|
4889
|
+
try {
|
|
4890
|
+
await (0, import_promises4.unlink)(paths.current);
|
|
4891
|
+
} catch {
|
|
4892
|
+
}
|
|
4893
|
+
await (0, import_promises4.symlink)(releasePath, paths.current);
|
|
4894
|
+
const envLink = (0, import_node_path3.join)(releasePath, ".env");
|
|
4895
|
+
try {
|
|
4896
|
+
await (0, import_promises4.unlink)(envLink);
|
|
4897
|
+
} catch {
|
|
4898
|
+
}
|
|
4899
|
+
await (0, import_promises4.symlink)(paths.sharedEnv, envLink);
|
|
4900
|
+
logger.info(`active release: ${releasePath}`);
|
|
4901
|
+
}
|
|
4902
|
+
|
|
4903
|
+
// src/deploy/deps.js
|
|
4904
|
+
var import_node_crypto = require("crypto");
|
|
4905
|
+
var import_promises5 = require("fs/promises");
|
|
4906
|
+
var import_node_path4 = require("path");
|
|
4907
|
+
async function pathExists2(path5) {
|
|
4908
|
+
try {
|
|
4909
|
+
await (0, import_promises5.access)(path5);
|
|
4910
|
+
return true;
|
|
4911
|
+
} catch {
|
|
4912
|
+
return false;
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
async function lockfileHash(releasePath) {
|
|
4916
|
+
const lockPath = (0, import_node_path4.join)(releasePath, "package-lock.json");
|
|
4917
|
+
try {
|
|
4918
|
+
const buf = await (0, import_promises5.readFile)(lockPath);
|
|
4919
|
+
return (0, import_node_crypto.createHash)("sha256").update(buf).digest("hex");
|
|
4920
|
+
} catch {
|
|
4921
|
+
return null;
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
async function readHashFile(path5) {
|
|
4925
|
+
try {
|
|
4926
|
+
return (await (0, import_promises5.readFile)(path5, "utf8")).trim();
|
|
4927
|
+
} catch {
|
|
4928
|
+
return null;
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
async function npmInstall(releasePath, hasLock, logger) {
|
|
4932
|
+
await (0, import_promises5.rm)((0, import_node_path4.join)(releasePath, "node_modules"), { recursive: true, force: true });
|
|
4933
|
+
const cmd = hasLock ? ["ci", "--include=dev"] : ["install", "--include=dev", "--no-audit", "--no-fund"];
|
|
4934
|
+
logger.info(`running npm ${cmd.join(" ")} in ${releasePath}`);
|
|
4935
|
+
await run("npm", cmd, { cwd: releasePath, logger, env: npmInstallEnv() });
|
|
4936
|
+
}
|
|
4937
|
+
async function installDeps(service, releasePath, paths, options = {}) {
|
|
4938
|
+
const { dryRun = false, logger = console } = options;
|
|
4939
|
+
if (dryRun) {
|
|
4940
|
+
logger.info(`[dryRun] would install deps in ${releasePath}`);
|
|
4941
|
+
return { hash: null, copied: false };
|
|
4942
|
+
}
|
|
4943
|
+
const hash = await lockfileHash(releasePath);
|
|
4944
|
+
const hasLock = hash !== null;
|
|
4945
|
+
const hashMarker = (0, import_node_path4.join)(releasePath, ".deploy-package-lock.sha256");
|
|
4946
|
+
const currentPath = await readCurrentRelease(paths);
|
|
4947
|
+
let copied = false;
|
|
4948
|
+
if (hasLock && currentPath && await pathExists2((0, import_node_path4.join)(currentPath, "node_modules"))) {
|
|
4949
|
+
const currentHash = await readHashFile((0, import_node_path4.join)(currentPath, ".deploy-package-lock.sha256"));
|
|
4950
|
+
if (currentHash === hash) {
|
|
4951
|
+
logger.info("lockfile unchanged \u2014 copying node_modules from current release");
|
|
4952
|
+
await (0, import_promises5.cp)((0, import_node_path4.join)(currentPath, "node_modules"), (0, import_node_path4.join)(releasePath, "node_modules"), {
|
|
4953
|
+
recursive: true,
|
|
4954
|
+
force: true
|
|
4955
|
+
});
|
|
4956
|
+
copied = true;
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
if (!copied) {
|
|
4960
|
+
await npmInstall(releasePath, hasLock, logger);
|
|
4961
|
+
}
|
|
4962
|
+
if (hasLock) {
|
|
4963
|
+
await (0, import_promises5.writeFile)(hashMarker, `${hash}
|
|
4964
|
+
`, "utf8");
|
|
4965
|
+
await (0, import_promises5.mkdir)(paths.shared, { recursive: true });
|
|
4966
|
+
await (0, import_promises5.writeFile)(paths.lockHashFile, `${hash}
|
|
4967
|
+
`, "utf8");
|
|
4968
|
+
}
|
|
4969
|
+
return { hash, copied };
|
|
4970
|
+
}
|
|
4971
|
+
|
|
4972
|
+
// src/deploy/test.js
|
|
4973
|
+
var import_promises6 = require("fs/promises");
|
|
4974
|
+
var import_node_path5 = require("path");
|
|
4975
|
+
async function runReleaseTests(service, releasePath, paths, options = {}) {
|
|
4976
|
+
const { dryRun = false, logger = console } = options;
|
|
4977
|
+
if (!service.testCommand) {
|
|
4978
|
+
logger.info("no testCommand configured \u2014 skipping tests");
|
|
4979
|
+
return;
|
|
4980
|
+
}
|
|
4981
|
+
if (dryRun) {
|
|
4982
|
+
logger.info(`[dryRun] would run "${service.testCommand}" in ${releasePath}`);
|
|
4983
|
+
return;
|
|
4984
|
+
}
|
|
4985
|
+
const envLink = (0, import_node_path5.join)(releasePath, ".env");
|
|
4986
|
+
try {
|
|
4987
|
+
await (0, import_promises6.unlink)(envLink);
|
|
4988
|
+
} catch {
|
|
4989
|
+
}
|
|
4990
|
+
await (0, import_promises6.symlink)(paths.sharedEnv, envLink);
|
|
4991
|
+
logger.info(`running "${service.testCommand}" in ${releasePath}`);
|
|
4992
|
+
await runShell(service.testCommand, { cwd: releasePath, logger, env: npmInstallEnv() });
|
|
4993
|
+
}
|
|
4994
|
+
|
|
4995
|
+
// src/deploy/prune.js
|
|
4996
|
+
var import_promises7 = require("fs/promises");
|
|
4997
|
+
async function pruneReleases(service, paths, options = {}) {
|
|
4998
|
+
const { dryRun = false, logger = console } = options;
|
|
4999
|
+
const keep = service.keepReleases ?? 3;
|
|
5000
|
+
const releases = await listReleases(paths);
|
|
5001
|
+
let activeName = null;
|
|
5002
|
+
try {
|
|
5003
|
+
const target = await (0, import_promises7.readlink)(paths.current);
|
|
5004
|
+
activeName = target.split("/").pop();
|
|
5005
|
+
} catch {
|
|
5006
|
+
}
|
|
5007
|
+
const keepSet = /* @__PURE__ */ new Set();
|
|
5008
|
+
for (const rel of releases) {
|
|
5009
|
+
if (keepSet.size < keep) keepSet.add(rel.name);
|
|
5010
|
+
}
|
|
5011
|
+
if (activeName) keepSet.add(activeName);
|
|
5012
|
+
const toRemove = releases.filter((rel) => !keepSet.has(rel.name));
|
|
5013
|
+
for (const rel of toRemove) {
|
|
5014
|
+
if (dryRun) {
|
|
5015
|
+
logger.info(`[dryRun] would rm -rf ${rel.path}`);
|
|
5016
|
+
} else {
|
|
5017
|
+
await (0, import_promises7.rm)(rel.path, { recursive: true, force: true });
|
|
5018
|
+
logger.info(`pruned ${rel.path}`);
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
return { removed: toRemove.map((r) => r.name) };
|
|
5022
|
+
}
|
|
5023
|
+
|
|
5024
|
+
// src/deploy/pm2.js
|
|
5025
|
+
async function reloadPm2(paths, options = {}) {
|
|
5026
|
+
const { dryRun = false, logger = console } = options;
|
|
5027
|
+
if (dryRun) {
|
|
5028
|
+
logger.info(`[dryRun] would pm2 startOrReload ${paths.ecosystem} --update-env`);
|
|
5029
|
+
return;
|
|
5030
|
+
}
|
|
5031
|
+
await runShell(`pm2 startOrReload "${paths.ecosystem}" --update-env`, { logger });
|
|
5032
|
+
logger.info("pm2 reloaded");
|
|
5033
|
+
}
|
|
5034
|
+
|
|
5035
|
+
// src/deploy/nginx.js
|
|
5036
|
+
var import_promises8 = require("fs/promises");
|
|
5037
|
+
var import_node_os = require("os");
|
|
5038
|
+
var import_node_path6 = require("path");
|
|
5039
|
+
async function hasTlsCert(certPath) {
|
|
5040
|
+
try {
|
|
5041
|
+
await runShell(`sudo test -f '${certPath}'`, {});
|
|
5042
|
+
return true;
|
|
5043
|
+
} catch {
|
|
5044
|
+
return false;
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
5047
|
+
function proxyBlock(port) {
|
|
5048
|
+
return ` location / {
|
|
5049
|
+
proxy_pass http://127.0.0.1:${port};
|
|
5050
|
+
proxy_http_version 1.1;
|
|
5051
|
+
proxy_set_header Host $host;
|
|
5052
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
5053
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
5054
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
5055
|
+
proxy_read_timeout 120s;
|
|
5056
|
+
}`;
|
|
5057
|
+
}
|
|
5058
|
+
function buildNginxConfig(service) {
|
|
5059
|
+
const { nginx, pm2 } = service;
|
|
5060
|
+
const certDir = `/etc/letsencrypt/live/${nginx.fqdn}`;
|
|
5061
|
+
return `# ${nginx.siteName} \u2014 managed by cli-toolkit deploy (proxy mode)
|
|
5062
|
+
# ${nginx.fqdn} \u2192 127.0.0.1:${pm2.port}
|
|
5063
|
+
|
|
5064
|
+
server {
|
|
5065
|
+
listen 80;
|
|
5066
|
+
listen [::]:80;
|
|
5067
|
+
server_name ${nginx.fqdn};
|
|
5068
|
+
location / { return 301 https://$host$request_uri; }
|
|
5069
|
+
}
|
|
5070
|
+
|
|
5071
|
+
server {
|
|
5072
|
+
listen 443 ssl;
|
|
5073
|
+
listen [::]:443 ssl;
|
|
5074
|
+
server_name ${nginx.fqdn};
|
|
5075
|
+
|
|
5076
|
+
ssl_certificate ${certDir}/fullchain.pem;
|
|
5077
|
+
ssl_certificate_key ${certDir}/privkey.pem;
|
|
5078
|
+
include /etc/letsencrypt/options-ssl-nginx.conf;
|
|
5079
|
+
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
|
5080
|
+
|
|
5081
|
+
client_max_body_size 25m;
|
|
5082
|
+
|
|
5083
|
+
${proxyBlock(pm2.port)}
|
|
5084
|
+
}
|
|
5085
|
+
`;
|
|
5086
|
+
}
|
|
5087
|
+
function buildNginxConfigHttpOnly(service) {
|
|
5088
|
+
const { nginx, pm2 } = service;
|
|
5089
|
+
return `# ${nginx.siteName} \u2014 managed by cli-toolkit deploy (HTTP proxy, no TLS cert yet)
|
|
5090
|
+
|
|
5091
|
+
server {
|
|
5092
|
+
listen 80;
|
|
5093
|
+
listen [::]:80;
|
|
5094
|
+
server_name ${nginx.fqdn};
|
|
5095
|
+
|
|
5096
|
+
client_max_body_size 25m;
|
|
5097
|
+
|
|
5098
|
+
${proxyBlock(pm2.port)}
|
|
5099
|
+
}
|
|
5100
|
+
`;
|
|
5101
|
+
}
|
|
5102
|
+
async function enableNginxUpstream(service, options = {}) {
|
|
5103
|
+
const { dryRun = false, logger = console } = options;
|
|
5104
|
+
const { nginx } = service;
|
|
5105
|
+
if (!nginx) {
|
|
5106
|
+
logger.info("no nginx config on service \u2014 skipping nginx step");
|
|
5107
|
+
return { skipped: true };
|
|
5108
|
+
}
|
|
5109
|
+
const siteAvailable = `/etc/nginx/sites-available/${nginx.siteName}`;
|
|
5110
|
+
const siteEnabled = `/etc/nginx/sites-enabled/${nginx.siteName}`;
|
|
5111
|
+
const cert = `/etc/letsencrypt/live/${nginx.fqdn}/fullchain.pem`;
|
|
5112
|
+
if (dryRun) {
|
|
5113
|
+
logger.info(`[dryRun] would write ${siteAvailable} (proxy \u2192 127.0.0.1:${service.pm2.port}) and reload nginx`);
|
|
5114
|
+
return { hasCert: null };
|
|
5115
|
+
}
|
|
5116
|
+
const hasCert = await hasTlsCert(cert);
|
|
5117
|
+
const config2 = hasCert ? buildNginxConfig(service) : buildNginxConfigHttpOnly(service);
|
|
5118
|
+
const tmp = (0, import_node_path6.join)((0, import_node_os.tmpdir)(), `${service.name}-nginx.conf`);
|
|
5119
|
+
await (0, import_promises8.writeFile)(tmp, config2);
|
|
5120
|
+
await runShell(
|
|
5121
|
+
`sudo cp '${tmp}' '${siteAvailable}' && sudo ln -sf '${siteAvailable}' '${siteEnabled}' && sudo nginx -t && sudo systemctl reload nginx`,
|
|
5122
|
+
{ logger }
|
|
5123
|
+
);
|
|
5124
|
+
logger.info(`nginx upstream enabled for ${nginx.fqdn} \u2192 127.0.0.1:${service.pm2.port} (tls=${hasCert})`);
|
|
5125
|
+
return { hasCert };
|
|
5126
|
+
}
|
|
5127
|
+
|
|
5128
|
+
// src/deploy/sync-env.js
|
|
5129
|
+
var import_promises9 = require("fs/promises");
|
|
5130
|
+
function scrubEnvContent(content, patterns = []) {
|
|
5131
|
+
const regexes = patterns.map((p) => p instanceof RegExp ? p : new RegExp(p));
|
|
5132
|
+
if (regexes.length === 0) return content;
|
|
5133
|
+
return content.split("\n").filter((line) => !regexes.some((re) => re.test(line.trim()))).join("\n");
|
|
5134
|
+
}
|
|
5135
|
+
async function pathExists3(path5) {
|
|
5136
|
+
try {
|
|
5137
|
+
await (0, import_promises9.access)(path5);
|
|
5138
|
+
return true;
|
|
5139
|
+
} catch {
|
|
5140
|
+
return false;
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
async function syncEnv(service, options = {}) {
|
|
5144
|
+
const { dryRun = false, logger = console } = options;
|
|
5145
|
+
const paths = servicePaths(service);
|
|
5146
|
+
const patterns = service.envScrubPatterns ?? [];
|
|
5147
|
+
if (dryRun) {
|
|
5148
|
+
logger.info(`[dryRun] would sync ${paths.repoEnv} \u2192 ${paths.sharedEnv}`);
|
|
5149
|
+
return { source: paths.repoEnv, dest: paths.sharedEnv };
|
|
5150
|
+
}
|
|
5151
|
+
let source = paths.repoEnv;
|
|
5152
|
+
if (!await pathExists3(source) && service.legacyRepoEnv && await pathExists3(service.legacyRepoEnv)) {
|
|
5153
|
+
logger.info(`using legacy env: ${service.legacyRepoEnv}`);
|
|
5154
|
+
source = service.legacyRepoEnv;
|
|
5155
|
+
}
|
|
5156
|
+
await (0, import_promises9.mkdir)(paths.shared, { recursive: true });
|
|
5157
|
+
if (!await pathExists3(source)) {
|
|
5158
|
+
if (service.requireEnv) {
|
|
5159
|
+
throw new Error(
|
|
5160
|
+
`No .env found at ${paths.repoEnv}` + (service.legacyRepoEnv ? ` or ${service.legacyRepoEnv}` : "") + " \u2014 place .env on the host or run the remote deploy from a laptop with a local .env (auto-scp)"
|
|
5161
|
+
);
|
|
5162
|
+
}
|
|
5163
|
+
if (!await pathExists3(paths.sharedEnv)) {
|
|
5164
|
+
await (0, import_promises9.writeFile)(paths.sharedEnv, "", { mode: 384 });
|
|
5165
|
+
}
|
|
5166
|
+
logger.warn(`no .env found (source ${source}) \u2014 using empty ${paths.sharedEnv}`);
|
|
5167
|
+
return { source: null, dest: paths.sharedEnv };
|
|
5168
|
+
}
|
|
5169
|
+
const raw = await (0, import_promises9.readFile)(source, "utf8");
|
|
5170
|
+
await (0, import_promises9.writeFile)(paths.sharedEnv, scrubEnvContent(raw, patterns), { mode: 384 });
|
|
5171
|
+
logger.info(`synced ${source} \u2192 ${paths.sharedEnv}`);
|
|
5172
|
+
return { source, dest: paths.sharedEnv };
|
|
5173
|
+
}
|
|
5174
|
+
|
|
5175
|
+
// src/deploy/build-info.js
|
|
5176
|
+
var import_promises10 = require("fs/promises");
|
|
5177
|
+
var import_node_path7 = require("path");
|
|
5178
|
+
var import_node_child_process2 = require("child_process");
|
|
5179
|
+
var import_node_util = require("util");
|
|
5180
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
|
|
5181
|
+
function bumpPatchVersion(version) {
|
|
5182
|
+
const parts = String(version).trim().split(".");
|
|
5183
|
+
const major = Number.parseInt(parts[0], 10) || 0;
|
|
5184
|
+
const minor = Number.parseInt(parts[1], 10) || 0;
|
|
5185
|
+
const patch = Number.parseInt(parts[2], 10) || 0;
|
|
5186
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
5187
|
+
}
|
|
5188
|
+
async function readJson(path5) {
|
|
5189
|
+
try {
|
|
5190
|
+
return JSON.parse(await (0, import_promises10.readFile)(path5, "utf8"));
|
|
5191
|
+
} catch {
|
|
5192
|
+
return null;
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
async function gitShortCommit(repoPath) {
|
|
5196
|
+
try {
|
|
5197
|
+
const { stdout } = await execFileAsync("git", ["-C", repoPath, "rev-parse", "--short", "HEAD"], {
|
|
5198
|
+
encoding: "utf8"
|
|
5199
|
+
});
|
|
5200
|
+
return stdout.trim() || null;
|
|
5201
|
+
} catch {
|
|
5202
|
+
return null;
|
|
5203
|
+
}
|
|
5204
|
+
}
|
|
5205
|
+
async function resolveNextVersion(service, paths, pkgPath) {
|
|
5206
|
+
const pkg = await readJson(pkgPath) ?? {};
|
|
5207
|
+
const baseVersion = pkg.version || "0.1.0";
|
|
5208
|
+
const currentPath = await readCurrentRelease(paths);
|
|
5209
|
+
if (currentPath) {
|
|
5210
|
+
const active = await readJson((0, import_node_path7.join)(currentPath, service.buildInfoPath));
|
|
5211
|
+
if (active?.version) return bumpPatchVersion(active.version);
|
|
5212
|
+
}
|
|
5213
|
+
return baseVersion;
|
|
5214
|
+
}
|
|
5215
|
+
async function readReleaseBuildInfo(service, releasePath) {
|
|
5216
|
+
return readJson((0, import_node_path7.join)(releasePath, service.buildInfoPath));
|
|
5217
|
+
}
|
|
5218
|
+
async function writeReleaseBuildInfo(service, releasePath, { stamp, dryRun = false, logger = console }) {
|
|
5219
|
+
const paths = servicePaths(service);
|
|
5220
|
+
const pkgPath = (0, import_node_path7.join)(paths.repoRun, "package.json");
|
|
5221
|
+
const version = await resolveNextVersion(service, paths, pkgPath);
|
|
5222
|
+
const gitCommit = await gitShortCommit(paths.repo);
|
|
5223
|
+
const buildInfo = {
|
|
5224
|
+
version,
|
|
5225
|
+
release: stamp,
|
|
5226
|
+
deployedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5227
|
+
gitCommit,
|
|
5228
|
+
service: service.name
|
|
5229
|
+
};
|
|
5230
|
+
const dest = (0, import_node_path7.join)(releasePath, service.buildInfoPath);
|
|
5231
|
+
if (dryRun) {
|
|
5232
|
+
logger.info(`[dryRun] would write ${dest} (${JSON.stringify(buildInfo)})`);
|
|
5233
|
+
return buildInfo;
|
|
5234
|
+
}
|
|
5235
|
+
await (0, import_promises10.mkdir)((0, import_node_path7.dirname)(dest), { recursive: true });
|
|
5236
|
+
await (0, import_promises10.writeFile)(dest, `${JSON.stringify(buildInfo, null, 2)}
|
|
5237
|
+
`, { mode: 420 });
|
|
5238
|
+
const gitSuffix = gitCommit ? ` git=${gitCommit}` : "";
|
|
5239
|
+
logger.info(`build info: v${version} release=${stamp}${gitSuffix}`);
|
|
5240
|
+
return buildInfo;
|
|
5241
|
+
}
|
|
5242
|
+
|
|
5243
|
+
// src/deploy/init-structure.js
|
|
5244
|
+
var import_promises11 = require("fs/promises");
|
|
5245
|
+
var import_node_path8 = require("path");
|
|
5246
|
+
async function pathExists4(path5) {
|
|
5247
|
+
try {
|
|
5248
|
+
await (0, import_promises11.access)(path5);
|
|
5249
|
+
return true;
|
|
5250
|
+
} catch {
|
|
5251
|
+
return false;
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
async function requireDeployRoot(parentDir, serviceRoot) {
|
|
5255
|
+
if (await pathExists4(parentDir)) return;
|
|
5256
|
+
throw new Error(
|
|
5257
|
+
`Cannot create ${serviceRoot}: parent directory ${parentDir} does not exist.
|
|
5258
|
+
This is meant to run on the target host (where ${parentDir} exists). For a local dry run use --appsRoot=/tmp/<service>.`
|
|
5259
|
+
);
|
|
5260
|
+
}
|
|
5261
|
+
function buildEcosystemConfig(service, paths) {
|
|
5262
|
+
const { pm2 } = service;
|
|
5263
|
+
const outLog = (0, import_node_path8.join)(paths.logs, `${pm2.appName}.out.log`);
|
|
5264
|
+
const errLog = (0, import_node_path8.join)(paths.logs, `${pm2.appName}.err.log`);
|
|
5265
|
+
return `/**
|
|
5266
|
+
* pm2 ecosystem for ${service.name} \u2014 seeded by cli-toolkit deploy (init).
|
|
5267
|
+
*
|
|
5268
|
+
* cwd points at the \`current\` symlink (created on first deploy).
|
|
5269
|
+
* Tweak \`args\` here, then: pm2 reload ${paths.ecosystem} --update-env
|
|
5270
|
+
*/
|
|
5271
|
+
module.exports = {
|
|
5272
|
+
apps: [
|
|
5273
|
+
{
|
|
5274
|
+
name: "${pm2.appName}",
|
|
5275
|
+
script: "${pm2.script}",
|
|
5276
|
+
cwd: "${paths.current}",
|
|
5277
|
+
args: "${pm2.args}",
|
|
5278
|
+
instances: 1,
|
|
5279
|
+
exec_mode: "fork",
|
|
5280
|
+
autorestart: true,
|
|
5281
|
+
min_uptime: "10s",
|
|
5282
|
+
max_restarts: 10,
|
|
5283
|
+
restart_delay: 2000,
|
|
5284
|
+
max_memory_restart: "1500M",
|
|
5285
|
+
out_file: "${outLog}",
|
|
5286
|
+
error_file: "${errLog}",
|
|
5287
|
+
merge_logs: true,
|
|
5288
|
+
time: true,
|
|
5289
|
+
env: {
|
|
5290
|
+
NODE_ENV: "production",
|
|
5291
|
+
},
|
|
5292
|
+
},
|
|
5293
|
+
],
|
|
5294
|
+
};
|
|
5295
|
+
`;
|
|
5296
|
+
}
|
|
5297
|
+
async function initServiceStructure(service, options = {}) {
|
|
5298
|
+
const { dryRun = false, logger = console } = options;
|
|
5299
|
+
const paths = servicePaths(service);
|
|
5300
|
+
const dirs = [paths.releases, paths.shared, paths.logs];
|
|
5301
|
+
const created = [];
|
|
5302
|
+
const skipped = [];
|
|
5303
|
+
if (!dryRun) {
|
|
5304
|
+
await requireDeployRoot((0, import_node_path8.dirname)(paths.root), paths.root);
|
|
5305
|
+
}
|
|
5306
|
+
for (const dir of dirs) {
|
|
5307
|
+
if (await pathExists4(dir)) {
|
|
5308
|
+
skipped.push(dir);
|
|
5309
|
+
continue;
|
|
5310
|
+
}
|
|
5311
|
+
if (!dryRun) await (0, import_promises11.mkdir)(dir, { recursive: true });
|
|
5312
|
+
created.push(dir);
|
|
5313
|
+
}
|
|
5314
|
+
let ecosystemCreated = false;
|
|
5315
|
+
if (await pathExists4(paths.ecosystem)) {
|
|
5316
|
+
skipped.push(paths.ecosystem);
|
|
5317
|
+
} else {
|
|
5318
|
+
if (!dryRun) {
|
|
5319
|
+
await (0, import_promises11.writeFile)(paths.ecosystem, buildEcosystemConfig(service, paths), { mode: 420 });
|
|
5320
|
+
}
|
|
5321
|
+
ecosystemCreated = true;
|
|
5322
|
+
created.push(paths.ecosystem);
|
|
5323
|
+
}
|
|
5324
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] init-structure service=${service.name} dryRun=${dryRun} created=${created.length} skipped=${skipped.length}
|
|
5325
|
+
`;
|
|
5326
|
+
if (!dryRun) {
|
|
5327
|
+
await (0, import_promises11.mkdir)(paths.logs, { recursive: true });
|
|
5328
|
+
await (0, import_promises11.appendFile)(paths.deployLog, line);
|
|
5329
|
+
}
|
|
5330
|
+
logger.info(`service=${service.name} appsRoot=${paths.root}`);
|
|
5331
|
+
logger.info(`created: ${created.length ? created.join(", ") : "(none)"}`);
|
|
5332
|
+
logger.info(`already present: ${skipped.length ? skipped.join(", ") : "(none)"}`);
|
|
5333
|
+
if (ecosystemCreated) logger.info(`seeded ${paths.ecosystem}`);
|
|
5334
|
+
return { paths, created, skipped, ecosystemCreated };
|
|
5335
|
+
}
|
|
5336
|
+
|
|
5337
|
+
// src/deploy/bootstrap-host.js
|
|
5338
|
+
var import_node_child_process3 = require("child_process");
|
|
5339
|
+
var import_promises12 = require("fs/promises");
|
|
5340
|
+
var import_node_os2 = require("os");
|
|
5341
|
+
var import_node_path9 = require("path");
|
|
5342
|
+
async function pathExists5(path5) {
|
|
5343
|
+
try {
|
|
5344
|
+
await (0, import_promises12.access)(path5);
|
|
5345
|
+
return true;
|
|
5346
|
+
} catch {
|
|
5347
|
+
return false;
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
function expandHome(path5) {
|
|
5351
|
+
return path5.startsWith("~/") ? (0, import_node_path9.join)((0, import_node_os2.homedir)(), path5.slice(2)) : path5;
|
|
5352
|
+
}
|
|
5353
|
+
async function ensurePm2Startup(options = {}) {
|
|
5354
|
+
const { user = "ubuntu", dryRun = false, logger = console } = options;
|
|
5355
|
+
if (dryRun) {
|
|
5356
|
+
logger.info("[dryRun] would configure pm2 startup systemd");
|
|
5357
|
+
return;
|
|
5358
|
+
}
|
|
5359
|
+
try {
|
|
5360
|
+
(0, import_node_child_process3.execSync)("pm2 ping", { stdio: "ignore" });
|
|
5361
|
+
} catch {
|
|
5362
|
+
}
|
|
5363
|
+
try {
|
|
5364
|
+
const out = (0, import_node_child_process3.execSync)(`pm2 startup systemd -u ${user} --hp /home/${user}`, { encoding: "utf8" });
|
|
5365
|
+
const sudoLine = out.split("\n").find((l) => l.trim().startsWith("sudo"));
|
|
5366
|
+
if (sudoLine) {
|
|
5367
|
+
(0, import_node_child_process3.execSync)(sudoLine.trim(), { stdio: "inherit" });
|
|
5368
|
+
logger.info("pm2 startup systemd configured");
|
|
5369
|
+
}
|
|
5370
|
+
} catch (err) {
|
|
5371
|
+
logger.warn(`pm2 startup skipped or already configured: ${err.message}`);
|
|
5372
|
+
}
|
|
5373
|
+
}
|
|
5374
|
+
async function installDeployKey(deployKeyPath, options = {}) {
|
|
5375
|
+
const { dryRun = false, logger = console } = options;
|
|
5376
|
+
const keyPath = expandHome(deployKeyPath);
|
|
5377
|
+
if (!await pathExists5(keyPath)) {
|
|
5378
|
+
logger.warn(`deploy key not found at ${keyPath} \u2014 skipping git ssh setup`);
|
|
5379
|
+
return;
|
|
5380
|
+
}
|
|
5381
|
+
const keyBase = (0, import_node_path9.basename)(keyPath);
|
|
5382
|
+
const sshDir = (0, import_node_path9.join)((0, import_node_os2.homedir)(), ".ssh");
|
|
5383
|
+
const destKey = (0, import_node_path9.join)(sshDir, keyBase);
|
|
5384
|
+
const configPath = (0, import_node_path9.join)(sshDir, "config");
|
|
5385
|
+
const block = `
|
|
5386
|
+
Host github.com
|
|
5387
|
+
HostName github.com
|
|
5388
|
+
User git
|
|
5389
|
+
IdentityFile ${destKey}
|
|
5390
|
+
IdentitiesOnly yes
|
|
5391
|
+
`;
|
|
5392
|
+
if (dryRun) {
|
|
5393
|
+
logger.info(`[dryRun] would install deploy key ${keyPath} \u2192 ${destKey}`);
|
|
5394
|
+
return;
|
|
5395
|
+
}
|
|
5396
|
+
await (0, import_promises12.mkdir)(sshDir, { recursive: true, mode: 448 });
|
|
5397
|
+
await (0, import_promises12.copyFile)(keyPath, destKey);
|
|
5398
|
+
await (0, import_promises12.chmod)(destKey, 384);
|
|
5399
|
+
let config2 = "";
|
|
5400
|
+
if (await pathExists5(configPath)) config2 = await (0, import_promises12.readFile)(configPath, "utf8");
|
|
5401
|
+
if (!config2.includes("Host github.com")) {
|
|
5402
|
+
await (0, import_promises12.writeFile)(configPath, `${config2.trimEnd()}
|
|
5403
|
+
${block}
|
|
5404
|
+
`, { mode: 384 });
|
|
5405
|
+
logger.info("updated ~/.ssh/config for github.com");
|
|
5406
|
+
}
|
|
5407
|
+
logger.info(`deploy key installed at ${destKey}`);
|
|
5408
|
+
}
|
|
5409
|
+
async function installLogrotate(service, options = {}) {
|
|
5410
|
+
const { dryRun = false, logger = console } = options;
|
|
5411
|
+
const conf = `/etc/logrotate.d/${service.name}`;
|
|
5412
|
+
const body = `${service.appsRoot}/logs/*.log {
|
|
5413
|
+
daily
|
|
5414
|
+
rotate 14
|
|
5415
|
+
compress
|
|
5416
|
+
delaycompress
|
|
5417
|
+
missingok
|
|
5418
|
+
notifempty
|
|
5419
|
+
copytruncate
|
|
5420
|
+
}
|
|
5421
|
+
`;
|
|
5422
|
+
if (dryRun) {
|
|
5423
|
+
logger.info(`[dryRun] would write ${conf}`);
|
|
5424
|
+
return;
|
|
5425
|
+
}
|
|
5426
|
+
const tmp = (0, import_node_path9.join)("/tmp", `${service.name}-logrotate.conf`);
|
|
5427
|
+
await (0, import_promises12.writeFile)(tmp, body);
|
|
5428
|
+
await runShell(`sudo cp '${tmp}' '${conf}'`, { logger });
|
|
5429
|
+
logger.info(`logrotate config written: ${conf}`);
|
|
5430
|
+
}
|
|
5431
|
+
async function bootstrapHost(service, options = {}) {
|
|
5432
|
+
const { deployKey = service.deployKey, user = "ubuntu", dryRun = false, logger = console } = options;
|
|
5433
|
+
await ensurePm2Startup({ user, dryRun, logger });
|
|
5434
|
+
if (deployKey) await installDeployKey(deployKey, { dryRun, logger });
|
|
5435
|
+
await installLogrotate(service, { dryRun, logger });
|
|
5436
|
+
logger.info("bootstrap-host complete");
|
|
5437
|
+
}
|
|
5438
|
+
|
|
5439
|
+
// src/deploy/deploy-service.js
|
|
5440
|
+
async function deployService(service, options = {}) {
|
|
5441
|
+
const {
|
|
5442
|
+
dryRun = false,
|
|
5443
|
+
skipPull = false,
|
|
5444
|
+
skipTests = false,
|
|
5445
|
+
skipNginx = false,
|
|
5446
|
+
logger = console
|
|
5447
|
+
} = options;
|
|
5448
|
+
const paths = servicePaths(service);
|
|
5449
|
+
await initServiceStructure(service, { dryRun, logger });
|
|
5450
|
+
if (!skipPull) await pullRepo(service, { dryRun, logger });
|
|
5451
|
+
await syncEnv(service, { dryRun, logger });
|
|
5452
|
+
const { stamp, path: releasePath } = await createRelease(service, { dryRun, logger });
|
|
5453
|
+
await writeReleaseBuildInfo(service, releasePath, { stamp, dryRun, logger });
|
|
5454
|
+
await installDeps(service, releasePath, paths, { dryRun, logger });
|
|
5455
|
+
if (!skipTests) await runReleaseTests(service, releasePath, paths, { dryRun, logger });
|
|
5456
|
+
await activateRelease(releasePath, paths, { dryRun, logger });
|
|
5457
|
+
await pruneReleases(service, paths, { dryRun, logger });
|
|
5458
|
+
await reloadPm2(paths, { dryRun, logger });
|
|
5459
|
+
if (!skipNginx) await enableNginxUpstream(service, { dryRun, logger });
|
|
5460
|
+
const summary = `deploy complete stamp=${stamp} dryRun=${dryRun}`;
|
|
5461
|
+
logger.info(summary);
|
|
5462
|
+
if (!dryRun) await appendDeployLog(paths.deployLog, summary);
|
|
5463
|
+
return { stamp, releasePath };
|
|
5464
|
+
}
|
|
5465
|
+
|
|
5466
|
+
// src/deploy/provision-service.js
|
|
5467
|
+
var import_promises13 = require("fs/promises");
|
|
5468
|
+
async function pathExists6(path5) {
|
|
5469
|
+
try {
|
|
5470
|
+
await (0, import_promises13.access)(path5);
|
|
5471
|
+
return true;
|
|
5472
|
+
} catch {
|
|
5473
|
+
return false;
|
|
5474
|
+
}
|
|
5475
|
+
}
|
|
5476
|
+
async function provisionService(service, options = {}) {
|
|
5477
|
+
const {
|
|
5478
|
+
dryRun = false,
|
|
5479
|
+
deploy = true,
|
|
5480
|
+
skipBootstrap = true,
|
|
5481
|
+
deployKey,
|
|
5482
|
+
logger = console
|
|
5483
|
+
} = options;
|
|
5484
|
+
if (!skipBootstrap) {
|
|
5485
|
+
await bootstrapHost(service, { deployKey, dryRun, logger });
|
|
5486
|
+
}
|
|
5487
|
+
await initServiceStructure(service, { dryRun, logger });
|
|
5488
|
+
const paths = servicePaths(service);
|
|
5489
|
+
if (await pathExists6(paths.repo)) {
|
|
5490
|
+
logger.info(`repo exists at ${paths.repo} \u2014 pulling`);
|
|
5491
|
+
await pullRepo(service, { dryRun, logger });
|
|
5492
|
+
} else {
|
|
5493
|
+
await cloneRepo(service, { dryRun, logger });
|
|
5494
|
+
}
|
|
5495
|
+
await syncEnv(service, { dryRun, logger });
|
|
5496
|
+
if (deploy) {
|
|
5497
|
+
await deployService(service, { dryRun, logger, skipPull: true });
|
|
5498
|
+
} else {
|
|
5499
|
+
logger.info("provision complete (deploy skipped)");
|
|
5500
|
+
}
|
|
5501
|
+
}
|
|
5502
|
+
|
|
5503
|
+
// src/deploy/rollback-service.js
|
|
5504
|
+
var import_promises14 = require("fs/promises");
|
|
5505
|
+
async function rollbackService(service, options = {}) {
|
|
5506
|
+
const { release: targetName, dryRun = false, logger = console } = options;
|
|
5507
|
+
const paths = servicePaths(service);
|
|
5508
|
+
const releases = await listReleases(paths);
|
|
5509
|
+
if (releases.length === 0) throw new Error("No releases to roll back to");
|
|
5510
|
+
let activeName = null;
|
|
5511
|
+
try {
|
|
5512
|
+
const target = await (0, import_promises14.readlink)(paths.current);
|
|
5513
|
+
activeName = target.split("/").pop();
|
|
5514
|
+
} catch {
|
|
5515
|
+
throw new Error("No active release (current symlink missing)");
|
|
5516
|
+
}
|
|
5517
|
+
let rollbackTarget;
|
|
5518
|
+
if (targetName) {
|
|
5519
|
+
rollbackTarget = releases.find((r) => r.name === targetName);
|
|
5520
|
+
if (!rollbackTarget) throw new Error(`Release not found: ${targetName}`);
|
|
5521
|
+
} else {
|
|
5522
|
+
rollbackTarget = releases.find((r) => r.name !== activeName);
|
|
5523
|
+
if (!rollbackTarget) throw new Error("No previous release to roll back to");
|
|
5524
|
+
}
|
|
5525
|
+
if (rollbackTarget.name === activeName) throw new Error(`Already on release ${activeName}`);
|
|
5526
|
+
logger.info(`rollback ${activeName} \u2192 ${rollbackTarget.name}`);
|
|
5527
|
+
const buildInfo = await readReleaseBuildInfo(service, rollbackTarget.path);
|
|
5528
|
+
if (buildInfo?.version) {
|
|
5529
|
+
logger.info(`rollback target: v${buildInfo.version} release=${buildInfo.release ?? rollbackTarget.name}`);
|
|
5530
|
+
}
|
|
5531
|
+
await activateRelease(rollbackTarget.path, paths, { dryRun, logger });
|
|
5532
|
+
await reloadPm2(paths, { dryRun, logger });
|
|
5533
|
+
const summary = `rollback ${activeName} \u2192 ${rollbackTarget.name} dryRun=${dryRun}`;
|
|
5534
|
+
if (!dryRun) await appendDeployLog(paths.deployLog, summary);
|
|
5535
|
+
return { from: activeName, to: rollbackTarget.name, path: rollbackTarget.path };
|
|
5536
|
+
}
|
|
5537
|
+
|
|
5538
|
+
// src/deploy/ssh-remote.js
|
|
5539
|
+
var import_promises15 = require("fs/promises");
|
|
5540
|
+
var import_node_os3 = require("os");
|
|
5541
|
+
var import_node_path10 = require("path");
|
|
5542
|
+
var import_node_child_process4 = require("child_process");
|
|
5543
|
+
var REMOTE_CLI_REL = "node_modules/@nmakarov/cli-toolkit/scripts/deploy/cli.js";
|
|
5544
|
+
async function pathExists7(path5) {
|
|
5545
|
+
try {
|
|
5546
|
+
await (0, import_promises15.access)(path5);
|
|
5547
|
+
return true;
|
|
5548
|
+
} catch {
|
|
5549
|
+
return false;
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5552
|
+
function expandHome2(path5) {
|
|
5553
|
+
return path5.startsWith("~/") ? (0, import_node_path10.join)((0, import_node_os3.homedir)(), path5.slice(2)) : path5;
|
|
5554
|
+
}
|
|
5555
|
+
function resolveLocalEnvPath(envFile) {
|
|
5556
|
+
if (envFile) {
|
|
5557
|
+
const expanded = expandHome2(envFile);
|
|
5558
|
+
return expanded.startsWith("/") ? expanded : (0, import_node_path10.join)(process.cwd(), expanded);
|
|
5559
|
+
}
|
|
5560
|
+
return (0, import_node_path10.join)(process.cwd(), ".env");
|
|
5561
|
+
}
|
|
5562
|
+
function parseGitHost(repoUrl) {
|
|
5563
|
+
const u = String(repoUrl ?? "");
|
|
5564
|
+
let m = u.match(/^[^@]+@([^:]+):/);
|
|
5565
|
+
if (m) return m[1];
|
|
5566
|
+
m = u.match(/^ssh:\/\/[^@]+@([^/:]+)/);
|
|
5567
|
+
if (m) return m[1];
|
|
5568
|
+
return null;
|
|
5569
|
+
}
|
|
5570
|
+
function shellQuote(value) {
|
|
5571
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
5572
|
+
}
|
|
5573
|
+
function sshRun(host, remoteCommand, options = {}) {
|
|
5574
|
+
const { logger } = options;
|
|
5575
|
+
return new Promise((resolve3, reject) => {
|
|
5576
|
+
logger?.info?.(`ssh ${host} ${remoteCommand.slice(0, 120)}${remoteCommand.length > 120 ? "\u2026" : ""}`);
|
|
5577
|
+
const child = (0, import_node_child_process4.spawn)("ssh", [host, remoteCommand], { stdio: "inherit" });
|
|
5578
|
+
child.on("error", reject);
|
|
5579
|
+
child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`ssh ${host} exited with code ${code}`)));
|
|
5580
|
+
});
|
|
5581
|
+
}
|
|
5582
|
+
function scp(localPath, remoteSpec) {
|
|
5583
|
+
return new Promise((resolve3, reject) => {
|
|
5584
|
+
const child = (0, import_node_child_process4.spawn)("scp", [localPath, remoteSpec], { stdio: "inherit" });
|
|
5585
|
+
child.on("error", reject);
|
|
5586
|
+
child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`scp exited with code ${code}`)));
|
|
5587
|
+
});
|
|
5588
|
+
}
|
|
5589
|
+
async function ensureDeployKeyOnRemote(host, deployKeyPath, options = {}) {
|
|
5590
|
+
const { logger = console } = options;
|
|
5591
|
+
if (!deployKeyPath) return false;
|
|
5592
|
+
const localPath = expandHome2(deployKeyPath);
|
|
5593
|
+
if (!await pathExists7(localPath)) return false;
|
|
5594
|
+
const keyBase = (0, import_node_path10.basename)(localPath);
|
|
5595
|
+
logger.info(`copying deploy key ${localPath} \u2192 ${host}:~/.ssh/${keyBase}`);
|
|
5596
|
+
await sshRun(host, "mkdir -p ~/.ssh && chmod 700 ~/.ssh", { logger });
|
|
5597
|
+
await scp(localPath, `${host}:.ssh/${keyBase}`, { logger });
|
|
5598
|
+
await sshRun(host, `chmod 600 ~/.ssh/${keyBase}`, { logger });
|
|
5599
|
+
return true;
|
|
5600
|
+
}
|
|
5601
|
+
async function prepareGitHost(host, options = {}) {
|
|
5602
|
+
const { logger = console, gitHost, keyBasename } = options;
|
|
5603
|
+
if (!gitHost) return;
|
|
5604
|
+
const configBlock = keyBasename ? `if ! grep -q 'Host ${gitHost}' ~/.ssh/config 2>/dev/null; then
|
|
5605
|
+
printf '%s\\n' '' 'Host ${gitHost}' ' HostName ${gitHost}' ' User git' ' IdentityFile ~/.ssh/${keyBasename}' ' IdentitiesOnly yes' >> ~/.ssh/config
|
|
5606
|
+
chmod 600 ~/.ssh/config
|
|
5607
|
+
echo "configured ~/.ssh/config for ${gitHost}"
|
|
5608
|
+
fi` : `:`;
|
|
5609
|
+
const script = `
|
|
5610
|
+
set -euo pipefail
|
|
5611
|
+
mkdir -p ~/.ssh
|
|
5612
|
+
chmod 700 ~/.ssh
|
|
5613
|
+
if ! grep -q '^${gitHost}' ~/.ssh/known_hosts 2>/dev/null; then
|
|
5614
|
+
ssh-keyscan -t ed25519,rsa ${gitHost} >> ~/.ssh/known_hosts 2>/dev/null
|
|
5615
|
+
echo "added ${gitHost} to known_hosts"
|
|
5616
|
+
fi
|
|
5617
|
+
${configBlock}
|
|
5618
|
+
`.trim();
|
|
5619
|
+
await sshRun(host, script, { logger });
|
|
5620
|
+
}
|
|
5621
|
+
async function ensureRepoDependencies(host, service, options = {}) {
|
|
5622
|
+
const { logger = console } = options;
|
|
5623
|
+
const paths = servicePaths(service);
|
|
5624
|
+
const run2 = shellQuote(paths.repoRun);
|
|
5625
|
+
await sshRun(
|
|
5626
|
+
host,
|
|
5627
|
+
`cd ${run2} && if [ ! -d node_modules/@nmakarov/cli-toolkit ] || [ package-lock.json -nt node_modules/.package-lock.json ]; then npm ci; fi`,
|
|
5628
|
+
{ logger }
|
|
5629
|
+
);
|
|
5630
|
+
}
|
|
5631
|
+
async function ensureEnvOnRemote(host, service, options = {}) {
|
|
5632
|
+
const { logger = console, envFile } = options;
|
|
5633
|
+
const paths = servicePaths(service);
|
|
5634
|
+
const localPath = resolveLocalEnvPath(envFile);
|
|
5635
|
+
if (!await pathExists7(localPath)) return false;
|
|
5636
|
+
logger.info(`copying .env ${localPath} \u2192 ${host}:${paths.repoEnv}`);
|
|
5637
|
+
await sshRun(host, `mkdir -p ${shellQuote((0, import_node_path10.dirname)(paths.repoEnv))}`, { logger });
|
|
5638
|
+
const scrubbed = scrubEnvContent(await (0, import_promises15.readFile)(localPath, "utf8"), service.envScrubPatterns ?? []);
|
|
5639
|
+
const tmp = (0, import_node_path10.join)((0, import_node_os3.tmpdir)(), `deploy-env-${Date.now()}`);
|
|
5640
|
+
await (0, import_promises15.writeFile)(tmp, scrubbed, { mode: 384 });
|
|
5641
|
+
await scp(tmp, `${host}:${paths.repoEnv}`, { logger });
|
|
5642
|
+
await sshRun(host, `chmod 600 ${shellQuote(paths.repoEnv)}`, { logger });
|
|
5643
|
+
return true;
|
|
5644
|
+
}
|
|
5645
|
+
async function ensureRemoteRepo(host, service, options = {}) {
|
|
5646
|
+
const { logger = console, deployKey = service.deployKey, envFile } = options;
|
|
5647
|
+
const paths = servicePaths(service);
|
|
5648
|
+
const repo = shellQuote(paths.repo);
|
|
5649
|
+
const repoUrl = shellQuote(service.repoUrl);
|
|
5650
|
+
const root = shellQuote(paths.root);
|
|
5651
|
+
const localKeyBase = deployKey ? (0, import_node_path10.basename)(expandHome2(deployKey)) : null;
|
|
5652
|
+
await ensureDeployKeyOnRemote(host, deployKey, { logger });
|
|
5653
|
+
await prepareGitHost(host, { logger, gitHost: parseGitHost(service.repoUrl), keyBasename: localKeyBase });
|
|
5654
|
+
await sshRun(
|
|
5655
|
+
host,
|
|
5656
|
+
`mkdir -p ${root} && if [ -d ${repo}/.git ]; then git -C ${repo} pull --ff-only; else git clone ${repoUrl} ${repo}; fi`,
|
|
5657
|
+
{ logger }
|
|
5658
|
+
);
|
|
5659
|
+
await ensureRepoDependencies(host, service, { logger });
|
|
5660
|
+
await ensureEnvOnRemote(host, service, { logger, envFile });
|
|
5661
|
+
}
|
|
5662
|
+
async function runRemoteCli(host, service, command, args = [], options = {}) {
|
|
5663
|
+
const { logger = console, manifests, skipPull = false, deployKey = service.deployKey, envFile } = options;
|
|
5664
|
+
const paths = servicePaths(service);
|
|
5665
|
+
const run2 = shellQuote(paths.repoRun);
|
|
5666
|
+
const cli = shellQuote(REMOTE_CLI_REL);
|
|
5667
|
+
if (!skipPull) {
|
|
5668
|
+
await ensureRemoteRepo(host, service, { logger, deployKey, envFile });
|
|
5669
|
+
} else {
|
|
5670
|
+
await ensureRepoDependencies(host, service, { logger });
|
|
5671
|
+
await ensureEnvOnRemote(host, service, { logger, envFile });
|
|
5672
|
+
}
|
|
5673
|
+
const passthrough = [
|
|
5674
|
+
`--service=${service.name}`,
|
|
5675
|
+
...manifests ? [`--manifests=${manifests}`] : [],
|
|
5676
|
+
...args
|
|
5677
|
+
].map(shellQuote).join(" ");
|
|
5678
|
+
await sshRun(host, `cd ${run2} && node ${cli} ${shellQuote(command)} ${passthrough}`.trim(), { logger });
|
|
5679
|
+
}
|
|
5680
|
+
async function runRemoteStatus(host, service, options = {}) {
|
|
5681
|
+
const { logger = console } = options;
|
|
5682
|
+
const paths = servicePaths(service);
|
|
5683
|
+
const port = service.pm2.port;
|
|
5684
|
+
const app = service.pm2.appName;
|
|
5685
|
+
const script = `
|
|
5686
|
+
echo "=== apps root ==="
|
|
5687
|
+
ls -la ${shellQuote(paths.root)} 2>/dev/null || echo "(missing)"
|
|
5688
|
+
echo ""
|
|
5689
|
+
echo "=== current ==="
|
|
5690
|
+
readlink ${shellQuote(paths.current)} 2>/dev/null || echo "(not set)"
|
|
5691
|
+
echo ""
|
|
5692
|
+
echo "=== releases ==="
|
|
5693
|
+
ls -1 ${shellQuote(paths.releases)} 2>/dev/null || echo "(none)"
|
|
5694
|
+
echo ""
|
|
5695
|
+
echo "=== pm2 ==="
|
|
5696
|
+
pm2 describe ${app} 2>/dev/null | head -20 || pm2 status ${app} 2>/dev/null || echo "(not running)"
|
|
5697
|
+
${port ? `echo ""
|
|
5698
|
+
echo "=== app health (localhost) ==="
|
|
5699
|
+
curl -sf http://127.0.0.1:${port}/healthz 2>/dev/null || echo "(no /healthz on :${port})"` : ""}
|
|
5700
|
+
`.trim();
|
|
5701
|
+
await sshRun(host, script, { logger });
|
|
5702
|
+
}
|
|
5703
|
+
|
|
5704
|
+
// src/deploy/manifests.js
|
|
5705
|
+
var import_node_path11 = require("path");
|
|
5706
|
+
var import_node_url = require("url");
|
|
5707
|
+
async function loadServices({ manifests = "deploy/services.js", cwd = process.cwd() } = {}) {
|
|
5708
|
+
const abs = (0, import_node_path11.isAbsolute)(manifests) ? manifests : (0, import_node_path11.resolve)(cwd, manifests);
|
|
5709
|
+
let mod;
|
|
5710
|
+
try {
|
|
5711
|
+
mod = await import((0, import_node_url.pathToFileURL)(abs).href);
|
|
5712
|
+
} catch (err) {
|
|
5713
|
+
throw new Error(`Could not load deploy manifests from ${abs}: ${err.message}`);
|
|
5714
|
+
}
|
|
5715
|
+
const raw = mod.services ?? mod.default;
|
|
5716
|
+
if (!raw) {
|
|
5717
|
+
throw new Error(`Manifests module ${abs} must export \`services\` (or default): a map or array of service manifests`);
|
|
5718
|
+
}
|
|
5719
|
+
const list = Array.isArray(raw) ? raw : Object.values(raw);
|
|
5720
|
+
const out = {};
|
|
5721
|
+
for (const entry of list) {
|
|
5722
|
+
const svc = defineService(entry);
|
|
5723
|
+
out[svc.name] = svc;
|
|
5724
|
+
}
|
|
5725
|
+
return out;
|
|
5726
|
+
}
|
|
5727
|
+
function resolveServiceFrom(serviceMap, name, { appsRoot } = {}) {
|
|
5728
|
+
const svc = serviceMap[name];
|
|
5729
|
+
if (!svc) {
|
|
5730
|
+
throw new Error(`Unknown service "${name}". Known: ${Object.keys(serviceMap).join(", ") || "(none)"}`);
|
|
5731
|
+
}
|
|
5732
|
+
return appsRoot ? { ...svc, appsRoot } : svc;
|
|
5733
|
+
}
|
|
5734
|
+
|
|
4443
5735
|
// src/tasks/index.js
|
|
4444
|
-
var
|
|
5736
|
+
var import_node_os6 = __toESM(require("os"), 1);
|
|
4445
5737
|
|
|
4446
5738
|
// src/utils/core-utils.js
|
|
4447
5739
|
function sleepMs(ms) {
|
|
4448
|
-
return new Promise((
|
|
5740
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4449
5741
|
}
|
|
4450
5742
|
function toJsonColumn(value) {
|
|
4451
5743
|
if (value === void 0 || value === null) return null;
|
|
@@ -4453,10 +5745,10 @@ function toJsonColumn(value) {
|
|
|
4453
5745
|
}
|
|
4454
5746
|
|
|
4455
5747
|
// src/tasks/servicesRegistry.js
|
|
4456
|
-
var
|
|
5748
|
+
var import_node_os4 = __toESM(require("os"), 1);
|
|
4457
5749
|
|
|
4458
5750
|
// src/tasks/taskUtils.js
|
|
4459
|
-
var
|
|
5751
|
+
var import_node_crypto2 = require("crypto");
|
|
4460
5752
|
|
|
4461
5753
|
// src/tasks/time-matcher.js
|
|
4462
5754
|
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
@@ -4553,7 +5845,7 @@ function defineTasksTable(t, db, tableNameForIndex) {
|
|
|
4553
5845
|
t.timestamp("past_due").defaultTo(null);
|
|
4554
5846
|
t.text("name").notNullable();
|
|
4555
5847
|
t.text("opid");
|
|
4556
|
-
t.
|
|
5848
|
+
t.jsonb("params");
|
|
4557
5849
|
t.text("service_group");
|
|
4558
5850
|
t.integer("instance_number");
|
|
4559
5851
|
t.text("service_name");
|
|
@@ -4562,7 +5854,7 @@ function defineTasksTable(t, db, tableNameForIndex) {
|
|
|
4562
5854
|
t.timestamp("status_changed_at").defaultTo(null);
|
|
4563
5855
|
t.text("progress");
|
|
4564
5856
|
t.boolean("success");
|
|
4565
|
-
t.
|
|
5857
|
+
t.jsonb("results");
|
|
4566
5858
|
t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
|
|
4567
5859
|
t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
|
|
4568
5860
|
}
|
|
@@ -4577,11 +5869,29 @@ function taskHistoryInsertFromQueueRow(row, overrides) {
|
|
|
4577
5869
|
async function ensureTaskTables(context, options = {}) {
|
|
4578
5870
|
const queueName = options.queueName ?? "tasks";
|
|
4579
5871
|
const recreate = options.recreate ?? false;
|
|
5872
|
+
const dryRun = options.dryRun ?? false;
|
|
4580
5873
|
const db = getDb(context);
|
|
5874
|
+
const log = context.logger ?? console;
|
|
4581
5875
|
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
4582
5876
|
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
4583
5877
|
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
4584
5878
|
const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
|
|
5879
|
+
if (dryRun) {
|
|
5880
|
+
const plan = [];
|
|
5881
|
+
if (recreate) {
|
|
5882
|
+
plan.push(`DROP TABLE IF EXISTS ${historyTable}, ${tasksTable}, ${registryTable}`);
|
|
5883
|
+
}
|
|
5884
|
+
if (needsTasks) plan.push(`CREATE TABLE ${tasksTable} (tasks queue)`);
|
|
5885
|
+
if (needsHistory) plan.push(`CREATE TABLE ${historyTable} (history mirror)`);
|
|
5886
|
+
if (needsRegistry) plan.push(`CREATE TABLE ${registryTable} (services registry)`);
|
|
5887
|
+
if (plan.length === 0) {
|
|
5888
|
+
log.info?.(`[tasks-schema] dryRun \u2014 queue "${queueName}" already up to date; no DDL`);
|
|
5889
|
+
} else {
|
|
5890
|
+
log.info?.(`[tasks-schema] dryRun \u2014 would run ${plan.length} statement(s) for queue "${queueName}":`);
|
|
5891
|
+
for (const s of plan) log.info?.(` - ${s}`);
|
|
5892
|
+
}
|
|
5893
|
+
return;
|
|
5894
|
+
}
|
|
4585
5895
|
if (recreate) {
|
|
4586
5896
|
await db.schema.dropTableIfExists(historyTable);
|
|
4587
5897
|
await db.schema.dropTableIfExists(tasksTable);
|
|
@@ -4620,7 +5930,7 @@ async function enqueueTask(context, options) {
|
|
|
4620
5930
|
const db = getDb(context);
|
|
4621
5931
|
const queueName = options.queueName ?? "tasks";
|
|
4622
5932
|
const { tasksTable } = queueToTableNames(queueName);
|
|
4623
|
-
const id = (0,
|
|
5933
|
+
const id = (0, import_node_crypto2.randomUUID)();
|
|
4624
5934
|
const name = options.name ?? options.task;
|
|
4625
5935
|
if (!name) {
|
|
4626
5936
|
throw new Error("enqueueTask: name (or task) is required");
|
|
@@ -4754,7 +6064,7 @@ async function registerInServicesRegistry(context, options) {
|
|
|
4754
6064
|
if (!serviceGroup) {
|
|
4755
6065
|
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
4756
6066
|
}
|
|
4757
|
-
const serverName =
|
|
6067
|
+
const serverName = import_node_os4.default.hostname();
|
|
4758
6068
|
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
4759
6069
|
const meta = buildMetadata(options);
|
|
4760
6070
|
const groupBase = sanitizeNamePart(serviceGroup);
|
|
@@ -4867,7 +6177,7 @@ async function registerInServicesRegistry(context, options) {
|
|
|
4867
6177
|
}
|
|
4868
6178
|
async function touchServicesRegistry(context, registration) {
|
|
4869
6179
|
const db = getDb2(context);
|
|
4870
|
-
const serverName =
|
|
6180
|
+
const serverName = import_node_os4.default.hostname();
|
|
4871
6181
|
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
4872
6182
|
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
4873
6183
|
last_seen_at: db.fn.now(),
|
|
@@ -4904,7 +6214,7 @@ async function listServicesRegistry(context, options = { queueName: "tasks" }) {
|
|
|
4904
6214
|
}
|
|
4905
6215
|
|
|
4906
6216
|
// src/tasks/taskLogs.js
|
|
4907
|
-
var
|
|
6217
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
4908
6218
|
function getLogsState(context) {
|
|
4909
6219
|
const holder = context;
|
|
4910
6220
|
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
@@ -5012,7 +6322,7 @@ function resolveIpcFileLogsDir(context, target) {
|
|
|
5012
6322
|
const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
|
|
5013
6323
|
const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
|
|
5014
6324
|
const segments = target.tableName.split("/").filter(Boolean);
|
|
5015
|
-
return
|
|
6325
|
+
return import_node_path12.default.resolve(basePath, namespace, ...segments);
|
|
5016
6326
|
}
|
|
5017
6327
|
function getLogsStateForTarget(context, target) {
|
|
5018
6328
|
const holder = context;
|
|
@@ -5486,10 +6796,10 @@ var TaskSampleProcess = class extends AbstractTask {
|
|
|
5486
6796
|
};
|
|
5487
6797
|
|
|
5488
6798
|
// src/tasks/coreTasks/TaskShellCommand.js
|
|
5489
|
-
var
|
|
6799
|
+
var import_node_child_process5 = require("child_process");
|
|
5490
6800
|
function runShellCommand(command, cwd) {
|
|
5491
|
-
return new Promise((
|
|
5492
|
-
const child = (0,
|
|
6801
|
+
return new Promise((resolve3, reject) => {
|
|
6802
|
+
const child = (0, import_node_child_process5.spawn)(command, {
|
|
5493
6803
|
shell: true,
|
|
5494
6804
|
cwd: cwd || process.cwd(),
|
|
5495
6805
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -5506,7 +6816,7 @@ function runShellCommand(command, cwd) {
|
|
|
5506
6816
|
reject(error);
|
|
5507
6817
|
});
|
|
5508
6818
|
child.on("close", (exitCode, signal) => {
|
|
5509
|
-
|
|
6819
|
+
resolve3({
|
|
5510
6820
|
exitCode,
|
|
5511
6821
|
output: output.trim(),
|
|
5512
6822
|
stderr: stderr.trim(),
|
|
@@ -5585,8 +6895,8 @@ var TaskShellCommand = class extends AbstractTask {
|
|
|
5585
6895
|
};
|
|
5586
6896
|
|
|
5587
6897
|
// src/tasks/coreTasks/TaskSystemInfo.js
|
|
5588
|
-
var
|
|
5589
|
-
var
|
|
6898
|
+
var import_node_os5 = __toESM(require("os"), 1);
|
|
6899
|
+
var import_promises16 = __toESM(require("fs/promises"), 1);
|
|
5590
6900
|
function toGb(valueBytes) {
|
|
5591
6901
|
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
5592
6902
|
}
|
|
@@ -5594,7 +6904,7 @@ function toMb(valueBytes) {
|
|
|
5594
6904
|
return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
|
|
5595
6905
|
}
|
|
5596
6906
|
async function getDiskStats() {
|
|
5597
|
-
const stats = await
|
|
6907
|
+
const stats = await import_promises16.default.statfs("/");
|
|
5598
6908
|
const total = Number(stats.bsize) * Number(stats.blocks);
|
|
5599
6909
|
const free = Number(stats.bsize) * Number(stats.bavail);
|
|
5600
6910
|
const used = total - free;
|
|
@@ -5616,10 +6926,10 @@ var TaskSystemInfo = class extends AbstractTask {
|
|
|
5616
6926
|
*/
|
|
5617
6927
|
async run() {
|
|
5618
6928
|
try {
|
|
5619
|
-
const totalMemory =
|
|
5620
|
-
const freeMemory =
|
|
6929
|
+
const totalMemory = import_node_os5.default.totalmem();
|
|
6930
|
+
const freeMemory = import_node_os5.default.freemem();
|
|
5621
6931
|
const usedMemory = totalMemory - freeMemory;
|
|
5622
|
-
const cpus =
|
|
6932
|
+
const cpus = import_node_os5.default.cpus();
|
|
5623
6933
|
const cpuUtilization = cpus.map((cpu) => {
|
|
5624
6934
|
const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
|
|
5625
6935
|
const usage = (total - cpu.times.idle) / total * 100;
|
|
@@ -5645,10 +6955,10 @@ var TaskSystemInfo = class extends AbstractTask {
|
|
|
5645
6955
|
utilization: cpuUtilization
|
|
5646
6956
|
},
|
|
5647
6957
|
runtime: {
|
|
5648
|
-
platform:
|
|
5649
|
-
arch:
|
|
5650
|
-
uptimeSec:
|
|
5651
|
-
hostname:
|
|
6958
|
+
platform: import_node_os5.default.platform(),
|
|
6959
|
+
arch: import_node_os5.default.arch(),
|
|
6960
|
+
uptimeSec: import_node_os5.default.uptime(),
|
|
6961
|
+
hostname: import_node_os5.default.hostname()
|
|
5652
6962
|
}
|
|
5653
6963
|
};
|
|
5654
6964
|
this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
|
|
@@ -5978,7 +7288,7 @@ function mergeAllowedTasksWithServiceTasks(names) {
|
|
|
5978
7288
|
}
|
|
5979
7289
|
|
|
5980
7290
|
// src/tasks/taskScriptRunner.js
|
|
5981
|
-
var
|
|
7291
|
+
var import_node_child_process6 = require("child_process");
|
|
5982
7292
|
var MAX_PROGRESS_TEXT_LEN = 4e3;
|
|
5983
7293
|
function toCliArgs(args = []) {
|
|
5984
7294
|
return args.filter((a) => typeof a === "string" && a.length > 0);
|
|
@@ -6024,8 +7334,7 @@ function forwardChildLogToParent(context, prefix, message) {
|
|
|
6024
7334
|
}
|
|
6025
7335
|
function buildNodeArgs(scriptPath, cliArgs) {
|
|
6026
7336
|
const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
|
|
6027
|
-
|
|
6028
|
-
return hasTsRuntimeInParent ? [...inheritedExecArgs, scriptPath, ...cliArgs] : ["--import", "tsx", scriptPath, ...cliArgs];
|
|
7337
|
+
return [...inheritedExecArgs, scriptPath, ...cliArgs];
|
|
6029
7338
|
}
|
|
6030
7339
|
function resolveTasksTableName(context) {
|
|
6031
7340
|
return context.tasksQueueName || context.params?.get?.("table") || "tasks";
|
|
@@ -6057,7 +7366,7 @@ function createSerializedQueue() {
|
|
|
6057
7366
|
async function runNodeTaskScript(context, options) {
|
|
6058
7367
|
const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
|
|
6059
7368
|
const nodeArgs = buildNodeArgs(options.scriptPath, cliArgs);
|
|
6060
|
-
const child = (0,
|
|
7369
|
+
const child = (0, import_node_child_process6.spawn)(process.execPath, nodeArgs, {
|
|
6061
7370
|
cwd: options.cwd || process.cwd(),
|
|
6062
7371
|
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
6063
7372
|
env: {
|
|
@@ -6144,13 +7453,13 @@ async function runNodeTaskScript(context, options) {
|
|
|
6144
7453
|
}
|
|
6145
7454
|
forwardChildLogToParent(context, prefix, message);
|
|
6146
7455
|
});
|
|
6147
|
-
return await new Promise((
|
|
7456
|
+
return await new Promise((resolve3, reject) => {
|
|
6148
7457
|
child.on("error", (error) => reject(error));
|
|
6149
7458
|
child.on("close", (exitCode, signal) => {
|
|
6150
7459
|
void (async () => {
|
|
6151
7460
|
await flushTaskIpcLogs(context);
|
|
6152
7461
|
await progressQueue.drain();
|
|
6153
|
-
|
|
7462
|
+
resolve3({
|
|
6154
7463
|
exitCode,
|
|
6155
7464
|
signal,
|
|
6156
7465
|
stdout: state.stdout.trim(),
|
|
@@ -6265,9 +7574,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
6265
7574
|
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
6266
7575
|
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
6267
7576
|
const fallbackRecoverCommand = [
|
|
6268
|
-
"
|
|
6269
|
-
"
|
|
6270
|
-
"examples/tasks/recover-task.ts",
|
|
7577
|
+
"node",
|
|
7578
|
+
"examples/tasks/recover-task.js",
|
|
6271
7579
|
`--dbName='${dbName.replace(/'/g, `'\\''`)}'`,
|
|
6272
7580
|
`--table='${tableName.replace(/'/g, `'\\''`)}'`,
|
|
6273
7581
|
`--id='${String(row.id).replace(/'/g, `'\\''`)}'`
|
|
@@ -6333,6 +7641,8 @@ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry
|
|
|
6333
7641
|
const db = getDb3(context);
|
|
6334
7642
|
let query = db(tasksTable).where({ status: "idle" }).where(function() {
|
|
6335
7643
|
this.whereNull("service_group").orWhere({ service_group: serviceGroup });
|
|
7644
|
+
}).where(function() {
|
|
7645
|
+
this.whereNull("next_run_at").orWhere("next_run_at", "<=", db.fn.now());
|
|
6336
7646
|
}).orderByRaw("CASE WHEN past_due IS NULL THEN 1 ELSE 0 END ASC").orderBy([{ column: "priority", order: "asc" }]).orderByRaw("CASE WHEN completed_at IS NULL THEN 0 ELSE 1 END ASC").orderBy([{ column: "completed_at", order: "asc" }, { column: "created_at", order: "asc" }]).limit(scanLimit);
|
|
6337
7647
|
if (taskNames && taskNames.length > 0) {
|
|
6338
7648
|
query = query.whereIn("name", taskNames);
|
|
@@ -6427,7 +7737,7 @@ async function runTasksLoop(context, options) {
|
|
|
6427
7737
|
});
|
|
6428
7738
|
runnerIdentity = {
|
|
6429
7739
|
service_name: registryReg.serviceName,
|
|
6430
|
-
server_name:
|
|
7740
|
+
server_name: import_node_os6.default.hostname(),
|
|
6431
7741
|
instance_number: registryReg.instanceNumber
|
|
6432
7742
|
};
|
|
6433
7743
|
registryInterval = setInterval(() => {
|
|
@@ -6716,6 +8026,7 @@ var TasksManager = class _TasksManager {
|
|
|
6716
8026
|
0 && (module.exports = {
|
|
6717
8027
|
AbstractTask,
|
|
6718
8028
|
Args,
|
|
8029
|
+
Aws,
|
|
6719
8030
|
Box,
|
|
6720
8031
|
Db,
|
|
6721
8032
|
Divider,
|
|
@@ -6729,6 +8040,7 @@ var TasksManager = class _TasksManager {
|
|
|
6729
8040
|
MultiColumnListComponent,
|
|
6730
8041
|
MultiColumnListWithPreviewComponent,
|
|
6731
8042
|
Params,
|
|
8043
|
+
REMOTE_CLI_REL,
|
|
6732
8044
|
React,
|
|
6733
8045
|
S3,
|
|
6734
8046
|
SERVICE_TASK_NAMES,
|
|
@@ -6749,51 +8061,94 @@ var TasksManager = class _TasksManager {
|
|
|
6749
8061
|
TasksRegistry,
|
|
6750
8062
|
Text,
|
|
6751
8063
|
TextBlock,
|
|
8064
|
+
activateRelease,
|
|
8065
|
+
appendDeployLog,
|
|
6752
8066
|
appendTaskIpcLog,
|
|
8067
|
+
bootstrapHost,
|
|
6753
8068
|
buildBreadcrumb,
|
|
6754
8069
|
buildDetailBreadcrumb,
|
|
6755
8070
|
buildFooter,
|
|
8071
|
+
bumpPatchVersion,
|
|
8072
|
+
cloneRepo,
|
|
6756
8073
|
convertPattern,
|
|
8074
|
+
createRelease,
|
|
6757
8075
|
defaultFileSynopsisFunction,
|
|
6758
8076
|
defaultTasksRegistry,
|
|
6759
8077
|
defaultVersionSynopsisFunction,
|
|
8078
|
+
defineService,
|
|
8079
|
+
deployService,
|
|
8080
|
+
deriveRepoDirName,
|
|
8081
|
+
enableNginxUpstream,
|
|
6760
8082
|
enqueueStopTask,
|
|
6761
8083
|
enqueueTask,
|
|
8084
|
+
ensureDeployKeyOnRemote,
|
|
8085
|
+
ensureEnvOnRemote,
|
|
8086
|
+
ensureRemoteRepo,
|
|
8087
|
+
ensureRepoDependencies,
|
|
6762
8088
|
ensureTaskTables,
|
|
6763
8089
|
flushTaskIpcLogs,
|
|
6764
8090
|
getArgsInstance,
|
|
6765
8091
|
h,
|
|
8092
|
+
initServiceStructure,
|
|
8093
|
+
installDeps,
|
|
6766
8094
|
ipcFileLogsTableNameForSourceResource,
|
|
6767
8095
|
joiEdateType,
|
|
6768
8096
|
joiStringArrayType,
|
|
6769
8097
|
listAliveRunnerHeartbeats,
|
|
8098
|
+
listReleases,
|
|
6770
8099
|
listServicesRegistry,
|
|
6771
8100
|
listSources,
|
|
6772
8101
|
listTables,
|
|
6773
8102
|
load,
|
|
8103
|
+
loadServices,
|
|
6774
8104
|
matchesParsedPattern,
|
|
6775
8105
|
memo,
|
|
6776
8106
|
mergeAllowedTasksWithServiceTasks,
|
|
6777
8107
|
nextTimeMatch,
|
|
6778
8108
|
normalizeAllowedTasks,
|
|
8109
|
+
npmEnv,
|
|
8110
|
+
npmInstallEnv,
|
|
6779
8111
|
organizeFooterMessages,
|
|
8112
|
+
parseGitHost,
|
|
8113
|
+
prepareGitHost,
|
|
8114
|
+
provisionService,
|
|
8115
|
+
pruneReleases,
|
|
8116
|
+
pullRepo,
|
|
6780
8117
|
queueToTableNames,
|
|
8118
|
+
readCurrentRelease,
|
|
8119
|
+
readReleaseBuildInfo,
|
|
6781
8120
|
readTaskIpcLogsSnapshot,
|
|
6782
8121
|
registerInServicesRegistry,
|
|
6783
8122
|
registerRunnerHeartbeat,
|
|
8123
|
+
releaseDir,
|
|
8124
|
+
releaseStamp,
|
|
8125
|
+
reloadPm2,
|
|
6784
8126
|
resolveAsterisks,
|
|
6785
8127
|
resolveIpcFileLogsDir,
|
|
8128
|
+
resolveNextVersion,
|
|
6786
8129
|
resolveRanges,
|
|
8130
|
+
resolveServiceFrom,
|
|
6787
8131
|
resolveSteps,
|
|
8132
|
+
rollbackService,
|
|
8133
|
+
run,
|
|
6788
8134
|
runNodeTaskScript,
|
|
8135
|
+
runReleaseTests,
|
|
8136
|
+
runRemoteCli,
|
|
8137
|
+
runRemoteStatus,
|
|
8138
|
+
runShell,
|
|
6789
8139
|
runTasksLoop,
|
|
8140
|
+
scrubEnvContent,
|
|
8141
|
+
servicePaths,
|
|
6790
8142
|
setupContext,
|
|
8143
|
+
shellQuote,
|
|
6791
8144
|
showListScreen,
|
|
6792
8145
|
showMenuScreen,
|
|
6793
8146
|
showMultiColumnListScreen,
|
|
6794
8147
|
showMultiColumnListWithPreviewScreen,
|
|
6795
8148
|
showScreen,
|
|
6796
8149
|
showWordGridScreen,
|
|
8150
|
+
sshRun,
|
|
8151
|
+
syncEnv,
|
|
6797
8152
|
taskHistoryInsertFromQueueRow,
|
|
6798
8153
|
timeMatcher,
|
|
6799
8154
|
touchRunnerHeartbeat,
|
|
@@ -6809,6 +8164,7 @@ var TasksManager = class _TasksManager {
|
|
|
6809
8164
|
useMemo,
|
|
6810
8165
|
useRef,
|
|
6811
8166
|
useState,
|
|
6812
|
-
waitForTaskResult
|
|
8167
|
+
waitForTaskResult,
|
|
8168
|
+
writeReleaseBuildInfo
|
|
6813
8169
|
});
|
|
6814
8170
|
//# sourceMappingURL=index.cjs.map
|