11 Commits

Author SHA1 Message Date
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>
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
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
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
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
fd59a74912 ci(bundle): recognize a linux-aarch64 link asset
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 5s
Step 1 of PLAN.md §5.2's four, and it has to be first. Two rules in this
job are strict in opposite directions: an unrecognized link asset name
fails the run, and a missing REQUIRED platform key fails it too. So the
name must be taught before the release that carries it, and the key can
only be required after one exists -- requiring it first would fail every
bundle for as long as the gap lasts.

This is therefore the mapping only. linux-aarch64 is not in REQUIRED
yet; step 3 promotes it once a link release actually ships the binary,
after which a dropped target reddens CI instead of vanishing silently
from every bundle.

The compose step needed no change: it builds the asset map from the
platform TSV, so a third key costs it nothing.

Edited on `main` and deliberately not on `edge`. The compose job runs
from `main`, and leaving `edge`'s copy untouched means the eventual
cutover merge has nothing to conflict over.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 05:22:59 -05:00
3 changed files with 154 additions and 62 deletions

View File

@@ -13,15 +13,23 @@
# byte-identical.
#
# ── Where it is published, and why not as a release ──────────────────────────
# Bundles are COMMITTED to this repo under bundles/:
# Bundles are COMMITTED to this repo, on their own `bundles` branch, at its root:
#
# bundles/current.json the bundle the installer uses by default
# bundles/bundle-<tag>.json every bundle ever published, kept for --bundle
# current.json the bundle the installer uses by default
# bundle-<tag>.json every bundle ever published, kept for --bundle
#
# so the installer's two fetches are plain anonymous raw URLs on a public repo:
#
# https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/main/bundles/current.json
# https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/main/bundles/bundle-2026.08.04.json
# https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/bundles/current.json
# https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/bundles/bundle-2026.08.04.json
#
# A BRANCH, not `main`, because `main` is protected and this job is unattended:
# the pre-receive hook declines a push from CI, which is not a thing a nightly
# cron can resolve. Publishing to a branch of its own keeps everything the
# original choice was for — a reviewable diff, a git history of the compat
# matrix, plain raw URLs, no auth on the shard host — and needs no protection
# exception. The alternative, whitelisting a scheduled job for pushes to the
# default branch, buys nothing this does not.
#
# The obvious alternative — one Gitea release per bundle — was rejected because
# it collides with this repo's own product. release.yml publishes the installer
@@ -30,8 +38,8 @@
# intermittently resolve to a release containing no installer binary. Committing
# also gets a reviewable diff and a git history of the compat matrix for free.
#
# The push to `main` needs no new branch-protection exception: release.yml's
# version-bump commit already requires REGISTRY_USER to be able to push here.
# `main` is never pushed to by this workflow. (release.yml does not push to it
# either — it tags and lets the release API do the rest.)
#
# ── Triggers (PLAN.md §7.2) ──────────────────────────────────────────────────
# workflow_dispatch — POSTed by link's and servuo-plugins' release workflows
@@ -86,14 +94,44 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
# Full history: the push step rebases onto main if release.yml's version
# bump landed while this job was composing, and a depth-1 clone has no
# base to rebase onto.
- name: Check out the bundles directory
# Full history: the publish step rebases onto the bundles branch if another
# run landed while this one was composing, and a depth-1 clone has no base
# to rebase onto.
- name: Check out the repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# The published bundles live on their own branch (see the header), so they
# are materialized into a worktree rather than being part of the checkout.
# Everything downstream reads and writes `published/`, which means the
# ".2 suffix" scan and the idempotence check both see what is actually
# published rather than a stale copy on main.
- name: Materialize the bundles branch
run: |
set -euo pipefail
git config user.name "installer-ci"
git config user.email "ci@whitlocktech.com"
# `prune` matters on a re-run in an existing checkout: removing the
# directory leaves the worktree registered, and `worktree add` then
# refuses the path. CI checks out fresh every time, so this only shows
# up when driving the job by hand — which is how it is tested.
rm -rf published
git worktree prune
if git ls-remote --exit-code --heads origin bundles >/dev/null 2>&1; then
git fetch origin bundles
git worktree add -B bundles published origin/bundles
echo "==> bundles branch: $(ls published/*.json 2>/dev/null | wc -l) published bundle(s)"
else
# First run. A root commit with an empty tree gives the worktree a
# branch to sit on without inheriting main's history, which has
# nothing to do with the compat matrix.
EMPTY_TREE="$(git hash-object -t tree /dev/null)"
ROOT="$(git commit-tree "$EMPTY_TREE" -m 'chore(bundle): start the bundles branch')"
git worktree add -B bundles published "$ROOT"
echo "==> bundles branch does not exist yet; it will be created by the first publish"
fi
- name: Install jq and curl
run: |
set -euo pipefail
@@ -173,22 +211,33 @@ jobs:
# Map link's binaries onto platform keys. The pattern is asserted, not
# assumed: an unrecognized asset name is a hard failure so that adding
# a target to link's release.yml (aarch64, macOS) surfaces here as a
# red run, rather than being silently dropped from every bundle.
# a target to link's release.yml (macOS, a Windows arm64) surfaces here
# as a red run, rather than being silently dropped from every bundle.
#
# linux-aarch64 was recognized here one merge BEFORE link published one
# (PLAN.md §5.2, steps 1 and 3). That order was forced by the two rules
# below being strict in opposite directions: an unknown name fails the
# run, and a missing REQUIRED key fails it too. So the name had to be
# taught before the release that carried it, and the key could only be
# required after — requiring it first would have failed every bundle
# for as long as the gap lasted. link v1.1.1 ships the binary, so the
# key is now required: a dropped target reddens this job instead of
# vanishing from every bundle.
: > work/link-platforms.tsv
while IFS="$(printf '\t')" read -r NAME URL; do
[ -n "$NAME" ] || continue
case "$NAME" in
*-linux-x86_64) PLAT=linux-x86_64 ;;
*-linux-aarch64) PLAT=linux-aarch64 ;;
*-windows-x86_64.exe) PLAT=windows-x86_64 ;;
*) fail "unrecognized link asset '${NAME}' — bundle.yml does not know what platform to file it under. Teach it this name or the bundle would silently omit the asset." ;;
esac
printf '%s\t%s\t%s\t%s\n' "$PLAT" "$NAME" "$URL" \
"$(sha256sum "work/link/${NAME}" | cut -d' ' -f1)" >> work/link-platforms.tsv
done < work/link/asset-list.tsv
for REQUIRED in linux-x86_64 windows-x86_64; do
for REQUIRED in linux-x86_64 linux-aarch64 windows-x86_64; do
grep -q "^${REQUIRED}$(printf '\t')" work/link-platforms.tsv \
|| fail "link release is missing a ${REQUIRED} binary; the installer ships for both"
|| fail "link release is missing a ${REQUIRED} binary; the installer ships for all three"
done
# The overlay release carries exactly one artifact: the tarball.
@@ -330,8 +379,8 @@ jobs:
# commit a dated duplicate of the same matrix forever. Compare only
# what the installer would actually act on.
CHANGED=true
if [ -f bundles/current.json ]; then
if jq -S 'del(.bundle, .generated)' bundles/current.json > work/old-content.json \
if [ -f published/current.json ]; then
if jq -S 'del(.bundle, .generated)' published/current.json > work/old-content.json \
&& jq -S '.' work/content.json > work/new-content.json \
&& cmp -s work/old-content.json work/new-content.json; then
CHANGED=false
@@ -340,7 +389,7 @@ jobs:
echo "changed=${CHANGED}" >> "$GITHUB_OUTPUT"
if [ "$CHANGED" = false ]; then
echo "==> identical to bundles/current.json — nothing to publish."
echo "==> identical to the published current.json — nothing to publish."
exit 0
fi
@@ -350,15 +399,14 @@ jobs:
# always names exactly one matrix and `--bundle` stays reproducible.
BASE="$(date -u +%Y.%m.%d)"
TAG="$BASE"; N=1
while [ -f "bundles/bundle-${TAG}.json" ]; do
while [ -f "published/bundle-${TAG}.json" ]; do
N=$((N+1)); TAG="${BASE}.${N}"
done
mkdir -p bundles
jq --arg bundle "$TAG" --arg generated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{ schema: .schema, bundle: $bundle, generated: $generated } + del(.schema)' \
work/content.json > "bundles/bundle-${TAG}.json"
cp "bundles/bundle-${TAG}.json" bundles/current.json
work/content.json > "published/bundle-${TAG}.json"
cp "published/bundle-${TAG}.json" published/current.json
echo "bundle_tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "==> composed bundle ${TAG}"
@@ -387,9 +435,11 @@ jobs:
set -euo pipefail
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN:-}" | tr -d '\r\n')"
# Warnings go to a FILE, not a step output. The job summary below
# reads it with `cat`; interpolating a multi-line `${{ }}` value into
# reads it with `cat`; interpolating a multi-line template value into
# a shell string there would let any character in a commit-derived
# message change what that script does.
# message change what that script does. (Do not write that token
# literally in a comment: the runner parses it, fails, and silently
# skips the whole step.)
: > work/stale-warnings.md
for pair in "${LINK_REPO}:${{ steps.resolve.outputs.link_tag }}" \
@@ -470,27 +520,25 @@ jobs:
# cannot be parsed").
CI_USER="$(printf '%s' "${REGISTRY_USER}" | tr -d '\r\n')"
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')"
git config user.name "installer-ci"
git config user.email "ci@whitlocktech.com"
git remote set-url origin "https://${CI_USER}:${CI_TOKEN}@${GITEA_HOST}/${REPO}.git"
git add bundles
cd published
git add -A
git commit -m "chore(bundle): publish ${TAG} (link ${{ steps.resolve.outputs.link_tag }}, overlay ${{ steps.resolve.outputs.overlay_tag }}, protocol ${{ steps.protocol.outputs.protocol }}) [skip ci]"
# The checkout is a detached snapshot of main; push the commit at HEAD
# to the branch the installer reads its raw URLs from. release.yml
# pushes its version-bump commit to the same branch, so losing the
# race is normal rather than exceptional — rebase and retry once
# instead of failing and leaving the bundle unpublished until the
# next cron. Only bundles/ is touched here, so a rebase over a bump
# commit cannot conflict.
if ! git push origin "HEAD:main"; then
echo "::warning::push rejected (main moved during compose) — rebasing and retrying once"
git fetch origin main
git rebase origin/main
git push origin "HEAD:main"
# Two runs can compose at once — a component release dispatches this
# while the nightly cron is mid-flight — so losing the race is normal
# rather than exceptional. Rebase and retry once instead of failing and
# leaving the bundle unpublished until tomorrow. Every file here is a
# bundle nobody else edits, and a bundle tag names exactly one matrix,
# so a rebase cannot conflict.
if ! git push origin bundles; then
echo "::warning::push rejected (the bundles branch moved during compose) — rebasing and retrying once"
git fetch origin bundles
git rebase origin/bundles
git push origin bundles
fi
echo "==> published bundles/bundle-${TAG}.json and bundles/current.json"
echo "==> published bundle-${TAG}.json and current.json on the bundles branch"
- name: Job summary
if: always()

