ci: add tag-based multi-arch Docker publish to GHCR and Docker Hub

CHANGES
- Add GitHub Actions workflow to publish Docker images on tags
- Build multi-arch images with Buildx and QEMU across amd64, arm64
- Tag images using semver; push to GHCR and Docker Hub
- Set :latest only for highest semver tag via imagetools
- Gate patterns workflow steps on detected changes instead of failing
- Auto-detect GitHub owner and repo from git remote URL
- Remove hardcoded repository values in changelog release manager
- Normalize image names to lowercase for registry compatibility
- Enable GitHub Actions cache for faster Docker builds
- Add VS Code dictionary entries for Docker-related terms
This commit is contained in:
Kayvan Sylvan
2025-08-27 23:18:20 -07:00
parent e40d4e6623
commit 08eb48c2e7
5 changed files with 205 additions and 3 deletions

View File

@@ -0,0 +1,149 @@
name: Release Docker image on tag (GHCR + Docker Hub)
on:
push:
tags: ["v*"] # e.g., v1.4.300
permissions:
contents: read
packages: write # needed for GHCR with GITHUB_TOKEN
jobs:
build-and-push:
# Optional safety: only run from your fork
if: ${{ github.repository_owner == 'ksylvan' }}
runs-on: ubuntu-latest
outputs:
is_latest: ${{ steps.latest.outputs.is_latest }}
owner_lc: ${{ steps.vars.outputs.owner_lc }}
repo_lc: ${{ steps.vars.outputs.repo_lc }}
dockerhub_user_lc: ${{ steps.dh.outputs.user_lc }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # full history for tag comparisons
- name: Fetch all tags
run: git fetch --tags --force
# More reliable cross-builds
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Compute lowercase owner/repo for registry image names
- name: Compute image names
id: vars
run: |
OWNER="${GITHUB_REPOSITORY_OWNER}"
REPO="${GITHUB_REPOSITORY#*/}"
echo "owner_lc=${OWNER,,}" >> "$GITHUB_OUTPUT"
echo "repo_lc=${REPO,,}" >> "$GITHUB_OUTPUT"
# Lowercase Docker Hub username (belt & suspenders)
- name: Lowercase Docker Hub username
id: dh
run: echo "user_lc=${DOCKERHUB_USERNAME,,}" >> "$GITHUB_OUTPUT"
env:
DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
# Determine if the current tag is the highest vX.Y.Z (no pre-releases)
- name: Is this the latest semver tag?
id: latest
shell: bash
run: |
CTAG="${GITHUB_REF_NAME}"
LATEST="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n1)"
echo "current_tag=$CTAG" >> "$GITHUB_OUTPUT"
echo "latest_tag=$LATEST" >> "$GITHUB_OUTPUT"
if [[ "$CTAG" == "$LATEST" ]]; then
echo "is_latest=true" >> "$GITHUB_OUTPUT"
else
echo "is_latest=false" >> "$GITHUB_OUTPUT"
fi
# Login to GHCR (uses built-in GITHUB_TOKEN)
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Login to Docker Hub
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ steps.dh.outputs.user_lc }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Generate versioned tags/labels for BOTH registries (no :latest here)
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ steps.vars.outputs.owner_lc }}/${{ steps.vars.outputs.repo_lc }}
docker.io/${{ steps.dh.outputs.user_lc }}/${{ steps.vars.outputs.repo_lc }}
tags: |
type=ref,event=tag # v1.4.300
type=semver,pattern={{version}} # 1.4.300 (optional)
type=semver,pattern={{major}}.{{minor}} # 1.4 (optional)
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
- name: Build and push (multi-arch)
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Separate job to (re)point :latest — serialized to avoid races
move-latest:
needs: build-and-push
if: ${{ needs.build-and-push.outputs.is_latest == 'true' }}
runs-on: ubuntu-latest
# Only one "latest" move at a time; newer runs cancel older in-progress ones
concurrency:
group: latest-${{ github.repository }}
cancel-in-progress: true
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Tag :latest on GHCR
run: |
SRC="ghcr.io/${{ needs.build-and-push.outputs.owner_lc }}/${{ needs.build-and-push.outputs.repo_lc }}:${{ github.ref_name }}"
DST="ghcr.io/${{ needs.build-and-push.outputs.owner_lc }}/${{ needs.build-and-push.outputs.repo_lc }}:latest"
docker buildx imagetools create -t "$DST" "$SRC"
- name: Tag :latest on Docker Hub
run: |
SRC="docker.io/${{ needs.build-and-push.outputs.dockerhub_user_lc }}/${{ needs.build-and-push.outputs.repo_lc }}:${{ github.ref_name }}"
DST="docker.io/${{ needs.build-and-push.outputs.dockerhub_user_lc }}/${{ needs.build-and-push.outputs.repo_lc }}:latest"
docker buildx imagetools create -t "$DST" "$SRC"

View File

@@ -16,17 +16,23 @@ jobs:
fetch-depth: 0
- name: Verify Changes in Patterns Folder
id: check-changes
run: |
git fetch origin
if git diff --quiet HEAD~1 -- data/patterns; then
echo "No changes detected in patterns folder."
exit 1
echo "changes=false" >> $GITHUB_OUTPUT
else
echo "Changes detected in patterns folder."
echo "changes=true" >> $GITHUB_OUTPUT
fi
- name: Zip the Patterns Folder
if: steps.check-changes.outputs.changes == 'true'
run: zip -r patterns.zip data/patterns/
- name: Upload Patterns Artifact
if: steps.check-changes.outputs.changes == 'true'
uses: actions/upload-artifact@v4
with:
name: patterns