1515 required : false
1616 default : false
1717 type : boolean
18+ relay_image_only :
19+ description : " Publish only the Relay Server image and signed descriptor (no Desktop packages)."
20+ required : false
21+ default : false
22+ type : boolean
1823
1924permissions :
2025 contents : write
26+ packages : write
2127
2228concurrency :
2329 group : desktop-package-${{ github.event.release.tag_name || inputs.tag_name || github.sha }}
3339 release_tag : ${{ steps.meta.outputs.release_tag }}
3440 upload_to_release : ${{ steps.meta.outputs.upload_to_release }}
3541 checkout_ref : ${{ steps.meta.outputs.checkout_ref }}
42+ relay_image_only : ${{ steps.meta.outputs.relay_image_only }}
3643 steps :
3744 - uses : actions/checkout@v5
3845
4552 RELEASE_TAG_NAME : ${{ github.event.release.tag_name }}
4653 INPUT_TAG_NAME : ${{ inputs.tag_name }}
4754 INPUT_UPLOAD_TO_RELEASE : ${{ inputs.upload_to_release }}
55+ INPUT_RELAY_IMAGE_ONLY : ${{ inputs.relay_image_only }}
4856 run : |
4957 set -euo pipefail
5058
5664 elif [[ -n "${INPUT_TAG_NAME}" ]]; then
5765 TAG="${INPUT_TAG_NAME}"
5866 VERSION="${TAG#v}"
59- CHECKOUT_REF="${TAG}"
67+ # A one-off image backfill must use the workflow branch: an older
68+ # tag does not contain Dockerfile.release or this publishing job.
69+ if [[ "${INPUT_RELAY_IMAGE_ONLY}" == "true" ]]; then
70+ CHECKOUT_REF="${GITHUB_SHA}"
71+ else
72+ CHECKOUT_REF="${TAG}"
73+ fi
6074 if [[ "${INPUT_UPLOAD_TO_RELEASE}" == "true" ]]; then
6175 UPLOAD="true"
6276 else
@@ -73,12 +87,14 @@ jobs:
7387 echo "release_tag=$TAG" >> "$GITHUB_OUTPUT"
7488 echo "upload_to_release=$UPLOAD" >> "$GITHUB_OUTPUT"
7589 echo "checkout_ref=$CHECKOUT_REF" >> "$GITHUB_OUTPUT"
90+ echo "relay_image_only=${INPUT_RELAY_IMAGE_ONLY:-false}" >> "$GITHUB_OUTPUT"
7691
7792 # ── Build per platform ─────────────────────────────────────────────
7893 package :
7994 name : Package (${{ matrix.platform.name }})
8095 runs-on : ${{ matrix.platform.os }}
8196 needs : prepare
97+ if : needs.prepare.outputs.relay_image_only != 'true'
8298 env :
8399 NODE_OPTIONS : --max-old-space-size=6144
84100 BITFUN_ENABLE_UPDATER_ARTIFACTS : ${{ needs.prepare.outputs.upload_to_release }}
@@ -239,6 +255,7 @@ jobs:
239255 linux-binaries :
240256 name : Linux CLI and Relay Server
241257 needs : prepare
258+ if : needs.prepare.outputs.relay_image_only != 'true'
242259 uses : ./.github/workflows/linux-binaries.yml
243260 secrets :
244261 release_signing_key : ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
@@ -249,11 +266,208 @@ jobs:
249266 version : ${{ needs.prepare.outputs.version }}
250267 artifact_prefix : ${{ needs.prepare.outputs.release_tag }}
251268
269+ # Publish the Relay once, as a multi-platform image. User servers only pull
270+ # this image; they no longer download an archive and build a runtime image.
271+ publish-relay-image :
272+ name : Publish Relay Server Image
273+ needs : [prepare, linux-binaries]
274+ if : >-
275+ always() &&
276+ (needs.prepare.outputs.upload_to_release == 'true' ||
277+ needs.prepare.outputs.relay_image_only == 'true') &&
278+ (needs.prepare.outputs.relay_image_only == 'true' ||
279+ needs.linux-binaries.result == 'success')
280+ runs-on : ubuntu-latest
281+ permissions :
282+ contents : write
283+ packages : write
284+ env :
285+ IMAGE : ghcr.io/gcwing/bitfun-relay-server
286+
287+ steps :
288+ - name : Checkout
289+ uses : actions/checkout@v5
290+ with :
291+ ref : ${{ needs.prepare.outputs.checkout_ref }}
292+
293+ - name : Download Relay archives from this release run
294+ if : needs.prepare.outputs.relay_image_only != 'true'
295+ uses : actions/download-artifact@v7
296+ with :
297+ pattern : bitfun-linux-${{ needs.prepare.outputs.release_tag }}-*
298+ path : linux-release-assets
299+ merge-multiple : true
300+
301+ - name : Download Relay archives from the existing release (image-only backfill)
302+ if : needs.prepare.outputs.relay_image_only == 'true'
303+ shell : bash
304+ env :
305+ GH_TOKEN : ${{ github.token }}
306+ RELEASE_TAG : ${{ needs.prepare.outputs.release_tag }}
307+ run : |
308+ set -euo pipefail
309+ mkdir -p linux-release-assets
310+ gh release download "${RELEASE_TAG}" \
311+ --repo GCWing/BitFun \
312+ --dir linux-release-assets \
313+ --pattern 'bitfun-relay-server-*.tar.gz' \
314+ --pattern 'bitfun-relay-server-*.tar.gz.sha256'
315+
316+ - name : Verify image inputs
317+ shell : bash
318+ run : |
319+ set -euo pipefail
320+ test -f linux-release-assets/bitfun-relay-server-x86_64-unknown-linux-gnu.tar.gz
321+ test -f linux-release-assets/bitfun-relay-server-aarch64-unknown-linux-gnu.tar.gz
322+ for archive in linux-release-assets/bitfun-relay-server-*.tar.gz; do
323+ (cd linux-release-assets && sha256sum --check "$(basename "${archive}").sha256")
324+ done
325+ cp src/apps/relay-server/Dockerfile.release linux-release-assets/Dockerfile.release
326+
327+ - name : Set up QEMU
328+ uses : docker/setup-qemu-action@v4
329+
330+ - name : Set up Docker Buildx
331+ uses : docker/setup-buildx-action@v4
332+
333+ - name : Log in to GHCR
334+ uses : docker/login-action@v4
335+ with :
336+ registry : ghcr.io
337+ username : ${{ github.actor }}
338+ password : ${{ github.token }}
339+
340+ - name : Resolve image tags
341+ id : image-tags
342+ shell : bash
343+ env :
344+ GH_TOKEN : ${{ github.token }}
345+ RELEASE_TAG : ${{ needs.prepare.outputs.release_tag }}
346+ RELEASE_VERSION : ${{ needs.prepare.outputs.version }}
347+ IMAGE_ONLY : ${{ needs.prepare.outputs.relay_image_only }}
348+ RELEASE_PRERELEASE : ${{ github.event.release.prerelease }}
349+ run : |
350+ set -euo pipefail
351+ asset_version="${RELEASE_VERSION%%+*}"
352+ {
353+ echo 'value<<EOF'
354+ echo "${IMAGE}:${RELEASE_TAG}"
355+ echo "${IMAGE}:${asset_version}"
356+ if [[ "${IMAGE_ONLY}" == "true" ]]; then
357+ # Backfilling an older release must not roll the floating tag
358+ # backwards. GitHub's latest endpoint excludes prereleases.
359+ latest_release="$(gh api repos/GCWing/BitFun/releases/latest --jq .tag_name)"
360+ if [[ "${RELEASE_TAG}" == "${latest_release}" ]]; then
361+ echo "${IMAGE}:latest"
362+ fi
363+ elif [[ "${RELEASE_PRERELEASE:-false}" != "true" ]]; then
364+ # The normal release workflow can create the GitHub Release only
365+ # after packaging, so it cannot rely on /releases/latest yet.
366+ echo "${IMAGE}:latest"
367+ fi
368+ echo EOF
369+ } >>"$GITHUB_OUTPUT"
370+
371+ - name : Build and push multi-platform image
372+ id : image
373+ uses : docker/build-push-action@v7
374+ with :
375+ context : linux-release-assets
376+ file : linux-release-assets/Dockerfile.release
377+ platforms : linux/amd64,linux/arm64
378+ push : true
379+ provenance : false
380+ sbom : false
381+ build-args : |
382+ VERSION=${{ needs.prepare.outputs.version }}
383+ REVISION=${{ needs.prepare.outputs.release_tag }}
384+ tags : ${{ steps.image-tags.outputs.value }}
385+
386+ - name : Smoke-test published image on both platforms
387+ shell : bash
388+ env :
389+ IMAGE_DIGEST : ${{ steps.image.outputs.digest }}
390+ run : bash scripts/relay/smoke-image.sh "${IMAGE}@${IMAGE_DIGEST}"
391+
392+ - name : Verify manifest and generate signed descriptor
393+ shell : bash
394+ env :
395+ IMAGE_DIGEST : ${{ steps.image.outputs.digest }}
396+ RELEASE_TAG : ${{ needs.prepare.outputs.release_tag }}
397+ RELEASE_VERSION : ${{ needs.prepare.outputs.version }}
398+ BITFUN_SIGNING_KEY : ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
399+ BITFUN_SIGNING_PASSWORD : ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
400+ BITFUN_SIGNING_PUBKEY : ${{ secrets.TAURI_UPDATER_PUBKEY }}
401+ run : |
402+ set -euo pipefail
403+ [[ "${IMAGE_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]
404+ docker buildx imagetools inspect "${IMAGE}@${IMAGE_DIGEST}" --raw >relay-image-manifest.json
405+ jq -e '
406+ [.manifests[].platform | .os + "/" + .architecture] as $platforms
407+ | ($platforms | index("linux/amd64")) != null
408+ and ($platforms | index("linux/arm64")) != null
409+ ' relay-image-manifest.json >/dev/null
410+ jq -n \
411+ --arg image "${IMAGE}" \
412+ --arg tag "${RELEASE_TAG}" \
413+ --arg version "${RELEASE_VERSION}" \
414+ --arg digest "${IMAGE_DIGEST}" \
415+ '{
416+ schema_version: 1,
417+ image: $image,
418+ tag: $tag,
419+ version: $version,
420+ digest: $digest,
421+ platforms: ["linux/amd64", "linux/arm64"]
422+ }' >relay-image.json
423+ bash scripts/sign-release-assets.sh relay-image.json
424+ test -s relay-image.json.sig
425+
426+ - name : Upload signed image descriptor
427+ uses : actions/upload-artifact@v6
428+ with :
429+ name : bitfun-relay-image-${{ needs.prepare.outputs.release_tag }}
430+ if-no-files-found : error
431+ retention-days : 7
432+ path : |
433+ relay-image.json
434+ relay-image.json.sig
435+
436+ # The package is private on first creation. This deliberately fails until
437+ # its visibility is changed to public, preventing an apparently green
438+ # release that anonymous customer servers cannot pull.
439+ - name : Verify anonymous pull access
440+ shell : bash
441+ env :
442+ IMAGE_DIGEST : ${{ steps.image.outputs.digest }}
443+ run : |
444+ set -euo pipefail
445+ docker logout ghcr.io >/dev/null 2>&1 || true
446+ clean_config="$(mktemp -d)"
447+ trap 'rm -rf "$clean_config"' EXIT
448+ DOCKER_CONFIG="$clean_config" docker buildx imagetools inspect \
449+ "${IMAGE}@${IMAGE_DIGEST}" >/dev/null
450+
451+ - name : Attach descriptor to an existing release (image-only backfill)
452+ if : needs.prepare.outputs.relay_image_only == 'true'
453+ uses : softprops/action-gh-release@v3
454+ with :
455+ tag_name : ${{ needs.prepare.outputs.release_tag }}
456+ files : |
457+ relay-image.json
458+ relay-image.json.sig
459+ fail_on_unmatched_files : true
460+
252461 # ── Upload assets to GitHub Release ────────────────────────────────
253462 upload-release-assets :
254463 name : Upload Release Assets
255- needs : [prepare, package, linux-binaries]
256- if : needs.prepare.outputs.upload_to_release == 'true'
464+ needs : [prepare, package, linux-binaries, publish-relay-image]
465+ if : >-
466+ always() &&
467+ needs.prepare.outputs.upload_to_release == 'true' &&
468+ needs.package.result == 'success' &&
469+ needs.linux-binaries.result == 'success' &&
470+ needs.publish-relay-image.result == 'success'
257471 runs-on : ubuntu-latest
258472 env :
259473 REQUIRED_UPDATER_PLATFORMS : windows-x86_64,darwin-x86_64,darwin-aarch64,linux-x86_64,linux-aarch64
@@ -276,12 +490,20 @@ jobs:
276490 path : linux-release-assets
277491 merge-multiple : true
278492
493+ - name : Download Relay image descriptor
494+ uses : actions/download-artifact@v7
495+ with :
496+ name : bitfun-relay-image-${{ needs.prepare.outputs.release_tag }}
497+ path : relay-image-assets
498+
279499 - name : List release assets
280500 run : |
281501 echo "Release assets:"
282502 find release-assets -type f | sort
283503 echo "Linux CLI and Relay Server assets:"
284504 find linux-release-assets -type f | sort
505+ echo "Relay image descriptor:"
506+ find relay-image-assets -type f | sort
285507
286508 - name : Collect updater assets
287509 run : |
@@ -367,6 +589,8 @@ jobs:
367589 linux-release-assets/*.tar.gz.sig
368590 linux-release-assets/*.tar.gz.sha256.sig
369591 linux-release-assets/linux-binaries.json
592+ relay-image-assets/relay-image.json
593+ relay-image-assets/relay-image.json.sig
370594 fail_on_unmatched_files : true
371595
372596 - name : Verify published updater manifest
@@ -391,6 +615,18 @@ jobs:
391615 curl -fsSL --retry 5 --retry-delay 3 "${cli_url}.sha256.sig" -o /dev/null
392616 done < <(jq -r '.platforms[].cli.url' linux-binaries.published.json)
393617
618+ - name : Verify published Relay image descriptor
619+ run : |
620+ curl -fsSL --retry 5 --retry-delay 3 \
621+ "https://github.com/GCWing/BitFun/releases/download/${{ needs.prepare.outputs.release_tag }}/relay-image.json" \
622+ -o relay-image.published.json
623+ test "$(jq -r '.tag' relay-image.published.json)" = "${{ needs.prepare.outputs.release_tag }}"
624+ test "$(jq -r '.image' relay-image.published.json)" = "ghcr.io/gcwing/bitfun-relay-server"
625+ jq -e '.digest | test("^sha256:[0-9a-f]{64}$")' relay-image.published.json >/dev/null
626+ curl -fsSL --retry 5 --retry-delay 3 \
627+ "https://github.com/GCWing/BitFun/releases/download/${{ needs.prepare.outputs.release_tag }}/relay-image.json.sig" \
628+ -o /dev/null
629+
394630 # Nudge the openbitfun.com mirror to sync now instead of on its next
395631 # 10-minute cron tick. Until the mirror has these bytes, CN clients have
396632 # only the GitHub origin to fall back to. Best effort: the cron run is
0 commit comments