Deploy Amethyst API to AWS #329
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 Amethyst API to AWS | |
| on: | |
| push: | |
| branches: | |
| - main | |
| schedule: | |
| # Runs at 06:00 UTC every day during MLB season (March–October) | |
| - cron: '0 6 * 3-10 *' | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: "Run mode: redeploy, restart, sync-players, or describe-env (dump App Runner config — read-only)" | |
| type: choice | |
| required: true | |
| default: redeploy | |
| options: | |
| - redeploy | |
| - restart | |
| - sync-players | |
| - describe-env | |
| jobs: | |
| sync-players: | |
| runs-on: ubuntu-latest | |
| # Run on the daily schedule or on manual `sync-players` dispatch. | |
| if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.mode == 'sync-players') | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v4 | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: 'pnpm' | |
| - name: Install Dependencies | |
| run: pnpm install | |
| - name: Sync MLB Players | |
| env: | |
| MONGO_URI: ${{ secrets.MONGO_URI }} | |
| run: pnpm sync-players -- --confirm-universe-write | |
| # Optional: repo secret CI_FAILURE_WEBHOOK (Slack incoming webhook or Discord; | |
| # payload sets both `text` and `content` so either accepts the message.) | |
| - name: Notify on sync failure (optional webhook) | |
| if: failure() | |
| env: | |
| WEBHOOK_URL: ${{ secrets.CI_FAILURE_WEBHOOK }} | |
| GH_REPO: ${{ github.repository }} | |
| GH_RUN: ${{ github.run_id }} | |
| run: | | |
| if [ -z "${WEBHOOK_URL}" ]; then | |
| echo "No CI_FAILURE_WEBHOOK secret; skipping external notify." | |
| exit 0 | |
| fi | |
| msg="sync-players failed in ${GH_REPO} (run ${GH_RUN})" | |
| payload=$(jq -n --arg text "$msg" --arg content "$msg" '{text: $text, content: $content}') | |
| curl -fsS -X POST -H 'Content-Type: application/json' \ | |
| -d "$payload" "$WEBHOOK_URL" | |
| describe-env: | |
| runs-on: ubuntu-latest | |
| # Read-only diagnostic. Dumps the App Runner service config (env vars, | |
| # image identifier, observability config) so we can confirm what's | |
| # actually running in production. AWS masks plaintext secret values | |
| # ("RuntimeEnvironmentSecrets" returns ARNs only; plaintext | |
| # "RuntimeEnvironmentVariables" come back verbatim — we redact | |
| # passwords from MONGO_URI / REDIS_URL in the post-processing step). | |
| if: github.event_name == 'workflow_dispatch' && inputs.mode == 'describe-env' | |
| steps: | |
| - name: Configure AWS Credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| aws-region: us-east-1 | |
| - name: Describe service (redacted) | |
| env: | |
| APP_RUNNER_SERVICE_NAME: amethyst-standalone-api | |
| run: | | |
| set -euo pipefail | |
| SERVICE_ARN=$(aws apprunner list-services \ | |
| --query "ServiceSummaryList[?ServiceName=='${APP_RUNNER_SERVICE_NAME}'].ServiceArn | [0]" \ | |
| --output text) | |
| if [ -z "$SERVICE_ARN" ] || [ "$SERVICE_ARN" = "None" ]; then | |
| echo "Error: App Runner service '${APP_RUNNER_SERVICE_NAME}' not found." | |
| exit 1 | |
| fi | |
| echo "Service ARN: $SERVICE_ARN" | |
| echo | |
| echo "==== service status / image identifier / observability ====" | |
| aws apprunner describe-service --service-arn "$SERVICE_ARN" \ | |
| --query "{Status: Service.Status, ServiceUrl: Service.ServiceUrl, CreatedAt: Service.CreatedAt, UpdatedAt: Service.UpdatedAt, ImageIdentifier: Service.SourceConfiguration.ImageRepository.ImageIdentifier, ImageRepositoryType: Service.SourceConfiguration.ImageRepository.ImageRepositoryType, AutoDeploymentsEnabled: Service.SourceConfiguration.AutoDeploymentsEnabled, AuthenticationConfiguration: Service.SourceConfiguration.AuthenticationConfiguration, InstanceConfigCpu: Service.InstanceConfiguration.Cpu, InstanceConfigMemory: Service.InstanceConfiguration.Memory, AutoScalingConfigurationSummary: Service.AutoScalingConfigurationSummary, ObservabilityConfiguration: Service.ObservabilityConfiguration}" \ | |
| --output json | |
| echo | |
| echo "==== environment variables (passwords redacted) ====" | |
| aws apprunner describe-service --service-arn "$SERVICE_ARN" \ | |
| --query "Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables" \ | |
| --output json \ | |
| | jq 'to_entries | |
| | map({key: .key, | |
| value: (.value | |
| | if type == "string" | |
| then gsub("(?<a>://[^:]+:)[^@]+(?<b>@)"; "\(.a)***\(.b)") | |
| else . end)}) | |
| | sort_by(.key) | |
| | from_entries' | |
| echo | |
| echo "==== secret refs (ARNs only) ====" | |
| aws apprunner describe-service --service-arn "$SERVICE_ARN" \ | |
| --query "Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentSecrets" \ | |
| --output json | |
| echo | |
| echo "==== latest two deployment operations ====" | |
| aws apprunner list-operations --service-arn "$SERVICE_ARN" --max-results 2 \ | |
| --query "OperationSummaryList[].{Id: Id, Type: Type, Status: Status, StartedAt: StartedAt, EndedAt: EndedAt, TargetArn: TargetArn}" \ | |
| --output json | |
| echo | |
| echo "==== ECR image manifest for currently-pinned identifier (digest + pushedAt) ====" | |
| IMG=$(aws apprunner describe-service --service-arn "$SERVICE_ARN" \ | |
| --query "Service.SourceConfiguration.ImageRepository.ImageIdentifier" --output text) | |
| REPO=${IMG##*/} | |
| REPO=${REPO%%:*} | |
| TAG=${IMG##*:} | |
| echo "ECR repository: $REPO tag: $TAG" | |
| aws ecr describe-images --repository-name "$REPO" \ | |
| --image-ids imageTag="$TAG" \ | |
| --query "imageDetails[].{Digest: imageDigest, PushedAt: imagePushedAt, Size: imageSizeInBytes, Tags: imageTags}" \ | |
| --output json | |
| restart: | |
| runs-on: ubuntu-latest | |
| # Hard cycle App Runner instances: pause -> wait -> resume. This is the | |
| # only reliable way to drop in-process catalog caches on stuck instances | |
| # when `start-deployment` doesn't roll them on its own (e.g. when the | |
| # new image is a no-op vs. the running one). Triggered manually via | |
| # `gh workflow run deploy.yml -f mode=restart`. | |
| if: github.event_name == 'workflow_dispatch' && inputs.mode == 'restart' | |
| steps: | |
| - name: Configure AWS Credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| aws-region: us-east-1 | |
| - name: Pause + resume App Runner service | |
| env: | |
| APP_RUNNER_SERVICE_NAME: amethyst-standalone-api | |
| run: | | |
| set -euo pipefail | |
| SERVICE_ARN=$(aws apprunner list-services \ | |
| --query "ServiceSummaryList[?ServiceName=='${APP_RUNNER_SERVICE_NAME}'].ServiceArn | [0]" \ | |
| --output text) | |
| if [ -z "$SERVICE_ARN" ] || [ "$SERVICE_ARN" = "None" ]; then | |
| echo "Error: App Runner service '${APP_RUNNER_SERVICE_NAME}' not found." | |
| exit 1 | |
| fi | |
| echo "Service ARN: $SERVICE_ARN" | |
| echo "Pausing service..." | |
| aws apprunner pause-service --service-arn "$SERVICE_ARN" >/dev/null | |
| for i in {1..20}; do | |
| STATUS=$(aws apprunner describe-service --service-arn "$SERVICE_ARN" --query "Service.Status" --output text) | |
| echo "[$i/20] status=$STATUS" | |
| if [ "$STATUS" = "PAUSED" ]; then break; fi | |
| sleep 15 | |
| done | |
| if [ "$STATUS" != "PAUSED" ]; then | |
| echo "Error: service did not reach PAUSED in time (last status: $STATUS)." | |
| exit 1 | |
| fi | |
| echo "Resuming service..." | |
| aws apprunner resume-service --service-arn "$SERVICE_ARN" >/dev/null | |
| for i in {1..20}; do | |
| STATUS=$(aws apprunner describe-service --service-arn "$SERVICE_ARN" --query "Service.Status" --output text) | |
| echo "[$i/20] status=$STATUS" | |
| if [ "$STATUS" = "RUNNING" ]; then break; fi | |
| sleep 15 | |
| done | |
| if [ "$STATUS" != "RUNNING" ]; then | |
| echo "Error: service did not return to RUNNING in time (last status: $STATUS)." | |
| exit 1 | |
| fi | |
| echo "Service is RUNNING again — fresh instances will warm against current Mongo." | |
| deploy: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.mode == 'redeploy') | |
| services: | |
| mongo: | |
| image: mongo:7 | |
| ports: | |
| - 27017:27017 | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v4 | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: 'pnpm' | |
| - name: Install Dependencies | |
| run: pnpm install | |
| - name: Test portal | |
| run: pnpm --filter amethyst-portal exec vitest run | |
| - name: Build API | |
| run: pnpm build | |
| - name: Check Types | |
| run: pnpm check-types | |
| - name: Lint | |
| run: pnpm lint | |
| - name: Lint portal | |
| run: pnpm --filter amethyst-portal exec eslint src --max-warnings 0 | |
| - name: Test | |
| env: | |
| KEY_ISSUANCE_INTEGRATION_MONGO_URI: mongodb://127.0.0.1:27017/key_issue_ci | |
| run: pnpm test | |
| - name: Replay evaluator (checkpoint smoke) | |
| run: pnpm replay-eval | |
| - name: Configure AWS Credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| aws-region: us-east-1 | |
| - name: Login to Amazon ECR | |
| id: login-ecr | |
| uses: aws-actions/amazon-ecr-login@v2 | |
| - name: Build, tag, and push API image to Amazon ECR | |
| env: | |
| ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} | |
| ECR_REPOSITORY: amethyst-standalone-api | |
| IMAGE_TAG: latest | |
| run: | | |
| docker build \ | |
| --build-arg BUILD_GIT_SHA="${{ github.sha }}" \ | |
| -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . | |
| docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG | |
| - name: Deploy to App Runner | |
| env: | |
| APP_RUNNER_SERVICE_NAME: amethyst-standalone-api | |
| run: | | |
| SERVICE_ARN=$(aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='${APP_RUNNER_SERVICE_NAME}'].ServiceArn | [0]" --output text) | |
| if [ -z "$SERVICE_ARN" ] || [ "$SERVICE_ARN" = "None" ]; then | |
| echo "Error: App Runner service '${APP_RUNNER_SERVICE_NAME}' not found." | |
| exit 1 | |
| fi | |
| echo "Found Service ARN: $SERVICE_ARN" | |
| # Wait up to 5 minutes for any existing operations (like a previous push) to finish | |
| for i in {1..10}; do | |
| STATUS=$(aws apprunner describe-service --service-arn "$SERVICE_ARN" --query "Service.Status" --output text) | |
| if [ "$STATUS" = "RUNNING" ]; then | |
| echo "Service is RUNNING. Starting deployment..." | |
| aws apprunner start-deployment --service-arn "$SERVICE_ARN" | |
| exit 0 | |
| fi | |
| echo "Service is currently $STATUS. Waiting 30s before retry ($i/10)..." | |
| sleep 30 | |
| done | |
| echo "Error: Service did not return to RUNNING state in time." | |
| exit 1 |