@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.js
CHANGED
|
@@ -542,7 +542,7 @@ async function showScreen(config2) {
|
|
|
542
542
|
onRender,
|
|
543
543
|
parentData = {}
|
|
544
544
|
} = config2;
|
|
545
|
-
return new Promise((
|
|
545
|
+
return new Promise((resolve3) => {
|
|
546
546
|
let instance2;
|
|
547
547
|
const keyBindings = [];
|
|
548
548
|
const actions = {};
|
|
@@ -717,7 +717,7 @@ async function showScreen(config2) {
|
|
|
717
717
|
};
|
|
718
718
|
const cleanup = (result) => {
|
|
719
719
|
if (instance2) instance2.unmount();
|
|
720
|
-
setTimeout(() =>
|
|
720
|
+
setTimeout(() => resolve3(result), 50);
|
|
721
721
|
};
|
|
722
722
|
instance2 = render(h3(Screen));
|
|
723
723
|
});
|
|
@@ -1692,7 +1692,8 @@ var Params = class _Params {
|
|
|
1692
1692
|
/**
|
|
1693
1693
|
* Resolved early in constructor so cleanup does not read params lazily.
|
|
1694
1694
|
* One of: false (off) | "end" (print at exit) | "top" (print after init,
|
|
1695
|
-
* via context.showUsedParamsIfNeeded())
|
|
1695
|
+
* via context.showUsedParamsIfNeeded()) | "stop" (print after init, then
|
|
1696
|
+
* exit the process — also via context.showUsedParamsIfNeeded()).
|
|
1696
1697
|
*/
|
|
1697
1698
|
_showUsedParamsMode = false;
|
|
1698
1699
|
/** Guard so the dump prints at most once (top OR end, never both). */
|
|
@@ -1716,8 +1717,9 @@ var Params = class _Params {
|
|
|
1716
1717
|
* (absent) / --no-showUsedParams / =false -> false (off)
|
|
1717
1718
|
* --showUsedParams / =true -> "end" (print at exit)
|
|
1718
1719
|
* --showUsedParams=top -> "top" (print after init)
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1720
|
+
* --showUsedParams=stop -> "stop" (print after init, then exit)
|
|
1721
|
+
* Read raw (uncoerced) from args so the string "top"/"stop" isn't forced to
|
|
1722
|
+
* a boolean, then track it under the "script" module for the dump itself.
|
|
1721
1723
|
*/
|
|
1722
1724
|
_resolveShowUsedParams() {
|
|
1723
1725
|
const raw = this.args.get("showUsedParams");
|
|
@@ -1727,6 +1729,8 @@ var Params = class _Params {
|
|
|
1727
1729
|
mode = false;
|
|
1728
1730
|
} else if (typeof raw === "string" && raw.trim().toLowerCase() === "top") {
|
|
1729
1731
|
mode = "top";
|
|
1732
|
+
} else if (typeof raw === "string" && raw.trim().toLowerCase() === "stop") {
|
|
1733
|
+
mode = "stop";
|
|
1730
1734
|
} else {
|
|
1731
1735
|
const s = typeof raw === "string" ? raw.trim().toLowerCase() : raw;
|
|
1732
1736
|
const falsey = s === false || s === "false" || s === "0" || s === "no" || s === "off";
|
|
@@ -1740,7 +1744,7 @@ var Params = class _Params {
|
|
|
1740
1744
|
getShowUsedParams() {
|
|
1741
1745
|
return this._showUsedParamsMode !== false;
|
|
1742
1746
|
}
|
|
1743
|
-
/** Resolved mode: false | "end" | "top". */
|
|
1747
|
+
/** Resolved mode: false | "end" | "top" | "stop". */
|
|
1744
1748
|
getShowUsedParamsMode() {
|
|
1745
1749
|
return this._showUsedParamsMode;
|
|
1746
1750
|
}
|
|
@@ -2434,8 +2438,8 @@ var FileDatabase = class _FileDatabase {
|
|
|
2434
2438
|
const items = await fs3.promises.readdir(destPath);
|
|
2435
2439
|
const versions = items.filter((item) => {
|
|
2436
2440
|
const itemPath = path3.join(destPath, item);
|
|
2437
|
-
const
|
|
2438
|
-
return
|
|
2441
|
+
const stat2 = fs3.statSync(itemPath);
|
|
2442
|
+
return stat2.isDirectory() && isTimestampFolder(item);
|
|
2439
2443
|
});
|
|
2440
2444
|
return versions.sort();
|
|
2441
2445
|
} catch (error) {
|
|
@@ -2500,8 +2504,8 @@ var FileDatabase = class _FileDatabase {
|
|
|
2500
2504
|
}
|
|
2501
2505
|
const versionFolders = items.filter((item) => {
|
|
2502
2506
|
const itemPath = path3.join(tablePath, item);
|
|
2503
|
-
const
|
|
2504
|
-
return
|
|
2507
|
+
const stat2 = fs3.statSync(itemPath);
|
|
2508
|
+
return stat2.isDirectory() && isTimestampFolder(item);
|
|
2505
2509
|
});
|
|
2506
2510
|
if (versionFolders.length > 0) {
|
|
2507
2511
|
const latestVersion = versionFolders.sort().pop();
|
|
@@ -3786,6 +3790,7 @@ var S3 = class _S3 {
|
|
|
3786
3790
|
secretAccessKey: config2.secretAccessKey
|
|
3787
3791
|
};
|
|
3788
3792
|
}
|
|
3793
|
+
process.env.AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED ??= "true";
|
|
3789
3794
|
this.client = new S3Client(clientConfig);
|
|
3790
3795
|
}
|
|
3791
3796
|
// ── info ────────────────────────────────────────────────────────────────
|
|
@@ -3967,6 +3972,204 @@ var S3 = class _S3 {
|
|
|
3967
3972
|
}
|
|
3968
3973
|
};
|
|
3969
3974
|
|
|
3975
|
+
// src/aws/index.js
|
|
3976
|
+
import {
|
|
3977
|
+
STSClient,
|
|
3978
|
+
GetCallerIdentityCommand
|
|
3979
|
+
} from "@aws-sdk/client-sts";
|
|
3980
|
+
import {
|
|
3981
|
+
EC2Client,
|
|
3982
|
+
DescribeRegionsCommand,
|
|
3983
|
+
DescribeAvailabilityZonesCommand,
|
|
3984
|
+
DescribeVpcsCommand,
|
|
3985
|
+
DescribeSubnetsCommand,
|
|
3986
|
+
DescribeSecurityGroupsCommand,
|
|
3987
|
+
DescribeKeyPairsCommand,
|
|
3988
|
+
DescribeImagesCommand
|
|
3989
|
+
} from "@aws-sdk/client-ec2";
|
|
3990
|
+
import {
|
|
3991
|
+
Route53Client,
|
|
3992
|
+
ListHostedZonesCommand
|
|
3993
|
+
} from "@aws-sdk/client-route-53";
|
|
3994
|
+
var DEFAULT_REGION = "us-east-1";
|
|
3995
|
+
var CANONICAL_OWNER_ID = "099720109477";
|
|
3996
|
+
var DEFAULT_UBUNTU_PATTERN = "ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*";
|
|
3997
|
+
function nameTag(tags) {
|
|
3998
|
+
const t = (tags ?? []).find((x) => x.Key === "Name");
|
|
3999
|
+
return t?.Value ?? null;
|
|
4000
|
+
}
|
|
4001
|
+
var Aws = class _Aws {
|
|
4002
|
+
/**
|
|
4003
|
+
* Resolve credentials/region from params and return an Aws instance.
|
|
4004
|
+
* Does not call AWS until you invoke a method (clients are lazy).
|
|
4005
|
+
*/
|
|
4006
|
+
static async init(context, options = {}) {
|
|
4007
|
+
const getOpt = async (k) => {
|
|
4008
|
+
try {
|
|
4009
|
+
const v = await context?.params?.get?.(k, "string");
|
|
4010
|
+
return v != null && String(v).trim() !== "" ? v : void 0;
|
|
4011
|
+
} catch {
|
|
4012
|
+
return void 0;
|
|
4013
|
+
}
|
|
4014
|
+
};
|
|
4015
|
+
const config2 = {
|
|
4016
|
+
region: options.region ?? await getOpt("awsRegion") ?? DEFAULT_REGION,
|
|
4017
|
+
accessKeyId: options.accessKeyId ?? await getOpt("awsAccessKeyId"),
|
|
4018
|
+
secretAccessKey: options.secretAccessKey ?? await getOpt("awsSecretAccessKey"),
|
|
4019
|
+
sessionToken: options.sessionToken ?? await getOpt("awsSessionToken")
|
|
4020
|
+
};
|
|
4021
|
+
const aws = new _Aws(context, config2);
|
|
4022
|
+
context?.logger?.debug?.(
|
|
4023
|
+
`[aws] region=${config2.region} credentials=${config2.accessKeyId ? "explicit" : "default-chain"}`
|
|
4024
|
+
);
|
|
4025
|
+
return aws;
|
|
4026
|
+
}
|
|
4027
|
+
constructor(context, config2) {
|
|
4028
|
+
this.logger = context?.logger ?? console;
|
|
4029
|
+
this.region = config2.region || DEFAULT_REGION;
|
|
4030
|
+
this._credentials = config2.accessKeyId && config2.secretAccessKey ? {
|
|
4031
|
+
accessKeyId: config2.accessKeyId,
|
|
4032
|
+
secretAccessKey: config2.secretAccessKey,
|
|
4033
|
+
...config2.sessionToken ? { sessionToken: config2.sessionToken } : {}
|
|
4034
|
+
} : void 0;
|
|
4035
|
+
process.env.AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED ??= "true";
|
|
4036
|
+
this._clients = {};
|
|
4037
|
+
}
|
|
4038
|
+
// ── clients (lazy, optionally region-scoped) ──────────────────────────────
|
|
4039
|
+
_clientConfig(region) {
|
|
4040
|
+
const cfg = { region: region || this.region };
|
|
4041
|
+
if (this._credentials) cfg.credentials = this._credentials;
|
|
4042
|
+
return cfg;
|
|
4043
|
+
}
|
|
4044
|
+
_sts() {
|
|
4045
|
+
return this._clients.sts ??= new STSClient(this._clientConfig());
|
|
4046
|
+
}
|
|
4047
|
+
_ec2(region) {
|
|
4048
|
+
const key = `ec2:${region || this.region}`;
|
|
4049
|
+
return this._clients[key] ??= new EC2Client(this._clientConfig(region));
|
|
4050
|
+
}
|
|
4051
|
+
_route53() {
|
|
4052
|
+
return this._clients.route53 ??= new Route53Client(this._clientConfig());
|
|
4053
|
+
}
|
|
4054
|
+
getRegion() {
|
|
4055
|
+
return this.region;
|
|
4056
|
+
}
|
|
4057
|
+
// ── identity ──────────────────────────────────────────────────────────────
|
|
4058
|
+
/** { account, arn, userId } — confirm which account/identity the keys belong to. */
|
|
4059
|
+
async whoAmI() {
|
|
4060
|
+
const out = await this._sts().send(new GetCallerIdentityCommand({}));
|
|
4061
|
+
return { account: out.Account, arn: out.Arn, userId: out.UserId };
|
|
4062
|
+
}
|
|
4063
|
+
// ── regions / AZs ──────────────────────────────────────────────────────────
|
|
4064
|
+
/** Enabled region names, sorted. */
|
|
4065
|
+
async listRegions({ allRegions = false } = {}) {
|
|
4066
|
+
const out = await this._ec2().send(new DescribeRegionsCommand({ AllRegions: allRegions }));
|
|
4067
|
+
return (out.Regions ?? []).map((r) => r.RegionName).sort();
|
|
4068
|
+
}
|
|
4069
|
+
/** Availability zone names for a region (defaults to the instance region). */
|
|
4070
|
+
async listAvailabilityZones(region) {
|
|
4071
|
+
const out = await this._ec2(region).send(new DescribeAvailabilityZonesCommand({}));
|
|
4072
|
+
return (out.AvailabilityZones ?? []).filter((z) => z.State === "available").map((z) => z.ZoneName).sort();
|
|
4073
|
+
}
|
|
4074
|
+
// ── VPC / subnets / SGs / key pairs ────────────────────────────────────────
|
|
4075
|
+
/** [{ id, cidr, isDefault, name }] in a region. */
|
|
4076
|
+
async listVpcs(region) {
|
|
4077
|
+
const out = await this._ec2(region).send(new DescribeVpcsCommand({}));
|
|
4078
|
+
return (out.Vpcs ?? []).map((v) => ({
|
|
4079
|
+
id: v.VpcId,
|
|
4080
|
+
cidr: v.CidrBlock,
|
|
4081
|
+
isDefault: !!v.IsDefault,
|
|
4082
|
+
name: nameTag(v.Tags)
|
|
4083
|
+
}));
|
|
4084
|
+
}
|
|
4085
|
+
/** [{ id, vpcId, az, cidr, public, name }]; pass vpcId to scope. */
|
|
4086
|
+
async listSubnets({ vpcId, region } = {}) {
|
|
4087
|
+
const Filters = vpcId ? [{ Name: "vpc-id", Values: [vpcId] }] : void 0;
|
|
4088
|
+
const out = await this._ec2(region).send(new DescribeSubnetsCommand({ Filters }));
|
|
4089
|
+
return (out.Subnets ?? []).map((s) => ({
|
|
4090
|
+
id: s.SubnetId,
|
|
4091
|
+
vpcId: s.VpcId,
|
|
4092
|
+
az: s.AvailabilityZone,
|
|
4093
|
+
cidr: s.CidrBlock,
|
|
4094
|
+
public: !!s.MapPublicIpOnLaunch,
|
|
4095
|
+
name: nameTag(s.Tags)
|
|
4096
|
+
}));
|
|
4097
|
+
}
|
|
4098
|
+
/** [{ id, name, vpcId, description }]; pass vpcId to scope. */
|
|
4099
|
+
async listSecurityGroups({ vpcId, region } = {}) {
|
|
4100
|
+
const Filters = vpcId ? [{ Name: "vpc-id", Values: [vpcId] }] : void 0;
|
|
4101
|
+
const out = await this._ec2(region).send(new DescribeSecurityGroupsCommand({ Filters }));
|
|
4102
|
+
return (out.SecurityGroups ?? []).map((g) => ({
|
|
4103
|
+
id: g.GroupId,
|
|
4104
|
+
name: g.GroupName,
|
|
4105
|
+
vpcId: g.VpcId,
|
|
4106
|
+
description: g.Description
|
|
4107
|
+
}));
|
|
4108
|
+
}
|
|
4109
|
+
/** [{ name, fingerprint }] EC2 key pairs in a region. */
|
|
4110
|
+
async listKeyPairs(region) {
|
|
4111
|
+
const out = await this._ec2(region).send(new DescribeKeyPairsCommand({}));
|
|
4112
|
+
return (out.KeyPairs ?? []).map((k) => ({ name: k.KeyName, fingerprint: k.KeyFingerprint }));
|
|
4113
|
+
}
|
|
4114
|
+
// ── Route53 ────────────────────────────────────────────────────────────────
|
|
4115
|
+
/** [{ id, name, private, recordCount }] — id is the bare zone id (no /hostedzone/). */
|
|
4116
|
+
async listHostedZones() {
|
|
4117
|
+
const zones = [];
|
|
4118
|
+
let marker;
|
|
4119
|
+
do {
|
|
4120
|
+
const out = await this._route53().send(new ListHostedZonesCommand({ Marker: marker }));
|
|
4121
|
+
for (const z of out.HostedZones ?? []) {
|
|
4122
|
+
zones.push({
|
|
4123
|
+
id: (z.Id ?? "").replace("/hostedzone/", ""),
|
|
4124
|
+
name: (z.Name ?? "").replace(/\.$/, ""),
|
|
4125
|
+
// strip trailing dot
|
|
4126
|
+
private: !!z.Config?.PrivateZone,
|
|
4127
|
+
recordCount: z.ResourceRecordSetCount
|
|
4128
|
+
});
|
|
4129
|
+
}
|
|
4130
|
+
marker = out.IsTruncated ? out.NextMarker : void 0;
|
|
4131
|
+
} while (marker);
|
|
4132
|
+
return zones;
|
|
4133
|
+
}
|
|
4134
|
+
/**
|
|
4135
|
+
* Best hosted zone for a domain: exact match, else the longest zone name that
|
|
4136
|
+
* is a suffix of the domain (so "api.foo.com" matches the "foo.com" zone).
|
|
4137
|
+
* Returns the zone object or null.
|
|
4138
|
+
*/
|
|
4139
|
+
async findHostedZoneForDomain(domain) {
|
|
4140
|
+
const target = String(domain ?? "").replace(/\.$/, "").toLowerCase();
|
|
4141
|
+
if (!target) return null;
|
|
4142
|
+
const zones = await this.listHostedZones();
|
|
4143
|
+
const exact = zones.find((z) => z.name.toLowerCase() === target);
|
|
4144
|
+
if (exact) return exact;
|
|
4145
|
+
return zones.filter((z) => target.endsWith(`.${z.name.toLowerCase()}`)).sort((a, b) => b.name.length - a.name.length)[0] ?? null;
|
|
4146
|
+
}
|
|
4147
|
+
// ── AMI lookup ──────────────────────────────────────────────────────────────
|
|
4148
|
+
/**
|
|
4149
|
+
* Latest Ubuntu AMI matching a name pattern in a region.
|
|
4150
|
+
* Returns { id, name, creationDate, architecture } or null.
|
|
4151
|
+
*/
|
|
4152
|
+
async findLatestUbuntuAmi({ region, pattern = DEFAULT_UBUNTU_PATTERN, architecture = "x86_64" } = {}) {
|
|
4153
|
+
const out = await this._ec2(region).send(new DescribeImagesCommand({
|
|
4154
|
+
Owners: [CANONICAL_OWNER_ID],
|
|
4155
|
+
Filters: [
|
|
4156
|
+
{ Name: "name", Values: [pattern] },
|
|
4157
|
+
{ Name: "virtualization-type", Values: ["hvm"] },
|
|
4158
|
+
{ Name: "state", Values: ["available"] },
|
|
4159
|
+
...architecture ? [{ Name: "architecture", Values: [architecture] }] : []
|
|
4160
|
+
]
|
|
4161
|
+
}));
|
|
4162
|
+
const newest = (out.Images ?? []).sort((a, b) => String(b.CreationDate).localeCompare(String(a.CreationDate)))[0];
|
|
4163
|
+
if (!newest) return null;
|
|
4164
|
+
return {
|
|
4165
|
+
id: newest.ImageId,
|
|
4166
|
+
name: newest.Name,
|
|
4167
|
+
creationDate: newest.CreationDate,
|
|
4168
|
+
architecture: newest.Architecture
|
|
4169
|
+
};
|
|
4170
|
+
}
|
|
4171
|
+
};
|
|
4172
|
+
|
|
3970
4173
|
// src/logger/index.js
|
|
3971
4174
|
import chalk from "chalk";
|
|
3972
4175
|
import util from "util";
|
|
@@ -4317,9 +4520,18 @@ function setup(opts = {}) {
|
|
|
4317
4520
|
// the used-params list now (after the script has initialized all its
|
|
4318
4521
|
// own components), instead of at exit. No-op for the default mode,
|
|
4319
4522
|
// which prints at exit via the cleanup registered by Params.
|
|
4523
|
+
//
|
|
4524
|
+
// --showUsedParams=stop behaves like "top" but then exits immediately —
|
|
4525
|
+
// a quick "show me the figured params and quit" that skips the flow's
|
|
4526
|
+
// actual work. Like --stopAfter=init, this is a hard exit(0) (registered
|
|
4527
|
+
// cleanups are skipped); call it once components/params are resolved.
|
|
4320
4528
|
showUsedParamsIfNeeded: () => {
|
|
4321
|
-
|
|
4322
|
-
|
|
4529
|
+
const mode = params.getShowUsedParamsMode?.();
|
|
4530
|
+
if (mode !== "top" && mode !== "stop") return;
|
|
4531
|
+
params.printUsedParams(logger);
|
|
4532
|
+
if (mode === "stop") {
|
|
4533
|
+
logger.debug?.("[showUsedParams=stop] params printed \u2014 exiting");
|
|
4534
|
+
process.exit(0);
|
|
4323
4535
|
}
|
|
4324
4536
|
}
|
|
4325
4537
|
};
|
|
@@ -4330,12 +4542,1061 @@ function setupContext(opts = {}) {
|
|
|
4330
4542
|
return setup(opts);
|
|
4331
4543
|
}
|
|
4332
4544
|
|
|
4545
|
+
// src/deploy/service.js
|
|
4546
|
+
function deriveRepoDirName(repoUrl) {
|
|
4547
|
+
const tail = String(repoUrl ?? "").split("/").pop() ?? "repo";
|
|
4548
|
+
return tail.replace(/\.git$/, "") || "repo";
|
|
4549
|
+
}
|
|
4550
|
+
function defineService(service) {
|
|
4551
|
+
if (!service?.name) throw new Error("service manifest needs a `name`");
|
|
4552
|
+
if (!service.appsRoot) throw new Error(`service "${service.name}" needs an appsRoot`);
|
|
4553
|
+
if (!service.repoUrl) throw new Error(`service "${service.name}" needs a repoUrl`);
|
|
4554
|
+
if (!service.pm2?.script) throw new Error(`service "${service.name}" needs pm2.script`);
|
|
4555
|
+
const pm2 = {
|
|
4556
|
+
appName: service.name,
|
|
4557
|
+
args: "",
|
|
4558
|
+
...service.pm2
|
|
4559
|
+
};
|
|
4560
|
+
const nginx = service.nginx ? { siteName: service.name, ...service.nginx } : null;
|
|
4561
|
+
return {
|
|
4562
|
+
repoDirName: deriveRepoDirName(service.repoUrl),
|
|
4563
|
+
repoSubdir: "",
|
|
4564
|
+
keepReleases: 3,
|
|
4565
|
+
testCommand: null,
|
|
4566
|
+
envScrubPatterns: [],
|
|
4567
|
+
legacyRepoEnv: null,
|
|
4568
|
+
buildInfoPath: "build-info.json",
|
|
4569
|
+
deployKey: null,
|
|
4570
|
+
requireEnv: false,
|
|
4571
|
+
...service,
|
|
4572
|
+
pm2,
|
|
4573
|
+
nginx
|
|
4574
|
+
};
|
|
4575
|
+
}
|
|
4576
|
+
|
|
4577
|
+
// src/deploy/paths.js
|
|
4578
|
+
import { join as join2 } from "path";
|
|
4579
|
+
function servicePaths(service) {
|
|
4580
|
+
const root = service.appsRoot;
|
|
4581
|
+
const repoDirName = service.repoDirName ?? "repo";
|
|
4582
|
+
const repoSubdir = service.repoSubdir ?? "";
|
|
4583
|
+
const repo = join2(root, repoDirName);
|
|
4584
|
+
const repoRun = repoSubdir ? join2(repo, repoSubdir) : repo;
|
|
4585
|
+
return {
|
|
4586
|
+
root,
|
|
4587
|
+
repo,
|
|
4588
|
+
repoRun,
|
|
4589
|
+
repoEnv: join2(repoRun, ".env"),
|
|
4590
|
+
releases: join2(root, "releases"),
|
|
4591
|
+
shared: join2(root, "shared"),
|
|
4592
|
+
logs: join2(root, "logs"),
|
|
4593
|
+
current: join2(root, "current"),
|
|
4594
|
+
sharedEnv: join2(root, "shared", ".env"),
|
|
4595
|
+
ecosystem: join2(root, "shared", "ecosystem.config.cjs"),
|
|
4596
|
+
deployLog: join2(root, "logs", "deploy.log"),
|
|
4597
|
+
lockHashFile: join2(root, "shared", ".package-lock.sha256")
|
|
4598
|
+
};
|
|
4599
|
+
}
|
|
4600
|
+
function releaseStamp(date = /* @__PURE__ */ new Date()) {
|
|
4601
|
+
return date.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
4602
|
+
}
|
|
4603
|
+
function releaseDir(releasesRoot, stamp) {
|
|
4604
|
+
return join2(releasesRoot, stamp);
|
|
4605
|
+
}
|
|
4606
|
+
|
|
4607
|
+
// src/deploy/run.js
|
|
4608
|
+
import { spawn } from "child_process";
|
|
4609
|
+
function npmEnv(baseEnv = process.env) {
|
|
4610
|
+
const env = { ...baseEnv };
|
|
4611
|
+
delete env.NODE_ENV;
|
|
4612
|
+
return env;
|
|
4613
|
+
}
|
|
4614
|
+
function npmInstallEnv(baseEnv = process.env) {
|
|
4615
|
+
return {
|
|
4616
|
+
...npmEnv(baseEnv),
|
|
4617
|
+
NPM_CONFIG_PRODUCTION: "false",
|
|
4618
|
+
npm_config_production: "false"
|
|
4619
|
+
};
|
|
4620
|
+
}
|
|
4621
|
+
function run(cmd, args, options = {}) {
|
|
4622
|
+
const { cwd, env, logger } = options;
|
|
4623
|
+
return new Promise((resolve3, reject) => {
|
|
4624
|
+
logger?.info?.(`$ ${cmd} ${args.join(" ")}${cwd ? ` (cwd=${cwd})` : ""}`);
|
|
4625
|
+
const child = spawn(cmd, args, {
|
|
4626
|
+
cwd,
|
|
4627
|
+
env: env ?? process.env,
|
|
4628
|
+
stdio: "inherit"
|
|
4629
|
+
});
|
|
4630
|
+
child.on("error", reject);
|
|
4631
|
+
child.on("close", (code) => {
|
|
4632
|
+
if (code === 0) resolve3();
|
|
4633
|
+
else reject(new Error(`${cmd} exited with code ${code}`));
|
|
4634
|
+
});
|
|
4635
|
+
});
|
|
4636
|
+
}
|
|
4637
|
+
function runShell(command, options = {}) {
|
|
4638
|
+
return run("bash", ["-lc", command], options);
|
|
4639
|
+
}
|
|
4640
|
+
|
|
4641
|
+
// src/deploy/log.js
|
|
4642
|
+
import { appendFile, mkdir } from "fs/promises";
|
|
4643
|
+
async function appendDeployLog(deployLogPath, message) {
|
|
4644
|
+
await mkdir(deployLogPath.replace(/\/[^/]+$/, ""), { recursive: true });
|
|
4645
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
|
|
4646
|
+
`;
|
|
4647
|
+
await appendFile(deployLogPath, line);
|
|
4648
|
+
}
|
|
4649
|
+
|
|
4650
|
+
// src/deploy/git.js
|
|
4651
|
+
import { access } from "fs/promises";
|
|
4652
|
+
async function pathExists(path5) {
|
|
4653
|
+
try {
|
|
4654
|
+
await access(path5);
|
|
4655
|
+
return true;
|
|
4656
|
+
} catch {
|
|
4657
|
+
return false;
|
|
4658
|
+
}
|
|
4659
|
+
}
|
|
4660
|
+
async function cloneRepo(service, options = {}) {
|
|
4661
|
+
const { dryRun = false, logger = console } = options;
|
|
4662
|
+
const paths = servicePaths(service);
|
|
4663
|
+
if (await pathExists(paths.repo)) {
|
|
4664
|
+
throw new Error(`Repo already exists at ${paths.repo} \u2014 use git pull instead`);
|
|
4665
|
+
}
|
|
4666
|
+
if (dryRun) {
|
|
4667
|
+
logger.info(`[dryRun] would git clone ${service.repoUrl} ${paths.repo}`);
|
|
4668
|
+
return;
|
|
4669
|
+
}
|
|
4670
|
+
await run("git", ["clone", service.repoUrl, paths.repo], { logger });
|
|
4671
|
+
logger.info(`cloned ${service.repoUrl} \u2192 ${paths.repo}`);
|
|
4672
|
+
}
|
|
4673
|
+
async function pullRepo(service, options = {}) {
|
|
4674
|
+
const { dryRun = false, logger = console } = options;
|
|
4675
|
+
const paths = servicePaths(service);
|
|
4676
|
+
if (!await pathExists(paths.repo)) {
|
|
4677
|
+
throw new Error(`Repo missing at ${paths.repo} \u2014 run provision first`);
|
|
4678
|
+
}
|
|
4679
|
+
if (dryRun) {
|
|
4680
|
+
logger.info(`[dryRun] would git -C ${paths.repo} pull --ff-only`);
|
|
4681
|
+
return;
|
|
4682
|
+
}
|
|
4683
|
+
await run("git", ["-C", paths.repo, "pull", "--ff-only"], { logger });
|
|
4684
|
+
logger.info(`pulled ${paths.repo}`);
|
|
4685
|
+
}
|
|
4686
|
+
|
|
4687
|
+
// src/deploy/release.js
|
|
4688
|
+
import { cp, mkdir as mkdir2, readlink, readdir, stat } from "fs/promises";
|
|
4689
|
+
import { join as join3, dirname as pathDirname, sep } from "path";
|
|
4690
|
+
var SKIP_TOP = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
4691
|
+
function shouldCopyEntry(srcPath) {
|
|
4692
|
+
const parts = srcPath.split(sep);
|
|
4693
|
+
if (parts.some((p) => p === "node_modules" || p === ".git")) return false;
|
|
4694
|
+
const top = parts.at(-1);
|
|
4695
|
+
if (parts.length === 1 && SKIP_TOP.has(top)) return false;
|
|
4696
|
+
return true;
|
|
4697
|
+
}
|
|
4698
|
+
async function createRelease(service, options = {}) {
|
|
4699
|
+
const { stamp = releaseStamp(), dryRun = false, logger = console } = options;
|
|
4700
|
+
const paths = servicePaths(service);
|
|
4701
|
+
const dest = releaseDir(paths.releases, stamp);
|
|
4702
|
+
if (dryRun) {
|
|
4703
|
+
logger.info(`[dryRun] would copy ${paths.repoRun} \u2192 ${dest}`);
|
|
4704
|
+
return { stamp, path: dest };
|
|
4705
|
+
}
|
|
4706
|
+
await mkdir2(paths.releases, { recursive: true });
|
|
4707
|
+
await cp(paths.repoRun, dest, {
|
|
4708
|
+
recursive: true,
|
|
4709
|
+
filter: (src) => shouldCopyEntry(src)
|
|
4710
|
+
});
|
|
4711
|
+
logger.info(`release ${stamp} created at ${dest}`);
|
|
4712
|
+
return { stamp, path: dest };
|
|
4713
|
+
}
|
|
4714
|
+
async function readCurrentRelease(paths) {
|
|
4715
|
+
try {
|
|
4716
|
+
const target = await readlink(paths.current);
|
|
4717
|
+
return target.startsWith("/") ? target : join3(pathDirname(paths.current), target);
|
|
4718
|
+
} catch {
|
|
4719
|
+
return null;
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
async function listReleases(paths) {
|
|
4723
|
+
let names;
|
|
4724
|
+
try {
|
|
4725
|
+
names = await readdir(paths.releases);
|
|
4726
|
+
} catch {
|
|
4727
|
+
return [];
|
|
4728
|
+
}
|
|
4729
|
+
const entries = [];
|
|
4730
|
+
for (const name of names) {
|
|
4731
|
+
const full = releaseDir(paths.releases, name);
|
|
4732
|
+
const s = await stat(full);
|
|
4733
|
+
if (s.isDirectory()) entries.push({ name, path: full, mtime: s.mtime });
|
|
4734
|
+
}
|
|
4735
|
+
entries.sort((a, b) => b.name.localeCompare(a.name));
|
|
4736
|
+
return entries;
|
|
4737
|
+
}
|
|
4738
|
+
|
|
4739
|
+
// src/deploy/activate.js
|
|
4740
|
+
import { symlink, unlink } from "fs/promises";
|
|
4741
|
+
import { join as join4 } from "path";
|
|
4742
|
+
async function activateRelease(releasePath, paths, options = {}) {
|
|
4743
|
+
const { dryRun = false, logger = console } = options;
|
|
4744
|
+
if (dryRun) {
|
|
4745
|
+
logger.info(`[dryRun] would activate ${releasePath} \u2192 ${paths.current}`);
|
|
4746
|
+
return;
|
|
4747
|
+
}
|
|
4748
|
+
try {
|
|
4749
|
+
await unlink(paths.current);
|
|
4750
|
+
} catch {
|
|
4751
|
+
}
|
|
4752
|
+
await symlink(releasePath, paths.current);
|
|
4753
|
+
const envLink = join4(releasePath, ".env");
|
|
4754
|
+
try {
|
|
4755
|
+
await unlink(envLink);
|
|
4756
|
+
} catch {
|
|
4757
|
+
}
|
|
4758
|
+
await symlink(paths.sharedEnv, envLink);
|
|
4759
|
+
logger.info(`active release: ${releasePath}`);
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4762
|
+
// src/deploy/deps.js
|
|
4763
|
+
import { createHash } from "crypto";
|
|
4764
|
+
import { access as access2, cp as cp2, mkdir as mkdir3, readFile, rm, writeFile } from "fs/promises";
|
|
4765
|
+
import { join as join5 } from "path";
|
|
4766
|
+
async function pathExists2(path5) {
|
|
4767
|
+
try {
|
|
4768
|
+
await access2(path5);
|
|
4769
|
+
return true;
|
|
4770
|
+
} catch {
|
|
4771
|
+
return false;
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
async function lockfileHash(releasePath) {
|
|
4775
|
+
const lockPath = join5(releasePath, "package-lock.json");
|
|
4776
|
+
try {
|
|
4777
|
+
const buf = await readFile(lockPath);
|
|
4778
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
4779
|
+
} catch {
|
|
4780
|
+
return null;
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
async function readHashFile(path5) {
|
|
4784
|
+
try {
|
|
4785
|
+
return (await readFile(path5, "utf8")).trim();
|
|
4786
|
+
} catch {
|
|
4787
|
+
return null;
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4790
|
+
async function npmInstall(releasePath, hasLock, logger) {
|
|
4791
|
+
await rm(join5(releasePath, "node_modules"), { recursive: true, force: true });
|
|
4792
|
+
const cmd = hasLock ? ["ci", "--include=dev"] : ["install", "--include=dev", "--no-audit", "--no-fund"];
|
|
4793
|
+
logger.info(`running npm ${cmd.join(" ")} in ${releasePath}`);
|
|
4794
|
+
await run("npm", cmd, { cwd: releasePath, logger, env: npmInstallEnv() });
|
|
4795
|
+
}
|
|
4796
|
+
async function installDeps(service, releasePath, paths, options = {}) {
|
|
4797
|
+
const { dryRun = false, logger = console } = options;
|
|
4798
|
+
if (dryRun) {
|
|
4799
|
+
logger.info(`[dryRun] would install deps in ${releasePath}`);
|
|
4800
|
+
return { hash: null, copied: false };
|
|
4801
|
+
}
|
|
4802
|
+
const hash = await lockfileHash(releasePath);
|
|
4803
|
+
const hasLock = hash !== null;
|
|
4804
|
+
const hashMarker = join5(releasePath, ".deploy-package-lock.sha256");
|
|
4805
|
+
const currentPath = await readCurrentRelease(paths);
|
|
4806
|
+
let copied = false;
|
|
4807
|
+
if (hasLock && currentPath && await pathExists2(join5(currentPath, "node_modules"))) {
|
|
4808
|
+
const currentHash = await readHashFile(join5(currentPath, ".deploy-package-lock.sha256"));
|
|
4809
|
+
if (currentHash === hash) {
|
|
4810
|
+
logger.info("lockfile unchanged \u2014 copying node_modules from current release");
|
|
4811
|
+
await cp2(join5(currentPath, "node_modules"), join5(releasePath, "node_modules"), {
|
|
4812
|
+
recursive: true,
|
|
4813
|
+
force: true
|
|
4814
|
+
});
|
|
4815
|
+
copied = true;
|
|
4816
|
+
}
|
|
4817
|
+
}
|
|
4818
|
+
if (!copied) {
|
|
4819
|
+
await npmInstall(releasePath, hasLock, logger);
|
|
4820
|
+
}
|
|
4821
|
+
if (hasLock) {
|
|
4822
|
+
await writeFile(hashMarker, `${hash}
|
|
4823
|
+
`, "utf8");
|
|
4824
|
+
await mkdir3(paths.shared, { recursive: true });
|
|
4825
|
+
await writeFile(paths.lockHashFile, `${hash}
|
|
4826
|
+
`, "utf8");
|
|
4827
|
+
}
|
|
4828
|
+
return { hash, copied };
|
|
4829
|
+
}
|
|
4830
|
+
|
|
4831
|
+
// src/deploy/test.js
|
|
4832
|
+
import { symlink as symlink2, unlink as unlink2 } from "fs/promises";
|
|
4833
|
+
import { join as join6 } from "path";
|
|
4834
|
+
async function runReleaseTests(service, releasePath, paths, options = {}) {
|
|
4835
|
+
const { dryRun = false, logger = console } = options;
|
|
4836
|
+
if (!service.testCommand) {
|
|
4837
|
+
logger.info("no testCommand configured \u2014 skipping tests");
|
|
4838
|
+
return;
|
|
4839
|
+
}
|
|
4840
|
+
if (dryRun) {
|
|
4841
|
+
logger.info(`[dryRun] would run "${service.testCommand}" in ${releasePath}`);
|
|
4842
|
+
return;
|
|
4843
|
+
}
|
|
4844
|
+
const envLink = join6(releasePath, ".env");
|
|
4845
|
+
try {
|
|
4846
|
+
await unlink2(envLink);
|
|
4847
|
+
} catch {
|
|
4848
|
+
}
|
|
4849
|
+
await symlink2(paths.sharedEnv, envLink);
|
|
4850
|
+
logger.info(`running "${service.testCommand}" in ${releasePath}`);
|
|
4851
|
+
await runShell(service.testCommand, { cwd: releasePath, logger, env: npmInstallEnv() });
|
|
4852
|
+
}
|
|
4853
|
+
|
|
4854
|
+
// src/deploy/prune.js
|
|
4855
|
+
import { rm as rm2, readlink as readlink2 } from "fs/promises";
|
|
4856
|
+
async function pruneReleases(service, paths, options = {}) {
|
|
4857
|
+
const { dryRun = false, logger = console } = options;
|
|
4858
|
+
const keep = service.keepReleases ?? 3;
|
|
4859
|
+
const releases = await listReleases(paths);
|
|
4860
|
+
let activeName = null;
|
|
4861
|
+
try {
|
|
4862
|
+
const target = await readlink2(paths.current);
|
|
4863
|
+
activeName = target.split("/").pop();
|
|
4864
|
+
} catch {
|
|
4865
|
+
}
|
|
4866
|
+
const keepSet = /* @__PURE__ */ new Set();
|
|
4867
|
+
for (const rel of releases) {
|
|
4868
|
+
if (keepSet.size < keep) keepSet.add(rel.name);
|
|
4869
|
+
}
|
|
4870
|
+
if (activeName) keepSet.add(activeName);
|
|
4871
|
+
const toRemove = releases.filter((rel) => !keepSet.has(rel.name));
|
|
4872
|
+
for (const rel of toRemove) {
|
|
4873
|
+
if (dryRun) {
|
|
4874
|
+
logger.info(`[dryRun] would rm -rf ${rel.path}`);
|
|
4875
|
+
} else {
|
|
4876
|
+
await rm2(rel.path, { recursive: true, force: true });
|
|
4877
|
+
logger.info(`pruned ${rel.path}`);
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
return { removed: toRemove.map((r) => r.name) };
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4883
|
+
// src/deploy/pm2.js
|
|
4884
|
+
async function reloadPm2(paths, options = {}) {
|
|
4885
|
+
const { dryRun = false, logger = console } = options;
|
|
4886
|
+
if (dryRun) {
|
|
4887
|
+
logger.info(`[dryRun] would pm2 startOrReload ${paths.ecosystem} --update-env`);
|
|
4888
|
+
return;
|
|
4889
|
+
}
|
|
4890
|
+
await runShell(`pm2 startOrReload "${paths.ecosystem}" --update-env`, { logger });
|
|
4891
|
+
logger.info("pm2 reloaded");
|
|
4892
|
+
}
|
|
4893
|
+
|
|
4894
|
+
// src/deploy/nginx.js
|
|
4895
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
4896
|
+
import { tmpdir } from "os";
|
|
4897
|
+
import { join as join7 } from "path";
|
|
4898
|
+
async function hasTlsCert(certPath) {
|
|
4899
|
+
try {
|
|
4900
|
+
await runShell(`sudo test -f '${certPath}'`, {});
|
|
4901
|
+
return true;
|
|
4902
|
+
} catch {
|
|
4903
|
+
return false;
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4906
|
+
function proxyBlock(port) {
|
|
4907
|
+
return ` location / {
|
|
4908
|
+
proxy_pass http://127.0.0.1:${port};
|
|
4909
|
+
proxy_http_version 1.1;
|
|
4910
|
+
proxy_set_header Host $host;
|
|
4911
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
4912
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
4913
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
4914
|
+
proxy_read_timeout 120s;
|
|
4915
|
+
}`;
|
|
4916
|
+
}
|
|
4917
|
+
function buildNginxConfig(service) {
|
|
4918
|
+
const { nginx, pm2 } = service;
|
|
4919
|
+
const certDir = `/etc/letsencrypt/live/${nginx.fqdn}`;
|
|
4920
|
+
return `# ${nginx.siteName} \u2014 managed by cli-toolkit deploy (proxy mode)
|
|
4921
|
+
# ${nginx.fqdn} \u2192 127.0.0.1:${pm2.port}
|
|
4922
|
+
|
|
4923
|
+
server {
|
|
4924
|
+
listen 80;
|
|
4925
|
+
listen [::]:80;
|
|
4926
|
+
server_name ${nginx.fqdn};
|
|
4927
|
+
location / { return 301 https://$host$request_uri; }
|
|
4928
|
+
}
|
|
4929
|
+
|
|
4930
|
+
server {
|
|
4931
|
+
listen 443 ssl;
|
|
4932
|
+
listen [::]:443 ssl;
|
|
4933
|
+
server_name ${nginx.fqdn};
|
|
4934
|
+
|
|
4935
|
+
ssl_certificate ${certDir}/fullchain.pem;
|
|
4936
|
+
ssl_certificate_key ${certDir}/privkey.pem;
|
|
4937
|
+
include /etc/letsencrypt/options-ssl-nginx.conf;
|
|
4938
|
+
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
|
4939
|
+
|
|
4940
|
+
client_max_body_size 25m;
|
|
4941
|
+
|
|
4942
|
+
${proxyBlock(pm2.port)}
|
|
4943
|
+
}
|
|
4944
|
+
`;
|
|
4945
|
+
}
|
|
4946
|
+
function buildNginxConfigHttpOnly(service) {
|
|
4947
|
+
const { nginx, pm2 } = service;
|
|
4948
|
+
return `# ${nginx.siteName} \u2014 managed by cli-toolkit deploy (HTTP proxy, no TLS cert yet)
|
|
4949
|
+
|
|
4950
|
+
server {
|
|
4951
|
+
listen 80;
|
|
4952
|
+
listen [::]:80;
|
|
4953
|
+
server_name ${nginx.fqdn};
|
|
4954
|
+
|
|
4955
|
+
client_max_body_size 25m;
|
|
4956
|
+
|
|
4957
|
+
${proxyBlock(pm2.port)}
|
|
4958
|
+
}
|
|
4959
|
+
`;
|
|
4960
|
+
}
|
|
4961
|
+
async function enableNginxUpstream(service, options = {}) {
|
|
4962
|
+
const { dryRun = false, logger = console } = options;
|
|
4963
|
+
const { nginx } = service;
|
|
4964
|
+
if (!nginx) {
|
|
4965
|
+
logger.info("no nginx config on service \u2014 skipping nginx step");
|
|
4966
|
+
return { skipped: true };
|
|
4967
|
+
}
|
|
4968
|
+
const siteAvailable = `/etc/nginx/sites-available/${nginx.siteName}`;
|
|
4969
|
+
const siteEnabled = `/etc/nginx/sites-enabled/${nginx.siteName}`;
|
|
4970
|
+
const cert = `/etc/letsencrypt/live/${nginx.fqdn}/fullchain.pem`;
|
|
4971
|
+
if (dryRun) {
|
|
4972
|
+
logger.info(`[dryRun] would write ${siteAvailable} (proxy \u2192 127.0.0.1:${service.pm2.port}) and reload nginx`);
|
|
4973
|
+
return { hasCert: null };
|
|
4974
|
+
}
|
|
4975
|
+
const hasCert = await hasTlsCert(cert);
|
|
4976
|
+
const config2 = hasCert ? buildNginxConfig(service) : buildNginxConfigHttpOnly(service);
|
|
4977
|
+
const tmp = join7(tmpdir(), `${service.name}-nginx.conf`);
|
|
4978
|
+
await writeFile2(tmp, config2);
|
|
4979
|
+
await runShell(
|
|
4980
|
+
`sudo cp '${tmp}' '${siteAvailable}' && sudo ln -sf '${siteAvailable}' '${siteEnabled}' && sudo nginx -t && sudo systemctl reload nginx`,
|
|
4981
|
+
{ logger }
|
|
4982
|
+
);
|
|
4983
|
+
logger.info(`nginx upstream enabled for ${nginx.fqdn} \u2192 127.0.0.1:${service.pm2.port} (tls=${hasCert})`);
|
|
4984
|
+
return { hasCert };
|
|
4985
|
+
}
|
|
4986
|
+
|
|
4987
|
+
// src/deploy/sync-env.js
|
|
4988
|
+
import { access as access3, mkdir as mkdir4, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
4989
|
+
function scrubEnvContent(content, patterns = []) {
|
|
4990
|
+
const regexes = patterns.map((p) => p instanceof RegExp ? p : new RegExp(p));
|
|
4991
|
+
if (regexes.length === 0) return content;
|
|
4992
|
+
return content.split("\n").filter((line) => !regexes.some((re) => re.test(line.trim()))).join("\n");
|
|
4993
|
+
}
|
|
4994
|
+
async function pathExists3(path5) {
|
|
4995
|
+
try {
|
|
4996
|
+
await access3(path5);
|
|
4997
|
+
return true;
|
|
4998
|
+
} catch {
|
|
4999
|
+
return false;
|
|
5000
|
+
}
|
|
5001
|
+
}
|
|
5002
|
+
async function syncEnv(service, options = {}) {
|
|
5003
|
+
const { dryRun = false, logger = console } = options;
|
|
5004
|
+
const paths = servicePaths(service);
|
|
5005
|
+
const patterns = service.envScrubPatterns ?? [];
|
|
5006
|
+
if (dryRun) {
|
|
5007
|
+
logger.info(`[dryRun] would sync ${paths.repoEnv} \u2192 ${paths.sharedEnv}`);
|
|
5008
|
+
return { source: paths.repoEnv, dest: paths.sharedEnv };
|
|
5009
|
+
}
|
|
5010
|
+
let source = paths.repoEnv;
|
|
5011
|
+
if (!await pathExists3(source) && service.legacyRepoEnv && await pathExists3(service.legacyRepoEnv)) {
|
|
5012
|
+
logger.info(`using legacy env: ${service.legacyRepoEnv}`);
|
|
5013
|
+
source = service.legacyRepoEnv;
|
|
5014
|
+
}
|
|
5015
|
+
await mkdir4(paths.shared, { recursive: true });
|
|
5016
|
+
if (!await pathExists3(source)) {
|
|
5017
|
+
if (service.requireEnv) {
|
|
5018
|
+
throw new Error(
|
|
5019
|
+
`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)"
|
|
5020
|
+
);
|
|
5021
|
+
}
|
|
5022
|
+
if (!await pathExists3(paths.sharedEnv)) {
|
|
5023
|
+
await writeFile3(paths.sharedEnv, "", { mode: 384 });
|
|
5024
|
+
}
|
|
5025
|
+
logger.warn(`no .env found (source ${source}) \u2014 using empty ${paths.sharedEnv}`);
|
|
5026
|
+
return { source: null, dest: paths.sharedEnv };
|
|
5027
|
+
}
|
|
5028
|
+
const raw = await readFile2(source, "utf8");
|
|
5029
|
+
await writeFile3(paths.sharedEnv, scrubEnvContent(raw, patterns), { mode: 384 });
|
|
5030
|
+
logger.info(`synced ${source} \u2192 ${paths.sharedEnv}`);
|
|
5031
|
+
return { source, dest: paths.sharedEnv };
|
|
5032
|
+
}
|
|
5033
|
+
|
|
5034
|
+
// src/deploy/build-info.js
|
|
5035
|
+
import { readFile as readFile3, writeFile as writeFile4, mkdir as mkdir5 } from "fs/promises";
|
|
5036
|
+
import { join as join8, dirname as dirname2 } from "path";
|
|
5037
|
+
import { execFile } from "child_process";
|
|
5038
|
+
import { promisify } from "util";
|
|
5039
|
+
var execFileAsync = promisify(execFile);
|
|
5040
|
+
function bumpPatchVersion(version) {
|
|
5041
|
+
const parts = String(version).trim().split(".");
|
|
5042
|
+
const major = Number.parseInt(parts[0], 10) || 0;
|
|
5043
|
+
const minor = Number.parseInt(parts[1], 10) || 0;
|
|
5044
|
+
const patch = Number.parseInt(parts[2], 10) || 0;
|
|
5045
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
5046
|
+
}
|
|
5047
|
+
async function readJson(path5) {
|
|
5048
|
+
try {
|
|
5049
|
+
return JSON.parse(await readFile3(path5, "utf8"));
|
|
5050
|
+
} catch {
|
|
5051
|
+
return null;
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
async function gitShortCommit(repoPath) {
|
|
5055
|
+
try {
|
|
5056
|
+
const { stdout } = await execFileAsync("git", ["-C", repoPath, "rev-parse", "--short", "HEAD"], {
|
|
5057
|
+
encoding: "utf8"
|
|
5058
|
+
});
|
|
5059
|
+
return stdout.trim() || null;
|
|
5060
|
+
} catch {
|
|
5061
|
+
return null;
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
async function resolveNextVersion(service, paths, pkgPath) {
|
|
5065
|
+
const pkg = await readJson(pkgPath) ?? {};
|
|
5066
|
+
const baseVersion = pkg.version || "0.1.0";
|
|
5067
|
+
const currentPath = await readCurrentRelease(paths);
|
|
5068
|
+
if (currentPath) {
|
|
5069
|
+
const active = await readJson(join8(currentPath, service.buildInfoPath));
|
|
5070
|
+
if (active?.version) return bumpPatchVersion(active.version);
|
|
5071
|
+
}
|
|
5072
|
+
return baseVersion;
|
|
5073
|
+
}
|
|
5074
|
+
async function readReleaseBuildInfo(service, releasePath) {
|
|
5075
|
+
return readJson(join8(releasePath, service.buildInfoPath));
|
|
5076
|
+
}
|
|
5077
|
+
async function writeReleaseBuildInfo(service, releasePath, { stamp, dryRun = false, logger = console }) {
|
|
5078
|
+
const paths = servicePaths(service);
|
|
5079
|
+
const pkgPath = join8(paths.repoRun, "package.json");
|
|
5080
|
+
const version = await resolveNextVersion(service, paths, pkgPath);
|
|
5081
|
+
const gitCommit = await gitShortCommit(paths.repo);
|
|
5082
|
+
const buildInfo = {
|
|
5083
|
+
version,
|
|
5084
|
+
release: stamp,
|
|
5085
|
+
deployedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5086
|
+
gitCommit,
|
|
5087
|
+
service: service.name
|
|
5088
|
+
};
|
|
5089
|
+
const dest = join8(releasePath, service.buildInfoPath);
|
|
5090
|
+
if (dryRun) {
|
|
5091
|
+
logger.info(`[dryRun] would write ${dest} (${JSON.stringify(buildInfo)})`);
|
|
5092
|
+
return buildInfo;
|
|
5093
|
+
}
|
|
5094
|
+
await mkdir5(dirname2(dest), { recursive: true });
|
|
5095
|
+
await writeFile4(dest, `${JSON.stringify(buildInfo, null, 2)}
|
|
5096
|
+
`, { mode: 420 });
|
|
5097
|
+
const gitSuffix = gitCommit ? ` git=${gitCommit}` : "";
|
|
5098
|
+
logger.info(`build info: v${version} release=${stamp}${gitSuffix}`);
|
|
5099
|
+
return buildInfo;
|
|
5100
|
+
}
|
|
5101
|
+
|
|
5102
|
+
// src/deploy/init-structure.js
|
|
5103
|
+
import { access as access4, appendFile as appendFile2, mkdir as mkdir6, writeFile as writeFile5 } from "fs/promises";
|
|
5104
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
5105
|
+
async function pathExists4(path5) {
|
|
5106
|
+
try {
|
|
5107
|
+
await access4(path5);
|
|
5108
|
+
return true;
|
|
5109
|
+
} catch {
|
|
5110
|
+
return false;
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
async function requireDeployRoot(parentDir, serviceRoot) {
|
|
5114
|
+
if (await pathExists4(parentDir)) return;
|
|
5115
|
+
throw new Error(
|
|
5116
|
+
`Cannot create ${serviceRoot}: parent directory ${parentDir} does not exist.
|
|
5117
|
+
This is meant to run on the target host (where ${parentDir} exists). For a local dry run use --appsRoot=/tmp/<service>.`
|
|
5118
|
+
);
|
|
5119
|
+
}
|
|
5120
|
+
function buildEcosystemConfig(service, paths) {
|
|
5121
|
+
const { pm2 } = service;
|
|
5122
|
+
const outLog = join9(paths.logs, `${pm2.appName}.out.log`);
|
|
5123
|
+
const errLog = join9(paths.logs, `${pm2.appName}.err.log`);
|
|
5124
|
+
return `/**
|
|
5125
|
+
* pm2 ecosystem for ${service.name} \u2014 seeded by cli-toolkit deploy (init).
|
|
5126
|
+
*
|
|
5127
|
+
* cwd points at the \`current\` symlink (created on first deploy).
|
|
5128
|
+
* Tweak \`args\` here, then: pm2 reload ${paths.ecosystem} --update-env
|
|
5129
|
+
*/
|
|
5130
|
+
module.exports = {
|
|
5131
|
+
apps: [
|
|
5132
|
+
{
|
|
5133
|
+
name: "${pm2.appName}",
|
|
5134
|
+
script: "${pm2.script}",
|
|
5135
|
+
cwd: "${paths.current}",
|
|
5136
|
+
args: "${pm2.args}",
|
|
5137
|
+
instances: 1,
|
|
5138
|
+
exec_mode: "fork",
|
|
5139
|
+
autorestart: true,
|
|
5140
|
+
min_uptime: "10s",
|
|
5141
|
+
max_restarts: 10,
|
|
5142
|
+
restart_delay: 2000,
|
|
5143
|
+
max_memory_restart: "1500M",
|
|
5144
|
+
out_file: "${outLog}",
|
|
5145
|
+
error_file: "${errLog}",
|
|
5146
|
+
merge_logs: true,
|
|
5147
|
+
time: true,
|
|
5148
|
+
env: {
|
|
5149
|
+
NODE_ENV: "production",
|
|
5150
|
+
},
|
|
5151
|
+
},
|
|
5152
|
+
],
|
|
5153
|
+
};
|
|
5154
|
+
`;
|
|
5155
|
+
}
|
|
5156
|
+
async function initServiceStructure(service, options = {}) {
|
|
5157
|
+
const { dryRun = false, logger = console } = options;
|
|
5158
|
+
const paths = servicePaths(service);
|
|
5159
|
+
const dirs = [paths.releases, paths.shared, paths.logs];
|
|
5160
|
+
const created = [];
|
|
5161
|
+
const skipped = [];
|
|
5162
|
+
if (!dryRun) {
|
|
5163
|
+
await requireDeployRoot(dirname3(paths.root), paths.root);
|
|
5164
|
+
}
|
|
5165
|
+
for (const dir of dirs) {
|
|
5166
|
+
if (await pathExists4(dir)) {
|
|
5167
|
+
skipped.push(dir);
|
|
5168
|
+
continue;
|
|
5169
|
+
}
|
|
5170
|
+
if (!dryRun) await mkdir6(dir, { recursive: true });
|
|
5171
|
+
created.push(dir);
|
|
5172
|
+
}
|
|
5173
|
+
let ecosystemCreated = false;
|
|
5174
|
+
if (await pathExists4(paths.ecosystem)) {
|
|
5175
|
+
skipped.push(paths.ecosystem);
|
|
5176
|
+
} else {
|
|
5177
|
+
if (!dryRun) {
|
|
5178
|
+
await writeFile5(paths.ecosystem, buildEcosystemConfig(service, paths), { mode: 420 });
|
|
5179
|
+
}
|
|
5180
|
+
ecosystemCreated = true;
|
|
5181
|
+
created.push(paths.ecosystem);
|
|
5182
|
+
}
|
|
5183
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] init-structure service=${service.name} dryRun=${dryRun} created=${created.length} skipped=${skipped.length}
|
|
5184
|
+
`;
|
|
5185
|
+
if (!dryRun) {
|
|
5186
|
+
await mkdir6(paths.logs, { recursive: true });
|
|
5187
|
+
await appendFile2(paths.deployLog, line);
|
|
5188
|
+
}
|
|
5189
|
+
logger.info(`service=${service.name} appsRoot=${paths.root}`);
|
|
5190
|
+
logger.info(`created: ${created.length ? created.join(", ") : "(none)"}`);
|
|
5191
|
+
logger.info(`already present: ${skipped.length ? skipped.join(", ") : "(none)"}`);
|
|
5192
|
+
if (ecosystemCreated) logger.info(`seeded ${paths.ecosystem}`);
|
|
5193
|
+
return { paths, created, skipped, ecosystemCreated };
|
|
5194
|
+
}
|
|
5195
|
+
|
|
5196
|
+
// src/deploy/bootstrap-host.js
|
|
5197
|
+
import { execSync as execSync2 } from "child_process";
|
|
5198
|
+
import { access as access5, chmod, copyFile, mkdir as mkdir7, readFile as readFile4, writeFile as writeFile6 } from "fs/promises";
|
|
5199
|
+
import { homedir } from "os";
|
|
5200
|
+
import { basename as basename2, join as join10 } from "path";
|
|
5201
|
+
async function pathExists5(path5) {
|
|
5202
|
+
try {
|
|
5203
|
+
await access5(path5);
|
|
5204
|
+
return true;
|
|
5205
|
+
} catch {
|
|
5206
|
+
return false;
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
function expandHome(path5) {
|
|
5210
|
+
return path5.startsWith("~/") ? join10(homedir(), path5.slice(2)) : path5;
|
|
5211
|
+
}
|
|
5212
|
+
async function ensurePm2Startup(options = {}) {
|
|
5213
|
+
const { user = "ubuntu", dryRun = false, logger = console } = options;
|
|
5214
|
+
if (dryRun) {
|
|
5215
|
+
logger.info("[dryRun] would configure pm2 startup systemd");
|
|
5216
|
+
return;
|
|
5217
|
+
}
|
|
5218
|
+
try {
|
|
5219
|
+
execSync2("pm2 ping", { stdio: "ignore" });
|
|
5220
|
+
} catch {
|
|
5221
|
+
}
|
|
5222
|
+
try {
|
|
5223
|
+
const out = execSync2(`pm2 startup systemd -u ${user} --hp /home/${user}`, { encoding: "utf8" });
|
|
5224
|
+
const sudoLine = out.split("\n").find((l) => l.trim().startsWith("sudo"));
|
|
5225
|
+
if (sudoLine) {
|
|
5226
|
+
execSync2(sudoLine.trim(), { stdio: "inherit" });
|
|
5227
|
+
logger.info("pm2 startup systemd configured");
|
|
5228
|
+
}
|
|
5229
|
+
} catch (err) {
|
|
5230
|
+
logger.warn(`pm2 startup skipped or already configured: ${err.message}`);
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
5233
|
+
async function installDeployKey(deployKeyPath, options = {}) {
|
|
5234
|
+
const { dryRun = false, logger = console } = options;
|
|
5235
|
+
const keyPath = expandHome(deployKeyPath);
|
|
5236
|
+
if (!await pathExists5(keyPath)) {
|
|
5237
|
+
logger.warn(`deploy key not found at ${keyPath} \u2014 skipping git ssh setup`);
|
|
5238
|
+
return;
|
|
5239
|
+
}
|
|
5240
|
+
const keyBase = basename2(keyPath);
|
|
5241
|
+
const sshDir = join10(homedir(), ".ssh");
|
|
5242
|
+
const destKey = join10(sshDir, keyBase);
|
|
5243
|
+
const configPath = join10(sshDir, "config");
|
|
5244
|
+
const block = `
|
|
5245
|
+
Host github.com
|
|
5246
|
+
HostName github.com
|
|
5247
|
+
User git
|
|
5248
|
+
IdentityFile ${destKey}
|
|
5249
|
+
IdentitiesOnly yes
|
|
5250
|
+
`;
|
|
5251
|
+
if (dryRun) {
|
|
5252
|
+
logger.info(`[dryRun] would install deploy key ${keyPath} \u2192 ${destKey}`);
|
|
5253
|
+
return;
|
|
5254
|
+
}
|
|
5255
|
+
await mkdir7(sshDir, { recursive: true, mode: 448 });
|
|
5256
|
+
await copyFile(keyPath, destKey);
|
|
5257
|
+
await chmod(destKey, 384);
|
|
5258
|
+
let config2 = "";
|
|
5259
|
+
if (await pathExists5(configPath)) config2 = await readFile4(configPath, "utf8");
|
|
5260
|
+
if (!config2.includes("Host github.com")) {
|
|
5261
|
+
await writeFile6(configPath, `${config2.trimEnd()}
|
|
5262
|
+
${block}
|
|
5263
|
+
`, { mode: 384 });
|
|
5264
|
+
logger.info("updated ~/.ssh/config for github.com");
|
|
5265
|
+
}
|
|
5266
|
+
logger.info(`deploy key installed at ${destKey}`);
|
|
5267
|
+
}
|
|
5268
|
+
async function installLogrotate(service, options = {}) {
|
|
5269
|
+
const { dryRun = false, logger = console } = options;
|
|
5270
|
+
const conf = `/etc/logrotate.d/${service.name}`;
|
|
5271
|
+
const body = `${service.appsRoot}/logs/*.log {
|
|
5272
|
+
daily
|
|
5273
|
+
rotate 14
|
|
5274
|
+
compress
|
|
5275
|
+
delaycompress
|
|
5276
|
+
missingok
|
|
5277
|
+
notifempty
|
|
5278
|
+
copytruncate
|
|
5279
|
+
}
|
|
5280
|
+
`;
|
|
5281
|
+
if (dryRun) {
|
|
5282
|
+
logger.info(`[dryRun] would write ${conf}`);
|
|
5283
|
+
return;
|
|
5284
|
+
}
|
|
5285
|
+
const tmp = join10("/tmp", `${service.name}-logrotate.conf`);
|
|
5286
|
+
await writeFile6(tmp, body);
|
|
5287
|
+
await runShell(`sudo cp '${tmp}' '${conf}'`, { logger });
|
|
5288
|
+
logger.info(`logrotate config written: ${conf}`);
|
|
5289
|
+
}
|
|
5290
|
+
async function bootstrapHost(service, options = {}) {
|
|
5291
|
+
const { deployKey = service.deployKey, user = "ubuntu", dryRun = false, logger = console } = options;
|
|
5292
|
+
await ensurePm2Startup({ user, dryRun, logger });
|
|
5293
|
+
if (deployKey) await installDeployKey(deployKey, { dryRun, logger });
|
|
5294
|
+
await installLogrotate(service, { dryRun, logger });
|
|
5295
|
+
logger.info("bootstrap-host complete");
|
|
5296
|
+
}
|
|
5297
|
+
|
|
5298
|
+
// src/deploy/deploy-service.js
|
|
5299
|
+
async function deployService(service, options = {}) {
|
|
5300
|
+
const {
|
|
5301
|
+
dryRun = false,
|
|
5302
|
+
skipPull = false,
|
|
5303
|
+
skipTests = false,
|
|
5304
|
+
skipNginx = false,
|
|
5305
|
+
logger = console
|
|
5306
|
+
} = options;
|
|
5307
|
+
const paths = servicePaths(service);
|
|
5308
|
+
await initServiceStructure(service, { dryRun, logger });
|
|
5309
|
+
if (!skipPull) await pullRepo(service, { dryRun, logger });
|
|
5310
|
+
await syncEnv(service, { dryRun, logger });
|
|
5311
|
+
const { stamp, path: releasePath } = await createRelease(service, { dryRun, logger });
|
|
5312
|
+
await writeReleaseBuildInfo(service, releasePath, { stamp, dryRun, logger });
|
|
5313
|
+
await installDeps(service, releasePath, paths, { dryRun, logger });
|
|
5314
|
+
if (!skipTests) await runReleaseTests(service, releasePath, paths, { dryRun, logger });
|
|
5315
|
+
await activateRelease(releasePath, paths, { dryRun, logger });
|
|
5316
|
+
await pruneReleases(service, paths, { dryRun, logger });
|
|
5317
|
+
await reloadPm2(paths, { dryRun, logger });
|
|
5318
|
+
if (!skipNginx) await enableNginxUpstream(service, { dryRun, logger });
|
|
5319
|
+
const summary = `deploy complete stamp=${stamp} dryRun=${dryRun}`;
|
|
5320
|
+
logger.info(summary);
|
|
5321
|
+
if (!dryRun) await appendDeployLog(paths.deployLog, summary);
|
|
5322
|
+
return { stamp, releasePath };
|
|
5323
|
+
}
|
|
5324
|
+
|
|
5325
|
+
// src/deploy/provision-service.js
|
|
5326
|
+
import { access as access6 } from "fs/promises";
|
|
5327
|
+
async function pathExists6(path5) {
|
|
5328
|
+
try {
|
|
5329
|
+
await access6(path5);
|
|
5330
|
+
return true;
|
|
5331
|
+
} catch {
|
|
5332
|
+
return false;
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
async function provisionService(service, options = {}) {
|
|
5336
|
+
const {
|
|
5337
|
+
dryRun = false,
|
|
5338
|
+
deploy = true,
|
|
5339
|
+
skipBootstrap = true,
|
|
5340
|
+
deployKey,
|
|
5341
|
+
logger = console
|
|
5342
|
+
} = options;
|
|
5343
|
+
if (!skipBootstrap) {
|
|
5344
|
+
await bootstrapHost(service, { deployKey, dryRun, logger });
|
|
5345
|
+
}
|
|
5346
|
+
await initServiceStructure(service, { dryRun, logger });
|
|
5347
|
+
const paths = servicePaths(service);
|
|
5348
|
+
if (await pathExists6(paths.repo)) {
|
|
5349
|
+
logger.info(`repo exists at ${paths.repo} \u2014 pulling`);
|
|
5350
|
+
await pullRepo(service, { dryRun, logger });
|
|
5351
|
+
} else {
|
|
5352
|
+
await cloneRepo(service, { dryRun, logger });
|
|
5353
|
+
}
|
|
5354
|
+
await syncEnv(service, { dryRun, logger });
|
|
5355
|
+
if (deploy) {
|
|
5356
|
+
await deployService(service, { dryRun, logger, skipPull: true });
|
|
5357
|
+
} else {
|
|
5358
|
+
logger.info("provision complete (deploy skipped)");
|
|
5359
|
+
}
|
|
5360
|
+
}
|
|
5361
|
+
|
|
5362
|
+
// src/deploy/rollback-service.js
|
|
5363
|
+
import { readlink as readlink3 } from "fs/promises";
|
|
5364
|
+
async function rollbackService(service, options = {}) {
|
|
5365
|
+
const { release: targetName, dryRun = false, logger = console } = options;
|
|
5366
|
+
const paths = servicePaths(service);
|
|
5367
|
+
const releases = await listReleases(paths);
|
|
5368
|
+
if (releases.length === 0) throw new Error("No releases to roll back to");
|
|
5369
|
+
let activeName = null;
|
|
5370
|
+
try {
|
|
5371
|
+
const target = await readlink3(paths.current);
|
|
5372
|
+
activeName = target.split("/").pop();
|
|
5373
|
+
} catch {
|
|
5374
|
+
throw new Error("No active release (current symlink missing)");
|
|
5375
|
+
}
|
|
5376
|
+
let rollbackTarget;
|
|
5377
|
+
if (targetName) {
|
|
5378
|
+
rollbackTarget = releases.find((r) => r.name === targetName);
|
|
5379
|
+
if (!rollbackTarget) throw new Error(`Release not found: ${targetName}`);
|
|
5380
|
+
} else {
|
|
5381
|
+
rollbackTarget = releases.find((r) => r.name !== activeName);
|
|
5382
|
+
if (!rollbackTarget) throw new Error("No previous release to roll back to");
|
|
5383
|
+
}
|
|
5384
|
+
if (rollbackTarget.name === activeName) throw new Error(`Already on release ${activeName}`);
|
|
5385
|
+
logger.info(`rollback ${activeName} \u2192 ${rollbackTarget.name}`);
|
|
5386
|
+
const buildInfo = await readReleaseBuildInfo(service, rollbackTarget.path);
|
|
5387
|
+
if (buildInfo?.version) {
|
|
5388
|
+
logger.info(`rollback target: v${buildInfo.version} release=${buildInfo.release ?? rollbackTarget.name}`);
|
|
5389
|
+
}
|
|
5390
|
+
await activateRelease(rollbackTarget.path, paths, { dryRun, logger });
|
|
5391
|
+
await reloadPm2(paths, { dryRun, logger });
|
|
5392
|
+
const summary = `rollback ${activeName} \u2192 ${rollbackTarget.name} dryRun=${dryRun}`;
|
|
5393
|
+
if (!dryRun) await appendDeployLog(paths.deployLog, summary);
|
|
5394
|
+
return { from: activeName, to: rollbackTarget.name, path: rollbackTarget.path };
|
|
5395
|
+
}
|
|
5396
|
+
|
|
5397
|
+
// src/deploy/ssh-remote.js
|
|
5398
|
+
import { access as access7, readFile as readFile5, writeFile as writeFile7 } from "fs/promises";
|
|
5399
|
+
import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
|
|
5400
|
+
import { basename as basename3, dirname as dirname4, join as join11 } from "path";
|
|
5401
|
+
import { spawn as spawn2 } from "child_process";
|
|
5402
|
+
var REMOTE_CLI_REL = "node_modules/@nmakarov/cli-toolkit/scripts/deploy/cli.js";
|
|
5403
|
+
async function pathExists7(path5) {
|
|
5404
|
+
try {
|
|
5405
|
+
await access7(path5);
|
|
5406
|
+
return true;
|
|
5407
|
+
} catch {
|
|
5408
|
+
return false;
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
function expandHome2(path5) {
|
|
5412
|
+
return path5.startsWith("~/") ? join11(homedir2(), path5.slice(2)) : path5;
|
|
5413
|
+
}
|
|
5414
|
+
function resolveLocalEnvPath(envFile) {
|
|
5415
|
+
if (envFile) {
|
|
5416
|
+
const expanded = expandHome2(envFile);
|
|
5417
|
+
return expanded.startsWith("/") ? expanded : join11(process.cwd(), expanded);
|
|
5418
|
+
}
|
|
5419
|
+
return join11(process.cwd(), ".env");
|
|
5420
|
+
}
|
|
5421
|
+
function parseGitHost(repoUrl) {
|
|
5422
|
+
const u = String(repoUrl ?? "");
|
|
5423
|
+
let m = u.match(/^[^@]+@([^:]+):/);
|
|
5424
|
+
if (m) return m[1];
|
|
5425
|
+
m = u.match(/^ssh:\/\/[^@]+@([^/:]+)/);
|
|
5426
|
+
if (m) return m[1];
|
|
5427
|
+
return null;
|
|
5428
|
+
}
|
|
5429
|
+
function shellQuote(value) {
|
|
5430
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
5431
|
+
}
|
|
5432
|
+
function sshRun(host, remoteCommand, options = {}) {
|
|
5433
|
+
const { logger } = options;
|
|
5434
|
+
return new Promise((resolve3, reject) => {
|
|
5435
|
+
logger?.info?.(`ssh ${host} ${remoteCommand.slice(0, 120)}${remoteCommand.length > 120 ? "\u2026" : ""}`);
|
|
5436
|
+
const child = spawn2("ssh", [host, remoteCommand], { stdio: "inherit" });
|
|
5437
|
+
child.on("error", reject);
|
|
5438
|
+
child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`ssh ${host} exited with code ${code}`)));
|
|
5439
|
+
});
|
|
5440
|
+
}
|
|
5441
|
+
function scp(localPath, remoteSpec) {
|
|
5442
|
+
return new Promise((resolve3, reject) => {
|
|
5443
|
+
const child = spawn2("scp", [localPath, remoteSpec], { stdio: "inherit" });
|
|
5444
|
+
child.on("error", reject);
|
|
5445
|
+
child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`scp exited with code ${code}`)));
|
|
5446
|
+
});
|
|
5447
|
+
}
|
|
5448
|
+
async function ensureDeployKeyOnRemote(host, deployKeyPath, options = {}) {
|
|
5449
|
+
const { logger = console } = options;
|
|
5450
|
+
if (!deployKeyPath) return false;
|
|
5451
|
+
const localPath = expandHome2(deployKeyPath);
|
|
5452
|
+
if (!await pathExists7(localPath)) return false;
|
|
5453
|
+
const keyBase = basename3(localPath);
|
|
5454
|
+
logger.info(`copying deploy key ${localPath} \u2192 ${host}:~/.ssh/${keyBase}`);
|
|
5455
|
+
await sshRun(host, "mkdir -p ~/.ssh && chmod 700 ~/.ssh", { logger });
|
|
5456
|
+
await scp(localPath, `${host}:.ssh/${keyBase}`, { logger });
|
|
5457
|
+
await sshRun(host, `chmod 600 ~/.ssh/${keyBase}`, { logger });
|
|
5458
|
+
return true;
|
|
5459
|
+
}
|
|
5460
|
+
async function prepareGitHost(host, options = {}) {
|
|
5461
|
+
const { logger = console, gitHost, keyBasename } = options;
|
|
5462
|
+
if (!gitHost) return;
|
|
5463
|
+
const configBlock = keyBasename ? `if ! grep -q 'Host ${gitHost}' ~/.ssh/config 2>/dev/null; then
|
|
5464
|
+
printf '%s\\n' '' 'Host ${gitHost}' ' HostName ${gitHost}' ' User git' ' IdentityFile ~/.ssh/${keyBasename}' ' IdentitiesOnly yes' >> ~/.ssh/config
|
|
5465
|
+
chmod 600 ~/.ssh/config
|
|
5466
|
+
echo "configured ~/.ssh/config for ${gitHost}"
|
|
5467
|
+
fi` : `:`;
|
|
5468
|
+
const script = `
|
|
5469
|
+
set -euo pipefail
|
|
5470
|
+
mkdir -p ~/.ssh
|
|
5471
|
+
chmod 700 ~/.ssh
|
|
5472
|
+
if ! grep -q '^${gitHost}' ~/.ssh/known_hosts 2>/dev/null; then
|
|
5473
|
+
ssh-keyscan -t ed25519,rsa ${gitHost} >> ~/.ssh/known_hosts 2>/dev/null
|
|
5474
|
+
echo "added ${gitHost} to known_hosts"
|
|
5475
|
+
fi
|
|
5476
|
+
${configBlock}
|
|
5477
|
+
`.trim();
|
|
5478
|
+
await sshRun(host, script, { logger });
|
|
5479
|
+
}
|
|
5480
|
+
async function ensureRepoDependencies(host, service, options = {}) {
|
|
5481
|
+
const { logger = console } = options;
|
|
5482
|
+
const paths = servicePaths(service);
|
|
5483
|
+
const run2 = shellQuote(paths.repoRun);
|
|
5484
|
+
await sshRun(
|
|
5485
|
+
host,
|
|
5486
|
+
`cd ${run2} && if [ ! -d node_modules/@nmakarov/cli-toolkit ] || [ package-lock.json -nt node_modules/.package-lock.json ]; then npm ci; fi`,
|
|
5487
|
+
{ logger }
|
|
5488
|
+
);
|
|
5489
|
+
}
|
|
5490
|
+
async function ensureEnvOnRemote(host, service, options = {}) {
|
|
5491
|
+
const { logger = console, envFile } = options;
|
|
5492
|
+
const paths = servicePaths(service);
|
|
5493
|
+
const localPath = resolveLocalEnvPath(envFile);
|
|
5494
|
+
if (!await pathExists7(localPath)) return false;
|
|
5495
|
+
logger.info(`copying .env ${localPath} \u2192 ${host}:${paths.repoEnv}`);
|
|
5496
|
+
await sshRun(host, `mkdir -p ${shellQuote(dirname4(paths.repoEnv))}`, { logger });
|
|
5497
|
+
const scrubbed = scrubEnvContent(await readFile5(localPath, "utf8"), service.envScrubPatterns ?? []);
|
|
5498
|
+
const tmp = join11(tmpdir2(), `deploy-env-${Date.now()}`);
|
|
5499
|
+
await writeFile7(tmp, scrubbed, { mode: 384 });
|
|
5500
|
+
await scp(tmp, `${host}:${paths.repoEnv}`, { logger });
|
|
5501
|
+
await sshRun(host, `chmod 600 ${shellQuote(paths.repoEnv)}`, { logger });
|
|
5502
|
+
return true;
|
|
5503
|
+
}
|
|
5504
|
+
async function ensureRemoteRepo(host, service, options = {}) {
|
|
5505
|
+
const { logger = console, deployKey = service.deployKey, envFile } = options;
|
|
5506
|
+
const paths = servicePaths(service);
|
|
5507
|
+
const repo = shellQuote(paths.repo);
|
|
5508
|
+
const repoUrl = shellQuote(service.repoUrl);
|
|
5509
|
+
const root = shellQuote(paths.root);
|
|
5510
|
+
const localKeyBase = deployKey ? basename3(expandHome2(deployKey)) : null;
|
|
5511
|
+
await ensureDeployKeyOnRemote(host, deployKey, { logger });
|
|
5512
|
+
await prepareGitHost(host, { logger, gitHost: parseGitHost(service.repoUrl), keyBasename: localKeyBase });
|
|
5513
|
+
await sshRun(
|
|
5514
|
+
host,
|
|
5515
|
+
`mkdir -p ${root} && if [ -d ${repo}/.git ]; then git -C ${repo} pull --ff-only; else git clone ${repoUrl} ${repo}; fi`,
|
|
5516
|
+
{ logger }
|
|
5517
|
+
);
|
|
5518
|
+
await ensureRepoDependencies(host, service, { logger });
|
|
5519
|
+
await ensureEnvOnRemote(host, service, { logger, envFile });
|
|
5520
|
+
}
|
|
5521
|
+
async function runRemoteCli(host, service, command, args = [], options = {}) {
|
|
5522
|
+
const { logger = console, manifests, skipPull = false, deployKey = service.deployKey, envFile } = options;
|
|
5523
|
+
const paths = servicePaths(service);
|
|
5524
|
+
const run2 = shellQuote(paths.repoRun);
|
|
5525
|
+
const cli = shellQuote(REMOTE_CLI_REL);
|
|
5526
|
+
if (!skipPull) {
|
|
5527
|
+
await ensureRemoteRepo(host, service, { logger, deployKey, envFile });
|
|
5528
|
+
} else {
|
|
5529
|
+
await ensureRepoDependencies(host, service, { logger });
|
|
5530
|
+
await ensureEnvOnRemote(host, service, { logger, envFile });
|
|
5531
|
+
}
|
|
5532
|
+
const passthrough = [
|
|
5533
|
+
`--service=${service.name}`,
|
|
5534
|
+
...manifests ? [`--manifests=${manifests}`] : [],
|
|
5535
|
+
...args
|
|
5536
|
+
].map(shellQuote).join(" ");
|
|
5537
|
+
await sshRun(host, `cd ${run2} && node ${cli} ${shellQuote(command)} ${passthrough}`.trim(), { logger });
|
|
5538
|
+
}
|
|
5539
|
+
async function runRemoteStatus(host, service, options = {}) {
|
|
5540
|
+
const { logger = console } = options;
|
|
5541
|
+
const paths = servicePaths(service);
|
|
5542
|
+
const port = service.pm2.port;
|
|
5543
|
+
const app = service.pm2.appName;
|
|
5544
|
+
const script = `
|
|
5545
|
+
echo "=== apps root ==="
|
|
5546
|
+
ls -la ${shellQuote(paths.root)} 2>/dev/null || echo "(missing)"
|
|
5547
|
+
echo ""
|
|
5548
|
+
echo "=== current ==="
|
|
5549
|
+
readlink ${shellQuote(paths.current)} 2>/dev/null || echo "(not set)"
|
|
5550
|
+
echo ""
|
|
5551
|
+
echo "=== releases ==="
|
|
5552
|
+
ls -1 ${shellQuote(paths.releases)} 2>/dev/null || echo "(none)"
|
|
5553
|
+
echo ""
|
|
5554
|
+
echo "=== pm2 ==="
|
|
5555
|
+
pm2 describe ${app} 2>/dev/null | head -20 || pm2 status ${app} 2>/dev/null || echo "(not running)"
|
|
5556
|
+
${port ? `echo ""
|
|
5557
|
+
echo "=== app health (localhost) ==="
|
|
5558
|
+
curl -sf http://127.0.0.1:${port}/healthz 2>/dev/null || echo "(no /healthz on :${port})"` : ""}
|
|
5559
|
+
`.trim();
|
|
5560
|
+
await sshRun(host, script, { logger });
|
|
5561
|
+
}
|
|
5562
|
+
|
|
5563
|
+
// src/deploy/manifests.js
|
|
5564
|
+
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
5565
|
+
import { pathToFileURL } from "url";
|
|
5566
|
+
async function loadServices({ manifests = "deploy/services.js", cwd = process.cwd() } = {}) {
|
|
5567
|
+
const abs = isAbsolute2(manifests) ? manifests : resolve2(cwd, manifests);
|
|
5568
|
+
let mod;
|
|
5569
|
+
try {
|
|
5570
|
+
mod = await import(pathToFileURL(abs).href);
|
|
5571
|
+
} catch (err) {
|
|
5572
|
+
throw new Error(`Could not load deploy manifests from ${abs}: ${err.message}`);
|
|
5573
|
+
}
|
|
5574
|
+
const raw = mod.services ?? mod.default;
|
|
5575
|
+
if (!raw) {
|
|
5576
|
+
throw new Error(`Manifests module ${abs} must export \`services\` (or default): a map or array of service manifests`);
|
|
5577
|
+
}
|
|
5578
|
+
const list = Array.isArray(raw) ? raw : Object.values(raw);
|
|
5579
|
+
const out = {};
|
|
5580
|
+
for (const entry of list) {
|
|
5581
|
+
const svc = defineService(entry);
|
|
5582
|
+
out[svc.name] = svc;
|
|
5583
|
+
}
|
|
5584
|
+
return out;
|
|
5585
|
+
}
|
|
5586
|
+
function resolveServiceFrom(serviceMap, name, { appsRoot } = {}) {
|
|
5587
|
+
const svc = serviceMap[name];
|
|
5588
|
+
if (!svc) {
|
|
5589
|
+
throw new Error(`Unknown service "${name}". Known: ${Object.keys(serviceMap).join(", ") || "(none)"}`);
|
|
5590
|
+
}
|
|
5591
|
+
return appsRoot ? { ...svc, appsRoot } : svc;
|
|
5592
|
+
}
|
|
5593
|
+
|
|
4333
5594
|
// src/tasks/index.js
|
|
4334
5595
|
import os3 from "os";
|
|
4335
5596
|
|
|
4336
5597
|
// src/utils/core-utils.js
|
|
4337
5598
|
function sleepMs(ms) {
|
|
4338
|
-
return new Promise((
|
|
5599
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4339
5600
|
}
|
|
4340
5601
|
function toJsonColumn(value) {
|
|
4341
5602
|
if (value === void 0 || value === null) return null;
|
|
@@ -4443,7 +5704,7 @@ function defineTasksTable(t, db, tableNameForIndex) {
|
|
|
4443
5704
|
t.timestamp("past_due").defaultTo(null);
|
|
4444
5705
|
t.text("name").notNullable();
|
|
4445
5706
|
t.text("opid");
|
|
4446
|
-
t.
|
|
5707
|
+
t.jsonb("params");
|
|
4447
5708
|
t.text("service_group");
|
|
4448
5709
|
t.integer("instance_number");
|
|
4449
5710
|
t.text("service_name");
|
|
@@ -4452,7 +5713,7 @@ function defineTasksTable(t, db, tableNameForIndex) {
|
|
|
4452
5713
|
t.timestamp("status_changed_at").defaultTo(null);
|
|
4453
5714
|
t.text("progress");
|
|
4454
5715
|
t.boolean("success");
|
|
4455
|
-
t.
|
|
5716
|
+
t.jsonb("results");
|
|
4456
5717
|
t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
|
|
4457
5718
|
t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
|
|
4458
5719
|
}
|
|
@@ -4467,11 +5728,29 @@ function taskHistoryInsertFromQueueRow(row, overrides) {
|
|
|
4467
5728
|
async function ensureTaskTables(context, options = {}) {
|
|
4468
5729
|
const queueName = options.queueName ?? "tasks";
|
|
4469
5730
|
const recreate = options.recreate ?? false;
|
|
5731
|
+
const dryRun = options.dryRun ?? false;
|
|
4470
5732
|
const db = getDb(context);
|
|
5733
|
+
const log = context.logger ?? console;
|
|
4471
5734
|
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
4472
5735
|
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
4473
5736
|
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
4474
5737
|
const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
|
|
5738
|
+
if (dryRun) {
|
|
5739
|
+
const plan = [];
|
|
5740
|
+
if (recreate) {
|
|
5741
|
+
plan.push(`DROP TABLE IF EXISTS ${historyTable}, ${tasksTable}, ${registryTable}`);
|
|
5742
|
+
}
|
|
5743
|
+
if (needsTasks) plan.push(`CREATE TABLE ${tasksTable} (tasks queue)`);
|
|
5744
|
+
if (needsHistory) plan.push(`CREATE TABLE ${historyTable} (history mirror)`);
|
|
5745
|
+
if (needsRegistry) plan.push(`CREATE TABLE ${registryTable} (services registry)`);
|
|
5746
|
+
if (plan.length === 0) {
|
|
5747
|
+
log.info?.(`[tasks-schema] dryRun \u2014 queue "${queueName}" already up to date; no DDL`);
|
|
5748
|
+
} else {
|
|
5749
|
+
log.info?.(`[tasks-schema] dryRun \u2014 would run ${plan.length} statement(s) for queue "${queueName}":`);
|
|
5750
|
+
for (const s of plan) log.info?.(` - ${s}`);
|
|
5751
|
+
}
|
|
5752
|
+
return;
|
|
5753
|
+
}
|
|
4475
5754
|
if (recreate) {
|
|
4476
5755
|
await db.schema.dropTableIfExists(historyTable);
|
|
4477
5756
|
await db.schema.dropTableIfExists(tasksTable);
|
|
@@ -5376,10 +6655,10 @@ var TaskSampleProcess = class extends AbstractTask {
|
|
|
5376
6655
|
};
|
|
5377
6656
|
|
|
5378
6657
|
// src/tasks/coreTasks/TaskShellCommand.js
|
|
5379
|
-
import { spawn } from "child_process";
|
|
6658
|
+
import { spawn as spawn3 } from "child_process";
|
|
5380
6659
|
function runShellCommand(command, cwd) {
|
|
5381
|
-
return new Promise((
|
|
5382
|
-
const child =
|
|
6660
|
+
return new Promise((resolve3, reject) => {
|
|
6661
|
+
const child = spawn3(command, {
|
|
5383
6662
|
shell: true,
|
|
5384
6663
|
cwd: cwd || process.cwd(),
|
|
5385
6664
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -5396,7 +6675,7 @@ function runShellCommand(command, cwd) {
|
|
|
5396
6675
|
reject(error);
|
|
5397
6676
|
});
|
|
5398
6677
|
child.on("close", (exitCode, signal) => {
|
|
5399
|
-
|
|
6678
|
+
resolve3({
|
|
5400
6679
|
exitCode,
|
|
5401
6680
|
output: output.trim(),
|
|
5402
6681
|
stderr: stderr.trim(),
|
|
@@ -5868,7 +7147,7 @@ function mergeAllowedTasksWithServiceTasks(names) {
|
|
|
5868
7147
|
}
|
|
5869
7148
|
|
|
5870
7149
|
// src/tasks/taskScriptRunner.js
|
|
5871
|
-
import { spawn as
|
|
7150
|
+
import { spawn as spawn4 } from "child_process";
|
|
5872
7151
|
var MAX_PROGRESS_TEXT_LEN = 4e3;
|
|
5873
7152
|
function toCliArgs(args = []) {
|
|
5874
7153
|
return args.filter((a) => typeof a === "string" && a.length > 0);
|
|
@@ -5914,8 +7193,7 @@ function forwardChildLogToParent(context, prefix, message) {
|
|
|
5914
7193
|
}
|
|
5915
7194
|
function buildNodeArgs(scriptPath, cliArgs) {
|
|
5916
7195
|
const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
|
|
5917
|
-
|
|
5918
|
-
return hasTsRuntimeInParent ? [...inheritedExecArgs, scriptPath, ...cliArgs] : ["--import", "tsx", scriptPath, ...cliArgs];
|
|
7196
|
+
return [...inheritedExecArgs, scriptPath, ...cliArgs];
|
|
5919
7197
|
}
|
|
5920
7198
|
function resolveTasksTableName(context) {
|
|
5921
7199
|
return context.tasksQueueName || context.params?.get?.("table") || "tasks";
|
|
@@ -5947,7 +7225,7 @@ function createSerializedQueue() {
|
|
|
5947
7225
|
async function runNodeTaskScript(context, options) {
|
|
5948
7226
|
const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
|
|
5949
7227
|
const nodeArgs = buildNodeArgs(options.scriptPath, cliArgs);
|
|
5950
|
-
const child =
|
|
7228
|
+
const child = spawn4(process.execPath, nodeArgs, {
|
|
5951
7229
|
cwd: options.cwd || process.cwd(),
|
|
5952
7230
|
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
5953
7231
|
env: {
|
|
@@ -6034,13 +7312,13 @@ async function runNodeTaskScript(context, options) {
|
|
|
6034
7312
|
}
|
|
6035
7313
|
forwardChildLogToParent(context, prefix, message);
|
|
6036
7314
|
});
|
|
6037
|
-
return await new Promise((
|
|
7315
|
+
return await new Promise((resolve3, reject) => {
|
|
6038
7316
|
child.on("error", (error) => reject(error));
|
|
6039
7317
|
child.on("close", (exitCode, signal) => {
|
|
6040
7318
|
void (async () => {
|
|
6041
7319
|
await flushTaskIpcLogs(context);
|
|
6042
7320
|
await progressQueue.drain();
|
|
6043
|
-
|
|
7321
|
+
resolve3({
|
|
6044
7322
|
exitCode,
|
|
6045
7323
|
signal,
|
|
6046
7324
|
stdout: state.stdout.trim(),
|
|
@@ -6155,9 +7433,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
6155
7433
|
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
6156
7434
|
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
6157
7435
|
const fallbackRecoverCommand = [
|
|
6158
|
-
"
|
|
6159
|
-
"
|
|
6160
|
-
"examples/tasks/recover-task.ts",
|
|
7436
|
+
"node",
|
|
7437
|
+
"examples/tasks/recover-task.js",
|
|
6161
7438
|
`--dbName='${dbName.replace(/'/g, `'\\''`)}'`,
|
|
6162
7439
|
`--table='${tableName.replace(/'/g, `'\\''`)}'`,
|
|
6163
7440
|
`--id='${String(row.id).replace(/'/g, `'\\''`)}'`
|
|
@@ -6223,6 +7500,8 @@ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry
|
|
|
6223
7500
|
const db = getDb3(context);
|
|
6224
7501
|
let query = db(tasksTable).where({ status: "idle" }).where(function() {
|
|
6225
7502
|
this.whereNull("service_group").orWhere({ service_group: serviceGroup });
|
|
7503
|
+
}).where(function() {
|
|
7504
|
+
this.whereNull("next_run_at").orWhere("next_run_at", "<=", db.fn.now());
|
|
6226
7505
|
}).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);
|
|
6227
7506
|
if (taskNames && taskNames.length > 0) {
|
|
6228
7507
|
query = query.whereIn("name", taskNames);
|
|
@@ -6605,6 +7884,7 @@ var TasksManager = class _TasksManager {
|
|
|
6605
7884
|
export {
|
|
6606
7885
|
AbstractTask,
|
|
6607
7886
|
Args,
|
|
7887
|
+
Aws,
|
|
6608
7888
|
Box4 as Box,
|
|
6609
7889
|
Db,
|
|
6610
7890
|
Divider,
|
|
@@ -6618,6 +7898,7 @@ export {
|
|
|
6618
7898
|
MultiColumnListComponent,
|
|
6619
7899
|
MultiColumnListWithPreviewComponent,
|
|
6620
7900
|
Params,
|
|
7901
|
+
REMOTE_CLI_REL,
|
|
6621
7902
|
React2 as React,
|
|
6622
7903
|
S3,
|
|
6623
7904
|
SERVICE_TASK_NAMES,
|
|
@@ -6638,51 +7919,94 @@ export {
|
|
|
6638
7919
|
TasksRegistry,
|
|
6639
7920
|
Text5 as Text,
|
|
6640
7921
|
TextBlock,
|
|
7922
|
+
activateRelease,
|
|
7923
|
+
appendDeployLog,
|
|
6641
7924
|
appendTaskIpcLog,
|
|
7925
|
+
bootstrapHost,
|
|
6642
7926
|
buildBreadcrumb,
|
|
6643
7927
|
buildDetailBreadcrumb,
|
|
6644
7928
|
buildFooter,
|
|
7929
|
+
bumpPatchVersion,
|
|
7930
|
+
cloneRepo,
|
|
6645
7931
|
convertPattern,
|
|
7932
|
+
createRelease,
|
|
6646
7933
|
defaultFileSynopsisFunction,
|
|
6647
7934
|
defaultTasksRegistry,
|
|
6648
7935
|
defaultVersionSynopsisFunction,
|
|
7936
|
+
defineService,
|
|
7937
|
+
deployService,
|
|
7938
|
+
deriveRepoDirName,
|
|
7939
|
+
enableNginxUpstream,
|
|
6649
7940
|
enqueueStopTask,
|
|
6650
7941
|
enqueueTask,
|
|
7942
|
+
ensureDeployKeyOnRemote,
|
|
7943
|
+
ensureEnvOnRemote,
|
|
7944
|
+
ensureRemoteRepo,
|
|
7945
|
+
ensureRepoDependencies,
|
|
6651
7946
|
ensureTaskTables,
|
|
6652
7947
|
flushTaskIpcLogs,
|
|
6653
7948
|
getArgsInstance,
|
|
6654
7949
|
createElement2 as h,
|
|
7950
|
+
initServiceStructure,
|
|
7951
|
+
installDeps,
|
|
6655
7952
|
ipcFileLogsTableNameForSourceResource,
|
|
6656
7953
|
joiEdateType,
|
|
6657
7954
|
joiStringArrayType,
|
|
6658
7955
|
listServicesRegistry as listAliveRunnerHeartbeats,
|
|
7956
|
+
listReleases,
|
|
6659
7957
|
listServicesRegistry,
|
|
6660
7958
|
listSources,
|
|
6661
7959
|
listTables,
|
|
6662
7960
|
load,
|
|
7961
|
+
loadServices,
|
|
6663
7962
|
matchesParsedPattern,
|
|
6664
7963
|
memo,
|
|
6665
7964
|
mergeAllowedTasksWithServiceTasks,
|
|
6666
7965
|
nextTimeMatch,
|
|
6667
7966
|
normalizeAllowedTasks,
|
|
7967
|
+
npmEnv,
|
|
7968
|
+
npmInstallEnv,
|
|
6668
7969
|
organizeFooterMessages,
|
|
7970
|
+
parseGitHost,
|
|
7971
|
+
prepareGitHost,
|
|
7972
|
+
provisionService,
|
|
7973
|
+
pruneReleases,
|
|
7974
|
+
pullRepo,
|
|
6669
7975
|
queueToTableNames,
|
|
7976
|
+
readCurrentRelease,
|
|
7977
|
+
readReleaseBuildInfo,
|
|
6670
7978
|
readTaskIpcLogsSnapshot,
|
|
6671
7979
|
registerInServicesRegistry,
|
|
6672
7980
|
registerInServicesRegistry as registerRunnerHeartbeat,
|
|
7981
|
+
releaseDir,
|
|
7982
|
+
releaseStamp,
|
|
7983
|
+
reloadPm2,
|
|
6673
7984
|
resolveAsterisks,
|
|
6674
7985
|
resolveIpcFileLogsDir,
|
|
7986
|
+
resolveNextVersion,
|
|
6675
7987
|
resolveRanges,
|
|
7988
|
+
resolveServiceFrom,
|
|
6676
7989
|
resolveSteps,
|
|
7990
|
+
rollbackService,
|
|
7991
|
+
run,
|
|
6677
7992
|
runNodeTaskScript,
|
|
7993
|
+
runReleaseTests,
|
|
7994
|
+
runRemoteCli,
|
|
7995
|
+
runRemoteStatus,
|
|
7996
|
+
runShell,
|
|
6678
7997
|
runTasksLoop,
|
|
7998
|
+
scrubEnvContent,
|
|
7999
|
+
servicePaths,
|
|
6679
8000
|
setupContext,
|
|
8001
|
+
shellQuote,
|
|
6680
8002
|
showListScreen,
|
|
6681
8003
|
showMenuScreen,
|
|
6682
8004
|
showMultiColumnListScreen,
|
|
6683
8005
|
showMultiColumnListWithPreviewScreen,
|
|
6684
8006
|
showScreen,
|
|
6685
8007
|
showWordGridScreen,
|
|
8008
|
+
sshRun,
|
|
8009
|
+
syncEnv,
|
|
6686
8010
|
taskHistoryInsertFromQueueRow,
|
|
6687
8011
|
timeMatcher,
|
|
6688
8012
|
touchServicesRegistry as touchRunnerHeartbeat,
|
|
@@ -6698,6 +8022,7 @@ export {
|
|
|
6698
8022
|
useMemo,
|
|
6699
8023
|
useRef2 as useRef,
|
|
6700
8024
|
useState3 as useState,
|
|
6701
|
-
waitForTaskResult
|
|
8025
|
+
waitForTaskResult,
|
|
8026
|
+
writeReleaseBuildInfo
|
|
6702
8027
|
};
|
|
6703
8028
|
//# sourceMappingURL=index.js.map
|