-
Notifications
You must be signed in to change notification settings - Fork 0
287 lines (261 loc) · 12.2 KB
/
Copy pathbuild.yml
File metadata and controls
287 lines (261 loc) · 12.2 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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Netresearch DTT GmbH
#
# The daily rebuild is the whole point of this repository: upstream refreshes
# its image when phpMyAdmin is released, we refresh ours when its base picks up
# fixes. Two variants per run — `pinned` ships the libraries as released,
# `rolling` refreshes them within phpMyAdmin's own constraints.
name: build
on:
schedule:
- cron: '0 3 * * *' # 03:00 UTC, before the workday
push:
branches: [main]
paths-ignore:
- 'docs/**'
- 'examples/**'
- '**.md'
pull_request:
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/phpmyadmin-php-fpm
# nginx with the configuration and document root baked in, so a consumer
# needs two containers and no mounted files. Always deployed at the same
# tag as IMAGE_NAME — see the note on the web stage in the Dockerfile.
WEB_IMAGE_NAME: ${{ github.repository_owner }}/phpmyadmin-nginx
permissions: {}
concurrency:
group: build-${{ github.ref }}
# A scheduled multi-arch rebuild must not be interrupted half-pushed.
cancel-in-progress: ${{ github.event_name != 'schedule' }}
jobs:
build:
name: ${{ matrix.variant }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
attestations: write
strategy:
fail-fast: false
matrix:
include:
- variant: pinned
rolling: "false"
suffix: ""
- variant: rolling
rolling: "true"
suffix: "-rolling"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Read the pinned phpMyAdmin release
id: version
run: echo "pma=$(cat .phpmyadmin-version)" >> "$GITHUB_OUTPUT"
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
# Pushing only happens from main and the schedule: a pull request must be
# able to prove the image still builds without being able to publish it.
- name: Log in to the registry
if: github.event_name != 'pull_request'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Tags and labels
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
suffix=${{ matrix.suffix }},onlatest=true
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=${{ steps.version.outputs.pma }},enable={{is_default_branch}}
type=raw,value=${{ steps.version.outputs.pma }}-{{date 'YYYYMMDD'}},enable={{is_default_branch}}
type=ref,event=pr
- name: Build and push
id: push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
# Named explicitly. Without it Docker builds the LAST stage in the
# Dockerfile, and adding the web stage after runtime silently turned
# this build into the nginx one - php-fpm:latest was published as
# nginx until the smoke test on main caught it on /bin/bash.
target: runtime
# A pull request builds one architecture and keeps it local: `--load`
# cannot import a manifest list, so asking for both there fails with
# "docker exporter does not currently support exporting manifest
# lists" before a single layer is built.
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
push: ${{ github.event_name != 'pull_request' }}
load: ${{ github.event_name == 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.variant }}
cache-to: type=gha,mode=max,scope=${{ matrix.variant }}
build-args: |
PMA_VERSION=${{ steps.version.outputs.pma }}
ROLLING_DEPS=${{ matrix.rolling }}
BUILD_DATE=${{ github.event.repository.updated_at }}
VCS_REF=${{ github.sha }}
# The SBOM and provenance attestations travel with the manifest, so
# a consumer can ask the registry what is inside the image it pulled.
# Both need an exporter that writes a manifest, which the local
# `--load` on pull requests is not.
sbom: ${{ github.event_name != 'pull_request' }}
provenance: ${{ github.event_name != 'pull_request' && 'mode=max' || 'false' }}
# The smoke test would have caught a swapped image, but it is a separate
# job and the pull-request build never leaves this runner, so it only
# runs on main - which is where the wrong image had already been
# published. This asserts the identity of what was just built, in the
# job that builds it, where a missing `target:` actually goes wrong.
- name: The image is php-fpm, not the web stage
if: github.event_name == 'pull_request'
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
set -euo pipefail
tag="$(printf '%s\n' "$TAGS" | head -n1)"
docker run --rm --entrypoint sh "$tag" -c \
'command -v php-fpm >/dev/null && test -x /bin/bash && ! command -v nginx >/dev/null'
- name: Attest the build
if: github.event_name != 'pull_request'
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
# The web image is built here rather than in a job of its own so that
# both images come out of one run with one tag set. They share the
# document root, and deploying them at different tags means running two
# phpMyAdmin versions against each other.
- name: Tags and labels (web)
id: meta-web
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.REGISTRY }}/${{ env.WEB_IMAGE_NAME }}
flavor: |
suffix=${{ matrix.suffix }},onlatest=true
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=${{ steps.version.outputs.pma }},enable={{is_default_branch}}
type=raw,value=${{ steps.version.outputs.pma }}-{{date 'YYYYMMDD'}},enable={{is_default_branch}}
type=ref,event=pr
- name: Build and push (web)
id: push-web
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
target: web
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
push: ${{ github.event_name != 'pull_request' }}
load: ${{ github.event_name == 'pull_request' }}
tags: ${{ steps.meta-web.outputs.tags }}
labels: ${{ steps.meta-web.outputs.labels }}
# Same cache scope as the runtime build: both share the fetcher
# stage, so the second build reuses it instead of downloading and
# verifying the release a second time.
cache-from: type=gha,scope=${{ matrix.variant }}
cache-to: type=gha,mode=max,scope=${{ matrix.variant }}
build-args: |
PMA_VERSION=${{ steps.version.outputs.pma }}
ROLLING_DEPS=${{ matrix.rolling }}
BUILD_DATE=${{ github.event.repository.updated_at }}
VCS_REF=${{ github.sha }}
sbom: ${{ github.event_name != 'pull_request' }}
provenance: ${{ github.event_name != 'pull_request' && 'mode=max' || 'false' }}
- name: The web image is nginx, not the runtime stage
if: github.event_name == 'pull_request'
env:
TAGS: ${{ steps.meta-web.outputs.tags }}
run: |
set -euo pipefail
tag="$(printf '%s\n' "$TAGS" | head -n1)"
docker run --rm --entrypoint sh "$tag" -c \
'command -v nginx >/dev/null && ! command -v php-fpm >/dev/null'
- name: Attest the build (web)
if: github.event_name != 'pull_request'
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.WEB_IMAGE_NAME }}
subject-digest: ${{ steps.push-web.outputs.digest }}
push-to-registry: true
# Proves the image actually serves phpMyAdmin, not merely that it built.
# A dependency refresh that breaks the application would pass every static
# check and fail here.
smoke-test:
name: smoke test (${{ matrix.variant }})
needs: build
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
strategy:
fail-fast: false
matrix:
variant: [pinned, rolling]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Start the stack
run: |
set -euo pipefail
cp .env.example .env
{
echo "MARIADB_ROOT_PASSWORD=smoke-test-only"
echo "PMA_BLOWFISH_SECRET=smoketestsmoketestsmoketest12345"
if [ "${{ matrix.variant }}" = "rolling" ]; then
echo "PMA_IMAGE_TAG=latest-rolling"
fi
} >> .env
docker compose --profile standalone up -d --wait --wait-timeout 300
- name: phpMyAdmin answers and logs in
run: |
set -euo pipefail
jar=$(mktemp)
code=$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/)
[ "$code" = "200" ] || { echo "::error::login page returned $code"; exit 1; }
token=$(curl -sS -c "$jar" http://127.0.0.1:8080/index.php \
| grep -oE 'name="token" value="[^"]+"' | head -1 | sed 's/.*value="//;s/"//')
[ -n "$token" ] || { echo "::error::no CSRF token in the login form"; exit 1; }
curl -sS -b "$jar" -c "$jar" -o /dev/null \
-X POST -d "pma_username=root&pma_password=smoke-test-only&server=1&target=index.php&token=$token" \
http://127.0.0.1:8080/index.php
# Assert on the absence of the login form, not on a phrase from the
# authenticated page: that wording depends on the phpMyAdmin version
# and the negotiated language. Rejected credentials re-render the
# form, so its presence is the discriminator — verified both ways:
# the unauthenticated page carries `name="pma_username"` exactly
# once, the authenticated one not at all.
page=$(curl -sS -b "$jar" -w '\n%{http_code}' http://127.0.0.1:8080/index.php)
code=${page##*$'\n'}
[ "$code" = "200" ] || { echo "::error::authenticated page returned $code"; exit 1; }
if printf '%s' "$page" | grep -q 'name="pma_username"'; then
echo "::error::still on the login form — the credentials were rejected"
printf '%s' "$page" | grep -oE 'class="alert[^"]*"[^<]*<[^>]*>[^<]{0,200}' | head -3
exit 1
fi
echo "logged in against the database"
- name: php-fpm endpoints are not public
run: |
set -euo pipefail
for path in ping status; do
code=$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:8080/$path")
[ "$code" = "403" ] || { echo "::error::/$path answered $code, expected 403"; exit 1; }
done
- name: Logs on failure
if: failure()
run: docker compose --profile standalone logs