Staging Deployment #23
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: Staging Deployment | |
| on: | |
| workflow_run: | |
| workflows: ["Publish Main Artifacts"] # must be the exact name of the workflow in publish-main.yml | |
| types: | |
| - completed | |
| branches: | |
| - main | |
| # Only ever deploy the newest staging state. If publishes pile up, supersede the | |
| # in-flight staging deploy with the latest. | |
| concurrency: | |
| group: deploy-stg | |
| cancel-in-progress: true | |
| jobs: | |
| deploy-staging: | |
| name: Deploy to Staging | |
| runs-on: ubuntu-latest | |
| if: ${{ github.event.workflow_run.conclusion == 'success' }} | |
| permissions: | |
| id-token: write | |
| contents: read | |
| env: | |
| AWS_STG_ACCOUNT_ID: "071308038858" | |
| SHA: ${{ github.event.workflow_run.head_sha }} # the exact commit the publish-main run built the artifact set for | |
| ARTIFACT_BUCKET: "apiforge-artifacts-071308038858" # artifacts for backend and frontend | |
| FRONTEND_BUCKET: "apiforge-frontend-071308038858" # frontend artifact copied from ARTIFACT_BUCKET | |
| BUILD_PREFIX: "s3://apiforge-artifacts-071308038858/builds/${{ github.event.workflow_run.head_sha }}" | |
| steps: | |
| # check out the exact commit that was built, so the deployed SAM template / | |
| # scripts match the downloaded artifacts | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.workflow_run.head_sha }} | |
| - uses: ./.github/actions/setup-just | |
| - uses: ./.github/actions/setup-node | |
| with: | |
| module-name: web | |
| - name: Install frontend dependencies | |
| run: just web install | |
| - name: Install Playwright browsers | |
| run: cd web && npx playwright install --with-deps chromium | |
| - name: Configure AWS Staging Credentials (OIDC) | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| role-to-assume: arn:aws:iam::${{ env.AWS_STG_ACCOUNT_ID }}:role/GitHubActionsDeploymentRole # role name should match the one in bootstrap/template.yml | |
| aws-region: eu-central-1 | |
| audience: sts.amazonaws.com | |
| - name: Setup AWS SAM CLI | |
| uses: aws-actions/setup-sam@v2 | |
| - name: Download Backend Artifacts from S3 | |
| run: | | |
| for MODULE in backend-api-lambda sqs-dispatcher-lambda function-deployer-lambda; do | |
| echo "Downloading $MODULE ($SHA)..." | |
| mkdir -p lambda/$MODULE/target | |
| aws s3 cp "$BUILD_PREFIX/$MODULE/function.zip" "lambda/$MODULE/target/function.zip" | |
| done | |
| # the frontend sync depends on the bucket existing, so it must come after this step. | |
| - name: Deploy to Staging | |
| run: just deploy-stack stg | |
| - name: Deploy Frontend Artifact to Frontend S3 Bucket | |
| run: | | |
| aws s3 cp "$BUILD_PREFIX/frontend.zip" ./frontend-local.zip | |
| echo "Unzipping frontend package..." | |
| unzip -q frontend-local.zip -d ./frontend-dist | |
| echo "Synchronizing files with the web S3 bucket..." | |
| # CRITICAL: We must use --exclude "config.js" here. | |
| # config.js is dynamically generated by the `connect-frontend.sh` script during deploy-stack above, | |
| # so it is missing from our local './frontend-dist' artifact. Without the exclude flag, | |
| # the `--delete` parameter would wipe out the config.js that connect-frontend.sh just wrote to the bucket, | |
| # breaking the app until the next deploy. | |
| aws s3 sync "./frontend-dist/" "s3://$FRONTEND_BUCKET/" --delete --exclude "config.js" | |
| # we need to invalidate the CloudFront cache after deployment to make sure the new frontend version is served, otherwise users might still get the old cached version. | |
| - name: Invalidate CloudFront CDN Cache | |
| run: | | |
| STACK_NAME="apiforge-stg" | |
| echo "Fetching CloudFront Distribution ID from CloudFormation Outputs..." | |
| DISTRIBUTION_ID=$(aws cloudformation describe-stacks \ | |
| --stack-name "$STACK_NAME" \ | |
| --query "Stacks[0].Outputs[?OutputKey=='CloudFrontDistributionId'].OutputValue" \ | |
| --output text) | |
| if [ -z "$DISTRIBUTION_ID" ] || [ "$DISTRIBUTION_ID" == "None" ]; then | |
| echo "❌ ERROR: Could not find CloudFrontDistributionId in Stack Outputs" | |
| exit 1 | |
| fi | |
| echo "🚀 Triggering cache invalidation for Distribution: $DISTRIBUTION_ID" | |
| INVALIDATION_ID=$(aws cloudfront create-invalidation \ | |
| --distribution-id "$DISTRIBUTION_ID" \ | |
| --paths "/*" \ | |
| --query "Invalidation.Id" \ | |
| --output text) | |
| echo "✅ Invalidation submitted successfully. ID: $INVALIDATION_ID" | |
| echo "Note: It usually takes 1-3 minutes for AWS CloudFront to apply this globally." | |
| # Hand the IDs to later steps so the e2e run can wait for the new build to go live. | |
| echo "DISTRIBUTION_ID=$DISTRIBUTION_ID" >> "$GITHUB_ENV" | |
| echo "INVALIDATION_ID=$INVALIDATION_ID" >> "$GITHUB_ENV" | |
| - name: Smoke Test | |
| run: | | |
| API_ID=$(aws cloudformation describe-stacks \ | |
| --stack-name "apiforge-stg" \ | |
| --query "Stacks[0].Outputs[?OutputKey=='ApiId'].OutputValue" \ | |
| --output text) | |
| echo "Checking health endpoint..." | |
| STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://$API_ID.execute-api.eu-central-1.amazonaws.com/stg/api/v1/health") | |
| if [ "$STATUS" -ne 200 ]; then | |
| echo "❌ ERROR: Smoke Test failed - API responded with status $STATUS" | |
| exit 1 | |
| fi | |
| echo "✅ API is healthy (200 OK)" | |
| - name: E2E Tests against Staging | |
| run: | | |
| # CloudFront invalidation is async; wait for it so we don't test a stale frontend build. | |
| echo "Waiting for CloudFront invalidation $INVALIDATION_ID to complete..." | |
| aws cloudfront wait invalidation-completed \ | |
| --distribution-id "$DISTRIBUTION_ID" \ | |
| --id "$INVALIDATION_ID" | |
| DOMAIN=$(aws cloudformation describe-stacks \ | |
| --stack-name "apiforge-stg" \ | |
| --query "Stacks[0].Outputs[?OutputKey=='CloudFrontDistributionName'].OutputValue" \ | |
| --output text) | |
| if [ -z "$DOMAIN" ] || [ "$DOMAIN" == "None" ]; then | |
| echo "❌ ERROR: Could not resolve CloudFront domain from stack outputs" | |
| exit 1 | |
| fi | |
| echo "Running e2e against https://$DOMAIN" | |
| E2E_BASE_URL="https://$DOMAIN" just web test-e2e | |
| - name: Upload Playwright Report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: playwright-report-stg | |
| path: web/playwright-report/ | |
| retention-days: 7 | |
| # Runs only if every previous step (incl. the smoke test and e2e) passed. The marker is | |
| # the gate for production: trigger-release.yml / deploy-prod.yml only accept a commit that | |
| # has staged/<sha>. staged/latest is the default release target. | |
| - name: Mark commit as staged | |
| run: | | |
| printf '%s' "$SHA" > sha.txt | |
| echo "Writing staging marker staged/$SHA ..." | |
| aws s3 cp sha.txt "s3://$ARTIFACT_BUCKET/staged/$SHA" | |
| aws s3 cp sha.txt "s3://$ARTIFACT_BUCKET/staged/latest" | |
| echo "✅ Marked $SHA as staged." |