diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2985af5..5cd9c85e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/cife.yml b/.github/workflows/cife.yml index 6c8c301f..5f801b24 100644 --- a/.github/workflows/cife.yml +++ b/.github/workflows/cife.yml @@ -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" diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 9037f758..2ad7c716 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -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. diff --git a/.github/workflows/sandbox-image.yml b/.github/workflows/sandbox-image.yml index c26e5b8d..c020187a 100644 --- a/.github/workflows/sandbox-image.yml +++ b/.github/workflows/sandbox-image.yml @@ -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"