release: a tag builds nothing and carries a changelog instead
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 18s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m0s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m17s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 8m4s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 18s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m0s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m17s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 8m4s
Step 7 of M314, the last one. Rule 22 — the old path comes out completely. ## A release stops building `desktop.yml` no longer triggers on `v*`, and its two `Publish release` steps are gone. `ci.yml` lost its tag trigger in step 6. So a tag now reaches exactly one lane: the new `release.yml`, which builds nothing. That is not a simplification for its own sake. The merge to `main` already published everything a user can receive — `:latest` + `:<sha>`, both channel feeds, the updater manifest. A tag rebuilding that source produces identical artifacts under identical names and re-pushes `:<sha>` with different bytes, which rule 145 forbids even when they match. ## So what a release is FOR The changelog (note 3127 §5). Two halves to "what am I running", and the version answers only the first: which build is this (the footer, /api/config, the APK's versionName) and what is in it that was not in the one I ran last month (nothing, until now). `packaging/release-notes.sh` derives it from git rather than a hand-maintained CHANGELOG, which drifts into recording what someone MEANT to ship. Capped at 60 entries with the omitted count stated — the first dated release spans 181 commits since `v0.1.0`, and a truncated list that does not say it is truncated is a lie. It publishes through `publish-release.sh` rather than making its own API calls, for the create-or-PATCH-on-409 path: a fixed-tag release that only ever POSTs keeps whatever body its first run wrote, which is #2182, and reimplementing that correctly in a second place is how it comes back. ## Retired `MANIFEST_TAG` and the whole branch behind it. It let the manifest live on a `stable` pointer release while the bundles sat on a versioned one — a split step 3 removed when `stable` started holding its own bundles. Nothing had passed it since; a parameter that can only ever receive its own default is a branch nobody exercises and a comment that goes stale, and its stale text was still telling readers the installable builds live on the versioned releases. `desktop/src-tauri/Cargo.toml`'s version and `thoughtsync/__init__.py`'s both now say out loud that they are not shipped values. The Cargo one carries the history worth keeping: the old scheme took its base from that line, so `0.2.<run>` on dev outranked a bare `0.2.0` on main, and the remedy was "remember to bump the minor before tagging" — documented in a comment, enforced nowhere. #2183 is what that looked like in the field. **That ritual is now formally dead**, and this is the deliberate act of killing it rather than a side effect. ## Still there on purpose `install.sh`'s transitional stable fallback. It cannot go until `main` has published to `stable` at least once, and that is gated on an operator request. Removing it now would break the DEFAULT install channel. #3147 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,12 +25,11 @@ on:
|
||||
# one. That is a ~15s container for a decision, against a lane that cannot
|
||||
# silently fail to run.
|
||||
branches: [dev, main]
|
||||
tags: ["v*"]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: desktop-${{ github.ref }}
|
||||
cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
# write (not read) so the tag build can publish a Release with the bundles
|
||||
@@ -52,7 +51,7 @@ jobs:
|
||||
# from "we derived a stale value that happens to match" unless something checks.
|
||||
decide:
|
||||
name: Build, or is the channel already serving this?
|
||||
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main'
|
||||
runs-on: python-ci
|
||||
container:
|
||||
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||
@@ -69,15 +68,6 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# A tag has no channel to compare against and still builds bundles until
|
||||
# step 7 retires that lane. Always build, never skip.
|
||||
case "${{ github.ref }}" in
|
||||
refs/tags/*)
|
||||
echo "Tag build — no channel to compare against."
|
||||
echo "build=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
case "$GITHUB_REF_NAME" in
|
||||
main) channel=stable ;;
|
||||
*) channel=dev ;;
|
||||
@@ -252,17 +242,6 @@ jobs:
|
||||
# failure, not as a green run with an empty artifact.
|
||||
if-no-files-found: error
|
||||
|
||||
# Tag builds only: publish a real, versioned Fabled-Git Release with the
|
||||
# AppImage + .deb attached — the stable fetch target the install script and
|
||||
# the in-app updater consume (Actions artifacts above are ephemeral/test).
|
||||
# Cutting the tag is the operator's action (rule 2); this only publishes a
|
||||
# Release for a tag that already exists. Dormant on dev/main pushes.
|
||||
- name: Publish release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: bash desktop/packaging/publish-release.sh
|
||||
|
||||
# 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/<asset> route, so "newest"
|
||||
@@ -391,15 +370,6 @@ jobs:
|
||||
path: target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
|
||||
if-no-files-found: error
|
||||
|
||||
# Publishes to the SAME release as the Linux job. Safe to run twice: the
|
||||
# script reuses an existing release (409) and nullglob means each job uploads
|
||||
# only the bundles present in its own workspace.
|
||||
- name: Publish release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: bash desktop/packaging/publish-release.sh
|
||||
|
||||
# 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/<asset> route, so "newest"
|
||||
@@ -447,7 +417,7 @@ jobs:
|
||||
manifest:
|
||||
name: Update manifest
|
||||
needs: [build, windows]
|
||||
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main'
|
||||
runs-on: python-ci
|
||||
container:
|
||||
image: git.fabledsword.com/bvandeusen/ci-tauri:1.97
|
||||
@@ -477,31 +447,17 @@ jobs:
|
||||
# match the binary it points at is an updater that never settles. It must
|
||||
# be `key`: this value is matched against bundle filenames.
|
||||
version="$(sh packaging/version.sh key desktop)"
|
||||
# POSIX `case` (rule 81). For a tag, GITHUB_REF_NAME is the tag name, so
|
||||
# the default arm is the tag path.
|
||||
# Both channels are rolling: 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.
|
||||
#
|
||||
# No tag arm any more. A `v*` tag does not reach this workflow at all — it
|
||||
# triggers release.yml, which writes a changelog and builds nothing.
|
||||
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
|
||||
;;
|
||||
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
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
name: Release
|
||||
|
||||
# A RELEASE BUILDS NOTHING. That is the whole point of this lane (M314 step 7).
|
||||
#
|
||||
# The merge to `main` already published everything a user can receive: the server
|
||||
# image as `:latest` + `:<sha>`, the desktop bundles and the APK to the `stable`
|
||||
# channel, and the updater manifest that advertises them. A tag rebuilding that same
|
||||
# source would produce identical artifacts under identical names, and would re-push
|
||||
# `:<sha>` with different bytes — which rule 145 forbids even when they match.
|
||||
#
|
||||
# So the tag is a BOOKMARK, and this lane gives it the only job it has left: saying
|
||||
# what was in it. Note 3127 §5 — there are two halves to "what am I running", and
|
||||
# the version answers only the first:
|
||||
#
|
||||
# which build is this? the footer, /api/config, the APK's versionName
|
||||
# what changed since the one ← this
|
||||
# I was running last month?
|
||||
#
|
||||
# Cutting the tag is the operator's act (rule 2). This only responds to one.
|
||||
#
|
||||
# THE TAG IS NOT AN IMAGE TAG and never becomes one. `ci.yml` does not trigger on
|
||||
# tags at all. The image is addressed by channel or by commit; the release by date.
|
||||
# Same string as the artifact version (rule 148, `vYYYY.MM.DD.HHMM`), different
|
||||
# system.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
notes:
|
||||
name: Write the changelog
|
||||
runs-on: python-ci
|
||||
container:
|
||||
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# The whole history AND every tag: the notes are the commit range between
|
||||
# this tag and the previous `v*` one, and neither end exists in a shallow
|
||||
# clone. A depth-limited checkout here does not fail — it produces a
|
||||
# shorter changelog, which is the kind of wrong nobody notices.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Publish the release notes
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
notes="$(sh packaging/release-notes.sh "$GITHUB_REF_NAME")"
|
||||
echo "$notes"
|
||||
echo "---"
|
||||
|
||||
# JSON-escaped HERE rather than in publish-release.sh, which cannot assume
|
||||
# python3 is on PATH in the three images that call it. `json.dumps` then
|
||||
# strip the surrounding quotes — the script supplies those.
|
||||
RELEASE_BODY_JSON="$(printf '%s' "$notes" \
|
||||
| python3 -c 'import json,sys; print(json.dumps(sys.stdin.read())[1:-1])')"
|
||||
export RELEASE_BODY_JSON
|
||||
|
||||
# Through publish-release.sh for its create-or-PATCH-on-409 path: a
|
||||
# release that is only ever POSTed keeps whatever body its first run
|
||||
# wrote (#2182), so re-tagging or re-running must rewrite it. No bundles
|
||||
# exist in this workspace, so its asset globs match nothing and it
|
||||
# uploads none — which is the intended behaviour, not a side effect.
|
||||
RELEASE_TAG="$GITHUB_REF_NAME" bash desktop/packaging/publish-release.sh
|
||||
@@ -145,6 +145,21 @@ BODY=$(cat <<JSON
|
||||
"body":"ThoughtSync $TAG.\n\n**Desktop**\n\n- **Debian / Ubuntu** — native \`.deb\`\n- **Arch / CachyOS** — native \`.pkg.tar.*\`\n- **everything else** — \`.AppImage\` (de-bundled graphics: renders on any GPU/Wayland setup)\n\nInstall / update — picks the right one for your system:\n\`\`\`\ncurl -fsSL $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/raw/branch/dev/desktop/packaging/install.sh | $INSTALL_TAIL\n\`\`\`\n\n**Android** — \`thoughtsync.apk\`. Copy it and \`thoughtsync-android.json\` into your server's \`/var/thoughtsync/client/\` and the server will offer it to your devices; see docs/android-distribution.md.$CHANNEL_NOTE"}
|
||||
JSON
|
||||
)
|
||||
|
||||
# An explicit body, replacing the install instructions above.
|
||||
#
|
||||
# Used by the RELEASE lane, whose job is a changelog rather than artifacts (M314
|
||||
# step 7). It goes through this script rather than making its own API calls so that
|
||||
# the create-or-PATCH-on-409 path is shared: a fixed-tag release that only ever
|
||||
# POSTs keeps whatever text its FIRST build wrote, which is #2182 exactly, and
|
||||
# re-implementing that correctly in a second place is how it comes back.
|
||||
#
|
||||
# CONTRACT: already JSON-escaped, without surrounding quotes. The caller knows
|
||||
# whether it has a JSON encoder; this script cannot assume python3 is on PATH in
|
||||
# every image that sources it.
|
||||
if [ -n "${RELEASE_BODY_JSON:-}" ]; then
|
||||
BODY="{\"tag_name\":\"$TAG\",\"name\":\"ThoughtSync $TAG\",\"draft\":false,\"prerelease\":$RELEASE_PRERELEASE,\"body\":\"$RELEASE_BODY_JSON\"}"
|
||||
fi
|
||||
# 409 = a release for this tag already exists (re-run) — fall through to lookup.
|
||||
release="$(ALLOW_CODES=409 api POST "$API/releases" -H "Content-Type: application/json" -d "$BODY")"
|
||||
RELEASE_ID="$(printf '%s' "$release" | first_id || true)"
|
||||
|
||||
@@ -25,14 +25,15 @@ set -euo pipefail
|
||||
: "${RELEASE_TAG:?RELEASE_TAG is required (the release holding the bundles)}"
|
||||
: "${APP_VERSION:?APP_VERSION is required (the version the bundles carry)}"
|
||||
|
||||
# Where the manifest is PUBLISHED, which need not be where the bundles live.
|
||||
# The manifest is published to the release that HOLDS the bundles. There is no
|
||||
# second place any more.
|
||||
#
|
||||
# That split is what makes the stable channel work at all. A versioned release
|
||||
# (`v0.2.0`) holds the real assets, but the app can only read a URL that never
|
||||
# changes — so the same manifest is also attached to a `stable` release whose tag is
|
||||
# permanent and whose only content is this file. It points back at the versioned
|
||||
# assets, so nothing is duplicated.
|
||||
MANIFEST_TAG="${MANIFEST_TAG:-$RELEASE_TAG}"
|
||||
# There used to be: `MANIFEST_TAG` let the manifest live on a `stable` pointer
|
||||
# release while the bundles sat on a versioned `v*` one, because the app can only
|
||||
# read a URL that never changes and a versioned tag is not that. M314 step 3 made
|
||||
# `stable` a rolling release that holds its own bundles, exactly like `dev`, so the
|
||||
# split had nothing left to bridge — and a parameter that can only ever be passed
|
||||
# its own default is a branch nobody exercises and a comment that goes stale.
|
||||
|
||||
API="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY"
|
||||
AUTH=(-H "Authorization: token $GITHUB_TOKEN")
|
||||
@@ -115,25 +116,11 @@ pub_date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
echo "==> Manifest:"
|
||||
cat "$work/latest.json"
|
||||
|
||||
# --- resolve the release the manifest is published TO ------------------------
|
||||
if [ "$MANIFEST_TAG" = "$RELEASE_TAG" ]; then
|
||||
target_id="$release_id"
|
||||
target_assets="$assets"
|
||||
else
|
||||
echo "==> Resolving the $MANIFEST_TAG channel release"
|
||||
target="$(curl -sS "${AUTH[@]}" "$API/releases/tags/$MANIFEST_TAG")"
|
||||
target_id="$(printf '%s' "$target" | grep -oE '"id"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+' || true)"
|
||||
if [ -z "${target_id:-}" ]; then
|
||||
# First publish to this channel. A pointer release: no bundles of its own, just
|
||||
# a permanent tag for the manifest to live under.
|
||||
echo " creating it (pointer release, manifest only)"
|
||||
body="{\"tag_name\":\"$MANIFEST_TAG\",\"name\":\"ThoughtSync ($MANIFEST_TAG channel)\",\"draft\":false,\"prerelease\":false,\"body\":\"Update channel pointer. The installable builds live on the versioned releases; this holds only the updater manifest.\"}"
|
||||
target="$(curl -sS -X POST "${AUTH[@]}" -H "Content-Type: application/json" -d "$body" "$API/releases")"
|
||||
target_id="$(printf '%s' "$target" | grep -oE '"id"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+')"
|
||||
fi
|
||||
[ -n "${target_id:-}" ] || { echo "ERROR: could not resolve the $MANIFEST_TAG release" >&2; exit 1; }
|
||||
target_assets="$(curl -sS "${AUTH[@]}" "$API/releases/$target_id/assets")"
|
||||
fi
|
||||
# The manifest goes on the same release the bundles were just read from — which is
|
||||
# also the one `publish-release.sh` created or refreshed moments earlier, so it is
|
||||
# guaranteed to exist by the time this runs.
|
||||
target_id="$release_id"
|
||||
target_assets="$assets"
|
||||
|
||||
# Replace rather than duplicate: Forgejo rejects a second asset with the same name,
|
||||
# and this file is rewritten on every publish by design.
|
||||
@@ -145,11 +132,11 @@ if [ -n "${old_id:-}" ]; then
|
||||
curl -fsS -X DELETE "${AUTH[@]}" "$API/releases/$target_id/assets/$old_id" >/dev/null
|
||||
fi
|
||||
|
||||
echo "==> Uploading latest.json to $MANIFEST_TAG"
|
||||
echo "==> Uploading latest.json to $RELEASE_TAG"
|
||||
curl -fsS -X POST "${AUTH[@]}" "$API/releases/$target_id/assets?name=latest.json" \
|
||||
-F "attachment=@$work/latest.json" >/dev/null
|
||||
|
||||
echo "==> Done. $MANIFEST_TAG now advertises $APP_VERSION for ${#entries[@]} platform(s)."
|
||||
echo "==> Done. $RELEASE_TAG now advertises $APP_VERSION for ${#entries[@]} platform(s)."
|
||||
|
||||
# --- prune superseded builds from a rolling channel ---------------------------
|
||||
#
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
[package]
|
||||
name = "thoughtsync-desktop"
|
||||
# NOT THE SHIPPED VERSION, and bumping it has no effect on anything a user sees.
|
||||
#
|
||||
# Cargo requires a version here, and Tauri reads one from `tauri.conf.json` — both
|
||||
# are overridden per build by `cargo tauri build --config '{"version": ...}'` with
|
||||
# the value `packaging/version.sh key desktop` derives. See #3144.
|
||||
#
|
||||
# It used to matter: the old scheme took its base from this line and appended the CI
|
||||
# run number on dev, so `0.2.<run>` on dev sat against a bare `0.2.0` on main and
|
||||
# every dev build outranked every stable one. The remedy was "remember to bump the
|
||||
# minor before tagging" — documented in a comment, enforced nowhere, and #2183 is
|
||||
# what that looked like in the field. A scheme needing a human to remember something
|
||||
# before each release has not removed the decision, only hidden it.
|
||||
version = "0.2.0"
|
||||
description = "ThoughtSync desktop — local-first Keep-style thought capture"
|
||||
authors = ["bvandeusen"]
|
||||
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env sh
|
||||
#
|
||||
# The markdown body for a release: what went live since the previous one.
|
||||
#
|
||||
# release-notes.sh <tag>
|
||||
#
|
||||
# A release BUILDS NOTHING now (M314 step 7). The merge to `main` already published
|
||||
# `:latest`, `:<sha>` and both channel feeds, so a tag rebuilding that same source
|
||||
# would produce identical artifacts and re-push `:<sha>` with different bytes —
|
||||
# which rule 145 forbids even when the bytes match.
|
||||
#
|
||||
# So what is a release FOR? Note 3127 §5 answers it: the changelog. There are two
|
||||
# halves to "what am I running" and the version answers only the first —
|
||||
#
|
||||
# which build is this the footer, /api/config, the APK's versionName
|
||||
# what is in it that was not ← this
|
||||
# in the one I ran last month
|
||||
#
|
||||
# DERIVED FROM GIT, not hand-maintained. A CHANGELOG.md drifts into being
|
||||
# aspirational — it records what someone meant to ship. `git log` records what
|
||||
# shipped, and cannot say otherwise.
|
||||
set -eu
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
tag="${1:?usage: release-notes.sh <tag>}"
|
||||
|
||||
# The previous release tag, by DATE rather than by name.
|
||||
#
|
||||
# `v*` only: this repo also carries `dev` and `stable` tags, which are the fixed-tag
|
||||
# pointer releases the updater reads. They move constantly and are not releases in
|
||||
# this sense; sorting them in would make "the previous release" mean whichever
|
||||
# channel published most recently.
|
||||
#
|
||||
# Excludes the tag being described, so re-running on an existing tag still produces
|
||||
# the range that tag covers rather than an empty one.
|
||||
prev="$(git tag -l 'v*' --sort=-creatordate | grep -vxF "$tag" | head -1 || true)"
|
||||
|
||||
if [ -n "$prev" ]; then
|
||||
range="$prev..$tag"
|
||||
header="Changes since \`$prev\`."
|
||||
else
|
||||
# The first release. Everything is new, and listing the entire history would be
|
||||
# noise — say so instead.
|
||||
range="$tag"
|
||||
header="First release."
|
||||
fi
|
||||
|
||||
printf 'ThoughtSync %s\n\n%s\n\n' "$tag" "$header"
|
||||
|
||||
# `--no-merges`: a merge commit's subject is "Merge branch ..." and says nothing
|
||||
# about what shipped. The commits it brought in are listed individually, which is
|
||||
# what somebody reading this wants.
|
||||
#
|
||||
# `%s` alone, not `%s (%h)`: the sha is in the forge's own view of the release and
|
||||
# a reader chasing a specific change clicks through rather than copying a hash out
|
||||
# of prose.
|
||||
# CAPPED, because an unbounded list is not a changelog — it is a wall.
|
||||
#
|
||||
# The first dated release spans everything since `v0.1.0` — 181 commits at the
|
||||
# time of writing: nobody reads that, and burying twelve interesting changes in it is worse
|
||||
# than not writing one. Later releases will be short and the cap will never bite.
|
||||
#
|
||||
# The most RECENT are kept, not the oldest, and the count of what was dropped is
|
||||
# stated — a truncated list that does not say it is truncated is a lie.
|
||||
CAP=60
|
||||
total="$(git log --no-merges --format='%s' "$range" | wc -l | tr -d ' ')"
|
||||
|
||||
git log --no-merges --reverse --format='- %s' "$range" | tail -"$CAP"
|
||||
|
||||
if [ "$total" -gt "$CAP" ]; then
|
||||
printf '\n_...and %s earlier commits in this range, omitted for length._\n' \
|
||||
"$((total - CAP))"
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
printf '%s\n' "_No artifacts here. Builds reach users from \`main\`: the desktop and Android"
|
||||
printf '%s\n' "channels and the server image all publish on merge, with no tag required. This"
|
||||
printf '%s\n' "release is a bookmark — it names a moment and says what was in it._"
|
||||
@@ -1,3 +1,10 @@
|
||||
"""ThoughtSync — self-hosted personal thought-capture web app (FabledSword family)."""
|
||||
|
||||
# The FALLBACK version, used only when APP_VERSION is absent from the environment —
|
||||
# i.e. running from a checkout rather than from an image. A built image always has
|
||||
# it, derived from the server's own shipped file set (packaging/version.sh), so this
|
||||
# string never reaches a deployed instance and bumping it changes nothing a user
|
||||
# sees. Kept because a package needs a version and "unknown" is not a valid one for
|
||||
# packaging metadata; the honest "I cannot say" for a running server is APP_VERSION
|
||||
# being missing, which app.py already handles.
|
||||
__version__ = "0.2.0"
|
||||
|
||||
Reference in New Issue
Block a user