Commit Graph

72 Commits

Author SHA1 Message Date
3a3676a22d Merge pull request 'fix(rust): drop the stray spaces in the running-server handoff line' (#34) from fix/rust-handoff-spaces into main
All checks were successful
Release installer / release (push) Successful in 4m57s
sync-project-tree / sync (push) Successful in -1m31s
Reviewed-on: #34
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
v0.3.2
2026-09-26 15:41:23 +00:00
9d73367e63 test(rust): pin the running-server handoff line whole
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m0s
The existing assertions checked fragments ("doctor --game rust
--server-id alpha"), which the stray-space version also contained. The
whole first line is now compared, so a wrapped source line cannot put
its indent back into the sentence. Fails against the pre-fix literal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-26 10:14:22 -05:00
d61f46ffb6 fix(rust): drop the stray spaces in the running-server handoff line
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 57s
The D156 line printed "confirms it                  connected." — a
wrapped source line left a run of spaces inside the string literal.
Found by the phase 18 step 7 re-walk on Debian 12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-26 10:09:33 -05:00
dc3d360cf2 Merge pull request 'fix(release): static musl Linux binaries, so the installer runs on Debian 12 (D158)' (#33) from ci/static-musl-installer into main
All checks were successful
sync-project-tree / sync (push) Successful in 19s
Release installer / release (push) Successful in 7m50s
Reviewed-on: #33
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
v0.3.1
2026-09-26 14:53:13 +00:00
9f43260ee7 fix(release): static musl Linux binaries, so the installer runs on Debian 12 (D158)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m1s
The Linux installers were linked against the release runner's glibc and
needed glibc 2.39: v0.3.0 (and v0.2.0) failed on the first line on
Debian 12 (2.36) and Ubuntu 22.04 (2.35) with "GLIBC_2.39 not found" --
found by the Rust phase 18 walk, step 7. Both Linux targets are now
x86_64/aarch64-unknown-linux-musl, built with cargo-zigbuild (zig is the
one C compiler `ring` needs for both musl targets; Ubuntu packages a
musl gcc for x86_64 only), and the package step refuses a Linux binary
that `file` does not call statically linked. Windows is unchanged.

Tried in rust:1-slim-bookworm: both targets build, both are "statically
linked", and the x86_64 binary runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-26 04:47:07 -05:00
0710a3e33e Merge pull request 'feat(rust): cut edge over to main — --game rust, named instances, schema-2 bundles (rust phase 18)' (#32) from edge into main
All checks were successful
sync-project-tree / sync (push) Successful in 15s
Release installer / release (push) Successful in 4m55s
Reviewed-on: #32
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
v0.3.0
2026-09-26 09:15:43 +00:00
0f19fd6413 Merge pull request 'fix(service): start the Windows service after granting it its config (D157)' (#31) from fix/windows-start-after-grant into edge
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m2s
Reviewed-on: #31
2026-09-26 09:00:29 +00:00
70eb073826 fix(service): start the Windows service after granting it its config (D157)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m8s
`sc start` ran inside registration, before `grant_service_access` let the
new virtual account read `sidecar.toml` — which `protect_config` had
locked to SYSTEM and Administrators. The first start died on "Access is
denied (os error 5)" and the service sat STOPPED: the SCM's
restart-on-failure policy never fires for a clean exit with an error
code. A later manual start worked, because by then the grant existed.

Found on the phase 18 walk (step 4, one Rust instance under the SCM),
from the sidecar's own log. The code is shared, so the released ServUO
installer has the same first-start failure; the org lead chose to fix
both here (D157).

Registration no longer starts the service; a new `start_registered`
runs after the grant and refreshes the reported state. A no-op on Linux,
where the service user exists before the config and registration starts
the unit itself.

Walked (Windows, elevated): install -> START_PENDING/running, /health on
the first probe, clean stop and restart, doctor clean, uninstall clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-26 03:54:57 -05:00
285f7df17b Merge pull request 'fix(rust): an update on a running server reloads the plugin' (#30) from fix/rust-plugin-reload into edge
Reviewed-on: #30
2026-09-26 08:46:02 +00:00
e5611a01eb fix(service): say why the service account stays, truthfully for Rust too
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 54s
`uninstall --game rust --server-id beta` printed "left the runicgateway
account alone — this installer did not create it" on a host where this
installer had created it, for alpha. A Rust instance's service never owns
the shared account (the last instance's removal decides), so the reason
now reads "it is not this service's to remove" — true for both games.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-26 03:45:27 -05:00
397e0b92e9 fix(rust): an update on a running server reloads the plugin
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 52s
The plugin was placed with write_atomic: remove the old file, rename a
.tmp over it. Oxide and Carbon watch the plugins directory and reload on
a change; both saw the remove as a delete and UNLOADED the bridge, and
ignored the rename, so the new file was never loaded. Every `update` (or
re-install) on a running server took the bridge down until the next
boot, silently — the phase 18 walk found it on all three instances and
both frameworks, after the move to v0.1.1.

The plugin is now overwritten in place, which is what an operator's `cp`
does and what both frameworks reload on. write_atomic stays for the
record and the configs, where atomicity is the point.

`update` on the same bundle also no longer says "nothing moved" after it
has just put back a hand-edited plugin — the remedy `doctor` names.

Walked: hand-edit, then `update`, on a running Oxide and a running
Carbon server: replaced, reloaded, reconnected, doctor clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-26 03:42:42 -05:00
0c07b8a967 Merge pull request 'fix(rust): the handoff says what the run did, not that a link exists (D156)' (#29) from fix/rust-handoff-wording into edge
Reviewed-on: #29
2026-09-26 06:27:30 +00:00
3b2881eb9c fix(rust): the handoff says what the run did, not that a link exists (D156)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 58s
`install --game rust` ended every instance with "Rust server "alpha" is
connected to its sidecar" — printed unconditionally, before the plugin had
dialled anything, on a stopped server where it had not even loaded, and
beside "No service was registered, so nothing is listening yet". The
phase 18 walk read it as a claim and then found beta's plugin connected to
the wrong sidecar.

It now reads "Rust server "alpha" is set up." followed by either "The
plugin loads now; `doctor --game rust --server-id alpha` confirms it
connected." or "The plugin connects when the server next starts.".

The top-level --help no longer describes `install` as the uo-link sidecar
and overlay for both games, and says what `uninstall` removes for Rust.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-26 01:21:32 -05:00
0fad3e3b2d Merge pull request 'feat(rust): --game rust, named instances, and schema-2 bundles (phase 18)' (#28) from feat/phase-18-rust into edge
Reviewed-on: #28
2026-09-26 05:33:51 +00:00
7027a78a23 feat(rust): --game rust, named instances, and schema-2 bundles (phase 18)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m7s
Module-rust phase 18, step 5 of docs/modules/rust/PLAN.md §34.2.7 (D146,
D148, D149, D153).

Bundles: ServUO is read at schema 2 from v2/servuo/ and lowered into the
schema-1 model. Schema 1 at the root is the fallback, so a pin from before
schema 2 still reproduces. Rust bundles are read from v2/rust/. v2 reads use
the contents API, because /raw/ is CDN-cached for six hours.

--game rust runs install, update, doctor and uninstall for Rust servers
(src/rustgame/):
- the framework is detected from its marker files, which were read off both
  rigs; both or neither is refused;
- --server-id names an instance: its own service (runicgateway-rust@<id>, or
  RunicGatewayRust-<id>), config, database and ports;
- the plugin config is written once, with ServerId and Port only. An existing
  one is never rewritten, and one naming another server refuses the run;
- each instance's sidecar.toml is written once with its ports and an absolute
  database path, and the sidecar generates the token into it;
- one binary per host. update moves every instance, and a replaced binary
  restarts all of them;
- doctor checks the plugin file hash, the plugin config's ServerId, the
  required uMod plugins (a warning), the service and /health, and passes when
  the plugin is connected;
- uninstall removes our plugin and keeps its config. --purge also removes the
  sidecar config and database. The last instance takes the binary, the
  template and the record, and the shared user only when no ServUO record
  remains.

service.rs takes the service name as a parameter internally. The ServUO
public API is unchanged.

Finding: Carbon 2.0.259's config.json has no folder keys, so carbon/plugins
and carbon/configs are what the installer uses. The plan expected a moved
directory to be readable there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-25 23:40:11 -05:00
8e4cbc6406 Merge pull request 'ci(bundle): compose schema 2 for ServUO and Rust beside schema 1 (rust phase 18, step 2)' (#27) from ci/bundle-schema-2 into main
All checks were successful
Release installer / release (push) Successful in 12s
sync-project-tree / sync (push) Successful in 21s
Compose bundle / compose (push) Successful in 15s
Reviewed-on: #27
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-09-26 03:54:15 +00:00
4f268f7ffb ci(bundle): compose schema 2 for ServUO and Rust beside schema 1
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m12s
Module-rust phase 18, step 2 of docs/modules/rust/PLAN.md §34.2.7 (D146,
D147). The compose job now writes three streams on the bundles branch:

  current.json         schema 1, ServUO, exactly as before
  v2/servuo/           schema 2, game "servuo"
  v2/rust/             schema 2, game "rust" (Rust-Link + Rust-Plugins)

The gates move into .gitea/scripts/compose-bundles.sh, so they can be run by
hand against the real release API or a mock without a runner. Each game
composes independently: a Rust failure still lets a ServUO bundle publish, and
the run goes red afterwards. A game whose repos have never released composes
nothing and does not fail, which is where Rust is until its cutover.

A ServUO matrix carries the same tag at both schemas, so the first run
publishes v2/servuo/bundle-2026.09.15.json rather than a second name for the
pair. Schema 1 stops being composed on 2027-01-01 and is left frozen.

Tested in debian:12 against the real releases (v2/servuo published as
2026.09.15, schema 1 unchanged, Rust skipped; rerun is a no-op; past the
retirement date schema 1 is frozen) and against a mock Gitea for Rust
(document composed, protocol mismatch refused, missing manifest key refused,
rerun a no-op, ServUO failing independently).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-25 22:49:47 -05:00
f865660c7c Merge pull request 'feat(doctor): the one host prerequisite the Asset Bridge added (Asset Bridge cutover, 4 of 5)' (#26) from edge into main
All checks were successful
sync-project-tree / sync (push) Successful in -1m23s
Release installer / release (push) Successful in 5m56s
Compose bundle / compose (push) Successful in -1m32s
Reviewed-on: #26
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
v0.2.0
2026-09-14 23:11:53 +00:00
2285fff759 Merge pull request 'feat(doctor): the one host prerequisite the Asset Bridge added (Phase 9a)' (#25) from feat/asset-bridge-p9 into edge
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m10s
Reviewed-on: #25
2026-09-14 22:25:52 +00:00
ead105d5bf feat(doctor): the one host prerequisite the Asset Bridge added (Phase 9a)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m39s
`libgdiplus`, on Linux hosts only. ServUO targets net48, so on Linux it runs
under Mono, and Mono's `System.Drawing` is a thin layer over that library -- which
sits in the DECODE path, not merely the encode: `Ultima.Frame` writes ARGB1555
through a LockBits pointer, so without it the shard cannot read one sprite out of
the operator's UO client.

docs/link/v8.md §17.2 took all three answers to this rather than one -- the
prerequisite in SHARD_PREREQS.md, a named NO_IMAGING status instead of an error,
and a `doctor` check. The first two shipped in phase 1. This is the third, and
SHARD_PREREQS.md has been claiming it existed since then.

Three things about the row are deliberate:

- **Linux only, and absent elsewhere.** A Windows host ships System.Drawing with
  .NET Framework and has nothing to check, so there is no row rather than a row
  saying "not applicable" on three quarters of the hosts that run this.
- **A warning, never a failure.** Names and the shard's own spawn files have no
  pixels in them, so a host without this library still runs a useful bridge, and
  `doctor`'s exit code -- which monitoring reads -- must not go red over one
  absent feature.
- **`ldconfig -p` first, paths second.** The loader's own cache is the same
  question Mono asks at runtime, so a distro that puts the file somewhere unusual
  answers correctly; the path probe is the fallback for a slim container with no
  ldconfig, and a missing ldconfig degrades to it rather than taking doctor down.

The verdict is split from the detection so the mark and the wording are testable
on a host that has the library and one that does not; all three tests are
Linux-gated, which is where CI runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-14 13:07:23 -05:00
1996a32153 Merge pull request 'ci(release): sweep every tag for a missing release, not just this run's' (#24) from ci/release-orphan-tag-sweep into main
All checks were successful
Release installer / release (push) Successful in 8s
sync-project-tree / sync (push) Successful in 7s
Compose bundle / compose (push) Successful in -59s
Reviewed-on: #24
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-24 19:42:02 +00:00
65998692ae ci(release): sweep every tag for a missing release, not just this run's
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m28s
The first commit on this branch said the plan step "CAN recover an orphan, but
only on a run that reaches it". Checking link and servuo-plugins for the same
gaps showed that understated it.

The recovery is VERSION-SCOPED. It computes VERSION from the newest tag plus the
conventional-commit bump, then only checks refs/tags/v${VERSION}. So it recovers
an orphan on the very next run and is useless afterwards: once any releasable
commit lands, the next run computes a NEW version and never looks at the old tag
again. The orphan becomes permanent and silent.

servuo-plugins proved it, and the proof is pointed. Its v0.1.0 had been orphaned
since 2026-08-04 -- tag present, no release, no assets -- while v0.1.1, v0.2.0
and v1.0.0 all published normally. The commit that ADDED the recovery to that
repo was itself typed "fix(release): preflight credentials and recover the
orphaned v0.1.0 tag", so it bumped to v0.1.1, and the run that introduced the
recovery stepped straight past the tag it was written to rescue.

The retry added in the previous commit makes an orphan much less likely, but it
does not make one impossible -- a cancelled job or a dying runner produces the
same state with no 500 anywhere -- and until now nothing would ever have
mentioned it again.

So the plan step now sweeps every v* tag and warns about any without a release.

It WARNS rather than recovers, on the org lead's decision. Publishing an old
version would mean building today's tree and shipping it under a tag whose tree
it is not, which is worse than the inconsistency it fixes; and a routine push
silently republishing ancient history is not a thing this pipeline should be
able to do. Recovery stays limited to the version the run computed.

It also never fails the run. A sweep that can break a good release is a sweep
someone will delete.

Verified by running the loop against the real repositories rather than a stub,
since the only thing worth proving is that it tells a clean repo from a dirty
one:

  link (9 tags): clean
  servuo-plugins (4 tags): :⚠️:Tags with no release: v0.1.0
  installer (2 tags): clean

and again after servuo-plugins#15 deleted that tag, where all three report
clean. Every run block bash -n clean, the YAML parses, and no empty template
token.

Companion PRs: link#33 and servuo-plugins#15.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 13:57:59 -05:00
094da1776b Merge pull request 'ci(release): show the error body, and retry the release POST' (#23) from ci/release-post-retry-and-error-body into main
All checks were successful
sync-project-tree / sync (push) Successful in 7s
Release installer / release (push) Successful in -59s
Reviewed-on: #23
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-24 17:32:27 +00:00
188e6eb882 ci(release): show the error body, and retry the release POST
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m32s
Run 75 built every artifact, pushed tag v0.1.1, then took a 500 from
POST /releases one second later and exited 22. The tag was left orphaned with
no release and no binaries, so the handoff fix in #22 reached no operator until
the workflow was re-run by hand today.

Re-running published the same four assets untouched, via the orphan-tag
recovery the plan step already has. So the 500 was a race with the tag push --
Gitea had not finished processing the pushed tag when the POST arrived -- and
not a bad request.

Two separate gaps made that worse than it needed to be.

`curl -sSf` prints no response body on an error status. All the log carried was
"curl: (22) ... error: 500", so the cause had to be inferred from timestamps
rather than read. Every call in this step now captures the body and prints it
on failure, including the asset uploads.

And nothing retried. The plan step can recover an orphan tag, but only on a run
that reaches it, and a later push with no releasable commits stands down before
it gets there -- which is why this one sat until someone looked. The POST now
retries five times with a 5/10/15/20s backoff.

4xx is deliberately not retried: a bad token or a malformed body will not
improve by being sent again, and retrying would turn a clear failure into a
slow one. A give-up message names the orphan tag and says a re-run republishes.

The asset uploads get the same treatment, because a release whose SHA256SUMS
does not cover every binary it advertises is worse than no release -- that file
is the trust anchor for an unsigned download.

Verified by extracting the step's shell from the YAML and running the loop
against a stubbed curl: first-try success, 500-then-success (the case that
actually happened), two 500s then success, five 500s giving up, 403 and 404
aborting without retrying, and a 000 network failure being retried. bash -n
clean and the YAML parses.

Typed ci(...) rather than fix(...) on purpose: the plan step bumps on feat/fix,
and this changes no binary, so a release here would be an empty one.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 11:54:47 -05:00
9cc109910c Merge pull request 'fix(handoff): print the shard screen's real path' (#22) from fix/admin-shard-path into main
Some checks failed
sync-project-tree / sync (push) Successful in 6s
Release installer / release (push) Failing after 4m3s
Reviewed-on: #22
v0.1.1
2026-08-24 16:30:49 +00:00
6da385425e fix(handoff): print the shard screen's real path
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m39s
The end-of-run block told operators to paste the four values at
`<site>/admin/shard`. That page moved when the shard screens became part of the
`uo` module: a module owns one path segment wherever it appears (website
`MODULE_SYSTEM.md` §2.8), so it is `/admin/uo/link`, labelled "Shard (uo-link)".

The old path is worse than a 404. The SPA has no route for it, so it sends the
operator to the dashboard — the link looks like it worked, and the values they
were told to paste have nowhere to go.

- The path is now a named constant, `ADMIN_SHARD_PATH`, carrying why it is not
  the obvious string and the fact that API routes are NOT affected by the module
  namespacing rule (they keep `/api/v1/admin/shard/*`).
- Both handoff tests assert the new path, so this cannot regress quietly.
- The two user-facing labels that name the screen — the `--site-url` help text
  and `update`'s protocol-change instruction — say "Admin → Shard (uo-link)",
  matching what the sidebar actually reads.

Found while writing the runicgateway.com installation journey, by pasting the
printed link into a real deployment and landing on the dashboard.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 11:22:45 -05:00
5d4c68eaf5 Merge pull request 'docs(backup): correct why the sidecar database is not backed up (Teams cutover 3/6)' (#21) from edge into main
All checks were successful
Release installer / release (push) Successful in -51s
sync-project-tree / sync (push) Successful in 9s
Compose bundle / compose (push) Successful in 23s
Reviewed-on: #21
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-19 08:56:06 +00:00
c3771d22f2 Merge pull request 'docs(backup): correct why the sidecar database is not backed up' (#20) from feat/teams-phase1-guild-roster into edge
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m46s
Reviewed-on: #20
2026-08-17 19:28:25 +00:00
6c49217e9c docs(backup): correct why the sidecar database is not backed up
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m49s
backup.rs justified skipping the sidecar's database on two claims. Protocol 4
falsifies one and reveals the other was already wrong.

It said the database is safe because store.rs creates every table IF NOT EXISTS.
That held only while every schema change added a whole table — which, up to and
including Protocol 3.0, every one of them did. Protocol 4 adds a COLUMN to a table
that already exists, which IF NOT EXISTS cannot do, so link now carries a real
migration. A run can change the database's structure, not only its contents.

It also said every table holds state the sweeps repopulate. `events` does not: it
is never pruned, and the website backfills what it missed from GET /history on
every reconnect. So a lost database costs the gap-recovery window for whatever
happened while the site was down. That claim was untrue before this workstream
existed.

The behaviour does not change — the database is still not copied — because the
argument against backing up unbounded bulk survives both corrections: `events`
grows without limit, the migration is transactional and additive, and the website
holds its own durable copy of everything already ingested. Only the reasoning was
wrong, and a wrong reason left in place is what lets the next person extend it to
a case it never covered.

Whether that unbounded table should be pruned or protected belongs to link, on its
own merits, rather than being settled inside a backup policy.

No logic change; docs only.

Refs: docs/website/TEAMS.md Part 12 Phase 1

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 12:58:24 -05:00
7758724eb5 Merge pull request 'docs(readme): describe a released installer, not an unreleased one' (#19) from docs/installer-first-setup into main
All checks were successful
sync-project-tree / sync (push) Successful in 7s
Release installer / release (push) Successful in -35s
Compose bundle / compose (push) Successful in 20s
Reviewed-on: #19
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-07 21:30:39 +00:00
484f00ddee docs(readme): describe a released installer, not an unreleased one
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m17s
The status section still said "Phases 1 to 4 are built, on edge. Nothing
is released yet" and told visitors the way to install was by hand — both
untrue as of v0.1.0.

- Add an "Install a shard with it" section up top: verify, run, paste the
  four values. That is what a visitor to this repo is here for.
- Status: released, all five phases, both cutover gates recorded.
- Fix a broken link: bundles/current.json is no longer on main, it lives
  on the bundles branch.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 16:05:56 -05:00
f81cbcdd04 Merge pull request 'fix(release): actually build the arm64 binary it packages' (#18) from fix/release-arm64-build into main
All checks were successful
sync-project-tree / sync (push) Successful in 7s
Release installer / release (push) Successful in 3m39s
Reviewed-on: #18
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
v0.1.0
2026-08-07 20:35:53 +00:00
007791c4fc fix(release): actually build the arm64 binary it packages
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m56s
The first release attempt failed at packaging:

  cp: cannot stat 'target/aarch64-unknown-linux-gnu/release/runicgateway-installer':
      No such file or directory

installer#10 added linux-aarch64 in three of the four places it belongs — the
rustup target, the `cp` into dist/, and the SHA256SUMS line — but never added a
build step for it. Nothing ever produced the binary, so the run got all the way
to packaging before noticing. No tag or release was created, so a retry is clean.

Two changes:

- Build arm64, with the same linker/CC/AR env pattern the Windows cross build
  already uses.
- Name `libc6-dev-arm64-cross` in the apt install. gcc-aarch64-linux-gnu only
  *recommends* it and this step runs --no-install-recommends, so without it the
  Rust half builds and then `ring` (under ureq's rustls) dies compiling C on a
  missing bits/libc-header-start.h.

Verified by reproducing CI in rust:1-slim-bookworm — the same apt line including
--no-install-recommends, then the same cargo invocation. Builds clean and emits
a 4.6 MB binary at exactly the path the packaging step reads.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 15:30:55 -05:00
1173a10049 Merge pull request 'Cutover: promote the installer from edge to main' (#17) from edge into main
Some checks failed
Release installer / release (push) Failing after 3m21s
sync-project-tree / sync (push) Successful in -22s
Reviewed-on: #17
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-07 20:05:42 +00:00
84c1106d58 Merge branch 'main' into edge
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m59s
Brings main's publishing fixes onto edge so the cutover PR is a clean merge:
bundle.yml publishing to the `bundles` branch, release.yml going tag-only, and
the removal of bundles/*.json from main.

One conflict, resolved in favour of edge: main deleted bundles/bundle-2026.08.04.json
while edge had renamed it to tests/fixtures/published-bundle.json. Both changes say
the same thing — published bundles no longer live on main — so the fixture is kept.
It stays frozen at 2026.08.04 on purpose: it is the crate's test input, not a mirror
of what is currently published.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 14:44:12 -05:00
d6f0bcf2cf Merge pull request 'fix(release): tag only, before the cutover fires this for the first time' (#14) from fix/release-tag-only-installer into main
All checks were successful
Release installer / release (push) Successful in 6s
sync-project-tree / sync (push) Successful in -34s
Reviewed-on: #14
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-07 19:16:47 +00:00
065edab8cd Merge branch 'main' into fix/release-tag-only-installer
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 4s
2026-08-07 19:16:32 +00:00
8e5258358c Merge pull request 'feat(installer): read bundles from the branch they are published to' (#15) from feat/bundle-branch-url into edge
Reviewed-on: #15
2026-08-07 19:16:01 +00:00
09eafe2911 Merge pull request 'fix(bundle): publish to a bundles branch, and unbreak the stale check' (#13) from ci/bundle-require-aarch64 into main
All checks were successful
Release installer / release (push) Successful in 5s
sync-project-tree / sync (push) Successful in -14s
Reviewed-on: #13
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-07 19:12:36 +00:00
7771e2e68d Merge pull request 'fix(service): diagnose 1053 as a handshake, not a bad config' (#16) from fix/service-start-diagnosis into edge
Reviewed-on: #16
2026-08-07 18:52:53 +00:00
b7d1bbbc78 fix(service): diagnose 1053 as a handshake, not a bad config
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m2s
Every failed `sc.exe start` was reported with "a service that exits immediately
usually cannot read its config", which for the one error code that actually
occurs is the wrong place to look. 1053 is the SCM giving up after 30 seconds
waiting for the process to identify itself; the process started fine and is
very likely serving traffic. A reader who follows the old sentence goes and
stares at a config file that is correct.

Replace it with windows_start_failure(), which names the real cause per code:

- 1053: a handshake failure, almost always a sidecar older than v1.2.0 (the
  first release that speaks the SCM protocol). Says how to check the version,
  and how to prove the binary is healthy by running it in the foreground.
- 1069: the virtual service account was refused, which is local policy rather
  than a bad credential, and points at INSTALL.md Appendix A4.
- anything else: does not guess, and hands over the event log, `sc query` for
  the service's own exit code, and the foreground command.

Pure and tested on both platforms, like windows_bin_path above it, so the text
is covered on the Linux CI runner that never sees an SCM.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 13:36:05 -05:00
2787eaadff feat(installer): read bundles from the branch they are published to
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m58s
`main` is protected, so the compose job cannot push a bundle there --
the pre-receive hook declines it, which is not something a nightly cron
can resolve. Bundles now publish to a branch of their own, at its root,
so BUNDLE_BASE follows them. Everything the original choice was for
survives the move: a reviewable diff, a git history of the compat
matrix, and a plain anonymous URL that needs no credentials on the
shard host.

The test's bundle is now a frozen fixture rather than an include of the
published file, which this checkout no longer carries. Frozen is the
honest shape anyway: a test that silently re-targeted whatever CI
published last would change meaning without a commit. It is still a
real CI-emitted document, copied verbatim.

Nothing is released from `edge`, so no shipped binary ever read the old
URL.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 17:20:18 -05:00
ea7e491ba3 fix(release): tag only, before the cutover fires this for the first time
All checks were successful
PR Checks / rust-gates (pull_request) Successful in -35s
The same two faults link/release.yml has, in the copy this repo was
forked from -- and this one has never run at all, so the cutover would
have been its first execution.

An empty template expression written literally in a comment makes the
runner fail to build the "Commit version bump and push tag" step and
skip it WITHOUT failing the job. link carried that for six releases,
which is why its Cargo.toml still says 0.1.0 while its tags reach
v1.1.1; the tags exist because the release API creates one when it
publishes.

And the step pushes to main, which is protected -- the bundle job
proved that today with `pre-receive hook declined`. A first release
must not depend on a write to a protected branch.

So the tag is the version, as in servuo-plugins. The version is still
written into Cargo.toml before building, so a released binary
self-reports correctly; it is simply not committed back.

The prerequisites header said `main` must accept a direct push from the
CI user. It does not, and it should not; that line is replaced with the
reason.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 17:18:42 -05:00
7db58031c7 fix(bundle): publish to a bundles branch, and unbreak the stale check
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 6s
Three things, all found by the first compose run that ever had a bundle
to write.

1. `main` is protected, so the push was declined by the pre-receive
   hook -- twice, since the retry rebases and pushes to the same place.
   Every bundle since v1.1.1 has been composed correctly and thrown
   away. Bundles now go to a `bundles` branch of their own, at its
   root, which needs no protection exception and keeps everything the
   original choice was for: a reviewable diff, a git history of the
   compat matrix, plain anonymous raw URLs, no credentials on the shard
   host. The header's claim that this push "needs no new
   branch-protection exception" was simply false.

2. A `${{ }}` written literally in a shell comment silently disabled
   the entire stale-component check. The runner scans a step's script
   for template expressions before running it, fails to parse the empty
   one, and skips the step WITHOUT failing the job -- so the dispatch
   that is supposed to fire a component's release workflow has never
   run once. Reworded, with a warning not to write that token in a
   comment again. (link/release.yml and this repo's release.yml carry
   the same bug in their bump-and-tag step; handled separately.)

3. linux-aarch64 is now a REQUIRED platform key, which was step 3 of
   PLAN.md §5.2 and was waiting on link publishing one. v1.1.1 does, so
   from here a dropped target reddens this job instead of vanishing
   from every bundle.

The published bundles are materialized into a worktree at `published/`,
so the ".2 suffix" scan and the idempotence check read what is actually
published rather than a stale copy on main. The branch is created from
an empty-tree root commit on first use, so it carries no history that
has nothing to do with the compat matrix; it has been seeded already
with bundle 2026.08.04, because every bundle is kept forever and the
move must not lose the one that exists.

bundles/*.json is deleted from main -- it is now a stale copy of data
that lives elsewhere, and a wrong "current" is worse than none. The
README stays and documents the branch.

Verified by running the whole job in a container against a bare repo
standing in for the remote: first run creates the branch and publishes
both files with all three asset keys, second and third runs report
"identical to the published current.json -- nothing to publish" and
push nothing, and the stale check now runs and reports both components
as having nothing releasable.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 17:14:57 -05:00
07a2cca5f6 Merge pull request 'feat(installer): back up what a run is about to overwrite' (#11) from feat/backup-before-overwrite into edge
Reviewed-on: #11
2026-08-05 17:53:43 +00:00
10bc2224a6 Merge pull request 'feat(installer): build for and install on linux-aarch64' (#10) from ci/installer-linux-aarch64 into edge
Reviewed-on: #10
2026-08-05 17:53:00 +00:00
461192a470 Merge pull request 'docs(readme): describe the tool that exists, not Phase 1' (#12) from docs/readme-and-install-refresh into edge
Reviewed-on: #12
2026-08-05 17:51:31 +00:00
ae53546446 Merge pull request 'ci(bundle): recognize a linux-aarch64 link asset' (#9) from ci/bundle-aarch64-key into main
Some checks failed
sync-project-tree / sync (push) Successful in 6s
Release installer / release (push) Successful in -32s
Compose bundle / compose (push) Failing after -15s
Reviewed-on: #9
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-05 17:51:06 +00:00
c79374ff06 docs(readme): describe the tool that exists, not Phase 1
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m0s
PLAN.md §5.4. The status section still announced Phase 1 as built and
Phase 2 as next, four phases later -- it is the first thing a visitor to
this repo reads, and it has been wrong since Phase 2 merged.

- The phase table now shows 1-4 built on `edge` and 5 in progress, and
  the opening says what the binary actually does.
- "What the cutover is waiting on" is stated, because "nothing is
  released yet" invites the question: Phase 5, and the Windows SCM half
  never having been executed anywhere.
- "Planned commands" is now "Commands". All four are implemented.
- RUNICGATEWAY_STATE_DIR was described as relocating install.json. Since
  Phase 2 it relocates everything the installer writes, including the
  sidecar binary, and suppresses service registration -- an out-of-date
  description of where a tool writes is worse than none.
- A design-constraint bullet for the backup behaviour Phase 5 adds.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 05:52:27 -05:00
82900da939 feat(installer): back up what a run is about to overwrite
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m59s
PLAN.md §5.3. Before anything is written, every file this run will
replace is copied into <state>/backups/<utc-stamp>/ with a manifest
naming where each came from. --no-backup opts out; --verify takes none.

Scoped by what cannot be fetched again. The sidecar binary and the
overlay files are re-downloadable and hash-named in the bundle, and the
database is a cache with a schema -- link's store.rs creates every table
IF NOT EXISTS over shard state the sweeps repopulate. What a run can
destroy for good is an operator's edits to a deployed .cs file, which
Phase 1 overwrites unconditionally and by design, and sidecar.toml,
whose token the website already holds.

Two deviations from §5.3 as written, both found by building it:

- The trigger is "this run is about to overwrite something", not "an
  update, or an install over an existing record". §5.3 justified the
  latter with "a first install overwrites nothing" -- which is not true
  of a tree deployed by hand per INSTALL.md Appendix A2, a documented
  path. There the first install finds .cs files that differ, plans them
  as Change, and overwrites them with no record anywhere. The direct
  test covers that case and still writes nothing for a genuine first
  install, because there is nothing to copy.
- sidecar.toml joins a backup that is already being taken and is never
  the reason for one. Nothing here rewrites it, so making it a trigger
  would put a dated directory on disk after every no-op update; it is
  copied so a restored set of files comes with the token that matches
  them.

The directory is created lazily and the manifest is written last, so a
directory carrying one is a complete backup -- and pruning only
considers those, so a run interrupted mid-copy cannot evict a good
backup by being newer than it. Three are kept. uninstall keeps them and
names them in its report; --purge removes them, alongside the config,
the database and the cached patch set. doctor reports the newest.

Restoring stays printed rather than done, as the uninstall report is:
the installer cannot know what has changed since, and putting an old
.cs file back over a newer overlay eats work rather than saving it.

Verified live against two scratch ServUO trees built from the real 57.4
files: a clean first install leaving no backups directory at all, an
update after editing a deployed .cs (copy holds the edit, tree gets the
release's file, manifest lists both it and sidecar.toml), a no-op update
taking none, --no-backup and --verify each taking none, a fourth backup
pruning the oldest, doctor's row, uninstall keeping three and listing
them, --purge removing them, and a --patches run capturing the
pre-patch Logging.cs while the two rung-0 patches correctly captured
nothing. fmt, clippy -D warnings and 144 tests on both Linux and
Windows.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 05:47:25 -05:00