release-on-upstream #11
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 CLI's next release when busbar core ships. The admin client (src/client.rs) is hand-rolled | |
| # against the committed openapi.json, and ci.yml's spec-drift job fails when that spec falls behind | |
| # core's latest release. So on a new core release this refreshes openapi.json, proves the CLI still | |
| # builds against it, then patch-bumps + tags v* — release.yml then builds + uploads the binaries. | |
| # | |
| # There is no code generator here: a refreshed spec never rewrites client.rs, so this only ever | |
| # patch-bumps (semantic client changes are a human's job, surfaced by the failing spec-drift check). | |
| # | |
| # 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-tag. | |
| # - workflow_dispatch: always acts (a human explicitly asked). | |
| # The final gate is a green `cargo build --locked` on the refreshed spec: a broken build never tags. | |
| on: | |
| repository_dispatch: | |
| types: [upstream-release] | |
| schedule: | |
| - cron: "53 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: dtolnay/rust-toolchain@stable | |
| - 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 | |
| elif [ "$(printf '%s\n%s\n' "$committed" "$ver" | sort -V | tail -1)" = "$ver" ] && [ "$ver" != "$committed" ]; then | |
| proceed=yes | |
| 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, prove it builds, and cut a patch release | |
| 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 | |
| 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 | |
| if git diff --quiet -- openapi.json; then | |
| echo "::notice::spec already current -> idempotent no-op"; exit 0 | |
| fi | |
| # Green gate: the hand-rolled client must still build against the refreshed spec. | |
| cargo build --locked --verbose | |
| next="$(git tag -l 'v*' --sort=-v:refname | head -1)" | |
| base="${next#v}"; IFS=. read -r MA MI PA <<< "$base" | |
| next="v${MA}.${MI}.$((PA + 1))" | |
| if git rev-parse -q --verify "refs/tags/${next}" >/dev/null; then | |
| echo "::notice::${next} already exists -> idempotent no-op"; exit 0 | |
| fi | |
| nver="${next#v}" | |
| # Bump the package version; a plain build re-syncs Cargo.lock so release.yml's --locked holds. | |
| sed -i -E "s/^version = \"[^\"]*\"/version = \"${nver}\"/" Cargo.toml | |
| cargo build | |
| git add openapi.json Cargo.toml Cargo.lock | |
| git commit -m "release: ${next} — refresh openapi.json to busbar ${CORE_TAG}" | |
| git push origin HEAD:main | |
| git tag "$next" | |
| git push origin "refs/tags/${next}" | |
| echo "::notice::pushed ${next} — release.yml will build + upload the binaries" |