release-on-upstream #12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)" |