Skip to content

chore(build): add signing key to AppImage and update snapcraft workflow - #529

Merged
alecdotdev merged 3 commits into
masterfrom
fix/build-appimage-signature
Aug 7, 2026
Merged

chore(build): add signing key to AppImage and update snapcraft workflow#529
alecdotdev merged 3 commits into
masterfrom
fix/build-appimage-signature

Conversation

@alecdotdev

Copy link
Copy Markdown
Collaborator

Changes

  1. Strip host-coupled libraries from AppImage fails during build due to no signing key. Added keys to build step explicitly

  2. Snapcraft build failed due to rustup dep missing.

Verification

All tests in scripts/releaseWorkflow.test.ts passed (8/8).

@PathGao

PathGao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Chased the AppImage half out of curiosity and landed on a different root cause than "no signing key" — the key is there, the subcommand just reads a different variable name.

In the failed run (31192564528, linux job) both secrets were injected fine:

TAURI_SIGNING_PRIVATE_KEY: ***
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ***
> tauri signer sign .../Markpad_2.7.2_amd64.AppImage
       Error Key generation aborted: Unable to find the private key

signer sign is still on the v1 names (tauri-cli 2.9.6, the version this lockfile resolves):

$ npx tauri signer sign --help
  -k, --private-key         [env: TAURI_PRIVATE_KEY=]
  -f, --private-key-path    [env: TAURI_PRIVATE_KEY_PATH=]
  -p, --password            [env: TAURI_PRIVATE_KEY_PASSWORD=]

TAURI_SIGNING_PRIVATE_KEY is what tauri build reads, which is why every release before this one signed fine — #499 is the first place in this workflow that calls signer sign directly, so it is the first to hit the mismatch.

This PR fixes it: pulling the values out of the env and passing them as flags does get the key to the CLI. One tradeoff worth naming — Actions masks secrets in log text but not in the runner's process list, and -k <key> puts the private key on the command line. Renaming the env vars is equivalent and keeps it out:

env:
  TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
  TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}

Secret names stay as they are — only the variable the step exports changes — and the comment above the call ("reads the key/password from the env above") stays true. Your call; the current form works either way.

Separately: I could not find the snapcraft failure in that run — it died at the AppImage step, so snapcraft pack never executed. If the missing rustup came from a Snap Store build log rather than Actions, that would be worth a line in the PR body for whoever reads this later.

@alecdotdev

alecdotdev commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

thanks for catching that, I only saw references to TAURI_SIGNING_PRIVATE_KEY etc. fixing that now.


Separately: I could not find the snapcraft failure in that run — it died at the AppImage step, so snapcraft pack never executed. If the missing rustup came from a Snap Store build log rather than Actions, that would be worth a line in the PR body for whoever reads this later.

the snapcraft error is found in the previous successful build v2.7.1:

Run # Build snap
Launching managed ubuntu 24.04 instance...
Creating new instance from remote
Creating new base instance from remote
Creating new instance from base instance
Starting instance
Initializing lifecycle
Installing build-packages
Installing build-snaps
Environment validation failed for part 'markpad': 'rustup' not found and part 'markpad' does not depend on a part named 'rust-deps' that would satisfy the dependency.
Failed to run snapcraft in instance
Full execution log: '/home/runner/.local/state/snapcraft/log/snapcraft-20260806-043952.793260.log'
Error: Process completed with exit code 1.

@alecdotdev
alecdotdev merged commit 934aa3b into master Aug 7, 2026
4 checks passed
@PathGao

PathGao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Correction to my previous comment — renaming the env vars would have been going backwards. Upstream already fixed this, and the real root cause is that our lockfile is pinned to an old CLI.

Root cause

signer sign read the v1 variable names until 2.10.0 (2026-02-02), which added the v2 ones:

2.10.0 — "Added new environment variables for tauri signer sign command … TAURI_SIGNING_PRIVATE_KEY, TAURI_SIGNING_PRIVATE_KEY_PATH, TAURI_SIGNING_PRIVATE_KEY_PASSWORD" with deprecation of older TAURI_PRIVATE_KEY variants.

package-lock.json resolves @tauri-apps/cli to 2.9.6 (2025-12-09), one release short of the fix, and npm ci installs exactly that. package.json already says ^2, which permits 2.11.4 — only the lockfile is holding it back.

Reproduced locally with a throwaway key, same v2 env vars in both runs:

$ TAURI_SIGNING_PRIVATE_KEY=… TAURI_SIGNING_PRIVATE_KEY_PASSWORD=… \
    npx @tauri-apps/cli@2.9.6 signer sign fake.AppImage
       Error Key generation aborted: Unable to find the private key     ← identical to the CI failure

