From cfb0e7bd8782108bfc45df3881e17ab994d567bf Mon Sep 17 00:00:00 2001 From: Gerd Zellweger Date: Thu, 13 Aug 2026 01:11:13 -0700 Subject: [PATCH 1/4] [ci] Make the release workflows backport-safe Shipping v0.291.1 from its release branch required hand-patching every floating pointer out of the branch's workflow copies, because the release workflows assume each release is the newest version. Compute whether the released version is the newest existing tag and gate the floating pointers on it: the GitHub 'latest' marker, the :latest docker tag, and the docs.feldera.com deploy. Skip the post-release version bump on main when the published release is older than main's current version. Bump the action-gh-release pin: the old commit predates the make_latest input and silently ignored it. With this, a backport release runs through the normal release path without touching what newer releases own. Signed-off-by: Gerd Zellweger --- .github/workflows/ci-post-release.yml | 25 +++++++++++++++++++- .github/workflows/ci-release.yml | 33 +++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-post-release.yml b/.github/workflows/ci-post-release.yml index e0c7d9e3025..0e723d43f2a 100644 --- a/.github/workflows/ci-post-release.yml +++ b/.github/workflows/ci-post-release.yml @@ -189,36 +189,59 @@ jobs: - name: Determine current version based on pipeline-manager run: | echo "CURRENT_VERSION=$(cargo metadata --no-deps | jq -r '.packages[]|select(.name == "pipeline-manager")|.version')" >> $GITHUB_ENV + # A backport release (published from an old release branch) must not + # bump main's version: main is already versions ahead of it. Bump only + # when the published release is at least main's current version. A + # manual workflow_dispatch carries no release tag and always bumps. + - name: Check that the published release is not a backport + id: bump-check + env: + RELEASED_TAG: ${{ github.event.release.tag_name }} + run: | + released="${RELEASED_TAG#v}" + if [ -n "$released" ] && [ "$(printf '%s\n%s\n' "$released" "$CURRENT_VERSION" | sort -V | head -1)" = "$released" ] && [ "$released" != "$CURRENT_VERSION" ]; then + echo "bump=false" >> "$GITHUB_OUTPUT" + echo "Release v$released is older than main's v$CURRENT_VERSION: skipping the version bump" + else + echo "bump=true" >> "$GITHUB_OUTPUT" + fi - name: Bump cargo versions + if: ${{ steps.bump-check.outputs.bump == 'true' }} run: | cargo set-version --bump ${{ vars.RELEASE_NEXT_VERSION }} cargo run --release --locked --bin pipeline-manager -- --dump-openapi - name: Determine next version based on pipeline-manager + if: ${{ steps.bump-check.outputs.bump == 'true' }} run: | echo "NEXT_VERSION=$(cargo metadata --no-deps | jq -r '.packages[]|select(.name == "pipeline-manager")|.version')" >> $GITHUB_ENV - name: Adjust python version + if: ${{ steps.bump-check.outputs.bump == 'true' }} working-directory: ./python run: | sed -i "s/version = \"${{ env.CURRENT_VERSION }}\"/version = \"${{ env.NEXT_VERSION }}\"/g" pyproject.toml uv sync - name: Adjust dbt-feldera version + if: ${{ steps.bump-check.outputs.bump == 'true' }} working-directory: ./python/dbt-feldera run: | sed -i "s/version = \"${{ env.CURRENT_VERSION }}\"/version = \"${{ env.NEXT_VERSION }}\"/g" pyproject.toml sed -i "s/version: '${{ env.CURRENT_VERSION }}'/version: '${{ env.NEXT_VERSION }}'/g" dbt/include/feldera/dbt_project.yml - name: Adjust felderize version + if: ${{ steps.bump-check.outputs.bump == 'true' }} working-directory: ./python/felderize run: | sed -i "s/version = \"${{ env.CURRENT_VERSION }}\"/version = \"${{ env.NEXT_VERSION }}\"/g" pyproject.toml - name: Adjust sql compiler version + if: ${{ steps.bump-check.outputs.bump == 'true' }} working-directory: ./sql-to-dbsp-compiler/SQL-compiler run: | sed -i "s|${{ env.CURRENT_VERSION }}|${{ env.NEXT_VERSION }}|g" pom.xml - name: List changes + if: ${{ steps.bump-check.outputs.bump == 'true' }} run: | git diff - uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # a94899bca583c204427a224a7af87c02f9b325d5 - if: ${{ vars.RELEASE_DRY_RUN == 'false' }} + if: ${{ vars.RELEASE_DRY_RUN == 'false' && steps.bump-check.outputs.bump == 'true' }} with: message: "ci: Prepare for v${{ env.NEXT_VERSION }}" push: origin main diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index f503eff2af2..b99fc3b519e 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -64,6 +64,21 @@ jobs: ref: ${{ env.SHA_TO_RELEASE }} persist-credentials: false + # A backport release on an old version line must not move any floating + # pointer: the GitHub "latest" marker, the :latest docker tag, or the + # live docs site all have to keep tracking the newest version. + - name: Determine whether this version is the newest + id: newest + run: | + highest=$(git tag --list 'v*' | sed 's/^v//' | sort -V | tail -1) + if [ "$(printf '%s\n%s\n' "$highest" "$CURRENT_VERSION" | sort -V | tail -1)" = "$CURRENT_VERSION" ]; then + echo "is_newest=true" >> "$GITHUB_OUTPUT" + echo "v$CURRENT_VERSION is the newest version (highest existing tag: v$highest)" + else + echo "is_newest=false" >> "$GITHUB_OUTPUT" + echo "v$CURRENT_VERSION is a backport (highest existing tag: v$highest); floating pointers stay put" + fi + - name: Download artifact id: download-artifact uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 @@ -100,13 +115,15 @@ jobs: repositories: ${{ github.event.repository.name }} - name: Release on GitHub - uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # 72f2c25fcb47643c292f7107632f7a47c1df5cd8 + # The previously pinned commit predates the make_latest input and + # silently ignored it, so every release became "latest". + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: target_commitish: ${{ env.SHA_TO_RELEASE }} tag_name: v${{ env.CURRENT_VERSION }} draft: ${{ vars.RELEASE_DRY_RUN }} generate_release_notes: true - make_latest: true + make_latest: ${{ steps.newest.outputs.is_newest }} files: | pipeline-manager-aarch64-unknown-linux-gnu.zip pipeline-manager-x86_64-unknown-linux-gnu.zip @@ -139,7 +156,9 @@ jobs: run: unzip feldera-docs.zip -d docs - name: Deploy docs.feldera.com - if: ${{ vars.RELEASE_DRY_RUN == 'false' }} + # is_newest: publishing a backport's docs would replace the live site + # with an old version's documentation. + if: ${{ vars.RELEASE_DRY_RUN == 'false' && steps.newest.outputs.is_newest == 'true' }} uses: JamesIves/github-pages-deploy-action@4ef313c6a410f0883308623d45510e4a11625465 # v4.7.5 with: folder: docs @@ -163,8 +182,14 @@ jobs: with: version: latest - - name: Tag docker image with version and latest + - name: Tag docker image with version if: ${{ vars.RELEASE_DRY_RUN == 'false' }} run: | docker buildx imagetools create -t ${{ vars.FELDERA_IMAGE_NAME }}:${{ env.CURRENT_VERSION }} ${{ vars.FELDERA_IMAGE_NAME }}:sha-${{ env.SHA_TO_RELEASE }} + + - name: Tag docker image as latest + # is_newest: moving :latest to a backport would downgrade every user + # who pulls the floating tag. + if: ${{ vars.RELEASE_DRY_RUN == 'false' && steps.newest.outputs.is_newest == 'true' }} + run: | docker buildx imagetools create -t ${{ vars.FELDERA_IMAGE_NAME }}:latest ${{ vars.FELDERA_IMAGE_NAME }}:sha-${{ env.SHA_TO_RELEASE }} From 59c4af3c76a81993907111d2100b1c3e2a31d3af Mon Sep 17 00:00:00 2001 From: Gerd Zellweger Date: Thu, 13 Aug 2026 10:23:57 -0700 Subject: [PATCH 2/4] [ci] Extract the is-newest-version check into a composite action feldera/cloud's release workflow needs the identical comparison; a composite action gives both repos one source of truth. Signed-off-by: Gerd Zellweger --- .github/actions/is-newest-version/action.yml | 34 ++++++++++++++++++++ .github/workflows/ci-release.yml | 12 ++----- 2 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 .github/actions/is-newest-version/action.yml diff --git a/.github/actions/is-newest-version/action.yml b/.github/actions/is-newest-version/action.yml new file mode 100644 index 00000000000..ebefdf19b03 --- /dev/null +++ b/.github/actions/is-newest-version/action.yml @@ -0,0 +1,34 @@ +name: Is newest version +description: > + Decide whether the version being released is the newest of the repository's + v* tags, so release workflows can keep floating pointers (the GitHub + "latest" marker, :latest image tags, live docs and sandboxes) on the newest + version when shipping a backport from an old release line. Requires a + checkout with tags fetched; the new release's own tag must not exist yet. + +inputs: + version: + description: The version being released, without the leading v. + required: true + +outputs: + is_newest: + description: '"true" when the version is >= every existing v* tag, else "false".' + value: ${{ steps.compare.outputs.is_newest }} + +runs: + using: composite + steps: + - id: compare + shell: bash + env: + VERSION: ${{ inputs.version }} + run: | + highest=$(git tag --list 'v*' | sed 's/^v//' | sort -V | tail -1) + if [ "$(printf '%s\n%s\n' "$highest" "$VERSION" | sort -V | tail -1)" = "$VERSION" ]; then + echo "is_newest=true" >> "$GITHUB_OUTPUT" + echo "v$VERSION is the newest version (highest existing tag: v$highest)" + else + echo "is_newest=false" >> "$GITHUB_OUTPUT" + echo "v$VERSION is a backport (highest existing tag: v$highest); floating pointers stay put" + fi diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index b99fc3b519e..2aa66fe2bd8 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -69,15 +69,9 @@ jobs: # live docs site all have to keep tracking the newest version. - name: Determine whether this version is the newest id: newest - run: | - highest=$(git tag --list 'v*' | sed 's/^v//' | sort -V | tail -1) - if [ "$(printf '%s\n%s\n' "$highest" "$CURRENT_VERSION" | sort -V | tail -1)" = "$CURRENT_VERSION" ]; then - echo "is_newest=true" >> "$GITHUB_OUTPUT" - echo "v$CURRENT_VERSION is the newest version (highest existing tag: v$highest)" - else - echo "is_newest=false" >> "$GITHUB_OUTPUT" - echo "v$CURRENT_VERSION is a backport (highest existing tag: v$highest); floating pointers stay put" - fi + uses: ./.github/actions/is-newest-version + with: + version: ${{ env.CURRENT_VERSION }} - name: Download artifact id: download-artifact From f86d601966b2694bfe0d514ae56d8ada8f227c02 Mon Sep 17 00:00:00 2001 From: Gerd Zellweger Date: Thu, 13 Aug 2026 10:25:45 -0700 Subject: [PATCH 3/4] [ci] Address review: fail closed and filter tags strictly The is-newest-version check now errors out when no vX.Y.Z tag is visible instead of defaulting to newest (a tagless checkout would have moved every floating pointer onto a backport), and only strict X.Y.Z tags participate in the comparison (a stray vnext or v1.2.3-rc1 tag outranks real versions under sort -V and would flip a normal release to a backport). Also correct the pin-bump rationale: v2.3.2 accepts make_latest but fails to apply it when publishing. Signed-off-by: Gerd Zellweger --- .github/actions/is-newest-version/action.yml | 12 +++++++++++- .github/workflows/ci-release.yml | 6 +++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/actions/is-newest-version/action.yml b/.github/actions/is-newest-version/action.yml index ebefdf19b03..89c8d2e775f 100644 --- a/.github/actions/is-newest-version/action.yml +++ b/.github/actions/is-newest-version/action.yml @@ -24,7 +24,17 @@ runs: env: VERSION: ${{ inputs.version }} run: | - highest=$(git tag --list 'v*' | sed 's/^v//' | sort -V | tail -1) + # Strict X.Y.Z filter: a stray non-numeric tag (vnext) or a + # pre-release tag (v1.2.3-rc1) would otherwise outrank real versions + # under sort -V and silently flip a normal release to a backport. + highest=$(git tag --list 'v[0-9]*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sed 's/^v//' | sort -V | tail -1 || true) + # Fail closed: an empty tag list on a release repo means the checkout + # did not fetch tags. Guessing "newest" here would move every + # floating pointer onto whatever is being released. + if [ -z "$highest" ]; then + echo "::error::no vX.Y.Z tags visible; was the checkout made with fetch-tags?" + exit 1 + fi if [ "$(printf '%s\n%s\n' "$highest" "$VERSION" | sort -V | tail -1)" = "$VERSION" ]; then echo "is_newest=true" >> "$GITHUB_OUTPUT" echo "v$VERSION is the newest version (highest existing tag: v$highest)" diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 2aa66fe2bd8..b1d5d13bcf9 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -109,9 +109,9 @@ jobs: repositories: ${{ github.event.repository.name }} - name: Release on GitHub - # The previously pinned commit predates the make_latest input and - # silently ignored it, so every release became "latest". - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + # The previously pinned v2.3.2 accepts make_latest but fails to apply + # it when publishing, so every release became "latest" regardless. + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 with: target_commitish: ${{ env.SHA_TO_RELEASE }} tag_name: v${{ env.CURRENT_VERSION }} From 255e61f1d2f12797311f36258c40c90cc29dd39e Mon Sep 17 00:00:00 2001 From: Gerd Zellweger Date: Thu, 13 Aug 2026 10:26:20 -0700 Subject: [PATCH 4/4] [ci] Drop the action-pin rationale comment Signed-off-by: Gerd Zellweger --- .github/workflows/ci-release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index b1d5d13bcf9..62bc06bdcc4 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -109,8 +109,6 @@ jobs: repositories: ${{ github.event.repository.name }} - name: Release on GitHub - # The previously pinned v2.3.2 accepts make_latest but fails to apply - # it when publishing, so every release became "latest" regardless. uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 with: target_commitish: ${{ env.SHA_TO_RELEASE }}