Skip to content

Deployment Gates: Reviewers and Wait Timers


The Fargate service from the last chapter deploys the instant you push to main — no pause, no second opinion, nobody watching. That’s exactly the kind of unattended deploy that made sense for practice, and exactly the kind you’d (might) never want for a real production target. This chapter goes back to the Environment protection rules introduced early in this section and actually turns them on.

Creating the production Environment

The workflow from the last chapter referenced an aws-ecs environment with no protection rules attached — GitHub created it automatically the first time that job ran. This chapter uses a differently-named environment, production, specifically so the gate only applies to the job you actually want gated:

  1. Settings → Environments → New environment, name it production.
  2. Under Deployment protection rules, enable Required reviewers — add yourself (or a teammate, if you’re working with one) as a reviewer. You can list up to 6 people or teams.
  3. Enable Wait timer and set it to something short but real for practice — 5 minutes is enough to prove the mechanism works without making every test run tedious.
  4. Save.

Note

A required reviewer can’t approve their own pull request-triggered deployment in some GitHub plans, but that restriction is about PR review, not this. Deployment approval is a separate permission — as the repo owner, you can approve your own gated deployment here even solo.

Splitting the Workflow

The single deploy job from the last chapter did everything — build, push, render, deploy — in one gated unit. That means the gate would block the build too, which wastes the reviewer’s wait pointlessly: there’s nothing to review yet until an image actually exists. Split it into two jobs instead, so the gate sits only in front of the part that actually touches production:

name: Deploy to ECS Fargate (production)

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    outputs:
      image: ${{ steps.build-image.outputs.image }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v6
        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: Log in to Amazon ECR
        id: ecr-login
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build and push image
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.ecr-login.outputs.registry }}
          ECR_REPOSITORY: devstuffs-hello
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker image build -t "$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" .
          docker image push "$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
          echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> "$GITHUB_OUTPUT"

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    environment:
      name: production
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v6
        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: Fetch current task definition
        run: |
          aws ecs describe-task-definition \
            --task-definition devstuffs-hello \
            --query taskDefinition \
            > task-definition.json

      - name: Render new image into task definition
        id: render
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: devstuffs-hello
          image: ${{ needs.build-and-push.outputs.image }}

      - name: Deploy to ECS
        uses: aws-actions/amazon-ecs-deploy-task-definition@v2
        with:
          task-definition: ${{ steps.render.outputs.task-definition }}
          service: devstuffs-service
          cluster: devstuffs-cluster
          wait-for-service-stability: true

Two things changed structurally from the last chapter, both worth noticing:

  • outputs: image: on the build-and-push job is what carries the resolved image reference across the job boundary. A step output only lives inside its own job by default — needs.build-and-push.outputs.image in the deploy job is how a downstream job reads a value an upstream job produced, which you haven’t needed until now because everything used to happen inside one job.
  • environment: production sits only on deploy, not on build-and-push. That’s deliberate: the build can run immediately and unattended, while only the step that actually touches the live service waits on a human.
    flowchart LR
    A[Push to main] --> B[build-and-push job<br/>runs immediately]
    B --> C{production environment<br/>required reviewer}
    C -- waiting --> C
    C -- approved --> D{wait timer<br/>5 minutes}
    D --> E[deploy job<br/>updates ECS service]
  

Watching a Gated Run

Push a commit and open the run from the Actions tab. build-and-push finishes normally, and deploy sits in a Waiting state instead of starting — the Actions run page shows a Review deployments button right there. Click it, select production, approve, and watch the run log show the wait timer counting down before the actual ECS update call fires.

Tip

If you don’t want to sit and watch a 5-minute timer every time you’re testing this chapter, GitHub also lets you reject a pending deployment from that same Review deployments panel — useful for confirming the gate genuinely blocks the job, without waiting out the timer on every single test push.

What This Buys You

Nothing about the ECS deploy step itself changed — same task definition render, same amazon-ecs-deploy-task-definition call. What changed is that a bad or accidental push to main no longer reaches the live service unattended: someone has to look at the run and consciously let it through, and even after they do, there’s a fixed window where a second thought (or a fast Cancel workflow) can still stop it before AWS ever sees the update. That’s the entire value of a deployment gate — it doesn’t make deploys safer by inspecting anything, it just guarantees a human is in the loop before the risky step runs.

Last updated on