8.2 Making CI Repeatable and Verifiable
Prerequisites: From Source to Runnable Artifact
Estimated time: 20 minutes
Reading note: this workflow demonstrates permissions and structure; replace every action SHA placeholder before use
You are ready to continue when: you can distinguish CI, continuous delivery, and continuous deployment, and review the minimum permission GitHub Actions workflow
You manually completed the build, verification, image creation, and cleanup sequence. The next day, a one-line change required every command again. Those repeatable steps are good automation targets; people must still design permissions and release conditions.
Keep the Three Terms Separate
- Continuous Integration (CI): Frequent merging of changes, with automatic compilation, testing, and verification.
- Continuous Delivery: Candidate versions are always in a deployable state, and production releases may require manual approval.
- Continuous Deployment: Once a gate is cleared, the release is automatically moved into production.
CD may refer to the latter two, so write the intended term in full.
Start with Read-Only CI
The following is a structural template. <FULL_COMMIT_SHA> must be replaced with the full commit SHA of the corresponding official action's release; GitHub notes that the full SHA is an immutable reference. This example keeps the placeholders visible for instruction; do not present it as directly runnable.
name: ci
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<FULL_COMMIT_SHA>
with:
persist-credentials: false
- uses: actions/setup-java@<FULL_COMMIT_SHA>
with:
distribution: temurin
java-version: "17"
cache: maven
- run: ./mvnw --batch-mode verifySave the complete workflow as .github/workflows/ci.yml. It assumes the repository contains an executable Maven Wrapper, POM, source, and tests. Checkout does not retain Git credentials, reducing their exposure to later build scripts. ubuntu-latest still changes; record the runner image and JDK version when diagnosing environment differences.
CI does not require packages: write and should not use production secrets. Untrusted code in a pull request may read the working directory, network, and runner status; be particularly cautious with forks and self-hosted runners.
Publish Images within a Separate Permission Boundary
Run this job only on protected branches or release tags, and grant the minimum necessary permissions at the job level:
permissions:
contents: read
packages: writeUse a short-lived GITHUB_TOKEN to log in to GHCR. Do not print the token in the logs. Tag the image with at least the commit SHA, and record the digest returned by the push. :latest can serve as a human-friendly alias, but deployment cannot rely solely on it for version identification.
- name: Push an already built image
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
IMAGE_NAME: ghcr.io/owner/repo
run: |
printf '%s' "$GH_TOKEN" | docker login ghcr.io \
--username "$GITHUB_ACTOR" --password-stdin
docker push "$IMAGE_NAME:sha-${GITHUB_SHA}"This steps fragment belongs to a publishing job and assumes an earlier step built and tagged that image. GitHub provides GITHUB_ACTOR and GITHUB_SHA, but GH_TOKEN must be explicitly mapped as shown; the platform does not create that alias. Replace owner/repo with an authorized lowercase image path. See GitHub Token usage. Do not write the real token into shell history or the repository.
Deployment Jobs Need Access Controls
Production jobs should reference a GitHub environment, such as production. Environments can restrict branches, require approvals, and expose environment secrets only after the protection rules pass. Use a concurrency group per target environment to prevent overlapping deployments. Set cancel-in-progress: false so an active release can finish; cancel deployments only when interruption and recovery are known to be safe. Concurrency is not a complete release queue: inspect how pending runs can be replaced in the GitHub concurrency documentation.
Before deployment, you must have: candidate digest, migration compatibility, rollback version, health check, observation window, and a responsible person. After deployment, first verify instance health, critical requests, and error rates before gradually increasing traffic.
Supply Chain Boundaries
- Pin third-party actions to a full commit SHA and verify that the SHA comes from the original repository.
GITHUB_TOKENgrants only the scope required for the job; permissions not listed becomenone.- Avoid using long-lived cloud credentials as a primary option; use OIDC to exchange for short-term credentials when the platform supports it.
- Artifact attestation can prove which workflow, commit, and event built the artifact, but it does not prove that the artifact is free of vulnerabilities.
- For production deployment, prefer to reference the digest and retain records of the commit, testing, SBOM, signatures, and approvals.
Common Failures
| Phenomenon | Check |
|---|---|
| workflow not triggered | path .github/workflows/, event, branch filtering, YAML |
| Passes locally, fails in CI | JDK/Maven version, locked dependencies, case sensitivity, time zone and uncommitted files |
| GHCR 403 | job's packages: write, package and repository access relationship, event source |
| secret is empty | secret scope, environment access control, fork event restrictions |
| two deployments overlap | environment and concurrency strategy |
GitHub environment, approvals, and concurrency strategies may be affected by the repository plan and account plan. Check GitHub deployment environment documentation before implementation; cloud credentials prioritize OIDC configuration guide.
Remote registry and production permissions vary by project. First, clarify artifacts, images, health checks, and rollback decisions locally.