channels: the dev channel publishes on dev-rolling, so its tag stops shadowing the branch
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python tests (push) Successful in 10s
CI & Build / integration (push) Successful in 44s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 3m20s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m20s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 9m13s

The rolling dev release lived on a tag named `dev`, beside the branch
named `dev`. Once a clone had fetched tags, `git push origin dev` failed
with "src refspec dev matches more than one" (Scribe #2184, note #3042),
and every session had to know to spell out refs/heads/dev.

The channel is still `dev` everywhere a person sees it: the app's
setting, `install.sh --channel dev`, the stored pref. Only the release
tag moves, to `dev-rolling`, matching roundtable-android. `stable` has no
branch to collide with and keeps its name.

- packaging/channel-tag.sh is the one channel -> tag mapping CI reads:
  the publish steps in android.yml and desktop.yml, the manifest job,
  fetch-clients.sh and guard-forward.sh. guard-forward exits 2 on an
  unmapped channel instead of fetching an empty URL and passing.
- update.rs and install.sh carry their own copy because neither can run
  it; update.rs gains a test that no channel feed is named like a branch.
- tests/test_channel_tag.py runs the script: no tag is a branch name,
  dev is exactly dev-rolling, an unknown channel fails with no output.
- publish-release.sh titles the release "ThoughtSync dev (rolling)", so
  the tag name does not leak into what people read.

TEMPORARY bridge: desktop apps installed before this have
.../download/dev/latest.json compiled in. The dev manifest job sets
BRIDGE_TAG=dev, and write-manifest.sh writes the same latest.json to
the old `dev` release. Its URLs name dev-rolling assets, so those apps
update once into a build that reads the new tag. The bridge, and the old
release and tag, are removed once installed apps have crossed over.
Until then the push still needs the explicit refspec, as
ci-requirements.md now says.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwoKYuw3qJmUUYsJeNherB
This commit is contained in:
2026-09-10 18:34:11 -04:00
co-authored by Claude Opus 5
parent 53d51ce01c
commit 72968897ab
12 changed files with 227 additions and 33 deletions
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env sh
#
# The release tag that holds a channel's builds.
#
# channel-tag.sh <dev|stable>
#
# A channel and its release tag are two different names, and for `dev` they differ.
# The channel is what a person picks and sees (`install.sh --channel dev`, the app's
# settings); the tag is where CI publishes and where every reader fetches from.
#
# `dev` used to be both. A tag named `dev` shadowed the branch named `dev`, so once
# a clone had fetched tags, `git push origin dev` failed with "src refspec dev
# matches more than one" (Scribe #2184). The tag is `dev-rolling` now, matching
# roundtable-android. Never spell a channel tag like a branch — tests/test_channel_tag.py
# fails if one is.
#
# The ONE mapping CI reads. Two copies exist because their readers cannot run this:
# `desktop/src-tauri/src/update.rs` (compiled into the app) and
# `desktop/packaging/install.sh` (fetched alone through a pipe). Change all three.
set -eu
case "${1:-}" in
dev) echo dev-rolling ;;
stable) echo stable ;;
*)
echo "channel-tag.sh: unknown channel '${1:-}' (expected dev or stable)" >&2
exit 2
;;
esac
+4 -1
View File
@@ -38,7 +38,10 @@ esac
SERVER="${GITHUB_SERVER_URL:-https://git.fabledsword.com}"
REPO="${GITHUB_REPOSITORY:-bvandeusen/thoughtsync}"
BASE="$SERVER/$REPO/releases/download/$channel"
# The channel's release tag, not its name — `dev` lives on `dev-rolling`
# (packaging/channel-tag.sh, Scribe #2184).
tag="$(sh "$(dirname "$0")/channel-tag.sh" "$channel")"
BASE="$SERVER/$REPO/releases/download/$tag"
mkdir -p "$dest"
+9 -3
View File
@@ -96,16 +96,21 @@ fetch() {
# merge to `main` (run 4857): exit 1, no output, 0.16 seconds, on the one channel that
# had no APK published yet. Its three neighbours hid it until then.
published_for() {
# The channel's release TAG, not its name — `dev` lives on `dev-rolling`
# (packaging/channel-tag.sh, Scribe #2184). `|| exit 2` rather than letting an
# empty tag through: a lookup against `.../download//latest.json` would find
# nothing and read as "nothing published", which this guard PASSES.
_tag="$(sh "$ROOT/packaging/channel-tag.sh" "$2")" || exit 2
case "$1" in
desktop)
# What the UPDATER reads. The manifest is the thing that decides whether a
# client is offered a build, so it is the authority on what is published.
{ fetch "$SERVER/$REPO/releases/download/$2/latest.json" \
{ fetch "$SERVER/$REPO/releases/download/$_tag/latest.json" \
| grep -oE '"version"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1 \
| sed -E 's/.*"([^"]+)"$/\1/'; } || true
;;
android)
{ fetch "$SERVER/$REPO/releases/download/$2/thoughtsync-android.json" \
{ fetch "$SERVER/$REPO/releases/download/$_tag/thoughtsync-android.json" \
| grep -oE '"version_code"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 \
| grep -oE '[0-9]+$'; } || true
;;
@@ -117,10 +122,11 @@ published_for() {
# compares identity — for Android those are different fields, and conflating them
# would make every build look like a change (the code is build-time; it always moves).
published_name() {
_tag="$(sh "$ROOT/packaging/channel-tag.sh" "$2")" || exit 2
case "$1" in
desktop) published_for desktop "$2" ;;
android)
{ fetch "$SERVER/$REPO/releases/download/$2/thoughtsync-android.json" \
{ fetch "$SERVER/$REPO/releases/download/$_tag/thoughtsync-android.json" \
| grep -oE '"version_name"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1 \
| sed -E 's/.*"([^"]+)"$/\1/'; } || true
;;