.busbar-ref: record busbar 1.5.4 (4f42bb9dd5aad4e65d8524ba71a534cfecd… #10
Workflow file for this run
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
| # Release: on a version tag (e.g. v1.0.0), build the store-valkey plugin cdylib for all 5 | |
| # supported targets, pack + sign each into a loadable busbar plugin tarball, and publish them to | |
| # THIS repo's own GitHub Release. | |
| # | |
| # This mirrors busbarAI/.github/workflows/release.yml's `store-plugins` job — same 5-target | |
| # matrix, same busbar-plugin-pack pack/sign step (built from a sibling busbar checkout), same | |
| # BUSBAR_SIGN_KEY-or-`--allow-unsigned` fallback, same `gh release upload` + build-provenance | |
| # attestation — just scoped to this one plugin instead of building sqlite/postgres/valkey together | |
| # in one monorepo job. | |
| # | |
| # Supersedes the prior stopgap release.yml, which only built raw UNSIGNED cdylibs for 3 targets | |
| # and left packing/signing as a manual follow-up (busbar-plugin-pack wasn't available from this | |
| # repo at the time). Now that plugin-ci.yml/ci.yml already establish the sibling busbar checkout + | |
| # BUSBAR_REF convention, this workflow builds busbar-plugin-pack from that same sibling checkout | |
| # and does the real pack+sign+upload+attest in CI. | |
| # | |
| # This plugin's own version (the `v*` tag pushed here) is NOT tied to busbar's version number — | |
| # it is independently versioned starting at 0.5.0 (see README), separate from whichever busbar | |
| # branch it happens to build against via the .busbar-ref file at repo root. | |
| name: release | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| permissions: | |
| contents: write # create the Release + upload assets | |
| id-token: write # OIDC identity for keyless Sigstore signing (provenance) | |
| attestations: write # record the build-provenance attestation | |
| jobs: | |
| # Create the Release first so the parallel per-target upload jobs have something to attach to | |
| # (uploading from a matrix without a pre-existing release races -> "release not found"). | |
| create-release: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Create GitHub Release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set +e | |
| out="$(gh release create "${GITHUB_REF_NAME}" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --title "busbar-store-valkey ${GITHUB_REF_NAME}" \ | |
| --draft \ | |
| --verify-tag --generate-notes 2>&1)" | |
| status=$? | |
| echo "$out" | |
| if [ "$status" -eq 0 ]; then | |
| exit 0 | |
| fi | |
| # Only swallow the "release already exists" race between the create-release job and a | |
| # concurrent/retried run; any other failure (auth, bad tag, API error, etc.) must fail | |
| # the job loudly rather than be silently papered over by `gh release view`. | |
| if echo "$out" | grep -qi "already exists"; then | |
| echo "Release ${GITHUB_REF_NAME} already exists — treating as success (concurrent/retried run)." | |
| gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" | |
| else | |
| exit "$status" | |
| fi | |
| # THE PLATFORM LIST, IN EXACTLY ONE PLACE. This job reads .github/release-targets.json and emits | |
| # BOTH the build matrix `store-plugin` runs AND the exact set of asset filenames that matrix is | |
| # contractually obliged to produce. `store-plugin` consumes the first; `verify-assets` consumes the | |
| # second -- so "the set that was supposed to be built" and "the set that gets verified" are | |
| # literally the same computation and cannot drift apart. | |
| # | |
| # WHY IT IS A JOB AND NOT A LITERAL MATRIX. busbar v1.5.3 published FIVE assets where seven were | |
| # expected, and the guard of the day asserted `assets != 0`, which a five-asset release passes | |
| # comfortably. A count can never see a MISSING platform; only a name can. A hardcoded | |
| # expected-names list in the verifier would be a second place to forget a platform, which is the | |
| # same defect one level up. | |
| targets: | |
| name: release target matrix (single source of truth) | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.emit.outputs.matrix }} | |
| assets: ${{ steps.emit.outputs.assets }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Emit the target matrix and the asset names it must produce | |
| id: emit | |
| run: | | |
| set -euo pipefail | |
| python3 - <<'PY' >> "$GITHUB_OUTPUT" | |
| import json, os | |
| spec = json.load(open(".github/release-targets.json")) | |
| tag = os.environ["GITHUB_REF_NAME"] | |
| ver = tag[1:] if tag.startswith("v") else tag | |
| # EVERY per-target difference travels in the matrix as a PARAMETER, so there is no `if:` | |
| # and no second build path for a property to be established on one and unproven on the | |
| # other. | |
| fields = ("target", "os", "libext", "libprefix") | |
| inc = [{k: t[k] for k in fields} for t in spec["targets"]] | |
| assets = ["%s-%s-%s.tar.gz" % (spec["asset_prefix"], ver, t["target"]) | |
| for t in spec["targets"]] | |
| # A FLOOR, BECAUSE A LOOP OVER A DISCOVERED SET WITH NO FLOOR PASSES WHEN THE SET IS EMPTY. | |
| # Both the build matrix and the expectation list are enumerated from this output, so a | |
| # truncated or mis-parsed manifest would otherwise build nothing, expect nothing, and | |
| # report green all the way to a promoted release with no assets on it. | |
| if len(inc) < 5: | |
| raise SystemExit( | |
| "release-targets.json declares %d targets; this plugin ships 5. Refusing to " | |
| "run a build matrix and an expectation list over a set this small: an empty " | |
| "expectation list passes for a release that published nothing." % len(inc)) | |
| print("matrix=" + json.dumps({"include": inc})) | |
| print("assets=" + json.dumps(assets)) | |
| PY | |
| cat "$GITHUB_OUTPUT" | |
| # One signed .tar.gz per target: {cdylib + manifest.json}, packed by busbar-plugin-pack and | |
| # signed with the busbar release PRIVATE key (BUSBAR_SIGN_KEY secret) so it verifies as | |
| # first-party against the PUBLIC key embedded in busbar's own release binaries. If that secret | |
| # isn't provisioned on this repo, falls back to an UNSIGNED tarball (loadable only under | |
| # plugins.trust.allow_unsigned) rather than blocking the release — same seam busbarAI's own | |
| # release.yml documents (TODO(release-keys)). | |
| store-plugin: | |
| needs: [create-release, targets] | |
| name: store-plugin (${{ matrix.target }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJSON(needs.targets.outputs.matrix) }} | |
| steps: | |
| - name: Checkout store-valkey | |
| uses: actions/checkout@v7 | |
| with: | |
| path: store-valkey | |
| - name: Resolve busbar ref from .busbar-ref | |
| id: ref | |
| run: echo "sha=$(cut -d' ' -f1 store-valkey/.busbar-ref)" >> "$GITHUB_OUTPUT" | |
| shell: bash | |
| - name: Checkout busbar (sibling path dependency) | |
| uses: actions/checkout@v7 | |
| with: | |
| repository: GetBusbar/busbar | |
| ref: ${{ steps.ref.outputs.sha }} | |
| path: busbarAI | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Build the store-valkey plugin cdylib | |
| working-directory: store-valkey | |
| run: cargo build --release --target ${{ matrix.target }} | |
| shell: bash | |
| - name: Build busbar-plugin-pack (from the sibling busbar checkout) | |
| working-directory: busbarAI | |
| run: cargo build --release --target ${{ matrix.target }} -p busbar-plugin-pack | |
| shell: bash | |
| - name: Package + sign the plugin tarball | |
| env: | |
| BUSBAR_SIGN_KEY: ${{ secrets.BUSBAR_SIGN_KEY }} | |
| run: | | |
| set -euo pipefail | |
| ver="${GITHUB_REF_NAME#v}" | |
| target="${{ matrix.target }}" | |
| outdir="plugin-dist"; mkdir -p "$outdir" | |
| pack="busbarAI/target/${target}/release/busbar-plugin-pack" | |
| [ -x "$pack" ] || pack="busbarAI/target/${target}/release/busbar-plugin-pack.exe" | |
| unsigned_flag="" | |
| if [ -z "${BUSBAR_SIGN_KEY:-}" ]; then | |
| echo "::warning::BUSBAR_SIGN_KEY secret is not provisioned — packaging an UNSIGNED plugin tarball (loadable only under plugins.trust.allow_unsigned). See busbarAI's release.yml TODO(release-keys) seam." | |
| unsigned_flag="--allow-unsigned" | |
| fi | |
| lib="store-valkey/target/${target}/release/${{ matrix.libprefix }}busbar_store_valkey_plugin.${{ matrix.libext }}" | |
| "$pack" pack \ | |
| --lib "$lib" \ | |
| --name "busbar-store-valkey-plugin" --alias "valkey" --kind store \ | |
| --version "$ver" --publisher busbar \ | |
| --description "The Valkey store as a droppable busbar plugin — a cdylib exporting the store C ABI. Drop it in the plugins folder and set store.module: valkey. One Valkey behind a fleet of busbar nodes means shared virtual keys, budgets, usage, and audit across the cluster." \ | |
| --license Apache-2.0 \ | |
| --out "${outdir}/busbar-store-valkey-${ver}-${target}.tar.gz" \ | |
| $unsigned_flag | |
| ls -l "$outdir" | |
| shell: bash | |
| - name: Attach plugin tarball to Release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: gh release upload "${GITHUB_REF_NAME}" plugin-dist/*.tar.gz --repo "${GITHUB_REPOSITORY}" --clobber | |
| shell: bash | |
| # Keyless (Sigstore/OIDC) build-provenance attestation binding this tarball's digest to this | |
| # workflow run + commit — `gh attestation verify <tarball> --repo ${{ github.repository }}`. | |
| - name: Attest plugin build provenance | |
| uses: actions/attest-build-provenance@v4 | |
| with: | |
| subject-path: "plugin-dist/*.tar.gz" | |
| # PHANTOM- AND PARTIAL-RELEASE GUARD, AND THE ONLY THING THAT EVER PUBLISHES. It asserts the DRAFT | |
| # carries every asset the matrix owes it, BY NAME, and only then promotes it to published+latest. | |
| # Nothing above this job is user-facing: `create-release` makes a DRAFT, which does not resolve as | |
| # `releases/latest` and is invisible to `gh release download`, so a red verdict here stops the | |
| # release before a single user-facing name is minted instead of reporting damage already done. | |
| # | |
| # WHY BY NAME. The check this replaces asserted `assets != 0`. `store-plugin` runs `fail-fast: false`, so | |
| # a release that built ONE target out of 5 passed that check comfortably -- which is exactly | |
| # how busbar v1.5.3 shipped five assets where seven were expected and the two most common | |
| # platforms 404'd for every user who followed the documented download link. A count cannot see a | |
| # missing platform. The expected names come from the same `targets` job that produced the build | |
| # matrix, so the expectation cannot drift away from the thing being built. | |
| # | |
| # | |
| # THIS REPO ALREADY ASSERTED NAMES, FROM A HARDCODED LIST OF FIVE TRIPLES INSIDE THIS STEP, and | |
| # the reason survives the move: v1.0.4 shipped a COMPLETE 5-asset set that was correctly built, | |
| # correctly signed and completely unloadable, because every asset was packed under the retired | |
| # `redis` identity, so `store.module: valkey` resolved against nothing. The names now come from | |
| # .github/release-targets.json -- the same file the build matrix comes from -- so the published | |
| # stem is still asserted to be EXACTLY the one busbar names | |
| # (crates/busbar/src/config/mod.rs STORE_MODULE_VALKEY_ASSET_STEM), for every target built, and | |
| # there is no longer a second list to keep in step with the first. | |
| # | |
| # `!cancelled()` IS LOAD-BEARING, and it is the second half of that same defect: `store-plugin` runs | |
| # fail-fast:false, so a partial matrix FAILS the job, and a `needs:` on a failed job SKIPS its | |
| # dependent by default -- the one guard that exists to notice a broken release would be switched | |
| # off precisely when the release is broken. Running on `!cancelled()` turns a partial matrix into | |
| # a RED verify-assets that NAMES the missing platforms, instead of a grey one that names nothing. | |
| verify-assets: | |
| name: the draft owes every asset the manifest names | |
| needs: [targets, store-plugin] | |
| if: ${{ !cancelled() }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Assert the draft carries every asset, then promote it | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| EXPECTED: ${{ needs.targets.outputs.assets }} | |
| run: | | |
| set -euo pipefail | |
| # `!cancelled()` means this runs even when `targets` itself failed, and an empty | |
| # expectation list would then "verify" every release vacuously. Refuse instead. | |
| if [ -z "${EXPECTED:-}" ]; then | |
| echo "::error::The targets job produced no expected-asset list, so there is nothing to" \ | |
| "verify ${GITHUB_REF_NAME} against. Refusing to promote: it stays a draft." >&2 | |
| exit 1 | |
| fi | |
| gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" \ | |
| --json assets --jq '.assets[] | "\(.name)\t\(.size)"' > /tmp/got.tsv || : > /tmp/got.tsv | |
| echo "Draft ${GITHUB_REF_NAME} carries these assets:" | |
| cat /tmp/got.tsv | |
| python3 - <<'PY' | |
| import json, os, sys | |
| expected = json.loads(os.environ["EXPECTED"]) | |
| tag = os.environ["GITHUB_REF_NAME"] | |
| got = {} | |
| for line in open("/tmp/got.tsv"): | |
| line = line.rstrip("\n") | |
| if not line: | |
| continue | |
| name, _, size = line.partition("\t") | |
| got[name] = int(size or 0) | |
| missing = [a for a in expected if a not in got] | |
| # A NAME IN THE ASSET LIST IS NOT A USABLE ARTIFACT: GitHub creates the row as soon as the | |
| # upload starts, so a 0-byte or truncated upload lists identically to a good one. 1 KiB is | |
| # far below any real plugin tarball and far above an empty or header-only file. | |
| empty = [a for a in expected if a in got and got[a] < 1024] | |
| lines = ["### Draft asset verification", "", | |
| "| asset | bytes | verdict |", "| --- | --- | --- |"] | |
| for a in expected: | |
| if a not in got: | |
| lines.append("| `%s` | - | MISSING |" % a) | |
| elif got[a] < 1024: | |
| lines.append("| `%s` | %d | TOO SMALL |" % (a, got[a])) | |
| else: | |
| lines.append("| `%s` | %d | ok |" % (a, got[a])) | |
| extra = sorted(set(got) - set(expected)) | |
| if extra: | |
| lines += ["", "Also present (not required): " + ", ".join("`%s`" % e for e in extra)] | |
| summary = os.environ.get("GITHUB_STEP_SUMMARY") | |
| if summary: | |
| open(summary, "a").write("\n".join(lines) + "\n") | |
| print("\n".join(lines)) | |
| if not got: | |
| print("::error::PHANTOM RELEASE: the %s draft has 0 assets. Every build/pack target " | |
| "failed to upload a tarball. Nothing is public and nothing was promoted, so " | |
| "this is a clean retry: fix the build (check Cargo.lock freshness vs --locked " | |
| "and the plugin cdylib build step) and re-run this workflow." % tag, | |
| file=sys.stderr) | |
| sys.exit(1) | |
| if missing: | |
| print("::error::INCOMPLETE RELEASE: the %s draft is missing %d of %d required " | |
| "asset(s): %s. Each missing name is a PLATFORM whose users would get a 404 from " | |
| "the documented download URL, and busbar's plugin-registry-gate resolves the " | |
| "first-party plugin by exactly this name. Nothing was promoted, so fix that " | |
| "target's leg and re-run: no tag to delete, no release to unpublish." % | |
| (tag, len(missing), len(expected), ", ".join(missing)), file=sys.stderr) | |
| if empty: | |
| print("::error::TRUNCATED RELEASE: these %s draft assets are under 1 KiB, which means " | |
| "the upload was cut short and the asset is useless to anyone who downloads it: " | |
| "%s" % (tag, ", ".join(empty)), file=sys.stderr) | |
| if missing or empty: | |
| sys.exit(1) | |
| print("All %d required assets present and plausibly sized." % len(expected)) | |
| PY | |
| # Only now, with EVERY promised asset provably attached and plausibly sized, does this | |
| # stop being a draft and become the release that `releases/latest` resolves to. | |
| gh release edit "${GITHUB_REF_NAME}" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --draft=false --latest | |
| echo "::notice::Published ${GITHUB_REF_NAME} with every asset in the contract." | |
| # Instant marketing-site rebuild the moment this plugin ships a real release -- marketing's | |
| # deploy.yml listens for this exact repository_dispatch event type (plus its own daily-poll | |
| # fallback, so a missed/failed dispatch here is never a permanent gap). Same RELEASE_DISPATCH_TOKEN | |
| # pattern busbar core's own release.yml uses for its downstream fan-out -- see that file's | |
| # notify-downstream job for the template this mirrors. Fails loud (not a silent no-op) if the | |
| # secret isn't provisioned, so a missing secret can't masquerade as "nothing to notify." | |
| notify-marketing: | |
| needs: [store-plugin, verify-assets] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Dispatch upstream-release to the marketing site | |
| env: | |
| GH_TOKEN: ${{ secrets.RELEASE_DISPATCH_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${GH_TOKEN:-}" ]; then | |
| echo "::error::RELEASE_DISPATCH_TOKEN is not provisioned on this repo -- cannot notify" \ | |
| "marketing instantly. marketing's own daily poll will still pick this release up" \ | |
| "within 24h (self-healing fallback intact)." >&2 | |
| exit 1 | |
| fi | |
| gh api "repos/GetBusbar/marketing/dispatches" \ | |
| -f event_type=upstream-release \ | |
| -f "client_payload[repo]=${GITHUB_REPOSITORY}" \ | |
| -f "client_payload[tag]=${GITHUB_REF_NAME}" |