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
3439 required : true
3540 DOCKERHUB_TOKEN :
3641 required : true
42+ PYPI_TOKEN :
43+ required : false
44+ TEST_PYPI_TOKEN :
45+ required : false
3746
3847env :
3948 GIT_USER_NAME : ${{ vars.GIT_USER_NAME || 'GitHub Pipeline' }}
4352 GHCR_NAMESPACE : ${{ github.repository_owner }}
4453
4554jobs :
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
0 commit comments