Merge pull request #3130 from kikora/codex/deploy-infra.yml-to-dev-fo… #1101
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy infrastructure | |
| permissions: | |
| contents: read | |
| id-token: write | |
| on: | |
| push: | |
| branches: [prod] | |
| workflow_dispatch: | |
| inputs: | |
| cloudfront_invalidation_paths: | |
| description: "Space-separated list of paths to invalidate (defaults to /*)" | |
| required: false | |
| default: "/*" | |
| jobs: | |
| deploy-iac: | |
| name: Deploy CloudFormation stacks | |
| runs-on: ubuntu-latest | |
| if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.merged }} | |
| env: | |
| # Required GitHub secrets for AWS access and stack configuration | |
| AWS_REGION: ${{ secrets.AWS_REGION }} | |
| AWS_IAC_ROLE_ARN: ${{ secrets.AWS_IAC_ROLE_ARN }} | |
| # Required when provisioning the data stack internally; pass the Redis password directly from GitHub Secrets. | |
| REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD || '' }} | |
| # Prod-static bucket must be reused (see deployment-notes.md). Default to the | |
| # existing bucket so deploys don't create a new one if the secret is missing. | |
| STATIC_SITE_BUCKET_NAME: ${{ secrets.STATIC_SITE_BUCKET_NAME || 'math-visuals-static-site-796063593326-202512172100921' }} | |
| STATIC_SITE_CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.STATIC_SITE_CLOUDFRONT_DISTRIBUTION_ID }} | |
| STATIC_SITE_API_DOMAIN: ${{ secrets.STATIC_SITE_API_DOMAIN }} | |
| STATIC_SITE_API_ORIGIN_PATH: ${{ secrets.STATIC_SITE_API_ORIGIN_PATH }} | |
| STATIC_SITE_CLOUDFRONT_PRICE_CLASS: ${{ secrets.STATIC_SITE_CLOUDFRONT_PRICE_CLASS }} | |
| API_ARTIFACT_BUCKET: ${{ secrets.API_ARTIFACT_BUCKET }} | |
| API_ARTIFACT_KEY: ${{ secrets.API_ARTIFACT_KEY }} | |
| REDIS_ENDPOINT: ${{ secrets.REDIS_ENDPOINT || '' }} | |
| REDIS_PORT: ${{ secrets.REDIS_PORT || '' }} | |
| REDIS_PASSWORD_SECRET_ARN: ${{ secrets.REDIS_PASSWORD_SECRET_ARN || '' }} | |
| PRIVATE_SUBNET_1_ID: ${{ secrets.PRIVATE_SUBNET_1_ID || '' }} | |
| PRIVATE_SUBNET_2_ID: ${{ secrets.PRIVATE_SUBNET_2_ID || '' }} | |
| LAMBDA_SECURITY_GROUP_ID: ${{ secrets.LAMBDA_SECURITY_GROUP_ID || '' }} | |
| # Default to the template's prod stage when the secret is missing so the | |
| # deploy step doesn't fail with an empty StageName parameter. | |
| API_STAGE_NAME: ${{ secrets.API_STAGE_NAME || 'prod' }} | |
| CLOUDFRONT_INVALIDATION_PATHS: ${{ github.event.inputs.cloudfront_invalidation_paths || secrets.CLOUDFRONT_INVALIDATION_PATHS || '/*' }} | |
| # Stack names and defaults | |
| STATIC_STACK_NAME: math-visuals-static-site | |
| API_STACK_NAME: math-visuals-api | |
| DATA_STACK_NAME: ${{ secrets.DATA_STACK_NAME || 'math-visuals-redis' }} | |
| DATA_ENVIRONMENT_NAME: prod | |
| SHARED_PARAMETERS_STACK_NAME: math-visuals-shared | |
| SKIP_EXAMPLES_SEEDING: "true" | |
| USE_EXTERNAL_REDIS: ${{ secrets.USE_EXTERNAL_REDIS || 'false' }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| - name: Install workspace dependencies | |
| run: npm ci | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v2 | |
| with: | |
| role-to-assume: ${{ env.AWS_IAC_ROLE_ARN }} | |
| aws-region: ${{ env.AWS_REGION }} | |
| - name: Debug CloudFront invalidation paths | |
| run: | | |
| if [ -z "${STATIC_SITE_CLOUDFRONT_DISTRIBUTION_ID:-}" ]; then | |
| echo "STATIC_SITE_CLOUDFRONT_DISTRIBUTION_ID is empty; CloudFront invalidation will be resolved from stack outputs." | |
| else | |
| echo "STATIC_SITE_CLOUDFRONT_DISTRIBUTION_ID is set." | |
| fi | |
| if [ -n "${{ github.event.inputs.cloudfront_invalidation_paths }}" ]; then | |
| echo "Using workflow_dispatch input for invalidation paths." | |
| elif [ -n "${CLOUDFRONT_INVALIDATION_PATHS:-}" ]; then | |
| echo "Using CLOUDFRONT_INVALIDATION_PATHS from secrets/default." | |
| else | |
| echo "CLOUDFRONT_INVALIDATION_PATHS is empty." | |
| fi | |
| if [ -z "${CLOUDFRONT_INVALIDATION_PATHS:-}" ]; then | |
| echo "CLOUDFRONT_INVALIDATION_PATHS length: 0" | |
| else | |
| echo "CLOUDFRONT_INVALIDATION_PATHS length: ${#CLOUDFRONT_INVALIDATION_PATHS}" | |
| fi | |
| - name: Ensure App Runner service-linked role exists | |
| run: | | |
| # App Runner stacks require the AWSServiceRoleForAppRunner role. See infra/README.md for IAM guidance. | |
| bash scripts/ensure-apprunner-slr.sh | |
| - name: Deploy shared parameters stack | |
| run: | | |
| set -euo pipefail | |
| echo "Ensuring $SHARED_PARAMETERS_STACK_NAME is ready for an update..." | |
| scripts/helpers/wait-for-stable-stack.sh "$SHARED_PARAMETERS_STACK_NAME" | |
| PARAM_OVERRIDES="EnvironmentName=$DATA_ENVIRONMENT_NAME" | |
| if [ -n "${REDIS_ENDPOINT:-}" ]; then | |
| PARAM_OVERRIDES="$PARAM_OVERRIDES RedisEndpoint=$REDIS_ENDPOINT" | |
| fi | |
| if [ -n "${REDIS_PORT:-}" ]; then | |
| PARAM_OVERRIDES="$PARAM_OVERRIDES RedisPort=$REDIS_PORT" | |
| fi | |
| if [ -n "${REDIS_PASSWORD_SECRET_ARN:-}" ]; then | |
| PARAM_OVERRIDES="$PARAM_OVERRIDES RedisPasswordSecretName=$REDIS_PASSWORD_SECRET_ARN" | |
| fi | |
| if [ -n "${PRIVATE_SUBNET_1_ID:-}" ]; then | |
| PARAM_OVERRIDES="$PARAM_OVERRIDES PrivateSubnet1Id=$PRIVATE_SUBNET_1_ID" | |
| fi | |
| if [ -n "${PRIVATE_SUBNET_2_ID:-}" ]; then | |
| PARAM_OVERRIDES="$PARAM_OVERRIDES PrivateSubnet2Id=$PRIVATE_SUBNET_2_ID" | |
| fi | |
| if [ -n "${LAMBDA_SECURITY_GROUP_ID:-}" ]; then | |
| PARAM_OVERRIDES="$PARAM_OVERRIDES LambdaSecurityGroupId=$LAMBDA_SECURITY_GROUP_ID" | |
| fi | |
| aws cloudformation deploy \ | |
| --stack-name "$SHARED_PARAMETERS_STACK_NAME" \ | |
| --template-file infra/shared-parameters.yaml \ | |
| --parameter-overrides $PARAM_OVERRIDES | |
| - name: Compute Lambda artefact key | |
| run: | | |
| set -euo pipefail | |
| base_key="${API_ARTIFACT_KEY:-lambda/api-lambda.zip}" | |
| base_without_zip="${base_key%.zip}" | |
| new_key="${base_without_zip}-${GITHUB_SHA}.zip" | |
| echo "LAMBDA_CODE_KEY=$new_key" >> "$GITHUB_ENV" | |
| - name: Build Lambda artefact | |
| run: | | |
| set -euo pipefail | |
| scripts/package-api-lambda.sh | |
| - name: Upload Lambda artefact to S3 | |
| run: | | |
| set -euo pipefail | |
| artefact_path="infra/api/api-lambda.zip" | |
| aws s3 cp "$artefact_path" "s3://$API_ARTIFACT_BUCKET/$LAMBDA_CODE_KEY" | |
| version_id=$(aws s3api head-object --bucket "$API_ARTIFACT_BUCKET" --key "$LAMBDA_CODE_KEY" --query 'VersionId' --output text 2>/dev/null || true) | |
| if [ -n "${version_id:-}" ] && [ "$version_id" != "None" ]; then | |
| echo "LAMBDA_CODE_VERSION=$version_id" >> "$GITHUB_ENV" | |
| fi | |
| - name: Deploy data stack when missing | |
| if: env.USE_EXTERNAL_REDIS != 'true' | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${REDIS_PASSWORD:-}" ]; then | |
| echo "REDIS_PASSWORD secret is required when provisioning the data stack internally." >&2 | |
| exit 1 | |
| fi | |
| if aws cloudformation describe-stacks --stack-name "$DATA_STACK_NAME" >/dev/null 2>&1; then | |
| echo "Stack $DATA_STACK_NAME already exists; skipping creation." | |
| exit 0 | |
| fi | |
| echo "Ensuring $DATA_STACK_NAME is ready for creation..." | |
| scripts/helpers/wait-for-stable-stack.sh "$DATA_STACK_NAME" | |
| aws cloudformation deploy \ | |
| --stack-name "$DATA_STACK_NAME" \ | |
| --template-file infra/data/template.yaml \ | |
| --parameter-overrides \ | |
| EnvironmentName=$DATA_ENVIRONMENT_NAME \ | |
| SharedParametersStackName=$SHARED_PARAMETERS_STACK_NAME \ | |
| RedisAuthToken="$REDIS_PASSWORD" | |
| echo "Waiting for $DATA_STACK_NAME to stabilize after creation..." | |
| scripts/helpers/wait-for-stable-stack.sh "$DATA_STACK_NAME" | |
| - name: Validate external Redis exports | |
| if: env.USE_EXTERNAL_REDIS == 'true' | |
| run: | | |
| set -euo pipefail | |
| require_export() { | |
| local export_name="$1" | |
| local value | |
| value=$(aws cloudformation list-exports \ | |
| --query "Exports[?Name=='${export_name}'].Value" \ | |
| --output text) | |
| if [ -z "$value" ] || [ "$value" = "None" ]; then | |
| echo "Fant ikke eksporten '${export_name}'. Sørg for at datasstacken '${DATA_STACK_NAME}' har denne eksporten før workflowen fortsetter." >&2 | |
| return 1 | |
| fi | |
| } | |
| require_export "${SHARED_PARAMETERS_STACK_NAME}-PrivateSubnet1Id" | |
| require_export "${SHARED_PARAMETERS_STACK_NAME}-PrivateSubnet2Id" | |
| require_export "${SHARED_PARAMETERS_STACK_NAME}-LambdaSecurityGroupId" | |
| require_export "${SHARED_PARAMETERS_STACK_NAME}-RedisEndpointParameterName" | |
| require_export "${SHARED_PARAMETERS_STACK_NAME}-RedisPortParameterName" | |
| require_export "${SHARED_PARAMETERS_STACK_NAME}-RedisPasswordSecretName" | |
| - name: Recover data stack from ROLLBACK_COMPLETE | |
| run: | | |
| set -euo pipefail | |
| aws cloudformation describe-stacks --stack-name "$DATA_STACK_NAME" >/dev/null 2>&1 \ | |
| && bash scripts/helpers/delete-rollback-complete-stack.sh "$DATA_STACK_NAME" \ | |
| || echo "Data stack not found; skipping cleanup." | |
| - name: Deploy API stack | |
| run: | | |
| set -euo pipefail | |
| echo "Ensuring $API_STACK_NAME is ready for an update..." | |
| scripts/helpers/wait-for-stable-stack.sh "$API_STACK_NAME" | |
| log_stack_events() { | |
| echo "Capturing recent events for $API_STACK_NAME after failure..." >&2 | |
| aws cloudformation describe-stack-events \ | |
| --stack-name "$API_STACK_NAME" \ | |
| --max-items 20 \ | |
| --query 'StackEvents[?ResourceStatus!="DELETE_COMPLETE"][].{Time:Timestamp,LogicalId:LogicalResourceId,Status:ResourceStatus,Reason:ResourceStatusReason}' \ | |
| --output table || true | |
| } | |
| trap log_stack_events ERR | |
| PARAM_OVERRIDES="LambdaCodeS3Bucket=$API_ARTIFACT_BUCKET LambdaCodeS3Key=$LAMBDA_CODE_KEY StageName=$API_STAGE_NAME" | |
| if [ -n "${LAMBDA_CODE_VERSION:-}" ]; then | |
| PARAM_OVERRIDES="$PARAM_OVERRIDES LambdaCodeS3ObjectVersion=$LAMBDA_CODE_VERSION" | |
| fi | |
| data_exports_stack="$DATA_STACK_NAME" | |
| network_params="false" | |
| if [ "${USE_EXTERNAL_REDIS}" = "true" ]; then | |
| data_exports_stack="$SHARED_PARAMETERS_STACK_NAME" | |
| fi | |
| if [ "$data_exports_stack" = "$SHARED_PARAMETERS_STACK_NAME" ]; then | |
| network_params="true" | |
| elif [ -n "${PRIVATE_SUBNET_1_ID:-}" ] || [ -n "${PRIVATE_SUBNET_2_ID:-}" ] || [ -n "${LAMBDA_SECURITY_GROUP_ID:-}" ]; then | |
| # Shared stack writes the provided VPC IDs into SSM parameters and exports the parameter names | |
| network_params="true" | |
| fi | |
| PARAM_OVERRIDES="$PARAM_OVERRIDES DataStackName=$data_exports_stack SharedParametersStackName=$SHARED_PARAMETERS_STACK_NAME ResolveNetworkFromParameters=$network_params" | |
| if [ -n "${LAMBDA_SECURITY_GROUP_ID:-}" ]; then | |
| PARAM_OVERRIDES="$PARAM_OVERRIDES LambdaSecurityGroupIdOverride=$LAMBDA_SECURITY_GROUP_ID" | |
| fi | |
| aws cloudformation deploy \ | |
| --stack-name "$API_STACK_NAME" \ | |
| --template-file infra/api/template.yaml \ | |
| --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND \ | |
| --parameter-overrides $PARAM_OVERRIDES | |
| - name: Resolve API endpoint for static site | |
| run: | | |
| set -euo pipefail | |
| endpoint=$(aws cloudformation describe-stacks \ | |
| --stack-name "$API_STACK_NAME" \ | |
| --query "Stacks[0].Outputs[?OutputKey=='ApiEndpoint'].OutputValue" \ | |
| --output text) | |
| if [ -z "$endpoint" ] || [ "$endpoint" = "None" ]; then | |
| echo "Fant ikke ApiEndpoint-output fra $API_STACK_NAME" >&2 | |
| exit 1 | |
| fi | |
| endpoint="${endpoint%%/}" | |
| export API_ENDPOINT_URL="$endpoint" | |
| echo "API_ENDPOINT_URL=$endpoint" >> "$GITHUB_ENV" | |
| API_ENDPOINT_URL="$endpoint" python - <<'PY' >> "$GITHUB_ENV" | |
| import os | |
| from urllib.parse import urlparse | |
| endpoint = os.environ["API_ENDPOINT_URL"].rstrip("/") | |
| parsed = urlparse(endpoint) | |
| domain = parsed.netloc | |
| path = parsed.path | |
| if not path or path == "/": | |
| path = "" | |
| elif not path.startswith('/'): | |
| path = '/' + path | |
| if path.endswith('/'): | |
| path = path.rstrip('/') | |
| examples = endpoint.rstrip('/') + '/api/examples' | |
| print(f"API_GATEWAY_DOMAIN_NAME={domain}") | |
| print(f"API_GATEWAY_ORIGIN_PATH={path}") | |
| print(f"API_EXAMPLES_URL={examples}") | |
| PY | |
| - name: Run examples API health check | |
| run: | | |
| set -euo pipefail | |
| npm run check-examples-api -- --url="${API_EXAMPLES_URL}" | |
| - name: Materialize examples dataset for seeding | |
| env: | |
| EXAMPLES_SEED_JSON: ${{ secrets.EXAMPLES_SEED_JSON }} | |
| EXAMPLES_SEED_JSON_PROD: ${{ secrets.EXAMPLES_SEED_JSON_PROD }} | |
| EXAMPLES_SEED_JSON_DEV: ${{ secrets.EXAMPLES_SEED_JSON_DEV }} | |
| EXAMPLES_SEED_JSON_PREVIEW: ${{ secrets.EXAMPLES_SEED_JSON_PREVIEW }} | |
| run: | | |
| set -euo pipefail | |
| env_name="${DATA_ENVIRONMENT_NAME:-prod}" | |
| select_dataset() { | |
| local selected="$1" | |
| case "$env_name" in | |
| prod) echo "${EXAMPLES_SEED_JSON_PROD:-$selected}" ;; | |
| dev) echo "${EXAMPLES_SEED_JSON_DEV:-$selected}" ;; | |
| preview) echo "${EXAMPLES_SEED_JSON_PREVIEW:-$selected}" ;; | |
| *) echo "$selected" ;; | |
| esac | |
| } | |
| dataset=$(select_dataset "${EXAMPLES_SEED_JSON:-}" ) | |
| if [ -z "$dataset" ]; then | |
| echo "Ingen EXAMPLES_SEED_JSON*-secret er konfigurert for miljøet '$env_name'. Hopper over seeding." >&2 | |
| echo "SKIP_EXAMPLES_SEEDING=true" >> "$GITHUB_ENV" | |
| exit 0 | |
| fi | |
| mkdir -p docs | |
| printf '%s' "$dataset" > docs/examples-seed.json | |
| echo "SKIP_EXAMPLES_SEEDING=false" >> "$GITHUB_ENV" | |
| - name: Validate examples dataset (--dry-run) | |
| if: env.SKIP_EXAMPLES_SEEDING != 'true' | |
| env: | |
| REGION: ${{ env.AWS_REGION }} | |
| DATA_STACK: ${{ env.DATA_STACK_NAME }} | |
| API_URL: ${{ env.API_EXAMPLES_URL }} | |
| run: | | |
| set -euo pipefail | |
| bash scripts/cloudshell-seed-examples.sh --dataset=docs/examples-seed.json --dry-run | |
| - name: Seed examples dataset | |
| if: env.SKIP_EXAMPLES_SEEDING != 'true' | |
| env: | |
| REGION: ${{ env.AWS_REGION }} | |
| DATA_STACK: ${{ env.DATA_STACK_NAME }} | |
| API_URL: ${{ env.API_EXAMPLES_URL }} | |
| run: | | |
| set -euo pipefail | |
| bash scripts/cloudshell-seed-examples.sh --dataset=docs/examples-seed.json | |
| - name: Recover static site stack from rollback failures | |
| run: | | |
| set -euo pipefail | |
| bash scripts/helpers/recover-stack-from-rollback.sh "$STATIC_STACK_NAME" | |
| - name: Validate shared exports for static site | |
| run: | | |
| set -euo pipefail | |
| require_export() { | |
| local export_name="$1" | |
| local value | |
| value=$(aws cloudformation list-exports \ | |
| --query "Exports[?Name=='${export_name}'].Value" \ | |
| --output text) | |
| if [ -z "$value" ] || [ "$value" = "None" ]; then | |
| echo "Fant ikke eksporten '${export_name}'. Sørg for at stacken '${SHARED_PARAMETERS_STACK_NAME}' har publisert den før statisk-side deployen." >&2 | |
| return 1 | |
| fi | |
| } | |
| require_export "${SHARED_PARAMETERS_STACK_NAME}-ExamplesAllowedOriginsParameterName" | |
| require_export "${SHARED_PARAMETERS_STACK_NAME}-SvgAllowedOriginsParameterName" | |
| - name: Detect static site bucket | |
| run: | | |
| set -euo pipefail | |
| if aws s3api head-bucket --bucket "$STATIC_SITE_BUCKET_NAME" >/dev/null 2>&1; then | |
| echo "Bucket $STATIC_SITE_BUCKET_NAME already exists; reusing it." | |
| echo "CREATE_SITE_BUCKET=false" >> "$GITHUB_ENV" | |
| else | |
| echo "Bucket $STATIC_SITE_BUCKET_NAME does not exist; creating it via CloudFormation." | |
| echo "CREATE_SITE_BUCKET=true" >> "$GITHUB_ENV" | |
| fi | |
| - name: Deploy static site stack | |
| run: | | |
| set -euo pipefail | |
| log_stack_events() { | |
| echo "Capturing recent events for $STATIC_STACK_NAME after failure..." >&2 | |
| aws cloudformation describe-stack-events \ | |
| --stack-name "$STATIC_STACK_NAME" \ | |
| --max-items 20 \ | |
| --query 'StackEvents[?ResourceStatus!="DELETE_COMPLETE"][].{Time:Timestamp,LogicalId:LogicalResourceId,Status:ResourceStatus,Reason:ResourceStatusReason}' \ | |
| --output table || true | |
| } | |
| trap log_stack_events ERR | |
| api_domain="${STATIC_SITE_API_DOMAIN:-$API_GATEWAY_DOMAIN_NAME}" | |
| api_origin_path="${STATIC_SITE_API_ORIGIN_PATH:-$API_GATEWAY_ORIGIN_PATH}" | |
| if [ -n "${STATIC_SITE_API_DOMAIN:-}" ]; then | |
| api_domain="${api_domain#http://}" | |
| api_domain="${api_domain#https://}" | |
| api_domain="${api_domain%%/*}" | |
| if [[ ! "$api_domain" =~ ^[A-Za-z0-9.-]+\.execute-api\.[A-Za-z0-9-]+\.amazonaws\.com$ ]]; then | |
| echo "STATIC_SITE_API_DOMAIN='$STATIC_SITE_API_DOMAIN' er ikke et execute-api-domene; bruker ApiEndpoint i stedet." >&2 | |
| api_domain="$API_GATEWAY_DOMAIN_NAME" | |
| fi | |
| fi | |
| if [[ ! "$api_domain" =~ ^[A-Za-z0-9.-]+\.execute-api\.[A-Za-z0-9-]+\.amazonaws\.com$ ]]; then | |
| echo "ApiGatewayDomainName must be an execute-api hostname without protocol or path (got '$api_domain')" >&2 | |
| exit 1 | |
| fi | |
| if [ -z "$api_domain" ]; then | |
| echo "Fant ikke API-domene for CloudFront-oppsettet" >&2 | |
| exit 1 | |
| fi | |
| if [ -z "$api_origin_path" ] || [ "$api_origin_path" == "/" ]; then | |
| api_origin_path="" | |
| fi | |
| create_site_bucket="${CREATE_SITE_BUCKET:-false}" | |
| aws cloudformation deploy \ | |
| --stack-name "$STATIC_STACK_NAME" \ | |
| --template-file infra/static-site/template.yaml \ | |
| --capabilities CAPABILITY_IAM \ | |
| --parameter-overrides \ | |
| SiteBucketName=$STATIC_SITE_BUCKET_NAME \ | |
| CreateSiteBucket=$create_site_bucket \ | |
| SiteEnvironmentName=$DATA_ENVIRONMENT_NAME \ | |
| ApiGatewayDomainName=$api_domain \ | |
| ApiGatewayOriginPath=$api_origin_path \ | |
| CloudFrontPriceClass=$STATIC_SITE_CLOUDFRONT_PRICE_CLASS \ | |
| SharedParametersStackName=$SHARED_PARAMETERS_STACK_NAME | |
| - name: Resolve static site outputs | |
| run: | | |
| set -euo pipefail | |
| site_bucket=$(aws cloudformation describe-stacks \ | |
| --stack-name "$STATIC_STACK_NAME" \ | |
| --query "Stacks[0].Outputs[?OutputKey=='StaticSiteBucketName'].OutputValue" \ | |
| --output text) | |
| distribution_id=$(aws cloudformation describe-stacks \ | |
| --stack-name "$STATIC_STACK_NAME" \ | |
| --query "Stacks[0].Outputs[?OutputKey=='CloudFrontDistributionId'].OutputValue" \ | |
| --output text) | |
| if [ -n "$site_bucket" ] && [ "$site_bucket" != "None" ]; then | |
| echo "STATIC_SITE_BUCKET_NAME=$site_bucket" >> "$GITHUB_ENV" | |
| fi | |
| if [ -n "$distribution_id" ] && [ "$distribution_id" != "None" ]; then | |
| echo "STATIC_SITE_CLOUDFRONT_DISTRIBUTION_ID=$distribution_id" >> "$GITHUB_ENV" | |
| fi | |
| - name: Build and upload static assets | |
| env: | |
| SITE_BUCKET_NAME: ${{ env.STATIC_SITE_BUCKET_NAME }} | |
| run: | | |
| set -euo pipefail | |
| echo "🧹 Sletter gamle builds og cache (Nuclear Option)..." | |
| rm -rf public dist packages/*/dist .turbo node_modules/.cache | |
| echo "🏗️ Bygger applikasjonen på nytt fra bunnen..." | |
| npm run build | |
| echo "🔍 VERIFISERER at fixen er med..." | |
| if grep -q "svgDataUrl" public/svg-export-helper.js; then | |
| echo "✅ SUKSESS: Bygget inneholder den nye fixen!" | |
| else | |
| echo "❌ FEIL: Bygget inneholder fortsatt gammel kode! Stopper deploy." | |
| exit 1 | |
| fi | |
| echo "🚀 Laster opp ferske filer til S3..." | |
| aws s3 sync public/ "s3://$SITE_BUCKET_NAME/" \ | |
| --delete \ | |
| --exclude "index.html" \ | |
| --exclude ".git/*" \ | |
| --exclude "infra/*" \ | |
| --exclude "**/node_modules/*" \ | |
| --exclude "node_modules/*" \ | |
| --cache-control "max-age=31536000,public" | |
| echo "📄 Laster opp index.html..." | |
| aws s3 cp public/index.html "s3://$SITE_BUCKET_NAME/index.html" \ | |
| --cache-control "no-cache,no-store,must-revalidate" | |
| - name: Invalidate CloudFront cache | |
| if: ${{ env.STATIC_SITE_CLOUDFRONT_DISTRIBUTION_ID != '' }} | |
| run: | | |
| set -o noglob | |
| PATHS="${CLOUDFRONT_INVALIDATION_PATHS:-/*}" | |
| set +o noglob | |
| if [[ " $PATHS " != *" /* "* ]]; then | |
| PATHS="$PATHS /*" | |
| fi | |
| echo "Using invalidation paths: $PATHS" | |
| IFS=' ' read -r -a path_args <<< "$PATHS" | |
| aws cloudfront create-invalidation \ | |
| --distribution-id "$STATIC_SITE_CLOUDFRONT_DISTRIBUTION_ID" \ | |
| --paths "${path_args[@]}" |