From 1f121546fbfa88f17047635966799e176340964e Mon Sep 17 00:00:00 2001 From: Jatin K Malik Date: Wed, 16 Sep 2026 19:40:13 -0700 Subject: [PATCH] fix(release): attach the snap to GitHub Releases (#839) * fix(release): attach the snap to GitHub Releases v0.17.0 packed the snap, then Store review of uinput failed the job, so the file never reached the tag. Build and attach first. A Store hold is a warning, not a failed release. SHA256SUMS covers the .snap. snap-backfill.yml rebuilds an existing tag, appends the snap to the manifest, and attests only that file. * fix(release): keep snap tags out of the shell and fail real Store errors Pass the tag through the environment like verify-release.yml. Treat only an allow-installation review as a successful Store publish. snap-backfill runs verify_release.py after it rewrites SHA256SUMS. * style(test): format snap-backfill shell-injection guard with black --- .github/workflows/release.yml | 122 +++++++++++++++++++++++----- .github/workflows/snap-backfill.yml | 101 +++++++++++++++++++++++ CHANGELOG.md | 2 +- README.md | 9 +- docs/INSTALL.md | 19 ++++- docs/RELEASE_PROCESS.md | 26 ++++-- docs/UPDATE.md | 4 +- tests/test_release_integrity.py | 2 +- tests/test_snap_packaging.py | 83 ++++++++++++++++--- web/src/app/changelog/page.tsx | 2 +- 10 files changed, 321 insertions(+), 49 deletions(-) create mode 100644 .github/workflows/snap-backfill.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf19983..7f7fea7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -176,10 +176,22 @@ jobs: Whisper.cpp engine only. Hotkeys need device access as documented in packaging/flatpak/README.md. Not on Flathub (https://github.com/flathub/flathub/pull/9368). + ### Snap + Tagged releases also attach `vocalinux_${{ steps.get_version.outputs.VERSION }}_amd64.snap` + and try the Snap Store (`edge` and `candidate`). `stable` is a manual promote. + Super-privileged plugs such as `uinput` can hold the Store revision in human + review; the GitHub `.snap` is the sideload in that case: + ```bash + sudo snap install --dangerous ./vocalinux_${{ steps.get_version.outputs.VERSION }}_amd64.snap + sudo snap connect vocalinux:uinput + ``` + `--dangerous` is required because this file is not a Store revision. It will not + refresh from the Store. amd64 only. + ### Verifying what you downloaded - `SHA256SUMS` covers every file here and is attached once the aarch64 AppImage and - both Flatpak builds finish, a few minutes after this release appears. The wheel and - sdist below are the same bytes published to PyPI. + `SHA256SUMS` covers every file here and is attached once the aarch64 AppImage, + both Flatpak builds, and the snap finish, a few minutes after this release appears. + The wheel and the sdist below are the same bytes published to PyPI. ```bash sha256sum -c --ignore-missing SHA256SUMS ``` @@ -355,12 +367,12 @@ jobs: --repo ${{ github.repository }} \ --clobber - # Runs last because the aarch64 AppImage and both Flatpak bundles land after - # the release is created: a checksum manifest that covers some of the files - # is worse than none, since the missing one looks like the tampered one. + # Runs last because the aarch64 AppImage, both Flatpak bundles, and the snap + # land after the release is created: a checksum manifest that covers some of + # the files is worse than none, since the missing one looks like the tampered one. publish-checksums: name: Checksums and Provenance - needs: [build-and-release, build-appimage-arm64, build-flatpak-amd64, build-flatpak-arm64, attach-flatpak] + needs: [build-and-release, build-appimage-arm64, build-flatpak-amd64, build-flatpak-arm64, attach-flatpak, attach-snap] runs-on: ubuntu-latest permissions: contents: write @@ -380,7 +392,7 @@ jobs: ls -la # Bare names, so `sha256sum -c` works in whatever directory the user # downloaded the release into. - sha256sum -- *.whl *.tar.gz *.AppImage *.flatpak | tee SHA256SUMS + sha256sum -- *.whl *.tar.gz *.AppImage *.flatpak *.snap | tee SHA256SUMS # Attesting from the manifest rather than a second glob: the file we # publish and the set we attest cannot drift apart. @@ -477,12 +489,13 @@ jobs: ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} commit_message: 'Release ${{ github.ref_name }}' - # Store-only: do not attach the .snap to the GitHub Release. grade: stable - # allows candidate; do not auto-release to stable — promote after QA. + # GitHub Release first, Store second. uinput is super-privileged, so the + # Store can queue a human review and refuse edge/candidate; the .snap still + # has to land on the tag for sideload. grade: stable allows candidate; do + # not auto-release to stable — promote after QA. # Secret: SNAPCRAFT_STORE_CREDENTIALS (snapcraft export-login). - publish-snap: - name: Build and Publish Snap - needs: build-and-release + build-snap: + name: Build Snap runs-on: ubuntu-latest permissions: contents: read @@ -490,6 +503,47 @@ jobs: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Build snap + id: build + uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1 + + - name: Upload the snap + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: snap-amd64 + path: ${{ steps.build.outputs.snap }} + if-no-files-found: error + retention-days: 7 + + attach-snap: + name: Attach Snap + needs: [build-and-release, build-snap] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download the snap + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: snap-amd64 + path: dist + + - name: Attach to release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + ls -la dist/*.snap + gh release upload "$TAG" dist/*.snap --clobber + + publish-snap: + name: Publish Snap to Store + needs: build-snap + runs-on: ubuntu-latest + permissions: + contents: read + steps: - name: Require Snap Store credentials env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} @@ -499,17 +553,45 @@ jobs: exit 1 fi - - name: Build snap - id: build - uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1 + - name: Download the snap + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: snap-amd64 + path: dist + + - name: Locate the snap + id: snapfile + run: | + set -euo pipefail + file=$(ls dist/*.snap) + echo "file=$file" >> "$GITHUB_OUTPUT" + ls -la "$file" + + - name: Install snapcraft + run: sudo snap install snapcraft --classic - name: Publish snap to edge and candidate - uses: snapcore/action-publish@214b86e5ca036ead1668c79afb81e550e6c54d40 # v1 env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} - with: - snap: ${{ steps.build.outputs.snap }} - release: edge,candidate + SNAP_FILE: ${{ steps.snapfile.outputs.file }} + run: | + set -euo pipefail + log=/tmp/snap-upload.log + set +e + /snap/bin/snapcraft upload "$SNAP_FILE" --release edge,candidate >"$log" 2>&1 + status=$? + set -e + cat "$log" + if [ "$status" -eq 0 ]; then + exit 0 + fi + if grep -F -q "will need manual review" "$log" \ + && grep -F -q "allow-installation" "$log"; then + echo "::warning::Snap Store queued a human review (allow-installation). The .snap is on the GitHub Release." + exit 0 + fi + echo "::error::Snap Store publish failed (exit $status) for a reason other than allow-installation review." + exit "$status" deploy-website: name: Deploy Website diff --git a/.github/workflows/snap-backfill.yml b/.github/workflows/snap-backfill.yml new file mode 100644 index 0000000..bd3b2cd --- /dev/null +++ b/.github/workflows/snap-backfill.yml @@ -0,0 +1,101 @@ +name: Attach snap to an existing release + +# v0.17.0 built the snap and then lost it: Store review failed the job, and +# the workflow did not attach the .snap to the GitHub Release. Run this after +# that policy change for a tag that already exists: +# gh workflow run snap-backfill.yml -f tag=v0.17.0 +# +# Attests only the snap. Other release assets keep the original provenance. +# Caller-supplied tag values stay in the environment, never in run: source. + +on: + workflow_dispatch: + inputs: + tag: + description: "Release tag to attach the snap to (e.g. v0.17.0)" + required: true + type: string + +permissions: + contents: read + +jobs: + backfill: + name: Build, attach, checksum, attest + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + attestations: write + steps: + - name: Require a version tag + env: + TAG: ${{ inputs.tag }} + run: | + set -euo pipefail + case "$TAG" in + v[0-9]*) ;; + *) + echo "refusing tag that is not v…: $TAG" >&2 + exit 1 + ;; + esac + if printf '%s' "$TAG" | grep -q '[^A-Za-z0-9._-]'; then + echo "refusing tag with unexpected characters: $TAG" >&2 + exit 1 + fi + + - name: Checkout the tagged commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.tag }} + + - name: Build snap + id: build + uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1 + + - name: Stage the snap and extend SHA256SUMS + id: stage + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ inputs.tag }} + SNAP_PATH: ${{ steps.build.outputs.snap }} + run: | + set -euo pipefail + mkdir -p dist + test -f "$SNAP_PATH" + name=$(basename "$SNAP_PATH") + cp -v "$SNAP_PATH" "dist/$name" + echo "name=$name" >> "$GITHUB_OUTPUT" + gh release download "$TAG" --pattern SHA256SUMS --output dist/SHA256SUMS + grep -v " ${name}$" dist/SHA256SUMS > dist/SHA256SUMS.next || true + (cd dist && sha256sum -- "$name") >> dist/SHA256SUMS.next + mv dist/SHA256SUMS.next dist/SHA256SUMS + cat dist/SHA256SUMS + + - name: Attest the snap + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4 + with: + subject-path: dist/${{ steps.stage.outputs.name }} + + - name: Attach snap and SHA256SUMS + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ inputs.tag }} + run: | + set -euo pipefail + gh release upload "$TAG" dist/*.snap dist/SHA256SUMS --clobber + + verify: + name: Verify the published release + needs: backfill + runs-on: ubuntu-latest + permissions: + contents: read + attestations: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ inputs.tag }} + run: python3 scripts/verify_release.py "$TAG" diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c38516..d1dfa83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Release history for Vocalinux. **[v0.17.0](https://github.com/VocaHQ/vocalinux/releases/tag/v0.17.0)** (2026-09-16) -Minor on the stable line: Faster Whisper and Parakeet local engines, Speech Model simple setup, first-run system language, Snap packaging with ydotool/`uinput`, Flatpak bundles attached by the release workflow, and injection/settings/audio fixes (HDA analog mics at native layout, native GTK OS dark/light, English-only models no longer trap other languages). +Minor on the stable line: Faster Whisper and Parakeet local engines, Speech Model simple setup, first-run system language, Snap packaging with ydotool/`uinput` (GitHub `.snap` for sideload while Store review is pending), Flatpak bundles attached by the release workflow, and injection/settings/audio fixes (HDA analog mics at native layout, native GTK OS dark/light, English-only models no longer trap other languages). See [docs/UPDATE.md](docs/UPDATE.md#whats-new-in-v0170) for the highlight table, or the [GitHub Release](https://github.com/VocaHQ/vocalinux/releases/tag/v0.17.0). diff --git a/README.md b/README.md index 48478df..63ee00c 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ Ships whisper.cpp with Vulkan. It is **not on Flathub** (submission [flathub#936 ### Snap (Ubuntu Snap Store) -Listing: [snapcraft.io/vocalinux](https://snapcraft.io/vocalinux). `stable` is still a manual promote after QA. +Listing: [snapcraft.io/vocalinux](https://snapcraft.io/vocalinux). `stable` is still a manual promote after QA. Tagged releases also attach `vocalinux__amd64.snap` on GitHub. ```bash sudo snap install vocalinux --edge @@ -186,6 +186,13 @@ sudo snap connect vocalinux:uinput # native Wayland typing (ydotool) v0.17.0 includes ydotool and the `uinput` plug. The Store still has to finish review of that plug, so `snap info vocalinux` may still show **v0.16.2** (rev 7) on edge. That revision has no `uinput` plug; `snap connect vocalinux:uinput` fails until a 0.17.0+ revision is listed. +Until then, sideload the GitHub file: + +```bash +sudo snap install --dangerous ./vocalinux_0.17.0_amd64.snap +sudo snap connect vocalinux:uinput +``` + ### From source ```bash diff --git a/docs/INSTALL.md b/docs/INSTALL.md index f718b03..d546338 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -8,7 +8,7 @@ How to install Vocalinux on Linux. Short overview: [project README](../README.md | [AppImage](#appimage) | Portable binary; no system package install | | [AUR](#arch-linux-aur) | Arch / Manjaro | | [Flatpak](#flatpak) | Release `.flatpak` or local build | -| [Snap](#snap-ubuntu-snap-store) | Ubuntu Snap Store (`--edge`) | +| [Snap](#snap-ubuntu-snap-store) | Ubuntu Snap Store (`--edge`) or GitHub `.snap` | | [From source](#from-source) | Contributors or custom trees | | [Manual / PyPI](INSTALL_MANUAL.md) | Full control or pip-only workflows | | [Troubleshooting](TROUBLESHOOTING.md) | Tray, audio, injection, models | @@ -112,7 +112,9 @@ Whisper.cpp + Vulkan. It is **not on Flathub** (submission [flathub#9368](https: ## Snap (Ubuntu Snap Store) -Listing: [snapcraft.io/vocalinux](https://snapcraft.io/vocalinux) (issue [#48](https://github.com/VocaHQ/vocalinux/issues/48)). Recipe: `snap/snapcraft.yaml`. Tagged `v*` releases publish to Snap Store `edge` and `candidate` when credentials are set. `stable` is still a manual promote after QA. +Listing: [snapcraft.io/vocalinux](https://snapcraft.io/vocalinux) (issue [#48](https://github.com/VocaHQ/vocalinux/issues/48)). Recipe: `snap/snapcraft.yaml`. Tagged `v*` releases attach `vocalinux__amd64.snap` to the GitHub Release and try Snap Store `edge` and `candidate` when credentials are set. `stable` is still a manual promote after QA. + +Store install, once Canonical lists the revision: ```bash sudo snap install vocalinux --edge @@ -121,7 +123,18 @@ sudo snap connect vocalinux:raw-input # global keyboard shortcuts (evdev) sudo snap connect vocalinux:uinput # native Wayland typing (ydotool) ``` -**v0.17.0** ships ydotool and the `uinput` plug. After `sudo snap install vocalinux --edge`, connect `uinput` and restart Vocalinux for native GNOME/GTK/Qt windows. **v0.16.2 edge (rev 7)** has no such plug and only types into XWayland apps; `sudo snap refresh vocalinux` first. +**v0.17.0** ships ydotool and the `uinput` plug. That plug is super-privileged, so the Store held 0.17.0 for human review (`allow-installation`). Until a 0.17.0+ revision is listed, `snap info vocalinux` still shows **v0.16.2 (rev 7)** on edge. That revision has no `uinput` plug; `sudo snap connect vocalinux:uinput` fails. + +Sideload the GitHub `.snap` (amd64) while the Store is waiting: + +```bash +sudo snap install --dangerous ./vocalinux_0.17.0_amd64.snap +sudo snap connect vocalinux:audio-record +sudo snap connect vocalinux:raw-input +sudo snap connect vocalinux:uinput +``` + +`--dangerous` is required because this file is not a Store revision. It will not refresh from the Store. After Canonical grants `uinput`, switch to `sudo snap install vocalinux --edge` (or `snap refresh`). ## From source diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index 6aba2fa..8b32a2d 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -88,7 +88,9 @@ Install `just` from https://just.systems or the distro package `just`. `verify-release.yml` runs the same check when the release workflow finishes. It cannot see a hand edit made afterwards, which is how every v0.16.2 defect -arrived, so run it yourself after touching a published release. +arrived, so run it yourself after touching a published release. The one +exception is `snap-backfill.yml`: that workflow attaches the snap, rewrites +`SHA256SUMS`, and then runs `scripts/verify_release.py` itself. ## Detailed Release Steps @@ -166,7 +168,8 @@ Use these rules for every GitHub Release body (and for the draft pasted into the 5. `## Bug Fixes` - group by area (IBus, Installer, AUR, Text injection, …) 6. Optional: `## Improvements`, `## Docs`, `## Packaging` 7. `## Thanks` - external PR authors and issue reporters by `@handle` -8. `## Install / Upgrade` - `install.sh`, AUR, PyPI, **AppImage**, Flatpak status (honest) +8. `## Install / Upgrade` - `install.sh`, AUR, PyPI, **AppImage**, Flatpak, Snap + (Store channels plus GitHub `.snap` sideload when Store review is pending) 9. `### Verifying what you downloaded` (required, and easy to lose). `release.yml` generates it, with `sha256sum -c --ignore-missing SHA256SUMS` and `gh attestation verify`. A hand-written body replaces the generated one, so carry @@ -380,21 +383,28 @@ After pushing the tag, the GitHub Actions workflow will automatically: rebuilding, so the wheel on PyPI is byte-for-byte the wheel on the release 2. Build and attach AppImages for x86_64 and aarch64, both from that same wheel 3. Create a GitHub Release with auto-generated notes -4. Attach `SHA256SUMS` covering all four artifacts, and generate build provenance - attestations from that manifest (runs after the aarch64 AppImage lands, so a - partial manifest never gets published) +4. Attach `SHA256SUMS` covering every GitHub asset (wheel, sdist, both AppImages, + both Flatpaks, the amd64 snap) and generate build provenance from that + manifest (runs after the aarch64 AppImage, both Flatpaks, and the snap land, + so a partial manifest never gets published) 5. Publish to PyPI via trusted publishing 6. Publish the AUR package (when the `AUR_SSH_PRIVATE_KEY` secret is configured) 7. Deploy the website to vocalinux.com -8. Mark as pre-release if version contains alpha/beta/rc +8. Build the amd64 snap, attach it to the GitHub Release, and try Snap Store + `edge`/`candidate`. Store human review (for example `uinput` allow-installation) + must not block the GitHub `.snap`. `stable` is still a manual promote +9. Mark as pre-release if version contains alpha/beta/rc Monitor at: https://github.com/VocaHQ/vocalinux/actions ### Step 9: Post-Release Tasks - [ ] Verify GitHub Release was created correctly -- [ ] Verify `SHA256SUMS` is attached and lists all four artifacts (wheel, sdist, - both AppImages) - the release notes tell users to run `sha256sum -c` against it +- [ ] Verify `SHA256SUMS` is attached and lists every GitHub asset (wheel, sdist, + both AppImages, both Flatpaks, the amd64 snap) - the release notes tell + users to run `sha256sum -c` against it +- [ ] If the Store held the snap for `uinput` review, confirm the GitHub `.snap` + is still attached and the notes document `snap install --dangerous` - [ ] Verify provenance: `gh attestation verify --repo VocaHQ/vocalinux` - [ ] Verify PyPI package was published (if applicable), and that its wheel sha256 matches the line for that wheel in `SHA256SUMS` diff --git a/docs/UPDATE.md b/docs/UPDATE.md index 4756b12..7a228d0 100644 --- a/docs/UPDATE.md +++ b/docs/UPDATE.md @@ -38,7 +38,7 @@ git pull origin main |--------|---------| | AUR | `yay -S vocalinux` (or your AUR helper) | | AppImage | Download the new file from [Releases](https://github.com/VocaHQ/vocalinux/releases) | -| Snap | `sudo snap refresh vocalinux` (`--edge` until stable is promoted). **v0.17.0** ships `uinput`; `sudo snap connect vocalinux:uinput` then restart for native Wayland apps. v0.16.2 rev 7 has no such plug. | +| Snap | Store: `sudo snap refresh vocalinux` (`--edge` until stable is promoted). Until Canonical lists 0.17.0, sideload `vocalinux_0.17.0_amd64.snap` from the GitHub Release (`sudo snap install --dangerous ./vocalinux_0.17.0_amd64.snap`), then `sudo snap connect vocalinux:uinput`. v0.16.2 rev 7 has no such plug. | | Flatpak (release bundle) | Install the new `.flatpak` from Releases; bundles do not auto-update | | PyPI | Reinstall in the same venv after system packages are current | @@ -88,7 +88,7 @@ Missing system packages: see [INSTALL.md](INSTALL.md) or [DISTRO_COMPATIBILITY.m | **Searchable open picker** | Language list filters while open (#798) | | **Localized punctuation commands** | it/fr/de/es/pt/nl/pl/ru; English phrases still work (#642) | | **Bare F-keys** | F1–F24 are valid push-to-talk shortcuts (#815) | -| **Snap** | Store listing, ydotool + `uinput` for native Wayland (#519, #823, #822) | +| **Snap** | Store listing, ydotool + `uinput` for native Wayland; GitHub `.snap` for sideload while Store review is pending (#519, #823, #822) | | **Flatpak on the tag** | Workflow attaches `.flatpak` assets and checksums them (#786) | ### Also in v0.17.0 diff --git a/tests/test_release_integrity.py b/tests/test_release_integrity.py index b3ac796..8351983 100644 --- a/tests/test_release_integrity.py +++ b/tests/test_release_integrity.py @@ -136,7 +136,7 @@ def test_the_manifest_covers_every_kind_of_artifact_we_publish(): assert "merge-multiple: true" in block, "it collects one artifact, not all of them" checksum_line = re.search(r"sha256sum -- (.+)$", block, re.M) assert checksum_line, "publish-checksums does not generate SHA256SUMS" - for pattern in ("*.whl", "*.tar.gz", "*.AppImage", "*.flatpak"): + for pattern in ("*.whl", "*.tar.gz", "*.AppImage", "*.flatpak", "*.snap"): assert pattern in checksum_line.group(1), f"{pattern} is published but unchecksummed" diff --git a/tests/test_snap_packaging.py b/tests/test_snap_packaging.py index a9f044b..911a767 100644 --- a/tests/test_snap_packaging.py +++ b/tests/test_snap_packaging.py @@ -80,17 +80,76 @@ def test_snap_strips_pygobject_and_uses_gnome_gi() -> None: assert "python3-gi-cairo" not in stage -def test_release_publishes_snap_to_edge_and_candidate() -> None: - """v* tags must ship the snap to the store, not attach it to GitHub.""" +def test_release_attaches_snap_then_tries_the_store() -> None: + """Store review of uinput must not swallow the GitHub .snap (v0.17.0).""" text = (REPO_ROOT / ".github" / "workflows" / "release.yml").read_text(encoding="utf-8") + assert " build-snap:\n" in text + assert " attach-snap:\n" in text assert " publish-snap:\n" in text - job = text.split(" publish-snap:\n", 1)[1].split("\n deploy-website:", 1)[0] - assert "needs: build-and-release" in job - assert "snapcore/action-build@" in job - assert "snapcore/action-publish@" in job - assert "release: edge,candidate" in job - assert "release: stable" not in job - assert "SNAPCRAFT_STORE_CREDENTIALS is unset; cannot publish the snap" in job - assert "gh release upload" not in job - assert "action-gh-release" not in job - assert "upload-artifact" not in job + + build = text.split(" build-snap:\n", 1)[1].split("\n attach-snap:", 1)[0] + assert "snapcore/action-build@" in build + assert "upload-artifact" in build + assert "name: snap-amd64" in build + assert "gh release upload" not in build + assert "action-publish" not in build + + attach = text.split(" attach-snap:\n", 1)[1].split("\n publish-snap:", 1)[0] + assert "needs: [build-and-release, build-snap]" in attach + assert "gh release upload" in attach + assert "dist/*.snap" in attach + assert "--clobber" in attach + assert "contents: write" in attach + assert "TAG: ${{ github.ref_name }}" in attach + assert 'gh release upload "$TAG"' in attach + assert '"${{ github.ref_name }}"' not in attach + + store = text.split(" publish-snap:\n", 1)[1].split("\n deploy-website:", 1)[0] + assert "needs: build-snap" in store + assert "--release edge,candidate" in store + assert "release: stable" not in store + assert "SNAPCRAFT_STORE_CREDENTIALS is unset; cannot publish the snap" in store + assert "will need manual review" in store + assert "allow-installation" in store + assert "continue-on-error" not in store + assert "snapcore/action-publish" not in store + assert "SNAP_FILE:" in store + assert 'snapcraft upload "$SNAP_FILE"' in store + assert "gh release upload" not in store + assert "action-gh-release" not in store + + +def test_release_notes_document_snap_sideload() -> None: + body = (REPO_ROOT / ".github" / "workflows" / "release.yml").read_text(encoding="utf-8") + assert "snap install --dangerous" in body + assert "snap connect vocalinux:uinput" in body + assert "vocalinux_${{ steps.get_version.outputs.VERSION }}_amd64.snap" in body + + +def test_snap_backfill_workflow_attests_only_the_snap() -> None: + """Existing tags (v0.17.0) need a dispatch that does not re-attest AppImages.""" + path = REPO_ROOT / ".github" / "workflows" / "snap-backfill.yml" + text = path.read_text(encoding="utf-8") + assert "workflow_dispatch:" in text + assert "snapcore/action-build@" in text + assert "gh release upload" in text + assert "SHA256SUMS" in text + assert "actions/attest-build-provenance@" in text + assert "subject-path: dist/${{ steps.stage.outputs.name }}" in text + assert "subject-checksums:" not in text + assert " verify:\n" in text + assert "scripts/verify_release.py" in text + assert 'python3 scripts/verify_release.py "$TAG"' in text + + +def test_snap_backfill_dispatch_input_never_reaches_the_shell() -> None: + """Same rule as verify-release.yml: interpolating the tag into run: is injection.""" + import re + + text = (REPO_ROOT / ".github" / "workflows" / "snap-backfill.yml").read_text(encoding="utf-8") + run_lines = [line for line in text.splitlines() if re.match(r"^\s*-?\s*run:", line)] + assert run_lines, "found no run: step, so this guard is scanning nothing" + for line in run_lines: + assert "${{" not in line, f"a run: step interpolates a template expression: {line.strip()}" + assert "TAG: ${{ inputs.tag }}" in text + assert 'gh release upload "$TAG"' in text diff --git a/web/src/app/changelog/page.tsx b/web/src/app/changelog/page.tsx index 0df5698..78abd39 100644 --- a/web/src/app/changelog/page.tsx +++ b/web/src/app/changelog/page.tsx @@ -27,7 +27,7 @@ const releases = [ "Recommended model button sets size and specialization together and will not ignore a file already on disk (PR #797, #778)", "Localized punctuation voice commands for it/fr/de/es/pt/nl/pl/ru (PR #642, #640)", "Bare F1–F24 push-to-talk shortcuts (PR #815)", - "Snap packaging with ydotool and uinput for native Wayland; Store --edge (PR #519, #823, #822)", + "Snap packaging with ydotool and uinput for native Wayland; GitHub .snap sideload while Store review of uinput is pending (PR #519, #823, #822)", "Release workflow attaches Vocalinux--{x86_64,aarch64}.flatpak (PR #786, #784)", "XWayland clipboard paste instead of layout-garbled xdotool type (PR #680, #657)", "Layout-aware Ctrl+V paste; ydotool releases Ctrl after a timed-out paste (PR #788, #675, #658)",