Skip to content

.busbar-ref: record busbar 1.5.4 (4f42bb9dd5aad4e65d8524ba71a534cfecd… #10

.busbar-ref: record busbar 1.5.4 (4f42bb9dd5aad4e65d8524ba71a534cfecd…

.busbar-ref: record busbar 1.5.4 (4f42bb9dd5aad4e65d8524ba71a534cfecd… #10

Workflow file for this run

# 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}"