Skip to content

Commit b0dfb06

Browse files
committed
Publish Alpine Docker images
1 parent 1e0b2c1 commit b0dfb06

6 files changed

Lines changed: 279 additions & 183 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: 109 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,50 @@ 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+
action: ${{ steps.plan.outputs.action }}
2830
steps:
2931
- name: Plan
3032
id: plan
3133
shell: bash
3234
env:
33-
HAS_PLAN: ${{ inputs.plan != '' }}
34-
HAS_IMPLICIT_TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag_is_implicit }}
35+
DRY_RUN: ${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }}
36+
TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag }}
3537
run: |
36-
[[ "$HAS_PLAN" == "true" && "$HAS_IMPLICIT_TAG" != "true" ]] && publish=1
37-
38-
if [[ "$publish" ]]; then
39-
echo "publish=true" >> "$GITHUB_OUTPUT"
38+
if [[ "$DRY_RUN" == "false" ]]; then
39+
version="${TAG#v}"
40+
echo "push=true" >> "$GITHUB_OUTPUT"
41+
echo "tag=${version}" >> "$GITHUB_OUTPUT"
42+
echo "base-tag=${version}" >> "$GITHUB_OUTPUT"
43+
echo "action=Build and publish" >> "$GITHUB_OUTPUT"
4044
else
41-
echo "publish=false" >> "$GITHUB_OUTPUT"
45+
echo "push=false" >> "$GITHUB_OUTPUT"
46+
echo "tag=dry-run" >> "$GITHUB_OUTPUT"
47+
echo "base-tag=latest" >> "$GITHUB_OUTPUT"
48+
echo "action=Build" >> "$GITHUB_OUTPUT"
4249
fi
4350
44-
docker-build:
45-
name: Build Docker image ghcr.io/j178/prek for ${{ matrix.platform }}
51+
docker-publish-base:
52+
name: ${{ needs.plan.outputs.action }} Docker image
4653
needs: plan
4754
runs-on: ubuntu-latest
4855
environment:
4956
name: release
50-
strategy:
51-
fail-fast: false
52-
matrix:
53-
platform:
54-
- linux/amd64
55-
- linux/arm64
57+
permissions:
58+
contents: read
59+
packages: write
60+
id-token: write
61+
attestations: write
5662
steps:
5763
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
5864
with:
@@ -61,19 +67,19 @@ jobs:
6167
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
6268

6369
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
64-
if: ${{ needs.plan.outputs.publish == 'true' }}
70+
if: ${{ needs.plan.outputs.push == 'true' }}
6571
with:
6672
registry: ghcr.io
6773
username: ${{ github.repository_owner }}
6874
password: ${{ secrets.GITHUB_TOKEN }}
6975

7076
- name: Check tag consistency
71-
if: ${{ needs.plan.outputs.publish == 'true' }}
77+
if: ${{ needs.plan.outputs.push == 'true' }}
7278
env:
73-
TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }}
79+
TAG: ${{ needs.plan.outputs.tag }}
7480
run: |
7581
version=$(grep -m 1 "^version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
76-
if [ "${TAG}" != "v${version}" ]; then
82+
if [ "${TAG}" != "${version}" ]; then
7783
echo "The input tag does not match the version from pyproject.toml:" >&2
7884
echo "${TAG}" >&2
7985
echo "${version}" >&2
@@ -91,114 +97,120 @@ jobs:
9197
images: ${{ env.PREK_BASE_IMG }}
9298
# Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name
9399
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' }}
100+
type=raw,value=dry-run,enable=${{ needs.plan.outputs.push == 'false' }}
101+
type=semver,pattern={{ raw }},value=${{ needs.plan.outputs.tag }},enable=${{ needs.plan.outputs.push == 'true' }}
102+
type=semver,pattern={{ major }}.{{ minor }},value=${{ needs.plan.outputs.tag }},enable=${{ needs.plan.outputs.push == 'true' }}
96103
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
104+
- name: Build and push
104105
id: build
105106
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
106107
with:
107108
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 }}
109+
platforms: linux/amd64,linux/arm64
110+
cache-from: type=gha,scope=prek
111+
cache-to: type=gha,mode=min,scope=prek
112+
push: ${{ needs.plan.outputs.push }}
113+
tags: ${{ steps.meta.outputs.tags }}
111114
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:}"
115+
annotations: ${{ steps.meta.outputs.annotations }}
116+
provenance: mode=max
117+
sbom: true
120118

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

