Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions .github/workflows/build-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,21 @@

name: Build Python Package and Create Release

# Dispatched by the Release workflow (bump-version.yaml) after it tags a release,
# so this always runs from master's definition (single source of truth) while
# building the tagged code. Can also be dispatched manually to re-run a release.
on:
push:
tags:
- v*.*.*
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build (e.g. v1.2.3)'
required: true
type: string
make_latest:
description: 'Mark the GitHub release as latest (true/false)'
required: false
default: 'false'
type: string

env:
COMPONENTS: '["gooddata-api-client","gooddata-pandas","gooddata-fdw","gooddata-sdk","gooddata-dbt","gooddata-flight-server","gooddata-flexconnect","gooddata-pipelines"]'
Expand All @@ -39,6 +50,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.tag }}
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Build ${{ matrix.component }}
Expand All @@ -63,6 +77,12 @@ jobs:
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
Comment thread
coderabbitai[bot] marked this conversation as resolved.
persist-credentials: false
- name: Obtain artifacts
uses: actions/download-artifact@v6
with:
Expand All @@ -72,18 +92,21 @@ jobs:
uses: requarks/changelog-action@v1
with:
token: "${{ secrets.GITHUB_TOKEN }}"
tag: ${{ github.ref_name }}
tag: ${{ inputs.tag }}
writeToFile: false
includeRefIssues: false
useGitmojis: false
- name: Create GitHub release
uses: "softprops/action-gh-release@v2"
with:
tag_name: ${{ inputs.tag }}
body: ${{ steps.changelog.outputs.changes }}
token: "${{ secrets.GITHUB_TOKEN }}"
draft: false
prerelease: false
make_latest: true
# accepts the strings "true"/"false"/"legacy"; the Release workflow
# passes "true"/"false" from scripts/is_latest_tag.sh
make_latest: ${{ inputs.make_latest }}
files: |
dist/**/*.whl
dist/**/*.tar.gz
Expand Down Expand Up @@ -120,4 +143,4 @@ jobs:
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: "#releases"
text: "The release of *gooddata-python-sdk@${{ github.ref_name }}*, has been successful. :tada:"
text: "The release of *gooddata-python-sdk@${{ inputs.tag }}*, has been successful. :tada:"
115 changes: 78 additions & 37 deletions .github/workflows/bump-version.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# (C) 2023 GoodData Corporation
name: Bump version & trigger release
name: Release (bump version, publish packages & docs)

# Single source of truth: always run this workflow from master ("Use workflow
# from: master"). Major/minor releases use base_branch=master; patch releases
# for an old LTS line set base_branch to that line's rel/X.Y.0 maintenance
# branch. The workflow definition is always master's; only the code it operates
# on is taken from base_branch.
on:
workflow_dispatch:
inputs:
Expand All @@ -13,20 +18,50 @@ on:
- major
- minor
- patch
base_branch:
description: 'Branch to release from. Use master for major/minor; use rel/X.Y.0 for a patch.'
type: string
required: true
default: 'master'

permissions:
contents: write
pull-requests: write

# Serialize releases so overlapping runs cannot race the is_latest computation
# or contend on the master/tag pushes.
concurrency:
group: release
cancel-in-progress: false

jobs:
bump-version:
runs-on: ubuntu-latest
outputs:
new_version: ${{ steps.bump.outputs.new_version }}
steps:
- name: Validate bump type and base branch
env:
BASE: ${{ inputs.base_branch }}
BUMP: ${{ inputs.bump_type }}
run: |
if [ "$BUMP" = "patch" ]; then
if [[ ! "$BASE" =~ ^rel/[0-9]+\.[0-9]+\.0$ ]]; then
echo "::error::patch releases require base_branch to be a rel/X.Y.0 maintenance branch (got: $BASE)."
exit 1
fi
else
if [ "$BASE" != "master" ]; then
echo "::error::$BUMP releases must use base_branch=master (got: $BASE)."
exit 1
fi
fi

- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ inputs.base_branch }}
fetch-depth: 0 # need full history and tags for is_latest_tag.sh
token: ${{ secrets.TOKEN_GITHUB_YENKINS_ADMIN }} # needed to push to the protected branch

- name: Install uv
Expand All @@ -38,52 +73,58 @@ jobs:

- name: Bump version
id: bump
env:
BUMP: ${{ inputs.bump_type }}
run: |
NEW_VERSION=$(uv run python ./scripts/bump_version.py ${{ github.event.inputs.bump_type }})
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
NEW_VERSION=$(uv run python ./scripts/bump_version.py "$BUMP")
echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"

- name: Bump version in documentation
env:
NEW_VERSION: ${{ steps.bump.outputs.new_version }}
run: |
uv run python ./scripts/bump_doc_dependencies.py ${{ steps.bump.outputs.new_version }}
uv run python ./scripts/bump_doc_dependencies.py "$NEW_VERSION"

- name: Bump version in codebase
env:
NEW_VERSION: ${{ steps.bump.outputs.new_version }}
run: |
make release-ci VERSION=${{ steps.bump.outputs.new_version }}
make release-ci VERSION="$NEW_VERSION"

- name: Specify release branch
id: branch
run: |
if [ "${{ github.event.inputs.bump_type }}" == "patch" ]; then
RELEASE_BRANCH="patch/${{ steps.bump.outputs.new_version }}"
else
RELEASE_BRANCH="rel/${{ steps.bump.outputs.new_version }}"
fi
echo "release_branch=$RELEASE_BRANCH" >> $GITHUB_OUTPUT

- name: Create and push the new version ${{steps.bump.outputs.new_version}}
- name: Commit, tag and push the new version ${{steps.bump.outputs.new_version}}
env:
NEW_VERSION: ${{ steps.bump.outputs.new_version }}
BASE: ${{ inputs.base_branch }}
run: |
git config user.name github-actions
git config user.email github-actions@github.com
git checkout -b ${{ steps.branch.outputs.release_branch }}
git add -A
git commit -m "Release ${{steps.bump.outputs.new_version}}"
git push origin ${{ steps.branch.outputs.release_branch }}
git checkout master
git merge ${{ steps.branch.outputs.release_branch }}
git push origin master
git commit -m "Release ${NEW_VERSION}"
git tag "v${NEW_VERSION}"
# Push the release commit, its tag, and (for master releases) the new
# rel/X.Y.0 maintenance branch atomically -- all refs land or none do.
REFSPECS=("HEAD:${BASE}" "refs/tags/v${NEW_VERSION}")
if [ "$BASE" = "master" ]; then
# Only create the maintenance branch if it does not already exist,
# otherwise the atomic push would be rejected and abort the release.
if git ls-remote --exit-code --heads origin "rel/${NEW_VERSION}" >/dev/null 2>&1; then
echo "::warning::rel/${NEW_VERSION} already exists; leaving it untouched."
else
REFSPECS+=("HEAD:refs/heads/rel/${NEW_VERSION}")
fi
fi
git push --atomic origin "${REFSPECS[@]}"

# TODO: this part waits for docs build and publish optimization it takes too long (~15 minutes)
# trigger-release:
# needs:
# - bump-version
# - create-release-branch
# runs-on: ubuntu-latest
# steps:
# - name: Checkout
# uses: actions/checkout@v5
# - name: Push new tag – v${{ needs.bump-version.outputs.new_version }}
# run: |
# git config user.name GitHub Actions
# git config user.email github-actions@github.com
# git tag v${{ needs.bump-version.outputs.new_version }}
# git push origin v${{ needs.bump-version.outputs.new_version }}
- name: Dispatch build, publish and docs
env:
GH_TOKEN: ${{ secrets.TOKEN_GITHUB_YENKINS_ADMIN }} # GITHUB_TOKEN cannot trigger downstream workflow_dispatch
NEW_VERSION: ${{ steps.bump.outputs.new_version }}
run: |
git fetch --tags origin # ensure the comparison sees the freshest remote tag set
TAG="v${NEW_VERSION}"
IS_LATEST=$(bash scripts/is_latest_tag.sh "$TAG")
echo "Tag $TAG is_latest=$IS_LATEST"
gh workflow run build-release.yaml --ref master -f tag="$TAG" -f make_latest="$IS_LATEST"
if [ "$IS_LATEST" = "true" ]; then
gh workflow run netlify-deploy.yaml --ref master
fi
46 changes: 39 additions & 7 deletions MAINTENANCE.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,45 @@
# Repository maintenance and release

