Skip to content

Commit e5cd924

Browse files
j178pat-s
andcommitted
Publish Alpine Docker images
Co-authored-by: Patrick Schratz <patrick.schratz@gmail.com>
1 parent 1e0b2c1 commit e5cd924

6 files changed

Lines changed: 289 additions & 184 deletions

File tree

.github/renovate.json5

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
'pre-commit',
3333
'cargo',
3434
'custom.regex',
35+
'dockerfile',
3536
'pep621',
3637
],
3738
customManagers: [
@@ -133,7 +134,7 @@
133134
commitMessageTopic: "Rust",
134135
},
135136
{
136-
matchManagers: [ 'cargo', 'pre-commit', 'github-actions', 'pep621', 'custom.regex' ],
137+
matchManagers: [ 'cargo', 'pre-commit', 'github-actions', 'pep621', 'custom.regex', 'dockerfile' ],
137138
minimumReleaseAge: '7 days'
138139
},
139140
{

.github/workflows/build-docker.yml

Lines changed: 117 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# Build and publish a Docker image.
1+
# Build and publish Docker images.
22
#
33
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a local
44
# artifacts job within `cargo-dist`.
55
#
66
# Adapted from https://github.com/astral-sh/ty/blob/main/.github/workflows/build-docker.yml
7-
name: "Build Docker image"
7+
name: "Build Docker images"
88

99
on:
1010
workflow_call:
@@ -15,44 +15,58 @@ on:
1515

1616
env:
1717
PREK_BASE_IMG: ghcr.io/${{ github.repository_owner }}/prek
18+
ALPINE_VERSION: "3.24"
1819

19-
permissions:
20-
contents: read
21-
packages: write # zizmor: ignore[excessive-permissions]
20+
permissions: {}
2221

2322
jobs:
2423
plan:
2524
runs-on: ubuntu-slim
2625
outputs:
27-
publish: ${{ steps.plan.outputs.publish }}
26+
push: ${{ steps.plan.outputs.push }}
27+
tag: ${{ steps.plan.outputs.tag }}
28+
base-tag: ${{ steps.plan.outputs.base-tag }}
29+
floating-tags: ${{ steps.plan.outputs.floating-tags }}
30+
action: ${{ steps.plan.outputs.action }}
2831
steps:
2932
- name: Plan
3033
id: plan
3134
shell: bash
3235
env:
33-
HAS_PLAN: ${{ inputs.plan != '' }}
34-
HAS_IMPLICIT_TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag_is_implicit }}
36+
DRY_RUN: ${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }}
37+
PRERELEASE: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_is_prerelease }}
38+
TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag }}
3539
run: |
36-
[[ "$HAS_PLAN" == "true" && "$HAS_IMPLICIT_TAG" != "true" ]] && publish=1
37-
38-
if [[ "$publish" ]]; then
39-
echo "publish=true" >> "$GITHUB_OUTPUT"
40+
if [[ "$DRY_RUN" == "false" ]]; then
41+
version="${TAG#v}"
42+
echo "push=true" >> "$GITHUB_OUTPUT"
43+
echo "tag=${version}" >> "$GITHUB_OUTPUT"
44+
echo "base-tag=${version}" >> "$GITHUB_OUTPUT"
45+
echo "action=Build and publish" >> "$GITHUB_OUTPUT"
46+
if [[ "$PRERELEASE" == "true" ]]; then
47+
echo "floating-tags=false" >> "$GITHUB_OUTPUT"
48+
else
49+
echo "floating-tags=true" >> "$GITHUB_OUTPUT"
50+
fi
4051
else
41-
echo "publish=false" >> "$GITHUB_OUTPUT"
52+
echo "push=false" >> "$GITHUB_OUTPUT"
53+
echo "tag=dry-run" >> "$GITHUB_OUTPUT"
54+
echo "base-tag=latest" >> "$GITHUB_OUTPUT"
55+
echo "floating-tags=false" >> "$GITHUB_OUTPUT"
56+
echo "action=Build" >> "$GITHUB_OUTPUT"
4257
fi
4358
44-
docker-build:
45-
name: Build Docker image ghcr.io/j178/prek for ${{ matrix.platform }}
59+
docker-publish-base:
60+
name: ${{ needs.plan.outputs.action }} Docker image
4661
needs: plan
4762
runs-on: ubuntu-latest
4863
environment:
4964
name: release
50-
strategy:
51-
fail-fast: false
52-
matrix:
53-
platform:
54-
- linux/amd64
55-
- linux/arm64
65+
permissions:
66+
contents: read
67+
packages: write
68+
id-token: write
69+
attestations: write
5670
steps:
5771
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
5872
with:
@@ -61,19 +75,19 @@ jobs:
6175
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
6276

