mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.6.0 to 4.7.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/dbcb813823bdd20940b903addbd779551569679f...81c6f93d4bc991e510b342a00b419b3bb4efc5c6) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
505 lines
24 KiB
YAML
505 lines
24 KiB
YAML
name: Build & Publish Docker Image
|
|
|
|
# v1.4.34 IW-A — release pipeline fires on tag pushes only. Earlier
|
|
# releases triggered the workflow twice on each cut: the squash-merge
|
|
# to `main` and the corresponding `v*` tag push landed within a second
|
|
# of each other and each kicked off its own build. Both runs ended
|
|
# green and produced the same image at the same SHA with overlapping
|
|
# tag sets (the main run added `latest`, the tag run added the
|
|
# semver tags), but the duplication doubled GHCR CI minutes for
|
|
# every release. Dropping `push.branches: [main]` collapses the
|
|
# pipeline to a single run per release tag. The `latest` rule in
|
|
# the metadata-action (`type=raw,value=latest,...`) now keys on
|
|
# the v* tag presence so each release still refreshes the `latest`
|
|
# alias in the same run. PR + workflow_dispatch triggers stay so
|
|
# the image still builds on PR validation and manual reruns.
|
|
on:
|
|
push:
|
|
tags: ["v*"]
|
|
pull_request:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
|
|
# Default for any job that doesn't declare its own block; both jobs
|
|
# below elevate to packages:write explicitly.
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
REGISTRY: ghcr.io
|
|
IMAGE_NAME: ${{ github.repository }}
|
|
|
|
# Cancel in-progress runs when a new commit is pushed to the same ref.
|
|
# Different ref types (tag vs PR head) don't share a group, so a tag
|
|
# build and a concurrent PR build won't cancel each other.
|
|
concurrency:
|
|
group: docker-publish-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
release-policy:
|
|
name: Validate release policy
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 5
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
steps:
|
|
- name: Checkout full history
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Validate release policy
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
|
|
echo "::notice::Pull-request image validation does not publish release tags."
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$GITHUB_REF_TYPE" != "tag" ] || [[ ! "$GITHUB_REF_NAME" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
|
|
echo "::error::Release ref '$GITHUB_REF_NAME' is not a strict SemVer tag (expected vMAJOR.MINOR.PATCH with no leading zeroes or prerelease suffix)." >&2
|
|
exit 1
|
|
fi
|
|
|
|
package_version="$(node -p "require('./package.json').version")"
|
|
tag_version="${GITHUB_REF_NAME#v}"
|
|
if [ "$package_version" != "$tag_version" ]; then
|
|
echo "::error::package.json version '$package_version' does not match release tag '$GITHUB_REF_NAME'." >&2
|
|
exit 1
|
|
fi
|
|
|
|
git fetch --no-tags origin main:refs/remotes/origin/main
|
|
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
|
|
echo "::error::Release commit $GITHUB_SHA is not an ancestor of origin/main." >&2
|
|
exit 1
|
|
fi
|
|
|
|
successful_quality_runs="$(
|
|
gh api "repos/$GITHUB_REPOSITORY/actions/runs?head_sha=$GITHUB_SHA&event=push&status=success&per_page=100" \
|
|
--jq '[.workflow_runs[] | select(.name == "Security & Quality" and .head_branch == "main")] | length'
|
|
)"
|
|
if [ "$successful_quality_runs" -lt 1 ]; then
|
|
echo "::error::Release commit $GITHUB_SHA has no successful prior Security & Quality push run on main." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Release policy passed for $GITHUB_REF_NAME at $GITHUB_SHA."
|
|
|
|
# ── Stage 1 ────────────────────────────────────────────────────────
|
|
# Build each architecture on its own native GitHub-hosted runner so
|
|
# Next.js's static-page workers run on real silicon instead of QEMU
|
|
# user-mode emulation. The v1.4.16 C5 attempt at
|
|
# `platforms: linux/amd64,linux/arm64` on a single amd64 runner
|
|
# deterministically SIGILL-crashed Next 15's static export under
|
|
# qemu-arm64 (`Next.js build worker exited with code: null and signal:
|
|
# SIGILL`); v8's optimising tier emits instructions qemu cannot
|
|
# reliably translate. ubuntu-24.04-arm is a standard GitHub-hosted
|
|
# arm64 runner, free for public repositories, and native — no qemu in
|
|
# the loop.
|
|
#
|
|
# Each platform pushes its layer set by digest only (`outputs: push-by-digest=true`).
|
|
# The `merge` job below stitches the two digests into a multi-arch
|
|
# manifest in a third step, so consumers see a single tag that
|
|
# auto-selects on `docker pull` per local platform.
|
|
build:
|
|
needs: [release-policy]
|
|
name: Build ${{ matrix.platform }}
|
|
runs-on: ${{ matrix.runner }}
|
|
# 25 min wall clock — native amd64 build finished inside 12 min
|
|
# at v1.4.24; the native arm64 path is comparable. The qemu-arm64
|
|
# hang at 47 min on `[linux/amd64 runner 2/14]` is what motivated
|
|
# this matrix in the first place.
|
|
timeout-minutes: 25
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- platform: linux/amd64
|
|
runner: ubuntu-24.04
|
|
cache_scope: amd64
|
|
- platform: linux/arm64
|
|
runner: ubuntu-24.04-arm
|
|
cache_scope: arm64
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
|
|
|
# GHCR rejects repository refs with uppercase letters. `IMAGE_NAME`
|
|
# is derived from `github.repository`, which preserves the case of
|
|
# the GitHub org/user (`MBombeck/HealthLog`). `docker/metadata-action`
|
|
# lowercases its `images:` input transparently, so the legacy
|
|
# single-job workflow worked; the multi-arch matrix now feeds
|
|
# `outputs:` of build-push-action directly and needs the lowercase
|
|
# form there too. Resolve it once at the top of the job and reuse
|
|
# via $GITHUB_ENV.
|
|
- name: Lowercase image ref
|
|
run: |
|
|
echo "IMAGE_REF=${REGISTRY}/${IMAGE_NAME,,}" >> "$GITHUB_ENV"
|
|
|
|
# Skip login on PRs from forks — they don't have package:write.
|
|
# Image is built but not pushed in that case.
|
|
- name: Log in to GitHub Container Registry
|
|
if: github.event_name != 'pull_request'
|
|
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Compute image labels
|
|
id: meta
|
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
|
with:
|
|
images: ${{ env.IMAGE_REF }}
|
|
labels: |
|
|
org.opencontainers.image.title=HealthLog
|
|
org.opencontainers.image.description=Self-hosted health-tracking PWA
|
|
org.opencontainers.image.licenses=PolyForm-Noncommercial-1.0.0
|
|
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
|
|
|
# v1.4.27 B3 — GeoLite2-City + GeoLite2-ASN databases are too large
|
|
# (~80 MB combined) to vendor in git. `scripts/fetch-geolite2.sh`
|
|
# downloads both editions into `assets/geolite2/` so the Dockerfile
|
|
# COPY at line 78 picks them up.
|
|
#
|
|
# Two intentional paths:
|
|
# - `MAXMIND_LICENSE_KEY` unset → emit a `::warning::`, drop a
|
|
# `.empty` marker into the assets directory so the Dockerfile
|
|
# COPY still has a non-empty source, and continue cleanly. The
|
|
# runtime resolver in `src/lib/geo.ts` detects the marker on
|
|
# first lookup and emits a one-shot admin notification — the
|
|
# maintainer hears about the gap from the running app rather
|
|
# than from a red CI badge that blocks every release.
|
|
# - `MAXMIND_LICENSE_KEY` set → run `fetch-geolite2.sh` and
|
|
# fail the build if it errors. The "Verify GeoLite2 databases"
|
|
# step below adds a belt-and-braces assertion that both MMDB
|
|
# files landed in `assets/geolite2/` before the Dockerfile
|
|
# COPY reads the directory, so v1.4.28-era silent fallback
|
|
# (offlineGeoEnabled: false on both nodes after a tag build)
|
|
# can no longer ship.
|
|
#
|
|
# To enable the offline feature, set the secret under repo
|
|
# Settings → Secrets and variables → Actions and redeploy.
|
|
- name: Fetch GeoLite2 databases
|
|
env:
|
|
MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }}
|
|
run: |
|
|
if [ -z "$MAXMIND_LICENSE_KEY" ]; then
|
|
echo "::warning::MAXMIND_LICENSE_KEY secret is unset. The image will ship without the offline GeoLite2 databases; the runtime resolver continues to fall back to the online ipwho.is provider and will send a one-shot admin notification on first use. To enable the offline tier, set MAXMIND_LICENSE_KEY under repo Settings → Secrets and variables → Actions."
|
|
mkdir -p assets/geolite2
|
|
touch assets/geolite2/.empty
|
|
exit 0
|
|
fi
|
|
./scripts/fetch-geolite2.sh
|
|
|
|
# Build-time assertion: with the license key present, both MMDB
|
|
# files must land in `assets/geolite2/` before the Dockerfile
|
|
# COPY at line 78 picks them up. The fetch script already fails
|
|
# loud on a curl / tarball error, but a stale `.empty` marker
|
|
# from a previous keyless run or a workspace-cache surprise could
|
|
# otherwise survive into the image — this step is the operator
|
|
# guarantee that "key set" really means "MMDBs in the image".
|
|
#
|
|
# `secrets.*` is not available in step-level `if:` expressions,
|
|
# so we surface the presence flag through env and gate inline.
|
|
- name: Verify GeoLite2 databases
|
|
env:
|
|
MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }}
|
|
run: |
|
|
if [ -z "$MAXMIND_LICENSE_KEY" ]; then
|
|
echo "MAXMIND_LICENSE_KEY unset — verification skipped (offline geo tier intentionally absent on this build)."
|
|
exit 0
|
|
fi
|
|
missing=()
|
|
for mmdb in GeoLite2-City.mmdb GeoLite2-ASN.mmdb; do
|
|
if [ ! -s "assets/geolite2/$mmdb" ]; then
|
|
missing+=("$mmdb")
|
|
fi
|
|
done
|
|
if [ "${#missing[@]}" -gt 0 ]; then
|
|
echo "::error::MAXMIND_LICENSE_KEY is set but the following GeoLite2 MMDB files are missing or empty in assets/geolite2/: ${missing[*]}. The Dockerfile would COPY them into /opt/geolite2/ in the final image, leaving the offline geo tier disabled at runtime. Investigate the previous 'Fetch GeoLite2 databases' step before retrying."
|
|
exit 1
|
|
fi
|
|
if [ -f assets/geolite2/.empty ]; then
|
|
echo "::error::Stale .empty marker present in assets/geolite2/ despite MAXMIND_LICENSE_KEY being set. The runtime resolver would treat the image as missing the offline tier. Investigate the previous 'Fetch GeoLite2 databases' step before retrying."
|
|
exit 1
|
|
fi
|
|
city_size="$(stat -c%s assets/geolite2/GeoLite2-City.mmdb)"
|
|
asn_size="$(stat -c%s assets/geolite2/GeoLite2-ASN.mmdb)"
|
|
echo "::notice::GeoLite2 MMDBs verified — GeoLite2-City.mmdb ${city_size} bytes; GeoLite2-ASN.mmdb ${asn_size} bytes."
|
|
|
|
# Build provenance for /api/version: the short commit SHA and an
|
|
# ISO-8601 build timestamp travel into the image as build-args
|
|
# (NEXT_PUBLIC_APP_BUILD_SHA / NEXT_PUBLIC_APP_BUILT_AT) so the
|
|
# deploy runbook can verify which commit a running container
|
|
# actually carries — the `:latest` pull is the source of truth,
|
|
# not the queued deploy status. 12 hex chars is plenty to be
|
|
# unambiguous in this repository.
|
|
- name: Resolve build provenance
|
|
id: provenance
|
|
run: |
|
|
echo "build_sha=${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT"
|
|
echo "built_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
|
|
|
# Per-ref + per-platform gha cache scope. Tag builds warm-start
|
|
# off main's last good layer set for the same platform; tag and
|
|
# main builds at the same SHA do not contend on writes, and
|
|
# amd64 / arm64 caches stay isolated from each other (a stale
|
|
# arm64 layer never poisons an amd64 build).
|
|
- name: Build & push by digest
|
|
id: build
|
|
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
|
with:
|
|
context: .
|
|
file: ./Dockerfile
|
|
platforms: ${{ matrix.platform }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
# v1.4.43 B11 — thread the tag ref into the build as
|
|
# NEXT_PUBLIC_APP_VERSION. The Dockerfile picks it up as an
|
|
# ARG/ENV and /api/version reads it at runtime. The build-arg
|
|
# becomes part of the BuildKit layer cache key, so a release
|
|
# tag bump invalidates every downstream layer that depends on
|
|
# it (`pnpm build`) and the prior release's version string
|
|
# can never survive a cache hit into the new image.
|
|
build-args: |
|
|
NEXT_PUBLIC_APP_VERSION=${{ github.ref_name }}
|
|
NEXT_PUBLIC_APP_BUILD_SHA=${{ steps.provenance.outputs.build_sha }}
|
|
NEXT_PUBLIC_APP_BUILT_AT=${{ steps.provenance.outputs.built_at }}
|
|
outputs: |
|
|
type=image,name=${{ env.IMAGE_REF }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
|
cache-from: |
|
|
type=gha,scope=build-${{ matrix.cache_scope }}-${{ github.ref_name }}
|
|
type=gha,scope=build-${{ matrix.cache_scope }}-main
|
|
cache-to: type=gha,mode=max,scope=build-${{ matrix.cache_scope }}-${{ github.ref_name }}
|
|
provenance: true
|
|
sbom: true
|
|
|
|
# Persist the per-platform digest so the `merge` job can stitch
|
|
# both into a single multi-arch manifest. One artifact per
|
|
# platform avoids upload races.
|
|
- name: Export digest
|
|
if: github.event_name != 'pull_request'
|
|
run: |
|
|
mkdir -p /tmp/digests
|
|
digest='${{ steps.build.outputs.digest }}'
|
|
touch "/tmp/digests/${digest#sha256:}"
|
|
|
|
- name: Upload digest artifact
|
|
if: github.event_name != 'pull_request'
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
with:
|
|
name: digest-${{ matrix.cache_scope }}
|
|
path: /tmp/digests/*
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
|
|
# ── Stage 2 ────────────────────────────────────────────────────────
|
|
# Stitch the per-platform digests into a single multi-arch manifest
|
|
# under all the tags `docker/metadata-action` would normally apply.
|
|
# `docker buildx imagetools create` is a manifest-only operation —
|
|
# no layers are re-uploaded, the existing per-platform digests are
|
|
# referenced in place. This is the same end state as the
|
|
# single-buildx `platforms:` path would produce, just assembled in
|
|
# the registry instead of on one cross-emulated runner.
|
|
merge:
|
|
name: Merge multi-arch manifest
|
|
runs-on: ubuntu-24.04
|
|
if: github.event_name != 'pull_request'
|
|
needs: [build]
|
|
timeout-minutes: 10
|
|
outputs:
|
|
image_ref: ${{ steps.published.outputs.image_ref }}
|
|
image_tag: ${{ steps.published.outputs.image_tag }}
|
|
image_digest: ${{ steps.published.outputs.image_digest }}
|
|
build_sha: ${{ steps.published.outputs.build_sha }}
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
# v1.16.4 — keyless image signing. The job requests an OIDC
|
|
# token from GitHub's issuer; cosign exchanges it for a
|
|
# short-lived Fulcio certificate bound to this workflow identity
|
|
# and records the signature in the Rekor transparency log. No
|
|
# long-lived signing key exists anywhere in this pipeline.
|
|
id-token: write
|
|
|
|
steps:
|
|
- name: Download digest artifacts
|
|
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
with:
|
|
path: /tmp/digests
|
|
pattern: digest-*
|
|
merge-multiple: true
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
|
|
|
# GHCR requires the repository ref to be lowercase. See the same
|
|
# step in the `build` job above for the longer rationale.
|
|
- name: Lowercase image ref
|
|
run: |
|
|
echo "IMAGE_REF=${REGISTRY}/${IMAGE_NAME,,}" >> "$GITHUB_ENV"
|
|
|
|
- name: Log in to GitHub Container Registry
|
|
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Compute image tags & labels
|
|
id: meta
|
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
|
with:
|
|
images: ${{ env.IMAGE_REF }}
|
|
tags: |
|
|
type=ref,event=branch
|
|
type=ref,event=pr
|
|
type=semver,pattern={{version}}
|
|
type=semver,pattern={{major}}.{{minor}}
|
|
type=match,pattern=v(.+),group=1
|
|
type=ref,event=tag
|
|
# v1.4.34 IW-A — release pipeline fires on `v*` tag pushes
|
|
# only (the `push.branches: [main]` trigger was dropped to
|
|
# eliminate the per-release double-build). `is_default_branch`
|
|
# is false for tag refs, so the prior condition stopped
|
|
# refreshing the `latest` alias once main pushes no longer
|
|
# trigger the workflow. Enable on every release tag that
|
|
# does not carry a pre-release suffix (`-rc.1`, `-beta.2`,
|
|
# etc.) so each stable release rotates the `latest` alias
|
|
# while pre-release tags stay off it.
|
|
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') }}
|
|
type=sha,prefix=sha-,format=short
|
|
labels: |
|
|
org.opencontainers.image.title=HealthLog
|
|
org.opencontainers.image.description=Self-hosted health-tracking PWA
|
|
org.opencontainers.image.licenses=PolyForm-Noncommercial-1.0.0
|
|
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
|
|
|
- name: Create multi-arch manifest
|
|
working-directory: /tmp/digests
|
|
run: |
|
|
# shellcheck disable=SC2046,SC2086
|
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
|
$(printf '${{ env.IMAGE_REF }}@sha256:%s ' *)
|
|
|
|
- name: Resolve published image
|
|
id: published
|
|
env:
|
|
IMAGE_TAG: ${{ github.ref_name }}
|
|
run: |
|
|
image_digest="$(docker buildx imagetools inspect "${IMAGE_REF}:${IMAGE_TAG}" --format '{{json .Manifest.Digest}}' | tr -d '"')"
|
|
if [[ ! "$image_digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then
|
|
echo "::error::Could not resolve a valid manifest digest for ${IMAGE_REF}:${IMAGE_TAG}." >&2
|
|
exit 1
|
|
fi
|
|
echo "image_ref=$IMAGE_REF" >> "$GITHUB_OUTPUT"
|
|
echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
|
|
echo "image_digest=$image_digest" >> "$GITHUB_OUTPUT"
|
|
echo "build_sha=${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT"
|
|
docker buildx imagetools inspect "${IMAGE_REF}@${image_digest}"
|
|
|
|
# v1.16.4 — sign the multi-arch manifest index (keyless, Sigstore).
|
|
# Signing the INDEX digest covers both per-platform manifests in
|
|
# one signature, and the digest is the only stable identity — every
|
|
# tag (`:latest`, `:1.16.4`, …) is a mutable alias that a registry
|
|
# compromise could repoint. Operators verify with:
|
|
#
|
|
# cosign verify \
|
|
# --certificate-identity-regexp 'https://github.com/MBombeck/HealthLog/\.github/workflows/docker-publish\.yml@refs/tags/v.*' \
|
|
# --certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
|
# ghcr.io/mbombeck/healthlog:<tag>
|
|
#
|
|
# See docs/ops/deploy.md ("Verify the image signature") for the
|
|
# digest-pinned deploy recommendation that pairs with this.
|
|
- name: Install cosign
|
|
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
|
|
|
- name: Sign multi-arch manifest (keyless)
|
|
env:
|
|
IMAGE_DIGEST: ${{ steps.published.outputs.image_digest }}
|
|
run: |
|
|
echo "Signing ${IMAGE_REF}@${IMAGE_DIGEST}"
|
|
cosign sign --yes "${IMAGE_REF}@${IMAGE_DIGEST}"
|
|
echo "::notice::Image index signed (keyless): ${IMAGE_REF}@${IMAGE_DIGEST}. Verify with cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp 'https://github.com/${{ github.repository }}/' ${IMAGE_REF}@${IMAGE_DIGEST}"
|
|
|
|
verify-published-image:
|
|
name: Verify exact published image
|
|
needs: merge
|
|
permissions:
|
|
contents: read
|
|
packages: read
|
|
uses: ./.github/workflows/post-publish-verify.yml
|
|
with:
|
|
image_ref: ${{ needs.merge.outputs.image_ref }}
|
|
image_tag: ${{ needs.merge.outputs.image_tag }}
|
|
image_digest: ${{ needs.merge.outputs.image_digest }}
|
|
build_sha: ${{ needs.merge.outputs.build_sha }}
|
|
secrets: inherit
|
|
|
|
promote:
|
|
name: Promote verified image to Coolify
|
|
needs: [merge, verify-published-image]
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 5
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- name: Trigger Coolify deploy
|
|
env:
|
|
COOLIFY_WEBHOOK: ${{ secrets.COOLIFY_WEBHOOK }}
|
|
COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }}
|
|
COOLIFY_AUTO_DEPLOY: ${{ vars.COOLIFY_AUTO_DEPLOY }}
|
|
IMAGE_TAG: ${{ needs.merge.outputs.image_tag }}
|
|
IMAGE_DIGEST: ${{ needs.merge.outputs.image_digest }}
|
|
BUILD_SHA: ${{ needs.merge.outputs.build_sha }}
|
|
run: |
|
|
mode="${COOLIFY_AUTO_DEPLOY:-}"
|
|
if [ "$mode" = "off" ]; then
|
|
echo "::notice::COOLIFY_AUTO_DEPLOY=off — skipping registry deploy hook."
|
|
exit 0
|
|
fi
|
|
if [ "$mode" != "on" ]; then
|
|
echo "::warning::COOLIFY_AUTO_DEPLOY is not 'on' or 'off' — skipping auto-deploy. Set an explicit mode in repository variables."
|
|
exit 0
|
|
fi
|
|
if [ -z "$COOLIFY_WEBHOOK" ] || [ -z "$COOLIFY_TOKEN" ]; then
|
|
echo "::error::COOLIFY_AUTO_DEPLOY=on but COOLIFY_WEBHOOK or COOLIFY_TOKEN secret is missing. Set both credentials or disable promotion explicitly." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Waiting 90 s for registry propagation before promoting ${IMAGE_TAG}@${IMAGE_DIGEST}."
|
|
sleep 90
|
|
deploy_url="$COOLIFY_WEBHOOK"
|
|
if ! echo "$deploy_url" | grep -q "force="; then
|
|
if echo "$deploy_url" | grep -q "?"; then
|
|
deploy_url="${deploy_url}&force=true"
|
|
else
|
|
deploy_url="${deploy_url}?force=true"
|
|
fi
|
|
fi
|
|
|
|
echo "Triggering Coolify deploy for ${IMAGE_TAG}@${IMAGE_DIGEST} (source ${BUILD_SHA})."
|
|
response="$(curl --silent --show-error --max-time 30 \
|
|
--write-out "\n__HTTP_STATUS__:%{http_code}" \
|
|
--request POST "$deploy_url" \
|
|
--header "Authorization: Bearer $COOLIFY_TOKEN")"
|
|
status="${response##*__HTTP_STATUS__:}"
|
|
body="${response%__HTTP_STATUS__:*}"
|
|
echo "Coolify response (HTTP $status):"
|
|
echo "$body"
|
|
if [ "$status" -lt 200 ] || [ "$status" -ge 300 ]; then
|
|
echo "::error::Coolify deploy webhook returned HTTP $status for verified image ${IMAGE_TAG}@${IMAGE_DIGEST}." >&2
|
|
exit 1
|
|
fi
|
|
echo "Coolify deploy queued for verified image ${IMAGE_TAG}@${IMAGE_DIGEST}."
|