From 0e25b8de03c48b04ee461be22a24bd7ec0de0706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BCger?= Date: Tue, 8 Jul 2025 21:05:25 +0200 Subject: [PATCH] feat: update release workflow and enhance version check functionality --- .github/workflows/release.yml | 10 ++--- README.md | 24 ++++++++---- main.go | 73 +++++++++++++++++++++++++++++------ 3 files changed, 82 insertions(+), 25 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8149c4e..08dcf92 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,11 +18,10 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: '^1.24.2' + go-version: 'stable' - name: Update version in main.go to ${{ github.event.release.tag_name }} run: | - # Use sed to update the currentVersion variable in main.go sed -i 's/var currentVersion = ".*"/var currentVersion = "${{ github.event.release.tag_name }}"/' ./main.go - name: Commit main.go with updated version @@ -33,8 +32,9 @@ jobs: git commit -m "Updated ./main.go" git push origin HEAD:${{ github.event.release.target_commitish }} - - name: Install rsrc (for embedding icon) - run: go install github.com/akavel/rsrc@latest + - name: Install rsrc to embed icon into application + run: | + go install github.com/akavel/rsrc@latest - name: Generate Windows resource file with icon run: | @@ -44,7 +44,7 @@ jobs: run: | GOOS=windows GOARCH=amd64 go build -ldflags="-H windowsgui" -o AutoExitNode.exe - - name: Upload AutoExitNode.exe to release + - name: Upload AutoExitNode.exe to the release uses: svenstaro/upload-release-action@2.11.1 with: repo_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index b85f6b7..611030b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # AutoExitNode +[![GitHub Release][releases-shield]][releases] +[![GitHub Downloads][downloads-shield]][downloads] +[![License][license-shield]][license] +[![BuyMeCoffee][buymecoffeebadge]][buymecoffee] + +![Icon](icon_active.png) + **AutoExitNode** is a Windows system tray application that automatically manages your Tailscale exit node based on your network (WiFi SSID or cellular connection). ## Features @@ -49,14 +56,6 @@ - Check for update (checks GitHub for new version) - Quit (exit the app) -## Development & Testing - -- Run `go build` to build the program. -- Unit tests are in `main_test.go`: - ``` - go test - ``` - ## Requirements - Windows 10 or newer @@ -76,3 +75,12 @@ The app automatically checks for new versions on GitHub and shows a Windows noti **Note:** This project is not officially affiliated with Tailscale. + +[releases-shield]: https://img.shields.io/github/v/release/woopstar/AutoExitNode?style=for-the-badge +[releases]: https://github.com/woopstar/AutoExitNode/releases +[downloads-shield]: https://img.shields.io/github/downloads/woopstar/AutoExitNode/total.svg?style=for-the-badge +[downloads]: https://github.com/woopstar/AutoExitNode/releases +[license-shield]: https://img.shields.io/github/license/woopstar/AutoExitNode?style=for-the-badge +[license]: https://github.com/woopstar/AutoExitNode/blob/main/LICENSE +[buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-FFDD00.svg?style=for-the-badge&logo=buymeacoffee +[buymecoffee]: https://www.buymeacoffee.com/woopstar diff --git a/main.go b/main.go index 46e7169..e3ba069 100644 --- a/main.go +++ b/main.go @@ -17,7 +17,6 @@ import ( "github.com/getlantern/systray" "github.com/go-ole/go-ole" "github.com/go-ole/go-ole/oleutil" - "github.com/go-toast/toast" ) //go:embed icon_active.ico @@ -41,6 +40,9 @@ var tailscaleAvailable = true var currentVersion = "v1.2.1" // Default version, will be overwritten by config if present +var latestVersion string +var latestVersionURL string + func main() { loadConfig() tailscaleAvailable = checkTailscaleExists() @@ -102,7 +104,11 @@ func onReady() { mRunAtStartup.Check() } case <-mCheckUpdate.ClickedCh: - go checkForUpdate() + go func() { + checkForUpdate(func(ver, url string) { + updateVersionMenu(mVersion, ver, url) + }) + }() case <-mQuit.ClickedCh: systray.Quit() return @@ -117,8 +123,31 @@ func onReady() { } }() - // Automatic update check at startup (can be removed if not desired) - go checkForUpdate() + // Periodically check for updates in the background + go func() { + for { + checkForUpdate(func(ver, url string) { + updateVersionMenu(mVersion, ver, url) + }) + time.Sleep(15 * time.Minute) + } + }() + + // Initial update check at startup + go checkForUpdate(func(ver, url string) { + updateVersionMenu(mVersion, ver, url) + }) +} + +// Update the version menu item if a new version is available +func updateVersionMenu(mVersion *systray.MenuItem, ver, url string) { + if ver != "" && ver != currentVersion { + mVersion.SetTitle(fmt.Sprintf("Version: %s (Update: %s)", currentVersion, ver)) + mVersion.SetTooltip(fmt.Sprintf("New version available: %s\n%s", ver, url)) + } else { + mVersion.SetTitle(fmt.Sprintf("Version: %s", currentVersion)) + mVersion.SetTooltip("Current version") + } } func checkAndApply(mStatus *systray.MenuItem) { @@ -317,41 +346,61 @@ func removeStartupShortcut() { } } -func checkForUpdate() { - const repo = "woopstar/AutoExitNode" // Set to your repo +func checkForUpdate(cb func(version, url string)) { + const repo = "woopstar/AutoExitNode" // Ensure this matches your GitHub repo (owner/repo) url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repo) req, err := http.NewRequest("GET", url, nil) if err != nil { + fmt.Println("checkForUpdate: failed to create request:", err) return } req.Header.Set("Accept", "application/vnd.github+json") client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Do(req) if err != nil { + fmt.Println("checkForUpdate: HTTP request failed:", err) return } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + fmt.Printf("checkForUpdate: unexpected status code: %d\n", resp.StatusCode) + return + } + var data struct { TagName string `json:"tag_name"` HTMLURL string `json:"html_url"` } if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + fmt.Println("checkForUpdate: failed to decode JSON:", err) return } if data.TagName != "" && data.TagName != currentVersion { + latestVersion = data.TagName + latestVersionURL = data.HTMLURL showWindowsNotification("Update available!", fmt.Sprintf("New version: %s\nSee: %s", data.TagName, data.HTMLURL)) + if cb != nil { + cb(data.TagName, data.HTMLURL) + } + } else if cb != nil { + cb("", "") } } // showWindowsNotification displays a notification on Windows using go-toast. func showWindowsNotification(title, message string) { - (&toast.Notification{ - AppID: "AutoExitNode", - Title: title, - Message: message, - Icon: "icon_active.ico", - }).Push() + // Show a simple popup using Windows MessageBox via PowerShell for maximum compatibility. + cmd := exec.Command("powershell", "-Command", fmt.Sprintf(`Add-Type -AssemblyName PresentationFramework;[System.Windows.MessageBox]::Show('%s', '%s')`, escapeForPowerShell(message), escapeForPowerShell(title))) + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + if err := cmd.Run(); err != nil { + fmt.Println("showWindowsNotification: failed to show popup:", err) + } +} + +// escapeForPowerShell escapes single quotes for PowerShell string literals. +func escapeForPowerShell(s string) string { + return strings.ReplaceAll(s, "'", "''") }