6377
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
64-
if: ${{ needs.plan.outputs.publish == 'true' }}
78+
if: ${{ needs.plan.outputs.push == 'true' }}
6579
with:
6680
registry: ghcr.io
6781
username: ${{ github.repository_owner }}
6882
password: ${{ secrets.GITHUB_TOKEN }}
6983

7084
- name: Check tag consistency
71-
if: ${{ needs.plan.outputs.publish == 'true' }}
85+
if: ${{ needs.plan.outputs.push == 'true' }}
7286
env:
73-
TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }}
87+
TAG: ${{ needs.plan.outputs.tag }}
7488
run: |
7589
version=$(grep -m 1 "^version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
76-
if [ "${TAG}" != "v${version}" ]; then
90+
if [ "${TAG}" != "${version}" ]; then
7791
echo "The input tag does not match the version from pyproject.toml:" >&2
7892
echo "${TAG}" >&2
7993
echo "${version}" >&2
@@ -91,114 +105,120 @@ jobs:
91105
images: ${{ env.PREK_BASE_IMG }}
92106
# Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name
93107
tags: |
94-
type=raw,value=dry-run,enable=${{ needs.plan.outputs.publish != 'true' }}
95-
type=semver,pattern={{ raw }},value=${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }},enable=${{ needs.plan.outputs.publish == 'true' }}
108+
type=raw,value=dry-run,enable=${{ needs.plan.outputs.push == 'false' }}
109+
type=semver,pattern={{ raw }},value=${{ needs.plan.outputs.tag }},enable=${{ needs.plan.outputs.push == 'true' }}
110+
type=semver,pattern={{ major }}.{{ minor }},value=${{ needs.plan.outputs.tag }},enable=${{ needs.plan.outputs.push == 'true' }}
96111
97-
- name: Normalize Platform Pair (replace / with -)
98-
run: |
99-
platform=${{ matrix.platform }}
100-
echo "PLATFORM_TUPLE=${platform//\//-}" >> "$GITHUB_ENV"
101-
102-
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
103-
- name: Build and push by digest
112+
- name: Build and push
104113
id: build
105114
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
106115
with:
107116
context: .
108-
platforms: ${{ matrix.platform }}
109-
cache-from: type=gha,scope=prek-${{ env.PLATFORM_TUPLE }}
110-
cache-to: type=gha,mode=min,scope=prek-${{ env.PLATFORM_TUPLE }}
117+
platforms: linux/amd64,linux/arm64
118+
cache-from: type=gha,scope=prek
119+
cache-to: type=gha,mode=min,scope=prek
120+
push: ${{ needs.plan.outputs.push }}
121+
tags: ${{ steps.meta.outputs.tags }}
111122
labels: ${{ steps.meta.outputs.labels }}
112-
outputs: type=image,name=${{ env.PREK_BASE_IMG }},push-by-digest=true,name-canonical=true,push=${{ needs.plan.outputs.publish == 'true' }}
113-
114-
- name: Export digests
115-
env:
116-
digest: ${{ steps.build.outputs.digest }}
117-
run: |
118-
mkdir -p /tmp/digests
119-
touch "/tmp/digests/${digest#sha256:}"
123+
annotations: ${{ steps.meta.outputs.annotations }}
124+
provenance: mode=max
125+
sbom: true
120126

