827de044713a7eeeda67a958c55a6181a6ff2053
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 8195454201 |
ci(release): show the error body, retry the POST, and sweep for orphan tags
installer#22's release run built every artifact, pushed its tag, then took a 500
from POST /releases one second later and exited 22 -- leaving the tag orphaned
with no binaries. Re-running published it unchanged, so the 500 was a race with
the tag push rather than a bad request.
This repo's release step has the same two gaps verbatim, and it is where the
whole problem was first seen.
`curl -sSf` prints no response body on an error status, so such a failure leaves
only "curl: (22) ... error: 500" in the log and the cause has to be inferred
from timestamps. Every call now captures the body and prints it on failure.
Nothing retried, so a transient 5xx became a permanent orphan. 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.
The asset upload gets the same treatment. That matters more here than anywhere
else: this release ships an overlay tarball and a SHA256SUMS, and a release
whose checksums do not cover the tarball they advertise is worse than no
release, because that file is the trust anchor and the installer verifies
against it.
The third gap is the one this repo proves. The orphan-tag recovery in the plan
step is VERSION-SCOPED -- it computes VERSION from the newest tag plus the bump,
then only checks refs/tags/v${VERSION}. That recovers an orphan on the very next
run and is useless afterwards, because once any releasable commit lands the next
run computes a NEW version and never looks at the old tag again.
v0.1.0 was the proof, and the proof is pointed: the commit that ADDED that
recovery was itself typed "fix(release): preflight credentials and recover the
orphaned v0.1.0 tag", so it bumped the version to v0.1.1 -- and the run that
introduced the recovery stepped straight past the tag it was written to rescue.
The tag stayed orphaned from 2026-08-04 until today.
So the plan step now sweeps every v* tag and warns about any without a release.
It warns rather than recovers, deliberately: 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. It never fails the run either --
a sweep that can break a good release is a sweep someone will delete.
v0.1.0 itself is deleted, on the org lead's decision. Nothing referenced it: it
is three releases behind, and no published bundle names it -- not even
bundle-2026.08.04, because the tag never had a release for a bundle to point at.
It was
|
|||
| 8828382e41 |
feat(patches): declare the patch tier in tier.json and the manifest
A .patch file does not carry enough for an installer to run the tier safely. The installer additionally needs to know which patches form one all-or-nothing unit (the two vendor-sale patches are useless apart), which companion .cs may only be copied once that unit has landed, whether the change needs a core solution rebuild or just ServUO's dynamic script build, and what capability an operator loses by declining. None of that is derivable from the diffs. patches/tier.json declares it, and release.yml folds it into manifest.json as `patch_tier` — so a new or changed patch regenerates release metadata rather than requiring an installer release, which is the same rule §7.1 already applies to the bundle. The staged copy is removed from patches/ so the tarball carries exactly one statement of the table. The release gate now checks the table in both directions: every .patch described by exactly one feature, every named patch and companion present, every declared target equal to the file the diff actually edits, and every rebuild kind one the installer understands. All four were previously invisible until someone ran the tier on a live shard. Refs: docs/installer/PLAN.md §2.2, §7.0 Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 4720a214a2 |
ci(release): recompose the installer bundle after publishing
Phase 0 item 3 of docs/installer/PLAN.md wired up from this side. The installer does not resolve "latest" at run time — it installs the exact combination named by a published bundle manifest (PLAN.md §7.1), so until now a new overlay release was invisible to operators until the installer repo's nightly cron noticed it. Adds a final step that POSTs to RunicGateway/installer's bundle workflow-dispatch endpoint. That job re-reads this tarball's manifest.json and checks its declared `protocol` against the sidecar's PROTOCOL_VERSION before publishing anything (gate 1) — the check this repo cannot perform for itself, since the C# plugin announces no version on the wire. It replaces the TODO the header has carried since #7, which was deliberately left unimplemented while there was nothing to dispatch. Dispatch, don't wait (PLAN.md §7.3): Gitea's dispatch endpoint returns no run handle, so there is nothing to poll — a waiting step would have to guess which run is its own while holding a runner idle. The bundle job runs its own gates regardless of who started it. A dispatch failure is a warning, never a failure of this job. By the time this step runs the release is published and correct, so failing the run would misreport that; the installer's nightly cron recomposes from whatever the latest releases actually are, making a dropped dispatch cost latency rather than correctness. That also means REGISTRY_TOKEN having write on the installer repo is a nicety, not a new hard requirement — noted in the header. Verified the workflow still parses and that the new step is last, gated on release=='true', and contains no path that can exit non-zero. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| eebc74ac8d |
fix(release): preflight credentials and recover the orphaned v0.1.0 tag
The first release run tagged the repo and then failed, leaving v0.1.0 with no release behind it and no way to ever get one. REGISTRY_USER and REGISTRY_TOKEN are not configured on this repo, but the tag push SUCCEEDED anyway: actions/checkout leaves an `http.<host>.extraheader` credential in the local git config, so `git remote set-url` to a URL with empty credentials still authenticated through that leftover header. The release API call had no such fallback and returned 401 (visible in the run log as `REGISTRY_USER:` / `REGISTRY_TOKEN:` with empty values). So the run got exactly far enough to do the one thing that is hard to undo. Worse, that state was self-perpetuating. The plan step treated any existing tag as "nothing to release", so every subsequent push to main would see v0.1.0, set RELEASE=false, and stand down — the release would never appear, and no amount of re-running would fix it. Two fixes: A credential preflight, before anything is built or pushed, gated on the run intending to publish so a docs:/chore:-only merge still passes on a repo without secrets. It names the missing secrets and the scope they need, rather than failing at whichever step happens to use them first. Orphan-tag recovery. The plan step now asks the API whether a release exists for the tag: 200 means stand down, 404 means an earlier run died after tagging, so reuse the tag and publish the release it is missing. This deliberately overrides the RELEASE=false the bump logic just decided — with the tag already in place there are no releasable commits after it, which is precisely why the stuck state could not clear itself. Anything other than 200/404 (network failure, bad token) is refused rather than guessed, because assuming "no release" would republish over a good one. The tag step reuses an existing tag instead of failing on `git tag`, and a recovery run's changelog summarizes what the tag contains (previous-tag..this-tag) instead of the empty range after it. Once REGISTRY_USER / REGISTRY_TOKEN are set, the next push to main will finish the release that the first run started — v0.1.0, from the same commit it already points at. Verified against the live repo state: the plan step now reports release=true reuse_tag=true for the orphaned v0.1.0 and renders the correct changelog; a tag that does have a release (checked against link's v0.3.0) still stands down; a fresh repo still takes the seed path; and the preflight fails loudly on empty secrets and passes on populated ones. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| ebbfab51fc |
ci(release): publish the overlay as a release tarball with a manifest
Phase 0 item 1 of the installer plan (docs/installer/PLAN.md §5). The
installer deploys the plugin from a release tarball rather than from git,
because the shard host gets neither git nor Gitea credentials — but this
repo published no releases at all, so there was nothing for it to fetch.
`link` was the only repo with a release workflow.
Reuses link/.gitea/workflows/release.yml's conventional-commit engine, as
that file's own header anticipated: the plan and release steps consume only
{version, changelog, artifacts}. Three things had to change, each forced by
this repo rather than chosen:
No build. The plugin ships as C# source and ServUO compiles it at boot;
it needs ServUO reference assemblies, so nothing here can be compiled in
CI. The build gates are replaced by structural ones that assert what can
honestly be asserted without a ServUO tree: Bridge.cfg and the Bridge
scripts are present, Scripts.csproj (the silent-build-bug fix) is present,
every .patch parses as a unified diff via `git apply --stat`, and each
patch's companion .cs exists. Each of those has a way of shipping broken
and only failing on an operator's live shard.
No bump commit, so no push to main. link writes the version into
Cargo.toml because the binary embeds it; a tarball embeds nothing but the
manifest CI generates, so the git tag is the version. This workflow
therefore never needs main to accept a direct push — no branch-protection
exception for it.
A manifest. The tarball carries manifest.json: version, commit, declared
protocol version, ServUO compatibility, and a SHA256 per shipped file.
The manifest matters more than it looks. The plugin announces no version on
the wire and none is queryable before ServUO boots (PLAN.md §2.6), so its
declared protocol version is the ONLY thing that lets the installer's bundle
CI verify sidecar/overlay agreement before an operator installs the pair
(PLAN.md §7.1 gate 1). That declaration lives in the new overlay.toml
alongside the ServUO compatibility values, so it is one commented line to
maintain rather than a literal buried in a workflow — currently protocol 3,
per docs/link/v3.md.
Tarball layout uses a FIXED top-level directory (runicgateway-overlay/)
rather than a versioned one, so the installer can find overlay/, patches/
and manifest.json at known paths instead of parsing the version it is trying
to read. tar's member order, mtime and ownership are pinned, so a given tree
produces a byte-identical tarball and its checksum changes only when the
contents do.
Verified locally against the real tree before pushing: YAML parses, all six
run blocks pass bash -n, the plan step produces v0.1.0 from actual history,
the gates pass (22 bridge scripts, all three patches parse), the manifest
renders with protocol=3 and 30 file hashes, and two consecutive builds of
the tarball produce the same SHA256.
One real bug caught by running it rather than reading it: sha256sum marks
binary mode by prefixing the path with `*` instead of the two-space
text-mode separator, which would have put a leading `*` on every key in the
manifest. The capture now tolerates both.
Not included: the workflow-dispatch call into the installer's bundle CI
(PLAN.md §7.2). That is Phase 0 item 3 and there is nothing to dispatch yet;
the insertion point is marked in the header. A step that 404s on every
release is worse than no step.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
|
|
4e8be1a086 |
chore: add open-source governance files (GPLv3 + contributing docs)
Add standard open-source project files: - LICENSE.md — GNU GPL v3.0 or later (verbatim) - CONTRIBUTING.md — setup, workflow, and required AI-usage disclosure - CONTRIBUTORS.md — maintainers, contributors, AI-assistance policy - CODE_OF_CONDUCT.md — Contributor Covenant 2.1 - SECURITY.md — private vulnerability reporting - .gitea/ISSUE_TEMPLATE/* + PULL_REQUEST_TEMPLATE.md - README: License section (Copyright (C) 2026 Runic Gateway) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmHdsbnLzDMAVQkAoTQSBe |