Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
acb374e
fix(ci): coalesce Bandit pull request scans
seonghobae Sep 8, 2026
55d601c
chore(ci): defer Bandit concurrency to canonical owner
seonghobae Sep 8, 2026
c1286c1
merge(ci): adopt protected develop while preserving Bandit provenance
seonghobae Sep 8, 2026
b609301
experiment: add PR-scoped concurrency to Bandit scan
seonghobae Sep 9, 2026
f6ad4dd
experiment: scope Docker PR validation to job-level concurrency
seonghobae Sep 9, 2026
2aa1235
experiment: standardize PR concurrency groups to workflow-repository-…
seonghobae Sep 9, 2026
1032993
fix(ci): serialize Docker tag publishes per ref, never cancel releases
seonghobae Sep 9, 2026
22f3532
fix(ci): scope Docker PR validation concurrency per matrix component
seonghobae Sep 9, 2026
562338e
fix(ci): isolate matrix image publication groups
seonghobae Sep 9, 2026
55e0588
test(ci): pin queued release publication semantics
seonghobae Sep 9, 2026
12d8cac
fix(ci): queue release image publishes without cross-component eviction
seonghobae Sep 9, 2026
86ea640
test(ci): require whole-release image serialization
seonghobae Sep 9, 2026
c8b565e
fix(ci): serialize whole release image sets
seonghobae Sep 9, 2026
e4ab2ab
test(ci): follow release workflow decomposition
seonghobae Sep 9, 2026
2e09118
docs(ci): record release-set serialization decision
seonghobae Sep 9, 2026
d5b1e09
test(ci): reject prerelease latest publication
seonghobae Sep 9, 2026
0d45b53
fix(ci): fail closed on prerelease latest tags
seonghobae Sep 9, 2026
6e1378c
docs(ci): bind stable latest to release contract
seonghobae Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 8 additions & 191 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ concurrency:
# First-attempt PR validations share a generation key so a newer event
# supersedes only another first attempt. Manual reruns add their stable
# run_id and therefore cannot cancel, or be cancelled by, a newer PR event.
# Tag publication is unique per run and cancel-in-progress remains false.
# Tag runs remain unique here because release-set serialization is held by
# the reusable-workflow caller job for the full publication duration.
group: docker-publish-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name == 'pull_request' && github.run_attempt == 1 && 'first-attempt' || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

Expand Down Expand Up @@ -180,200 +181,16 @@ jobs:
sbom: false

publish_images:
name: publish ${{ matrix.component }} image
name: publish release image set
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
concurrency:
group: Build and Publish Docker Images-publish-set-${{ github.repository }}-${{ github.ref }}
queue: max
Comment thread
seonghobae marked this conversation as resolved.
cancel-in-progress: false
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- component: backend
image: ai_email_client-backend
dockerfile: Dockerfile
base_dockerfile: Dockerfile
context: .
build_args: |
BUILDKIT_INLINE_CACHE=1
- component: naruon
image: naruon
dockerfile: Dockerfile
base_dockerfile: Dockerfile
context: .
build_args: |
BUILDKIT_INLINE_CACHE=1
- component: frontend
image: ai_email_client-frontend
dockerfile: frontend/Dockerfile
base_dockerfile: frontend/Dockerfile
context: .
build_args: |
BUILDKIT_INLINE_CACHE=1
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit

- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Read release version
id: version
run: |
VERSION="$(cat VERSION)"
TAG_VERSION="${GITHUB_REF_NAME#v}"
if [ "$TAG_VERSION" != "$VERSION" ]; then
printf 'Tag %s does not match VERSION %s\n' "$GITHUB_REF_NAME" "$VERSION" >&2
exit 1
fi
printf 'version=%s\n' "$VERSION" >> "$GITHUB_OUTPUT"

- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

