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
This commit is contained in:
Jatin K Malik
2026-09-16 19:40:13 -07:00
committed by GitHub
parent 47dd983def
commit 1f121546fb
10 changed files with 321 additions and 49 deletions
+102 -20
View File
@@ -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
+101
View File
@@ -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<digit>…: $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"
+1 -1
View File
@@ -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).
+8 -1
View File
@@ -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_<version>_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
+16 -3
View File
@@ -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_<version>_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
+18 -8
View File
@@ -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 <artifact> --repo VocaHQ/vocalinux`
- [ ] Verify PyPI package was published (if applicable), and that its wheel sha256
matches the line for that wheel in `SHA256SUMS`
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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"
+71 -12
View File
@@ -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
+1 -1
View File
@@ -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-<version>-{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)",