## How to release
* manually run [Bump version & trigger release](.github/workflows/bump-version.yaml) workflow
* after the previous workflow finishes, dispatch the GitHub workflow [Netlify Deploy](.github/workflows/netlify-deploy.yaml) on the `master` branch (takes ~15 minutes)
* The styling of the documentation is taken from the `master` branch. For more details see [generate.sh](scripts/generate.sh).
* after the previous workflow finishes, push tag
* the version should be the same as the one in [Bump version & trigger release](.github/workflows/bump-version.yaml) workflow log
* checkout latest master branch and tag it `vX.Y.Z`
* push the tag to the gooddata/gooddata-python-sdk repository (e.g. `git push <remote> vX.Y.Z`)

Releases are fully automated by a single workflow dispatch. **Always run the
[Release](.github/workflows/bump-version.yaml) workflow from `master`** ("Use
workflow from: master") — the workflow definition is always master's (single
source of truth); the `base_branch` input selects which line's code it acts on.

> The Release workflow dispatches `build-release.yaml` and `netlify-deploy.yaml`
> from `master`, so those workflows must already exist on `master`. The first
> orchestrated release therefore has to run after this change is merged, not from
> a feature branch.

### Standard release (major / minor)
1. Run the [Release](.github/workflows/bump-version.yaml) workflow from `master` with `bump_type = major` or `minor` and `base_branch = master` (the default).
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.
3. It then dispatches — from `master` — the downstream workflows:
* [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.
* [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)).

> `patch` is intentionally rejected when `base_branch = master` — patches come from a `rel/X.Y.Z` maintenance branch (see below).

### Releasing an LTS patch (patching an old version)
Old minor lines (e.g. for CN / on-prem LTS support) are patched from their long-lived `rel/X.Y.0` maintenance branch:

> If the line predates this release scheme it has no `rel/X.Y.0` branch yet — create it once from that line's tag before patching: `git branch rel/X.Y.0 vX.Y.0 && git push origin rel/X.Y.0`.

1. Merge the fix to `master` via a normal PR.
2. Cherry-pick the fix onto the target `rel/X.Y.0` branch and push it.
3. Run the [Release](.github/workflows/bump-version.yaml) workflow **from `master`** with `bump_type = patch` and `base_branch = rel/X.Y.0`.
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.
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.)

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` → …).

### Re-running a release (escape hatch)
To rebuild/republish an existing tag without bumping again, dispatch the build workflow directly from `master`:

```shell
gh workflow run build-release.yaml --ref master -f tag=vX.Y.Z -f make_latest=false
```


### How-to dev release
Expand Down
20 changes: 20 additions & 0 deletions scripts/is_latest_tag.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash
# (C) 2026 GoodData Corporation
# Prints "true" if the given tag is the highest semver among all v*.*.* tags,
# otherwise "false". Requires the repository's tags to be present locally
# (callers should checkout with fetch-depth: 0).
# Usage: is_latest_tag.sh vX.Y.Z
set -e

tag="$1"
if [ -z "$tag" ]; then
echo "Usage: is_latest_tag.sh vX.Y.Z" >&2
exit 1
fi

highest=$(git tag -l 'v*.*.*' | sort -V | tail -1)
if [ "$tag" = "$highest" ]; then
echo "true"
else
echo "false"
fi
Loading