From a865c93a2e46363b8d630bc802d0399f2c596399 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 14:13:36 +0000 Subject: [PATCH 1/6] feat(awscdk): add CloudFormation resource count warnings Implements issue #158 to enhance CDK synth with resource limit warnings: - Add count-resources script to analyze CloudFormation templates - Count resources per stack and warn when approaching 500 resource limit - Default warning threshold at 450 resources (configurable) - Add resource counts to GitHub workflow summary - Add PR comments with resource count details on pull requests - New CDKPipelineOptions: resourceCountWarningThreshold and enableResourceCounting The feature is enabled by default and helps prevent hitting CloudFormation's hard limit of 500 resources per stack by providing early warnings. --- .projenrc.ts | 1 + API.md | 308 ++++++++++++++++++ package-lock.json | 1 + package.json | 1 + src/awscdk/base.ts | 17 + src/awscdk/count-resources.ts | 272 ++++++++++++++++ src/awscdk/github.ts | 166 ++++++++++ src/awscdk/index.ts | 1 + src/awscdk/resource-count-step.ts | 63 ++++ .../__snapshots__/amplify-deploy.test.ts.snap | 21 ++ test/__snapshots__/github.test.ts.snap | 77 +++++ 11 files changed, 928 insertions(+) create mode 100644 src/awscdk/count-resources.ts create mode 100644 src/awscdk/resource-count-step.ts diff --git a/.projenrc.ts b/.projenrc.ts index 18da779..ae37a3a 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -49,6 +49,7 @@ const project = new cdk.JsiiProject({ bin: { 'pipelines-release': 'lib/release.js', 'detect-drift': 'lib/drift/detect-drift.js', + 'count-resources': 'lib/awscdk/count-resources.js', }, releaseToNpm: true, gitpod: true, diff --git a/API.md b/API.md index 51105f7..0d091a5 100644 --- a/API.md +++ b/API.md @@ -2371,6 +2371,7 @@ const bashCDKPipelineOptions: BashCDKPipelineOptions = { ... } | stages | DeploymentStage[] | This field specifies a list of stages that should be deployed using a CI/CD pipeline. | | branchName | string | the name of the branch to deploy from. | | deploySubStacks | boolean | If set to true all CDK actions will also include /* to deploy/diff/destroy sub stacks of the main stack. | +| enableResourceCounting | boolean | Whether to enable resource counting in the synth step. | | featureStages | StageOptions | This specifies details for feature stages. | | independentStages | IndependentStage[] | This specifies details for independent stages. | | personalStage | StageOptions | This specifies details for a personal stage. | @@ -2381,6 +2382,7 @@ const bashCDKPipelineOptions: BashCDKPipelineOptions = { ... } | preInstallSteps | PipelineStep[] | *No description.* | | preSynthCommands | string[] | *No description.* | | preSynthSteps | PipelineStep[] | *No description.* | +| resourceCountWarningThreshold | number | Resource count warning threshold. | | stackPrefix | string | This field is used to define a prefix for the AWS Stack resources created during the pipeline's operation. | | versioning | VersioningConfig | Versioning configuration. | @@ -2438,6 +2440,21 @@ You can use this to deploy CDk applications containing multiple stacks. --- +##### `enableResourceCounting`Optional + +```typescript +public readonly enableResourceCounting: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Whether to enable resource counting in the synth step. + +When enabled, counts CloudFormation resources in each stack and warns if approaching the limit. + +--- + ##### `featureStages`Optional ```typescript @@ -2552,6 +2569,22 @@ public readonly preSynthSteps: PipelineStep[]; --- +##### `resourceCountWarningThreshold`Optional + +```typescript +public readonly resourceCountWarningThreshold: number; +``` + +- *Type:* number +- *Default:* 450 + +Resource count warning threshold. + +When a stack exceeds this number of resources, a warning will be displayed. +CloudFormation has a hard limit of 500 resources per stack. + +--- + ##### `stackPrefix`Optional ```typescript @@ -2744,6 +2777,7 @@ const cDKPipelineOptions: CDKPipelineOptions = { ... } | stages | DeploymentStage[] | This field specifies a list of stages that should be deployed using a CI/CD pipeline. | | branchName | string | the name of the branch to deploy from. | | deploySubStacks | boolean | If set to true all CDK actions will also include /* to deploy/diff/destroy sub stacks of the main stack. | +| enableResourceCounting | boolean | Whether to enable resource counting in the synth step. | | featureStages | StageOptions | This specifies details for feature stages. | | independentStages | IndependentStage[] | This specifies details for independent stages. | | personalStage | StageOptions | This specifies details for a personal stage. | @@ -2754,6 +2788,7 @@ const cDKPipelineOptions: CDKPipelineOptions = { ... } | preInstallSteps | PipelineStep[] | *No description.* | | preSynthCommands | string[] | *No description.* | | preSynthSteps | PipelineStep[] | *No description.* | +| resourceCountWarningThreshold | number | Resource count warning threshold. | | stackPrefix | string | This field is used to define a prefix for the AWS Stack resources created during the pipeline's operation. | | versioning | VersioningConfig | Versioning configuration. | @@ -2811,6 +2846,21 @@ You can use this to deploy CDk applications containing multiple stacks. --- +##### `enableResourceCounting`Optional + +```typescript +public readonly enableResourceCounting: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Whether to enable resource counting in the synth step. + +When enabled, counts CloudFormation resources in each stack and warns if approaching the limit. + +--- + ##### `featureStages`Optional ```typescript @@ -2925,6 +2975,22 @@ public readonly preSynthSteps: PipelineStep[]; --- +##### `resourceCountWarningThreshold`Optional + +```typescript +public readonly resourceCountWarningThreshold: number; +``` + +- *Type:* number +- *Default:* 450 + +Resource count warning threshold. + +When a stack exceeds this number of resources, a warning will be displayed. +CloudFormation has a hard limit of 500 resources per stack. + +--- + ##### `stackPrefix`Optional ```typescript @@ -4026,6 +4092,7 @@ const githubCDKPipelineOptions: GithubCDKPipelineOptions = { ... } | stages | DeploymentStage[] | This field specifies a list of stages that should be deployed using a CI/CD pipeline. | | branchName | string | the name of the branch to deploy from. | | deploySubStacks | boolean | If set to true all CDK actions will also include /* to deploy/diff/destroy sub stacks of the main stack. | +| enableResourceCounting | boolean | Whether to enable resource counting in the synth step. | | featureStages | StageOptions | This specifies details for feature stages. | | independentStages | IndependentStage[] | This specifies details for independent stages. | | personalStage | StageOptions | This specifies details for a personal stage. | @@ -4036,6 +4103,7 @@ const githubCDKPipelineOptions: GithubCDKPipelineOptions = { ... } | preInstallSteps | PipelineStep[] | *No description.* | | preSynthCommands | string[] | *No description.* | | preSynthSteps | PipelineStep[] | *No description.* | +| resourceCountWarningThreshold | number | Resource count warning threshold. | | stackPrefix | string | This field is used to define a prefix for the AWS Stack resources created during the pipeline's operation. | | versioning | VersioningConfig | Versioning configuration. | | runnerTags | string[] | runner tags to use to select runners. | @@ -4096,6 +4164,21 @@ You can use this to deploy CDk applications containing multiple stacks. --- +##### `enableResourceCounting`Optional + +```typescript +public readonly enableResourceCounting: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Whether to enable resource counting in the synth step. + +When enabled, counts CloudFormation resources in each stack and warns if approaching the limit. + +--- + ##### `featureStages`Optional ```typescript @@ -4210,6 +4293,22 @@ public readonly preSynthSteps: PipelineStep[]; --- +##### `resourceCountWarningThreshold`Optional + +```typescript +public readonly resourceCountWarningThreshold: number; +``` + +- *Type:* number +- *Default:* 450 + +Resource count warning threshold. + +When a stack exceeds this number of resources, a warning will be displayed. +CloudFormation has a hard limit of 500 resources per stack. + +--- + ##### `stackPrefix`Optional ```typescript @@ -4652,6 +4751,7 @@ const gitlabCDKPipelineOptions: GitlabCDKPipelineOptions = { ... } | stages | DeploymentStage[] | This field specifies a list of stages that should be deployed using a CI/CD pipeline. | | branchName | string | the name of the branch to deploy from. | | deploySubStacks | boolean | If set to true all CDK actions will also include /* to deploy/diff/destroy sub stacks of the main stack. | +| enableResourceCounting | boolean | Whether to enable resource counting in the synth step. | | featureStages | StageOptions | This specifies details for feature stages. | | independentStages | IndependentStage[] | This specifies details for independent stages. | | personalStage | StageOptions | This specifies details for a personal stage. | @@ -4662,6 +4762,7 @@ const gitlabCDKPipelineOptions: GitlabCDKPipelineOptions = { ... } | preInstallSteps | PipelineStep[] | *No description.* | | preSynthCommands | string[] | *No description.* | | preSynthSteps | PipelineStep[] | *No description.* | +| resourceCountWarningThreshold | number | Resource count warning threshold. | | stackPrefix | string | This field is used to define a prefix for the AWS Stack resources created during the pipeline's operation. | | versioning | VersioningConfig | Versioning configuration. | | image | string | The Docker image to use for running the pipeline jobs. | @@ -4721,6 +4822,21 @@ You can use this to deploy CDk applications containing multiple stacks. --- +##### `enableResourceCounting`Optional + +```typescript +public readonly enableResourceCounting: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Whether to enable resource counting in the synth step. + +When enabled, counts CloudFormation resources in each stack and warns if approaching the limit. + +--- + ##### `featureStages`Optional ```typescript @@ -4835,6 +4951,22 @@ public readonly preSynthSteps: PipelineStep[]; --- +##### `resourceCountWarningThreshold`Optional + +```typescript +public readonly resourceCountWarningThreshold: number; +``` + +- *Type:* number +- *Default:* 450 + +Resource count warning threshold. + +When a stack exceeds this number of resources, a warning will be displayed. +CloudFormation has a hard limit of 500 resources per stack. + +--- + ##### `stackPrefix`Optional ```typescript @@ -5685,6 +5817,79 @@ public readonly splitParameters: boolean; --- +### ResourceCountStepProps + +#### Initializer + +```typescript +import { ResourceCountStepProps } from 'projen-pipelines' + +const resourceCountStepProps: ResourceCountStepProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| cloudAssemblyDir | string | Path to the cloud assembly directory. | +| githubSummary | boolean | Whether to write results to GitHub step summary. | +| outputFile | string | Output file for results. | +| warningThreshold | number | Warning threshold for resource count. | + +--- + +##### `cloudAssemblyDir`Optional + +```typescript +public readonly cloudAssemblyDir: string; +``` + +- *Type:* string +- *Default:* 'cdk.out' + +Path to the cloud assembly directory. + +--- + +##### `githubSummary`Optional + +```typescript +public readonly githubSummary: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Whether to write results to GitHub step summary. + +--- + +##### `outputFile`Optional + +```typescript +public readonly outputFile: string; +``` + +- *Type:* string +- *Default:* 'resource-count-results.json' + +Output file for results. + +--- + +##### `warningThreshold`Optional + +```typescript +public readonly warningThreshold: number; +``` + +- *Type:* number +- *Default:* 450 + +Warning threshold for resource count. + +--- + ### StageOptions Options for a CDK stage like the target environment. @@ -7220,6 +7425,109 @@ Converts the step into a GitLab CI configuration. +### ResourceCountStep + +#### Initializers + +```typescript +import { ResourceCountStep } from 'projen-pipelines' + +new ResourceCountStep(project: Project, props?: ResourceCountStepProps) +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| project | projen.Project | - The projen project reference. | +| props | ResourceCountStepProps | *No description.* | + +--- + +##### `project`Required + +- *Type:* projen.Project + +The projen project reference. + +--- + +##### `props`Optional + +- *Type:* ResourceCountStepProps + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| toBash | Converts the sequence of steps into a Bash script configuration. | +| toCodeCatalyst | Converts the sequence of steps into a CodeCatalyst Actions step configuration. | +| toGithub | Converts the sequence of steps into a GitHub Actions step configuration. | +| toGitlab | Converts the sequence of steps into a GitLab CI configuration. | +| addSteps | *No description.* | +| prependSteps | *No description.* | + +--- + +##### `toBash` + +```typescript +public toBash(): BashStepConfig +``` + +Converts the sequence of steps into a Bash script configuration. + +##### `toCodeCatalyst` + +```typescript +public toCodeCatalyst(): CodeCatalystStepConfig +``` + +Converts the sequence of steps into a CodeCatalyst Actions step configuration. + +##### `toGithub` + +```typescript +public toGithub(): GithubStepConfig +``` + +Converts the sequence of steps into a GitHub Actions step configuration. + +##### `toGitlab` + +```typescript +public toGitlab(): GitlabStepConfig +``` + +Converts the sequence of steps into a GitLab CI configuration. + +##### `addSteps` + +```typescript +public addSteps(steps: ...PipelineStep[]): void +``` + +###### `steps`Required + +- *Type:* ...PipelineStep[] + +--- + +##### `prependSteps` + +```typescript +public prependSteps(steps: ...PipelineStep[]): void +``` + +###### `steps`Required + +- *Type:* ...PipelineStep[] + +--- + + + + ### SimpleCommandStep Concrete implementation of PipelineStep that executes simple commands. diff --git a/package-lock.json b/package-lock.json index 4b3956d..3345b05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "standard-version": "^9.5.0" }, "bin": { + "count-resources": "lib/awscdk/count-resources.js", "detect-drift": "lib/drift/detect-drift.js", "pipelines-release": "lib/release.js" }, diff --git a/package.json b/package.json index 3551e4b..5b33928 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "url": "https://github.com/open-constructs/projen-pipelines.git" }, "bin": { + "count-resources": "lib/awscdk/count-resources.js", "detect-drift": "lib/drift/detect-drift.js", "pipelines-release": "lib/release.js" }, diff --git a/src/awscdk/base.ts b/src/awscdk/base.ts index fa2c438..adb691a 100644 --- a/src/awscdk/base.ts +++ b/src/awscdk/base.ts @@ -180,6 +180,23 @@ export interface CDKPipelineOptions { * Versioning configuration */ readonly versioning?: VersioningConfig; + + /** + * Resource count warning threshold. + * When a stack exceeds this number of resources, a warning will be displayed. + * CloudFormation has a hard limit of 500 resources per stack. + * + * @default 450 + */ + readonly resourceCountWarningThreshold?: number; + + /** + * Whether to enable resource counting in the synth step. + * When enabled, counts CloudFormation resources in each stack and warns if approaching the limit. + * + * @default true + */ + readonly enableResourceCounting?: boolean; } /** diff --git a/src/awscdk/count-resources.ts b/src/awscdk/count-resources.ts new file mode 100644 index 0000000..31b10f8 --- /dev/null +++ b/src/awscdk/count-resources.ts @@ -0,0 +1,272 @@ +#!/usr/bin/env node + +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { join } from 'path'; + +interface ResourceCountOptions { + cloudAssemblyDir: string; + warningThreshold?: number; + outputFile?: string; + githubSummary?: boolean; +} + +interface StackResourceCount { + stackName: string; + resourceCount: number; + templateFile: string; + warning?: string; +} + +interface ResourceCountResult { + stacks: StackResourceCount[]; + totalResources: number; + maxResourcesInStack: number; + timestamp: string; +} + +class ResourceCounter { + private readonly options: ResourceCountOptions; + private readonly warningThreshold: number; + private readonly results: StackResourceCount[] = []; + + constructor(options: ResourceCountOptions) { + this.options = options; + this.warningThreshold = options.warningThreshold ?? 450; + } + + public run(): void { + console.log(`Counting resources in cloud assembly: ${this.options.cloudAssemblyDir}`); + console.log(`Warning threshold: ${this.warningThreshold} resources`); + + try { + this.countResources(); + this.printSummary(); + this.saveResults(); + + if (this.options.githubSummary) { + this.writeGitHubSummary(); + } + } catch (error) { + console.error('Error counting resources:', error); + process.exit(1); + } + } + + private countResources(): void { + const manifestPath = join(this.options.cloudAssemblyDir, 'manifest.json'); + + if (!existsSync(manifestPath)) { + throw new Error(`Cloud assembly manifest not found at ${manifestPath}`); + } + + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); + + // Find all CloudFormation stack artifacts + const artifacts = manifest.artifacts || {}; + + for (const [artifactId, artifact] of Object.entries(artifacts)) { + if ((artifact as any).type === 'aws:cloudformation:stack') { + const stackArtifact = artifact as any; + const templateFile = stackArtifact.properties?.templateFile; + + if (templateFile) { + const templatePath = join(this.options.cloudAssemblyDir, templateFile); + const resourceCount = this.countResourcesInTemplate(templatePath); + + const stackResult: StackResourceCount = { + stackName: artifactId, + resourceCount, + templateFile, + }; + + // Add warning if threshold is crossed + if (resourceCount >= this.warningThreshold) { + const percentOfLimit = Math.round((resourceCount / 500) * 100); + stackResult.warning = `Stack is at ${percentOfLimit}% of CloudFormation's 500 resource limit`; + } + + this.results.push(stackResult); + } + } + } + } + + private countResourcesInTemplate(templatePath: string): number { + try { + const template = JSON.parse(readFileSync(templatePath, 'utf8')); + const resources = template.Resources || {}; + return Object.keys(resources).length; + } catch (error) { + console.warn(`Warning: Could not read template ${templatePath}:`, error); + return 0; + } + } + + private printSummary(): void { + console.log('\n' + '='.repeat(80)); + console.log('Resource Count Summary'); + console.log('='.repeat(80)); + + if (this.results.length === 0) { + console.log('No stacks found in cloud assembly'); + return; + } + + const totalResources = this.results.reduce((sum, r) => sum + r.resourceCount, 0); + const maxResources = Math.max(...this.results.map(r => r.resourceCount)); + const stacksWithWarnings = this.results.filter(r => r.warning); + + console.log(`\nTotal stacks: ${this.results.length}`); + console.log(`Total resources: ${totalResources}`); + console.log(`Max resources in a single stack: ${maxResources}`); + + console.log('\nPer-stack breakdown:'); + for (const stack of this.results) { + const percentage = Math.round((stack.resourceCount / 500) * 100); + const statusIcon = stack.warning ? '⚠️ ' : '✓ '; + console.log(` ${statusIcon} ${stack.stackName}: ${stack.resourceCount} resources (${percentage}% of limit)`); + + if (stack.warning) { + console.log(` ⚠️ ${stack.warning}`); + } + } + + if (stacksWithWarnings.length > 0) { + console.log('\n' + '!'.repeat(80)); + console.log(`WARNING: ${stacksWithWarnings.length} stack(s) approaching CloudFormation resource limit!`); + console.log('!'.repeat(80)); + console.log('\nConsider:'); + console.log(' - Breaking large stacks into smaller, focused stacks'); + console.log(' - Using nested stacks for reusable components'); + console.log(' - Reviewing resource usage and removing unnecessary resources'); + console.log(' - CloudFormation hard limit is 500 resources per stack'); + } + + console.log('\n' + '='.repeat(80) + '\n'); + } + + private writeGitHubSummary(): void { + const summaryFile = process.env.GITHUB_STEP_SUMMARY; + if (!summaryFile) { + console.log('GITHUB_STEP_SUMMARY not set, skipping GitHub summary'); + return; + } + + const totalResources = this.results.reduce((sum, r) => sum + r.resourceCount, 0); + const maxResources = Math.max(...this.results.map(r => r.resourceCount)); + const stacksWithWarnings = this.results.filter(r => r.warning); + + let summary = '## 📊 CloudFormation Resource Count\n\n'; + summary += '### Summary\n'; + summary += `- **Total stacks:** ${this.results.length}\n`; + summary += `- **Total resources:** ${totalResources}\n`; + summary += `- **Max resources in single stack:** ${maxResources}\n`; + summary += `- **Warning threshold:** ${this.warningThreshold} resources\n`; + summary += '- **CloudFormation limit:** 500 resources per stack\n\n'; + + if (stacksWithWarnings.length > 0) { + summary += '### ⚠️ Stacks Approaching Limit\n\n'; + summary += `${stacksWithWarnings.length} stack(s) have crossed the warning threshold:\n\n`; + + for (const stack of stacksWithWarnings) { + const percentage = Math.round((stack.resourceCount / 500) * 100); + summary += `- **${stack.stackName}**: ${stack.resourceCount} resources (${percentage}% of limit)\n`; + } + + summary += '\n**Recommendations:**\n'; + summary += '- Consider breaking large stacks into smaller, focused stacks\n'; + summary += '- Use nested stacks for reusable components\n'; + summary += '- Review resource usage and remove unnecessary resources\n\n'; + } + + summary += '### Stack Details\n\n'; + summary += '| Stack | Resources | % of Limit | Status |\n'; + summary += '|-------|-----------|------------|--------|\n'; + + for (const stack of this.results.sort((a, b) => b.resourceCount - a.resourceCount)) { + const percentage = Math.round((stack.resourceCount / 500) * 100); + const status = stack.warning ? '⚠️ Warning' : '✅ OK'; + summary += `| ${stack.stackName} | ${stack.resourceCount} | ${percentage}% | ${status} |\n`; + } + + try { + writeFileSync(summaryFile, summary, { flag: 'a' }); + console.log('GitHub summary written successfully'); + } catch (error) { + console.error('Failed to write GitHub summary:', error); + } + } + + private saveResults(): void { + const outputFile = this.options.outputFile || 'resource-count-results.json'; + + const result: ResourceCountResult = { + stacks: this.results, + totalResources: this.results.reduce((sum, r) => sum + r.resourceCount, 0), + maxResourcesInStack: Math.max(...this.results.map(r => r.resourceCount), 0), + timestamp: new Date().toISOString(), + }; + + writeFileSync(outputFile, JSON.stringify(result, null, 2)); + console.log(`Results saved to ${outputFile}`); + } +} + +// Parse command line arguments +function parseArgs(): ResourceCountOptions { + const args = process.argv.slice(2); + const options: ResourceCountOptions = { + cloudAssemblyDir: 'cdk.out', + warningThreshold: 450, + githubSummary: false, + }; + + for (let i = 0; i < args.length; i++) { + switch (args[i]) { + case '--cloud-assembly-dir': + case '-d': + options.cloudAssemblyDir = args[++i]; + break; + case '--warning-threshold': + case '-t': + options.warningThreshold = parseInt(args[++i], 10); + break; + case '--output-file': + case '-o': + options.outputFile = args[++i]; + break; + case '--github-summary': + case '-g': + options.githubSummary = true; + break; + case '--help': + case '-h': + console.log(` +Usage: count-resources.ts [options] + +Options: + -d, --cloud-assembly-dir Cloud assembly directory (default: cdk.out) + -t, --warning-threshold Warning threshold for resource count (default: 450) + -o, --output-file Output file for results (default: resource-count-results.json) + -g, --github-summary Write results to GitHub step summary + -h, --help Show this help message +`); + process.exit(0); + break; + default: + console.error(`Unknown option: ${args[i]}`); + process.exit(1); + } + } + + return options; +} + +// Main execution +if (require.main === module) { + const options = parseArgs(); + const counter = new ResourceCounter(options); + counter.run(); +} + +export { ResourceCounter, ResourceCountOptions, ResourceCountResult, StackResourceCount }; diff --git a/src/awscdk/github.ts b/src/awscdk/github.ts index 36cc651..f037a7e 100644 --- a/src/awscdk/github.ts +++ b/src/awscdk/github.ts @@ -5,6 +5,7 @@ import { CdkDiffType, CDKPipeline, CDKPipelineOptions, DeploymentStage, Independ import { PipelineEngine } from '../engine'; import { mergeJobPermissions } from '../engines'; import { AwsAssumeRoleStep, PipelineStep, ProjenScriptStep, SimpleCommandStep } from '../steps'; +import { ResourceCountStep } from './resource-count-step'; import { DownloadArtifactStep, UploadArtifactStep } from '../steps/artifact-steps'; import { GithubPackagesLoginStep } from '../steps/registries'; @@ -108,6 +109,11 @@ export class GithubCDKPipeline extends CDKPipeline { if (options.featureStages) { this.createFeatureWorkflows(); } + + // Create PR workflow for resource counting if enabled + if (this.baseOptions.enableResourceCounting !== false) { + this.createResourceCountPRWorkflow(); + } } /** the type of engine this implementation of CDKPipeline is for */ @@ -243,6 +249,22 @@ export class GithubCDKPipeline extends CDKPipeline { steps.push(this.provideInstallStep()); steps.push(this.provideSynthStep()); + // Add resource counting step if enabled + if (this.baseOptions.enableResourceCounting !== false) { + steps.push(new ResourceCountStep(this.project, { + cloudAssemblyDir: this.app.cdkConfig.cdkout, + warningThreshold: this.baseOptions.resourceCountWarningThreshold ?? 450, + outputFile: 'resource-count-results.json', + githubSummary: true, + })); + + // Upload resource count results as artifact + steps.push(new UploadArtifactStep(this.project, { + name: 'resource-count-results', + path: 'resource-count-results.json', + })); + } + steps.push(new UploadArtifactStep(this.project, { name: 'cloud-assembly', path: `${this.app.cdkConfig.cdkout}/`, @@ -505,4 +527,148 @@ export class GithubCDKPipeline extends CDKPipeline { } } + + /** + * Creates a workflow for commenting resource counts on pull requests. + */ + private createResourceCountPRWorkflow(): void { + const workflow = this.app.github!.addWorkflow('resource-count-pr'); + + workflow.on({ + pullRequest: { + types: ['opened', 'synchronize', 'reopened'], + }, + }); + + const steps: PipelineStep[] = []; + steps.push(this.provideInstallStep()); + steps.push(this.provideSynthStep()); + steps.push(new ResourceCountStep(this.project, { + cloudAssemblyDir: this.app.cdkConfig.cdkout, + warningThreshold: this.baseOptions.resourceCountWarningThreshold ?? 450, + outputFile: 'resource-count-results.json', + githubSummary: true, + })); + + const githubSteps = steps.map(s => s.toGithub()); + + workflow.addJob('resource-count', { + name: 'Count CloudFormation Resources', + runsOn: this.options.runnerTags ?? DEFAULT_RUNNER_TAGS, + permissions: mergeJobPermissions({ + contents: JobPermission.READ, + pullRequests: JobPermission.WRITE, + }, ...(githubSteps.flatMap(s => s.permissions).filter(p => p != undefined) as JobPermissions[])), + env: { + CI: 'true', + ...githubSteps.reduce((acc, step) => ({ ...acc, ...step.env }), {}), + }, + tools: { + node: { + version: this.minNodeVersion ?? '20', + }, + }, + steps: [ + { + name: 'Checkout', + uses: 'actions/checkout@v5', + }, + ...githubSteps.flatMap(s => s.steps), + { + name: 'Comment on PR', + uses: 'actions/github-script@v8', + with: { + script: this.generatePRCommentScript(), + }, + }, + ], + }); + } + + /** + * Generates the script for commenting resource counts on PRs. + */ + private generatePRCommentScript(): string { + const warningThreshold = this.baseOptions.resourceCountWarningThreshold ?? 450; + return ` +const fs = require('fs'); +const resultsFile = 'resource-count-results.json'; + +if (!fs.existsSync(resultsFile)) { + console.log('No results file found'); + return; +} + +const results = JSON.parse(fs.readFileSync(resultsFile, 'utf8')); +const stacks = results.stacks || []; +const stacksWithWarnings = stacks.filter(s => s.resourceCount >= ${warningThreshold}); + +// Create comment body +let body = '## 📊 CloudFormation Resource Count\\n\\n'; +body += '### Summary\\n'; +body += \`- **Total stacks:** \${stacks.length}\\n\`; +body += \`- **Total resources:** \${results.totalResources}\\n\`; +body += \`- **Max resources in single stack:** \${results.maxResourcesInStack}\\n\`; +body += \`- **Warning threshold:** ${warningThreshold} resources\\n\`; +body += \`- **CloudFormation limit:** 500 resources per stack\\n\\n\`; + +if (stacksWithWarnings.length > 0) { + body += '### ⚠️ Stacks Approaching Limit\\n\\n'; + body += \`\${stacksWithWarnings.length} stack(s) have crossed the warning threshold:\\n\\n\`; + + for (const stack of stacksWithWarnings) { + const percentage = Math.round((stack.resourceCount / 500) * 100); + body += \`- **\${stack.stackName}**: \${stack.resourceCount} resources (\${percentage}% of limit)\\n\`; + } + + body += '\\n**Recommendations:**\\n'; + body += '- Consider breaking large stacks into smaller, focused stacks\\n'; + body += '- Use nested stacks for reusable components\\n'; + body += '- Review resource usage and remove unnecessary resources\\n\\n'; +} + +body += '### Stack Details\\n\\n'; +body += '| Stack | Resources | % of Limit | Status |\\n'; +body += '|-------|-----------|------------|--------|\\n'; + +const sortedStacks = stacks.sort((a, b) => b.resourceCount - a.resourceCount); +for (const stack of sortedStacks) { + const percentage = Math.round((stack.resourceCount / 500) * 100); + const status = stack.resourceCount >= ${warningThreshold} ? '⚠️ Warning' : '✅ OK'; + body += \`| \${stack.stackName} | \${stack.resourceCount} | \${percentage}% | \${status} |\\n\`; +} + +// Find existing comment +const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, +}); + +const botComment = comments.data.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('📊 CloudFormation Resource Count') +); + +if (botComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body, + }); + console.log('Updated existing PR comment'); +} else { + // Create new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body, + }); + console.log('Created new PR comment'); +} +`; + } } diff --git a/src/awscdk/index.ts b/src/awscdk/index.ts index 82b75be..151d451 100644 --- a/src/awscdk/index.ts +++ b/src/awscdk/index.ts @@ -3,3 +3,4 @@ export * from './github'; export * from './gitlab'; // export * from './codecatalyst'; export * from './bash'; +export * from './resource-count-step'; diff --git a/src/awscdk/resource-count-step.ts b/src/awscdk/resource-count-step.ts new file mode 100644 index 0000000..ca730a6 --- /dev/null +++ b/src/awscdk/resource-count-step.ts @@ -0,0 +1,63 @@ +import { Project } from 'projen'; +import { StepSequence, PipelineStep, SimpleCommandStep } from '../steps'; + +export interface ResourceCountStepProps { + /** + * Path to the cloud assembly directory + * @default 'cdk.out' + */ + readonly cloudAssemblyDir?: string; + + /** + * Warning threshold for resource count + * @default 450 + */ + readonly warningThreshold?: number; + + /** + * Output file for results + * @default 'resource-count-results.json' + */ + readonly outputFile?: string; + + /** + * Whether to write results to GitHub step summary + * @default true + */ + readonly githubSummary?: boolean; +} + +export class ResourceCountStep extends StepSequence { + private static generateCommand(props: ResourceCountStepProps): string { + const args: string[] = [ + 'count-resources', + '--cloud-assembly-dir', props.cloudAssemblyDir ?? 'cdk.out', + ]; + + if (props.warningThreshold !== undefined) { + args.push('--warning-threshold', props.warningThreshold.toString()); + } + + if (props.outputFile) { + args.push('--output-file', props.outputFile); + } + + if (props.githubSummary !== false) { + args.push('--github-summary'); + } + + return args.join(' '); + } + + constructor(project: Project, props: ResourceCountStepProps = {}) { + const steps: PipelineStep[] = []; + + // Add command step to run resource counting + const command = ResourceCountStep.generateCommand(props); + steps.push(new SimpleCommandStep(project, [command], { + CLOUD_ASSEMBLY_DIR: props.cloudAssemblyDir ?? 'cdk.out', + })); + + super(project, steps); + } +} diff --git a/test/__snapshots__/amplify-deploy.test.ts.snap b/test/__snapshots__/amplify-deploy.test.ts.snap index 2c4e141..41dad2a 100644 --- a/test/__snapshots__/amplify-deploy.test.ts.snap +++ b/test/__snapshots__/amplify-deploy.test.ts.snap @@ -18,6 +18,7 @@ jobs: contents: read env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -28,6 +29,12 @@ jobs: fetch-depth: 0 - run: npx projen install:ci - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: @@ -555,6 +562,7 @@ jobs: contents: read env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -565,6 +573,12 @@ jobs: fetch-depth: 0 - run: npx projen install:ci - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: @@ -694,6 +708,7 @@ jobs: contents: read env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -704,6 +719,12 @@ jobs: fetch-depth: 0 - run: npx projen install:ci - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: diff --git a/test/__snapshots__/github.test.ts.snap b/test/__snapshots__/github.test.ts.snap index 1553ba8..8df2f35 100644 --- a/test/__snapshots__/github.test.ts.snap +++ b/test/__snapshots__/github.test.ts.snap @@ -19,6 +19,7 @@ jobs: id-token: write env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -35,6 +36,12 @@ jobs: role-session-name: GitHubAction aws-region: us-east-1 - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: @@ -671,6 +678,7 @@ jobs: id-token: write env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -687,6 +695,12 @@ jobs: role-session-name: GitHubAction aws-region: us-east-1 - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: @@ -745,6 +759,7 @@ jobs: id-token: write env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -761,6 +776,12 @@ jobs: role-session-name: GitHubAction aws-region: us-east-1 - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: @@ -819,6 +840,7 @@ jobs: id-token: write env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -835,6 +857,12 @@ jobs: role-session-name: GitHubAction aws-region: us-east-1 - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: @@ -1044,6 +1072,7 @@ jobs: id-token: write env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -1060,6 +1089,12 @@ jobs: role-session-name: GitHubAction aws-region: us-east-1 - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: @@ -1253,6 +1288,7 @@ jobs: id-token: write env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -1269,6 +1305,12 @@ jobs: role-session-name: GitHubAction aws-region: us-east-1 - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: @@ -1854,6 +1896,7 @@ jobs: id-token: write env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -1870,6 +1913,12 @@ jobs: role-session-name: GitHubAction aws-region: us-east-1 - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: @@ -2032,6 +2081,7 @@ jobs: id-token: write env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -2049,6 +2099,12 @@ jobs: role-session-name: GitHubAction aws-region: us-east-1 - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: @@ -2159,6 +2215,7 @@ jobs: id-token: write env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -2175,6 +2232,12 @@ jobs: role-session-name: GitHubAction aws-region: us-east-1 - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: @@ -2530,6 +2593,7 @@ jobs: env: CI: "true" FOO: bar + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -2547,6 +2611,12 @@ jobs: role-session-name: GitHubAction aws-region: us-east-1 - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: @@ -2644,6 +2714,7 @@ jobs: id-token: write env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -2660,6 +2731,12 @@ jobs: role-session-name: GitHubAction aws-region: us-east-1 - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: From cc4d113ef41eb4f6974473bb70af83c25ae4d693 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 14:23:03 +0000 Subject: [PATCH 2/6] refactor(awscdk): move resource counting to base class and add GitLab support - Move provideResourceCountStep() to base CDKPipeline class - Update GitHub to use base implementation - Add resource counting to GitLab synth job (writes to console) - Add GitLab MR comment workflow for resource counts - GitLab MR comments use GitLab API to post/update comments - Resource counting now works consistently across GitHub and GitLab --- src/awscdk/base.ts | 14 + src/awscdk/github.ts | 22 +- src/awscdk/gitlab.ts | 135 ++++++ test/__snapshots__/gitlab.test.ts.snap | 586 ++++++++++++++++++++++++- 4 files changed, 738 insertions(+), 19 deletions(-) diff --git a/src/awscdk/base.ts b/src/awscdk/base.ts index adb691a..d18fa2a 100644 --- a/src/awscdk/base.ts +++ b/src/awscdk/base.ts @@ -4,6 +4,7 @@ import { NodePackageManager } from 'projen/lib/javascript'; import { PipelineEngine } from '../engine'; import { AwsAssumeRoleStep, PipelineStep, ProjenScriptStep, SimpleCommandStep, StepSequence } from '../steps'; import { VersioningConfig, VersioningSetup } from '../versioning'; +import { ResourceCountStep } from './resource-count-step'; /** * The Environment interface is designed to hold AWS related information @@ -294,6 +295,19 @@ export abstract class CDKPipeline extends Component { return seq; } + protected provideResourceCountStep(githubSummary: boolean = false): PipelineStep | undefined { + if (this.baseOptions.enableResourceCounting === false) { + return undefined; + } + + return new ResourceCountStep(this.project, { + cloudAssemblyDir: this.app.cdkConfig.cdkout, + warningThreshold: this.baseOptions.resourceCountWarningThreshold ?? 450, + outputFile: 'resource-count-results.json', + githubSummary, + }); + } + protected provideAssetUploadStep(stageName?: string): PipelineStep { const seq = new StepSequence(this.project, []); diff --git a/src/awscdk/github.ts b/src/awscdk/github.ts index f037a7e..5cf7bf6 100644 --- a/src/awscdk/github.ts +++ b/src/awscdk/github.ts @@ -5,7 +5,6 @@ import { CdkDiffType, CDKPipeline, CDKPipelineOptions, DeploymentStage, Independ import { PipelineEngine } from '../engine'; import { mergeJobPermissions } from '../engines'; import { AwsAssumeRoleStep, PipelineStep, ProjenScriptStep, SimpleCommandStep } from '../steps'; -import { ResourceCountStep } from './resource-count-step'; import { DownloadArtifactStep, UploadArtifactStep } from '../steps/artifact-steps'; import { GithubPackagesLoginStep } from '../steps/registries'; @@ -250,13 +249,9 @@ export class GithubCDKPipeline extends CDKPipeline { steps.push(this.provideSynthStep()); // Add resource counting step if enabled - if (this.baseOptions.enableResourceCounting !== false) { - steps.push(new ResourceCountStep(this.project, { - cloudAssemblyDir: this.app.cdkConfig.cdkout, - warningThreshold: this.baseOptions.resourceCountWarningThreshold ?? 450, - outputFile: 'resource-count-results.json', - githubSummary: true, - })); + const resourceCountStep = this.provideResourceCountStep(true); + if (resourceCountStep) { + steps.push(resourceCountStep); // Upload resource count results as artifact steps.push(new UploadArtifactStep(this.project, { @@ -543,12 +538,11 @@ export class GithubCDKPipeline extends CDKPipeline { const steps: PipelineStep[] = []; steps.push(this.provideInstallStep()); steps.push(this.provideSynthStep()); - steps.push(new ResourceCountStep(this.project, { - cloudAssemblyDir: this.app.cdkConfig.cdkout, - warningThreshold: this.baseOptions.resourceCountWarningThreshold ?? 450, - outputFile: 'resource-count-results.json', - githubSummary: true, - })); + + const resourceCountStep = this.provideResourceCountStep(true); + if (resourceCountStep) { + steps.push(resourceCountStep); + } const githubSteps = steps.map(s => s.toGithub()); diff --git a/src/awscdk/gitlab.ts b/src/awscdk/gitlab.ts index dfac5f7..527f875 100644 --- a/src/awscdk/gitlab.ts +++ b/src/awscdk/gitlab.ts @@ -87,6 +87,11 @@ export class GitlabCDKPipeline extends CDKPipeline { if (options.featureStages) { this.createFeatureWorkflows(); } + + // Create MR workflow for resource counting if enabled + if (this.baseOptions.enableResourceCounting !== false) { + this.createResourceCountMRJob(); + } } /** @@ -145,6 +150,12 @@ export class GitlabCDKPipeline extends CDKPipeline { this.provideSynthStep(), ]; + // Add resource counting step if enabled + const resourceCountStep = this.provideResourceCountStep(false); + if (resourceCountStep) { + steps.push(resourceCountStep); + } + const gitlabSteps = steps.map(s => s.toGitlab()); this.config.addStages('synth'); @@ -292,5 +303,129 @@ export class GitlabCDKPipeline extends CDKPipeline { return PipelineEngine.GITLAB; } + /** + * Creates a job to count resources and comment on merge requests. + */ + private createResourceCountMRJob(): void { + const steps: PipelineStep[] = [ + this.provideInstallStep(), + this.provideSynthStep(), + ]; + + const resourceCountStep = this.provideResourceCountStep(false); + if (resourceCountStep) { + steps.push(resourceCountStep); + } + + const gitlabSteps = steps.map(s => s.toGitlab()); + + this.config.addStages('resource-count-mr'); + this.config.addJobs({ + 'resource-count-mr': { + extends: ['.aws_base', ...gitlabSteps.flatMap(s => s.extensions)], + stage: 'resource-count-mr', + tags: this.options.runnerTags?.synth ?? this.options.runnerTags?.default, + rules: [ + { + if: '$CI_PIPELINE_SOURCE == "merge_request_event"', + }, + ], + needs: gitlabSteps.flatMap(s => s.needs), + script: [ + ...gitlabSteps.flatMap(s => s.commands), + // Add script to post comment to MR + this.generateMRCommentScript(), + ], + variables: gitlabSteps.reduce((acc, step) => ({ ...acc, ...step.env }), {}), + }, + }); + } + + /** + * Generates the script for posting resource counts to GitLab MR comments. + */ + private generateMRCommentScript(): string { + const warningThreshold = this.baseOptions.resourceCountWarningThreshold ?? 450; + return ` +# Post comment to GitLab MR +if [ -f resource-count-results.json ]; then + echo "Posting resource count to merge request..." + + # Parse results + STACKS_COUNT=$(jq '.stacks | length' resource-count-results.json) + TOTAL_RESOURCES=$(jq '.totalResources' resource-count-results.json) + MAX_RESOURCES=$(jq '.maxResourcesInStack' resource-count-results.json) + STACKS_WITH_WARNINGS=$(jq '[.stacks[] | select(.resourceCount >= ${warningThreshold})] | length' resource-count-results.json) + + # Build comment body + COMMENT_BODY="## 📊 CloudFormation Resource Count\\n\\n" + COMMENT_BODY+="### Summary\\n" + COMMENT_BODY+="- **Total stacks:** $STACKS_COUNT\\n" + COMMENT_BODY+="- **Total resources:** $TOTAL_RESOURCES\\n" + COMMENT_BODY+="- **Max resources in single stack:** $MAX_RESOURCES\\n" + COMMENT_BODY+="- **Warning threshold:** ${warningThreshold} resources\\n" + COMMENT_BODY+="- **CloudFormation limit:** 500 resources per stack\\n\\n" + + # Add warnings if present + if [ "$STACKS_WITH_WARNINGS" -gt 0 ]; then + COMMENT_BODY+="### ⚠️ Stacks Approaching Limit\\n\\n" + COMMENT_BODY+="$STACKS_WITH_WARNINGS stack(s) have crossed the warning threshold:\\n\\n" + + jq -r '.stacks[] | select(.resourceCount >= ${warningThreshold}) | "- **\\(.stackName)**: \\(.resourceCount) resources (\\((.resourceCount * 100 / 500) | floor)% of limit)"' resource-count-results.json | while read line; do + COMMENT_BODY+="$line\\n" + done + + COMMENT_BODY+="\\n**Recommendations:**\\n" + COMMENT_BODY+="- Consider breaking large stacks into smaller, focused stacks\\n" + COMMENT_BODY+="- Use nested stacks for reusable components\\n" + COMMENT_BODY+="- Review resource usage and remove unnecessary resources\\n\\n" + fi + + # Add stack details table + COMMENT_BODY+="### Stack Details\\n\\n" + COMMENT_BODY+="| Stack | Resources | % of Limit | Status |\\n" + COMMENT_BODY+="| ----- | --------- | ---------- | ------ |\\n" + + jq -r '.stacks | sort_by(-.resourceCount)[] | "| \\(.stackName) | \\(.resourceCount) | \\((.resourceCount * 100 / 500) | floor)% | \\(if .resourceCount >= ${warningThreshold} then "⚠️ Warning" else "✅ OK" end) |"' resource-count-results.json | while read line; do + COMMENT_BODY+="$line\\n" + done + + # Post comment to MR using GitLab API + if [ -n "\${CI_MERGE_REQUEST_IID}" ]; then + # Check for existing comment + EXISTING_COMMENT_ID=$(curl --silent --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes" \\ + | jq -r '.[] | select(.body | contains("📊 CloudFormation Resource Count")) | .id' | head -1) + + # Prepare JSON payload + JSON_PAYLOAD=$(jq -n --arg body "$COMMENT_BODY" '{body: $body}') + + if [ -n "\${EXISTING_COMMENT_ID}" ]; then + # Update existing comment + curl --silent --request PUT \\ + --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + --header "Content-Type: application/json" \\ + --data "$JSON_PAYLOAD" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes/\${EXISTING_COMMENT_ID}" + echo "Updated existing MR comment" + else + # Create new comment + curl --silent --request POST \\ + --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + --header "Content-Type: application/json" \\ + --data "$JSON_PAYLOAD" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes" + echo "Created new MR comment" + fi + else + echo "Not running in merge request context, skipping comment" + fi +else + echo "Resource count results not found" +fi +`; + } + } + diff --git a/test/__snapshots__/gitlab.test.ts.snap b/test/__snapshots__/gitlab.test.ts.snap index bb7d54c..e22a25b 100644 --- a/test/__snapshots__/gitlab.test.ts.snap +++ b/test/__snapshots__/gitlab.test.ts.snap @@ -8,6 +8,7 @@ stages: - publish_assets - dev - prod + - resource-count-mr .artifacts_cdk: artifacts: when: on_success @@ -42,7 +43,9 @@ synth: - npx projen install:ci - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role-with-web-identity --role-arn "synthRole" --role-session-name "GitLabRunner-\${CI_PROJECT_ID}-\${CI_PIPELINE_ID}}" --web-identity-token \${AWS_TOKEN} --duration-seconds 3600 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)) - npx projen build - variables: {} + - count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json + variables: + CLOUD_ASSEMBLY_DIR: cdk.out publish_assets: extends: - .aws_base @@ -130,6 +133,98 @@ deploy-prod: - npx projen deploy:prod variables: AWS_REGION: eu-central-1 +resource-count-mr: + extends: + - .aws_base + stage: resource-count-mr + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + needs: [] + script: + - npx projen install:ci + - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role-with-web-identity --role-arn "synthRole" --role-session-name "GitLabRunner-\${CI_PROJECT_ID}-\${CI_PIPELINE_ID}}" --web-identity-token \${AWS_TOKEN} --duration-seconds 3600 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)) + - npx projen build + - count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json + - | + + # Post comment to GitLab MR + if [ -f resource-count-results.json ]; then + echo "Posting resource count to merge request..." + + # Parse results + STACKS_COUNT=$(jq '.stacks | length' resource-count-results.json) + TOTAL_RESOURCES=$(jq '.totalResources' resource-count-results.json) + MAX_RESOURCES=$(jq '.maxResourcesInStack' resource-count-results.json) + STACKS_WITH_WARNINGS=$(jq '[.stacks[] | select(.resourceCount >= 450)] | length' resource-count-results.json) + + # Build comment body + COMMENT_BODY="## 📊 CloudFormation Resource Count\\n\\n" + COMMENT_BODY+="### Summary\\n" + COMMENT_BODY+="- **Total stacks:** $STACKS_COUNT\\n" + COMMENT_BODY+="- **Total resources:** $TOTAL_RESOURCES\\n" + COMMENT_BODY+="- **Max resources in single stack:** $MAX_RESOURCES\\n" + COMMENT_BODY+="- **Warning threshold:** 450 resources\\n" + COMMENT_BODY+="- **CloudFormation limit:** 500 resources per stack\\n\\n" + + # Add warnings if present + if [ "$STACKS_WITH_WARNINGS" -gt 0 ]; then + COMMENT_BODY+="### ⚠️ Stacks Approaching Limit\\n\\n" + COMMENT_BODY+="$STACKS_WITH_WARNINGS stack(s) have crossed the warning threshold:\\n\\n" + + jq -r '.stacks[] | select(.resourceCount >= 450) | "- **\\(.stackName)**: \\(.resourceCount) resources (\\((.resourceCount * 100 / 500) | floor)% of limit)"' resource-count-results.json | while read line; do + COMMENT_BODY+="$line\\n" + done + + COMMENT_BODY+="\\n**Recommendations:**\\n" + COMMENT_BODY+="- Consider breaking large stacks into smaller, focused stacks\\n" + COMMENT_BODY+="- Use nested stacks for reusable components\\n" + COMMENT_BODY+="- Review resource usage and remove unnecessary resources\\n\\n" + fi + + # Add stack details table + COMMENT_BODY+="### Stack Details\\n\\n" + COMMENT_BODY+="| Stack | Resources | % of Limit | Status |\\n" + COMMENT_BODY+="| ----- | --------- | ---------- | ------ |\\n" + + jq -r '.stacks | sort_by(-.resourceCount)[] | "| \\(.stackName) | \\(.resourceCount) | \\((.resourceCount * 100 / 500) | floor)% | \\(if .resourceCount >= 450 then "⚠️ Warning" else "✅ OK" end) |"' resource-count-results.json | while read line; do + COMMENT_BODY+="$line\\n" + done + + # Post comment to MR using GitLab API + if [ -n "\${CI_MERGE_REQUEST_IID}" ]; then + # Check for existing comment + EXISTING_COMMENT_ID=$(curl --silent --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes" \\ + | jq -r '.[] | select(.body | contains("📊 CloudFormation Resource Count")) | .id' | head -1) + + # Prepare JSON payload + JSON_PAYLOAD=$(jq -n --arg body "$COMMENT_BODY" '{body: $body}') + + if [ -n "\${EXISTING_COMMENT_ID}" ]; then + # Update existing comment + curl --silent --request PUT \\ + --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + --header "Content-Type: application/json" \\ + --data "$JSON_PAYLOAD" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes/\${EXISTING_COMMENT_ID}" + echo "Updated existing MR comment" + else + # Create new comment + curl --silent --request POST \\ + --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + --header "Content-Type: application/json" \\ + --data "$JSON_PAYLOAD" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes" + echo "Created new MR comment" + fi + else + echo "Not running in merge request context, skipping comment" + fi + else + echo "Resource count results not found" + fi + variables: + CLOUD_ASSEMBLY_DIR: cdk.out " `; @@ -249,6 +344,7 @@ stages: - publish_assets - independent1 - independent2 + - resource-count-mr .artifacts_cdk: artifacts: when: on_success @@ -283,7 +379,9 @@ synth: - npx projen install:ci - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role-with-web-identity --role-arn "synthRole" --role-session-name "GitLabRunner-\${CI_PROJECT_ID}-\${CI_PIPELINE_ID}}" --web-identity-token \${AWS_TOKEN} --duration-seconds 3600 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)) - npx projen build - variables: {} + - count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json + variables: + CLOUD_ASSEMBLY_DIR: cdk.out publish_assets: extends: - .aws_base @@ -334,6 +432,98 @@ deploy-independent2: variables: AWS_REGION: eu-central-1 FOO: bar +resource-count-mr: + extends: + - .aws_base + stage: resource-count-mr + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + needs: [] + script: + - npx projen install:ci + - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role-with-web-identity --role-arn "synthRole" --role-session-name "GitLabRunner-\${CI_PROJECT_ID}-\${CI_PIPELINE_ID}}" --web-identity-token \${AWS_TOKEN} --duration-seconds 3600 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)) + - npx projen build + - count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json + - | + + # Post comment to GitLab MR + if [ -f resource-count-results.json ]; then + echo "Posting resource count to merge request..." + + # Parse results + STACKS_COUNT=$(jq '.stacks | length' resource-count-results.json) + TOTAL_RESOURCES=$(jq '.totalResources' resource-count-results.json) + MAX_RESOURCES=$(jq '.maxResourcesInStack' resource-count-results.json) + STACKS_WITH_WARNINGS=$(jq '[.stacks[] | select(.resourceCount >= 450)] | length' resource-count-results.json) + + # Build comment body + COMMENT_BODY="## 📊 CloudFormation Resource Count\\n\\n" + COMMENT_BODY+="### Summary\\n" + COMMENT_BODY+="- **Total stacks:** $STACKS_COUNT\\n" + COMMENT_BODY+="- **Total resources:** $TOTAL_RESOURCES\\n" + COMMENT_BODY+="- **Max resources in single stack:** $MAX_RESOURCES\\n" + COMMENT_BODY+="- **Warning threshold:** 450 resources\\n" + COMMENT_BODY+="- **CloudFormation limit:** 500 resources per stack\\n\\n" + + # Add warnings if present + if [ "$STACKS_WITH_WARNINGS" -gt 0 ]; then + COMMENT_BODY+="### ⚠️ Stacks Approaching Limit\\n\\n" + COMMENT_BODY+="$STACKS_WITH_WARNINGS stack(s) have crossed the warning threshold:\\n\\n" + + jq -r '.stacks[] | select(.resourceCount >= 450) | "- **\\(.stackName)**: \\(.resourceCount) resources (\\((.resourceCount * 100 / 500) | floor)% of limit)"' resource-count-results.json | while read line; do + COMMENT_BODY+="$line\\n" + done + + COMMENT_BODY+="\\n**Recommendations:**\\n" + COMMENT_BODY+="- Consider breaking large stacks into smaller, focused stacks\\n" + COMMENT_BODY+="- Use nested stacks for reusable components\\n" + COMMENT_BODY+="- Review resource usage and remove unnecessary resources\\n\\n" + fi + + # Add stack details table + COMMENT_BODY+="### Stack Details\\n\\n" + COMMENT_BODY+="| Stack | Resources | % of Limit | Status |\\n" + COMMENT_BODY+="| ----- | --------- | ---------- | ------ |\\n" + + jq -r '.stacks | sort_by(-.resourceCount)[] | "| \\(.stackName) | \\(.resourceCount) | \\((.resourceCount * 100 / 500) | floor)% | \\(if .resourceCount >= 450 then "⚠️ Warning" else "✅ OK" end) |"' resource-count-results.json | while read line; do + COMMENT_BODY+="$line\\n" + done + + # Post comment to MR using GitLab API + if [ -n "\${CI_MERGE_REQUEST_IID}" ]; then + # Check for existing comment + EXISTING_COMMENT_ID=$(curl --silent --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes" \\ + | jq -r '.[] | select(.body | contains("📊 CloudFormation Resource Count")) | .id' | head -1) + + # Prepare JSON payload + JSON_PAYLOAD=$(jq -n --arg body "$COMMENT_BODY" '{body: $body}') + + if [ -n "\${EXISTING_COMMENT_ID}" ]; then + # Update existing comment + curl --silent --request PUT \\ + --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + --header "Content-Type: application/json" \\ + --data "$JSON_PAYLOAD" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes/\${EXISTING_COMMENT_ID}" + echo "Updated existing MR comment" + else + # Create new comment + curl --silent --request POST \\ + --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + --header "Content-Type: application/json" \\ + --data "$JSON_PAYLOAD" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes" + echo "Created new MR comment" + fi + else + echo "Not running in merge request context, skipping comment" + fi + else + echo "Resource count results not found" + fi + variables: + CLOUD_ASSEMBLY_DIR: cdk.out " `; @@ -346,6 +536,7 @@ stages: - synth - publish_assets - prod + - resource-count-mr .artifacts_cdk: artifacts: when: on_success @@ -381,8 +572,10 @@ synth: - npx projen install:ci - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role-with-web-identity --role-arn "synthRole" --role-session-name "GitLabRunner-\${CI_PROJECT_ID}-\${CI_PIPELINE_ID}}" --web-identity-token \${AWS_TOKEN} --duration-seconds 3600 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)) - npx projen build + - count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json variables: FOO: bar + CLOUD_ASSEMBLY_DIR: cdk.out publish_assets: extends: - .aws_base @@ -437,6 +630,100 @@ deploy-prod: variables: FOO: bar AWS_REGION: eu-central-1 +resource-count-mr: + extends: + - .aws_base + stage: resource-count-mr + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + needs: [] + script: + - echo Login + - npx projen install:ci + - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role-with-web-identity --role-arn "synthRole" --role-session-name "GitLabRunner-\${CI_PROJECT_ID}-\${CI_PIPELINE_ID}}" --web-identity-token \${AWS_TOKEN} --duration-seconds 3600 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)) + - npx projen build + - count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json + - | + + # Post comment to GitLab MR + if [ -f resource-count-results.json ]; then + echo "Posting resource count to merge request..." + + # Parse results + STACKS_COUNT=$(jq '.stacks | length' resource-count-results.json) + TOTAL_RESOURCES=$(jq '.totalResources' resource-count-results.json) + MAX_RESOURCES=$(jq '.maxResourcesInStack' resource-count-results.json) + STACKS_WITH_WARNINGS=$(jq '[.stacks[] | select(.resourceCount >= 450)] | length' resource-count-results.json) + + # Build comment body + COMMENT_BODY="## 📊 CloudFormation Resource Count\\n\\n" + COMMENT_BODY+="### Summary\\n" + COMMENT_BODY+="- **Total stacks:** $STACKS_COUNT\\n" + COMMENT_BODY+="- **Total resources:** $TOTAL_RESOURCES\\n" + COMMENT_BODY+="- **Max resources in single stack:** $MAX_RESOURCES\\n" + COMMENT_BODY+="- **Warning threshold:** 450 resources\\n" + COMMENT_BODY+="- **CloudFormation limit:** 500 resources per stack\\n\\n" + + # Add warnings if present + if [ "$STACKS_WITH_WARNINGS" -gt 0 ]; then + COMMENT_BODY+="### ⚠️ Stacks Approaching Limit\\n\\n" + COMMENT_BODY+="$STACKS_WITH_WARNINGS stack(s) have crossed the warning threshold:\\n\\n" + + jq -r '.stacks[] | select(.resourceCount >= 450) | "- **\\(.stackName)**: \\(.resourceCount) resources (\\((.resourceCount * 100 / 500) | floor)% of limit)"' resource-count-results.json | while read line; do + COMMENT_BODY+="$line\\n" + done + + COMMENT_BODY+="\\n**Recommendations:**\\n" + COMMENT_BODY+="- Consider breaking large stacks into smaller, focused stacks\\n" + COMMENT_BODY+="- Use nested stacks for reusable components\\n" + COMMENT_BODY+="- Review resource usage and remove unnecessary resources\\n\\n" + fi + + # Add stack details table + COMMENT_BODY+="### Stack Details\\n\\n" + COMMENT_BODY+="| Stack | Resources | % of Limit | Status |\\n" + COMMENT_BODY+="| ----- | --------- | ---------- | ------ |\\n" + + jq -r '.stacks | sort_by(-.resourceCount)[] | "| \\(.stackName) | \\(.resourceCount) | \\((.resourceCount * 100 / 500) | floor)% | \\(if .resourceCount >= 450 then "⚠️ Warning" else "✅ OK" end) |"' resource-count-results.json | while read line; do + COMMENT_BODY+="$line\\n" + done + + # Post comment to MR using GitLab API + if [ -n "\${CI_MERGE_REQUEST_IID}" ]; then + # Check for existing comment + EXISTING_COMMENT_ID=$(curl --silent --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes" \\ + | jq -r '.[] | select(.body | contains("📊 CloudFormation Resource Count")) | .id' | head -1) + + # Prepare JSON payload + JSON_PAYLOAD=$(jq -n --arg body "$COMMENT_BODY" '{body: $body}') + + if [ -n "\${EXISTING_COMMENT_ID}" ]; then + # Update existing comment + curl --silent --request PUT \\ + --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + --header "Content-Type: application/json" \\ + --data "$JSON_PAYLOAD" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes/\${EXISTING_COMMENT_ID}" + echo "Updated existing MR comment" + else + # Create new comment + curl --silent --request POST \\ + --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + --header "Content-Type: application/json" \\ + --data "$JSON_PAYLOAD" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes" + echo "Created new MR comment" + fi + else + echo "Not running in merge request context, skipping comment" + fi + else + echo "Resource count results not found" + fi + variables: + FOO: bar + CLOUD_ASSEMBLY_DIR: cdk.out " `; @@ -448,6 +735,7 @@ stages: - publish_assets - dev - prod + - resource-count-mr .artifacts_cdk: artifacts: when: on_success @@ -484,7 +772,9 @@ synth: - npx projen install:ci - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role-with-web-identity --role-arn "synthRole" --role-session-name "GitLabRunner-\${CI_PROJECT_ID}-\${CI_PIPELINE_ID}}" --web-identity-token \${AWS_TOKEN} --duration-seconds 3600 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)) - npx projen build - variables: {} + - count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json + variables: + CLOUD_ASSEMBLY_DIR: cdk.out publish_assets: extends: - .aws_base @@ -580,6 +870,100 @@ deploy-prod: - npx projen deploy:prod variables: AWS_REGION: eu-central-1 +resource-count-mr: + extends: + - .aws_base + stage: resource-count-mr + tags: + - defaultTag + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + needs: [] + script: + - npx projen install:ci + - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role-with-web-identity --role-arn "synthRole" --role-session-name "GitLabRunner-\${CI_PROJECT_ID}-\${CI_PIPELINE_ID}}" --web-identity-token \${AWS_TOKEN} --duration-seconds 3600 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)) + - npx projen build + - count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json + - | + + # Post comment to GitLab MR + if [ -f resource-count-results.json ]; then + echo "Posting resource count to merge request..." + + # Parse results + STACKS_COUNT=$(jq '.stacks | length' resource-count-results.json) + TOTAL_RESOURCES=$(jq '.totalResources' resource-count-results.json) + MAX_RESOURCES=$(jq '.maxResourcesInStack' resource-count-results.json) + STACKS_WITH_WARNINGS=$(jq '[.stacks[] | select(.resourceCount >= 450)] | length' resource-count-results.json) + + # Build comment body + COMMENT_BODY="## 📊 CloudFormation Resource Count\\n\\n" + COMMENT_BODY+="### Summary\\n" + COMMENT_BODY+="- **Total stacks:** $STACKS_COUNT\\n" + COMMENT_BODY+="- **Total resources:** $TOTAL_RESOURCES\\n" + COMMENT_BODY+="- **Max resources in single stack:** $MAX_RESOURCES\\n" + COMMENT_BODY+="- **Warning threshold:** 450 resources\\n" + COMMENT_BODY+="- **CloudFormation limit:** 500 resources per stack\\n\\n" + + # Add warnings if present + if [ "$STACKS_WITH_WARNINGS" -gt 0 ]; then + COMMENT_BODY+="### ⚠️ Stacks Approaching Limit\\n\\n" + COMMENT_BODY+="$STACKS_WITH_WARNINGS stack(s) have crossed the warning threshold:\\n\\n" + + jq -r '.stacks[] | select(.resourceCount >= 450) | "- **\\(.stackName)**: \\(.resourceCount) resources (\\((.resourceCount * 100 / 500) | floor)% of limit)"' resource-count-results.json | while read line; do + COMMENT_BODY+="$line\\n" + done + + COMMENT_BODY+="\\n**Recommendations:**\\n" + COMMENT_BODY+="- Consider breaking large stacks into smaller, focused stacks\\n" + COMMENT_BODY+="- Use nested stacks for reusable components\\n" + COMMENT_BODY+="- Review resource usage and remove unnecessary resources\\n\\n" + fi + + # Add stack details table + COMMENT_BODY+="### Stack Details\\n\\n" + COMMENT_BODY+="| Stack | Resources | % of Limit | Status |\\n" + COMMENT_BODY+="| ----- | --------- | ---------- | ------ |\\n" + + jq -r '.stacks | sort_by(-.resourceCount)[] | "| \\(.stackName) | \\(.resourceCount) | \\((.resourceCount * 100 / 500) | floor)% | \\(if .resourceCount >= 450 then "⚠️ Warning" else "✅ OK" end) |"' resource-count-results.json | while read line; do + COMMENT_BODY+="$line\\n" + done + + # Post comment to MR using GitLab API + if [ -n "\${CI_MERGE_REQUEST_IID}" ]; then + # Check for existing comment + EXISTING_COMMENT_ID=$(curl --silent --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes" \\ + | jq -r '.[] | select(.body | contains("📊 CloudFormation Resource Count")) | .id' | head -1) + + # Prepare JSON payload + JSON_PAYLOAD=$(jq -n --arg body "$COMMENT_BODY" '{body: $body}') + + if [ -n "\${EXISTING_COMMENT_ID}" ]; then + # Update existing comment + curl --silent --request PUT \\ + --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + --header "Content-Type: application/json" \\ + --data "$JSON_PAYLOAD" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes/\${EXISTING_COMMENT_ID}" + echo "Updated existing MR comment" + else + # Create new comment + curl --silent --request POST \\ + --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + --header "Content-Type: application/json" \\ + --data "$JSON_PAYLOAD" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes" + echo "Created new MR comment" + fi + else + echo "Not running in merge request context, skipping comment" + fi + else + echo "Resource count results not found" + fi + variables: + CLOUD_ASSEMBLY_DIR: cdk.out " `; @@ -591,6 +975,7 @@ stages: - publish_assets - dev - prod + - resource-count-mr .artifacts_cdk: artifacts: when: on_success @@ -627,7 +1012,9 @@ synth: - npx projen install:ci - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role-with-web-identity --role-arn "synthRole" --role-session-name "GitLabRunner-\${CI_PROJECT_ID}-\${CI_PIPELINE_ID}}" --web-identity-token \${AWS_TOKEN} --duration-seconds 3600 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)) - npx projen build - variables: {} + - count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json + variables: + CLOUD_ASSEMBLY_DIR: cdk.out publish_assets: extends: - .aws_base @@ -723,6 +1110,100 @@ deploy-prod: - npx projen deploy:prod variables: AWS_REGION: eu-central-1 +resource-count-mr: + extends: + - .aws_base + stage: resource-count-mr + tags: + - synthTag + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + needs: [] + script: + - npx projen install:ci + - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role-with-web-identity --role-arn "synthRole" --role-session-name "GitLabRunner-\${CI_PROJECT_ID}-\${CI_PIPELINE_ID}}" --web-identity-token \${AWS_TOKEN} --duration-seconds 3600 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)) + - npx projen build + - count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json + - | + + # Post comment to GitLab MR + if [ -f resource-count-results.json ]; then + echo "Posting resource count to merge request..." + + # Parse results + STACKS_COUNT=$(jq '.stacks | length' resource-count-results.json) + TOTAL_RESOURCES=$(jq '.totalResources' resource-count-results.json) + MAX_RESOURCES=$(jq '.maxResourcesInStack' resource-count-results.json) + STACKS_WITH_WARNINGS=$(jq '[.stacks[] | select(.resourceCount >= 450)] | length' resource-count-results.json) + + # Build comment body + COMMENT_BODY="## 📊 CloudFormation Resource Count\\n\\n" + COMMENT_BODY+="### Summary\\n" + COMMENT_BODY+="- **Total stacks:** $STACKS_COUNT\\n" + COMMENT_BODY+="- **Total resources:** $TOTAL_RESOURCES\\n" + COMMENT_BODY+="- **Max resources in single stack:** $MAX_RESOURCES\\n" + COMMENT_BODY+="- **Warning threshold:** 450 resources\\n" + COMMENT_BODY+="- **CloudFormation limit:** 500 resources per stack\\n\\n" + + # Add warnings if present + if [ "$STACKS_WITH_WARNINGS" -gt 0 ]; then + COMMENT_BODY+="### ⚠️ Stacks Approaching Limit\\n\\n" + COMMENT_BODY+="$STACKS_WITH_WARNINGS stack(s) have crossed the warning threshold:\\n\\n" + + jq -r '.stacks[] | select(.resourceCount >= 450) | "- **\\(.stackName)**: \\(.resourceCount) resources (\\((.resourceCount * 100 / 500) | floor)% of limit)"' resource-count-results.json | while read line; do + COMMENT_BODY+="$line\\n" + done + + COMMENT_BODY+="\\n**Recommendations:**\\n" + COMMENT_BODY+="- Consider breaking large stacks into smaller, focused stacks\\n" + COMMENT_BODY+="- Use nested stacks for reusable components\\n" + COMMENT_BODY+="- Review resource usage and remove unnecessary resources\\n\\n" + fi + + # Add stack details table + COMMENT_BODY+="### Stack Details\\n\\n" + COMMENT_BODY+="| Stack | Resources | % of Limit | Status |\\n" + COMMENT_BODY+="| ----- | --------- | ---------- | ------ |\\n" + + jq -r '.stacks | sort_by(-.resourceCount)[] | "| \\(.stackName) | \\(.resourceCount) | \\((.resourceCount * 100 / 500) | floor)% | \\(if .resourceCount >= 450 then "⚠️ Warning" else "✅ OK" end) |"' resource-count-results.json | while read line; do + COMMENT_BODY+="$line\\n" + done + + # Post comment to MR using GitLab API + if [ -n "\${CI_MERGE_REQUEST_IID}" ]; then + # Check for existing comment + EXISTING_COMMENT_ID=$(curl --silent --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes" \\ + | jq -r '.[] | select(.body | contains("📊 CloudFormation Resource Count")) | .id' | head -1) + + # Prepare JSON payload + JSON_PAYLOAD=$(jq -n --arg body "$COMMENT_BODY" '{body: $body}') + + if [ -n "\${EXISTING_COMMENT_ID}" ]; then + # Update existing comment + curl --silent --request PUT \\ + --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + --header "Content-Type: application/json" \\ + --data "$JSON_PAYLOAD" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes/\${EXISTING_COMMENT_ID}" + echo "Updated existing MR comment" + else + # Create new comment + curl --silent --request POST \\ + --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + --header "Content-Type: application/json" \\ + --data "$JSON_PAYLOAD" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes" + echo "Created new MR comment" + fi + else + echo "Not running in merge request context, skipping comment" + fi + else + echo "Resource count results not found" + fi + variables: + CLOUD_ASSEMBLY_DIR: cdk.out " `; @@ -734,6 +1215,7 @@ stages: - publish_assets - dev - prod + - resource-count-mr .artifacts_cdk: artifacts: when: on_success @@ -768,7 +1250,9 @@ synth: - npx projen install:ci - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role-with-web-identity --role-arn "synthRole" --role-session-name "GitLabRunner-\${CI_PROJECT_ID}-\${CI_PIPELINE_ID}}" --web-identity-token \${AWS_TOKEN} --duration-seconds 3600 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)) - npx projen build - variables: {} + - count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json + variables: + CLOUD_ASSEMBLY_DIR: cdk.out publish_assets: extends: - .aws_base @@ -853,6 +1337,98 @@ deploy-prod: - npx projen deploy:prod variables: AWS_REGION: eu-central-1 +resource-count-mr: + extends: + - .aws_base + stage: resource-count-mr + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + needs: [] + script: + - npx projen install:ci + - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role-with-web-identity --role-arn "synthRole" --role-session-name "GitLabRunner-\${CI_PROJECT_ID}-\${CI_PIPELINE_ID}}" --web-identity-token \${AWS_TOKEN} --duration-seconds 3600 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)) + - npx projen build + - count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json + - | + + # Post comment to GitLab MR + if [ -f resource-count-results.json ]; then + echo "Posting resource count to merge request..." + + # Parse results + STACKS_COUNT=$(jq '.stacks | length' resource-count-results.json) + TOTAL_RESOURCES=$(jq '.totalResources' resource-count-results.json) + MAX_RESOURCES=$(jq '.maxResourcesInStack' resource-count-results.json) + STACKS_WITH_WARNINGS=$(jq '[.stacks[] | select(.resourceCount >= 450)] | length' resource-count-results.json) + + # Build comment body + COMMENT_BODY="## 📊 CloudFormation Resource Count\\n\\n" + COMMENT_BODY+="### Summary\\n" + COMMENT_BODY+="- **Total stacks:** $STACKS_COUNT\\n" + COMMENT_BODY+="- **Total resources:** $TOTAL_RESOURCES\\n" + COMMENT_BODY+="- **Max resources in single stack:** $MAX_RESOURCES\\n" + COMMENT_BODY+="- **Warning threshold:** 450 resources\\n" + COMMENT_BODY+="- **CloudFormation limit:** 500 resources per stack\\n\\n" + + # Add warnings if present + if [ "$STACKS_WITH_WARNINGS" -gt 0 ]; then + COMMENT_BODY+="### ⚠️ Stacks Approaching Limit\\n\\n" + COMMENT_BODY+="$STACKS_WITH_WARNINGS stack(s) have crossed the warning threshold:\\n\\n" + + jq -r '.stacks[] | select(.resourceCount >= 450) | "- **\\(.stackName)**: \\(.resourceCount) resources (\\((.resourceCount * 100 / 500) | floor)% of limit)"' resource-count-results.json | while read line; do + COMMENT_BODY+="$line\\n" + done + + COMMENT_BODY+="\\n**Recommendations:**\\n" + COMMENT_BODY+="- Consider breaking large stacks into smaller, focused stacks\\n" + COMMENT_BODY+="- Use nested stacks for reusable components\\n" + COMMENT_BODY+="- Review resource usage and remove unnecessary resources\\n\\n" + fi + + # Add stack details table + COMMENT_BODY+="### Stack Details\\n\\n" + COMMENT_BODY+="| Stack | Resources | % of Limit | Status |\\n" + COMMENT_BODY+="| ----- | --------- | ---------- | ------ |\\n" + + jq -r '.stacks | sort_by(-.resourceCount)[] | "| \\(.stackName) | \\(.resourceCount) | \\((.resourceCount * 100 / 500) | floor)% | \\(if .resourceCount >= 450 then "⚠️ Warning" else "✅ OK" end) |"' resource-count-results.json | while read line; do + COMMENT_BODY+="$line\\n" + done + + # Post comment to MR using GitLab API + if [ -n "\${CI_MERGE_REQUEST_IID}" ]; then + # Check for existing comment + EXISTING_COMMENT_ID=$(curl --silent --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes" \\ + | jq -r '.[] | select(.body | contains("📊 CloudFormation Resource Count")) | .id' | head -1) + + # Prepare JSON payload + JSON_PAYLOAD=$(jq -n --arg body "$COMMENT_BODY" '{body: $body}') + + if [ -n "\${EXISTING_COMMENT_ID}" ]; then + # Update existing comment + curl --silent --request PUT \\ + --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + --header "Content-Type: application/json" \\ + --data "$JSON_PAYLOAD" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes/\${EXISTING_COMMENT_ID}" + echo "Updated existing MR comment" + else + # Create new comment + curl --silent --request POST \\ + --header "PRIVATE-TOKEN: \${CI_JOB_TOKEN}" \\ + --header "Content-Type: application/json" \\ + --data "$JSON_PAYLOAD" \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests/\${CI_MERGE_REQUEST_IID}/notes" + echo "Created new MR comment" + fi + else + echo "Not running in merge request context, skipping comment" + fi + else + echo "Resource count results not found" + fi + variables: + CLOUD_ASSEMBLY_DIR: cdk.out " `; From 3026656fd250f3c31e3063a771d90ed843ef2739 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:55:15 +0000 Subject: [PATCH 3/6] chore: self mutation Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- test/__snapshots__/github.test.ts.snap | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/__snapshots__/github.test.ts.snap b/test/__snapshots__/github.test.ts.snap index 683178c..79f7904 100644 --- a/test/__snapshots__/github.test.ts.snap +++ b/test/__snapshots__/github.test.ts.snap @@ -2073,6 +2073,7 @@ jobs: id-token: write env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -2098,6 +2099,12 @@ jobs: role-chaining: true role-skip-session-tagging: true - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: From 369519c361e0384adc88361e97ad8c95250c408d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:37:37 +0000 Subject: [PATCH 4/6] chore: self mutation Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- test/__snapshots__/github.test.ts.snap | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/__snapshots__/github.test.ts.snap b/test/__snapshots__/github.test.ts.snap index e1a1646..386700d 100644 --- a/test/__snapshots__/github.test.ts.snap +++ b/test/__snapshots__/github.test.ts.snap @@ -3687,6 +3687,7 @@ jobs: id-token: write env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -3703,6 +3704,12 @@ jobs: role-session-name: GitHubAction aws-region: us-east-1 - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: From ba983f0400b5426661e02de7f5c7956f6425e3a2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:54:04 +0000 Subject: [PATCH 5/6] chore: self mutation Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- API.md | 55 +++++++++++++++----------- test/__snapshots__/github.test.ts.snap | 7 ++++ 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/API.md b/API.md index bb8eee2..78562f4 100644 --- a/API.md +++ b/API.md @@ -6152,14 +6152,6 @@ public readonly splitParameters: boolean; --- -### ResourceCountStepProps - -#### Initializer - -```typescript -import { ResourceCountStepProps } from 'projen-pipelines' - -const resourceCountStepProps: ResourceCountStepProps = { ... } ### PnpmSetupStepOptions Options for the PnpmSetupStep. @@ -6174,6 +6166,38 @@ const pnpmSetupStepOptions: PnpmSetupStepOptions = { ... } #### Properties +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| version | string | The version of pnpm to install. | + +--- + +##### `version`Optional + +```typescript +public readonly version: string; +``` + +- *Type:* string + +The version of pnpm to install. + +If not provided, defaults to '9'. + +--- + +### ResourceCountStepProps + +#### Initializer + +```typescript +import { ResourceCountStepProps } from 'projen-pipelines' + +const resourceCountStepProps: ResourceCountStepProps = { ... } +``` + +#### Properties + | **Name** | **Type** | **Description** | | --- | --- | --- | | cloudAssemblyDir | string | Path to the cloud assembly directory. | @@ -6232,21 +6256,6 @@ public readonly warningThreshold: number; - *Default:* 450 Warning threshold for resource count. -| version | string | The version of pnpm to install. | - ---- - -##### `version`Optional - -```typescript -public readonly version: string; -``` - -- *Type:* string - -The version of pnpm to install. - -If not provided, defaults to '9'. --- diff --git a/test/__snapshots__/github.test.ts.snap b/test/__snapshots__/github.test.ts.snap index 7762e8e..9261b00 100644 --- a/test/__snapshots__/github.test.ts.snap +++ b/test/__snapshots__/github.test.ts.snap @@ -3979,6 +3979,7 @@ jobs: id-token: write env: CI: "true" + CLOUD_ASSEMBLY_DIR: cdk.out steps: - uses: actions/setup-node@v5 with: @@ -3999,6 +4000,12 @@ jobs: role-session-name: GitHubAction aws-region: us-east-1 - run: npx projen build + - run: count-resources --cloud-assembly-dir cdk.out --warning-threshold 450 --output-file resource-count-results.json --github-summary + - name: Upload Artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: resource-count-results + path: resource-count-results.json - name: Upload Artifact uses: actions/upload-artifact@v4.6.2 with: From f9335c81d401f7801c720734aeb9dd3f82387039 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:40:32 +0000 Subject: [PATCH 6/6] chore: self mutation Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- API.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/API.md b/API.md index c78fa6f..0146c72 100644 --- a/API.md +++ b/API.md @@ -7895,7 +7895,6 @@ The projen project reference. | **Name** | **Description** | | --- | --- | | toBash | Converts the sequence of steps into a Bash script configuration. | -| toCodeCatalyst | Converts the sequence of steps into a CodeCatalyst Actions step configuration. | | toGithub | Converts the sequence of steps into a GitHub Actions step configuration. | | toGitlab | Converts the sequence of steps into a GitLab CI configuration. | | addSteps | *No description.* | @@ -7911,14 +7910,6 @@ public toBash(): BashStepConfig Converts the sequence of steps into a Bash script configuration. -##### `toCodeCatalyst` - -```typescript -public toCodeCatalyst(): CodeCatalystStepConfig -``` - -Converts the sequence of steps into a CodeCatalyst Actions step configuration. - ##### `toGithub` ```typescript