-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat(v3): add selfupdate service for application self-updating #4820
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v3-alpha
Are you sure you want to change the base?
Conversation
Add a new selfupdate service that enables Wails v3 applications to check for and apply updates from GitHub, GitLab, or Gitea releases. Features: - Support for GitHub, GitLab, and Gitea release sources - ECDSA and PGP signature verification for secure updates - Checksum verification option - Themable progress UI with customizable colors and icons - Progress events for download tracking in frontend - Automatic platform detection (OS/arch) - CLI commands for key generation, signing, and verification Closes #1178 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughThis PR introduces a comprehensive self-update feature for Wails v3 applications, including a service for checking and installing updates from GitHub/GitLab/Gitea, CLI tools for cryptographic signing and verification, a full example application, and detailed documentation with configuration options, signing workflows, and UI customization. Changes
Sequence Diagram(s)sequenceDiagram
participant Frontend as Frontend/UI
participant Service as Self-Update Service
participant Source as GitHub/GitLab/Gitea
participant FS as File System
participant App as Application
rect rgb(200, 220, 240)
note over Frontend,Source: Check for Updates
Frontend->>Service: Check(ctx)
Service->>Source: Query latest release
Source-->>Service: Release info
Service->>Service: Validate version & signature
Service-->>Frontend: UpdateInfo (available, version, notes)
end
rect rgb(240, 220, 200)
note over Service,App: Download & Install
Frontend->>Service: DownloadAndInstall(ctx)
Service->>Source: Download release binary
Source-->>Service: Binary stream
Service->>Service: emit DownloadProgress event
Frontend->>Frontend: Update progress bar
Service->>FS: Extract & replace binary
Service->>Service: Verify installation
Service->>Service: Clear pending update state
Service-->>Frontend: Success
end
rect rgb(220, 240, 200)
note over Service,App: Restart Application
Frontend->>Service: Restart()
Service->>App: Relaunch updated app
App-->>App: Exit & restart
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (6)
docs/src/content/docs/guides/selfupdate.mdx (1)
391-397: Consider adding cleanup trap for CI/CD example.The example writes the private key to a temporary file and removes it after signing. If the
wails3 tool selfupdate signcommand fails, the key file remains on disk. Consider suggesting a trap for cleanup:run: | trap 'rm -f /tmp/key.pem' EXIT echo "$SIGNING_KEY" > /tmp/key.pem wails3 tool selfupdate sign -key /tmp/key.pem -file ./build/myappThis is a minor documentation improvement for security best practices.
v3/examples/selfupdate/assets/index.html (1)
469-475: Consider re-enabling update button on failure.On error, only
checkBtnis re-enabled (line 473), leavingupdateBtndisabled. This means users must re-check for updates before retrying the download. This may be intentional (to refresh update info), but consider if allowing immediate retry would provide better UX.🔎 Optional: Allow retry without re-check
} catch (err) { setStatus('error', 'Update Failed'); log('Error during update: ' + err.message, 'error'); checkBtn.disabled = false; + updateBtn.disabled = false; // Allow retry }v3/pkg/services/selfupdate/selfupdate.go (4)
541-556: CanUpdate check may not work correctly on Windows.On Windows, the running executable is typically locked and cannot be opened for writing. This check would always return
falseeven if the update mechanism (which replaces the executable via rename) would succeed. Also,file.Close()error is ignored.🔎 Consider platform-specific checks or removing this heuristic
// CanUpdate returns true if the current process has permission to update itself. -// This checks if the executable is writable. +// On Windows, this checks if we can write to the executable's directory. +// On other platforms, this checks if the executable itself is writable. func (s *Service) CanUpdate() bool { exe, err := selfupdate.ExecutablePath() if err != nil { return false } + if runtime.GOOS == "windows" { + // On Windows, check if we can create a file in the same directory + dir := filepath.Dir(exe) + testFile := filepath.Join(dir, ".wails-update-test") + f, err := os.Create(testFile) + if err != nil { + return false + } + f.Close() + os.Remove(testFile) + return true + } + // Check if we can write to the executable. file, err := os.OpenFile(exe, os.O_WRONLY, 0) if err != nil { return false } - file.Close() + _ = file.Close() return true }This would require adding
"path/filepath"to imports.
558-586: Restart method could benefit from a brief delay before quitting.The current implementation starts a new process and immediately calls
Quit(). On some systems (especially Windows), this could cause issues if the new process tries to access resources (files, ports, sockets) still held by the exiting process.🔎 Proposed fix: add a small delay before quitting
+ // Give the new process a moment to start before we exit. + // This helps avoid resource contention on some platforms. + time.Sleep(100 * time.Millisecond) + // Quit the current application. s.app.Quit()This would require adding
"time"to imports.
598-642: Consider extracting shared logic between Check and GetLatestRelease.
GetLatestReleaseduplicates ~40 lines fromCheck. The only difference is thatCheckstorespendingUpdate. Consider extracting the common release-fetching and info-building logic to reduce duplication.
486-503: Struct documents "progress" state that is never emitted during download.The
DownloadProgressstruct documents a "progress" state, but only "started" and "finished" are emitted. No progress callbacks are supported by the creativeprojects/go-selfupdate library, so intermediate updates cannot be wired in without a different approach—either replacing the library or implementing custom download tracking outside the library's UpdateTo method.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
v3/go.sumis excluded by!**/*.sum
📒 Files selected for processing (8)
docs/src/content/docs/guides/selfupdate.mdx(1 hunks)v3/UNRELEASED_CHANGELOG.md(1 hunks)v3/cmd/wails3/main.go(1 hunks)v3/examples/selfupdate/assets/index.html(1 hunks)v3/examples/selfupdate/main.go(1 hunks)v3/go.mod(6 hunks)v3/internal/commands/selfupdate.go(1 hunks)v3/pkg/services/selfupdate/selfupdate.go(1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2024-09-30T06:13:46.595Z
Learnt from: leaanthony
Repo: wailsapp/wails PR: 3763
File: v3/examples/window/main.go:472-475
Timestamp: 2024-09-30T06:13:46.595Z
Learning: In `v3/examples/window/main.go`, `time.Sleep` is used within a goroutine and does not block the UI thread.
Applied to files:
v3/examples/selfupdate/main.go
📚 Learning: 2024-09-30T06:14:32.602Z
Learnt from: leaanthony
Repo: wailsapp/wails PR: 3763
File: v3/internal/commands/appimage_testfiles/main.go:295-299
Timestamp: 2024-09-30T06:14:32.602Z
Learning: In `v3/internal/commands/appimage_testfiles/main.go`, `time.Sleep` is used within a goroutine and does not block the UI thread.
Applied to files:
v3/examples/selfupdate/main.go
📚 Learning: 2025-02-04T23:59:43.956Z
Learnt from: fbbdev
Repo: wailsapp/wails PR: 4045
File: v3/internal/generator/render/info.go:28-68
Timestamp: 2025-02-04T23:59:43.956Z
Learning: In the Wails v3 project, internal functions are designed to panic on nil parameters as they represent contract violations, rather than adding defensive nil checks.
Applied to files:
v3/go.mod
📚 Learning: 2025-04-29T23:54:07.488Z
Learnt from: popaprozac
Repo: wailsapp/wails PR: 4256
File: v2/internal/frontend/desktop/linux/notifications.go:27-28
Timestamp: 2025-04-29T23:54:07.488Z
Learning: In Wails v2, unlike v3-alpha which has a `ServiceShutdown` method for services, there is no standardized teardown pattern for frontend implementations. When implementing features that require cleanup (like goroutines or resources), add explicit cleanup methods (e.g., `CleanupNotifications()`) that handle resource release, context cancellation, and connection closure.
Applied to files:
v3/pkg/services/selfupdate/selfupdate.go
🧬 Code graph analysis (3)
v3/internal/commands/selfupdate.go (1)
v3/internal/term/term.go (2)
Println(109-111)Success(80-82)
v3/examples/selfupdate/main.go (1)
v3/pkg/services/selfupdate/selfupdate.go (7)
New(196-198)Service(185-192)NewWithConfig(202-208)Config(60-92)Source(34-34)SourceGitHub(38-38)UIConfig(111-137)
v3/pkg/services/selfupdate/selfupdate.go (1)
v3/pkg/application/services.go (2)
ServiceStartup(98-100)ServiceOptions(17-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: Run Go Tests v3 (ubuntu-latest, 1.24)
- GitHub Check: Run Go Tests v3 (windows-latest, 1.24)
- GitHub Check: Run Go Tests v3 (macos-latest, 1.24)
- GitHub Check: semgrep/ci
- GitHub Check: Analyze (go)
- GitHub Check: semgrep-cloud-platform/scan
- GitHub Check: Cloudflare Pages
🔇 Additional comments (22)
v3/UNRELEASED_CHANGELOG.md (1)
20-29: LGTM!The changelog entries are well-structured, following the Keep a Changelog format. They accurately describe the new selfupdate service features and CLI commands, and correctly reference the linked issue #1178.
v3/cmd/wails3/main.go (1)
106-128: LGTM!The selfupdate CLI command wiring follows the established patterns in this file. The three subcommands (keygen, sign, verify) are correctly structured with their respective options structs and action handlers, consistent with other tool commands like
signat lines 99-104.v3/internal/commands/selfupdate.go (4)
43-68: LGTM - Secure key generation implementation.The ECDSA P-256 key generation is well-implemented:
- Directory creation with appropriate permissions (0755)
- Proper check for existing keys to prevent accidental overwrites
- Force flag option for intentional overwrites
70-86: LGTM - Appropriate certificate configuration for code signing.The certificate template correctly specifies
KeyUsageDigitalSignatureandExtKeyUsageCodeSigning, which is appropriate for update signing. The configurable validity period viaValidYearsis a nice touch.
170-182: LGTM - Signing implementation is correct.The signing flow correctly reads the file, computes SHA-256 hash, and produces an ASN.1-encoded ECDSA signature. For typical application binaries, loading the file into memory is acceptable.
214-266: LGTM - Verification implementation is secure.The verification correctly:
- Parses the X.509 certificate to extract the ECDSA public key
- Validates the key type with a clear error message
- Uses
VerifyASN1which properly handles ASN.1-encoded signatures- Returns distinct error vs success states
v3/examples/selfupdate/main.go (1)
18-57: LGTM - Well-structured example application.The example effectively demonstrates:
- Service registration with
NewWithConfig- All major configuration options (version, source, repository, prerelease, UI theming)
- Commented-out signature config showing optional security setup
- UI colors that match the frontend CSS variables
The placeholder repository
"example/myapp"is appropriate for demo code.v3/examples/selfupdate/assets/index.html (2)
385-407: LGTM - Progress event handling.The event listener correctly handles all three progress states (
started,progress,finished) and updates the UI appropriately. The progress bar animation and text updates provide good user feedback.
1-256: LGTM - Clean and well-organized UI implementation.The HTML/CSS implementation is well-structured with:
- CSS custom properties for theming that align with the Go UIConfig
- Accessible status indicators with both visual (dot color) and text feedback
- Responsive layout with proper spacing
- Good visual hierarchy for the card-based design
v3/go.mod (2)
3-3: Go version 1.24.11 is valid — Go 1.24 is a major release that arrived in February 2025, making versions like 1.24.11 entirely legitimate and well-supported.
13-13: New dependency: go-selfupdate v1.5.2.This library enables the self-update functionality with no known vulnerabilities. The implementation supports secure update mechanisms including SHA256 hash and ECDSA signature validation, aligning with the PR objectives.
v3/pkg/services/selfupdate/selfupdate.go (11)
1-31: LGTM!The package documentation is clear and the imports are appropriate for the functionality.
33-57: LGTM!Well-documented type definitions with clear constants for update sources and signature types.
59-92: LGTM!The
Configstruct is well-organized with clear documentation. The optional fields have sensible defaults mentioned in the comments.
139-182: LGTM!Well-structured data types with appropriate JSON tags for frontend integration.
210-232: LGTM!Service lifecycle methods follow the Wails v3
ServiceStartup/ServiceShutdownpattern correctly. The lock inServiceStartupis appropriate sinceinitSourcedoesn't perform blocking network operations.
265-339: LGTM!Good implementation with proper error wrapping and handling of all source types. The guard clause for nil config is appropriate.
341-385: LGTM!Good defensive programming with panic recovery for the underlying library. All signature types are properly handled with appropriate validation.
387-394: LGTM!Simple and effective parsing logic for the repository format.
396-450: LGTM!Proper use of read locks for accessing configuration and appropriate write lock for storing pending updates. The version comparison logic correctly handles both upgrade and downgrade scenarios.
513-518: LGTM!Clean helper with appropriate nil check for the app reference.
644-657: LGTM!Correctly returns a copy of the UI configuration to prevent external modification, with proper nil handling.
| ```javascript | ||
| // Get the selfupdate service | ||
| const Selfupdate = wails.Service("github.com/wailsapp/wails/v3/services/selfupdate"); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verify the service path for frontend binding.
The service path "github.com/wailsapp/wails/v3/services/selfupdate" appears to be missing the pkg segment. Based on the Go imports in the example code (line 38-39), the actual package path is github.com/wailsapp/wails/v3/pkg/services/selfupdate.
🔎 Suggested fix
-const Selfupdate = wails.Service("github.com/wailsapp/wails/v3/services/selfupdate");
+const Selfupdate = wails.Service("github.com/wailsapp/wails/v3/pkg/services/selfupdate");📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ```javascript | |
| // Get the selfupdate service | |
| const Selfupdate = wails.Service("github.com/wailsapp/wails/v3/services/selfupdate"); |
🤖 Prompt for AI Agents
In docs/src/content/docs/guides/selfupdate.mdx around lines 73 to 76, the
frontend service binding uses the path
"github.com/wailsapp/wails/v3/services/selfupdate" which omits the required
"pkg" segment; update the service path to
"github.com/wailsapp/wails/v3/pkg/services/selfupdate" so it matches the actual
Go package import and allows the frontend to correctly bind to the selfupdate
service.
| // Service binding - adjust the path based on your generated bindings | ||
| const Selfupdate = wails.Service("github.com/wailsapp/wails/v3/services/selfupdate"); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same service path issue as documentation.
The service binding path appears to be missing the pkg segment, same as in the documentation. This should be consistent with the actual Go package path.
🔎 Suggested fix
- const Selfupdate = wails.Service("github.com/wailsapp/wails/v3/services/selfupdate");
+ const Selfupdate = wails.Service("github.com/wailsapp/wails/v3/pkg/services/selfupdate");📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Service binding - adjust the path based on your generated bindings | |
| const Selfupdate = wails.Service("github.com/wailsapp/wails/v3/services/selfupdate"); | |
| // Service binding - adjust the path based on your generated bindings | |
| const Selfupdate = wails.Service("github.com/wailsapp/wails/v3/pkg/services/selfupdate"); |
🤖 Prompt for AI Agents
In v3/examples/selfupdate/assets/index.html around lines 329-331, the Wails
service binding path is missing the pkg segment; update the binding to use the
correct Go package path by inserting "pkg" after "v3" (i.e. change the service
path to the package path that includes pkg) so it matches the actual Go package
location.
| // NewWithConfig creates a new selfupdate service with the given configuration. | ||
| // If config is nil, you must call Configure before using the service. | ||
| func NewWithConfig(config *Config) *Service { | ||
| s := &Service{} | ||
| if config != nil { | ||
| _ = s.Configure(config) | ||
| } | ||
| return s | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Configuration errors silently ignored in constructor.
When NewWithConfig is called with an invalid config (e.g., missing Repository or CurrentVersion), the error from Configure is discarded. Users may not realize their configuration is invalid until ServiceStartup fails.
Consider returning the error or at minimum documenting this behavior.
🔎 Option 1: Return error from constructor
-// NewWithConfig creates a new selfupdate service with the given configuration.
-// If config is nil, you must call Configure before using the service.
-func NewWithConfig(config *Config) *Service {
+// NewWithConfig creates a new selfupdate service with the given configuration.
+// If config is nil, you must call Configure before using the service.
+// Returns an error if the provided config is invalid.
+func NewWithConfig(config *Config) (*Service, error) {
s := &Service{}
if config != nil {
- _ = s.Configure(config)
+ if err := s.Configure(config); err != nil {
+ return nil, err
+ }
}
- return s
+ return s, nil
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In v3/pkg/services/selfupdate/selfupdate.go around lines 200 to 208, the
constructor NewWithConfig swallows the error returned by Configure which hides
invalid configuration; change the API to return (*Service, error) and propagate
Configure's error (i.e., call Configure and if it returns an error return nil
and that error, otherwise return the service and nil); update any callers to
handle the new error return (or alternatively, add clear doc comment if you
choose not to change the signature), and run tests to ensure call sites are
updated accordingly.
| // Clone to prevent changes from the outside. | ||
| clone := new(Config) | ||
| *clone = *config | ||
|
|
||
| // Set defaults. | ||
| if clone.Source == "" { | ||
| clone.Source = SourceGitHub | ||
| } | ||
|
|
||
| s.lock.Lock() | ||
| defer s.lock.Unlock() | ||
|
|
||
| s.config = clone |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shallow clone doesn't protect nested pointer fields.
The clone operation *clone = *config performs a shallow copy. If the caller modifies config.Signature or config.UI fields after calling Configure, those changes will affect the service's internal state since the pointers are copied by value.
🔎 Proposed fix for deep cloning nested structs
// Clone to prevent changes from the outside.
clone := new(Config)
*clone = *config
+ if config.Signature != nil {
+ sig := *config.Signature
+ clone.Signature = &sig
+ }
+ if config.UI != nil {
+ ui := *config.UI
+ clone.UI = &ui
+ }
// Set defaults.
if clone.Source == "" {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Clone to prevent changes from the outside. | |
| clone := new(Config) | |
| *clone = *config | |
| // Set defaults. | |
| if clone.Source == "" { | |
| clone.Source = SourceGitHub | |
| } | |
| s.lock.Lock() | |
| defer s.lock.Unlock() | |
| s.config = clone | |
| // Clone to prevent changes from the outside. | |
| clone := new(Config) | |
| *clone = *config | |
| if config.Signature != nil { | |
| sig := *config.Signature | |
| clone.Signature = &sig | |
| } | |
| if config.UI != nil { | |
| ui := *config.UI | |
| clone.UI = &ui | |
| } | |
| // Set defaults. | |
| if clone.Source == "" { | |
| clone.Source = SourceGitHub | |
| } | |
| s.lock.Lock() | |
| defer s.lock.Unlock() | |
| s.config = clone |
🤖 Prompt for AI Agents
In v3/pkg/services/selfupdate/selfupdate.go around lines 249 to 261, the current
clone uses a shallow copy (*clone = *config) which leaves nested pointer fields
(e.g., config.Signature, config.UI or any other pointer members) shared with the
caller; update Configure to perform a deep clone instead: create a new Config,
copy primitive fields as before, and for each pointer field allocate new
instances and copy their values (or call a Config.Clone method that deep-copies
all nested structs), ensuring s.config receives independent copies before
assigning under the lock.
Deploying wails with
|
| Latest commit: |
053bd7f
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://9716f314.wails.pages.dev |
| Branch Preview URL: | https://feature-selfupdate-service.wails.pages.dev |
- Fix JavaScript API in docs and example to use ES module imports instead of non-existent wails.Service() function - Use correct binding path with pkg segment - Store configuration errors from NewWithConfig for proper error reporting in ServiceStartup - Deep clone nested pointer fields (Signature, UI) in Configure to prevent external modifications affecting service state 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
|
I've had a look through this and like the implementation but don't believe creativeprojects/go-selfupdate is a good choice for wails applications due to needing variations of the same build (webkit2_41) with it only supporting {cmd}{goos}{goarch}{.ext}. I would also suggest the practise of directly self updating self from the binary should be avoided, if anything goes wrong (Windows doesn't support it properly to start with) users are up the creek. I would like to suggest at update, using an intermediary program the performs the replace such as this. Would you be open to an alternative implementation? |
Absolutely! |
|
@leaanthony i have just made a small push to gophorth including a full github-release example if you are still curious about alternatives for self-updating. i'm going to write tests, documentation, a tiny github site, and then park this |


Summary
Adds a new selfupdate service that enables Wails v3 applications to check for and apply updates from GitHub, GitLab, or Gitea releases.
New Files
v3/pkg/services/selfupdate/selfupdate.go- Main service implementationv3/internal/commands/selfupdate.go- CLI commands for keygen/sign/verifyv3/examples/selfupdate/- Example application with themed UIdocs/src/content/docs/guides/selfupdate.mdx- Comprehensive documentationUsage Example
Closes #1178
Test plan
wails3 tool selfupdate keygen -o ./keyswails3 tool selfupdate sign -key ./keys/update_private.pem -file ./binarywails3 tool selfupdate verify -key ./keys/update_public.pem -file ./binary🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
✏️ Tip: You can customize this high-level summary in your review settings.