Continuous Delivery (CD) completes the engineering lifecycle by transforming verified source code into an immutable, deployable artifact (Immutable Artifact). In modern cloud environments and Kubernetes clusters, the de facto standard packaging format is the OCI (Open Container Initiative) container image.
Enterprise-grade release automation is anchored by five fundamental engineering pillars:
semantic-release tool analyzes commit messages following the Conventional Commits specification across the Git Abstract Syntax Tree (AST):
fix(...): triggers a PATCH increment (backward-compatible bug fix);feat(...): triggers a MINOR increment (backward-compatible feature addition);feat(...)! or a BREAKING CHANGE: footer: triggers a MAJOR increment (backward-incompatible contract or API modification).verifyConditions: verifies access credentials, GITHUB_TOKEN permissions, and registry tokens;analyzeCommits: classifies commit deltas since the preceding Git tag;generateNotes: produces automated Markdown release notes (CHANGELOG.md);prepare: updates version metadata in package.json and commits changes with a [skip ci] token;publish: creates a GitHub Release and tags the Git commit (vX.Y.Z).type=gha): Container builds on ephemeral runners lack a local Docker daemon cache by default. Configuring cache-from=type=gha and cache-to=type=gha,mode=max offloads intermediate image layers to the GitHub Actions Cache API, slashing incremental build times from 5 minutes down to 20 seconds.ghcr.io, authenticated via GITHUB_TOKEN) from public or customer distribution registries (Docker Hub docker.io). Images are pushed simultaneously with deterministic, immutable tagging: Git commit SHA (sha-a1b2c3d), exact SemVer (1.4.2), major floating tag (v1), and latest.The continuous delivery orchestration integrates commit analysis, Buildx multi-arch compilation, and federated registry publishing:
[ Merge PR to Release Branch: main ]
|
v
+-----------------------------+
| GitHub Actions CD Job |
| (Ephemeral VM: ubuntu-latest|
+-----------------------------+
|
v
+-------------------------------------------------------------------+
| 1. Semantic Release Lifecycle |
| AST Parse Commits: [feat, fix, perf, BREAKING CHANGE] |
| ├── Calculate Version Bump: 1.3.4 -> 1.4.0 (Minor Bump) |
| ├── Generate Artifact: CHANGELOG.md |
| ├── Push Git Tag: v1.4.0 |
| └── Export to Environment: NEW_VERSION=1.4.0 |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| 2. Initialize Docker Buildx & QEMU |
| Target Platforms: linux/amd64 + linux/arm64 |
| Restore Layer Cache: actions/cache (type=gha) |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| 3. Authenticate with Container Registries |
| ├── ghcr.io (GITHUB_TOKEN) |
| └── docker.io (DOCKERHUB_USERNAME / DOCKERHUB_TOKEN) |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| 4. Multi-Architecture Build & Push |
| Tags: |
| ├── ghcr.io/org/app:1.4.0 |
| ├── ghcr.io/org/app:sha-7f9e2b1 |
| ├── ghcr.io/org/app:latest |
| └── docker.io/company/app:1.4.0 |
| Publish OCI Image Manifest List (Fat Manifest) |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| 5. Publish GitHub Release & Trigger GitOps CD Webhooks |
| (ArgoCD / FluxCD / Kubernetes Operator Sync) |
+-------------------------------------------------------------------+output: 'standalone'). The image is tagged with the commit SHA and pushed to private GHCR. A GitOps operator (ArgoCD) monitors GHCR and reconciles the Kubernetes deployment automatically.cosign.Continuous Delivery is orchestrated through the synchronization of .releaserc.json, an optimized multi-stage Dockerfile, and a GitHub Actions workflow.
The .releaserc.json manifest configures plugins and version increment rules:
{
"branches": [
"main",
{ "name": "beta", "prerelease": true },
{ "name": "next", "prerelease": true }
],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits",
"releaseRules": [
{ "type": "feat", "release": "minor" },
{ "type": "fix", "release": "patch" },
{ "type": "perf", "release": "patch" },
{ "type": "refactor", "release": "patch" },
{ "type": "chore", "release": false },
{ "breaking": true, "release": "major" }
]
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
[
"@semantic-release/npm",
{
"npmPublish": false
}
],
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
"@semantic-release/github"
]
}The [skip ci] token in @semantic-release/git is essential: it prevents an infinite workflow execution loop when the bot commits CHANGELOG.md back to the release branch.
A minimal container image is constructed using a multi-stage Dockerfile running under an unprivileged user (non-root):
# -------------------------------------------------------------
# Stage 1: Base Alpine image and package manager
# -------------------------------------------------------------
FROM node:22-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare [email protected] --activate
# -------------------------------------------------------------
# Stage 2: Dependency installation
# -------------------------------------------------------------
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prefer-offline
# -------------------------------------------------------------
# Stage 3: Application compilation
# -------------------------------------------------------------
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
# Compile Next.js with output: 'standalone'
RUN pnpm build
# -------------------------------------------------------------
# Stage 4: Minimal production runner
# -------------------------------------------------------------
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Security: run under non-root user
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Copy static assets and standalone bundle
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]Analyze commits using Conventional Commits rules, generate CHANGELOG.md, and create annotated Git tag vX.Y.Z.
Install hardware architecture emulators and initialize a multi-platform Docker BuildKit instance.
Log into GitHub Container Registry (ghcr.io) and Docker Hub in parallel using secret access tokens.
Compile container image for linux/amd64 and linux/arm64 leveraging GitHub Actions Cache (type=gha).
Upload federated manifest list to registries with exact SemVer, commit SHA, and latest tags.
Below is the complete .github/workflows/release-and-publish.yml workflow executing automated version bumping, changelog synthesis, and multi-architecture image publishing.
name: Continuous Delivery & Release
on:
push:
branches:
- main
concurrency:
group: cd-release-production
cancel-in-progress: false
permissions:
contents: write
packages: write
issues: write
pull-requests: write
env:
REGISTRY_GHCR: ghcr.io
IMAGE_NAME_GHCR: ${{ github.repository }}
REGISTRY_DOCKERHUB: docker.io
IMAGE_NAME_DOCKERHUB: company/nextjs-storefront
jobs:
# -------------------------------------------------------------
# Phase 1: Semantic Versioning and Release Creation
# -------------------------------------------------------------
release:
name: Semantic Release & Versioning
runs-on: ubuntu-latest
outputs:
new_release_published: ${{ steps.semantic.outputs.new_release_published }}
new_release_version: ${{ steps.semantic.outputs.new_release_version }}
new_release_git_tag: ${{ steps.semantic.outputs.new_release_git_tag }}
steps:
- name: Checkout Code with Complete History
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
- name: Install Semantic Release Tooling
run: |
npm install -g \
semantic-release \
@semantic-release/commit-analyzer \
@semantic-release/release-notes-generator \
@semantic-release/changelog \
@semantic-release/git \
@semantic-release/github \
conventional-changelog-conventionalcommits
- name: Execute Semantic Release
id: semantic
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release
# -------------------------------------------------------------
# Phase 2: Multi-Architecture Build and Container Publishing
# -------------------------------------------------------------
publish-container:
name: Build & Push Multi-Arch Container
needs: [release]
if: needs.release.outputs.new_release_published == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup QEMU Architecture Emulators
uses: docker/setup-qemu-action@v3
with:
platforms: linux/amd64,linux/arm64
- name: Initialize Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Authenticate to GitHub Container Registry (GHCR)
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY_GHCR }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Authenticate to Docker Hub
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY_DOCKERHUB }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract Metadata and Generate Docker Tags
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY_GHCR }}/${{ env.IMAGE_NAME_GHCR }}
${{ env.REGISTRY_DOCKERHUB }}/${{ env.IMAGE_NAME_DOCKERHUB }}
tags: |
type=raw,value=${{ needs.release.outputs.new_release_version }}
type=raw,value=latest
type=sha,format=short,prefix=sha-
- name: Build & Push Multi-Architecture OCI Image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=maxThe @semantic-release/git plugin commits the updated CHANGELOG.md and package.json back to the main branch. If the commit message template lacks an explicit CI skip instruction, GitHub Actions interprets the commit as a new external push to main and launches a fresh pipeline run. This triggers an infinite recursive loop that drains action minute quotas.
// INCORRECT: Commit template lacks CI skip marker
{
"plugins": [
["@semantic-release/git", { "message": "release: update changelog" }]
]
}
// CORRECT: Adding [skip ci] to the commit message
{
"plugins": [
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}Deploying containers using only the :latest tag without pinning a specific SemVer (1.4.2) or Git SHA (sha-7a1b8c). In production incidents, Kubernetes cannot execute a deterministic rollback because :latest has already been overwritten by the failing deployment.
# INCORRECT: Publishing only the mutable latest tag
tags: my-org/app:latest
# CORRECT: Publishing immutable version tags alongside floating tags
tags: |
type=raw,value=${{ needs.release.outputs.new_release_version }}
type=sha,format=short,prefix=sha-
type=raw,value=latestBuilding linux/arm64 images on standard x86 runners causes RUN pnpm build to execute under CPU instruction emulation via QEMU. Emulated JavaScript and Rust compilation runs 5–10x slower (builds take 25 minutes instead of 2 minutes). The performant solution uses native ARM GitHub Actions runners (runs-on: ubuntu-24.04-arm) or builds on the host architecture before copying assets into Docker.
Passing private tokens via ARG NPM_TOKEN followed by RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc. Even if the file is deleted in a subsequent instruction, the secret remains readable in the image metadata layer via docker history.
# INCORRECT: Secret persisted in image layer metadata
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
# CORRECT: Mounting ephemeral BuildKit secret without writing to image layers
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc pnpm install --frozen-lockfileAdopt commitlint and Husky to enforce Conventional Commits formatting across all engineering teams.
Configure Next.js output: standalone, multi-stage builds, non-root user execution, and layer isolation.
Create .releaserc.json configuration automating SemVer calculation, CHANGELOG.md authoring, and Git tag creation.
Set up QEMU emulators and Buildx to compile for linux/amd64 and linux/arm64 with GitHub Actions layer caching.
Authenticate to GHCR and Docker Hub, publish OCI Image Manifest Lists, and trigger GitOps deployment pipelines.