Merge pull request #2765 from arc53/fix/release-recovery-and-attestations

ci: fix PyPI publishing from the release chain, make image builds recoverable
This commit is contained in:
Alex authored and GitHub committed 2026-09-12 23:50:26 +01:00
commit eae635b6dd
4 files changed
+125 -22

No files matched your search

+43 -6
View File
@@ -14,11 +14,25 @@ on:
description: Release tag to build and push (the images are tagged with it)
type: string
required: true
move_latest:
description: Also move the `latest` tag onto this build
type: boolean
default: true
secrets:
DOCKER_USERNAME:
required: true
DOCKER_PASSWORD:
required: true
workflow_dispatch:
inputs:
version:
description: Release tag to build and push (the images are tagged with it)
type: string
required: true
move_latest:
description: Also move the `latest` tag onto this build
type: boolean
default: true
permissions:
contents: read
@@ -51,6 +65,13 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
# Build the source the tag names, not whatever the run was started
# from: a manual run dispatched off main would otherwise publish the
# current branch under an older version's tags. Resolved under
# refs/tags/ so a version that is also a branch name cannot win, and a
# version with no tag fails here instead of mislabelling an image.
# Falls back to the run's own ref for the push that tags `develop`.
ref: ${{ inputs.version && format('refs/tags/{0}', inputs.version) || github.ref }}
persist-credentials: false
- name: Set up Docker Buildx
@@ -142,15 +163,24 @@ jobs:
- name: Create and push multi-arch manifests
env:
TAG: ${{ env.RELEASE_TAG }}${{ matrix.variant }}
# A stable release moves `latest`; a prerelease published by hand
# moves only its own tag (the release trigger fires for those too).
LATEST: ${{ (inputs.version || !github.event.release.prerelease) && format('latest{0}', matrix.variant) || '' }}
VARIANT: ${{ matrix.variant }}
# Raw facts; the shell below decides from them what `latest` does.
IS_PRERELEASE: ${{ github.event.release.prerelease || false }}
MOVE_LATEST: ${{ inputs.move_latest }}
run: |
set -e
# $LATEST is deliberately unquoted: it is empty for a prerelease, and
# an empty word would create a manifest named "$repo:".
# `latest` follows this build, except for a release marked prerelease
# (the release trigger fires for those too) or a manual run that asked
# it not to. MOVE_LATEST is empty for a release event, where only the
# prerelease flag decides.
moving="latest${VARIANT}"
if [ "$IS_PRERELEASE" = true ] || [ "$MOVE_LATEST" = false ]; then
moving=""
fi
# $moving is deliberately unquoted: an empty word would create a
# manifest named "$repo:".
for repo in "$DOCKERHUB_NAMESPACE/docsgpt" "ghcr.io/${{ github.repository_owner }}/docsgpt"; do
for name in "$TAG" $LATEST; do
for name in "$TAG" $moving; do
docker manifest create "$repo:$name" \
--amend "$repo:$TAG-amd64" \
--amend "$repo:$TAG-arm64"
@@ -167,6 +197,13 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
# Build the source the tag names, not whatever the run was started
# from: a manual run dispatched off main would otherwise publish the
# current branch under an older version's tags. Resolved under
# refs/tags/ so a version that is also a branch name cannot win, and a
# version with no tag fails here instead of mislabelling an image.
# Falls back to the run's own ref for the push that tags `develop`.
ref: ${{ inputs.version && format('refs/tags/{0}', inputs.version) || github.ref }}
persist-credentials: false
- name: Attach the standalone compose file to the release
+35 -8
View File
@@ -14,11 +14,25 @@ on:
description: Release tag to build and push (the images are tagged with it)
type: string
required: true
move_latest:
description: Also move the `latest` tag onto this build
type: boolean
default: true
secrets:
DOCKER_USERNAME:
required: true
DOCKER_PASSWORD:
required: true
workflow_dispatch:
inputs:
version:
description: Release tag to build and push (the images are tagged with it)
type: string
required: true
move_latest:
description: Also move the `latest` tag onto this build
type: boolean
default: true
permissions:
contents: read
@@ -26,10 +40,6 @@ permissions:
env:
# The tag being published: passed in by the caller, or the release's own.
RELEASE_TAG: ${{ inputs.version || github.event.release.tag_name }}
# A stable release also moves `latest`. A prerelease published by hand moves
# only its own tag: the release trigger fires for those too, and they must not
# become `latest`.
MOVING_TAG: ${{ (inputs.version || !github.event.release.prerelease) && 'latest' || '' }}
jobs:
build:
@@ -58,6 +68,13 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
# Build the source the tag names, not whatever the run was started
# from: a manual run dispatched off main would otherwise publish the
# current branch under an older version's tags. Resolved under
# refs/tags/ so a version that is also a branch name cannot win, and a
# version with no tag fails here instead of mislabelling an image.
# Falls back to the run's own ref for the push that tags `develop`.
ref: ${{ inputs.version && format('refs/tags/{0}', inputs.version) || github.ref }}
persist-credentials: false
- name: Set up Docker Buildx
@@ -143,13 +160,23 @@ jobs:
- name: Create and push multi-arch manifests
env:
TAG: ${{ env.RELEASE_TAG }}
MOVING: ${{ env.MOVING_TAG }}
# Raw facts; the shell below decides from them what `latest` does.
IS_PRERELEASE: ${{ github.event.release.prerelease || false }}
MOVE_LATEST: ${{ inputs.move_latest }}
run: |
set -e
# $MOVING is deliberately unquoted: it is empty for a prerelease, and
# an empty word would create a manifest named "$repo:".
# `latest` follows this build, except for a release marked prerelease
# (the release trigger fires for those too) or a manual run that asked
# it not to. MOVE_LATEST is empty for a release event, where only the
# prerelease flag decides.
moving=latest
if [ "$IS_PRERELEASE" = true ] || [ "$MOVE_LATEST" = false ]; then
moving=""
fi
# $moving is deliberately unquoted: an empty word would create a
# manifest named "$repo:".
for repo in "$DOCKERHUB_NAMESPACE/docsgpt-fe" "ghcr.io/${{ github.repository_owner }}/docsgpt-fe"; do
for name in "$TAG" $MOVING; do
for name in "$TAG" $moving; do
docker manifest create "$repo:$name" \
--amend "$repo:$TAG-amd64" \
--amend "$repo:$TAG-arm64"
+12
View File
@@ -6,6 +6,13 @@ name: Publish to PyPI
# 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.
#
# Attestations are on only when this file is the workflow that STARTED the run.
# A PEP 740 attestation records the entry workflow in its certificate, while
# trusted publishing matches the called workflow, so when backend-release calls
# this one the two disagree and PyPI rejects the upload with "Certificate's
# Build Config URI ... does not match expected Trusted Publisher". No publisher
# configuration satisfies both, so the release chain publishes unattested.
on:
release:
@@ -116,6 +123,9 @@ jobs:
path: dist/
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
# Only when this workflow is the entry point: see the note at the top.
attestations: ${{ contains(github.workflow_ref, '/pypi-publish.yml@') }}
publish-testpypi:
if: inputs.target == 'testpypi'
@@ -134,6 +144,8 @@ jobs:
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
# Only when this workflow is the entry point: see the note at the top.
attestations: ${{ contains(github.workflow_ref, '/pypi-publish.yml@') }}
repository-url: https://test.pypi.org/legacy/
# TestPyPI is a rehearsal target: a version that is already there
# must not fail the run.
+35 -8
View File
@@ -21,11 +21,25 @@ on:
description: Release tag to build and push (the images are tagged with it)
type: string
required: true
move_latest:
description: Also move the `latest` tag onto this build
type: boolean
default: true
secrets:
DOCKER_USERNAME:
required: true
DOCKER_PASSWORD:
required: true
workflow_dispatch:
inputs:
version:
description: Release tag to build and push (the images are tagged with it)
type: string
required: true
move_latest:
description: Also move the `latest` tag onto this build
type: boolean
default: true
push:
branches: [main]
paths:
@@ -38,10 +52,6 @@ permissions:
env:
# The version being published, or `develop` for a push to main.
RELEASE_TAG: ${{ inputs.version || github.event.release.tag_name || 'develop' }}
# A stable release also moves `latest`. A push to main moves nothing but
# `develop`, and a prerelease published by hand moves only its own tag: the
# release trigger fires for those too, and they must not become `latest`.
MOVING_TAG: ${{ (inputs.version || (github.event.release.tag_name && !github.event.release.prerelease)) && 'latest' || '' }}
jobs:
build:
@@ -70,6 +80,13 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
# Build the source the tag names, not whatever the run was started
# from: a manual run dispatched off main would otherwise publish the
# current branch under an older version's tags. Resolved under
# refs/tags/ so a version that is also a branch name cannot win, and a
# version with no tag fails here instead of mislabelling an image.
# Falls back to the run's own ref for the push that tags `develop`.
ref: ${{ inputs.version && format('refs/tags/{0}', inputs.version) || github.ref }}
persist-credentials: false
- name: Set up Docker Buildx
@@ -155,13 +172,23 @@ jobs:
- name: Create and push multi-arch manifests
env:
TAG: ${{ env.RELEASE_TAG }}
MOVING: ${{ env.MOVING_TAG }}
# Raw facts; the shell below decides from them what `latest` does.
IS_PRERELEASE: ${{ github.event.release.prerelease || false }}
MOVE_LATEST: ${{ inputs.move_latest }}
run: |
set -e
# $MOVING is deliberately unquoted: it is empty for a push to main,
# and an empty word would create a manifest named "$repo:".
# `latest` follows this build, except for the rolling `develop` build,
# a release marked prerelease (the release trigger fires for those
# too), or a manual run that asked it not to. MOVE_LATEST is empty for
# a release event and for a push, where the other two rules decide.
moving=latest
if [ "$TAG" = develop ] || [ "$IS_PRERELEASE" = true ] || [ "$MOVE_LATEST" = false ]; then
moving=""
fi
# $moving is deliberately unquoted: an empty word would create a
# manifest named "$repo:".
for repo in "$DOCKERHUB_NAMESPACE/docsgpt-sandbox" "ghcr.io/${{ github.repository_owner }}/docsgpt-sandbox"; do
for name in "$TAG" $MOVING; do
for name in "$TAG" $moving; do
docker manifest create "$repo:$name" \
--amend "$repo:$TAG-amd64" \
--amend "$repo:$TAG-arm64"