Skip to content

Commit ca12701

Browse files
committed
feat(ci): include Linux CLI and Relay artifacts in beta releases
1 parent 26e02fe commit ca12701

5 files changed

Lines changed: 217 additions & 66 deletions

File tree

‎.github/workflows/desktop-package.yml‎

Lines changed: 34 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,7 @@ jobs:
336336
name: Linux CLI and Relay Server
337337
needs: prepare
338338
if: >-
339-
needs.prepare.outputs.relay_image_only != 'true' &&
340-
needs.prepare.outputs.release_channel == 'stable'
339+
needs.prepare.outputs.relay_image_only != 'true'
341340
uses: ./.github/workflows/linux-binaries.yml
342341
secrets:
343342
release_signing_key: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
@@ -355,19 +354,19 @@ jobs:
355354
needs: [prepare, linux-binaries]
356355
if: >-
357356
always() &&
358-
((needs.prepare.outputs.upload_to_release == 'true' &&
359-
needs.prepare.outputs.release_channel == 'stable') ||
357+
(needs.prepare.outputs.upload_to_release == 'true' ||
360358
needs.prepare.outputs.relay_image_only == 'true') &&
361359
(needs.prepare.outputs.relay_image_only == 'true' ||
362360
needs.linux-binaries.result == 'success')
363361
runs-on: ubuntu-latest
364362
permissions:
365363
contents: write
366364
packages: write
367-
env:
368-
IMAGE: ghcr.io/gcwing/openbitfun-relay-server
369-
370365
steps:
366+
- name: Resolve image repository
367+
shell: bash
368+
run: echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/openbitfun-relay-server" >> "$GITHUB_ENV"
369+
371370
- name: Checkout
372371
uses: actions/checkout@v5
373372
with:
@@ -391,7 +390,7 @@ jobs:
391390
set -euo pipefail
392391
mkdir -p linux-release-assets
393392
gh release download "${RELEASE_TAG}" \
394-
--repo GCWing/OpenBitFun \
393+
--repo "${GITHUB_REPOSITORY}" \
395394
--dir linux-release-assets \
396395
--pattern 'openbitfun-relay-server-*.tar.gz' \
397396
--pattern 'openbitfun-relay-server-*.tar.gz.sha256'
@@ -436,10 +435,10 @@ jobs:
436435
echo 'value<<EOF'
437436
echo "${IMAGE}:${RELEASE_TAG}"
438437
echo "${IMAGE}:${asset_version}"
439-
if [[ "${IMAGE_ONLY}" == "true" ]]; then
438+
if [[ "${IMAGE_ONLY}" == "true" && "${RELEASE_CHANNEL}" == "stable" ]]; then
440439
# Backfilling an older release must not roll the floating tag
441440
# backwards. GitHub's latest endpoint excludes prereleases.
442-
latest_release="$(gh api repos/GCWing/OpenBitFun/releases/latest --jq .tag_name)"
441+
latest_release="$(gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq .tag_name)"
443442
if [[ "${RELEASE_TAG}" == "${latest_release}" ]]; then
444443
echo "${IMAGE}:latest"
445444
fi
@@ -536,6 +535,8 @@ jobs:
536535
uses: softprops/action-gh-release@v3
537536
with:
538537
tag_name: ${{ needs.prepare.outputs.release_tag }}
538+
prerelease: ${{ needs.prepare.outputs.release_channel == 'beta' }}
539+
make_latest: "false"
539540
files: |
540541
relay-image.json
541542
relay-image.json.sig
@@ -549,9 +550,8 @@ jobs:
549550
always() &&
550551
needs.prepare.outputs.upload_to_release == 'true' &&
551552
needs.package.result == 'success' &&
552-
(needs.prepare.outputs.release_channel == 'beta' ||
553-
(needs.linux-binaries.result == 'success' &&
554-
needs.publish-relay-image.result == 'success'))
553+
needs.linux-binaries.result == 'success' &&
554+
needs.publish-relay-image.result == 'success'
555555
runs-on: ubuntu-latest
556556
env:
557557
REQUIRED_UPDATER_PLATFORMS: windows-x86_64,darwin-x86_64,darwin-aarch64,linux-x86_64,linux-aarch64
@@ -568,32 +568,26 @@ jobs:
568568
merge-multiple: true
569569

570570
- name: Download Linux binary artifacts
571-
if: needs.prepare.outputs.release_channel == 'stable'
572571
uses: actions/download-artifact@v7
573572
with:
574573
pattern: openbitfun-linux-${{ needs.prepare.outputs.release_tag }}-*
575574
path: linux-release-assets
576575
merge-multiple: true
577576

578577
- name: Download Relay image descriptor
579-
if: needs.prepare.outputs.release_channel == 'stable'
580578
uses: actions/download-artifact@v7
581579
with:
582580
name: openbitfun-relay-image-${{ needs.prepare.outputs.release_tag }}
583581
path: relay-image-assets
584582

585583
- name: List release assets
586-
env:
587-
RELEASE_CHANNEL: ${{ needs.prepare.outputs.release_channel }}
588584
run: |
589585
echo "Release assets:"
590586
find release-assets -type f | sort
591-
if [[ "${RELEASE_CHANNEL}" == "stable" ]]; then
592-
echo "Linux CLI and Relay Server assets:"
593-
find linux-release-assets -type f | sort
594-
echo "Relay image descriptor:"
595-
find relay-image-assets -type f | sort
596-
fi
587+
echo "Linux CLI and Relay Server assets:"
588+
find linux-release-assets -type f | sort
589+
echo "Relay image descriptor:"
590+
find relay-image-assets -type f | sort
597591
598592
- name: Prepare versioned Windows installer
599593
run: |
@@ -638,13 +632,12 @@ jobs:
638632
--required-manual-platforms "windows-x86_64"
639633
640634
- name: Generate Linux binaries manifest
641-
if: needs.prepare.outputs.release_channel == 'stable'
642635
run: |
643636
node scripts/generate-linux-binaries-manifest.mjs \
644637
--assets-dir linux-release-assets \
645638
--version "${{ needs.prepare.outputs.version }}" \
646639
--tag "${{ needs.prepare.outputs.release_tag }}" \
647-
--repo "GCWing/OpenBitFun" \
640+
--repo "${{ github.repository }}" \
648641
--out linux-release-assets/linux-binaries.json
649642
650643
# The Tauri bundler signs the five updater artifacts during `tauri build`,
@@ -677,8 +670,7 @@ jobs:
677670
node scripts/write-minisign-public-key.mjs \
678671
--out release-assets/minisign.pub
679672
680-
- name: Stage stable release assets
681-
if: needs.prepare.outputs.release_channel == 'stable'
673+
- name: Stage release assets
682674
shell: bash
683675
run: |
684676
set -euo pipefail
@@ -707,27 +699,6 @@ jobs:
707699
relay-image-assets/relay-image.json \
708700
relay-image-assets/relay-image.json.sig
709701
710-
- name: Stage beta release assets
711-
if: needs.prepare.outputs.release_channel == 'beta'
712-
shell: bash
713-
run: |
714-
set -euo pipefail
715-
shopt -s globstar
716-
node scripts/stage-github-release-assets.mjs \
717-
--out-dir release-upload-assets \
718-
release-updater-assets/* \
719-
release-manual-assets/*.exe \
720-
release-manual-assets/*.exe.sig \
721-
release-assets/**/*.AppImage \
722-
release-assets/**/*.AppImage.sig \
723-
release-assets/**/*.deb \
724-
release-assets/**/*.deb.sig \
725-
release-assets/**/*.dmg \
726-
release-assets/**/*.dmg.sig \
727-
release-assets/**/*.rpm \
728-
release-assets/**/*.rpm.sig \
729-
release-assets/minisign.pub
730-
731702
- name: Upload to release
732703
uses: softprops/action-gh-release@v3
733704
with:
@@ -752,28 +723,33 @@ jobs:
752723
--check-urls true
753724
754725
- name: Verify published Linux binaries manifest
755-
if: needs.prepare.outputs.release_channel == 'stable'
756726
run: |
757727
curl -fsSL --retry 5 --retry-delay 3 \
758-
"https://github.com/GCWing/OpenBitFun/releases/download/${{ needs.prepare.outputs.release_tag }}/linux-binaries.json" \
728+
"https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/linux-binaries.json" \
759729
-o linux-binaries.published.json
760730
test "$(jq -r '.version' linux-binaries.published.json)" = "${{ needs.prepare.outputs.version }}"
761-
while IFS= read -r cli_url; do
762-
curl -fsSL --retry 5 --retry-delay 3 "${cli_url}.sig" -o /dev/null
763-
curl -fsSL --retry 5 --retry-delay 3 "${cli_url}.sha256.sig" -o /dev/null
764-
done < <(jq -r '.platforms[].cli.url' linux-binaries.published.json)
731+
jq -e '.platforms | has("linux-x86_64") and has("linux-aarch64")' linux-binaries.published.json >/dev/null
732+
while IFS= read -r archive_url; do
733+
test -n "${archive_url}"
734+
curl -fsSLI --retry 5 --retry-delay 3 "${archive_url}" -o /dev/null
735+
curl -fsSL --retry 5 --retry-delay 3 "${archive_url}.sha256" -o /dev/null
736+
curl -fsSL --retry 5 --retry-delay 3 "${archive_url}.sig" -o /dev/null
737+
curl -fsSL --retry 5 --retry-delay 3 "${archive_url}.sha256.sig" -o /dev/null
738+
done < <(jq -r '.platforms[] | .cli.url, .relay.url' linux-binaries.published.json)
765739
766740
- name: Verify published Relay image descriptor
767-
if: needs.prepare.outputs.release_channel == 'stable'
741+
shell: bash
768742
run: |
769743
curl -fsSL --retry 5 --retry-delay 3 \
770-
"https://github.com/GCWing/OpenBitFun/releases/download/${{ needs.prepare.outputs.release_tag }}/relay-image.json" \
744+
"https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/relay-image.json" \
771745
-o relay-image.published.json
772746
test "$(jq -r '.tag' relay-image.published.json)" = "${{ needs.prepare.outputs.release_tag }}"
773-
test "$(jq -r '.image' relay-image.published.json)" = "ghcr.io/gcwing/openbitfun-relay-server"
747+
test "$(jq -r '.version' relay-image.published.json)" = "${{ needs.prepare.outputs.version }}"
748+
test "$(jq -r '.image' relay-image.published.json)" = "ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/openbitfun-relay-server"
749+
jq -e '.platforms | sort == ["linux/amd64", "linux/arm64"]' relay-image.published.json >/dev/null
774750
jq -e '.digest | test("^sha256:[0-9a-f]{64}$")' relay-image.published.json >/dev/null
775751
curl -fsSL --retry 5 --retry-delay 3 \
776-
"https://github.com/GCWing/OpenBitFun/releases/download/${{ needs.prepare.outputs.release_tag }}/relay-image.json.sig" \
752+
"https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/relay-image.json.sig" \
777753
-o /dev/null
778754
779755
- name: Resolve beta channel promotion

‎docs/development/releasing.md‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,29 @@ Public beta assets are stored on the immutable version tag. After every asset
2828
and signature is verified, the workflow updates only the `latest.json` asset on
2929
the `channel-beta` pre-release. Beta Desktop builds read that pointer and fall
3030
back to `https://openbitfun.com/release/beta/latest.json`.
31-
The beta release contains Desktop and Installer assets only. CLI and Relay
32-
floating releases remain stable-only.
31+
Beta releases include Desktop and Installer assets, Linux CLI and Relay Server
32+
archives for x86_64 and aarch64, and a multi-platform Relay image for linux/amd64
33+
and linux/arm64. Release publication requires every producer to succeed. Archives
34+
include checksums and signatures; `linux-binaries.json` and the signed
35+
`relay-image.json` descriptor live on the immutable version release.
36+
37+
Relay images use `ghcr.io/<repository-owner>/openbitfun-relay-server` with the
38+
version tags `v1.0.0-beta.N` and `1.0.0-beta.N`. Beta never updates the `latest`
39+
image tag. Fork builds publish to the fork owner's image namespace and their
40+
own GitHub Release URLs. GHCR credentials must permit package publication, and
41+
the package must be publicly readable for the anonymous-pull verification to
42+
pass.
43+
44+
With `upload_to_release` disabled, the workflow keeps CLI/Relay archives in
45+
Actions artifacts and validates the runtime image build without pushing it.
46+
The explicit `relay_image_only` backfill mode remains a publishing operation.
47+
48+
Install Beta CLI archives manually and deploy the Relay with an explicit Beta
49+
image tag or its signed descriptor's digest. The default CLI install/update and
50+
Relay one-click deployment paths stay on stable; Beta CLI builds do not run
51+
stable-feed automatic update checks. This does not add a runtime channel switch
52+
or a Beta option to one-click deployment. The stable CLI/Relay mirror manifests
53+
and the Desktop-only `channel-beta/latest.json` pointer remain unchanged.
3354

3455
The selected ref must resolve to a commit in the protected `main` history. The
3556
workflow pins that SHA before dispatching platform jobs and rejects an existing
@@ -61,3 +82,11 @@ stable-only CLI and Relay floating manifests.
6182
Production cron must run this in-repo script from the OpenBitFun checkout. Do not
6283
create a detached copy. Host paths, Nginx, and the rest of the origin restore
6384
steps live in [`deploy/openbitfun-host/README.md`](../../deploy/openbitfun-host/README.md).
85+
86+
## Focused packaging checks
87+
88+
For release workflow and channel-isolation changes, run
89+
`pnpm run check:github-config` and
90+
`node --test scripts/relay/package-contract.test.mjs scripts/tauri-release-manifest.test.mjs`.
91+
These checks exercise release conditions, image tag selection, Beta manifest
92+
generation, and asset collection with fixtures; they do not build or publish packages.

‎scripts/check-github-config.test.mjs‎

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,8 +1076,7 @@ test('stages unique release asset names before publishing', () => {
10761076
);
10771077
const steps = workflow.jobs['upload-release-assets'].steps;
10781078
const stagingIndexes = [
1079-
steps.findIndex((step) => step.name === 'Stage stable release assets'),
1080-
steps.findIndex((step) => step.name === 'Stage beta release assets'),
1079+
steps.findIndex((step) => step.name === 'Stage release assets'),
10811080
];
10821081
const uploadIndex = steps.findIndex((step) => step.name === 'Upload to release');
10831082

@@ -1182,10 +1181,10 @@ test('Desktop packaging keeps beta identity explicit and stable-safe', () => {
11821181
(step) => step.name === 'Publish beta channel manifest',
11831182
);
11841183
assert.ok(verifyIndexPublished >= 0 && verifyIndexPublished < promoteIndex);
1185-
assert.match(workflow.jobs['linux-binaries'].if, /release_channel == 'stable'/);
1184+
assert.doesNotMatch(workflow.jobs['linux-binaries'].if, /release_channel/);
11861185
assert.equal(
1187-
uploadSteps.find((step) => step.name === 'Stage beta release assets').if,
1188-
"needs.prepare.outputs.release_channel == 'beta'",
1186+
uploadSteps.find((step) => step.name === 'Stage release assets').if,
1187+
undefined,
11891188
);
11901189
assert.match(
11911190
uploadSteps.find((step) => step.name === 'Generate updater manifest').run,
@@ -1230,6 +1229,106 @@ test('beta publishing cannot advance the Relay latest image tag', () => {
12301229
assert.doesNotMatch(imageTags.run, /RELEASE_PRERELEASE/);
12311230
});
12321231

1232+
test('stable and beta publication require every producer, while artifact-only runs never publish', () => {
1233+
const { jobs } = yaml.parse(readFileSync(
1234+
path.join(repoRoot, '.github/workflows/desktop-package.yml'), 'utf8',
1235+
));
1236+
// These workflow conditions use the shared Boolean/string subset of Actions
1237+
// expressions and JavaScript. Exercise the actual conditions, not a copy.
1238+
const condition = (job, needs) => {
1239+
const expression = job.if.replace(/needs\.([\w-]+)/g, 'needs["$1"]');
1240+
return Function('needs', 'always', `return (${expression});`)(needs, () => true);
1241+
};
1242+
const results = ['success', 'failure', 'cancelled', 'skipped'];
1243+
for (const channel of ['stable', 'beta']) {
1244+
for (const upload of ['true', 'false']) {
1245+
for (const desktop of results) for (const linux of results) for (const image of results) {
1246+
const needs = {
1247+
prepare: { outputs: { release_channel: channel, upload_to_release: upload, relay_image_only: 'false' } },
1248+
package: { result: desktop },
1249+
'linux-binaries': { result: linux },
1250+
'publish-relay-image': { result: image },
1251+
};
1252+
assert.equal(condition(jobs['linux-binaries'], needs), true);
1253+
assert.equal(condition(jobs['publish-relay-image'], needs), upload === 'true' && linux === 'success');
1254+
assert.equal(condition(jobs['upload-release-assets'], needs),
1255+
upload === 'true' && [desktop, linux, image].every((result) => result === 'success'),
1256+
JSON.stringify(needs));
1257+
}
1258+
}
1259+
const backfill = {
1260+
prepare: { outputs: { release_channel: channel, upload_to_release: 'true', relay_image_only: 'true' } },
1261+
package: { result: 'skipped' },
1262+
'linux-binaries': { result: 'skipped' },
1263+
'publish-relay-image': { result: 'success' },
1264+
};
1265+
assert.equal(condition(jobs['linux-binaries'], backfill), false);
1266+
assert.equal(condition(jobs['publish-relay-image'], backfill), true);
1267+
assert.equal(condition(jobs['upload-release-assets'], backfill), false);
1268+
}
1269+
const backfillRelease = jobs['publish-relay-image'].steps.find(
1270+
(step) => step.name === 'Attach descriptor to an existing release (image-only backfill)',
1271+
);
1272+
assert.equal(backfillRelease.with.prerelease, "${{ needs.prepare.outputs.release_channel == 'beta' }}");
1273+
assert.equal(backfillRelease.with.make_latest, 'false');
1274+
const steps = jobs['upload-release-assets'].steps;
1275+
for (const name of [
1276+
'Download Linux binary artifacts', 'Download Relay image descriptor',
1277+
'Generate Linux binaries manifest', 'Stage release assets',
1278+
'Verify published Linux binaries manifest', 'Verify published Relay image descriptor',
1279+
]) {
1280+
assert.equal(steps.find((step) => step.name === name)?.if, undefined, name);
1281+
assert.ok(steps.some((step) => step.name === name), name);
1282+
}
1283+
const stage = steps.find((step) => step.name === 'Stage release assets');
1284+
for (const pattern of [
1285+
'linux-release-assets/openbitfun-cli-*.tar.gz',
1286+
'linux-release-assets/openbitfun-relay-server-*.tar.gz',
1287+
'linux-release-assets/*.tar.gz.sig', 'linux-release-assets/*.tar.gz.sha256.sig',
1288+
'linux-release-assets/linux-binaries.json', 'relay-image-assets/relay-image.json.sig',
1289+
]) assert.ok(stage.run.includes(pattern), pattern);
1290+
assert.match(steps.find((step) => step.name === 'Generate Linux binaries manifest').run, /--repo "\$\{\{ github.repository \}\}"/);
1291+
for (const name of ['Verify published Linux binaries manifest', 'Verify published Relay image descriptor']) {
1292+
const verification = steps.find((step) => step.name === name);
1293+
assert.match(verification.run, /github.repository/);
1294+
assert.ok(steps.indexOf(verification) < steps.findIndex((step) => step.name === 'Publish beta channel manifest'));
1295+
}
1296+
});
1297+
1298+
test('Relay image tag selection keeps Beta and old stable backfills away from latest', {
1299+
skip: process.platform === 'win32',
1300+
}, (t) => {
1301+
const { jobs } = yaml.parse(readFileSync(
1302+
path.join(repoRoot, '.github/workflows/desktop-package.yml'), 'utf8',
1303+
));
1304+
const step = jobs['publish-relay-image'].steps.find((entry) => entry.name === 'Resolve image tags');
1305+
const root = mkdtempSync(path.join(tmpdir(), 'openbitfun-image-tags-'));
1306+
t.after(() => rmSync(root, { recursive: true, force: true }));
1307+
for (const scenario of [
1308+
{ channel: 'beta', version: '1.0.0-beta.3', imageOnly: 'false', latest: false },
1309+
{ channel: 'beta', version: '1.0.0-beta.3', imageOnly: 'true', latest: false },
1310+
{ channel: 'stable', version: '1.0.0', imageOnly: 'false', latest: true },
1311+
{ channel: 'stable', version: '1.0.0', imageOnly: 'true', latest: true },
1312+
{ channel: 'stable', version: '0.2.19', imageOnly: 'true', latest: false },
1313+
]) {
1314+
const output = path.join(root, 'output');
1315+
writeFileSync(output, '');
1316+
const result = spawnSync('bash', ['-c', `gh() { printf 'v1.0.0\\n'; }\n${step.run}`], {
1317+
env: { ...process.env, GITHUB_OUTPUT: output, GITHUB_REPOSITORY: 'test-owner/BitFun',
1318+
IMAGE: 'ghcr.io/test-owner/openbitfun-relay-server', RELEASE_TAG: `v${scenario.version}`,
1319+
RELEASE_VERSION: scenario.version, RELEASE_CHANNEL: scenario.channel, IMAGE_ONLY: scenario.imageOnly },
1320+
encoding: 'utf8', windowsHide: true,
1321+
});
1322+
assert.equal(result.status, 0, result.stderr);
1323+
const tags = readFileSync(output, 'utf8').trim().split('\n');
1324+
assert.deepEqual(tags, [
1325+
'value<<EOF', `ghcr.io/test-owner/openbitfun-relay-server:v${scenario.version}`,
1326+
`ghcr.io/test-owner/openbitfun-relay-server:${scenario.version}`,
1327+
...(scenario.latest ? ['ghcr.io/test-owner/openbitfun-relay-server:latest'] : []), 'EOF',
1328+
]);
1329+
}
1330+
});
1331+
12331332
test('beta channel readback retries stale content and fails if it never converges', {
12341333
skip: process.platform === 'win32' || spawnSync('jq', ['--version'], { windowsHide: true }).status !== 0,
12351334
}, (t) => {

0 commit comments

Comments
 (0)