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"