@stacksjs/ts-cloud 0.5.1 → 0.5.2
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 +222 -18
- 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"
|
|
@@ -83284,10 +83361,10 @@ async function deployServerlessApp(config6, environment, opts = {}) {
|
|
|
83284
83361
|
if (app.assets) {
|
|
83285
83362
|
const assetsDir3 = join16(projectRoot, app.assets);
|
|
83286
83363
|
if (existsSync17(assetsDir3)) {
|
|
83287
|
-
const cdn = outputs.AssetsCdnDomain;
|
|
83364
|
+
const cdn = app.assetDomain || outputs.AssetsCdnDomain;
|
|
83288
83365
|
assetUrl = cdn ? `https://${cdn}/${artifactSha}` : undefined;
|
|
83289
83366
|
step(`Syncing assets from ${app.assets}`);
|
|
83290
|
-
const n = await uploadAssets(s32, ctx.assetsBucket, assetsDir3, artifactSha);
|
|
83367
|
+
const n = await uploadAssets(s32, ctx.assetsBucket, assetsDir3, artifactSha, app.dotFilesAsAssets);
|
|
83291
83368
|
info(`Uploaded ${n} asset(s)${assetUrl ? ` → ${assetUrl}` : ""}`);
|
|
83292
83369
|
} else {
|
|
83293
83370
|
warn(`Assets directory not found: ${assetsDir3}`);
|
|
@@ -83621,6 +83698,19 @@ function buildManagedDbEnv(database) {
|
|
|
83621
83698
|
}
|
|
83622
83699
|
|
|
83623
83700
|
// src/drivers/shared/php-provision.ts
|
|
83701
|
+
var PRODUCTION_PHP_INI = {
|
|
83702
|
+
"opcache.enable": "1",
|
|
83703
|
+
"opcache.enable_cli": "1",
|
|
83704
|
+
"opcache.memory_consumption": "256",
|
|
83705
|
+
"opcache.interned_strings_buffer": "16",
|
|
83706
|
+
"opcache.max_accelerated_files": "20000",
|
|
83707
|
+
"opcache.validate_timestamps": "0",
|
|
83708
|
+
"opcache.revalidate_freq": "0",
|
|
83709
|
+
"opcache.save_comments": "1",
|
|
83710
|
+
"opcache.fast_shutdown": "1",
|
|
83711
|
+
realpath_cache_size: "4096K",
|
|
83712
|
+
realpath_cache_ttl: "600"
|
|
83713
|
+
};
|
|
83624
83714
|
var PHP_FPM_LISTEN = "127.0.0.1:9074";
|
|
83625
83715
|
function phpFpmSocketPath(_version) {
|
|
83626
83716
|
return PHP_FPM_LISTEN;
|
|
@@ -83629,6 +83719,45 @@ function resolveDefaultPhpVersion(options = {}) {
|
|
|
83629
83719
|
const versions = options.versions?.length ? options.versions : ["8.3"];
|
|
83630
83720
|
return options.default && versions.includes(options.default) ? options.default : versions[0];
|
|
83631
83721
|
}
|
|
83722
|
+
function resolvePhpIni(options = {}) {
|
|
83723
|
+
const base = options.optimizeForProduction === false ? {} : { ...PRODUCTION_PHP_INI };
|
|
83724
|
+
return { ...base, ...options.ini || {} };
|
|
83725
|
+
}
|
|
83726
|
+
function buildPhpTuningScript(options = {}) {
|
|
83727
|
+
const ini = resolvePhpIni(options);
|
|
83728
|
+
const keys = Object.keys(ini);
|
|
83729
|
+
if (keys.length === 0)
|
|
83730
|
+
return [];
|
|
83731
|
+
const body = keys.map((k) => `${k}=${ini[k]}`);
|
|
83732
|
+
const marker = "ts-cloud-managed";
|
|
83733
|
+
return [
|
|
83734
|
+
pantryEnvActivation(),
|
|
83735
|
+
"TS_CLOUD_SCAN_DIR=$(php -i 2>/dev/null | awk -F' => ' '/^Scan this dir for additional .ini files/{print $2}' | head -1)",
|
|
83736
|
+
`TS_CLOUD_LOADED_INI=$(php -r 'echo php_ini_loaded_file() ?: "";' 2>/dev/null)`,
|
|
83737
|
+
'if [ -n "$TS_CLOUD_SCAN_DIR" ] && [ "$TS_CLOUD_SCAN_DIR" != "(none)" ]; then',
|
|
83738
|
+
' mkdir -p "$TS_CLOUD_SCAN_DIR"',
|
|
83739
|
+
' TS_CLOUD_PHP_INI="$TS_CLOUD_SCAN_DIR/zz-ts-cloud.ini"',
|
|
83740
|
+
` cat > "$TS_CLOUD_PHP_INI" <<'TS_CLOUD_PHPINI_EOF'`,
|
|
83741
|
+
`; ${marker} — production PHP tuning`,
|
|
83742
|
+
...body,
|
|
83743
|
+
"TS_CLOUD_PHPINI_EOF",
|
|
83744
|
+
"else",
|
|
83745
|
+
' if [ -z "$TS_CLOUD_LOADED_INI" ]; then',
|
|
83746
|
+
" TS_CLOUD_INI_DIR=$(php -i 2>/dev/null | awk -F' => ' '/^Configuration File \\(php.ini\\) Path/{print $2}' | head -1)",
|
|
83747
|
+
' TS_CLOUD_LOADED_INI="$TS_CLOUD_INI_DIR/php.ini"',
|
|
83748
|
+
' mkdir -p "$TS_CLOUD_INI_DIR"',
|
|
83749
|
+
' touch "$TS_CLOUD_LOADED_INI"',
|
|
83750
|
+
" fi",
|
|
83751
|
+
` sed -i '/; ${marker} BEGIN/,/; ${marker} END/d' "$TS_CLOUD_LOADED_INI"`,
|
|
83752
|
+
` cat >> "$TS_CLOUD_LOADED_INI" <<'TS_CLOUD_PHPINI_EOF'`,
|
|
83753
|
+
`; ${marker} BEGIN`,
|
|
83754
|
+
...body,
|
|
83755
|
+
`; ${marker} END`,
|
|
83756
|
+
"TS_CLOUD_PHPINI_EOF",
|
|
83757
|
+
"fi",
|
|
83758
|
+
`(cd ${PANTRY_PROJECT_DIR} && pantry restart php-fpm) 2>/dev/null || true`
|
|
83759
|
+
];
|
|
83760
|
+
}
|
|
83632
83761
|
function buildPhpProvisionScript(options = {}) {
|
|
83633
83762
|
const defaultVersion = resolveDefaultPhpVersion(options);
|
|
83634
83763
|
const installNginx = options.installNginx !== false;
|
|
@@ -83640,11 +83769,22 @@ function buildPhpProvisionScript(options = {}) {
|
|
|
83640
83769
|
specs.push("nginx.org");
|
|
83641
83770
|
return [
|
|
83642
83771
|
...buildPantryInstallScript(specs),
|
|
83643
|
-
...buildPantryServiceScript(["php-fpm"])
|
|
83772
|
+
...buildPantryServiceScript(["php-fpm"]),
|
|
83773
|
+
...buildPhpTuningScript(options)
|
|
83644
83774
|
];
|
|
83645
83775
|
}
|
|
83646
83776
|
|
|
83647
83777
|
// src/drivers/shared/nginx-vhost.ts
|
|
83778
|
+
function resolveNginxSnippet(nginx, templates) {
|
|
83779
|
+
if (!nginx)
|
|
83780
|
+
return [];
|
|
83781
|
+
const out = [];
|
|
83782
|
+
if (nginx.template && templates?.[nginx.template])
|
|
83783
|
+
out.push(...templates[nginx.template]);
|
|
83784
|
+
if (nginx.serverSnippet)
|
|
83785
|
+
out.push(...nginx.serverSnippet);
|
|
83786
|
+
return out;
|
|
83787
|
+
}
|
|
83648
83788
|
function htpasswdPath(siteName) {
|
|
83649
83789
|
return `/etc/nginx/.htpasswd-${siteName}`;
|
|
83650
83790
|
}
|
|
@@ -83677,6 +83817,9 @@ function vhostBody(options) {
|
|
|
83677
83817
|
" charset utf-8;",
|
|
83678
83818
|
""
|
|
83679
83819
|
];
|
|
83820
|
+
if (options.clientMaxBodySize) {
|
|
83821
|
+
lines.push(` client_max_body_size ${options.clientMaxBodySize};`, "");
|
|
83822
|
+
}
|
|
83680
83823
|
if (options.auth) {
|
|
83681
83824
|
lines.push(` auth_basic "${options.auth.realm || "Restricted"}";`, ` auth_basic_user_file ${htpasswdPath(options.siteName)};`, "");
|
|
83682
83825
|
}
|
|
@@ -83692,6 +83835,11 @@ function vhostBody(options) {
|
|
|
83692
83835
|
} else {
|
|
83693
83836
|
lines.push(" location / {", " try_files $uri $uri/ =404;", " }");
|
|
83694
83837
|
}
|
|
83838
|
+
if (options.serverSnippet && options.serverSnippet.length > 0) {
|
|
83839
|
+
lines.push("");
|
|
83840
|
+
for (const line of options.serverSnippet)
|
|
83841
|
+
lines.push(line ? ` ${line}` : "");
|
|
83842
|
+
}
|
|
83695
83843
|
return lines;
|
|
83696
83844
|
}
|
|
83697
83845
|
function buildNginxVhost(options) {
|
|
@@ -84149,7 +84297,9 @@ function buildComputeProvisionScripts(config6) {
|
|
|
84149
84297
|
versions: compute.php?.versions,
|
|
84150
84298
|
default: compute.php?.default,
|
|
84151
84299
|
extensions: compute.php?.extensions,
|
|
84152
|
-
installNginx: useNginx
|
|
84300
|
+
installNginx: useNginx,
|
|
84301
|
+
optimizeForProduction: compute.php?.optimizeForProduction,
|
|
84302
|
+
ini: compute.php?.ini
|
|
84153
84303
|
}),
|
|
84154
84304
|
...useNginx ? buildNginxServiceScript() : []
|
|
84155
84305
|
] : undefined;
|
|
@@ -85269,7 +85419,9 @@ class HetznerDriver {
|
|
|
85269
85419
|
versions: compute.php?.versions,
|
|
85270
85420
|
default: compute.php?.default,
|
|
85271
85421
|
extensions: compute.php?.extensions,
|
|
85272
|
-
installNginx: compute.webServer !== "rpx"
|
|
85422
|
+
installNginx: compute.webServer !== "rpx",
|
|
85423
|
+
optimizeForProduction: compute.php?.optimizeForProduction,
|
|
85424
|
+
ini: compute.php?.ini
|
|
85273
85425
|
});
|
|
85274
85426
|
const appUserData = wrapCloudInitUserData(buildUbuntuBootstrapScript({ runtime: "php", phpProvision: appPhp, servicesProvision: appProvision, baked }));
|
|
85275
85427
|
const existingApp = all.filter((s) => matchesTsCloudLabels(s.labels, slug, environment, "app"));
|
|
@@ -85883,6 +86035,16 @@ function shellQuote(value) {
|
|
|
85883
86035
|
// src/drivers/shared/releases.ts
|
|
85884
86036
|
var DEFAULT_SHARED_PATHS = ["storage", ".env"];
|
|
85885
86037
|
var DEFAULT_KEEP_RELEASES = 4;
|
|
86038
|
+
var DEFAULT_KEEP_DEPLOY_LOGS = 20;
|
|
86039
|
+
function deployMetaDir(base) {
|
|
86040
|
+
return `${base.replace(/\/+$/, "")}/.ts-cloud`;
|
|
86041
|
+
}
|
|
86042
|
+
function deployHistoryPath(base) {
|
|
86043
|
+
return `${deployMetaDir(base)}/deploy-history.log`;
|
|
86044
|
+
}
|
|
86045
|
+
function deployLogPath(base, releaseId) {
|
|
86046
|
+
return `${deployMetaDir(base)}/deploys/${releaseId}.log`;
|
|
86047
|
+
}
|
|
85886
86048
|
function releasePaths(base, releaseId) {
|
|
85887
86049
|
const root = base.replace(/\/+$/, "");
|
|
85888
86050
|
return {
|
|
@@ -85936,6 +86098,26 @@ function buildPruneReleases(paths, keep = DEFAULT_KEEP_RELEASES) {
|
|
|
85936
86098
|
"done"
|
|
85937
86099
|
];
|
|
85938
86100
|
}
|
|
86101
|
+
function buildDeployHistoryHeader(base, options) {
|
|
86102
|
+
const meta = deployMetaDir(base);
|
|
86103
|
+
const log4 = deployLogPath(base, options.releaseId);
|
|
86104
|
+
const history = deployHistoryPath(base);
|
|
86105
|
+
const keepLogs = Math.max(1, options.keepLogs ?? DEFAULT_KEEP_DEPLOY_LOGS);
|
|
86106
|
+
const commit = options.commit || "";
|
|
86107
|
+
const branch = options.branch || "";
|
|
86108
|
+
return [
|
|
86109
|
+
`mkdir -p ${meta}/deploys`,
|
|
86110
|
+
`exec > >(tee -a ${log4}) 2>&1`,
|
|
86111
|
+
`echo "[ts-cloud] deploy ${options.releaseId} commit=${commit} branch=${branch} starting $(date -u +%Y-%m-%dT%H:%M:%SZ)"`,
|
|
86112
|
+
"ts_cloud_record_deploy() {",
|
|
86113
|
+
" TS_CLOUD_RC=$?",
|
|
86114
|
+
' if [ "$TS_CLOUD_RC" -eq 0 ]; then TS_CLOUD_ST=success; else TS_CLOUD_ST=failed; fi',
|
|
86115
|
+
` 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}`,
|
|
86116
|
+
"}",
|
|
86117
|
+
"trap ts_cloud_record_deploy EXIT",
|
|
86118
|
+
`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`
|
|
86119
|
+
];
|
|
86120
|
+
}
|
|
85939
86121
|
|
|
85940
86122
|
// src/drivers/shared/laravel-deploy.ts
|
|
85941
86123
|
var MACRO_CREATE_RELEASE = "$CREATE_RELEASE";
|
|
@@ -86020,6 +86202,12 @@ function buildLaravelDeployScript(options) {
|
|
|
86020
86202
|
"export COMPOSER_ALLOW_SUPERUSER=1",
|
|
86021
86203
|
pantryEnvActivation()
|
|
86022
86204
|
];
|
|
86205
|
+
out.push(...buildDeployHistoryHeader(base, {
|
|
86206
|
+
releaseId,
|
|
86207
|
+
commit,
|
|
86208
|
+
branch: site.repository.branch,
|
|
86209
|
+
keepLogs: keepReleases
|
|
86210
|
+
}));
|
|
86023
86211
|
out.push(...buildEnsureReleaseLayout(paths, sharedPaths));
|
|
86024
86212
|
if (site.env && Object.keys(site.env).length > 0)
|
|
86025
86213
|
out.push(...writeSharedEnv(`${paths.shared}/.env`, site.env));
|
|
@@ -86052,6 +86240,9 @@ function pantryExec(cmd) {
|
|
|
86052
86240
|
function reEscape(value) {
|
|
86053
86241
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
86054
86242
|
}
|
|
86243
|
+
function cronQuote(value) {
|
|
86244
|
+
return `'${value.split("'").join("'\\''")}'`;
|
|
86245
|
+
}
|
|
86055
86246
|
function slugify(value) {
|
|
86056
86247
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
|
|
86057
86248
|
}
|
|
@@ -86153,9 +86344,18 @@ function buildSiteServicesScript(options) {
|
|
|
86153
86344
|
out.push(`systemctl enable ${name}.service`, `systemctl restart ${name}.service`);
|
|
86154
86345
|
}
|
|
86155
86346
|
const cronPath = schedulerCronPath(slug, siteName);
|
|
86156
|
-
|
|
86157
|
-
|
|
86158
|
-
|
|
86347
|
+
const scheduler2 = site.scheduler;
|
|
86348
|
+
const schedulerEnabled = scheduler2 === true || typeof scheduler2 === "object" && scheduler2 !== null;
|
|
86349
|
+
if (schedulerEnabled) {
|
|
86350
|
+
const heartbeat = typeof scheduler2 === "object" && scheduler2 !== null ? scheduler2 : undefined;
|
|
86351
|
+
let command = `cd ${current} && ${PANTRY_ENV_EVAL} && ${phpBin} artisan schedule:run >> /dev/null 2>&1`;
|
|
86352
|
+
if (heartbeat?.heartbeatUrl) {
|
|
86353
|
+
const method = heartbeat.heartbeatMethod || "GET";
|
|
86354
|
+
const methodFlag = method === "GET" ? "" : `-X ${method} `;
|
|
86355
|
+
command += ` && curl -fsS -m 10 ${methodFlag}${cronQuote(heartbeat.heartbeatUrl)} >/dev/null 2>&1`;
|
|
86356
|
+
}
|
|
86357
|
+
const cron = `* * * * * root ${command}
|
|
86358
|
+
`.replace(/%/g, "\\%");
|
|
86159
86359
|
out.push(`cat > ${cronPath} <<'TS_CLOUD_CRON_EOF'`, cron.replace(/\n$/, ""), "TS_CLOUD_CRON_EOF", `chmod 644 ${cronPath}`);
|
|
86160
86360
|
} else {
|
|
86161
86361
|
out.push(`rm -f ${cronPath}`);
|
|
@@ -86223,7 +86423,9 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
|
86223
86423
|
phpVersion,
|
|
86224
86424
|
redirects: site.redirects,
|
|
86225
86425
|
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
|
|
86426
|
+
auth: site.auth && site.auth.enabled !== false && site.auth.password ? { username: site.auth.username || "admin", password: site.auth.password, realm: site.auth.realm } : undefined,
|
|
86427
|
+
serverSnippet: resolveNginxSnippet(site.nginx, compute2?.nginxTemplates),
|
|
86428
|
+
clientMaxBodySize: site.nginx?.clientMaxBodySize
|
|
86227
86429
|
}) : [];
|
|
86228
86430
|
const sslScript = useNginx ? buildSslScript(site) : [];
|
|
86229
86431
|
const servicesScript = siteHasServices(site) ? buildSiteServicesScript({ slug, siteName, site, phpVersion, appBase }) : [];
|
|
@@ -86286,7 +86488,9 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
|
86286
86488
|
appDir: `/var/www/${siteName}`,
|
|
86287
86489
|
webDirectory: "",
|
|
86288
86490
|
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
|
|
86491
|
+
auth: site.auth && site.auth.enabled !== false && site.auth.password ? { username: site.auth.username || "admin", password: site.auth.password, realm: site.auth.realm } : undefined,
|
|
86492
|
+
serverSnippet: resolveNginxSnippet(site.nginx, compute?.nginxTemplates),
|
|
86493
|
+
clientMaxBodySize: site.nginx?.clientMaxBodySize
|
|
86290
86494
|
}) : [];
|
|
86291
86495
|
const staticSsl = wantsNginxStatic ? buildSslScript(site) : [];
|
|
86292
86496
|
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.2",
|
|
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.2",
|
|
93
|
+
"@ts-cloud/core": "0.5.2",
|
|
94
94
|
"@stacksjs/ts-xml": "^0.1.0"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|