Skip to content

feat(packaging): AUR, Flathub, Scoop and Microsoft Store channels - #151

Merged
kipavy merged 20 commits into
devfrom
chore/packaging-reach
Aug 19, 2026
Merged

feat(packaging): AUR, Flathub, Scoop and Microsoft Store channels#151
kipavy merged 20 commits into
devfrom
chore/packaging-reach

Conversation

@kipavy

@kipavy kipavy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

The release already automated Homebrew (own tap), winget and the signed apt/yum repos. What was missing were the channels users actually browse — and the Microsoft Store, which is now free to publish to and needs no code-signing certificate on the MSIX path.

Channels added

Channel Kept current by
AUR (voltius-bin) publish-installers.yml, every release
Microsoft Store publish-msix.yml, every release
Scoop Scoop's excavator bot, after the first merge
Flathub Flathub's external-data-checker, after the first merge

Scoop and Flathub carry checkver / x-checker-data, so each ecosystem's own bot follows releases once the manifest is merged — no job here is needed for them.

Bugs found while packaging

  • The .deb linked libsecret-1.so.0 without declaring it. On a minimal Debian the app fails to start with a missing-so error. Declared for deb and rpm.
  • The generated .desktop had an empty Categories=, leaving the app uncategorised in every application menu. Set bundle.category.
  • An MSIX install lives under C:\Program Files\WindowsApps, which is not writable, so the in-app updater would have downloaded an update and then failed at the install step. classify_install now reports that path as externally updated, with a unit test. Flatpak needed no change: no APPIMAGE env already means External.

Verification

  • cargo test --lib updater_tests — 8 passed, including the new MSIX case.
  • makepkg in archlinux:base-devel — the PKGBUILD builds; the package contains usr/bin/voltius, the desktop file and all three icons. The generated sha256 matches the released .deb.
  • flatpak-builder-lint manifest — clean apart from three errors that need Flathub exceptions (below). Runtime bumped 48 → 50 after the linter flagged 48 EOL.
  • appstreamcli validate and desktop-file-validate — both pass.
  • Scoop manifest validates against ScoopInstaller/Scoop schema.json.

Known gaps

  • Scoop's install/uninstall scripts drive the NSIS installer with /S and assume %LOCALAPPDATA%\Voltius. They have never run on a real Windows machine. Test before opening the Extras PR.
  • The MSIX build itself needs Windows + the Windows SDK, so it is unverified here.
  • Whether bundle.category actually populates the Linux .desktop Categories= needs a real tauri build. Worst case it stays empty, as today.

Flathub exceptions

