Files
inkwell/.forgejo/workflows/desktop.yml
T
bvandeusenandClaude Opus 5.5 535331c5b2
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 1s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Failing after 12s
CI & Build / integration (push) Failing after 27s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 4m25s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m28s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m19s
Desktop (Tauri) / Update manifest (push) Successful in 9s
Android / Kotlin + Rust (APK) (push) Canceled after 11m21s
tests: one fixture for the note grammar, run by the core, the server and the web
The checklist grammar and the #tag rule are implemented three times (derive.rs,
checklist.py/tags.py, markdown.ts), and the tag colour twice (colors.ts,
DerivedTint.kt). Only Rust and Kotlin had tests. core/testdata/grammar.json now
holds one set of cases (task lines, rendered items, tags, standalone-tag lifts
and the tint hashes), and every suite reads it.

- web: vitest, a dev dependency approved for #5166, with `npm test`.
  grammar.test.ts runs the fixture, and titles.test.ts pins #5165's palette fix.
- ci.yml runs the web tests in the job the image build needs. desktop.yml's
  verify job runs them too, because the installers embed this frontend and
  can't see ci.yml's verdict (rule 177).
- core: derive.rs reads the fixture. server: tests/test_grammar_fixture.py.
- Android keeps its hand-written tint values; its doc now points at the fixture.

The server is expected red here, on purpose. tags.py only takes a tag after
whitespace and lets it start with a digit or `_`, while the core (the
definition) takes any non-tag boundary and needs a letter. So `(#todo)` is a
label on the phone and plain text on the server. The fix follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:11:26 -04:00

535 lines
28 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
# The workspace checks, in their OWN job, so that BOTH publishing jobs can need
# them (rule 177: nothing publishes on red).
#
# They used to be steps inside `build`, which gated the Linux publish and nothing
# else. `windows` needed only `decide`, so on run 8411 clippy failed, the Linux job
# stopped, and the Windows installer built and published to the dev release in the
# same minute (#5184). An edge from each publisher to this job is the gate; a
# verdict inside a sibling job is only a report.
#
# Both publishers need it directly rather than through each other, so a Windows
# failure still never blocks the Linux bundles, and neither waits on the other's
# bundling. No `always()` or `continue-on-error` anywhere on this path: a skipped
# or failed verify must leave both publishers skipped.
verify:
name: Web tests, clippy, Rust tests and rustfmt
needs: [decide]
if: needs.decide.outputs.build == 'true'
runs-on: python-ci
container:
image: git.fabledsword.com/bvandeusen/ci-tauri:1.97
steps:
# No version is derived here, so the default shallow checkout is enough.
- uses: actions/checkout@v6
# tauri's generate_context! embeds the built frontend at compile time, so the
# frontend must exist before clippy or the tests can compile the desktop crate.
- name: Build the shared frontend
run: npm ci && npm run build
working-directory: frontend
# The web's unit tests, HERE as well as in ci.yml. The installers embed this
# frontend, and ci.yml's verdict is invisible to this workflow — run there only,
# a red web test would still let both installers publish (rule 177).
- name: Web unit tests
run: npm test
working-directory: frontend
# --locked on the FIRST cargo invocation is the lockfile gate: it fails the
# run if Cargo.toml and the committed Cargo.lock disagree, instead of silently
# re-resolving (issue 2102). Both publishing jobs need this job, so neither
# bundles a commit whose lockfile drifted.
#
# 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 either bundle
# build, so a fmt failure doesn't burn one.
- name: Rust format check
run: cargo fmt --all --check
build:
name: Tauri desktop (Linux)
needs: [decide, verify]
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 cargo compiles anything, not just at bundle time.
- name: Build the shared frontend
run: npm ci && npm run build
working-directory: frontend
# 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"
# The DISPLAY version, baked into the binary by `option_env!` (#3181).
# A different value for a different audience: this is the one a person
# quotes in a bug report, the key above is the one only a comparator
# sees. Exported rather than passed as a flag because the macro that
# reads it is in Rust source, not in Tauri's config.
INKWELL_DISPLAY_VERSION="$(sh ../../packaging/version.sh display desktop)"
export INKWELL_DISPLAY_VERSION
echo "Baking display version $INKWELL_DISPLAY_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).
# Stock action: it works on this forge since the runner moved to
# gitea/runner 3.x, which edits upload-artifact's client-side GHES refusal
# out of the action bundle (Scribe snippet #2271). Never @v3 — it uploads
# something Gitea stores but will never serve back (it returns artifacts only
# through the v4 API, which filters on content_encoding='application/zip').
# No continue-on-error: a swallowed upload failure is exactly how 110
# unreachable artifacts accumulated here unnoticed. Fail loudly instead.
- name: Upload bundles
uses: actions/upload-artifact@v7
with:
name: inkwell-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) channel=stable; RELEASE_PRERELEASE=false ;;
*) channel=dev; RELEASE_PRERELEASE=true ;;
esac
# The channel's release TAG, not its name: `dev` publishes on `dev-rolling`
# (packaging/channel-tag.sh). A tag named `dev` shadowed the branch.
RELEASE_TAG="$(sh packaging/channel-tag.sh "$channel")"
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, verify]
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 own lockfile check (`verify` has already run `cargo clippy
# --locked` for the workspace). 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"
# The DISPLAY version, baked into the binary by `option_env!` (#3181).
# A different value for a different audience: this is the one a person
# quotes in a bug report, the key above is the one only a comparator
# sees. Exported rather than passed as a flag because the macro that
# reads it is in Rust source, not in Tauri's config.
INKWELL_DISPLAY_VERSION="$(sh ../../packaging/version.sh display desktop)"
export INKWELL_DISPLAY_VERSION
echo "Baking display version $INKWELL_DISPLAY_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
# Stock action — see the Linux job's Upload bundles step for why.
- name: Upload installer
uses: actions/upload-artifact@v7
with:
name: inkwell-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) channel=stable; RELEASE_PRERELEASE=false ;;
*) channel=dev; RELEASE_PRERELEASE=true ;;
esac
# The channel's release TAG, not its name: `dev` publishes on `dev-rolling`
# (packaging/channel-tag.sh). A tag named `dev` shadowed the branch.
RELEASE_TAG="$(sh packaging/channel-tag.sh "$channel")"
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)"
# The version a PERSON reads, published beside the manifest as
# `inkwell-desktop.json`. The image build reads it to describe the
# bundles it bakes in (packaging/fetch-clients.sh) without re-deriving
# anything from its own checkout — which would be a different commit
# whenever the desktop did not rebuild.
display="$(sh packaging/version.sh display 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) RELEASE_TAG="$(sh packaging/channel-tag.sh stable)"
export RELEASE_NOTES="Stable build from ${GITHUB_SHA}" ;;
*) RELEASE_TAG="$(sh packaging/channel-tag.sh dev)"
# TEMPORARY one-shot bridge (Scribe #2184). Desktop apps installed
# before the rename have .../download/dev/latest.json compiled in,
# so the manifest is also written to the old `dev` release; its
# URLs name dev-rolling assets, so those apps update once into a
# build that reads the new tag. Delete this line together with the
# old `dev` release and tag.
export BRIDGE_TAG=dev
export RELEASE_NOTES="Development build from ${GITHUB_SHA}" ;;
esac
# Assigned bare above so a failing channel-tag.sh fails this step;
# `export X="$(...)"` would swallow its exit status.
export RELEASE_TAG
export PRUNE_OLD_ASSETS=true
APP_VERSION="$version" DISPLAY_VERSION="$display" \
bash desktop/packaging/write-manifest.sh