Skip to content

Commit d57fced

Browse files
committed
feat: enhance CI/CD workflows for Python package publishing and Docker image management
1 parent 8c3d7e5 commit d57fced

11 files changed

Lines changed: 335 additions & 195 deletions

File tree

.github/workflows/pre-release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ jobs:
3939
uses: ./.github/workflows/reusable-release.yml
4040
with:
4141
version: ${{ needs.build.outputs.version }}
42+
pep440_version: ${{ needs.build.outputs.pep440_version }}
4243
components: '["api"]'
4344
release_type: 'feature'
4445
create_github_release: false

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ jobs:
6262
uses: ./.github/workflows/reusable-release.yml
6363
with:
6464
version: ${{ needs.build.outputs.version }}
65+
pep440_version: ${{ needs.build.outputs.pep440_version }}
6566
components: '["api"]'
6667
release_type: 'final'
6768
tag_prefix: 'docker-v'

.github/workflows/reusable-build.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,32 @@ jobs:
184184
name: sbom-${{ matrix.component }}-${{ needs.version.outputs.version }}
185185
path: sboms/sbom-${{ matrix.component }}.spdx.json
186186
retention-days: 1
187+
188+
python-build:
189+
name: Python Package Build
190+
needs: version
191+
runs-on: ubuntu-latest
192+
permissions:
193+
contents: read
194+
195+
steps:
196+
- uses: actions/checkout@v4
197+
with:
198+
fetch-depth: 0
199+
200+
- uses: astral-sh/setup-uv@v5
201+
202+
- name: Build Python packages (wheels + sdists)
203+
env:
204+
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ needs.version.outputs.pep440_version }}
205+
run: |
206+
mkdir -p dist
207+
uv build --package fairagro-middleware-shared --out-dir dist
208+
uv build --package fairagro-middleware-api-client --out-dir dist
209+
210+
- name: Upload Python packages artifact
211+
uses: actions/upload-artifact@v4
212+
with:
213+
name: python-packages-${{ needs.version.outputs.version }}
214+
path: dist/
215+
retention-days: 1

.github/workflows/reusable-release.yml

Lines changed: 125 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ on:
1010
description: 'Version calculated in Build phase'
1111
required: true
1212
type: string
13+
pep440_version:
14+
description: 'PEP 440 compliant version for Python packages'
15+
required: false
16+
type: string
17+
default: ''
1318
components:
1419
description: 'A JSON string array of components to release'
1520
required: false
@@ -34,6 +39,10 @@ on:
3439
required: true
3540
DOCKERHUB_TOKEN:
3641
required: true
42+
PYPI_TOKEN:
43+
required: false
44+
TEST_PYPI_TOKEN:
45+
required: false
3746

3847
env:
3948
GIT_USER_NAME: ${{ vars.GIT_USER_NAME || 'GitHub Pipeline' }}
@@ -43,17 +52,16 @@ env:
4352
GHCR_NAMESPACE: ${{ github.repository_owner }}
4453

4554
jobs:
46-
docker-push:
47-
name: DockerPush/Release
55+
push-dockerhub:
56+
name: DockerPush/DockerHub
4857
runs-on: ubuntu-latest
4958
permissions:
5059
contents: read
51-
packages: write
5260
strategy:
5361
matrix:
5462
component: ${{ fromJson(inputs.components) }}
5563
outputs:
56-
digests: ${{ steps.outputs.outputs.digests }}
64+
digest: ${{ steps.push.outputs.digest }}
5765

5866
steps:
5967
- name: Download Docker image artifact
@@ -64,48 +72,86 @@ jobs:
6472
- name: Load Docker image
6573
run: |
6674
docker load < docker-image-${{ matrix.component }}.tar.gz
67-
LOCAL_TAG="local/${{ env.IMAGE_BASE_NAME }}-${{ matrix.component }}:${{ inputs.version }}"
68-
echo "LOCAL_TAG=$LOCAL_TAG" >> $GITHUB_ENV
75+
echo "LOCAL_TAG=local/${{ env.IMAGE_BASE_NAME }}-${{ matrix.component }}:${{ inputs.version }}" >> $GITHUB_ENV
6976
7077
- name: Login to DockerHub
7178
uses: docker/login-action@v3
7279
with:
7380
username: ${{ secrets.DOCKERHUB_USER }}
7481
password: ${{ secrets.DOCKERHUB_TOKEN }}
7582

