1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# Docker image for the `forgedb` CLI — epic #181 / Docker channel.
#
# Triggered by the "Release" workflow (cargo-dist / release.yml) COMPLETING, via
# `workflow_run`. That ordering is load-bearing: the Dockerfile's `fetch` stage
# does an UNAUTHENTICATED `curl` of `releases/download/v<version>/forgedb-<triple>
# .tar.xz`, so the image can only build once release.yml's `host` job has created
# the (published, non-draft) GitHub Release and uploaded every per-platform
# tarball. `workflow_run: [completed]` fires only after the whole Release run
# (build → host → announce) finishes, so the assets are guaranteed present.
#
# Why not `on: release: [published]`? cargo-dist creates the Release with the
# built-in GITHUB_TOKEN, and GitHub suppresses `release` events for
# GITHUB_TOKEN-authored releases — that trigger would never fire. `workflow_run`
# keys off the *workflow completing* (the Release run was triggered by a human tag
# push), so it is not subject to that suppression.
#
# Why not a cargo-dist custom publish job (like publish-npm.yml)? dist hardcodes
# custom-publish call-site permissions to {id-token, packages} with no way to add
# `contents: read`, and this job needs checkout (for the Dockerfile / build
# context). A reusable workflow requesting a scope the caller didn't grant fails
# the caller at startup, so Docker must stay a standalone workflow_run job with
# its own permissions.
#
# workflow_run runs the copy of this file on the DEFAULT branch (main). Builds a
# multi-arch image (linux/amd64 + linux/arm64), pushes to GHCR always and to
# Docker Hub when its credentials are configured.
#
# Registries / go-live gating:
# - GHCR (ghcr.io/hoodiecollin/forgedb): auth via the built-in GITHUB_TOKEN — no
# secret to provision (this workflow grants itself `packages: write`).
# - Docker Hub (docker.io/hoodiecollin/forgedb): needs `DOCKERHUB_USERNAME` +
# `DOCKERHUB_TOKEN` secrets. Absent → the image still ships to GHCR; Docker Hub
# is skipped (not failed).
#
# `workflow_dispatch` stays for manual rebuilds of an already-published tag.
name: Docker
on:
workflow_run:
workflows:
types:
workflow_dispatch:
inputs:
tag:
description: Release tag to build (e.g. v0.2.0) — must already be published
required: true
permissions:
contents: read
packages: write # push to GHCR
jobs:
image:
name: build & push (multi-arch)
runs-on: ubuntu-22.04
# Manual dispatch always runs. For a workflow_run trigger, require the upstream
# Release to have SUCCEEDED and the tag to be a stable `v*` (skip prereleases —
# a `-rc`/`-beta` tag gets no image or `latest`). head_branch is the tag name
# for a tag-triggered upstream run.
if: >-
${{ github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
startsWith(github.event.workflow_run.head_branch, 'v') &&
!contains(github.event.workflow_run.head_branch, '-')) }}
env:
TAG_INPUT: ${{ github.event.inputs.tag }}
RUN_TAG: ${{ github.event.workflow_run.head_branch }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
# Resolve the tag from the upstream Release run's head_branch (workflow_run)
# or the manual dispatch input, then the bare version the Dockerfile fetches by.
- name: Resolve version
id: v
shell: bash
run: |
tag="${TAG_INPUT:-$RUN_TAG}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
# Only push to Docker Hub when creds exist (secrets can't be used in `if:`
# directly, so surface presence as a step output first).
- name: Detect Docker Hub credentials
id: hub
shell: bash
run: |
if [ -n "$DOCKERHUB_USERNAME" ] && [ -n "$DOCKERHUB_TOKEN" ]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::notice::Docker Hub secrets not set — pushing to GHCR only."
fi
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
if: ${{ steps.hub.outputs.enabled == 'true' }}
uses: docker/login-action@v3
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: Compute tags
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/hoodiecollin/forgedb
name=docker.io/hoodiecollin/forgedb,enable=${{ steps.hub.outputs.enabled }}
tags: |
type=semver,pattern={{version}},value=${{ steps.v.outputs.tag }}
type=semver,pattern={{major}}.{{minor}},value=${{ steps.v.outputs.tag }}
type=raw,value=latest
- name: Build & push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
build-args: |
VERSION=${{ steps.v.outputs.version }}
REPO=${{ github.repository }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}