$ … npx @tauri-apps/cli@2.11.4 signer sign fake.AppImage
  dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkK…
  → fake.AppImage.sig written

So npm update @tauri-apps/cli fixes this workflow without touching it: the env block stays as it is, the private key never reaches the command line, and the comment above the call stays accurate.

2.11.4 also carries an AppImage fix

Relevant to this PR specifically:

2.11.4 — "Fixed an issue in the AppImage bundler that caused the /.desktop and .DirIcon files to be absolute symlinks instead of relative symlinks which caused problems with AppImage installers"

Different problem from the bundled-libwayland one #499 fixed — this is about the bundler's own output — but it lands in the same artifact you are currently repackaging by hand, so it seems worth having.

Upgrade impact, as far as I checked

2.10.0 additive: v2 names added, TAURI_PRIVATE_KEY kept and deprecated
2.11.0 breaking change scoped to "tauri-bundler lib users" — CLI consumers unaffected
2.11.0 new config fields uninstallerIcon, bundle > windows > minimumWebview2Versiontauri.conf.json uses neither
npm test 840 pass
real npm run tauri build succeeds; artifacts land where the workflow looks for them: macos/Markpad.app.tar.gz, macos/Markpad.app.tar.gz.sig, dmg/Markpad_<ver>_<arch>.dmg

Limit of what I verified: I am on macOS, so only the macOS artifact line was exercised end to end. The Windows *-setup.exe and Linux *.AppImage naming is unchanged as far as the changelogs go, but I did not build them.

Entirely your call whether to fold that into this PR or keep it separate — happy to open the lockfile bump as its own PR if that is easier to review.

@PathGao

PathGao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

One more, and this one is the bigger picture rather than another suggestion — my first two comments were about the symptom (no key) and the direct fix (the CLI pin). This is about why this workflow ended up needing signer sign at all, because the answer is not something we chose.

The chain

#498  AppImage ships libwayland-client.so.0 from the runner → host libEGL aborts → blank window
  ↓
#499  the only available fix: unpack the AppImage, delete the libs, repack
  ↓
      the repacked bytes are not the bytes tauri build signed → the .sig is void
  ↓
      so we must re-sign → first call to `tauri signer sign` in this repo's history
  ↓
#529  and that command sat on the v1 env var names until 2.10.0

Signing is a commitment to bytes, not to a filename. The moment we touch the artifact after tauri build signed it, we owe a new signature — there is no way around that half.

Could we avoid touching the artifact?

Three ways I looked at, none of them currently open:

  1. Stop tauri from bundling the libs in the first place. AppImageConfig has exactly two fields — bundle_media_framework and files. Both add; neither removes. No exclusion knob exists.
  2. Have tauri apply the AppImage excludelist. That list exists precisely because those libraries must come from the host, which is AppImage 2.7.1 shows a blank window: bundled libwayland-client.so.0 breaks EGL on newer Mesa #498's diagnosis verbatim. Upstream change.
  3. Skip signing in build and sign everything manually at the end. Coherent, but it would rewrite the macOS and Windows paths — both of which work — to accommodate a Linux-only problem.

What would actually put us back on the main path

linuxdeploy — which tauri downloads and invokes — has supported exclusion all along:

// linuxdeploy/src/main.cpp
args::ValueFlagList<std::string> excludeLibraryPatterns(
    parser, "pattern",
    "Shared library to exclude from deployment (glob pattern)",
    {"exclude-library"});

tauri's invocation is hardcoded, with no config field and no env var that can append to it:

// tauri-bundler/src/bundle/linux/appimage/linuxdeploy.rs
cmd.args([
  "--appimage-extract-and-run",
  "--verbosity", log_level,
  "--appdir", &app_dir_path,
  "--plugin", "gtk",
]);

So the whole detour comes down to one flag that cannot be passed:

today      linuxdeploy packs → libs are in → sign → we unpack, strip, repack → sig void → re-sign
should be  linuxdeploy --exclude-library='libwayland-*' → libs never enter → sign → done

