Skip to content

Commit 07a7f2d

Browse files
committed
ci: add multiarch docker image test workflow
1 parent 51b3555 commit 07a7f2d

7 files changed

Lines changed: 663 additions & 0 deletions

File tree

.github/workflows/test.yml

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
# testing : https://github.com/postgis/docker-postgis/pull/432
2+
# original author: https://github.com/BowlesCR
3+
4+
# To pin GitHub Actions versions by commit hash: 'pinact run -u --min-age 7' ( https://github.com/suzuki-shunsuke/pinact )
5+
6+
name: TEST - Docker PostGIS CI
7+
# Serialize publish-capable runs per ref so scheduled/push/manual runs queue instead of racing.
8+
concurrency:
9+
group: test-${{ github.workflow }}-${{ github.ref }}
10+
cancel-in-progress: false
11+
12+
on:
13+
push:
14+
pull_request:
15+
workflow_dispatch:
16+
schedule:
17+
- cron: '15 13 * * *'
18+
19+
# ============================================================================
20+
# FOR FORKING: Modify these settings in your forked repository
21+
# ============================================================================
22+
env:
23+
DOCKERHUB_REPO: postgis/docker-postgis-test
24+
GITHUB_REPO: postgis/docker-postgis
25+
LATEST_VERSION: 17-3.5
26+
DOCKERHUB_SHORT_DESCRIPTION: "TEST REPO - PostGIS Docker"
27+
DOCKERHUB_README_PREFIX: "# WARNING: This is a TEST repository ONLY\n\n"
28+
RUNNER_PLATFORMS_JSON: '["ubuntu-24.04","ubuntu-24.04-arm"]' # Runner platforms used to expand the build matrix
29+
#
30+
# Also add these secrets in your repository settings:
31+
# https://github.com/${GITHUB_REPO}/settings/secrets/actions
32+
# - secrets.DOCKERHUB_USERNAME
33+
# - secrets.DOCKERHUB_ACCESS_TOKEN ( READ, Write, Delete access )
34+
# ============================================================================
35+
36+
defaults:
37+
run:
38+
shell: bash --noprofile --norc -euo pipefail {0}
39+
40+
jobs:
41+
42+
setup:
43+
# This job sets up configuration constants and loads the CI matrix from matrix.yml.
44+
# - Constants: CANONICAL_REPO and SHOULD_PUBLISH flag (fork-friendly configuration)
45+
# - Matrix: BUILD_TARGETS and BUILD_INCLUDE arrays (automatically generated by ./update.sh)
46+
name: Setup and Load Configuration
47+
runs-on: ubuntu-latest
48+
outputs:
49+
CANONICAL_REPO: ${{ steps.constants.outputs.CANONICAL_REPO }}
50+
SHOULD_PUBLISH: ${{ steps.constants.outputs.SHOULD_PUBLISH }}
51+
BUILD_INCLUDE: ${{ steps.matrix.outputs.BUILD_INCLUDE }}
52+
BUILD_TARGETS: ${{ steps.matrix.outputs.BUILD_TARGETS }}
53+
steps:
54+
- name: Set constants
55+
id: constants
56+
env:
57+
VAR_CANONICAL: ${{ vars.CANONICAL_REPO }}
58+
run: |
59+
CANONICAL_REPO="${VAR_CANONICAL:-$GITHUB_REPO}"
60+
echo "CANONICAL_REPO=$CANONICAL_REPO" >> "$GITHUB_OUTPUT"
61+
62+
# Compute if we should publish
63+
if [[ "${{ github.repository }}" == "$CANONICAL_REPO" ]] && \
64+
[[ "${{ github.ref }}" == "refs/heads/master" ]] && \
65+
[[ "${{ github.event_name }}" != "pull_request" ]]; then
66+
echo "SHOULD_PUBLISH=true" >> "$GITHUB_OUTPUT"
67+
else
68+
echo "SHOULD_PUBLISH=false" >> "$GITHUB_OUTPUT"
69+
fi
70+
71+
- name: Checkout repository
72+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
73+
74+
- name: Load and validate matrix
75+
id: matrix
76+
run: bash ci/matrix.sh
77+
78+
make-docker-images:
79+
needs: setup
80+
strategy:
81+
matrix:
82+
include: ${{ fromJSON(needs.setup.outputs.BUILD_INCLUDE) }}
83+
84+
name: "Build:${{ matrix.postgres }}-${{ matrix.postgis }}-${{ matrix.variant }} (${{ contains(matrix.runner-platform, 'arm') && 'arm64' || 'x86-64' }}) Docker image"
85+
runs-on: ${{ matrix.runner-platform }}
86+
continue-on-error: ${{ matrix.postgis == 'master' }}
87+
env:
88+
VERSION: ${{ matrix.postgres }}-${{ matrix.postgis }}
89+
VARIANT: ${{ matrix.variant }}
90+
# the "postgis/postgis" name is the expected test name; ( via ./test/postgis-config.sh )
91+
# changing it will break the official-images test script
92+
# this is only for CI test and not for Docker hub publishing
93+
CI_IMAGE_TAG: postgis/postgis:ci-${{ github.run_id }}-${{ matrix.postgres }}-${{ matrix.postgis }}-${{ matrix.variant }}-${{ contains(matrix.runner-platform, 'arm') && 'arm' || 'x64' }}
94+
95+
steps:
96+
- name: Checkout source
97+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
98+
99+
- name: Set up Docker Buildx
100+
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
101+
102+
- name: Compute build directory
103+
run: |
104+
if [[ "$VARIANT" == "alpine" ]]; then
105+
echo "BUILD_DIR=$VERSION/alpine" >> "$GITHUB_ENV"
106+
else
107+
echo "BUILD_DIR=$VERSION" >> "$GITHUB_ENV"
108+
fi
109+
110+
- name: Build Docker image for ${{ env.VERSION }} ${{ env.VARIANT }}
111+
id: build
112+
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
113+
with:
114+
context: ${{ env.BUILD_DIR }}
115+
file: ${{ env.BUILD_DIR }}/Dockerfile
116+
tags: ${{ env.CI_IMAGE_TAG }}
117+
load: true
118+
push: false # don't push until after testing
119+
120+
- name: Check out official-images repo
121+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
122+
with:
123+
repository: docker-library/official-images
124+
path: official-images
125+
sparse-checkout: |
126+
test
127+
128+
- name: Test image with official-images
129+
run: bash ci/test-image.sh "$CI_IMAGE_TAG"
130+
131+
- name: Login to dockerhub
132+
id: login-dockerhub
133+
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
134+
if: ${{ needs.setup.outputs.SHOULD_PUBLISH == 'true' }}
135+
with:
136+
username: ${{ secrets.DOCKERHUB_USERNAME }}
137+
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
138+
139+
- name: Push image by digest
140+
id: push
141+
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
142+
if: ${{ needs.setup.outputs.SHOULD_PUBLISH == 'true' && steps.login-dockerhub.outcome == 'success' }}
143+
with:
144+
context: ${{ env.BUILD_DIR }}
145+
file: ${{ env.BUILD_DIR }}/Dockerfile
146+
outputs: type=image,"name=${{ env.DOCKERHUB_REPO }}",push-by-digest=true,name-canonical=true,push=true
147+
148+
- name: Export digest
149+
if: ${{ steps.push.outcome == 'success' }}
150+
run: |
151+
mkdir -p ${{ runner.temp }}/digests
152+
digest="${{ steps.push.outputs.digest }}"
153+
touch "${{ runner.temp }}/digests/${digest#sha256:}"
154+
155+
- name: Upload digests
156+
if: ${{ steps.push.outcome == 'success' }}
157+
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
158+
with:
159+
name: digests-${{ github.run_id }}-${{ env.VERSION }}-${{ env.VARIANT }}-${{ matrix.runner-platform }}
160+
path: ${{ runner.temp }}/digests/*
161+
if-no-files-found: error
162+
retention-days: 10
163+
164+
merge-manifests:
165+
name: "Merge:${{ matrix.postgres }}-${{ matrix.postgis }}-${{ matrix.variant }} manifests and push to DockerHub"
166+
needs: [setup, make-docker-images]
167+
runs-on: ubuntu-24.04-arm # Run on arm runner for manifest merge
168+
if: ${{ needs.setup.outputs.SHOULD_PUBLISH == 'true' }}
169+
# Ensure each tag variant is published by only one workflow run at a time to keep manifests consistent.
170+
concurrency:
171+
group: merge-${{ matrix.postgres }}-${{ matrix.postgis }}-${{ matrix.variant }}
172+
cancel-in-progress: false
173+
continue-on-error: ${{ matrix.postgis == 'master' }}
174+
env:
175+
VERSION: ${{ matrix.postgres }}-${{ matrix.postgis }}
176+
VARIANT: ${{ matrix.variant }}
177+
strategy:
178+
matrix:
179+
include: ${{ fromJSON(needs.setup.outputs.BUILD_TARGETS) }}
180+
181+
steps:
182+
- name: Checkout source
183+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
184+
185+
- name: Login to dockerhub
186+
id: login-dockerhub
187+
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
188+
with:
189+
username: ${{ secrets.DOCKERHUB_USERNAME }}
190+
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
191+
192+
- name: Set up Docker Buildx
193+
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
194+
195+
- name: Download digests
196+
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
197+
with:
198+
path: ${{ runner.temp }}/digests
199+
pattern: digests-${{ github.run_id }}-${{ env.VERSION }}-${{ env.VARIANT }}-*
200+
merge-multiple: true
201+
202+
- name: Verify digests for all platforms
203+
working-directory: ${{ runner.temp }}/digests
204+
run: |
205+
expected="$(jq 'length' <<< "$RUNNER_PLATFORMS_JSON")"
206+
found="$(find . -maxdepth 1 -type f | wc -l | tr -d '[:space:]')"
207+
if [[ "$found" -ne "$expected" ]]; then
208+
echo "ERROR: Expected ${expected} digest(s) (one per runner platform), found ${found}."
209+
echo "RUNNER_PLATFORMS_JSON=${RUNNER_PLATFORMS_JSON}"
210+
ls -la
211+
exit 1
212+
fi
213+
echo "[OK] Found ${found}/${expected} digests"
214+
215+
- name: Create manifest list and push
216+
env:
217+
MATRIX_TAGS: ${{ matrix.tags }}
218+
run: |
219+
build_month="$(date -u +%Y%m)"
220+
read -r -a tags_arr <<< "$MATRIX_TAGS"
221+
extra_tags=""
222+
if [[ "${#tags_arr[@]}" -ge 2 ]]; then
223+
extra_tags+=" ${tags_arr[1]}-${build_month}"
224+
fi
225+
bash ci/push-manifest.sh "${{ env.DOCKERHUB_REPO }}" "${MATRIX_TAGS}${extra_tags}" "${{ runner.temp }}/digests"
226+
227+
- name: Inspect image # Purely for debugging
228+
run: |
229+
sleep 5
230+
docker buildx imagetools inspect ${{ env.DOCKERHUB_REPO }}:${{ env.VERSION }}${{ env.VARIANT == 'alpine' && '-alpine' || ''}}
231+
232+
dockerHubDescription:
233+
needs: [merge-manifests]
234+
runs-on: ubuntu-latest
235+
steps:
236+
- name: Checkout source
237+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
238+
239+
- name: Prepare README with prefix (create ./_DOCKER-HUB-README.md )
240+
run: bash ci/prepare-dockerhub-readme.sh && ls -la ./_DOCKER-HUB-README.md
241+
242+
- name: Debug ./_DOCKER-HUB-README.md
243+
run: cat ./_DOCKER-HUB-README.md
244+
- name: Update Docker Hub Description
245+
uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
246+
with:
247+
username: ${{ secrets.DOCKERHUB_USERNAME }}
248+
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
249+
repository: ${{ env.DOCKERHUB_REPO }}
250+
short-description: "${{ env.DOCKERHUB_SHORT_DESCRIPTION }}"
251+
readme-filepath: ./_DOCKER-HUB-README.md

ci/matrix.sh

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env bash
2+
#
3+
# matrix.sh - Parse matrix.yml and output build targets for CI workflows
4+
#
5+
# Called by: .github/workflows/*.yml
6+
# Outputs: BUILD_TARGETS and BUILD_INCLUDE for GitHub Actions matrix strategy
7+
#
8+
set -Eeuo pipefail
9+
10+
# --- Logging (CI-only, no colors) ---
11+
log_info() { echo "[INFO] $*" >&2; }
12+
log_warn() { echo "[WARN] $*" >&2; }
13+
log_error() { echo "[ERROR] $*" >&2; }
14+
die() { log_error "$1"; exit "${2:-1}"; }
15+
16+
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
17+
cd "$repo_root"
18+
19+
readonly MATRIX_FILE="matrix.yml"
20+
21+
# Environment variables (set by CI workflow)
22+
runner_platforms_json="${RUNNER_PLATFORMS_JSON:-}"
23+
github_output_file="${GITHUB_OUTPUT:-}"
24+
25+
# Write a line to GitHub Actions output file, or stdout if not in CI
26+
set_github_output() {
27+
local output_line="$1"
28+
if [[ -n "$github_output_file" ]]; then
29+
echo "$output_line" >> "$github_output_file"
30+
else
31+
echo "$output_line"
32+
fi
33+
}
34+
35+
# --- Input Validation ---
36+
if [[ ! -f "$MATRIX_FILE" ]]; then
37+
die "$MATRIX_FILE not found in repo root"
38+
fi
39+
40+
if [[ -z "$runner_platforms_json" ]]; then
41+
die "RUNNER_PLATFORMS_JSON environment variable is required"
42+
fi
43+
44+
if ! command -v ruby >/dev/null 2>&1; then
45+
die "ruby is required to parse ${MATRIX_FILE}"
46+
fi
47+
48+
# --- Parse Matrix File ---
49+
build_targets="$(
50+
ruby -ryaml -rjson -e '
51+
matrix = YAML.load_file(ARGV.fetch(0))
52+
targets = matrix.fetch("build_targets")
53+
puts JSON.generate(targets)
54+
' "$MATRIX_FILE"
55+
)"
56+
set_github_output "BUILD_TARGETS=$build_targets"
57+
58+
# Expand build_targets with runner platforms to create full build matrix.
59+
# Each target is combined with each platform to create BUILD_INCLUDE entries.
60+
runner_platforms="$(jq -c '.' <<< "$runner_platforms_json")"
61+
build_include="$(jq -c --argjson platforms "$runner_platforms" '
62+
[ .[] as $combo | $platforms[] | $combo + {"runner-platform": .} ]
63+
' <<< "$build_targets")"
64+
set_github_output "BUILD_INCLUDE=$build_include"
65+
66+
target_count="$(jq 'length' <<< "$build_targets")"
67+
include_count="$(jq 'length' <<< "$build_include")"
68+
log_info "Loaded BUILD_TARGETS with ${target_count} entries"
69+
log_info "Expanded BUILD_INCLUDE with ${include_count} entries (targets x platforms)"
70+
71+
# --- Validation ---
72+
log_info "Validating ./${MATRIX_FILE}..."
73+
74+
# 1. Check build_targets is not empty
75+
if [[ "$target_count" -eq 0 ]]; then
76+
die "matrix.yml has no build_targets"
77+
fi
78+
79+
# 2. Check required fields: postgres, postgis, variant, tags (all must be non-empty)
80+
invalid_entries="$(jq -c '
81+
[ .[] | select(
82+
.postgres == null or .postgres == "" or
83+
.postgis == null or .postgis == "" or
84+
.variant == null or .variant == "" or
85+
.tags == null or .tags == ""
86+
)]
87+
' <<< "$build_targets")"
88+
89+
invalid_count="$(jq 'length' <<< "$invalid_entries")"
90+
if [[ "$invalid_count" -gt 0 ]]; then
91+
log_error "Found ${invalid_count} entries with missing or empty required fields (postgres/postgis/variant/tags):"
92+
jq '.' <<< "$invalid_entries" >&2
93+
exit 1
94+
fi
95+
96+
# 3. Verify exactly one entry has 'latest' tag (prevents accidental duplicate latest)
97+
latest_count="$(jq '
98+
[ .[] | select(.tags | tostring | test("(^| )latest( |$)")) ] | length
99+
' <<< "$build_targets")"
100+
101+
if [[ "$latest_count" -ne 1 ]]; then
102+
log_error "Expected exactly 1 entry with 'latest' tag, found: $latest_count"
103+
jq -r '.[] | select(.tags | tostring | test("(^| )latest( |$)"))' <<< "$build_targets" >&2
104+
exit 1
105+
fi
106+
107+
# 4. Verify exactly one entry has 'alpine' tag (the alpine equivalent of 'latest')
108+
alpine_count="$(jq '
109+
[ .[] | select(.tags | tostring | test("(^| )alpine( |$)")) ] | length
110+
' <<< "$build_targets")"
111+
112+
if [[ "$alpine_count" -ne 1 ]]; then
113+
log_error "Expected exactly 1 entry with 'alpine' tag, found: $alpine_count"
114+
jq -r '.[] | select(.tags | tostring | test("(^| )alpine( |$)"))' <<< "$build_targets" >&2
115+
exit 1
116+
fi
117+
118+
log_info "[OK] matrix.yml valid: ${target_count} targets, all have required fields, 1 'latest' tag, 1 'alpine' tag"

0 commit comments

Comments
 (0)