name: Validate and deploy dev on: pull_request: branches: [dev] push: branches: [dev] workflow_dispatch: permissions: contents: read jobs: validate: name: Validate deployable source bundle runs-on: ubuntu-latest steps: - name: Checkout # actions/checkout @ v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: fetch-depth: 0 - name: Set up .NET # actions/setup-dotnet @ v6.0.0 uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 with: dotnet-version: "8.0.x" - name: Set up Node.js # actions/setup-node @ v6.5.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: node-version: "22.22.1" cache: npm cache-dependency-path: infra/cdk/package-lock.json - name: Repository quality gate run: bash scripts/governance-check.sh - name: Validate CDK deployment infrastructure run: | npm ci --prefix infra/cdk npm run synth --prefix infra/cdk - name: Build Elastic Beanstalk source bundle run: bash scripts/package-elastic-beanstalk.sh - name: Inspect source bundle contract run: | set -euo pipefail unzip -t .artifacts/elastic-beanstalk/site.zip unzip -Z1 .artifacts/elastic-beanstalk/site.zip \ > .artifacts/elastic-beanstalk/zip-contents.txt grep -Fxq "efbundle" .artifacts/elastic-beanstalk/zip-contents.txt grep -Fxq ".ebextensions/01_migrations.config" \ .artifacts/elastic-beanstalk/zip-contents.txt grep -Fxq ".ebextensions/02_webhook_config.config" \ .artifacts/elastic-beanstalk/zip-contents.txt unzip -p .artifacts/elastic-beanstalk/site.zip \ .ebextensions/02_webhook_config.config \ > .artifacts/elastic-beanstalk/webhook-config.txt grep -Fxq ' WorkOrderWebhook__Enabled: "true"' \ .artifacts/elastic-beanstalk/webhook-config.txt grep -Fxq ' WorkOrderWebhook__Region: us-east-1' \ .artifacts/elastic-beanstalk/webhook-config.txt grep -Fxq \ ' WorkOrderWebhook__SecretId: arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB' \ .artifacts/elastic-beanstalk/webhook-config.txt deploy: name: Deploy shoc-backend to Elastic Beanstalk dev if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev') needs: validate runs-on: ubuntu-latest permissions: contents: read id-token: write environment: name: dev concurrency: group: deploy-dev cancel-in-progress: false steps: - name: Checkout # actions/checkout @ v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Set up .NET # actions/setup-dotnet @ v6.0.0 uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 with: dotnet-version: "8.0.x" - name: Build Elastic Beanstalk source bundle run: bash scripts/package-elastic-beanstalk.sh - name: Configure AWS credentials (OIDC) # aws-actions/configure-aws-credentials @ v6.2.3 uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} aws-region: us-east-1 audience: sts.amazonaws.com - name: Capture current environment version run: | set -euo pipefail prev="$(aws elasticbeanstalk describe-environments \ --environment-names shoc-backend-dev \ --region us-east-1 \ --query 'Environments[0].VersionLabel' \ --output text)" echo "$prev" > .artifacts/elastic-beanstalk/previous-version.txt echo "Previous version label: $prev" - name: Deploy prebuilt bundle to existing environment # aws-actions/aws-elasticbeanstalk-deploy @ v1.0.6 uses: aws-actions/aws-elasticbeanstalk-deploy@7883cdd454c162051bf6fc13389536b045149b4c with: aws-region: us-east-1 application-name: shoc-backend environment-name: shoc-backend-dev version-label: ${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }} deployment-package-path: .artifacts/elastic-beanstalk/site.zip s3-bucket-name: elasticbeanstalk-us-east-1-396287094661 create-application-if-not-exists: "false" create-environment-if-not-exists: "false" create-s3-bucket-if-not-exists: "false" use-existing-application-version-if-available: "false" wait-for-deployment: "true" wait-for-environment-recovery: "true" - name: Verify exact application version is active run: | set -euo pipefail expected="${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}" status="Unknown" current="Unknown" health="Unknown" for _ in $(seq 1 80); do read -r status current health < <( aws elasticbeanstalk describe-environments \ --environment-names shoc-backend-dev \ --region us-east-1 \ --query 'Environments[0].[Status,VersionLabel,Health]' \ --output text ) echo "environment status: $status; version: $current; health: $health" if [ "$status" = "Ready" ]; then if [ "$current" = "$expected" ] && { [ "$health" = "Green" ] || [ "$health" = "Yellow" ]; }; then echo "Expected application version is Ready and healthy." exit 0 fi echo "Environment became Ready without activating expected version $expected." >&2 exit 1 fi sleep 15 done echo "Expected application version did not become Ready within the deployment window." >&2 exit 1 - name: Post-deploy smoke run: bash scripts/smoke-elastic-beanstalk.sh https://api.dev.seahaven.com - name: Verify webhook secret source is operational run: | set -euo pipefail response_file="$(mktemp)" trap 'rm -f "$response_file"' EXIT status="$(curl --silent --show-error \ --output "$response_file" \ --write-out '%{http_code}' \ --request POST \ --header 'Content-Type: application/json' \ --header "X-SH-Timestamp: $(date +%s)" \ --header 'X-SH-Key-Id: deployment-smoke-invalid-key' \ --header "X-SH-Signature: v1=$(printf '0%.0s' {1..64})" \ --data '{}' \ https://api.dev.seahaven.com/api/webhooks/work-orders)" if [ "$status" != "401" ]; then echo "Expected enabled webhook with an operational secret source to reject the invalid probe with 401; received $status." >&2 sed -n '1,20p' "$response_file" >&2 exit 1 fi - name: Restore previous application version on failure (schema is not reverted) if: failure() run: | set -euo pipefail prev_file=".artifacts/elastic-beanstalk/previous-version.txt" if [ ! -f "$prev_file" ]; then echo "No previous version captured; nothing to roll back." >&2 exit 0 fi prev="$(cat "$prev_file")" if [ -z "$prev" ] || [ "$prev" = "null" ] || [ "$prev" = "None" ] || [ "$prev" = "N/A" ]; then echo "No previous version recorded; nothing to roll back." >&2 exit 0 fi echo "Waiting for any in-flight environment update to settle..." status="Unknown" current="Unknown" health="Unknown" for _ in $(seq 1 80); do read -r status current health < <( aws elasticbeanstalk describe-environments \ --environment-names shoc-backend-dev \ --region us-east-1 \ --query 'Environments[0].[Status,VersionLabel,Health]' \ --output text ) echo "environment status: $status; version: $current; health: $health" if [ "$status" = "Ready" ]; then break fi sleep 15 done if [ "$status" != "Ready" ]; then echo "Environment did not settle before rollback." >&2 exit 1 fi if [ "$current" = "$prev" ]; then echo "Environment is already on previous version $prev." exit 0 fi echo "Restoring shoc-backend-dev application code to version label: $prev" echo "Database migrations are not reverted; deployable migrations must follow the expand/contract policy." aws elasticbeanstalk update-environment \ --environment-name shoc-backend-dev \ --version-label "$prev" \ --region us-east-1 echo "Waiting for previous version to become healthy..." for _ in $(seq 1 80); do read -r status current health < <( aws elasticbeanstalk describe-environments \ --environment-names shoc-backend-dev \ --region us-east-1 \ --query 'Environments[0].[Status,VersionLabel,Health]' \ --output text ) echo "environment status: $status; version: $current; health: $health" if [ "$status" = "Ready" ]; then if [ "$current" = "$prev" ] && { [ "$health" = "Green" ] || [ "$health" = "Yellow" ]; }; then echo "Application version restore complete; previous code is Ready and healthy." exit 0 fi echo "Rollback reached Ready in an unexpected version/health state." >&2 exit 1 fi sleep 15 done echo "Environment did not return to Ready within rollback window." >&2 exit 1