Skip to content

Commit 44038d3

Browse files
committed
ci: master-orchestrated release with LTS patch support
Rework the release process into a single workflow that always runs from master, so all release logic has one source of truth. Previously the release relied on dispatching from a branch and on tag-push triggers, both of which run the workflow file as it exists on that ref -- meaning old rel/* branches would run stale release logic. Release workflow (bump-version.yaml) is now an orchestrator, always run from master: - inputs: bump_type + base_branch (default master) - major/minor require base_branch=master; patch requires a rel/X.Y.Z branch (validated, fails fast otherwise) - checks out base_branch, bumps, commits, and tags on it; for master releases it also creates the long-lived rel/X.Y.0 maintenance branch - computes whether the tag is the highest semver (is_latest_tag.sh) and dispatches the downstream workflows from master via `gh workflow run --ref master`, passing the tag and make_latest This lets old LTS lines be patched by running the master workflow with base_branch=rel/X.Y.0 -- needed now that we offer LTS support for CN / on-prem deployments. Multiple patches accumulate on the same rel/X.Y.0 branch (bump_version.py increments from the branch's current version). build-release.yaml switches from a tag-push trigger to workflow_dispatch with tag + make_latest inputs, building the tagged code. Its filename is unchanged, so PyPI trusted publishing (OIDC) needs no reconfiguration. Old-line patches therefore publish to PyPI but produce a non-latest GitHub release. netlify-deploy.yaml reverts to a plain workflow_dispatch; the orchestrator only dispatches it for the latest release, so old-line patches skip docs. scripts/is_latest_tag.sh reports whether a tag is the highest semver. MAINTENANCE.md documents the master-orchestrated flow, the LTS patch process, and the re-run escape hatch.
1 parent 88138a3 commit 44038d3

4 files changed

Lines changed: 129 additions & 47 deletions

File tree

.github/workflows/build-release.yaml

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,21 @@
99

1010
name: Build Python Package and Create Release
1111

12+
# Dispatched by the Release workflow (bump-version.yaml) after it tags a release,
13+
# so this always runs from master's definition (single source of truth) while
14+
# building the tagged code. Can also be dispatched manually to re-run a release.
1215
on:
13-
push:
14-
tags:
15-
- v*.*.*
16+
workflow_dispatch:
17+
inputs:
18+
tag:
19+
description: 'Release tag to build (e.g. v1.2.3)'
20+
required: true
21+
type: string
22+
make_latest:
23+
description: 'Mark the GitHub release as latest (true/false)'
24+
required: false
25+
default: 'false'
26+
type: string
1627

1728
env:
1829
COMPONENTS: '["gooddata-api-client","gooddata-pandas","gooddata-fdw","gooddata-sdk","gooddata-dbt","gooddata-flight-server","gooddata-flexconnect","gooddata-pipelines"]'
@@ -39,6 +50,8 @@ jobs:
3950
runs-on: ubuntu-latest
4051
steps:
4152
- uses: actions/checkout@v5
53+
with:
54+
ref: ${{ inputs.tag }}
4255
- name: Install uv
4356
uses: astral-sh/setup-uv@v7
4457
- name: Build ${{ matrix.component }}
@@ -63,6 +76,11 @@ jobs:
6376
permissions:
6477
contents: write
6578
steps:
79+
- name: Checkout
80+
uses: actions/checkout@v5
81+
with:
82+
ref: ${{ inputs.tag }}
83+
fetch-depth: 0
6684
- name: Obtain artifacts
6785
uses: actions/download-artifact@v6
6886
with:
@@ -72,18 +90,19 @@ jobs:
7290
uses: requarks/changelog-action@v1
7391
with:
7492
token: "${{ secrets.GITHUB_TOKEN }}"
75-
tag: ${{ github.ref_name }}
93+
tag: ${{ inputs.tag }}
7694
writeToFile: false
7795
includeRefIssues: false
7896
useGitmojis: false
7997
- name: Create GitHub release
8098
uses: "softprops/action-gh-release@v2"
8199
with:
100+
tag_name: ${{ inputs.tag }}
82101
body: ${{ steps.changelog.outputs.changes }}
83102
token: "${{ secrets.GITHUB_TOKEN }}"
84103
draft: false
85104
prerelease: false
86-
make_latest: true
105+
make_latest: ${{ inputs.make_latest }}
87106
files: |
88107
dist/**/*.whl
89108
dist/**/*.tar.gz
@@ -120,4 +139,4 @@ jobs:
120139
token: ${{ secrets.SLACK_BOT_TOKEN }}
121140
payload: |
122141
channel: "#releases"
123-
text: "The release of *gooddata-python-sdk@${{ github.ref_name }}*, has been successful. :tada:"
142+
text: "The release of *gooddata-python-sdk@${{ inputs.tag }}*, has been successful. :tada:"
Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# (C) 2023 GoodData Corporation
2-
name: Bump version & trigger release
2+
name: Release (bump version, publish packages & docs)
33

