Skip to content

Commit 6e45ea3

Browse files
committed
ci: drive the whole release from a single tag push
Releasing needed three manual actions: dispatch bump-version, dispatch netlify-deploy, then hand-tag master. Now bump-version pushes vX.Y.Z at the end of the bump job, and that one tag event triggers build-release and netlify-deploy in parallel. The tag is pushed from the bump job itself rather than the commented-out trigger-release job, which would have tagged the pre-bump commit: its fresh checkout resolves to master as of dispatch time. The push must also carry TOKEN_GITHUB_YENKINS_ADMIN, already used for the master push, because GitHub does not trigger workflows from GITHUB_TOKEN pushes -- the likely reason that job was left disabled. Release branches are now rel/X.Y.Z for every bump type. The old patch/X.Y.Z naming was the repository's only reference to patch/, and it hid patch releases from both the pre-merge pipeline and the docs build, which key off rel/** and rel/* respectively. Adds an is-latest-release guard consumed by both downstream workflows. Without it, tagging a patch of an older line would deploy that tag's documentation over the current site and take the "Latest" badge from the newest release. Also quotes $GITHUB_OUTPUT in the bump step, clearing the file's last shellcheck warning.
1 parent ca239dd commit 6e45ea3

6 files changed

Lines changed: 255 additions & 41 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# (C) 2026 GoodData Corporation
2+
name: Is latest release
3+
description: >
4+
Decides whether the tag that triggered the workflow is the highest released version.
5+
Patch releases of older lines (e.g. v1.60.1 while master is at 1.73.0) must not displace
6+
the current release on the releases page, nor overwrite the production documentation.
7+
8+
Requires the repository to be checked out with fetch-depth: 0 so that all tags are present.
9+
On a non-tag ref (e.g. a manual workflow_dispatch) the result is 'true'.
10+
11+
outputs:
12+
is_latest:
13+
description: "'true' when the triggering tag is the highest v*.*.* tag, otherwise 'false'"
14+
value: ${{ steps.check.outputs.is_latest }}
15+
16+
runs:
17+
using: composite
18+
steps:
19+
- id: check
20+
shell: bash
21+
env:
22+
TAG: ${{ github.ref_name }}
23+
REF_TYPE: ${{ github.ref_type }}
24+
run: |
25+
set -euo pipefail
26+
27+
if [ "$REF_TYPE" != "tag" ]; then
28+
echo "Ref '$TAG' is not a tag; treating as latest."
29+
echo "is_latest=true" >> "$GITHUB_OUTPUT"
30+
exit 0
31+
fi
32+
33+
highest=$(git tag -l 'v*.*.*' | sort -V | tail -n 1)
34+
echo "Triggering tag: $TAG"
35+
echo "Highest tag: $highest"
36+
37+
if [ -z "$highest" ]; then
38+
echo "No v*.*.* tags found -- the checkout is probably missing tags (needs fetch-depth: 0)."
39+
exit 1
40+
fi
41+
42+
if [ "$TAG" = "$highest" ]; then
43+
echo "is_latest=true" >> "$GITHUB_OUTPUT"
44+
else
45+
echo "is_latest=false" >> "$GITHUB_OUTPUT"
46+
fi

.github/workflows/build-release.yaml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,25 @@ jobs:
5656
path: |
5757
${{ matrix.component == 'gooddata-api-client' && format('{0}/dist/', matrix.component) || format('packages/{0}/dist/', matrix.component) }}
5858
if-no-files-found: error
59+
check-latest:
60+
name: Check whether the tag is the latest release
61+
runs-on: ubuntu-latest
62+
outputs:
63+
is_latest: ${{ steps.check.outputs.is_latest }}
64+
steps:
65+
- name: Checkout
66+
uses: actions/checkout@v5
67+
with:
68+
fetch-depth: 0 # the guard compares against every v*.*.* tag
69+
- id: check
70+
uses: ./.github/actions/is-latest-release
71+
5972
github_release:
6073
name: Create GitHub release
6174
runs-on: ubuntu-latest
62-
needs: build
75+
needs:
76+
- build
77+
- check-latest
6378
permissions:
6479
contents: write
6580
steps:
@@ -83,7 +98,9 @@ jobs:
8398
token: "${{ secrets.GITHUB_TOKEN }}"
8499
draft: false
85100
prerelease: false
86-
make_latest: true
101+
# A patch of an older line must not take the "Latest" badge from the
102+
# current release, so this is false for e.g. v1.60.1 while v1.73.0 exists.
103+
make_latest: ${{ needs.check-latest.outputs.is_latest }}
87104
files: |
88105
dist/**/*.whl
89106
dist/**/*.tar.gz

