Merge pull request #2748 from arc53/ci/release-chain

ci: publish Docker images and the PyPI package from the release workflow
This commit is contained in:
Alex authored and GitHub committed 2026-09-08 22:20:54 +01:00
commit 0c348917b1
4 files changed
+120 -22

No files matched your search

+55 -8
View File
@@ -1,5 +1,12 @@
name: Backend release
# A version bump on main tags the commit, creates the GitHub release, then
# publishes the Docker images and the PyPI package by calling those workflows.
# The release is created with GITHUB_TOKEN, and GitHub never starts workflows
# from events that token produces, so the `release: published` triggers on the
# publish workflows would not fire (0.18.0 got no images that way). Releases
# created by hand still publish through those triggers.
on:
push:
branches: [main]
@@ -7,8 +14,7 @@ on:
- 'docsgpt/version.py'
workflow_dispatch:
permissions:
contents: write
permissions: {}
concurrency:
group: backend-release
@@ -18,6 +24,14 @@ jobs:
release:
if: github.repository == 'arc53/DocsGPT'
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
version: ${{ steps.ver.outputs.version }}
# The publish jobs run only for a release this run created. A release
# that already existed was either made by hand (its release event
# published it) or made by an earlier run: re-run that run's failed jobs.
created: ${{ steps.check.outputs.release == 'missing' }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
@@ -34,20 +48,30 @@ jobs:
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved version: $VERSION"
- name: Check if tag already exists
- name: Check what already exists
id: check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.ver.outputs.version }}
run: |
# The tag and the release are checked separately: a run that pushed
# the tag and then failed to create the release must still create
# it (and publish) when re-run.
if git ls-remote --tags --exit-code origin "refs/tags/$VERSION" >/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Tag $VERSION already exists on origin — skipping."
echo "tag=exists" >> "$GITHUB_OUTPUT"
echo "Tag $VERSION already exists on origin."
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "tag=missing" >> "$GITHUB_OUTPUT"
fi
if gh release view "$VERSION" >/dev/null 2>&1; then
echo "release=exists" >> "$GITHUB_OUTPUT"
echo "Release $VERSION already exists — nothing to publish."
else
echo "release=missing" >> "$GITHUB_OUTPUT"
fi
- name: Create and push tag
if: steps.check.outputs.exists == 'false'
if: steps.check.outputs.tag == 'missing'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.ver.outputs.version }}
@@ -63,7 +87,7 @@ jobs:
"$VERSION"
- name: Create GitHub release
if: steps.check.outputs.exists == 'false'
if: steps.check.outputs.release == 'missing'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.ver.outputs.version }}
@@ -71,3 +95,26 @@ jobs:
gh release create "$VERSION" \
--title "v$VERSION" \
--generate-notes
docker:
needs: release
if: needs.release.outputs.created == 'true'
uses: $/.github/workflows/ci.yml
with:
version: ${{ needs.release.outputs.version }}
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
permissions:
contents: write # release-assets attaches the compose file to the release
packages: write
pypi:
needs: release
if: needs.release.outputs.created == 'true'
uses: $/.github/workflows/pypi-publish.yml
with:
version: ${{ needs.release.outputs.version }}
permissions:
contents: read
id-token: write
+28 -5
View File
@@ -1,8 +1,31 @@
name: Build and push DocsGPT Docker image
# Runs for a release created by hand (the release event), or called by the
# backend-release workflow with the version it just tagged: GitHub never starts
# workflows from events GITHUB_TOKEN produces, so a bot-created release does not
# fire the release trigger on its own.
on:
release:
types: [published]
workflow_call:
inputs:
version:
description: Release tag to build and push (the images are tagged with it)
type: string
required: true
secrets:
DOCKER_USERNAME:
required: true
DOCKER_PASSWORD:
required: true
permissions:
contents: read
env:
# The tag being published: passed in by the caller, or the release's own.
RELEASE_TAG: ${{ inputs.version || github.event.release.tag_name }}
jobs:
build:
@@ -58,7 +81,7 @@ jobs:
ghcr.io/${{ github.repository_owner }}/docsgpt
labels: |
org.opencontainers.image.title=DocsGPT${{ matrix.variant }}
org.opencontainers.image.version=${{ github.event.release.tag_name }}
org.opencontainers.image.version=${{ env.RELEASE_TAG }}
- name: Build and push platform-specific images
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
@@ -71,8 +94,8 @@ jobs:
EXTRAS=${{ matrix.variant == '-docling' && 'docling' || '' }}
INSTALL_TESSERACT=${{ matrix.variant == '-docling' && 'true' || 'false' }}
tags: |
${{ env.DOCKERHUB_NAMESPACE }}/docsgpt:${{ github.event.release.tag_name }}${{ matrix.variant }}-${{ matrix.platform == 'linux/arm64' && 'arm64' || 'amd64' }}
ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}${{ matrix.variant }}-${{ matrix.platform == 'linux/arm64' && 'arm64' || 'amd64' }}
${{ env.DOCKERHUB_NAMESPACE }}/docsgpt:${{ env.RELEASE_TAG }}${{ matrix.variant }}-${{ matrix.platform == 'linux/arm64' && 'arm64' || 'amd64' }}
ghcr.io/${{ github.repository_owner }}/docsgpt:${{ env.RELEASE_TAG }}${{ matrix.variant }}-${{ matrix.platform == 'linux/arm64' && 'arm64' || 'amd64' }}
labels: ${{ steps.meta.outputs.labels }}
provenance: false
sbom: false
@@ -118,7 +141,7 @@ jobs:
- name: Create and push multi-arch manifests
env:
TAG: ${{ github.event.release.tag_name }}${{ matrix.variant }}
TAG: ${{ env.RELEASE_TAG }}${{ matrix.variant }}
LATEST: latest${{ matrix.variant }}
run: |
set -e
@@ -145,6 +168,6 @@ jobs:
- name: Attach the standalone compose file to the release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.event.release.tag_name }}
TAG: ${{ env.RELEASE_TAG }}
run: |
gh release upload "$TAG" deployment/docker-compose-standalone.yaml --clobber
+36 -8
View File
@@ -1,13 +1,25 @@
name: Publish to PyPI
# Trusted publishing: PyPI trusts this workflow file in arc53/DocsGPT running
# in the `pypi` environment, so no API token is stored. A published release
# (which the backend-release workflow creates when docsgpt/version.py changes
# on main) publishes to PyPI; a manual run publishes to TestPyPI by default.
# in the `pypi` environment, so no API token is stored. The backend-release
# workflow calls this one after it tags a version bump on main (a release it
# creates with GITHUB_TOKEN never fires the release trigger); a release created
# by hand publishes through that trigger; a manual run publishes to TestPyPI
# by default.
on:
release:
types: [published]
workflow_call:
inputs:
version:
description: Version the archives must carry (empty skips the check)
type: string
default: ''
target:
description: Package index to publish to (pypi or testpypi)
type: string
default: pypi
workflow_dispatch:
inputs:
target:
@@ -19,6 +31,11 @@ on:
permissions:
contents: read
env:
# The version being released: passed in by the caller, or the release's tag;
# empty for a manual run, which publishes whatever main builds.
RELEASE_VERSION: ${{ inputs.version || github.event.release.tag_name }}
jobs:
build:
if: github.repository == 'arc53/DocsGPT'
@@ -33,15 +50,22 @@ jobs:
# A publishing workflow must not restore a cache another job could have poisoned.
enable-cache: false
- name: The publish target is one this workflow knows
# A caller's typo would otherwise skip both publish jobs and upload nothing.
if: inputs.target != '' && inputs.target != 'pypi' && inputs.target != 'testpypi'
env:
TARGET: ${{ inputs.target }}
run: |
echo "::error::unknown target '$TARGET': expected pypi or testpypi"
exit 1
- name: Build the sdist and wheel
run: uv build
- name: The archives carry the released version
if: github.event_name == 'release'
env:
TAG: ${{ github.event.release.tag_name }}
if: env.RELEASE_VERSION != ''
run: |
ls "dist/docsgpt-${TAG}.tar.gz" "dist/docsgpt-${TAG}-py3-none-any.whl"
ls "dist/docsgpt-${RELEASE_VERSION}.tar.gz" "dist/docsgpt-${RELEASE_VERSION}-py3-none-any.whl"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
@@ -50,6 +74,7 @@ jobs:
if-no-files-found: error
publish-pypi:
# A release event, or a call or manual run aimed at PyPI (the call default).
if: github.event_name == 'release' || inputs.target == 'pypi'
needs: build
runs-on: ubuntu-latest
@@ -67,7 +92,7 @@ jobs:
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
publish-testpypi:
if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
if: inputs.target == 'testpypi'
needs: build
runs-on: ubuntu-latest
environment:
@@ -84,3 +109,6 @@ jobs:
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
repository-url: https://test.pypi.org/legacy/
# TestPyPI is a rehearsal target: a version that is already there
# must not fail the run.
skip-existing: true
+1 -1
View File
@@ -22,4 +22,4 @@ jobs:
persist-credentials: false
- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3