83+
- name: Tag and Push to DockerHub
84+
id: push
85+
run: |
86+
DH_TAG="${{ env.DOCKERHUB_NAMESPACE }}/${{ env.IMAGE_BASE_NAME }}-${{ matrix.component }}:${{ inputs.version }}"
87+
docker tag ${{ env.LOCAL_TAG }} $DH_TAG
88+
docker push $DH_TAG
89+
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $DH_TAG | cut -d'@' -f2)
90+
echo "digest=$DIGEST" >> $GITHUB_OUTPUT
91+
echo "✅ Pushed to DockerHub: $DH_TAG"
92+
93+
push-ghcr:
94+
name: DockerPush/GHCR
95+
runs-on: ubuntu-latest
96+
permissions:
97+
contents: read
98+
packages: write
99+
strategy:
100+
matrix:
101+
component: ${{ fromJson(inputs.components) }}
102+
103+
steps:
104+
- name: Download Docker image artifact
105+
uses: actions/download-artifact@v4
106+
with:
107+
name: docker-image-${{ matrix.component }}-${{ inputs.version }}
108+
109+
- name: Load Docker image
110+
run: |
111+
docker load < docker-image-${{ matrix.component }}.tar.gz
112+
echo "LOCAL_TAG=local/${{ env.IMAGE_BASE_NAME }}-${{ matrix.component }}:${{ inputs.version }}" >> $GITHUB_ENV
113+
76114
- name: Login to GitHub Container Registry
77115
uses: docker/login-action@v3
78116
with:
79117
registry: ghcr.io
80118
username: ${{ github.actor }}
81119
password: ${{ secrets.GITHUB_TOKEN }}
82120

83-
- name: Tag and Push Images
84-
id: push
121+
- name: Tag and Push to GHCR
85122
run: |
86-
# DockerHub
87-
DH_TAG="${{ env.DOCKERHUB_NAMESPACE }}/${{ env.IMAGE_BASE_NAME }}-${{ matrix.component }}:${{ inputs.version }}"
88-
docker tag ${{ env.LOCAL_TAG }} $DH_TAG
89-
docker push $DH_TAG
90-
echo "✅ Pushed to DockerHub: $DH_TAG"
91-
92-
# GHCR
93123
REPO_LOWER=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
94124
GHCR_TAG="ghcr.io/$REPO_LOWER/${{ matrix.component }}:${{ inputs.version }}"
95125
docker tag ${{ env.LOCAL_TAG }} $GHCR_TAG
96126
docker push $GHCR_TAG
97127
echo "✅ Pushed to GHCR: $GHCR_TAG"
98128
99-
# Extract digest
100-
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $DH_TAG | cut -d'@' -f2)
101-
echo "digest=$DIGEST" >> $GITHUB_OUTPUT
129+
publish-pypi:
130+
name: PyPI/Publish
131+
runs-on: ubuntu-latest
132+
permissions:
133+
contents: read
102134

103-
- name: Collect digests
104-
id: outputs
105-
run: |
106-
# Since matrix runs in parallel, we need a way to collect them if there were more than 1.
107-
# For now, with one component, we just output it.
108-
echo "digests={\"${{ matrix.component }}\": \"${{ steps.push.outputs.digest }}\"}" >> $GITHUB_OUTPUT
135+
steps:
136+
- name: Download Python packages artifact
137+
uses: actions/download-artifact@v4
138+
with:
139+
name: python-packages-${{ inputs.version }}
140+
path: dist/
141+
142+
- uses: astral-sh/setup-uv@v5
143+
144+
- name: Publish to PyPI (final release)
145+
if: inputs.release_type == 'final'
146+
env:
147+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
148+
run: uv publish --publish-url https://upload.pypi.org/legacy/ dist/*
149+
150+
- name: Publish to TestPyPI (pre-release)
151+
if: inputs.release_type == 'feature'
152+
env:
153+
UV_PUBLISH_TOKEN: ${{ secrets.TEST_PYPI_TOKEN }}
154+
run: uv publish --publish-url https://test.pypi.org/legacy/ dist/*
109155

110156
create-release-tag:
111157
name: CreateReleaseTag/Release
@@ -133,8 +179,8 @@ jobs:
133179