(For what it's worth, the libs arrive indirectly: the GTK plugin hands libgtk/libgdk/libpango to linuxdeploy via --library=, and linuxdeploy pulls in their dependencies recursively — libwayland-client rides in that way.)

Three options from here:

  • Upstream feature — an excludeLibraries field on bundle > linux > appimage, or tauri applying the excludelist by default. If that lands, this repo deletes the strip step, the repack, and the signer sign call outright, and all three platforms go back to one code path. @LargeModGames's analysis in fix(ci): strip host-coupled libwayland from the AppImage so it starts on newer Mesa #499 is already most of a high-quality upstream issue, and this hits every GTK-based Tauri app on Linux, not just Markpad — AppImage 2.7.1 shows a blank window: bundled libwayland-client.so.0 breaks EGL on newer Mesa #498 was just the first report.
  • Wrap linuxdeploy locallyprepare_tools skips the download when the binary already exists, so CI could drop a shim at that path that appends --exclude-library. It works in principle, but it trades one upstream weakness for a dependency on that same upstream's undocumented internals, and the next person to read the workflow would have no idea why a fake linuxdeploy is sitting there. I would not.
  • Status quo — smallest diff, but it is what puts this repo on the seldom-travelled path, and that path turned out to be unmaintained: 16 months between v2.0.0 and the CLI learning the v2 variable names, because in a normal release tauri build signs and signer sign never appears.

Not our call to make — you own the release process, and the upstream ask is yours to file or not. Flagging it because right now the cost of #498 is a permanently non-standard packaging path, and that cost is not obvious from the diff.

What I did not verify: I am on macOS. Everything above is read from the tauri/linuxdeploy sources and the CI logs — I have not run a Linux bundle to confirm --exclude-library actually keeps libwayland-client out in this specific setup.

@alecdotdev

Copy link
Copy Markdown
Collaborator Author

@PathGao defined new env vars, waiting on the build to pass for now.

if an upstream issue is the cause we could try and report it as such?

if wrapping the linuxdeploy is simpler/more reliable than the current work around we can give it a try, but I'd like to add the respective workflow to PRs to see if it passes that specific build before merging to master

@PathGao

PathGao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

One footnote on the deprecated-env-var point, because this repo has two live examples of the cost of pinning to something old.

Today, in this repo. #528dompurify was pinned to exactly 3.4.12 in both dependencies and overrides. GHSA-55q2-fjhq-7xh7 landed covering <= 3.4.12, npm audit is a required check, and every open PR went red at once, including ones touching nothing related. The pin did not hold anything still; it just decided when the forced move would happen and how disruptive it would be.

In the file this PR edits. snapcraft.yaml hardcodes node/20/stable, while all three workflows use node-version: lts/*. Those have already drifted apart, and the snap build is the one that will notice first. Since you are adding rustup/latest/stable right below it — latest for one toolchain, a hardcoded major for the other — that line might be worth a second look while you are in there.

Which is the whole of my concern about TAURI_PRIVATE_KEY: deprecated is a countdown, not a stable state. Upstream kept the old names in 2.10.0 to avoid breaking anyone, and that grace has an end. Using them now means this step is fine until the day the CLI pin moves for some unrelated reason — a security bump, a transitive resolve — and then it fails the same silent way, on a release, again.

Not urgent and not blocking; -k is the only thing here that stops the next release from working.

@PathGao

PathGao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Better precedent than the two I just cited, and it is one this repo already wrote down — from .github/dependabot.yml:

On GitHub Actions somebody else always sets it: runner images retire toolchains on their own timetable, which is how build.yml came to build releases on a node-version: '20' that had been pulled from the image toolcache. Nobody chose that date.

Same file, same release path, same shape as the env var question: a pin that was fine on the day it was written, kept working right up until someone else's timetable ran out, and surfaced on a release.

And the timetable here is already scheduled rather than hypothetical — npm updates run quarterly, grouped for minor and patch. @tauri-apps/cli 2.9.6 → 2.11.x is a minor, so the CLI pin moves on its own at the next quarterly run whether or not anyone revisits this step. That bump alone is harmless (2.10.0 added the new names and only deprecated the old ones, it did not remove them) — the date that matters is whichever release upstream finally drops them, and that one is not ours to pick either.

Genuinely the last word from me on this. -k is the only thing that blocks the next release; everything else is a note for whenever this step is next opened.

@PathGao

PathGao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

I said that was my last word on the signing step, and it was — this one is different. Re-reading my own comment, I quoted .github/dependabot.yml as if it were established repo context. It is not: I wrote that file and those notes in #484, a few days ago. You may never have had reason to read the comment block, so quoting it back at you was not much use. Here is the actual background, since it now has a direct bearing on how this release path ages.

What #484 set up

Three ecosystems, deliberately different cadences, because the cost of being late is not the same for each:

ecosystem cadence why
github-actions monthly someone else's deadline — runner images retire toolchains on their own schedule
npm quarterly (Jan/Apr/Jul/Oct 1) our deadline; nothing external forces a TypeScript bump
cargo quarterly same

Each group is limited to minor and patch. Majors fall outside every group and arrive one PR per dependency with its own changelog — that was the fix for #477/#478, where a monthly chore(deps) title turned out to be wrapping TypeScript 5 → 7 and Vite 6 → 8 in a shape nobody can review as a batch.

Security advisories bypass this schedule entirely, which is why #528 showed up on its own timetable rather than waiting for October.

Why it matters here

@tauri-apps/cli 2.9.6 → 2.11.x is a minor. It is inside the npm group, so the CLI pin moves on its own at the next quarterly run — nobody has to decide to do it. That bump by itself is harmless; 2.10.0 added the new env names and only deprecated the old ones. The date that matters is whichever upstream release finally removes them, and neither of us picks that one.

The pattern, recently

The reason I keep returning to this is that the last two weeks have four instances of the same shape, all found rather than chosen:

what expired how it surfaced
#476 actions v4 → v7 routine, caught by the monthly cadence
#485 updater endpoint still named alecdotdev/Markpad working only via GitHub's transfer redirect — a compatibility layer with documented conditions for being voided
#510 Node 25 added a global localStorage without the Web Storage methods 57 subtests failed at import time; the old typeof localStorage === 'undefined' guard silently stopped meaning anything
#528 dompurify pinned at exactly the version an advisory then covered every open PR red at once

And the one that is literally this file: build.yml was building releases on a node-version: '20' that had already been pulled from the runner image's toolcache. Nobody chose that date either.

None of this asks you to change anything in this PR — -k is still the only blocker. It is context for the release process as a whole, and it was unfair of me to cite it as though it were already yours.

@PathGao

PathGao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Answering your three questions properly.

Reporting upstream

Worth doing, but the framing decides whether it survives triage.

Not as a feature request. "Give us an excludeLibraries config" reads as new API surface plus new support burden — someone excludes too much, their app won't start, upstream gets the bug. In a project of tauri's size that kind of ask usually just sinks.

As a bug it has a real chance, because it needs no new API at all:

tauri's AppImage bundle ships libraries that are on the AppImage excludelist. libwayland-client.so.0 from the build host shadows the system copy and makes host libEGL abort with EGL_BAD_PARAMETER — blank window on any machine whose Mesa is newer than the runner's. Affects every GTK-based Tauri app on Linux.

That has what triage looks for: a community standard being violated (the excludelist exists precisely because those libraries must come from the host — not our preference), a concrete reproducible symptom, and a fix that is one --exclude-library argument inside tauri's existing linuxdeploy invocation. No schema change, nothing new to support.

#499 already contains most of that write-up. @LargeModGames did the diagnosis, so it is his call whether he wants to be the one to file it.

The wrapper

I'd skip it — it is not more reliable. It depends on prepare_tools skipping the download when the file already exists, which is undocumented internal behaviour. If that ever changes, the shim silently stops applying: no error, the libraries come back, and the blank-window bug quietly returns for anyone on newer Mesa. The current workaround at least fails loudly.

Testing the release path before merging

Strongly agree, and it is the actual lesson from this whole thread — build.yml only runs on tag push, so the strip + sign path has zero CI coverage and its first execution is always a real release. A workflow_dispatch Linux-only job going as far as strip + sign with a throwaway keypair (not the release secret), uploading nothing, would have caught the env-name mismatch before it ever reached a tag.

(I deleted an earlier version of this comment — it led with a problem in -k that you had already fixed in bb821f1 twenty minutes before I posted, and I didn't want to leave a stale alarm at the top. The answers above are unchanged.)

@alecdotdev

Copy link
Copy Markdown
Collaborator Author

@PathGao

can we include both TAURI_PRIVATE_KEY and TAURI_SIGNING_PRIVATE_KEY var definitions to accommodate for the deprecation of the former, at least while we are doing the unpack and resign workaround.

re: snapcraft build. adding the rustup/latest resulted in the same error during build. unfortunately I currently do not have access to a Linux machine so all I can test is through the GitHub Actions as well.

as of latest build, AppImage completed successfully. I'll cut 2.7.2 to address #498.

@PathGao

PathGao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Both env var names — yes, and it's the better answer

That works, and it is strictly better than picking one. The two sets are read by different CLI generations, so defining both means the step is correct no matter which version the lockfile resolves to:

tauri build reads signer sign reads
≤ 2.9.6 TAURI_SIGNING_PRIVATE_KEY TAURI_PRIVATE_KEY
≥ 2.10.0 TAURI_SIGNING_PRIVATE_KEY TAURI_SIGNING_PRIVATE_KEY (old names still accepted, deprecated)

Defining both covers every cell, including the day upstream finally removes the v1 names — at which point the v1 lines become dead weight and can be deleted, but nothing breaks in the meantime. Same secrets on the right-hand side, four lines instead of two.

For what it's worth I still think bumping the CLI is worth doing on its own — it drops the whole question, and 2.11.4 additionally fixes an AppImage bundler bug (.desktop/.DirIcon written as absolute rather than relative symlinks). @PathGao is planning to look at that upgrade separately, so it needn't ride along here.

The snapcraft error

I think rustup/latest/stable had no effect because the rust plugin already requests that exact snap. From craft_parts/plugins/rust_plugin.py:

def get_build_snaps(self) -> set[str]:
    if options.rust_channel == "none" or "rust-deps" in (options.after or []):
        return set()
    return {"rustup"}

So the snap was being installed before your change too. What fails is the check that runs after installation:

# Check if rustup is properly installed
self.validate_dependency(
    dependency="rustup",
    argument="dump-testament",
    plugin_name="rust",
    part_dependencies=part_dependencies,
)

It shells out to rustup dump-testament, and inside the LXD build instance that is not resolving — hence 'rustup' not found, with the rust-deps half of the message being the plugin's suggested escape hatch rather than a second missing thing.

The part doesn't need the rust plugin at all. override-build already does the entire build by hand:

plugin: rust
override-build: |
  npm ci
  npx tauri build --no-bundle
  install -D -m755 src-tauri/target/release/Markpad $CRAFT_PART_INSTALL/bin/markpad

None of the plugin's build logic runs — it is overridden. All that remains of plugin: rust is the environment check standing in the way. So:

-    plugin: rust
+    plugin: nil
     build-packages:
       - libwebkit2gtk-4.1-dev
+      - rustup            # noble universe, 1.26.0-5ubuntu0.1
       ...
     build-snaps:
       - node/20/stable
-      - rustup/latest/stable
     override-build: |
       set -e
+      rustup default stable
       craftctl set version="..."

rustup is an apt package in noble (universe), so it lands in /usr/bin and no snap PATH question arises. The plugin's other implicit build-packages (curl gcc git pkg-config findutils) go away with plugin: rust, so any of those the build actually needs would have to be listed — build-essential, curl, pkg-config are already there; git may be worth adding.

The documented alternative, if you'd rather keep plugin: rust, is the rust-deps part the error names — a plugin: nil part that installs rustup via the upstream script, with after: [rust-deps] on the markpad part. More moving pieces for a plugin whose build step you are already replacing.

One thing you may not have seen

Both steps carry continue-on-error: true, so a failing snap build reports as a green step. Checking the last releases:

release run Environment validation failed Snap file not found
2026-08-05 yes yes
2026-08-06 (v2.7.1) yes yes
2026-08-07 — (died at AppImage first)

So the snap has been failing on the same line for at least the last two releases while the step showed green. Worth knowing before cutting 2.7.2 — the AppImage fix for #498 will ship regardless, but the snap channel will not update.

Caveat: I'm on macOS too, so the diff above is reasoned from the craft-parts source and the noble package index, not from a build I ran. plugin: nil removing the failing validation is solid; whether the build then completes is what a workflow_dispatch run would tell you — which is exactly the thing you proposed earlier.

@PathGao

PathGao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

2.7.2 finished while I was writing that — the important half worked. Markpad_2.7.2_amd64.AppImage and its .sig are both on the release, latest.json published, so #498 reaches users on the next update check.

The snap did not, and it is the same line again:

Build and Publish Snap Package
  sg lxd -c 'snapcraft pack'
  Environment validation failed for part 'markpad': 'rustup' not found and part 'markpad'
  does not depend on a part named 'rust-deps' that would satisfy the dependency.
  Snap file not found!

That makes three consecutive releases, and there is no .snap among the v2.7.2 assets — the store channel is still on whatever last succeeded. Nothing to do tonight; just confirming the diagnosis above against a run that happened after rustup/latest/stable was added, which rules out that line being the fix.

@PathGao has gone to bed — he'll pick this back up tomorrow, along with the CLI upgrade. Thanks for cutting 2.7.2 so quickly.

@PathGao

PathGao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

I left my claude code opus 5 bot here,so you can ask questions in about 1 hour it will reply

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.

2 participants