Created
October 21, 2025 15:55
-
-
Save brianaqp/45fe885c6ec0f41ffb3c28fbcca92ee6 to your computer and use it in GitHub Desktop.
Script to install .zip applications using Gtihub releases with semantic release
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "context" | |
| "io" | |
| "log" | |
| "net/http" | |
| "os" | |
| "path/filepath" | |
| "strconv" | |
| "strings" | |
| "github.com/google/go-github/v76/github" | |
| "github.com/mholt/archives" | |
| ) | |
| // Utility to check errors with an identifier | |
| func checkError(identifier string, err error) { | |
| if err != nil { | |
| log.Panicf("Error. %s: %v\n", identifier, err) | |
| } | |
| } | |
| // Compare semantic versions: returns 1 if v1 > v2, -1 if v1 < v2, 0 if equal | |
| func compareVersions(v1, v2 string) int { | |
| s1 := strings.Split(strings.TrimSpace(v1), ".") | |
| s2 := strings.Split(strings.TrimSpace(v2), ".") | |
| for i := 0; i < 3; i++ { | |
| n1, _ := strconv.Atoi(s1[i]) | |
| n2, _ := strconv.Atoi(s2[i]) | |
| if n1 > n2 { | |
| return 1 | |
| } else if n1 < n2 { | |
| return -1 | |
| } | |
| } | |
| return 0 | |
| } | |
| func main() { | |
| workingDir, err := os.Getwd() | |
| checkError("get-working-dir", err) | |
| // --- Configuration | |
| owner := "your-github-username" | |
| repo := "your-repo-name" | |
| appDir := "app" | |
| versionFile := filepath.Join(appDir, "internal", "version.txt") | |
| githubToken := os.Getenv("GITHUB_TOKEN") | |
| client := github.NewClient(nil).WithAuthToken(githubToken) | |
| // --- Fetch latest release | |
| ctx := context.TODO() | |
| release, githubResp, err := client.Repositories.GetLatestRelease(ctx, owner, repo) | |
| checkError("get-latest-release", err) | |
| log.Printf("GitHub request status: %d", githubResp.StatusCode) | |
| // --- Check local version | |
| doesAppExist := false | |
| var localTag string | |
| if _, err = os.Stat(appDir); err == nil { | |
| data, err := os.ReadFile(versionFile) | |
| checkError("read-version-file", err) | |
| localTag = string(data) | |
| doesAppExist = true | |
| } | |
| remoteTag := release.GetTagName() | |
| if remoteTag == "" { | |
| log.Panicln("Remote tag is empty. Aborting.") | |
| } | |
| if doesAppExist && compareVersions(remoteTag, localTag) == -1 { | |
| log.Printf("No newer version found. Remote: %s, Local: %s", remoteTag, localTag) | |
| return | |
| } | |
| // --- Download release zipball | |
| zipURL := release.GetZipballURL() | |
| if zipURL == "" { | |
| log.Fatalln("Zipball URL is empty. Aborting.") | |
| return | |
| } | |
| req, err := http.NewRequestWithContext(ctx, http.MethodGet, zipURL, nil) | |
| checkError("create-request", err) | |
| req.Header.Set("Authorization", "Bearer "+githubToken) | |
| req.Header.Set("Accept", "application/zip") | |
| resp, err := http.DefaultClient.Do(req) | |
| checkError("download-zipball", err) | |
| defer resp.Body.Close() | |
| if resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusMovedPermanently { | |
| loc := resp.Header.Get("Location") | |
| if loc == "" { | |
| log.Println("Redirect without location. Aborting.") | |
| return | |
| } | |
| resp.Body.Close() | |
| resp, err = http.Get(loc) | |
| checkError("follow-redirect", err) | |
| defer resp.Body.Close() | |
| } | |
| if resp.StatusCode < 200 || resp.StatusCode >= 300 { | |
| log.Fatalf("Download error, status: %d", resp.StatusCode) | |
| } | |
| log.Printf("Zip downloaded. Storing in memory.") | |
| // --- Backup existing app directory | |
| if doesAppExist { | |
| log.Println("Renaming app/ to temp/") | |
| os.Rename(appDir, "temp") | |
| } | |
| // --- Extract zipball | |
| zipPath := "release.zip" | |
| out, err := os.Create(zipPath) | |
| checkError("save-release-zip", err) | |
| _, err = io.Copy(out, resp.Body) | |
| out.Close() | |
| checkError("copy-zip-to-disk", err) | |
| zipFile, err := os.Open(zipPath) | |
| checkError("open-release-zip", err) | |
| defer func() { | |
| zipFile.Close() | |
| os.Remove(zipPath) | |
| }() | |
| handleFile := func(ctx context.Context, file archives.FileInfo) error { | |
| rc, err := file.Open() | |
| if err != nil { | |
| return err | |
| } | |
| defer rc.Close() | |
| outPath := filepath.Join(appDir, file.Name()) | |
| if file.IsDir() { | |
| return os.MkdirAll(outPath, 0755) | |
| } | |
| if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil { | |
| return err | |
| } | |
| outFile, err := os.Create(outPath) | |
| if err != nil { | |
| return err | |
| } | |
| defer outFile.Close() | |
| _, err = io.Copy(outFile, rc) | |
| return err | |
| } | |
| err = archives.Zip{}.Extract(context.Background(), zipFile, handleFile) | |
| if err != nil { | |
| log.Println("Extraction error, restoring backup.") | |
| if _, err := os.Stat(appDir); err == nil { | |
| os.RemoveAll(appDir) | |
| } | |
| os.Rename("temp", appDir) | |
| } | |
| if doesAppExist { | |
| os.RemoveAll("temp") | |
| } | |
| log.Println("Update completed successfully.") | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment