Skip to content

Commit 1799bbd

Browse files
jamesli124claude
andcommitted
ci(docker): build, test and publish the container image
Builds the image on every PR and runs the packaged test suite inside it, which proves the environment is correct rather than merely importable. On pushes to main and on v* tags it publishes to GHCR, making the image a release artifact alongside the PyPI wheel from python-publish.yml. The version is resolved on the runner and passed as a build arg, mirroring docker/build.sh, because .git is excluded from the build context. The step fails outright if setuptools-scm yields the 0.0.0.dev0 fallback rather than publishing an untraceable image. `latest` is applied to v* tags only, not to main. A user pulling :latest should get the newest release, never a dev build off the default branch; main pushes get the `main` tag and a sha tag instead. The image name is lowercased explicitly since GHCR rejects uppercase and this repository is epifluidlab/FinaleToolkit. Verified locally against a real build: the image builds clean with no apt packages (every manylinux wheel is self-contained on python:3.12-slim), reports 276MB, runs as a non-root user, and the containerized suite matches the host exactly at 225 passed / 11 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent fc835a3 commit 1799bbd

1 file changed

Lines changed: 127 additions & 0 deletions

File tree

.github/workflows/docker.yml

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Builds the container image defined in the repository root Dockerfile,
2+
# runs the test suite inside it, and publishes to GitHub Container Registry.
3+
#
4+
# The image is a release artifact alongside the PyPI wheel published by
5+
# python-publish.yml, and is tagged from the same git tags.
6+
7+
name: Docker image
8+
9+
on:
10+
push:
11+
branches: [main]
12+
tags: ["v*"]
13+
pull_request:
14+
workflow_dispatch:
15+
16+
env:
17+
REGISTRY: ghcr.io
18+
19+
permissions:
20+
contents: read
21+
22+
jobs:
23+
build:
24+
runs-on: ubuntu-latest
25+
permissions:
26+
contents: read
27+
# Required to push to GHCR. Pull requests from forks get a read-only
28+
# token, which is why the push step below is gated on the event type.
29+
packages: write
30+
31+
steps:
32+
- uses: actions/checkout@v4
33+
with:
34+
fetch-depth: 0 # full history + tags, needed by setuptools-scm to derive the version
35+
36+
# .git is excluded from the Docker build context, so setuptools-scm
37+
# cannot derive the version inside the container. Resolve it here and
38+
# pass it in as a build arg, exactly as docker/build.sh does locally.
39+
- uses: actions/setup-python@v5
40+
with:
41+
python-version: "3.x"
42+
43+
- name: Resolve package version
44+
id: version
45+
run: |
46+
python -m pip install --quiet setuptools-scm
47+
version="$(python -m setuptools_scm)"
48+
if [ -z "$version" ] || [ "${version#0.0.0.dev0}" != "$version" ]; then
49+
echo "::error::setuptools-scm produced no usable version ('$version'); check that the checkout has full history and tags (fetch-depth: 0)"
50+
exit 1
51+
fi
52+
echo "version=$version" >> "$GITHUB_OUTPUT"
53+
echo "Resolved version: $version"
54+
55+
# GHCR rejects uppercase image names, and this repository is
56+
# "epifluidlab/FinaleToolkit". metadata-action lowercases internally, but
57+
# deriving it explicitly keeps the value correct wherever it is referenced.
58+
- name: Derive lowercase image name
59+
id: image
60+
run: |
61+
echo "name=$(printf '%s' "$GITHUB_REPOSITORY" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
62+
63+
- uses: docker/setup-buildx-action@v3
64+
65+
- name: Build test image
66+
uses: docker/build-push-action@v6
67+
with:
68+
context: .
69+
target: test
70+
load: true # into the local daemon so the next step can run it
71+
tags: finaletoolkit:test
72+
build-args: |
73+
SETUPTOOLS_SCM_PRETEND_VERSION=${{ steps.version.outputs.version }}
74+
cache-from: type=gha
75+
cache-to: type=gha,mode=max
76+
77+
# Running the suite inside the image proves the packaged environment is
78+
# correct, not merely that the package imports. tests/ is excluded from
79+
# the build context, so it is mounted at run time.
80+
- name: Run test suite inside the image
81+
run: |
82+
docker run --rm \
83+
-v "${{ github.workspace }}/tests:/app/tests:ro" \
84+
--entrypoint pytest \
85+
finaletoolkit:test -q
86+
87+
- name: Smoke-test the CLI entry point
88+
run: |
89+
docker run --rm --entrypoint finaletoolkit finaletoolkit:test --version
90+
docker run --rm --entrypoint finaletoolkit finaletoolkit:test --help > /dev/null
91+
92+
- name: Log in to ${{ env.REGISTRY }}
93+
if: github.event_name != 'pull_request'
94+
uses: docker/login-action@v3
95+
with:
96+
registry: ${{ env.REGISTRY }}
97+
username: ${{ github.actor }}
98+
password: ${{ secrets.GITHUB_TOKEN }}
99+
100+
- name: Derive image tags
101+
if: github.event_name != 'pull_request'
102+
id: meta
103+
uses: docker/metadata-action@v5
104+
with:
105+
images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}
106+
tags: |
107+
type=semver,pattern={{version}}
108+
type=semver,pattern={{major}}.{{minor}}
109+
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
110+
type=raw,value=main,enable={{is_default_branch}}
111+
type=sha,format=long
112+
113+
# Rebuilds the runtime target. The heavy layers are shared with the test
114+
# image above and come from the buildx cache, so this is cheap.
115+
- name: Build and push runtime image
116+
if: github.event_name != 'pull_request'
117+
uses: docker/build-push-action@v6
118+
with:
119+
context: .
120+
target: runtime
121+
push: true
122+
tags: ${{ steps.meta.outputs.tags }}
123+
labels: ${{ steps.meta.outputs.labels }}
124+
build-args: |
125+
SETUPTOOLS_SCM_PRETEND_VERSION=${{ steps.version.outputs.version }}
126+
cache-from: type=gha
127+
cache-to: type=gha,mode=max

0 commit comments

Comments
 (0)