Compare commits

..
11 Commits
Author SHA1 Message Date
logan 186f6a5839 fix(release): seed the version floor on the parent, not on HEAD (#3)
Release / release (push) Successful in 32s
CI / e2e (push) Successful in 6m7s
CI / check (push) Successful in 2m29s
Build & publish the Android APK / apk (push) Successful in 1m24s
Build & publish Arch package / arch-package (push) Successful in 2m26s
Attach the desktop build to the release / linux (push) Successful in 2m29s
Sync Homebrew formula / sync-formula (push) Successful in 6s
2026-08-18 03:10:31 +00:00
logan 786d9c6110 fix(release): seed the version floor on the parent, not on HEAD
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m38s
CI / e2e (pull_request) Successful in 6m14s
The floor tag marks what has already been released, so tagging the
commit being pushed leaves nothing between the floor and HEAD --
semantic-release then correctly reports there is nothing to release.
That is what the first run did: it seeded v0.0.0 on the merge commit
itself and cut no release.

HEAD^ is the first parent, so on a merge commit it is main as it was
before the merge and everything the merge brought in is releasable.

The tag has been moved to 6fb7b5e by hand; this is so the next repo
never needs that.
2026-08-17 20:37:43 -04:00
logan 0019310ca4 ci(release): cut releases from main automatically (#2)
Release / release (push) Failing after 40s
CI / check (push) Successful in 3m6s
CI / e2e (push) Successful in 7m16s
Implements .planning/plans/active/017-release-automation.md.

Merges to main now compute the version from Conventional Commits, cut
the tag and the Gitea release, and the four v* workflows publish and
attach their artifacts. First release is v0.0.1.
2026-08-18 00:22:13 +00:00
logan 1940cb548f fix(test): stop asserting a cache hit against a one-second deadline
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 3m51s
CI / e2e (pull_request) Successful in 6m21s
TestCacheTTLExpiry set a 1s TTL and immediately asserted a hit, so it
depended on an upper bound of elapsed wall-clock time between Set and
Get. Nothing can promise that: on the capacity-1 runner, with the rest
of the suite running in parallel, the goroutine can be descheduled for
longer than the TTL and the entry is then correctly gone.

It failed that way on this PR while passing five times out of five
locally, and it touches no code this branch changed.

Two entries now: one with an hour to live carries the presence
assertions, one with a second carries the expiry. Sleeping past a TTL
is always safe, so only the direction that cannot flake is timed.
2026-08-17 19:55:22 -04:00
logan 37e3373db9 docs: correct the workflow counts these comments name
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Failing after 1m35s
CI / e2e (pull_request) Skipped
Adding release.yml and desktop-assets.yml made 'the three workflows a
tag fires' wrong in three files that each said it slightly differently.
2026-08-17 19:47:55 -04:00
t 8d5d8af297 ci(release): keep the changelog out of a protected branch
CI / check (push) Skipped
CI / e2e (push) Skipped
main is protected (enable_push: false, empty whitelist), so
@semantic-release/git's commit-back is rejected by the pre-receive
hook -- and it would be rejected *after* the tag was pushed, leaving a
tagged release the run then reports as failed. Found by trying to push
this branch to main.

Whitelisting the CI user was the alternative and is declined: it
weakens a protection someone set deliberately and lets a bot push to
main without the checks every human PR has to pass.

So the release page is the changelog. The changelog plugin now writes a
gitignored .release-notes.md, which exists only to carry the notes into
gitea-release.sh without interpolating them into a shell command, and
CHANGELOG.md is a signpost -- a file claiming to be a changelog while
silently never updating is worse than no file.

Tags are not protected, so the tag push is unaffected.
2026-08-17 19:46:48 -04:00
logan 9ce79ee416 ci(release): release from a branch, not a detached HEAD
semantic-release resolves the release branch and then pushes a commit
and a tag to it, so a local branch named main is a better starting
point than the --detach the other five workflows use. Still pinned to
the pushed commit rather than to whatever main points at by the time
the container starts.

The floor tag falls back to the PAT when GITEA_TOKEN is unset, which is
safe rather than merely convenient: all four publishers skip v0.0.0
explicitly, so the worst case is four jobs that start and immediately
say there is nothing to build.
2026-08-17 19:21:19 -04:00
logan b3a0814f24 docs: describe the release pipeline where the claims used to be wrong
CLAUDE.md said .releaserc.yml was a config nothing ran and that there
were five workflows; both stop being true with this branch. The CI
section now names release.yml as the entry point and records the four
things in it that are load-bearing, including the two silent failure
modes worth pinning against.

packaging/homebrew/README.md and docs/android-release.md say where a
user would actually look that upgrading from 1.x needs a reinstall --
Homebrew offers nothing silently, and Android refuses outright.
2026-08-17 18:43:17 -04:00
logan 2c576fa1e8 ci(release): attach the Linux, Arch and Android builds to the release
A release page with nothing to download is one nobody can use. The
Arch package and the APK are already built and merely go unattached;
the plain Linux binary is new, and is what answers 'get the latest
version' without a package manager.

scripts/release-asset.sh waits for the release to exist first.
semantic-release pushes the tag in prepare and creates the release in
publish, so the tag push that starts these workflows happens before
there is an id to upload to -- and a capacity-1 runner serialises that
into working by accident, which is the worst kind of bug.

macOS is absent because it cannot be built here: GOOS=darwin
CGO_ENABLED=0 fails at wails/v3/pkg/mac, the darwin backend being
Objective-C behind cgo. Homebrew builds from source on the user's Mac
and stays the macOS channel. Windows cross-compiles cleanly and is
still withheld: no build of it has ever been run.

All three skip v0.0.0, which is semantic-release's version floor rather
than a shipment.
2026-08-17 18:39:06 -04:00
logan 544dbdb4db fix(packaging): stop publishing an Arch package on every merge to main
arch-package.yml ran on push to main and took its version from
`git describe`, so the pacman registry accumulated one package per
merge and not one of them corresponded to a version a user could be
told to install. It builds the tag release.yml cuts instead.

pkgver's literal drops to 0.0.1 with it. That is a downgrade from the
1.x already in the registry, so pacman offers no upgrade and an
existing install has to be removed once; epoch=1 would have avoided
that and is declined in a comment, because an epoch can never be
removed again.
2026-08-17 18:39:06 -04:00
logan 087eb77875 ci(release): cut a release from main with semantic-release
The config has been sitting in .releaserc.yml complete and uninvoked;
this is the workflow that runs it, and the one Gitea-shaped adaptation
it needs.

@semantic-release/github speaks GitHub's API, not Gitea's /api/v1, so
@semantic-release/exec calls scripts/gitea-release.sh instead. That
script reads the notes out of CHANGELOG.md rather than taking them as an
argument: release notes are rendered commit messages, so interpolating
the notes into a shell command would be an injection whose input is the
commit log.

The tag is pushed with a user PAT because Gitea does not start a
workflow from a ref pushed by a workflow's own token, and the three
publishing workflows are keyed on it.
2026-08-17 18:38:55 -04:00
19 changed files with 1310 additions and 417 deletions
+47
View File
@@ -129,6 +129,18 @@ jobs:
fi
v="${v#v}"
# v0.0.0 is semantic-release's version floor, not a shipment —
# see the bootstrap step in release.yml. It is skipped cleanly
# rather than failing the guard below, because a 45-minute red
# run against a tag that was never meant to ship is noise, and
# this is the most expensive of the four workflows a tag fires.
if [ "$v" = "0.0.0" ]; then
echo "v0.0.0 is the version floor, not a release; nothing to build"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
# Android orders releases by an integer and refuses anything
# not greater than what is installed. 1.3.1 -> 10301, which
# increases as long as minor and patch stay below 100.
@@ -143,9 +155,24 @@ jobs:
echo "version=$v" >> "$GITHUB_OUTPUT"
echo "code=$code" >> "$GITHUB_OUTPUT"
echo "tag=v$v" >> "$GITHUB_OUTPUT"
echo "building $v (versionCode $code)"
# Releases restarted at 0.0.1 when they became automatic (plan
# 017), so versionCode restarted at 1 — *below* the 10300 an
# installed 1.3.0 build carries. Android refuses a downgrade
# outright, and the only remedy is an uninstall, which takes the
# user's library with it. Said here because this is the file
# that computes the number.
if [ "$code" -lt 10600 ]; then
echo
echo "note: versionCode $code is below the 10600 that v1.6.0 shipped."
echo " An existing install must be removed before this one will"
echo " install, and that removal takes its library with it."
fi
- name: Go toolchain
if: steps.version.outputs.skip == 'false'
run: |
set -eu
if [ ! -x /cache/tool/go/bin/go ] || ! /cache/tool/go/bin/go version | grep -q "$GO_VERSION"; then
@@ -156,6 +183,7 @@ jobs:
/cache/tool/go/bin/go version
- name: Node toolchain
if: steps.version.outputs.skip == 'false'
run: |
set -eu
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
@@ -167,6 +195,7 @@ jobs:
# but still spends minutes verifying, so the guards are what make
# this cheap on every run after the first.
- name: Android SDK and NDK (cached)
if: steps.version.outputs.skip == 'false'
run: |
set -eu
mkdir -p "$ANDROID_HOME/cmdline-tools"
@@ -226,6 +255,7 @@ jobs:
# that are *verbatim* a secret, so a trimmed one could print in
# clear — or repeating the trimming logic in both.
- name: Build the signed APK
if: steps.version.outputs.skip == 'false'
working-directory: /src
env:
KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }}
@@ -349,6 +379,7 @@ jobs:
- name: Verify the APK
id: apk
if: steps.version.outputs.skip == 'false'
working-directory: /src
run: |
set -eu
@@ -390,6 +421,7 @@ jobs:
# file, so `latest` is deleted first. Credentials are the same
# OWNER/PACKAGE_TOKEN pair arch-package.yml publishes with.
- name: Publish to the Gitea package registry
if: steps.version.outputs.skip == 'false'
working-directory: /src
env:
VERSION: ${{ steps.version.outputs.version }}
@@ -425,3 +457,18 @@ jobs:
echo
echo "Obtainium URL:"
echo " $base/latest/yellowjacket.apk"
# The generic registry is what Obtainium polls; the release page is
# what a person looks at. Same file, already built and already
# verified by the step above — so this cannot publish something the
# signature check would have refused.
- name: Attach the APK to the release
if: steps.version.outputs.skip == 'false'
working-directory: /src
env:
TAG: ${{ steps.version.outputs.tag }}
VERSION: ${{ steps.version.outputs.version }}
run: |
set -eu
./scripts/release-asset.sh "$TAG" bin/yellowjacket.apk \
"yellowjacket-${VERSION}-android-arm64.apk"
+65 -2
View File
@@ -1,8 +1,23 @@
name: Build & publish Arch package
# Keyed on the tag, not on main. It used to publish on every push,
# deriving a version from `git describe` — so the registry accumulated a
# package per merge and none of them corresponded to anything a user
# could be told to install. release.yml decides what a release is now,
# and this builds the tag it cuts.
on:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:
inputs:
version:
description: "Version to build (default: the latest v* tag)"
required: false
concurrency:
group: arch-${{ github.ref }}
cancel-in-progress: true
jobs:
arch-package:
@@ -17,6 +32,7 @@ jobs:
REPO: ${{ github.repository }}
OWNER: ${{ github.repository_owner }}
SHA: ${{ github.sha }}
REF_NAME: ${{ github.ref_name }}
# Arch registry name (the "$repo" in clients' pacman.conf). Arbitrary label.
ARCH_REPO: stable
steps:
@@ -26,8 +42,9 @@ jobs:
# gtk3 was v2's stack and is now only the `-tags gtk3` escape hatch.
# These must match the PKGBUILD's depends=() — makepkg installs
# nothing itself, so a mismatch fails at link time, not at check time.
# jq is scripts/release-asset.sh's, not the build's.
pacman -Syu --noconfirm --needed \
base-devel git go nodejs pnpm curl sudo \
base-devel git go nodejs pnpm curl sudo jq \
webkitgtk-6.0 gtk4 alsa-lib
- name: Create unprivileged build user
@@ -36,15 +53,43 @@ jobs:
install -d -o builder -g builder /build
echo 'builder ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builder
# v0.0.0 is semantic-release's version floor, not a shipment — see
# the bootstrap step in release.yml. A clean skip rather than a
# failure: a red run against a tag that was never meant to ship is
# noise, and this is one of the four workflows that would otherwise
# fire on it.
- name: Resolve the version
id: version
run: |
set -eu
v="${{ inputs.version }}"
[ -n "$v" ] || v="$REF_NAME"
case "$v" in v*) ;; *) v="v$v" ;; esac
if [ "$v" = "v0.0.0" ]; then
echo "v0.0.0 is the version floor, not a release; nothing to build"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "tag=$v" >> "$GITHUB_OUTPUT"
echo "building $v"
- name: Clone repo at the pushed commit
if: steps.version.outputs.skip == 'false'
run: |
# Token auth works for private repos and needs no SSH key in CI.
sudo -u builder git clone \
"https://x-access-token:${PACKAGE_TOKEN}@${SERVER_URL#https://}/${REPO}.git" \
/build/yellowjacket
# A tag push carries the tag's own commit in $SHA, so this checks
# out exactly what was tagged. pkgver() then reads the tag from
# the clone's own git history.
sudo -u builder git -C /build/yellowjacket checkout --detach "$SHA"
- name: Build package with makepkg
if: steps.version.outputs.skip == 'false'
run: |
cd /build/yellowjacket/packaging/arch
# Point the PKGBUILD at this local clone / exact commit; pkgver() then
@@ -54,6 +99,7 @@ jobs:
makepkg -f --noconfirm --cleanbuild
- name: Publish to the Gitea Arch registry
if: steps.version.outputs.skip == 'false'
run: |
cd /build/yellowjacket/packaging/arch
# makepkg also produces a -debug package (detached symbols); end users
@@ -67,3 +113,20 @@ jobs:
--upload-file "$pkg" \
"${SERVER_URL}/api/packages/${OWNER}/arch/${ARCH_REPO}"
done
# The pacman registry is for people who have added it to pacman.conf;
# the release page is for everyone else. Same file, and it is
# already built.
- name: Attach the package to the release
if: steps.version.outputs.skip == 'false'
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
set -eu
cd /build/yellowjacket/packaging/arch
for pkg in yellowjacket-*.pkg.tar.zst; do
case "$pkg" in
yellowjacket-debug-*) continue ;;
esac
/build/yellowjacket/scripts/release-asset.sh "$TAG" "$(pwd)/$pkg"
done
+2 -2
View File
@@ -1,6 +1,6 @@
name: CI
# The other three workflows package and publish; none of them test
# The other five workflows package, publish or release; none of them test
# anything, so a green tick on this repo used to mean "the Arch package
# built", which is not the question anyone was asking. This is the
# workflow that gates.
@@ -92,7 +92,7 @@ jobs:
# Cloned by hand rather than with actions/checkout: that is a JS
# action and needs node inside the job container before any step
# has had a chance to install it. Same approach as the other
# three workflows in this directory.
# other workflows in this directory.
- name: Clone repo at this commit
run: |
set -eu
+176
View File
@@ -0,0 +1,176 @@
name: Attach the desktop build to the release
# The Arch package goes to the pacman registry and the APK to the generic
# one, but a release page with nothing on it to download is a release page
# nobody can use. This builds the plain Linux x86_64 binary and attaches
# it, so "get the latest version" has an answer that needs no package
# manager at all.
#
# **Linux only, and macOS is not an oversight.** `GOOS=darwin
# CGO_ENABLED=0` fails at `wails/v3/pkg/mac: build constraints exclude all
# Go files` — the darwin backend is Objective-C behind cgo, so a .app
# needs a macOS host, and the runner is a Linux container. That is
# exactly why the Homebrew formula builds from source on the user's own
# Mac, and it stays the macOS channel.
#
# Windows *does* cross-compile (GOOS=windows CGO_ENABLED=0 succeeds in a
# couple of seconds — nothing in the audio, database or webview path needs
# cgo there), and is deliberately not published: no Windows build of this
# app has ever been run, and no tier here can exercise one. Shipping it
# would be a promise nothing in this repo can keep. Revisit when someone
# has actually booted it.
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
version:
description: "Version to build and attach (default: the latest v* tag)"
required: false
concurrency:
group: desktop-assets-${{ github.ref }}
cancel-in-progress: true
jobs:
linux:
runs-on: ubuntu-latest
container:
image: ubuntu:24.04
volumes:
- /home/logan/docker/gitea/data/runner/cache/tool:/cache/tool
- /home/logan/docker/gitea/data/runner/cache/pnpm-store:/cache/pnpm-store
env:
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
SERVER_URL: ${{ github.server_url }}
REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
REF_NAME: ${{ github.ref_name }}
DEBIAN_FRONTEND: noninteractive
GO_VERSION: '1.25.0'
npm_config_store_dir: /cache/pnpm-store
steps:
# The same set ci.yml's check job installs: the app is cgo, and
# without alsa.pc oto/v3 fails at `pkg-config --cflags -- alsa`
# before anything is compiled.
- name: System packages
run: |
set -eu
apt-get update -qq
apt-get install -y -qq --no-install-recommends \
ca-certificates curl git jq build-essential pkg-config \
libwebkitgtk-6.0-dev libgtk-4-dev libasound2-dev
- name: Clone repo at this commit
run: |
set -eu
git clone --quiet \
"https://x-access-token:${PACKAGE_TOKEN}@${SERVER_URL#https://}/${REPO}.git" /src
git -C /src checkout --quiet --detach "$SHA"
git config --global --add safe.directory /src
git -C /src log --oneline -1
- name: Resolve the version
id: version
working-directory: /src
run: |
set -eu
v="${{ inputs.version }}"
if [ -z "$v" ]; then
case "$REF_NAME" in
v*) v="$REF_NAME" ;;
*) v=$(git describe --tags --abbrev=0 --match 'v[0-9]*') ;;
esac
fi
case "$v" in v*) ;; *) v="v$v" ;; esac
# v0.0.0 is semantic-release's version floor, not a shipment —
# see the bootstrap step in release.yml. Nothing is built for
# it, and this is a clean skip rather than a failure because a
# red run against a tag that was never meant to ship is noise.
if [ "$v" = "v0.0.0" ]; then
echo "v0.0.0 is the version floor, not a release; nothing to build"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "tag=$v" >> "$GITHUB_OUTPUT"
echo "version=${v#v}" >> "$GITHUB_OUTPUT"
echo "building $v"
- name: Go toolchain
if: steps.version.outputs.skip == 'false'
run: |
set -eu
if [ ! -x /cache/tool/go/bin/go ] || ! /cache/tool/go/bin/go version | grep -q "$GO_VERSION"; then
mkdir -p /cache/tool && rm -rf /cache/tool/go
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" | tar -C /cache/tool -xz
fi
echo "/cache/tool/go/bin" >> "$GITHUB_PATH"
/cache/tool/go/bin/go version
- name: Node toolchain
if: steps.version.outputs.skip == 'false'
run: |
set -eu
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y -qq --no-install-recommends nodejs
corepack enable
node --version
# `make build-prod` is the production task: -trimpath and -w -s are
# already in it, so only the version stamp is passed, through the
# LDFLAGS_EXTRA variable this repo added to build/linux/Taskfile.yml.
# (`wails3 build` has no -ldflags of its own; that was v2.)
- name: Build
if: steps.version.outputs.skip == 'false'
working-directory: /src
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
set -eu
export PATH="/src/scripts/toolbin:$PATH"
commit=$(git rev-parse --short HEAD)
go generate ./...
go tool wails3 task build \
LDFLAGS_EXTRA="-X 'main.version=${TAG}' -X 'main.commit=${commit}'"
# Described, never run: main.go has no flag parsing, so any
# invocation here would try to open a window in a container with
# no display and hang the job rather than printing a version.
test -x bin/yellowjacket
ls -la bin/yellowjacket
file bin/yellowjacket || true
# The .desktop file and the icon go in the tarball because without
# them the binary is a window with no menu entry — the Arch package
# installs both, and this is the same app for people not using it.
- name: Package the tarball
if: steps.version.outputs.skip == 'false'
working-directory: /src
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -eu
dir="yellowjacket-${VERSION}-linux-amd64"
mkdir -p "/tmp/$dir"
cp bin/yellowjacket "/tmp/$dir/"
cp packaging/arch/yellowjacket.desktop "/tmp/$dir/"
cp frontend/src/assets/images/icons/music/compact-disc.svg \
"/tmp/$dir/yellowjacket.svg"
tar -C /tmp -czf "/tmp/${dir}.tar.gz" "$dir"
ls -la "/tmp/${dir}.tar.gz"
- name: Attach it to the release
if: steps.version.outputs.skip == 'false'
working-directory: /src
env:
TAG: ${{ steps.version.outputs.tag }}
VERSION: ${{ steps.version.outputs.version }}
run: |
set -eu
./scripts/release-asset.sh "$TAG" \
"/tmp/yellowjacket-${VERSION}-linux-amd64.tar.gz"
+28 -2
View File
@@ -14,6 +14,15 @@ on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
version:
description: "Version to sync (default: the pushed tag)"
required: false
concurrency:
group: homebrew-${{ github.ref }}
cancel-in-progress: true
jobs:
sync-formula:
@@ -30,10 +39,25 @@ jobs:
uses: actions/checkout@v4
- name: Compute version and tarball checksum
id: version
run: |
set -euo pipefail
TAG="${GITHUB_REF_NAME}" # e.g. v1.3.0
VERSION="${TAG#v}" # e.g. 1.3.0
TAG="${{ inputs.version }}"
[ -n "$TAG" ] || TAG="${GITHUB_REF_NAME}" # e.g. v0.0.1
case "$TAG" in v*) ;; *) TAG="v$TAG" ;; esac
VERSION="${TAG#v}" # e.g. 0.0.1
# v0.0.0 is semantic-release's version floor, not a shipment —
# see the bootstrap step in release.yml. Skipped cleanly rather
# than failing: this one would otherwise push a formula for a
# version that does not exist into a *public* tap.
if [ "$VERSION" = "0.0.0" ]; then
echo "v0.0.0 is the version floor, not a release; nothing to sync"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
TARBALL="${SOURCE_TARBALL_BASE}/${TAG}.tar.gz"
echo "Fetching ${TARBALL}"
@@ -53,6 +77,7 @@ jobs:
echo "SHA256=${SHA256}" >> "$GITHUB_ENV"
- name: Render the formula with the new version and checksum
if: steps.version.outputs.skip == 'false'
run: |
set -euo pipefail
src="packaging/homebrew/Formula/yellowjacket.rb"
@@ -66,6 +91,7 @@ jobs:
cat yellowjacket.rb
- name: Push to the Homebrew tap repo
if: steps.version.outputs.skip == 'false'
run: |
set -euo pipefail
git clone "https://x-access-token:${TAP_TOKEN}@github.com/${TAP_REPO}.git" tap
+182
View File
@@ -0,0 +1,182 @@
name: Release
# The sixth workflow, and the one that decides whether the other three
# run at all. On every push to main it reads the Conventional Commits
# since the last tag, and if any of them is releasable it writes the
# changelog, pushes the tag, and creates the Gitea release whose body is
# that changelog section. The publishing workflows are keyed on `v*`, so
# the tag push is what starts them.
#
# **Why the tag is pushed with PACKAGE_TOKEN and not the Actions token.**
# Gitea, like GitHub, does not start a workflow from a ref pushed by a
# workflow's own token (go-gitea#33123). The token is what decides this,
# not the workflow — so semantic-release is handed a repositoryUrl
# carrying a *user* PAT, and the resulting push is attributed to a person
# and triggers the `v*` workflows normally.
#
# That limitation is used deliberately in the bootstrap step below, where
# a tag that must *not* trigger anything is pushed with the Actions token
# instead.
on:
push:
branches: [main]
workflow_dispatch:
# Cutting a tag is not a thing to cancel halfway: a superseded run must
# finish, not be killed between `git push --tags` and the release POST.
concurrency:
group: release-main
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
container:
image: ubuntu:24.04
env:
SERVER_URL: ${{ github.server_url }}
OWNER: ${{ github.repository_owner }}
REPO: ${{ github.repository }}
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
DEBIAN_FRONTEND: noninteractive
steps:
- name: System packages
run: |
set -eu
apt-get update -qq
apt-get install -y -qq --no-install-recommends ca-certificates curl git jq
- name: Node toolchain
run: |
set -eu
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y -qq --no-install-recommends nodejs
node --version
# By hand rather than actions/checkout, like the other five: that is
# a JS action and needs node inside the container before any step has
# installed it. The full history is required — semantic-release
# reads tags and walks commits, and a shallow clone silently makes
# every release look like the first one.
- name: Clone repo at this commit
run: |
set -eu
git clone --quiet \
"https://x-access-token:${PACKAGE_TOKEN}@${SERVER_URL#https://}/${REPO}.git" /src
# -B main rather than --detach, which the other five workflows
# use: semantic-release resolves the release branch and then
# pushes a commit and a tag to it, and a detached HEAD is a
# worse starting point for both than a local branch named after
# the one being released. Pinned to this commit, not to
# whatever main points at by the time the container started.
git -C /src checkout --quiet -B main "${{ github.sha }}"
git config --global --add safe.directory /src
git -C /src log --oneline -1
# Nothing currently pushes a `chore(release):` commit — main is a
# protected branch, so .releaserc.yml carries no @semantic-release/git
# and the release page is the changelog. This guard is kept for the
# day someone adds that plugin back: without it the commit-back is a
# push to the branch this workflow runs on, and the loop is a release
# per release. Six lines against that is cheap.
- name: Skip a changelog commit, if one ever exists
id: guard
working-directory: /src
run: |
set -eu
subject=$(git log -1 --format='%s')
case "$subject" in
"chore(release):"*)
echo "this is the release commit itself; nothing to do"
echo "skip=true" >> "$GITHUB_OUTPUT"
;;
*)
echo "skip=false" >> "$GITHUB_OUTPUT"
;;
esac
# semantic-release calls the first release of a repo with no tags
# 1.0.0, and offers no option to say otherwise. A floor tag is the
# only way to start at 0.0.1, so this creates one — once, ever.
#
# **It is pushed with the Actions token on purpose.** v0.0.0 is a
# floor, not a shipment: pushing it with a user PAT would start the
# Arch, Homebrew and Android workflows for a version that does not
# exist. The very limitation the header describes is what makes
# this inert.
- name: Seed the version floor
if: steps.guard.outputs.skip == 'false'
working-directory: /src
env:
ACTIONS_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -eu
git fetch --quiet --tags origin
if [ -n "$(git tag --list 'v[0-9]*')" ]; then
echo "floor already set; newest tag is $(git describe --tags --abbrev=0 --match 'v[0-9]*')"
exit 0
fi
# Prefer the Actions token because a ref it pushes starts no
# workflow, which is the whole point for a tag that is a floor
# rather than a shipment. Falling back to the PAT is safe
# rather than merely convenient: all four publishing workflows
# skip v0.0.0 explicitly, so the worst case is four jobs that
# start and immediately say there is nothing to build.
token="${ACTIONS_TOKEN:-$PACKAGE_TOKEN}"
[ -n "$ACTIONS_TOKEN" ] || echo "note: GITEA_TOKEN is unset; using the PAT"
# **On the parent, not on HEAD.** The floor marks what has
# already been released, so tagging the commit being pushed
# leaves nothing between the floor and HEAD — semantic-release
# then correctly reports there is nothing to release, which is
# exactly what the first run of this workflow did. HEAD^ is the
# first parent, so on the merge commit this fires for it is main
# as it was before the merge, and everything the merge brought
# in is releasable.
floor=$(git rev-parse "${{ github.sha }}^" 2>/dev/null || true)
if [ -z "$floor" ]; then
echo "HEAD has no parent, so no commit can precede the floor" >&2
exit 1
fi
echo "no v* tag exists — seeding v0.0.0 so the first release is 0.0.1"
git tag v0.0.0 "$floor"
git push --quiet \
"https://x-access-token:${token}@${SERVER_URL#https://}/${REPO}.git" \
refs/tags/v0.0.0
echo "seeded v0.0.0 at $floor (parent of ${{ github.sha }})"
# Pinned rather than installed into the repo: this is a Go project
# and a package.json at its root invites the npm plugin and every
# tool that looks for one. conventional-changelog-conventionalcommits
# is in the list because both the analyzer and the notes generator
# name that preset and neither depends on it.
#
# **That preset is held at 9 and the reason is worth keeping.** At
# 10 it is silently incompatible with the writer that
# release-notes-generator@14 pulls in (^8): every release note comes
# out as a bare `## 0.0.1 (date)` heading with **no sections and no
# commits under it**, and nothing errors. The version would have
# been right, the tag would have been right, every job would have
# been green, and the release body would have been empty. Check the
# notes, not the exit code, before moving any of these.
- name: Run semantic-release
if: steps.guard.outputs.skip == 'false'
working-directory: /src
run: |
set -eu
git config user.name "yellowjacket-ci"
git config user.email "yj@yellowjacket.app"
npx --yes \
-p semantic-release@25 \
-p @semantic-release/commit-analyzer@13 \
-p @semantic-release/release-notes-generator@14 \
-p @semantic-release/changelog@7 \
-p @semantic-release/exec@7 \
-p conventional-changelog-conventionalcommits@9 \
semantic-release \
--repository-url "https://x-access-token:${PACKAGE_TOKEN}@${SERVER_URL#https://}/${REPO}.git"
+4
View File
@@ -84,3 +84,7 @@ build/android/build/
build/android/.gradle/
build/android/gen/
build/android/overlay.json
# Written by @semantic-release/changelog purely to carry the release notes
# into scripts/gitea-release.sh; the release page is the changelog.
.release-notes.md
@@ -0,0 +1,358 @@
# 017 — Releases that happen by themselves
> **Status: built, not yet run.** Phases 04 have landed on this branch;
> phase 5 is the merge itself and cannot be done until then. The old
> `v1.x` tags are already deleted from `origin`. Verified locally against
> a scratch remote: semantic-release computes **0.0.1** from these
> commits and renders correct sectioned notes.
>
> **One thing found by testing that no amount of reading would have
> caught.** `conventional-changelog-conventionalcommits@10` — the current
> release, and my first pin — is silently incompatible with the writer
> `release-notes-generator@14` depends on: the version is right, the tag
> is right, every step reports success, and the release body is a bare
> `## 0.0.1 (date)` heading with **nothing under it**. It is pinned to 9
> in both `release.yml` and `make release-dry`, with the reason written
> beside it. Four of my seven original pins were wrong majors besides;
> they were guesses, and `npm view` was the fix.
The goal in one sentence: **a merge to `main` computes the next version
from the commits it contains, cuts a tag and a Gitea release whose body
is the changelog, and every publishing channel builds that tag.** The
first release under this scheme is `v0.0.1`, and the five existing `v1.x`
tags go.
## What is there now
Measured, not remembered:
- **Five tags and zero releases.** `v1.3.0`, `v1.4.0`, `v1.4.1`,
`v1.5.0`, `v1.6.0` exist on `origin`;
`GET /api/v1/repos/yonlu/yellowjacket/releases` returns `[]`. So there
is no release page to preserve and nothing but the tags to remove.
- **`CHANGELOG.md` is stale and belongs to another repo.** Its newest
entry is `1.3.0` and every link in it points at
`github.com/onion-4-dinner/yellowjacket` — it was written by a
semantic-release run against a GitHub remote this project no longer
has.
- **`.releaserc.yml` is a complete semantic-release config that nothing
invokes**, which CLAUDE.md already says in as many words.
- **Root `package.json` is literally `{}`** — the stub left behind by
whatever was going to run it.
- The triggers today are: `arch-package` on **push to `main`**,
`homebrew-formula` on **`v*`**, `android-apk` on **`v*`**, `ci` on
every branch, `index-artifact` on cron/dispatch. So Arch publishes a
`git describe` version on every merge and the other two publish only
when a human remembers to push a tag.
## Decision 1 — semantic-release, with `exec` in place of the `github` plugin
**Revised: the first draft of this plan proposed a shell script and the
argument for it does not hold.** Recorded here rather than deleted,
because the reasoning is what the decision rests on.
What I said, and what checking it showed:
- *"The two plugins that would carry the work do not fit."* Half true.
`@semantic-release/github` genuinely does not speak Gitea's `/api/v1`
— but the replacement is **`@semantic-release/exec`**, which is
first-party, published 2026-06, and peer-deps `semantic-release >=24.1`.
Its `publishCmd` is one `curl` at the Gitea release endpoint with
`${nextRelease.notes}` as the body. The Gitea-shaped part of this is
five lines, and the part I proposed to hand-roll — parsing conventional
commits, ordering semver, rendering grouped notes — is the part with
the edge cases and none of it is Gitea-shaped at all.
- *"`@semantic-release/git` commits the changelog back to `main`, which
re-triggers everything."* True, and it is the one real risk — but it
is a two-line guard (skip the job when `HEAD`'s subject is
`chore(release):`), not a reason to write a version calculator. That
guard is needed under **either** design, since either one writes a
changelog commit.
- *"A Node dependency tree at the root of a Go repo."* The commitlint
precedent does not transfer. commitlint was a dependency to regex one
line; this is a dependency to do something with real complexity, it is
`npx`-only so nothing lands in the repo, and Node is already installed
in CI for the frontend.
- *"It cannot be told to produce `0.0.1`."* Wrong — that is a property
of which commits are in the range, not of the tool. Identical under
both designs. See below.
Note also that **`@saithodev/semantic-release-gitea` is a dead end** and
should not be reached for: last published 2022, depends on `got@10` and
`fs-extra@8`, and declares no peer dependency on semantic-release at all
— i.e. it is untested against anything since v19, against a core now at
v25. `exec` + `curl` is both simpler and maintained.
So `.releaserc.yml` stays, and its plugin list becomes five **first-party**
plugins, all published within the last six months:
| plugin | job |
| --- | --- |
| `commit-analyzer` | the version |
| `release-notes-generator` | the notes |
| `changelog` | writes `CHANGELOG.md` |
| `git` | commits it back |
| `exec` | `curl`s the Gitea release |
The `releaseRules` and `presetConfig` blocks already in the file are
kept verbatim — they are the same bump table `commit-check.sh` already
enforces the grammar for, and nothing about the project's commit
convention changes.
Two mechanical details that decide whether this works at all:
- **semantic-release pushes the tag itself**, as core behaviour, using
`repositoryUrl`. The remote here is `ssh://git@git.ljones.me:2222/…`,
which would need an SSH key in CI — so the run passes
`--repository-url "https://x-access-token:$PACKAGE_TOKEN@git.ljones.me/yonlu/yellowjacket.git"`
on the command line rather than committing a token to the config.
**That is also what satisfies Decision 2**: the tag push is attributed
to a real user, not to the Actions token.
- **The empty root `package.json` (`{}`) goes.** semantic-release does
not need one when `--repository-url` is explicit, and leaving a
package manifest at the root of a Go repo invites the npm plugin and
every tool that looks for one.
Invocation is pinned in the workflow, not installed into the repo:
```
npx --yes \
-p semantic-release@25 \
-p @semantic-release/commit-analyzer@14 \
-p @semantic-release/release-notes-generator@15 \
-p @semantic-release/changelog@6 \
-p @semantic-release/git@10 \
-p @semantic-release/exec@7 \
-p conventional-changelog-conventionalcommits@9 \
semantic-release --repository-url "…"
```
(Exact majors get pinned from `npm view` at implementation time;
`conventional-changelog-conventionalcommits` is in the list because both
the analyzer and the notes generator name that preset and neither
depends on it.)
## Decision 2 — how the publish workflows learn about the tag
**Gitea, like GitHub, does not start a workflow from a tag pushed by a
workflow's own token** (go-gitea#33123, and the forum thread it points
at). This is the one load-bearing unknown in the plan.
The remedy is to push the tag with a *user* PAT — `secrets.PACKAGE_TOKEN`
is already in this repo and already used by `arch-package` and
`android-apk` to clone and to publish — so the push is attributed to a
person and the `v*` triggers fire normally. That keeps the three publish
workflows completely unchanged in shape.
**It is verified in phase 5, not assumed.** The fallback, if it does not
fire, is an explicit `POST
/api/v1/repos/{owner}/{repo}/actions/workflows/{file}/dispatches` per
channel from the release job. That needs `workflow_dispatch` (with a
`version` input) added to `homebrew-formula.yml` and `arch-package.yml`;
`android-apk.yml` already has both. **Add those inputs in phase 3
regardless** — a hand-triggered rebuild of one channel is worth having
whether or not the fallback is needed.
The alternative — one `release.yml` with the three publishes as
`needs:` jobs — is rejected: it means either copying ~400 lines of
Android and Arch setup into it or relying on `workflow_call`, and it
puts every merge to `main` behind an up-to-60-minute Android build on a
runner with capacity 1.
## Decision 3 — 1.6.0 → 0.0.1 is a downgrade, and the answer is reinstall
**Decided: no version-code offset, no epoch. The version number stays
honest and existing installs are replaced by hand.** Every channel is a
downgrade and each declines differently, so what to expect:
- **Arch: no upgrade is offered, silently.** `pkgver()` derives from
`git describe`, so after the wipe it reads `0.0.1.rN.gHASH`, which
pacman orders *below* the `1.3.0.rN.*` in the registry. `pacman -R
yellowjacket && pacman -S yellowjacket` is the remedy. (`epoch=1` in
the PKGBUILD would have avoided it for one line — but an epoch can
never be removed, and it puts a permanent `1:` in front of every
version string this project will ever have.)
- **Homebrew: no upgrade is offered, silently.** Brew has no epoch at
all. `brew uninstall yellowjacket && brew install …`.
- **Android: a hard refusal.** `versionCode` is
`maj*10000 + min*100 + pat`, so `0.0.1` is **1** against the **10300**
an installed 1.3.0 carries, and the install fails with
`INSTALL_FAILED_VERSION_DOWNGRADE`. Uninstall first — **and that takes
the app's library and config with it**, which is the same data loss
`android-apk.yml`'s keystore guard exists to prevent, arrived at from
the other direction. The workflow's own `code -le 0` guard still passes
at 1, so nothing in CI stops or warns about this.
All three go in the release notes for `v0.0.1` and in
`packaging/homebrew/README.md` / `docs/android-release.md`, because a
channel that silently offers no upgrade is indistinguishable from a
broken pipeline six months from now.
## Landing exactly `v0.0.1`
Determinism comes from two things:
1. **Seed `v0.0.0` on `6fb7b5e`** (current `origin/main`) after wiping
the old tags. That is the floor, and the analyser's range starts
there.
2. **This branch carries no `feat:` commit.** Everything in it is
`ci:`/`docs:`/`chore:`/`build:`, plus at least one `fix:` — which is
honest, since wiring up release machinery that was configured and
never run *is* a fix. One patch-level commit in `v0.0.0..HEAD`
computes `0.0.1` and nothing else can.
This is a property of the commit range, not of the tool — it would have
been the same constraint under the shell script.
This is a real constraint on the branch, not an accounting trick: a
single `feat:` commit here makes the first release `v0.1.0`.
`v0.0.0` itself gets no release object — it is a floor, not a shipment.
## Decision 4 — what the release page carries
Four artifacts, and the fourth is the interesting one. Measured on this
machine rather than assumed:
| asset | built by | state |
| --- | --- | --- |
| `yellowjacket-<v>-android-arm64.apk` | `android-apk.yml` | already built, verified, signed |
| `yellowjacket-<v>-linux-amd64.tar.gz` | new job | binary + `.desktop` + icon |
| `yellowjacket-<v>-x86_64.pkg.tar.zst` | `arch-package.yml` | already built; free to attach |
| `yellowjacket-<v>-windows-amd64.zip` | new job | **compiles; has never been run** |
**macOS cannot be one of them.** `GOOS=darwin CGO_ENABLED=0` fails at
`wails/v3/pkg/mac: build constraints exclude all Go files` — the darwin
backend is Objective-C behind cgo, so a `.app` needs a macOS host and
the runner is a Linux container. That is precisely why the Homebrew
channel builds from source on the user's own Mac, and it stays the
answer for macOS.
**Windows is newly possible and should be labelled honestly.**
`GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -tags production`
succeeds in 2.5 s and produces a 40 MB `.exe` — nothing in the audio,
database or webview path needs cgo on Windows (oto uses WinMM through
`x/sys`, sqlite is modernc's pure-Go driver, WebView2 is COM syscalls,
and MPRIS is `linux && !android`-tagged). But **compiling is not
running**: no Windows build of this app has ever been started, no CI tier
can exercise one, and `backend/system`'s `%LOCALAPPDATA%` path has never
resolved on a real machine. It ships marked as untested in the release
notes, or it does not ship — an unlabelled Windows download is a promise
nothing here can keep.
### The race the ordering creates
semantic-release runs **prepare** (changelog commit, tag push) before
**publish** (the `exec` curl that creates the release object). The tag
push is what starts the publishing workflows — so a fast one can reach
its upload step *before the release exists*, and
`POST /releases/{id}/assets` needs an id.
The capacity-1 runner serialises things enough that this would usually
work, which is the worst kind of bug. So each upload step **polls
`GET /api/v1/repos/…/releases/tags/{tag}` with a bounded retry** before
uploading, and fails loudly on timeout rather than skipping the asset.
That is ~8 lines of shell, shared by all three publishers.
## Phases
**Phase 0 — clear the ground.**
Delete `v1.3.0``v1.6.0` locally and on `origin`; push `v0.0.0` at
`6fb7b5e` — this is the floor semantic-release reads, and without it the
first release is `1.0.0` by its own rule. Delete the empty root
`package.json`. Truncate `CHANGELOG.md` to a header plus a line saying
history before `0.0.1` is in `git log` — the existing content is another
repo's links and cannot be repaired, only replaced, and the `changelog`
plugin prepends to whatever it finds.
**Phase 1 — `.releaserc.yml`.**
Swap `@semantic-release/github` for `@semantic-release/exec`, whose
`publishCmd` POSTs to
`/api/v1/repos/yonlu/yellowjacket/releases` with `tag_name`, `name` and
`body` taken from `${nextRelease.*}`. Keep `commit-analyzer`,
`release-notes-generator`, `changelog` and `git` exactly as written; fix
the `git` plugin's commit message so it passes `commit-check`
(`chore(release): ${nextRelease.version}` — the existing one already
does, but the trailing `${nextRelease.notes}` in the body is worth
keeping deliberate rather than incidental). `make release-dry` wraps
`semantic-release --dry-run` so the next version is answerable without
pushing anything.
`scripts/commit-check.sh`'s header already points at `.releaserc.yml`
for the type list and stays correct — that coupling survives this plan
rather than being broken by it.
**Phase 2 — `.gitea/workflows/release.yml`.**
On `push: branches: [main]`. Node 22, the pinned `npx` line from
Decision 1, `--repository-url` carrying `PACKAGE_TOKEN`. Concurrency
group `release-main` with `cancel-in-progress: false` — cutting a tag is
not a thing to cancel halfway.
The one guard that matters: **the job exits early when `HEAD`'s subject
starts `chore(release):`**, so the changelog commit the `git` plugin
pushes cannot re-enter this workflow. That is checked in shell rather
than left to `[skip ci]`, whose handling in Gitea is one more thing that
would have to be verified.
**Phase 3 — rewire the publish workflows.**
`arch-package.yml` moves from `push: branches: [main]` to
`push: tags: ['v*']` plus `workflow_dispatch`, so a merge no longer
publishes an untagged package. `homebrew-formula.yml` gains
`workflow_dispatch` with a `version` input and takes its version from
the input when there is no tag. `android-apk.yml` needs neither.
**Phase 3b — the assets.**
`scripts/release-asset.sh` is the shared uploader: wait for the release
by tag, then `POST /releases/{id}/assets?name=…`. `android-apk.yml` and
`arch-package.yml` each call it with the artifact they already built.
A new `desktop-assets` job — `push: tags: ['v*']`, in the same
`ubuntu:24.04` container `ci.yml` uses — builds the Linux binary via
`make build-prod` and the Windows one via the `CGO_ENABLED=0`
cross-compile, and uploads both. It is a separate job from the Arch one
because that runs in an `archlinux` container as an unprivileged
`makepkg` user, and grafting two unrelated builds onto it would make one
failure look like the other.
**Phase 4 — say that the upgrade is a reinstall, and that Windows is untried.**
No code change: a note in `packaging/homebrew/README.md`, one in
`docs/android-release.md`, the three-channel downgrade warning written
into the `v0.0.1` release notes, and a standing line in the notes
template marking the Windows asset unverified until someone runs it.
**Phase 5 — cut it and watch.** *(the only phase left)*
Merge, then verify with `gitea_ci` that (a) `release.yml` ran, seeded
`v0.0.0` and produced `v0.0.1`, (b) the release exists **with a non-empty
body** — check the body, not the exit code — and (c) **all four publish
workflows started from the tag**. If (c) is empty, that is Decision 2's
fallback and the `workflow_dispatch` inputs added in phase 3 are already
there to drive it.
The expected sequence on the merge is: `release.yml` seeds `v0.0.0`
(triggering nothing), releases `0.0.1`, and pushes both the changelog
commit and the tag — at which point `release.yml` fires a second time on
the changelog commit and exits at the `chore(release):` guard, while the
four `v*` workflows start. On a capacity-1 runner they will queue behind
each other, Android last and longest.
**Phase 6 — the documentation that will otherwise be wrong.**
CLAUDE.md's *Commits* section currently explains `.releaserc.yml` and
says nothing runs it; the CI section says there are five workflows and
that only `ci.yml` gates. Both change. `docs/android-release.md`
describes tags as hand-pushed. `make skill-check` fails on a `.pi/`
reference to a make target that does not exist, so `make release-dry`
gets documented or nothing does.
## Open questions for you
1. **Ship the Windows `.exe` or not?** It builds, and it has never run.
Marked-as-untested is the assumption; say if you would rather hold it
back until someone boots it.
Resolved: semantic-release stays, with `exec` in place of the `github`
plugin (Decision 1). Reinstalls are accepted, so no epoch and no
versionCode offset (Decision 3). The release carries the APK, a Linux
tarball, the Arch package and — pending (1) — a Windows zip; macOS is
not buildable here and stays a Homebrew-from-source channel (Decision 4).
`v0.0.0` has to be a real tag under this design — semantic-release reads
git tags for its floor and has no "treat absence as 0.0.0" knob that
also stops it calling the first release `1.0.0`.
+52 -16
View File
@@ -1,6 +1,20 @@
# semantic-release configuration
# Runs on main branch pushes to auto-determine version from conventional commits.
# Creates a git tag + GitHub Release draft; a separate workflow builds binaries.
# semantic-release configuration.
#
# Runs on pushes to main from .gitea/workflows/release.yml: determine the
# version from the Conventional Commits since the last tag, write the
# changelog, commit it, push the tag, and create the Gitea release.
#
# **There is no `@semantic-release/github` plugin here and there must not
# be.** Gitea's API is `/api/v1` and is not GitHub's surface. The Gitea
# community plugin (@saithodev/semantic-release-gitea) was considered and
# rejected: last published 2022, depends on got@10, and declares no peer
# dependency on semantic-release at all — i.e. untested against anything
# since v19, against a core now at v25. `exec` is first-party, current,
# and the Gitea-shaped part is one curl.
#
# The type list below is the one scripts/commit-check.sh enforces the
# grammar for — keep the two in step, or semantic-release will silently
# decline to release something the commit hook accepted.
branches:
- main
@@ -63,19 +77,41 @@ plugins:
section: Build
hidden: true
# Write CHANGELOG.md.
# Render the notes to a file.
#
# **This plugin is here to carry the notes, not to maintain a document.**
# It is how they reach the Gitea API *without being interpolated into a
# shell command*: release notes are rendered commit messages — arbitrary
# text carrying backticks, quotes and `$` — so templating
# ${nextRelease.notes} into `publishCmd` would be a shell injection with
# the commit log as its input. scripts/gitea-release.sh reads the top
# section of this file instead, and the only thing interpolated below is
# a semver string.
#
# The target is a gitignored build artifact rather than CHANGELOG.md,
# because nothing commits it back — see below.
- - "@semantic-release/changelog"
- changelogFile: CHANGELOG.md
- changelogFile: .release-notes.md
changelogTitle: "# Release notes"
# Commit the changelog back to the repo.
- - "@semantic-release/git"
- assets:
- CHANGELOG.md
message: "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
# Create the Gitea release, whose body is that section.
# `publish` runs after `prepare`, so the tag already exists by here.
- - "@semantic-release/exec"
- publishCmd: "./scripts/gitea-release.sh ${nextRelease.version}"
# **There is deliberately no @semantic-release/git here.**
#
# `main` is a protected branch with `enable_push: false` and an empty
# push whitelist, so a changelog commit-back would be rejected by the
# pre-receive hook — *after* the tag had already been pushed, leaving a
# tagged release the run then reported as failed. The alternative was to
# whitelist the CI user, which weakens a protection someone set on
# purpose and lets a bot push to main without passing the checks every
# human PR has to.
#
# So the release page is the changelog. Tags are not protected, so the
# tag push semantic-release does itself is unaffected. CHANGELOG.md in
# the repo is a signpost to the releases page and is not written by any
# of this; a file that claimed to be a changelog and silently stopped
# updating would be worse than no file at all.
# Create the GitHub Release (draft, so the build workflow can attach binaries).
- - "@semantic-release/github"
- draft: true
successComment: false
failComment: false
releasedLabels: false
+16 -372
View File
@@ -1,377 +1,21 @@
## [1.3.0](https://github.com/onion-4-dinner/yellowjacket/compare/v1.2.3...v1.3.0) (2026-03-20)
# Changelog
### Features
The changelog is the releases page:
* **09-01:** add scan control events and cancelled metrics field ([c695024](https://github.com/onion-4-dinner/yellowjacket/commit/c695024241a7513b8fedb3fbf7ff364d0515b392))
* **09-01:** add scan control fields and per-scan cancellable context ([cf22e52](https://github.com/onion-4-dinner/yellowjacket/commit/cf22e52a64850a80b9fcc63c21d81313e6bd56ab))
* **09-02:** add frontend keyboard shortcut service, store, and controller ([40d4815](https://github.com/onion-4-dinner/yellowjacket/commit/40d48151dd798b57eed9f54a572ae4735356d09e))
* **09-02:** add shortcuts config package with default bindings and Wails persistence ([6285ca9](https://github.com/onion-4-dinner/yellowjacket/commit/6285ca9dc4e6f211197e377d01c485b1ef65c300))
* **09-03:** add scan control UI with pause/resume/cancel and confirmation dialog ([3914369](https://github.com/onion-4-dinner/yellowjacket/commit/391436927c826f2f17a4523be7829aefc04a6b12))
* **09-04:** add keyboard shortcuts section to config page with conflict detection ([0451fb3](https://github.com/onion-4-dinner/yellowjacket/commit/0451fb38805ff2c27e43deb152daa892e733d2db))
* **10-01:** implement migration 6 and pre-migration backup ([1179f56](https://github.com/onion-4-dinner/yellowjacket/commit/1179f56c3680112692e71e8dc7ce946446fa8a8a))
* **10-01:** update SQL schema files for multi-library fresh installs ([535855b](https://github.com/onion-4-dinner/yellowjacket/commit/535855b383a457dd2be3298b4361313bef22b39d))
* **10-02:** add migration 6 integration tests and NewTestDBWithLibrary helper ([bc15189](https://github.com/onion-4-dinner/yellowjacket/commit/bc151891b50e59e41da2e00dbfafbecaad11b4ac))
* **10-02:** add sqlc queries for libraries and update playlist queries for phantom support ([02548dd](https://github.com/onion-4-dinner/yellowjacket/commit/02548dd55e59b28f3d6c8d9614f209140c979250))
* **11-01:** per-library scan pipeline with queue coordinator ([943db1c](https://github.com/onion-4-dinner/yellowjacket/commit/943db1cf274bdf59daf28ab6c20f78ef5ef53105))
* **11-02:** update config-page with per-library progress display and queue-aware cancel dialog ([d01591d](https://github.com/onion-4-dinner/yellowjacket/commit/d01591d6cc054a63b832c05a3164a72fdcaba342))
* **11-02:** update library-manager with per-library progress and Scan All button ([d61f122](https://github.com/onion-4-dinner/yellowjacket/commit/d61f122b567e8ac2b30fa96c637cbebc14493c89))
* **12-01:** add queue compaction method and wire removal hooks ([5995dfd](https://github.com/onion-4-dinner/yellowjacket/commit/5995dfd01d61cd4d2c0749eeeee2a1f93b739d68))
* **12-01:** implement library CRUD methods and orphan cleanup pipeline ([bd44f83](https://github.com/onion-4-dinner/yellowjacket/commit/bd44f8306c9129b9420ad81938bcf8105a1cb55a))
* **12-02:** make config sections collapsible with chevron dropdown ([12c6782](https://github.com/onion-4-dinner/yellowjacket/commit/12c678284c7582bd85cd52722f4d405b0bd0e20f))
* **12-02:** remove Libraries sidebar nav item and view routing ([e199712](https://github.com/onion-4-dinner/yellowjacket/commit/e199712a56e1cb3c0fc43d3340abb892a6f5fa7b))
* **12-02:** replace config-page library section with full library management UI ([ffc5d96](https://github.com/onion-4-dinner/yellowjacket/commit/ffc5d9639cf7c916a4f846590ae0d67cf13afe27))
* **12-02:** selectable library list with checkbox scan targeting ([13a42ae](https://github.com/onion-4-dinner/yellowjacket/commit/13a42aea2287d7ed0ec9ff9856f52c1fa7767338))
* **12-02:** show scan progress bar inline in library list entry ([df824c6](https://github.com/onion-4-dinner/yellowjacket/commit/df824c6989e92b2aefaa1ddf05b131ee319612d8))
* **13-01:** add library-filtered Go query methods and FTS search ([5f7de50](https://github.com/onion-4-dinner/yellowjacket/commit/5f7de5060a5bc557b96203267de694ef366ed507))
* **13-01:** add library-filtered sqlc queries for all browse views ([5cc58ce](https://github.com/onion-4-dinner/yellowjacket/commit/5cc58ce66ab70d8d5a570df5067f79ae2201037e))
* **13-02:** add library filter dropdown and wire all views to respect active filter ([42b8cf9](https://github.com/onion-4-dinner/yellowjacket/commit/42b8cf9f52133499ffcd7363bd39dd0c1069e091))
* **15-01:** migrate FTS5 search_index to contentless_delete=1 ([cb5155b](https://github.com/onion-4-dinner/yellowjacket/commit/cb5155b8906357ff77c5c579d57d02cf2eec6abe))
* **15-02:** create backend/fileutil package with AtomicWrite ([4d64b5d](https://github.com/onion-4-dinner/yellowjacket/commit/4d64b5dcfe43951e8ec63383bbf72c99107c63c4))
* **16-01:** add selectAll() to SelectionController and dispatch shortcut:select-all event ([f567762](https://github.com/onion-4-dinner/yellowjacket/commit/f5677628ef283b67370630b564f23178e43da3d2))
* **16-01:** wire shortcut:select-all listener in track-list, queue-panel, and playlist-view ([906ea28](https://github.com/onion-4-dinner/yellowjacket/commit/906ea28751ce9f96fdeeb9410ab5f6518f09fcb9))
* **16-02:** add go-flac dependencies and implement FLAC tag writer ([3642cbe](https://github.com/onion-4-dinner/yellowjacket/commit/3642cbe0d58f8912a786a4fc5380c40403add94a))
* **16-03:** implement DB sync module for tag write pipeline ([2966079](https://github.com/onion-4-dinner/yellowjacket/commit/2966079625cd42412411429af02184d015526e9b))
* **16-03:** WriteTrackTags pipeline with player safety, scan mutex, events, and app wiring ([64322f9](https://github.com/onion-4-dinner/yellowjacket/commit/64322f93538515d5a3e486dc14691b9c9dcf6f66))
* **17-01:** add TrackMetadataChanged handler and remove selection gate on Track Details ([fc5cf70](https://github.com/onion-4-dinner/yellowjacket/commit/fc5cf70e4c1be3d3f1545c140db5202601a08109))
* **17-01:** add WriteTrackTagsByPath and ImageFilePicker backend methods ([4235b4a](https://github.com/onion-4-dinner/yellowjacket/commit/4235b4a4d555882ce86628a88dd4e4eeee2c9097))
* **17-02:** implement save flow, cover art editing, and error handling ([265a9ea](https://github.com/onion-4-dinner/yellowjacket/commit/265a9ea8ceba893f956a03546e9ac4189adc7716))
* **18-01:** add BatchWriteProgress event constant ([3dba0e1](https://github.com/onion-4-dinner/yellowjacket/commit/3dba0e143c091327d305d39d2fa7a687ec47e172))
* **18-01:** add BatchWriteTrackTags with progress, cancellation, and partial failure ([f557ffd](https://github.com/onion-4-dinner/yellowjacket/commit/f557ffd652179b7cf8f8ff4a06824f30edf08007))
* **18-02:** add batch edit mode to track-details component ([6dab32b](https://github.com/onion-4-dinner/yellowjacket/commit/6dab32b36b497d54e8645e969aa79737ad3523ab))
* **18-02:** wire batch track-details to all 4 view context menus ([656985a](https://github.com/onion-4-dinner/yellowjacket/commit/656985add92663440baebb871f8cd6d5723117fd))
* **19-01:** implement WAV RIFF parser/writer and writeWavTags ([e6610ff](https://github.com/onion-4-dinner/yellowjacket/commit/e6610ff15e041213b6898ad48ff63b7060b312e7))
* **20-01:** implement OGG Vorbis tag writer with custom page parser and CRC32 ([5e98c03](https://github.com/onion-4-dinner/yellowjacket/commit/5e98c036342b9e174abdc6d00db21c2e2901f18b))
* **quick-17:** create playlist-details subpage component ([dc5c7d6](https://github.com/onion-4-dinner/yellowjacket/commit/dc5c7d6ca6cfbfac15546c048f1b33aaf47209c6))
* **quick-18:** replace track-info with multi-column grid layout in playlist-details ([ce23177](https://github.com/onion-4-dinner/yellowjacket/commit/ce2317722870f932792dc6456a63235ff4611466))
<https://git.ljones.me/yonlu/yellowjacket/releases>
### Bug Fixes
Every release there is generated from the Conventional Commits it
contains, by `.gitea/workflows/release.yml` on merge to `main`. Each one
carries its notes as its body, grouped by change type, with a link to the
commit behind every line.
* **09-05:** emit VolumeChanged event and persist state in ChangeVolume and MuteToggle ([bb3fd20](https://github.com/onion-4-dinner/yellowjacket/commit/bb3fd204f0895f357a14479b40754f397aae74c4))
* **10-01:** move library_id index to migration 6 to fix existing DB startup ([75b2a34](https://github.com/onion-4-dinner/yellowjacket/commit/75b2a349ebd6fada5cbc92bfae9854cc2cd53c63))
* **12-02:** claim orphaned tracks when adding library with matching path ([f60b6b5](https://github.com/onion-4-dinner/yellowjacket/commit/f60b6b525546ef77a3329fe92f03f336b7435a0e))
* **12-02:** count failed saves as skipped so scan progress bar advances ([b36e472](https://github.com/onion-4-dinner/yellowjacket/commit/b36e472212957ff089f4f5d35f3978a754e23502))
* **12-02:** delete artist_credit_artist before artist_credit in removal pipeline ([890284d](https://github.com/onion-4-dinner/yellowjacket/commit/890284ddb1d0fb95e423bddf27b40fb0db2d11e5))
* **12-02:** dismiss inline rename on click outside ([9272b06](https://github.com/onion-4-dinner/yellowjacket/commit/9272b060bf98118e37f19a8c0834034691bfe6a2))
* **12-02:** downgrade per-file save error to Debug, add warning count to scan summary ([cf18c39](https://github.com/onion-4-dinner/yellowjacket/commit/cf18c39dbd849d60218228cf1d2285ab2071e788))
* **12-02:** invalidate library store cache on LibraryRemoved event ([b093fbb](https://github.com/onion-4-dinner/yellowjacket/commit/b093fbb10a24054c4ef62b0bd13f28d9bfe6f121))
* **12-02:** keep Add Library button visible during scan ([649e516](https://github.com/onion-4-dinner/yellowjacket/commit/649e516aa30090665e9f10e89c1ccce378e36b96))
* **12-02:** move Add Library button inline with scan buttons ([771345d](https://github.com/onion-4-dinner/yellowjacket/commit/771345dd9d3870b3a907e1cce09c7456ab7ccd85))
* **12-02:** move scan buttons above library list, default to none selected ([ba3f840](https://github.com/onion-4-dinner/yellowjacket/commit/ba3f840a28fe2c6ca40c558305814d29c233d6e0))
* **12-02:** refresh library track counts after scan completes ([1f872aa](https://github.com/onion-4-dinner/yellowjacket/commit/1f872aa005a9405d9bc1f64a4b1dd2f1f1d4a16c))
* **12-02:** reorder orphan cleanup to delete FK children before recordings ([1d735c3](https://github.com/onion-4-dinner/yellowjacket/commit/1d735c3a5f5a78996d6ddbe5c787adf040fe2f21))
* **12-02:** replace removed Scan() import with ScanAllLibraries() ([0559822](https://github.com/onion-4-dinner/yellowjacket/commit/05598224e4d5532d2e2a3a7e5d3b5411240b1024))
* **12-02:** resolve phantom tracks caused by empty library root after TOML cleanup ([717e249](https://github.com/onion-4-dinner/yellowjacket/commit/717e249c368fd1cc8d5c8f945c352175708691cf))
* **12-02:** serialize ScanWarning.Err as string instead of error interface ([ac8cbb3](https://github.com/onion-4-dinner/yellowjacket/commit/ac8cbb3296bd561a305627668c211dce7209df25))
* **12-02:** soft scan claims orphaned library_id=0 tracks on startup ([1ad099a](https://github.com/onion-4-dinner/yellowjacket/commit/1ad099a9d35fc722475e238d3443fd5473566acd))
* **12-02:** soft scan on launch — only scan libraries with changed file counts ([92c4d23](https://github.com/onion-4-dinner/yellowjacket/commit/92c4d23a9a1e545fab497816ee3dce43a181cded))
* **12-02:** wait for scan to stop before library removal, surface errors in UI ([cf00498](https://github.com/onion-4-dinner/yellowjacket/commit/cf004986c95732d00208e83467267904ea3f2ef6))
* **13-02:** auto-resolve phantom playlist tracks after library scan ([93262b9](https://github.com/onion-4-dinner/yellowjacket/commit/93262b9ae0f737d2893839ac585776207b3b44b6))
* **13-02:** defer virtualizer event delegation until element exists ([f05d2bb](https://github.com/onion-4-dinner/yellowjacket/commit/f05d2bb603f5ea827164466fd0795a6c6e662529))
* **13-02:** resolve phantom playlist tracks using M3U8 paths after scan ([9f595b7](https://github.com/onion-4-dinner/yellowjacket/commit/9f595b7ac10c2191b5469004901cbbc1331c1abb))
* **14-01:** downgrade main-panel from contain:strict to layout+style+paint ([4b7d35d](https://github.com/onion-4-dinner/yellowjacket/commit/4b7d35d7ec4c8b14453a8f8250cd154b8c4c2537))
* **14-perf:** fix scroll jumping and input latency ([3b2e189](https://github.com/onion-4-dinner/yellowjacket/commit/3b2e189e7d0e6d00393d087565190fd307774257))
* **17-02:** fix cover art replace and remove ([d7c2965](https://github.com/onion-4-dinner/yellowjacket/commit/d7c2965752ae0ac9009d00f2431d5919a24558b7))
* **17-02:** handle float64 numeric values from Wails JSON deserialization ([900db2e](https://github.com/onion-4-dinner/yellowjacket/commit/900db2e56cca254873a3a5a7a384008feac4211b))
* **17-02:** refresh cover art URLs after save ([8cd4914](https://github.com/onion-4-dinner/yellowjacket/commit/8cd4914842f61c0c6b49e0216c7816e201a3c94a))
* **17-02:** refresh track-details dialog data after successful save ([ffcdc41](https://github.com/onion-4-dinner/yellowjacket/commit/ffcdc41b0d4fad8ed428dbaa55f6cdd38c096822))
* **18-02:** add field labels above title/artist/album inputs in batch edit mode ([9df2d67](https://github.com/onion-4-dinner/yellowjacket/commit/9df2d6764a0b0566dda33cff675debea4a61dea8))
* **18-02:** add field labels to all track-details states (single/batch, read/edit) ([d430ad8](https://github.com/onion-4-dinner/yellowjacket/commit/d430ad884bfd38bea93389d8be730ff00388a7be))
* **19-01:** add album_artist TPE2 mapping to applyTextChanges ([8f4c4a0](https://github.com/onion-4-dinner/yellowjacket/commit/8f4c4a0c2b14eeeaeccb972a40addb11f3d65437))
* preserve scroll position in cached grid views ([54df917](https://github.com/onion-4-dinner/yellowjacket/commit/54df917ffdd69c4f7ffaeccf2d161261ca80d84e))
* **queue-panel:** set flow layout _itemSize to match actual track item height ([288d9de](https://github.com/onion-4-dinner/yellowjacket/commit/288d9deae22d437fcd7857b368827db7b62c24f6))
* **queue-panel:** suppress virtualizer scroll corrections during scrollbar drag ([0bd8cef](https://github.com/onion-4-dinner/yellowjacket/commit/0bd8cefa00dcae2f8bd9579de2aefd58e0a9e6c9))
* **quick-19:** multi-root path resolution for playlist M3U8 tracks ([9144ded](https://github.com/onion-4-dinner/yellowjacket/commit/9144dedc2742925dc252d491763b4f2929238d0e))
* **S21/T01:** fix all lint warnings and upgrade wsl to wsl_v5 ([f16157a](https://github.com/onion-4-dinner/yellowjacket/commit/f16157a2134cbeb1787ff851d4875d77f2f3f86b))
**This file is not generated and is not a copy of that.** `main` is a
protected branch, so nothing pushes a changelog commit back to it — and a
file that claimed to be a changelog while silently never updating would
be worse than no file at all. `make release-dry` prints what the next
merge would release.
### Performance
* **12-02:** increase scan batch size from 50 to 300 ([21ea71e](https://github.com/onion-4-dinner/yellowjacket/commit/21ea71e2575d76258bd81d89ab8ac883aa3bed36))
* **12-02:** skip FTS5 rebuild during library removal ([30f4461](https://github.com/onion-4-dinner/yellowjacket/commit/30f4461e6957e20d3dc607fa0886a75b5c21b3cf))
* **14-01:** add CSS containment to app shell layout boundaries ([efa06f7](https://github.com/onion-4-dinner/yellowjacket/commit/efa06f7edf1e4acdc3d8865cad264403257ae40d))
* **14-01:** add GPU promotion and containment to all scroll containers ([ac8a52e](https://github.com/onion-4-dinner/yellowjacket/commit/ac8a52e110f9f8ebdc3433b60594370352126a18))
* **14-02:** replace innerHTML navigation with view caching system ([ad91043](https://github.com/onion-4-dinner/yellowjacket/commit/ad9104374a628342e0ea30cf409ff43de2c2f86e))
* **14-03:** add notification batching to queue store and granular change tracking to library store ([d0c05dc](https://github.com/onion-4-dinner/yellowjacket/commit/d0c05dc1d43a4fe12cc07f3cff25375b08a74ba0))
* **14-03:** eliminate per-item closure allocation in scroll render paths ([2f7ed70](https://github.com/onion-4-dinner/yellowjacket/commit/2f7ed7030425ed0ebb7a1a186917a79a7b26b850))
* **14-04:** RAF-throttle scroll position saves and add overflow-anchor to queue panel ([6ca0b3c](https://github.com/onion-4-dinner/yellowjacket/commit/6ca0b3c5a84769af064ebe45a6eaac014d1a270a))
* auto-detect NVIDIA+Wayland for DMABuf workaround ([915591a](https://github.com/onion-4-dinner/yellowjacket/commit/915591aea962beb60da2e96ac0f57307f646f675))
* inline SVGs, memoize grid slices, batch store notifications ([a4eac39](https://github.com/onion-4-dinner/yellowjacket/commit/a4eac394cebefd29d0ebcb4b1e331444dcb8fbaf))
* reduce software rendering overhead for NVIDIA+Wayland ([199c910](https://github.com/onion-4-dinner/yellowjacket/commit/199c91013fd806f6aefce49357df8a32b46faaa0))
### Refactoring
* **quick-17:** simplify playlist-view to navigate instead of expand ([955cd68](https://github.com/onion-4-dinner/yellowjacket/commit/955cd68be2dbf7a9071ef1c93084d687b59b6bd7))
## [1.2.2](https://github.com/onion-4-dinner/yellowjacket/compare/v1.2.1...v1.2.2) (2026-03-06)
### Bug Fixes
* recover from go-mp3 seek panic on startup ([#86](https://github.com/onion-4-dinner/yellowjacket/issues/86)) ([2f9d9f8](https://github.com/onion-4-dinner/yellowjacket/commit/2f9d9f8508b90b6188fe894c282c5b8e330e8046))
## [1.2.1](https://github.com/onion-4-dinner/yellowjacket/compare/v1.2.0...v1.2.1) (2026-03-06)
### Bug Fixes
* **deps:** pin go-webview2 to v1.0.21 for Wails v2 compat ([25f0fe8](https://github.com/onion-4-dinner/yellowjacket/commit/25f0fe81560eeff36a0b2beb52ce1bdf13d5e122))
## [1.2.0](https://github.com/onion-4-dinner/yellowjacket/compare/v1.1.3...v1.2.0) (2026-03-06)
### Features
* **02-02:** add ScanWarning type and reclassify scan errors as warnings ([e6866de](https://github.com/onion-4-dinner/yellowjacket/commit/e6866ded9dc0ea30ff942cd31b6c5ea3269e9584))
* **03-01:** create NewTestDB helper for in-memory SQLite test databases ([bae9d70](https://github.com/onion-4-dinner/yellowjacket/commit/bae9d70d23157ef4e79e60dd713d9a02ab63790b))
* **03-01:** extract shared applyPRAGMAs and add production PRAGMAs to NewDB ([d348815](https://github.com/onion-4-dinner/yellowjacket/commit/d34881530adda7fb75be84737798da46d17bfa8c))
* **06-01:** create track_metadata VIEW schema and migration 4 ([9c7e5a9](https://github.com/onion-4-dinner/yellowjacket/commit/9c7e5a96344a81bf132de487b4763f1dc3ff6df9))
* **06-02:** create Go→TypeScript event constant codegen tool ([3e9edd0](https://github.com/onion-4-dinner/yellowjacket/commit/3e9edd05e87395499ac24e456640d1f6d9b97f04))
* **06-03:** migrate lookupChunk to sqlc-generated LookupTrackMetaByPaths query ([2221a68](https://github.com/onion-4-dinner/yellowjacket/commit/2221a68459850a837c996c6e6d2bc95d41b20fb3))
* **08-01:** define design token CSS custom properties for icon sizes and type scale ([1444a66](https://github.com/onion-4-dinner/yellowjacket/commit/1444a66bb201ce5fdf16552a32bcd281089c64ed))
* **08-04:** apply design tokens to cover-grid, track-list, queue-panel, and detail components ([1303422](https://github.com/onion-4-dinner/yellowjacket/commit/1303422e69c27d528363900b3ca5287a48cc9f8e))
* **08-04:** convert sidebar em-based spacing to px and apply icon/type tokens ([aed90d7](https://github.com/onion-4-dinner/yellowjacket/commit/aed90d7b1710d0c5cece2e4956c0a6ce77b9a999))
* add scan progress bar with phase indicator ([a28b4d1](https://github.com/onion-4-dinner/yellowjacket/commit/a28b4d1e0673658824750d4c702359321dc9a78e))
* **quick-001:** add multi-file picker and batch import support ([c34e4ad](https://github.com/onion-4-dinner/yellowjacket/commit/c34e4ad029c119bff8f70a07ccc6bca58b11ea3c))
* **quick-001:** regenerate bindings and update frontend for multi-import ([2a542bf](https://github.com/onion-4-dinner/yellowjacket/commit/2a542bf3bcdc7772edb1aceb41f488774494f656))
* **quick-002:** add CountPlaylistsByName SQL query and regenerate sqlc ([04b2088](https://github.com/onion-4-dinner/yellowjacket/commit/04b2088b28b84a4d4df25b23d97112c5a955dff1))
* **quick-002:** add uniquePlaylistName helper and wire into ImportPlaylist ([8ba8bbe](https://github.com/onion-4-dinner/yellowjacket/commit/8ba8bbe7bed2ecff97613ebaa42a49a662050353))
* **quick-006:** remove list icon from playlists, add favorites icon to default ([3c19766](https://github.com/onion-4-dinner/yellowjacket/commit/3c19766fd0885d4171cf9929db6d69a3d5c1a3ff))
* **quick-11:** add configurable log level via YJ_LOG_LEVEL env var ([55b4902](https://github.com/onion-4-dinner/yellowjacket/commit/55b4902fac7b7f2c04ad5efac398ecedc5fedc2f))
* **quick-11:** add make dev-debug target for verbose logging ([c45bca4](https://github.com/onion-4-dinner/yellowjacket/commit/c45bca411ba1d4f32deea6027acf91237173dd15))
* **quick-12:** add favorite icon to album dropdown track rows ([12a0bbc](https://github.com/onion-4-dinner/yellowjacket/commit/12a0bbc89c19128485d597a61bd16bd0786450ad))
* **quick-15:** add BufferedStreamer with goroutine read-ahead ([85b23ac](https://github.com/onion-4-dinner/yellowjacket/commit/85b23acb24a048d2f7b85808e477bb991ae124e6))
* **quick-15:** insert BufferedStreamer into player pipeline and increase speaker buffer ([8a0b16a](https://github.com/onion-4-dinner/yellowjacket/commit/8a0b16a4ec08a95bfd3834c8216e21dce854432d))
* **quick-3:** add playlist-level multi-select state and selection handling ([e13151f](https://github.com/onion-4-dinner/yellowjacket/commit/e13151ffa5dc86e41ce242421679d65a740c3af0))
* **quick-3:** wire playlist context menu for batch delete of selected playlists ([c92ced2](https://github.com/onion-4-dinner/yellowjacket/commit/c92ced2c74e72bfc123c880c047462dc969cde34))
* **quick-4:** add 'Set as Default Playlist' context menu option ([9971b63](https://github.com/onion-4-dinner/yellowjacket/commit/9971b635b81fe3f8621c80a6664eccb3e1fc4bb8))
* **quick-5:** add CreatedAt/UpdatedAt to playlist Summary struct ([bdaff47](https://github.com/onion-4-dinner/yellowjacket/commit/bdaff478e802ee5c0745327c52dd9b190fcfef7d))
* **quick-5:** add sort dropdown UI and client-side sorting to playlist view ([5c07485](https://github.com/onion-4-dinner/yellowjacket/commit/5c074855351f1363cc7918837a78bbd3c0b7ebf5))
* **quick-7:** add PinDefault config field with backend getter/setter ([6e123bd](https://github.com/onion-4-dinner/yellowjacket/commit/6e123bd47f55e6d565f20bf7f19950e65f80787f))
* **quick-7:** wire frontend pin-default-playlist feature end-to-end ([e6378e1](https://github.com/onion-4-dinner/yellowjacket/commit/e6378e1f0d3b0f2a7604b8ef6097dba9050cdd16))
* **quick-8:** add FindDuplicateTracksInPlaylist backend method ([83de934](https://github.com/onion-4-dinner/yellowjacket/commit/83de934c39ca7d850a8b5925c90e6d0b3fe0a487))
* **quick-8:** create duplicate-tracks-dialog component ([9f3ba2b](https://github.com/onion-4-dinner/yellowjacket/commit/9f3ba2b9d474fa30dcb4934b01d4650e0d0d3cba))
* **quick-8:** wire duplicate detection into playlist-picker and playlist-view ([917a79a](https://github.com/onion-4-dinner/yellowjacket/commit/917a79a8d6e30dddd2170323bb26692386794872))
### Bug Fixes
* **01-01:** add mutex protection to Queue, Library, and Playlist SetContext methods ([daaa6b7](https://github.com/onion-4-dinner/yellowjacket/commit/daaa6b7f9779385979fe9dddae4e7bb388b3e5fb))
* **01-01:** collapse Player.SetContext double-lock into single acquisition ([3abaeba](https://github.com/onion-4-dinner/yellowjacket/commit/3abaeba3afb0f4d0edb81e26ca55b31bf59990ac))
* **02-01:** eliminate package-level startupErr and fix config file permissions ([2a86408](https://github.com/onion-4-dinner/yellowjacket/commit/2a864082017e489ffa086c136f1002277a77a7c4))
* **02-01:** log MPRIS callback errors instead of discarding them ([0860b2f](https://github.com/onion-4-dinner/yellowjacket/commit/0860b2fd4b2250da1eeb80c21f14fdf341697501))
* **08-02:** revert repeat() inside lit-virtualizer, restore .renderItem + .keyFunction ([72ef719](https://github.com/onion-4-dinner/yellowjacket/commit/72ef719ba70eeca0fa4bae47df092706f6fbaeed))
* drop+recreate contentless FTS5 index instead of DELETE ([8e9a616](https://github.com/onion-4-dinner/yellowjacket/commit/8e9a61603779eacbee7013b9bc760b315baf782a))
* **frontend:** reposition search indicator into toolbar and fix album cover art lookup ([a29137b](https://github.com/onion-4-dinner/yellowjacket/commit/a29137b2ba4c6b33ce9a5f868cbd6013e0e3b116))
* include full track metadata in GetAudioFilesByReleaseGroup query ([97f256d](https://github.com/onion-4-dinner/yellowjacket/commit/97f256d67f463d752f7adc5b400c4bf34eae1df1))
* **quick-10:** add migration 5 and fix entity cache for composite album key ([d43ba7b](https://github.com/onion-4-dinner/yellowjacket/commit/d43ba7bd0c7ace2a9ed71990a19498f8e9f90751))
* **quick-10:** update release_groups schema and queries for composite uniqueness ([999ab96](https://github.com/onion-4-dinner/yellowjacket/commit/999ab967beb9107a3f30ba287acbffad22f0b0de))
* **quick-13:** resolve lint issues in main source files ([e1a95e6](https://github.com/onion-4-dinner/yellowjacket/commit/e1a95e65a9f0f436b2e2d92befa9c881b6e8e430))
* **quick-14:** add roll-back-on-failure to queue index advancement ([2820de2](https://github.com/onion-4-dinner/yellowjacket/commit/2820de2510560fcd6d1015c18542d5ac30468247))
* **quick-9:** set fixed height on queue track items for stable virtualizer scroll ([ebde5e5](https://github.com/onion-4-dinner/yellowjacket/commit/ebde5e5a8bc4da8f40bef8f171c7ed86c213a336))
### Performance
* **07-01:** add incremental persistence helpers for queue mutations ([cdd17db](https://github.com/onion-4-dinner/yellowjacket/commit/cdd17db27509908514c21517631306655a2b3bd7))
* **07-01:** eliminate redundant lookups in SetQueue Phase 2 ([ced58fe](https://github.com/onion-4-dinner/yellowjacket/commit/ced58fe6a93d6f220137562b8ff09ffc33c69266))
* **07-02:** defer eagerFetch to after DOM ready for instant app shell ([cd98ad6](https://github.com/onion-4-dinner/yellowjacket/commit/cd98ad6dc8c2e4e6e0f01a48099b0c0511bf5a98))
* **08-01:** add queueMicrotask coalescing to library store and debounce search input ([3bf66ed](https://github.com/onion-4-dinner/yellowjacket/commit/3bf66ed125ed55bfbde95b0bc973710c2f2243b8))
* **08-02:** migrate cover-grid, artists-view, and genres-view virtualizers to repeat() directive ([1c3514d](https://github.com/onion-4-dinner/yellowjacket/commit/1c3514da1d0491b9758d7a6f9f72d59ef78fc8ed))
* **08-02:** migrate track-list and queue-panel virtualizers to repeat() directive ([d2d7d8c](https://github.com/onion-4-dinner/yellowjacket/commit/d2d7d8c6ce22923772cae4858b02804d15f74bb7))
* **08-03:** optimize column rendering and apply classMap to queue-panel renderTrackItem ([62f41c2](https://github.com/onion-4-dinner/yellowjacket/commit/62f41c24910632b270f9f5765e20e48db4b95ec9))
* **08-03:** replace class string construction with classMap directive in renderTrackRow ([ad21027](https://github.com/onion-4-dinner/yellowjacket/commit/ad210278fc20729dc76390e6bba9bff050549046))
### Refactoring
* **06-01:** consolidate search queries to use track_metadata VIEW ([9159b40](https://github.com/onion-4-dinner/yellowjacket/commit/9159b409dcd2afaa7dcc97bf5b0694edf85f06a4))
* **quick-14:** make playOrLoadCurrentTrack and playCurrentTrack return bool ([6eeddda](https://github.com/onion-4-dinner/yellowjacket/commit/6eeddda97669258cc5b7ba175a3c98d598a2871f))
## [1.1.3](https://github.com/onion-4-dinner/yellowjacket/compare/v1.1.2...v1.1.3) (2026-02-21)
### Bug Fixes
* add typescript as explicit devDependency and auto-install frontend deps in setup ([#70](https://github.com/onion-4-dinner/yellowjacket/issues/70)) ([7316587](https://github.com/onion-4-dinner/yellowjacket/commit/73165877fa79656ab9bc6f60bd8e9e52d6be206c))
* use local tsc binary in pre-commit hook to avoid PATH issues ([#71](https://github.com/onion-4-dinner/yellowjacket/issues/71)) ([6079e55](https://github.com/onion-4-dinner/yellowjacket/commit/6079e558ff913d38c7f1c4aeb52cc09474c4ed20))
## [1.1.2](https://github.com/onion-4-dinner/yellowjacket/compare/v1.1.1...v1.1.2) (2026-02-15)
### Bug Fixes
* r2 upload ([#69](https://github.com/onion-4-dinner/yellowjacket/issues/69)) ([0252466](https://github.com/onion-4-dinner/yellowjacket/commit/0252466f615b4e2fd9694790c6d311a9eac1ccf2))
## [1.1.1](https://github.com/onion-4-dinner/yellowjacket/compare/v1.1.0...v1.1.1) (2026-02-15)
### Bug Fixes
* **ci:** remove build-check job from CI workflow ([#66](https://github.com/onion-4-dinner/yellowjacket/issues/66)) ([42d3f45](https://github.com/onion-4-dinner/yellowjacket/commit/42d3f45d85afa694e9545997af3ff4ac814ad021))
## [1.1.0](https://github.com/onion-4-dinner/yellowjacket/compare/v1.0.3...v1.1.0) (2026-02-15)
### Features
* **ci:** upload release artifacts to Cloudflare R2 ([#65](https://github.com/onion-4-dinner/yellowjacket/issues/65)) ([8985084](https://github.com/onion-4-dinner/yellowjacket/commit/89850848cbf7783e5c85348ff18f7cd11d60231a))
## [1.0.3](https://github.com/onion-4-dinner/yellowjacket/compare/v1.0.2...v1.0.3) (2026-02-15)
### ⚠ BREAKING CHANGES
* **deps:** update module github.com/evilmartians/lefthook to v2 (#61)
* **deps:** update actions/checkout action to v6 (#45)
* **deps:** update dependency vite to v7 (#53)
### Bug Fixes
* resolve all lint errors and make linting a required CI check ([#62](https://github.com/onion-4-dinner/yellowjacket/issues/62)) ([30b2480](https://github.com/onion-4-dinner/yellowjacket/commit/30b2480df49f57878b0e8c923da6ad8d6fe99416))
* virtual list and cover grid ([#63](https://github.com/onion-4-dinner/yellowjacket/issues/63)) ([7579a76](https://github.com/onion-4-dinner/yellowjacket/commit/7579a768be84225ed46db4e7a90781f3e30e2953))
### Miscellaneous
* **deps:** update actions/checkout action to v6 ([#45](https://github.com/onion-4-dinner/yellowjacket/issues/45)) ([2d6e221](https://github.com/onion-4-dinner/yellowjacket/commit/2d6e22105d2daed1dc5b586c0442e2941949a165))
* **deps:** update dependency vite to v7 ([#53](https://github.com/onion-4-dinner/yellowjacket/issues/53)) ([f0006c4](https://github.com/onion-4-dinner/yellowjacket/commit/f0006c4c4335b60b58cccdd29de4792965e39694))
* **deps:** update module github.com/evilmartians/lefthook to v2 ([#61](https://github.com/onion-4-dinner/yellowjacket/issues/61)) ([e32b217](https://github.com/onion-4-dinner/yellowjacket/commit/e32b2179129ae7f26037697a125710ff7587566d))
## [1.0.2](https://github.com/onion-4-dinner/yellowjacket/compare/v1.0.1...v1.0.2) (2026-02-14)
### ⚠ BREAKING CHANGES
* **deps:** update actions/setup-node action to v6 (#48)
* **deps:** update dependency stylelint-config-standard to v40 (#52)
* **deps:** update dependency node to v24 (#51)
* **deps:** update dependency vite-plugin-static-copy to v3 (#54)
* **deps:** update golangci/golangci-lint-action action to v9 (#55)
* **deps:** update amannn/action-semantic-pull-request action to v6 (#50)
* **deps:** update actions/upload-artifact action to v6 (#49)
* **deps:** update actions/setup-go action to v6 (#47)
* **deps:** update actions/download-artifact action to v7 (#46)
### Bug Fixes
* **ci:** use allowedPostUpgradeCommands for Renovate post-upgrade tasks ([#60](https://github.com/onion-4-dinner/yellowjacket/issues/60)) ([0aef483](https://github.com/onion-4-dinner/yellowjacket/commit/0aef483b3cccd0616fd5be2d06d0856b46851d09))
### Miscellaneous
* **deps:** update actions/download-artifact action to v7 ([#46](https://github.com/onion-4-dinner/yellowjacket/issues/46)) ([1910f99](https://github.com/onion-4-dinner/yellowjacket/commit/1910f99cf64e9bdc5ce91e89cab254ecca15d030))
* **deps:** update actions/setup-go action to v6 ([#47](https://github.com/onion-4-dinner/yellowjacket/issues/47)) ([8911fb2](https://github.com/onion-4-dinner/yellowjacket/commit/8911fb2400047cf2f3dfa719edc1d1bf474cdaa5))
* **deps:** update actions/setup-node action to v6 ([#48](https://github.com/onion-4-dinner/yellowjacket/issues/48)) ([d7382fd](https://github.com/onion-4-dinner/yellowjacket/commit/d7382fd8444b6618dbfe991f5f97231528a07f13))
* **deps:** update actions/upload-artifact action to v6 ([#49](https://github.com/onion-4-dinner/yellowjacket/issues/49)) ([a2c644b](https://github.com/onion-4-dinner/yellowjacket/commit/a2c644b00eed83acc0ed38a2eb8c73868b7b79af))
* **deps:** update amannn/action-semantic-pull-request action to v6 ([#50](https://github.com/onion-4-dinner/yellowjacket/issues/50)) ([643ba27](https://github.com/onion-4-dinner/yellowjacket/commit/643ba27f066164aeb47e8d9aaf20fe98b9b69d30))
* **deps:** update dependency node to v24 ([#51](https://github.com/onion-4-dinner/yellowjacket/issues/51)) ([e7d3971](https://github.com/onion-4-dinner/yellowjacket/commit/e7d39711078ce86b0c029f0d03ff81162c5dc28a))
* **deps:** update dependency stylelint-config-standard to v40 ([#52](https://github.com/onion-4-dinner/yellowjacket/issues/52)) ([422aabc](https://github.com/onion-4-dinner/yellowjacket/commit/422aabcc07e9700ff189302b363e13d87c69163a))
* **deps:** update dependency vite-plugin-static-copy to v3 ([#54](https://github.com/onion-4-dinner/yellowjacket/issues/54)) ([77fa643](https://github.com/onion-4-dinner/yellowjacket/commit/77fa6435a5298f58ef83607d99c59b876132c66c))
* **deps:** update golangci/golangci-lint-action action to v9 ([#55](https://github.com/onion-4-dinner/yellowjacket/issues/55)) ([aedb7d1](https://github.com/onion-4-dinner/yellowjacket/commit/aedb7d1e6d204c56c468dd26b340752fd6bfeaeb))
## [1.0.1](https://github.com/onion-4-dinner/yellowjacket/compare/v1.0.0...v1.0.1) (2026-02-14)
### Bug Fixes
* resolve Renovate repo detection and pre-push hook hang ([#36](https://github.com/onion-4-dinner/yellowjacket/issues/36)) ([b205889](https://github.com/onion-4-dinner/yellowjacket/commit/b205889128f01e9eb75b607cf7c4034887cda3f4))
## 1.0.0 (2026-02-14)
### Features
* **ci:** add semantic-release pipeline, cross-platform builds, and lefthook git hooks ([caf3e84](https://github.com/onion-4-dinner/yellowjacket/commit/caf3e843af7da37e05da36da5c41b6dc3c53ded1))
### Bug Fixes
* allow library to initialize without config and fix lefthook lint flag ([5a958db](https://github.com/onion-4-dinner/yellowjacket/commit/5a958db16284a74e19c43259757b163b347cda7d))
* **ci:** configure git credentials explicitly for semantic-release PAT ([24f21af](https://github.com/onion-4-dinner/yellowjacket/commit/24f21af8350227e77fc1fef9243c238e6417aca0))
* **ci:** fix golangci-lint version, skip player test in CI, remove standalone frontend build ([7317e09](https://github.com/onion-4-dinner/yellowjacket/commit/7317e093a7f92651ab65b2f83381d02105bdc0df))
* **ci:** resolve CI failures for Go checks, codegen, and frontend type-checking ([d4f9361](https://github.com/onion-4-dinner/yellowjacket/commit/d4f936143ac75fbf3247cdbe2113bd89b0795d83))
* **ci:** use PAT for semantic-release to trigger build workflow ([68d41c0](https://github.com/onion-4-dinner/yellowjacket/commit/68d41c0ff22fede57acab7a2bfed42df7814bb90))
* rename downloaded artifacts to platform-specific names for release ([e3bda0e](https://github.com/onion-4-dinner/yellowjacket/commit/e3bda0e2fc7700fad382cabe00aeb46f91fbb0a0))
* resolve frontend build failures in CI ([330a53c](https://github.com/onion-4-dinner/yellowjacket/commit/330a53c9f4b1292840ad0f75479b76b3d429c954))
* trigger build workflow from release event instead of tag push ([47772f7](https://github.com/onion-4-dinner/yellowjacket/commit/47772f73cc04093c55414bf20ebe2ef442418d19))
* use path.Join for embed.FS paths to fix Windows build ([672fe24](https://github.com/onion-4-dinner/yellowjacket/commit/672fe24ee99debf4a394fff7eec55f17b0e44476))
## 1.0.0 (2026-02-14)
### Features
* **ci:** add semantic-release pipeline, cross-platform builds, and lefthook git hooks ([caf3e84](https://github.com/onion-4-dinner/yellowjacket/commit/caf3e843af7da37e05da36da5c41b6dc3c53ded1))
### Bug Fixes
* allow library to initialize without config and fix lefthook lint flag ([5a958db](https://github.com/onion-4-dinner/yellowjacket/commit/5a958db16284a74e19c43259757b163b347cda7d))
* **ci:** configure git credentials explicitly for semantic-release PAT ([24f21af](https://github.com/onion-4-dinner/yellowjacket/commit/24f21af8350227e77fc1fef9243c238e6417aca0))
* **ci:** fix golangci-lint version, skip player test in CI, remove standalone frontend build ([7317e09](https://github.com/onion-4-dinner/yellowjacket/commit/7317e093a7f92651ab65b2f83381d02105bdc0df))
* **ci:** resolve CI failures for Go checks, codegen, and frontend type-checking ([d4f9361](https://github.com/onion-4-dinner/yellowjacket/commit/d4f936143ac75fbf3247cdbe2113bd89b0795d83))
* **ci:** use PAT for semantic-release to trigger build workflow ([68d41c0](https://github.com/onion-4-dinner/yellowjacket/commit/68d41c0ff22fede57acab7a2bfed42df7814bb90))
* resolve frontend build failures in CI ([330a53c](https://github.com/onion-4-dinner/yellowjacket/commit/330a53c9f4b1292840ad0f75479b76b3d429c954))
* trigger build workflow from release event instead of tag push ([47772f7](https://github.com/onion-4-dinner/yellowjacket/commit/47772f73cc04093c55414bf20ebe2ef442418d19))
* use path.Join for embed.FS paths to fix Windows build ([672fe24](https://github.com/onion-4-dinner/yellowjacket/commit/672fe24ee99debf4a394fff7eec55f17b0e44476))
## [1.0.3](https://github.com/onion-4-dinner/yellowjacket/compare/v1.0.2...v1.0.3) (2026-02-14)
### Bug Fixes
* use path.Join for embed.FS paths to fix Windows build ([672fe24](https://github.com/onion-4-dinner/yellowjacket/commit/672fe24ee99debf4a394fff7eec55f17b0e44476))
## [1.0.2](https://github.com/onion-4-dinner/yellowjacket/compare/v1.0.1...v1.0.2) (2026-02-14)
### Bug Fixes
* resolve frontend build failures in CI ([330a53c](https://github.com/onion-4-dinner/yellowjacket/commit/330a53c9f4b1292840ad0f75479b76b3d429c954))
## [1.0.1](https://github.com/onion-4-dinner/yellowjacket/compare/v1.0.0...v1.0.1) (2026-02-14)
### Bug Fixes
* allow library to initialize without config and fix lefthook lint flag ([5a958db](https://github.com/onion-4-dinner/yellowjacket/commit/5a958db16284a74e19c43259757b163b347cda7d))
## 1.0.0 (2026-02-14)
### Features
* **ci:** add semantic-release pipeline, cross-platform builds, and lefthook git hooks ([caf3e84](https://github.com/onion-4-dinner/yellowjacket/commit/caf3e843af7da37e05da36da5c41b6dc3c53ded1))
### Bug Fixes
* **ci:** configure git credentials explicitly for semantic-release PAT ([24f21af](https://github.com/onion-4-dinner/yellowjacket/commit/24f21af8350227e77fc1fef9243c238e6417aca0))
* **ci:** fix golangci-lint version, skip player test in CI, remove standalone frontend build ([7317e09](https://github.com/onion-4-dinner/yellowjacket/commit/7317e093a7f92651ab65b2f83381d02105bdc0df))
* **ci:** resolve CI failures for Go checks, codegen, and frontend type-checking ([d4f9361](https://github.com/onion-4-dinner/yellowjacket/commit/d4f936143ac75fbf3247cdbe2113bd89b0795d83))
* **ci:** use PAT for semantic-release to trigger build workflow ([68d41c0](https://github.com/onion-4-dinner/yellowjacket/commit/68d41c0ff22fede57acab7a2bfed42df7814bb90))
## [1.0.2](https://github.com/onion-4-dinner/yellowjacket/compare/v1.0.1...v1.0.2) (2026-02-14)
### Bug Fixes
* **ci:** fix golangci-lint version, skip player test in CI, remove standalone frontend build ([7317e09](https://github.com/onion-4-dinner/yellowjacket/commit/7317e093a7f92651ab65b2f83381d02105bdc0df))
## [1.0.1](https://github.com/onion-4-dinner/yellowjacket/compare/v1.0.0...v1.0.1) (2026-02-14)
### Bug Fixes
* **ci:** resolve CI failures for Go checks, codegen, and frontend type-checking ([d4f9361](https://github.com/onion-4-dinner/yellowjacket/commit/d4f936143ac75fbf3247cdbe2113bd89b0795d83))
## 1.0.0 (2026-02-14)
### Features
* **ci:** add semantic-release pipeline, cross-platform builds, and lefthook git hooks ([caf3e84](https://github.com/onion-4-dinner/yellowjacket/commit/caf3e843af7da37e05da36da5c41b6dc3c53ded1))
### Bug Fixes
* **ci:** configure git credentials explicitly for semantic-release PAT ([24f21af](https://github.com/onion-4-dinner/yellowjacket/commit/24f21af8350227e77fc1fef9243c238e6417aca0))
* **ci:** use PAT for semantic-release to trigger build workflow ([68d41c0](https://github.com/onion-4-dinner/yellowjacket/commit/68d41c0ff22fede57acab7a2bfed42df7814bb90))
## 1.0.0 (2026-02-14)
### Features
* **ci:** add semantic-release pipeline, cross-platform builds, and lefthook git hooks ([caf3e84](https://github.com/onion-4-dinner/yellowjacket/commit/caf3e843af7da37e05da36da5c41b6dc3c53ded1))
### Bug Fixes
* **ci:** configure git credentials explicitly for semantic-release PAT ([24f21af](https://github.com/onion-4-dinner/yellowjacket/commit/24f21af8350227e77fc1fef9243c238e6417aca0))
* **ci:** use PAT for semantic-release to trigger build workflow ([68d41c0](https://github.com/onion-4-dinner/yellowjacket/commit/68d41c0ff22fede57acab7a2bfed42df7814bb90))
## 1.0.0 (2026-02-14)
### Features
* **ci:** add semantic-release pipeline, cross-platform builds, and lefthook git hooks ([caf3e84](https://github.com/onion-4-dinner/yellowjacket/commit/caf3e843af7da37e05da36da5c41b6dc3c53ded1))
### Bug Fixes
* **ci:** use PAT for semantic-release to trigger build workflow ([68d41c0](https://github.com/onion-4-dinner/yellowjacket/commit/68d41c0ff22fede57acab7a2bfed42df7814bb90))
## 1.0.0 (2026-02-14)
### Features
* **ci:** add semantic-release pipeline, cross-platform builds, and lefthook git hooks ([caf3e84](https://github.com/onion-4-dinner/yellowjacket/commit/caf3e843af7da37e05da36da5c41b6dc3c53ded1))
History before `v0.0.1` is in `git log`. The versions before it were cut
by hand and are not on the releases page; the entries this file used to
hold were generated against a GitHub remote this project no longer has,
and every link in them was dead.
+98 -12
View File
@@ -2027,13 +2027,31 @@ Pre-commit hooks verify generated code is fresh — always run `make generate` a
two in step or semantic-release will decline to release something the
check accepted.
`.releaserc.yml` is a complete semantic-release config that **nothing
currently runs** — no workflow invokes it, and `CHANGELOG.md` is not
being written by it. That is deliberate for now (wiring it means pushing
tags, committing a changelog back, and interacting with the three
publish workflows); it is recorded here rather than implied, because
this file claimed for five phases that commitlint gated CI and that
semantic release ran, and neither was true.
`.releaserc.yml` **is** what runs now, from `release.yml`, and it is why
the commit grammar is load-bearing rather than decorative: a merge to
`main` whose commits are all `chore`/`ci`/`docs` releases nothing, and a
mistyped `feat` ships a minor version. `make release-dry` answers "what
would this merge release" without pushing.
**`@semantic-release/github` is not in that config and must not be.**
Gitea's API is `/api/v1` and is not GitHub's surface, so
`@semantic-release/exec` calls `scripts/gitea-release.sh` instead — one
`POST`, which is the whole of the Gitea-shaped work. The community
plugin (`@saithodev/semantic-release-gitea`) was considered and
rejected: last published 2022, on `got@10`, declaring no peer
dependency on semantic-release at all.
Two things in it fail *silently* and are therefore pinned with their
reasons. **The notes come from `CHANGELOG.md`, not from an argument**:
release notes are rendered commit messages — arbitrary text carrying
backticks, quotes and `$` — so templating `${nextRelease.notes}` into
`publishCmd` would be a shell injection whose input is the commit log.
And **`conventional-changelog-conventionalcommits` is held at 9**,
because at 10 it is quietly incompatible with the writer
`release-notes-generator@14` pulls in: every release note renders as a
bare `## 0.0.1 (date)` heading with no sections and no commits beneath
it, no step fails, and the release ships with an empty body. Check the
rendered notes, never the exit code.
## Testing
@@ -2042,17 +2060,85 @@ Tests use `database.NewTestDB(t)` for in-memory SQLite, built by the same
## Git Workflow
Feature branches and PRs are the norm, but direct pushes to `main` are allowed. Pre-commit runs vet, lint, codegen check, and frontend typecheck in parallel. Pre-push runs the full test suite.
Feature branches and PRs are the only way in: **`main` is a protected
branch** (`enable_push: false`, an empty push whitelist, and `CI / check*`
+ `CI / e2e*` as required status checks), so a direct push is rejected by
the pre-receive hook. This file said otherwise for a long time. Tags are
*not* protected, which is what lets `release.yml` push one.
Pre-commit runs vet, lint, codegen check, and frontend typecheck in parallel. Pre-push runs the full test suite.
## CI
Five workflows in `.gitea/workflows/`. Four of them package and
Seven workflows in `.gitea/workflows/`. Five of them package and
publish (`arch-package`, `homebrew-formula`, `index-artifact`,
`android-apk`); only `ci.yml` gates, and it is the one to look at when
`android-apk`, `desktop-assets`); `release.yml` decides *whether* four of
those run at all; only `ci.yml` gates, and it is the one to look at when
deciding whether a push was healthy.
**`android-apk.yml` is the only one keyed on a tag and the only one
that can lose something irrecoverable.** It builds the signed
**`release.yml` is the entry point for all of it.** On every push to
`main` it reads the Conventional Commits since the last tag and, if any
is releasable, writes the changelog, pushes the tag and creates the Gitea
release whose body is that changelog section. `arch-package`,
`homebrew-formula`, `android-apk` and `desktop-assets` are all keyed on
`v*`, so **the tag push is what starts them** — nothing is released by
hand any more.
Four things about it are load-bearing:
- **The tag is pushed with a user PAT, not the Actions token.** Gitea,
like GitHub, does not start a workflow from a ref pushed by a
workflow's own token (go-gitea#33123). The token is what decides this,
so `PACKAGE_TOKEN` is handed to semantic-release as the
`repositoryUrl` credential and the push is attributed to a person.
- **That same limitation is used deliberately, once.** semantic-release
calls the first release of a tagless repo `1.0.0` and offers no way to
say otherwise, so a `v0.0.0` floor tag is what makes the first release
`0.0.1` — and it is pushed with the *Actions* token precisely so it
triggers nothing. All four publishers additionally skip `v0.0.0`
explicitly, cleanly rather than by failing, because a floor is not a
shipment.
- **The release page is the changelog, and that follows from the branch
protection.** `@semantic-release/git` would push a `chore(release):`
commit back to `main`, which the pre-receive hook rejects — *after* the
tag had been pushed, leaving a tagged release the run then reports as
failed. Whitelisting the CI user was the alternative and was declined:
it weakens a protection someone set on purpose and lets a bot push to
`main` without the checks every human PR passes. So the plugin is
absent, `@semantic-release/changelog` writes to a gitignored
`.release-notes.md` purely to carry the notes into
`scripts/gitea-release.sh`, and `CHANGELOG.md` is a signpost to the
releases page rather than a file that would silently stop updating.
The workflow keeps its `chore(release):` guard anyway, for the day
someone adds the plugin back.
- **An asset upload waits for the release to exist.** semantic-release
pushes the tag in `prepare` and creates the release in `publish`, so
the tag push that starts these workflows happens *before* there is a
release id to attach to. `scripts/release-asset.sh` polls for it. The
capacity-1 runner serialises things enough that this would usually work
by accident, which is the worst kind of bug.
**Releases restarted at `0.0.1`, which is a downgrade on every channel.**
pacman and Homebrew both silently offer no upgrade from the old `1.x`,
and Android refuses the install outright — its remedy is an uninstall
that takes the user's library. This was chosen over pacman's `epoch` and
over offsetting `versionCode`, on the grounds that both are permanent and
a reinstall is once. `packaging/homebrew/README.md` and
`docs/android-release.md` say so where a user would look.
**`desktop-assets.yml` publishes Linux and nothing else, and macOS is not
an oversight.** `GOOS=darwin CGO_ENABLED=0` fails at
`wails/v3/pkg/mac: build constraints exclude all Go files` — the darwin
backend is Objective-C behind cgo, so a `.app` needs a macOS host and the
runner is a Linux container. That is exactly why the Homebrew formula
builds from source on the user's own Mac. Windows *does* cross-compile
cleanly (`GOOS=windows CGO_ENABLED=0`, a couple of seconds — oto uses
WinMM through `x/sys`, sqlite is modernc's pure-Go driver, WebView2 is
COM syscalls, MPRIS is `linux && !android`-tagged) and is deliberately
not published: no Windows build of this app has ever been *run*, and no
tier here can exercise one.
**`android-apk.yml` is the one that can lose something irrecoverable.** It builds the signed
`arm64-v8a` APK (the only ABI Android can run this app on — see
`app/build.gradle`) on every `v*` tag and publishes it to the *generic* registry, which is
readable without credentials — the reason Obtainium can poll a plain
+19 -1
View File
@@ -192,6 +192,24 @@ skill-check: ## Fail if the agent docs name a missing make target, or AGENTS.md
commit-check: ## Fail if a commit subject is not a Conventional Commit
@./scripts/commit-check.sh $(if $(RANGE),--range $(RANGE))
# What a merge to main would release, without releasing it. Reads the
# same .releaserc.yml CI does, so "why did that not cut a version" is
# answerable locally instead of by pushing and watching. Needs no
# credentials: --dry-run neither tags nor publishes.
#
# The pins must stay identical to release.yml's, which is where the note
# on holding the conventionalcommits preset at 9 lives -- at 10 the
# release notes come out empty with everything green.
release-dry: ## Print the version a merge to main would release
@npx --yes \
-p semantic-release@25 \
-p @semantic-release/commit-analyzer@13 \
-p @semantic-release/release-notes-generator@14 \
-p @semantic-release/changelog@7 \
-p @semantic-release/exec@7 \
-p conventional-changelog-conventionalcommits@9 \
semantic-release --dry-run --no-ci
# v3 generates TypeScript into frontend/bindings/, nested by Go import
# path, rather than v2's frontend/wailsjs/. The `@go` alias absorbs the
# constant prefix, so a call site imports '@go/library/library.js'.
@@ -207,7 +225,7 @@ bindings: ## Regenerate frontend/bindings from the bound Go services
sandbox-seed sandbox-seed-bulk sandbox-seeds e2e e2e-setup e2e-report \
perf perf-compare \
ui-test ui-watch ui-visual ui-visual-update ui-setup \
bindings bindings-check skill-check commit-check
bindings bindings-check skill-check commit-check release-dry
# Base directory for fresh-install sandboxes. Deliberately NOT $TMPDIR:
# on most Linux distros /tmp is tmpfs (RAM-backed) and only a few GB, so
+25 -6
View File
@@ -46,23 +46,42 @@ func TestCacheMiss(t *testing.T) {
}
}
// TestCacheTTLExpiry checks both halves of the TTL contract, and uses two
// entries to do it.
//
// **No assertion here may depend on an upper bound of elapsed wall-clock
// time**, which is what the single-entry version of this test did: it set
// a 1s TTL and immediately asserted a *hit*, so on a loaded runner — one
// goroutine descheduled for over a second while the rest of the suite
// runs — the entry was correctly gone and the test failed with "expected
// cache hit immediately after set". It did exactly that in CI while
// passing five times out of five locally.
//
// Sleeping *past* a TTL is always safe, so the expiry half keeps a short
// one; the presence half gets a TTL nothing can outrun.
func TestCacheTTLExpiry(t *testing.T) {
c := newTestCache(t)
data := []byte(`{"ephemeral":true}`)
c.Set("ttl-test-key", data, 1*time.Second, "", "")
c.Set("ttl-live-key", data, time.Hour, "", "")
c.Set("ttl-expiring-key", data, 1*time.Second, "", "")
// Verify it's there immediately.
if _, ok := c.Get("ttl-test-key"); !ok {
t.Fatal("expected cache hit immediately after set")
if _, ok := c.Get("ttl-live-key"); !ok {
t.Fatal("expected a cache hit on an entry with an hour to live")
}
// Wait for expiry.
// Wait for the short one to expire.
time.Sleep(2 * time.Second)
if _, ok := c.Get("ttl-test-key"); ok {
if _, ok := c.Get("ttl-expiring-key"); ok {
t.Error("expected cache miss after TTL expiry, got hit")
}
// And the long-lived entry is still there, which is what says the
// sweep above expired an entry rather than the cache.
if _, ok := c.Get("ttl-live-key"); !ok {
t.Error("the hour-long entry expired too")
}
}
func TestCacheMBID(t *testing.T) {
+27 -1
View File
@@ -3,7 +3,33 @@
`.gitea/workflows/android-apk.yml` builds a signed `arm64-v8a` APK on
every `v*` tag and publishes it to Gitea's
**generic** package registry, which is readable without credentials —
which is what lets Obtainium poll a plain URL with no token.
which is what lets Obtainium poll a plain URL with no token. It also
attaches the same file to the Gitea release, which is what a person
looking at the release page downloads.
**Tags are not pushed by hand any more.** `.gitea/workflows/release.yml`
reads the Conventional Commits on every merge to `main`, decides the
version, and pushes the tag this workflow is keyed on — so releasing the
APK means merging a `fix:` or `feat:` commit, not running `git tag`. The
`workflow_dispatch` path below remains, for rebuilding a tag that already
exists.
## The 1.x installs cannot be upgraded to 0.0.x
Releases restarted at **0.0.1** when they became automatic (plan 017).
`versionCode` is computed as `maj*10000 + min*100 + pat`, so 0.0.1 is
**1** against the **10300** an installed 1.3.0 build carries — and
**Android refuses a downgrade outright**, with
`INSTALL_FAILED_VERSION_DOWNGRADE`.
The only way through is `adb uninstall app.yellowjacket` (or the
launcher's own uninstall) before installing 0.0.1, **and that takes the
device's library, playlists and play counts with it** — the same loss the
signing key section below exists to prevent, arrived at from the other
direction. This was chosen deliberately over offsetting `versionCode` by
a constant, on the grounds that the honest number is worth one reinstall
while an offset is permanent. The workflow prints a warning whenever the
code it computes is below 10600.
```
https://git.ljones.me/api/packages/yonlu/generic/yellowjacket-android/latest/yellowjacket.apk
-2
View File
@@ -1,2 +0,0 @@
{
}
+12 -1
View File
@@ -1,6 +1,17 @@
# Maintainer: yonlu <yj@yellowjacket.app>
pkgname=yellowjacket
pkgver=1.3.0
# A fallback and the default tag for a manual build; pkgver() below is what
# actually decides the version, from the clone's own git history. Versions
# restarted at 0.0.1 when releases became automatic (plan 017) — which is a
# *downgrade* from the 1.x packages already in the registry, so pacman offers
# no upgrade and an existing install has to be removed and reinstalled once:
#
# pacman -R yellowjacket && pacman -S yellowjacket
#
# `epoch=1` would have avoided that for one line, and was declined: an epoch
# can never be removed, and it would put a permanent `1:` in front of every
# version string this package will ever have.
pkgver=0.0.1
pkgrel=1
pkgdesc="Cross-platform desktop music player — local library, MusicBrainz explore & auto-tag"
arch=('x86_64')
+15
View File
@@ -22,6 +22,21 @@ brew install shadow-puppet/yellowjacket/yellowjacket
no separate `brew tap` step. To build the tip of `main` instead of the latest
release, add `--HEAD`.
### Upgrading from 1.x needs a reinstall, once
Releases became automatic and restarted at **0.0.1** (plan 017), which is
*lower* than the `1.3.0` this tap last published. Homebrew compares versions
and has no equivalent of pacman's `epoch`, so `brew upgrade` sees a downgrade
and offers **nothing at all** — silently, which is indistinguishable from the
tap having gone stale.
```bash
brew uninstall yellowjacket && brew install shadow-puppet/yellowjacket/yellowjacket
```
Nothing is stored inside the Cellar, so this costs a rebuild and no data. It is
a one-time step: 0.0.2 onwards upgrade normally.
## How publishing works
This directory holds the **canonical** formula. The tap users install from lives
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
#
# Create the Gitea release for a version semantic-release has just tagged.
#
# This is `@semantic-release/exec`'s publishCmd, and it exists because
# Gitea's API is /api/v1 and @semantic-release/github speaks GitHub's.
# That is the whole of the Gitea-shaped work: one POST.
#
# **The notes come from a file, not from an argument.** Release notes are
# rendered commit messages — arbitrary text carrying backticks, quotes and
# `$` — so interpolating ${nextRelease.notes} into a shell command would
# be an injection whose input is the commit log. @semantic-release/changelog
# has already written them to .release-notes.md by the time `publish` runs,
# so the only thing crossing the shell boundary here is a semver string,
# which is validated below anyway.
#
# That file is a gitignored build artifact, not a document: `main` is a
# protected branch, so nothing commits a changelog back to it and the
# release page is the changelog. See .releaserc.yml.
#
# Usage: scripts/gitea-release.sh <version> # e.g. 0.0.1
#
# Environment (all set by .gitea/workflows/release.yml):
# SERVER_URL https://git.ljones.me
# OWNER yonlu
# REPO yonlu/yellowjacket
# PACKAGE_TOKEN a user PAT with write access
set -euo pipefail
cd "$(dirname "$0")/.."
version="${1:?usage: gitea-release.sh <version>}"
# Validated rather than trusted: this is the one value that reaches a URL
# and a JSON document, and semantic-release is not the only thing that
# could ever call this.
if ! printf '%s' "$version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
echo "gitea-release: '$version' is not a semver version" >&2
exit 1
fi
: "${SERVER_URL:?SERVER_URL is not set}"
: "${REPO:?REPO is not set}"
: "${PACKAGE_TOKEN:?PACKAGE_TOKEN is not set}"
tag="v${version}"
# The top section of the changelog is this release's notes: everything
# from the first `## ` heading to the one after it. awk rather than sed
# so the "there is no second heading" case (the first release) needs no
# special handling.
notes=$(awk '
/^## / { seen++; if (seen > 1) exit }
seen { print }
' .release-notes.md)
if [ -z "$notes" ]; then
echo "gitea-release: found no release section at the top of .release-notes.md" >&2
echo ' the changelog plugin runs in prepare and this runs in publish, so' >&2
echo ' an empty section means the plugin order in .releaserc.yml moved.' >&2
exit 1
fi
echo "gitea-release: creating $tag from $(printf '%s' "$notes" | wc -l) lines of notes"
# jq builds the body, so a backtick or a quote in a commit subject is data
# rather than syntax.
payload=$(jq -n \
--arg tag "$tag" \
--arg name "$tag" \
--arg body "$notes" \
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')
code=$(curl -sS -o /tmp/gitea-release.out -w '%{http_code}' \
-X POST \
-H "Authorization: token ${PACKAGE_TOKEN}" \
-H "Content-Type: application/json" \
-d "$payload" \
"${SERVER_URL}/api/v1/repos/${REPO}/releases")
case "$code" in
201)
echo "gitea-release: created ${SERVER_URL}/${REPO}/releases/tag/${tag}"
;;
409)
# Already there. The correct outcome for a re-run of the same tag,
# and not a failure — the publish workflows are idempotent for the
# same reason.
echo "gitea-release: $tag already has a release; leaving it alone"
;;
*)
echo "gitea-release: POST /releases returned $code" >&2
cat /tmp/gitea-release.out >&2
exit 1
;;
esac
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
#
# Attach a built artifact to the Gitea release for a tag.
#
# **It waits for the release to exist, and that is the point of the
# file.** semantic-release pushes the tag in its `prepare` step and
# creates the release object in `publish` — so the tag push, which is
# what starts every publishing workflow, happens *before* there is a
# release id to upload to. A fast publisher can therefore arrive first.
#
# The runner has capacity 1, which serialises things enough that this
# would usually work by accident; that is the worst kind of bug, so the
# wait is explicit and a timeout is a loud failure rather than a silently
# skipped asset.
#
# Usage: scripts/release-asset.sh <tag> <file> [upload-name]
#
# Environment:
# SERVER_URL https://git.ljones.me
# REPO yonlu/yellowjacket
# PACKAGE_TOKEN a user PAT with write access
set -euo pipefail
tag="${1:?usage: release-asset.sh <tag> <file> [name]}"
file="${2:?usage: release-asset.sh <tag> <file> [name]}"
name="${3:-$(basename "$file")}"
: "${SERVER_URL:?SERVER_URL is not set}"
: "${REPO:?REPO is not set}"
: "${PACKAGE_TOKEN:?PACKAGE_TOKEN is not set}"
[ -s "$file" ] || { echo "release-asset: $file is missing or empty" >&2; exit 1; }
auth="Authorization: token ${PACKAGE_TOKEN}"
api="${SERVER_URL}/api/v1/repos/${REPO}"
# Up to five minutes. A release that has not appeared by then means the
# release job failed, and this should say so rather than time out quietly.
release_id=""
for attempt in $(seq 1 60); do
release_id=$(curl -sS -H "$auth" "${api}/releases/tags/${tag}" |
jq -r 'if type == "object" and has("id") then .id else empty end')
if [ -n "$release_id" ]; then
echo "release-asset: release for $tag is id $release_id (after ${attempt} check(s))"
break
fi
[ "$attempt" -eq 1 ] && echo "release-asset: waiting for the release for $tag to be created"
sleep 5
done
if [ -z "$release_id" ]; then
echo "release-asset: no release for $tag after 5 minutes." >&2
echo " The tag is pushed in semantic-release's prepare step and the release" >&2
echo " is created in publish, so this means the release job did not get that" >&2
echo " far. Check the run of release.yml for this commit." >&2
exit 1
fi
# Gitea refuses a duplicate asset name rather than replacing it, so a
# re-run of the same tag deletes the old one first. That keeps a manual
# workflow_dispatch rebuild idempotent, which is the only reason anyone
# re-runs one of these.
existing=$(curl -sS -H "$auth" "${api}/releases/${release_id}/assets" |
jq -r --arg n "$name" '.[]? | select(.name == $n) | .id')
if [ -n "$existing" ]; then
echo "release-asset: replacing the existing '$name' (asset $existing)"
curl -sS -o /dev/null -H "$auth" -X DELETE \
"${api}/releases/${release_id}/assets/${existing}"
fi
echo "release-asset: uploading $name ($(du -h "$file" | cut -f1))"
code=$(curl -sS -o /tmp/release-asset.out -w '%{http_code}' \
-H "$auth" \
-X POST \
-F "attachment=@${file};filename=${name}" \
"${api}/releases/${release_id}/assets?name=${name}")
if [ "$code" != "201" ]; then
echo "release-asset: upload returned $code" >&2
cat /tmp/release-asset.out >&2
exit 1
fi
echo "release-asset: attached $name to $tag"