121-
- name: Upload digests
122-
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
127+
- name: Generate artifact attestation
128+
if: ${{ needs.plan.outputs.push == 'true' }}
129+
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
123130
with:
124-
name: digests-${{ env.PLATFORM_TUPLE }}
125-
path: /tmp/digests/*
126-
if-no-files-found: error
127-
retention-days: 1
131+
subject-name: ${{ env.PREK_BASE_IMG }}
132+
subject-digest: ${{ steps.build.outputs.digest }}
133+
push-to-registry: true
128134

129-
docker-publish:
130-
name: Publish Docker image (ghcr.io/j178/prek)
135+
docker-publish-alpine:
136+
name: ${{ needs.plan.outputs.action }} Alpine Docker image
131137
runs-on: ubuntu-latest
132138
environment:
133139
name: release
134140
needs:
135141
- plan
136-
- docker-build
142+
- docker-publish-base
137143
permissions:
138144
contents: read
139145
packages: write
140146
id-token: write
141147
attestations: write
142-
if: ${{ needs.plan.outputs.publish == 'true' }}
148+
if: ${{ needs.plan.result == 'success' && needs.docker-publish-base.result == 'success' }}
143149
steps:
144-
- name: Download digests
145-
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
150+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
146151
with:
147-
path: /tmp/digests
148-
pattern: digests-*
149-
merge-multiple: true
150-
151-
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
152+
persist-credentials: false
152153

153-
- name: Extract metadata (tags, labels) for Docker
154-
id: meta
155-
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
156-
env:
157-
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
154+
# Installing Alpine packages executes target-architecture binaries.
155+
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
158156
with:
159-
images: ${{ env.PREK_BASE_IMG }}
160-
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
161-
tags: |
162-
type=raw,value=${{ fromJson(inputs.plan).announcement_tag }}
163-
type=semver,pattern=v{{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
157+
platforms: arm64
158+
159+
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
164160

165161
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
162+
if: ${{ needs.plan.outputs.push == 'true' }}
166163
with:
167164
registry: ghcr.io
168165
username: ${{ github.repository_owner }}
169166
password: ${{ secrets.GITHUB_TOKEN }}
170167

171-
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
172-
- name: Create manifest list and push
173-
working-directory: /tmp/digests
174-
# The jq command expands the docker/metadata json "tags" array entry to `-t tag1 -t tag2 ...` for each tag in the array
175-
# The printf will expand the base image with the `<PREK_BASE_IMG>@sha256:<sha256> ...` for each sha256 in the directory
176-
# The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <PREK_BASE_IMG>@sha256:<sha256_1> <PREK_BASE_IMG>@sha256:<sha256_2> ...`
168+
- name: Generate Alpine Dockerfile
169+
env:
170+
PREK_IMAGE: ${{ format('{0}:{1}', env.PREK_BASE_IMG, needs.plan.outputs.base-tag) }}
177171
run: |
178-
# shellcheck disable=SC2046
179-
readarray -t lines <<< "$DOCKER_METADATA_OUTPUT_ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done
172+
cat <<EOF > Dockerfile.alpine
173+
FROM ${PREK_IMAGE} AS prek
174+
FROM alpine:${ALPINE_VERSION}
175+
176+
RUN apk add --no-cache \
177+
ca-certificates \
178+
git
180179
181-
docker buildx imagetools create \
182-
"${annotations[@]}" \
183-
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
184-
$(printf "${PREK_BASE_IMG}@sha256:%s " *)
180+
COPY --from=prek /prek /usr/local/bin/prek
181+
WORKDIR /io
182+
ENTRYPOINT ["/usr/local/bin/prek"]
183+
EOF
185184
186-
- name: Export manifest digest
187-
id: manifest-digest
185+
- name: Extract metadata (tags, labels) for Docker
186+
id: meta
187+
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
188188
env:
189-
IMAGE: ${{ env.PREK_BASE_IMG }}
190-
VERSION: ${{ steps.meta.outputs.version }}
191-
run: |
192-
digest="$(
193-
docker buildx imagetools inspect \
194-
"${IMAGE}:${VERSION}" \
195-
--format '{{json .Manifest}}' \
196-
| jq -r '.digest'
197-
)"
198-
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
189+
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
190+
with:
191+
images: ${{ env.PREK_BASE_IMG }}
192+
flavor: latest=false
193+
tags: |
194+
type=raw,value=dry-run-alpine,enable=${{ needs.plan.outputs.push == 'false' }}
195+
type=semver,pattern={{ raw }},suffix=-alpine${{ env.ALPINE_VERSION }},value=${{ needs.plan.outputs.tag }},enable=${{ needs.plan.outputs.push == 'true' }}
196+
type=semver,pattern={{ major }}.{{ minor }},suffix=-alpine${{ env.ALPINE_VERSION }},value=${{ needs.plan.outputs.tag }},enable=${{ needs.plan.outputs.push == 'true' }}
197+
type=raw,value=alpine${{ env.ALPINE_VERSION }},enable=${{ needs.plan.outputs.floating-tags == 'true' }}
198+
type=semver,pattern={{ raw }},suffix=-alpine,value=${{ needs.plan.outputs.tag }},enable=${{ needs.plan.outputs.push == 'true' }}
199+
type=semver,pattern={{ major }}.{{ minor }},suffix=-alpine,value=${{ needs.plan.outputs.tag }},enable=${{ needs.plan.outputs.push == 'true' }}
200+
type=raw,value=alpine,enable=${{ needs.plan.outputs.floating-tags == 'true' }}
201+
202+
- name: Build and push
203+
id: build
204+
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
205+
with:
206+
context: .
207+
file: Dockerfile.alpine
208+
platforms: linux/amd64,linux/arm64
209+
cache-from: type=gha,scope=prek-alpine
210+
cache-to: type=gha,mode=min,scope=prek-alpine
211+
push: ${{ needs.plan.outputs.push }}
212+
tags: ${{ steps.meta.outputs.tags }}
213+
labels: ${{ steps.meta.outputs.labels }}
214+
annotations: ${{ steps.meta.outputs.annotations }}
215+
provenance: mode=max
216+
sbom: true
199217

200218
- name: Generate artifact attestation
219+
if: ${{ needs.plan.outputs.push == 'true' }}
201220
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
202221
with:
203222
subject-name: ${{ env.PREK_BASE_IMG }}
204-
subject-digest: ${{ steps.manifest-digest.outputs.digest }}
223+
subject-digest: ${{ steps.build.outputs.digest }}
224+
push-to-registry: true

Dockerfile

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
1-
FROM --platform=$BUILDPLATFORM ubuntu AS build
1+
FROM --platform=$BUILDPLATFORM ghcr.io/astral-sh/uv:0.11.28@sha256:0f36cb9361a3346885ca3677e3767016687b5a170c1a6b88465ec14aefec90aa AS uv
2+
3+
FROM --platform=$BUILDPLATFORM ubuntu:24.04@sha256:d1e2e92c075e5ca139d51a140fff46f84315c0fdce203eab2807c7e495eff4f9 AS build
4+
5+
ARG UBUNTU_SNAPSHOT=20260301T000000Z
6+
ARG RUSTUP_VERSION=1.28.2
7+
28
ENV HOME="/root"
39
WORKDIR $HOME
410

5-
RUN apt update \
6-
&& apt install -y --no-install-recommends \
11+
# Retry apt downloads to handle transient mirror failures.
12+
RUN echo 'Acquire::Retries "3";' > /etc/apt/apt.conf.d/80-retries
13+
14+
# Install dependencies from an Ubuntu snapshot for reproducibility.
15+
RUN --mount=type=cache,target=/var/lib/apt/lists \
16+
apt install -y --update ca-certificates \
17+
&& apt install -y --update --snapshot ${UBUNTU_SNAPSHOT} --no-install-recommends \
718
build-essential \
8-
curl \
9-
python3-venv \
10-
&& apt clean \
11-
&& rm -rf /var/lib/apt/lists/*
19+
curl
20+
21+
# Install uv
22+
COPY --from=uv /uv /usr/local/bin/uv
1223

1324
# Setup zig as cross compiling linker
14-
RUN python3 -m venv $HOME/.venv
15-
RUN .venv/bin/pip install cargo-zigbuild
25+
COPY pyproject.toml uv.lock ./
26+
RUN uv sync --only-group docker --locked
1627
ENV PATH="$HOME/.venv/bin:$PATH"
1728

1829
# Install rust
@@ -23,11 +34,17 @@ RUN case "$TARGETPLATFORM" in \
2334
*) exit 1 ;; \
2435
esac
2536

26-
# Update rustup whenever we bump the rust version
27-
COPY rust-toolchain.toml rust-toolchain.toml
28-
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --target $(cat rust_target.txt) --profile minimal --default-toolchain none
37+
# Install a pinned rustup release.
38+
RUN curl --proto '=https' --tlsv1.2 -sSf \
39+
"https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/$(uname -m)-unknown-linux-gnu/rustup-init" \
40+
-o rustup-init \
41+
&& chmod +x rustup-init \
42+
&& ./rustup-init -y --target $(cat rust_target.txt) --profile minimal --default-toolchain none \
43+
&& rm rustup-init
2944
ENV PATH="$HOME/.cargo/bin:$PATH"
30-
# Install the toolchain then the musl target
45+
46+
# Install the toolchain in the musl target
47+
COPY rust-toolchain.toml rust-toolchain.toml
3148
RUN rustup toolchain install
3249
RUN rustup target add $(cat rust_target.txt)
3350

0 commit comments

Comments
 (0)