diff --git a/.gitea/workflows/android-apk.yml b/.gitea/workflows/android-apk.yml index f91c042..93165d4 100644 --- a/.gitea/workflows/android-apk.yml +++ b/.gitea/workflows/android-apk.yml @@ -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" diff --git a/.gitea/workflows/arch-package.yml b/.gitea/workflows/arch-package.yml index 24bfd58..701d386 100644 --- a/.gitea/workflows/arch-package.yml +++ b/.gitea/workflows/arch-package.yml @@ -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 diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 7f57cc0..78a2763 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -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 diff --git a/.gitea/workflows/desktop-assets.yml b/.gitea/workflows/desktop-assets.yml new file mode 100644 index 0000000..34db86e --- /dev/null +++ b/.gitea/workflows/desktop-assets.yml @@ -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" diff --git a/.gitea/workflows/homebrew-formula.yml b/.gitea/workflows/homebrew-formula.yml index 0b70221..c19d14e 100644 --- a/.gitea/workflows/homebrew-formula.yml +++ b/.gitea/workflows/homebrew-formula.yml @@ -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 diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..5941245 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,168 @@ +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" + + echo "no v* tag exists — seeding v0.0.0 so the first release is 0.0.1" + git tag v0.0.0 "${{ github.sha }}" + git push --quiet \ + "https://x-access-token:${token}@${SERVER_URL#https://}/${REPO}.git" \ + refs/tags/v0.0.0 + echo "seeded v0.0.0 at ${{ 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" diff --git a/.gitignore b/.gitignore index f9b2c5e..3b3893d 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.planning/plans/active/017-release-automation.md b/.planning/plans/active/017-release-automation.md new file mode 100644 index 0000000..29aed26 --- /dev/null +++ b/.planning/plans/active/017-release-automation.md @@ -0,0 +1,358 @@ +# 017 — Releases that happen by themselves + +> **Status: built, not yet run.** Phases 0–4 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--android-arm64.apk` | `android-apk.yml` | already built, verified, signed | +| `yellowjacket--linux-amd64.tar.gz` | new job | binary + `.desktop` + icon | +| `yellowjacket--x86_64.pkg.tar.zst` | `arch-package.yml` | already built; free to attach | +| `yellowjacket--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`. diff --git a/.releaserc.yml b/.releaserc.yml index 7ec9350..1c375b2 100644 --- a/.releaserc.yml +++ b/.releaserc.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 885b41e..f58ba04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) + -### 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. diff --git a/CLAUDE.md b/CLAUDE.md index 1c3fca4..d5634d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/Makefile b/Makefile index cd9c499..83354a0 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/backend/explore/cache_test.go b/backend/explore/cache_test.go index 8feb4d2..a1bc0a4 100644 --- a/backend/explore/cache_test.go +++ b/backend/explore/cache_test.go @@ -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) { diff --git a/docs/android-release.md b/docs/android-release.md index 3284d1c..67f3999 100644 --- a/docs/android-release.md +++ b/docs/android-release.md @@ -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 diff --git a/package.json b/package.json deleted file mode 100644 index 2c63c08..0000000 --- a/package.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index e2fa916..d6d2949 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -1,6 +1,17 @@ # Maintainer: yonlu 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') diff --git a/packaging/homebrew/README.md b/packaging/homebrew/README.md index b5beb1b..5c0a193 100644 --- a/packaging/homebrew/README.md +++ b/packaging/homebrew/README.md @@ -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 diff --git a/scripts/gitea-release.sh b/scripts/gitea-release.sh new file mode 100755 index 0000000..f78c31f --- /dev/null +++ b/scripts/gitea-release.sh @@ -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 # 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 }" + +# 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 diff --git a/scripts/release-asset.sh b/scripts/release-asset.sh new file mode 100755 index 0000000..474b58a --- /dev/null +++ b/scripts/release-asset.sh @@ -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 [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 [name]}" +file="${2:?usage: release-asset.sh [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"