-
Notifications
You must be signed in to change notification settings - Fork 0
338 lines (320 loc) · 17.7 KB
/
Copy pathrelease.yml
File metadata and controls
338 lines (320 loc) · 17.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# 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}"