View File

@@ -43,11 +43,11 @@
# Prerequisites (Settings → Actions → Secrets on RunicGateway/installer):
# REGISTRY_USER — Gitea username the token below belongs to
# REGISTRY_TOKEN — Gitea access token with `write:repository`, so it can push
# the bump commit + tag and create the release.
# Also: `main` must accept a direct push from that user (disable branch
# protection for it, or add it as an exception) — the bump commit lands on main.
# the release tag and create the release.
#
# The bump commit carries `[skip ci]`, so it does not re-trigger this workflow.
# `main` needs NO push exception: this workflow tags and publishes, and never
# writes to a branch. Keeping it that way is deliberate — a first release that
# depends on a write to a protected branch fails at the worst possible moment.
name: Release installer
@@ -73,7 +73,8 @@ env:
jobs:
release:
runs-on: ubuntu-latest
# Don't loop on our own bump commit (belt-and-suspenders with [skip ci]).
# Vestigial since this workflow stopped writing a bump commit, and kept as
# belt-and-braces in case one ever returns.
# Quoted because the expression contains a colon (`chore(release):`), which an
# unquoted YAML scalar would misparse as a mapping value.
if: "${{ !contains(github.event.head_commit.message, 'chore(release): bump version') }}"
@@ -246,8 +247,13 @@ jobs:
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
$SUDO apt-get update
# libc6-dev-arm64-cross is named explicitly on purpose: gcc-aarch64-linux-gnu only
# *recommends* it, and this install runs --no-install-recommends. Without it the Rust
# half of the arm64 build succeeds and then `ring` (under ureq's rustls) dies compiling
# C, on a missing bits/libc-header-start.h.
$SUDO apt-get install -y --no-install-recommends \
build-essential gcc-mingw-w64-x86-64 curl ca-certificates git jq
build-essential gcc-mingw-w64-x86-64 gcc-aarch64-linux-gnu libc6-dev-arm64-cross \
curl ca-certificates git jq
if ! command -v cargo >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
@@ -295,6 +301,18 @@ jobs:
AR_x86_64_pc_windows_gnu: x86_64-w64-mingw32-ar
run: cargo build --release --locked --target "${WINDOWS_TARGET}"
# The installer has to run wherever the sidecar it installs can run, and link publishes an
# arm64 Linux binary (PLAN.md §5.2). Without this step the target is installed and the
# artifact is packaged, but nothing ever builds it — which is exactly how the first release
# attempt failed, at `cp: cannot stat target/aarch64-unknown-linux-gnu/release/...`.
- name: cargo build --release (Linux arm64, cross)
if: ${{ steps.plan.outputs.release == 'true' }}
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
AR_aarch64_unknown_linux_gnu: aarch64-linux-gnu-ar
run: cargo build --release --locked --target "${ARM64_TARGET}"
# ── RUST ADAPTER: package artifacts (+ checksums) ────────────────────
# SHA256SUMS is the trust anchor for these unsigned binaries (PLAN.md §3),
# so it ships with every release and the docs lead with the verify command.
@@ -312,33 +330,45 @@ jobs:
ls -l dist && echo "----" && cat dist/SHA256SUMS
# ── RELEASE ENGINE: commit the bump, tag, push ───────────────────────
- name: Commit version bump and push tag
# Tag only — `main` is never pushed to.
#
# This step used to commit the version bump back to main first, and it has
# never executed in any repo that carries it: an EMPTY template expression
# written literally in the comment below (the `$`+`{{ }}` token, spelled
# out here for that reason) makes the runner fail to build the script and
# skip the step WITHOUT failing the job. link/release.yml carried the same
# bug for six releases, which is why its Cargo.toml still says 0.1.0 while
# its tags reach v1.1.1 — the release API creates the tag when it
# publishes, so the pipeline worked by accident.
#
# It also would have been declined if it had run: `main` is protected, and
# the bundle job proved that on 2026-08-05 (`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 next
# version is computed from the newest tag, never from the file.
- name: Push the release tag
if: ${{ steps.plan.outputs.release == 'true' }}
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -euo pipefail
VERSION="${{ steps.plan.outputs.version }}"
TAG="${{ steps.plan.outputs.tag }}"
# Secrets can arrive with a trailing newline (depending on how they were
# pasted); a stray CR/LF corrupts the remote URL ("credential url cannot
# be parsed"). Strip line breaks before building the URL. Passing them via
# env (not inline ${{ }}) also keeps a newline from breaking this script.
# be parsed"). Strip line breaks before building the URL. They are passed
# via env rather than interpolated into this script, so a newline cannot
# break it — do NOT write a template token literally in a comment here,
# or the runner will skip this step without failing the job.
CI_USER="$(printf '%s' "${REGISTRY_USER}" | tr -d '\r\n')"
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')"
git config user.name "installer-ci"
git config user.email "ci@whitlocktech.com"
git remote set-url origin \
"https://${CI_USER}:${CI_TOKEN}@${GITEA_HOST}/${REPO}.git"
git add Cargo.toml Cargo.lock
if ! git diff --cached --quiet; then
git commit -m "chore(release): bump version to ${TAG} [skip ci]"
git push origin "HEAD:main"
else
echo "Version unchanged (first release) — no bump commit needed."
fi
# The tag may already exist when finishing a run that died after
# tagging (see the plan step). `git tag` on an existing name fails
# under `set -e`; pushing an identical existing tag is a harmless

