Compare commits
8 Commits
05e192ca70
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 295defb89f | |||
| 5c4b77d957 | |||
| f4b71f58fd | |||
| ef639679d1 | |||
| 8c4dc0ee93 | |||
| 2301c57768 | |||
| cfe9ec9017 | |||
| 5f50b881ca |
101
.gitea/workflows/pr-checks.yml
Normal file
101
.gitea/workflows/pr-checks.yml
Normal 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
|
||||||
12
README.md
12
README.md
@@ -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))
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ So you can never accidentally run without auth. Rotate by editing the token and
|
|||||||
|
|
||||||
## Protocol version
|
## Protocol version
|
||||||
|
|
||||||
The wire protocol has a version (`PROTOCOL_VERSION`, currently **1**), so the website and sidecar detect a mismatch immediately instead of failing in strange ways when a message shape changes.
|
The wire protocol has a version (`PROTOCOL_VERSION`, currently **3**), so the website and sidecar detect a mismatch immediately instead of failing in strange ways when a message shape changes.
|
||||||
|
|
||||||
- Every response carries an `X-UOLink-Version: 1` header.
|
- Every response carries an `X-UOLink-Version: 3` header.
|
||||||
- `/health` and the WebSocket `ws.hello` include `"protocol": 1`.
|
- `/health` and the WebSocket `ws.hello` include `"protocol": 3`.
|
||||||
- If a request sends `X-UOLink-Version` and it disagrees with the sidecar, the request is rejected **409 Conflict** with `{sidecar_protocol, client_protocol}` so the mismatch is obvious.
|
- If a request sends `X-UOLink-Version` and it disagrees with the sidecar, the request is rejected **409 Conflict** with `{sidecar_protocol, client_protocol}` so the mismatch is obvious.
|
||||||
|
|
||||||
Bump `PROTOCOL_VERSION` in `main.rs` whenever an event or endpoint's shape changes.
|
Bump `PROTOCOL_VERSION` in `main.rs` whenever an event or endpoint's shape changes.
|
||||||
|
|||||||
@@ -24,7 +24,12 @@ use tracing_subscriber::EnvFilter;
|
|||||||
/// v2 (Protocol 2.0): adds the account-provisioning verbs/endpoints (`POST /accounts/create`,
|
/// v2 (Protocol 2.0): adds the account-provisioning verbs/endpoints (`POST /accounts/create`,
|
||||||
/// `DELETE /link/:account`) and their events. Outbound event kinds are additive, so a v1 website
|
/// `DELETE /link/:account`) and their events. Outbound event kinds are additive, so a v1 website
|
||||||
/// keeps working against the live feed; the new *endpoints* require a v2 sidecar.
|
/// keeps working against the live feed; the new *endpoints* require a v2 sidecar.
|
||||||
pub const PROTOCOL_VERSION: u32 = 2;
|
///
|
||||||
|
/// v3 (Protocol 3.0): adds `world.ruleset`, `points.board` and `vendor.listing` /
|
||||||
|
/// `vendor.listing.remove`, with the `GET /ruleset`, `/points` and `/market` reads that serve them
|
||||||
|
/// from the store. Same shape as the v2 bump — the kinds are additive, the endpoints are not — and
|
||||||
|
/// there is deliberately no feature-negotiation array: v3 implies all three kinds.
|
||||||
|
pub const PROTOCOL_VERSION: u32 = 3;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
@@ -257,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");
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user