Files
sencho/.github/workflows/docker-publish.yml
dependabot[bot] 457cb04fd0 chore(deps): bump the all-actions group across 1 directory with 5 updates (#1672)
Bumps the all-actions group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `7.0.0` | `7.0.1` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.37.0` | `4.37.3` |
| [github/codeql-action/autobuild](https://github.com/github/codeql-action) | `4.37.0` | `4.37.3` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.37.0` | `4.37.3` |
| [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.37.0` | `4.37.3` |



Updates `actions/checkout` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1)

Updates `github/codeql-action/init` from 4.37.0 to 4.37.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/99df26d4f13ea111d4ec1a7dddef6063f76b97e9...e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81)

Updates `github/codeql-action/autobuild` from 4.37.0 to 4.37.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/99df26d4f13ea111d4ec1a7dddef6063f76b97e9...e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81)

Updates `github/codeql-action/analyze` from 4.37.0 to 4.37.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/99df26d4f13ea111d4ec1a7dddef6063f76b97e9...e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81)

Updates `github/codeql-action/upload-sarif` from 4.37.0 to 4.37.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/99df26d4f13ea111d4ec1a7dddef6063f76b97e9...e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-actions
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 14:06:28 -04:00

269 lines
13 KiB
YAML

name: Build and Publish Docker Image
# Release-only path: fires on v* tag pushes and publishes the public release
# (Docker Hub + GHCR, latest/semver/moving-minor, SBOM, cosign, GitHub Release).
# Integration images for pre-release testing are built on every push to main by
# docker-dev.yml and published as ghcr.io/studio-saelix/sencho-dev:dev; nothing
# here runs for those. Keep release artifacts out of the dev path and vice versa.
on:
push:
tags:
- 'v*'
workflow_dispatch:
concurrency:
# Ref-scoped so two different release tags (e.g. v0.86.4 and v0.86.5) cannot
# cancel each other. Reruns of the exact same ref still cancel the prior
# run for that ref, which is the cancel behavior we actually want.
group: docker-publish-${{ github.ref }}
cancel-in-progress: true
jobs:
push_to_registry:
name: Push Docker image to Docker Hub and GHCR
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: write
# Required for cosign keyless signing via GitHub OIDC and for uploading
# SBOM/VEX files to GitHub Releases via softprops/action-gh-release.
id-token: write
# Required to push the multi-arch image to ghcr.io/studio-saelix/sencho
# using the auto-provisioned GITHUB_TOKEN. Docker Hub credentials still
# come from the production environment above.
packages: write
steps:
- name: Check out the repo
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
# Computed once per job run. Feeds Dockerfile's APK_CACHE_BUST arg so
# the `apk upgrade` layer rebuilds at least once per calendar day, even
# when every other input to the layer is cached. See Dockerfile:115-122
# for the rationale. Both the pre-publish scan build and the multi-arch
# push build below consume this so Trivy scans a fresh layer.
- name: Compute daily apk cache bust value
id: apk-bust
run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT"
- name: Log in to Docker Hub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ghcr.io
# repository_owner resolves to a fixed value (studio-saelix) on every
# event type. github.actor varies (a maintainer on workflow_dispatch,
# github-actions[bot] on the release tag push) and is just a label;
# GITHUB_TOKEN is what actually authenticates.
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
with:
# Publish the same manifest under both registries so users on either
# platform can pull. Docker Hub remains primary for discoverability;
# GHCR mirrors at ghcr.io/studio-saelix/sencho with identical tags.
images: |
saelix/sencho
ghcr.io/studio-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@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
push: false
load: true
platforms: linux/amd64
tags: localhost/sencho:release-scan
cache-from: type=registry,ref=saelix/sencho:buildcache
build-args: |
APK_CACHE_BUST=${{ steps.apk-bust.outputs.date }}
- name: Re-scan release image for vulnerabilities (Trivy)
# Gates the release on the same HIGH/CRITICAL policy as the PR scan.
# CVEs suppressed via the OpenVEX document (trivy.yaml -> security/vex/
# sencho.openvex.json) are the single source of truth across PR CI and
# release CI.
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: localhost/sencho:release-scan
exit-code: '1'
severity: 'CRITICAL,HIGH'
format: 'table'
trivy-config: trivy.yaml
# Start the scanned image headless on the runner and poll /api/health
# until it returns 200. Catches entrypoint regressions, native module
# ABI breakage, and first-boot crashes on the exact artifact that the
# multi-arch push below will republish moments later. Intentionally
# placed BEFORE the publish step so a smoke failure does not move
# `latest` or the semver tags on Docker Hub. The health endpoint is
# public and returns before any DB or Docker-socket work, so the
# container only needs a free port, no env vars, and no volume mounts.
- name: Smoke test release image (pre-publish)
run: |
set -euo pipefail
# Verify source-built Docker CLI and Compose binaries are present and functional.
# Both checks run in one container to avoid double start-up overhead.
docker run --rm --entrypoint sh localhost/sencho:release-scan -c \
'docker --version && docker compose version'
# Not using --rm so that a crashed container sticks around long
# enough for `docker logs` in the trap to surface the stack trace.
# The trap force-removes it explicitly whether it is still running
# or already exited.
docker run -d --name sencho-smoke -p 1852:1852 localhost/sencho:release-scan
trap 'docker logs sencho-smoke 2>&1 || true; docker rm -f sencho-smoke >/dev/null 2>&1 || true' EXIT
for i in $(seq 1 30); do
if curl -fsS http://localhost:1852/api/health >/dev/null 2>&1; then
echo "Container healthy after ${i}s"
curl -s http://localhost:1852/api/health
exit 0
fi
sleep 1
done
echo "FAILED: /api/health did not become ready within 30s"
exit 1
- name: Build and push Docker image
id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # 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
build-args: |
APK_CACHE_BUST=${{ steps.apk-bust.outputs.date }}
# 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/studio-saelix/sencho/.*" \
# --certificate-oidc-issuer https://token.actions.githubusercontent.com
# Each ref is pinned to the immutable digest so signatures are bound to
# the exact manifest, not the mutable tag pointer.
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
- name: Generate CycloneDX SBOM
# syft scans the published manifest by digest for richer package
# extraction than the BuildKit-native SBOM. Also installs syft into
# PATH so the SPDX step below can reuse the OCI layer cache.
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.20.2
with:
image: saelix/sencho@${{ steps.build.outputs.digest }}
format: cyclonedx-json
output-file: sbom.cdx.json
upload-artifact: false
- name: Generate SPDX SBOM
# Reuses the syft binary and OCI layer cache from the prior step.
run: |
syft saelix/sencho@${{ steps.build.outputs.digest }} \
-o spdx-json=sbom.spdx.json
- name: Attest SBOMs and VEX with cosign (keyless)
# Attaches CycloneDX SBOM, SPDX SBOM, and OpenVEX document as signed
# OCI referrer attestations on the published digest, separately to each
# registry path. Attestations live next to the image manifest in the
# registry, so a verifier pulling from GHCR cannot resolve attestations
# written only to Docker Hub. Verification commands are documented in
# docs/operations/verifying-images.mdx.
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
for IMAGE_REF in \
"saelix/sencho@${DIGEST}" \
"ghcr.io/studio-saelix/sencho@${DIGEST}"
do
cosign attest --yes --predicate sbom.cdx.json --type cyclonedx "${IMAGE_REF}"
cosign attest --yes --predicate sbom.spdx.json --type spdxjson "${IMAGE_REF}"
cosign attest --yes --predicate security/vex/sencho.openvex.json --type openvex "${IMAGE_REF}"
done
- name: Upload SBOM and VEX to GitHub Release
# Fallback for consumers who do not use cosign; files are also
# available as signed OCI attestations via the attest step above.
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2.3.3.0.2
with:
files: |
sbom.cdx.json
sbom.spdx.json
security/vex/sencho.openvex.json