Skip to content

Commit 551e3ad

Browse files
committed
release: refuse to pack a cdylib against an ABI that cannot carry it
.busbar-ref pins the exact busbar commit a release builds and packs against, and nothing checked that the pinned commit's plugin-sdk dispatch actually routes the Store methods this repo implements. It doesn't have to: busbar_api::Store gives every method a default body, so an unrouted method compiles, packs, signs, attests and then silently takes the default at runtime -- reporting success while dropping the write. busbar 74a1f9fa ("a task written through a plugin store is no longer discarded") is that bug, already shipped once. The new abi-completeness job runs before create-release, so a failure leaves no tag-attached Release, no artifact, no signature and no attestation. It derives the method list from this repo's own `impl Store for` block rather than naming any one feature, so it stays correct as the trait grows and cannot go stale the way a hardcoded pin can. Against the currently pinned ref this job is RED, and correctly so: ten Store methods this repo implements (the six task methods and the four mcp-call methods) are not routed by the pinned plugin-sdk. .busbar-ref is deliberately left alone -- it is written by release-on-upstream from a real upstream release, and busbar has not cut 1.6.0 yet.
1 parent 9bcb71f commit 551e3ad

2 files changed

Lines changed: 112 additions & 1 deletion

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# ABI-COMPLETENESS GATE: refuse to build a plugin artifact whose source implements Store methods
5+
# that the PINNED busbar ABI cannot carry.
6+
#
7+
# WHY THIS EXISTS. `.busbar-ref` pins the exact busbar commit a release builds and packs against.
8+
# The plugin's `impl Store for ...` is compiled against `busbar-api` from that commit, but the wire
9+
# is `busbar-plugin-sdk`'s `dispatch` — the match arm that turns a decoded StoreRequest into a call
10+
# on the store. `busbar_api::Store` gives every method a DEFAULT body, so a method the pinned SDK's
11+
# dispatch never routes does not fail to compile: it compiles, ships, signs, attests, and then
12+
# silently takes the default at runtime. busbar 74a1f9fa ("a task written through a plugin store is
13+
# no longer discarded") is exactly that failure, already shipped once: a store that implemented
14+
# `put_task` perfectly had every task DISCARDED at the ABI while `put_task` reported success.
15+
#
16+
# That class of defect is invisible to every other gate in this repo. The test suite runs against a
17+
# busbar checkout too, but through the in-process trait, not the ABI; fmt/clippy/build cannot see it;
18+
# verify-assets only proves an asset exists. So this script checks the one thing nothing else does:
19+
#
20+
# for every method this repo implements in `impl Store for <T>`,
21+
# the pinned busbar's crates/plugin-sdk/src/lib.rs `dispatch` must actually call `store.<method>(`.
22+
#
23+
# A method implemented here but unrouted there is a SILENT DATA-LOSS PATH. It is a hard failure.
24+
#
25+
# This is deliberately generic — it is not a list of task methods. Any future Store method added to
26+
# a plugin ahead of the pinned engine trips it the same way, so the gate cannot go stale the way a
27+
# hardcoded pin can.
28+
#
29+
# Usage: verify-abi-completeness.sh <path-to-plugin-repo> <path-to-busbar-checkout>
30+
set -euo pipefail
31+
32+
plugin_root="${1:?usage: verify-abi-completeness.sh <plugin-repo> <busbar-checkout>}"
33+
busbar_root="${2:?usage: verify-abi-completeness.sh <plugin-repo> <busbar-checkout>}"
34+
35+
sdk="${busbar_root}/crates/plugin-sdk/src/lib.rs"
36+
[ -f "$sdk" ] || { echo "::error::not a busbar checkout: ${sdk} does not exist" >&2; exit 1; }
37+
38+
# The single `impl Store for <T>` block in this repo's store crate. Found, not hardcoded, so the
39+
# script is identical in all four store repos.
40+
impl_file="$(grep -rl --include='*.rs' '^impl Store for ' "$plugin_root" \
41+
| grep -v '/target/' | head -1 || true)"
42+
[ -n "$impl_file" ] || { echo "::error::no 'impl Store for' block found under ${plugin_root}" >&2; exit 1; }
43+
44+
# Slice the impl block: from `impl Store for` to the first column-0 `}`, then take the method names.
45+
methods="$(awk '/^impl Store for /{inblock=1} inblock{print} inblock&&/^\}/{exit}' "$impl_file" \
46+
| sed -n 's/^ fn \([a-z0-9_]*\)(.*/\1/p' | sort -u)"
47+
[ -n "$methods" ] || { echo "::error::parsed ZERO methods out of ${impl_file} — the parser is wrong, not the ABI" >&2; exit 1; }
48+
49+
echo "plugin store impl : ${impl_file}"
50+
echo "pinned busbar sdk : ${sdk}"
51+
echo "methods implemented here: $(echo "$methods" | wc -l | tr -d ' ')"
52+
echo
53+
54+
missing=""
55+
for m in $methods; do
56+
if grep -q "store\.${m}(" "$sdk"; then
57+
echo " ok ${m}"
58+
else
59+
echo " MISSING ${m}"
60+
missing="${missing} ${m}"
61+
fi
62+
done
63+
64+
if [ -n "$missing" ]; then
65+
echo
66+
echo "::error::ABI-COMPLETENESS FAILURE. The pinned busbar commit's plugin-sdk dispatch does not" \
67+
"route these Store methods that this plugin implements:${missing}." \
68+
"Packing a cdylib against this ABI would ship a plugin whose calls to those methods take" \
69+
"busbar_api::Store's DEFAULT bodies at runtime — succeeding silently while dropping the data." \
70+
"Do not release. Advance .busbar-ref to a busbar commit whose crates/plugin-sdk dispatch" \
71+
"carries these methods (and whose crates/api defines the row types they take)." >&2
72+
exit 1
73+
fi
74+
75+
echo
76+
echo "ABI-completeness OK: every Store method implemented here is routed by the pinned busbar's dispatch."

