diff --git a/.github/workflows/cd-cdk.yaml b/.github/workflows/cd-cdk.yaml index 53a5a27..2aed4c6 100644 --- a/.github/workflows/cd-cdk.yaml +++ b/.github/workflows/cd-cdk.yaml @@ -6,7 +6,7 @@ on: node-version: description: "Node.js version to use" type: string - default: "22" + default: "24" python-version: description: "Python version for Python CDK repos (leave empty for TypeScript CDK)" type: string @@ -23,6 +23,10 @@ on: description: "Enable QEMU for cross-platform Docker builds (arm64 on x86 runners)" type: boolean default: false + stack-name: + description: "CloudFormation stack name (for pre-flight checks)" + type: string + default: "" secrets: deploy-role-arn: description: "OIDC deploy role ARN" @@ -67,6 +71,57 @@ jobs: role-to-assume: ${{ secrets.deploy-role-arn }} aws-region: ${{ inputs.region }} + - name: Pre-flight checks + if: ${{ inputs.stack-name != '' }} + run: | + echo "Pre-flight: checking stack ${{ inputs.stack-name }}..." + STATUS=$(aws cloudformation describe-stacks \ + --stack-name "${{ inputs.stack-name }}" \ + --query 'Stacks[0].StackStatus' --output text 2>/dev/null || echo "NOT_FOUND") + case "$STATUS" in + *ROLLBACK_COMPLETE|*FAILED) + echo "::error::Stack ${{ inputs.stack-name }} is in $STATUS — manual intervention required." + exit 1 + ;; + *IN_PROGRESS) + echo "::error::Stack ${{ inputs.stack-name }} has an operation in progress ($STATUS) — wait for it to complete." + exit 1 + ;; + NOT_FOUND) + echo "Pre-flight: stack not found — will be created on first deploy." + ;; + *) + echo "Pre-flight: stack status is $STATUS — OK to deploy." + ;; + esac + - name: CDK deploy working-directory: ${{ inputs.cdk-dir }} run: npx -y cdk deploy --all --require-approval never + + - name: Post-deploy health check + if: ${{ inputs.stack-name != '' }} + run: | + echo "Health check: verifying stack ${{ inputs.stack-name }}..." + + STATUS=$(aws cloudformation describe-stacks \ + --stack-name "${{ inputs.stack-name }}" \ + --query 'Stacks[0].StackStatus' --output text) + if [[ "$STATUS" != *"COMPLETE" ]] || [[ "$STATUS" == *"ROLLBACK"* ]]; then + echo "::error::Stack ${{ inputs.stack-name }} ended in $STATUS after deploy." + exit 1 + fi + echo "Stack status: $STATUS" + + echo "Stack outputs:" + aws cloudformation describe-stacks \ + --stack-name "${{ inputs.stack-name }}" \ + --query 'Stacks[0].Outputs[*].[OutputKey,OutputValue]' --output table + + # Run project-specific health check if it exists + if [[ -f scripts/health-check.sh ]]; then + echo "Running project health check..." + bash scripts/health-check.sh "${{ inputs.stack-name }}" "${{ inputs.region }}" + fi + + echo "Health check passed." diff --git a/.github/workflows/cd-sam.yaml b/.github/workflows/cd-sam.yaml index e5a53d3..0d8f69f 100644 --- a/.github/workflows/cd-sam.yaml +++ b/.github/workflows/cd-sam.yaml @@ -53,6 +53,29 @@ jobs: role-to-assume: ${{ secrets.deploy-role-arn }} aws-region: ${{ inputs.region }} + - name: Pre-flight checks + run: | + echo "Pre-flight: checking stack ${{ inputs.stack-name }}..." + STATUS=$(aws cloudformation describe-stacks \ + --stack-name "${{ inputs.stack-name }}" \ + --query 'Stacks[0].StackStatus' --output text 2>/dev/null || echo "NOT_FOUND") + case "$STATUS" in + *ROLLBACK_COMPLETE|*FAILED) + echo "::error::Stack ${{ inputs.stack-name }} is in $STATUS — manual intervention required." + exit 1 + ;; + *IN_PROGRESS) + echo "::error::Stack ${{ inputs.stack-name }} has an operation in progress ($STATUS) — wait for it to complete." + exit 1 + ;; + NOT_FOUND) + echo "Pre-flight: stack not found — will be created on first deploy." + ;; + *) + echo "Pre-flight: stack status is $STATUS — OK to deploy." + ;; + esac + - name: SAM build run: sam build --template ${{ inputs.sam-template }} @@ -71,3 +94,29 @@ jobs: --no-fail-on-empty-changeset \ --role-arn ${{ inputs.cfn-role-arn }} \ $PARAMS + + - name: Post-deploy health check + run: | + echo "Health check: verifying stack ${{ inputs.stack-name }}..." + + STATUS=$(aws cloudformation describe-stacks \ + --stack-name "${{ inputs.stack-name }}" \ + --query 'Stacks[0].StackStatus' --output text) + if [[ "$STATUS" != *"COMPLETE" ]] || [[ "$STATUS" == *"ROLLBACK"* ]]; then + echo "::error::Stack ${{ inputs.stack-name }} ended in $STATUS after deploy." + exit 1 + fi + echo "Stack status: $STATUS" + + echo "Stack outputs:" + aws cloudformation describe-stacks \ + --stack-name "${{ inputs.stack-name }}" \ + --query 'Stacks[0].Outputs[*].[OutputKey,OutputValue]' --output table + + # Run project-specific health check if it exists + if [[ -f scripts/health-check.sh ]]; then + echo "Running project health check..." + bash scripts/health-check.sh "${{ inputs.stack-name }}" "${{ inputs.region }}" + fi + + echo "Health check passed." diff --git a/.github/workflows/ci-python-sam.yaml b/.github/workflows/ci-python-sam.yaml index eae99be..d64faf7 100644 --- a/.github/workflows/ci-python-sam.yaml +++ b/.github/workflows/ci-python-sam.yaml @@ -34,7 +34,11 @@ on: node-version: description: "Node.js version for CDK CLI" type: string - default: "22" + default: "24" + run-conventions-check: + description: "Run lightweight conventions audit" + type: boolean + default: true jobs: ci: @@ -81,6 +85,52 @@ jobs: working-directory: ${{ inputs.cdk-dir }} run: npx -y cdk synth --quiet + - name: Conventions check + if: ${{ inputs.run-conventions-check }} + run: | + errors=0 + warn() { echo "::warning::$1"; } + fail() { echo "::error::$1"; errors=$((errors + 1)); } + + # README must exist + if [[ ! -f README.md ]]; then + fail "Missing README.md" + fi + + # .gitignore must cover .env + if [[ -f .gitignore ]]; then + if ! grep -qE '^\\.env$|^\\.env\\b' .gitignore; then + fail ".gitignore does not include .env" + fi + else + fail "Missing .gitignore" + fi + + # SAM: check template for non-arm64 and missing log retention + TEMPLATE="${{ inputs.sam-template }}" + if [[ -f "$TEMPLATE" ]]; then + if grep -qi 'x86_64' "$TEMPLATE" 2>/dev/null; then + fail "SAM template: Lambda using x86_64 instead of arm64" + fi + if grep -qi 'AWS::Serverless::Function' "$TEMPLATE" 2>/dev/null; then + if ! grep -qi 'RetentionInDays\|AWS::Logs::LogGroup' "$TEMPLATE" 2>/dev/null; then + warn "SAM template: Lambda found but no explicit log retention" + fi + fi + # Check for secrets in environment variables + if grep -qiE '(API_KEY|SECRET|TOKEN|PASSWORD|WEBHOOK)' "$TEMPLATE" 2>/dev/null; then + if grep -A5 'Environment:' "$TEMPLATE" | grep -qiE '(API_KEY|SECRET|TOKEN|PASSWORD|WEBHOOK)' 2>/dev/null; then + fail "SAM template: possible secret in Lambda environment variables — use Secrets Manager" + fi + fi + fi + + if [[ $errors -gt 0 ]]; then + echo "Conventions check failed with $errors error(s)." + exit 1 + fi + echo "Conventions check passed." + - name: Setup SAM CLI if: ${{ inputs.run-sam-validate }} uses: aws-actions/setup-sam@v2 diff --git a/.github/workflows/ci-typescript-cdk.yaml b/.github/workflows/ci-typescript-cdk.yaml index 9467b47..7048973 100644 --- a/.github/workflows/ci-typescript-cdk.yaml +++ b/.github/workflows/ci-typescript-cdk.yaml @@ -6,7 +6,7 @@ on: node-version: description: "Node.js version to use" type: string - default: "22" + default: "24" run-typecheck: description: "Run tsc --noEmit" type: boolean @@ -27,6 +27,10 @@ on: description: "Enable QEMU for cross-platform Docker builds (arm64 on x86 runners)" type: boolean default: false + run-conventions-check: + description: "Run lightweight conventions audit" + type: boolean + default: true run-sam-validate: description: "Run sam validate --lint (for Node.js SAM repos)" type: boolean @@ -39,7 +43,7 @@ on: jobs: ci: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 30 steps: - uses: actions/checkout@v4 @@ -70,6 +74,52 @@ jobs: if: ${{ inputs.run-cdk-synth }} run: npx cdk synth --quiet + - name: Conventions check + if: ${{ inputs.run-conventions-check }} + run: | + errors=0 + warn() { echo "::warning::$1"; } + fail() { echo "::error::$1"; errors=$((errors + 1)); } + + # README must exist + if [[ ! -f README.md ]]; then + fail "Missing README.md" + fi + + # .gitignore must cover .env + if [[ -f .gitignore ]]; then + if ! grep -qE '^\\.env$|^\\.env\\b' .gitignore; then + fail ".gitignore does not include .env" + fi + else + fail "Missing .gitignore" + fi + + # CDK: check synth output for non-arm64 Lambdas and missing log retention + if [[ -d cdk.out ]]; then + for tmpl in cdk.out/*.template.json; do + [[ -f "$tmpl" ]] || continue + + # Check for x86_64 Lambdas + if grep -q '"Architectures".*x86_64' "$tmpl" 2>/dev/null; then + fail "$(basename "$tmpl"): Lambda using x86_64 instead of arm64" + fi + + # Check Lambdas exist but no log retention set + if grep -q '"AWS::Lambda::Function"' "$tmpl" 2>/dev/null; then + if ! grep -q '"AWS::Logs::LogGroup"' "$tmpl" 2>/dev/null; then + warn "$(basename "$tmpl"): Lambda found but no explicit LogGroup with retention" + fi + fi + done + fi + + if [[ $errors -gt 0 ]]; then + echo "Conventions check failed with $errors error(s)." + exit 1 + fi + echo "Conventions check passed." + - name: Setup SAM CLI if: ${{ inputs.run-sam-validate }} uses: aws-actions/setup-sam@v2