Gates, secrets and deploys

What must pass before code moves forward, how credentials reach the pipeline safely, and how to roll a bad deploy back.

Gates

A gate is a condition that must hold before code moves forward. Gates are what make a pipeline a control rather than a suggestion.

GateStops
Required status checksMerging a pull request whose tests failed
Required reviewMerging without a second pair of eyes
Coverage thresholdDropping test coverage below an agreed floor
Dependency scanShipping a package with a known critical vulnerability
Environment approvalDeploying to production without a human sign-off
  deploy:
    needs: [test]
    runs-on: ubuntu-latest
    environment: production        # attaches protection rules and approvals
    if: ${{ github.ref == 'refs/heads/main' }}
    steps:
      - run: ./deploy.sh

Secrets

      - name: Push image
        env:
          REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
        run: |
          echo "$REGISTRY_TOKEN" | docker login registry.example.com --password-stdin
  • Values are masked in logs, but any command that prints its environment (env, set -x, an error dump) can still leak them. Prefer --password-stdin over a command-line argument.
  • Repository secrets are readable by any workflow on any branch. Scope sensitive values to an Environment and restrict which branches may use it.
  • Pull requests from forks do not receive secrets, which is exactly why the test job must not need them.
  • Short-lived credentials from OIDC federation beat long-lived cloud keys: the pipeline exchanges its identity token for temporary access, so there is nothing static to rotate or leak.
  • Rotate any credential that was ever echoed into a log, even if that log is private.

Add a secret scanner such as gitleaks to the pipeline itself. The most common leak is not a clever attacker but an API key committed by accident in a hurry.

Deploying and rolling back

#!/usr/bin/env bash
set -euo pipefail

PREVIOUS=$(kubectl get deployment/app -o jsonpath='{.spec.template.spec.containers[0].image}')

if ! kubectl set image deployment/app app=$IMAGE; then
  echo "could not start rollout" >&2
  exit 1
fi

if ! kubectl rollout status deployment/app --timeout=120s; then
  echo "rollout failed, returning to $PREVIOUS" >&2
  kubectl rollout undo deployment/app
  exit 1
fi
  • Deploy the artefact that passed the tests, identified by an immutable tag or digest.
  • Make rollback a single command, and trigger it automatically when the health check or the canary fails.
  • Record which commit is live: a release note, a Git tag, or an annotation on the deployment.
  • Database migrations are the hard part. Make them backwards compatible so the previous version still runs during a rollback.
⚠️
A deploy job that runs on every branch, or that fires straight after a merge with no environment protection, gives anyone who can push a path to production. Gate the environment and limit who can approve it.

FAQ

How do I stop a bad commit reaching production?
Require the test job for the same commit, pin the deployment to an immutable image digest, and add an environment approval. Then roll back automatically when the post-deploy health check fails.
Where should deployment credentials live?
In the CI system's secret store, or an environment scoped to the deploy job, never in the repository. Prefer short-lived OIDC credentials from your cloud provider so there is no static key to rotate.

A GitHub Actions workflow Pipeline stages and artefacts

Last refreshed 2026-09-18.