Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 18s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m0s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m17s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 8m4s
Step 7 of M314, the last one. Rule 22 — the old path comes out completely. ## A release stops building `desktop.yml` no longer triggers on `v*`, and its two `Publish release` steps are gone. `ci.yml` lost its tag trigger in step 6. So a tag now reaches exactly one lane: the new `release.yml`, which builds nothing. That is not a simplification for its own sake. The merge to `main` already published everything a user can receive — `:latest` + `:<sha>`, both channel feeds, the updater manifest. A tag rebuilding that source produces identical artifacts under identical names and re-pushes `:<sha>` with different bytes, which rule 145 forbids even when they match. ## So what a release is FOR The changelog (note 3127 §5). Two halves to "what am I running", and the version answers only the first: which build is this (the footer, /api/config, the APK's versionName) and what is in it that was not in the one I ran last month (nothing, until now). `packaging/release-notes.sh` derives it from git rather than a hand-maintained CHANGELOG, which drifts into recording what someone MEANT to ship. Capped at 60 entries with the omitted count stated — the first dated release spans 181 commits since `v0.1.0`, and a truncated list that does not say it is truncated is a lie. It publishes through `publish-release.sh` rather than making its own API calls, for the create-or-PATCH-on-409 path: a fixed-tag release that only ever POSTs keeps whatever body its first run wrote, which is #2182, and reimplementing that correctly in a second place is how it comes back. ## Retired `MANIFEST_TAG` and the whole branch behind it. It let the manifest live on a `stable` pointer release while the bundles sat on a versioned one — a split step 3 removed when `stable` started holding its own bundles. Nothing had passed it since; a parameter that can only ever receive its own default is a branch nobody exercises and a comment that goes stale, and its stale text was still telling readers the installable builds live on the versioned releases. `desktop/src-tauri/Cargo.toml`'s version and `thoughtsync/__init__.py`'s both now say out loud that they are not shipped values. The Cargo one carries the history worth keeping: the old scheme took its base from that line, so `0.2.<run>` on dev outranked a bare `0.2.0` on main, and the remedy was "remember to bump the minor before tagging" — documented in a comment, enforced nowhere. #2183 is what that looked like in the field. **That ritual is now formally dead**, and this is the deliberate act of killing it rather than a side effect. ## Still there on purpose `install.sh`'s transitional stable fallback. It cannot go until `main` has published to `stable` at least once, and that is gated on an operator request. Removing it now would break the DEFAULT install channel. #3147 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
464 lines
24 KiB
YAML
464 lines
24 KiB
YAML
# Tauri desktop (Linux) build — SEPARATE from ci.yml on purpose: a Rust + AppImage
|
|
# build that shouldn't run on server-only pushes. Produces the .deb and .AppImage.
|
|
#
|
|
# It DOES run on frontend changes. tauri's generate_context! embeds the built
|
|
# frontend in the binary, so a frontend commit that never triggers this ships to
|
|
# the web and silently never reaches the desktop app — and desktop, web and Android
|
|
# are peer surfaces held to one quality bar, not a primary and its fallbacks. The
|
|
# filter was once narrowed to the adapter/bridge directories against a "~20-40 min"
|
|
# build; measured runs are 4-5 minutes, so the cost that justified the narrowing
|
|
# isn't there.
|
|
#
|
|
# Toolchain comes from the ci-tauri image (Rust + Node + WebKitGTK 4.1 + tauri-cli);
|
|
# runs-on is just a registered scheduling label (Label Model B), not a per-purpose
|
|
# runner. The frontend is built here because tauri's generate_context! embeds it.
|
|
name: Desktop (Tauri)
|
|
|
|
on:
|
|
push:
|
|
# NO `paths:` FILTER. It was a second, independent statement of this artifact's
|
|
# file set, hand-kept beside the one in `packaging/version.sh`, and it drifted
|
|
# from it within a day (85ead4d). The `decide` job below reads the real set and
|
|
# skips in seconds when nothing moved — one definition, one reader (§3).
|
|
#
|
|
# The cost is that this workflow starts on every push rather than on a matching
|
|
# one. That is a ~15s container for a decision, against a lane that cannot
|
|
# silently fail to run.
|
|
branches: [dev, main]
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: desktop-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
# write (not read) so the tag build can publish a Release with the bundles
|
|
# attached (the "Publish release" step). Read is enough for dev/main builds,
|
|
# but the token scope is per-workflow, so it's set once here.
|
|
contents: write
|
|
|
|
jobs:
|
|
# Does anything need building at all?
|
|
#
|
|
# ONE reader of ONE definition — the file sets in `packaging/version.sh` — replacing
|
|
# the `paths:` filters that used to state the same fact a second time. They drifted
|
|
# from it within a day: `packaging/` was added to the sets and not to the filters,
|
|
# so the commit fixing a derivation bug never ran on the two lanes it fixed
|
|
# (85ead4d). Note 3127 §3 warns about exactly that duplication.
|
|
#
|
|
# THE GUARD RUNS HERE, so it runs on every path INCLUDING the skip one (§6.3).
|
|
# Skipping because "the channel already serves this version" is indistinguishable
|
|
# from "we derived a stale value that happens to match" unless something checks.
|
|
decide:
|
|
name: Build, or is the channel already serving this?
|
|
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main'
|
|
runs-on: python-ci
|
|
container:
|
|
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
|
outputs:
|
|
build: ${{ steps.d.outputs.build }}
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
# Derives a version — depth-1 is silently wrong (§6.1).
|
|
fetch-depth: 0
|
|
|
|
- name: Decide
|
|
id: d
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
run: |
|
|
case "$GITHUB_REF_NAME" in
|
|
main) channel=stable ;;
|
|
*) channel=dev ;;
|
|
esac
|
|
sh packaging/guard-forward.sh desktop "$channel"
|
|
echo "build=$(sh packaging/should-build.sh desktop "$channel")" >> $GITHUB_OUTPUT
|
|
|
|
build:
|
|
name: Tauri desktop (Linux)
|
|
needs: [decide]
|
|
if: needs.decide.outputs.build == 'true'
|
|
runs-on: python-ci
|
|
container:
|
|
image: git.fabledsword.com/bvandeusen/ci-tauri:1.97
|
|
env:
|
|
# AppImage tooling (linuxdeploy) FUSE-mounts itself by default; CI containers
|
|
# have no /dev/fuse, so tell it to extract-and-run instead. Without this the
|
|
# AppImage bundle step fails with a FUSE error.
|
|
APPIMAGE_EXTRACT_AND_RUN: "1"
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
# DERIVES A VERSION -> needs the whole history. A depth-1 clone sees one
|
|
# commit and `git log -- <paths>` produces a too-LOW value, silently, with
|
|
# the lane green — note 3127 §6.1, and the direction you cannot recover
|
|
# from. `packaging/version.sh` fails loudly on an empty result rather than
|
|
# emitting something plausible, which is what turns this into a red lane
|
|
# if it is ever dropped.
|
|
fetch-depth: 0
|
|
|
|
# tauri's generate_context! embeds the built frontend at compile time, so the
|
|
# frontend must exist before any cargo compile (clippy/test/build), not just
|
|
# at bundle time.
|
|
- name: Build the shared frontend
|
|
run: npm ci && npm run build
|
|
working-directory: frontend
|
|
|
|
# --locked on the FIRST cargo invocation of the job is the lockfile gate: it
|
|
# fails the run if Cargo.toml and the committed Cargo.lock disagree, instead
|
|
# of silently re-resolving. Everything after it in this job then compiles the
|
|
# exact versions recorded in the lockfile, so the flag isn't repeated on the
|
|
# bundle build (issue 2102).
|
|
#
|
|
# Run from the REPO ROOT with --workspace, not from desktop/src-tauri.
|
|
#
|
|
# These three steps used to run inside the desktop crate, which was right when
|
|
# it was the only Rust in the repo. After the core was extracted (M12 step 1)
|
|
# it silently stopped being right: cargo scoped to the desktop PACKAGE, so the
|
|
# core's 89 tests stopped running and nothing lints the Android uniffi shim at
|
|
# all. Both crates are dependencies of the desktop, so they still COMPILED —
|
|
# which is exactly why the gap was invisible, and why a green run kept meaning
|
|
# less than it looked like it meant.
|
|
- name: Clippy
|
|
run: cargo clippy --locked --workspace --all-targets -- -D warnings
|
|
|
|
- name: Test
|
|
run: cargo test --locked --workspace
|
|
|
|
# Deliberately AFTER clippy + test, not before.
|
|
#
|
|
# It's the cheapest check, so fail-fast ordering would normally put it first —
|
|
# but there is no Rust toolchain on the workstation (the desktop lane is
|
|
# verified entirely here), so a formatting nit failing first SKIPS clippy and
|
|
# the tests, and one CI cycle teaches nothing but whitespace. Running it here
|
|
# means every push reports its real problems too. Still before the ~20-40 min
|
|
# bundle build, so a fmt failure doesn't burn that.
|
|
- name: Rust format check
|
|
run: cargo fmt --all --check
|
|
|
|
# Frontend already built above; skip the beforeBuildCommand rebuild.
|
|
#
|
|
# createUpdaterArtifacts is applied only when a signing key exists (M10.9):
|
|
# tauri FAILS the build if it's asked to produce updater artifacts with no key,
|
|
# so making it conditional is what lets the pipeline stay green before the
|
|
# operator has added the secret. With the key present, each bundle gets a
|
|
# `.sig` beside it — the file the updater actually verifies against.
|
|
- name: Tauri build (deb + AppImage)
|
|
env:
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
run: |
|
|
updater='{}'
|
|
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
|
echo "Signing key present — producing updater artifacts."
|
|
updater='{"bundle":{"createUpdaterArtifacts":true}}'
|
|
else
|
|
echo "No TAURI_SIGNING_PRIVATE_KEY — building unsigned, no updater artifacts."
|
|
fi
|
|
# The ORDERING KEY, not the display version: this string is what Tauri's
|
|
# updater parses as semver, and what it stamps into bundle FILENAMES that
|
|
# `write-manifest.sh` then selects on. The human-readable version is a
|
|
# separate value and arrives with the UI that shows it (#3181).
|
|
version="$(sh ../../packaging/version.sh key desktop)"
|
|
echo "Building desktop ordering key $version"
|
|
cargo tauri build \
|
|
--config '{"build":{"beforeBuildCommand":""}}' \
|
|
--config "{\"version\":\"$version\"}" \
|
|
--config "$updater"
|
|
working-directory: desktop/src-tauri
|
|
|
|
# Tauri's AppImage bundles the build host's graphics/display libs
|
|
# (libEGL/libGL/libdrm/libgbm/libwayland-*), which clash with end-user GPU
|
|
# drivers and abort to a black window (EGL_BAD_PARAMETER, issue 2021).
|
|
# Strip that host-coupled stack so the app uses the running system's
|
|
# graphics libs; webkit/gtk stay bundled. Runs from the repo root (the
|
|
# script resolves its own paths), overwriting the AppImage in place.
|
|
- name: De-bundle AppImage graphics libraries
|
|
run: bash desktop/packaging/appimage/debundle-graphics.sh
|
|
|
|
# MUST run after de-bundling, not before. The step above DELETES the AppImage
|
|
# and repackages it, so the signature tauri produced during the build now
|
|
# describes a file that no longer exists. Publishing that stale .sig would make
|
|
# every Linux update fail verification — and the error names a signature
|
|
# mismatch, which points nowhere near "a later build step rewrote the file".
|
|
# Windows needs no equivalent: nothing post-processes the NSIS installer.
|
|
- name: Re-sign the de-bundled AppImage
|
|
env:
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
run: |
|
|
if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
|
echo "No signing key — the build produced no signature to replace."
|
|
exit 0
|
|
fi
|
|
appimage="$(find target/release/bundle/appimage -name '*.AppImage' -type f | head -1)"
|
|
[ -n "$appimage" ] || { echo "ERROR: no AppImage found to re-sign" >&2; exit 1; }
|
|
rm -f "$appimage.sig"
|
|
cargo tauri signer sign "$appimage"
|
|
[ -s "$appimage.sig" ] || { echo "ERROR: re-signing produced no .sig" >&2; exit 1; }
|
|
echo "Re-signed $(basename "$appimage")"
|
|
|
|
# install.sh hands the .deb to every Debian/Ubuntu user, so the package's
|
|
# Depends must be right BEFORE a release exists. Prints the generated
|
|
# control file and cross-checks it against what the ELF actually needs
|
|
# (dpkg-shlibdeps).
|
|
#
|
|
# We deliberately do NOT set bundle.linux.deb.depends: run 2872 showed
|
|
# tauri already infers exactly libwebkit2gtk-4.1-0 + libgtk-3-0, so
|
|
# declaring them again only produced a control file listing each twice.
|
|
# This step is the guard instead — if tauri's inference ever stops
|
|
# covering what the binary links, the build fails here.
|
|
- name: Verify the .deb
|
|
run: bash desktop/packaging/deb/verify.sh
|
|
|
|
# Repackage the binary just built into a native pacman package, so Arch /
|
|
# CachyOS gets a real native install from install.sh instead of the AppImage
|
|
# fallback — without a second Rust build or an Arch CI image. Safe because
|
|
# nothing is bundled: the binary resolves webkit/gtk by soname, which is
|
|
# identical across the two distros. Can't be pacman-tested here (Debian
|
|
# runner), so the step logs .PKGINFO + the full file listing for audit.
|
|
- name: Package for Arch (pacman)
|
|
run: bash desktop/packaging/arch/package-prebuilt.sh
|
|
|
|
# Make the built .deb + .AppImage downloadable from the run (for hand-testing).
|
|
# Mirrored action, never actions/upload-artifact: @v4+ throws
|
|
# GHESNotSupportedError on the hostname before it connects, and @v3 uploads
|
|
# something Gitea stores but will never serve back (it returns artifacts only
|
|
# through the v4 API, which filters on content_encoding='application/zip').
|
|
# Pinned by SHA — the mirror auto-syncs, so a moved upstream tag would
|
|
# silently change what runs. See Scribe issues 2255 / 2270.
|
|
# No continue-on-error: a swallowed upload failure is exactly how 110
|
|
# unreachable artifacts accumulated here unnoticed. Fail loudly instead.
|
|
- name: Upload bundles
|
|
uses: https://git.fabledsword.com/bvandeusen/upload-artifact@cb8afe72b42edc798abfb8fcb556cf660d894245
|
|
with:
|
|
name: thoughtsync-linux
|
|
path: |
|
|
target/release/bundle/appimage/*.AppImage
|
|
target/release/bundle/deb/*.deb
|
|
target/release/bundle/arch/*.pkg.tar.*
|
|
# error, not warn: a build that bundles nothing should report as a
|
|
# failure, not as a green run with an empty artifact.
|
|
if-no-files-found: error
|
|
|
|
# The rolling channel for this branch: `dev` from dev, `stable` from main. Both
|
|
# are releases whose tag never moves, so the updater has a permanent URL to
|
|
# read — Forgejo has no /releases/latest/download/<asset> route, so "newest"
|
|
# cannot be named in a URL.
|
|
#
|
|
# MAIN PUBLISHING HERE is what makes a `v*` tag optional (note 3127 §0). Until
|
|
# M314 step 3 this job built on main and published nothing, so the stable
|
|
# channel moved only when somebody cut a tag — that section's diagnostic
|
|
# failing outright: main publishing was not sufficient for a user to receive
|
|
# the build.
|
|
#
|
|
# Gated on the signing key INSIDE the script rather than with an `if:`, because
|
|
# the secrets context isn't reliably available to step conditions. Publishing
|
|
# bundles the app would then refuse to verify is worse than publishing nothing:
|
|
# it looks like a working feed.
|
|
- name: Publish to the channel for this branch
|
|
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main'
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
run: |
|
|
if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
|
echo "No TAURI_SIGNING_PRIVATE_KEY — skipping the channel publish."
|
|
exit 0
|
|
fi
|
|
# POSIX `case`, not bash `[[ ]]` — these run under busybox sh (rule 81).
|
|
# `prerelease` is true for dev so it does not read as a supported build,
|
|
# and false for stable, which is the real thing.
|
|
case "$GITHUB_REF_NAME" in
|
|
main) RELEASE_TAG=stable; RELEASE_PRERELEASE=false ;;
|
|
*) RELEASE_TAG=dev; RELEASE_PRERELEASE=true ;;
|
|
esac
|
|
export RELEASE_TAG RELEASE_PRERELEASE
|
|
echo "Publishing to the $RELEASE_TAG channel."
|
|
bash desktop/packaging/publish-release.sh
|
|
|
|
# Windows installer, CROSS-COMPILED from Linux — there is no Windows build host.
|
|
# A Windows container can't run on a Linux host (containers share the host
|
|
# kernel), so cross-compiling is the only route without Windows hardware:
|
|
# cargo-xwin + LLVM's lld-link + makensis are Linux programs that emit Windows
|
|
# PE output. That toolchain is why this needs its own image rather than ci-tauri.
|
|
#
|
|
# NSIS only. `.msi` needs WiX v3, which is a Windows program — Tauri: ".msi
|
|
# installers can only be created on Windows". It returns if a Windows node does.
|
|
#
|
|
# A separate job, so a Windows-side failure never blocks the Linux artifacts that
|
|
# are the primary product today. Tauri calls this path "not tested as much" and a
|
|
# last resort, and nothing here can LAUNCH a Windows binary — green means it
|
|
# built, not that it runs. A real-machine check stays mandatory before trusting it.
|
|
windows:
|
|
name: Windows installer (cross-compiled)
|
|
needs: [decide]
|
|
if: needs.decide.outputs.build == 'true'
|
|
runs-on: python-ci
|
|
container:
|
|
image: git.fabledsword.com/bvandeusen/ci-tauri-win:1.97
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
# DERIVES A VERSION -> needs the whole history. A depth-1 clone sees one
|
|
# commit and `git log -- <paths>` produces a too-LOW value, silently, with
|
|
# the lane green — note 3127 §6.1, and the direction you cannot recover
|
|
# from. `packaging/version.sh` fails loudly on an empty result rather than
|
|
# emitting something plausible, which is what turns this into a red lane
|
|
# if it is ever dropped.
|
|
fetch-depth: 0
|
|
|
|
# Same reason as the Linux job: generate_context! embeds the built frontend
|
|
# at compile time, so it must exist before cargo runs.
|
|
- name: Build the shared frontend
|
|
run: npm ci && npm run build
|
|
working-directory: frontend
|
|
|
|
# tauri-build generates a Windows Resource file and needs `icons/icon.ico`,
|
|
# which the repo doesn't carry — only the PNG set the Linux bundles use.
|
|
# Generating it from the committed 1024px source keeps one icon of record
|
|
# instead of a hand-made .ico that could silently drift from the brand art.
|
|
# Linux doesn't need this step, which is why it lives here and not in `build`.
|
|
- name: Generate the Windows icon set
|
|
run: cargo tauri icon app-icon.png
|
|
working-directory: desktop/src-tauri
|
|
|
|
# This lane's lockfile gate (the Linux job gets it from `cargo clippy
|
|
# --locked`). It has to be its own step here because the build is this job's
|
|
# only crate-graph command, and discovering the drift 30 minutes into a
|
|
# cross-compile is the expensive way to learn it. Fetching for the Windows
|
|
# target also pre-warms exactly the crates the build will want.
|
|
- name: Verify the lockfile and fetch dependencies
|
|
run: cargo fetch --locked --target x86_64-pc-windows-msvc
|
|
working-directory: desktop/src-tauri
|
|
|
|
# --runner cargo-xwin swaps cargo for the cross-compiling driver (it supplies
|
|
# the MSVC CRT/SDK, pre-warmed into the image, and links with lld-link).
|
|
# Frontend already built above; skip the beforeBuildCommand rebuild.
|
|
- name: Tauri build (NSIS installer)
|
|
env:
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
run: |
|
|
# The ORDERING KEY, not the display version: this string is what Tauri's
|
|
# updater parses as semver, and what it stamps into bundle FILENAMES that
|
|
# `write-manifest.sh` then selects on. The human-readable version is a
|
|
# separate value and arrives with the UI that shows it (#3181).
|
|
version="$(sh ../../packaging/version.sh key desktop)"
|
|
echo "Building desktop ordering key $version"
|
|
updater='{}'
|
|
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
|
updater='{"bundle":{"createUpdaterArtifacts":true}}'
|
|
fi
|
|
cargo tauri build \
|
|
--runner cargo-xwin \
|
|
--target x86_64-pc-windows-msvc \
|
|
--bundles nsis \
|
|
--config '{"build":{"beforeBuildCommand":""}}' \
|
|
--config "{\"version\":\"$version\"}" \
|
|
--config "$updater"
|
|
working-directory: desktop/src-tauri
|
|
|
|
# Mirrored action, never actions/upload-artifact — see the Linux job's
|
|
# Upload bundles step for the full reasoning. Pinned by SHA because the
|
|
# mirror auto-syncs.
|
|
- name: Upload installer
|
|
uses: https://git.fabledsword.com/bvandeusen/upload-artifact@cb8afe72b42edc798abfb8fcb556cf660d894245
|
|
with:
|
|
name: thoughtsync-windows
|
|
path: target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
|
|
if-no-files-found: error
|
|
|
|
# The rolling channel for this branch: `dev` from dev, `stable` from main. Both
|
|
# are releases whose tag never moves, so the updater has a permanent URL to
|
|
# read — Forgejo has no /releases/latest/download/<asset> route, so "newest"
|
|
# cannot be named in a URL.
|
|
#
|
|
# MAIN PUBLISHING HERE is what makes a `v*` tag optional (note 3127 §0). Until
|
|
# M314 step 3 this job built on main and published nothing, so the stable
|
|
# channel moved only when somebody cut a tag — that section's diagnostic
|
|
# failing outright: main publishing was not sufficient for a user to receive
|
|
# the build.
|
|
#
|
|
# Gated on the signing key INSIDE the script rather than with an `if:`, because
|
|
# the secrets context isn't reliably available to step conditions. Publishing
|
|
# bundles the app would then refuse to verify is worse than publishing nothing:
|
|
# it looks like a working feed.
|
|
- name: Publish to the channel for this branch
|
|
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main'
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
run: |
|
|
if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
|
echo "No TAURI_SIGNING_PRIVATE_KEY — skipping the channel publish."
|
|
exit 0
|
|
fi
|
|
# POSIX `case`, not bash `[[ ]]` — these run under busybox sh (rule 81).
|
|
# `prerelease` is true for dev so it does not read as a supported build,
|
|
# and false for stable, which is the real thing.
|
|
case "$GITHUB_REF_NAME" in
|
|
main) RELEASE_TAG=stable; RELEASE_PRERELEASE=false ;;
|
|
*) RELEASE_TAG=dev; RELEASE_PRERELEASE=true ;;
|
|
esac
|
|
export RELEASE_TAG RELEASE_PRERELEASE
|
|
echo "Publishing to the $RELEASE_TAG channel."
|
|
bash desktop/packaging/publish-release.sh
|
|
|
|
# The updater manifest, written AFTER both bundle jobs — they run in separate
|
|
# workspaces and neither can see the other's output, but one latest.json has to
|
|
# describe both platforms. Building it inside either job would silently omit the
|
|
# other, and a missing platform reads to a user as "no update available" rather
|
|
# than as a broken feed.
|
|
#
|
|
# Reads what actually landed on the channel release, so it can never advertise a
|
|
# bundle that failed to upload.
|
|
manifest:
|
|
name: Update manifest
|
|
needs: [build, windows]
|
|
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main'
|
|
runs-on: python-ci
|
|
container:
|
|
image: git.fabledsword.com/bvandeusen/ci-tauri:1.97
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
# DERIVES A VERSION -> needs the whole history. A depth-1 clone sees one
|
|
# commit and `git log -- <paths>` produces a too-LOW value, silently, with
|
|
# the lane green — note 3127 §6.1, and the direction you cannot recover
|
|
# from. `packaging/version.sh` fails loudly on an empty result rather than
|
|
# emitting something plausible, which is what turns this into a red lane
|
|
# if it is ever dropped.
|
|
fetch-depth: 0
|
|
|
|
- name: Write and publish latest.json
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
run: |
|
|
if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
|
echo "No TAURI_SIGNING_PRIVATE_KEY — nothing was signed, so there is no"
|
|
echo "manifest to write. Add the secret to enable in-app updates."
|
|
exit 0
|
|
fi
|
|
# The SAME helper AND the same request the bundles were built with — a
|
|
# second derivation here could drift, and a manifest whose version doesn't
|
|
# match the binary it points at is an updater that never settles. It must
|
|
# be `key`: this value is matched against bundle filenames.
|
|
version="$(sh packaging/version.sh key desktop)"
|
|
# Both channels are rolling: the manifest lands on the same release that
|
|
# holds the bundles, and the previous build's bundles are dropped once it
|
|
# points at this one. Nothing can reach them, and they are ~100 MB a push.
|
|
#
|
|
# No tag arm any more. A `v*` tag does not reach this workflow at all — it
|
|
# triggers release.yml, which writes a changelog and builds nothing.
|
|
case "${GITHUB_REF_NAME}" in
|
|
main) export RELEASE_TAG=stable
|
|
export RELEASE_NOTES="Stable build from ${GITHUB_SHA}" ;;
|
|
*) export RELEASE_TAG=dev
|
|
export RELEASE_NOTES="Development build from ${GITHUB_SHA}" ;;
|
|
esac
|
|
export PRUNE_OLD_ASSETS=true
|
|
APP_VERSION="$version" bash desktop/packaging/write-manifest.sh
|