From f13705e525eca7683625158b495d62b7b37973e2 Mon Sep 17 00:00:00 2001 From: Matthew Jackson Date: Sat, 8 Aug 2026 11:22:18 -0700 Subject: [PATCH] release: keep Cargo.lock in step with the pinned busbar ref, and never publish an assetless release The v1.0.4 release run failed on every target with: error: cannot update the lock file .../Cargo.lock because --locked was passed Cargo.toml's busbar-* dependencies are path deps into a sibling ../busbarAI checkout. release-on-upstream re-pins that checkout to a new commit in .busbar-ref but never regenerated Cargo.lock, so when busbar 1.5.3 pulled `valuable` in behind `tracing`, the committed lock described a graph that no longer resolved and release.yml's `--locked` build correctly refused to proceed. `--locked` is kept deliberately: it is what makes a release build exactly the graph that was resolved at pin time. Instead the lock is now refreshed in the same step that re-pins, against the exact commit being pinned, and committed alongside .busbar-ref so the two can never drift apart. Plain cargo (no --locked) is used for that refresh so the resolution is minimal, and the result is proven to satisfy --locked before anything is committed or tagged. Cargo.lock here is that refresh for the busbar 1.5.3 ref already recorded on main, which is otherwise unbuildable. Separately, create-release published the Release before the build matrix ran, so a total build failure left a tag whose releases/latest carried zero assets. verify-assets already detected that, but only after the empty release was public. The Release is now created as a draft, which releases/latest and releases/tags/ do not resolve, and verify-assets promotes it to published only once assets are provably attached. A release is now either complete or absent. Also brings two test fixtures up to busbar 1.5.3: RoutingRequest/Candidate gained request_id and signals, and inline module entries under auth.admin_auth were retired in favour of a named identity-providers definition. --- .github/workflows/release-on-upstream.yml | 38 +++++++++++++++++-- .github/workflows/release.yml | 46 +++++++++++++++-------- Cargo.lock | 14 ++++++- tests/e2e.rs | 7 ++++ tests/full_stack_e2e.rs | 9 ++++- 5 files changed, 91 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release-on-upstream.yml b/.github/workflows/release-on-upstream.yml index 041e48e..789497d 100644 --- a/.github/workflows/release-on-upstream.yml +++ b/.github/workflows/release-on-upstream.yml @@ -156,6 +156,36 @@ jobs: *) echo "::error::dry-run: ${TAG} is not a valid semver tag"; exit 1 ;; esac + # Re-pinning .busbar-ref moves the SIBLING ../busbarAI checkout that every busbar-* entry in + # Cargo.toml is a path dependency on, and that move can change the resolved dependency graph + # (busbar 1.5.2 -> 1.5.3, for instance, pulled `valuable` in behind `tracing`). Cargo.lock is + # committed in this repo and release.yml builds with `--locked`, so a lock left describing the + # OLD graph makes `cargo build --locked` refuse to update it and every release target fails. + # + # So the lock is refreshed HERE, against the exact commit being pinned, and committed in the + # SAME commit as .busbar-ref. `--locked` in release.yml is deliberately kept: it is what + # guarantees a release builds exactly the graph resolved and reviewed at pin time, and dropping + # it would turn this loud failure into a silent drift. + - name: Refresh Cargo.lock against the busbar ref being pinned + if: steps.guard.outputs.exists == 'no' && steps.resolve.outputs.in_sha != '' && github.event.inputs.dry_run != 'true' + env: + IN_SHA: ${{ steps.resolve.outputs.in_sha }} + run: | + set -euo pipefail + # Cargo.toml's path deps point at ../busbarAI, a SIBLING of this checkout. actions/checkout + # cannot write outside the workspace, and GetBusbar/busbar is public, so clone it directly. + sib="$(cd .. && pwd)/busbarAI" + rm -rf "$sib" + git clone --quiet --no-checkout https://github.com/GetBusbar/busbar.git "$sib" + git -C "$sib" checkout --quiet "${IN_SHA}" + # Plain cargo (NO --locked) performs the MINIMAL resolution: every existing registry pin is + # preserved and only what the new busbar graph actually requires is added or moved. + cargo metadata --format-version 1 >/dev/null + # Prove the refreshed lock satisfies the exact flag release.yml will build under, BEFORE we + # commit and tag. If this fails, no tag is pushed and no phantom release can be created. + cargo metadata --format-version 1 --locked >/dev/null + echo "::notice::Cargo.lock is fresh against busbar ${IN_SHA} and satisfies --locked" + - name: Record the new busbar ref in .busbar-ref if: steps.guard.outputs.exists == 'no' && steps.resolve.outputs.in_sha != '' && github.event.inputs.dry_run != 'true' env: @@ -165,10 +195,12 @@ jobs: run: | set -euo pipefail printf '%s %s\n' "${IN_SHA}" "${IN_VER}" > .busbar-ref - if git diff --quiet .busbar-ref; then - echo "::notice::.busbar-ref already at ${IN_SHA} ${IN_VER}, no commit needed" + # Cargo.lock rides along with the ref it was resolved against: the two must never be + # committed apart, or release.yml's --locked build sees a lock for a different busbar. + if git diff --quiet -- .busbar-ref Cargo.lock; then + echo "::notice::.busbar-ref already at ${IN_SHA} ${IN_VER} and Cargo.lock already fresh, no commit needed" else - git add .busbar-ref + git add .busbar-ref Cargo.lock git commit -m ".busbar-ref: record busbar ${IN_VER} (${IN_SHA}) for the ${TAG} release" git push origin HEAD:main fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fea3ffd..df0fb0b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,18 +31,25 @@ permissions: 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"). + # + # It is created as a DRAFT. A draft is addressable by tag to `gh release upload` / `gh release + # view` with this token, but it is NOT returned by the public `releases/latest` or + # `releases/tags/` endpoints -- so while the build matrix is still running, and for good if it + # fails, nothing following `releases/latest` can observe an assetless release. `verify-assets` + # publishes it only once assets are actually attached, which is what makes a release here either + # complete or absent, never an empty shell. create-release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - name: Create GitHub Release + - name: Create GitHub Release (draft) env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release create "${GITHUB_REF_NAME}" \ --repo "${GITHUB_REPOSITORY}" \ --title "busbar-webrequest ${GITHUB_REF_NAME}" \ - --verify-tag --generate-notes \ + --draft --verify-tag --generate-notes \ || gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" # One signed .tar.gz per target: {cdylib + manifest.json}, packed by busbar-plugin-pack and @@ -153,19 +160,20 @@ jobs: with: subject-path: "plugin-dist/*.tar.gz" - # PHANTOM-RELEASE GUARD: assert the published Release actually carries assets before we treat this - # as a real release. The per-target build/upload jobs run with fail-fast:false, and `create-release` - # always makes the (initially empty) Release up front — so a build/pack failure on EVERY target - # (e.g. a stale Cargo.lock tripping `--locked`, as happened on the first v1.0.3 cut) leaves a tag + - # Release with ZERO assets: a "phantom" that silently breaks busbar's plugin-registry-gate. This job - # fails the whole release run loud if assets == 0, so a phantom can never ship (or notify marketing) - # unnoticed again. It depends on the build matrix but does NOT inherit its fail-fast:false — one - # green target is enough to have assets, but zero across the board must hard-fail here. + # PHANTOM-RELEASE GATE: this is the only step that turns the draft into a published release, and it + # does so only after proving assets are attached. The per-target build/upload jobs run with + # fail-fast:false and `create-release` makes the (initially empty) Release up front, so a build/pack + # failure on EVERY target (e.g. a stale Cargo.lock tripping `--locked`) leaves a Release with ZERO + # assets. Previously that Release was published immediately and such a failure shipped a "phantom": + # a tag whose `releases/latest` carried nothing, silently breaking busbar's plugin-registry-gate. + # Now the empty Release is a draft nothing can resolve, and this job either publishes it or fails + # the run with the draft left unpublished. It depends on the build matrix but does NOT inherit its + # fail-fast:false: one green target is enough to have assets, zero across the board must hard-fail. verify-assets: needs: [webrequest-plugin] runs-on: ubuntu-latest steps: - - name: Assert the Release has at least one asset + - name: Assert the Release has at least one asset, then publish it env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -175,13 +183,19 @@ jobs: --json assets --jq '.assets | length')" echo "Release ${GITHUB_REF_NAME} has ${count} asset(s)." if [ "${count}" -eq 0 ]; then - echo "::error::PHANTOM RELEASE: ${GITHUB_REF_NAME} was published with 0 assets." \ - "Every build/pack target failed to upload a tarball. Failing the release run so this" \ - "tag is not mistaken for a real release by busbar's plugin-registry-gate. Fix the" \ - "build (check Cargo.lock freshness vs --locked and the plugin cdylib build step)," \ - "delete this tag+release, and re-cut." >&2 + echo "::error::PHANTOM RELEASE PREVENTED: ${GITHUB_REF_NAME} has 0 assets." \ + "Every build/pack target failed to upload a tarball. The release is still a DRAFT," \ + "so it was never visible to anything following releases/latest and there is no" \ + "phantom to clean up -- just fix the build (check Cargo.lock freshness vs --locked" \ + "and the plugin cdylib build step) and re-run. Failing the release run." >&2 exit 1 fi + # Only now, with assets provably attached, 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 ${count} asset(s)." # 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 diff --git a/Cargo.lock b/Cargo.lock index c83c7e0..a41cf51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.11.0", + "smallvec", "zeroize", ] @@ -166,6 +167,8 @@ dependencies = [ "busbar-api", "busbar-plugin-abi", "serde_json", + "tracing", + "tracing-core", ] [[package]] @@ -772,9 +775,9 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libloading" -version = "0.8.9" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" dependencies = [ "cfg-if", "windows-link", @@ -1497,6 +1500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", ] [[package]] @@ -1541,6 +1545,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" diff --git a/tests/e2e.rs b/tests/e2e.rs index ef695d7..3e71e57 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -267,6 +267,11 @@ fn cfg(url: &str) -> String { fn req_with_prompt(text: &str) -> RoutingRequest<'static> { RoutingRequest { + // busbar 1.5.3 added the correlation id and the declared-signal bag to the hook projection. + // These fixtures pin a fixed id and an EMPTY bag: no test here declares a signal, and an + // empty bag serialises to nothing, so the forwarded envelope is unchanged by their presence. + request_id: 1, + signals: busbar_api::SignalBag::new(), pool: "p", ingress_protocol: "anthropic", requested_model: None, @@ -299,6 +304,8 @@ fn cand(idx: usize) -> Candidate<'static> { available_concurrency: 1, budget_remaining: None, rate_headroom: None, + // Same contract as RoutingRequest::signals above: empty unless a consumer declares one. + signals: busbar_api::SignalBag::new(), } } diff --git a/tests/full_stack_e2e.rs b/tests/full_stack_e2e.rs index 9510cc8..89b9b00 100644 --- a/tests/full_stack_e2e.rs +++ b/tests/full_stack_e2e.rs @@ -327,15 +327,20 @@ mockup: // `auth.chain: []` (open relay) on purpose: this test's whole point is the admin-install -> // plugin-load -> hook-invocation -> webhook round trip, not the client auth chain (covered // elsewhere in busbar's own suite) — narrowing scope here keeps the failure surface honest. + // + // `admin-tokens` is DEFINED once under `identity-providers:` and REFERENCED by bare name from + // `auth.admin_auth`. busbar 1.5.3 retired the inline-module form this used to use, and refuses + // to boot a config still carrying it, so the inline shape would fail this test at startup. let config_yaml = format!( r#" listen: "127.0.0.1:{data_port}" admin_listen: "127.0.0.1:{admin_port}" +identity-providers: + admin-tokens: {{ module: admin-tokens, token: {{ env: BUSBAR_E2E_ADMIN_TOKEN }} }} auth: chain: [] signing_key: {{ env: BUSBAR_SIGNING_KEY }} - admin_auth: - - admin-tokens: {{ token: {{ env: BUSBAR_E2E_ADMIN_TOKEN }} }} + admin_auth: [admin-tokens] plugins: enabled: true dir: "{plugins_dir}"