Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 50 additions & 8 deletions .github/workflows/build-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,28 @@ jobs:
with:
language: ${{ matrix.language }}

# Get the list of changed files, excluding Markdown files
# Get the list of changed files, excluding Markdown files and deleted files
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@27ae6b33eaed7bf87272fdeb9f1c54f9facc9d99
with:
files: ${{ matrix.language }}/**
files_ignore: '**/*.md'
files_separator: ' '

# Build the changed files for the specified language
- name: Build changed files
if: steps.changed-files.outputs.any_changed == 'true'
run: |
# Create a temporary directory for build logs
mkdir -p /tmp/build_logs
npm install -g eslint > /dev/null

# Function to build a single file
build_file() {
echo "Build File $1"
local file="$1"
local log_file="/tmp/build_logs/$(basename "$file" | sed 's/\//_/g').log"
IFS="/" read -ra path_parts <<< "$file"
language=${path_parts[0]}

Expand All @@ -57,14 +63,35 @@ jobs:
echo "Build Path $file"

# Run the build script for the current language, passing the project directory and extra path
echo "::group::$file"
if ../scripts/build-${language}.sh "$file"; then
echo "::endgroup::"
echo "::group::Building $file"
# Run the build command and capture output to both the log file and stdout
../scripts/build-${language}.sh "$file" 2>&1 | tee "$log_file"
local exit_code=${PIPESTATUS[0]}

if [ $exit_code -eq 0 ]; then
echo "✅ Build succeeded for $file"

# Clean up node_modules and cdk.out for this project immediately after successful build
echo "Cleaning up build artifacts for $(dirname "$file")"
local project_dir=$(dirname "$file")

# Remove node_modules directory if it exists
if [ -d "$project_dir/node_modules" ]; then
echo "Removing $project_dir/node_modules"
rm -rf "$project_dir/node_modules"
fi

# Remove cdk.out directory if it exists
if [ -d "$project_dir/cdk.out" ]; then
echo "Removing $project_dir/cdk.out"
rm -rf "$project_dir/cdk.out"
fi
else
echo "::endgroup::"
echo "::error::Build failed for $file"
return 1
echo "❌ Build failed for $file with exit code $exit_code"
echo "::error::Build failed for $file with exit code $exit_code"
fi
echo "::endgroup::"
return $exit_code
}

# Export the build_file function for use in parallel
Expand All @@ -73,7 +100,8 @@ jobs:
# Create an array to store directories to be built
apps_to_build=()

files=(${{ steps.changed-files.outputs.all_modified_files }})
# Use only added and modified files, ignoring deleted files
files=(${{ steps.changed-files.outputs.added_files }} ${{ steps.changed-files.outputs.modified_files }})

# Check the directories of each changed file for cdk.json
for file in "${files[@]}"; do
Expand Down Expand Up @@ -110,4 +138,18 @@ jobs:

# Run the build_file function in parallel for each project to be built
# Halt the execution if any of the build_file invocations fail
echo "::group::Build Output"
echo "Starting builds for all projects..."
parallel --keep-order --halt-on-error 2 build_file ::: "${apps_to_build[@]}"
echo "::endgroup::"

# Check the exit status of parallel
parallel_exit=$?

# If parallel failed, make sure the workflow fails too
if [ $parallel_exit -ne 0 ]; then
echo "::error::One or more builds failed. See build output above for details."
exit $parallel_exit
else
echo "✅ All builds completed successfully!"
fi
106 changes: 106 additions & 0 deletions SNAPSHOT_TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# CDK Snapshot Testing Guide

This guide explains how to use the asset hash normalization utility for consistent CDK snapshot testing across different environments.

## Problem

CDK snapshot tests often fail in CI/CD environments because asset hashes change between different environments. This happens because:

- Asset hashes are generated based on content and environment variables
- Different machines or CI/CD environments produce different hashes
- This causes snapshot tests to fail even when there are no actual changes to the infrastructure

## Solution

We've created a utility function that normalizes asset hashes and other environment-specific values in CloudFormation templates before comparing them with snapshots.

## How to Use the Normalization Utility

### 1. Import the Utility

```typescript
import { normalizeTemplate } from '../../test-utils/normalize-template';
```

### 2. Apply Normalization Before Snapshot Comparison

```typescript
import { App } from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { YourStack } from '../lib/your-stack';
import { normalizeTemplate } from '../../test-utils/normalize-template';

test('YourStack creates the expected resources', () => {
const app = new App();
const stack = new YourStack(app, 'TestStack');

// Get the CloudFormation template
const template = Template.fromStack(stack);

// Normalize the template before snapshot comparison
const normalizedTemplate = normalizeTemplate(template.toJSON());

// Compare with snapshot
expect(normalizedTemplate).toMatchSnapshot();
});
```

