Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Version updates. Alerts and security updates are repository settings and need nothing
# here; this file is only the scheduled "bump things that are behind" pull requests.
#
# Two settings below are load-bearing rather than taste, and both are explained where they
# appear: `target-branch`, because the default would aim at the release branch, and the
# `groups` blocks, because an ungrouped cargo update is expensive in a way that is not
# obvious from the diff.
version: 2

updates:
# The workflows themselves. This is the cheapest of the three and arguably the most
# useful: it is what would have flagged actions-rs/toolchain being archived in 2023,
# which test.yml had to notice by hand.
- package-ecosystem: "github-actions"
directory: "/"
# Dependabot aims at the default branch unless told otherwise, and the default branch
# here is `main` - where a merge *is* the release decision. A dependency pull request
# merged there would start release.yml. It would not actually release (the tag for the
# current version already exists, so `prepare` decides should_release=false and stops),
# but it would run the whole suite to reach that conclusion, and it is the wrong branch
# for how this repository works. Everything lands on `develop` and is promoted.
target-branch: "develop"
schedule:
interval: "monthly"
open-pull-requests-limit: 3
groups:
# One pull request for everything routine. Majors stay individual - those are the
# ones that need reading.
actions-minor-and-patch:
update-types: ["minor", "patch"]

- package-ecosystem: "npm"
directory: "/"
target-branch: "develop"
schedule:
interval: "monthly"
open-pull-requests-limit: 3
groups:
npm-minor-and-patch:
update-types: ["minor", "patch"]

# The Rust crate. Grouping matters most here, and the reason is the Rust cache: its key
# includes a hash of Cargo.lock, so *any* crate bump is a cache miss, and the pull request
# then compiles all 799 crates cold on three operating systems. One grouped pull request a
# month pays that once; a pull request per crate would pay it over and over.
#
# The `drag` dependency is a git dependency (crabnebula-dev/drag-rs) and Dependabot does
# not update cargo git sources, so that one stays manual.
- package-ecosystem: "cargo"
directory: "/src-tauri"
target-branch: "develop"
schedule:
interval: "monthly"
open-pull-requests-limit: 3
groups:
cargo-minor-and-patch:
update-types: ["minor", "patch"]
76 changes: 76 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: CodeQL

# Static analysis, deliberately kept off the release path.
#
# This is a separate workflow, not a job in test.yml and not called by release.yml, and it
# must stay that way. test.yml is the gate that decides whether a merge to main ships a
# binary; a finding from a scanner that updates its query pack on its own schedule is not a
# reason to refuse to release. The same argument the clippy and fmt steps already make in
# test.yml, for the same reason - so do not add these jobs to the repository's required
# status checks.
#
# All three languages analyse without a build. Rust analysis went generally available in
# October 2025 as a buildless extractor, which is what makes this affordable here: it reads
# src-tauri without compiling any of its 799 crates.
on:
pull_request:
branches: [main, develop]
# The default branch needs analysing on its own for the Security tab to have a baseline to
# compare pull requests against, and nothing pushes to main except a release. A schedule
# runs against the default branch, which covers it without putting a second workflow in
# front of every merge.
schedule:
- cron: '41 4 * * 1'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.sha }}
cancel-in-progress: true

jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
# The only one that is not read-only: this is what lets the run upload its results to
# the Security tab. Everything else is narrowed to read.
security-events: write
actions: read
contents: read

strategy:
# One language failing should still leave the other two reporting.
fail-fast: false
matrix:
include:
# The workflows themselves, and the most interesting of the three in this
# repository: release.yml interpolates ${{ }} expressions directly into `run:`
# shell blocks in several places, which is exactly the shape this pack looks at.
- language: actions
build-mode: none

# Covers src/lib/**/*.ts, which is where the logic lives. Note what it does *not*
# cover: CodeQL's extractor does not parse .svelte files, so the script blocks
# inside components are invisible to it. Worth remembering before reading a clean
# result as "the frontend is clean".
- language: javascript-typescript
build-mode: none

- language: rust
build-mode: none

steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}