4+
# Single source of truth: always run this workflow from master ("Use workflow
5+
# from: master"). Major/minor releases use base_branch=master; patch releases
6+
# for an old LTS line set base_branch to that line's rel/X.Y.0 maintenance
7+
# branch. The workflow definition is always master's; only the code it operates
8+
# on is taken from base_branch.
49
on:
510
workflow_dispatch:
611
inputs:
@@ -13,6 +18,11 @@ on:
1318
- major
1419
- minor
1520
- patch
21+
base_branch:
22+
description: 'Branch to release from. Use master for major/minor; use rel/X.Y.Z for a patch.'
23+
type: string
24+
required: true
25+
default: 'master'
1626

1727
permissions:
1828
contents: write
@@ -24,9 +34,27 @@ jobs:
2434
outputs:
2535
new_version: ${{ steps.bump.outputs.new_version }}
2636
steps:
37+
- name: Validate bump type and base branch
38+
run: |
39+
BASE="${{ inputs.base_branch }}"
40+
BUMP="${{ inputs.bump_type }}"
41+
if [ "$BUMP" = "patch" ]; then
42+
if [[ "$BASE" != rel/* ]]; then
43+
echo "::error::patch releases require base_branch to be a rel/X.Y.Z maintenance branch (got: $BASE)."
44+
exit 1
45+
fi
46+
else
47+
if [ "$BASE" != "master" ]; then
48+
echo "::error::$BUMP releases must use base_branch=master (got: $BASE)."
49+
exit 1
50+
fi
51+
fi
52+
2753
- name: Checkout
2854
uses: actions/checkout@v5
2955
with:
56+
ref: ${{ inputs.base_branch }}
57+
fetch-depth: 0 # need full history and tags for is_latest_tag.sh
3058
token: ${{ secrets.TOKEN_GITHUB_YENKINS_ADMIN }} # needed to push to the protected branch
3159

3260
- name: Install uv
@@ -39,7 +67,7 @@ jobs:
3967
- name: Bump version
4068
id: bump
4169
run: |
42-
NEW_VERSION=$(uv run python ./scripts/bump_version.py ${{ github.event.inputs.bump_type }})
70+
NEW_VERSION=$(uv run python ./scripts/bump_version.py ${{ inputs.bump_type }})
4371
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
4472
4573
- name: Bump version in documentation
@@ -50,40 +78,31 @@ jobs:
5078
run: |
5179
make release-ci VERSION=${{ steps.bump.outputs.new_version }}
5280
53-
- name: Specify release branch
54-
id: branch
55-
run: |
56-
if [ "${{ github.event.inputs.bump_type }}" == "patch" ]; then
57-
RELEASE_BRANCH="patch/${{ steps.bump.outputs.new_version }}"
58-
else
59-
RELEASE_BRANCH="rel/${{ steps.bump.outputs.new_version }}"
60-
fi
61-
echo "release_branch=$RELEASE_BRANCH" >> $GITHUB_OUTPUT
62-
63-
- name: Create and push the new version ${{steps.bump.outputs.new_version}}
81+
- name: Commit, tag and push the new version ${{steps.bump.outputs.new_version}}
6482
run: |
6583
git config user.name github-actions
6684
git config user.email github-actions@github.com
67-
git checkout -b ${{ steps.branch.outputs.release_branch }}
85+
NEW_VERSION="${{ steps.bump.outputs.new_version }}"
86+
BASE="${{ inputs.base_branch }}"
6887
git add -A
69-
git commit -m "Release ${{steps.bump.outputs.new_version}}"
70-
git push origin ${{ steps.branch.outputs.release_branch }}
71-
git checkout master
72-
git merge ${{ steps.branch.outputs.release_branch }}
73-
git push origin master
88+
git commit -m "Release ${NEW_VERSION}"
89+
git push origin "HEAD:${BASE}"
90+
if [ "$BASE" = "master" ]; then
91+
# Create the long-lived maintenance branch for this line so it can
92+
# be patched later (points at the release commit).
93+
git push origin "HEAD:refs/heads/rel/${NEW_VERSION}"
94+
fi
95+
git tag "v${NEW_VERSION}"
96+
git push origin "v${NEW_VERSION}"
7497
75-
# TODO: this part waits for docs build and publish optimization it takes too long (~15 minutes)
76-
# trigger-release:
77-
# needs:
78-
# - bump-version
79-
# - create-release-branch
80-
# runs-on: ubuntu-latest
81-
# steps:
82-
# - name: Checkout
83-
# uses: actions/checkout@v5
84-
# - name: Push new tag – v${{ needs.bump-version.outputs.new_version }}
85-
# run: |
86-
# git config user.name GitHub Actions
87-
# git config user.email github-actions@github.com
88-
# git tag v${{ needs.bump-version.outputs.new_version }}
89-
# git push origin v${{ needs.bump-version.outputs.new_version }}
98+
- name: Dispatch build, publish and docs
99+
env:
100+
GH_TOKEN: ${{ secrets.TOKEN_GITHUB_YENKINS_ADMIN }} # GITHUB_TOKEN cannot trigger downstream workflow_dispatch
101+
run: |
102+
TAG="v${{ steps.bump.outputs.new_version }}"
103+
IS_LATEST=$(bash scripts/is_latest_tag.sh "$TAG")
104+
echo "Tag $TAG is_latest=$IS_LATEST"
105+
gh workflow run build-release.yaml --ref master -f tag="$TAG" -f make_latest="$IS_LATEST"
106+
if [ "$IS_LATEST" = "true" ]; then
107+
gh workflow run netlify-deploy.yaml --ref master
108+
fi

MAINTENANCE.md

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,37 @@
11
# Repository maintenance and release
22

33
## How to release
4-
* manually run [Bump version & trigger release](.github/workflows/bump-version.yaml) workflow
5-
* after the previous workflow finishes, dispatch the GitHub workflow [Netlify Deploy](.github/workflows/netlify-deploy.yaml) on the `master` branch (takes ~15 minutes)
6-
* The styling of the documentation is taken from the `master` branch. For more details see [generate.sh](scripts/generate.sh).
7-
* after the previous workflow finishes, push tag
8-
* the version should be the same as the one in [Bump version & trigger release](.github/workflows/bump-version.yaml) workflow log
9-
* checkout latest master branch and tag it `vX.Y.Z`
10-
* push the tag to the gooddata/gooddata-python-sdk repository (e.g. `git push <remote> vX.Y.Z`)
4+
5+
Releases are fully automated by a single workflow dispatch. **Always run the
6+
[Release](.github/workflows/bump-version.yaml) workflow from `master`** ("Use
7+
workflow from: master") — the workflow definition is always master's (single
8+
source of truth); the `base_branch` input selects which line's code it acts on.
9+
10+
### Standard release (major / minor)
11+
1. Run the [Release](.github/workflows/bump-version.yaml) workflow from `master` with `bump_type = major` or `minor` and `base_branch = master` (the default).
12+
2. The run bumps the version, commits onto `master`, creates the long-lived `rel/X.Y.0` maintenance branch (for later patching), and pushes the `vX.Y.0` tag.
13+
3. It then dispatches — from `master` — the downstream workflows:
14+
* [Build Python Package and Create Release](.github/workflows/build-release.yaml) builds all components, publishes them to PyPI, creates the GitHub release (marked latest), and notifies Slack.
15+
* [Netlify Deploy](.github/workflows/netlify-deploy.yaml) rebuilds and deploys the documentation (styling is taken from the `master` branch; see [generate.sh](scripts/generate.sh)).
16+
17+
> `patch` is intentionally rejected when `base_branch = master` — patches come from a `rel/X.Y.Z` maintenance branch (see below).
18+
19+
### Releasing an LTS patch (patching an old version)
20+
Old minor lines (e.g. for CN / on-prem LTS support) are patched from their long-lived `rel/X.Y.0` maintenance branch:
21+
1. Merge the fix to `master` via a normal PR.
22+
2. Cherry-pick the fix onto the target `rel/X.Y.0` branch and push it.
23+
3. Run the [Release](.github/workflows/bump-version.yaml) workflow **from `master`** with `bump_type = patch` and `base_branch = rel/X.Y.0`.
24+
4. The workflow checks out `rel/X.Y.0`, bumps the patch (e.g. `1.5.0``1.5.1`), commits it back onto that branch, and pushes the `vX.Y.Z` tag. It does **not** create a new branch.
25+
5. It dispatches `build-release.yaml` from `master`, which publishes every component to PyPI and creates the GitHub release. Because the tag is not the highest semver, the release is marked **non-latest** and documentation is **not** redeployed. (Patching the current latest line is the exception — that tag *is* the highest, so it is marked latest and docs deploy.)
26+
27+
Subsequent patches repeat steps 1-4 on the same `rel/X.Y.0` branch; each reads the branch's current version and increments the patch component (`1.5.1``1.5.2` → …).
28+
29+
### Re-running a release (escape hatch)
30+
To rebuild/republish an existing tag without bumping again, dispatch the build workflow directly from `master`:
31+
32+
```
33+
gh workflow run build-release.yaml --ref master -f tag=vX.Y.Z -f make_latest=false
34+
```
1135

1236

1337
### How-to dev release

scripts/is_latest_tag.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash
2+
# (C) 2026 GoodData Corporation
3+
# Prints "true" if the given tag is the highest semver among all v*.*.* tags,
4+
# otherwise "false". Requires the repository's tags to be present locally
5+
# (callers should checkout with fetch-depth: 0).
6+
# Usage: is_latest_tag.sh vX.Y.Z
7+
set -e
8+
9+
tag="$1"
10+
if [ -z "$tag" ]; then
11+
echo "Usage: is_latest_tag.sh vX.Y.Z" >&2
12+
exit 1
13+
fi
14+
15+
highest=$(git tag -l 'v*.*.*' | sort -V | tail -1)
16+
if [ "$tag" = "$highest" ]; then
17+
echo "true"
18+
else
19+
echo "false"
20+
fi

0 commit comments

Comments
 (0)