infra/cdk/scripts/validate-stack.ts

import fs from 'node:fs'; import { App } from 'aws-cdk-lib'; import path from 'node:path'; import { Template, Match } from 'aws-cdk-lib/assertions'; import { PortfolioStack } from '../lib/portfolio-stack';

const OPEN_NEXT_OUTPUT_FILE = 'open-next.output.json';

const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;

interface LambdaEnvironment { Variables?: Record<string, unknown>; }

interface LambdaProperties { Environment?: LambdaEnvironment; }

interface LambdaTemplateResource { Properties?: LambdaProperties; }

const isLambdaEnvironment = (value: unknown): value is LambdaEnvironment => { if (!isRecord(value)) { return false; }

const { Variables } = value; return Variables === undefined || isRecord(Variables); };

const isLambdaProperties = (value: unknown): value is LambdaProperties => { if (!isRecord(value)) { return false; }

const { Environment } = value; return Environment === undefined || isLambdaEnvironment(Environment); };

const isLambdaTemplateResource = (value: unknown): value is LambdaTemplateResource => { if (!isRecord(value)) { return false; }

const { Properties } = value; return Properties === undefined || isLambdaProperties(Properties); };

function resolveOpenNextPath(): string { const repoRoot = path.resolve(__dirname, '../../..'); const repoOpenNextDir = path.join(repoRoot, '.open-next'); const repoOutputFile = path.join(repoOpenNextDir, OPEN_NEXT_OUTPUT_FILE);

if (fs.existsSync(repoOutputFile)) { console.log(Using OpenNext output at ${repoOpenNextDir} for validation.); return repoOpenNextDir; }

const fixtureDir = path.resolve(__dirname, '../fixtures/open-next'); const fixtureOutputFile = path.join(fixtureDir, OPEN_NEXT_OUTPUT_FILE);

if (fs.existsSync(fixtureOutputFile)) { console.warn('OpenNext build output not found; falling back to fixture bundle.'); return fixtureDir; }

throw new Error(Unable to locate ${OPEN_NEXT_OUTPUT_FILE}. Run 'pnpm run build:web' before validating the stack.); }

function validateStack() { const app = new App(); const openNextPath = resolveOpenNextPath();

const requireEnv = (name: string): string => { const value = process.env[name]; if (!value) { throw new Error(${name} must be set before running validate-stack.ts); } return value; };

const stack = new PortfolioStack(app, 'ValidationStack', { env: { account: '000000000000', region: 'us-east-1', }, openNextPath, environment: { NEXT_PUBLIC_SITE_URL: requireEnv('NEXT_PUBLIC_SITE_URL'), }, validationMode: true, });

const template = Template.fromStack(stack);

template.resourceCountIs('AWS::CloudFront::Distribution', 1);

const lambdaResources = template.findResources('AWS::Lambda::Function'); if (Object.keys(lambdaResources).length < 2) { throw new Error('Expected at least two Lambda functions in the stack'); }

const bucketResources = template.findResources('AWS::S3::Bucket'); if (Object.keys(bucketResources).length < 1) { throw new Error('Expected at least one S3 bucket in the stack'); }

const expectedSiteUrl = requireEnv('NEXT_PUBLIC_SITE_URL'); const lambdaResourceValues: unknown[] = Object.values(lambdaResources); const lambdaHasSiteEnv = lambdaResourceValues.some((resource) => { if (!isLambdaTemplateResource(resource)) { return false; }

const variables = resource.Properties?.Environment?.Variables;
return (
  typeof variables?.NEXT_PUBLIC_SITE_URL === 'string' &&
  variables.NEXT_PUBLIC_SITE_URL === expectedSiteUrl &&
  typeof variables.NODE_ENV === 'string' &&
  variables.NODE_ENV === 'production'
);

}); if (!lambdaHasSiteEnv) { throw new Error( Expected at least one Lambda function to include NEXT_PUBLIC_SITE_URL=${expectedSiteUrl} and NODE_ENV=production ); }

template.hasResourceProperties('AWS::S3::Bucket', { VersioningConfiguration: Match.objectLike({ Status: 'Enabled', }), });

console.log('CDK stack validated successfully.'); }

try { validateStack(); } catch (error) { console.error('CDK stack validation failed.'); if (error instanceof Error) { console.error(error.message); } else { console.error(error); } process.exit(1); }