- name: Analyze
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"
64 changes: 52 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -237,20 +237,32 @@ jobs:
# succeeds and `cleanup` deletes the draft otherwise, so two more legs are two more ways
# for a release to fail wholesale. That is the intended trade - a release that ships half
# the platforms it announces is worse than one that does not ship.
#
# `cache_key` names each leg's Rust cache. rust-cache keys on the job name, the runner's
# os and arch, the rustc version and the lockfile - and on none of the matrix values, so
# the two macOS legs would otherwise compute one key between them and each would restore
# the other architecture's artifacts. The other four are already distinguished by arch,
# and are named here anyway so no leg's cache identity is implicit.
matrix:
include:
- platform: 'macos-latest' # for Arm64
args: '--target aarch64-apple-darwin'
cache_key: 'darwin-aarch64'
- platform: 'macos-latest' # for Intel
args: '--target x86_64-apple-darwin'
cache_key: 'darwin-x86_64'
- platform: 'ubuntu-22.04' # for Linux x86_64
args: ''
cache_key: 'linux-x86_64'
- platform: 'ubuntu-22.04-arm' # for Linux arm64
args: ''
cache_key: 'linux-aarch64'
- platform: 'windows-latest' # for Windows x86_64
args: ''
cache_key: 'windows-x86_64'
- platform: 'windows-11-arm' # for Windows arm64
args: ''
cache_key: 'windows-aarch64'

runs-on: ${{ matrix.platform }}
# A hung step otherwise runs to GitHub's 6 hour default before anyone notices. The slowest
Expand All @@ -262,22 +274,23 @@ jobs:
with:
fetch-depth: 0

# Must run *before* setup-node: this deletes $AGENT_TOOLSDIRECTORY, which is the tool
# cache setup-node installs Node into. Running it afterwards removes the very toolchain
# the next steps depend on.
# Backgrounded, and no longer ordered against setup-node. It used to delete
# $AGENT_TOOLSDIRECTORY, which is where setup-node installs Node - so it had to run
# first, and it blocked for fifty seconds doing it. Dropping that one path retires the
# ordering constraint, and the rm can then run alongside apt and npm ci instead of in
# front of them.
#
# What is left still frees ~15 GB. A release build needs nowhere near it: apt adds
# 216 MB and the release target directory is a few gigabytes against ~21 GB free.
#
# `startsWith`, not `==`: ubuntu-22.04-arm is a different runner label and an exact
# match would quietly skip this on the arm64 leg.
- name: Free Disk Space (Ubuntu)
- name: Reclaim disk in the background
if: startsWith(matrix.platform, 'ubuntu-')
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
if [ -n "${AGENT_TOOLSDIRECTORY:-}" ]; then
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
fi
df -h /
sudo rm -rf /usr/local/lib/android /usr/share/dotnet >/dev/null 2>&1 &
sudo rm -rf /opt/ghc /usr/local/share/boost >/dev/null 2>&1 &

- name: setup node
uses: actions/setup-node@v7
Expand All @@ -291,6 +304,30 @@ jobs:
# Those targets are only used on macos runners so it's skip on linux and windows
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}

# This workflow had no Rust cache at all, so every release compiled all 799 crates from
# scratch on six platforms - 408 of the Linux leg's 568 seconds inside tauri-action, and
# roughly 600 of windows-latest's 694. test.yml has cached since it was written; the
# build matrix simply never got one.
- name: Cache Rust dependencies
uses: swatinem/rust-cache@v2
with:
workspaces: src-tauri -> target

# One key per leg - see the note on `cache_key` in the matrix above. Spelled as its
# own matrix field rather than derived from `args`, which is empty on four legs and
# contains a space on the other two, and a cache key is a poor place for either.
key: ${{ matrix.cache_key }}

# `main` because it is the default branch, which is the one scope every run can
# read: a cache written from any other ref is invisible to everything but that ref.
# A workflow_dispatch rehearsal also runs on `main` and so writes too, which is
# wanted - it warms the same six keys a real release would, at no extra footprint.
save-if: ${{ github.ref == 'refs/heads/main' }}

# Left at its default, said out loud: a leg that dies mid-build must not persist a
# half-written target directory into the next release.
cache-on-failure: false

# Same widening as above, and it matters more here: skipping this step does not fail
# the leg, it fails the build twenty minutes later with a missing webkit2gtk.
- name: install dependencies (ubuntu only)
Expand Down Expand Up @@ -330,8 +367,11 @@ jobs:
echo "::error::Could not install Linux build dependencies after 3 attempts."
exit 1

# The flags are worth their noise on the arm64 legs: windows-11-arm spent 123 seconds
# here, where x86_64 takes 19. An audit and a funding lookup are two network round
# trips a release build has no use for.
- name: install frontend dependencies
run: npm ci
run: npm ci --prefer-offline --no-audit --fund=false