.github/workflows/bump-version.yaml

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
id: bump
4141
run: |
4242
NEW_VERSION=$(uv run python ./scripts/bump_version.py ${{ github.event.inputs.bump_type }})
43-
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
43+
echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
4444
4545
- name: Bump version in documentation
4646
run: |
@@ -50,40 +50,28 @@ jobs:
5050
run: |
5151
make release-ci VERSION=${{ steps.bump.outputs.new_version }}
5252
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-
6353
- name: Create and push the new version ${{steps.bump.outputs.new_version}}
54+
env:
55+
VERSION: ${{ steps.bump.outputs.new_version }}
6456
run: |
6557
git config user.name github-actions
6658
git config user.email github-actions@github.com
67-
git checkout -b ${{ steps.branch.outputs.release_branch }}
59+
60+
# Every release branch is rel/X.Y.Z, patches included. The docs build
61+
# (scripts/generate.sh) and the pre-merge pipeline both key off rel/**.
62+
git checkout -b "rel/$VERSION"
6863
git add -A
69-
git commit -m "Release ${{steps.bump.outputs.new_version}}"
70-
git push origin ${{ steps.branch.outputs.release_branch }}
64+
git commit -m "Release $VERSION"
65+
66+
# Order matters: the docs build enumerates remote rel/* branches, so
67+
# rel/$VERSION has to be on the remote before the tag starts anything.
68+
git push origin "rel/$VERSION"
7169
git checkout master
72-
git merge ${{ steps.branch.outputs.release_branch }}
70+
git merge "rel/$VERSION"
7371
git push origin master
7472
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 }}
73+
# The tag push is the single trigger for build-release and netlify-deploy.
74+
# It works only because the checkout above uses TOKEN_GITHUB_YENKINS_ADMIN --
75+
# GitHub does not trigger workflows from pushes made with GITHUB_TOKEN.
76+
git tag "v$VERSION"
77+
git push origin "v$VERSION"

.github/workflows/netlify-deploy.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,33 @@
11
name: Netlify Deploy
22
on:
33
workflow_dispatch:
4+
# Released together with the packages: the tag pushed by bump-version triggers
5+
# this workflow and build-release.yaml at the same time, so docs and packages
6+
# build in parallel.
7+
push:
8+
tags:
9+
- v*.*.*
410

511
jobs:
12+
check-latest:
13+
name: Check whether the tag is the latest release
14+
runs-on: ubuntu-latest
15+
outputs:
16+
is_latest: ${{ steps.check.outputs.is_latest }}
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v5
20+
with:
21+
fetch-depth: 0 # the guard compares against every v*.*.* tag
22+
- id: check
23+
uses: ./.github/actions/is-latest-release
24+
625
netlify-deploy:
26+
# A patch of an older line (e.g. v1.60.1 while master is at 1.73.0) must not
27+
# publish its documentation: the build takes docs/content/en from the checked-out
28+
# tag and deploys it with --prod, which would overwrite the current docs.
29+
needs: check-latest
30+
if: needs.check-latest.outputs.is_latest == 'true'
731
runs-on: ubuntu-latest
832
steps:
933
- name: Checkout

MAINTENANCE.md

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,94 @@
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+
Manually run the [Bump version & trigger release](.github/workflows/bump-version.yaml) workflow and pick the
5+
bump type. That is the whole release.
116