134180
github-release:
135181
name: GitHubRelease/Release
136-
if: inputs.create_github_release == true
137-
needs: [docker-push, create-release-tag]
182+
if: ${{ always() && inputs.create_github_release == true && needs.create-release-tag.result == 'success' }}
183+
needs: [push-dockerhub, push-ghcr, publish-pypi, create-release-tag]
138184
runs-on: ubuntu-latest
139185
permissions:
140186
contents: write
@@ -147,35 +193,73 @@ jobs:
147193
merge-multiple: true
148194
path: sboms
149195

196+
- name: Download Python packages artifact
197+
if: needs.publish-pypi.result == 'success'
198+
uses: actions/download-artifact@v4
199+
with:
200+
name: python-packages-${{ inputs.version }}
201+
path: pypi-dist/
202+
150203
- name: Generate release body
151204
run: |
152205
COMPONENT=$(echo '${{ inputs.components }}' | jq -r '.[0]')
153-
DIGEST=$(echo '${{ needs.docker-push.outputs.digests }}' | jq -r ".[\"$COMPONENT\"]")
154206
DH_IMAGE="${{ env.DOCKERHUB_NAMESPACE }}/${{ env.IMAGE_BASE_NAME }}-${COMPONENT}:${{ inputs.version }}"
155207
REPO_LOWER=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
156208
GHCR_IMAGE="ghcr.io/${REPO_LOWER}/${COMPONENT}:${{ inputs.version }}"
157209
TAG="${{ needs.create-release-tag.outputs.timestamp }}-${{ inputs.tag_prefix }}${{ inputs.version }}"
158210
REPO_BASENAME=$(basename "${{ github.repository }}")
211+
PYPI_VERSION="${{ inputs.pep440_version || inputs.version }}"
212+
PYPI_REGISTRY=$([ "${{ inputs.release_type }}" = "final" ] && echo "https://pypi.org" || echo "https://test.pypi.org")
159213
160214
{
161215
echo "## Docker Images"
162216
echo ""
163217
echo "| Platform | Image Digest |"
164218
echo "| --- | --- |"
165-
echo "| linux/amd64 | \`$DIGEST\` |"
219+
if [[ "${{ needs.push-dockerhub.result }}" == "success" ]]; then
220+
echo "| linux/amd64 (DockerHub) | \`${{ needs.push-dockerhub.outputs.digest }}\` |"
221+
fi
166222
echo ""
167223
echo "### Registry Links"
168-
echo "* **DockerHub**: [${DH_IMAGE}](https://hub.docker.com/r/${{ env.DOCKERHUB_NAMESPACE }}/${{ env.IMAGE_BASE_NAME }}-${COMPONENT})"
169-
echo "* **GHCR**: \`${GHCR_IMAGE}\`"
170-
echo ""
171-
echo "## Usage"
172-
echo ""
173-
echo "Pull the Docker image:"
174-
echo '```bash'
175-
echo "docker pull ${DH_IMAGE}"
176-
echo '```'
224+
if [[ "${{ needs.push-dockerhub.result }}" == "success" ]]; then
225+
echo "* **DockerHub**: [${DH_IMAGE}](https://hub.docker.com/r/${{ env.DOCKERHUB_NAMESPACE }}/${{ env.IMAGE_BASE_NAME }}-${COMPONENT})"
226+
echo ""
227+
echo "Pull from DockerHub:"
228+
echo '```bash'
229+
echo "docker pull ${DH_IMAGE}"
230+
echo '```'
231+
fi
232+
if [[ "${{ needs.push-ghcr.result }}" == "success" ]]; then
233+
echo "* **GHCR**: \`${GHCR_IMAGE}\`"
234+
echo ""
235+
echo "Pull from GHCR:"
236+
echo '```bash'
237+
echo "docker pull ${GHCR_IMAGE}"
238+
echo '```'
239+
fi
240+
if [[ "${{ needs.push-dockerhub.result }}" != "success" && "${{ needs.push-ghcr.result }}" != "success" ]]; then
241+
echo "_No Docker registries were successfully updated for this release._"
242+
fi
243+
if [[ "${{ needs.publish-pypi.result }}" == "success" ]]; then
244+
echo ""
245+
echo "## Python Packages"
246+
echo ""
247+
echo "Published to [$PYPI_REGISTRY]($PYPI_REGISTRY):"
248+
echo '```bash'
249+
echo "pip install fairagro-middleware-shared==${PYPI_VERSION}"
250+
echo "pip install fairagro-middleware-api-client==${PYPI_VERSION}"
251+
echo '```'
252+
echo ""
253+
echo "### Fallback (install from source)"
254+
echo '```bash'
255+
echo "git clone https://github.com/${{ github.repository }}.git"
256+
echo "cd ${REPO_BASENAME}"
257+
echo "git checkout ${TAG}"
258+
echo "pip install middleware/shared middleware/api_client"
259+
echo '```'
260+
fi
177261
echo ""
178-
echo "## Fallback (Build from source)"
262+
echo "## Fallback (Build Docker image from source)"
179263
echo '```bash'
180264
echo "git clone https://github.com/${{ github.repository }}.git"
181265
echo "cd ${REPO_BASENAME}"
@@ -194,7 +278,9 @@ jobs:
194278
draft: true
195279
prerelease: ${{ inputs.release_type == 'feature' }}
196280
generate_release_notes: true
197-
files: sboms/*
281+
files: |
282+
sboms/*
283+
${{ needs.publish-pypi.result == 'success' && 'pypi-dist/*' || '' }}
198284
199285
- name: Publish Release
200286
env:
@@ -205,14 +291,11 @@ jobs:
205291
# Give GitHub API a moment to index the new release
206292
sleep 5
207293
208-
# Try to get the release ID from the draft release we just created
209294
if RELEASE_ID=$(gh api repos/${{ github.repository }}/releases/tags/${{ needs.create-release-tag.outputs.timestamp }}-${{ inputs.tag_prefix }}${{ inputs.version }} --jq '.id' 2>/dev/null); then
210295
echo "✅ Found release ID via tag: $RELEASE_ID"
211296
else
212297
echo "⚠️ Release not found via tag, searching in all releases..."
213-
# Search for the release by tag name in all release list (including drafts)
214298
RELEASE_ID=$(gh api repos/${{ github.repository }}/releases --jq '.[] | select(.tag_name == "${{ needs.create-release-tag.outputs.timestamp }}-${{ inputs.tag_prefix }}${{ inputs.version }}") | .id')
215-
216299
if [[ -n "$RELEASE_ID" ]]; then
217300
echo "✅ Found release ID via search: $RELEASE_ID"
218301
else

docker/Dockerfile.api

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ COPY middleware ./middleware
1818
RUN pip install --no-cache-dir --upgrade pip==26.0.1 uv==0.11.7
1919

2020
# Build wheels
21-
RUN uv build --package shared --wheel && \
21+
RUN uv build --package fairagro-middleware-shared --wheel && \
2222
uv build --package api --wheel
2323

2424

middleware/api/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "The FAIRagro advanced middleware API"
55
readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [
8-
"shared",
8+
"fairagro-middleware-shared",
99
"aiocouch>=4.0.1",
1010
"arctrl>=3.0.0b15",
1111
"asn1crypto>=1.5.1",
@@ -27,7 +27,7 @@ dependencies = [
2727
middleware-couchdb-setup = "middleware.api.cli:main"
2828

2929
[tool.uv.sources]
30-
shared = { workspace = true }
30+
fairagro-middleware-shared = { workspace = true }
3131

3232
[tool.ruff]
3333
extend = "../../pyproject.toml"

middleware/api_client/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[project]
2-
name = "api_client"
2+
name = "fairagro-middleware-api-client"
33
dynamic = ["version"]
44
description = "The FAIRagro advanced middleware API client"
55
readme = "README.md"

middleware/shared/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[project]
2-
name = "shared"
2+
name = "fairagro-middleware-shared"
33
dynamic = ["version"]
44
description = "The FAIRagro advanced middleware shared components"
55
readme = "README.md"

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [
88
"api",
9-
"api_client",
10-
"shared",
9+
"fairagro-middleware-api-client",
10+
"fairagro-middleware-shared",
1111
]
1212

1313
[dependency-groups]
@@ -30,8 +30,8 @@ dev = [
3030

3131
[tool.uv.sources]
3232
api = { workspace = true }
33-
api_client = { workspace = true }
34-
shared = { workspace = true }
33+
fairagro-middleware-api-client = { workspace = true }
34+
fairagro-middleware-shared = { workspace = true }
3535

3636
[tool.uv.workspace]
3737
members = [

0 commit comments

Comments
 (0)