refactor: move updater to dedicated core/go-update repo
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
338a0a4c5e
commit
b95d465499
44 changed files with 0 additions and 11948 deletions
18
cmd/updater/.gitignore
vendored
18
cmd/updater/.gitignore
vendored
|
|
@ -1,18 +0,0 @@
|
|||
# Go
|
||||
updater
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.test
|
||||
*.out
|
||||
*.prof
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
EUROPEAN UNION PUBLIC LICENCE v. 1.2
|
||||
EUPL © the European Union 2007, 2016
|
||||
|
||||
This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
|
||||
below) which is provided under the terms of this Licence. Any use of the Work,
|
||||
other than as authorised under this Licence is prohibited (to the extent such
|
||||
use is covered by a right of the copyright holder of the Work).
|
||||
|
||||
The Work is provided under the terms of this Licence when the Licensor (as
|
||||
defined below) has placed the following notice immediately following the
|
||||
copyright notice for the Work:
|
||||
|
||||
Licensed under the EUPL
|
||||
|
||||
or has expressed by any other means his willingness to license under the EUPL.
|
||||
|
||||
1. Definitions
|
||||
|
||||
In this Licence, the following terms have the following meaning:
|
||||
|
||||
- ‘The Licence’: this Licence.
|
||||
|
||||
- ‘The Original Work’: the work or software distributed or communicated by the
|
||||
Licensor under this Licence, available as Source Code and also as Executable
|
||||
Code as the case may be.
|
||||
|
||||
- ‘Derivative Works’: the works or software that could be created by the
|
||||
Licensee, based upon the Original Work or modifications thereof. This Licence
|
||||
does not define the extent of modification or dependence on the Original Work
|
||||
required in order to classify a work as a Derivative Work; this extent is
|
||||
determined by copyright law applicable in the country mentioned in Article 15.
|
||||
|
||||
- ‘The Work’: the Original Work or its Derivative Works.
|
||||
|
||||
- ‘The Source Code’: the human-readable form of the Work which is the most
|
||||
convenient for people to study and modify.
|
||||
|
||||
- ‘The Executable Code’: any code which has generally been compiled and which is
|
||||
meant to be interpreted by a computer as a program.
|
||||
|
||||
- ‘The Licensor’: the natural or legal person that distributes or communicates
|
||||
the Work under the Licence.
|
||||
|
||||
- ‘Contributor(s)’: any natural or legal person who modifies the Work under the
|
||||
Licence, or otherwise contributes to the creation of a Derivative Work.
|
||||
|
||||
- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
|
||||
the Work under the terms of the Licence.
|
||||
|
||||
- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
|
||||
renting, distributing, communicating, transmitting, or otherwise making
|
||||
available, online or offline, copies of the Work or providing access to its
|
||||
essential functionalities at the disposal of any other natural or legal
|
||||
person.
|
||||
|
||||
2. Scope of the rights granted by the Licence
|
||||
|
||||
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
|
||||
sublicensable licence to do the following, for the duration of copyright vested
|
||||
in the Original Work:
|
||||
|
||||
- use the Work in any circumstance and for all usage,
|
||||
- reproduce the Work,
|
||||
- modify the Work, and make Derivative Works based upon the Work,
|
||||
- communicate to the public, including the right to make available or display
|
||||
the Work or copies thereof to the public and perform publicly, as the case may
|
||||
be, the Work,
|
||||
- distribute the Work or copies thereof,
|
||||
- lend and rent the Work or copies thereof,
|
||||
- sublicense rights in the Work or copies thereof.
|
||||
|
||||
Those rights can be exercised on any media, supports and formats, whether now
|
||||
known or later invented, as far as the applicable law permits so.
|
||||
|
||||
In the countries where moral rights apply, the Licensor waives his right to
|
||||
exercise his moral right to the extent allowed by law in order to make effective
|
||||
the licence of the economic rights here above listed.
|
||||
|
||||
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
|
||||
any patents held by the Licensor, to the extent necessary to make use of the
|
||||
rights granted on the Work under this Licence.
|
||||
|
||||
3. Communication of the Source Code
|
||||
|
||||
The Licensor may provide the Work either in its Source Code form, or as
|
||||
Executable Code. If the Work is provided as Executable Code, the Licensor
|
||||
provides in addition a machine-readable copy of the Source Code of the Work
|
||||
along with each copy of the Work that the Licensor distributes or indicates, in
|
||||
a notice following the copyright notice attached to the Work, a repository where
|
||||
the Source Code is easily and freely accessible for as long as the Licensor
|
||||
continues to distribute or communicate the Work.
|
||||
|
||||
4. Limitations on copyright
|
||||
|
||||
Nothing in this Licence is intended to deprive the Licensee of the benefits from
|
||||
any exception or limitation to the exclusive rights of the rights owners in the
|
||||
Work, of the exhaustion of those rights or of other applicable limitations
|
||||
thereto.
|
||||
|
||||
5. Obligations of the Licensee
|
||||
|
||||
The grant of the rights mentioned above is subject to some restrictions and
|
||||
obligations imposed on the Licensee. Those obligations are the following:
|
||||
|
||||
Attribution right: The Licensee shall keep intact all copyright, patent or
|
||||
trademarks notices and all notices that refer to the Licence and to the
|
||||
disclaimer of warranties. The Licensee must include a copy of such notices and a
|
||||
copy of the Licence with every copy of the Work he/she distributes or
|
||||
communicates. The Licensee must cause any Derivative Work to carry prominent
|
||||
notices stating that the Work has been modified and the date of modification.
|
||||
|
||||
Copyleft clause: If the Licensee distributes or communicates copies of the
|
||||
Original Works or Derivative Works, this Distribution or Communication will be
|
||||
done under the terms of this Licence or of a later version of this Licence
|
||||
unless the Original Work is expressly distributed only under this version of the
|
||||
Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
|
||||
(becoming Licensor) cannot offer or impose any additional terms or conditions on
|
||||
the Work or Derivative Work that alter or restrict the terms of the Licence.
|
||||
|
||||
Compatibility clause: If the Licensee Distributes or Communicates Derivative
|
||||
Works or copies thereof based upon both the Work and another work licensed under
|
||||
a Compatible Licence, this Distribution or Communication can be done under the
|
||||
terms of this Compatible Licence. For the sake of this clause, ‘Compatible
|
||||
Licence’ refers to the licences listed in the appendix attached to this Licence.
|
||||
Should the Licensee's obligations under the Compatible Licence conflict with
|
||||
his/her obligations under this Licence, the obligations of the Compatible
|
||||
Licence shall prevail.
|
||||
|
||||
Provision of Source Code: When distributing or communicating copies of the Work,
|
||||
the Licensee will provide a machine-readable copy of the Source Code or indicate
|
||||
a repository where this Source will be easily and freely available for as long
|
||||
as the Licensee continues to distribute or communicate the Work.
|
||||
|
||||
Legal Protection: This Licence does not grant permission to use the trade names,
|
||||
trademarks, service marks, or names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the copyright notice.
|
||||
|
||||
6. Chain of Authorship
|
||||
|
||||
The original Licensor warrants that the copyright in the Original Work granted
|
||||
hereunder is owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each Contributor warrants that the copyright in the modifications he/she brings
|
||||
to the Work are owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each time You accept the Licence, the original Licensor and subsequent
|
||||
Contributors grant You a licence to their contributions to the Work, under the
|
||||
terms of this Licence.
|
||||
|
||||
7. Disclaimer of Warranty
|
||||
|
||||
The Work is a work in progress, which is continuously improved by numerous
|
||||
Contributors. It is not a finished work and may therefore contain defects or
|
||||
‘bugs’ inherent to this type of development.
|
||||
|
||||
For the above reason, the Work is provided under the Licence on an ‘as is’ basis
|
||||
and without warranties of any kind concerning the Work, including without
|
||||
limitation merchantability, fitness for a particular purpose, absence of defects
|
||||
or errors, accuracy, non-infringement of intellectual property rights other than
|
||||
copyright as stated in Article 6 of this Licence.
|
||||
|
||||
This disclaimer of warranty is an essential part of the Licence and a condition
|
||||
for the grant of any rights to the Work.
|
||||
|
||||
8. Disclaimer of Liability
|
||||
|
||||
Except in the cases of wilful misconduct or damages directly caused to natural
|
||||
persons, the Licensor will in no event be liable for any direct or indirect,
|
||||
material or moral, damages of any kind, arising out of the Licence or of the use
|
||||
of the Work, including without limitation, damages for loss of goodwill, work
|
||||
stoppage, computer failure or malfunction, loss of data or any commercial
|
||||
damage, even if the Licensor has been advised of the possibility of such damage.
|
||||
However, the Licensor will be liable under statutory product liability laws as
|
||||
far such laws apply to the Work.
|
||||
|
||||
9. Additional agreements
|
||||
|
||||
While distributing the Work, You may choose to conclude an additional agreement,
|
||||
defining obligations or services consistent with this Licence. However, if
|
||||
accepting obligations, You may act only on your own behalf and on your sole
|
||||
responsibility, not on behalf of the original Licensor or any other Contributor,
|
||||
and only if You agree to indemnify, defend, and hold each Contributor harmless
|
||||
for any liability incurred by, or claims asserted against such Contributor by
|
||||
the fact You have accepted any warranty or additional liability.
|
||||
|
||||
10. Acceptance of the Licence
|
||||
|
||||
The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
|
||||
placed under the bottom of a window displaying the text of this Licence or by
|
||||
affirming consent in any other similar way, in accordance with the rules of
|
||||
applicable law. Clicking on that icon indicates your clear and irrevocable
|
||||
acceptance of this Licence and all of its terms and conditions.
|
||||
|
||||
Similarly, you irrevocably accept this Licence and all of its terms and
|
||||
conditions by exercising any rights granted to You by Article 2 of this Licence,
|
||||
such as the use of the Work, the creation by You of a Derivative Work or the
|
||||
Distribution or Communication by You of the Work or copies thereof.
|
||||
|
||||
11. Information to the public
|
||||
|
||||
In case of any Distribution or Communication of the Work by means of electronic
|
||||
communication by You (for example, by offering to download the Work from a
|
||||
remote location) the distribution channel or media (for example, a website) must
|
||||
at least provide to the public the information requested by the applicable law
|
||||
regarding the Licensor, the Licence and the way it may be accessible, concluded,
|
||||
stored and reproduced by the Licensee.
|
||||
|
||||
12. Termination of the Licence
|
||||
|
||||
The Licence and the rights granted hereunder will terminate automatically upon
|
||||
any breach by the Licensee of the terms of the Licence.
|
||||
|
||||
Such a termination will not terminate the licences of any person who has
|
||||
received the Work from the Licensee under the Licence, provided such persons
|
||||
remain in full compliance with the Licence.
|
||||
|
||||
13. Miscellaneous
|
||||
|
||||
Without prejudice of Article 9 above, the Licence represents the complete
|
||||
agreement between the Parties as to the Work.
|
||||
|
||||
If any provision of the Licence is invalid or unenforceable under applicable
|
||||
law, this will not affect the validity or enforceability of the Licence as a
|
||||
whole. Such provision will be construed or reformed so as necessary to make it
|
||||
valid and enforceable.
|
||||
|
||||
The European Commission may publish other linguistic versions or new versions of
|
||||
this Licence or updated versions of the Appendix, so far this is required and
|
||||
reasonable, without reducing the scope of the rights granted by the Licence. New
|
||||
versions of the Licence will be published with a unique version number.
|
||||
|
||||
All linguistic versions of this Licence, approved by the European Commission,
|
||||
have identical value. Parties can take advantage of the linguistic version of
|
||||
their choice.
|
||||
|
||||
14. Jurisdiction
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
- any litigation resulting from the interpretation of this License, arising
|
||||
between the European Union institutions, bodies, offices or agencies, as a
|
||||
Licensor, and any Licensee, will be subject to the jurisdiction of the Court
|
||||
of Justice of the European Union, as laid down in article 272 of the Treaty on
|
||||
the Functioning of the European Union,
|
||||
|
||||
- any litigation arising between other parties and resulting from the
|
||||
interpretation of this License, will be subject to the exclusive jurisdiction
|
||||
of the competent court where the Licensor resides or conducts its primary
|
||||
business.
|
||||
|
||||
15. Applicable Law
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
- this Licence shall be governed by the law of the European Union Member State
|
||||
where the Licensor has his seat, resides or has his registered office,
|
||||
|
||||
- this licence shall be governed by Belgian law if the Licensor has no seat,
|
||||
residence or registered office inside a European Union Member State.
|
||||
|
||||
Appendix
|
||||
|
||||
‘Compatible Licences’ according to Article 5 EUPL are:
|
||||
|
||||
- GNU General Public License (GPL) v. 2, v. 3
|
||||
- GNU Affero General Public License (AGPL) v. 3
|
||||
- Open Software License (OSL) v. 2.1, v. 3.0
|
||||
- Eclipse Public License (EPL) v. 1.0
|
||||
- CeCILL v. 2.0, v. 2.1
|
||||
- Mozilla Public Licence (MPL) v. 2
|
||||
- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
|
||||
- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
|
||||
works other than software
|
||||
- European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||
- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
|
||||
Reciprocity (LiLiQ-R+).
|
||||
|
||||
The European Commission may update this Appendix to later versions of the above
|
||||
licences without producing a new version of the EUPL, as long as they provide
|
||||
the rights granted in Article 2 of this Licence and protect the covered Source
|
||||
Code from exclusive appropriation.
|
||||
|
||||
All other changes or additions to this Appendix require the production of a new
|
||||
EUPL version.
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
.PHONY: build dev release-local test coverage
|
||||
|
||||
BINARY_NAME=updater
|
||||
CMD_PATH=./cmd/updater
|
||||
|
||||
# Default LDFLAGS to empty
|
||||
LDFLAGS = ""
|
||||
|
||||
# If VERSION is set, override LDFLAGS
|
||||
ifdef VERSION
|
||||
LDFLAGS = -ldflags "-X 'github.com/snider/updater.Version=$(VERSION)'"
|
||||
endif
|
||||
|
||||
.PHONY: generate
|
||||
generate:
|
||||
@echo "Generating code..."
|
||||
@go generate ./...
|
||||
|
||||
build: generate
|
||||
@echo "Building $(BINARY_NAME)..."
|
||||
@cd $(CMD_PATH) && go build $(LDFLAGS)
|
||||
|
||||
dev: build
|
||||
@echo "Running $(BINARY_NAME)..."
|
||||
@$(CMD_PATH)/$(BINARY_NAME) --check-update
|
||||
|
||||
release-local:
|
||||
@echo "Running local release with GoReleaser..."
|
||||
@~/go/bin/goreleaser release --snapshot --clean
|
||||
|
||||
test:
|
||||
@echo "Running tests..."
|
||||
@go test ./...
|
||||
|
||||
coverage:
|
||||
@echo "Generating code coverage report..."
|
||||
@go test -coverprofile=coverage.out ./...
|
||||
@echo "Coverage report generated: coverage.out"
|
||||
@echo "To view in browser: go tool cover -html=coverage.out"
|
||||
@echo "To upload to Codecov, ensure you have the Codecov CLI installed (e.g., 'go install github.com/codecov/codecov-cli@latest') and run: codecov -f coverage.out"
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
# Core Element Template
|
||||
|
||||
This repository is a template for developers to create custom HTML elements for the core web3 framework. It includes a Go backend, an Angular custom element, and a full release cycle configuration.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/your-username/core-element-template.git
|
||||
```
|
||||
|
||||
2. **Install the dependencies:**
|
||||
```bash
|
||||
cd core-element-template
|
||||
go mod tidy
|
||||
cd ui
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Run the development server:**
|
||||
```bash
|
||||
go run ./cmd/demo-cli serve
|
||||
```
|
||||
This will start the Go backend and serve the Angular custom element.
|
||||
|
||||
## Building the Custom Element
|
||||
|
||||
To build the Angular custom element, run the following command:
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
npm run build
|
||||
```
|
||||
|
||||
This will create a single JavaScript file in the `dist` directory that you can use in any HTML page.
|
||||
|
||||
## Usage
|
||||
|
||||
To use the updater library in your Go project, you can use the `UpdateService`.
|
||||
|
||||
### GitHub-based Updates
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/snider/updater"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := updater.UpdateServiceConfig{
|
||||
RepoURL: "https://github.com/owner/repo",
|
||||
Channel: "stable",
|
||||
CheckOnStartup: updater.CheckAndUpdateOnStartup,
|
||||
}
|
||||
|
||||
updateService, err := updater.NewUpdateService(config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create update service: %v", err)
|
||||
}
|
||||
|
||||
if err := updateService.Start(); err != nil {
|
||||
fmt.Printf("Update check failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Generic HTTP Updates
|
||||
|
||||
For updates from a generic HTTP server, the server should provide a `latest.json` file at the root of the `RepoURL`. The JSON file should have the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.2.3",
|
||||
"url": "https://your-server.com/path/to/release-asset"
|
||||
}
|
||||
```
|
||||
|
||||
You can then configure the `UpdateService` as follows:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/snider/updater"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := updater.UpdateServiceConfig{
|
||||
RepoURL: "https://your-server.com",
|
||||
CheckOnStartup: updater.CheckAndUpdateOnStartup,
|
||||
}
|
||||
|
||||
updateService, err := updater.NewUpdateService(config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create update service: %v", err)
|
||||
}
|
||||
|
||||
if err := updateService.Start(); err != nil {
|
||||
fmt.Printf("Update check failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the EUPL-1.2 License - see the [LICENSE](LICENSE) file for details.
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Read package.json
|
||||
data, err := os.ReadFile("package.json")
|
||||
if err != nil {
|
||||
fmt.Println("Error reading package.json, skipping version file generation.")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Parse package.json
|
||||
var pkg struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &pkg); err != nil {
|
||||
fmt.Println("Error parsing package.json, skipping version file generation.")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Create the version file
|
||||
content := fmt.Sprintf("package updater\n\n// Generated by go:generate. DO NOT EDIT.\n\nconst PkgVersion = %q\n", pkg.Version)
|
||||
err = os.WriteFile("version.go", []byte(content), 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("Error writing version file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Generated version.go with version:", pkg.Version)
|
||||
}
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Repository configuration for updates
|
||||
const (
|
||||
repoOwner = "host-uk"
|
||||
repoName = "core"
|
||||
)
|
||||
|
||||
// Command flags
|
||||
var (
|
||||
updateChannel string
|
||||
updateForce bool
|
||||
updateCheck bool
|
||||
updateWatchPID int
|
||||
)
|
||||
|
||||
func init() {
|
||||
cli.RegisterCommands(AddUpdateCommands)
|
||||
}
|
||||
|
||||
// AddUpdateCommands registers the update command and subcommands.
|
||||
func AddUpdateCommands(root *cobra.Command) {
|
||||
updateCmd := &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update core CLI to the latest version",
|
||||
Long: `Update the core CLI to the latest version from GitHub releases.
|
||||
|
||||
By default, checks the 'stable' channel for tagged releases (v*.*.*)
|
||||
Use --channel=dev for the latest development build.
|
||||
|
||||
Examples:
|
||||
core update # Update to latest stable release
|
||||
core update --check # Check for updates without applying
|
||||
core update --channel=dev # Update to latest dev build
|
||||
core update --force # Force update even if already on latest`,
|
||||
RunE: runUpdate,
|
||||
}
|
||||
|
||||
updateCmd.PersistentFlags().StringVar(&updateChannel, "channel", "stable", "Release channel: stable, beta, alpha, or dev")
|
||||
updateCmd.PersistentFlags().BoolVar(&updateForce, "force", false, "Force update even if already on latest version")
|
||||
updateCmd.Flags().BoolVar(&updateCheck, "check", false, "Only check for updates, don't apply")
|
||||
updateCmd.Flags().IntVar(&updateWatchPID, "watch-pid", 0, "Internal: watch for parent PID to die then restart")
|
||||
_ = updateCmd.Flags().MarkHidden("watch-pid")
|
||||
|
||||
updateCmd.AddCommand(&cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Check for available updates",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
updateCheck = true
|
||||
return runUpdate(cmd, args)
|
||||
},
|
||||
})
|
||||
|
||||
root.AddCommand(updateCmd)
|
||||
}
|
||||
|
||||
func runUpdate(cmd *cobra.Command, args []string) error {
|
||||
// If we're in watch mode, wait for parent to die then restart
|
||||
if updateWatchPID > 0 {
|
||||
return watchAndRestart(updateWatchPID)
|
||||
}
|
||||
|
||||
currentVersion := cli.AppVersion
|
||||
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render("Current version:"), cli.ValueStyle.Render(currentVersion))
|
||||
cli.Print("%s %s/%s\n", cli.DimStyle.Render("Platform:"), runtime.GOOS, runtime.GOARCH)
|
||||
cli.Print("%s %s\n\n", cli.DimStyle.Render("Channel:"), updateChannel)
|
||||
|
||||
// Handle dev channel specially - it's a prerelease tag, not a semver channel
|
||||
if updateChannel == "dev" {
|
||||
return handleDevUpdate(currentVersion)
|
||||
}
|
||||
|
||||
// Check for newer version
|
||||
release, updateAvailable, err := CheckForNewerVersion(repoOwner, repoName, updateChannel, true)
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "failed to check for updates")
|
||||
}
|
||||
|
||||
if release == nil {
|
||||
cli.Print("%s No releases found in %s channel\n", cli.WarningStyle.Render("!"), updateChannel)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !updateAvailable && !updateForce {
|
||||
cli.Print("%s Already on latest version (%s)\n",
|
||||
cli.SuccessStyle.Render(cli.Glyph(":check:")),
|
||||
release.TagName)
|
||||
return nil
|
||||
}
|
||||
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render("Latest version:"), cli.SuccessStyle.Render(release.TagName))
|
||||
|
||||
if updateCheck {
|
||||
if updateAvailable {
|
||||
cli.Print("\n%s Update available: %s → %s\n",
|
||||
cli.WarningStyle.Render("!"),
|
||||
currentVersion,
|
||||
release.TagName)
|
||||
cli.Print("Run %s to update\n", cli.ValueStyle.Render("core update"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Spawn watcher before applying update
|
||||
if err := spawnWatcher(); err != nil {
|
||||
// If watcher fails, continue anyway - update will still work
|
||||
cli.Print("%s Could not spawn restart watcher: %v\n", cli.DimStyle.Render("!"), err)
|
||||
}
|
||||
|
||||
// Apply update
|
||||
cli.Print("\n%s Downloading update...\n", cli.DimStyle.Render("→"))
|
||||
|
||||
downloadURL, err := GetDownloadURL(release, "")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "failed to get download URL")
|
||||
}
|
||||
|
||||
if err := DoUpdate(downloadURL); err != nil {
|
||||
return cli.Wrap(err, "failed to apply update")
|
||||
}
|
||||
|
||||
cli.Print("%s Updated to %s\n", cli.SuccessStyle.Render(cli.Glyph(":check:")), release.TagName)
|
||||
cli.Print("%s Restarting...\n", cli.DimStyle.Render("→"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleDevUpdate handles updates from the dev release (rolling prerelease)
|
||||
func handleDevUpdate(currentVersion string) error {
|
||||
client := NewGithubClient()
|
||||
|
||||
// Fetch the dev release directly by tag
|
||||
release, err := client.GetLatestRelease(context.TODO(), repoOwner, repoName, "beta")
|
||||
if err != nil {
|
||||
// Try fetching the "dev" tag directly
|
||||
return handleDevTagUpdate(currentVersion)
|
||||
}
|
||||
|
||||
if release == nil {
|
||||
return handleDevTagUpdate(currentVersion)
|
||||
}
|
||||
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render("Latest dev:"), cli.ValueStyle.Render(release.TagName))
|
||||
|
||||
if updateCheck {
|
||||
cli.Print("\nRun %s to update\n", cli.ValueStyle.Render("core update --channel=dev"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Spawn watcher before applying update
|
||||
if err := spawnWatcher(); err != nil {
|
||||
cli.Print("%s Could not spawn restart watcher: %v\n", cli.DimStyle.Render("!"), err)
|
||||
}
|
||||
|
||||
cli.Print("\n%s Downloading update...\n", cli.DimStyle.Render("→"))
|
||||
|
||||
downloadURL, err := GetDownloadURL(release, "")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "failed to get download URL")
|
||||
}
|
||||
|
||||
if err := DoUpdate(downloadURL); err != nil {
|
||||
return cli.Wrap(err, "failed to apply update")
|
||||
}
|
||||
|
||||
cli.Print("%s Updated to %s\n", cli.SuccessStyle.Render(cli.Glyph(":check:")), release.TagName)
|
||||
cli.Print("%s Restarting...\n", cli.DimStyle.Render("→"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleDevTagUpdate fetches the dev release using the direct tag
|
||||
func handleDevTagUpdate(currentVersion string) error {
|
||||
// Construct download URL directly for dev release
|
||||
downloadURL := fmt.Sprintf(
|
||||
"https://github.com/%s/%s/releases/download/dev/core-%s-%s",
|
||||
repoOwner, repoName, runtime.GOOS, runtime.GOARCH,
|
||||
)
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
downloadURL += ".exe"
|
||||
}
|
||||
|
||||
cli.Print("%s dev (rolling)\n", cli.DimStyle.Render("Latest:"))
|
||||
|
||||
if updateCheck {
|
||||
cli.Print("\nRun %s to update\n", cli.ValueStyle.Render("core update --channel=dev"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Spawn watcher before applying update
|
||||
if err := spawnWatcher(); err != nil {
|
||||
cli.Print("%s Could not spawn restart watcher: %v\n", cli.DimStyle.Render("!"), err)
|
||||
}
|
||||
|
||||
cli.Print("\n%s Downloading from dev release...\n", cli.DimStyle.Render("→"))
|
||||
|
||||
if err := DoUpdate(downloadURL); err != nil {
|
||||
return cli.Wrap(err, "failed to apply update")
|
||||
}
|
||||
|
||||
cli.Print("%s Updated to latest dev build\n", cli.SuccessStyle.Render(cli.Glyph(":check:")))
|
||||
cli.Print("%s Restarting...\n", cli.DimStyle.Render("→"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
//go:build !windows
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// spawnWatcher spawns a background process that watches for the current process
|
||||
// to exit, then restarts the binary with --version to confirm the update.
|
||||
func spawnWatcher() error {
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pid := os.Getpid()
|
||||
|
||||
// Spawn: core update --watch-pid=<pid>
|
||||
cmd := exec.Command(executable, "update", "--watch-pid", strconv.Itoa(pid))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// Detach from parent process group
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
// watchAndRestart waits for the given PID to exit, then restarts the binary.
|
||||
func watchAndRestart(pid int) error {
|
||||
// Wait for the parent process to die
|
||||
for isProcessRunning(pid) {
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Small delay to ensure file handle is released
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Get executable path
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use exec to replace this process
|
||||
return syscall.Exec(executable, []string{executable, "--version"}, os.Environ())
|
||||
}
|
||||
|
||||
// isProcessRunning checks if a process with the given PID is still running.
|
||||
func isProcessRunning(pid int) bool {
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// On Unix, FindProcess always succeeds, so we need to send signal 0
|
||||
// to check if the process actually exists
|
||||
err = process.Signal(syscall.Signal(0))
|
||||
return err == nil
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
//go:build windows
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// spawnWatcher spawns a background process that watches for the current process
|
||||
// to exit, then restarts the binary with --version to confirm the update.
|
||||
func spawnWatcher() error {
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pid := os.Getpid()
|
||||
|
||||
// Spawn: core update --watch-pid=<pid>
|
||||
cmd := exec.Command(executable, "update", "--watch-pid", strconv.Itoa(pid))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// On Windows, use CREATE_NEW_PROCESS_GROUP to detach
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
||||
}
|
||||
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
// watchAndRestart waits for the given PID to exit, then restarts the binary.
|
||||
func watchAndRestart(pid int) error {
|
||||
// Wait for the parent process to die
|
||||
for {
|
||||
if !isProcessRunning(pid) {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Small delay to ensure file handle is released
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Get executable path
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// On Windows, spawn new process and exit
|
||||
cmd := exec.Command(executable, "--version")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// isProcessRunning checks if a process with the given PID is still running.
|
||||
func isProcessRunning(pid int) bool {
|
||||
// On Windows, try to open the process with query rights
|
||||
handle, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION, false, uint32(pid))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
syscall.CloseHandle(handle)
|
||||
return true
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# Documentation
|
||||
|
||||
Welcome to the documentation for the `updater` library. This library provides self-update functionality for Go applications, supporting both GitHub Releases and generic HTTP endpoints.
|
||||
|
||||
## Contents
|
||||
|
||||
* [Getting Started](getting-started.md): Installation and basic usage.
|
||||
* [Configuration](configuration.md): Detailed configuration options for `UpdateService` and CLI flags.
|
||||
* [Architecture](architecture.md): How the updater works, including GitHub integration and version comparison.
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
# Architecture
|
||||
|
||||
The `updater` library is designed to facilitate self-updates for Go applications by replacing the running binary with a newer version downloaded from a remote source.
|
||||
|
||||
## Update Mechanisms
|
||||
|
||||
The library supports two primary update sources:
|
||||
|
||||
1. **GitHub Releases:** Fetches releases directly from a GitHub repository.
|
||||
2. **Generic HTTP:** Fetches update information from a generic HTTP endpoint.
|
||||
|
||||
### GitHub Releases
|
||||
|
||||
When configured with a GitHub repository URL (e.g., `https://github.com/owner/repo`), the updater uses the GitHub API to find releases.
|
||||
|
||||
* **Channel Support:** You can specify a "channel" (e.g., "stable", "beta"). The updater will filter releases based on this channel.
|
||||
* Ideally, this maps to release tags or pre-release status (though the specific implementation details of how "channel" maps to GitHub release types should be verified in the code).
|
||||
* **Pull Request Updates:** The library supports updating to a specific pull request artifact, useful for testing pre-release builds.
|
||||
|
||||
### Generic HTTP
|
||||
|
||||
When configured with a generic HTTP URL, the updater expects the endpoint to return a JSON object describing the latest version.
|
||||
|
||||
**Expected JSON Format:**
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.2.3",
|
||||
"url": "https://your-server.com/path/to/release-asset"
|
||||
}
|
||||
```
|
||||
|
||||
The updater compares the `version` from the JSON with the current application version. If the remote version is newer, it downloads the binary from the `url`.
|
||||
|
||||
## Version Comparison
|
||||
|
||||
The library uses Semantic Versioning (SemVer) to compare versions.
|
||||
|
||||
* **Prefix Handling:** The `ForceSemVerPrefix` configuration option allows you to standardize version tags by enforcing a `v` prefix (e.g., `v1.0.0` vs `1.0.0`) for consistent comparison.
|
||||
* **Logic:**
|
||||
* If `Remote Version` > `Current Version`: Update available.
|
||||
* If `Remote Version` <= `Current Version`: Up to date.
|
||||
|
||||
## Self-Update Process
|
||||
|
||||
The actual update process is handled by the `minio/selfupdate` library.
|
||||
|
||||
1. **Download:** The new binary is downloaded from the source.
|
||||
2. **Verification:** (Depending on configuration/implementation) Checksums may be verified.
|
||||
3. **Apply:** The current executable file is replaced with the new binary.
|
||||
* **Windows:** The old binary is renamed (often to `.old`) before replacement to allow the write operation.
|
||||
* **Linux/macOS:** The file is unlinked and replaced.
|
||||
4. **Restart:** The application usually needs to be restarted for the changes to take effect. The `updater` library currently handles the *replacement*, but the *restart* logic is typically left to the application.
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# Configuration
|
||||
|
||||
The `updater` library is highly configurable via the `UpdateServiceConfig` struct.
|
||||
|
||||
## UpdateServiceConfig
|
||||
|
||||
When creating a new `UpdateService`, you pass a `UpdateServiceConfig` struct. Here are the available fields:
|
||||
|
||||
| Field | Type | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `RepoURL` | `string` | The URL to the repository for updates. Can be a GitHub repository URL (e.g., `https://github.com/owner/repo`) or a base URL for a generic HTTP update server. |
|
||||
| `Channel` | `string` | Specifies the release channel to track (e.g., "stable", "prerelease"). This is **only used for GitHub-based updates**. |
|
||||
| `CheckOnStartup` | `StartupCheckMode` | Determines the behavior when the service starts. See [Startup Modes](#startup-modes) below. |
|
||||
| `ForceSemVerPrefix` | `bool` | Toggles whether to enforce a 'v' prefix on version tags for display and comparison. If `true`, a 'v' prefix is added if missing. |
|
||||
| `ReleaseURLFormat` | `string` | A template for constructing the download URL for a release asset. The placeholder `{tag}` will be replaced with the release tag. |
|
||||
|
||||
### Startup Modes
|
||||
|
||||
The `CheckOnStartup` field can take one of the following values:
|
||||
|
||||
* `updater.NoCheck`: Disables any checks on startup.
|
||||
* `updater.CheckOnStartup`: Checks for updates on startup but does not apply them.
|
||||
* `updater.CheckAndUpdateOnStartup`: Checks for and applies updates on startup.
|
||||
|
||||
## CLI Flags
|
||||
|
||||
If you are using the example CLI provided in `cmd/updater`, the following flags are available:
|
||||
|
||||
* `--check-update`: Check for new updates without applying them.
|
||||
* `--do-update`: Perform an update if available.
|
||||
* `--channel`: Set the update channel (e.g., stable, beta, alpha). If not set, it's determined from the current version tag.
|
||||
* `--force-semver-prefix`: Force 'v' prefix on semver tags (default `true`).
|
||||
* `--release-url-format`: A URL format for release assets.
|
||||
* `--pull-request`: Update to a specific pull request (integer ID).
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
# Getting Started
|
||||
|
||||
This guide will help you integrate the `updater` library into your Go application.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the library, run:
|
||||
|
||||
```bash
|
||||
go get github.com/snider/updater
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
The `updater` library provides an `UpdateService` that simplifies the process of checking for and applying updates.
|
||||
|
||||
### GitHub-based Updates
|
||||
|
||||
If you are hosting your releases on GitHub, you can configure the service to check your repository.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/snider/updater"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Configure the update service
|
||||
config := updater.UpdateServiceConfig{
|
||||
RepoURL: "https://github.com/your-username/your-repo",
|
||||
Channel: "stable", // or "beta", "alpha", etc.
|
||||
CheckOnStartup: updater.CheckAndUpdateOnStartup,
|
||||
}
|
||||
|
||||
// Create the service
|
||||
updateService, err := updater.NewUpdateService(config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create update service: %v", err)
|
||||
}
|
||||
|
||||
// Start the service (checks for updates and applies them if configured)
|
||||
if err := updateService.Start(); err != nil {
|
||||
fmt.Printf("Update check/apply failed: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("Update check completed.")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Generic HTTP Updates
|
||||
|
||||
If you are hosting your releases on a generic HTTP server, the server must provide a way to check for the latest version.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/snider/updater"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := updater.UpdateServiceConfig{
|
||||
RepoURL: "https://your-server.com/updates",
|
||||
CheckOnStartup: updater.CheckOnStartup, // Check only, don't apply automatically
|
||||
}
|
||||
|
||||
updateService, err := updater.NewUpdateService(config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create update service: %v", err)
|
||||
}
|
||||
|
||||
if err := updateService.Start(); err != nil {
|
||||
fmt.Printf("Update check failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For Generic HTTP updates, the endpoint is expected to return a JSON object with `version` and `url` fields. See [Architecture](architecture.md) for more details.
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
package updater
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// GenericUpdateInfo holds the information from a latest.json file.
|
||||
// This file is expected to be at the root of a generic HTTP update server.
|
||||
type GenericUpdateInfo struct {
|
||||
Version string `json:"version"` // The version number of the update.
|
||||
URL string `json:"url"` // The URL to download the update from.
|
||||
}
|
||||
|
||||
// GetLatestUpdateFromURL fetches and parses a latest.json file from a base URL.
|
||||
// The server at the baseURL should host a 'latest.json' file that contains
|
||||
// the version and download URL for the latest update.
|
||||
//
|
||||
// Example of latest.json:
|
||||
//
|
||||
// {
|
||||
// "version": "1.2.3",
|
||||
// "url": "https://your-server.com/path/to/release-asset"
|
||||
// }
|
||||
func GetLatestUpdateFromURL(baseURL string) (*GenericUpdateInfo, error) {
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid base URL: %w", err)
|
||||
}
|
||||
// Append latest.json to the path
|
||||
u.Path += "/latest.json"
|
||||
|
||||
resp, err := http.Get(u.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch latest.json: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to fetch latest.json: status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var info GenericUpdateInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse latest.json: %w", err)
|
||||
}
|
||||
|
||||
if info.Version == "" || info.URL == "" {
|
||||
return nil, fmt.Errorf("invalid latest.json content: version or url is missing")
|
||||
}
|
||||
|
||||
return &info, nil
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
package updater
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetLatestUpdateFromURL(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
handler http.HandlerFunc
|
||||
expectError bool
|
||||
expectedVersion string
|
||||
expectedURL string
|
||||
}{
|
||||
{
|
||||
name: "Valid latest.json",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprintln(w, `{"version": "v1.1.0", "url": "http://example.com/release.zip"}`)
|
||||
},
|
||||
expectedVersion: "v1.1.0",
|
||||
expectedURL: "http://example.com/release.zip",
|
||||
},
|
||||
{
|
||||
name: "Invalid JSON",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprintln(w, `{"version": "v1.1.0", "url": "http://example.com/release.zip"`) // Missing closing brace
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "Missing version",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprintln(w, `{"url": "http://example.com/release.zip"}`)
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "Missing URL",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprintln(w, `{"version": "v1.1.0"}`)
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "Server error",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
server := httptest.NewServer(tc.handler)
|
||||
defer server.Close()
|
||||
|
||||
info, err := GetLatestUpdateFromURL(server.URL)
|
||||
|
||||
if (err != nil) != tc.expectError {
|
||||
t.Errorf("Expected error: %v, got: %v", tc.expectError, err)
|
||||
}
|
||||
|
||||
if !tc.expectError {
|
||||
if info.Version != tc.expectedVersion {
|
||||
t.Errorf("Expected version: %s, got: %s", tc.expectedVersion, info.Version)
|
||||
}
|
||||
if info.URL != tc.expectedURL {
|
||||
t.Errorf("Expected URL: %s, got: %s", tc.expectedURL, info.URL)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,302 +0,0 @@
|
|||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Repo represents a repository from the GitHub API.
|
||||
type Repo struct {
|
||||
CloneURL string `json:"clone_url"` // The URL to clone the repository.
|
||||
}
|
||||
|
||||
// ReleaseAsset represents a single asset from a GitHub release.
|
||||
type ReleaseAsset struct {
|
||||
Name string `json:"name"` // The name of the asset.
|
||||
DownloadURL string `json:"browser_download_url"` // The URL to download the asset.
|
||||
}
|
||||
|
||||
// Release represents a GitHub release.
|
||||
type Release struct {
|
||||
TagName string `json:"tag_name"` // The name of the tag for the release.
|
||||
PreRelease bool `json:"prerelease"` // Indicates if the release is a pre-release.
|
||||
Assets []ReleaseAsset `json:"assets"` // A list of assets associated with the release.
|
||||
}
|
||||
|
||||
// GithubClient defines the interface for interacting with the GitHub API.
|
||||
// This allows for mocking the client in tests.
|
||||
type GithubClient interface {
|
||||
// GetPublicRepos fetches the public repositories for a user or organization.
|
||||
GetPublicRepos(ctx context.Context, userOrOrg string) ([]string, error)
|
||||
// GetLatestRelease fetches the latest release for a given repository and channel.
|
||||
GetLatestRelease(ctx context.Context, owner, repo, channel string) (*Release, error)
|
||||
// GetReleaseByPullRequest fetches a release associated with a specific pull request number.
|
||||
GetReleaseByPullRequest(ctx context.Context, owner, repo string, prNumber int) (*Release, error)
|
||||
}
|
||||
|
||||
type githubClient struct{}
|
||||
|
||||
// NewAuthenticatedClient creates a new HTTP client that authenticates with the GitHub API.
|
||||
// It uses the GITHUB_TOKEN environment variable for authentication.
|
||||
// If the token is not set, it returns the default HTTP client.
|
||||
var NewAuthenticatedClient = func(ctx context.Context) *http.Client {
|
||||
token := os.Getenv("GITHUB_TOKEN")
|
||||
if token == "" {
|
||||
return http.DefaultClient
|
||||
}
|
||||
ts := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: token},
|
||||
)
|
||||
return oauth2.NewClient(ctx, ts)
|
||||
}
|
||||
|
||||
func (g *githubClient) GetPublicRepos(ctx context.Context, userOrOrg string) ([]string, error) {
|
||||
return g.getPublicReposWithAPIURL(ctx, "https://api.github.com", userOrOrg)
|
||||
}
|
||||
|
||||
func (g *githubClient) getPublicReposWithAPIURL(ctx context.Context, apiURL, userOrOrg string) ([]string, error) {
|
||||
client := NewAuthenticatedClient(ctx)
|
||||
var allCloneURLs []string
|
||||
url := fmt.Sprintf("%s/users/%s/repos", apiURL, userOrOrg)
|
||||
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Borg-Data-Collector")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
_ = resp.Body.Close()
|
||||
// Try organization endpoint
|
||||
url = fmt.Sprintf("%s/orgs/%s/repos", apiURL, userOrOrg)
|
||||
req, err = http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Borg-Data-Collector")
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
_ = resp.Body.Close()
|
||||
return nil, fmt.Errorf("failed to fetch repos: %s", resp.Status)
|
||||
}
|
||||
|
||||
var repos []Repo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&repos); err != nil {
|
||||
_ = resp.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
|
||||
for _, repo := range repos {
|
||||
allCloneURLs = append(allCloneURLs, repo.CloneURL)
|
||||
}
|
||||
|
||||
linkHeader := resp.Header.Get("Link")
|
||||
if linkHeader == "" {
|
||||
break
|
||||
}
|
||||
nextURL := g.findNextURL(linkHeader)
|
||||
if nextURL == "" {
|
||||
break
|
||||
}
|
||||
url = nextURL
|
||||
}
|
||||
|
||||
return allCloneURLs, nil
|
||||
}
|
||||
|
||||
func (g *githubClient) findNextURL(linkHeader string) string {
|
||||
links := strings.Split(linkHeader, ",")
|
||||
for _, link := range links {
|
||||
parts := strings.Split(link, ";")
|
||||
if len(parts) == 2 && strings.TrimSpace(parts[1]) == `rel="next"` {
|
||||
return strings.Trim(strings.TrimSpace(parts[0]), "<>")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetLatestRelease fetches the latest release for a given repository and channel.
|
||||
// The channel can be "stable", "beta", or "alpha".
|
||||
func (g *githubClient) GetLatestRelease(ctx context.Context, owner, repo, channel string) (*Release, error) {
|
||||
client := NewAuthenticatedClient(ctx)
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", owner, repo)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Borg-Data-Collector")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to fetch releases: %s", resp.Status)
|
||||
}
|
||||
|
||||
var releases []Release
|
||||
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return filterReleases(releases, channel), nil
|
||||
}
|
||||
|
||||
// filterReleases filters releases based on the specified channel.
|
||||
func filterReleases(releases []Release, channel string) *Release {
|
||||
for _, release := range releases {
|
||||
releaseChannel := determineChannel(release.TagName, release.PreRelease)
|
||||
if releaseChannel == channel {
|
||||
return &release
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// determineChannel determines the stability channel of a release based on its tag and PreRelease flag.
|
||||
func determineChannel(tagName string, isPreRelease bool) string {
|
||||
tagLower := strings.ToLower(tagName)
|
||||
if strings.Contains(tagLower, "alpha") {
|
||||
return "alpha"
|
||||
}
|
||||
if strings.Contains(tagLower, "beta") {
|
||||
return "beta"
|
||||
}
|
||||
if isPreRelease { // A pre-release without alpha/beta is treated as beta
|
||||
return "beta"
|
||||
}
|
||||
return "stable"
|
||||
}
|
||||
|
||||
// GetReleaseByPullRequest fetches a release associated with a specific pull request number.
|
||||
func (g *githubClient) GetReleaseByPullRequest(ctx context.Context, owner, repo string, prNumber int) (*Release, error) {
|
||||
client := NewAuthenticatedClient(ctx)
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", owner, repo)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Borg-Data-Collector")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to fetch releases: %s", resp.Status)
|
||||
}
|
||||
|
||||
var releases []Release
|
||||
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The pr number is included in the tag name with the format `vX.Y.Z-alpha.pr.123` or `vX.Y.Z-beta.pr.123`
|
||||
prTagSuffix := fmt.Sprintf(".pr.%d", prNumber)
|
||||
for _, release := range releases {
|
||||
if strings.Contains(release.TagName, prTagSuffix) {
|
||||
return &release, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil // No release found for the given PR number
|
||||
}
|
||||
|
||||
// GetDownloadURL finds the appropriate download URL for the current operating system and architecture.
|
||||
//
|
||||
// It supports two modes of operation:
|
||||
// 1. Using a 'releaseURLFormat' template: If 'releaseURLFormat' is provided,
|
||||
// it will be used to construct the download URL. The template can contain
|
||||
// placeholders for the release tag '{tag}', operating system '{os}', and
|
||||
// architecture '{arch}'.
|
||||
// 2. Automatic detection: If 'releaseURLFormat' is empty, the function will
|
||||
// inspect the assets of the release to find a suitable download URL. It
|
||||
// searches for an asset name that contains both the current OS and architecture
|
||||
// (e.g., "my-app-linux-amd64"). If no match is found, it falls back to
|
||||
// matching only the OS.
|
||||
//
|
||||
// Example with releaseURLFormat:
|
||||
//
|
||||
// release := &updater.Release{TagName: "v1.2.3"}
|
||||
// url, err := updater.GetDownloadURL(release, "https://example.com/downloads/{tag}/{os}/{arch}")
|
||||
// if err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
// fmt.Println(url) // "https://example.com/downloads/v1.2.3/linux/amd64" (on a Linux AMD64 system)
|
||||
//
|
||||
// Example with automatic detection:
|
||||
//
|
||||
// release := &updater.Release{
|
||||
// Assets: []updater.ReleaseAsset{
|
||||
// {Name: "my-app-linux-amd64", DownloadURL: "https://example.com/download/linux-amd64"},
|
||||
// {Name: "my-app-windows-amd64", DownloadURL: "https://example.com/download/windows-amd64"},
|
||||
// },
|
||||
// }
|
||||
// url, err := updater.GetDownloadURL(release, "")
|
||||
// if err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
// fmt.Println(url) // "https://example.com/download/linux-amd64" (on a Linux AMD64 system)
|
||||
func GetDownloadURL(release *Release, releaseURLFormat string) (string, error) {
|
||||
if release == nil {
|
||||
return "", fmt.Errorf("no release provided")
|
||||
}
|
||||
|
||||
if releaseURLFormat != "" {
|
||||
// Replace {tag}, {os}, and {arch} placeholders
|
||||
r := strings.NewReplacer(
|
||||
"{tag}", release.TagName,
|
||||
"{os}", runtime.GOOS,
|
||||
"{arch}", runtime.GOARCH,
|
||||
)
|
||||
return r.Replace(releaseURLFormat), nil
|
||||
}
|
||||
|
||||
osName := runtime.GOOS
|
||||
archName := runtime.GOARCH
|
||||
|
||||
for _, asset := range release.Assets {
|
||||
assetNameLower := strings.ToLower(asset.Name)
|
||||
// Match asset that contains both OS and architecture
|
||||
if strings.Contains(assetNameLower, osName) && strings.Contains(assetNameLower, archName) {
|
||||
return asset.DownloadURL, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for OS only if no asset matched both OS and arch
|
||||
for _, asset := range release.Assets {
|
||||
assetNameLower := strings.ToLower(asset.Name)
|
||||
if strings.Contains(assetNameLower, osName) {
|
||||
return asset.DownloadURL, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no suitable download asset found for %s/%s", osName, archName)
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
package updater
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/Snider/Borg/pkg/mocks"
|
||||
)
|
||||
|
||||
func TestGetPublicRepos(t *testing.T) {
|
||||
mockClient := mocks.NewMockClient(map[string]*http.Response{
|
||||
"https://api.github.com/users/testuser/repos": {
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(bytes.NewBufferString(`[{"clone_url": "https://github.com/testuser/repo1.git"}]`)),
|
||||
},
|
||||
"https://api.github.com/orgs/testorg/repos": {
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}, "Link": []string{`<https://api.github.com/organizations/123/repos?page=2>; rel="next"`}},
|
||||
Body: io.NopCloser(bytes.NewBufferString(`[{"clone_url": "https://github.com/testorg/repo1.git"}]`)),
|
||||
},
|
||||
"https://api.github.com/organizations/123/repos?page=2": {
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(bytes.NewBufferString(`[{"clone_url": "https://github.com/testorg/repo2.git"}]`)),
|
||||
},
|
||||
})
|
||||
|
||||
client := &githubClient{}
|
||||
oldClient := NewAuthenticatedClient
|
||||
NewAuthenticatedClient = func(ctx context.Context) *http.Client {
|
||||
return mockClient
|
||||
}
|
||||
defer func() {
|
||||
NewAuthenticatedClient = oldClient
|
||||
}()
|
||||
|
||||
// Test user repos
|
||||
repos, err := client.getPublicReposWithAPIURL(context.Background(), "https://api.github.com", "testuser")
|
||||
if err != nil {
|
||||
t.Fatalf("getPublicReposWithAPIURL for user failed: %v", err)
|
||||
}
|
||||
if len(repos) != 1 || repos[0] != "https://github.com/testuser/repo1.git" {
|
||||
t.Errorf("unexpected user repos: %v", repos)
|
||||
}
|
||||
|
||||
// Test org repos with pagination
|
||||
repos, err = client.getPublicReposWithAPIURL(context.Background(), "https://api.github.com", "testorg")
|
||||
if err != nil {
|
||||
t.Fatalf("getPublicReposWithAPIURL for org failed: %v", err)
|
||||
}
|
||||
if len(repos) != 2 || repos[0] != "https://github.com/testorg/repo1.git" || repos[1] != "https://github.com/testorg/repo2.git" {
|
||||
t.Errorf("unexpected org repos: %v", repos)
|
||||
}
|
||||
}
|
||||
func TestGetPublicRepos_Error(t *testing.T) {
|
||||
u, _ := url.Parse("https://api.github.com/users/testuser/repos")
|
||||
mockClient := mocks.NewMockClient(map[string]*http.Response{
|
||||
"https://api.github.com/users/testuser/repos": {
|
||||
StatusCode: http.StatusNotFound,
|
||||
Status: "404 Not Found",
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(bytes.NewBufferString("")),
|
||||
Request: &http.Request{Method: "GET", URL: u},
|
||||
},
|
||||
"https://api.github.com/orgs/testuser/repos": {
|
||||
StatusCode: http.StatusNotFound,
|
||||
Status: "404 Not Found",
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(bytes.NewBufferString("")),
|
||||
Request: &http.Request{Method: "GET", URL: u},
|
||||
},
|
||||
})
|
||||
expectedErr := "failed to fetch repos: 404 Not Found"
|
||||
|
||||
client := &githubClient{}
|
||||
oldClient := NewAuthenticatedClient
|
||||
NewAuthenticatedClient = func(ctx context.Context) *http.Client {
|
||||
return mockClient
|
||||
}
|
||||
defer func() {
|
||||
NewAuthenticatedClient = oldClient
|
||||
}()
|
||||
|
||||
// Test user repos
|
||||
_, err := client.getPublicReposWithAPIURL(context.Background(), "https://api.github.com", "testuser")
|
||||
if err.Error() != expectedErr {
|
||||
t.Fatalf("getPublicReposWithAPIURL for user failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindNextURL(t *testing.T) {
|
||||
client := &githubClient{}
|
||||
linkHeader := `<https://api.github.com/organizations/123/repos?page=2>; rel="next", <https://api.github.com/organizations/123/repos?page=1>; rel="prev"`
|
||||
nextURL := client.findNextURL(linkHeader)
|
||||
if nextURL != "https://api.github.com/organizations/123/repos?page=2" {
|
||||
t.Errorf("unexpected next URL: %s", nextURL)
|
||||
}
|
||||
|
||||
linkHeader = `<https://api.github.com/organizations/123/repos?page=1>; rel="prev"`
|
||||
nextURL = client.findNextURL(linkHeader)
|
||||
if nextURL != "" {
|
||||
t.Errorf("unexpected next URL: %s", nextURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAuthenticatedClient(t *testing.T) {
|
||||
// Test with no token
|
||||
client := NewAuthenticatedClient(context.Background())
|
||||
if client != http.DefaultClient {
|
||||
t.Errorf("expected http.DefaultClient, but got something else")
|
||||
}
|
||||
|
||||
// Test with token
|
||||
t.Setenv("GITHUB_TOKEN", "test-token")
|
||||
client = NewAuthenticatedClient(context.Background())
|
||||
if client == http.DefaultClient {
|
||||
t.Errorf("expected an authenticated client, but got http.DefaultClient")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// MockGithubClient is a mock implementation of the GithubClient interface for testing.
|
||||
type MockGithubClient struct {
|
||||
GetLatestReleaseFunc func(ctx context.Context, owner, repo, channel string) (*Release, error)
|
||||
GetReleaseByPullRequestFunc func(ctx context.Context, owner, repo string, prNumber int) (*Release, error)
|
||||
GetPublicReposFunc func(ctx context.Context, userOrOrg string) ([]string, error)
|
||||
}
|
||||
|
||||
// GetLatestRelease mocks the GetLatestRelease method of the GithubClient interface.
|
||||
func (m *MockGithubClient) GetLatestRelease(ctx context.Context, owner, repo, channel string) (*Release, error) {
|
||||
if m.GetLatestReleaseFunc != nil {
|
||||
return m.GetLatestReleaseFunc(ctx, owner, repo, channel)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetReleaseByPullRequest mocks the GetReleaseByPullRequest method of the GithubClient interface.
|
||||
func (m *MockGithubClient) GetReleaseByPullRequest(ctx context.Context, owner, repo string, prNumber int) (*Release, error) {
|
||||
if m.GetReleaseByPullRequestFunc != nil {
|
||||
return m.GetReleaseByPullRequestFunc(ctx, owner, repo, prNumber)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetPublicRepos mocks the GetPublicRepos method of the GithubClient interface.
|
||||
func (m *MockGithubClient) GetPublicRepos(ctx context.Context, userOrOrg string) ([]string, error) {
|
||||
if m.GetPublicReposFunc != nil {
|
||||
return m.GetPublicReposFunc(ctx, userOrOrg)
|
||||
}
|
||||
return []string{"repo1", "repo2"}, nil
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"name": "updater",
|
||||
"version": "1.2.3"
|
||||
}
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
//go:generate go run forge.lthn.ai/core/cli/cmd/updater/build
|
||||
|
||||
// Package updater provides functionality for self-updating Go applications.
|
||||
// It supports updates from GitHub releases and generic HTTP endpoints.
|
||||
package updater
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StartupCheckMode defines the updater's behavior on startup.
|
||||
type StartupCheckMode int
|
||||
|
||||
const (
|
||||
// NoCheck disables any checks on startup.
|
||||
NoCheck StartupCheckMode = iota
|
||||
// CheckOnStartup checks for updates on startup but does not apply them.
|
||||
CheckOnStartup
|
||||
// CheckAndUpdateOnStartup checks for and applies updates on startup.
|
||||
CheckAndUpdateOnStartup
|
||||
)
|
||||
|
||||
// UpdateServiceConfig holds the configuration for the UpdateService.
|
||||
type UpdateServiceConfig struct {
|
||||
// RepoURL is the URL to the repository for updates. It can be a GitHub
|
||||
// repository URL (e.g., "https://github.com/owner/repo") or a base URL
|
||||
// for a generic HTTP update server.
|
||||
RepoURL string
|
||||
// Channel specifies the release channel to track (e.g., "stable", "prerelease").
|
||||
// This is only used for GitHub-based updates.
|
||||
Channel string
|
||||
// CheckOnStartup determines the update behavior when the service starts.
|
||||
CheckOnStartup StartupCheckMode
|
||||
// ForceSemVerPrefix toggles whether to enforce a 'v' prefix on version tags for display.
|
||||
// If true, a 'v' prefix is added if missing. If false, it's removed if present.
|
||||
ForceSemVerPrefix bool
|
||||
// ReleaseURLFormat provides a template for constructing the download URL for a
|
||||
// release asset. The placeholder {tag} will be replaced with the release tag.
|
||||
ReleaseURLFormat string
|
||||
}
|
||||
|
||||
// UpdateService provides a configurable interface for handling application updates.
|
||||
// It can be configured to check for updates on startup and, if desired, apply
|
||||
// them automatically. The service can handle updates from both GitHub releases
|
||||
// and generic HTTP servers.
|
||||
type UpdateService struct {
|
||||
config UpdateServiceConfig
|
||||
isGitHub bool
|
||||
owner string
|
||||
repo string
|
||||
}
|
||||
|
||||
// NewUpdateService creates and configures a new UpdateService.
|
||||
// It parses the repository URL to determine if it's a GitHub repository
|
||||
// and extracts the owner and repo name.
|
||||
func NewUpdateService(config UpdateServiceConfig) (*UpdateService, error) {
|
||||
isGitHub := strings.Contains(config.RepoURL, "github.com")
|
||||
var owner, repo string
|
||||
var err error
|
||||
|
||||
if isGitHub {
|
||||
owner, repo, err = ParseRepoURL(config.RepoURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse GitHub repo URL: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &UpdateService{
|
||||
config: config,
|
||||
isGitHub: isGitHub,
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start initiates the update check based on the service configuration.
|
||||
// It determines whether to perform a GitHub or HTTP-based update check
|
||||
// based on the RepoURL. The behavior of the check is controlled by the
|
||||
// CheckOnStartup setting in the configuration.
|
||||
func (s *UpdateService) Start() error {
|
||||
if s.isGitHub {
|
||||
return s.startGitHubCheck()
|
||||
}
|
||||
return s.startHTTPCheck()
|
||||
}
|
||||
|
||||
func (s *UpdateService) startGitHubCheck() error {
|
||||
switch s.config.CheckOnStartup {
|
||||
case NoCheck:
|
||||
return nil // Do nothing
|
||||
case CheckOnStartup:
|
||||
return CheckOnly(s.owner, s.repo, s.config.Channel, s.config.ForceSemVerPrefix, s.config.ReleaseURLFormat)
|
||||
case CheckAndUpdateOnStartup:
|
||||
return CheckForUpdates(s.owner, s.repo, s.config.Channel, s.config.ForceSemVerPrefix, s.config.ReleaseURLFormat)
|
||||
default:
|
||||
return fmt.Errorf("unknown startup check mode: %d", s.config.CheckOnStartup)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UpdateService) startHTTPCheck() error {
|
||||
switch s.config.CheckOnStartup {
|
||||
case NoCheck:
|
||||
return nil // Do nothing
|
||||
case CheckOnStartup:
|
||||
return CheckOnlyHTTP(s.config.RepoURL)
|
||||
case CheckAndUpdateOnStartup:
|
||||
return CheckForUpdatesHTTP(s.config.RepoURL)
|
||||
default:
|
||||
return fmt.Errorf("unknown startup check mode: %d", s.config.CheckOnStartup)
|
||||
}
|
||||
}
|
||||
|
||||
// ParseRepoURL extracts the owner and repository name from a GitHub URL.
|
||||
// It handles standard GitHub URL formats.
|
||||
func ParseRepoURL(repoURL string) (owner string, repo string, err error) {
|
||||
u, err := url.Parse(repoURL)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
parts := strings.Split(strings.Trim(u.Path, "/"), "/")
|
||||
if len(parts) < 2 {
|
||||
return "", "", fmt.Errorf("invalid repo URL path: %s", u.Path)
|
||||
}
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package updater_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"forge.lthn.ai/core/cli/cmd/updater"
|
||||
)
|
||||
|
||||
func ExampleNewUpdateService() {
|
||||
// Mock the update check functions to prevent actual updates during tests
|
||||
updater.CheckForUpdates = func(owner, repo, channel string, forceSemVerPrefix bool, releaseURLFormat string) error {
|
||||
fmt.Println("CheckForUpdates called")
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
updater.CheckForUpdates = nil // Restore original function
|
||||
}()
|
||||
|
||||
config := updater.UpdateServiceConfig{
|
||||
RepoURL: "https://github.com/owner/repo",
|
||||
Channel: "stable",
|
||||
CheckOnStartup: updater.CheckAndUpdateOnStartup,
|
||||
}
|
||||
updateService, err := updater.NewUpdateService(config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create update service: %v", err)
|
||||
}
|
||||
if err := updateService.Start(); err != nil {
|
||||
log.Printf("Update check failed: %v", err)
|
||||
}
|
||||
// Output: CheckForUpdates called
|
||||
}
|
||||
|
||||
func ExampleParseRepoURL() {
|
||||
owner, repo, err := updater.ParseRepoURL("https://github.com/owner/repo")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse repo URL: %v", err)
|
||||
}
|
||||
fmt.Printf("Owner: %s, Repo: %s", owner, repo)
|
||||
// Output: Owner: owner, Repo: repo
|
||||
}
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
package updater
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewUpdateService(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
config UpdateServiceConfig
|
||||
expectError bool
|
||||
isGitHub bool
|
||||
}{
|
||||
{
|
||||
name: "Valid GitHub URL",
|
||||
config: UpdateServiceConfig{
|
||||
RepoURL: "https://github.com/owner/repo",
|
||||
},
|
||||
isGitHub: true,
|
||||
},
|
||||
{
|
||||
name: "Valid non-GitHub URL",
|
||||
config: UpdateServiceConfig{
|
||||
RepoURL: "https://example.com/updates",
|
||||
},
|
||||
isGitHub: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid GitHub URL",
|
||||
config: UpdateServiceConfig{
|
||||
RepoURL: "https://github.com/owner",
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
service, err := NewUpdateService(tc.config)
|
||||
if (err != nil) != tc.expectError {
|
||||
t.Errorf("Expected error: %v, got: %v", tc.expectError, err)
|
||||
}
|
||||
if err == nil && service.isGitHub != tc.isGitHub {
|
||||
t.Errorf("Expected isGitHub: %v, got: %v", tc.isGitHub, service.isGitHub)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateService_Start(t *testing.T) {
|
||||
// Setup a mock server for HTTP tests
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"version": "v1.1.0", "url": "http://example.com/release.zip"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
config UpdateServiceConfig
|
||||
checkOnlyGitHub int
|
||||
checkAndDoGitHub int
|
||||
checkOnlyHTTPCalls int
|
||||
checkAndDoHTTPCalls int
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "GitHub: NoCheck",
|
||||
config: UpdateServiceConfig{
|
||||
RepoURL: "https://github.com/owner/repo",
|
||||
CheckOnStartup: NoCheck,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GitHub: CheckOnStartup",
|
||||
config: UpdateServiceConfig{
|
||||
RepoURL: "https://github.com/owner/repo",
|
||||
CheckOnStartup: CheckOnStartup,
|
||||
},
|
||||
checkOnlyGitHub: 1,
|
||||
},
|
||||
{
|
||||
name: "GitHub: CheckAndUpdateOnStartup",
|
||||
config: UpdateServiceConfig{
|
||||
RepoURL: "https://github.com/owner/repo",
|
||||
CheckOnStartup: CheckAndUpdateOnStartup,
|
||||
},
|
||||
checkAndDoGitHub: 1,
|
||||
},
|
||||
{
|
||||
name: "HTTP: NoCheck",
|
||||
config: UpdateServiceConfig{
|
||||
RepoURL: server.URL,
|
||||
CheckOnStartup: NoCheck,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "HTTP: CheckOnStartup",
|
||||
config: UpdateServiceConfig{
|
||||
RepoURL: server.URL,
|
||||
CheckOnStartup: CheckOnStartup,
|
||||
},
|
||||
checkOnlyHTTPCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "HTTP: CheckAndUpdateOnStartup",
|
||||
config: UpdateServiceConfig{
|
||||
RepoURL: server.URL,
|
||||
CheckOnStartup: CheckAndUpdateOnStartup,
|
||||
},
|
||||
checkAndDoHTTPCalls: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var checkOnlyGitHub, checkAndDoGitHub, checkOnlyHTTP, checkAndDoHTTP int
|
||||
|
||||
// Mock GitHub functions
|
||||
originalCheckOnly := CheckOnly
|
||||
CheckOnly = func(owner, repo, channel string, forceSemVerPrefix bool, releaseURLFormat string) error {
|
||||
checkOnlyGitHub++
|
||||
return nil
|
||||
}
|
||||
defer func() { CheckOnly = originalCheckOnly }()
|
||||
|
||||
originalCheckForUpdates := CheckForUpdates
|
||||
CheckForUpdates = func(owner, repo, channel string, forceSemVerPrefix bool, releaseURLFormat string) error {
|
||||
checkAndDoGitHub++
|
||||
return nil
|
||||
}
|
||||
defer func() { CheckForUpdates = originalCheckForUpdates }()
|
||||
|
||||
// Mock HTTP functions
|
||||
originalCheckOnlyHTTP := CheckOnlyHTTP
|
||||
CheckOnlyHTTP = func(baseURL string) error {
|
||||
checkOnlyHTTP++
|
||||
return nil
|
||||
}
|
||||
defer func() { CheckOnlyHTTP = originalCheckOnlyHTTP }()
|
||||
|
||||
originalCheckForUpdatesHTTP := CheckForUpdatesHTTP
|
||||
CheckForUpdatesHTTP = func(baseURL string) error {
|
||||
checkAndDoHTTP++
|
||||
return nil
|
||||
}
|
||||
defer func() { CheckForUpdatesHTTP = originalCheckForUpdatesHTTP }()
|
||||
|
||||
service, _ := NewUpdateService(tc.config)
|
||||
err := service.Start()
|
||||
|
||||
if (err != nil) != tc.expectError {
|
||||
t.Errorf("Expected error: %v, got: %v", tc.expectError, err)
|
||||
}
|
||||
if checkOnlyGitHub != tc.checkOnlyGitHub {
|
||||
t.Errorf("Expected GitHub CheckOnly calls: %d, got: %d", tc.checkOnlyGitHub, checkOnlyGitHub)
|
||||
}
|
||||
if checkAndDoGitHub != tc.checkAndDoGitHub {
|
||||
t.Errorf("Expected GitHub CheckForUpdates calls: %d, got: %d", tc.checkAndDoGitHub, checkAndDoGitHub)
|
||||
}
|
||||
if checkOnlyHTTP != tc.checkOnlyHTTPCalls {
|
||||
t.Errorf("Expected HTTP CheckOnly calls: %d, got: %d", tc.checkOnlyHTTPCalls, checkOnlyHTTP)
|
||||
}
|
||||
if checkAndDoHTTP != tc.checkAndDoHTTPCalls {
|
||||
t.Errorf("Expected HTTP CheckForUpdates calls: %d, got: %d", tc.checkAndDoHTTPCalls, checkAndDoHTTP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
ij_typescript_use_double_quotes = false
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
43
cmd/updater/ui/.gitignore
vendored
43
cmd/updater/ui/.gitignore
vendored
|
|
@ -1,43 +0,0 @@
|
|||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
__screenshots__/
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
4
cmd/updater/ui/.vscode/extensions.json
vendored
4
cmd/updater/ui/.vscode/extensions.json
vendored
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||
"recommendations": ["angular.ng-template"]
|
||||
}
|
||||
20
cmd/updater/ui/.vscode/launch.json
vendored
20
cmd/updater/ui/.vscode/launch.json
vendored
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ng serve",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: start",
|
||||
"url": "http://localhost:4200/"
|
||||
},
|
||||
{
|
||||
"name": "ng test",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: test",
|
||||
"url": "http://localhost:9876/debug.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
42
cmd/updater/ui/.vscode/tasks.json
vendored
42
cmd/updater/ui/.vscode/tasks.json
vendored
|
|
@ -1,42 +0,0 @@
|
|||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
# CoreElementTemplate
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.9.
|
||||
|
||||
## Development server
|
||||
|
||||
To start a local development server, run:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
|
||||
```bash
|
||||
ng generate component component-name
|
||||
```
|
||||
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To build the project run:
|
||||
|
||||
```bash
|
||||
ng build
|
||||
```
|
||||
|
||||
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"core-element-template": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"standalone": false
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"standalone": false
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"standalone": false
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "none"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "core-element-template:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "core-element-template:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular/build:extract-i18n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9037
cmd/updater/ui/package-lock.json
generated
9037
cmd/updater/ui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,49 +0,0 @@
|
|||
{
|
||||
"name": "core-element-template",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.html",
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "^20.3.0",
|
||||
"@angular/compiler": "^20.3.0",
|
||||
"@angular/core": "^20.3.0",
|
||||
"@angular/elements": "^20.3.10",
|
||||
"@angular/forms": "^20.3.0",
|
||||
"@angular/platform-browser": "^20.3.0",
|
||||
"@angular/router": "^20.3.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^20.3.9",
|
||||
"@angular/cli": "^20.3.9",
|
||||
"@angular/compiler-cli": "^20.3.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.9.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.9.2"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
|
|
@ -1,23 +0,0 @@
|
|||
import { DoBootstrap, Injector, NgModule, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { createCustomElement } from '@angular/elements';
|
||||
|
||||
import { App } from './app';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
App
|
||||
],
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners()
|
||||
]
|
||||
})
|
||||
export class AppModule implements DoBootstrap {
|
||||
constructor(private injector: Injector) {
|
||||
const el = createCustomElement(App, { injector });
|
||||
customElements.define('core-element-template', el);
|
||||
}
|
||||
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<h1>Hello, {{ title() }}</h1>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { Component, signal } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'core-element-template',
|
||||
templateUrl: './app.html',
|
||||
standalone: true
|
||||
})
|
||||
export class App {
|
||||
protected readonly title = signal('core-element-template');
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>CoreElementTemplate</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<core-element-template></core-element-template>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { platformBrowser } from '@angular/platform-browser';
|
||||
import { AppModule } from './app/app-module';
|
||||
|
||||
platformBrowser().bootstrapModule(AppModule, {
|
||||
ngZoneEventCoalescing: true,
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
|
|
@ -1 +0,0 @@
|
|||
/* You can add global styles to this file, and also import other style files */
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/selfupdate"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// Version holds the current version of the application.
|
||||
// It is set at build time via ldflags or fallback to the version in package.json.
|
||||
var Version = PkgVersion
|
||||
|
||||
// NewGithubClient is a variable that holds a function to create a new GithubClient.
|
||||
// This can be replaced in tests to inject a mock client.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// updater.NewGithubClient = func() updater.GithubClient {
|
||||
// return &mockClient{} // or your mock implementation
|
||||
// }
|
||||
var NewGithubClient = func() GithubClient {
|
||||
return &githubClient{}
|
||||
}
|
||||
|
||||
// DoUpdate is a variable that holds the function to perform the actual update.
|
||||
// This can be replaced in tests to prevent actual updates.
|
||||
var DoUpdate = func(url string) error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to close response body: %v\n", err)
|
||||
}
|
||||
}(resp.Body)
|
||||
|
||||
err = selfupdate.Apply(resp.Body, selfupdate.Options{})
|
||||
if err != nil {
|
||||
if rerr := selfupdate.RollbackError(err); rerr != nil {
|
||||
return fmt.Errorf("failed to rollback from failed update: %v", rerr)
|
||||
}
|
||||
return fmt.Errorf("update failed: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("Update applied successfully.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckForNewerVersion checks if a newer version of the application is available on GitHub.
|
||||
// It fetches the latest release for the given owner, repository, and channel, and compares its tag
|
||||
// with the current application version.
|
||||
var CheckForNewerVersion = func(owner, repo, channel string, forceSemVerPrefix bool) (*Release, bool, error) {
|
||||
client := NewGithubClient()
|
||||
ctx := context.Background()
|
||||
|
||||
release, err := client.GetLatestRelease(ctx, owner, repo, channel)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error fetching latest release: %w", err)
|
||||
}
|
||||
|
||||
if release == nil {
|
||||
return nil, false, nil // No release found
|
||||
}
|
||||
|
||||
// Always normalize to 'v' prefix for semver comparison
|
||||
vCurrent := formatVersionForComparison(Version)
|
||||
vLatest := formatVersionForComparison(release.TagName)
|
||||
|
||||
if semver.Compare(vCurrent, vLatest) >= 0 {
|
||||
return release, false, nil // Current version is up-to-date or newer
|
||||
}
|
||||
|
||||
return release, true, nil // A newer version is available
|
||||
}
|
||||
|
||||
// CheckForUpdates checks for new updates on GitHub and applies them if a newer version is found.
|
||||
// It uses the provided owner, repository, and channel to find the latest release.
|
||||
var CheckForUpdates = func(owner, repo, channel string, forceSemVerPrefix bool, releaseURLFormat string) error {
|
||||
release, updateAvailable, err := CheckForNewerVersion(owner, repo, channel, forceSemVerPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !updateAvailable {
|
||||
if release != nil {
|
||||
fmt.Printf("Current version %s is up-to-date with latest release %s.\n",
|
||||
formatVersionForDisplay(Version, forceSemVerPrefix),
|
||||
formatVersionForDisplay(release.TagName, forceSemVerPrefix))
|
||||
} else {
|
||||
fmt.Println("No releases found.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Newer version %s found (current: %s). Applying update...\n",
|
||||
formatVersionForDisplay(release.TagName, forceSemVerPrefix),
|
||||
formatVersionForDisplay(Version, forceSemVerPrefix))
|
||||
|
||||
downloadURL, err := GetDownloadURL(release, releaseURLFormat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting download URL: %w", err)
|
||||
}
|
||||
|
||||
return DoUpdate(downloadURL)
|
||||
}
|
||||
|
||||
// CheckOnly checks for new updates on GitHub without applying them.
|
||||
// It prints a message indicating if a new release is available.
|
||||
var CheckOnly = func(owner, repo, channel string, forceSemVerPrefix bool, releaseURLFormat string) error {
|
||||
release, updateAvailable, err := CheckForNewerVersion(owner, repo, channel, forceSemVerPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !updateAvailable {
|
||||
if release != nil {
|
||||
fmt.Printf("Current version %s is up-to-date with latest release %s.\n",
|
||||
formatVersionForDisplay(Version, forceSemVerPrefix),
|
||||
formatVersionForDisplay(release.TagName, forceSemVerPrefix))
|
||||
} else {
|
||||
fmt.Println("No new release found.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("New release found: %s (current version: %s)\n",
|
||||
formatVersionForDisplay(release.TagName, forceSemVerPrefix),
|
||||
formatVersionForDisplay(Version, forceSemVerPrefix))
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckForUpdatesByTag checks for and applies updates from GitHub based on the channel
|
||||
// determined by the current application's version tag (e.g., 'stable' or 'prerelease').
|
||||
var CheckForUpdatesByTag = func(owner, repo string) error {
|
||||
channel := determineChannel(Version, false) // isPreRelease is false for current version
|
||||
return CheckForUpdates(owner, repo, channel, true, "")
|
||||
}
|
||||
|
||||
// CheckOnlyByTag checks for updates from GitHub based on the channel determined by the
|
||||
// current version tag, without applying them.
|
||||
var CheckOnlyByTag = func(owner, repo string) error {
|
||||
channel := determineChannel(Version, false) // isPreRelease is false for current version
|
||||
return CheckOnly(owner, repo, channel, true, "")
|
||||
}
|
||||
|
||||
// CheckForUpdatesByPullRequest finds a release associated with a specific pull request number
|
||||
// on GitHub and applies the update.
|
||||
var CheckForUpdatesByPullRequest = func(owner, repo string, prNumber int, releaseURLFormat string) error {
|
||||
client := NewGithubClient()
|
||||
ctx := context.Background()
|
||||
|
||||
release, err := client.GetReleaseByPullRequest(ctx, owner, repo, prNumber)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching release for pull request: %w", err)
|
||||
}
|
||||
|
||||
if release == nil {
|
||||
fmt.Printf("No release found for PR #%d.\n", prNumber)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Release %s found for PR #%d. Applying update...\n", release.TagName, prNumber)
|
||||
|
||||
downloadURL, err := GetDownloadURL(release, releaseURLFormat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting download URL: %w", err)
|
||||
}
|
||||
|
||||
return DoUpdate(downloadURL)
|
||||
}
|
||||
|
||||
// CheckForUpdatesHTTP checks for and applies updates from a generic HTTP endpoint.
|
||||
// The endpoint is expected to provide update information in a structured format.
|
||||
var CheckForUpdatesHTTP = func(baseURL string) error {
|
||||
info, err := GetLatestUpdateFromURL(baseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vCurrent := formatVersionForComparison(Version)
|
||||
vLatest := formatVersionForComparison(info.Version)
|
||||
|
||||
if semver.Compare(vCurrent, vLatest) >= 0 {
|
||||
fmt.Printf("Current version %s is up-to-date with latest release %s.\n", Version, info.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Newer version %s found (current: %s). Applying update...\n", info.Version, Version)
|
||||
return DoUpdate(info.URL)
|
||||
}
|
||||
|
||||
// CheckOnlyHTTP checks for updates from a generic HTTP endpoint without applying them.
|
||||
// It prints a message if a new version is available.
|
||||
var CheckOnlyHTTP = func(baseURL string) error {
|
||||
info, err := GetLatestUpdateFromURL(baseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vCurrent := formatVersionForComparison(Version)
|
||||
vLatest := formatVersionForComparison(info.Version)
|
||||
|
||||
if semver.Compare(vCurrent, vLatest) >= 0 {
|
||||
fmt.Printf("Current version %s is up-to-date with latest release %s.\n", Version, info.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("New release found: %s (current version: %s)\n", info.Version, Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatVersionForComparison ensures the version string has a 'v' prefix for semver comparison.
|
||||
func formatVersionForComparison(version string) string {
|
||||
if version != "" && !strings.HasPrefix(version, "v") {
|
||||
return "v" + version
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
// formatVersionForDisplay ensures the version string has the correct 'v' prefix based on the forceSemVerPrefix flag.
|
||||
func formatVersionForDisplay(version string, forceSemVerPrefix bool) string {
|
||||
hasV := strings.HasPrefix(version, "v")
|
||||
if forceSemVerPrefix && !hasV {
|
||||
return "v" + version
|
||||
}
|
||||
if !forceSemVerPrefix && hasV {
|
||||
return strings.TrimPrefix(version, "v")
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
|
@ -1,261 +0,0 @@
|
|||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// mockGithubClient is a mock implementation of the GithubClient interface for testing.
|
||||
type mockGithubClient struct {
|
||||
getLatestRelease func(ctx context.Context, owner, repo, channel string) (*Release, error)
|
||||
getReleaseByPR func(ctx context.Context, owner, repo string, prNumber int) (*Release, error)
|
||||
getPublicRepos func(ctx context.Context, userOrOrg string) ([]string, error)
|
||||
getLatestReleaseCount int
|
||||
getReleaseByPRCount int
|
||||
getPublicReposCount int
|
||||
}
|
||||
|
||||
func (m *mockGithubClient) GetLatestRelease(ctx context.Context, owner, repo, channel string) (*Release, error) {
|
||||
m.getLatestReleaseCount++
|
||||
return m.getLatestRelease(ctx, owner, repo, channel)
|
||||
}
|
||||
|
||||
func (m *mockGithubClient) GetReleaseByPullRequest(ctx context.Context, owner, repo string, prNumber int) (*Release, error) {
|
||||
m.getReleaseByPRCount++
|
||||
return m.getReleaseByPR(ctx, owner, repo, prNumber)
|
||||
}
|
||||
|
||||
func (m *mockGithubClient) GetPublicRepos(ctx context.Context, userOrOrg string) ([]string, error) {
|
||||
m.getPublicReposCount++
|
||||
if m.getPublicRepos != nil {
|
||||
return m.getPublicRepos(ctx, userOrOrg)
|
||||
}
|
||||
return nil, fmt.Errorf("GetPublicRepos not implemented")
|
||||
}
|
||||
|
||||
func ExampleCheckForNewerVersion() {
|
||||
originalNewGithubClient := NewGithubClient
|
||||
defer func() { NewGithubClient = originalNewGithubClient }()
|
||||
|
||||
NewGithubClient = func() GithubClient {
|
||||
return &mockGithubClient{
|
||||
getLatestRelease: func(ctx context.Context, owner, repo, channel string) (*Release, error) {
|
||||
return &Release{TagName: "v1.1.0"}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Version = "1.0.0"
|
||||
release, available, err := CheckForNewerVersion("owner", "repo", "stable", true)
|
||||
if err != nil {
|
||||
log.Fatalf("CheckForNewerVersion failed: %v", err)
|
||||
}
|
||||
|
||||
if available {
|
||||
fmt.Printf("Newer version available: %s", release.TagName)
|
||||
} else {
|
||||
fmt.Println("No newer version available.")
|
||||
}
|
||||
// Output: Newer version available: v1.1.0
|
||||
}
|
||||
|
||||
func ExampleCheckForUpdates() {
|
||||
// Mock the functions to prevent actual updates and network calls
|
||||
originalDoUpdate := DoUpdate
|
||||
originalNewGithubClient := NewGithubClient
|
||||
defer func() {
|
||||
DoUpdate = originalDoUpdate
|
||||
NewGithubClient = originalNewGithubClient
|
||||
}()
|
||||
|
||||
NewGithubClient = func() GithubClient {
|
||||
return &mockGithubClient{
|
||||
getLatestRelease: func(ctx context.Context, owner, repo, channel string) (*Release, error) {
|
||||
return &Release{
|
||||
TagName: "v1.1.0",
|
||||
Assets: []ReleaseAsset{{Name: fmt.Sprintf("test-asset-%s-%s", runtime.GOOS, runtime.GOARCH), DownloadURL: "http://example.com/asset"}},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
DoUpdate = func(url string) error {
|
||||
fmt.Printf("Update would be applied from: %s", url)
|
||||
return nil
|
||||
}
|
||||
|
||||
Version = "1.0.0"
|
||||
err := CheckForUpdates("owner", "repo", "stable", true, "")
|
||||
if err != nil {
|
||||
log.Fatalf("CheckForUpdates failed: %v", err)
|
||||
}
|
||||
// Output:
|
||||
// Newer version v1.1.0 found (current: v1.0.0). Applying update...
|
||||
// Update would be applied from: http://example.com/asset
|
||||
}
|
||||
|
||||
func ExampleCheckOnly() {
|
||||
originalNewGithubClient := NewGithubClient
|
||||
defer func() { NewGithubClient = originalNewGithubClient }()
|
||||
|
||||
NewGithubClient = func() GithubClient {
|
||||
return &mockGithubClient{
|
||||
getLatestRelease: func(ctx context.Context, owner, repo, channel string) (*Release, error) {
|
||||
return &Release{TagName: "v1.1.0"}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Version = "1.0.0"
|
||||
err := CheckOnly("owner", "repo", "stable", true, "")
|
||||
if err != nil {
|
||||
log.Fatalf("CheckOnly failed: %v", err)
|
||||
}
|
||||
// Output: New release found: v1.1.0 (current version: v1.0.0)
|
||||
}
|
||||
|
||||
func ExampleCheckForUpdatesByTag() {
|
||||
// Mock the functions to prevent actual updates and network calls
|
||||
originalDoUpdate := DoUpdate
|
||||
originalNewGithubClient := NewGithubClient
|
||||
defer func() {
|
||||
DoUpdate = originalDoUpdate
|
||||
NewGithubClient = originalNewGithubClient
|
||||
}()
|
||||
|
||||
NewGithubClient = func() GithubClient {
|
||||
return &mockGithubClient{
|
||||
getLatestRelease: func(ctx context.Context, owner, repo, channel string) (*Release, error) {
|
||||
if channel == "stable" {
|
||||
return &Release{
|
||||
TagName: "v1.1.0",
|
||||
Assets: []ReleaseAsset{{Name: fmt.Sprintf("test-asset-%s-%s", runtime.GOOS, runtime.GOARCH), DownloadURL: "http://example.com/asset"}},
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
DoUpdate = func(url string) error {
|
||||
fmt.Printf("Update would be applied from: %s", url)
|
||||
return nil
|
||||
}
|
||||
|
||||
Version = "1.0.0" // A version that resolves to the "stable" channel
|
||||
err := CheckForUpdatesByTag("owner", "repo")
|
||||
if err != nil {
|
||||
log.Fatalf("CheckForUpdatesByTag failed: %v", err)
|
||||
}
|
||||
// Output:
|
||||
// Newer version v1.1.0 found (current: v1.0.0). Applying update...
|
||||
// Update would be applied from: http://example.com/asset
|
||||
}
|
||||
|
||||
func ExampleCheckOnlyByTag() {
|
||||
originalNewGithubClient := NewGithubClient
|
||||
defer func() { NewGithubClient = originalNewGithubClient }()
|
||||
|
||||
NewGithubClient = func() GithubClient {
|
||||
return &mockGithubClient{
|
||||
getLatestRelease: func(ctx context.Context, owner, repo, channel string) (*Release, error) {
|
||||
if channel == "stable" {
|
||||
return &Release{TagName: "v1.1.0"}, nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Version = "1.0.0" // A version that resolves to the "stable" channel
|
||||
err := CheckOnlyByTag("owner", "repo")
|
||||
if err != nil {
|
||||
log.Fatalf("CheckOnlyByTag failed: %v", err)
|
||||
}
|
||||
// Output: New release found: v1.1.0 (current version: v1.0.0)
|
||||
}
|
||||
|
||||
func ExampleCheckForUpdatesByPullRequest() {
|
||||
// Mock the functions to prevent actual updates and network calls
|
||||
originalDoUpdate := DoUpdate
|
||||
originalNewGithubClient := NewGithubClient
|
||||
defer func() {
|
||||
DoUpdate = originalDoUpdate
|
||||
NewGithubClient = originalNewGithubClient
|
||||
}()
|
||||
|
||||
NewGithubClient = func() GithubClient {
|
||||
return &mockGithubClient{
|
||||
getReleaseByPR: func(ctx context.Context, owner, repo string, prNumber int) (*Release, error) {
|
||||
if prNumber == 123 {
|
||||
return &Release{
|
||||
TagName: "v1.1.0-alpha.pr.123",
|
||||
Assets: []ReleaseAsset{{Name: fmt.Sprintf("test-asset-%s-%s", runtime.GOOS, runtime.GOARCH), DownloadURL: "http://example.com/asset-pr"}},
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
DoUpdate = func(url string) error {
|
||||
fmt.Printf("Update would be applied from: %s", url)
|
||||
return nil
|
||||
}
|
||||
|
||||
err := CheckForUpdatesByPullRequest("owner", "repo", 123, "")
|
||||
if err != nil {
|
||||
log.Fatalf("CheckForUpdatesByPullRequest failed: %v", err)
|
||||
}
|
||||
// Output:
|
||||
// Release v1.1.0-alpha.pr.123 found for PR #123. Applying update...
|
||||
// Update would be applied from: http://example.com/asset-pr
|
||||
}
|
||||
|
||||
func ExampleCheckForUpdatesHTTP() {
|
||||
// Create a mock HTTP server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/latest.json" {
|
||||
_, _ = fmt.Fprintln(w, `{"version": "1.1.0", "url": "http://example.com/update"}`)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Mock the doUpdateFunc to prevent actual updates
|
||||
originalDoUpdate := DoUpdate
|
||||
defer func() { DoUpdate = originalDoUpdate }()
|
||||
DoUpdate = func(url string) error {
|
||||
fmt.Printf("Update would be applied from: %s", url)
|
||||
return nil
|
||||
}
|
||||
|
||||
Version = "1.0.0"
|
||||
err := CheckForUpdatesHTTP(server.URL)
|
||||
if err != nil {
|
||||
log.Fatalf("CheckForUpdatesHTTP failed: %v", err)
|
||||
}
|
||||
// Output:
|
||||
// Newer version 1.1.0 found (current: 1.0.0). Applying update...
|
||||
// Update would be applied from: http://example.com/update
|
||||
}
|
||||
|
||||
func ExampleCheckOnlyHTTP() {
|
||||
// Create a mock HTTP server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/latest.json" {
|
||||
_, _ = fmt.Fprintln(w, `{"version": "1.1.0", "url": "http://example.com/update"}`)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
Version = "1.0.0"
|
||||
err := CheckOnlyHTTP(server.URL)
|
||||
if err != nil {
|
||||
log.Fatalf("CheckOnlyHTTP failed: %v", err)
|
||||
}
|
||||
// Output: New release found: 1.1.0 (current version: 1.0.0)
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package updater
|
||||
|
||||
// Generated by go:generate. DO NOT EDIT.
|
||||
|
||||
const PkgVersion = "1.2.3"
|
||||
Loading…
Add table
Reference in a new issue