@stacksjs/ts-cloud 0.5.1 → 0.5.3
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/dist/bin/cli.js +712 -707
- package/dist/deploy/serverless-app.d.ts +7 -0
- package/dist/drivers/shared/compute-ops.d.ts +59 -0
- package/dist/drivers/shared/nginx-vhost.d.ts +16 -1
- package/dist/drivers/shared/php-provision.d.ts +26 -2
- package/dist/drivers/shared/releases.d.ts +28 -0
- package/dist/index.js +237 -20
- package/package.json +3 -3
|
@@ -51,3 +51,10 @@ export declare function rollbackServerlessApp(config: CloudConfig, environment:
|
|
|
51
51
|
export declare function setMaintenance(config: CloudConfig, environment: EnvironmentType, enabled: boolean, bypassSecret?: string): Promise<void>;
|
|
52
52
|
/** Invoke the CLI function with an arbitrary command (e.g. `cloud command "migrate"`). */
|
|
53
53
|
export declare function runRemoteCommand(config: CloudConfig, environment: EnvironmentType, command: string): Promise<string>;
|
|
54
|
+
/**
|
|
55
|
+
* Run a SQL statement against a (private, in-VPC) serverless database via the CLI
|
|
56
|
+
* function — no bastion needed. Requires the `tscloud/serverless` PHP bridge
|
|
57
|
+
* (`tscloud:db-query`). The SQL is base64-encoded so it survives the runtime's
|
|
58
|
+
* whitespace argument parsing.
|
|
59
|
+
*/
|
|
60
|
+
export declare function runDbQuery(config: CloudConfig, environment: EnvironmentType, sql: string): Promise<string>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forge-style operational commands for a provisioned compute fleet, run over
|
|
3
|
+
* the active driver (SSH/SSM): roll a site back to a previous release, read a
|
|
4
|
+
* site's deployment history, and run a reusable server recipe across servers.
|
|
5
|
+
*
|
|
6
|
+
* Each finds the project's targets via the driver, builds a small shell script
|
|
7
|
+
* with the shared generators, and runs it on every box — mirroring how
|
|
8
|
+
* {@link import('./compute-deploy').deploySiteRelease} drives a deploy.
|
|
9
|
+
*/
|
|
10
|
+
import type { CloudDriver, EnvironmentType, RemoteDeployInstanceResult } from '@ts-cloud/core';
|
|
11
|
+
export interface ComputeOpsLogger {
|
|
12
|
+
info(message: string): void;
|
|
13
|
+
warn(message: string): void;
|
|
14
|
+
error(message: string): void;
|
|
15
|
+
step(message: string): void;
|
|
16
|
+
success(message: string): void;
|
|
17
|
+
}
|
|
18
|
+
export interface ComputeOpsContext {
|
|
19
|
+
driver: CloudDriver;
|
|
20
|
+
slug: string;
|
|
21
|
+
environment: EnvironmentType;
|
|
22
|
+
/** Target role label. @default 'app' */
|
|
23
|
+
role?: string;
|
|
24
|
+
logger?: ComputeOpsLogger;
|
|
25
|
+
}
|
|
26
|
+
export interface ComputeOpsResult {
|
|
27
|
+
success: boolean;
|
|
28
|
+
error?: string;
|
|
29
|
+
perInstance?: RemoteDeployInstanceResult[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Roll a site back to a previous release (Forge-style). With `to`, points
|
|
33
|
+
* `current` at `releases/<to>`; otherwise the most recent prior release. After
|
|
34
|
+
* flipping the symlink, php-fpm is restarted and queue workers are signalled so
|
|
35
|
+
* they pick up the rolled-back code.
|
|
36
|
+
*/
|
|
37
|
+
export declare function rollbackComputeSite(ctx: ComputeOpsContext, options: {
|
|
38
|
+
siteName: string;
|
|
39
|
+
to?: string;
|
|
40
|
+
}): Promise<ComputeOpsResult>;
|
|
41
|
+
/**
|
|
42
|
+
* Read a site's on-box deployment history (the log written by
|
|
43
|
+
* {@link import('./releases').buildDeployHistoryHeader}). Returns the per-server
|
|
44
|
+
* output so the caller can print the most recent deploys.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getComputeDeployHistory(ctx: ComputeOpsContext, options: {
|
|
47
|
+
siteName: string;
|
|
48
|
+
limit?: number;
|
|
49
|
+
}): Promise<ComputeOpsResult>;
|
|
50
|
+
/**
|
|
51
|
+
* Run a reusable server recipe (a bash script) across the project's servers as
|
|
52
|
+
* a chosen user (Forge's Recipes). The recipe runs through a login shell so
|
|
53
|
+
* pantry's env is loaded; output is captured per server.
|
|
54
|
+
*/
|
|
55
|
+
export declare function runComputeRecipe(ctx: ComputeOpsContext, options: {
|
|
56
|
+
name: string;
|
|
57
|
+
script: string[];
|
|
58
|
+
user?: string;
|
|
59
|
+
}): Promise<ComputeOpsResult>;
|
|
@@ -13,8 +13,15 @@
|
|
|
13
13
|
* The block listens on :80 only; TLS (the `:443` block + redirect) is layered
|
|
14
14
|
* on by certbot in the SSL step, so this stays stable across cert renewals.
|
|
15
15
|
*/
|
|
16
|
-
import type { SiteConfig } from '@ts-cloud/core';
|
|
16
|
+
import type { SiteConfig, SiteNginxConfig } from '@ts-cloud/core';
|
|
17
17
|
export type NginxSiteType = NonNullable<SiteConfig['type']>;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the nginx directive lines for a site's vhost from its
|
|
20
|
+
* {@link SiteNginxConfig} and the server's reusable templates: the referenced
|
|
21
|
+
* template's lines first, then the per-site `serverSnippet`. Unknown template
|
|
22
|
+
* names resolve to nothing. Returns `[]` when there's no customization.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveNginxSnippet(nginx: SiteNginxConfig | undefined, templates: Record<string, string[]> | undefined): string[];
|
|
18
25
|
export interface NginxVhostOptions {
|
|
19
26
|
/** Site key — names the config file (`/etc/nginx/sites-available/<siteName>`). */
|
|
20
27
|
siteName: string;
|
|
@@ -56,6 +63,14 @@ export interface NginxVhostOptions {
|
|
|
56
63
|
password: string;
|
|
57
64
|
realm?: string;
|
|
58
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* Custom nginx directive lines injected into the server block (after the
|
|
68
|
+
* managed directives) — a resolved reusable template plus per-site snippet.
|
|
69
|
+
* See {@link import('@ts-cloud/core').SiteNginxConfig}.
|
|
70
|
+
*/
|
|
71
|
+
serverSnippet?: string[];
|
|
72
|
+
/** `client_max_body_size` override for this vhost (e.g. `'256M'`). */
|
|
73
|
+
clientMaxBodySize?: string;
|
|
59
74
|
}
|
|
60
75
|
/** Path of the htpasswd file for a site. */
|
|
61
76
|
export declare function htpasswdPath(siteName: string): string;
|
|
@@ -16,7 +16,21 @@ export interface PhpProvisionOptions {
|
|
|
16
16
|
installNginx?: boolean;
|
|
17
17
|
/** Install Composer. @default true */
|
|
18
18
|
installComposer?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Apply production OPcache + php.ini tuning (Forge's "Optimize for
|
|
21
|
+
* Production"). @default true
|
|
22
|
+
*/
|
|
23
|
+
optimizeForProduction?: boolean;
|
|
24
|
+
/** Extra `php.ini` directives merged on top of (or instead of) the tuning. */
|
|
25
|
+
ini?: Record<string, string>;
|
|
19
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The production OPcache + runtime tuning Forge applies via "Optimize for
|
|
29
|
+
* Production". Timestamp validation is off because every deploy restarts
|
|
30
|
+
* php-fpm (so a fresh release is always recompiled); the larger buffers and
|
|
31
|
+
* realpath cache cut per-request overhead.
|
|
32
|
+
*/
|
|
33
|
+
export declare const PRODUCTION_PHP_INI: Readonly<Record<string, string>>;
|
|
20
34
|
/** PHP-FPM listen address for nginx `fastcgi_pass` (pantry's php-fpm is TCP). */
|
|
21
35
|
export declare const PHP_FPM_LISTEN = "127.0.0.1:9074";
|
|
22
36
|
/**
|
|
@@ -27,10 +41,20 @@ export declare const PHP_FPM_LISTEN = "127.0.0.1:9074";
|
|
|
27
41
|
export declare function phpFpmSocketPath(_version?: string): string;
|
|
28
42
|
/** Resolve the default PHP version from the requested set. */
|
|
29
43
|
export declare function resolveDefaultPhpVersion(options?: PhpProvisionOptions): string;
|
|
44
|
+
/** Resolve the effective php.ini directives for the given options. */
|
|
45
|
+
export declare function resolvePhpIni(options?: PhpProvisionOptions): Record<string, string>;
|
|
46
|
+
/**
|
|
47
|
+
* Write a ts-cloud-managed `php.ini` drop-in with the resolved directives and
|
|
48
|
+
* restart php-fpm so it takes effect. PHP's ini layout is discovered at runtime
|
|
49
|
+
* via `php -i` (works regardless of pantry's compiled paths): the additional-ini
|
|
50
|
+
* scan dir is used when present, otherwise a managed marker block is merged into
|
|
51
|
+
* the loaded `php.ini`. Returns `[]` when there's nothing to set.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildPhpTuningScript(options?: PhpProvisionOptions): string[];
|
|
30
54
|
/**
|
|
31
55
|
* Build the shell command lines that install PHP-FPM + Composer (+ the nginx
|
|
32
|
-
* binary) via pantry
|
|
33
|
-
* CLI is already bootstrapped (see
|
|
56
|
+
* binary) via pantry, start php-fpm as a system service, and apply production
|
|
57
|
+
* php.ini tuning. Assumes the pantry CLI is already bootstrapped (see
|
|
34
58
|
* {@link import('./package-manager').buildPantryBootstrapScript}).
|
|
35
59
|
*/
|
|
36
60
|
export declare function buildPhpProvisionScript(options?: PhpProvisionOptions): string[];
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
export declare const DEFAULT_SHARED_PATHS: readonly string[];
|
|
17
17
|
/** Default number of past releases to retain for rollback. */
|
|
18
18
|
export declare const DEFAULT_KEEP_RELEASES = 4;
|
|
19
|
+
/** Number of per-deploy output logs to keep on the box. */
|
|
20
|
+
export declare const DEFAULT_KEEP_DEPLOY_LOGS = 20;
|
|
21
|
+
/** ts-cloud metadata dir for a site (deploy history + per-deploy logs). */
|
|
22
|
+
export declare function deployMetaDir(base: string): string;
|
|
23
|
+
/** Append-only deploy history log path for a site. */
|
|
24
|
+
export declare function deployHistoryPath(base: string): string;
|
|
25
|
+
/** Per-deploy output log path for a release. */
|
|
26
|
+
export declare function deployLogPath(base: string, releaseId: string): string;
|
|
19
27
|
export interface ReleasePaths {
|
|
20
28
|
/** Site base directory (`/var/www/<site>`). */
|
|
21
29
|
base: string;
|
|
@@ -61,3 +69,23 @@ export declare function buildRollbackScript(paths: ReleasePaths, options?: {
|
|
|
61
69
|
* at the newest, so it is never pruned.
|
|
62
70
|
*/
|
|
63
71
|
export declare function buildPruneReleases(paths: ReleasePaths, keep?: number): string[];
|
|
72
|
+
export interface DeployHistoryOptions {
|
|
73
|
+
/** This deploy's release id. */
|
|
74
|
+
releaseId: string;
|
|
75
|
+
/** Commit SHA being deployed (recorded in the history line). */
|
|
76
|
+
commit?: string;
|
|
77
|
+
/** Branch being deployed. */
|
|
78
|
+
branch?: string;
|
|
79
|
+
/** Per-deploy logs to retain. @default {@link DEFAULT_KEEP_DEPLOY_LOGS} */
|
|
80
|
+
keepLogs?: number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Header lines that record deployment history + capture per-deploy output
|
|
84
|
+
* (Forge's deployment log). Emitted near the top of the deploy script: it tees
|
|
85
|
+
* all stdout/stderr to `<base>/.ts-cloud/deploys/<releaseId>.log` and installs
|
|
86
|
+
* an EXIT trap that appends a `<ts>\t<releaseId>\t<commit>\t<status>` line to
|
|
87
|
+
* `<base>/.ts-cloud/deploy-history.log` — so both successful and failed deploys
|
|
88
|
+
* are recorded (the trap reads `$?`). Requires bash (the deploy script already
|
|
89
|
+
* uses `set -euo pipefail`).
|
|
90
|
+
*/
|
|
91
|
+
export declare function buildDeployHistoryHeader(base: string, options: DeployHistoryOptions): string[];
|
package/dist/index.js
CHANGED
|
@@ -25951,11 +25951,16 @@ function composeServerlessAppTemplate(opts) {
|
|
|
25951
25951
|
...hasQueue ? { TSCLOUD_QUEUE: queueNames[0] } : {},
|
|
25952
25952
|
...app.env ?? {}
|
|
25953
25953
|
});
|
|
25954
|
+
const efsEnabled = Boolean(app.efs);
|
|
25955
|
+
const efsOpts = typeof app.efs === "object" ? app.efs : {};
|
|
25956
|
+
const efsMountPath = efsOpts.mountPath ?? "/mnt/local";
|
|
25957
|
+
const efsProvision = efsEnabled && !efsOpts.accessPointArn;
|
|
25958
|
+
const efsAccessPoint = efsOpts.accessPointArn ?? (efsProvision ? Fn2.getAtt("EfsAccessPoint", "Arn") : undefined);
|
|
25954
25959
|
const subnets = app.vpc?.subnets ?? [];
|
|
25955
25960
|
const hasVpc = subnets.length > 0;
|
|
25956
|
-
const needsDataVpc = app.cache?.driver === "elasticache" || app.database?.connection === "aurora-serverless" || Boolean(app.rdsProxy);
|
|
25961
|
+
const needsDataVpc = app.cache?.driver === "elasticache" || app.database?.connection === "aurora-serverless" || Boolean(app.rdsProxy) || efsEnabled;
|
|
25957
25962
|
if (needsDataVpc && !hasVpc) {
|
|
25958
|
-
throw new Error("serverless app: elasticache / aurora-serverless / rdsProxy require app.vpc.subnets (private subnets) to be set.");
|
|
25963
|
+
throw new Error("serverless app: elasticache / aurora-serverless / rdsProxy / efs require app.vpc.subnets (private subnets) to be set.");
|
|
25959
25964
|
}
|
|
25960
25965
|
const vpcConfig = hasVpc ? {
|
|
25961
25966
|
VpcConfig: {
|
|
@@ -25966,6 +25971,15 @@ function composeServerlessAppTemplate(opts) {
|
|
|
25966
25971
|
]
|
|
25967
25972
|
}
|
|
25968
25973
|
} : {};
|
|
25974
|
+
const efsDependsOn = efsProvision ? subnets.map((_, i) => `EfsMountTarget${i}`) : [];
|
|
25975
|
+
const efsConfig = efsEnabled ? { FileSystemConfigs: [{ Arn: efsAccessPoint, LocalMountPath: efsMountPath }] } : {};
|
|
25976
|
+
if (efsEnabled) {
|
|
25977
|
+
inlinePolicies[0].PolicyDocument.Statement.push({
|
|
25978
|
+
Effect: "Allow",
|
|
25979
|
+
Action: ["elasticfilesystem:ClientMount", "elasticfilesystem:ClientWrite", "elasticfilesystem:ClientRootAccess", "elasticfilesystem:DescribeMountTargets"],
|
|
25980
|
+
Resource: "*"
|
|
25981
|
+
});
|
|
25982
|
+
}
|
|
25969
25983
|
function addFunction(logicalId, name, handler8, mode, memory, timeout, reservedConcurrency, tmp = tmpStorage) {
|
|
25970
25984
|
resources[`${logicalId}LogGroup`] = {
|
|
25971
25985
|
Type: "AWS::Logs::LogGroup",
|
|
@@ -25983,7 +25997,7 @@ function composeServerlessAppTemplate(opts) {
|
|
|
25983
25997
|
};
|
|
25984
25998
|
resources[logicalId] = {
|
|
25985
25999
|
Type: "AWS::Lambda::Function",
|
|
25986
|
-
DependsOn: [`${logicalId}LogGroup
|
|
26000
|
+
DependsOn: [`${logicalId}LogGroup`, ...efsDependsOn],
|
|
25987
26001
|
Properties: {
|
|
25988
26002
|
FunctionName: name,
|
|
25989
26003
|
Architectures: [architecture],
|
|
@@ -25994,7 +26008,8 @@ function composeServerlessAppTemplate(opts) {
|
|
|
25994
26008
|
EphemeralStorage: { Size: tmp },
|
|
25995
26009
|
...codeProps,
|
|
25996
26010
|
...reservedConcurrency !== undefined ? { ReservedConcurrentExecutions: reservedConcurrency } : {},
|
|
25997
|
-
...vpcConfig
|
|
26011
|
+
...vpcConfig,
|
|
26012
|
+
...efsConfig
|
|
25998
26013
|
}
|
|
25999
26014
|
};
|
|
26000
26015
|
}
|
|
@@ -26243,10 +26258,37 @@ function composeServerlessAppTemplate(opts) {
|
|
|
26243
26258
|
DomainName: Fn2.getAtt("AssetsBucket", "RegionalDomainName"),
|
|
26244
26259
|
OriginAccessControlId: Fn2.ref("AssetsOAC"),
|
|
26245
26260
|
S3OriginConfig: { OriginAccessIdentity: "" }
|
|
26246
|
-
}]
|
|
26261
|
+
}],
|
|
26262
|
+
...app.assetDomain ? {
|
|
26263
|
+
Aliases: [app.assetDomain],
|
|
26264
|
+
ViewerCertificate: {
|
|
26265
|
+
AcmCertificateArn: app.assetCertificateArn,
|
|
26266
|
+
SslSupportMethod: "sni-only",
|
|
26267
|
+
MinimumProtocolVersion: "TLSv1.2_2021"
|
|
26268
|
+
}
|
|
26269
|
+
} : {}
|
|
26247
26270
|
}
|
|
26248
26271
|
}
|
|
26249
26272
|
};
|
|
26273
|
+
if (app.assetDomain) {
|
|
26274
|
+
if (!app.assetCertificateArn)
|
|
26275
|
+
throw new Error("serverless app: `assetDomain` requires `assetCertificateArn` (a us-east-1 ACM cert — CloudFront only accepts certs from us-east-1).");
|
|
26276
|
+
if (app.hostedZoneId) {
|
|
26277
|
+
resources.AssetsDomainRecord = {
|
|
26278
|
+
Type: "AWS::Route53::RecordSet",
|
|
26279
|
+
Properties: {
|
|
26280
|
+
HostedZoneId: app.hostedZoneId,
|
|
26281
|
+
Name: app.assetDomain,
|
|
26282
|
+
Type: "A",
|
|
26283
|
+
AliasTarget: {
|
|
26284
|
+
DNSName: Fn2.getAtt("AssetsDistribution", "DomainName"),
|
|
26285
|
+
HostedZoneId: "Z2FDTNDATAQYW2"
|
|
26286
|
+
}
|
|
26287
|
+
}
|
|
26288
|
+
};
|
|
26289
|
+
}
|
|
26290
|
+
outputs.AssetDomain = { Description: "Custom asset CDN host", Value: app.assetDomain };
|
|
26291
|
+
}
|
|
26250
26292
|
resources.AssetsBucketPolicy = {
|
|
26251
26293
|
Type: "AWS::S3::BucketPolicy",
|
|
26252
26294
|
Properties: {
|
|
@@ -26325,6 +26367,37 @@ function composeServerlessAppTemplate(opts) {
|
|
|
26325
26367
|
}
|
|
26326
26368
|
};
|
|
26327
26369
|
}
|
|
26370
|
+
if (efsProvision) {
|
|
26371
|
+
resources.EfsFileSystem = {
|
|
26372
|
+
Type: "AWS::EFS::FileSystem",
|
|
26373
|
+
Properties: {
|
|
26374
|
+
Encrypted: true,
|
|
26375
|
+
FileSystemTags: [{ Key: "Name", Value: `${slug}-${environment}-efs` }]
|
|
26376
|
+
}
|
|
26377
|
+
};
|
|
26378
|
+
subnets.forEach((subnetId, i) => {
|
|
26379
|
+
resources[`EfsMountTarget${i}`] = {
|
|
26380
|
+
Type: "AWS::EFS::MountTarget",
|
|
26381
|
+
Properties: {
|
|
26382
|
+
FileSystemId: Fn2.ref("EfsFileSystem"),
|
|
26383
|
+
SubnetId: subnetId,
|
|
26384
|
+
SecurityGroups: [Fn2.getAtt("DataSecurityGroup", "GroupId")]
|
|
26385
|
+
}
|
|
26386
|
+
};
|
|
26387
|
+
});
|
|
26388
|
+
resources.EfsAccessPoint = {
|
|
26389
|
+
Type: "AWS::EFS::AccessPoint",
|
|
26390
|
+
Properties: {
|
|
26391
|
+
FileSystemId: Fn2.ref("EfsFileSystem"),
|
|
26392
|
+
PosixUser: { Uid: 1001, Gid: 1001 },
|
|
26393
|
+
RootDirectory: {
|
|
26394
|
+
Path: "/lambda",
|
|
26395
|
+
CreationInfo: { OwnerUid: 1001, OwnerGid: 1001, Permissions: "0755" }
|
|
26396
|
+
}
|
|
26397
|
+
}
|
|
26398
|
+
};
|
|
26399
|
+
outputs.EfsFileSystemId = { Description: "EFS file system id", Value: Fn2.ref("EfsFileSystem") };
|
|
26400
|
+
}
|
|
26328
26401
|
if (app.cache?.driver === "elasticache") {
|
|
26329
26402
|
resources.CacheSubnetGroup = {
|
|
26330
26403
|
Type: "AWS::ElastiCache::SubnetGroup",
|
|
@@ -83132,6 +83205,8 @@ function buildFunctionEnv(app, ctx, environment, mode, secrets, assetUrl, queueN
|
|
|
83132
83205
|
TSCLOUD_ENV: environment,
|
|
83133
83206
|
MAINTENANCE_MODE: "0",
|
|
83134
83207
|
...app.octane ? { TSCLOUD_OCTANE: "1" } : {},
|
|
83208
|
+
...app.serveAssets ? { TSCLOUD_SERVE_ASSETS: "1" } : {},
|
|
83209
|
+
...app.redirectRobotsTxt === false ? { TSCLOUD_REDIRECT_ROBOTS_TXT: "0" } : {},
|
|
83135
83210
|
...laravelDefaults,
|
|
83136
83211
|
...infraEnv,
|
|
83137
83212
|
...ctx.app.cache?.driver !== "elasticache" ? { TSCLOUD_CACHE_TABLE: `${ctx.slug}-${environment}-cache` } : {},
|
|
@@ -83172,13 +83247,15 @@ function* walk3(dir) {
|
|
|
83172
83247
|
yield full;
|
|
83173
83248
|
}
|
|
83174
83249
|
}
|
|
83175
|
-
async function uploadAssets(s32, bucket, dir, prefix) {
|
|
83250
|
+
async function uploadAssets(s32, bucket, dir, prefix, includeDotfiles = false) {
|
|
83176
83251
|
let count = 0;
|
|
83177
83252
|
for (const file of walk3(dir)) {
|
|
83178
|
-
const
|
|
83253
|
+
const rel = relative6(dir, file).replace(/\\/g, "/");
|
|
83254
|
+
if (!includeDotfiles && rel.split("/").some((seg) => seg.startsWith(".")))
|
|
83255
|
+
continue;
|
|
83179
83256
|
await s32.putObject({
|
|
83180
83257
|
bucket,
|
|
83181
|
-
key
|
|
83258
|
+
key: `${prefix}/${rel}`,
|
|
83182
83259
|
body: readFileSync13(file),
|
|
83183
83260
|
contentType: contentType(file),
|
|
83184
83261
|
cacheControl: "public, max-age=31536000, immutable"
|
|
@@ -83249,7 +83326,7 @@ async function deployServerlessApp(config6, environment, opts = {}) {
|
|
|
83249
83326
|
step(`Creating artifact bucket ${artifactBucket}`);
|
|
83250
83327
|
await s32.createBucket(artifactBucket);
|
|
83251
83328
|
}
|
|
83252
|
-
const exists = await s32.headObject(artifactBucket, key).then(() =>
|
|
83329
|
+
const exists = await s32.headObject(artifactBucket, key).then((r) => r !== null).catch(() => false);
|
|
83253
83330
|
if (exists) {
|
|
83254
83331
|
info("Artifact already uploaded — reusing");
|
|
83255
83332
|
} else {
|
|
@@ -83262,7 +83339,20 @@ async function deployServerlessApp(config6, environment, opts = {}) {
|
|
|
83262
83339
|
const composed = composeServerlessAppTemplate({ config: config6, environment, app, handlers, runtimeLayers });
|
|
83263
83340
|
const templateBody = JSON.stringify(composed.template);
|
|
83264
83341
|
const capabilities = ["CAPABILITY_NAMED_IAM"];
|
|
83265
|
-
const
|
|
83342
|
+
const status = await cfn.describeStacks({ stackName }).then((r) => r.Stacks[0]?.StackStatus).catch(() => {
|
|
83343
|
+
return;
|
|
83344
|
+
});
|
|
83345
|
+
let stackExists = status !== undefined;
|
|
83346
|
+
if (status && /DELETE_IN_PROGRESS/.test(status)) {
|
|
83347
|
+
step("Waiting for an in-progress stack delete to finish");
|
|
83348
|
+
await cfn.waitForStack(stackName, "stack-delete-complete").catch(() => {});
|
|
83349
|
+
stackExists = false;
|
|
83350
|
+
} else if (status && /(?:ROLLBACK_COMPLETE|REVIEW_IN_PROGRESS|CREATE_FAILED)/.test(status)) {
|
|
83351
|
+
step(`Deleting unusable stack (status ${status}) before recreate`);
|
|
83352
|
+
await cfn.deleteStack(stackName);
|
|
83353
|
+
await cfn.waitForStack(stackName, "stack-delete-complete").catch(() => {});
|
|
83354
|
+
stackExists = false;
|
|
83355
|
+
}
|
|
83266
83356
|
step(stackExists ? "Updating infrastructure stack" : "Creating infrastructure stack");
|
|
83267
83357
|
try {
|
|
83268
83358
|
if (stackExists) {
|
|
@@ -83284,10 +83374,10 @@ async function deployServerlessApp(config6, environment, opts = {}) {
|
|
|
83284
83374
|
if (app.assets) {
|
|
83285
83375
|
const assetsDir3 = join16(projectRoot, app.assets);
|
|
83286
83376
|
if (existsSync17(assetsDir3)) {
|
|
83287
|
-
const cdn = outputs.AssetsCdnDomain;
|
|
83377
|
+
const cdn = app.assetDomain || outputs.AssetsCdnDomain;
|
|
83288
83378
|
assetUrl = cdn ? `https://${cdn}/${artifactSha}` : undefined;
|
|
83289
83379
|
step(`Syncing assets from ${app.assets}`);
|
|
83290
|
-
const n = await uploadAssets(s32, ctx.assetsBucket, assetsDir3, artifactSha);
|
|
83380
|
+
const n = await uploadAssets(s32, ctx.assetsBucket, assetsDir3, artifactSha, app.dotFilesAsAssets);
|
|
83291
83381
|
info(`Uploaded ${n} asset(s)${assetUrl ? ` → ${assetUrl}` : ""}`);
|
|
83292
83382
|
} else {
|
|
83293
83383
|
warn(`Assets directory not found: ${assetsDir3}`);
|
|
@@ -83621,6 +83711,19 @@ function buildManagedDbEnv(database) {
|
|
|
83621
83711
|
}
|
|
83622
83712
|
|
|
83623
83713
|
// src/drivers/shared/php-provision.ts
|
|
83714
|
+
var PRODUCTION_PHP_INI = {
|
|
83715
|
+
"opcache.enable": "1",
|
|
83716
|
+
"opcache.enable_cli": "1",
|
|
83717
|
+
"opcache.memory_consumption": "256",
|
|
83718
|
+
"opcache.interned_strings_buffer": "16",
|
|
83719
|
+
"opcache.max_accelerated_files": "20000",
|
|
83720
|
+
"opcache.validate_timestamps": "0",
|
|
83721
|
+
"opcache.revalidate_freq": "0",
|
|
83722
|
+
"opcache.save_comments": "1",
|
|
83723
|
+
"opcache.fast_shutdown": "1",
|
|
83724
|
+
realpath_cache_size: "4096K",
|
|
83725
|
+
realpath_cache_ttl: "600"
|
|
83726
|
+
};
|
|
83624
83727
|
var PHP_FPM_LISTEN = "127.0.0.1:9074";
|
|
83625
83728
|
function phpFpmSocketPath(_version) {
|
|
83626
83729
|
return PHP_FPM_LISTEN;
|
|
@@ -83629,6 +83732,45 @@ function resolveDefaultPhpVersion(options = {}) {
|
|
|
83629
83732
|
const versions = options.versions?.length ? options.versions : ["8.3"];
|
|
83630
83733
|
return options.default && versions.includes(options.default) ? options.default : versions[0];
|
|
83631
83734
|
}
|
|
83735
|
+
function resolvePhpIni(options = {}) {
|
|
83736
|
+
const base = options.optimizeForProduction === false ? {} : { ...PRODUCTION_PHP_INI };
|
|
83737
|
+
return { ...base, ...options.ini || {} };
|
|
83738
|
+
}
|
|
83739
|
+
function buildPhpTuningScript(options = {}) {
|
|
83740
|
+
const ini = resolvePhpIni(options);
|
|
83741
|
+
const keys = Object.keys(ini);
|
|
83742
|
+
if (keys.length === 0)
|
|
83743
|
+
return [];
|
|
83744
|
+
const body = keys.map((k) => `${k}=${ini[k]}`);
|
|
83745
|
+
const marker = "ts-cloud-managed";
|
|
83746
|
+
return [
|
|
83747
|
+
pantryEnvActivation(),
|
|
83748
|
+
"TS_CLOUD_SCAN_DIR=$(php -i 2>/dev/null | awk -F' => ' '/^Scan this dir for additional .ini files/{print $2}' | head -1)",
|
|
83749
|
+
`TS_CLOUD_LOADED_INI=$(php -r 'echo php_ini_loaded_file() ?: "";' 2>/dev/null)`,
|
|
83750
|
+
'if [ -n "$TS_CLOUD_SCAN_DIR" ] && [ "$TS_CLOUD_SCAN_DIR" != "(none)" ]; then',
|
|
83751
|
+
' mkdir -p "$TS_CLOUD_SCAN_DIR"',
|
|
83752
|
+
' TS_CLOUD_PHP_INI="$TS_CLOUD_SCAN_DIR/zz-ts-cloud.ini"',
|
|
83753
|
+
` cat > "$TS_CLOUD_PHP_INI" <<'TS_CLOUD_PHPINI_EOF'`,
|
|
83754
|
+
`; ${marker} — production PHP tuning`,
|
|
83755
|
+
...body,
|
|
83756
|
+
"TS_CLOUD_PHPINI_EOF",
|
|
83757
|
+
"else",
|
|
83758
|
+
' if [ -z "$TS_CLOUD_LOADED_INI" ]; then',
|
|
83759
|
+
" TS_CLOUD_INI_DIR=$(php -i 2>/dev/null | awk -F' => ' '/^Configuration File \\(php.ini\\) Path/{print $2}' | head -1)",
|
|
83760
|
+
' TS_CLOUD_LOADED_INI="$TS_CLOUD_INI_DIR/php.ini"',
|
|
83761
|
+
' mkdir -p "$TS_CLOUD_INI_DIR"',
|
|
83762
|
+
' touch "$TS_CLOUD_LOADED_INI"',
|
|
83763
|
+
" fi",
|
|
83764
|
+
` sed -i '/; ${marker} BEGIN/,/; ${marker} END/d' "$TS_CLOUD_LOADED_INI"`,
|
|
83765
|
+
` cat >> "$TS_CLOUD_LOADED_INI" <<'TS_CLOUD_PHPINI_EOF'`,
|
|
83766
|
+
`; ${marker} BEGIN`,
|
|
83767
|
+
...body,
|
|
83768
|
+
`; ${marker} END`,
|
|
83769
|
+
"TS_CLOUD_PHPINI_EOF",
|
|
83770
|
+
"fi",
|
|
83771
|
+
`(cd ${PANTRY_PROJECT_DIR} && pantry restart php-fpm) 2>/dev/null || true`
|
|
83772
|
+
];
|
|
83773
|
+
}
|
|
83632
83774
|
function buildPhpProvisionScript(options = {}) {
|
|
83633
83775
|
const defaultVersion = resolveDefaultPhpVersion(options);
|
|
83634
83776
|
const installNginx = options.installNginx !== false;
|
|
@@ -83640,11 +83782,22 @@ function buildPhpProvisionScript(options = {}) {
|
|
|
83640
83782
|
specs.push("nginx.org");
|
|
83641
83783
|
return [
|
|
83642
83784
|
...buildPantryInstallScript(specs),
|
|
83643
|
-
...buildPantryServiceScript(["php-fpm"])
|
|
83785
|
+
...buildPantryServiceScript(["php-fpm"]),
|
|
83786
|
+
...buildPhpTuningScript(options)
|
|
83644
83787
|
];
|
|
83645
83788
|
}
|
|
83646
83789
|
|
|
83647
83790
|
// src/drivers/shared/nginx-vhost.ts
|
|
83791
|
+
function resolveNginxSnippet(nginx, templates) {
|
|
83792
|
+
if (!nginx)
|
|
83793
|
+
return [];
|
|
83794
|
+
const out = [];
|
|
83795
|
+
if (nginx.template && templates?.[nginx.template])
|
|
83796
|
+
out.push(...templates[nginx.template]);
|
|
83797
|
+
if (nginx.serverSnippet)
|
|
83798
|
+
out.push(...nginx.serverSnippet);
|
|
83799
|
+
return out;
|
|
83800
|
+
}
|
|
83648
83801
|
function htpasswdPath(siteName) {
|
|
83649
83802
|
return `/etc/nginx/.htpasswd-${siteName}`;
|
|
83650
83803
|
}
|
|
@@ -83677,6 +83830,9 @@ function vhostBody(options) {
|
|
|
83677
83830
|
" charset utf-8;",
|
|
83678
83831
|
""
|
|
83679
83832
|
];
|
|
83833
|
+
if (options.clientMaxBodySize) {
|
|
83834
|
+
lines.push(` client_max_body_size ${options.clientMaxBodySize};`, "");
|
|
83835
|
+
}
|
|
83680
83836
|
if (options.auth) {
|
|
83681
83837
|
lines.push(` auth_basic "${options.auth.realm || "Restricted"}";`, ` auth_basic_user_file ${htpasswdPath(options.siteName)};`, "");
|
|
83682
83838
|
}
|
|
@@ -83692,6 +83848,11 @@ function vhostBody(options) {
|
|
|
83692
83848
|
} else {
|
|
83693
83849
|
lines.push(" location / {", " try_files $uri $uri/ =404;", " }");
|
|
83694
83850
|
}
|
|
83851
|
+
if (options.serverSnippet && options.serverSnippet.length > 0) {
|
|
83852
|
+
lines.push("");
|
|
83853
|
+
for (const line of options.serverSnippet)
|
|
83854
|
+
lines.push(line ? ` ${line}` : "");
|
|
83855
|
+
}
|
|
83695
83856
|
return lines;
|
|
83696
83857
|
}
|
|
83697
83858
|
function buildNginxVhost(options) {
|
|
@@ -84149,7 +84310,9 @@ function buildComputeProvisionScripts(config6) {
|
|
|
84149
84310
|
versions: compute.php?.versions,
|
|
84150
84311
|
default: compute.php?.default,
|
|
84151
84312
|
extensions: compute.php?.extensions,
|
|
84152
|
-
installNginx: useNginx
|
|
84313
|
+
installNginx: useNginx,
|
|
84314
|
+
optimizeForProduction: compute.php?.optimizeForProduction,
|
|
84315
|
+
ini: compute.php?.ini
|
|
84153
84316
|
}),
|
|
84154
84317
|
...useNginx ? buildNginxServiceScript() : []
|
|
84155
84318
|
] : undefined;
|
|
@@ -85269,7 +85432,9 @@ class HetznerDriver {
|
|
|
85269
85432
|
versions: compute.php?.versions,
|
|
85270
85433
|
default: compute.php?.default,
|
|
85271
85434
|
extensions: compute.php?.extensions,
|
|
85272
|
-
installNginx: compute.webServer !== "rpx"
|
|
85435
|
+
installNginx: compute.webServer !== "rpx",
|
|
85436
|
+
optimizeForProduction: compute.php?.optimizeForProduction,
|
|
85437
|
+
ini: compute.php?.ini
|
|
85273
85438
|
});
|
|
85274
85439
|
const appUserData = wrapCloudInitUserData(buildUbuntuBootstrapScript({ runtime: "php", phpProvision: appPhp, servicesProvision: appProvision, baked }));
|
|
85275
85440
|
const existingApp = all.filter((s) => matchesTsCloudLabels(s.labels, slug, environment, "app"));
|
|
@@ -85883,6 +86048,16 @@ function shellQuote(value) {
|
|
|
85883
86048
|
// src/drivers/shared/releases.ts
|
|
85884
86049
|
var DEFAULT_SHARED_PATHS = ["storage", ".env"];
|
|
85885
86050
|
var DEFAULT_KEEP_RELEASES = 4;
|
|
86051
|
+
var DEFAULT_KEEP_DEPLOY_LOGS = 20;
|
|
86052
|
+
function deployMetaDir(base) {
|
|
86053
|
+
return `${base.replace(/\/+$/, "")}/.ts-cloud`;
|
|
86054
|
+
}
|
|
86055
|
+
function deployHistoryPath(base) {
|
|
86056
|
+
return `${deployMetaDir(base)}/deploy-history.log`;
|
|
86057
|
+
}
|
|
86058
|
+
function deployLogPath(base, releaseId) {
|
|
86059
|
+
return `${deployMetaDir(base)}/deploys/${releaseId}.log`;
|
|
86060
|
+
}
|
|
85886
86061
|
function releasePaths(base, releaseId) {
|
|
85887
86062
|
const root = base.replace(/\/+$/, "");
|
|
85888
86063
|
return {
|
|
@@ -85936,6 +86111,26 @@ function buildPruneReleases(paths, keep = DEFAULT_KEEP_RELEASES) {
|
|
|
85936
86111
|
"done"
|
|
85937
86112
|
];
|
|
85938
86113
|
}
|
|
86114
|
+
function buildDeployHistoryHeader(base, options) {
|
|
86115
|
+
const meta = deployMetaDir(base);
|
|
86116
|
+
const log4 = deployLogPath(base, options.releaseId);
|
|
86117
|
+
const history = deployHistoryPath(base);
|
|
86118
|
+
const keepLogs = Math.max(1, options.keepLogs ?? DEFAULT_KEEP_DEPLOY_LOGS);
|
|
86119
|
+
const commit = options.commit || "";
|
|
86120
|
+
const branch = options.branch || "";
|
|
86121
|
+
return [
|
|
86122
|
+
`mkdir -p ${meta}/deploys`,
|
|
86123
|
+
`exec > >(tee -a ${log4}) 2>&1`,
|
|
86124
|
+
`echo "[ts-cloud] deploy ${options.releaseId} commit=${commit} branch=${branch} starting $(date -u +%Y-%m-%dT%H:%M:%SZ)"`,
|
|
86125
|
+
"ts_cloud_record_deploy() {",
|
|
86126
|
+
" TS_CLOUD_RC=$?",
|
|
86127
|
+
' if [ "$TS_CLOUD_RC" -eq 0 ]; then TS_CLOUD_ST=success; else TS_CLOUD_ST=failed; fi',
|
|
86128
|
+
` printf '%s\\t%s\\t%s\\t%s\\trc=%s\\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${options.releaseId}" "${commit}" "$TS_CLOUD_ST" "$TS_CLOUD_RC" >> ${history}`,
|
|
86129
|
+
"}",
|
|
86130
|
+
"trap ts_cloud_record_deploy EXIT",
|
|
86131
|
+
`ls -1t ${meta}/deploys/*.log 2>/dev/null | tail -n +${keepLogs + 1} | while read -r TS_CLOUD_OLDLOG; do rm -f "$TS_CLOUD_OLDLOG"; done`
|
|
86132
|
+
];
|
|
86133
|
+
}
|
|
85939
86134
|
|
|
85940
86135
|
// src/drivers/shared/laravel-deploy.ts
|
|
85941
86136
|
var MACRO_CREATE_RELEASE = "$CREATE_RELEASE";
|
|
@@ -86020,6 +86215,12 @@ function buildLaravelDeployScript(options) {
|
|
|
86020
86215
|
"export COMPOSER_ALLOW_SUPERUSER=1",
|
|
86021
86216
|
pantryEnvActivation()
|
|
86022
86217
|
];
|
|
86218
|
+
out.push(...buildDeployHistoryHeader(base, {
|
|
86219
|
+
releaseId,
|
|
86220
|
+
commit,
|
|
86221
|
+
branch: site.repository.branch,
|
|
86222
|
+
keepLogs: keepReleases
|
|
86223
|
+
}));
|
|
86023
86224
|
out.push(...buildEnsureReleaseLayout(paths, sharedPaths));
|
|
86024
86225
|
if (site.env && Object.keys(site.env).length > 0)
|
|
86025
86226
|
out.push(...writeSharedEnv(`${paths.shared}/.env`, site.env));
|
|
@@ -86052,6 +86253,9 @@ function pantryExec(cmd) {
|
|
|
86052
86253
|
function reEscape(value) {
|
|
86053
86254
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
86054
86255
|
}
|
|
86256
|
+
function cronQuote(value) {
|
|
86257
|
+
return `'${value.split("'").join("'\\''")}'`;
|
|
86258
|
+
}
|
|
86055
86259
|
function slugify(value) {
|
|
86056
86260
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
|
|
86057
86261
|
}
|
|
@@ -86153,9 +86357,18 @@ function buildSiteServicesScript(options) {
|
|
|
86153
86357
|
out.push(`systemctl enable ${name}.service`, `systemctl restart ${name}.service`);
|
|
86154
86358
|
}
|
|
86155
86359
|
const cronPath = schedulerCronPath(slug, siteName);
|
|
86156
|
-
|
|
86157
|
-
|
|
86158
|
-
|
|
86360
|
+
const scheduler2 = site.scheduler;
|
|
86361
|
+
const schedulerEnabled = scheduler2 === true || typeof scheduler2 === "object" && scheduler2 !== null;
|
|
86362
|
+
if (schedulerEnabled) {
|
|
86363
|
+
const heartbeat = typeof scheduler2 === "object" && scheduler2 !== null ? scheduler2 : undefined;
|
|
86364
|
+
let command = `cd ${current} && ${PANTRY_ENV_EVAL} && ${phpBin} artisan schedule:run >> /dev/null 2>&1`;
|
|
86365
|
+
if (heartbeat?.heartbeatUrl) {
|
|
86366
|
+
const method = heartbeat.heartbeatMethod || "GET";
|
|
86367
|
+
const methodFlag = method === "GET" ? "" : `-X ${method} `;
|
|
86368
|
+
command += ` && curl -fsS -m 10 ${methodFlag}${cronQuote(heartbeat.heartbeatUrl)} >/dev/null 2>&1`;
|
|
86369
|
+
}
|
|
86370
|
+
const cron = `* * * * * root ${command}
|
|
86371
|
+
`.replace(/%/g, "\\%");
|
|
86159
86372
|
out.push(`cat > ${cronPath} <<'TS_CLOUD_CRON_EOF'`, cron.replace(/\n$/, ""), "TS_CLOUD_CRON_EOF", `chmod 644 ${cronPath}`);
|
|
86160
86373
|
} else {
|
|
86161
86374
|
out.push(`rm -f ${cronPath}`);
|
|
@@ -86223,7 +86436,9 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
|
86223
86436
|
phpVersion,
|
|
86224
86437
|
redirects: site.redirects,
|
|
86225
86438
|
ssl: customCert,
|
|
86226
|
-
auth: site.auth && site.auth.enabled !== false && site.auth.password ? { username: site.auth.username || "admin", password: site.auth.password, realm: site.auth.realm } : undefined
|
|
86439
|
+
auth: site.auth && site.auth.enabled !== false && site.auth.password ? { username: site.auth.username || "admin", password: site.auth.password, realm: site.auth.realm } : undefined,
|
|
86440
|
+
serverSnippet: resolveNginxSnippet(site.nginx, compute2?.nginxTemplates),
|
|
86441
|
+
clientMaxBodySize: site.nginx?.clientMaxBodySize
|
|
86227
86442
|
}) : [];
|
|
86228
86443
|
const sslScript = useNginx ? buildSslScript(site) : [];
|
|
86229
86444
|
const servicesScript = siteHasServices(site) ? buildSiteServicesScript({ slug, siteName, site, phpVersion, appBase }) : [];
|
|
@@ -86286,7 +86501,9 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
|
86286
86501
|
appDir: `/var/www/${siteName}`,
|
|
86287
86502
|
webDirectory: "",
|
|
86288
86503
|
redirects: site.redirects,
|
|
86289
|
-
auth: site.auth && site.auth.enabled !== false && site.auth.password ? { username: site.auth.username || "admin", password: site.auth.password, realm: site.auth.realm } : undefined
|
|
86504
|
+
auth: site.auth && site.auth.enabled !== false && site.auth.password ? { username: site.auth.username || "admin", password: site.auth.password, realm: site.auth.realm } : undefined,
|
|
86505
|
+
serverSnippet: resolveNginxSnippet(site.nginx, compute?.nginxTemplates),
|
|
86506
|
+
clientMaxBodySize: site.nginx?.clientMaxBodySize
|
|
86290
86507
|
}) : [];
|
|
86291
86508
|
const staticSsl = wantsNginxStatic ? buildSslScript(site) : [];
|
|
86292
86509
|
const remoteScript = [...baseScript, ...staticVhost, ...staticSsl];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/ts-cloud",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.3",
|
|
5
5
|
"description": "A lightweight, performant infrastructure-as-code library and CLI for deploying both server-based (EC2) and serverless applications.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -89,8 +89,8 @@
|
|
|
89
89
|
"test": "bun test"
|
|
90
90
|
},
|
|
91
91
|
"dependencies": {
|
|
92
|
-
"@ts-cloud/aws-types": "0.5.
|
|
93
|
-
"@ts-cloud/core": "0.5.
|
|
92
|
+
"@ts-cloud/aws-types": "0.5.3",
|
|
93
|
+
"@ts-cloud/core": "0.5.3",
|
|
94
94
|
"@stacksjs/ts-xml": "^0.1.0"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|