├── LICENSE ├── .github └── workflows │ └── build.yml ├── README.md └── continuous-release-manager.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 probonopd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Continuous Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: '1.16' 20 | 21 | - name: Build binaries for Linux and FreeBSD 22 | run: | 23 | go mod init github.com/probonopd/continuous-release-manager 24 | go get github.com/google/go-github/github 25 | go get golang.org/x/oauth2 26 | go build -ldflags="-s -w" -o continuous-release-manager-linux 27 | GOOS=freebsd go build -ldflags="-s -w" -o continuous-release-manager-freebsd 28 | 29 | - name: Create "continuous" release 30 | if: github.event_name == 'push' # Only run for push events, not pull requests 31 | run: | 32 | RELEASE_ID=$(./continuous-release-manager-linux) 33 | echo "RELEASE_ID=${RELEASE_ID}" >> $GITHUB_ENV 34 | 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Upload binaries to release 39 | if: github.event_name == 'push' # Only run for push events, not pull requests 40 | uses: xresloader/upload-to-github-release@v1.3.12 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | with: 44 | file: "continuous-release-manager-*" 45 | draft: false 46 | verbose: true 47 | branches: main 48 | tag_name: continuous 49 | release_id: ${{ env.RELEASE_ID }} 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Continuous Release Manager 2 | 3 | This tool provides a "continuous" release that always reflects the latest commit hash of the branch being built on CI (e.g., **GitHub Actions** or **Cirrus CI**). The tool checks for an existing release named "continuous" in the repository. If the release does not exist, it creates a new one, or if it exists and its commit hash differs, it deletes the existing release and creates a new one with the latest commit hash. This ensures that the "continuous" release always points to the correct hash. The tool can be easily integrated into your repository's CI/CD pipeline, providing a reliable and automated way to manage continuous releases. It is especially useful if you want to upload binaries to releases from different CI systems (e.g., GitHub Actions and Cirrus CI). 4 | 5 | ## How it Works 6 | 7 | 1. The tool checks if a release with the name "continuous" already exists. 8 | - If a release with the name "continuous" exists, it compares the commit hash of the existing release with the desired commit hash. 9 | - If the commit hashes differ, the existing release is deleted to keep the releases in sync with the current state of the code. 10 | 11 | 2. After checking for an existing release, the tool creates a new release named "continuous" with the desired commit hash. 12 | 13 | ## Usage 14 | 15 | To use the tool, set up a GitHub Actions workflow (or any CI/CD system) to automatically trigger the tool whenever changes are pushed to the repository. The tool will handle creating, updating, or deleting the "continuous" release as needed. 16 | 17 | ## Examples 18 | 19 | ## GitHub Actions (for the Linux build) 20 | 21 | ```yaml 22 | - name: Create GitHub Release using Continuous Release Manager 23 | if: github.event_name == 'push' # Only run for push events, not pull requests 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | run: | 27 | curl -L -o continuous-release-manager-linux https://github.com/probonopd/continuous-release-manager/releases/download/continuous/continuous-release-manager-linux 28 | chmod +x continuous-release-manager-linux 29 | ./continuous-release-manager-linux 30 | RELEASE_ID=$(./continuous-release-manager-linux) 31 | echo "RELEASE_ID=${RELEASE_ID}" >> $GITHUB_ENV 32 | 33 | - name: Upload to GitHub Release 34 | if: github.event_name == 'push' # Only run for push events, not pull requests 35 | uses: xresloader/upload-to-github-release@v1.3.12 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | with: 39 | file: "build/*zip" 40 | draft: false 41 | verbose: true 42 | branches: main 43 | tag_name: continuous 44 | release_id: ${{ env.RELEASE_ID }} 45 | ``` 46 | 47 | ## Cirrus CI (for the FreeBSD build) 48 | 49 | ```yaml 50 | task: 51 | ... 52 | test_script: 53 | - ... 54 | - case "$CIRRUS_BRANCH" in *pull/*) echo "Skipping since it's a pull request" ;; * ) wget https://github.com/tcnksm/ghr/files/5247714/ghr.zip ; unzip ghr.zip ; rm ghr.zip ; fetch https://github.com/probonopd/continuous-release-manager/releases/download/continuous/continuous-release-manager-freebsd && chmod +x continuous-release-manager-freebsd && ./continuous-release-manager-freebsd && ./ghr -replace -t "${GITHUB_TOKEN}" -u "${CIRRUS_REPO_OWNER}" -r "${CIRRUS_REPO_NAME}" -c "${CIRRUS_CHANGE_IN_REPO}" continuous "${CIRRUS_WORKING_DIR}"/build/*zip ; esac 55 | only_if: $CIRRUS_TAG != 'continuous' 56 | ``` 57 | 58 | ## Example output 59 | 60 | ### For the first builder 61 | 62 | ``` 63 | [INFO] Starting release management... 64 | [VERBOSE] Repository Owner: probonopd 65 | [VERBOSE] Repository Name: Filer 66 | [VERBOSE] Release Tag: continuous 67 | [VERBOSE] Release Commit Hash: d8b990a61fcb2671a8621d282341fdcbf1e83cc7 68 | [INFO] Checking for existing release... 69 | [VERBOSE] Release found with ID: 114646674 70 | [VERBOSE] Existing release commit hash differs from the desired one. Deleting the existing release... 71 | [INFO] Existing release deleted successfully. 72 | [INFO] New release created successfully! 73 | [VERBOSE] Release ID: 114646855 74 | ``` 75 | 76 | ### For the subsequent builders 77 | 78 | ``` 79 | [INFO] Starting release management... 80 | [VERBOSE] Repository Owner: probonopd 81 | [VERBOSE] Repository Name: Filer 82 | [VERBOSE] Release Tag: continuous 83 | [VERBOSE] Release Commit Hash: d8b990a61fcb2671a8621d282341fdcbf1e83cc7 84 | [INFO] Checking for existing release... 85 | [VERBOSE] Release found with ID: 114646855 86 | [INFO] Release with the name 'continuous' already exists and has the desired commit hash. 87 | WARNING: found release (continuous). Use existing one. 88 | ``` 89 | 90 | ### Projects using this 91 | 92 | * https://github.com/helloSystem/Menu 93 | * https://github.com/helloSystem/launch 94 | * https://github.com/probonopd/Filer 95 | * ... 96 | 97 | ## License 98 | 99 | The GitHub Continuous Release Manager is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 100 | 101 | --- 102 | _This project is not affiliated with GitHub or any other third-party service mentioned._ 103 | -------------------------------------------------------------------------------- /continuous-release-manager.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/google/go-github/github" 10 | "golang.org/x/oauth2" 11 | ) 12 | 13 | func main() { 14 | ctx := context.Background() 15 | token := os.Getenv("GITHUB_TOKEN") 16 | if token == "" { 17 | fmt.Println("Error: GITHUB_TOKEN environment variable not set.") 18 | os.Exit(1) 19 | } 20 | 21 | ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) 22 | tc := oauth2.NewClient(ctx, ts) 23 | client := github.NewClient(tc) 24 | 25 | repoOwner := "" 26 | repoName := "" 27 | releaseCommitHash := "" 28 | releaseName := "continuous" 29 | releaseTag := "continuous" 30 | 31 | isGitHubActions := os.Getenv("GITHUB_ACTIONS") == "true" 32 | isCirrusCI := os.Getenv("CIRRUS_CI") == "true" 33 | 34 | if isGitHubActions { 35 | repoOwner = os.Getenv("GITHUB_REPOSITORY_OWNER") 36 | repoName = extractRepositoryName(os.Getenv("GITHUB_REPOSITORY")) 37 | releaseCommitHash = os.Getenv("GITHUB_SHA") 38 | } else if isCirrusCI { 39 | repoOwner = os.Getenv("CIRRUS_REPO_OWNER") 40 | repoName = os.Getenv("CIRRUS_REPO_NAME") 41 | releaseCommitHash = os.Getenv("CIRRUS_CHANGE_IN_REPO") 42 | } else { 43 | fmt.Println("Error: Unsupported CI environment.") 44 | os.Exit(1) 45 | } 46 | 47 | logInfo("Starting release management...") 48 | logVerbose(fmt.Sprintf("Repository Owner: %s", repoOwner)) 49 | logVerbose(fmt.Sprintf("Repository Name: %s", repoName)) 50 | logVerbose(fmt.Sprintf("Release Tag: %s", releaseTag)) 51 | logVerbose(fmt.Sprintf("Release Commit Hash: %s", releaseCommitHash)) 52 | 53 | var createdRelease *github.RepositoryRelease 54 | var releaseID int64 // Declare the variable to store the Release ID 55 | 56 | // Check if the release with the name "continuous" already exists 57 | logInfo("Checking for existing release...") 58 | release, _, err := client.Repositories.GetReleaseByTag(ctx, repoOwner, repoName, releaseTag) 59 | if err != nil { 60 | // An error occurred while retrieving the release 61 | if _, ok := err.(*github.ErrorResponse); ok && err.(*github.ErrorResponse).Response.StatusCode == 404 { 62 | // The release does not exist yet, proceed to create it 63 | logInfo("Release with the name 'continuous' does not exist. Creating a new release...") 64 | newRelease := &github.RepositoryRelease{ 65 | TagName: &releaseTag, 66 | TargetCommitish: &releaseCommitHash, 67 | Name: &releaseName, 68 | } 69 | createdRelease, _, err = client.Repositories.CreateRelease(ctx, repoOwner, repoName, newRelease) 70 | if err != nil { 71 | // Check if the error is due to insufficient permissions 72 | if strings.Contains(err.Error(), "403 Resource not accessible by integration") { 73 | logError("Error creating release: Insufficient permissions. Please ensure that you have the necessary access rights.\n") 74 | logError("To fix this, go to https://github.com/%s/%s/settings/actions, under \"Workflow permissions\" set \"Read and write permissions\".\n", repoOwner, repoName) 75 | } else { 76 | logError(fmt.Sprintf("Error creating release: %v", err)) 77 | } 78 | } else { 79 | logInfo("New release created successfully!") 80 | logVerbose(fmt.Sprintf("Release ID: %v", *createdRelease.ID)) 81 | 82 | // Store the Release ID 83 | releaseID = *createdRelease.ID 84 | } 85 | } else { 86 | // Another error occurred while retrieving the release 87 | logError(fmt.Sprintf("Error retrieving release: %v", err)) 88 | } 89 | } else { 90 | // The release exists, compare the commit hashes 91 | logVerbose(fmt.Sprintf("Release found with ID: %d", *release.ID)) 92 | if *release.TargetCommitish != releaseCommitHash { 93 | logVerbose("Existing release commit hash differs from the desired one. Deleting the existing release and tag...") 94 | _, err := client.Repositories.DeleteRelease(ctx, repoOwner, repoName, *release.ID) 95 | if err != nil { 96 | logError(fmt.Sprintf("Error deleting release: %v", err)) 97 | } else { 98 | logInfo("Existing release deleted successfully.") 99 | _, err := client.Git.DeleteRef(ctx, repoOwner, repoName, fmt.Sprintf("tags/%s", releaseTag)) 100 | if err != nil { 101 | logError(fmt.Sprintf("Error deleting tag: %v", err)) 102 | } else { 103 | logInfo("Existing tag deleted successfully.") 104 | } 105 | 106 | // Proceed to create a new release to replace the deleted one 107 | newRelease := &github.RepositoryRelease{ 108 | TagName: &releaseTag, 109 | TargetCommitish: &releaseCommitHash, 110 | Name: &releaseName, 111 | } 112 | createdRelease, _, err = client.Repositories.CreateRelease(ctx, repoOwner, repoName, newRelease) 113 | if err != nil { 114 | logError(fmt.Sprintf("Error creating release: %v", err)) 115 | } else { 116 | logInfo("New release created successfully!") 117 | logVerbose(fmt.Sprintf("Release ID: %v", *createdRelease.ID)) 118 | 119 | // Store the Release ID 120 | releaseID = *createdRelease.ID 121 | } 122 | } 123 | } else { 124 | logInfo("Release with the name 'continuous' already exists and has the desired commit hash.") 125 | 126 | // Store the Release ID 127 | releaseID = *release.ID 128 | } 129 | } 130 | 131 | targetRelease := release 132 | if createdRelease != nil { 133 | targetRelease = createdRelease 134 | } 135 | 136 | // At the end, after all other operations are done publish it to make it non-draft 137 | logVerbose("Publishing release...") 138 | targetRelease.Draft = github.Bool(false) // Set the Draft field to false 139 | _, _, err = client.Repositories.EditRelease(ctx, repoOwner, repoName, releaseID, targetRelease) 140 | if err != nil { 141 | logError(fmt.Sprintf("Error publishing release: %v", err)) 142 | } else { 143 | logInfo("Release published successfully.") 144 | } 145 | 146 | // Print the Release ID at the end 147 | fmt.Println(releaseID) 148 | } 149 | 150 | func extractRepositoryName(fullName string) string { 151 | parts := strings.Split(fullName, "/") 152 | if len(parts) > 1 { 153 | return parts[1] 154 | } 155 | return fullName 156 | } 157 | 158 | func logInfo(msg string) { 159 | fmt.Fprintf(os.Stderr, "[INFO] %s\n", msg) 160 | } 161 | 162 | func logVerbose(msg string) { 163 | fmt.Fprintf(os.Stderr, "[VERBOSE] %s\n", msg) 164 | } 165 | 166 | func logError(format string, a ...interface{}) { 167 | fmt.Fprintf(os.Stderr, "[ERROR] "+format+"\n", a...) 168 | } 169 | --------------------------------------------------------------------------------