@alienplatform/testing 0.1.0 → 1.3.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/LICENSE.md +105 -0
- package/README.md +61 -274
- package/dist/errors.js +37 -0
- package/dist/errors.js.map +1 -0
- package/dist/external-secrets.js +140 -0
- package/dist/external-secrets.js.map +1 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +458 -0
- package/dist/index.js.map +1 -0
- package/package.json +12 -14
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external-secrets.js","names":[],"sources":["../src/external-secrets.ts"],"sourcesContent":["/**\n * External secrets - platform-native secret management\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport { AlienError } from \"@alienplatform/core\"\nimport { PutParameterCommand, SSMClient } from \"@aws-sdk/client-ssm\"\nimport { ClientSecretCredential } from \"@azure/identity\"\nimport { SecretClient } from \"@azure/keyvault-secrets\"\nimport { SecretManagerServiceClient } from \"@google-cloud/secret-manager\"\nimport {\n TestingOperationFailedError,\n TestingUnsupportedPlatformError,\n withTestingContext,\n} from \"./errors.js\"\nimport type { Platform } from \"./types.js\"\n\n/**\n * Set an external secret using platform-native tools\n *\n * Falls back to environment variables for cloud provider credentials.\n */\nexport async function setExternalSecret(\n platform: Platform,\n resourcePrefix: string,\n vaultName: string,\n secretKey: string,\n secretValue: string,\n _namespace?: string,\n stateDir?: string,\n deploymentId?: string,\n): Promise<void> {\n try {\n switch (platform) {\n case \"aws\":\n await setAWSSecret(resourcePrefix, vaultName, secretKey, secretValue)\n break\n\n case \"gcp\":\n await setGCPSecret(resourcePrefix, vaultName, secretKey, secretValue)\n break\n\n case \"azure\":\n await setAzureSecret(resourcePrefix, vaultName, secretKey, secretValue)\n break\n\n case \"local\":\n await setLocalSecret(vaultName, secretKey, secretValue, stateDir, deploymentId)\n break\n\n default: {\n const exhaustive: never = platform\n throw new AlienError(\n TestingUnsupportedPlatformError.create({\n platform: String(exhaustive),\n operation: \"setExternalSecret\",\n }),\n )\n }\n }\n } catch (error) {\n throw await withTestingContext(error, \"setExternalSecret\", \"Failed to set external secret\", {\n platform,\n resourcePrefix,\n vaultName,\n secretKey,\n })\n }\n}\n\n/**\n * Set AWS SSM Parameter Store secret\n *\n * Uses environment variables for credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION).\n */\nasync function setAWSSecret(\n resourcePrefix: string,\n vaultName: string,\n secretKey: string,\n secretValue: string,\n): Promise<void> {\n const client = new SSMClient({\n region: process.env.AWS_REGION,\n })\n const parameterName = `/${resourcePrefix}-${vaultName}-${secretKey}`\n\n await client.send(\n new PutParameterCommand({\n Name: parameterName,\n Value: secretValue,\n Type: \"SecureString\",\n Overwrite: true,\n }),\n )\n}\n\n/**\n * Set GCP Secret Manager secret\n *\n * Uses environment variables for credentials (GOOGLE_APPLICATION_CREDENTIALS, GCP_PROJECT_ID).\n */\nasync function setGCPSecret(\n resourcePrefix: string,\n vaultName: string,\n secretKey: string,\n secretValue: string,\n): Promise<void> {\n // Falls back to GOOGLE_APPLICATION_CREDENTIALS\n const client = new SecretManagerServiceClient()\n const projectId = process.env.GCP_PROJECT_ID || process.env.GOOGLE_CLOUD_PROJECT\n\n if (!projectId) {\n throw new AlienError(\n TestingOperationFailedError.create({\n operation: \"setGCPSecret\",\n message: \"GCP project ID is required (set GCP_PROJECT_ID or GOOGLE_CLOUD_PROJECT env var)\",\n }),\n )\n }\n\n const secretName = `${resourcePrefix}-${vaultName}-${secretKey}`\n const parent = `projects/${projectId}`\n const secretPath = `${parent}/secrets/${secretName}`\n\n try {\n // Try to create the secret first\n await client.createSecret({\n parent,\n secretId: secretName,\n secret: {\n replication: {\n automatic: {},\n },\n },\n })\n } catch (error: any) {\n // Secret already exists, that's fine\n if (!error.message?.includes(\"ALREADY_EXISTS\")) {\n throw await withTestingContext(error, \"setGCPSecret\", \"Failed to create GCP secret\")\n }\n }\n\n // Add secret version\n await client.addSecretVersion({\n parent: secretPath,\n payload: {\n data: Buffer.from(secretValue, \"utf8\"),\n },\n })\n}\n\n/**\n * Set Azure Key Vault secret\n *\n * Uses environment variables for credentials (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET).\n */\nasync function setAzureSecret(\n resourcePrefix: string,\n vaultName: string,\n secretKey: string,\n secretValue: string,\n): Promise<void> {\n const vaultNameFull = `${resourcePrefix}-${vaultName}`\n const vaultUrl = `https://${vaultNameFull}.vault.azure.net`\n\n // Fall back to environment variables\n const tenantId = process.env.AZURE_TENANT_ID\n const clientId = process.env.AZURE_CLIENT_ID\n const clientSecret = process.env.AZURE_CLIENT_SECRET\n\n if (!tenantId || !clientId || !clientSecret) {\n throw new AlienError(\n TestingOperationFailedError.create({\n operation: \"setAzureSecret\",\n message:\n \"Azure credentials are required (set AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET env vars)\",\n }),\n )\n }\n\n const credential = new ClientSecretCredential(tenantId, clientId, clientSecret)\n const client = new SecretClient(vaultUrl, credential)\n\n // Azure Key Vault requires alphanumeric names with hyphens\n const azureSecretKey = secretKey.replace(/_/g, \"-\")\n\n await client.setSecret(azureSecretKey, secretValue)\n}\n\n/**\n * Set local dev secret by writing directly to the vault's secrets.json file.\n *\n * The local vault binding (LocalVault) reads from:\n * {stateDir}/{deploymentId}/vault/{vaultName}/secrets.json\n *\n * We write to the same path so the running function can read it immediately.\n */\nasync function setLocalSecret(\n vaultName: string,\n secretKey: string,\n secretValue: string,\n stateDir?: string,\n deploymentId?: string,\n): Promise<void> {\n if (!stateDir) {\n throw new AlienError(\n TestingOperationFailedError.create({\n operation: \"setLocalSecret\",\n message: \"stateDir is required for local vault set\",\n }),\n )\n }\n\n if (!deploymentId) {\n throw new AlienError(\n TestingOperationFailedError.create({\n operation: \"setLocalSecret\",\n message: \"deploymentId is required for local vault set\",\n }),\n )\n }\n\n // Path matches what LocalVault reads: {stateDir}/{deploymentId}/vault/{vaultName}/secrets.json\n const vaultDir = join(stateDir, deploymentId, \"vault\", vaultName)\n const secretsFile = join(vaultDir, \"secrets.json\")\n\n // Read existing secrets or start fresh\n let secrets: Record<string, string> = {}\n if (existsSync(secretsFile)) {\n secrets = JSON.parse(readFileSync(secretsFile, \"utf-8\"))\n }\n\n // Set the secret\n secrets[secretKey] = secretValue\n\n // Write back\n mkdirSync(vaultDir, { recursive: true })\n writeFileSync(secretsFile, JSON.stringify(secrets, null, 2))\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as node_child_process0 from "node:child_process";
|
|
2
|
+
import * as _alienplatform_core0 from "@alienplatform/core";
|
|
3
|
+
import { AlienError } from "@alienplatform/core";
|
|
4
|
+
import * as z from "zod/v4";
|
|
5
|
+
|
|
6
|
+
//#region src/types.d.ts
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Core types for @alienplatform/testing
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Target platform for deployment.
|
|
13
|
+
*
|
|
14
|
+
* - 'local' (default) — runs locally via `alien dev`, no credentials needed
|
|
15
|
+
* - 'aws' | 'gcp' | 'azure' — deploys to the cloud via the platform API (requires ALIEN_API_KEY)
|
|
16
|
+
*/
|
|
17
|
+
type Platform = "local" | "aws" | "gcp" | "azure";
|
|
18
|
+
/**
|
|
19
|
+
* Environment variable configuration for deployments
|
|
20
|
+
*/
|
|
21
|
+
interface EnvironmentVariable {
|
|
22
|
+
name: string;
|
|
23
|
+
value: string;
|
|
24
|
+
type?: "plain" | "secret";
|
|
25
|
+
targetResources?: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Options for deploying an application
|
|
29
|
+
*/
|
|
30
|
+
interface DeployOptions {
|
|
31
|
+
/** Path to application directory */
|
|
32
|
+
app: string;
|
|
33
|
+
/** Optional: specific config file to use (e.g., alien.function.ts) */
|
|
34
|
+
config?: string;
|
|
35
|
+
/** Target platform (default: 'local') */
|
|
36
|
+
platform?: Platform;
|
|
37
|
+
/** Environment variables */
|
|
38
|
+
environmentVariables?: EnvironmentVariable[];
|
|
39
|
+
/** Verbose logging */
|
|
40
|
+
verbose?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Options for upgrading a deployment
|
|
44
|
+
*/
|
|
45
|
+
interface UpgradeOptions {
|
|
46
|
+
environmentVariables?: EnvironmentVariable[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Deployment info response from alien dev server
|
|
50
|
+
*/
|
|
51
|
+
interface DeploymentInfo {
|
|
52
|
+
commands: {
|
|
53
|
+
url: string;
|
|
54
|
+
deploymentId: string;
|
|
55
|
+
};
|
|
56
|
+
resources: Record<string, {
|
|
57
|
+
resourceType: string;
|
|
58
|
+
publicUrl?: string;
|
|
59
|
+
}>;
|
|
60
|
+
status: string;
|
|
61
|
+
platform: Platform;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Init params for creating a Deployment instance (internal)
|
|
65
|
+
*/
|
|
66
|
+
interface DeploymentInit {
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
url: string;
|
|
70
|
+
platform: Platform;
|
|
71
|
+
commandsUrl: string;
|
|
72
|
+
appPath: string;
|
|
73
|
+
process?: node_child_process0.ChildProcess;
|
|
74
|
+
apiUrl?: string;
|
|
75
|
+
apiKey?: string;
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/deployment.d.ts
|
|
79
|
+
declare class Deployment {
|
|
80
|
+
readonly id: string;
|
|
81
|
+
readonly name: string;
|
|
82
|
+
readonly url: string;
|
|
83
|
+
readonly platform: Platform;
|
|
84
|
+
/** Whether the deployment has been destroyed */
|
|
85
|
+
destroyed: boolean;
|
|
86
|
+
private process?;
|
|
87
|
+
private commandsUrl;
|
|
88
|
+
private appPath;
|
|
89
|
+
private apiUrl?;
|
|
90
|
+
private apiKey?;
|
|
91
|
+
constructor(params: DeploymentInit);
|
|
92
|
+
/**
|
|
93
|
+
* Invoke a command on the deployment
|
|
94
|
+
*/
|
|
95
|
+
invokeCommand(name: string, params: any): Promise<any>;
|
|
96
|
+
/**
|
|
97
|
+
* Set an external secret using platform-native tools
|
|
98
|
+
*/
|
|
99
|
+
setExternalSecret(vaultName: string, secretKey: string, secretValue: string): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Upgrade the deployment by creating a new release and updating the deployment.
|
|
102
|
+
* Only works for API-mode deployments (cloud platforms).
|
|
103
|
+
*/
|
|
104
|
+
upgrade(options?: UpgradeOptions): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Destroy the deployment.
|
|
107
|
+
*
|
|
108
|
+
* For dev mode: kills the `alien dev` process.
|
|
109
|
+
* For API mode: calls DELETE on platform API.
|
|
110
|
+
*/
|
|
111
|
+
destroy(): Promise<void>;
|
|
112
|
+
private resolveCliPath;
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/deploy.d.ts
|
|
116
|
+
/**
|
|
117
|
+
* Deploy an Alien application for testing.
|
|
118
|
+
*
|
|
119
|
+
* Uses local dev mode by default. Set `platform` to a cloud provider
|
|
120
|
+
* to deploy via the platform API (requires ALIEN_API_KEY env var).
|
|
121
|
+
*/
|
|
122
|
+
declare function deploy(options: DeployOptions): Promise<Deployment>;
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/errors.d.ts
|
|
125
|
+
declare const TestingOperationFailedError: {
|
|
126
|
+
metadata: _alienplatform_core0.AlienErrorMetadata<z.ZodObject<{
|
|
127
|
+
operation: z.ZodString;
|
|
128
|
+
message: z.ZodString;
|
|
129
|
+
details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
130
|
+
}, z.core.$strip>>;
|
|
131
|
+
contextSchema: z.ZodObject<{
|
|
132
|
+
operation: z.ZodString;
|
|
133
|
+
message: z.ZodString;
|
|
134
|
+
details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
135
|
+
}, z.core.$strip>;
|
|
136
|
+
create: (context: {
|
|
137
|
+
operation: string;
|
|
138
|
+
message: string;
|
|
139
|
+
details?: Record<string, unknown> | undefined;
|
|
140
|
+
}) => _alienplatform_core0.AlienErrorDefinition<z.ZodObject<{
|
|
141
|
+
operation: z.ZodString;
|
|
142
|
+
message: z.ZodString;
|
|
143
|
+
details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
144
|
+
}, z.core.$strip>>;
|
|
145
|
+
};
|
|
146
|
+
declare const TestingUnsupportedPlatformError: {
|
|
147
|
+
metadata: _alienplatform_core0.AlienErrorMetadata<z.ZodObject<{
|
|
148
|
+
platform: z.ZodString;
|
|
149
|
+
operation: z.ZodString;
|
|
150
|
+
}, z.core.$strip>>;
|
|
151
|
+
contextSchema: z.ZodObject<{
|
|
152
|
+
platform: z.ZodString;
|
|
153
|
+
operation: z.ZodString;
|
|
154
|
+
}, z.core.$strip>;
|
|
155
|
+
create: (context: {
|
|
156
|
+
platform: string;
|
|
157
|
+
operation: string;
|
|
158
|
+
}) => _alienplatform_core0.AlienErrorDefinition<z.ZodObject<{
|
|
159
|
+
platform: z.ZodString;
|
|
160
|
+
operation: z.ZodString;
|
|
161
|
+
}, z.core.$strip>>;
|
|
162
|
+
};
|
|
163
|
+
//#endregion
|
|
164
|
+
export { type DeployOptions, Deployment, type DeploymentInfo, type EnvironmentVariable, type Platform, TestingOperationFailedError, TestingUnsupportedPlatformError, type UpgradeOptions, deploy };
|
|
165
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/deployment.ts","../src/deploy.ts","../src/errors.ts"],"mappings":";;;;;;;;;;;;;AAUA;AAKA;AAUA;AAA8B,KAflB,QAAA,GAekB,OAAA,GAAA,KAAA,GAAA,KAAA,GAAA,OAAA;;;;AAoBb,UA9BA,mBAAA,CA+BQ;EAMR,IAAA,EAAA,MAAA;EAAc,KAAA,EAAA,MAAA;MAKlB,CAAA,EAAA,OAAA,GAAA,QAAA;iBAQD,CAAA,EAAA,MAAA,EAAA;;AAMZ;;;AAIoB,UAlDH,aAAA,CAuDwB;EAAY;;;;EC9DxC;EAAU,QAAA,CAAA,EDeV,QCfU;;sBAiBD,CAAA,EDCG,mBCDH,EAAA;;SAkCjB,CAAA,EAAA,OAAA;;;;;UDxBY,cAAA;yBACQ;;AEuBzB;;;AAA8D,UFjB7C,cAAA,CEiB6C;UAAR,EAAA;IAAO,GAAA,EAAA,MAAA;;;aFZhD;IGtDA,YAAA,EAAA,MAAA;IAWX,SAAA,CAAA,EAAA,MAAA;;;YHmDU;;;;;UAMK,cAAA;;;;YAIL;;;YAAQ,mBAAA,CAKqB;;;;;;AAvDxB,cCPJ,UAAA,CDOiB;EAAA,SAAA,EAAA,EAAA,MAAA;WAQjB,IAAA,EAAA,MAAA;WAGY,GAAA,EAAA,MAAA;EAAmB,SAAA,QAAA,ECdvB,QDcuB;EAS3B;EAOA,SAAA,EAAA,OAAc;EAAA,QAAA,OAAA;UAKlB,WAAA;UAQD,OAAA;EAAQ,QAAA,MAAA;EAMH,QAAA,MAAA;EAAc,WAAA,CAAA,MAAA,ECpCT,cDoCS;;;;4CCrBmB;;;AAhClD;EAAuB,iBAAA,CAAA,SAAA,EAAA,MAAA,EAAA,SAAA,EAAA,MAAA,EAAA,WAAA,EAAA,MAAA,CAAA,EAmDlB,OAnDkB,CAAA,IAAA,CAAA;;;;;SAsEE,CAAA,OAAA,CAAA,EAAA,cAAA,CAAA,EAAsB,OAAtB,CAAA,IAAA,CAAA;;;;;;;ECnBH,OAAA,CAAA,CAAM,EDiGT,OCjGS,CAAA,IAAA,CAAA;EAAA,QAAA,cAAA;;;;;;;;AFxB5B;AAOA;AAA+B,iBEiBT,MAAA,CFjBS,OAAA,EEiBO,aFjBP,CAAA,EEiBuB,OFjBvB,CEiB+B,UFjB/B,CAAA;;;cGjDlB;;;;IHOD,OAAQ,eAAA,YAAA,YAAA,cAAA,CAAA,CAAA;EAKH,CAAA,eAAA,CAAA,CAAA;EAUA,aAAA,aAAa,CAAA;IAAA,SAAA,aAAA;IAQjB,OAAA,aAAA;IAGY,OAAA,eAAA,YAAA,YAAA,cAAA,CAAA,CAAA;EAAmB,CAAA,eAAA,CAAA;EAS3B,MAAA,EAAA,CAAA,OAAA,EAAc;IAOd,SAAA,EAAA,MAAc;IAAA,OAAA,EAAA,MAAA;IAKlB,OAAA,CAAA,QAAA,CAAA,MAAA,EAAA,OAAA,CAAA,GAAA,SAAA;KAQD,4CAAA,YAAA,CAAA;IAAQ,SAAA,aAAA;IAMH,OAAA,aAAc;IAAA,OAAA,eAAA,YAAA,YAAA,cAAA,CAAA,CAAA;kBAInB,CAAA,CAAA;;AAKyC,cGhExC,+BHgEwC,EAAA;;;;EC9DxC,CAAA,eAAU,CAAA,CAAA;EAAA,aAAA,aAAA,CAAA;IAIF,QAAA,aAAA;IAaC,SAAA,aAAA;kBAe4B,CAAA;QAmB7C,EAAA,CAAA,OAAA,EAAA;IAmBoB,QAAA,EAAA,MAAA;IAAsB,SAAA,EAAA,MAAA;KA8E5B,4CAAA,YAAA,CAAA;IAAO,QAAA,aAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { n as TestingUnsupportedPlatformError, r as withTestingContext, t as TestingOperationFailedError } from "./errors.js";
|
|
2
|
+
import { execFile, spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { AlienError } from "@alienplatform/core";
|
|
7
|
+
import getPort from "get-port";
|
|
8
|
+
import { CommandsClient } from "@alienplatform/sdk/commands";
|
|
9
|
+
//#region src/deployment.ts
|
|
10
|
+
/**
|
|
11
|
+
* Deployment — handle to a deployed application for testing
|
|
12
|
+
*
|
|
13
|
+
* Supports two deployment modes:
|
|
14
|
+
* - Dev mode: manages a child `alien dev` process (local platform)
|
|
15
|
+
* - API mode: manages a deployment via platform API (cloud platforms)
|
|
16
|
+
*/
|
|
17
|
+
const execFileAsync$1 = promisify(execFile);
|
|
18
|
+
var Deployment = class {
|
|
19
|
+
id;
|
|
20
|
+
name;
|
|
21
|
+
url;
|
|
22
|
+
platform;
|
|
23
|
+
/** Whether the deployment has been destroyed */
|
|
24
|
+
destroyed = false;
|
|
25
|
+
process;
|
|
26
|
+
commandsUrl;
|
|
27
|
+
appPath;
|
|
28
|
+
apiUrl;
|
|
29
|
+
apiKey;
|
|
30
|
+
constructor(params) {
|
|
31
|
+
this.id = params.id;
|
|
32
|
+
this.name = params.name;
|
|
33
|
+
this.url = params.url;
|
|
34
|
+
this.platform = params.platform;
|
|
35
|
+
this.commandsUrl = params.commandsUrl;
|
|
36
|
+
this.process = params.process;
|
|
37
|
+
this.appPath = params.appPath;
|
|
38
|
+
this.apiUrl = params.apiUrl;
|
|
39
|
+
this.apiKey = params.apiKey;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Invoke a command on the deployment
|
|
43
|
+
*/
|
|
44
|
+
async invokeCommand(name, params) {
|
|
45
|
+
const token = this.apiKey ?? "";
|
|
46
|
+
return new CommandsClient({
|
|
47
|
+
managerUrl: this.commandsUrl,
|
|
48
|
+
deploymentId: this.id,
|
|
49
|
+
token,
|
|
50
|
+
allowLocalStorage: this.platform === "local"
|
|
51
|
+
}).invoke(name, params);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Set an external secret using platform-native tools
|
|
55
|
+
*/
|
|
56
|
+
async setExternalSecret(vaultName, secretKey, secretValue) {
|
|
57
|
+
const { setExternalSecret } = await import("./external-secrets.js");
|
|
58
|
+
const stateDir = this.appPath ? `${this.appPath}/.alien` : void 0;
|
|
59
|
+
await setExternalSecret(this.platform, this.name, vaultName, secretKey, secretValue, void 0, stateDir, this.id);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Upgrade the deployment by creating a new release and updating the deployment.
|
|
63
|
+
* Only works for API-mode deployments (cloud platforms).
|
|
64
|
+
*/
|
|
65
|
+
async upgrade(options = {}) {
|
|
66
|
+
if (!this.apiUrl || !this.apiKey) throw new Error("upgrade() requires a cloud deployment (not local dev mode)");
|
|
67
|
+
const headers = {
|
|
68
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
69
|
+
"Content-Type": "application/json"
|
|
70
|
+
};
|
|
71
|
+
await execFileAsync$1(this.resolveCliPath(), [
|
|
72
|
+
"build",
|
|
73
|
+
"--platform",
|
|
74
|
+
this.platform
|
|
75
|
+
], { cwd: this.appPath });
|
|
76
|
+
const stackPath = resolve(this.appPath, ".alien", "stack.json");
|
|
77
|
+
const stack = JSON.parse(readFileSync(stackPath, "utf-8"));
|
|
78
|
+
const releaseResp = await fetch(`${this.apiUrl}/v1/releases`, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers,
|
|
81
|
+
body: JSON.stringify({ stack })
|
|
82
|
+
});
|
|
83
|
+
if (!releaseResp.ok) {
|
|
84
|
+
const body = await releaseResp.text();
|
|
85
|
+
throw new Error(`Failed to create release for upgrade: ${releaseResp.status} ${body}`);
|
|
86
|
+
}
|
|
87
|
+
const release = await releaseResp.json();
|
|
88
|
+
const patchBody = { releaseId: release.id };
|
|
89
|
+
if (options.environmentVariables?.length) patchBody.environmentVariables = options.environmentVariables;
|
|
90
|
+
const patchResp = await fetch(`${this.apiUrl}/v1/deployments/${this.id}`, {
|
|
91
|
+
method: "PATCH",
|
|
92
|
+
headers,
|
|
93
|
+
body: JSON.stringify(patchBody)
|
|
94
|
+
});
|
|
95
|
+
if (!patchResp.ok) {
|
|
96
|
+
const body = await patchResp.text();
|
|
97
|
+
throw new Error(`Failed to update deployment for upgrade: ${patchResp.status} ${body}`);
|
|
98
|
+
}
|
|
99
|
+
const timeout = 3e5;
|
|
100
|
+
const start = Date.now();
|
|
101
|
+
while (Date.now() - start < timeout) {
|
|
102
|
+
const resp = await fetch(`${this.apiUrl}/v1/deployments/${this.id}`, { headers: { Authorization: `Bearer ${this.apiKey}` } });
|
|
103
|
+
if (resp.ok) {
|
|
104
|
+
const data = await resp.json();
|
|
105
|
+
if (data.releaseId === release.id && data.status === "running") return;
|
|
106
|
+
if (data.status === "error" || data.status.includes("failed")) throw new Error(`Deployment failed during upgrade with status: ${data.status}`);
|
|
107
|
+
}
|
|
108
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
109
|
+
}
|
|
110
|
+
throw new Error("Timeout waiting for deployment to pick up upgrade");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Destroy the deployment.
|
|
114
|
+
*
|
|
115
|
+
* For dev mode: kills the `alien dev` process.
|
|
116
|
+
* For API mode: calls DELETE on platform API.
|
|
117
|
+
*/
|
|
118
|
+
async destroy() {
|
|
119
|
+
if (this.destroyed) return;
|
|
120
|
+
if (this.process) {
|
|
121
|
+
if (!this.process.killed) {
|
|
122
|
+
this.process.kill("SIGTERM");
|
|
123
|
+
await new Promise((resolve) => {
|
|
124
|
+
const timeout = setTimeout(() => {
|
|
125
|
+
if (!this.process.killed) this.process.kill("SIGKILL");
|
|
126
|
+
resolve();
|
|
127
|
+
}, 5e3);
|
|
128
|
+
this.process.once("exit", () => {
|
|
129
|
+
clearTimeout(timeout);
|
|
130
|
+
resolve();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
this.destroyed = true;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (this.apiUrl && this.apiKey) {
|
|
138
|
+
const headers = { Authorization: `Bearer ${this.apiKey}` };
|
|
139
|
+
const resp = await fetch(`${this.apiUrl}/v1/deployments/${this.id}`, {
|
|
140
|
+
method: "DELETE",
|
|
141
|
+
headers
|
|
142
|
+
});
|
|
143
|
+
if (!resp.ok && resp.status !== 404) {
|
|
144
|
+
const body = await resp.text();
|
|
145
|
+
throw new Error(`Failed to destroy deployment: ${resp.status} ${body}`);
|
|
146
|
+
}
|
|
147
|
+
const timeout = 3e5;
|
|
148
|
+
const start = Date.now();
|
|
149
|
+
while (Date.now() - start < timeout) {
|
|
150
|
+
const checkResp = await fetch(`${this.apiUrl}/v1/deployments/${this.id}`, { headers });
|
|
151
|
+
if (checkResp.status === 404) break;
|
|
152
|
+
if (checkResp.ok) {
|
|
153
|
+
const data = await checkResp.json();
|
|
154
|
+
if (data.status === "destroyed" || data.status === "deleted") break;
|
|
155
|
+
}
|
|
156
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
this.destroyed = true;
|
|
160
|
+
}
|
|
161
|
+
resolveCliPath() {
|
|
162
|
+
const raw = process.env.ALIEN_CLI_PATH?.trim();
|
|
163
|
+
if (raw) return raw.includes("/") || raw.includes("\\") ? resolve(raw) : raw;
|
|
164
|
+
return "alien";
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/deploy.ts
|
|
169
|
+
/**
|
|
170
|
+
* deploy() — single entry point for deploying an Alien application for testing
|
|
171
|
+
*
|
|
172
|
+
* Auto-detects the deployment method based on the target platform:
|
|
173
|
+
* - local (default): spawns `alien dev` and watches its status file contract
|
|
174
|
+
* - aws / gcp / azure: builds + creates release/deployment via platform API — reads ALIEN_API_KEY from env
|
|
175
|
+
*/
|
|
176
|
+
const execFileAsync = promisify(execFile);
|
|
177
|
+
/**
|
|
178
|
+
* Get the alien CLI path with fallback discovery.
|
|
179
|
+
*
|
|
180
|
+
* Resolution order:
|
|
181
|
+
* 1. ALIEN_CLI_PATH (if set)
|
|
182
|
+
* 2. nearest ../target/debug/alien (or alien.exe) walking upward from app path
|
|
183
|
+
* 3. "alien" from PATH
|
|
184
|
+
*/
|
|
185
|
+
function getAlienCliPath(appPath) {
|
|
186
|
+
const raw = process.env.ALIEN_CLI_PATH?.trim();
|
|
187
|
+
if (raw) {
|
|
188
|
+
if (raw.includes("/") || raw.includes("\\")) return resolve(raw);
|
|
189
|
+
return raw;
|
|
190
|
+
}
|
|
191
|
+
let current = resolve(appPath);
|
|
192
|
+
while (true) {
|
|
193
|
+
const unixCandidate = join(current, "target", "debug", "alien");
|
|
194
|
+
if (existsSync(unixCandidate)) return unixCandidate;
|
|
195
|
+
const windowsCandidate = `${unixCandidate}.exe`;
|
|
196
|
+
if (existsSync(windowsCandidate)) return windowsCandidate;
|
|
197
|
+
const parent = dirname(current);
|
|
198
|
+
if (parent === current) break;
|
|
199
|
+
current = parent;
|
|
200
|
+
}
|
|
201
|
+
return "alien";
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Deploy an Alien application for testing.
|
|
205
|
+
*
|
|
206
|
+
* Uses local dev mode by default. Set `platform` to a cloud provider
|
|
207
|
+
* to deploy via the platform API (requires ALIEN_API_KEY env var).
|
|
208
|
+
*/
|
|
209
|
+
async function deploy(options) {
|
|
210
|
+
if ((options.platform ?? "local") === "local") return deployViaDev(options);
|
|
211
|
+
return deployViaApi(options);
|
|
212
|
+
}
|
|
213
|
+
async function deployViaDev(options) {
|
|
214
|
+
const verbose = options.verbose ?? process.env.VERBOSE === "true";
|
|
215
|
+
const port = await getPort();
|
|
216
|
+
const cliPath = getAlienCliPath(options.app);
|
|
217
|
+
const statusFile = join(options.app, ".alien", "testing-dev-status.json");
|
|
218
|
+
const args = [
|
|
219
|
+
"dev",
|
|
220
|
+
"--port",
|
|
221
|
+
String(port),
|
|
222
|
+
"--status-file",
|
|
223
|
+
statusFile
|
|
224
|
+
];
|
|
225
|
+
if (options.config) args.push("--config", options.config);
|
|
226
|
+
for (const ev of options.environmentVariables ?? []) {
|
|
227
|
+
const flag = ev.type === "secret" ? "--secret" : "--env";
|
|
228
|
+
const targets = ev.targetResources?.length ? `:${ev.targetResources.join(",")}` : "";
|
|
229
|
+
args.push(flag, `${ev.name}=${ev.value}${targets}`);
|
|
230
|
+
}
|
|
231
|
+
if (verbose) console.log(`[testing] Spawning: ${cliPath} ${args.join(" ")}`);
|
|
232
|
+
rmSync(join(options.app, ".alien"), {
|
|
233
|
+
recursive: true,
|
|
234
|
+
force: true
|
|
235
|
+
});
|
|
236
|
+
const childEnv = { ...process.env };
|
|
237
|
+
const proc = spawn(cliPath, args, {
|
|
238
|
+
cwd: options.app,
|
|
239
|
+
env: childEnv,
|
|
240
|
+
stdio: [
|
|
241
|
+
"ignore",
|
|
242
|
+
"pipe",
|
|
243
|
+
"pipe"
|
|
244
|
+
]
|
|
245
|
+
});
|
|
246
|
+
let stdout = "";
|
|
247
|
+
let stderr = "";
|
|
248
|
+
proc.stdout?.on("data", (data) => {
|
|
249
|
+
stdout += data.toString();
|
|
250
|
+
if (verbose) process.stdout.write(data);
|
|
251
|
+
});
|
|
252
|
+
proc.stderr?.on("data", (data) => {
|
|
253
|
+
stderr += data.toString();
|
|
254
|
+
if (verbose) process.stderr.write(data);
|
|
255
|
+
});
|
|
256
|
+
let exited = false;
|
|
257
|
+
let exitCode = null;
|
|
258
|
+
proc.on("exit", (code) => {
|
|
259
|
+
exited = true;
|
|
260
|
+
exitCode = code;
|
|
261
|
+
});
|
|
262
|
+
proc.on("error", (err) => {
|
|
263
|
+
exited = true;
|
|
264
|
+
exitCode = 1;
|
|
265
|
+
stderr += `\nFailed to spawn alien CLI: ${err.message}`;
|
|
266
|
+
});
|
|
267
|
+
try {
|
|
268
|
+
const agent = findPrimaryAgent(await waitForDevStatusReady(statusFile, () => exited, () => exitCode, () => stderr));
|
|
269
|
+
const publicUrl = findPublicUrl(agent.resources);
|
|
270
|
+
if (!publicUrl) throw new AlienError(TestingOperationFailedError.create({
|
|
271
|
+
operation: "resolve-public-url",
|
|
272
|
+
message: "No public URL found in deployment resources",
|
|
273
|
+
details: { resources: agent.resources }
|
|
274
|
+
}));
|
|
275
|
+
if (verbose) {
|
|
276
|
+
console.log(`[testing] Public URL: ${publicUrl}`);
|
|
277
|
+
if (agent.commandsUrl) console.log(`[testing] Commands URL: ${agent.commandsUrl}`);
|
|
278
|
+
}
|
|
279
|
+
if (!agent.commandsUrl) throw new AlienError(TestingOperationFailedError.create({
|
|
280
|
+
operation: "resolve-commands-url",
|
|
281
|
+
message: "alien dev status file did not include a commands URL",
|
|
282
|
+
details: { agent }
|
|
283
|
+
}));
|
|
284
|
+
return new Deployment({
|
|
285
|
+
id: agent.id,
|
|
286
|
+
name: agent.name,
|
|
287
|
+
url: publicUrl,
|
|
288
|
+
platform: options.platform ?? "local",
|
|
289
|
+
commandsUrl: agent.commandsUrl,
|
|
290
|
+
process: proc,
|
|
291
|
+
appPath: options.app
|
|
292
|
+
});
|
|
293
|
+
} catch (error) {
|
|
294
|
+
proc.kill("SIGTERM");
|
|
295
|
+
throw await withTestingContext(error, "deploy", "Failed while waiting for alien dev to become ready", {
|
|
296
|
+
statusFile,
|
|
297
|
+
appPath: options.app,
|
|
298
|
+
platform: options.platform ?? "local"
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async function deployViaApi(options) {
|
|
303
|
+
const platform = options.platform ?? "local";
|
|
304
|
+
const verbose = options.verbose ?? process.env.VERBOSE === "true";
|
|
305
|
+
const apiKey = process.env.ALIEN_API_KEY;
|
|
306
|
+
if (!apiKey) throw new Error(`Cloud deployment (platform: '${platform}') requires the ALIEN_API_KEY environment variable to be set`);
|
|
307
|
+
const apiUrl = process.env.ALIEN_API_URL ?? "https://api.alien.dev";
|
|
308
|
+
const headers = {
|
|
309
|
+
Authorization: `Bearer ${apiKey}`,
|
|
310
|
+
"Content-Type": "application/json"
|
|
311
|
+
};
|
|
312
|
+
if (verbose) console.log("[testing:api] Building application...");
|
|
313
|
+
const cliPath = getAlienCliPath(options.app);
|
|
314
|
+
const buildArgs = [
|
|
315
|
+
"build",
|
|
316
|
+
"--platform",
|
|
317
|
+
platform
|
|
318
|
+
];
|
|
319
|
+
if (options.config) buildArgs.push("--config", options.config);
|
|
320
|
+
await execFileAsync(cliPath, buildArgs, { cwd: options.app });
|
|
321
|
+
const stackPath = resolve(options.app, ".alien", "stack.json");
|
|
322
|
+
if (!existsSync(stackPath)) throw new Error(`Build did not produce stack.json at ${stackPath}`);
|
|
323
|
+
const stack = JSON.parse(readFileSync(stackPath, "utf-8"));
|
|
324
|
+
if (verbose) console.log("[testing:api] Creating release...");
|
|
325
|
+
const releaseResp = await fetch(`${apiUrl}/v1/releases`, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
headers,
|
|
328
|
+
body: JSON.stringify({ stack })
|
|
329
|
+
});
|
|
330
|
+
if (!releaseResp.ok) {
|
|
331
|
+
const body = await releaseResp.text();
|
|
332
|
+
throw new Error(`Failed to create release: ${releaseResp.status} ${body}`);
|
|
333
|
+
}
|
|
334
|
+
const release = await releaseResp.json();
|
|
335
|
+
if (verbose) console.log("[testing:api] Creating deployment...");
|
|
336
|
+
const deploymentName = `e2e-${Date.now()}`;
|
|
337
|
+
const deployBody = {
|
|
338
|
+
releaseId: release.id,
|
|
339
|
+
platform,
|
|
340
|
+
name: deploymentName
|
|
341
|
+
};
|
|
342
|
+
if (options.environmentVariables?.length) deployBody.environmentVariables = options.environmentVariables;
|
|
343
|
+
const deployResp = await fetch(`${apiUrl}/v1/deployments`, {
|
|
344
|
+
method: "POST",
|
|
345
|
+
headers,
|
|
346
|
+
body: JSON.stringify(deployBody)
|
|
347
|
+
});
|
|
348
|
+
if (!deployResp.ok) {
|
|
349
|
+
const body = await deployResp.text();
|
|
350
|
+
throw new Error(`Failed to create deployment: ${deployResp.status} ${body}`);
|
|
351
|
+
}
|
|
352
|
+
const deployment = await deployResp.json();
|
|
353
|
+
if (verbose) console.log(`[testing:api] Waiting for deployment ${deployment.id} to be running...`);
|
|
354
|
+
const running = await waitForPlatformDeploymentRunning(apiUrl, apiKey, deployment.id, verbose);
|
|
355
|
+
return new Deployment({
|
|
356
|
+
id: running.id,
|
|
357
|
+
name: running.name,
|
|
358
|
+
url: running.url,
|
|
359
|
+
platform,
|
|
360
|
+
commandsUrl: running.commandsUrl,
|
|
361
|
+
appPath: options.app,
|
|
362
|
+
apiUrl,
|
|
363
|
+
apiKey
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Wait for `alien dev` to report a ready local session via its status file.
|
|
368
|
+
*/
|
|
369
|
+
async function waitForDevStatusReady(statusFile, hasExited, getExitCode, getStderr) {
|
|
370
|
+
const timeout = 9e5;
|
|
371
|
+
const pollInterval = 500;
|
|
372
|
+
const start = Date.now();
|
|
373
|
+
while (Date.now() - start < timeout) {
|
|
374
|
+
if (hasExited()) throw new AlienError(TestingOperationFailedError.create({
|
|
375
|
+
operation: "wait-for-ready",
|
|
376
|
+
message: "alien dev exited before the local session became ready",
|
|
377
|
+
details: {
|
|
378
|
+
exitCode: getExitCode(),
|
|
379
|
+
stderrTail: getStderr().slice(-1e3)
|
|
380
|
+
}
|
|
381
|
+
}));
|
|
382
|
+
if (!existsSync(statusFile)) {
|
|
383
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
const status = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
388
|
+
if (status.status === "error") throw new AlienError(TestingOperationFailedError.create({
|
|
389
|
+
operation: "wait-for-ready",
|
|
390
|
+
message: "alien dev reported an error status",
|
|
391
|
+
details: { status }
|
|
392
|
+
}));
|
|
393
|
+
if (status.status === "ready" && Object.keys(status.agents ?? {}).length > 0) return status;
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (error instanceof AlienError && error.code === "TESTING_OPERATION_FAILED") throw error;
|
|
396
|
+
}
|
|
397
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
398
|
+
}
|
|
399
|
+
throw new AlienError(TestingOperationFailedError.create({
|
|
400
|
+
operation: "wait-for-ready",
|
|
401
|
+
message: `Timeout waiting for alien dev to report readiness (${timeout / 1e3}s)`,
|
|
402
|
+
details: {
|
|
403
|
+
statusFile,
|
|
404
|
+
timeoutMs: timeout
|
|
405
|
+
}
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
function findPrimaryAgent(status) {
|
|
409
|
+
const agent = Object.values(status.agents ?? {})[0];
|
|
410
|
+
if (!agent) throw new AlienError(TestingOperationFailedError.create({
|
|
411
|
+
operation: "resolve-agent",
|
|
412
|
+
message: "alien dev reported readiness but no agents were present in the status file",
|
|
413
|
+
details: { status }
|
|
414
|
+
}));
|
|
415
|
+
return agent;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Wait for a platform API deployment to reach "running" status.
|
|
419
|
+
*/
|
|
420
|
+
async function waitForPlatformDeploymentRunning(apiUrl, apiKey, deploymentId, verbose) {
|
|
421
|
+
const timeout = 9e5;
|
|
422
|
+
const pollInterval = 5e3;
|
|
423
|
+
const start = Date.now();
|
|
424
|
+
const headers = { Authorization: `Bearer ${apiKey}` };
|
|
425
|
+
while (Date.now() - start < timeout) {
|
|
426
|
+
try {
|
|
427
|
+
const resp = await fetch(`${apiUrl}/v1/deployments/${deploymentId}`, {
|
|
428
|
+
headers,
|
|
429
|
+
signal: AbortSignal.timeout(1e4)
|
|
430
|
+
});
|
|
431
|
+
if (resp.ok) {
|
|
432
|
+
const data = await resp.json();
|
|
433
|
+
if (verbose) console.log(`[testing] Deployment ${deploymentId} status: ${data.status}`);
|
|
434
|
+
if (data.status === "running") {
|
|
435
|
+
if (!data.url) throw new Error(`Deployment is running but has no URL: ${JSON.stringify(data)}`);
|
|
436
|
+
return data;
|
|
437
|
+
}
|
|
438
|
+
if (data.status === "error" || data.status.includes("failed")) throw new Error(`Deployment failed with status: ${data.status}`);
|
|
439
|
+
}
|
|
440
|
+
} catch (error) {
|
|
441
|
+
if (error instanceof Error && error.message.includes("failed with status")) throw error;
|
|
442
|
+
}
|
|
443
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
444
|
+
}
|
|
445
|
+
throw new Error(`Timeout waiting for deployment ${deploymentId} to reach running status (${timeout / 1e3}s)`);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Find the public URL from deployment resources
|
|
449
|
+
*/
|
|
450
|
+
function findPublicUrl(resources) {
|
|
451
|
+
for (const [name, resource] of Object.entries(resources)) if (resource.url && (name.includes("router") || name.includes("gateway") || name.includes("proxy"))) return resource.url;
|
|
452
|
+
const publicResources = Object.entries(resources).filter(([_, r]) => (r.resourceType === "container" || r.resourceType === "function") && r.url);
|
|
453
|
+
if (publicResources.length > 0) return publicResources[publicResources.length - 1][1].url;
|
|
454
|
+
}
|
|
455
|
+
//#endregion
|
|
456
|
+
export { Deployment, TestingOperationFailedError, TestingUnsupportedPlatformError, deploy };
|
|
457
|
+
|
|
458
|
+
//# sourceMappingURL=index.js.map
|