Repository navigation
deploy #124
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 | |
| # A merge to main reaches production with no manual step: once ci passes on main, the same | |
| # commit deploys to staging, and only if staging starts cleanly does it deploy to production. | |
| # Running this workflow by hand redeploys any commit on main (for example, to roll back). | |
| on: | |
| workflow_run: | |
| workflows: [ci] | |
| types: [completed] | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| sha: | |
| description: Full SHA of a commit on main to deploy to staging and production | |
| required: true | |
| permissions: | |
| contents: read | |
| # Newer merges include older ones: a waiting run is replaced by the newest, never interrupted. | |
| concurrency: | |
| group: deploy-main | |
| cancel-in-progress: false | |
| jobs: | |
| release: | |
| if: >- | |
| github.event_name == 'workflow_dispatch' || | |
| (github.event.workflow_run.conclusion == 'success' && | |
| github.event.workflow_run.event == 'push') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| sha: ${{ steps.pick.outputs.sha }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - id: pick | |
| env: | |
| DISPATCHED: ${{ inputs.sha }} | |
| FROM_CI: ${{ github.event.workflow_run.head_sha }} | |
| EVENT: ${{ github.event_name }} | |
| # The person who pressed Run or Re-run; github.actor keeps the original actor on a | |
| # re-run, so it cannot be what this checks. | |
| TRIGGERED_BY: ${{ github.triggering_actor }} | |
| ATTEMPT: ${{ github.run_attempt }} | |
| PROD_DEPLOYERS: ${{ vars.PROD_DEPLOYERS }} | |
| run: | | |
| # Anyone with write access can press Run workflow or Re-run; only maintainers may | |
| # start a production deploy by hand. Merges reach production through ci. | |
| if [[ "${EVENT}" == "workflow_dispatch" || "${ATTEMPT}" != "1" ]] && | |
| [[ ",${PROD_DEPLOYERS}," != *",${TRIGGERED_BY},"* ]]; then | |
| echo "${TRIGGERED_BY} is not in PROD_DEPLOYERS; ask a maintainer to run this deploy" >&2 | |
| exit 1 | |
| fi | |
| sha="${DISPATCHED:-${FROM_CI}}" | |
| if [[ ! "${sha}" =~ ^[0-9a-f]{40}$ ]]; then | |
| echo "a full commit SHA is required" >&2 | |
| exit 1 | |
| fi | |
| if ! git merge-base --is-ancestor "${sha}" origin/main; then | |
| echo "${sha} is not on main" >&2 | |
| exit 1 | |
| fi | |
| # An automatic deploy only ships main's current tip, so replaying an old run can't | |
| # roll production back. Rolling back is a maintainer's Run workflow with the SHA. | |
| if [[ "${EVENT}" == "workflow_run" && "${sha}" != "$(git rev-parse origin/main)" ]]; then | |
| echo "::notice::${sha} is no longer main's tip; the newer merge deploys instead" | |
| exit 0 | |
| fi | |
| echo "sha=${sha}" >> "${GITHUB_OUTPUT}" | |
| staging: | |
| needs: release | |
| if: needs.release.outputs.sha != '' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| environment: | |
| name: staging | |
| url: https://app.staging.tin.computer | |
| permissions: | |
| contents: read | |
| id-token: write | |
| statuses: write | |
| steps: | |
| - name: Report start | |
| if: true | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| SHA: ${{ needs.release.outputs.sha }} | |
| STATE: pending | |
| DESCRIPTION: Deploying to staging… | |
| TARGET_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| gh api --method POST "repos/${REPO}/statuses/${SHA}" -f state="${STATE}" \ | |
| -f context=deploy/staging -f description="${DESCRIPTION}" -f target_url="${TARGET_URL}" --silent | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.release.outputs.sha }} | |
| persist-credentials: false | |
| - uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 | |
| with: | |
| workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }} | |
| service_account: ${{ vars.GCP_DEPLOY_SERVICE_ACCOUNT }} | |
| - uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 | |
| - name: Deploy to staging | |
| id: deploy | |
| shell: bash | |
| env: | |
| TIN_LITE_DEPLOY_TARGET: staging | |
| TIN_LITE_GCP_PROJECT: ${{ vars.GCP_PROJECT }} | |
| TIN_LITE_STATIC_IP: ${{ vars.STAGING_STATIC_IP }} | |
| run: bash infra/deploy.sh 2>&1 | tee "${RUNNER_TEMP}/deploy.log" | |
| # A summary never fails a deploy. | |
| - name: Summary | |
| continue-on-error: true | |
| if: always() | |
| env: | |
| SHA: ${{ needs.release.outputs.sha }} | |
| OUTCOME: ${{ steps.deploy.outcome == 'success' && 'deployed' || 'failed' }} | |
| run: bash .github/scripts/deploy-summary.sh staging "${SHA}" "${RUNNER_TEMP}/deploy.log" "${OUTCOME}" | |
| - name: Report result | |
| if: always() | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| SHA: ${{ needs.release.outputs.sha }} | |
| STATE: ${{ steps.deploy.outcome == 'success' && 'success' || 'failure' }} | |
| DESCRIPTION: ${{ steps.deploy.outcome == 'success' && 'Deployed to staging' || 'Staging deploy failed; production was not deployed' }} | |
| TARGET_URL: ${{ steps.deploy.outcome == 'success' && 'https://app.staging.tin.computer' || format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id) }} | |
| run: | | |
| gh api --method POST "repos/${REPO}/statuses/${SHA}" -f state="${STATE}" \ | |
| -f context=deploy/staging -f description="${DESCRIPTION}" -f target_url="${TARGET_URL}" --silent | |
| production: | |
| needs: [release, staging] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| # Only main may use this environment; Google trusts only it for the production deployer. | |
| environment: | |
| name: production | |
| url: https://app.tin.computer | |
| permissions: | |
| contents: read | |
| id-token: write | |
| statuses: write | |
| steps: | |
| # "Re-run failed jobs" reuses the release job's result, so this job checks again itself. | |
| - name: Check who started this and that it is still main's tip | |
| id: guard | |
| env: | |
| SHA: ${{ needs.release.outputs.sha }} | |
| EVENT: ${{ github.event_name }} | |
| TRIGGERED_BY: ${{ github.triggering_actor }} | |
| ATTEMPT: ${{ github.run_attempt }} | |
| PROD_DEPLOYERS: ${{ vars.PROD_DEPLOYERS }} | |
| run: | | |
| if [[ "${EVENT}" == "workflow_dispatch" || "${ATTEMPT}" != "1" ]] && | |
| [[ ",${PROD_DEPLOYERS}," != *",${TRIGGERED_BY},"* ]]; then | |
| echo "${TRIGGERED_BY} is not in PROD_DEPLOYERS; ask a maintainer to run this deploy" >&2 | |
| exit 1 | |
| fi | |
| if [[ "${EVENT}" == "workflow_run" ]]; then | |
| tip="$(git ls-remote "https://github.com/${GITHUB_REPOSITORY}.git" refs/heads/main | cut -f1)" | |
| if [[ "${SHA}" != "${tip}" ]]; then | |
| echo "${SHA} is no longer main's tip (${tip}); not deploying it to production" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| - name: Report start | |
| if: steps.guard.outcome == 'success' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| SHA: ${{ needs.release.outputs.sha }} | |
| STATE: ${{ (vars.PROD_AUTO_DEPLOY != 'false' || github.event_name == 'workflow_dispatch') && 'pending' || 'error' }} | |
| DESCRIPTION: ${{ (vars.PROD_AUTO_DEPLOY != 'false' || github.event_name == 'workflow_dispatch') && 'Deploying to production…' || 'Paused because PROD_AUTO_DEPLOY is false' }} | |
| TARGET_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| gh api --method POST "repos/${REPO}/statuses/${SHA}" -f state="${STATE}" \ | |
| -f context=deploy/production -f description="${DESCRIPTION}" -f target_url="${TARGET_URL}" --silent | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.release.outputs.sha }} | |
| persist-credentials: false | |
| - uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 | |
| with: | |
| workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }} | |
| service_account: ${{ vars.GCP_DEPLOY_SERVICE_ACCOUNT }} | |
| - uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 | |
| - name: Deploy to production | |
| id: deploy | |
| if: vars.PROD_AUTO_DEPLOY != 'false' || github.event_name == 'workflow_dispatch' | |
| shell: bash | |
| env: | |
| TIN_LITE_DEPLOY_TARGET: prod | |
| TIN_LITE_GCP_PROJECT: ${{ vars.GCP_PROJECT }} | |
| TIN_LITE_STATIC_IP: ${{ vars.PRODUCTION_STATIC_IP }} | |
| run: bash infra/deploy.sh 2>&1 | tee "${RUNNER_TEMP}/deploy.log" | |
| # A summary never fails a deploy. | |
| - name: Summary | |
| continue-on-error: true | |
| if: always() && steps.deploy.outcome != 'skipped' | |
| env: | |
| SHA: ${{ needs.release.outputs.sha }} | |
| OUTCOME: ${{ steps.deploy.outcome == 'success' && 'deployed' || 'failed' }} | |
| run: bash .github/scripts/deploy-summary.sh production "${SHA}" "${RUNNER_TEMP}/deploy.log" "${OUTCOME}" | |
| - name: Report result | |
| if: always() && steps.guard.outcome == 'success' && steps.deploy.outcome != 'skipped' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| SHA: ${{ needs.release.outputs.sha }} | |
| STATE: ${{ steps.deploy.outcome == 'success' && 'success' || 'failure' }} | |
| DESCRIPTION: ${{ steps.deploy.outcome == 'success' && 'Deployed to production' || 'Production deploy failed; see the run log' }} | |
| TARGET_URL: ${{ steps.deploy.outcome == 'success' && 'https://app.tin.computer' || format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id) }} | |
| run: | | |
| gh api --method POST "repos/${REPO}/statuses/${SHA}" -f state="${STATE}" \ | |
| -f context=deploy/production -f description="${DESCRIPTION}" -f target_url="${TARGET_URL}" --silent | |
| - name: Report a paused production deploy | |
| if: vars.PROD_AUTO_DEPLOY == 'false' && github.event_name != 'workflow_dispatch' | |
| run: echo "::warning::PROD_AUTO_DEPLOY is false; production was not deployed" |