6 Commits

Author SHA1 Message Date
295defb89f Merge pull request 'ci(link): gate pull requests into main on fmt, clippy, and test' (#23) from ci/pr-checks into main
All checks were successful
sync-project-tree / sync (push) Successful in 15s
Release sidecar / release (push) Successful in -18s
SonarQube / analysis (push) Successful in 40s
Reviewed-on: #23
2026-08-01 06:50:16 +00:00
5c4b77d957 Merge branch 'main' into ci/pr-checks
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m14s
2026-08-01 06:47:32 +00:00
f4b71f58fd ci(link): gate pull requests into main on fmt, clippy, and test
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m47s
This repo had no pull_request workflow at all — release.yml runs only
after merge, so its `cargo fmt --check` gate was the first thing to see
new code, and an unformatted commit killed the release job before it
could build, tag, or publish (run for 2301c57).

Add pr-checks.yml, mirroring release.yml's gates in the same order so a
green PR implies the release clears its own gates:

  cargo fmt --check
  cargo clippy --locked --all-targets -- -D warnings
  cargo test --locked

One job, not three: bootstrapping the toolchain costs more than the
checks, so parallel jobs would pay it three times for no wall-clock win.
Cargo registry and target/ are cached on Cargo.lock. The crate is
already clean at `-D warnings`, so clippy starts green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 01:44:23 -05:00
ef639679d1 Merge pull request 'fix(sidecar): apply rustfmt to the protocol 3.0 cutover code' (#22) from fix/rustfmt-release into main
All checks were successful
sync-project-tree / sync (push) Successful in 8s
SonarQube / analysis (push) Successful in 42s
Release sidecar / release (push) Successful in 6m22s
Reviewed-on: #22
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-01 06:41:46 +00:00
8c4dc0ee93 fix(sidecar): apply rustfmt to the protocol 3.0 cutover code
The release workflow's first gate is `cargo fmt --check`, and the 3.0
cutover merge (2301c57) landed two rustfmt violations in the
`world.ruleset` path, so the run failed before it could build or tag:

- main.rs: the `upsert_ruleset(..)` call fits on one line
- store.rs: the `upsert_ruleset` signature does not

No behavior change — formatting only.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 01:40:03 -05:00
2301c57768 Merge pull request 'feat(sidecar)!: Protocol 3.0 cutover — X-UOLink-Version 2 → 3' (#21) from edge into main
Some checks failed
sync-project-tree / sync (push) Successful in 7s
SonarQube / analysis (push) Successful in 48s
Release sidecar / release (push) Failing after 59s
Reviewed-on: #21
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-01 06:34:19 +00:00
4 changed files with 120 additions and 6 deletions

View File

@@ -0,0 +1,101 @@
# 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: this gates PRs into `main` only. Feature work that lands on an
# integration branch first (e.g. `edge`) is still caught on the branch's PR into
# `main`. To gate that earlier hop too, add the branch to the `branches:` list
# below — nothing else needs to 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]
# 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

View File

@@ -25,6 +25,7 @@ network-facing component, which is what keeps the game unreachable from the inte
| Path | What | | Path | What |
|------|------| |------|------|
| `sidecar/` | The Rust sidecar crate — terminates the loopback link to the shard, exposes WS + REST to the website. See [`sidecar/README.md`](sidecar/README.md). | | `sidecar/` | The Rust sidecar crate — terminates the loopback link to the shard, exposes WS + REST to the website. See [`sidecar/README.md`](sidecar/README.md). |
| `.gitea/workflows/pr-checks.yml` | Gates every PR into `main` on `cargo fmt --check`, `cargo clippy -D warnings`, and `cargo test`. |
| `.gitea/workflows/release.yml` | Builds + releases the sidecar binary (Linux + Windows) on every merge to `main`. | | `.gitea/workflows/release.yml` | Builds + releases the sidecar binary (Linux + Windows) on every merge to `main`. |
## Build & run ## Build & run
@@ -42,6 +43,17 @@ cargo run --release
every merge to `main` (conventional-commit versioning). See [`sidecar/README.md`](sidecar/README.md) every merge to `main` (conventional-commit versioning). See [`sidecar/README.md`](sidecar/README.md)
for configuration and the wire protocol. for configuration and the wire protocol.
Before that, `.gitea/workflows/pr-checks.yml` runs the same gates on every pull request into `main`
`cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, then `cargo test --locked`. Run them
locally before pushing and the PR will be green:
```bash
cd sidecar
cargo fmt # or --check to just report
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked
```
## Deployment & compatibility ## Deployment & compatibility
The plugin ([RunicGateway/servuo-plugins](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins)) The plugin ([RunicGateway/servuo-plugins](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins))

View File

@@ -262,11 +262,7 @@ async fn main() -> anyhow::Result<()> {
// lets a reader tell a re-send from an actual config change. // lets a reader tell a re-send from an actual config change.
"world.ruleset" => { "world.ruleset" => {
if let Err(e) = event_store if let Err(e) = event_store
.upsert_ruleset( .upsert_ruleset(ev.value.get("rev").and_then(|r| r.as_str()), &text, t)
ev.value.get("rev").and_then(|r| r.as_str()),
&text,
t,
)
.await .await
{ {
tracing::warn!(error = %e, "failed to upsert ruleset"); tracing::warn!(error = %e, "failed to upsert ruleset");

View File

@@ -299,7 +299,12 @@ impl Store {
/// `world.ruleset` frame per connect describing how it is configured, and only the latest one /// `world.ruleset` frame per connect describing how it is configured, and only the latest one
/// matters. `rev` is the shard's FNV-1a of the body, kept so a reader can tell "same ruleset, /// matters. `rev` is the shard's FNV-1a of the body, kept so a reader can tell "same ruleset,
/// re-sent on reconnect" from "the operator changed something" without diffing the JSON. /// re-sent on reconnect" from "the operator changed something" without diffing the JSON.
pub async fn upsert_ruleset(&self, rev: Option<&str>, json: &str, t: i64) -> anyhow::Result<()> { pub async fn upsert_ruleset(
&self,
rev: Option<&str>,
json: &str,
t: i64,
) -> anyhow::Result<()> {
sqlx::query( sqlx::query(
"INSERT INTO ruleset (id, rev, json, updated_t) VALUES (1, ?, ?, ?) "INSERT INTO ruleset (id, rev, json, updated_t) VALUES (1, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET rev = excluded.rev, json = excluded.json, updated_t = excluded.updated_t", ON CONFLICT(id) DO UPDATE SET rev = excluded.rev, json = excluded.json, updated_t = excluded.updated_t",