docs: update feedback pages from alpha to beta/GA policy#5483
docs: update feedback pages from alpha to beta/GA policy#5483leaanthony wants to merge 27 commits into
Conversation
Introduces app.Updater — a singleton update orchestrator wired into application.App. End users configure it once via Init then drive it with Check / DownloadAndInstall / CheckAndInstall. The Provider interface (Name / Check / Download) lets any release source back the updater — GitHub Releases, keygen.sh, AppCast feeds, custom — with verification, atomic writes, swap and the default window all owned by the Updater rather than the provider. Verification is built in: Ed25519, Ed25519ph (pre-hash, SHA-512) and ECDSA-P256 over SHA-256, with digest-only mode supported. The internal verifier registry is unexported in v1 so we can promote it to a public RegisterVerifier later without breaking callers. Subscribe to lifecycle events through the standard Wails event bus — both Go and JavaScript use the same `updater:*` event names. Event constants exported on the package with payload types documented inline. This commit lands the interface, types, verification, atomic download with streaming digest, and a fake-Provider test suite (19 tests). Window template, binary swap, restart, GitHub/keygen.sh/AppCast providers and docs follow in subsequent commits.
Add the swap-and-relaunch step that completes the update flow without asking the user to deal with overwriting-a-running-binary on any platform. Implements the inconshreveable/go-update pattern: when the Updater calls Restart, the app re-execs itself with sentinel env vars. application.New detects those vars at startup, hands control to runHelperSwap, and the parent exits. The helper-mode process waits for the parent PID to die, backs up the running binary, retries an atomic rename of the new artifact into place (Windows file-lock safety net), launches the replaced binary, and self-destructs the backup. Handles macOS .app bundles correctly via recursive copy/symlink-aware traversal, falls back to a single-file path for plain executables on Linux and Windows, and rolls back to the backup if either the swap or the relaunch fails so a broken update never leaves the app unbootable. No extra binary shipped: the helper IS the app, switched into helper mode by env vars. Cross-builds clean for darwin/linux/windows. Seven new swap-logic tests plus the existing 19 keep the package at 26 green.
Built-in HTML/CSS/JS template that listens to updater:* events and emits updater:user:* actions back to Go — no framework-specific tags, no custom elements, just plain Wails events through the runtime. Dark-mode CSS variables, status / progress / release-notes panels, four action buttons (Install / Skip / Remind / Cancel) plus a Restart button visible only in the ready state. Override the whole template via BuiltinWindow.HTML, layer extra CSS via BuiltinWindow.CSS, tweak chrome (frameless/size/AOT) via BuiltinWindow.Options. Window option types: nil → defaults, *BuiltinWindow → overrides, any value satisfying updater.WindowHandle (e.g. *application.WebviewWindow) → BYO, updater.WindowNone → headless. Host interface gains OpenWindow + OnEvent so the updater package can drive real Wails windows without importing application. The application package supplies an adapter (newUpdaterHost) that wraps *EventManager and *WindowManager.NewWithOptions, plus a small updaterWindowHandle that trims the fluent return values off SetHTML/Show/Hide. User-action listeners are wired the moment a session opens, dispatching updater:user:install → DownloadAndInstall, updater:user:cancel → closeWindow, updater:user:skip → SkipVersion + close, updater:user:remind → close, updater:user:restart → Restart. Skipped versions short-circuit subsequent Checks to up-to-date until cleared. 8 new lifecycle tests on top of the prior 26; full suite 34 green. App package tests still green.
First provider implementation: hits api.github.com (or a configurable base URL for GitHub Enterprise), picks an asset by GOOS/GOARCH heuristics, and optionally pulls a sibling SHA256SUMS-style checksum file to populate Release.Verification — letting the framework's verifier authenticate the download with zero extra config from the user. Asset matching defaults to substring-of-platform AND substring-of-arch on the filename, skipping .sig / .asc / *sum* sidecars. Recognises the common arch aliases (amd64 ↔ x86_64/x64, arm64 ↔ aarch64, 386 ↔ i386/x86/ia32). Custom selection via Config.AssetMatcher. Auth via Bearer PAT raises rate limits and unlocks private repos. Download follows the asset's signed redirect and strips Authorization on the cross-host hop so the token never reaches the storage backend. Eleven httptest tests cover the happy path, up-to-date / 404, prerelease toggle, asset filter, missing match, checksum-sidecar verification, the redirect-and-strip flow, validation, and API-error surfacing.
Port the keygen.sh integration written previously into the Wails v3
updater Provider shape. Key adaptation: keygen's per-artifact base64
checksum (SHA-512) and signature (Ed25519ph) fields now flow into
Release.Verification, so the framework's verifier authenticates the
download automatically once the consumer has set Config.PublicKey on the
Updater — no provider-specific verification path.
Handles keygen.sh quirks discovered while building the original library:
the /upgrade endpoint silently drops ?include=artifacts (fallback fetch
to /releases/{id}/artifacts), the artifact download 303s to Cloudflare R2
on a different host (CheckRedirect wrapper drops Authorization on cross-
origin hops), and 404 from /upgrade means "no newer release" rather than
a hard error.
Auth supports either Token (admi-/prod-/envi-/user- prefixed values) or
LicenseKey, with Token winning when both are set. Typed APIError surfaces
the keygen JSON error envelope (title / detail / code / status) for
debuggable failures.
Eleven httptest tests cover the happy path with verification mapping,
404=up-to-date, both auth schemes, error surfacing, no-matching artifact,
the include=artifacts fallback, the redirect-strip flow, validation, and
the Provider-interface assertion.
Parses the canonical Sparkle XML vocabulary so existing Sparkle/WinSparkle
feeds drop in unchanged: items are picked by sparkle:shortVersionString
(falling back to sparkle:version), the download comes from <enclosure>,
and Sparkle 2's sparkle:edSignature flows into Release.Verification as an
Ed25519 signature for the framework's verifier.
Item selection reconciles Sparkle's loose OS naming ("macos"/"mac"/"osx"
all map to Go's "darwin"), and a Config.Channel filter narrows to one
sparkle:channel when the feed publishes multiple. Sparkle 1's
sparkle:dsaSignature is intentionally not supported — projects on DSA
should rotate to EdDSA per Sparkle 2's guidance.
Ten httptest tests cover the happy path with verification, Windows
selection, up-to-date short-circuit, no-platform-match, 404=up-to-date,
500=error, channel filtering, download streaming with progress,
URL-required validation, and the Provider interface assertion.
Docs cover the 30-second quick start, the three built-in providers (GitHub Releases / keygen.sh / Sparkle AppCast) with config knobs, fallback chain semantics, cryptographic verification (Ed25519, Ed25519ph, ECDSA-P256, digest-only), default-window theming through CSS variables, full template replacement, window chrome override, BYO via application.Window, headless mode, the full event taxonomy for both Go and JavaScript, how to write your own provider, and how the same-binary helper-mode swap works under the hood. Example app exercises the GitHub provider end-to-end: APP_VERSION + GH_REPOSITORY env vars, "Check for Updates…" menu item that drives CheckAndInstall, a small JS-side log mirroring updater:* events alongside the framework's default window.
- Drop dead import-stabilising stubs: `var _ = url.Parse` + `net/url` import in github.go, `var _ = errors.New` in github_test.go, `var _ = io.EOF` in appcast_test.go. - Drop dead `sessionMu` type from window_lifecycle.go (never used — Updater uses a plain sync.Mutex named sessMu). - Remove the `req.Header.Set(\"Accept\", req.Header.Get(\"Accept\"))` no-op round-trip in github.Provider.setAuth. - Replace hand-rolled path concatenation with filepath.Join in download.go. - Drop the unused `target` parameter and vestigial `_ = path` / `_ = target` blanks from openHelperLog. - Use filepath.Dir instead of hand-rolled \"/\"-based parentOf in updater_test.go (broke on Windows). - Drop the package-level mockHost in github_test.go in favour of a test-local host variable (so future t.Parallel() doesn't race). - writeFile uses 0o644 for files instead of 0o755.
Each invocation of CheckAndInstall registered 5 fresh user-action listeners under sessMu but never closed any previous session. A periodic-check timer firing alongside a manual click (or two clicks) would leak listeners and windows; stale callbacks would still fire on the next user action, double-invoking DownloadAndInstall or Restart. Close any active session before opening the new one, and add a regression test that exercises three back-to-back CheckAndInstall calls and asserts the host's listener map is empty afterwards.
The download flow created an os.MkdirTemp(\"\", \"wails-update-*\") directory and staged the artifact inside. Two leak paths left orphan directories under os.TempDir across update attempts: - success path: after Restart spawned the helper and the helper renamed the staged binary into place, the (now-empty) directory persisted. - error paths: \`_ = os.Remove(tmpPath)\` removed the staged file but left the enclosing dir. Plus, when digestHasher errored before any download bytes flowed, the freshly-created directory was leaked outright (flagged by CodeRabbit). Three coordinated changes: 1. download() now returns (path, dir, err) and tears the directory down on every error path via a single deferred RemoveAll guarded by a success bool — including the hasher-setup branch. 2. Updater tracks stagingDir alongside resolved; DownloadAndInstall removes any stale dir before downloading again and RemoveAll's the current dir on verify/install failures. 3. runHelperSwap removes the parent directory of newPath after a successful rename, guarded by the \`wails-update-\` prefix so a caller-supplied path outside the temp area can never be deleted by accident. Adds a TempDir-counting regression test to keep this from regressing.
The previous composeHTML appended the override <style> tag after the closing </html> of the default template, producing a malformed document that HTML linters and CSP-strict configurations reject. Splice the extra <style> before </head> when one is present so the result stays well-formed. Custom-HTML templates without </head> fall back to appending at the document end with a comment. Tightens the existing CSS-injection test to assert the override sits before both </head> and </html>.
\`/repos/{r}/releases\` includes draft releases when the request is
authenticated, and \`per_page=1\` plus a recently-created draft would
surface that draft as an available update instead of the actual
latest prerelease. Asymmetric with the non-prerelease path, which
hits \`/releases/latest\` and is already draft-free.
Fetch up to 10 items and return the first non-draft, adding a
regression test that injects a draft at the top of the list.
asn1.Unmarshal returns the unconsumed tail of its input; the previous splitECDSASig discarded it, accepting non-canonical signatures with arbitrary trailing data. That is a known signature-malleability hazard and conforming signers never emit such data. Require asn1.Unmarshal to consume the entire signature and return an error otherwise. Includes a regression test that appends four bytes of junk to a valid signature and asserts verification fails closed.
The github and appcast providers each shipped a near-byte-identical isNewer/compareSemver/splitNumeric. Beyond duplication the home-rolled comparator had three correctness problems flagged in review: - prereleases sorted *newer* than the corresponding release (\"1.2.3-rc.1\" > \"1.2.3\" instead of <) - build metadata (\"+build.456\") was parsed as a numeric run rather than ignored, so two releases that differ only in metadata compared as different - non-semver inputs returned monotonic-ish ordering that depended on digit-run alignment Introduce v3/pkg/updater/internal/semver wrapping golang.org/x/mod/semver (already an indirect dep — go mod tidy promotes it to direct). Providers call IsNewer / TrimPrefix; tests cover the prerelease, numeric-segment, and build-metadata cases that the previous implementation got wrong.
The default window template emits updater:window:ready on load to ask the host for a state snapshot — useful when the window opens after a periodic-check timer already advanced the flow, e.g. straight into StateAvailable, so it can't rely on the live event stream alone. openSession previously registered listeners for the five user-action events but ignored EventWindowReady, so the window stayed in its initial \"looking for updates...\" state until the next event fired. Subscribe to EventWindowReady and replay the lifecycle event matching the current State (update-available, download-started, verifying, installing, update-ready, or no-update). Idle / unconfigured / error have nothing useful to replay and are skipped.
The Windows helper's waitForPID polled isAlive, which called os.Process.Signal(nil). On Windows that always errors (EWINDOWS for live processes, ErrProcessDone for dead ones), so isAlive reported every pid as dead. Result: the helper never actually waited for the parent to release the binary on Windows — it spun straight into the swap-retry loop and grinding the file lock. Split isAlive's per-platform probe into a platformIsAlive helper. - Unix path: unchanged — proc.Signal(syscall.Signal(0)). - Windows path: OpenProcess + GetExitCodeProcess. Pulling the exit code directly from the kernel distinguishes STILL_ACTIVE (alive) from any terminated state without requiring x/sys/windows.
The previous test raced a 10ms time.Sleep against listener registration in openSession: a slow CI runner could fire EventUserCancel before openSession installed its handler, so the cancel was dropped and the 500ms poll loop quietly timed out. The full CheckAndInstall flow is synchronous when the fake provider returns immediately, and StateReady leaves the window open by design. Drive it to completion first (listeners now guaranteed wired), then fire the cancel emit. fakeHost dispatches callbacks on the calling goroutine so closeWindow finishes before Emit returns — no polling, no sleep, no time import. 20x -count run stays green.
github: - followAndStrip now preserves a caller-supplied CheckRedirect by wrapping it (matches keygen / appcast) and uses strings.EqualFold on host comparison. - isChecksumName is anchored to suffixes / whole tokens so primary artifacts whose names contain a hash algorithm (e.g. \"myapp-sha256-amd64.zip\") are no longer mis-classified as sidecars and silently skipped. keygen: - Upgrade response body is now wrapped in io.LimitReader(8 MiB) — previously an unbounded io.ReadAll on a misbehaving / compromised endpoint could OOM the host. - Check stashes the chosen artifact's keygen.sh ID under rel.Metadata[\"keygen.artifact.id\"]; Download prefers that over the filename so releases that ship two artifacts with the same filename across platforms are unambiguous. - pickArtifact normalises operator-defined aliases via case- insensitive matchers — \"macos\" matches darwin, \"x86_64\"/\"x64\" matches amd64, \"aarch64\" matches arm64, \"i386\"/\"x86\" matches 386. appcast: - Download explicitly clears any Authorization header on the enclosure request — even though Check never sets one, a caller- supplied default on the http.Request would otherwise leak cross-origin on the initial CDN hop (the redirect wrapper only covers subsequent hops). Regression tests cover the github false-positive case, the keygen ID stash + download-by-ID flow, and the keygen alias matching.
- appcast.pickBestItem: an explicit Config.Channel no longer matches items that ship without a sparkle:channel. Sparkle's contract is that items opt *in* to a channel, so the previous "channel mismatch only if both sides non-empty" check could leak unlabelled (stable-default) items into a non-stable filter. - github.containsArch: a 386 request no longer false-positive-matches x86_64 / amd64 / x64 assets through the bare "x86" alias. 64-bit aliases on the filename now veto a 386 match outright. - window_test custom-HTML assertion: guard the 60-char preview slice so a short InitialHTML doesn't panic and mask the real failure. Regression tests cover both provider fixes.
The existing TestDefaultAssetMatcher_386_DoesNotMatchX86_64 covers this in a dedicated test, but adding the case to the table-driven TestDefaultAssetMatcher keeps the alias check next to the other matcher cases — where future readers (and reviewers) naturally look when expanding the matrix. Suggested in CodeRabbit's review nitpick.
The previous default of wailsapp/wails fails because Wails' own
release artifacts don't match the platform-naming heuristic the
updater's DefaultAssetMatcher expects ("darwin"/"arm64" tokens in
the filename, no `wails*` suffix etc.) — running the example
out-of-the-box surfaces "no asset for darwin/arm64" instead of
demoing the actual flow.
Point at wailsapp/updater-demo, a dedicated repo whose releases
publish updater-demo_<goos>_<goarch>.<ext> archives plus a
SHA256SUMS sidecar. The example also opts into ChecksumAsset
verification by default so digest-mismatch is exercised on first
run. Env-var overrides still work for users pointing at their own
repos.
The GitHub-provider example config used \`Repository: \"wailsapp/wails\"\`
which suggests pointing at the wails repository itself — but wails's
own release artifacts don't match the updater's filename heuristics
("darwin"/"arm64" tokens, no \`wails*\` prefix). A first-time user
copy/pasting hits "no asset for darwin/arm64" instead of seeing the
flow work.
Use \`myorg/myapp\` to match the Quick Start at the top of the page,
and call out the runnable example + its dedicated demo repo right
under the snippet so readers know where to look for a working
end-to-end target.
The README's Verification section talked only about publicKey/signature verification, but the example now ships ChecksumAsset: \"SHA256SUMS\" by default — so digest verification runs out of the box against the demo repo's sidecar. Lead with that (matching what the example actually does), then describe the publicKey path as the next step up.
Four fixes from @atterpac's Arch Linux testing on the PR: - SetHTML on webkit2gtk loads into the about:blank context which has no Wails runtime, so the bound JS could not Emit events back. Pass the HTML at construction via WebviewWindowOptions.HTML instead; drop SetHTML from the WindowHandle interface (it was dead surface). - Wails' EventProcessor.Emit dispatches to listeners + JS on fresh goroutines, so events arrive at the frontend out of source order (\"installing → verifying → update-ready → download-complete\" was observed). Fixing the bus is out of scope for this PR; instead make the default window state-monotonic — rank the lifecycle states, reject backward transitions, gate side effects on setState() returning true, and only paint download progress while we're still in the downloading state. - Restart() spawned the helper and returned, leaving the caller responsible for app.Quit() — which everyone missed, so the helper sat blocked on waitForPID until its 30s timeout. Add Quit() to the Host interface, have Restart() call it after the spawn so the helper handoff actually completes. Regression test mocks the spawn via new export_test.go seams. - The downloaded artifact is opened with os.Create which masks 0o666 against umask, dropping the executable bit on Unix. The swap renamed it into place and the relaunch then failed with EACCES. Snapshot the original target's mode before the swap and chmod the new file to match after.
TestRestart_QuitsAfterSpawn used /bin/true which doesn't exist on Windows. Replace with os.Executable + -test.run=^$ so the spawned command is the test binary itself, running no tests, and exits cleanly on all platforms. Restart() passed the raw os.Executable path as WAILS_UPDATER_HELPER_TARGET. On macOS, swapping the binary inside a .app bundle invalidates the code signature and Gatekeeper blocks relaunch. bundleTarget() now walks the path for a .app segment and returns the bundle root on darwin; on all other platforms it is a no-op passthrough. Also replace the hand-rolled constantTimeEqual with crypto/subtle.ConstantTimeCompare to handle the length-mismatch case in constant time.
Replace alpha-era Discord-centric feedback workflow with standard GitHub Issues (bugs) and GitHub Discussions (suggestions) as primary channels. Remove alpha PR title prefix requirements and the 'things we are looking for feedback on' section. Applied to all 8 locales (en, ja, ru, pt, zh-tw, de, ko, fr).
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (46)
WalkthroughThis PR introduces Wails v3's in-app self-update system with pluggable release providers, atomic binary swapping, cryptographic verification, and an integrated UI window. It includes GitHub/keygen/AppCast provider implementations, updates all localized feedback documentation to direct users to GitHub workflows, and adds a complete feature guide with working example. Changes Feedback Documentation Localization
User-Facing Documentation and Example
Core Updater Implementation
Application Integration
🎯 4 (Complex) | ⏱️ ~60 minutes
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
|
|
Closing in favour of #5484 which is correctly based on master. |
Summary
#v3as a secondary/community discussion channel[v3 alpha]/[v3 alpha test]PR title prefix requirements; link toCONTRIBUTING.mdinsteadTest plan
Summary by CodeRabbit
New Features
Documentation
Examples