Skip to content

Commit 0dce58b

Browse files
authored
Merge pull request #22 from specsnl/feat/GH-6-merge-tags
GH-6: Add merge job and tag scheme
2 parents 9ecf8be + e158ae2 commit 0dce58b

7 files changed

Lines changed: 844 additions & 1 deletion

File tree

‎.github/workflows/build.yml‎

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
name: Build
2+
3+
run-name: Build PHP ${{ inputs.php_version }}
4+
5+
on:
6+
workflow_dispatch:
7+
inputs:
8+
php_version:
9+
description: Exact PHP version to build, e.g. 8.4.13
10+
required: true
11+
type: string
12+
variants:
13+
description: Comma-separated variants, e.g. cli,zts (empty takes build-config.json's)
14+
required: false
15+
type: string
16+
default: ''
17+
base:
18+
description: Debian suite to build on, e.g. bookworm (empty takes the newest in versions.json)
19+
required: false
20+
type: string
21+
default: ''
22+
update_floating_tags:
23+
description: Also move the floating tags (8.4-fpm-trixie, 8-fpm, fpm, latest)
24+
required: false
25+
type: boolean
26+
default: false
27+
upstream_ref:
28+
description: docker-library/php ref to take the templates and versions.json from (empty uses this branch)
29+
required: false
30+
type: string
31+
default: ''
32+
33+
permissions:
34+
contents: read
35+
36+
jobs:
37+
prepare:
38+
name: Prepare
39+
runs-on: ubuntu-24.04
40+
outputs:
41+
image: ${{ steps.image.outputs.image }}
42+
matrix: ${{ steps.matrix.outputs.matrix }}
43+
directories: ${{ steps.directories.outputs.directories }}
44+
steps:
45+
# 8-fpm, fpm and latest carry no suite, so a run on any other suite than
46+
# the default would move them off it.
47+
- name: Refuse floating tags for a base override
48+
if: inputs.update_floating_tags && inputs.base != ''
49+
run: |
50+
echo "::error::update_floating_tags also moves the suite-less tags, which follow the default suite; leave base empty to update them"
51+
exit 1
52+
53+
- name: Checkout
54+
uses: actions/checkout@v7
55+
56+
- name: Read the image name
57+
id: image
58+
run: |
59+
source scripts/compute-tags.sh
60+
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
61+
62+
# The fork only adds files, so laying upstream's tree over the checkout
63+
# gives the same templates and versions.json that merging the ref would.
64+
- name: Check out upstream's files at ${{ inputs.upstream_ref }}
65+
if: inputs.upstream_ref != ''
66+
env:
67+
UPSTREAM_REF: ${{ inputs.upstream_ref }}
68+
run: |
69+
git fetch --depth=1 https://github.com/docker-library/php.git "$UPSTREAM_REF"
70+
git checkout FETCH_HEAD -- .
71+
72+
- name: Pin PHP ${{ inputs.php_version }} and render its Dockerfiles
73+
env:
74+
PHP_VERSION: ${{ inputs.php_version }}
75+
run: scripts/pin-version.sh "$PHP_VERSION"
76+
77+
- name: Select the build matrix
78+
id: matrix
79+
env:
80+
PHP_VERSION: ${{ inputs.php_version }}
81+
VARIANTS: ${{ inputs.variants }}
82+
BASE: ${{ inputs.base }}
83+
run: scripts/select-matrix.sh "$PHP_VERSION" "$VARIANTS" "$BASE" | tee -a "$GITHUB_OUTPUT"
84+
85+
- name: Select the directories to merge
86+
id: directories
87+
env:
88+
MATRIX: ${{ steps.matrix.outputs.matrix }}
89+
run: |
90+
directories="$(jq -c '{ include: [.include[] | { directory, variant, suite }] | unique }' <<< "$MATRIX")"
91+
echo "directories=$directories" | tee -a "$GITHUB_OUTPUT"
92+
93+
# A tarball keeps the executable bits on the docker-php-* scripts, which
94+
# upload-artifact drops.
95+
- name: Pack the build context
96+
env:
97+
PHP_VERSION: ${{ inputs.php_version }}
98+
run: tar -cf "$RUNNER_TEMP/build-context.tar" "${PHP_VERSION%.*}"
99+
100+
- name: Upload the build context
101+
uses: actions/upload-artifact@v7
102+
with:
103+
name: build-context
104+
path: ${{ runner.temp }}/build-context.tar
105+
if-no-files-found: error
106+
retention-days: 1
107+
108+
build:
109+
name: Build ${{ matrix.directory }} (${{ matrix.platform }})
110+
needs: prepare
111+
runs-on: ${{ matrix.runner }}
112+
permissions:
113+
contents: read
114+
packages: write
115+
strategy:
116+
matrix: ${{ fromJSON(needs.prepare.outputs.matrix) }}
117+
env:
118+
IMAGE: ${{ needs.prepare.outputs.image }}
119+
# Secrets cannot be read in a step condition; this env var can.
120+
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
121+
steps:
122+
- name: Download the build context
123+
uses: actions/download-artifact@v8
124+
with:
125+
name: build-context
126+
path: ${{ runner.temp }}
127+
128+
- name: Unpack the build context
129+
run: tar -xf "$RUNNER_TEMP/build-context.tar"
130+
131+
- name: Set up Docker Buildx
132+
uses: docker/setup-buildx-action@v4
133+
134+
- name: Log in to GHCR
135+
uses: docker/login-action@v4
136+
with:
137+
registry: ghcr.io
138+
username: ${{ github.actor }}
139+
password: ${{ secrets.GITHUB_TOKEN }}
140+
141+
# Pulling debian:<suite>-slim anonymously counts against Docker Hub's
142+
# rate limit for the runner's IP. With the DOCKERHUB_USERNAME and
143+
# DOCKERHUB_TOKEN secrets set, it pulls as that account instead.
144+
- name: Log in to Docker Hub
145+
if: env.DOCKERHUB_USERNAME != ''
146+
uses: docker/login-action@v4
147+
with:
148+
username: ${{ env.DOCKERHUB_USERNAME }}
149+
password: ${{ secrets.DOCKERHUB_TOKEN }}
150+
151+
# No tags yet: the merge job applies them once every digest has passed
152+
# the smoke test, so a half-failed run never leaves a broken tag behind.
153+
- name: Build and push by digest
154+
id: build
155+
uses: docker/build-push-action@v7
156+
with:
157+
context: ${{ matrix.directory }}
158+
platforms: ${{ matrix.platform }}
159+
provenance: mode=max
160+
sbom: true
161+
labels: |
162+
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
163+
org.opencontainers.image.version=${{ inputs.php_version }}
164+
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
165+
166+
- name: Export the digest
167+
env:
168+
DIGEST: ${{ steps.build.outputs.digest }}
169+
run: |
170+
mkdir -p "$RUNNER_TEMP/digests"
171+
touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}"
172+
173+
- name: Name the digest artifact
174+
id: artifact
175+
env:
176+
DIRECTORY: ${{ matrix.directory }}
177+
PLATFORM: ${{ matrix.platform }}
178+
run: echo "name=digests-${DIRECTORY//\//-}-${PLATFORM//\//-}" >> "$GITHUB_OUTPUT"
179+
180+
- name: Upload the digest
181+
uses: actions/upload-artifact@v7
182+
with:
183+
name: ${{ steps.artifact.outputs.name }}
184+
path: ${{ runner.temp }}/digests/*
185+
if-no-files-found: error
186+
retention-days: 1
187+
188+
smoke-test:
189+
name: Smoke test ${{ matrix.directory }} (${{ matrix.platform }})
190+
needs: [prepare, build]
191+
runs-on: ${{ matrix.runner }}
192+
permissions:
193+
contents: read
194+
packages: read
195+
strategy:
196+
matrix: ${{ fromJSON(needs.prepare.outputs.matrix) }}
197+
env:
198+
IMAGE: ${{ needs.prepare.outputs.image }}
199+
# The digest is an index that also holds the attestations; this picks
200+
# the platform's image out of it rather than relying on the runner's.
201+
DOCKER_DEFAULT_PLATFORM: ${{ matrix.platform }}
202+
steps:
203+
- name: Checkout
204+
uses: actions/checkout@v7
205+
206+
- name: Name the digest artifact
207+
id: artifact
208+
env:
209+
DIRECTORY: ${{ matrix.directory }}
210+
PLATFORM: ${{ matrix.platform }}
211+
run: echo "name=digests-${DIRECTORY//\//-}-${PLATFORM//\//-}" >> "$GITHUB_OUTPUT"
212+
213+
- name: Download the digest
214+
uses: actions/download-artifact@v8
215+
with:
216+
name: ${{ steps.artifact.outputs.name }}
217+
path: ${{ runner.temp }}/digests
218+
219+
- name: Log in to GHCR
220+
uses: docker/login-action@v4
221+
with:
222+
registry: ghcr.io
223+
username: ${{ github.actor }}
224+
password: ${{ secrets.GITHUB_TOKEN }}
225+
226+
- name: Smoke test
227+
env:
228+
VARIANT: ${{ matrix.variant }}
229+
run: |
230+
digests=( "$RUNNER_TEMP"/digests/* )
231+
scripts/smoke-test.sh "$IMAGE@sha256:${digests[0]##*/}" "$VARIANT"
232+
233+
merge:
234+
name: Merge ${{ matrix.directory }}
235+
needs: [prepare, smoke-test]
236+
runs-on: ubuntu-24.04
237+
permissions:
238+
contents: read
239+
packages: write
240+
strategy:
241+
matrix: ${{ fromJSON(needs.prepare.outputs.directories) }}
242+
env:
243+
IMAGE: ${{ needs.prepare.outputs.image }}
244+
steps:
245+
- name: Checkout
246+
uses: actions/checkout@v7
247+
248+
- name: Name the digest artifacts
249+
id: artifact
250+
env:
251+
DIRECTORY: ${{ matrix.directory }}
252+
run: echo "pattern=digests-${DIRECTORY//\//-}-*" >> "$GITHUB_OUTPUT"
253+
254+
- name: Download the digests
255+
uses: actions/download-artifact@v8
256+
with:
257+
pattern: ${{ steps.artifact.outputs.pattern }}
258+
path: ${{ runner.temp }}/digests
259+
merge-multiple: true
260+
261+
- name: Set up Docker Buildx
262+
uses: docker/setup-buildx-action@v4
263+
264+
- name: Log in to GHCR
265+
uses: docker/login-action@v4
266+
with:
267+
registry: ghcr.io
268+
username: ${{ github.actor }}
269+
password: ${{ secrets.GITHUB_TOKEN }}
270+
271+
- name: Compute the tags
272+
env:
273+
PHP_VERSION: ${{ inputs.php_version }}
274+
VARIANT: ${{ matrix.variant }}
275+
SUITE: ${{ matrix.suite }}
276+
UPDATE_FLOATING_TAGS: ${{ inputs.update_floating_tags }}
277+
run: scripts/compute-tags.sh "$PHP_VERSION" "$VARIANT" "$SUITE" "$UPDATE_FLOATING_TAGS" | tee "$RUNNER_TEMP/tags"
278+
279+
# The annotation links the multi-arch package to this repository on
280+
# GHCR, as the label does for the single-platform images.
281+
- name: Create the manifest list and apply the tags
282+
env:
283+
SOURCE: ${{ github.server_url }}/${{ github.repository }}
284+
run: |
285+
args=( --annotation "index:org.opencontainers.image.source=$SOURCE" )
286+
while read -r tag; do
287+
args+=( --tag "$tag" )
288+
done < "$RUNNER_TEMP/tags"
289+
for digest in "$RUNNER_TEMP"/digests/*; do
290+
args+=( "$IMAGE@sha256:${digest##*/}" )
291+
done
292+
docker buildx imagetools create "${args[@]}"
293+
294+
- name: Inspect the image
295+
run: docker buildx imagetools inspect "$(head -n 1 "$RUNNER_TEMP/tags")"