Three linter errors are expected and each needs a justification in the submission PR: --filesystem=home (SFTP needs the local side of a transfer), --socket=ssh-auth (agent forwarding), --talk-name=org.freedesktop.Flatpak (the local-terminal feature opens the user's real shell; without host spawn it silently becomes a shell inside the sandbox). Written out in packaging/README.md.

The Flatpak app id is app.voltius.Voltius, matching the domain Flathub verifies against, rather than the Tauri identifier com.voltius.app.

Manual setup still required

Documented per channel in packaging/README.md: an AUR account + AUR_SSH_PRIVATE_KEY, the Scoop PR, the Flathub PR, and — for the Store — reserving the name in Partner Center and completing one submission by hand, because the Store CLI can only update a listing that is already live.

kipavy added 20 commits August 19, 2026 01:27
The release already automated Homebrew (own tap), winget and signed
apt/yum repos. What was missing were the channels users actually browse.

- AUR: `voltius-bin` repackages the released .deb, pushed by a new job in
  publish-installers.yml. Skips with a warning when AUR_SSH_PRIVATE_KEY is
  unset so a release can never fail on it.
- Flathub: manifest, AppStream metainfo and desktop file under
  packaging/flatpak. App id is app.voltius.Voltius, matching the domain
  Flathub verifies against, not the Tauri identifier.
- Scoop and Flathub both carry checkver/x-checker-data, so their own bots
  follow releases after the first merge and need no job here.
- Microsoft Store: AppxManifest template, build-msix.ps1 and a manual
  build-msix workflow. MSIX needs no code-signing certificate because the
  Store re-signs it; the .exe path would have required one.

Three bugs found while packaging:

- The .deb linked libsecret-1.so.0 without declaring it, so the app failed
  to start on a minimal Debian. Declared for deb and rpm.
- The generated .desktop had an empty Categories, leaving the app
  uncategorised in every application menu. Set bundle.category.
- An MSIX install lives under C:\Program Files\WindowsApps, which is not
  writable, so the updater would have downloaded and then failed at the
  install step. classify_install now reports that path as externally
  updated. Flatpak needed no change: no APPIMAGE env already means
  External.

The four manifest generators shared one step - read the sha256 of a named
release asset - so that moved to scripts/lib/release-assets.sh along with
the asset naming. Tag resolution in publish-installers.yml was duplicated
per job and is now a single job the others consume.

README: dropped the stale star-count banner and the "coming soon" on a
docs site that is live, added release/stars badges, and finished the
comparison table's presentation - "(WIP)" gone, a legend added, and the
blank and "NOT TESTED" cells normalised to "?".
The Store was the one channel left needing hands on every release. It does
not: microsoft/microsoft-store-apppublisher sets up the Store Developer
CLI, and `msstore publish` submits a package against a listing.

build-msix.yml becomes publish-msix.yml, called from tag-release.yml
alongside the other channels. It builds both architectures, bundles them
with `makeappx bundle` (a listing serves one artifact per architecture
from a single bundle, and `msstore publish` takes exactly one path), and
submits. The submission step warns and skips unless all four Entra
secrets and MSSTORE_PRODUCT_ID are set, so it can never fail a release.

Two limits are inherent, not shortcuts, and are documented rather than
worked around:

- The CLI updates a listing, it cannot create one. The app must already be
  live in the Store, so the first submission stays manual. Running the
  workflow with `publish: false` produces the bundle for it.
- Microsoft supports updates through this action for free products only.
  Voltius is free in the Store — Pro is billed outside it — so this holds,
  but a paid Store product would break it.

Both build-msix.ps1 and the new bundle-msix.ps1 have to locate makeappx
from the Windows SDK, so that moved to scripts/lib/makeappx.ps1.
Both manifests make claims that cannot be checked by reading them: that
the Scoop install actually drives the NSIS installer and finds the binary
afterwards, and that the cask passes `brew audit --new`, the gate a
homebrew-cask core submission has to clear. Each needs an OS this project
is not built on, and both are cheap on a hosted runner.

verify-packaging.yml installs from the generated Scoop manifest on
windows-latest and asserts the exe lands and uninstalls, runs the new-cask
audit on macos-latest, and packs the MSIX (whose submission step is inert
without Store credentials). It runs on changes to the packaging inputs so
a manifest cannot rot silently between releases.

The Scoop uninstaller now reads UninstallString from the uninstall
registry key rather than assuming %LOCALAPPDATA%\Voltius\uninstall.exe,
falling back to that path only when no entry matches.
`brew style` only applies the cask cops when the file sits under a Casks/
directory. The new-cask check styled a bare voltius.rb at the repo root,
so rubocop fell back to generic Ruby cops and failed on Sorbet sigils and
a missing frozen_string_literal comment — nothing to do with the cask.

The release job already had the layout right, so rather than copy its
generate/style/tap/audit block a second time with one flag changed, that
block moves to a composite action both jobs call: the release audits the
tap variant, the packaging check audits the core variant with --new.

Also ignores the flatpak-builder output from a local test build, which a
container writes into packaging/flatpak as root.
`brew audit --cask --new` — the gate a core submission has to pass — runs
a signature scan, and it fails:

  Signature verification failed: Scan completed, but failed because the
  software is not signed by a distributor that meets the system Gatekeeper
  requirements.

Voltius is ad-hoc signed, not notarized, so no amount of manifest work
gets past this. `brew style` and the rest of the audit are clean, so
notarization is the only thing in the way.

Drops the core audit job rather than leaving a check that can only ever be
red, and records the exact error so the next person does not rediscover it.
The --core generator flag stays, ready for the day there is an Apple
Developer account. The tap is unaffected and keeps being audited on
release.
publish-msix used one global concurrency group, so a packaging check and a
release submission were members of the same queue. With
cancel-in-progress false, GitHub keeps only the newest *pending* entry and
cancels the rest — three pushes in a row left the MSIX builds cancelled
before they started.

Keying the group on the ref separates them: a release runs against a tag, a
check runs against a branch, and two runs of the same ref still serialize.

verify-packaging also gets its own group with cancel-in-progress, so a new
push supersedes the check rather than racing it. Its MSIX job is a full
Windows Rust build on two targets, which is not something to leave running
for a commit that has already been replaced.
The repository root is the Cargo workspace root, so builds land in
./target, not ./src-tauri/target. The pack step looked in the latter and
threw after a thirty-minute Windows build had already succeeded.

The error now lists the release binaries it did find. Getting a target
triple or the target directory wrong is the likely cause, and rediscovering
that costs another full build each time.
Rendered from src-tauri/icons/icon.png. The Store falls back to the icon
inside the package when this is absent, but an uploaded tile takes
priority and survives changes to the package icon set.

Also records the listing asset rules, including that desktop screenshots
are '1366x768 or larger' rather than one of two exact sizes — the existing
1600x1148 captures qualify unchanged.
packaging/msix/store-listing now holds every image the Partner Center
listing form asks for: six screenshots numbered in upload order, each with
the caption to go with it, plus the 300x300 app tile.

Two of the screenshots were picked over the obvious candidates for a
reason. The hosts view shows a single host, which sells "manage your
fleet" badly, so the folders-and-tags view takes its place. And the
dual-pane SFTP view leads, being the densest and cleanest of the set.

Recorded in the directory's README: the split panes capture carries
"tmux/screen not found - session will not survive disconnects" in three
panes, because the host it was taken against had neither installed. Small,
honest, and still a warning about a missing feature sitting in a shop
window — worth recapturing, at which point it demonstrates persistent
sessions instead of contradicting them.
Partner Center's Store logos section asks for poster art at exactly
720x1080 or 1440x2160 — no "or larger" tolerance, unlike screenshots.
Microsoft's own documentation says this field "does not apply to apps" and
is for games; the form asks regardless, so the form wins and both sizes
are provided.

The mark sits in the top two-thirds over a glow on the icon's own #010318,
leaving the bottom third empty for the text the Store draws there. No
wordmark: a font rendered in a container would not match the brand, and
the bolt carries the identity on its own.

scripts/make-store-poster.sh regenerates the poster and the app tile,
running ImageMagick in a container so neither has to be installed on the
host and neither ends up a hand-edited binary nobody can reproduce. The
glow's outer stop is black on purpose, and the script says why: it is
composited with 'screen', where black is a no-op, so the glow fades out
invisibly instead of leaving the hard horizontal seam that an outer stop
of #010318 produced.
makeappx refuses the package outright:

  error 80080204: App manifest validation error: The DefaultTile element
  must specify the Wide310x150Logo attribute if the Square310x310Logo
  attribute is specified.

`tauri icon` emits only square logos, so the wide tile is generated
alongside the Store listing art. It is transparent behind the mark, like
the square logos, because the manifest sets BackgroundColor="transparent"
and Windows paints the accent colour behind the tile.

Dropping Square310x310Logo would also have satisfied makeappx, at the cost
of the large Start tile. Producing the missing asset keeps both.

make-store-poster.sh becomes make-store-assets.sh: it now emits a package
asset as well as listing art, and the old name no longer describes it.
The MSIX version comes from tauri.conf.json, which only carries the
released version at a release tag: on dev it lags, because the bump lands
on main. The packaging check builds a branch, so its bundle came out
labelled 0.25.0 while the latest release was 0.27.0 — fine for proving the
package builds, and wrong to submit.

That matters more than a mislabelled test artifact. Store versions only
move forward, so a first submission at the wrong version cannot be taken
back. The publish job now compares the packaged version against the latest
release and says so, and the runbook says to build the first submission
from a tag.
The publish guard read `github.event_name != 'workflow_dispatch'`, but inside a
reusable workflow that is the CALLER's event. The packaging check calls
publish-msix on a push, so the guard evaluated to "publish" and `workflow_call`
had no input to say otherwise: once the Store credentials exist, any push
touching packaging/** would have submitted a branch build to Partner Center.

Callers now opt in through a `publish` input that defaults to false, and only
tag-release passes true. The version-staleness step gates the guard as well
instead of printing a warning the following steps ignored — a submission below
the latest release cannot be withdrawn, so refusing is the only safe answer.
`github.ref` is the caller's ref inside a reusable workflow, and tag-release
runs on a push to main rather than on the tag, so a release and a packaging
check both landed in publish-msix-refs/heads/main and queued behind each other
— the contention the group was split to avoid. `inputs.ref` is the ref the run
actually builds, which does tell the two apart.
`workspaces: 'src-tauri -> target'` points at src-tauri/target, which does not
exist: the [workspace] table lives in the repository root Cargo.toml, so cargo
writes to ./target. Nothing was ever cached, and every Windows MSIX build paid
for a cold compile from scratch. ci.yml already had this right.
makeappx falls back to the current date-time when `bundle` is called without
/bv, so the bundle built by the packaging check carried
Identity Version="2026.819.1209.0" while the packages inside it were 0.25.0.0.
Store versions only ever move forward: submitting one bundle at a date-derived
version would have locked the listing out of every semantic version for good.

The version now comes from a shared Get-MsixVersion, so the package and the
bundle cannot drift apart.
…king

Only the exact file about to be written was removed. bundle-msix.ps1 globs
*.msix, so in the local build-x64 / build-arm64 / bundle flow a package left
over from before a version bump was staged alongside the new one and makeappx
saw two packages for the same architecture.
The tag fell back to a hardcoded v0.27.0, so the job would have kept installing
an old release forever — and an asset renamed by a later release, which is the
one regression this job exists to catch, would have sailed past it. Resolve the
latest tag instead.
The listing screenshots were deleted up front and each source was only checked
as it was copied, so a renamed capture in the docs repo left the directory
holding part of the old set and part of the new, recoverable only from git.
flatpak-external-data-checker rewrites the source url and sha256 and nothing
else, so the <releases> block the store page reads its version from stays where
the submission left it. Both the manifest generator and the runbook claimed
nothing had to run again after the first merge.
@kipavy
kipavy merged commit 0853278 into dev Aug 19, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant