Releases on git.crt.fyi from a workflow: a v* tag push runs .forgejo/workflows/release.yml (tag = package.json version and every version copy, the full CI, Linux AppImage + deb and Windows NSIS on the VDS runners, privacy scan with the private terms, one Forgejo release with short CHANGELOG-summary notes, the installers and SHA256SUMS, older release entries deleted, tags kept); workflow_dispatch dry run makes a draft test-<sha> and deletes it; desktop.yml folded in, ci.yml and release.yml share .forgejo/actions/linux-setup and ci-run; tools/forgejo-release.ts (notes, upsert, prune, delete; uploads over 100 MB go to the origin because Cloudflare refuses them); desktop-release.ts upload-forgejo adds the notarized DMG and updater bundle from the Mac; docs/desktop-release.md release order
This commit is contained in:
parent
e5f148e534
commit
2590b2e828
11 changed files with 871 additions and 229 deletions
45
.forgejo/actions/ci-run/action.yml
Normal file
45
.forgejo/actions/ci-run/action.yml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# `bun run ci` after .forgejo/actions/linux-setup: the target / timings / Playwright caches, bun install, headless
|
||||
# Chromium, Node 20 (the npm bundle's oldest supported Node) and the full CI. Shared by ci.yml and release.yml.
|
||||
name: ci-run
|
||||
description: storebound's full CI (bun run ci) on the Linux runner
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: apps/desktop/src-tauri/target
|
||||
key: ci-target-${{ runner.os }}-${{ hashFiles('apps/desktop/src-tauri/Cargo.lock') }}
|
||||
restore-keys: ci-target-${{ runner.os }}-
|
||||
# recorded test durations (shard balance) and tsc's incremental build info
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules/.cache/storebound-ci
|
||||
key: ci-timings-${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: ci-timings-${{ runner.os }}-
|
||||
- name: Install
|
||||
shell: bash
|
||||
run: |
|
||||
bun --version && cargo --version
|
||||
bun install --frozen-lockfile
|
||||
# Playwright's Chromium and headless shell (~280 MB), keyed by the browser builds the installed playwright-core
|
||||
# wants (its browsers.json); the system libraries and fonts (--with-deps) are installed on every run, the job
|
||||
# container starts clean
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('node_modules/playwright-core/browsers.json') }}
|
||||
restore-keys: playwright-${{ runner.os }}-
|
||||
# the job runs as root in a container: storebound adds --no-sandbox to the Chromium it starts itself there
|
||||
# (cli/shots/browser.ts sandboxArgs), Playwright's own launches already pass it
|
||||
- name: Headless Chromium (browser tests, desktop boot check)
|
||||
shell: bash
|
||||
run: bunx playwright install --with-deps chromium
|
||||
# the npm bundle runs on Node 20+: tools/ci.ts smoke-tests dist/storebound.mjs with the oldest supported Node
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
# the runner sets CI=true in the job and it wins over the workflow env above: set CI=full for the command
|
||||
# itself, or tools/ci.ts skips the desktop Rust tests and clippy when the push did not touch src-tauri
|
||||
- name: bun run ci
|
||||
shell: bash
|
||||
run: CI=full bun run ci
|
||||
42
.forgejo/actions/linux-setup/action.yml
Normal file
42
.forgejo/actions/linux-setup/action.yml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Shared setup of the Linux jobs (ci.yml; release.yml's ci and linux jobs) on the VDS runner (label ubuntu-22.04, the
|
||||
# catthehacker act image): Tauri's system packages plus `packages`, the Bun / Cargo caches, Bun and Rust on PATH.
|
||||
name: linux-setup
|
||||
description: System packages, caches, Bun and Rust for storebound's Linux jobs
|
||||
inputs:
|
||||
packages:
|
||||
description: Extra apt packages
|
||||
default: ''
|
||||
cache-key:
|
||||
description: Cache key prefix (separate caches for CI and the installer build)
|
||||
default: ci
|
||||
rust-components:
|
||||
description: Extra rustup components (for example clippy)
|
||||
default: ''
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: System packages
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq --no-install-recommends libwebkit2gtk-4.1-dev libgtk-3-dev \
|
||||
libayatana-appindicator3-dev librsvg2-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev libssl-dev libxdo-dev \
|
||||
build-essential curl unzip pkg-config ${{ inputs.packages }} >/dev/null
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.bun/install/cache
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
key: ${{ inputs.cache-key }}-${{ runner.os }}-${{ hashFiles('bun.lock', 'apps/desktop/src-tauri/Cargo.lock') }}
|
||||
restore-keys: ${{ inputs.cache-key }}-${{ runner.os }}-
|
||||
- name: Bun and Rust
|
||||
shell: bash
|
||||
run: |
|
||||
P="${FORGEJO_PATH:-$GITHUB_PATH}"
|
||||
curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" >/dev/null
|
||||
echo "$HOME/.bun/bin" >> "$P"
|
||||
command -v rustup || curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
|
||||
echo "$HOME/.cargo/bin" >> "$P"
|
||||
for c in ${{ inputs.rust-components }}; do "$HOME/.cargo/bin/rustup" component add "$c"; done
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
# desktop i18n / typecheck / build, desktop boot check (headless Chromium), desktop Rust tests and clippy, scrub. Runs on the VDS runner (docs/desktop-release.md).
|
||||
# tools/ci.ts runs the steps as a task graph and the tests as parallel shards (CPU cores - 1, max 8) on the one
|
||||
# self-hosted runner, so a job matrix would only queue; CI=full makes it run the Rust steps on every push.
|
||||
# Publishes nothing: site/ is still published by Jenkins.
|
||||
# Publishes nothing: site/ is still published by Jenkins; releases come from release.yml (a v* tag).
|
||||
# release.yml's ci job runs the same steps (the setup is shared: .forgejo/actions/linux-setup).
|
||||
name: ci
|
||||
|
||||
on:
|
||||
|
|
@ -27,57 +28,8 @@ jobs:
|
|||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: System packages (Tauri on Linux)
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq --no-install-recommends libwebkit2gtk-4.1-dev libgtk-3-dev \
|
||||
libayatana-appindicator3-dev librsvg2-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev libssl-dev libxdo-dev \
|
||||
build-essential curl unzip pkg-config >/dev/null
|
||||
- uses: actions/cache@v4
|
||||
- uses: ./.forgejo/actions/linux-setup
|
||||
with:
|
||||
path: |
|
||||
~/.bun/install/cache
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
apps/desktop/src-tauri/target
|
||||
key: ci-${{ runner.os }}-${{ hashFiles('bun.lock', 'apps/desktop/src-tauri/Cargo.lock') }}
|
||||
restore-keys: ci-${{ runner.os }}-
|
||||
# recorded test durations (shard balance) and tsc's incremental build info
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules/.cache/storebound-ci
|
||||
key: ci-timings-${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: ci-timings-${{ runner.os }}-
|
||||
- name: Bun and Rust
|
||||
run: |
|
||||
P="${FORGEJO_PATH:-$GITHUB_PATH}"
|
||||
curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" >/dev/null
|
||||
echo "$HOME/.bun/bin" >> "$P"
|
||||
command -v rustup || curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --component clippy
|
||||
echo "$HOME/.cargo/bin" >> "$P"
|
||||
- name: Install
|
||||
run: |
|
||||
rustup component add clippy
|
||||
bun --version && cargo --version
|
||||
bun install --frozen-lockfile
|
||||
# Playwright's Chromium and headless shell (~280 MB), keyed by the browser builds the installed playwright-core
|
||||
# wants (its browsers.json); the system libraries and fonts (--with-deps) are installed on every run, the job
|
||||
# container starts clean
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('node_modules/playwright-core/browsers.json') }}
|
||||
restore-keys: playwright-${{ runner.os }}-
|
||||
# the job runs as root in a container: storebound adds --no-sandbox to the Chromium it starts itself there
|
||||
# (cli/shots/browser.ts sandboxArgs), Playwright's own launches already pass it
|
||||
- name: Headless Chromium (browser tests, desktop boot check)
|
||||
run: bunx playwright install --with-deps chromium
|
||||
# the npm bundle runs on Node 20+: tools/ci.ts smoke-tests dist/storebound.mjs with the oldest supported Node
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
# the runner sets CI=true in the job and it wins over the workflow env above: set CI=full for the command
|
||||
# itself, or tools/ci.ts skips the desktop Rust tests and clippy when the push did not touch src-tauri
|
||||
- name: bun run ci
|
||||
run: CI=full bun run ci
|
||||
cache-key: ci
|
||||
rust-components: clippy
|
||||
- uses: ./.forgejo/actions/ci-run
|
||||
|
|
|
|||
|
|
@ -1,162 +0,0 @@
|
|||
# storebound desktop: Linux (AppImage + deb) and Windows (NSIS, per user, unsigned) installers.
|
||||
# macOS is built, signed and notarized on the owner's Mac (tools/desktop-release.ts mac); the updater signing key
|
||||
# never reaches a runner: the Mac signs these installers after downloading them (tools/desktop-release.ts add).
|
||||
# Runner setup (Forgejo runners on the VDS: Linux in Docker, Windows in a KVM VM): docs/desktop-release.md.
|
||||
# Publishes nothing: the installers are workflow artifacts only (14 days); `tools/desktop-release.ts add` on the Mac
|
||||
# signs and stages them, and only `publish` puts anything on storebound.dev.
|
||||
name: desktop
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
windows:
|
||||
description: Build Windows too (needs a runner with the "windows" label)
|
||||
type: boolean
|
||||
default: false
|
||||
push:
|
||||
tags: ['desktop-v*']
|
||||
|
||||
env:
|
||||
BUN_VERSION: '1.4.0'
|
||||
# the path-free build keeps the target folder outside every home folder
|
||||
CARGO_TARGET_DIR: /tmp/storebound-target
|
||||
CARGO_TERM_COLOR: never
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
# runner label: ubuntu-22.04:docker://ghcr.io/catthehacker/ubuntu:act-22.04 (glibc 2.35: the AppImage and the
|
||||
# deb then run on Ubuntu 22.04 and newer, Debian 12)
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
# linuxdeploy (AppImage) runs without FUSE inside the container
|
||||
APPIMAGE_EXTRACT_AND_RUN: '1'
|
||||
# linuxdeploy must not strip: the storebound sidecar is a `bun build --compile` executable whose JS payload strip
|
||||
# removes (it then starts as plain bun). The Rust binary is already stripped by Cargo (profile.release strip).
|
||||
NO_STRIP: '1'
|
||||
# WebKitGTK's DMABUF renderer leaves the window blank on some GPU / VM setups; the app sets this itself at
|
||||
# start (src-tauri/src/lib.rs linux_webkit_env) unless the user did; set here too for anything run in CI
|
||||
WEBKIT_DISABLE_DMABUF_RENDERER: '1'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: System packages
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq --no-install-recommends libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev \
|
||||
librsvg2-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev libssl-dev libxdo-dev patchelf file build-essential \
|
||||
curl unzip squashfs-tools p7zip-full xz-utils >/dev/null
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.bun/install/cache
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
key: desktop-${{ runner.os }}-${{ hashFiles('bun.lock', 'apps/desktop/src-tauri/Cargo.lock') }}
|
||||
restore-keys: desktop-${{ runner.os }}-
|
||||
- name: Bun and Rust
|
||||
run: |
|
||||
P="${FORGEJO_PATH:-$GITHUB_PATH}"
|
||||
curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" >/dev/null
|
||||
echo "$HOME/.bun/bin" >> "$P"
|
||||
command -v rustup || curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
|
||||
echo "$HOME/.cargo/bin" >> "$P"
|
||||
- name: Install and check
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
bun tools/desktop-version.ts
|
||||
bun tools/build-sidecars.ts --target bun-linux-x64
|
||||
# linuxdeploy (AppImage) sets the rpath of every executable with its own patchelf, which corrupts the bun
|
||||
# sidecar (segfault, then ldd fails in the gtk plugin). With the rpath already set (Ubuntu's patchelf, which
|
||||
# keeps it working), linuxdeploy leaves the file as it is. The sidecar needs only libc, so the rpath is inert.
|
||||
patchelf --set-rpath '$ORIGIN/../lib' apps/desktop/src-tauri/binaries/storebound-x86_64-unknown-linux-gnu
|
||||
apps/desktop/src-tauri/binaries/storebound-x86_64-unknown-linux-gnu version
|
||||
- name: Build AppImage and deb (path-free)
|
||||
working-directory: apps/desktop
|
||||
run: bun ../../tools/desktop-release.ts exec -- bunx tauri build --bundles appimage,deb --ci
|
||||
- name: Self-test the exact artifacts
|
||||
run: |
|
||||
V=$(bun tools/desktop-version.ts --print)
|
||||
B="$CARGO_TARGET_DIR/release/bundle"
|
||||
mkdir -p out && cp "$B/appimage/storebound_${V}_amd64.AppImage" "$B/deb/storebound_${V}_amd64.deb" out/
|
||||
chmod +x out/*.AppImage
|
||||
(cd /tmp && rm -rf squashfs-root && "$GITHUB_WORKSPACE/out/storebound_${V}_amd64.AppImage" --appimage-extract >/dev/null && squashfs-root/usr/bin/storebound-desktop --self-test)
|
||||
rm -rf /tmp/deb && dpkg-deb -x "out/storebound_${V}_amd64.deb" /tmp/deb && /tmp/deb/usr/bin/storebound-desktop --self-test
|
||||
dpkg-deb -I "out/storebound_${V}_amd64.deb" | sed -n '1,20p'
|
||||
- name: Start the app under Xvfb (AppImage and deb)
|
||||
# --self-test checks only the sidecar; this opens the real window (GTK, WebKitGTK, the bundled libraries of the
|
||||
# AppImage) on a virtual display. "sidecar started" in host.log means the webview ran the UI and called the host.
|
||||
run: |
|
||||
sudo apt-get install -y -qq --no-install-recommends xvfb xauth dbus-x11 >/dev/null
|
||||
V=$(bun tools/desktop-version.ts --print)
|
||||
smoke() { # <label> <executable>
|
||||
local d; d=$(mktemp -d); local log="$d/data/dev.storebound.desktop/logs/host.log"
|
||||
XDG_DATA_HOME="$d/data" XDG_CONFIG_HOME="$d/config" XDG_CACHE_HOME="$d/cache" \
|
||||
xvfb-run -a dbus-run-session -- "$2" >"$d/out.txt" 2>&1 &
|
||||
local pid=$! ok=0
|
||||
for i in $(seq 1 90); do
|
||||
if grep -q 'sidecar started' "$log" 2>/dev/null; then ok=1; break; fi
|
||||
kill -0 "$pid" 2>/dev/null || break
|
||||
sleep 1
|
||||
done
|
||||
sleep 3; kill -0 "$pid" 2>/dev/null || ok=0
|
||||
pkill -TERM -f storebound-desktop || true; wait "$pid" 2>/dev/null || true
|
||||
echo "--- $1 host.log"; cat "$log" 2>/dev/null || true
|
||||
local wv="$d/data/dev.storebound.desktop/logs/webview.log"
|
||||
if [ -f "$wv" ]; then echo "--- $1 webview.log"; cat "$wv"; grep -qE '^\S+ error ' "$wv" && ok=0; fi
|
||||
grep -qE '^\S+ error |panicked' "$log" 2>/dev/null && ok=0
|
||||
if [ "$ok" != 1 ]; then echo "--- $1 output"; tail -n 60 "$d/out.txt"; echo "$1: the window did not start the UI" >&2; return 1; fi
|
||||
echo "$1: window started, UI reached the sidecar"
|
||||
}
|
||||
smoke AppImage "$GITHUB_WORKSPACE/out/storebound_${V}_amd64.AppImage"
|
||||
smoke deb /tmp/deb/usr/bin/storebound-desktop
|
||||
- name: Privacy scan
|
||||
run: bun tools/privacy-scan.ts out/
|
||||
- name: Checksums
|
||||
run: (cd out && sha256sum * | tee SHA256SUMS)
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: storebound-desktop-linux
|
||||
path: out/
|
||||
retention-days: 14
|
||||
|
||||
windows:
|
||||
# only on a manual run with "windows" ticked (a tag push never sets it), so without a Windows runner it is skipped
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.windows }}
|
||||
# the Windows runner on the VDS ("windows" label, host mode): Rust (MSVC), Bun, 7-Zip, pwsh
|
||||
runs-on: windows
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CARGO_TARGET_DIR: C:\storebound-target
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install and check
|
||||
shell: pwsh
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
bun tools/desktop-version.ts
|
||||
bun tools/build-sidecars.ts --target bun-windows-x64
|
||||
- name: Build NSIS installer (per user, WebView2 bootstrapper, path-free)
|
||||
shell: pwsh
|
||||
working-directory: apps/desktop
|
||||
run: bun ../../tools/desktop-release.ts exec -- bunx tauri build --bundles nsis --ci
|
||||
- name: Install per user, self-test, uninstall
|
||||
shell: pwsh
|
||||
run: |
|
||||
$V = bun tools/desktop-version.ts --print
|
||||
New-Item -ItemType Directory -Force out | Out-Null
|
||||
Copy-Item "$env:CARGO_TARGET_DIR\release\bundle\nsis\storebound_${V}_x64-setup.exe" out\
|
||||
Start-Process -Wait -FilePath "out\storebound_${V}_x64-setup.exe" -ArgumentList '/S'
|
||||
$exe = "$env:LOCALAPPDATA\storebound\storebound-desktop.exe"
|
||||
if (-not (Test-Path $exe)) { throw "not installed per user at $exe" }
|
||||
& $exe --self-test | Tee-Object -Variable st
|
||||
if ($LASTEXITCODE -ne 0) { throw "self-test failed" }
|
||||
Start-Process -Wait -FilePath "$env:LOCALAPPDATA\storebound\uninstall.exe" -ArgumentList '/S'
|
||||
- name: Privacy scan (no .pdb, no paths)
|
||||
shell: pwsh
|
||||
run: bun tools/privacy-scan.ts out
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: storebound-desktop-windows
|
||||
path: out/
|
||||
retention-days: 14
|
||||
305
.forgejo/workflows/release.yml
Normal file
305
.forgejo/workflows/release.yml
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
# The release of storebound: one version tag (v0.8.0) -> one Forgejo release with the Linux and Windows installers.
|
||||
#
|
||||
# push of a tag v* checks (tag = package.json version, every version copy equal), the full CI, the
|
||||
# AppImage + deb (Linux runner, Docker) and the NSIS installer (Windows runner, VM), the
|
||||
# privacy scan of every artifact, then ONE release "storebound <version>" with short notes
|
||||
# (tools/forgejo-release.ts notes: the CHANGELOG summary, install line, links), the
|
||||
# installers and SHA256SUMS; older release entries are deleted afterwards (tags stay).
|
||||
# workflow_dispatch dry (default): the same jobs on this commit (or on `tag`), a DRAFT release test-<sha>
|
||||
# with the real files, read back, then deleted. dry off + tag: (re)make that tag's release.
|
||||
#
|
||||
# Not here: macOS. The DMG is built, signed and notarized on the owner's Mac and added to this release with
|
||||
# `bun tools/desktop-release.ts upload-forgejo --out <dir> --yes`; then `publish` (storebound.dev) and `npm publish`
|
||||
# (the owner's step). Order and details: docs/desktop-release.md. The updater signing key never reaches a runner.
|
||||
# Secrets (repo Settings > Actions > Secrets): RELEASE_TOKEN (token, scope write:repository: releases and assets),
|
||||
# FORBIDDEN_TERMS (the private terms list of the privacy scan; never printed), FORGEJO_ORIGIN (the server's IP: asset
|
||||
# uploads over 100 MB bypass Cloudflare).
|
||||
# Runners on the VDS (labels ubuntu-22.04 and windows): docs/desktop-release.md.
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Version tag to build (v0.8.0); empty = the selected branch's commit
|
||||
type: string
|
||||
default: ''
|
||||
dry:
|
||||
description: Dry run (a draft release test-<sha> with the real files, deleted at the end)
|
||||
type: boolean
|
||||
default: true
|
||||
windows:
|
||||
description: Build Windows too (always on for a tag push)
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
env:
|
||||
BUN_VERSION: '1.4.0'
|
||||
CARGO_TERM_COLOR: never
|
||||
CARGO_INCREMENTAL: '0'
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
ref: ${{ steps.mode.outputs.ref }}
|
||||
tag: ${{ steps.check.outputs.tag }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
dry: ${{ steps.mode.outputs.dry }}
|
||||
steps:
|
||||
- name: Mode
|
||||
id: mode
|
||||
env:
|
||||
EVENT: ${{ github.event_name }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
SHA: ${{ github.sha }}
|
||||
IN_TAG: ${{ inputs.tag }}
|
||||
IN_DRY: ${{ inputs.dry }}
|
||||
run: |
|
||||
# a boolean input arrives as true / false (API: also "on" / "1"); only an explicit no is a real release
|
||||
case "$(echo "${IN_DRY:-true}" | tr A-Z a-z)" in false|0|off|no) want_dry=false;; *) want_dry=true;; esac
|
||||
echo "inputs: tag='$IN_TAG' dry='$IN_DRY'"
|
||||
if [ "$EVENT" = push ]; then ref="refs/tags/$REF_NAME"; dry=false
|
||||
elif [ -n "$IN_TAG" ]; then ref="refs/tags/$IN_TAG"; dry=$want_dry
|
||||
else ref="$SHA"; dry=true
|
||||
[ "$want_dry" = true ] || { echo "a real release needs a tag (input tag), or push the tag" >&2; exit 1; }
|
||||
fi
|
||||
echo "ref=$ref" >> "$GITHUB_OUTPUT"; echo "dry=$dry" >> "$GITHUB_OUTPUT"
|
||||
echo "event=$EVENT ref=$ref dry=$dry"
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.mode.outputs.ref }}
|
||||
- name: Tag, package.json version and every version copy agree
|
||||
id: check
|
||||
env:
|
||||
TAG_IN: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }}
|
||||
DRY: ${{ steps.mode.outputs.dry }}
|
||||
run: |
|
||||
curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" >/dev/null
|
||||
export PATH="$HOME/.bun/bin:$PATH"
|
||||
bun tools/desktop-version.ts
|
||||
V=$(bun tools/desktop-version.ts --print)
|
||||
if [ -n "$TAG_IN" ] && [ "$TAG_IN" != "v$V" ]; then echo "tag $TAG_IN is not v$V (package.json): bun tools/desktop-version.ts --set <version>, commit, tag that commit" >&2; exit 1; fi
|
||||
grep -q "^## $V" CHANGELOG.md || { [ "$DRY" = true ] && echo "note: no CHANGELOG section for $V (the notes use Unreleased)"; } || { echo "CHANGELOG.md has no '## $V' section" >&2; exit 1; }
|
||||
if [ "$DRY" = true ]; then tag="test-$(git rev-parse --short=10 HEAD)"; else tag="$TAG_IN"; fi
|
||||
echo "version=$V" >> "$GITHUB_OUTPUT"; echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
echo "release $tag (version $V, dry=$DRY)"
|
||||
|
||||
ci:
|
||||
needs: prepare
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: full
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.ref }}
|
||||
- uses: ./.forgejo/actions/linux-setup
|
||||
with:
|
||||
cache-key: ci
|
||||
rust-components: clippy
|
||||
- uses: ./.forgejo/actions/ci-run
|
||||
|
||||
linux:
|
||||
needs: prepare
|
||||
# runner label: ubuntu-22.04:docker://ghcr.io/catthehacker/ubuntu:act-22.04 (glibc 2.35: the AppImage and the
|
||||
# deb then run on Ubuntu 22.04 and newer, Debian 12)
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
# the path-free build keeps the target folder outside every home folder
|
||||
CARGO_TARGET_DIR: /tmp/storebound-target
|
||||
# linuxdeploy (AppImage) runs without FUSE inside the container
|
||||
APPIMAGE_EXTRACT_AND_RUN: '1'
|
||||
# linuxdeploy must not strip: the storebound sidecar is a `bun build --compile` executable whose JS payload strip
|
||||
# removes (it then starts as plain bun). The Rust binary is already stripped by Cargo (profile.release strip).
|
||||
NO_STRIP: '1'
|
||||
# WebKitGTK's DMABUF renderer leaves the window blank on some GPU / VM setups; the app sets this itself at
|
||||
# start (src-tauri/src/lib.rs linux_webkit_env) unless the user did; set here too for anything run in CI
|
||||
WEBKIT_DISABLE_DMABUF_RENDERER: '1'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.ref }}
|
||||
- uses: ./.forgejo/actions/linux-setup
|
||||
with:
|
||||
cache-key: desktop
|
||||
packages: patchelf file squashfs-tools p7zip-full xz-utils
|
||||
- name: Install and check
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
bun tools/desktop-version.ts
|
||||
bun tools/build-sidecars.ts --target bun-linux-x64
|
||||
# linuxdeploy (AppImage) sets the rpath of every executable with its own patchelf, which corrupts the bun
|
||||
# sidecar (segfault, then ldd fails in the gtk plugin). With the rpath already set (Ubuntu's patchelf, which
|
||||
# keeps it working), linuxdeploy leaves the file as it is. The sidecar needs only libc, so the rpath is inert.
|
||||
patchelf --set-rpath '$ORIGIN/../lib' apps/desktop/src-tauri/binaries/storebound-x86_64-unknown-linux-gnu
|
||||
apps/desktop/src-tauri/binaries/storebound-x86_64-unknown-linux-gnu version
|
||||
- name: Build AppImage and deb (path-free)
|
||||
working-directory: apps/desktop
|
||||
run: bun ../../tools/desktop-release.ts exec -- bunx tauri build --bundles appimage,deb --ci
|
||||
- name: Self-test the exact artifacts
|
||||
run: |
|
||||
V=$(bun tools/desktop-version.ts --print)
|
||||
B="$CARGO_TARGET_DIR/release/bundle"
|
||||
mkdir -p out && cp "$B/appimage/storebound_${V}_amd64.AppImage" "$B/deb/storebound_${V}_amd64.deb" out/
|
||||
chmod +x out/*.AppImage
|
||||
(cd /tmp && rm -rf squashfs-root && "$GITHUB_WORKSPACE/out/storebound_${V}_amd64.AppImage" --appimage-extract >/dev/null && squashfs-root/usr/bin/storebound-desktop --self-test)
|
||||
rm -rf /tmp/deb && dpkg-deb -x "out/storebound_${V}_amd64.deb" /tmp/deb && /tmp/deb/usr/bin/storebound-desktop --self-test
|
||||
dpkg-deb -I "out/storebound_${V}_amd64.deb" | sed -n '1,20p'
|
||||
- name: Start the app under Xvfb (AppImage and deb)
|
||||
# --self-test checks only the sidecar; this opens the real window (GTK, WebKitGTK, the bundled libraries of the
|
||||
# AppImage) on a virtual display. "sidecar started" in host.log means the webview ran the UI and called the host.
|
||||
run: |
|
||||
sudo apt-get install -y -qq --no-install-recommends xvfb xauth dbus-x11 >/dev/null
|
||||
V=$(bun tools/desktop-version.ts --print)
|
||||
smoke() { # <label> <executable>
|
||||
local d; d=$(mktemp -d); local log="$d/data/dev.storebound.desktop/logs/host.log"
|
||||
XDG_DATA_HOME="$d/data" XDG_CONFIG_HOME="$d/config" XDG_CACHE_HOME="$d/cache" \
|
||||
xvfb-run -a dbus-run-session -- "$2" >"$d/out.txt" 2>&1 &
|
||||
local pid=$! ok=0
|
||||
for i in $(seq 1 90); do
|
||||
if grep -q 'sidecar started' "$log" 2>/dev/null; then ok=1; break; fi
|
||||
kill -0 "$pid" 2>/dev/null || break
|
||||
sleep 1
|
||||
done
|
||||
sleep 3; kill -0 "$pid" 2>/dev/null || ok=0
|
||||
pkill -TERM -f storebound-desktop || true; wait "$pid" 2>/dev/null || true
|
||||
echo "--- $1 host.log"; cat "$log" 2>/dev/null || true
|
||||
local wv="$d/data/dev.storebound.desktop/logs/webview.log"
|
||||
if [ -f "$wv" ]; then echo "--- $1 webview.log"; cat "$wv"; grep -qE '^\S+ error ' "$wv" && ok=0; fi
|
||||
grep -qE '^\S+ error |panicked' "$log" 2>/dev/null && ok=0
|
||||
if [ "$ok" != 1 ]; then echo "--- $1 output"; tail -n 60 "$d/out.txt"; echo "$1: the window did not start the UI" >&2; return 1; fi
|
||||
echo "$1: window started, UI reached the sidecar"
|
||||
}
|
||||
smoke AppImage "$GITHUB_WORKSPACE/out/storebound_${V}_amd64.AppImage"
|
||||
smoke deb /tmp/deb/usr/bin/storebound-desktop
|
||||
- name: Privacy scan
|
||||
run: bun tools/privacy-scan.ts out/
|
||||
- name: Checksums
|
||||
run: (cd out && sha256sum * | tee SHA256SUMS)
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: storebound-desktop-linux
|
||||
path: out/
|
||||
retention-days: 14
|
||||
|
||||
|
||||
windows:
|
||||
needs: prepare
|
||||
# a tag push always builds Windows; a manual run unless "windows" is unticked
|
||||
if: ${{ github.event_name == 'push' || inputs.windows }}
|
||||
# the Windows runner on the VDS ("windows" label, host mode): Rust (MSVC), Bun, 7-Zip, pwsh
|
||||
runs-on: windows
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CARGO_TARGET_DIR: C:\storebound-target
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.ref }}
|
||||
- name: Install and check
|
||||
shell: pwsh
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
bun tools/desktop-version.ts
|
||||
bun tools/build-sidecars.ts --target bun-windows-x64
|
||||
- name: Build NSIS installer (per user, WebView2 bootstrapper, path-free)
|
||||
shell: pwsh
|
||||
working-directory: apps/desktop
|
||||
run: bun ../../tools/desktop-release.ts exec -- bunx tauri build --bundles nsis --ci
|
||||
- name: Install per user, self-test, uninstall
|
||||
shell: pwsh
|
||||
run: |
|
||||
$V = bun tools/desktop-version.ts --print
|
||||
New-Item -ItemType Directory -Force out | Out-Null
|
||||
Copy-Item "$env:CARGO_TARGET_DIR\release\bundle\nsis\storebound_${V}_x64-setup.exe" out\
|
||||
Start-Process -Wait -FilePath "out\storebound_${V}_x64-setup.exe" -ArgumentList '/S'
|
||||
$exe = "$env:LOCALAPPDATA\storebound\storebound-desktop.exe"
|
||||
if (-not (Test-Path $exe)) { throw "not installed per user at $exe" }
|
||||
& $exe --self-test | Tee-Object -Variable st
|
||||
if ($LASTEXITCODE -ne 0) { throw "self-test failed" }
|
||||
Start-Process -Wait -FilePath "$env:LOCALAPPDATA\storebound\uninstall.exe" -ArgumentList '/S'
|
||||
- name: Privacy scan (no .pdb, no paths)
|
||||
shell: pwsh
|
||||
run: bun tools/privacy-scan.ts out
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: storebound-desktop-windows
|
||||
path: out/
|
||||
retention-days: 14
|
||||
|
||||
release:
|
||||
needs: [prepare, ci, linux, windows]
|
||||
# a real release needs every platform; a dry run may skip Windows
|
||||
if: ${{ always() && needs.prepare.result == 'success' && needs.ci.result == 'success' && needs.linux.result == 'success' && (needs.windows.result == 'success' || (needs.windows.result == 'skipped' && needs.prepare.outputs.dry == 'true')) }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
FORGEJO_URL: ${{ github.server_url }}
|
||||
# uploads over 100 MB go to the origin directly (Cloudflare refuses them): tools/forgejo-release.ts
|
||||
FORGEJO_ORIGIN: ${{ secrets.FORGEJO_ORIGIN }}
|
||||
TAG: ${{ needs.prepare.outputs.tag }}
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
DRY: ${{ needs.prepare.outputs.dry }}
|
||||
SHA: ${{ github.sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.ref }}
|
||||
- name: Tools
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq --no-install-recommends squashfs-tools p7zip-full xz-utils binutils unzip file >/dev/null
|
||||
curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" >/dev/null
|
||||
echo "$HOME/.bun/bin" >> "${FORGEJO_PATH:-$GITHUB_PATH}"
|
||||
[ -n "$FORGEJO_TOKEN" ] || { echo "secret RELEASE_TOKEN is missing (repo Settings > Actions > Secrets)" >&2; exit 1; }
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: storebound-desktop-linux
|
||||
path: dist/linux
|
||||
- uses: actions/download-artifact@v3
|
||||
if: ${{ needs.windows.result == 'success' }}
|
||||
with:
|
||||
name: storebound-desktop-windows
|
||||
path: dist/windows
|
||||
- name: Collect, privacy scan (private terms), checksums
|
||||
env:
|
||||
FORBIDDEN_TERMS: ${{ secrets.FORBIDDEN_TERMS }}
|
||||
run: |
|
||||
mkdir -p out
|
||||
find dist -type f ! -name SHA256SUMS -exec cp {} out/ \;
|
||||
for f in out/*; do case "$f" in *"_${VERSION}_"*) ;; *) echo "$f is not version $VERSION" >&2; exit 1;; esac; done
|
||||
[ -n "$FORBIDDEN_TERMS" ] || { [ "$DRY" = true ] && echo "note: secret FORBIDDEN_TERMS is missing: generic checks only"; } || { echo "secret FORBIDDEN_TERMS is missing" >&2; exit 1; }
|
||||
umask 077; printf '%s\n' "$FORBIDDEN_TERMS" > "$RUNNER_TEMP/terms.txt"
|
||||
STOREBOUND_FORBIDDEN_TERMS="$RUNNER_TEMP/terms.txt" bun tools/privacy-scan.ts out
|
||||
rm -f "$RUNNER_TEMP/terms.txt"
|
||||
(cd out && sha256sum * | tee ../SHA256SUMS.local)
|
||||
ls -la out
|
||||
- name: Notes
|
||||
run: |
|
||||
bun tools/forgejo-release.ts notes "$VERSION" --tag "$([ "$DRY" = true ] && echo "v$VERSION" || echo "$TAG")" | tee notes.md
|
||||
[ "$(wc -l < notes.md)" -le 8 ] || { echo "notes are longer than 8 lines" >&2; exit 1; }
|
||||
- name: Release (create or update, attach, read back)
|
||||
run: |
|
||||
if [ "$DRY" = true ]; then
|
||||
bun tools/forgejo-release.ts upsert --tag "$TAG" --target "$SHA" --title "$TAG (dry run, deleted at the end)" --notes-file notes.md --draft --yes out/*
|
||||
else
|
||||
bun tools/forgejo-release.ts upsert --tag "$TAG" --title "storebound $VERSION" --notes-file notes.md --yes out/*
|
||||
fi
|
||||
- name: Keep only the newest release (tags stay)
|
||||
if: ${{ needs.prepare.outputs.dry != 'true' }}
|
||||
run: bun tools/forgejo-release.ts prune --keep "$TAG" --yes
|
||||
- name: Dry run - what prune would delete
|
||||
if: ${{ needs.prepare.outputs.dry == 'true' }}
|
||||
run: |
|
||||
KEEP=$(curl -fsS -H "Authorization: token $FORGEJO_TOKEN" "$FORGEJO_URL/api/v1/repos/$GITHUB_REPOSITORY/releases?limit=1&draft=false" | bun -e 'const r=await Bun.stdin.json(); console.log(r[0]?.tag_name ?? "")')
|
||||
[ -z "$KEEP" ] || bun tools/forgejo-release.ts prune --keep "$KEEP"
|
||||
- name: Dry run - delete the test draft
|
||||
if: ${{ always() && needs.prepare.outputs.dry == 'true' }}
|
||||
run: bun tools/forgejo-release.ts delete --tag "$TAG" --yes
|
||||
Loading…
Add table
Add a link
Reference in a new issue