‎scripts/compute-tags.sh‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Prints the tags the merge job applies to one directory's multi-arch image,
4+
# one full image reference per line.
5+
#
6+
# The exact tag, <version>-<variant>-<suite>, is always printed. The floating
7+
# tags follow only when update_floating_tags is true, so rebuilding an old
8+
# patch release never moves them backwards: <branch>-<variant>-<suite>, the
9+
# suite-less <major>-<variant> and <variant>, and latest for cli alone, as the
10+
# official image does.
11+
#
12+
# The suite-less tags track whichever suite the run built on, so build.yml
13+
# refuses update_floating_tags together with a base override.
14+
#
15+
# Usage: scripts/compute-tags.sh <version> <variant> <suite> <update_floating_tags>
16+
# e.g. scripts/compute-tags.sh 8.4.13 fpm trixie false
17+
set -Eeuo pipefail
18+
19+
# shellcheck source=scripts/pin-version.sh
20+
source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/pin-version.sh"
21+
22+
# GHCR only accepts lowercase repository names. build.yml reads this too.
23+
IMAGE=ghcr.io/specsnl/php
24+
25+
LATEST_VARIANT=cli
26+
27+
# Replaces pin-version.sh's, which parse_version calls.
28+
usage() {
29+
echo >&2 "usage: ${0##*/} <version> <variant> <suite> <update_floating_tags> e.g. ${0##*/} 8.4.13 fpm trixie false"
30+
exit 2
31+
}
32+
33+
# Fails with usage unless a variant or suite can go into a tag as-is.
34+
check_tag_part() {
35+
local name="$1" value="$2"
36+
37+
if [[ ! "$value" =~ ^[a-z0-9][a-z0-9.]*$ ]]; then
38+
echo >&2 "error: '$value' is not a $name like fpm or trixie"
39+
usage
40+
fi
41+
}
42+
43+
# Succeeds when a run may move the floating tags. Pre-releases never do,
44+
# whatever update_floating_tags says; parse_version rejects them for now, so
45+
# this is where that rule takes effect once it accepts them.
46+
floating_tags_allowed() {
47+
local version="$1" update_floating_tags="$2"
48+
49+
[ "$update_floating_tags" = true ] && [[ ! "$version" =~ (alpha|beta|RC)[0-9]+$ ]]
50+
}
51+
52+
main() {
53+
[ "$#" -eq 4 ] || usage
54+
local version="$1" variant="$2" suite="$3" update_floating_tags="$4"
55+
56+
parse_version "$version"
57+
check_tag_part variant "$variant"
58+
check_tag_part suite "$suite"
59+
60+
case "$update_floating_tags" in
61+
true | false) ;;
62+
*)
63+
echo >&2 "error: update_floating_tags must be true or false, not '$update_floating_tags'"
64+
usage
65+
;;
66+
esac
67+
68+
echo "$IMAGE:$version-$variant-$suite"
69+
70+
if floating_tags_allowed "$version" "$update_floating_tags"; then
71+
echo "$IMAGE:$branch-$variant-$suite"
72+
echo "$IMAGE:${branch%%.*}-$variant"
73+
echo "$IMAGE:$variant"
74+
if [ "$variant" = "$LATEST_VARIANT" ]; then
75+
echo "$IMAGE:latest"
76+
fi
77+
fi
78+
}
79+
80+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
81+
main "$@"
82+
fi

0 commit comments

Comments
 (0)