diff --git a/.forgejo/workflows/android.yml b/.forgejo/workflows/android.yml index e7e922b..c83d67f 100644 --- a/.forgejo/workflows/android.yml +++ b/.forgejo/workflows/android.yml @@ -199,19 +199,29 @@ jobs: JSON cat dist/thoughtsync-android.json - # The rolling dev channel, same fixed-tag release the desktop bundles use. - # CI artifacts are per-run and auth-gated, so they are no use as a fetch - # target; a release asset has a permanent URL. Only ever a SIGNED build — - # publishing an unsigned APK would offer people something they cannot - # install over what they already have. - - name: Publish to the dev channel - if: github.ref == 'refs/heads/dev' && steps.build.outputs.keystore != '' + # The rolling channel for this branch, the same fixed-tag releases the desktop + # bundles use. CI artifacts are per-run and auth-gated, so they are no use as a + # fetch target; a release asset has a permanent URL. Only ever a SIGNED build — + # publishing an unsigned APK would offer people something they cannot install + # over what they already have. + # + # `stable` from main is new in M314 step 3, and it is what lets the server image + # bake in a client that matches its own channel: a :latest image fetches the APK + # from `stable`, a :dev image from `dev`. Before this, main published no APK at + # all and every image — stable included — baked in the dev one. + - name: Publish to the channel for this branch + if: (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && steps.build.outputs.keystore != '' working-directory: . env: GITHUB_TOKEN: ${{ github.token }} - RELEASE_TAG: dev - RELEASE_PRERELEASE: "true" - run: bash desktop/packaging/publish-release.sh + run: | + case "$GITHUB_REF_NAME" in + main) RELEASE_TAG=stable; RELEASE_PRERELEASE=false ;; + *) RELEASE_TAG=dev; RELEASE_PRERELEASE=true ;; + esac + export RELEASE_TAG RELEASE_PRERELEASE + echo "Publishing the APK to the $RELEASE_TAG channel." + bash desktop/packaging/publish-release.sh - name: Upload the APK # Mirrored action, never actions/upload-artifact. @v4+ throws diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 5b7c82e..1a9817e 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -330,7 +330,18 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: | mkdir -p client - base="${{ github.server_url }}/${{ github.repository }}/releases/download/dev" + # THE CHANNEL IS A PROPERTY OF THE IMAGE. A :dev image serves the dev + # client; :latest serves the stable one. This read `download/dev` + # unconditionally until M314 step 3, on every branch — so every stable + # server shipped a dev-channel APK to anyone who downloaded the client + # from it. Not a versioning gap; a plain defect, fixed here because this + # is the step that gave `stable` an APK to point at. + case "${{ github.ref_name }}" in + main|v*) channel=stable ;; + *) channel=dev ;; + esac + echo "Baking in the $channel client." + base="${{ github.server_url }}/${{ github.repository }}/releases/download/$channel" ok=1 for f in thoughtsync.apk thoughtsync-android.json; do curl -fsSL -H "Authorization: token $GITHUB_TOKEN" -o "client/$f" "$base/$f" || ok=0 diff --git a/.forgejo/workflows/desktop.yml b/.forgejo/workflows/desktop.yml index ff5cd1f..a7a13a4 100644 --- a/.forgejo/workflows/desktop.yml +++ b/.forgejo/workflows/desktop.yml @@ -216,26 +216,40 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: bash desktop/packaging/publish-release.sh - # The rolling DEVELOPMENT channel (M10.9): a release whose tag never moves, so - # the updater has a permanent URL to read — Forgejo has no - # /releases/latest/download/ route, so "newest" can't be named in a URL. + # The rolling channel for this branch: `dev` from dev, `stable` from main. Both + # are releases whose tag never moves, so the updater has a permanent URL to + # read — Forgejo has no /releases/latest/download/ route, so "newest" + # cannot be named in a URL. + # + # MAIN PUBLISHING HERE is what makes a `v*` tag optional (note 3127 §0). Until + # M314 step 3 this job built on main and published nothing, so the stable + # channel moved only when somebody cut a tag — that section's diagnostic + # failing outright: main publishing was not sufficient for a user to receive + # the build. # # Gated on the signing key INSIDE the script rather than with an `if:`, because # the secrets context isn't reliably available to step conditions. Publishing # bundles the app would then refuse to verify is worse than publishing nothing: # it looks like a working feed. - - name: Publish to the dev channel - if: github.ref == 'refs/heads/dev' + - name: Publish to the channel for this branch + if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' env: GITHUB_TOKEN: ${{ github.token }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - RELEASE_TAG: dev - RELEASE_PRERELEASE: "true" run: | if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then - echo "No TAURI_SIGNING_PRIVATE_KEY — skipping the dev channel publish." + echo "No TAURI_SIGNING_PRIVATE_KEY — skipping the channel publish." exit 0 fi + # POSIX `case`, not bash `[[ ]]` — these run under busybox sh (rule 81). + # `prerelease` is true for dev so it does not read as a supported build, + # and false for stable, which is the real thing. + case "$GITHUB_REF_NAME" in + main) RELEASE_TAG=stable; RELEASE_PRERELEASE=false ;; + *) RELEASE_TAG=dev; RELEASE_PRERELEASE=true ;; + esac + export RELEASE_TAG RELEASE_PRERELEASE + echo "Publishing to the $RELEASE_TAG channel." bash desktop/packaging/publish-release.sh # Windows installer, CROSS-COMPILED from Linux — there is no Windows build host. @@ -326,26 +340,40 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: bash desktop/packaging/publish-release.sh - # The rolling DEVELOPMENT channel (M10.9): a release whose tag never moves, so - # the updater has a permanent URL to read — Forgejo has no - # /releases/latest/download/ route, so "newest" can't be named in a URL. + # The rolling channel for this branch: `dev` from dev, `stable` from main. Both + # are releases whose tag never moves, so the updater has a permanent URL to + # read — Forgejo has no /releases/latest/download/ route, so "newest" + # cannot be named in a URL. + # + # MAIN PUBLISHING HERE is what makes a `v*` tag optional (note 3127 §0). Until + # M314 step 3 this job built on main and published nothing, so the stable + # channel moved only when somebody cut a tag — that section's diagnostic + # failing outright: main publishing was not sufficient for a user to receive + # the build. # # Gated on the signing key INSIDE the script rather than with an `if:`, because # the secrets context isn't reliably available to step conditions. Publishing # bundles the app would then refuse to verify is worse than publishing nothing: # it looks like a working feed. - - name: Publish to the dev channel - if: github.ref == 'refs/heads/dev' + - name: Publish to the channel for this branch + if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' env: GITHUB_TOKEN: ${{ github.token }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - RELEASE_TAG: dev - RELEASE_PRERELEASE: "true" run: | if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then - echo "No TAURI_SIGNING_PRIVATE_KEY — skipping the dev channel publish." + echo "No TAURI_SIGNING_PRIVATE_KEY — skipping the channel publish." exit 0 fi + # POSIX `case`, not bash `[[ ]]` — these run under busybox sh (rule 81). + # `prerelease` is true for dev so it does not read as a supported build, + # and false for stable, which is the real thing. + case "$GITHUB_REF_NAME" in + main) RELEASE_TAG=stable; RELEASE_PRERELEASE=false ;; + *) RELEASE_TAG=dev; RELEASE_PRERELEASE=true ;; + esac + export RELEASE_TAG RELEASE_PRERELEASE + echo "Publishing to the $RELEASE_TAG channel." bash desktop/packaging/publish-release.sh # The updater manifest, written AFTER both bundle jobs — they run in separate @@ -359,7 +387,7 @@ jobs: manifest: name: Update manifest needs: [build, windows] - if: github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/v') + if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') runs-on: python-ci container: image: git.fabledsword.com/bvandeusen/ci-tauri:1.97 @@ -380,19 +408,31 @@ jobs: # could drift, and a manifest whose version doesn't match the binary it # points at is an updater that never settles. version="$(sh desktop/packaging/build-version.sh)" - if [ "${GITHUB_REF_NAME}" = "dev" ]; then - export RELEASE_TAG=dev - export RELEASE_NOTES="Development build from ${GITHUB_SHA}" - # Rolling channel: drop the previous build's bundles once the manifest - # points at this one. Nothing can reach them, and they're ~100 MB a push. - export PRUNE_OLD_ASSETS=true - APP_VERSION="$version" bash desktop/packaging/write-manifest.sh - else - export RELEASE_TAG="${GITHUB_REF_NAME}" - export RELEASE_NOTES="ThoughtSync ${GITHUB_REF_NAME}" - # Twice: once onto the versioned release itself, and once onto the - # permanent `stable` pointer the app actually reads. Same manifest both - # times — its URLs point at the versioned assets either way. - APP_VERSION="$version" bash desktop/packaging/write-manifest.sh - APP_VERSION="$version" MANIFEST_TAG=stable bash desktop/packaging/write-manifest.sh - fi + # POSIX `case` (rule 81). For a tag, GITHUB_REF_NAME is the tag name, so + # the default arm is the tag path. + case "${GITHUB_REF_NAME}" in + dev|main) + # Both are rolling channels: the manifest lands on the same release + # that holds the bundles, and the previous build's bundles are dropped + # once it points at this one. Nothing can reach them, and they are + # ~100 MB a push. + case "${GITHUB_REF_NAME}" in + main) export RELEASE_TAG=stable + export RELEASE_NOTES="Stable build from ${GITHUB_SHA}" ;; + *) export RELEASE_TAG=dev + export RELEASE_NOTES="Development build from ${GITHUB_SHA}" ;; + esac + export PRUNE_OLD_ASSETS=true + APP_VERSION="$version" bash desktop/packaging/write-manifest.sh + ;; + *) + # A versioned release gets its own manifest and NOTHING ELSE. It used + # to also write the `stable` pointer — that moved to main above, and + # two writers for one channel is a race with no winner worth having. + # The tag's build consequence goes entirely in M314 step 7; this only + # stops it fighting over `stable`. + export RELEASE_TAG="${GITHUB_REF_NAME}" + export RELEASE_NOTES="ThoughtSync ${GITHUB_REF_NAME}" + APP_VERSION="$version" bash desktop/packaging/write-manifest.sh + ;; + esac diff --git a/desktop/packaging/arch/README.md b/desktop/packaging/arch/README.md index 78ae1f0..03a6982 100644 --- a/desktop/packaging/arch/README.md +++ b/desktop/packaging/arch/README.md @@ -18,7 +18,7 @@ pacman system: curl -fsSL https://git.fabledsword.com/bvandeusen/thoughtsync/raw/branch/dev/desktop/packaging/install.sh | sh ``` -That installs the newest tagged release. To follow the rolling development +That installs the newest build from `main`. To follow the rolling development channel instead, pass the flag through the pipe: ```sh diff --git a/desktop/packaging/install.sh b/desktop/packaging/install.sh index 30cce5c..385fc8a 100755 --- a/desktop/packaging/install.sh +++ b/desktop/packaging/install.sh @@ -5,8 +5,12 @@ # curl -fsSL https://git.fabledsword.com/bvandeusen/thoughtsync/raw/branch/dev/desktop/packaging/install.sh | sh # # Two channels, the SAME two the app's own updater offers (src-tauri/src/update.rs): -# stable (default) — the newest tagged v* release. +# stable (default) — the rolling build from every merge to `main`. # dev — the rolling build from every green push to `dev`. +# Both are fixed-tag releases: the tag never moves and the assets are pruned to the +# current build, so the tag alone names the newest one. `stable` only became one in +# M314 step 3, when `main` started publishing — before that it was a manifest-only +# pointer at whatever `v*` tag somebody had last cut. # Pick one with `--channel dev` or `TS_CHANNEL=dev`. Through a pipe the options go # after a `--`: curl -fsSL | sh -s -- --channel dev # @@ -41,7 +45,7 @@ ThoughtSync desktop installer. install.sh [--channel stable|dev] - --channel stable newest tagged release (default) + --channel stable newest build from main (default) --channel dev rolling build from the latest green push to `dev` -h, --help this text @@ -82,20 +86,23 @@ esac # --- resolve the release for this channel ----------------------------------- say "Finding the latest ThoughtSync build on the $channel channel…" -if [ "$channel" = "dev" ]; then - # A release whose tag never moves and whose assets are pruned to the current - # build — so the tag alone always names the newest dev build. - json="$(curl -fsSL "$API/releases/tags/dev" 2>/dev/null)" || - die "the dev channel has nothing published yet." -else - # Ask the stable channel's own manifest which version is current, then install - # THAT release. This is the same file the in-app updater reads, so the installer - # and the updater can never disagree about what `stable` means. - # - # Not `/releases/latest`: that returns the newest non-prerelease release by date, - # and the `stable` pointer release (manifest only, no bundles — see - # write-manifest.sh) is itself a non-prerelease created moments after the - # versioned one. It would win, and it carries nothing installable. +# ONE lookup for both channels now. Each is a release whose tag never moves and whose +# assets are pruned to the current build, so the tag alone names the newest build on +# that channel — which is exactly what an installer wants and what the in-app updater +# already reads. +json="$(curl -fsSL "$API/releases/tags/$channel" 2>/dev/null)" || + die "the $channel channel has nothing published yet." + +# TRANSITIONAL — delete with the rest of the old scheme (M314 step 7). +# +# `stable` existed before this as a manifest-ONLY pointer: `latest.json` naming a +# version whose bundles lived on a separate `v` release. Between this commit +# and the first merge to `main` it still looks like that, and `stable` is the DEFAULT +# channel — so without this fallback `curl … | sh` is broken for everyone in that +# window. It costs nothing once main has published: the grep finds the bundles and +# this branch never runs again. +if [ "$channel" = "stable" ] && ! printf '%s' "$json" | grep -q "releases/download/stable/[^\"]*\.\(AppImage\|deb\|pkg\.tar\)"; then + say "stable has no bundles of its own yet — falling back to the version its manifest names." manifest="$(curl -fsSL "$INSTANCE/$REPO/releases/download/stable/latest.json" 2>/dev/null || true)" stable_version="$(printf '%s' "$manifest" | grep -oE '"version"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1 | @@ -104,11 +111,8 @@ else json="$(curl -fsSL "$API/releases/tags/v$stable_version" 2>/dev/null)" || die "the stable channel names $stable_version, but there is no v$stable_version release to install." else - # No stable pointer yet — the channel predates the updater. Fall back to the - # newest non-prerelease release, which is what stable meant before there was - # a manifest to ask. json="$(curl -fsSL "$API/releases/latest" 2>/dev/null)" || - die "no stable release published yet — try --channel dev, or ask the maintainer to tag one." + die "no stable build published yet — try --channel dev, or merge to main." fi fi diff --git a/desktop/packaging/publish-release.sh b/desktop/packaging/publish-release.sh index ae27987..496c443 100755 --- a/desktop/packaging/publish-release.sh +++ b/desktop/packaging/publish-release.sh @@ -118,6 +118,11 @@ first_id() { grep -oE '"id"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | grep -oE # install.sh defaults to stable, so the rolling dev release must opt in explicitly # — otherwise someone following the instructions here lands on a tagged build and # wonders why the version they were sent isn't what they got. +# +# Both CHANNELS are rolling pointer releases (M314 step 3): `dev` republishes on +# every green push to dev, `stable` on every merge to main. Each says so, because a +# release that prunes its own assets behaves differently from a versioned one and a +# reader deserves to know which they are looking at. if [ "$TAG" = "dev" ]; then INSTALL_TAIL='sh -s -- --channel dev' # Backticks BARE, not `\``. The heredoc below is unquoted, so there the backslash @@ -125,6 +130,10 @@ if [ "$TAG" = "dev" ]; then # Here single quotes already do that job, so a backslash would survive into the # body as `\``, which is not a legal JSON escape: Forgejo answers 422. CHANNEL_NOTE='\n\nThis is the rolling **dev** channel: republished on every green push to `dev`, and pruned to the current build.' +elif [ "$TAG" = "stable" ]; then + # install.sh defaults to stable, so no flag. + INSTALL_TAIL='sh' + CHANNEL_NOTE='\n\nThis is the rolling **stable** channel: republished on every merge to `main`, and pruned to the current build. No tag is required for a build to arrive here.' else INSTALL_TAIL='sh' CHANNEL_NOTE='' diff --git a/desktop/src-tauri/src/update.rs b/desktop/src-tauri/src/update.rs index c343d4a..ae25bd2 100644 --- a/desktop/src-tauri/src/update.rs +++ b/desktop/src-tauri/src/update.rs @@ -1,10 +1,16 @@ //! In-app updates (M10.9). //! -//! Two channels, because two audiences: `stable` follows tagged `v*` releases, -//! `dev` follows every green push. Each reads a `latest.json` published as an asset -//! on a release whose TAG NEVER CHANGES — verified necessary, because Forgejo has no -//! `/releases/latest/download/` route (it 404s), so "newest" cannot be named -//! in a URL. A fixed tag can. +//! Two channels, because two audiences: `stable` follows every merge to `main`, +//! `dev` follows every green push to `dev`. Each reads a `latest.json` published as +//! an asset on a release whose TAG NEVER CHANGES — verified necessary, because +//! Forgejo has no `/releases/latest/download/` route (it 404s), so "newest" +//! cannot be named in a URL. A fixed tag can. +//! +//! `stable` followed tagged `v*` releases until M314 step 3, and its manifest pointed +//! at bundles living on a different release. It holds its own bundles now, exactly as +//! `dev` always has — so a build reaches stable users with no tag cut anywhere, which +//! is the whole point of the change. NOTHING HERE MOVED: this code only ever read +//! `/latest.json`, and that is still where the manifest lands. //! //! The feed lives on Fabled-Git rather than on a ThoughtSync server, deliberately: //! this app is usable having never linked a server, and an install that can't reach