From 72584e5dfe0a9e025c4660f69dfd0fb152102642 Mon Sep 17 00:00:00 2001 From: Stelios Daveas Date: Fri, 19 Jun 2026 11:56:34 +0000 Subject: [PATCH 1/4] ci: add public release finalizer --- .github/actions/finalize-release/action.yaml | 44 ++++ .github/scripts/finalize-release.sh | 257 +++++++++++++++++++ .github/workflows/finalize-release.yaml | 71 +++++ 3 files changed, 372 insertions(+) create mode 100644 .github/actions/finalize-release/action.yaml create mode 100644 .github/scripts/finalize-release.sh create mode 100644 .github/workflows/finalize-release.yaml diff --git a/.github/actions/finalize-release/action.yaml b/.github/actions/finalize-release/action.yaml new file mode 100644 index 0000000..9dd4ae0 --- /dev/null +++ b/.github/actions/finalize-release/action.yaml @@ -0,0 +1,44 @@ +name: Finalize Public Release +description: Validate or finalize a public release PR. + +inputs: + mode: + description: validate or finalize. + required: true + repository-path: + description: Path to the checked out public repository. + required: false + default: . + release-namespace: + description: Release namespace to enforce. + required: false + default: auto + pr-body: + description: Release PR body. + required: true + pr-base-ref: + description: Release PR base branch. + required: true + pr-head-ref: + description: Release PR head branch. + required: true + merge-commit-sha: + description: Merge commit SHA for finalize mode. + required: false + default: "" + +runs: + using: composite + steps: + - name: Run release finalizer + shell: bash + env: + RELEASE_NAMESPACE: ${{ inputs.release-namespace }} + PR_BODY: ${{ inputs.pr-body }} + PR_BASE_REF: ${{ inputs.pr-base-ref }} + PR_HEAD_REF: ${{ inputs.pr-head-ref }} + MERGE_COMMIT_SHA: ${{ inputs.merge-commit-sha }} + run: | + set -euo pipefail + cd "${{ inputs.repository-path }}" + bash "${{ github.action_path }}/../../scripts/finalize-release.sh" "${{ inputs.mode }}" diff --git a/.github/scripts/finalize-release.sh b/.github/scripts/finalize-release.sh new file mode 100644 index 0000000..53abde8 --- /dev/null +++ b/.github/scripts/finalize-release.sh @@ -0,0 +1,257 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: + finalize-release.sh validate + finalize-release.sh finalize + +Environment: + PR_BODY Pull request body containing release metadata. + PR_BASE_REF Pull request base branch. + PR_HEAD_REF Pull request head branch. + MERGE_COMMIT_SHA Required for finalize mode. + RELEASE_NAMESPACE Optional: auto, production, or test. Defaults to auto. +USAGE +} + +die() { + echo "::error::$*" >&2 + exit 1 +} + +note() { + echo "$*" >&2 +} + +require_env() { + local name="$1" + if [[ -z "${!name:-}" ]]; then + die "${name} is required" + fi +} + +extract_body_field() { + local label="$1" + awk -v label="${label}: " ' + index($0, label) == 1 { + sub(label, "") + print + found = 1 + exit + } + END { + if (!found) { + exit 1 + } + } + ' <<< "${PR_BODY}" +} + +remote_ref_exists() { + local ref="$1" + git ls-remote --exit-code origin "${ref}" >/dev/null 2>&1 +} + +remote_ref_sha() { + local ref="$1" + git ls-remote origin "${ref}" | awk 'NR == 1 {print $1}' +} + +ensure_valid_ref() { + local ref="$1" + git check-ref-format "${ref}" >/dev/null 2>&1 || die "Invalid Git ref: ${ref}" +} + +resolve_namespace() { + local requested="${RELEASE_NAMESPACE:-auto}" + + case "${requested}" in + auto) + if [[ "${TAG}" == test/v* || "${RELEASE_BRANCH}" == test-release/* || "${PR_BASE_REF}" == test-main || "${PR_BASE_REF}" == test-release/* ]]; then + NAMESPACE="test" + else + NAMESPACE="production" + fi + ;; + production|test) + NAMESPACE="${requested}" + ;; + *) + die "Invalid RELEASE_NAMESPACE: ${requested}" + ;; + esac + + case "${NAMESPACE}" in + production) + TAG_PREFIX="v" + RELEASE_BRANCH_PREFIX="release/" + MAIN_BRANCH="main" + ;; + test) + TAG_PREFIX="test/v" + RELEASE_BRANCH_PREFIX="test-release/" + MAIN_BRANCH="test-main" + ;; + esac +} + +parse_release_metadata() { + require_env PR_BODY + require_env PR_BASE_REF + require_env PR_HEAD_REF + + [[ "${PR_HEAD_REF}" == sync/copybara-export-* ]] || die "Release finalizer only accepts Copybara export branches" + + TAG="$(extract_body_field "Release tag")" || die "PR body is missing 'Release tag: ...'" + RELEASE_BRANCH="$(extract_body_field "Release branch")" || die "PR body is missing 'Release branch: ...'" + RELEASE_KIND="$(extract_body_field "Release kind" || true)" + RELEASE_KIND="${RELEASE_KIND:-patch}" + + case "${RELEASE_KIND}" in + patch|minor|major) ;; + *) die "Invalid Release kind: ${RELEASE_KIND}" ;; + esac + + resolve_namespace + + [[ "${TAG}" == "${TAG_PREFIX}"* ]] || die "${NAMESPACE} release tags must start with ${TAG_PREFIX}: ${TAG}" + [[ "${RELEASE_BRANCH}" == "${RELEASE_BRANCH_PREFIX}"* ]] || die "${NAMESPACE} release branches must start with ${RELEASE_BRANCH_PREFIX}: ${RELEASE_BRANCH}" + + ensure_valid_ref "refs/tags/${TAG}" + ensure_valid_ref "refs/heads/${RELEASE_BRANCH}" + ensure_valid_ref "refs/heads/${MAIN_BRANCH}" + + VERSION="${TAG:${#TAG_PREFIX}}" + if [[ ! "${VERSION}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + die "Public finalization only supports final SemVer tags, got ${TAG}" + fi + + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + EXPECTED_RELEASE_BRANCH="${RELEASE_BRANCH_PREFIX}${MAJOR}.${MINOR}" + [[ "${RELEASE_BRANCH}" == "${EXPECTED_RELEASE_BRANCH}" ]] || die "Release branch must be ${EXPECTED_RELEASE_BRANCH}, got ${RELEASE_BRANCH}" + + EFFECTIVE_RELEASE_KIND="${RELEASE_KIND}" + if [[ "${RELEASE_KIND}" == "patch" && "${PATCH}" == "0" ]]; then + EFFECTIVE_RELEASE_KIND="minor" + fi + + case "${EFFECTIVE_RELEASE_KIND}" in + patch) + [[ "${PR_BASE_REF}" == "${RELEASE_BRANCH}" ]] || die "Patch release PRs must target ${RELEASE_BRANCH}, got ${PR_BASE_REF}" + ;; + minor) + [[ "${PATCH}" == "0" ]] || die "Minor release PRs require an X.Y.0 tag, got ${TAG}" + [[ "${PR_BASE_REF}" == "${MAIN_BRANCH}" ]] || die "Minor release PRs must target ${MAIN_BRANCH}, got ${PR_BASE_REF}" + ;; + major) + [[ "${MINOR}" == "0" && "${PATCH}" == "0" ]] || die "Major release PRs require an X.0.0 tag, got ${TAG}" + [[ "${PR_BASE_REF}" == "${MAIN_BRANCH}" ]] || die "Major release PRs must target ${MAIN_BRANCH}, got ${PR_BASE_REF}" + ;; + esac +} + +validate_remote_preconditions() { + if remote_ref_exists "refs/tags/${TAG}"; then + die "Release tag already exists: ${TAG}" + fi + + case "${EFFECTIVE_RELEASE_KIND}" in + patch) + remote_ref_exists "refs/heads/${RELEASE_BRANCH}" || die "Patch release branch does not exist: ${RELEASE_BRANCH}" + ;; + minor|major) + if remote_ref_exists "refs/heads/${RELEASE_BRANCH}"; then + die "Release branch already exists: ${RELEASE_BRANCH}" + fi + ;; + esac +} + +fetch_base_and_target() { + require_env MERGE_COMMIT_SHA + + git fetch --no-tags origin "+refs/heads/${PR_BASE_REF}:refs/remotes/origin/${PR_BASE_REF}" + TARGET_SHA="$(git rev-parse --verify "${MERGE_COMMIT_SHA}^{commit}")" || die "Merge commit does not exist locally: ${MERGE_COMMIT_SHA}" + BASE_SHA="$(git rev-parse --verify "origin/${PR_BASE_REF}^{commit}")" || die "Base branch does not exist locally: ${PR_BASE_REF}" + + if [[ "${BASE_SHA}" != "${TARGET_SHA}" ]]; then + die "Base branch ${PR_BASE_REF} is at ${BASE_SHA}, expected merge commit ${TARGET_SHA}" + fi +} + +ensure_release_branch() { + case "${EFFECTIVE_RELEASE_KIND}" in + patch) + remote_ref_exists "refs/heads/${RELEASE_BRANCH}" || die "Patch release branch does not exist: ${RELEASE_BRANCH}" + ;; + minor|major) + local existing_sha + existing_sha="$(remote_ref_sha "refs/heads/${RELEASE_BRANCH}")" + if [[ -n "${existing_sha}" ]]; then + [[ "${existing_sha}" == "${TARGET_SHA}" ]] || die "Release branch ${RELEASE_BRANCH} exists at ${existing_sha}, expected ${TARGET_SHA}" + note "Release branch already exists at ${TARGET_SHA}: ${RELEASE_BRANCH}" + return + fi + + git update-ref "refs/heads/${RELEASE_BRANCH}" "${TARGET_SHA}" + git push origin "refs/heads/${RELEASE_BRANCH}:refs/heads/${RELEASE_BRANCH}" + note "Created release branch ${RELEASE_BRANCH} at ${TARGET_SHA}" + ;; + esac +} + +ensure_release_tag() { + local existing_commit + + if remote_ref_exists "refs/tags/${TAG}"; then + git fetch --no-tags origin "refs/tags/${TAG}:refs/tags/${TAG}" + existing_commit="$(git rev-list -n 1 "${TAG}")" + [[ "${existing_commit}" == "${TARGET_SHA}" ]] || die "Release tag ${TAG} points to ${existing_commit}, expected ${TARGET_SHA}" + note "Release tag already exists at ${TARGET_SHA}: ${TAG}" + return + fi + + git config user.name "${GIT_COMMITTER_NAME:-github-actions[bot]}" + git config user.email "${GIT_COMMITTER_EMAIL:-41898282+github-actions[bot]@users.noreply.github.com}" + git tag -a "${TAG}" "${TARGET_SHA}" -m "Release ${TAG}" + git push origin "refs/tags/${TAG}:refs/tags/${TAG}" + note "Created release tag ${TAG} at ${TARGET_SHA}" +} + +write_outputs() { + [[ -n "${GITHUB_OUTPUT:-}" ]] || return 0 + { + echo "namespace=${NAMESPACE}" + echo "tag=${TAG}" + echo "release_branch=${RELEASE_BRANCH}" + echo "release_kind=${EFFECTIVE_RELEASE_KIND}" + } >> "${GITHUB_OUTPUT}" +} + +main() { + local mode="${1:-}" + case "${mode}" in + validate|finalize) ;; + -h|--help) usage; exit 0 ;; + *) usage; exit 1 ;; + esac + + parse_release_metadata + write_outputs + + if [[ "${mode}" == "validate" ]]; then + validate_remote_preconditions + note "Validated ${NAMESPACE} ${EFFECTIVE_RELEASE_KIND} release ${TAG}" + exit 0 + fi + + fetch_base_and_target + ensure_release_branch + ensure_release_tag +} + +main "$@" diff --git a/.github/workflows/finalize-release.yaml b/.github/workflows/finalize-release.yaml new file mode 100644 index 0000000..2c1035c --- /dev/null +++ b/.github/workflows/finalize-release.yaml @@ -0,0 +1,71 @@ +name: Finalize Public Release + +on: + pull_request_target: + types: [opened, edited, synchronize, reopened, labeled, unlabeled, closed] + branches: + - main + - test-main + - release/** + - test-release/** + +concurrency: + group: public-release-finalizer-${{ github.event.pull_request.number }} + cancel-in-progress: false + +permissions: + contents: read + pull-requests: read + +jobs: + validate-release-pr: + name: Validate release metadata + if: >- + github.event.action != 'closed' && + contains(github.event.pull_request.labels.*.name, 'release') && + startsWith(github.event.pull_request.head.ref, 'sync/copybara-export-') + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Checkout base branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.base.ref }} + fetch-depth: 0 + + - name: Validate release metadata + uses: ./.github/actions/finalize-release + with: + mode: validate + release-namespace: auto + pr-body: ${{ github.event.pull_request.body }} + pr-base-ref: ${{ github.event.pull_request.base.ref }} + pr-head-ref: ${{ github.event.pull_request.head.ref }} + + finalize-release: + name: Create release branch and tag + if: >- + github.event.pull_request.merged == true && + contains(github.event.pull_request.labels.*.name, 'release') && + startsWith(github.event.pull_request.head.ref, 'sync/copybara-export-') + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: write + pull-requests: read + steps: + - name: Checkout base branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.base.ref }} + fetch-depth: 0 + + - name: Create release branch and tag + uses: ./.github/actions/finalize-release + with: + mode: finalize + release-namespace: auto + pr-body: ${{ github.event.pull_request.body }} + pr-base-ref: ${{ github.event.pull_request.base.ref }} + pr-head-ref: ${{ github.event.pull_request.head.ref }} + merge-commit-sha: ${{ github.event.pull_request.merge_commit_sha }} From 11e1142753a373d7e0596755dbd653703f7c6b86 Mon Sep 17 00:00:00 2001 From: Stelios Daveas Date: Fri, 19 Jun 2026 12:02:44 +0000 Subject: [PATCH 2/4] ci: harden public release finalizer --- .github/scripts/finalize-release.sh | 4 ++-- .github/workflows/finalize-release.yaml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/scripts/finalize-release.sh b/.github/scripts/finalize-release.sh index 53abde8..dc03b87 100644 --- a/.github/scripts/finalize-release.sh +++ b/.github/scripts/finalize-release.sh @@ -178,8 +178,8 @@ fetch_base_and_target() { TARGET_SHA="$(git rev-parse --verify "${MERGE_COMMIT_SHA}^{commit}")" || die "Merge commit does not exist locally: ${MERGE_COMMIT_SHA}" BASE_SHA="$(git rev-parse --verify "origin/${PR_BASE_REF}^{commit}")" || die "Base branch does not exist locally: ${PR_BASE_REF}" - if [[ "${BASE_SHA}" != "${TARGET_SHA}" ]]; then - die "Base branch ${PR_BASE_REF} is at ${BASE_SHA}, expected merge commit ${TARGET_SHA}" + if ! git merge-base --is-ancestor "${TARGET_SHA}" "${BASE_SHA}"; then + die "Merge commit ${TARGET_SHA} is not reachable from base branch ${PR_BASE_REF}@${BASE_SHA}" fi } diff --git a/.github/workflows/finalize-release.yaml b/.github/workflows/finalize-release.yaml index 2c1035c..b0e725f 100644 --- a/.github/workflows/finalize-release.yaml +++ b/.github/workflows/finalize-release.yaml @@ -23,6 +23,7 @@ jobs: if: >- github.event.action != 'closed' && contains(github.event.pull_request.labels.*.name, 'release') && + github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.event.pull_request.head.ref, 'sync/copybara-export-') runs-on: ubuntu-24.04 timeout-minutes: 5 @@ -47,6 +48,7 @@ jobs: if: >- github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release') && + github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.event.pull_request.head.ref, 'sync/copybara-export-') runs-on: ubuntu-24.04 timeout-minutes: 5 From 09a149f809331981808b1791336a700e37cf999d Mon Sep 17 00:00:00 2001 From: Stelios Daveas Date: Fri, 19 Jun 2026 12:30:09 +0000 Subject: [PATCH 3/4] ci: chain public release artifacts --- .github/actions/finalize-release/action.yaml | 15 ++++++ .github/workflows/build-docker.yaml | 49 +++++++++++++++----- .github/workflows/finalize-release.yaml | 29 ++++++++++++ .github/workflows/release-binaries.yaml | 14 ++++-- 4 files changed, 92 insertions(+), 15 deletions(-) diff --git a/.github/actions/finalize-release/action.yaml b/.github/actions/finalize-release/action.yaml index 9dd4ae0..3023dcf 100644 --- a/.github/actions/finalize-release/action.yaml +++ b/.github/actions/finalize-release/action.yaml @@ -27,10 +27,25 @@ inputs: required: false default: "" +outputs: + namespace: + description: Resolved release namespace. + value: ${{ steps.finalizer.outputs.namespace }} + tag: + description: Resolved release tag. + value: ${{ steps.finalizer.outputs.tag }} + release_branch: + description: Resolved release branch. + value: ${{ steps.finalizer.outputs.release_branch }} + release_kind: + description: Resolved release kind. + value: ${{ steps.finalizer.outputs.release_kind }} + runs: using: composite steps: - name: Run release finalizer + id: finalizer shell: bash env: RELEASE_NAMESPACE: ${{ inputs.release-namespace }} diff --git a/.github/workflows/build-docker.yaml b/.github/workflows/build-docker.yaml index 436e512..de7926a 100644 --- a/.github/workflows/build-docker.yaml +++ b/.github/workflows/build-docker.yaml @@ -9,9 +9,15 @@ on: push: tags: - 'v*' + workflow_call: + inputs: + tag: + description: 'Tag to build and publish Docker images for (e.g. v0.6.0)' + required: true + type: string concurrency: - group: docker-${{ github.ref }} + group: docker-${{ github.event_name == 'workflow_call' && inputs.tag || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: @@ -22,6 +28,7 @@ jobs: build: if: >- github.event_name == 'push' || + github.event_name == 'workflow_call' || contains(github.event.pull_request.labels.*.name, 'build-docker') name: Build ${{ matrix.image }} (${{ matrix.platform }}) permissions: @@ -48,13 +55,33 @@ jobs: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: + ref: ${{ github.event_name == 'workflow_call' && format('refs/tags/{0}', inputs.tag) || github.ref }} submodules: recursive - - name: Compute short hash + - name: Resolve image metadata id: vars env: - SHA: ${{ github.sha }} - run: echo "short_hash=${SHA::8}" >> "$GITHUB_OUTPUT" + EVENT_NAME: ${{ github.event_name }} + INPUT_TAG: ${{ inputs.tag }} + run: | + SHA="$(git rev-parse HEAD)" + if [[ "$EVENT_NAME" == "workflow_call" ]]; then + RELEASE_TAG="$INPUT_TAG" + elif [[ "$GITHUB_REF" == refs/tags/* ]]; then + RELEASE_TAG="${GITHUB_REF#refs/tags/}" + else + RELEASE_TAG="" + fi + if [[ -n "$RELEASE_TAG" && ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.+-]+)?$ ]]; then + echo "::error::Invalid tag format: '$RELEASE_TAG' (expected e.g. v1.2.3 or v1.2.3-rc.1)" + exit 1 + fi + { + echo "full_hash=${SHA}" + echo "short_hash=${SHA::8}" + echo "git_version=$(git describe --tags --always --dirty 2>/dev/null || echo 'v0.0.0-unknown')" + echo "release_tag=${RELEASE_TAG}" + } >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 @@ -73,8 +100,8 @@ jobs: env: BUILDX_NO_DEFAULT_ATTESTATIONS: 1 GITHUB_TOKEN: ${{ github.token }} - GIT_COMMIT_HASH: ${{ github.sha }} - GIT_VERSION: ${{ github.ref_name }} + GIT_COMMIT_HASH: ${{ steps.vars.outputs.full_hash }} + GIT_VERSION: ${{ steps.vars.outputs.git_version }} GIT_SHORT_HASH: ${{ steps.vars.outputs.short_hash }} - name: Trivy vulnerability scan @@ -95,13 +122,13 @@ jobs: path: trivy-results.sarif - name: Login to Cloudsmith - if: ${{ github.event_name == 'push' }} + if: ${{ github.event_name == 'push' || github.event_name == 'workflow_call' }} uses: ./.github/actions/cloudsmith-login with: registry: ${{ env.REGISTRY }} - name: Push image by digest - if: ${{ github.event_name == 'push' }} + if: ${{ github.event_name == 'push' || github.event_name == 'workflow_call' }} env: IMAGE: ${{ env.REGISTRY }}/${{ env.REGISTRY_NAMESPACE }}/${{ matrix.image }} BAKE_METADATA: ${{ steps.build.outputs.metadata }} @@ -124,7 +151,7 @@ jobs: echo "${REGISTRY_DIGEST}" > "/tmp/digests/${MATRIX_IMAGE}/${PLATFORM_SLUG}" - name: Upload digest - if: ${{ github.event_name == 'push' }} + if: ${{ github.event_name == 'push' || github.event_name == 'workflow_call' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: digest-${{ matrix.image }}-${{ matrix.arch }} @@ -132,7 +159,7 @@ jobs: if-no-files-found: error manifest: - if: ${{ github.event_name == 'push' }} + if: ${{ github.event_name == 'push' || github.event_name == 'workflow_call' }} name: Manifest ${{ matrix.image }} permissions: contents: read @@ -171,7 +198,7 @@ jobs: id: manifest env: IMAGE: ${{ env.REGISTRY }}/${{ env.REGISTRY_NAMESPACE }}/${{ matrix.image }} - RELEASE_TAG: ${{ github.ref_name }} + RELEASE_TAG: ${{ github.event_name == 'workflow_call' && inputs.tag || github.ref_name }} run: | VERSION="${RELEASE_TAG#v}" TAG="${IMAGE}:${VERSION}" diff --git a/.github/workflows/finalize-release.yaml b/.github/workflows/finalize-release.yaml index b0e725f..3fd18df 100644 --- a/.github/workflows/finalize-release.yaml +++ b/.github/workflows/finalize-release.yaml @@ -55,6 +55,11 @@ jobs: permissions: contents: write pull-requests: read + outputs: + namespace: ${{ steps.finalize.outputs.namespace }} + tag: ${{ steps.finalize.outputs.tag }} + release_branch: ${{ steps.finalize.outputs.release_branch }} + release_kind: ${{ steps.finalize.outputs.release_kind }} steps: - name: Checkout base branch uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -63,6 +68,7 @@ jobs: fetch-depth: 0 - name: Create release branch and tag + id: finalize uses: ./.github/actions/finalize-release with: mode: finalize @@ -71,3 +77,26 @@ jobs: pr-base-ref: ${{ github.event.pull_request.base.ref }} pr-head-ref: ${{ github.event.pull_request.head.ref }} merge-commit-sha: ${{ github.event.pull_request.merge_commit_sha }} + + release-binaries: + name: Release binaries + needs: finalize-release + if: needs.finalize-release.outputs.namespace == 'production' + uses: ./.github/workflows/release-binaries.yaml + with: + tag: ${{ needs.finalize-release.outputs.tag }} + secrets: inherit + permissions: + contents: write + + release-docker: + name: Release Docker images + needs: finalize-release + if: needs.finalize-release.outputs.namespace == 'production' + uses: ./.github/workflows/build-docker.yaml + with: + tag: ${{ needs.finalize-release.outputs.tag }} + permissions: + attestations: write + contents: read + id-token: write diff --git a/.github/workflows/release-binaries.yaml b/.github/workflows/release-binaries.yaml index 80128ed..1bd5efc 100644 --- a/.github/workflows/release-binaries.yaml +++ b/.github/workflows/release-binaries.yaml @@ -4,6 +4,12 @@ on: push: tags: - 'v*' + workflow_call: + inputs: + tag: + description: 'Tag to build and release (e.g. v0.6.0)' + required: true + type: string workflow_dispatch: inputs: tag: @@ -47,7 +53,7 @@ jobs: EVENT_NAME: ${{ github.event_name }} INPUT_TAG: ${{ inputs.tag }} run: | - if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + if [[ "$EVENT_NAME" == "workflow_dispatch" || "$EVENT_NAME" == "workflow_call" ]]; then VERSION="$INPUT_TAG" elif [[ "$GITHUB_REF" == refs/tags/* ]]; then VERSION="${GITHUB_REF#refs/tags/}" @@ -64,7 +70,7 @@ jobs: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }} + ref: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && format('refs/tags/{0}', inputs.tag) || github.ref }} submodules: recursive - name: Install Linux system dependencies @@ -165,7 +171,7 @@ jobs: release: name: Create Release needs: sign - if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call' runs-on: ubuntu-24.04 timeout-minutes: 10 permissions: @@ -177,7 +183,7 @@ jobs: EVENT_NAME: ${{ github.event_name }} INPUT_TAG: ${{ inputs.tag }} run: | - if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + if [[ "$EVENT_NAME" == "workflow_dispatch" || "$EVENT_NAME" == "workflow_call" ]]; then TAG="$INPUT_TAG" elif [[ "$GITHUB_REF" == refs/tags/* ]]; then TAG="${GITHUB_REF#refs/tags/}" From 1dc79f4c89717de74261844205a622773f6b248b Mon Sep 17 00:00:00 2001 From: Stelios Daveas Date: Fri, 19 Jun 2026 12:38:53 +0000 Subject: [PATCH 4/4] ci: avoid duplicate public release publishing --- .github/workflows/build-docker.yaml | 29 +++++++++++++------------ .github/workflows/release-binaries.yaml | 26 +++------------------- 2 files changed, 18 insertions(+), 37 deletions(-) diff --git a/.github/workflows/build-docker.yaml b/.github/workflows/build-docker.yaml index de7926a..31bc469 100644 --- a/.github/workflows/build-docker.yaml +++ b/.github/workflows/build-docker.yaml @@ -6,18 +6,21 @@ on: branches: - main - release/* - push: - tags: - - 'v*' workflow_call: inputs: tag: description: 'Tag to build and publish Docker images for (e.g. v0.6.0)' required: true type: string + workflow_dispatch: + inputs: + tag: + description: 'Tag to build and publish Docker images for (e.g. v0.6.0)' + required: true + type: string concurrency: - group: docker-${{ github.event_name == 'workflow_call' && inputs.tag || github.ref }} + group: docker-${{ (github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch') && inputs.tag || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: @@ -27,8 +30,8 @@ env: jobs: build: if: >- - github.event_name == 'push' || github.event_name == 'workflow_call' || + github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'build-docker') name: Build ${{ matrix.image }} (${{ matrix.platform }}) permissions: @@ -55,7 +58,7 @@ jobs: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - ref: ${{ github.event_name == 'workflow_call' && format('refs/tags/{0}', inputs.tag) || github.ref }} + ref: ${{ (github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch') && format('refs/tags/{0}', inputs.tag) || github.ref }} submodules: recursive - name: Resolve image metadata @@ -65,10 +68,8 @@ jobs: INPUT_TAG: ${{ inputs.tag }} run: | SHA="$(git rev-parse HEAD)" - if [[ "$EVENT_NAME" == "workflow_call" ]]; then + if [[ "$EVENT_NAME" == "workflow_call" || "$EVENT_NAME" == "workflow_dispatch" ]]; then RELEASE_TAG="$INPUT_TAG" - elif [[ "$GITHUB_REF" == refs/tags/* ]]; then - RELEASE_TAG="${GITHUB_REF#refs/tags/}" else RELEASE_TAG="" fi @@ -122,13 +123,13 @@ jobs: path: trivy-results.sarif - name: Login to Cloudsmith - if: ${{ github.event_name == 'push' || github.event_name == 'workflow_call' }} + if: ${{ github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }} uses: ./.github/actions/cloudsmith-login with: registry: ${{ env.REGISTRY }} - name: Push image by digest - if: ${{ github.event_name == 'push' || github.event_name == 'workflow_call' }} + if: ${{ github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }} env: IMAGE: ${{ env.REGISTRY }}/${{ env.REGISTRY_NAMESPACE }}/${{ matrix.image }} BAKE_METADATA: ${{ steps.build.outputs.metadata }} @@ -151,7 +152,7 @@ jobs: echo "${REGISTRY_DIGEST}" > "/tmp/digests/${MATRIX_IMAGE}/${PLATFORM_SLUG}" - name: Upload digest - if: ${{ github.event_name == 'push' || github.event_name == 'workflow_call' }} + if: ${{ github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: digest-${{ matrix.image }}-${{ matrix.arch }} @@ -159,7 +160,7 @@ jobs: if-no-files-found: error manifest: - if: ${{ github.event_name == 'push' || github.event_name == 'workflow_call' }} + if: ${{ github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }} name: Manifest ${{ matrix.image }} permissions: contents: read @@ -198,7 +199,7 @@ jobs: id: manifest env: IMAGE: ${{ env.REGISTRY }}/${{ env.REGISTRY_NAMESPACE }}/${{ matrix.image }} - RELEASE_TAG: ${{ github.event_name == 'workflow_call' && inputs.tag || github.ref_name }} + RELEASE_TAG: ${{ inputs.tag }} run: | VERSION="${RELEASE_TAG#v}" TAG="${IMAGE}:${VERSION}" diff --git a/.github/workflows/release-binaries.yaml b/.github/workflows/release-binaries.yaml index 1bd5efc..22c2c4c 100644 --- a/.github/workflows/release-binaries.yaml +++ b/.github/workflows/release-binaries.yaml @@ -1,9 +1,6 @@ name: Release Binaries on: - push: - tags: - - 'v*' workflow_call: inputs: tag: @@ -50,17 +47,9 @@ jobs: - name: Resolve artifact version id: version env: - EVENT_NAME: ${{ github.event_name }} INPUT_TAG: ${{ inputs.tag }} run: | - if [[ "$EVENT_NAME" == "workflow_dispatch" || "$EVENT_NAME" == "workflow_call" ]]; then - VERSION="$INPUT_TAG" - elif [[ "$GITHUB_REF" == refs/tags/* ]]; then - VERSION="${GITHUB_REF#refs/tags/}" - else - echo "::error::Unsupported release trigger: $EVENT_NAME $GITHUB_REF" - exit 1 - fi + VERSION="$INPUT_TAG" if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.+-]+)?$ ]]; then echo "::error::Invalid tag format: '$VERSION' (expected e.g. v1.2.3 or v1.2.3-rc.1)" exit 1 @@ -70,7 +59,7 @@ jobs: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - ref: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && format('refs/tags/{0}', inputs.tag) || github.ref }} + ref: ${{ format('refs/tags/{0}', inputs.tag) }} submodules: recursive - name: Install Linux system dependencies @@ -171,7 +160,6 @@ jobs: release: name: Create Release needs: sign - if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call' runs-on: ubuntu-24.04 timeout-minutes: 10 permissions: @@ -180,17 +168,9 @@ jobs: - name: Resolve tag id: tag env: - EVENT_NAME: ${{ github.event_name }} INPUT_TAG: ${{ inputs.tag }} run: | - if [[ "$EVENT_NAME" == "workflow_dispatch" || "$EVENT_NAME" == "workflow_call" ]]; then - TAG="$INPUT_TAG" - elif [[ "$GITHUB_REF" == refs/tags/* ]]; then - TAG="${GITHUB_REF#refs/tags/}" - else - echo "::error::Unsupported release trigger: $EVENT_NAME $GITHUB_REF" - exit 1 - fi + TAG="$INPUT_TAG" if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.+-]+)?$ ]]; then echo "::error::Invalid tag format: '$TAG' (expected e.g. v1.2.3 or v1.2.3-rc.1)" exit 1