name: Build & Publish Docker Image on: push: branches: [main] tags: ["v*"] pull_request: branches: [main] workflow_dispatch: env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} # Cancel in-progress runs when a new commit is pushed to the same ref. # main + a v* tag at the same SHA are different refs, so the per-ref # gha cache scope below keeps them from contending on cache writes. concurrency: group: docker-publish-${{ github.ref }} cancel-in-progress: true jobs: build-and-push: name: Build & push image runs-on: ubuntu-latest # 25 min wall clock — a healthy amd64-only build finishes inside 12 min. # The qemu-arm64 path that was hanging at 47 min on `[linux/amd64 runner # 2/14]` is gone; see commit message for v1.4.16 C5 rationale below. timeout-minutes: 25 permissions: contents: read packages: write steps: - name: Checkout uses: actions/checkout@v6 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 # 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@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Compute image tags & labels id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable={{is_default_branch}} 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=AGPL-3.0-only org.opencontainers.image.source=https://github.com/${{ github.repository }} # v1.4.16 C5 — drop linux/arm64 from the multi-arch build. # # Root cause of the main-branch hang since v1.4.14: the qemu-arm64 # path (`docker/setup-qemu-action` + `tonistiigi/binfmt`) was # SIGILL-crashing Next.js's static-page-generation workers # (`Next.js build worker exited with code: null and signal: SIGILL` # at 64-66/86 pages). V8's optimising tier emits CPU instructions # that qemu's user-mode emulation cannot reliably translate under # heavy load, and Node 22 + Alpine 3.21 hit it deterministically. # Tag-builds appeared to "work" only because they happened to # finish their static pages before the crash window; main-branch # builds rarely got that lucky. # # Marc's prod target is x86_64 only (Coolify on apps-01, all other # self-hosted instances we know of); the arm64 image was a # courtesy build for OSS users, not production. v1.5 will re-add # arm64 via a matrix that uses the GitHub-hosted `ubuntu-24.04-arm` # runner (free for public repos, native arm64, no qemu) — see # `docs/audit/v1416-auto-deploy-fix.md` for the broader CI plan. # # Per-ref gha cache scope (kept from v1.4.15 C3): tag + main # builds at the same SHA do not contend on writes; tag warm-starts # off main's last good build for layer reuse. - name: Build & push image uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile platforms: linux/amd64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: | type=gha,scope=build-${{ github.ref_name }} type=gha,scope=build-main cache-to: type=gha,mode=max,scope=build-${{ github.ref_name }} provenance: true sbom: true # Phase C2 (v1.4.15): trigger Coolify deploy automatically once GHCR has # the new image. Removes the manual SSH + force-pull recipe documented # in `.planning/phase-8-report.md`. # # Runs only on the publish path (push to `main` or to a `v*` tag), never # on PRs. `continue-on-error: true` because the image is *already* # published to GHCR by the time we reach this step — a Coolify-side # outage must not retroactively fail the build. Marc still has the # manual recipe as fallback. # # Required GitHub repository secrets (set by Marc once): # COOLIFY_WEBHOOK = full deploy URL incl. ?uuid=...&force=false # COOLIFY_TOKEN = Bearer token from Coolify UI -> Keys & Tokens # Both secrets are referenced exclusively here. If either is unset, the # step short-circuits to a skip rather than calling Coolify with empty # auth. - name: Trigger Coolify deploy if: github.event_name != 'pull_request' continue-on-error: true env: COOLIFY_WEBHOOK: ${{ secrets.COOLIFY_WEBHOOK }} COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }} run: | if [ -z "$COOLIFY_WEBHOOK" ] || [ -z "$COOLIFY_TOKEN" ]; then echo "::warning::COOLIFY_WEBHOOK or COOLIFY_TOKEN secret missing — skipping auto-deploy. Configure in repo Settings → Secrets to enable." exit 0 fi # v1.4.22 C3 — force=true tells Coolify to skip its image-cache # and re-pull the registry digest. If the maintainer's secret # URL already carries a `?force=` query, leave it alone; # otherwise append `?force=true` (or `&force=true` if other # query params are present) so doc-only deploys still trigger # a registry-digest check. 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 ref ${{ github.ref_name }} (sha ${GITHUB_SHA::7})" response=$(curl --silent --show-error --max-time 30 \ --write-out "\n__HTTP_STATUS__:%{http_code}" \ --request GET "$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 — image is published, but auto-deploy did not fire. Marc: deploy manually via SSH recipe in .planning/phase-8-report.md." exit 1 fi echo "::notice::Coolify auto-deploy webhook fired at $(date -u +%FT%TZ) for ref ${{ github.ref_name }} (sha ${GITHUB_SHA::7}). If /api/version doesn't reach the new tag within ~60s, see .planning/coolify-auto-deploy-howto.md." echo "Coolify deploy queued — awaiting outgoing notification webhook to /api/internal/deploy-webhook for final status."