Files
sencho/.github/workflows/docker-publish.yml
T
Anso 3f9aeb229b ci: cut action-minute waste on bot PRs and non-docs pushes (#486)
Skip the backend, frontend, docker-validate, and e2e jobs on the
chore/refresh-screenshots auto-PR opened by the update-screenshots job.
Screenshot PRs only touch docs/images/ and cannot affect build, lint,
or test surfaces, so running the full PR CI suite on them burns ~6
action minutes per release for zero signal. This mirrors the existing
release-please--branches--main--components--sencho skip.

Also short-circuit sync-docs: when a push to main does not touch any
file under docs/, skip the clone, rsync, and commit steps. rsync
--delete would be a no-op anyway, but spinning up the runner and
cloning sencho-docs still burns ~30s per non-docs merge, which adds
up across multiple merges per day. Requires fetch-depth: 2 so the
detect step can diff HEAD~1 HEAD.

Finally, correct the scan-build comment in docker-publish.yml: the
push-build's amd64 leg reuses layers from the buildkit daemon's
in-memory cache (same job, same daemon), not from the shared registry
buildcache. The cache-from pull is a cold-start fallback for the
first-ever run.
2026-04-10 14:19:15 -04:00

143 lines
6.1 KiB
YAML

name: Build and Publish Docker Image
on:
push:
tags:
- 'v*'
workflow_dispatch:
concurrency:
group: docker-publish
cancel-in-progress: true
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
timeout-minutes: 30
# DOCKERHUB_USERNAME and DOCKERHUB_TOKEN live in the `production` environment,
# not repo-wide secrets. Any future workflow that tries to push to Docker Hub
# without declaring this environment will fail to resolve the credentials,
# which is exactly the blast-radius reduction we want. Adding a required
# reviewer to the environment in repo settings also turns every release into
# a manual-approval gate without any workflow change.
environment: production
permissions:
contents: read
# Required for cosign keyless signing via GitHub OIDC.
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Install cosign
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v6
with:
images: saelix/sencho
# On a v-tag push we publish:
# latest always points at the newest release
# X.Y.Z the immutable semver tag
# X.Y moving minor tag for users who want latest patch
# The pre-1.0 constraint hides the {{major}}-only tag until we ship 1.0,
# since on 0.x every minor is potentially breaking.
tags: |
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
# Build an amd64-only variant into the local daemon first so Trivy can
# scan the exact release artifact before it is tagged and pushed. This
# keeps vulnerable releases out of the `latest` and semver tags that
# users actually pull. Because the push-build step that follows runs in
# the same job against the same buildkit daemon, it reuses this build's
# layers from the daemon's in-memory cache (observed wall-time: the
# push-build typically finishes faster than the scan-build despite
# producing multi-arch output). The `cache-from` pull is just a
# cold-start fallback for the first-ever run or when the buildkit daemon
# is fresh; we intentionally do NOT write `cache-to` here because the
# push-build below writes a strictly better (multi-arch, mode=max)
# cache entry moments later. The tag lives in the `localhost/` namespace
# so a future `push: false` -> `push: true` mistake cannot publish it.
# Scanning only amd64 is a defensible proxy for the multi-arch release:
# distro package CVEs are arch-agnostic at the manifest level, and
# arch-specific container-relevant CVEs are extraordinarily rare.
- name: Build release image for pre-publish scan (amd64, loaded)
uses: docker/build-push-action@v7
with:
context: .
push: false
load: true
platforms: linux/amd64
tags: localhost/sencho:release-scan
cache-from: type=registry,ref=saelix/sencho:buildcache
- name: Re-scan release image for vulnerabilities (Trivy)
# Gates the release on the same HIGH/CRITICAL policy as the PR scan.
# Entries in .trivyignore at the repo root are honored so the acknowledged
# CVE list is the single source of truth across PR CI and release CI.
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
with:
image-ref: localhost/sencho:release-scan
exit-code: '1'
severity: 'CRITICAL,HIGH'
format: 'table'
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v7
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=saelix/sencho:buildcache
cache-to: type=registry,ref=saelix/sencho:buildcache,mode=max
# SBOM + provenance attestations are embedded as OCI referrers on the
# published image. Inspect with: docker buildx imagetools inspect <img>
sbom: true
provenance: mode=max
- name: Sign published image with cosign (keyless)
# Signs every tag produced by metadata-action using the ambient GitHub
# OIDC token. No private keys, no secrets. Users verify with:
# cosign verify saelix/sencho:<tag> \
# --certificate-identity-regexp "https://github.com/AnsoCode/Sencho/.*" \
# --certificate-oidc-issuer https://token.actions.githubusercontent.com
env:
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
# Build a single cosign invocation with every tag pinned to the digest.
# All tags resolve to the same manifest, so one call signs them all and
# saves N-1 Rekor/Fulcio round trips. On workflow_dispatch $TAGS is empty
# and the refs array stays empty, so we no-op cleanly.
refs=()
while IFS= read -r tag; do
[ -n "$tag" ] || continue
refs+=("${tag}@${DIGEST}")
done <<< "$TAGS"
if [ ${#refs[@]} -gt 0 ]; then
cosign sign --yes "${refs[@]}"
else
echo "No tags to sign (likely a workflow_dispatch run on a non-tag ref)."
fi