### 3. Update Existing Snapshots

After adding the normalization to your tests, update your snapshots:

```bash
npm test -- -u
```

## What Gets Normalized

The utility currently normalizes:

1. **S3 Asset Keys**: Replaces asset hashes in S3Key properties
- Pattern with extension: `[64 hex chars].zip` → `NORMALIZED_ASSET_HASH.zip`
- Pattern without extension: `[64 hex chars]` → `NORMALIZED_ASSET_HASH`

2. **Docker Image Digests**: Replaces image digests
- Pattern: `sha256:[digest]` → `NORMALIZED_IMAGE_DIGEST`

## Adding New Test Files

When creating new test files that use snapshot testing:

1. Import the normalization utility
2. Apply it to your template before comparing with snapshots
3. Update your snapshots with the `-u` flag

## Extending the Utility

If you encounter other environment-specific values that need normalization, you can extend the utility at `typescript/test-utils/normalize-template.ts`.

Example of adding a new normalization rule:

```typescript
// Inside the normalizeValues function
if (key === 'NewPropertyToNormalize' && typeof obj[key] === 'string' && /pattern-to-match/.test(obj[key])) {
obj[key] = 'NORMALIZED_VALUE';
}
```

## Troubleshooting

If you're still seeing snapshot test failures:

1. **Check for new patterns**: There might be new types of asset hashes or environment-specific values that need normalization
2. **Verify imports**: Make sure you're importing and using the utility correctly
3. **Check snapshot updates**: Ensure you've updated your snapshots after adding normalization

## Best Practices

1. **Always normalize before snapshot comparison**: This ensures consistent results
2. **Update snapshots deliberately**: Only use the `-u` flag when you expect changes
3. **Review snapshot diffs**: When updating snapshots, review the changes to ensure they're expected
4. **Keep the utility updated**: As new patterns emerge, add them to the normalization utility

## Additional Resources

