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.
| Gate | Stops |
|---|---|
| Required status checks | Merging a pull request whose tests failed |
| Required review | Merging without a second pair of eyes |
| Coverage threshold | Dropping test coverage below an agreed floor |
| Dependency scan | Shipping a package with a known critical vulnerability |
| Environment approval | Deploying 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.shSecrets
- 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-stdinover 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.
Related
A GitHub Actions workflow Pipeline stages and artefacts
Last refreshed 2026-09-18.