-
Notifications
You must be signed in to change notification settings - Fork 70
ci: drive the whole release from a single tag push #1766
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| # (C) 2026 GoodData Corporation | ||
| name: Release tag checks | ||
| description: > | ||
| Answers the two questions the release workflows ask about the tag that triggered them. | ||
| They are not the same question, and they disagree exactly on the backport cases: | ||
|
|
||
| is_latest -- is this the highest version released so far? Decides the "Latest" badge | ||
| on the GitHub release. A patch of an older line (v1.60.1 while v1.73.0 | ||
| exists) must not take it. | ||
| is_on_master -- is this tag an ancestor of the default branch? Decides whether the | ||
| documentation is rebuilt. A patch is branched from a release branch and | ||
| never merged back, so its tree is behind master; deploying it with --prod | ||
| would revert any documentation merged since that release. | ||
|
|
||
| Requires the repository to be checked out with fetch-depth: 0, so that every tag and the | ||
| default branch are present. A shallower checkout fails the ancestry check loudly rather | ||
| than answering either question wrongly. On a non-tag ref (e.g. a manual workflow_dispatch) | ||
| both outputs are 'true'. | ||
|
|
||
| outputs: | ||
| is_latest: | ||
| description: "'true' when the triggering tag is the highest v*.*.* tag, otherwise 'false'" | ||
| value: ${{ steps.check.outputs.is_latest }} | ||
| is_on_master: | ||
| description: "'true' when the triggering tag is an ancestor of the default branch, otherwise 'false'" | ||
| value: ${{ steps.check.outputs.is_on_master }} | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
| - id: check | ||
| shell: bash | ||
| env: | ||
| TAG: ${{ github.ref_name }} | ||
| REF_TYPE: ${{ github.ref_type }} | ||
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| if [ "$REF_TYPE" != "tag" ]; then | ||
| echo "Ref '$TAG' is not a tag; nothing to guard against." | ||
| echo "is_latest=true" >> "$GITHUB_OUTPUT" | ||
| echo "is_on_master=true" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Only stable vX.Y.Z tags count. The trigger glob v*.*.* would also match something | ||
| # like v0.0.1-test, which sort -V could pick as the highest -- marking every real | ||
| # release from then on as not-latest. A non-stable TAG never equals a stable | ||
| # $highest, so it correctly comes out as not-latest without a separate check. | ||
| highest=$(git tag -l 'v*.*.*' | { grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' || true; } | sort -V | tail -n 1) | ||
| if [ -n "$highest" ] && [ "$TAG" = "$highest" ]; then is_latest=true; else is_latest=false; fi | ||
|
|
||
| if git merge-base --is-ancestor "$TAG" "origin/$DEFAULT_BRANCH"; then | ||
| is_on_master=true | ||
| else | ||
| is_on_master=false | ||
| fi | ||
|
|
||
| echo "tag=$TAG highest=$highest is_latest=$is_latest is_on_master=$is_on_master" | ||
| { | ||
| echo "is_latest=$is_latest" | ||
| echo "is_on_master=$is_on_master" | ||
| } >> "$GITHUB_OUTPUT" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,15 +14,21 @@ on: | |
| - minor | ||
| - patch | ||
|
|
||
| # One bump per branch at a time, so two dispatches cannot race to bump, merge and tag. | ||
| # Keyed by branch rather than globally, so a bump from a hotfix branch is not | ||
| # blocked by one from master. Never cancels: interrupting this | ||
| # between the master push and the tag push would leave a release half-made. | ||
| concurrency: | ||
| group: bump-${{ github.ref_name }} | ||
| cancel-in-progress: false | ||
|
Comment on lines
+21
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: for f in .github/workflows/bump-version.yaml .github/workflows/build-release.yaml .github/workflows/netlify-deploy.yaml; do
echo "=== $f ==="
cat -n "$f" | sed -n '1,45p'
doneRepository: gooddata/gooddata-python-sdk Length of output: 5563 🌐 Web query:
💡 Result: In GitHub Actions, the Citations:
🌐 Web query:
💡 Result: In the context of GitHub Actions, the Citations:
Queue pending release runs.
📍 Affects 3 files
🤖 Prompt for AI Agents |
||
|
|
||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
|
|
||
| jobs: | ||
| bump-version: | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| new_version: ${{ steps.bump.outputs.new_version }} | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v5 | ||
|
|
@@ -40,7 +46,7 @@ jobs: | |
| id: bump | ||
| run: | | ||
| NEW_VERSION=$(uv run python ./scripts/bump_version.py ${{ github.event.inputs.bump_type }}) | ||
| echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT | ||
| echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Bump version in documentation | ||
| run: | | ||
|
|
@@ -50,40 +56,28 @@ jobs: | |
| run: | | ||
| make release-ci VERSION=${{ steps.bump.outputs.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}} | ||
| env: | ||
| VERSION: ${{ steps.bump.outputs.new_version }} | ||
| run: | | ||
| git config user.name github-actions | ||
| git config user.email github-actions@github.com | ||
| git checkout -b ${{ steps.branch.outputs.release_branch }} | ||
|
|
||
| # Every release branch is rel/X.Y.Z, patches included. The docs build | ||
| # (scripts/generate.sh) and the pre-merge pipeline both key off rel/**. | ||
| git checkout -b "rel/$VERSION" | ||
| git add -A | ||
| git commit -m "Release ${{steps.bump.outputs.new_version}}" | ||
| git push origin ${{ steps.branch.outputs.release_branch }} | ||
| git commit -m "Release $VERSION" | ||
|
|
||
| # Order matters: the docs build enumerates remote rel/* branches, so | ||
| # rel/$VERSION has to be on the remote before the tag starts anything. | ||
| git push origin "rel/$VERSION" | ||
| git checkout master | ||
| git merge ${{ steps.branch.outputs.release_branch }} | ||
| git merge "rel/$VERSION" | ||
| git push origin master | ||
|
|
||
| # 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 }} | ||
| # The tag push is the single trigger for build-release and netlify-deploy. | ||
| # It works only because the checkout above uses TOKEN_GITHUB_YENKINS_ADMIN -- | ||
| # GitHub does not trigger workflows from pushes made with GITHUB_TOKEN. | ||
| git tag "v$VERSION" | ||
| git push origin "v$VERSION" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,112 @@ | ||
| # 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`) | ||
| Manually run the [Bump version & trigger release](.github/workflows/bump-version.yaml) workflow and pick the | ||
| bump type. That is the whole release. | ||
|
|
||
| The workflow bumps the version, creates the `rel/X.Y.Z` branch, merges it to `master`, and pushes the tag | ||
| `vX.Y.Z`. That tag push triggers two workflows in parallel: | ||
|
|
||
| * [Build Python Package and Create Release](.github/workflows/build-release.yaml) — builds every component, | ||
| creates the GitHub release, publishes to PyPI, and posts to `#releases`. | ||
| * [Netlify Deploy](.github/workflows/netlify-deploy.yaml) — builds and publishes the documentation | ||
| (takes ~15 minutes, so the packages reach PyPI well before the docs go live). | ||
|
|
||
| The styling of the documentation is taken from the `master` branch. For more details see | ||
| [generate.sh](scripts/generate.sh). | ||
|
|
||
| ### Recovering a stuck release | ||
| Both downstream workflows key off the tag, so a release that stalled can be resumed by hand: | ||
|
|
||
| * if the tag was never pushed, check out the `Release X.Y.Z` commit on `master`, tag it `vX.Y.Z`, and push the | ||
| tag to the gooddata/gooddata-python-sdk repository (e.g. `git push <remote> vX.Y.Z`) | ||
| * if only the documentation failed, dispatch [Netlify Deploy](.github/workflows/netlify-deploy.yaml) manually; | ||
| it does not need the tag | ||
|
|
||
| The tag has to be pushed with a personal access token. GitHub does not trigger workflows from pushes made with | ||
| the default `GITHUB_TOKEN`, so a tag pushed by a workflow using it would silently start nothing. | ||
|
|
||
| ## How to patch an already released version | ||
| Use this whenever a release must contain a specific fix and *not* everything currently on `master` — whether | ||
| that is an old line (1.60 while `master` is at 1.73) or the newest one. | ||
|
|
||
| Do **not** use the [Bump version & trigger release](.github/workflows/bump-version.yaml) workflow for this. Its | ||
| last step is `git checkout master && git merge`, which would drag the old code and version numbers onto | ||
| `master`. Its `patch` bump type means "release master as a patch", not "patch the released line". | ||
|
|
||
| Only the tagging is automated; the rest is manual by nature. | ||
|
|
||
| **Prerequisite:** the fix is already merged to `master`. The patch branch is never merged back, so this is what | ||
| keeps the fix from being lost in the next release. | ||
|
|
||
| 1. **Pick the base and the new version.** List what the line already has with | ||
| `git branch -rl '<remote>/rel/1.60.*'`. The base is the newest of them, and the new version increments the | ||
| patch component **of that base** — so `rel/1.60.0` gives `1.60.1`, but if the line was already patched to | ||
| `rel/1.60.2` the next one is `1.60.3`. The steps below use `1.60.1`; substitute your version throughout. | ||
|
|
||
| 2. **Create the release branch first**, so the fix has somewhere to be reviewed into: | ||
| ```bash | ||
| git fetch <remote> | ||
| # Branch from the base chosen in step 1, not blindly from X.Y.0 -- on an already-patched | ||
| # line that would be rel/1.60.2, and starting from 1.60.0 would drop the earlier fixes. | ||
| git checkout -b rel/1.60.1 <remote>/rel/1.60.0 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Branch repeated patches from the selected base. When the newest base is Proposed correction- git checkout -b rel/1.60.1 <remote>/rel/1.60.0
+ git checkout -b rel/1.60.3 <remote>/rel/1.60.2🤖 Prompt for AI Agents |
||
| git push <remote> rel/1.60.1 | ||
| ``` | ||
|
|
||
| 3. **Cherry-pick the fix through a pull request:** | ||
| ```bash | ||
| git checkout -b fix/backport-1.60 rel/1.60.1 | ||
| git cherry-pick <sha-on-master> | ||
| git push <remote> fix/backport-1.60 | ||
| ``` | ||
| Open the PR against `rel/1.60.1`. The [pre-merge pipeline](.github/workflows/pre-merge.yaml) runs because it | ||
| triggers on `rel/**`. Merge once it is green. | ||
|
|
||
| 4. **Bump the version on the release branch.** These commands mirror the *Install dependencies* through | ||
| *Bump version in codebase* steps of [bump-version.yaml](.github/workflows/bump-version.yaml) — if that | ||
| workflow gains or reorders a step, update this block with it: | ||
| ```bash | ||
| git checkout rel/1.60.1 && git pull | ||
| uv sync --only-group release --locked | ||
| uv run python ./scripts/bump_doc_dependencies.py 1.60.1 | ||
| make release-ci VERSION=1.60.1 | ||
| git add -A && git commit -m "Release 1.60.1" | ||
| git push <remote> rel/1.60.1 | ||
| ``` | ||
| `git add -A` rather than `commit -am`, matching the workflow, so a newly created file is not dropped. On an | ||
| older line `uv sync --locked` can fail if the lock file predates the current uv; re-lock if so. | ||
|
|
||
| 5. **Tag it.** This is the only trigger; everything after it is automatic: | ||
| ```bash | ||
| git tag v1.60.1 | ||
| git push <remote> v1.60.1 | ||
| ``` | ||
|
|
||
| The release is then built and published exactly like any other. Two things differ, and | ||
| [release-tag-checks](.github/actions/release-tag-checks/action.yaml) handles both: the GitHub release does not | ||
| take the "Latest" badge from the newest version, and the documentation is not rebuilt — the docs build checks | ||
|
Comment on lines
+86
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 8 'is_latest|is_on_master|make_latest' .github/workflows/build-release.yamlRepository: gooddata/gooddata-python-sdk Length of output: 2129 🏁 Script executed: set -euo pipefail
printf '%s\n' '--- release-tag-checks action ---'
cat -n .github/actions/release-tag-checks/action.yaml
printf '%s\n' '--- workflow consumers ---'
sed -n '68,118p' .github/workflows/build-release.yaml
printf '%s\n' '--- maintenance context ---'
sed -n '78,96p' MAINTENANCE.mdRepository: gooddata/gooddata-python-sdk Length of output: 6382 🏁 Script executed: set -euo pipefail
rg -n -C 8 'release-tag-checks|is_on_master|docs' .github/workflowsRepository: gooddata/gooddata-python-sdk Length of output: 26925 Limit the “Latest” statement to non-latest patches.
🧰 Tools🪛 LanguageTool[uncategorized] ~86-~86: The official name of this software platform is spelled with a capital “H”. (GITHUB) 🤖 Prompt for AI Agents |
||
| out the triggering tag, so publishing from one would put an outdated site live. | ||
|
|
||
| > **A tag runs the workflows as they exist *at that tag*, not on master.** Release lines branched before the | ||
| > release automation was added therefore run their own older copies, in which `make_latest` is hardcoded to | ||
| > `true`. Before tagging such a line, cherry-pick `.github/workflows/build-release.yaml` and | ||
| > `.github/actions/release-tag-checks/` onto `rel/X.Y.Z` — otherwise the patch takes the "Latest" badge, which | ||
| > also changes what `GET /releases/latest` returns. If you only notice afterwards, untick "Set as the latest | ||
| > release" on the GitHub release by hand. Those older copies have no tag trigger on the docs workflow, so the | ||
| > documentation is safe either way. | ||
|
|
||
| ### What the documentation will show | ||
| The docs site keeps the four newest release branches, sorted by `major.minor`, and a section is named after the | ||
| `major.minor` only. Consequences worth knowing before someone goes looking: | ||
|
|
||
| * A patch never publishes its own documentation — the deploy is gated on the tag being on master. `rel/1.72.1` | ||
| does take over the `1.72` section from `rel/1.72.0`, but only at the next deploy from master: the following | ||
| release, or a manual dispatch of [Netlify Deploy](.github/workflows/netlify-deploy.yaml) if you need it | ||
| sooner. | ||
| * Both branches still occupy a slot of the four, so one patch inside the window drops the site from four | ||
| displayed versions to three. | ||
| * Patching an old line (`rel/1.60.1` while `master` is at 1.73) falls outside the window entirely and never | ||
| appears in the docs. | ||
|
|
||
| ### How-to dev release | ||
| To publish current master as a dev release version, use [Dev release from master](.github/workflows/dev-release.yaml) GitHub workflow. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Reject non-stable tags before release workflows continue.
v*.*.*also matches tags such asv1.2.3-rc1. This code marks such a tag as not latest but exits successfully.github_releasecan then create a non-prerelease GitHub release, and a tag onmastercan deploy documentation to production.Fail this action for tag names that do not match the stable version format, or expose an
is_stableoutput and require it in both release jobs.Proposed fix
🤖 Prompt for AI Agents