Repository navigation
239 lines (217 loc) · 9.29 KB
/
Copy pathdocker_build.yml
File metadata and controls
239 lines (217 loc) · 9.29 KB
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# =============================================================================
# Reusable Docker Build Workflow
# =============================================================================
#
# This workflow is called by other workflows to build and push Docker images.
# It centralizes Docker build configuration for consistency and maintainability.
#
# Features:
# - Uses docker/metadata-action for tag generation, slugification, and OCI labels
# - Simple boolean inputs instead of metadata-action DSL
# - Automatic OCI-compliant labels (source, revision, created, etc.)
# - Registry-based caching (no GitHub Actions 10GB limit)
# - Multi-source cache: reads from main + branch-specific caches
#
# Generated Tags:
# - SHA tag: {branch}-{sha} (immutable, for deployments/rollbacks)
# - Mutable tag: {branch} (latest build for this branch)
# - Latest tag: latest (optional, only for production)
#
# Cache Strategy:
# - Reads from: main cache (stable baseline) + branch cache (incremental)
# - Writes to: branch-specific cache (or main if on main/master branch)
#
# Usage Examples:
# # PR Preview (just defaults - SHA + mutable tags)
# build:
# uses: ./.github/workflows/docker_build.yml
# secrets: inherit
#
# # Staging (defaults + Heroku deploy)
# build:
# uses: ./.github/workflows/docker_build.yml
# with:
# heroku_app: dev-metaculus-web
# secrets: inherit
#
# # Production (add latest tag + Heroku deploy)
# build:
# uses: ./.github/workflows/docker_build.yml
# with:
# include_latest_tag: true
# heroku_app: metaculus-web
# secrets: inherit
#
# =============================================================================
name: Docker Build
on:
workflow_dispatch:
workflow_call:
inputs:
# Include latest tag (for production deployments)
include_latest_tag:
description: "Include latest tag"
required: false
type: boolean
default: false
# Heroku app name - if provided, builds and pushes Heroku-specific targets
heroku_app:
description: "Heroku app name (enables Heroku target builds if set)"
required: false
type: string
default: ""
# Heroku targets to build (space-separated)
heroku_targets:
description: "Heroku targets to build (space-separated)"
required: false
type: string
default: "release dramatiq_worker django_cron web"
outputs:
image_name:
description: "Full image name (without tag)"
value: ${{ jobs.build.outputs.image_name }}
image_tags:
description: "All image tags (newline-separated)"
value: ${{ jobs.build.outputs.image_tags }}
labels:
description: "OCI image labels (newline-separated)"
value: ${{ jobs.build.outputs.labels }}
primary_tag:
description: "Primary immutable tag (SHA-based)"
value: ${{ jobs.build.outputs.primary_tag }}
image_url:
description: "Primary full image URL (image:tag)"
value: ${{ jobs.build.outputs.image_url }}
branch_slug:
description: "Slugified branch name"
value: ${{ jobs.build.outputs.branch_slug }}
heroku_targets:
description: "Heroku targets that were built (space-separated)"
value: ${{ jobs.build.outputs.heroku_targets }}
permissions:
contents: read
packages: write
# Permissions inherited from caller, but documented here for reference
# Callers must have: contents: read, packages: write
jobs:
build:
name: Build Docker Image
runs-on: ubuntu-24.04
outputs:
image_name: ${{ steps.vars.outputs.image_name }}
image_tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
primary_tag: ${{ steps.meta.outputs.version }}
image_url: ${{ steps.vars.outputs.image_name }}:${{ steps.meta.outputs.version }}
branch_slug: ${{ steps.vars.outputs.branch_slug }}
heroku_targets: ${{ inputs.heroku_targets }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
# Use PR branch HEAD (not merge commit) for consistent caching
# Falls back to github.sha for non-PR events (push, workflow_dispatch)
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Heroku Container Registry
if: inputs.heroku_app != ''
uses: docker/login-action@v3
with:
registry: registry.heroku.com
username: _
password: ${{ secrets.HEROKU_API_KEY }}
- name: Compute image name and branch prefix
id: vars
env:
BRANCH_REF: ${{ github.head_ref || github.ref_name }}
# Use PR branch HEAD for PRs, otherwise default SHA
COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
# Image name must be lowercase for ghcr.io
IMAGE_NAME="ghcr.io/${{ github.repository }}"
IMAGE_NAME_LOWER="$(printf '%s' "$IMAGE_NAME" | tr '[:upper:]' '[:lower:]')"
echo "image_name=${IMAGE_NAME_LOWER}" >> $GITHUB_OUTPUT
# Compute branch prefix for SHA tag
# - For PRs: use head_ref (source branch)
# - For pushes: use ref_name
BRANCH_SLUG="$(bash .github/scripts/slugify_branch.sh "$BRANCH_REF" 60)"
echo "branch_slug=${BRANCH_SLUG}" >> $GITHUB_OUTPUT
echo "Branch slug: ${BRANCH_SLUG}"
# Short SHA for tags (7 chars, matching git's default)
SHORT_SHA="${COMMIT_SHA:0:7}"
echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT
echo "commit_sha=${COMMIT_SHA}" >> $GITHUB_OUTPUT
# Use docker/metadata-action for tag generation and OCI labels
# See: https://github.com/docker/metadata-action
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ steps.vars.outputs.image_name }}
# Control latest tag behavior explicitly via our input
flavor: |
latest=false
tags: |
# SHA tag: {branch}-{short_sha} (immutable, used for deployments/rollbacks)
# Uses actual checkout SHA (PR branch HEAD), not merge commit
type=raw,value=${{ steps.vars.outputs.branch_slug }}-${{ steps.vars.outputs.short_sha }},priority=1000
# Mutable tag: {branch} (always points to latest build for this branch)
type=raw,value=${{ steps.vars.outputs.branch_slug }},priority=600
# Latest tag (only for production builds)
type=raw,value=latest,enable=${{ inputs.include_latest_tag }},priority=200
labels: |
# Override revision to match actual checkout (PR branch HEAD, not merge commit)
org.opencontainers.image.revision=${{ steps.vars.outputs.commit_sha }}
- name: Compute cache scope
id: cache
run: |
BRANCH_SLUG="${{ steps.vars.outputs.branch_slug }}"
# Determine if this is the main branch (writes to main cache)
if [ "${{ github.ref_name }}" = "main" ] || [ "${{ github.ref_name }}" = "master" ]; then
echo "cache_write_tag=buildcache-main" >> $GITHUB_OUTPUT
else
echo "cache_write_tag=buildcache-${BRANCH_SLUG}" >> $GITHUB_OUTPUT
fi
echo "cache_scope=${BRANCH_SLUG}" >> $GITHUB_OUTPUT
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
target: all_runners
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Multi-source cache strategy:
# - Read from: main cache (stable baseline) + branch cache (incremental)
# - Write to: branch-specific cache (or main if on main branch)
cache-from: |
type=registry,ref=${{ steps.vars.outputs.image_name }}:buildcache-main
type=registry,ref=${{ steps.vars.outputs.image_name }}:buildcache-${{ steps.cache.outputs.cache_scope }}
cache-to: type=registry,ref=${{ steps.vars.outputs.image_name }}:${{ steps.cache.outputs.cache_write_tag }},mode=max,compression=zstd
- name: Build and push Heroku targets
if: inputs.heroku_app != ''
run: |
set -e
IMAGE_NAME="${{ steps.vars.outputs.image_name }}"
CACHE_SCOPE="${{ steps.cache.outputs.cache_scope }}"
for target in ${{ inputs.heroku_targets }}; do
echo "Building Heroku target: $target"
# Heroku registry only accepts Docker Manifest V2 Schema 2, not OCI
docker buildx build \
--provenance=false \
--sbom=false \
--platform linux/amd64 \
--target "$target" \
--output "type=image,name=registry.heroku.com/${{ inputs.heroku_app }}/$target,push=true,oci-mediatypes=false" \
--cache-from "type=registry,ref=${IMAGE_NAME}:buildcache-main" \
--cache-from "type=registry,ref=${IMAGE_NAME}:buildcache-${CACHE_SCOPE}" \
.
done