View File

@@ -13,7 +13,10 @@ nightly, so a missed dispatch self-heals. A run that finds nothing changed write
See `docs/installer/PLAN.md` §7 for the design.
## Layout
## Where they live: the `bundles` branch
**The JSON documents are not in this directory.** They are published to a branch of their own,
[`bundles`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/bundles), at its root:
| File | What it is |
|---|---|
@@ -24,14 +27,24 @@ Tags are UTC dates — `2026.08.04`. A second bundle on the same day (a sidecar
morning, an overlay release in the afternoon) becomes `2026.08.04.2`, so one tag always names
exactly one matrix.
**Why a branch rather than `main`.** `main` is protected and this job is unattended: the pre-receive
hook declines a push from CI, which is not something a nightly cron can resolve. A branch of its own
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 — and needs no protection
exception. Whitelisting a scheduled job for pushes to the default branch would buy nothing this does
not.
This directory keeps the documentation, because that is what belongs on `main`: the branch carries
data, and only data.
## How the installer fetches these
Plain anonymous `GET`s against a public repo. The shard host gets no git and no Gitea credentials
(`PLAN.md` §1), so nothing here may require auth:
```
https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/main/bundles/current.json
https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/main/bundles/bundle-2026.08.04.json
https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/bundles/current.json
https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/bundles/bundle-2026.08.04.json
```
Bundles are committed rather than published as Gitea releases because this repo's *own* releases are
@@ -56,8 +69,9 @@ protocol) or to either component's release version. All three move independently
"tag": "v1.1.0",
"version": "1.1.0",
"protocol": 3,
"assets": { // per-platform: the installer runs on both
"assets": { // per-platform: the installer runs on each
"linux-x86_64": { "name": "…", "url": "…", "sha256": "…" },
"linux-aarch64": { "name": "…", "url": "…", "sha256": "…" },
"windows-x86_64": { "name": "…", "url": "…", "sha256": "…" }
}
},