129-
docker-publish:
130-
name: Publish Docker image (ghcr.io/j178/prek)
127+
docker-alpine:
128+
name: ${{ needs.plan.outputs.action }} Alpine Docker image
131129
runs-on: ubuntu-latest
132130
environment:
133131
name: release
134132
needs:
135133
- plan
136-
- docker-build
134+
- docker-publish-base
137135
permissions:
138136
contents: read
139137
packages: write
140138
id-token: write
141139
attestations: write
142-
if: ${{ needs.plan.outputs.publish == 'true' }}
140+
if: ${{ needs.plan.result == 'success' && needs.docker-publish-base.result == 'success' }}
143141
steps:
144-
- name: Download digests
145-
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
142+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
146143
with:
147-
path: /tmp/digests
148-
pattern: digests-*
149-
merge-multiple: true
150-
151-
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
144+
persist-credentials: false
152145

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
146+
# Installing Alpine packages executes target-architecture binaries.
147+
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
158148
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 }}
149+
platforms: arm64
150+
151+
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
164152

165153
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
154+
if: ${{ needs.plan.outputs.push == 'true' }}
166155
with:
167156
registry: ghcr.io
168157
username: ${{ github.repository_owner }}
169158
password: ${{ secrets.GITHUB_TOKEN }}
170159

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> ...`
160+
- name: Generate Alpine Dockerfile
161+
env:
162+
PREK_IMAGE: ${{ format('{0}:{1}', env.PREK_BASE_IMG, needs.plan.outputs.base-tag) }}
177163
run: |
178-
# shellcheck disable=SC2046
179-
readarray -t lines <<< "$DOCKER_METADATA_OUTPUT_ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done
164+
cat <<EOF > Dockerfile.alpine
165+
FROM ${PREK_IMAGE} AS prek
166+
FROM alpine:${ALPINE_VERSION}
167+
168+
RUN apk add --no-cache \
169+
ca-certificates \
170+
git
180171
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 " *)
172+
COPY --from=prek /prek /usr/local/bin/prek
173+
WORKDIR /io
174+
ENTRYPOINT ["/usr/local/bin/prek"]
175+
EOF
185176
186-
- name: Export manifest digest
187-
id: manifest-digest
177+
- name: Extract metadata (tags, labels) for Docker
178+
id: meta
179+
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
188180
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"
181+
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
182+
with:
183+
images: ${{ env.PREK_BASE_IMG }}
184+
flavor: latest=false
185+
tags: |
186+
type=raw,value=dry-run-alpine,enable=${{ needs.plan.outputs.push == 'false' }}
187+
type=semver,pattern={{ raw }},suffix=-alpine${{ env.ALPINE_VERSION }},value=${{ needs.plan.outputs.tag }},enable=${{ needs.plan.outputs.push == 'true' }}
188+
type=semver,pattern={{ major }}.{{ minor }},suffix=-alpine${{ env.ALPINE_VERSION }},value=${{ needs.plan.outputs.tag }},enable=${{ needs.plan.outputs.push == 'true' }}
189+
type=raw,value=alpine${{ env.ALPINE_VERSION }},enable=${{ needs.plan.outputs.push == 'true' }}
190+
type=semver,pattern={{ raw }},suffix=-alpine,value=${{ needs.plan.outputs.tag }},enable=${{ needs.plan.outputs.push == 'true' }}
191+
type=semver,pattern={{ major }}.{{ minor }},suffix=-alpine,value=${{ needs.plan.outputs.tag }},enable=${{ needs.plan.outputs.push == 'true' }}
192+
type=raw,value=alpine,enable=${{ needs.plan.outputs.push == 'true' }}
193+
194+
- name: Build and push
195+
id: build
196+
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
197+
with:
198+
context: .
199+
file: Dockerfile.alpine
200+
platforms: linux/amd64,linux/arm64
201+
cache-from: type=gha,scope=prek-alpine
202+
cache-to: type=gha,mode=min,scope=prek-alpine
203+
push: ${{ needs.plan.outputs.push }}
204+
tags: ${{ steps.meta.outputs.tags }}
205+
labels: ${{ steps.meta.outputs.labels }}
206+
annotations: ${{ steps.meta.outputs.annotations }}
207+
provenance: mode=max
208+
sbom: true
199209

200210
- name: Generate artifact attestation
211+
if: ${{ needs.plan.outputs.push == 'true' }}
201212
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
202213
with:
203214
subject-name: ${{ env.PREK_BASE_IMG }}
204-
subject-digest: ${{ steps.manifest-digest.outputs.digest }}
215+
subject-digest: ${{ steps.build.outputs.digest }}
216+
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)