- name: Prepare OCI annotation values
id: oci
env:
BASE_DOCKERFILE: ${{ matrix.base_dockerfile }}
GIT_REF_NAME: ${{ github.ref_name }}
IMAGE_COMPONENT: ${{ matrix.component }}
IMAGE_NAME: ${{ matrix.image }}
REPOSITORY: ${{ github.repository }}
REVISION: ${{ github.sha }}
VERSION_VALUE: ${{ steps.version.outputs.version }}
run: |
version="${VERSION_VALUE:-$(cat VERSION)}"
created="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
vendor="${REPOSITORY%%/*}"
base_reference="$(awk 'toupper($1) == "FROM" { print $2; exit }' "$BASE_DOCKERFILE")"
if ! printf '%s\n' "$base_reference" | grep -Eq '^[A-Za-z0-9._/-]+:[A-Za-z0-9._-]+@sha256:[0-9a-f]{64}$'; then
printf '::error file=%s,line=1::Expected an exact tagged sha256 base pin; found %s\n' "$BASE_DOCKERFILE" "$base_reference"
exit 1
fi
base_digest="${base_reference##*@}"
base_repository="${base_reference%@*}"
case "$base_repository" in
*/*) base_name="$base_reference" ;;
*) base_name="docker.io/library/$base_reference" ;;
esac
case "$IMAGE_COMPONENT" in
frontend)
title="naruon frontend"
description="Naruon Next.js frontend runtime image"
;;
backend)
title="naruon backend"
description="Naruon FastAPI backend runtime image"
;;
*)
title="naruon"
description="Naruon combined FastAPI and Next.js runtime image"
;;
esac
{
printf 'created=%s\n' "$created"
printf 'authors=%s\n' "Seongho Bae"
printf 'url=https://github.com/%s/pkgs/container/%s\n' "$REPOSITORY" "$IMAGE_NAME"
printf 'documentation=https://github.com/%s#readme\n' "$REPOSITORY"
printf 'source=https://github.com/%s\n' "$REPOSITORY"
printf 'version=%s\n' "$version"
printf 'revision=%s\n' "$REVISION"
printf 'vendor=%s\n' "$vendor"
printf 'licenses=%s\n' "LicenseRef-Naruon-Proprietary"
printf 'ref_name=%s\n' "$GIT_REF_NAME"
printf 'title=%s\n' "$title"
printf 'description=%s\n' "$description"
printf 'base_digest=%s\n' "$base_digest"
printf 'base_name=%s\n' "$base_name"
} >> "$GITHUB_OUTPUT"

- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.image }}
tags: |
type=semver,pattern={{version}}
type=raw,value=${{ steps.version.outputs.version }}
type=raw,value=latest
labels: |
org.opencontainers.image.created=${{ steps.oci.outputs.created }}
org.opencontainers.image.authors=${{ steps.oci.outputs.authors }}
org.opencontainers.image.url=${{ steps.oci.outputs.url }}
org.opencontainers.image.documentation=${{ steps.oci.outputs.documentation }}
org.opencontainers.image.source=${{ steps.oci.outputs.source }}
org.opencontainers.image.version=${{ steps.oci.outputs.version }}
org.opencontainers.image.revision=${{ steps.oci.outputs.revision }}
org.opencontainers.image.vendor=${{ steps.oci.outputs.vendor }}
org.opencontainers.image.licenses=${{ steps.oci.outputs.licenses }}
org.opencontainers.image.ref.name=${{ steps.oci.outputs.ref_name }}
org.opencontainers.image.title=${{ steps.oci.outputs.title }}
org.opencontainers.image.description=${{ steps.oci.outputs.description }}
org.opencontainers.image.base.digest=${{ steps.oci.outputs.base_digest }}
org.opencontainers.image.base.name=${{ steps.oci.outputs.base_name }}

- name: Build and publish Docker image
id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
build-args: |
${{ matrix.build_args }}
OCI_IMAGE_CREATED=${{ steps.oci.outputs.created }}
OCI_IMAGE_AUTHORS=${{ steps.oci.outputs.authors }}
OCI_IMAGE_URL=${{ steps.oci.outputs.url }}
OCI_IMAGE_DOCUMENTATION=${{ steps.oci.outputs.documentation }}
OCI_IMAGE_SOURCE=${{ steps.oci.outputs.source }}
OCI_IMAGE_VERSION=${{ steps.oci.outputs.version }}
OCI_IMAGE_REVISION=${{ steps.oci.outputs.revision }}
OCI_IMAGE_VENDOR=${{ steps.oci.outputs.vendor }}
OCI_IMAGE_LICENSES=${{ steps.oci.outputs.licenses }}
OCI_IMAGE_REF_NAME=${{ steps.oci.outputs.ref_name }}
OCI_IMAGE_TITLE=${{ steps.oci.outputs.title }}
OCI_IMAGE_DESCRIPTION=${{ steps.oci.outputs.description }}
OCI_IMAGE_BASE_DIGEST=${{ steps.oci.outputs.base_digest }}
OCI_IMAGE_BASE_NAME=${{ steps.oci.outputs.base_name }}
provenance: true
sbom: true

- name: Record image digest
env:
IMAGE_COMPONENT: ${{ matrix.component }}
IMAGE_DIGEST: ${{ steps.build.outputs.digest }}
IMAGE_NAME: ${{ matrix.image }}
IMAGE_REGISTRY: ${{ env.REGISTRY }}
IMAGE_VERSION: ${{ steps.version.outputs.version }}
REPO_OWNER: ${{ github.repository_owner }}
run: |
{
printf '### %s image\n' "$IMAGE_COMPONENT"
printf -- '- Image: %s/%s/%s\n' "$IMAGE_REGISTRY" "$REPO_OWNER" "$IMAGE_NAME"
printf -- '- Version: %s\n' "$IMAGE_VERSION"
printf -- '- Digest: %s\n' "$IMAGE_DIGEST"
} >> "$GITHUB_STEP_SUMMARY"
uses: ./.github/workflows/docker-release-images.yml

deploy_preflight:
name: Detect AKS deploy configuration
Expand Down
Loading