Files
installer/.gitea/workflows/pr-checks.yml
wtclaude dff4ad41c9
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m31s
feat(installer): implement Phase 1 — the installer core
Adds the Rust crate at the repo root and implements `install` end to end for
the overlay half of a deployment: resolve the published bundle, find and
validate the ServUO root, refuse to deploy under a running shard, sync the
plugin overlay, and record what was deployed in install.json.

`doctor`, `update` and `uninstall` parse and answer with the phase they arrive
in rather than "unrecognized command", and the run states plainly that the
uo-link sidecar (Phase 2) and the patch tier (Phase 3) were not installed —
`--patches` in particular reports REQUESTED BUT NOT APPLIED, since a quiet
completion would be read as a patched shard.

Landing on `edge` rather than `main`: release.yml publishes a binary on every
push to main, and an installer that deploys the overlay but cannot install the
sidecar is not something to hand an operator. pr-checks.yml now gates PRs into
edge on the same rules, so the branch the work happens on is not the ungated
one.

Notable decisions, all documented in docs/installer/PLAN.md §5 Phase 1:

- The code lives in a library called `rgdeploy` with a thin binary that keeps
  the published name. Windows' UAC installer detection refuses to launch an
  unsigned executable whose file name contains "install" (os error 740), and
  Cargo names test harnesses after their target — so a target under that name
  makes `cargo test` unrunnable on Windows.
- The running-shard check matches processes by path, not by process name:
  on Linux a live shard is `mono`/`dotnet` with ServUO.exe as an argument, and
  a name match would report "not running" for a shard that is running.
- install.json records a state (`deployed` / `kept-operator-modified`), not the
  run's verb, so an unchanged re-run produces an identical record and writes
  nothing.
- The Bridge.cfg keep rule compares against the hash the installer last
  deployed, not the last hash it saw — otherwise a kept file is overwritten on
  the very next run.
- Downloads are verified against the bundle's SHA256 while being written, then
  every extracted file is re-hashed against the release's own manifest.json,
  whose protocol and version are cross-checked against the bundle.

Verified against a real ServUO 57.4 tree and end to end into a scratch tree:
24 files deployed, an unchanged re-run that writes nothing, an edited
Bridge.cfg kept across repeated runs while code files are overwritten, bundle
pinning, and a refusal with a shard running out of the tree.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 14:58:17 -05:00

124 lines
5.6 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.
#
# Mirrors RunicGateway/link's pr-checks.yml — same gates in the same order as
# release.yml, so a green PR means the release will get past its own gates too.
# The one structural difference is the crate guard below.
#
# ── Crate guard ──────────────────────────────────────────────────────────────
# The gates are conditional on a root Cargo.toml existing: before the crate
# landed, this job reported green with a notice so governance/docs PRs were not
# red-Xed by a workflow with nothing to build. Phase 1 has now added the crate on
# `edge`, so the gates arm themselves there automatically — and stay dormant on
# a `main` PR until the cutover merges the crate into it. Nothing here changes at
# that point either.
#
# The crate is expected at the REPO ROOT (not a subdirectory like link/sidecar):
# this repo's sole product is the one installer binary, so there is nothing to
# namespace it against.
#
# 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.
#
# 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:
# `edge` is gated as well as `main`. Phase 1 and 2 land there rather than on `main` so that
# release.yml — which fires on every push to `main` — does not publish an installer binary that
# can deploy the overlay but not yet install the sidecar. Ungating the branch where all the
# work actually happens would leave the gates running only at the cutover, which is the one
# moment a red build is most expensive.
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
jobs:
rust-gates:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Detect whether a crate exists yet
id: detect
run: |
set -euo pipefail
if [ -f Cargo.toml ]; then
echo "crate=true" >> "$GITHUB_OUTPUT"
echo "==> Cargo.toml found — running the full gate set."
else
echo "crate=false" >> "$GITHUB_OUTPUT"
echo "==> No Cargo.toml at the repo root yet (planning phase)."
echo " Skipping fmt/clippy/test. These gates arm themselves as"
echo " soon as Phase 1 lands the crate — see docs/installer/PLAN.md."
fi
# 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)
if: ${{ steps.detect.outputs.crate == 'true' }}
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
if: ${{ steps.detect.outputs.crate == 'true' }}
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('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
if: ${{ steps.detect.outputs.crate == 'true' }}
run: cargo fmt --check
# --all-targets covers tests and examples, not just the binary.
# -D warnings makes a lint a failure, so the crate starts clean at this bar
# and anything new is a regression introduced by the PR.
- name: cargo clippy
if: ${{ steps.detect.outputs.crate == 'true' }}
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
if: ${{ steps.detect.outputs.crate == 'true' }}
run: cargo test --locked