diff --git a/.projenrc.ts b/.projenrc.ts
index 4a25831..9034824 100644
--- a/.projenrc.ts
+++ b/.projenrc.ts
@@ -51,6 +51,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,
npmTrustedPublishing: true,
diff --git a/API.md b/API.md
index 9ba28f4..d798cd3 100644
--- a/API.md
+++ b/API.md
@@ -2622,6 +2622,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. |
| paths | string[] | File path patterns that should trigger the pipeline when changed. |
@@ -2634,6 +2635,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. |
@@ -2691,6 +2693,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
@@ -2844,6 +2861,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
@@ -3082,6 +3115,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. |
| paths | string[] | File path patterns that should trigger the pipeline when changed. |
@@ -3094,6 +3128,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. |
@@ -3151,6 +3186,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
@@ -3304,6 +3354,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
@@ -4403,6 +4469,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. |
| paths | string[] | File path patterns that should trigger the pipeline when changed. |
@@ -4415,6 +4482,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. |
@@ -4476,6 +4544,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
@@ -4629,6 +4712,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
@@ -5101,6 +5200,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. |
| paths | string[] | File path patterns that should trigger the pipeline when changed. |
@@ -5113,6 +5213,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. |
@@ -5172,6 +5273,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
@@ -5325,6 +5441,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
@@ -6270,6 +6402,79 @@ 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. |
+| 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.
@@ -7947,6 +8152,100 @@ 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. |
+| 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.
+
+##### `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 8dc8638..d4a07e2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"commit-and-tag-version": "^12.7.1"
},
"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 dc11a11..529890f 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 7e534eb..e5fc8e0 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, PnpmSetupStep, CorepackSetupStep } from '../steps';
import { VersioningConfig, VersioningSetup } from '../versioning';
+import { ResourceCountStep } from './resource-count-step';
/**
* The Environment interface is designed to hold AWS related information
@@ -214,6 +215,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;
}
/**
@@ -329,6 +347,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/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 857db1e..7d5d4d8 100644
--- a/src/awscdk/github.ts
+++ b/src/awscdk/github.ts
@@ -128,6 +128,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 */
@@ -267,6 +272,18 @@ export class GithubCDKPipeline extends CDKPipeline {
steps.push(this.provideInstallStep());
steps.push(this.provideSynthStep());
+ // Add resource counting step if enabled
+ const resourceCountStep = this.provideResourceCountStep(true);
+ if (resourceCountStep) {
+ steps.push(resourceCountStep);
+
+ // 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: `${this.namePrefix}cloud-assembly`,
path: `${this.app.cdkConfig.cdkout}/`,
@@ -535,4 +552,147 @@ 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());
+
+ const resourceCountStep = this.provideResourceCountStep(true);
+ if (resourceCountStep) {
+ steps.push(resourceCountStep);
+ }
+
+ 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/gitlab.ts b/src/awscdk/gitlab.ts
index b43afc3..684ff24 100644
--- a/src/awscdk/gitlab.ts
+++ b/src/awscdk/gitlab.ts
@@ -88,6 +88,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();
+ }
}
/**
@@ -146,6 +151,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');
@@ -298,5 +309,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/src/awscdk/index.ts b/src/awscdk/index.ts
index 6c2a1a6..0734c73 100644
--- a/src/awscdk/index.ts
+++ b/src/awscdk/index.ts
@@ -2,3 +2,4 @@ export * from './base';
export * from './github';
export * from './gitlab';
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 5c56bef..f50189a 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@v6
with:
@@ -29,6 +30,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@v7
with:
@@ -584,6 +591,7 @@ jobs:
contents: read
env:
CI: "true"
+ CLOUD_ASSEMBLY_DIR: cdk.out
steps:
- uses: actions/setup-node@v6
with:
@@ -595,6 +603,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@v7
with:
@@ -752,6 +766,7 @@ jobs:
contents: read
env:
CI: "true"
+ CLOUD_ASSEMBLY_DIR: cdk.out
steps:
- uses: actions/setup-node@v6
with:
@@ -763,6 +778,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@v7
with:
diff --git a/test/__snapshots__/github.test.ts.snap b/test/__snapshots__/github.test.ts.snap
index b46997e..74f7d07 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@v6
with:
@@ -36,6 +37,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@v7
with:
@@ -727,6 +734,7 @@ jobs:
id-token: write
env:
CI: "true"
+ CLOUD_ASSEMBLY_DIR: cdk.out
steps:
- uses: actions/setup-node@v6
with:
@@ -744,6 +752,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@v7
with:
@@ -803,6 +817,7 @@ jobs:
id-token: write
env:
CI: "true"
+ CLOUD_ASSEMBLY_DIR: cdk.out
steps:
- uses: actions/setup-node@v6
with:
@@ -820,6 +835,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@v7
with:
@@ -879,6 +900,7 @@ jobs:
id-token: write
env:
CI: "true"
+ CLOUD_ASSEMBLY_DIR: cdk.out
steps:
- uses: actions/setup-node@v6
with:
@@ -896,6 +918,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@v7
with:
@@ -1160,6 +1188,7 @@ jobs:
id-token: write
env:
CI: "true"
+ CLOUD_ASSEMBLY_DIR: cdk.out
steps:
- uses: actions/setup-node@v6
with:
@@ -1177,6 +1206,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@v7
with:
@@ -1425,6 +1460,7 @@ jobs:
id-token: write
env:
CI: "true"
+ CLOUD_ASSEMBLY_DIR: cdk.out
steps:
- uses: actions/setup-node@v6
with:
@@ -1442,6 +1478,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@v7
with:
@@ -2564,6 +2606,7 @@ jobs:
id-token: write
env:
CI: "true"
+ CLOUD_ASSEMBLY_DIR: cdk.out
steps:
- uses: actions/setup-node@v6
with:
@@ -2581,6 +2624,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@v7
with:
@@ -2788,6 +2837,7 @@ jobs:
id-token: write
env:
CI: "true"
+ CLOUD_ASSEMBLY_DIR: cdk.out
steps:
- uses: actions/setup-node@v6
with:
@@ -2814,6 +2864,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@v7
with:
@@ -3816,6 +3872,7 @@ jobs:
id-token: write
env:
CI: "true"
+ CLOUD_ASSEMBLY_DIR: cdk.out
steps:
- uses: actions/setup-node@v6
with:
@@ -3834,6 +3891,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@v7
with:
@@ -3972,6 +4035,7 @@ jobs:
id-token: write
env:
CI: "true"
+ CLOUD_ASSEMBLY_DIR: cdk.out
steps:
- uses: actions/setup-node@v6
with:
@@ -3989,6 +4053,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@v7
with:
@@ -4373,6 +4443,7 @@ jobs:
id-token: write
env:
CI: "true"
+ CLOUD_ASSEMBLY_DIR: cdk.out
steps:
- uses: actions/setup-node@v6
with:
@@ -4390,6 +4461,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@v7
with:
@@ -4925,6 +5002,7 @@ jobs:
env:
CI: "true"
FOO: bar
+ CLOUD_ASSEMBLY_DIR: cdk.out
steps:
- uses: actions/setup-node@v6
with:
@@ -4943,6 +5021,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@v7
with:
@@ -5068,6 +5152,7 @@ jobs:
id-token: write
env:
CI: "true"
+ CLOUD_ASSEMBLY_DIR: cdk.out
steps:
- uses: actions/setup-node@v6
with:
@@ -5085,6 +5170,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@v7
with:
@@ -6084,6 +6175,7 @@ jobs:
id-token: write
env:
CI: "true"
+ CLOUD_ASSEMBLY_DIR: cdk.out
steps:
- uses: actions/setup-node@v6
with:
@@ -6101,6 +6193,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@v7
with:
diff --git a/test/__snapshots__/gitlab.test.ts.snap b/test/__snapshots__/gitlab.test.ts.snap
index 608e68d..396afe6 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
@@ -160,6 +163,98 @@ deploy-prod:
- fi
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
"
`;
@@ -474,6 +569,7 @@ stages:
- publish_assets
- independent1
- independent2
+ - resource-count-mr
.artifacts_cdk:
artifacts:
when: on_success
@@ -508,7 +604,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
@@ -589,6 +687,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
"
`;
@@ -774,6 +964,7 @@ stages:
- synth
- publish_assets
- prod
+ - resource-count-mr
.artifacts_cdk:
artifacts:
when: on_success
@@ -809,8 +1000,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
@@ -880,6 +1073,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
"
`;
@@ -891,6 +1178,7 @@ stages:
- publish_assets
- dev
- prod
+ - resource-count-mr
.artifacts_cdk:
artifacts:
when: on_success
@@ -927,7 +1215,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
@@ -1053,6 +1343,100 @@ deploy-prod:
- fi
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
"
`;
@@ -1064,6 +1448,7 @@ stages:
- publish_assets
- dev
- prod
+ - resource-count-mr
.artifacts_cdk:
artifacts:
when: on_success
@@ -1100,7 +1485,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
@@ -1226,6 +1613,100 @@ deploy-prod:
- fi
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
"
`;
@@ -1237,6 +1718,7 @@ stages:
- publish_assets
- dev
- prod
+ - resource-count-mr
.artifacts_cdk:
artifacts:
when: on_success
@@ -1271,7 +1753,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
@@ -1386,6 +1870,98 @@ deploy-prod:
- fi
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
"
`;