6.2 Artifact Promotion, Deployment Strategies, and Recoverable Delivery
The pipeline can generate artifacts. The remaining decision is who may promote one, when that promotion is allowed, and how the exact artifact moves from testing into production.
Continuous Delivery keeps software ready for release and permits production deployment on demand. Continuous Deployment goes further: every change that passes the pipeline is deployed automatically. Both build on CI; the difference is whether and when production deployment is automated, which depends on business requirements and risk tolerance.
Build Once, Promote by Artifact Identity
An error-prone workflow rebuilds independently in testing, pre-production, and production:
commit -> staging build
-> production build # Dependencies or environments may have changedA more reliable approach is:
commit
-> build + test
-> image@sha256:...
-> deploy to staging using the same digest
-> deploy to production using the same digestTags are readable but movable; the image digest uniquely identifies the actual content. latest cannot determine which build is actually running in production, making rollback targets ambiguous.
A Safer Image
This Dockerfile assumes an existing Java 21 Spring Boot application with the Gradle Groovy DSL: settings.gradle, build.gradle, gradlew, gradle/, and src/. Include any required gradle.properties, buildSrc, convention plugins, or extra modules in the build context as appropriate. Fix the executable JAR name in build.gradle so a plain JAR cannot also match the runtime COPY:
tasks.named('bootJar') {
archiveFileName = 'app.jar'
}FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /src
COPY --chmod=0755 gradlew ./
COPY settings.gradle build.gradle ./
COPY gradle ./gradle
COPY src ./src
RUN ./gradlew --no-daemon check bootJar
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /src/build/libs/app.jar /app/app.jar
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]The build stage runs check before the image is exported, and copies only the named executable JAR into a JRE image. check runs the checks configured by the project; it does not invent integration tests. Tests that need Docker or other external services require a suitable builder/test setup, so this compact image build assumes its checks can run inside the build stage. USER selects a numeric runtime identity; provide writable directories explicitly if the app needs them. EXPOSE documents a port but does not publish it. In addition, the actual repository should:
- Exclude Git, build artifacts, and local credentials using
.dockerignore; - Pin and regularly update the base image digest;
- Run as a non-root user;
- Scan OS packages and application dependencies;
- Never store secrets in
ARG,ENV, or image layers; - Define health checks and graceful shutdown behaviors for containers.
Pinning the digest provides reproducibility, but does not automatically deliver security patches; updates and revalidation must be scheduled or automated.
Pipeline Output Digest and Provenance Evidence
The following is a complete publication-workflow example for .github/workflows/publish-image.yml. Replace IMAGE_NAME with a lowercase GHCR path the repository can publish to; protect main and require verification before merge. It publishes an image and attestation, so run it only in the intended trusted repository. It does not deploy the application. The action tags are readability references; pin reviewed full SHAs for use.
name: publish-image
on:
push:
branches: [main]
permissions:
contents: read
env:
IMAGE_NAME: ghcr.io/example/tournament
jobs:
publish:
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: read
packages: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: push
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ env.IMAGE_NAME }}:${{ github.sha }}
- uses: actions/attest@v4
with:
subject-name: ${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: trueProvenance evidence links artifacts to repositories, workflows, and commits, but does not verify that the code is free of vulnerabilities. Only when the deployment or consumption side actually validates the provenance and confirms it originates from authorized repositories and workflows will supply chain policies take effect during deployment.
Environments Are Not Fixed to Three
Local, preview, integration, pre-production, and production are all possible environments. The number should be driven by validation needs, not by a rigid requirement for just dev/staging/prod.
Environments must maintain:
- Consistent artifact identity;
- Identical configuration schema and deployment mechanisms;
- Compatible versions of critical dependencies;
- Separate credentials, accounts, network boundaries, and data;
- Clear access, approval, and audit policies.
Pre-production cannot fully replicate production traffic, data distribution, or external failures. It serves as a validation layer, not a promise of being "exactly like production." Synthetic data should be prioritized; if production-derived data is absolutely necessary, it must undergo minimization, de-identification, access controls, retention policies, and compliance review.
GitHub Environments can configure approvals for production jobs, allow specific branches or tags, enforce wait times, and manage environment-level secrets. Credentials should never be exposed to jobs until they pass through security policies.
Deployment Strategies Address Different Risk Profiles
| Strategy | Approach | Primary Cost |
|---|---|---|
| Rolling | Gradually replace instances | Old and new versions coexist temporarily |
| Blue–Green | Switch traffic between two fully deployed environments | Higher resource costs, state management, and coordination during migration |
| Canary | Gradually expose a small portion of real traffic to the new version | Traffic allocation, metrics evaluation, and automated rollback logic |
| Feature Flag | Deploy code and conditionally enable features | Complexity in branch combinations, cleanup, and consistency across environments |
Feature flags decouple deployment from feature release, but they are not a universal solution for rollback: process startup failures, database corruption, and resource leaks still require deployment-level recovery.
Canary deployments must predefine decision windows and termination criteria, such as error rate, tail latency, business success rate, and resource saturation. Simply observing that "the container is alive" is insufficient to determine a version's health. Compare old and new versions over comparable routes and traffic populations, and require enough requests before judging a percentile or error rate. Ten requests with no errors give little evidence about a 99.9% success target. State whether a breach stops promotion, shifts traffic back, or also rolls back instances.
Database Schema Changes Must Allow Coexistence of Old and New Versions
When rolling back to an older application version, the database may have already advanced. Destructive migrations should follow the expand–migrate–contract pattern:
1. Expand: Add compatible fields or tables so older code can still run
2. Migrate: Populate data, optionally with dual reads/writes and validation checks
3. Switch: Transition new code to the updated schema
4. Contract: Remove old structure only after confirming no dependencies remainEach step must define idempotency, pause points, recovery procedures, and data validation. Combining operations like "DROP COLUMN" with code that depends on the old column in the same release removes a rollback path.
For a display_name replacement of name, adding a nullable column and backfilling once is insufficient while old writers keep changing name. Keep one authoritative representation during coexistence, synchronize later writes, resume backfill from checkpoints, and compare the two columns before switching reads. Retain the old column until old readers/writers are gone and the rollback window has closed. A successful backfill count alone does not establish that values remain current.
Rollback, Roll-Forward, and Fault Recovery
Before deploying, you must answer:
- What is the digest of the previous healthy artifact?
- Is the current database schema compatible with the old application?
- What triggers a rollback, by whom, or under what condition?
- How are configurations and feature flags restored?
- Can message consumers that have rolled back understand new events?
- How long does a rollback take, and is it regularly exercised?
If data has already undergone irreversible changes, a forward recovery after repair may be safer than application-level rollback. Pipelines cannot automatically determine this for your team, so recovery documentation must cover both paths.
Concurrency and Auditing
Deployments in the same environment should avoid overlap. Place this fragment under the deployment job, alongside its runs-on and steps fields:
concurrency:
group: deploy-production
cancel-in-progress: false
environment: productionThe concurrency group prevents overlap among matching jobs in the same repository; Environment supplies configured protection and secret boundaries. With the default queue, at most one run is pending, and a newer run can replace it even when cancel-in-progress is false. That flag protects the running job, not every queued deployment. If every version must deploy, configure an appropriate queue or release controller and verify the desired ordering. Other repositories and manual deployments are outside this lock; keep the job running until its external rollout has finished. Deployment records must at least include commit information, artifact digest, operator/workflow, target environment, timestamp, and outcome.
DevOps Is Not About Handing Off Operations Work to Developers
DevOps emphasizes collaboration among development, testing, security, platform, and operations teams around a shared delivery process. Automation aims to reduce manual drift and shorten recovery times, but in some systems, emergency access, manual approvals, and change management remain necessary. The key principles are minimal permissions, auditability, and the ability to simulate and rehearse scenarios.
Delivery Readiness Checklist
- Are PR and release workflows separated by privilege?
- Is a build created once and promoted by digest?
- Can external Actions, base images, and dependencies be traced for updates?
- Are production credentials using short-lived identities and minimal permissions?
- Are environment approval gates aligned with risk levels?
- Are database migrations forward- and backward-compatible?
- Does canary deployment have stop conditions and automated observability?
- Have both rollbacks and roll-forwards been exercised?
- Can deployment records answer "who deployed what to where"?