carlin 1.40.2 → 1.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -330
  2. package/dist/index.js +219 -56
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -1,340 +1,12 @@
1
1
  # carlin
2
2
 
3
- **carlin** is a CLI tool for deploying AWS cloud resources using CloudFormation templates. It streamlines infrastructure deployment with automatic Lambda code building, S3 upload handling, stack naming, and multi-environment support.
4
-
5
- ## Installation
3
+ CLI tool for deploying AWS cloud resources using CloudFormation templates.
6
4
 
7
5
  ```bash
8
6
  pnpm add -D carlin
9
7
  ```
10
8
 
11
- Or install globally:
12
-
13
- ```bash
14
- pnpm add -g carlin
15
- ```
16
-
17
- ## Quick Start
18
-
19
- Deploy your CloudFormation template with a single command:
20
-
21
- ```bash
22
- carlin deploy
23
- ```
24
-
25
- carlin automatically:
26
-
27
- - Searches for your CloudFormation template (`cloudformation.ts`, `cloudformation.yml`, or `template.yml`)
28
- - Builds and uploads Lambda function code to S3
29
- - Creates or updates your CloudFormation stack
30
- - Displays stack outputs after deployment
31
-
32
- ### First Deployment Example
33
-
34
- Create a simple CloudFormation template:
35
-
36
- ```typescript
37
- // cloudformation.ts
38
- export const template = {
39
- Resources: {
40
- MyBucket: {
41
- Type: 'AWS::S3::Bucket',
42
- Properties: {
43
- BucketName: 'my-app-bucket',
44
- },
45
- },
46
- },
47
- Outputs: {
48
- BucketName: {
49
- Value: { Ref: 'MyBucket' },
50
- },
51
- },
52
- };
53
- ```
54
-
55
- Deploy it:
56
-
57
- ```bash
58
- carlin deploy
59
- ```
60
-
61
- That's it! carlin creates the stack and outputs the bucket name.
62
-
63
- ## Core Concepts
64
-
65
- ### Stack Naming
66
-
67
- carlin automatically generates CloudFormation stack names based on:
68
-
69
- 1. Package name from `package.json`
70
- 2. Environment (staging, production) or branch name
71
- 3. Custom `--stack-name` option (overrides automatic naming)
72
-
73
- Example stack names:
74
-
75
- - `my-app-production` (package: `my-app`, environment: `production`)
76
- - `my-app-feature-auth` (package: `my-app`, branch: `feature/auth`)
77
- - `CustomStack` (using `--stack-name CustomStack`)
78
-
79
- ### Environments
80
-
81
- Define environments using:
82
-
83
- - `--environment` flag: `carlin deploy --environment production`
84
- - `CARLIN_ENVIRONMENT` variable: `CARLIN_ENVIRONMENT=staging carlin deploy`
85
- - Config file: `carlin.yml` with `environment: production`
86
-
87
- Environments enable:
88
-
89
- - Automatic termination protection for production stacks
90
- - Environment-specific configurations
91
- - Multi-stage deployment workflows
92
-
93
- ### Lambda Functions
94
-
95
- carlin automatically handles Lambda deployment:
96
-
97
- 1. Detects Lambda functions in your CloudFormation template
98
- 2. Builds code using esbuild
99
- 3. Uploads to S3 with versioning
100
- 4. Injects S3 parameters into your template
101
-
102
- Example Lambda function:
103
-
104
- ```typescript
105
- // src/handler.ts
106
- export const handler = async (event) => {
107
- return { statusCode: 200, body: 'Hello World' };
108
- };
109
-
110
- // cloudformation.ts
111
- export const template = {
112
- Resources: {
113
- MyFunction: {
114
- Type: 'AWS::Lambda::Function',
115
- Properties: {
116
- Runtime: 'nodejs20.x',
117
- Handler: 'handler.handler', // carlin finds and builds src/handler.ts
118
- Code: {
119
- S3Bucket: { Ref: 'LambdaS3Bucket' },
120
- S3Key: { Ref: 'LambdaS3Key' },
121
- S3ObjectVersion: { Ref: 'LambdaS3ObjectVersion' },
122
- },
123
- },
124
- },
125
- },
126
- };
127
- ```
128
-
129
- ### Base Stack
130
-
131
- The base stack provides shared infrastructure:
132
-
133
- - S3 bucket for Lambda code storage
134
- - CloudFront distribution for static apps
135
- - Lambda image builder for container deployments
136
-
137
- Deploy base stack once per environment:
138
-
139
- ```bash
140
- carlin deploy base-stack --environment production
141
- ```
142
-
143
- ## Commands
144
-
145
- ### deploy
146
-
147
- Deploy CloudFormation stacks and resources.
148
-
149
- ```bash
150
- carlin deploy [options]
151
- ```
152
-
153
- #### Main Options
154
-
155
- - `--template-path <path>` - CloudFormation template file path
156
- - `--stack-name <name>` - Custom stack name
157
- - `--parameters <json>` - CloudFormation parameters as JSON object
158
- - `--environment <env>` - Environment name (enables termination protection)
159
- - `--region <region>` - AWS region (default: `us-east-1`)
160
- - `--destroy` - Delete the stack instead of deploying
161
-
162
- #### Subcommands
163
-
164
- - `deploy static-app` - Deploy static websites to S3 + CloudFront
165
- - `deploy lambda-layer` - Deploy Lambda layers
166
- - `deploy base-stack` - Deploy base infrastructure stack
167
- - `deploy cicd` - Deploy CI/CD pipeline infrastructure
168
- - `deploy vercel` - Deploy to Vercel platform
169
- - `deploy describe` - Show stack outputs without deploying
170
-
171
- See [full command documentation](https://ttoss.dev/docs/carlin/commands/deploy) for all options and examples.
172
-
173
- ### generate-env
174
-
175
- Generate `.env` files from CloudFormation stack outputs.
176
-
177
- ```bash
178
- carlin generate-env --stack-name MyStack --output .env
179
- ```
180
-
181
- Fetches stack outputs and creates environment variables file:
182
-
183
- ```bash
184
- # Generated by carlin
185
- API_URL=https://api.example.com
186
- BUCKET_NAME=my-app-bucket-xyz123
187
- ```
188
-
189
- ### cicd-ecs-task-report
190
-
191
- Report ECS task status to GitHub/Slack during CI/CD deployments.
192
-
193
- ```bash
194
- carlin cicd-ecs-task-report --cluster my-cluster --task-arn <arn>
195
- ```
196
-
197
- ## Configuration
198
-
199
- Configure carlin using CLI options, environment variables, or config files.
200
-
201
- ### Config File
202
-
203
- Create `carlin.yml` in your project root:
204
-
205
- ```yaml
206
- environment: staging
207
- region: us-east-1
208
- stackName: my-custom-stack
209
- parameters:
210
- DomainName: example.com
211
- CertificateArn: arn:aws:acm:...
212
- ```
213
-
214
- ### Environment Variables
215
-
216
- Prefix any option with `CARLIN_`:
217
-
218
- ```bash
219
- CARLIN_ENVIRONMENT=production carlin deploy
220
- CARLIN_REGION=eu-west-1 carlin deploy
221
- CARLIN_STACK_NAME=CustomStack carlin deploy
222
- ```
223
-
224
- ### Priority
225
-
226
- Configuration priority (highest to lowest):
227
-
228
- 1. CLI options (`--environment production`)
229
- 2. Environment variables (`CARLIN_ENVIRONMENT=production`)
230
- 3. Config file (`carlin.yml`)
231
- 4. Defaults
232
-
233
- ## Advanced Usage
234
-
235
- ### Multi-Environment Deployment
236
-
237
- Deploy to multiple environments with different configurations:
238
-
239
- ```bash
240
- # Deploy to staging
241
- carlin deploy --environment staging --parameters '{"InstanceType":"t3.micro"}'
242
-
243
- # Deploy to production
244
- carlin deploy --environment production --parameters '{"InstanceType":"t3.large"}'
245
- ```
246
-
247
- ### Custom Template Paths
248
-
249
- Search for templates in custom locations:
250
-
251
- ```bash
252
- carlin deploy --template-path infrastructure/cloudformation.ts
253
- ```
254
-
255
- ### Docker-Based Lambda Functions
256
-
257
- Build Lambda functions from Dockerfiles:
258
-
259
- ```bash
260
- carlin deploy --lambda-dockerfile Dockerfile
261
- ```
262
-
263
- carlin automatically:
264
-
265
- - Builds the Docker image
266
- - Pushes to ECR
267
- - Updates Lambda function with new image URI
268
-
269
- ### Termination Protection
270
-
271
- Production stacks are protected from accidental deletion:
272
-
273
- ```bash
274
- # This fails if environment is defined
275
- carlin deploy --destroy --environment production
276
-
277
- # Force deletion by removing environment
278
- carlin deploy --destroy --stack-name my-stack-production
279
- ```
280
-
281
- ### Branch-Based Deployments
282
-
283
- Deploy feature branches for testing:
284
-
285
- ```bash
286
- # On feature/auth branch
287
- carlin deploy
288
- # Creates stack: my-app-feature-auth
289
-
290
- # On main branch
291
- carlin deploy --environment production
292
- # Creates stack: my-app-production
293
- ```
294
-
295
- ## Troubleshooting
296
-
297
- ### Stack Already Exists
298
-
299
- If deployment fails with "Stack already exists", check:
300
-
301
- - Stack name conflicts (use `--stack-name` to customize)
302
- - Existing stacks in AWS Console (CloudFormation → Stacks)
303
- - Branch name conflicts (different branches may create same stack name)
304
-
305
- ### Lambda Build Failures
306
-
307
- If Lambda code building fails:
308
-
309
- - Verify `Handler` path points to existing file (`src/handler.ts`)
310
- - Check `--lambda-entry-points-base-dir` matches your source directory
311
- - Review build logs for missing dependencies
312
-
313
- ### Template Too Large
314
-
315
- CloudFormation templates over 51,200 bytes must be uploaded to S3:
316
-
317
- - carlin automatically uploads large templates to base stack bucket
318
- - Ensure base stack exists: `carlin deploy base-stack`
319
- - Or reduce template size by removing comments/whitespace
320
-
321
- ### Permission Errors
322
-
323
- Ensure AWS credentials have necessary permissions:
324
-
325
- - `cloudformation:*` for stack operations
326
- - `s3:*` for Lambda code uploads
327
- - `iam:*` for creating roles
328
- - `lambda:*` for function deployments
329
-
330
- ## Examples
331
-
332
- - [Terezinha Farm API](https://github.com/ttoss/ttoss/tree/main/terezinha-farm/api)
333
- - [POC - AWS Serverless REST API](https://github.com/ttoss/poc-aws-serverless-rest-api)
334
-
335
- ## Documentation
336
-
337
- Full documentation: https://ttoss.dev/docs/carlin/
9
+ **[Documentation →](https://ttoss.dev/docs/carlin/)**
338
10
 
339
11
  ## License
340
12
 
package/dist/index.js CHANGED
@@ -997,24 +997,24 @@ var getStackName = /* @__PURE__ */ __name(async () => {
997
997
  }).join("-");
998
998
  return limitStackName(name);
999
999
  }, "getStackName");
1000
- var deployErrorLogs = /* @__PURE__ */ __name(({ error, logPrefix: logPrefix23 }) => {
1001
- log5.error(logPrefix23, `An error occurred. Cannot deploy ${logPrefix23}.`);
1002
- log5.error(logPrefix23, "Error message: %j", error?.message);
1000
+ var deployErrorLogs = /* @__PURE__ */ __name(({ error, logPrefix: logPrefix24 }) => {
1001
+ log5.error(logPrefix24, `An error occurred. Cannot deploy ${logPrefix24}.`);
1002
+ log5.error(logPrefix24, "Error message: %j", error?.message);
1003
1003
  }, "deployErrorLogs");
1004
- var handleDeployError = /* @__PURE__ */ __name(({ error, logPrefix: logPrefix23 }) => {
1004
+ var handleDeployError = /* @__PURE__ */ __name(({ error, logPrefix: logPrefix24 }) => {
1005
1005
  deployErrorLogs({
1006
1006
  error,
1007
- logPrefix: logPrefix23
1007
+ logPrefix: logPrefix24
1008
1008
  });
1009
1009
  process.exit(1);
1010
1010
  }, "handleDeployError");
1011
- var handleDeployInitialization = /* @__PURE__ */ __name(async ({ logPrefix: logPrefix23, stackName: preDefinedStackName }) => {
1012
- log5.info(logPrefix23, `Starting deploy ${logPrefix23}...`);
1011
+ var handleDeployInitialization = /* @__PURE__ */ __name(async ({ logPrefix: logPrefix24, stackName: preDefinedStackName }) => {
1012
+ log5.info(logPrefix24, `Starting deploy ${logPrefix24}...`);
1013
1013
  if (preDefinedStackName) {
1014
1014
  setPreDefinedStackName(preDefinedStackName);
1015
1015
  }
1016
1016
  const stackName = await getStackName();
1017
- log5.info(logPrefix23, `stackName: ${stackName}`);
1017
+ log5.info(logPrefix24, `stackName: ${stackName}`);
1018
1018
  return {
1019
1019
  stackName
1020
1020
  };
@@ -3726,6 +3726,154 @@ var readDockerfile = /* @__PURE__ */ __name((dockerfilePath) => {
3726
3726
  return "";
3727
3727
  }
3728
3728
  }, "readDockerfile");
3729
+ var logPrefix14 = "report";
3730
+ var getGitHubErrorMessage = /* @__PURE__ */ __name(async (response) => {
3731
+ try {
3732
+ const body = await response.json();
3733
+ if (body.message) {
3734
+ return `${response.status} ${response.statusText} - ${body.message}`;
3735
+ }
3736
+ } catch {
3737
+ }
3738
+ return `${response.status} ${response.statusText}`;
3739
+ }, "getGitHubErrorMessage");
3740
+ var GITHUB_PR_COMMENT_MARKER = "<!-- carlin-deploy-outputs -->";
3741
+ var readAllDeployFiles = /* @__PURE__ */ __name(async () => {
3742
+ const files = await glob("**/.carlin/*.json", {
3743
+ absolute: true,
3744
+ ignore: [
3745
+ "**/node_modules/**",
3746
+ `**/.carlin/${LATEST_DEPLOY_OUTPUTS_FILENAME}`
3747
+ ]
3748
+ });
3749
+ const results = [];
3750
+ for (const file of files) {
3751
+ try {
3752
+ const raw = await fs3.promises.readFile(file, "utf-8");
3753
+ const content = JSON.parse(raw);
3754
+ if (content.stackName && content.outputs) {
3755
+ results.push(content);
3756
+ }
3757
+ } catch {
3758
+ log5.warn(logPrefix14, `Could not read deploy file: ${path.basename(file)}`);
3759
+ }
3760
+ }
3761
+ return results.sort((a, b) => {
3762
+ return a.packageName.localeCompare(b.packageName);
3763
+ });
3764
+ }, "readAllDeployFiles");
3765
+ var buildMarkdownComment = /* @__PURE__ */ __name((deploys) => {
3766
+ const header = `${GITHUB_PR_COMMENT_MARKER}
3767
+
3768
+ ## Deploy Outputs
3769
+ `;
3770
+ if (deploys.length === 0) {
3771
+ return `${header}
3772
+ No deploy outputs found.`;
3773
+ }
3774
+ const rows = deploys.flatMap(({ packageName, stackName, outputs }) => {
3775
+ return Object.values(outputs).map(({ OutputKey, OutputValue }) => {
3776
+ return `| \`${packageName}\` | \`${stackName}\` | \`${OutputKey}\` | ${OutputValue} |`;
3777
+ });
3778
+ });
3779
+ const table = [
3780
+ "| Package | Stack | Output Key | Output Value |",
3781
+ "|---------|-------|------------|--------------|",
3782
+ ...rows
3783
+ ].join("\n");
3784
+ return `${header}
3785
+ ${table}`;
3786
+ }, "buildMarkdownComment");
3787
+ var getPrNumber = /* @__PURE__ */ __name(async ({ branch, repo, token }) => {
3788
+ const [owner] = repo.split("/");
3789
+ const response = await fetch(`https://api.github.com/repos/${repo}/pulls?head=${owner}:${branch}&state=open&per_page=1`, {
3790
+ headers: {
3791
+ Accept: "application/vnd.github+json",
3792
+ Authorization: `Bearer ${token}`,
3793
+ "X-GitHub-Api-Version": "2022-11-28"
3794
+ }
3795
+ });
3796
+ if (!response.ok) {
3797
+ throw new Error(`GitHub API error fetching PR: ${await getGitHubErrorMessage(response)}`);
3798
+ }
3799
+ const prs = await response.json();
3800
+ if (prs.length === 0) {
3801
+ throw new Error(`No open PR found for branch: ${branch}`);
3802
+ }
3803
+ return prs[0].number;
3804
+ }, "getPrNumber");
3805
+ var findExistingComment = /* @__PURE__ */ __name(async ({ prNumber, repo, token }) => {
3806
+ const response = await fetch(`https://api.github.com/repos/${repo}/issues/${prNumber}/comments?per_page=100`, {
3807
+ headers: {
3808
+ Accept: "application/vnd.github+json",
3809
+ Authorization: `Bearer ${token}`,
3810
+ "X-GitHub-Api-Version": "2022-11-28"
3811
+ }
3812
+ });
3813
+ if (!response.ok) {
3814
+ throw new Error(`GitHub API error fetching comments: ${await getGitHubErrorMessage(response)}`);
3815
+ }
3816
+ const comments = await response.json();
3817
+ return comments.find(({ body }) => {
3818
+ return body.includes(GITHUB_PR_COMMENT_MARKER);
3819
+ });
3820
+ }, "findExistingComment");
3821
+ var createOrUpdateComment = /* @__PURE__ */ __name(async ({ body, existingCommentId, prNumber, repo, token }) => {
3822
+ const url = existingCommentId ? `https://api.github.com/repos/${repo}/issues/comments/${existingCommentId}` : `https://api.github.com/repos/${repo}/issues/${prNumber}/comments`;
3823
+ const method = existingCommentId ? "PATCH" : "POST";
3824
+ const response = await fetch(url, {
3825
+ body: JSON.stringify({
3826
+ body
3827
+ }),
3828
+ headers: {
3829
+ Accept: "application/vnd.github+json",
3830
+ Authorization: `Bearer ${token}`,
3831
+ "Content-Type": "application/json",
3832
+ "X-GitHub-Api-Version": "2022-11-28"
3833
+ },
3834
+ method
3835
+ });
3836
+ if (!response.ok) {
3837
+ throw new Error(`GitHub API error ${method} comment: ${await getGitHubErrorMessage(response)}`);
3838
+ }
3839
+ }, "createOrUpdateComment");
3840
+ var reportToGitHubPR = /* @__PURE__ */ __name(async () => {
3841
+ const token = process.env.GH_TOKEN;
3842
+ const repo = process.env.GITHUB_REPOSITORY;
3843
+ const branch = process.env.CARLIN_BRANCH;
3844
+ if (!token) {
3845
+ throw new Error("GH_TOKEN environment variable is required for --channel=github-pr");
3846
+ }
3847
+ if (!repo) {
3848
+ throw new Error("GITHUB_REPOSITORY environment variable is required for --channel=github-pr");
3849
+ }
3850
+ if (!branch) {
3851
+ throw new Error("CARLIN_BRANCH environment variable is required for --channel=github-pr");
3852
+ }
3853
+ log5.info(logPrefix14, "Reading deploy outputs from workspace...");
3854
+ const deploys = await readAllDeployFiles();
3855
+ log5.info(logPrefix14, `Found ${deploys.length} deploy file(s).`);
3856
+ const prNumber = await getPrNumber({
3857
+ branch,
3858
+ repo,
3859
+ token
3860
+ });
3861
+ log5.info(logPrefix14, `Reporting to PR #${prNumber}...`);
3862
+ const body = buildMarkdownComment(deploys);
3863
+ const existingComment = await findExistingComment({
3864
+ prNumber,
3865
+ repo,
3866
+ token
3867
+ });
3868
+ await createOrUpdateComment({
3869
+ body,
3870
+ existingCommentId: existingComment?.id,
3871
+ prNumber,
3872
+ repo,
3873
+ token
3874
+ });
3875
+ log5.info(logPrefix14, existingComment ? "PR comment updated." : "PR comment created.");
3876
+ }, "reportToGitHubPR");
3729
3877
 
3730
3878
  // src/deploy/staticApp/findDefaultBuildFolder.ts
3731
3879
  var defaultBuildFolders = [
@@ -3780,11 +3928,11 @@ var getStaticAppBucket = /* @__PURE__ */ __name(async ({ stackName }) => {
3780
3928
  }
3781
3929
  }, "getStaticAppBucket");
3782
3930
  var CLOUDFRONT_DISTRIBUTION_ID = "CloudFrontDistributionId";
3783
- var logPrefix14 = "static-app";
3931
+ var logPrefix15 = "static-app";
3784
3932
  var invalidateCloudFront = /* @__PURE__ */ __name(async ({ outputs }) => {
3785
- log5.info(logPrefix14, "Invalidating CloudFront...");
3933
+ log5.info(logPrefix15, "Invalidating CloudFront...");
3786
3934
  if (!outputs) {
3787
- log5.info(logPrefix14, "Invalidation: outputs do not exist.");
3935
+ log5.info(logPrefix15, "Invalidation: outputs do not exist.");
3788
3936
  return;
3789
3937
  }
3790
3938
  const cloudFrontDistributionIDOutput = outputs.find((output) => {
@@ -3807,13 +3955,13 @@ var invalidateCloudFront = /* @__PURE__ */ __name(async ({ outputs }) => {
3807
3955
  const cloudFront = new AWS.CloudFront();
3808
3956
  try {
3809
3957
  await cloudFront.createInvalidation(params).promise();
3810
- log5.info(logPrefix14, `CloudFront Distribution ID ${distributionId} invalidated with success.`);
3958
+ log5.info(logPrefix15, `CloudFront Distribution ID ${distributionId} invalidated with success.`);
3811
3959
  } catch (err) {
3812
- log5.error(logPrefix14, `Error while trying to invalidate CloudFront distribution ${distributionId}.`);
3813
- log5.error(logPrefix14, err);
3960
+ log5.error(logPrefix15, `Error while trying to invalidate CloudFront distribution ${distributionId}.`);
3961
+ log5.error(logPrefix15, err);
3814
3962
  }
3815
3963
  } else {
3816
- log5.info(logPrefix14, `Cannot invalidate because distribution does not exist.`);
3964
+ log5.info(logPrefix15, `Cannot invalidate because distribution does not exist.`);
3817
3965
  }
3818
3966
  }, "invalidateCloudFront");
3819
3967
 
@@ -4271,11 +4419,11 @@ var uploadBuiltAppToS3 = /* @__PURE__ */ __name(async ({ buildFolder: directory,
4271
4419
  }, "uploadBuiltAppToS3");
4272
4420
 
4273
4421
  // src/deploy/staticApp/deployStaticApp.ts
4274
- var logPrefix15 = "static-app";
4422
+ var logPrefix16 = "static-app";
4275
4423
  var deployStaticApp = /* @__PURE__ */ __name(async ({ acm, aliases, appendIndexHtml, buildFolder, cloudfront, spa, hostedZoneName, region, skipUpload }) => {
4276
4424
  try {
4277
4425
  const { stackName } = await handleDeployInitialization({
4278
- logPrefix: logPrefix15
4426
+ logPrefix: logPrefix16
4279
4427
  });
4280
4428
  const params = {
4281
4429
  StackName: stackName
@@ -4327,7 +4475,7 @@ var deployStaticApp = /* @__PURE__ */ __name(async ({ acm, aliases, appendIndexH
4327
4475
  } catch (error) {
4328
4476
  handleDeployError({
4329
4477
  error,
4330
- logPrefix: logPrefix15
4478
+ logPrefix: logPrefix16
4331
4479
  });
4332
4480
  }
4333
4481
  }, "deployStaticApp");
@@ -4403,7 +4551,7 @@ var deployStaticAppCommand = {
4403
4551
  }
4404
4552
  }, "handler")
4405
4553
  };
4406
- var logPrefix16 = "deploy vercel";
4554
+ var logPrefix17 = "deploy vercel";
4407
4555
  var makeCommand = /* @__PURE__ */ __name((cmds) => {
4408
4556
  return cmds.filter((cmd) => {
4409
4557
  return cmd !== void 0 && cmd !== null && cmd !== "";
@@ -4411,7 +4559,7 @@ var makeCommand = /* @__PURE__ */ __name((cmds) => {
4411
4559
  }, "makeCommand");
4412
4560
  var deployVercel = /* @__PURE__ */ __name(async ({ token }) => {
4413
4561
  try {
4414
- log5.info(logPrefix16, "Deploying on Vercel...");
4562
+ log5.info(logPrefix17, "Deploying on Vercel...");
4415
4563
  const environment = getEnvironment();
4416
4564
  const finalToken = token || process.env.VERCEL_TOKEN;
4417
4565
  if (!finalToken) {
@@ -4446,11 +4594,11 @@ var deployVercel = /* @__PURE__ */ __name(async ({ token }) => {
4446
4594
  } catch (error) {
4447
4595
  handleDeployError({
4448
4596
  error,
4449
- logPrefix: logPrefix16
4597
+ logPrefix: logPrefix17
4450
4598
  });
4451
4599
  }
4452
4600
  }, "deployVercel");
4453
- var logPrefix17 = "deploy vercel";
4601
+ var logPrefix18 = "deploy vercel";
4454
4602
  var options4 = {
4455
4603
  token: {
4456
4604
  describe: "Vercel authorization token.",
@@ -4465,7 +4613,7 @@ var deployVercelCommand = {
4465
4613
  }, "builder"),
4466
4614
  handler: /* @__PURE__ */ __name(({ destroy: destroy2, ...rest }) => {
4467
4615
  if (destroy2) {
4468
- log5.info(logPrefix17, "Destroy Vercel deployment not implemented yet.");
4616
+ log5.info(logPrefix18, "Destroy Vercel deployment not implemented yet.");
4469
4617
  } else {
4470
4618
  deployVercel(rest);
4471
4619
  }
@@ -4543,7 +4691,7 @@ var generateSSHCommandWithPwd = /* @__PURE__ */ __name(({ userName, host, passwo
4543
4691
  }, "generateSSHCommandWithPwd");
4544
4692
 
4545
4693
  // src/deploy/vm/deployVM.ts
4546
- var logPrefix18 = "deploy-vm";
4694
+ var logPrefix19 = "deploy-vm";
4547
4695
  var deployVM = /* @__PURE__ */ __name(async ({ userName, host, scriptPath, keyPath, password, port, fixPermissions = false }) => {
4548
4696
  if (!userName || !host || !scriptPath) {
4549
4697
  throw new Error("Missing required parameters: userName, host, scriptPath");
@@ -4564,27 +4712,27 @@ var deployVM = /* @__PURE__ */ __name(async ({ userName, host, scriptPath, keyPa
4564
4712
  const permissionStr = permissions.toString(8);
4565
4713
  const fixCommand = `chmod 400 ${keyPath}`;
4566
4714
  if (fixPermissions) {
4567
- log5.info(logPrefix18, `Fixing SSH key permissions: ${keyPath} (${permissionStr} \u2192 400)`);
4715
+ log5.info(logPrefix19, `Fixing SSH key permissions: ${keyPath} (${permissionStr} \u2192 400)`);
4568
4716
  chmodSync(keyPath, 256);
4569
- log5.info(logPrefix18, `Permissions set to 400 (read-only by owner)`);
4717
+ log5.info(logPrefix19, `Permissions set to 400 (read-only by owner)`);
4570
4718
  } else {
4571
- log5.error(logPrefix18, `SSH key permissions too open: ${permissionStr} (octal)`);
4572
- log5.error(logPrefix18, `SSH requires permissions 400 or 600`);
4573
- log5.error(logPrefix18, `Fix manually: ${fixCommand}`);
4574
- log5.error(logPrefix18, `Or run with: --fix-permissions`);
4719
+ log5.error(logPrefix19, `SSH key permissions too open: ${permissionStr} (octal)`);
4720
+ log5.error(logPrefix19, `SSH requires permissions 400 or 600`);
4721
+ log5.error(logPrefix19, `Fix manually: ${fixCommand}`);
4722
+ log5.error(logPrefix19, `Or run with: --fix-permissions`);
4575
4723
  throw new Error(`Invalid SSH key permissions: ${permissionStr}. Expected 400 or 600.`);
4576
4724
  }
4577
4725
  } else {
4578
- log5.info(logPrefix18, `SSH key permissions OK: ${permissions.toString(8)}`);
4726
+ log5.info(logPrefix19, `SSH key permissions OK: ${permissions.toString(8)}`);
4579
4727
  }
4580
4728
  } catch (error) {
4581
4729
  if (error instanceof Error) {
4582
4730
  if (error.message.includes("Invalid SSH key permissions")) {
4583
4731
  throw error;
4584
4732
  }
4585
- log5.warn(logPrefix18, `Warning: Could not check key permissions: ${error.message}`);
4733
+ log5.warn(logPrefix19, `Warning: Could not check key permissions: ${error.message}`);
4586
4734
  } else {
4587
- log5.warn(logPrefix18, "Warning: Could not check key permissions: Unknown error");
4735
+ log5.warn(logPrefix19, "Warning: Could not check key permissions: Unknown error");
4588
4736
  }
4589
4737
  }
4590
4738
  }
@@ -4619,15 +4767,15 @@ var deployVM = /* @__PURE__ */ __name(async ({ userName, host, scriptPath, keyPa
4619
4767
  });
4620
4768
  const validateStdin = /* @__PURE__ */ __name((stdin) => {
4621
4769
  if (!stdin) {
4622
- log5.error(logPrefix18, "SSH process stdin is null or undefined");
4770
+ log5.error(logPrefix19, "SSH process stdin is null or undefined");
4623
4771
  return false;
4624
4772
  }
4625
4773
  if (stdin.destroyed) {
4626
- log5.error(logPrefix18, "SSH process stdin has been destroyed");
4774
+ log5.error(logPrefix19, "SSH process stdin has been destroyed");
4627
4775
  return false;
4628
4776
  }
4629
4777
  if (!stdin.writable) {
4630
- log5.error(logPrefix18, "SSH process stdin is not writable");
4778
+ log5.error(logPrefix19, "SSH process stdin is not writable");
4631
4779
  return false;
4632
4780
  }
4633
4781
  return true;
@@ -4638,7 +4786,7 @@ var deployVM = /* @__PURE__ */ __name(async ({ userName, host, scriptPath, keyPa
4638
4786
  }
4639
4787
  if (!existsSync(scriptPath)) {
4640
4788
  const message = `Deployment script not found at path: ${scriptPath}`;
4641
- log5.error(logPrefix18, message);
4789
+ log5.error(logPrefix19, message);
4642
4790
  reject(new Error(message));
4643
4791
  return;
4644
4792
  }
@@ -4648,7 +4796,7 @@ var deployVM = /* @__PURE__ */ __name(async ({ userName, host, scriptPath, keyPa
4648
4796
  }
4649
4797
  deployScript.pipe(sshProcess.stdin);
4650
4798
  const sigintHandler = /* @__PURE__ */ __name(() => {
4651
- log5.info(logPrefix18, "Interrupting deployment...");
4799
+ log5.info(logPrefix19, "Interrupting deployment...");
4652
4800
  sshProcess.kill("SIGINT");
4653
4801
  process.exit(130);
4654
4802
  }, "sigintHandler");
@@ -4672,7 +4820,7 @@ var deployVM = /* @__PURE__ */ __name(async ({ userName, host, scriptPath, keyPa
4672
4820
  }, "deployVM");
4673
4821
 
4674
4822
  // src/deploy/vm/command.ts
4675
- var logPrefix19 = "deploy-vm";
4823
+ var logPrefix20 = "deploy-vm";
4676
4824
  var deployVMCommand = {
4677
4825
  command: "vm",
4678
4826
  describe: "Deploy to a VM via SSH by executing a deployment script",
@@ -4690,16 +4838,16 @@ var deployVMCommand = {
4690
4838
  port,
4691
4839
  fixPermissions
4692
4840
  });
4693
- log5.info(logPrefix19, "Deployment completed successfully!");
4841
+ log5.info(logPrefix20, "Deployment completed successfully!");
4694
4842
  } catch (error) {
4695
- log5.error(logPrefix19, "Deployment failed: %s", error.message);
4843
+ log5.error(logPrefix20, "Deployment failed: %s", error.message);
4696
4844
  process.exit(1);
4697
4845
  }
4698
4846
  }, "handler")
4699
4847
  };
4700
4848
 
4701
4849
  // src/deploy/command.ts
4702
- var logPrefix20 = "deploy";
4850
+ var logPrefix21 = "deploy";
4703
4851
  var checkAwsAccountId = /* @__PURE__ */ __name(async (awsAccountId) => {
4704
4852
  try {
4705
4853
  const currentAwsAccountId = await getAwsAccountId();
@@ -4710,21 +4858,36 @@ var checkAwsAccountId = /* @__PURE__ */ __name(async (awsAccountId) => {
4710
4858
  if (error.code === "CredentialsError") {
4711
4859
  return;
4712
4860
  }
4713
- log5.error(logPrefix20, error.message);
4861
+ log5.error(logPrefix21, error.message);
4714
4862
  process.exit();
4715
4863
  }
4716
4864
  }, "checkAwsAccountId");
4717
- var describeDeployCommand = {
4718
- command: "describe",
4719
- describe: "Print the outputs of the deployment.",
4720
- handler: /* @__PURE__ */ __name(async ({ stackName }) => {
4865
+ var reportDeployCommand = {
4866
+ command: "report",
4867
+ describe: "Report the outputs of the deployment.",
4868
+ builder: /* @__PURE__ */ __name((yargs3) => {
4869
+ return yargs3.options({
4870
+ channel: {
4871
+ choices: [
4872
+ "github-pr"
4873
+ ],
4874
+ describe: 'Report deploy outputs to the specified channel. Use "github-pr" to post or update a PR comment with all workspace deploy outputs.',
4875
+ type: "string"
4876
+ }
4877
+ });
4878
+ }, "builder"),
4879
+ handler: /* @__PURE__ */ __name(async ({ stackName, channel }) => {
4721
4880
  try {
4881
+ if (channel === "github-pr") {
4882
+ await reportToGitHubPR();
4883
+ return;
4884
+ }
4722
4885
  const newStackName = stackName || await getStackName();
4723
4886
  await printStackOutputsAfterDeploy({
4724
4887
  stackName: newStackName
4725
4888
  });
4726
4889
  } catch (error) {
4727
- log5.info(logPrefix20, "Cannot describe stack. Message: %s", error.message);
4890
+ log5.info(logPrefix21, "Cannot report stack. Message: %s", error.message);
4728
4891
  }
4729
4892
  }, "handler")
4730
4893
  };
@@ -4909,7 +5072,7 @@ var deployCommand = {
4909
5072
  }
4910
5073
  }).middleware(({ skipDeploy }) => {
4911
5074
  if (skipDeploy) {
4912
- log5.warn(logPrefix20, "Skip deploy flag is true, then the deploy command wasn't executed.");
5075
+ log5.warn(logPrefix21, "Skip deploy flag is true, then the deploy command wasn't executed.");
4913
5076
  process.exit(0);
4914
5077
  }
4915
5078
  }).middleware(({ lambdaExternals, lambdaInput }) => {
@@ -4922,7 +5085,7 @@ var deployCommand = {
4922
5085
  });
4923
5086
  const commands = [
4924
5087
  deployLambdaLayerCommand,
4925
- describeDeployCommand,
5088
+ reportDeployCommand,
4926
5089
  deployBaseStackCommand,
4927
5090
  deployStaticAppCommand,
4928
5091
  deployCicdCommand,
@@ -4950,10 +5113,10 @@ var deployCommand = {
4950
5113
  }
4951
5114
  }, "handler")
4952
5115
  };
4953
- var logPrefix21 = "cicd-ecs-task-report";
5116
+ var logPrefix22 = "cicd-ecs-task-report";
4954
5117
  var sendEcsTaskReport = /* @__PURE__ */ __name(async ({ status }) => {
4955
5118
  if (!process.env.ECS_TASK_REPORT_HANDLER_NAME) {
4956
- log5.info(logPrefix21, "ECS_TASK_REPORT_HANDLER_NAME not defined.");
5119
+ log5.info(logPrefix22, "ECS_TASK_REPORT_HANDLER_NAME not defined.");
4957
5120
  return;
4958
5121
  }
4959
5122
  const lambda = new AWS.Lambda();
@@ -4970,7 +5133,7 @@ var sendEcsTaskReport = /* @__PURE__ */ __name(async ({ status }) => {
4970
5133
  FunctionName: process.env.ECS_TASK_REPORT_HANDLER_NAME,
4971
5134
  InvokeArgs: JSON.stringify(payload)
4972
5135
  }).promise();
4973
- log5.info(logPrefix21, "Report sent.");
5136
+ log5.info(logPrefix22, "Report sent.");
4974
5137
  }, "sendEcsTaskReport");
4975
5138
  var options7 = {
4976
5139
  status: {
@@ -4993,7 +5156,7 @@ var ecsTaskReportCommand = {
4993
5156
  return sendEcsTaskReport(args);
4994
5157
  }, "handler")
4995
5158
  };
4996
- var logPrefix22 = "generate-env";
5159
+ var logPrefix23 = "generate-env";
4997
5160
  var readEnvFile = /* @__PURE__ */ __name(async ({ envFileName, envsPath }) => {
4998
5161
  try {
4999
5162
  const content = await fs3.promises.readFile(path.resolve(process.cwd(), envsPath, envFileName), "utf8");
@@ -5013,14 +5176,14 @@ var generateEnv = /* @__PURE__ */ __name(async ({ defaultEnvironment, path: envs
5013
5176
  envsPath
5014
5177
  });
5015
5178
  if (!envFile) {
5016
- log5.info(logPrefix22, "Env file %s doesn't exist. Skip generating env file.", envFileName);
5179
+ log5.info(logPrefix23, "Env file %s doesn't exist. Skip generating env file.", envFileName);
5017
5180
  return;
5018
5181
  }
5019
5182
  await writeEnvFile({
5020
5183
  content: envFile,
5021
5184
  envFileName: ".env"
5022
5185
  });
5023
- log5.info(logPrefix22, "Generate env file %s from %s successfully.", ".env", envFileName);
5186
+ log5.info(logPrefix23, "Generate env file %s from %s successfully.", ".env", envFileName);
5024
5187
  }, "generateEnv");
5025
5188
 
5026
5189
  // src/generateEnv/generateEnvCommand.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carlin",
3
- "version": "1.40.2",
3
+ "version": "1.42.0",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "author": "Pedro Arantes <arantespp@gmail.com> (https://twitter.com/arantespp)",
@@ -47,9 +47,9 @@
47
47
  "uglify-js": "^3.19.3",
48
48
  "vercel": "^39.1.1",
49
49
  "yargs": "^17.7.2",
50
- "@ttoss/cloudformation": "^0.11.10",
51
- "@ttoss/config": "^1.35.12",
52
- "@ttoss/read-config-file": "^2.0.20"
50
+ "@ttoss/cloudformation": "^0.12.0",
51
+ "@ttoss/config": "^1.36.0",
52
+ "@ttoss/read-config-file": "^2.1.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/adm-zip": "^0.5.6",
@@ -60,7 +60,7 @@
60
60
  "@types/jest": "^30.0.0",
61
61
  "@types/js-yaml": "^4.0.9",
62
62
  "@types/mime-types": "^2.1.4",
63
- "@types/node": "^22.19.0",
63
+ "@types/node": "^24.10.13",
64
64
  "@types/npmlog": "^7.0.0",
65
65
  "@types/semver": "^7.5.8",
66
66
  "@types/uglify-js": "^3.17.5",
@@ -69,7 +69,7 @@
69
69
  "jest": "^30.2.0",
70
70
  "tsup": "^8.5.1",
71
71
  "typescript": "~5.9.3",
72
- "@ttoss/test-utils": "^4.0.3"
72
+ "@ttoss/test-utils": "^4.1.0"
73
73
  },
74
74
  "keywords": [],
75
75
  "publishConfig": {