7+
The workflow bumps the version, creates the `rel/X.Y.Z` branch, merges it to `master`, and pushes the tag
8+
`vX.Y.Z`. That tag push triggers two workflows in parallel:
9+
10+
* [Build Python Package and Create Release](.github/workflows/build-release.yaml) — builds every component,
11+
creates the GitHub release, publishes to PyPI, and posts to `#releases`.
12+
* [Netlify Deploy](.github/workflows/netlify-deploy.yaml) — builds and publishes the documentation
13+
(takes ~15 minutes, so the packages reach PyPI well before the docs go live).
14+
15+
The styling of the documentation is taken from the `master` branch. For more details see
16+
[generate.sh](scripts/generate.sh).
17+
18+
### Recovering a stuck release
19+
Both downstream workflows key off the tag, so a release that stalled can be resumed by hand:
20+
21+
* if the tag was never pushed, check out the `Release X.Y.Z` commit on `master`, tag it `vX.Y.Z`, and push the
22+
tag to the gooddata/gooddata-python-sdk repository (e.g. `git push <remote> vX.Y.Z`)
23+
* if only the documentation failed, dispatch [Netlify Deploy](.github/workflows/netlify-deploy.yaml) manually;
24+
it does not need the tag
25+
26+
The tag has to be pushed with a personal access token. GitHub does not trigger workflows from pushes made with
27+
the default `GITHUB_TOKEN`, so a tag pushed by a workflow using it would silently start nothing.
28+
29+
## How to patch an already released version
30+
Use this whenever a release must contain a specific fix and *not* everything currently on `master` — whether
31+
that is an old line (1.60 while `master` is at 1.73) or the newest one.
32+
33+
Do **not** use the [Bump version & trigger release](.github/workflows/bump-version.yaml) workflow for this. Its
34+
last step is `git checkout master && git merge`, which would drag the old code and version numbers onto
35+
`master`. Its `patch` bump type means "release master as a patch", not "patch the released line".
36+
37+
Only the tagging is automated; the rest is manual by nature.
38+
39+
**Prerequisite:** the fix is already merged to `master`. The patch branch is never merged back, so this is what
40+
keeps the fix from being lost in the next release.
41+
42+
1. **Pick the base and the new version.** List what the line already has with
43+
`git branch -rl '<remote>/rel/1.60.*'`. The base is the newest of them — `rel/1.60.0`, or `rel/1.60.2` if the
44+
line was patched before. The new version increments the patch component: `1.60.1`.
45+
46+
2. **Create the release branch first**, so the fix has somewhere to be reviewed into:
47+
```bash
48+
git fetch <remote>
49+
git checkout -b rel/1.60.1 <remote>/rel/1.60.0
50+
git push <remote> rel/1.60.1
51+
```
52+
53+
3. **Cherry-pick the fix through a pull request:**
54+
```bash
55+
git checkout -b fix/backport-1.60 rel/1.60.1
56+
git cherry-pick <sha-on-master>
57+
git push <remote> fix/backport-1.60
58+
```
59+
Open the PR against `rel/1.60.1`. The [pre-merge pipeline](.github/workflows/pre-merge.yaml) runs because it
60+
triggers on `rel/**`. Merge once it is green.
61+
62+
4. **Bump the version on the release branch** — the same steps the bump workflow performs:
63+
```bash
64+
git checkout rel/1.60.1 && git pull
65+
uv sync --only-group release --locked
66+
uv run python ./scripts/bump_doc_dependencies.py 1.60.1
67+
make release-ci VERSION=1.60.1
68+
git commit -am "Release 1.60.1"
69+
git push <remote> rel/1.60.1
70+
```
71+
72+
5. **Tag it.** This is the only trigger; everything after it is automatic:
73+
```bash
74+
git tag v1.60.1
75+
git push <remote> v1.60.1
76+
```
77+
78+
The release is then built and published exactly like any other, with two differences handled automatically by
79+
[is-latest-release](.github/actions/is-latest-release/action.yaml): the GitHub release does not take the
80+
"Latest" badge from the newest version, and the documentation is not rebuilt — a docs build from an old tag
81+
would deploy that tag's content over the current site.
82+
83+
### What the documentation will show
84+
The docs site keeps the four newest release branches, sorted by `major.minor`, and a section is named after the
85+
`major.minor` only. Consequences worth knowing before someone goes looking:
86+
87+
* Patching a recent line replaces it: `rel/1.72.1` takes over the `1.72` section from `rel/1.72.0`.
88+
* Both branches still occupy a slot of the four, so one patch inside the window drops the site from four
89+
displayed versions to three.
90+
* Patching an old line (`rel/1.60.1` while `master` is at 1.73) falls outside the window entirely and never
91+
appears in the docs.
1292

1393
### How-to dev release
1494
To publish current master as a dev release version, use [Dev release from master](.github/workflows/dev-release.yaml) GitHub workflow.

docs/superpowers/specs/2026-08-20-release-automation-design.md

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Release automation: one dispatch to a published release
22

33
Date: 2026-08-20
4-
Status: Approved, pending implementation plan
4+
Status: Implemented
55

