Protocol 4 gives the guild board a real member list instead of the member *count* that was all Protocol 2 could express. `guild.roster` carries the set; `guild.leave` is forwarded but deliberately not projected. The roster lives in its own `members` column rather than as a field folded into `json`. That column holds the verbatim `guild.update` line, so a roster write into it would clobber the snapshot — name, abbreviation, leader, online count — that `guild.update` owns. Two writers across two columns of one row means both stay plain upserts: neither reads the other's value first, so there is no read-modify-write and no ordering requirement between the two kinds. `GET /guilds` folds the roster back in as `roster` at read time. `guild.leave` gets no board arm on purpose. The shard re-emits `guild.roster` whenever the member set changes, so the board self-corrects within one sweep, and keeping the delta out of the projection is what keeps the sidecar a forwarder rather than a thing that maintains state. This is also the repo's first store migration, and the reason it needed one: `SCHEMA` is `CREATE TABLE IF NOT EXISTS`, which can add a table but cannot add a column to a table that already exists. Every schema change up to and including Protocol 3.0 happened to add whole tables, so `ALTER TABLE` appears nowhere in this repo's history and the gap was invisible. `guilds.members` is the first column added to an existing table, so without a mechanism the column would simply never reach an installed sidecar and every roster write would fail. The counter is SQLite's own `PRAGMA user_version` — an integer in the database header, so it costs no table and cannot drift from the file it describes. Each step runs in a transaction together with the bump recording it, so a step lands completely or not at all. A database written by a *newer* sidecar warns and continues rather than failing: every step is additive, so a newer schema has only columns an older reader ignores, and refusing to start would turn rolling the binary back — a recovery path — into a dead end. A migration failure aborts startup, which was already the behaviour and is the right one: a half-migrated store answers the website with confusing partial data, and the shard dials *out*, so a sidecar that refuses to start never stalls the game. store.rs had no tests before this. The six added here cover the upgrade path that matters (an existing pre-Protocol-4 database gains the column and lands at the current version), that a restart re-running the migration is a no-op, that a roster does not clobber the snapshot, that the two writers work in either order, and that a guild with no roster yet has no `roster` key at all — "not known" and "known to be empty" must not be conflated, or a website renders an empty roster as fact. Also gates PRs into `edge`, not just `main`. This workstream lands ten phases there, and gating only the `main` hop would run these checks for the first time at the cutover. The precedent and the reasoning are already in RunicGateway/installer's copy of this workflow. Refs: docs/website/TEAMS.md Part 12 Phase 1 Co-Authored-By: Claude <noreply@anthropic.com>
104 lines
4.3 KiB
YAML
104 lines
4.3 KiB
YAML
# Gate every pull request into `main` on the same Rust checks the release runs,
|
|
# so a formatting slip, a lint regression, or a failing test can't reach the
|
|
# deployable branch.
|
|
#
|
|
# Why this exists: release.yml runs only AFTER merge (on push to `main`) and its
|
|
# FIRST Rust step is `cargo fmt --check`. Before this workflow, an unformatted
|
|
# commit merged cleanly and then killed the release job before it could build,
|
|
# tag, or publish anything — the repo had no pull_request workflow at all. These
|
|
# gates are deliberately a mirror of release.yml's, in the same order, so a green
|
|
# PR means the release will get past its gates too.
|
|
#
|
|
# Enforcement (one-time, in the Gitea UI):
|
|
# Repository Settings → Branches → Branch Protection (rule for `main`)
|
|
# • Enable Status Check
|
|
# • Status check patterns: PR Checks / *
|
|
# Note: Gitea only lists a context in its dropdown after it has reported once,
|
|
# so let this workflow run on one PR first. The `PR Checks / *` glob matches
|
|
# without needing the dropdown.
|
|
#
|
|
# Scope note: `edge` is gated as well as `main`. Multi-phase work lands there
|
|
# first, so gating only the `main` hop would run these checks for the first time
|
|
# at the cutover — the one moment a red build is most expensive to discover. This
|
|
# is the same call `RunicGateway/installer` made for the same reason, and it was
|
|
# taken here after a nine-PR Android workstream landed on an ungated `edge` with
|
|
# no CI at all. Adding a branch to the `branches:` list is the whole change.
|
|
#
|
|
# Runner: the same self-hosted `ubuntu-latest` runner release.yml uses. Rust is
|
|
# not assumed to be preinstalled, so the toolchain step bootstraps it the same
|
|
# way release.yml does (minus the MinGW cross-compile deps — PRs build for the
|
|
# host only; the Windows cross-build stays a release-time concern).
|
|
|
|
name: PR Checks
|
|
|
|
on:
|
|
pull_request:
|
|
branches: [main, edge]
|
|
|
|
# A newer push to the same PR cancels the in-flight run.
|
|
concurrency:
|
|
group: pr-checks-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
WORKDIR: sidecar
|
|
|
|
jobs:
|
|
rust-gates:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
# One job runs all three gates on purpose: installing the toolchain costs
|
|
# far more than the checks themselves, so splitting fmt/clippy/test into
|
|
# parallel jobs would pay that cost three times for no wall-clock win.
|
|
- name: Install Rust toolchain (rustfmt + clippy)
|
|
run: |
|
|
set -euo pipefail
|
|
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
|
|
$SUDO apt-get update
|
|
$SUDO apt-get install -y --no-install-recommends \
|
|
build-essential curl ca-certificates git
|
|
|
|
if ! command -v cargo >/dev/null 2>&1; then
|
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
|
| sh -s -- -y --profile minimal --default-toolchain stable
|
|
fi
|
|
echo "${HOME}/.cargo/bin" >> "$GITHUB_PATH"
|
|
export PATH="${HOME}/.cargo/bin:${PATH}"
|
|
rustup component add rustfmt clippy
|
|
cargo --version && cargo fmt --version && cargo clippy --version
|
|
|
|
# Keyed on Cargo.lock: dependency builds are reused until a dep actually
|
|
# changes. A cache miss only makes the run slower, never wrong.
|
|
- name: Cache cargo registry and build dir
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.cargo/registry
|
|
~/.cargo/git
|
|
sidecar/target
|
|
key: ${{ runner.os }}-cargo-${{ hashFiles('sidecar/Cargo.lock') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-cargo-
|
|
|
|
# Cheapest gate first — parses only, no compile, so a formatting slip
|
|
# fails in seconds instead of after a full build.
|
|
- name: cargo fmt --check
|
|
working-directory: sidecar
|
|
run: cargo fmt --check
|
|
|
|
# --all-targets covers tests and examples, not just the binary.
|
|
# -D warnings makes a lint a failure; the crate is clean at this bar today,
|
|
# so anything new here is a regression introduced by the PR.
|
|
- name: cargo clippy
|
|
working-directory: sidecar
|
|
run: cargo clippy --locked --all-targets -- -D warnings
|
|
|
|
# --locked matches release.yml: it also proves Cargo.lock is in sync with
|
|
# Cargo.toml, rather than letting the build silently update it.
|
|
- name: cargo test
|
|
working-directory: sidecar
|
|
run: cargo test --locked
|