.github/workflows/release.yml

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,45 @@ jobs:
6464
service: valkey
6565
busbar_ref: ${{ needs.resolve-busbar-ref.outputs.sha }}
6666

67+
# ── ABI-COMPLETENESS GATE ────────────────────────────────────────────────────────────────────
68+
# THE GATE THAT REFUSES A CDYLIB THE PINNED ABI CANNOT CARRY. This runs BEFORE `create-release`,
69+
# so a failure here leaves no tag-attached Release, no artifact, no signature and no attestation.
70+
#
71+
# `gate` above proves this repo's suite passes against the pinned core. It cannot prove the pinned
72+
# core's plugin-sdk `dispatch` actually ROUTES the Store methods this repo implements — the suite
73+
# exercises the trait in-process, while a shipped plugin is reached over the C ABI. Because
74+
# `busbar_api::Store` gives every method a DEFAULT body, an unrouted method does not fail to
75+
# compile: it compiles, packs, signs, attests, and then silently takes the default at runtime,
76+
# reporting success while dropping the write. busbar 74a1f9fa ("a task written through a plugin
77+
# store is no longer discarded") is that bug, already shipped once.
78+
#
79+
# The check is generic — it derives the method list from this repo's own `impl Store for` block
80+
# rather than naming any one feature, so it stays correct as the Store trait grows and cannot go
81+
# stale the way a hardcoded pin can. See .github/scripts/verify-abi-completeness.sh.
82+
abi-completeness:
83+
needs: resolve-busbar-ref
84+
runs-on: ubuntu-latest
85+
steps:
86+
- name: Checkout store-valkey
87+
uses: actions/checkout@v7
88+
with:
89+
path: store-valkey
90+
91+
- name: Checkout busbar at the PINNED .busbar-ref sha (not a branch)
92+
uses: actions/checkout@v7
93+
with:
94+
repository: GetBusbar/busbar
95+
ref: ${{ needs.resolve-busbar-ref.outputs.sha }}
96+
path: busbarAI
97+
98+
- name: Every Store method implemented here must be routed by the pinned ABI
99+
run: bash store-valkey/.github/scripts/verify-abi-completeness.sh store-valkey busbarAI
100+
shell: bash
101+
67102
# Create the Release first so the parallel per-target upload jobs have something to attach to
68103
# (uploading from a matrix without a pre-existing release races -> "release not found").
69104
create-release:
70-
needs: gate
105+
needs: [gate, abi-completeness]
71106
runs-on: ubuntu-latest
72107
steps:
73108
- uses: actions/checkout@v7

0 commit comments

Comments
 (0)