-
Notifications
You must be signed in to change notification settings - Fork 0
228 lines (206 loc) · 10.4 KB
/
Copy pathbuild-image.yml
File metadata and controls
228 lines (206 loc) · 10.4 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
name: build-image
# Builds and pushes the Sofra container image to GHCR. The single-box Docker
# Compose deploy (restaurant-app-deploy) pulls ghcr.io/piwas-21/sofra.
# Same security posture as restaurant-app-frontend: no untrusted-input
# expressions in run: steps, actions pinned to immutable SHAs, workflow-scoped
# GITHUB_TOKEN with packages:write only.
on:
push:
# `develop` was added with the staging bake: the build-staging job below is gated on
# that ref, and a job gate is useless if the WORKFLOW never runs on the branch. The
# tag gating inside each meta step is what keeps :latest and :migrate main-only.
branches: [main, develop]
tags: ['v*']
concurrency:
group: build-image-${{ github.ref }}
cancel-in-progress: true
# Workflow-level default is read-only; `packages: write` is granted per job
# (Sonar S8233), so the token is never broader than the job holding it needs.
permissions:
contents: read
jobs:
build-push:
name: build & push (GHCR)
# main + tags only. On develop the staging bake below is the one that runs; letting
# this job run too would publish a :sha and a migrate-<sha> image nothing pulls.
if: github.ref != 'refs/heads/develop'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: stamp
# Wall-clock build time, stamped here rather than taken from a github.event
# field: the event payloads differ per trigger (push vs tag vs dispatch), and
# repository.updated_at answers "when did the repo change", which is not the
# question /api/health is asked.
run: echo "built_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ghcr.io/${{ github.repository }}
# Gate :latest on the main branch explicitly, NOT {{is_default_branch}}:
# under GitFlow the repo's default branch is `develop`, so is_default_branch
# is false on the release push to main and :latest never advanced (fix 2026-07-12).
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=sha,format=long
type=ref,event=tag
- uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: ./Dockerfile
target: runner
push: true
build-args: |
NEXT_PUBLIC_SITE_URL=${{ vars.SITE_URL || 'https://sofrapiwas.com' }}
BUILD_SHA=${{ github.sha }}
BUILD_TIME=${{ steps.stamp.outputs.built_at }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Verify the build identity actually reached the image
# Without this, nothing in CI ever proves BUILD_SHA is wired: the Playwright smoke
# runs `next build` directly and never touches the Dockerfile, so `version` is always
# the "unknown" fallback there and the local spec's toHaveProperty passes regardless.
# Drop the build-arg during an unrelated edit and every gate stays green — the break
# would surface only when a human remembered to run the staging suite by hand.
# Reads the env straight out of the pushed image; no server boot needed.
env:
IMAGE: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
run: |
set -euo pipefail
got=$(docker run --rm --entrypoint node "$IMAGE" \
-e 'process.stdout.write(process.env.BUILD_SHA || "MISSING")')
echo "image reports BUILD_SHA=$got"
[ "$got" = "${GITHUB_SHA}" ] || {
echo "::error::image BUILD_SHA is '$got', expected '${GITHUB_SHA}' — /api/health would misreport what is deployed"
exit 1
}
# One-off DB tooling image (prisma migrate deploy + admin seed) — pulled
# on the box only when a release ships migrations. See DEPLOYMENT.md.
# Published as a TAG of the main (public) package — a separate
# ghcr.io/…/sofra-migrate package would be created private by org
# default, and GHCR visibility can't be changed via API.
- id: meta-migrate
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ghcr.io/${{ github.repository }}
# Same main-branch gate as the app image above (not {{is_default_branch}}).
tags: |
type=raw,value=migrate,enable=${{ github.ref == 'refs/heads/main' }}
type=sha,format=long,prefix=migrate-
- uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: ./Dockerfile
target: migrate
push: true
tags: ${{ steps.meta-migrate.outputs.tags }}
labels: ${{ steps.meta-migrate.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ── staging bake ──────────────────────────────────────────────────────────────
# A SEPARATE build, not just an extra tag on the one above: NEXT_PUBLIC_SITE_URL is a
# BUILD arg, so the same image cannot serve two hosts. Baked with the staging URL it
# emits staging canonicals — and, via lib/seo.ts IS_CANONICAL_SITE, a robots.txt that
# refuses every crawler. A staging twin of the marketing site left crawlable would
# compete with the real one for the citations the AEO work exists to win.
#
# develop only. `:staging` must never carry main's code, or rolling staging would
# silently deploy production build output to it.
build-staging:
name: build & push staging (GHCR)
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: stamp
# Wall-clock build time, stamped here rather than taken from a github.event
# field: the event payloads differ per trigger (push vs tag vs dispatch), and
# repository.updated_at answers "when did the repo change", which is not the
# question /api/health is asked.
run: echo "built_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- id: meta-staging
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ghcr.io/${{ github.repository }}
# `:staging` moves; `:staging-<sha>` is immutable, for rollback.
tags: |
type=raw,value=staging
type=sha,format=long,prefix=staging-
- uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: ./Dockerfile
target: runner
push: true
build-args: |
NEXT_PUBLIC_SITE_URL=${{ vars.STAGING_SITE_URL || 'https://staging.sofrapiwas.com' }}
BUILD_SHA=${{ github.sha }}
BUILD_TIME=${{ steps.stamp.outputs.built_at }}
tags: ${{ steps.meta-staging.outputs.tags }}
labels: ${{ steps.meta-staging.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Verify the build identity actually reached the image
# Without this, nothing in CI ever proves BUILD_SHA is wired: the Playwright smoke
# runs `next build` directly and never touches the Dockerfile, so `version` is always
# the "unknown" fallback there and the local spec's toHaveProperty passes regardless.
# Drop the build-arg during an unrelated edit and every gate stays green — the break
# would surface only when a human remembered to run the staging suite by hand.
# Reads the env straight out of the pushed image; no server boot needed.
env:
IMAGE: ghcr.io/${{ github.repository }}:staging-${{ github.sha }}
run: |
set -euo pipefail
got=$(docker run --rm --entrypoint node "$IMAGE" \
-e 'process.stdout.write(process.env.BUILD_SHA || "MISSING")')
echo "image reports BUILD_SHA=$got"
[ "$got" = "${GITHUB_SHA}" ] || {
echo "::error::image BUILD_SHA is '$got', expected '${GITHUB_SHA}' — /api/health would misreport what is deployed"
exit 1
}
# The DB-tooling image for staging. It has to be built HERE, in the job that runs
# on develop — putting the tag on `build-push`'s meta-migrate step made it dead
# code the moment that job started skipping on develop, and the tag simply never
# existed. Found by pulling it on the box.
#
# Staging needs its own: the migrate image carries prisma/migrations, so main's
# copy would apply main's schema to a database that tracks develop.
- id: meta-migrate-staging
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=migrate-staging
type=sha,format=long,prefix=migrate-staging-
- uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: ./Dockerfile
target: migrate
push: true
tags: ${{ steps.meta-migrate-staging.outputs.tags }}
labels: ${{ steps.meta-migrate-staging.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max