66
## Problem
77

@@ -135,16 +135,75 @@ tag-triggered run the checkout resolves to the tag, which is the release commit
135135
Applies to `patch` releases identically: the branch is named `patch/X.Y.Z`, but the tag
136136
is `vX.Y.Z` either way.
137137

138+
- Rename the release branch to `rel/X.Y.Z` for every bump type. The `patch/X.Y.Z`
139+
naming was the only reference to `patch/` in the repository, and it excluded patch
140+
releases from both the pre-merge pipeline and the docs build, which key off `rel/**`
141+
and `rel/*` respectively. The `Specify release branch` step disappears with it.
142+
143+
**`.github/actions/is-latest-release`** (new)
144+
145+
- Composite action outputting `is_latest`: whether `github.ref_name` is the highest
146+
`v*.*.*` tag, by `sort -V`. Returns `true` for non-tag refs so manual dispatches are
147+
unaffected. Requires the caller to check out with `fetch-depth: 0`.
148+
- A shared action rather than duplicated shell because its two consumers must agree
149+
exactly; if they drift, an old patch either steals the "Latest" badge or overwrites
150+
production documentation.
151+
138152
**`.github/workflows/netlify-deploy.yaml`**
139153

140154
- Add a `push` trigger on tags matching `v*.*.*`, alongside the existing
141-
`workflow_dispatch`. No other change; manual dispatch remains available.
155+
`workflow_dispatch`. Manual dispatch remains available.
156+
- Gate the deploy job on `is_latest == 'true'`. Without this, tagging a patch of an
157+
older line would check out that tag, build `docs/content/en` from its content, and
158+
`netlify deploy --prod` it over the current documentation. The guard is required by
159+
this change, not only by the patch runbook.
160+
161+
**`.github/workflows/build-release.yaml`**
162+
163+
- Set `make_latest` from `is_latest` instead of hardcoded `true`, so `v1.60.1` does not
164+
displace `v1.73.0` on the releases page.
142165

143166
**`MAINTENANCE.md`**
144167

145168
- Replace the three-step "How to release" with the single dispatch.
146169
- Keep the manual tag push and manual docs dispatch documented as the recovery path.
147170
- Leave the existing "Errors that may appear" section as is; it still applies.
171+
- Add "How to patch an already released version" (below).
172+
173+
**`.github/workflows/pre-merge.yaml`** — no change needed. Its existing `rel/**` filter
174+
covers patch branches once they are named `rel/`. Under the `patch/` naming it would
175+
have required one, and cherry-picked fixes would otherwise have merged untested.
176+
177+
## Patching an already released version
178+
179+
Applies whenever a release must contain a specific fix and not everything on `master`
180+
an old line, or the newest one. Only the tagging is automated; the rest is manual by
181+
nature and lives as a runbook in `MAINTENANCE.md`.
182+
183+
`bump-version.yaml` cannot serve this. Its final `git checkout master && git merge`
184+
would drag old code and version numbers onto `master`, and its `patch` bump type means
185+
"release master as a patch", not "patch the released line".
186+
187+
The flow: the fix must already be on `master` (the patch branch is never merged back,
188+
so nothing needs forward-porting). Base `rel/X.Y.Z` on the newest existing `rel/X.Y.*`
189+
for that line and push it empty; cherry-pick the fix onto a topic branch and PR it into
190+
`rel/X.Y.Z`, where pre-merge covers it; run the same bump commands the workflow runs;
191+
then tag and push. `build-release.yaml` then behaves as for any release, with the two
192+
guards above keeping the badge and the documentation intact.
193+
194+
### Docs-site behaviour, documented not fixed
195+
196+
`generate.sh` keeps the four newest release branches sorted by `major.minor` only, and
197+
names each section after the `major.minor`:
198+
199+
- Patching a recent line replaces its section: `rel/1.72.1` takes over `1.72`.
200+
- Both branches occupy a slot of the four, so one patch inside the window reduces the
201+
site from four displayed versions to three.
202+
- Patching an old line falls outside the window and never appears in the docs.
203+
204+
The draft `netlify-deploy-v2.yaml` already handles this correctly via
205+
`discover-versions.sh`, which dedupes to sections before applying the window, so this
206+
resolves itself when v2 lands. Not worth fixing in `generate.sh` in the meantime.
148207

149208
## Error handling
150209

0 commit comments

Comments
 (0)