- [Jest Snapshot Testing Documentation](https://jestjs.io/docs/snapshot-testing)
- [AWS CDK Testing Documentation](https://docs.aws.amazon.com/cdk/v2/guide/testing.html)
166 changes: 166 additions & 0 deletions scripts/build-all-typescript.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/bin/bash
#
# Script to build all TypeScript CDK projects in parallel
#
set -euo pipefail

# Get the directory of this script
SCRIPT_DIR=$(cd $(dirname $0) && pwd)
REPO_ROOT=$(dirname "$SCRIPT_DIR")
TYPESCRIPT_DIR="${REPO_ROOT}/typescript"
BUILD_SCRIPT="$SCRIPT_DIR/build-typescript.sh"

# Check if parallel is installed
if ! command -v parallel &> /dev/null; then
echo "GNU parallel is not installed. Please install it first."
exit 1
fi

# Check if the build script exists
if [ ! -f "$BUILD_SCRIPT" ]; then
echo "Error: Build script not found at $BUILD_SCRIPT"
exit 1
fi

# Create a temporary directory for build logs
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT

# Find all TypeScript CDK projects (directories with cdk.json)
# Exclude any matches from node_modules directories, cdk.out directories, and specific problematic projects
echo "Finding all TypeScript CDK projects..."
PROJECTS=$(find "$TYPESCRIPT_DIR" -name "cdk.json" -type f \
-not -path "*/node_modules/*" \
-not -path "*/cdk.out/*" \
| sort)

# Count total projects
TOTAL_PROJECTS=$(echo "$PROJECTS" | wc -l)
echo "Found $TOTAL_PROJECTS TypeScript CDK projects to build"
echo "=============================="

# Function to build a single project
build_project() {
CDK_JSON_PATH="$1"
PROJECT_DIR=$(dirname "$CDK_JSON_PATH")
REL_PATH=$(realpath --relative-to="$REPO_ROOT" "$PROJECT_DIR")
LOG_FILE="$TEMP_DIR/$(echo "$REL_PATH" | tr '/' '_').log"

# Show start message
echo "▶️ Starting build: $REL_PATH"

# Check for DO_NOT_AUTOTEST file
if [ -f "$PROJECT_DIR/DO_NOT_AUTOTEST" ]; then
echo "⏭️ Skipping $REL_PATH (DO_NOT_AUTOTEST flag found)"
echo "SKIPPED:$REL_PATH" > "$LOG_FILE.status"
return 0
fi

# Find the package.json in the project directory
PACKAGE_JSON="$PROJECT_DIR/package.json"
if [ ! -f "$PACKAGE_JSON" ]; then
echo "⏭️ Skipping $REL_PATH (no package.json found)"
echo "SKIPPED:$REL_PATH" > "$LOG_FILE.status"
return 0
fi

# Get the relative path to package.json for the build script
PACKAGE_JSON_REL_PATH=$(realpath --relative-to="$REPO_ROOT" "$PACKAGE_JSON")

# Create a modified version of the build script that suppresses cdk synth output
TEMP_BUILD_SCRIPT="$TEMP_DIR/build-$(basename "$REL_PATH").sh"
cat > "$TEMP_BUILD_SCRIPT" << 'EOF'
#!/bin/bash
set -euo pipefail

# Get the original script and arguments
ORIGINAL_SCRIPT="$1"
shift
ARGS="$@"

# Run the original script but capture and filter the output
"$ORIGINAL_SCRIPT" "$ARGS" 2>&1 | grep -v "cdk synth" | grep -v "Synthesizing"
EOF
chmod +x "$TEMP_BUILD_SCRIPT"

# Run the build script with filtered output
if "$TEMP_BUILD_SCRIPT" "$BUILD_SCRIPT" "$PACKAGE_JSON_REL_PATH" > "$LOG_FILE" 2>&1; then
echo "✅ Build successful: $REL_PATH"
echo "SUCCESS:$REL_PATH" > "$LOG_FILE.status"
else
echo "❌ Build failed: $REL_PATH"
echo "FAILED:$REL_PATH" > "$LOG_FILE.status"
fi
}
export -f build_project
export SCRIPT_DIR
export REPO_ROOT
export BUILD_SCRIPT
export TEMP_DIR

# Run builds in parallel
echo "$PROJECTS" | parallel --will-cite --jobs 50% build_project

# Collect results
SUCCESSFUL=0
FAILED=0
SKIPPED=0
ALL_PROJECTS=()

for STATUS_FILE in "$TEMP_DIR"/*.status; do
[ -f "$STATUS_FILE" ] || continue

STATUS_CONTENT=$(cat "$STATUS_FILE")
STATUS_TYPE=${STATUS_CONTENT%%:*}
PROJECT_PATH=${STATUS_CONTENT#*:}

case "$STATUS_TYPE" in
"SUCCESS")
((SUCCESSFUL++))
ALL_PROJECTS+=("✅ $PROJECT_PATH")
;;
"FAILED")
((FAILED++))
ALL_PROJECTS+=("❌ $PROJECT_PATH")
;;
"SKIPPED")
((SKIPPED++))
ALL_PROJECTS+=("⏭️ $PROJECT_PATH")
;;
esac
done

# Sort the projects list
IFS=$'\n' SORTED_PROJECTS=($(sort <<<"${ALL_PROJECTS[*]}"))
unset IFS

# Print summary
echo ""
echo "=============================="
echo "BUILD SUMMARY"
echo "=============================="
echo "Total: $((SUCCESSFUL + FAILED + SKIPPED)) (✅ $SUCCESSFUL succeeded, ❌ $FAILED failed, ⏭️ $SKIPPED skipped)"
echo ""
echo "Project Status:"
echo "-----------------------------"
for PROJ in "${SORTED_PROJECTS[@]}"; do
echo "$PROJ"
done

# If any builds failed, print their logs and exit with error
if [ $FAILED -gt 0 ]; then
echo ""
echo "Build logs for failed projects:"
echo "=============================="
for PROJ in "${SORTED_PROJECTS[@]}"; do
if [[ $PROJ == ❌* ]]; then
PROJECT_PATH=${PROJ#❌ }
echo ""
echo "Log for $PROJECT_PATH:"
echo "-------------------------------------------"
cat "$TEMP_DIR/$(echo "$PROJECT_PATH" | tr '/' '_').log"
echo "-------------------------------------------"
fi
done
exit 1
fi
10 changes: 5 additions & 5 deletions typescript/amazon-mq-rabbitmq-lambda/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@
"cdk": "cdk"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "22.5.4",
"aws-cdk": "2.160.0",
"@types/jest": "^29.5.14",
"@types/node": "22.7.9",
"aws-cdk": "2.1004.0",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript": "~5.6.2"
"typescript": "~5.6.3"
},
"dependencies": {
"@cdklabs/cdk-amazonmq": "^0.0.1",
"aws-cdk-lib": "2.160.0",
"aws-cdk-lib": "2.185.0",
"constructs": "^10.0.0"
}
}
Loading
Loading