Manual Deploy [boxel] #1285
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: Manual Deploy [boxel] | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| environment: | |
| description: Deployment environment | |
| required: false | |
| default: staging | |
| workflow_call: | |
| inputs: | |
| environment: | |
| required: true | |
| type: string | |
| permissions: | |
| contents: read | |
| deployments: write | |
| id-token: write | |
| jobs: | |
| create-deployment: | |
| name: Create GitHub deployment | |
| if: github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| deployment-id: ${{ steps.create.outputs.deployment_id }} | |
| environment-url: ${{ steps.env.outputs.environment_url }} | |
| steps: | |
| - id: env | |
| run: | | |
| if [ "${{ inputs.environment }}" = "production" ]; then | |
| echo "environment_url=https://app.boxel.ai" >> "$GITHUB_OUTPUT" | |
| elif [ "${{ inputs.environment }}" = "staging" ]; then | |
| echo "environment_url=https://realms-staging.stack.cards" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "environment_url=" >> "$GITHUB_OUTPUT" | |
| fi | |
| - id: create | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const environment = '${{ inputs.environment }}'; | |
| const response = await github.rest.repos.createDeployment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: context.sha, | |
| required_contexts: [], | |
| auto_merge: false, | |
| environment, | |
| description: `Manual deploy to ${environment}`, | |
| transient_environment: false, | |
| production_environment: environment === 'production', | |
| }); | |
| core.setOutput('deployment_id', response.data.id.toString()); | |
| - name: Mark deployment in progress | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const deployment_id = Number('${{ steps.create.outputs.deployment_id }}'); | |
| const environmentUrlValue = '${{ steps.env.outputs.environment_url }}'; | |
| const environment_url = | |
| environmentUrlValue && environmentUrlValue.length > 0 | |
| ? environmentUrlValue | |
| : undefined; | |
| await github.rest.repos.createDeploymentStatus({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| deployment_id, | |
| state: 'in_progress', | |
| environment_url, | |
| log_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, | |
| description: 'Deployment started', | |
| }); | |
| build-ai-bot: | |
| name: Build ai-bot Docker image | |
| uses: cardstack/gh-actions/.github/workflows/docker-ecr.yml@main | |
| secrets: inherit | |
| with: | |
| repository: "boxel-ai-bot-${{ inputs.environment }}" | |
| environment: ${{ inputs.environment }} | |
| dockerfile: "packages/ai-bot/Dockerfile" | |
| deploy-ai-bot: | |
| needs: [build-ai-bot, migrate-db] | |
| name: Deploy ai-bot to AWS ECS | |
| uses: cardstack/gh-actions/.github/workflows/ecs-deploy.yml@main | |
| secrets: inherit | |
| with: | |
| container-name: "boxel-ai-bot" | |
| environment: ${{ inputs.environment }} | |
| cluster: ${{ inputs.environment }} | |
| service-name: "boxel-ai-bot-${{ inputs.environment }}" | |
| image: ${{ needs.build-ai-bot.outputs.image }} | |
| wait-for-service-stability: false | |
| build-bot-runner: | |
| name: Build bot-runner Docker image | |
| uses: cardstack/gh-actions/.github/workflows/docker-ecr.yml@main | |
| secrets: inherit | |
| with: | |
| repository: "boxel-bot-runner-${{ inputs.environment }}" | |
| environment: ${{ inputs.environment }} | |
| dockerfile: "packages/bot-runner/Dockerfile" | |
| deploy-bot-runner: | |
| needs: [build-bot-runner, migrate-db] | |
| name: Deploy bot-runner to AWS ECS | |
| uses: cardstack/gh-actions/.github/workflows/ecs-deploy.yml@main | |
| secrets: inherit | |
| with: | |
| container-name: "boxel-bot-runner" | |
| environment: ${{ inputs.environment }} | |
| cluster: ${{ inputs.environment }} | |
| service-name: "boxel-bot-runner-${{ inputs.environment }}" | |
| image: ${{ needs.build-bot-runner.outputs.image }} | |
| wait-for-service-stability: false | |
| build-host: | |
| name: Build host | |
| uses: ./.github/workflows/build-host.yml | |
| secrets: inherit | |
| with: | |
| environment: ${{ inputs.environment }} | |
| deploy-host: | |
| name: Deploy host | |
| needs: [build-host] | |
| uses: ./.github/workflows/deploy-host.yml | |
| secrets: inherit | |
| with: | |
| environment: ${{ inputs.environment }} | |
| deploy-ui: | |
| name: Deploy boxel-ui | |
| if: inputs.environment == 'staging' | |
| uses: ./.github/workflows/deploy-ui.yml | |
| secrets: inherit | |
| with: | |
| environment: staging | |
| build-realm-server: | |
| name: Build realm-server Docker image | |
| uses: cardstack/gh-actions/.github/workflows/docker-ecr.yml@main | |
| secrets: inherit | |
| with: | |
| repository: "boxel-realm-server-${{ inputs.environment }}" | |
| environment: ${{ inputs.environment }} | |
| dockerfile: "packages/realm-server/realm-server.Dockerfile" | |
| build-args: | | |
| "realm_server_script=start:${{ inputs.environment }}" | |
| build-prerender-manager: | |
| name: Build prerender manager Docker image | |
| uses: cardstack/gh-actions/.github/workflows/docker-ecr.yml@main | |
| secrets: inherit | |
| with: | |
| repository: "boxel-prerender-manager-${{ inputs.environment }}" | |
| environment: ${{ inputs.environment }} | |
| dockerfile: "packages/realm-server/prerender-manager.Dockerfile" | |
| build-args: | | |
| "prerender_manager_script=start:prerender-manager" | |
| build-prerender: | |
| name: Build prerender Docker image | |
| uses: cardstack/gh-actions/.github/workflows/docker-ecr.yml@main | |
| secrets: inherit | |
| with: | |
| repository: "boxel-prerender-server-${{ inputs.environment }}" | |
| environment: ${{ inputs.environment }} | |
| dockerfile: "packages/realm-server/prerender.Dockerfile" | |
| build-args: | | |
| "prerender_script=start:prerender-${{ inputs.environment }}" | |
| build-worker: | |
| name: Build worker Docker image | |
| uses: cardstack/gh-actions/.github/workflows/docker-ecr.yml@main | |
| secrets: inherit | |
| with: | |
| repository: "boxel-worker-${{ inputs.environment }}" | |
| environment: ${{ inputs.environment }} | |
| dockerfile: "packages/realm-server/worker.Dockerfile" | |
| build-args: | | |
| "worker_script=start:worker-${{ inputs.environment }}" | |
| build-pg-migration: | |
| name: Build pg-migration Docker image | |
| uses: cardstack/gh-actions/.github/workflows/docker-ecr.yml@main | |
| secrets: inherit | |
| with: | |
| repository: "boxel-pg-migration-${{ inputs.environment }}" | |
| environment: ${{ inputs.environment }} | |
| dockerfile: "packages/postgres/Dockerfile" | |
| migrate-db: | |
| # use "deploy-host" and "build-realm-server" as deps so we can run | |
| # migrations at last possible moment in order to reduce the amount of time | |
| # that old code is pointing to new schema | |
| needs: [build-pg-migration, build-realm-server, deploy-host] | |
| name: Deploy and run DB migrations | |
| uses: cardstack/gh-actions/.github/workflows/ecs-deploy.yml@main | |
| secrets: inherit | |
| with: | |
| container-name: "boxel-pg-migration" | |
| environment: ${{ inputs.environment }} | |
| cluster: ${{ inputs.environment }} | |
| service-name: "boxel-pg-migration-${{ inputs.environment }}" | |
| image: ${{ needs.build-pg-migration.outputs.image }} | |
| timeout-minutes: 10 | |
| # The pg-migration container writes /tmp/migrations-complete after | |
| # `node-pg-migrate up` finishes (packages/postgres/Dockerfile) and its | |
| # HEALTHCHECK depends on that sentinel, so service-stability now waits | |
| # for migrations to actually finish rather than just for ECS to start | |
| # the task. This replaces the heuristic `sleep 180` post-migrate job. | |
| wait-for-service-stability: true | |
| deploy-prerender: | |
| name: Deploy prerender | |
| needs: [build-prerender] | |
| uses: cardstack/gh-actions/.github/workflows/ecs-deploy.yml@main | |
| secrets: inherit | |
| with: | |
| container-name: "boxel-prerender-server" | |
| environment: ${{ inputs.environment }} | |
| cluster: ${{ inputs.environment }} | |
| service-name: "boxel-prerender-server-${{ inputs.environment }}" | |
| image: ${{ needs.build-prerender.outputs.image }} | |
| timeout-minutes: 10 | |
| wait-for-service-stability: true | |
| deploy-prerender-manager: | |
| name: Deploy prerender manager | |
| needs: [build-prerender-manager, deploy-prerender] | |
| uses: cardstack/gh-actions/.github/workflows/ecs-deploy.yml@main | |
| secrets: inherit | |
| with: | |
| container-name: "boxel-prerender-manager" | |
| environment: ${{ inputs.environment }} | |
| cluster: ${{ inputs.environment }} | |
| service-name: "boxel-prerender-manager-${{ inputs.environment }}" | |
| image: ${{ needs.build-prerender-manager.outputs.image }} | |
| timeout-minutes: 10 | |
| wait-for-service-stability: true | |
| deploy-worker: | |
| name: Deploy worker | |
| needs: | |
| [build-worker, deploy-host, migrate-db, deploy-prerender-manager] | |
| uses: cardstack/gh-actions/.github/workflows/ecs-deploy.yml@main | |
| secrets: inherit | |
| with: | |
| container-name: "boxel-worker" | |
| environment: ${{ inputs.environment }} | |
| cluster: ${{ inputs.environment }} | |
| service-name: "boxel-worker-${{ inputs.environment }}" | |
| image: ${{ needs.build-worker.outputs.image }} | |
| timeout-minutes: 10 | |
| # The worker container's HEALTHCHECK curls `GET /` on the worker-manager, | |
| # which returns 200 only once `isReady = true` — i.e. all workers have | |
| # actually spawned (worker-manager.ts). Service-stability therefore | |
| # waits for true readiness, replacing the heuristic `sleep 180` we used | |
| # to do after deploy-worker. | |
| wait-for-service-stability: true | |
| deploy-realm-server: | |
| name: Deploy realm server | |
| needs: | |
| [deploy-worker, build-realm-server, deploy-host, migrate-db] | |
| uses: cardstack/gh-actions/.github/workflows/ecs-deploy.yml@main | |
| secrets: inherit | |
| with: | |
| container-name: "boxel-realm-server" | |
| environment: ${{ inputs.environment }} | |
| cluster: ${{ inputs.environment }} | |
| service-name: "boxel-realm-server-${{ inputs.environment }}" | |
| image: ${{ needs.build-realm-server.outputs.image }} | |
| timeout-minutes: 10 | |
| wait-for-service-stability: true | |
| post-deploy-realm-server: | |
| name: After realm server stable deployment | |
| needs: [deploy-realm-server] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Call post-deployment endpoint on realm server | |
| run: | | |
| if [ "${{ inputs.environment }}" = "production" ]; then | |
| URL="https://app.boxel.ai/_post-deployment" | |
| SECRET="${{ secrets.PRODUCTION_REALM_SERVER_SECRET }}" | |
| elif [ "${{ inputs.environment }}" = "staging" ]; then | |
| URL="https://realms-staging.stack.cards/_post-deployment" | |
| SECRET="${{ secrets.STAGING_REALM_SERVER_SECRET }}" | |
| else | |
| echo "Unknown environment: ${{ inputs.environment }}" | |
| exit 1 | |
| fi | |
| response=$(curl -s -w "\n%{http_code}" -X POST \ | |
| -H "Authorization: $SECRET" "$URL") | |
| response_body=$(echo "$response" | head -n -1) | |
| response_code=$(echo "$response" | tail -n 1) | |
| echo "Response body: $response_body" | |
| if [ "$response_code" != "200" ]; then | |
| echo "Post-deployment endpoint returned $response_code, expected 200" | |
| echo "Response body: $response_body" | |
| exit 1 | |
| fi | |
| apply-observability: | |
| # Push the observability/ package's dashboards/folders/data sources/alerts | |
| # into the production self-host Grafana as part of the deploy. The | |
| # called workflow's `environment: observability-production` gives the run | |
| # a production badge and a Grafana URL link, but doesn't gate (no required | |
| # reviewers — dispatching the manual deploy IS the approval). | |
| # | |
| # Production-only. Staging applies happen automatically on merge to main | |
| # via observability-apply-staging.yml — no need to re-run during a | |
| # manual staging deploy. | |
| name: Apply observability to production | |
| if: inputs.environment == 'production' | |
| needs: [post-deploy-realm-server] | |
| uses: ./.github/workflows/observability-apply-production.yml | |
| secrets: inherit | |
| finalize-deployment: | |
| name: Update GitHub deployment status | |
| needs: | |
| [ | |
| create-deployment, | |
| build-ai-bot, | |
| deploy-ai-bot, | |
| build-bot-runner, | |
| deploy-bot-runner, | |
| build-host, | |
| deploy-host, | |
| build-realm-server, | |
| build-prerender-manager, | |
| build-prerender, | |
| build-worker, | |
| build-pg-migration, | |
| migrate-db, | |
| deploy-prerender, | |
| deploy-prerender-manager, | |
| deploy-worker, | |
| deploy-realm-server, | |
| post-deploy-realm-server, | |
| apply-observability, | |
| ] | |
| if: github.event_name == 'workflow_dispatch' && always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Set deployment status | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| NEEDS: ${{ toJson(needs) }} | |
| DEPLOYMENT_ID: ${{ needs.create-deployment.outputs.deployment-id }} | |
| ENVIRONMENT_URL: ${{ needs.create-deployment.outputs.environment-url }} | |
| with: | |
| script: | | |
| const deploymentIdRaw = process.env.DEPLOYMENT_ID; | |
| if (!deploymentIdRaw) { | |
| core.info('No deployment id found; skipping status update.'); | |
| return; | |
| } | |
| const needs = JSON.parse(process.env.NEEDS); | |
| const results = Object.values(needs).map((job) => job.result); | |
| let state = 'success'; | |
| if (results.includes('failure')) { | |
| state = 'failure'; | |
| } else if (results.includes('cancelled')) { | |
| state = 'inactive'; | |
| } | |
| const environment_url = | |
| process.env.ENVIRONMENT_URL && | |
| process.env.ENVIRONMENT_URL.length > 0 | |
| ? process.env.ENVIRONMENT_URL | |
| : undefined; | |
| await github.rest.repos.createDeploymentStatus({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| deployment_id: Number(deploymentIdRaw), | |
| state, | |
| environment_url, | |
| log_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, | |
| description: | |
| state === 'success' | |
| ? 'Deployment finished' | |
| : 'Deployment finished with errors', | |
| }); |