Skip to content

Nightly Build

Nightly Build #146

Workflow file for this run

name: Nightly Build
on:
schedule:
# Weekdays at 02:00 UTC
- cron: "0 2 * * 1-5"
workflow_dispatch:
inputs:
build_only:
description: "Build and package every artifact without publishing it."
required: false
default: false
type: boolean
artifact_retention_days:
description: "Retention for build artifacts."
required: false
default: 7
type: number
permissions:
contents: read
concurrency:
# The release, tag, and GHCR image tags are repository-wide resources. Keep
# manual runs from other refs serialized with the scheduled main run.
group: nightly-${{ github.repository }}
cancel-in-progress: true
env:
NIGHTLY_TAG: nightly
jobs:
# ── Check if there are new commits since last nightly ──────────────
check-changes:
name: Check for Changes
runs-on: ubuntu-latest
outputs:
should_build: ${{ steps.check.outputs.should_build }}
nightly_version: ${{ steps.check.outputs.nightly_version }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Check for recent changes
id: check
shell: bash
run: |
set -euo pipefail
BASE_VERSION="$(jq -r '.version' package.json)"
DATE_SUFFIX="$(date -u '+%Y%m%d')"
SHORT_SHA="$(git rev-parse --short HEAD)"
NIGHTLY_VERSION="${BASE_VERSION}-nightly.${DATE_SUFFIX}+${SHORT_SHA}"
echo "nightly_version=$NIGHTLY_VERSION" >> "$GITHUB_OUTPUT"
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "should_build=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Check if any commits landed in the last 25 hours
LAST_COMMIT_TIME="$(git log -1 --format='%ct')"
NOW="$(date -u '+%s')"
HOURS_AGO=$(( (NOW - LAST_COMMIT_TIME) / 3600 ))
if [[ "$HOURS_AGO" -lt 25 ]]; then
echo "should_build=true" >> "$GITHUB_OUTPUT"
else
echo "No new commits in the last 25 hours, skipping nightly build."
echo "should_build=false" >> "$GITHUB_OUTPUT"
fi
# ── Build every Nightly artifact through the reusable CI contract ──
build-artifacts:
name: Build Nightly Artifacts
needs: check-changes
if: needs.check-changes.outputs.should_build == 'true'
uses: ./.github/workflows/nightly-artifacts.yml
permissions:
contents: read
secrets:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
TAURI_UPDATER_PUBKEY: ${{ secrets.TAURI_UPDATER_PUBKEY }}
with:
checkout_ref: ${{ github.sha }}
version: ${{ needs.check-changes.outputs.nightly_version }}
artifact_prefix: nightly
artifact_retention_days: ${{ fromJSON(inputs.artifact_retention_days || '7') }}
build_desktop_packages: true
desktop_platforms: '["linux-x64","linux-arm64","macos-arm64","macos-x64","windows-x64"]'
build_linux_binaries: true
build_relay_image: true
upload_artifacts: true
# Fork validation is intentionally cache-read-only. Only the upstream
# repository may seed long-lived Nightly caches.
cache_write: ${{ github.repository_owner == 'GCWing' }}
# ── Publish nightly pre-release ────────────────────────────────────
publish-nightly:
name: Publish Nightly
needs: [check-changes, build-artifacts]
if: >-
needs.check-changes.outputs.should_build == 'true' &&
inputs.build_only != true
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v5
- name: Download bundled artifacts
uses: actions/download-artifact@v7
with:
pattern: bitfun-nightly-*-bundle
path: release-assets
merge-multiple: true
- name: Download Linux binary artifacts
uses: actions/download-artifact@v7
with:
pattern: bitfun-linux-nightly-*
path: linux-release-assets
merge-multiple: true
- name: Verify Relay image inputs
shell: bash
run: |
set -euo pipefail
test -f linux-release-assets/bitfun-relay-server-x86_64-unknown-linux-gnu.tar.gz
test -f linux-release-assets/bitfun-relay-server-aarch64-unknown-linux-gnu.tar.gz
for archive in linux-release-assets/bitfun-relay-server-*.tar.gz; do
(cd linux-release-assets && sha256sum --check "$(basename "${archive}").sha256")
done
cp src/apps/relay-server/Dockerfile.release linux-release-assets/Dockerfile.release
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Resolve nightly image metadata
id: nightly-image-meta
shell: bash
env:
NIGHTLY_VERSION: ${{ needs.check-changes.outputs.nightly_version }}
run: |
echo "asset_version=${NIGHTLY_VERSION%%+*}" >>"$GITHUB_OUTPUT"
echo "image=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/bitfun-relay-server" >>"$GITHUB_OUTPUT"
- name: Build and push multi-platform Relay image
id: relay-image
uses: docker/build-push-action@v7
with:
context: linux-release-assets
file: linux-release-assets/Dockerfile.release
platforms: linux/amd64,linux/arm64
push: true
provenance: false
sbom: false
build-args: |
VERSION=${{ needs.check-changes.outputs.nightly_version }}
REVISION=${{ github.sha }}
tags: |
${{ steps.nightly-image-meta.outputs.image }}:${{ env.NIGHTLY_TAG }}
${{ steps.nightly-image-meta.outputs.image }}:${{ steps.nightly-image-meta.outputs.asset_version }}
- name: Smoke-test published Relay image on both platforms
shell: bash
env:
IMAGE_DIGEST: ${{ steps.relay-image.outputs.digest }}
run: |
bash scripts/relay/smoke-image.sh \
"${{ steps.nightly-image-meta.outputs.image }}@${IMAGE_DIGEST}"
- name: Generate signed Relay image descriptor
shell: bash
env:
IMAGE_DIGEST: ${{ steps.relay-image.outputs.digest }}
NIGHTLY_VERSION: ${{ needs.check-changes.outputs.nightly_version }}
BITFUN_SIGNING_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
BITFUN_SIGNING_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
BITFUN_SIGNING_PUBKEY: ${{ secrets.TAURI_UPDATER_PUBKEY }}
run: |
set -euo pipefail
asset_version="${NIGHTLY_VERSION%%+*}"
[[ "${IMAGE_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]
docker buildx imagetools inspect \
"${{ steps.nightly-image-meta.outputs.image }}@${IMAGE_DIGEST}" \
--raw >relay-image-manifest.json
jq -e '
[.manifests[].platform | .os + "/" + .architecture] as $platforms
| ($platforms | index("linux/amd64")) != null
and ($platforms | index("linux/arm64")) != null
' relay-image-manifest.json >/dev/null
jq -n \
--arg image "${{ steps.nightly-image-meta.outputs.image }}" \
--arg tag "${NIGHTLY_TAG}" \
--arg version "${asset_version}" \
--arg digest "${IMAGE_DIGEST}" \
'{
schema_version: 1,
image: $image,
tag: $tag,
version: $version,
digest: $digest,
platforms: ["linux/amd64", "linux/arm64"]
}' >relay-image.json
bash scripts/sign-release-assets.sh relay-image.json
test -s relay-image.json.sig
- name: Verify anonymous Relay image access
shell: bash
env:
IMAGE_DIGEST: ${{ steps.relay-image.outputs.digest }}
run: |
set -euo pipefail
docker logout ghcr.io >/dev/null 2>&1 || true
clean_config="$(mktemp -d)"
trap 'rm -rf "$clean_config"' EXIT
DOCKER_CONFIG="$clean_config" docker buildx imagetools inspect \
"${{ steps.nightly-image-meta.outputs.image }}@${IMAGE_DIGEST}" >/dev/null
- name: List release assets
run: |
echo "Nightly assets:"
find release-assets -type f | sort
echo "Nightly Linux CLI and Relay Server assets:"
find linux-release-assets -type f | sort
- name: Delete previous nightly release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release delete "${{ env.NIGHTLY_TAG }}" --yes --cleanup-tag 2>/dev/null || true
- name: Generate Linux binaries manifest
env:
NIGHTLY_VERSION: ${{ needs.check-changes.outputs.nightly_version }}
run: |
set -euo pipefail
# Asset names drop the `+<sha>` build metadata (see linux-binaries.yml).
node scripts/generate-linux-binaries-manifest.mjs \
--assets-dir linux-release-assets \
--version "${NIGHTLY_VERSION%%+*}" \
--tag "${{ env.NIGHTLY_TAG }}" \
--repo "${{ github.repository }}" \
--out linux-release-assets/linux-binaries.json
- name: Prepare versioned Windows installer
env:
NIGHTLY_VERSION: ${{ needs.check-changes.outputs.nightly_version }}
run: |
node scripts/prepare-windows-installer-asset.mjs \
--assets-dir release-assets \
--version "${NIGHTLY_VERSION%%+*}" \
--out-dir release-manual-assets
# The Tauri bundler signs the five updater artifacts during `tauri build`,
# but the installers people download by hand from the release page — dmg,
# deb, rpm, the Windows installer and the direct AppImages — shipped with
# no signature at all. Sign them with the same key so every published
# artifact is verifiable. (This is not OS-level code signing: Gatekeeper
# and SmartScreen still need Apple/Authenticode certificates.)
- name: Sign installer packages
shell: bash
env:
BITFUN_SIGNING_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
BITFUN_SIGNING_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
BITFUN_SIGNING_PUBKEY: ${{ secrets.TAURI_UPDATER_PUBKEY }}
run: |
set -euo pipefail
mapfile -t assets < <({
find release-assets -type f \
\( -name '*.AppImage' -o -name '*.deb' -o -name '*.rpm' \
-o -name '*.dmg' \)
find release-manual-assets -type f -name '*.exe'
} | sort)
if [[ "${#assets[@]}" -eq 0 ]]; then
echo "No installer packages found to sign."
exit 0
fi
bash scripts/sign-release-assets.sh "${assets[@]}"
# Publish the public key alongside the signatures: a signature nobody
# can fetch a key for is not verifiable.
node scripts/write-minisign-public-key.mjs \
--out release-assets/minisign.pub
- name: Create nightly release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ env.NIGHTLY_TAG }}
name: "Nightly Build (${{ needs.check-changes.outputs.nightly_version }})"
body: |
Automated nightly build from `main` branch.
**Version**: `${{ needs.check-changes.outputs.nightly_version }}`
**Commit**: ${{ github.sha }}
**Date**: ${{ github.event.head_commit.timestamp || github.event.repository.updated_at }}
> **Warning**: Nightly builds are untested and may be unstable.
prerelease: true
files: |
release-assets/**/*.AppImage
release-assets/**/*.deb
release-assets/**/*.dmg
release-assets/**/*.rpm
release-assets/**/*.sig
release-manual-assets/*.exe
release-manual-assets/*.exe.sig
release-assets/minisign.pub
release-assets/**/bitfun-cli-*-apple-darwin.tar.gz
release-assets/**/bitfun-cli-*-apple-darwin.tar.gz.sha256
linux-release-assets/bitfun-cli-*.tar.gz
linux-release-assets/bitfun-cli-*.tar.gz.sha256
linux-release-assets/bitfun-relay-server-*.tar.gz
linux-release-assets/bitfun-relay-server-*.tar.gz.sha256
linux-release-assets/*.tar.gz.sig
linux-release-assets/*.tar.gz.sha256.sig
linux-release-assets/linux-binaries.json
relay-image.json
relay-image.json.sig
- name: Verify published Linux CLI signatures
shell: bash
run: |
set -euo pipefail
while IFS= read -r cli_url; do
curl -fsSL --retry 5 --retry-delay 3 "${cli_url}.sig" -o /dev/null
curl -fsSL --retry 5 --retry-delay 3 "${cli_url}.sha256.sig" -o /dev/null
done < <(jq -r '.platforms[].cli.url' linux-release-assets/linux-binaries.json)
- name: Verify published Relay image descriptor
shell: bash
env:
NIGHTLY_VERSION: ${{ needs.check-changes.outputs.nightly_version }}
run: |
set -euo pipefail
curl -fsSL --retry 5 --retry-delay 3 \
"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/download/${NIGHTLY_TAG}/relay-image.json" \
-o relay-image.published.json
test "$(jq -r '.tag' relay-image.published.json)" = "${NIGHTLY_TAG}"
test "$(jq -r '.version' relay-image.published.json)" = "${NIGHTLY_VERSION%%+*}"
test "$(jq -r '.image' relay-image.published.json)" = "${{ steps.nightly-image-meta.outputs.image }}"
jq -e '.digest | test("^sha256:[0-9a-f]{64}$")' relay-image.published.json >/dev/null
curl -fsSL --retry 5 --retry-delay 3 \
"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/download/${NIGHTLY_TAG}/relay-image.json.sig" \
-o /dev/null
- name: Verify published macOS CLI assets
shell: bash
env:
NIGHTLY_VERSION: ${{ needs.check-changes.outputs.nightly_version }}
run: |
set -euo pipefail
ASSET_VERSION="${NIGHTLY_VERSION%%+*}"
RELEASE_ROOT="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/download/${{ env.NIGHTLY_TAG }}"
for target in aarch64-apple-darwin x86_64-apple-darwin; do
archive="${RELEASE_ROOT}/bitfun-cli-${ASSET_VERSION}-${target}.tar.gz"
curl -fsSL --retry 5 --retry-delay 3 "$archive" -o /dev/null
curl -fsSL --retry 5 --retry-delay 3 "${archive}.sha256" -o /dev/null
curl -fsSL --retry 5 --retry-delay 3 "${archive}.sig" -o /dev/null
curl -fsSL --retry 5 --retry-delay 3 "${archive}.sha256.sig" -o /dev/null
done