# Signing is enabled by exporting secrets into $GITHUB_ENV only when they are actually
# configured. Setting them directly on the build step would not work: a secret that does
Expand Down
58 changes: 49 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ jobs:
changelog:
name: Changelog
runs-on: ubuntu-latest
# Bounded because these are required contexts: a job that hangs, or sits in a runner
# queue, blocks a merge with no signal at all. One Tests run waited thirteen minutes for
# a windows-latest runner and nothing anywhere would have stopped it short of GitHub's
# six-hour default.
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@v7
Expand All @@ -56,6 +61,7 @@ jobs:
frontend-tests:
name: Frontend Tests
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
Expand All @@ -74,10 +80,13 @@ jobs:
- name: Install dependencies
run: npm ci

# Split by OS rather than run both: `test:coverage` is `vitest run --coverage`, the
# same fourteen files end to end, so ubuntu was running the whole suite twice.
- name: Run tests
if: matrix.os != 'ubuntu-latest'
run: npm run test

- name: Generate coverage report
- name: Run tests with coverage
if: matrix.os == 'ubuntu-latest'
run: npm run test:coverage

Expand All @@ -94,6 +103,7 @@ jobs:
backend-tests:
name: Backend Tests (Rust)
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
Expand All @@ -103,16 +113,22 @@ jobs:
- name: Checkout code
uses: actions/checkout@v7

- name: Free Disk Space (Ubuntu)
# Backgrounded, because as a blocking step this cost 91 seconds in a job whose actual
# work - `cargo test` - takes 24. Almost all of it is /usr/local/lib/android, nine
# gigabytes of small files. What matters is that the disk is free before the build
# needs it, not before the next step starts, so apt and cargo now run alongside it.
#
# $AGENT_TOOLSDIRECTORY is deliberately no longer deleted. Removing it is what forced
# this step to run before any setup-node, and a backgrounded rm would race whatever
# installs there. What is left still frees ~15 GB, against a debug target directory
# that is under a gigabyte since `[profile.dev.package."*"] debug = false` landed in
# Cargo.toml - the 37 GB build this step was written for cannot happen any more.
- name: Reclaim disk in the background
if: matrix.os == 'ubuntu-latest'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
if [ -n "${AGENT_TOOLSDIRECTORY:-}" ]; then
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
fi
df -h /
sudo rm -rf /usr/local/lib/android /usr/share/dotnet >/dev/null 2>&1 &
sudo rm -rf /opt/ghc /usr/local/share/boost >/dev/null 2>&1 &

- name: Install dependencies (Ubuntu only)
if: matrix.os == 'ubuntu-latest'
Expand All @@ -128,10 +144,27 @@ jobs:
with:
components: rustfmt, clippy

# `save-if` is what keeps this affordable. Every push and every pull_request run used to
# write a fresh entry per OS - ~800 MiB each - and twelve of them had grown to fill
# 8.6 GB of GitHub's 10 GB per-repository budget, at which point the cache evicts itself
# and nothing stays warm. Four of the twelve were scoped to a pull request that had long
# since merged, and could never be read again by anything.
#
# `main`, not `develop`, and that is the whole point of the line. A run may restore only
# caches scoped to its own ref or to the default branch, and the default branch here is
# `main` - so a cache written from `develop` is invisible to a release, and one written
# from a PR is invisible to everything. Saving from `main` is the only scope every run
# can read: develop pushes, PRs and releases alike restore from it.
#
# This suite does reach `main`: release.yml calls it through `workflow_call`, where
# `github.ref` is the caller's ref. So the cache refreshes once per release, which is
# roughly daily. A `Cargo.lock` or rustc change between releases means develop runs cold
# until the next one - the honest cost of three entries instead of nine.
- name: Cache Rust dependencies
uses: swatinem/rust-cache@v2
with:
workspaces: src-tauri -> target
save-if: ${{ github.ref == 'refs/heads/main' }}

- name: Run Rust tests
working-directory: src-tauri
Expand All @@ -149,19 +182,26 @@ jobs:
# over ten lint families - but it is a tree-wide reformat that would bury `git blame`, so
# it belongs in its own commit rather than riding along with a CI change. When that lands,
# drop the `continue-on-error` and the "(advisory)" from both names.
#
# One OS, not three. Neither lint is platform-dependent, and clippy's check-mode
# artifacts carry different fingerprints from the test build's - so on every leg it
# was a second near-full compile, for a result the other two legs had already given.
- name: Run Clippy (advisory, non-gating)
if: matrix.os == 'ubuntu-latest'
working-directory: src-tauri
run: cargo clippy -- -D warnings
continue-on-error: true

- name: Check formatting (advisory, non-gating)
if: matrix.os == 'ubuntu-latest'
working-directory: src-tauri
run: cargo fmt --check
continue-on-error: true

type-check:
name: TypeScript Type Check
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Checkout code
Expand Down
Loading