Skip to content

release-on-upstream #12

release-on-upstream

release-on-upstream #12

name: release-on-upstream
# Cut this SDK's next release when busbar core ships. Refresh openapi.json from the new core
# release, regenerate the client, and IF the generated client changed, bump + tag v* — release.yml
# then publishes to PyPI via OIDC trusted publishing (no stored token). If regen is a no-op, no tag.
#
# RUNAWAY-SAFE: no plain `push:` trigger, so merging this file cannot cut a release.
# - repository_dispatch [upstream-release]: acts on the dispatched core tag (client_payload).
# - schedule: reads GetBusbar/busbar's latest release and acts ONLY if its spec version is newer
# than the committed openapi.json — an idle day is a no-op, so cron can never runaway-publish.
# - workflow_dispatch: always acts (a human explicitly asked).
# The final publish gate is a real git diff of the generated client: an identical regen never tags.
on:
repository_dispatch:
types: [upstream-release]
schedule:
- cron: "17 5 * * *" # daily; minute staggered across the fleet so crons don't all fire at once
workflow_dispatch:
inputs:
tag:
description: "busbar core tag to build against (e.g. v1.6.0). Blank = core's latest release."
required: false
type: string
permissions:
contents: write
concurrency:
group: release-on-upstream-${{ github.repository }}
cancel-in-progress: false
jobs:
cut:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
persist-credentials: true
token: ${{ secrets.RELEASE_DISPATCH_TOKEN }}
- name: Configure git identity
run: |
git config user.name "busbar-bot"
git config user.email "bot@getbusbar.com"
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Resolve target core release
id: resolve
env:
GH_TOKEN: ${{ secrets.RELEASE_DISPATCH_TOKEN }}
EVENT_NAME: ${{ github.event_name }}
DISPATCH_TAG: ${{ github.event.client_payload.tag }}
INPUT_TAG: ${{ github.event.inputs.tag }}
run: |
set -euo pipefail
committed="$(jq -r '.info.version' openapi.json)"
echo "committed spec version: $committed"
tag=""
case "$EVENT_NAME" in
repository_dispatch) tag="${DISPATCH_TAG:-}" ;;
workflow_dispatch) tag="${INPUT_TAG:-}" ;;
esac
if [ -z "$tag" ]; then
tag="$(gh api repos/GetBusbar/busbar/releases/latest --jq .tag_name 2>/dev/null || true)"
fi
if [ -z "$tag" ]; then
echo "::notice::could not resolve a core release tag -> nothing to do"
echo "proceed=no" >> "$GITHUB_OUTPUT"; exit 0
fi
ver="${tag#v}"
echo "target core tag: $tag (spec version $ver)"
proceed=no
if [ "$EVENT_NAME" = workflow_dispatch ]; then
proceed=yes # manual always acts
elif [ "$(printf '%s\n%s\n' "$committed" "$ver" | sort -V | tail -1)" = "$ver" ] && [ "$ver" != "$committed" ]; then
proceed=yes # core spec is strictly newer than what we ship
else
echo "::notice::core spec v$ver not newer than committed v$committed -> nothing to do"
fi
{
echo "proceed=$proceed"
echo "tag=$tag"
echo "ver=$ver"
} >> "$GITHUB_OUTPUT"
- name: Refresh spec, regenerate, and cut a release if the client changed
if: steps.resolve.outputs.proceed == 'yes'
env:
GH_TOKEN: ${{ secrets.RELEASE_DISPATCH_TOKEN }}
CORE_TAG: ${{ steps.resolve.outputs.tag }}
CORE_VER: ${{ steps.resolve.outputs.ver }}
run: |
set -euo pipefail
ops() { jq -r '[.paths[] | (.get,.put,.post,.delete,.patch,.options,.head) | .operationId?] | map(select(. != null)) | sort | @json' "$1"; }
ops_old="$(ops openapi.json)"
# Pull the version-stamped OpenAPI asset the core release publishes.
gh release download "$CORE_TAG" --repo GetBusbar/busbar \
--pattern "busbar-openapi-v${CORE_VER}.json" --output openapi.json.new
mv openapi.json.new openapi.json
ops_new="$(ops openapi.json)"
pip install -r requirements-dev.txt
make generate
if git diff --quiet -- busbar_admin; then
# Generated client is byte-identical. Commit the spec refresh (so cron settles and the
# spec-currency signal advances) but do NOT bump or tag: nothing to publish.
if git diff --quiet -- openapi.json; then
echo "::notice::spec + client identical -> idempotent no-op"
else
git add openapi.json
git commit -m "spec: refresh openapi.json to busbar ${CORE_TAG} (generated client unchanged)"
git push origin HEAD:main
echo "::notice::spec refreshed to ${CORE_TAG}; client unchanged, no release cut"
fi
exit 0
fi
# Public surface (the generated function names track operationIds) changed => minor, else patch.
latest="$(git tag -l 'v*' --sort=-v:refname | head -1)"
base="${latest#v}"; IFS=. read -r MA MI PA <<< "$base"
if [ "$ops_old" != "$ops_new" ]; then
next="v${MA}.$((MI + 1)).0"; echo "::notice::operationId surface changed -> minor bump"
else
next="v${MA}.${MI}.$((PA + 1))"; echo "::notice::client changed, surface stable -> patch bump"
fi
nver="${next#v}"
if git rev-parse -q --verify "refs/tags/${next}" >/dev/null; then
echo "::notice::${next} already exists -> idempotent no-op"; exit 0
fi
# Bump the SDK version (source of truth: the generator config's override) and regenerate so
# the emitted setup.py carries the new version, then commit spec + config + generated client.
sed -i -E "s/^(package_version_override:[[:space:]]*).*/\1\"${nver}\"/" openapi-python-client.yaml
make generate
git add openapi.json openapi-python-client.yaml busbar_admin setup.py
git commit -m "release: ${next} — regenerate against busbar ${CORE_TAG} spec"
git push origin HEAD:main
git tag "$next"
git push origin "refs/tags/${next}"
echo "::notice::pushed ${next} — release.yml will publish to PyPI (OIDC)"