|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "encoding/json" |
| 6 | + "flag" |
| 7 | + "fmt" |
| 8 | + "io/ioutil" |
| 9 | + "log" |
| 10 | + "net/http" |
| 11 | + "os" |
| 12 | + "strings" |
| 13 | + "time" |
| 14 | +) |
| 15 | + |
| 16 | +type release struct { |
| 17 | + TagName string `json:"tag_name"` |
| 18 | + TargetBranch string `json:"target_commitish"` |
| 19 | + ReleaseName string `json:"name"` |
| 20 | + Body string `json:"body"` |
| 21 | + PreRelease bool `json:"prerelease"` |
| 22 | +} |
| 23 | + |
| 24 | +type userInputs struct { |
| 25 | + tag string |
| 26 | + releaseName string |
| 27 | + previousTag string |
| 28 | + preRelease bool |
| 29 | + projects []string |
| 30 | + user string |
| 31 | + source string |
| 32 | + fallbackBranch string |
| 33 | + timeout int |
| 34 | + supportBranchName string |
| 35 | +} |
| 36 | + |
| 37 | +/* { |
| 38 | + "ref": "refs/tags/4.29.1", |
| 39 | + "node_id": "MDM6UmVmMTIwMzAxOTM2OjQuMjkuMQ==", |
| 40 | + "url": "https://api.github.com/repos/idnowgmbh/de.idnow.ai/git/refs/tags/4.29.1", |
| 41 | + "object": { |
| 42 | + "sha": "1e6757d5d09730b84d1a29eff891c206457d1049", |
| 43 | + "type": "commit", |
| 44 | + "url": "https://api.github.com/repos/idnowgmbh/de.idnow.ai/git/commits/1e6757d5d09730b84d1a29eff891c206457d1049" |
| 45 | + } |
| 46 | +} */ |
| 47 | +type referenceResponse struct { |
| 48 | + Ref string `json:"ref"` |
| 49 | + NodeID string `json:"node_id"` |
| 50 | + URL string `json:"url"` |
| 51 | + Object objectInReference `json:"object"` |
| 52 | +} |
| 53 | + |
| 54 | +type objectInReference struct { |
| 55 | + Sha string `json:"sha"` |
| 56 | + TypeInfo string `json:"type"` |
| 57 | + URL string `json:"url"` |
| 58 | +} |
| 59 | + |
| 60 | +/* |
| 61 | +https://api.github.com/repos/<AUTHOR>/<REPO>/git/refs |
| 62 | +{ |
| 63 | + "ref": "refs/heads/<NEW-BRANCH-NAME>", |
| 64 | + "sha": "<HASH-TO-BRANCH-FROM>" |
| 65 | +} |
| 66 | +*/ |
| 67 | +type createBranchBody struct { |
| 68 | + Ref string `json:"ref"` |
| 69 | + Sha string `json:"sha"` |
| 70 | +} |
| 71 | + |
| 72 | +const environmentTokenKey = "OAUTH_TOKEN" |
| 73 | +const dockerNamesURL = "https://frightanic.com/goodies_content/docker-names.php" |
| 74 | +const apiBaseURL = "https://api.github.com/repos" |
| 75 | +const githubURL = "https://github.com/repos/%s/%s/compare/%s...%s" |
| 76 | + |
| 77 | +func main() { |
| 78 | + if token, present := os.LookupEnv(environmentTokenKey); !present || token == "" { |
| 79 | + log.Fatalf("Please set a environment variable named %s created on github.", environmentTokenKey) |
| 80 | + } |
| 81 | + |
| 82 | + var userInput userInputs |
| 83 | + flag.StringVar(&userInput.user, "user", "idnowgmbh", "The User / Owner of the repository") |
| 84 | + flag.StringVar(&userInput.source, "source", "master", "The source branch/tag to create the new tag") |
| 85 | + flag.StringVar(&userInput.fallbackBranch, "fallback-branch", "master", "The fallback branch to create the TAG on if the source branch does not exist in the repository.") |
| 86 | + flag.IntVar(&userInput.timeout, "timeout", 5, "The Timeout for Github API Calls") |
| 87 | + flag.StringVar(&userInput.supportBranchName, "support-branch-name", "", "The name of the support branch to create if source branch is a tag") |
| 88 | + |
| 89 | + flag.StringVar(&userInput.tag, "tag", "", "The tag to create.") |
| 90 | + flag.StringVar(&userInput.releaseName, "release-name", "", "The name of the Release") |
| 91 | + flag.StringVar(&userInput.previousTag, "previous-tag", "", "The previous tag to use in the message") |
| 92 | + flag.BoolVar(&userInput.preRelease, "pre-release", true, "If this is a pre-release, use -pre-release=false to change") |
| 93 | + |
| 94 | + flag.Usage = usage |
| 95 | + flag.Parse() |
| 96 | + |
| 97 | + userInput.projects = flag.Args() |
| 98 | + |
| 99 | + inputValidaton(userInput) |
| 100 | + userInput.releaseName = getReleaseName(userInput) |
| 101 | + |
| 102 | + client := &http.Client{ |
| 103 | + Timeout: time.Duration(time.Second * time.Duration(userInput.timeout)), |
| 104 | + } |
| 105 | + |
| 106 | + for index, project := range userInput.projects { |
| 107 | + log.Printf("%2d : Starting Release %s for %s with Tag Version %s on branch %s with fallback branch %s and possible support branch %s", index+1, userInput.releaseName, project, userInput.tag, userInput.source, userInput.fallbackBranch, userInput.supportBranchName) |
| 108 | + |
| 109 | + projectAPIBaseURL := fmt.Sprintf("%s/%s/%s", apiBaseURL, userInput.user, project) |
| 110 | + targetBranch, err := checkBranch(client, userInput, project, projectAPIBaseURL) |
| 111 | + if err != nil { |
| 112 | + log.Fatalf("Could not get the Target Branch %v", err) |
| 113 | + } |
| 114 | + log.Printf("Selected Branch %s to create tag %s", targetBranch, userInput.tag) |
| 115 | + createRelease(client, userInput, targetBranch, project, projectAPIBaseURL) |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +func usage() { |
| 120 | + executableName := os.Args[0] |
| 121 | + fmt.Fprintf(flag.CommandLine.Output(), "\n%s is an opinionated implementation of some Github APIs that can be used to create release tags for multiple projects\n", executableName) |
| 122 | + fmt.Fprintf(flag.CommandLine.Output(), "\nUsage: %s -user comdotlinux -source master -tag v0.0.2 -previous-tag v0.0.1 java-design-patterns TasteOfJavaEE7", executableName) |
| 123 | + fmt.Fprintf(flag.CommandLine.Output(), "\nUsage: %s -user comdotlinux -source support/v0.0.x -tag v0.0.3 -fallback-branch master -previous-tag v0.0.1 -release-name Duke -pre-release=false java-design-patterns TasteOfJavaEE7", executableName) |
| 124 | + fmt.Fprintf(flag.CommandLine.Output(), "\nUsage: %s -user comdotlinux -source v.0.0.1 -tag v0.0.2-RC.1 -support-branch-name support/v0.0.x -previous-tag v0.0.1 java-design-patterns TasteOfJavaEE7", executableName) |
| 125 | + fmt.Fprintf(flag.CommandLine.Output(), "\nWhen -source is a TAG -support-branch-name is mandatory. \n") |
| 126 | + fmt.Fprintf(flag.CommandLine.Output(), "An environment variable with the name %s is mandatory for all actions!\nSee https://developer.github.com/v3/#oauth2-token-sent-in-a-header to get one.\n\n", environmentTokenKey) |
| 127 | + fmt.Fprintf(flag.CommandLine.Output(), "Below are the possible parameters:\n") |
| 128 | + flag.PrintDefaults() |
| 129 | + os.Exit(3) |
| 130 | +} |
| 131 | + |
| 132 | +func checkBranch(client *http.Client, userInput userInputs, project string, projectAPIBaseURL string) (string, error) { |
| 133 | + url := fmt.Sprintf("%s/branches/%s", projectAPIBaseURL, userInput.source) |
| 134 | + res, err := doGet(client, url) |
| 135 | + if err != nil { |
| 136 | + log.Fatalf("Received error %v", err) |
| 137 | + } |
| 138 | + |
| 139 | + if statusSuccess(res.StatusCode) { |
| 140 | + log.Printf("Branch %s looks good, will be selected. Response : %v", userInput.source, http.StatusText(res.StatusCode)) |
| 141 | + return userInput.source, nil |
| 142 | + } |
| 143 | + |
| 144 | + if res.StatusCode == http.StatusNotFound { |
| 145 | + log.Printf("Checking if source %s is a TAG", userInput.source) |
| 146 | + url = fmt.Sprintf("%s/releases/tags/%s", projectAPIBaseURL, userInput.source) |
| 147 | + res, err := doGet(client, url) |
| 148 | + if err != nil { |
| 149 | + log.Fatalf("Could not Check release tags : %v", err) |
| 150 | + } |
| 151 | + |
| 152 | + if statusSuccess(res.StatusCode) { |
| 153 | + log.Printf("%s is a Tag", userInput.source) |
| 154 | + if userInput.supportBranchName == "" { |
| 155 | + log.Fatalf("If Source is a tag, name of support branch to create from this is necessary!") |
| 156 | + } |
| 157 | + log.Printf("Since %s is a Tag, Creating support branch %s", userInput.source, userInput.supportBranchName) |
| 158 | + |
| 159 | + url = fmt.Sprintf("%s/git/refs/tags/%s", projectAPIBaseURL, userInput.source) |
| 160 | + res, err := doGet(client, url) |
| 161 | + if err != nil { |
| 162 | + log.Fatalf("Could not get Tag %s commit userInput. : %v", userInput.source, err) |
| 163 | + } |
| 164 | + |
| 165 | + if statusSuccess(res.StatusCode) { |
| 166 | + log.Println("Reading response body to get commit hash") |
| 167 | + defer res.Body.Close() |
| 168 | + tagInfoResponseBody, errorReadingBody := ioutil.ReadAll(res.Body) |
| 169 | + if errorReadingBody != nil { |
| 170 | + log.Fatalf("Could not read Response body, cannot proceed : %v", errorReadingBody) |
| 171 | + } |
| 172 | + |
| 173 | + log.Println("Read Response body, trying to unmarshal the Json") |
| 174 | + var tagReference referenceResponse |
| 175 | + if err := json.Unmarshal(tagInfoResponseBody, &tagReference); err != nil { |
| 176 | + log.Fatalf("Could not read response for tag info to get sha commit %v", err) |
| 177 | + } |
| 178 | + |
| 179 | + log.Printf("Response body read into referenceResponse, sha hash : %s", tagReference.Object.Sha) |
| 180 | + |
| 181 | + createBranch := createBranchBody{ |
| 182 | + Ref: fmt.Sprintf("refs/heads/%s", userInput.supportBranchName), |
| 183 | + Sha: tagReference.Object.Sha, |
| 184 | + } |
| 185 | + |
| 186 | + log.Printf("Created Request Object to create branch : %v", createBranch) |
| 187 | + bodyBytes, err := json.Marshal(createBranch) |
| 188 | + if err != nil { |
| 189 | + log.Fatalf("Could not create body for creating branch. %v", err) |
| 190 | + } |
| 191 | + |
| 192 | + log.Println("structconverted to Json Bytes") |
| 193 | + url = fmt.Sprintf("%s/git/refs", projectAPIBaseURL) |
| 194 | + log.Printf("Calling URL %s to create Branch %s", url, createBranch.Ref) |
| 195 | + req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(bodyBytes)) |
| 196 | + if err != nil { |
| 197 | + log.Fatalf("Could not create Request for creating branch. %v", err) |
| 198 | + } |
| 199 | + |
| 200 | + addAuthAndAcceptHeader(req) |
| 201 | + req.Header.Add(http.CanonicalHeaderKey("Content-Type"), "application/json") |
| 202 | + res, err := client.Do(req) |
| 203 | + if err != nil { |
| 204 | + log.Fatalf("Error in creating the branch : %v", err) |
| 205 | + } |
| 206 | + log.Printf("POST call to create branch completed with status %s", http.StatusText(res.StatusCode)) |
| 207 | + if res.StatusCode == http.StatusCreated { |
| 208 | + defer res.Body.Close() |
| 209 | + createBranchResponseBody, errorReadingBody := ioutil.ReadAll(res.Body) |
| 210 | + if errorReadingBody != nil { |
| 211 | + log.Fatalf("Could not read Response body, cannot proceed : %v", errorReadingBody) |
| 212 | + } |
| 213 | + |
| 214 | + var branchReference referenceResponse |
| 215 | + if err := json.Unmarshal(createBranchResponseBody, &branchReference); err != nil { |
| 216 | + log.Fatalf("Could not read response for tag info to get sha commit %v", err) |
| 217 | + } |
| 218 | + |
| 219 | + log.Printf("Created Branch with Details : %v", branchReference) |
| 220 | + |
| 221 | + branchNameArray := strings.SplitN(branchReference.Ref, "/", 3) |
| 222 | + if len(branchNameArray) == 3 { |
| 223 | + return branchNameArray[2], nil |
| 224 | + } |
| 225 | + return branchReference.Ref, nil |
| 226 | + } |
| 227 | + log.Printf("POST call to create branch completed with response %v", res) |
| 228 | + } |
| 229 | + } else { |
| 230 | + log.Printf("Use fallback branch since %s is neither a branch nor a Tag", userInput.source) |
| 231 | + url := fmt.Sprintf("%s/branches/%s", projectAPIBaseURL, userInput.fallbackBranch) |
| 232 | + res, err := doGet(client, url) |
| 233 | + if err != nil { |
| 234 | + log.Fatalf("Received error %v", err) |
| 235 | + } |
| 236 | + |
| 237 | + if statusSuccess(res.StatusCode) { |
| 238 | + log.Printf("Branch %s looks good, will be selected. Response : %v", userInput.fallbackBranch, http.StatusText(res.StatusCode)) |
| 239 | + return userInput.fallbackBranch, nil |
| 240 | + } |
| 241 | + } |
| 242 | + } |
| 243 | + |
| 244 | + log.Println("The source branch parameter and the fallback cannot be used to create release, so using master") |
| 245 | + return "master", nil |
| 246 | +} |
| 247 | + |
| 248 | +func statusSuccess(statusCode int) bool { |
| 249 | + log.Printf("checking status code %d", statusCode) |
| 250 | + return statusCode >= http.StatusOK && statusCode <= 299 |
| 251 | +} |
| 252 | + |
| 253 | +func doGet(client *http.Client, url string) (*http.Response, error) { |
| 254 | + log.Printf("Calling URL %s", url) |
| 255 | + req, err := http.NewRequest(http.MethodGet, url, nil) |
| 256 | + if err != nil { |
| 257 | + return nil, err |
| 258 | + } |
| 259 | + |
| 260 | + addAuthAndAcceptHeader(req) |
| 261 | + |
| 262 | + res, err := client.Do(req) |
| 263 | + if err != nil { |
| 264 | + return nil, err |
| 265 | + } |
| 266 | + |
| 267 | + log.Printf("Response %v", res) |
| 268 | + return res, nil |
| 269 | +} |
| 270 | + |
| 271 | +func addAuthAndAcceptHeader(request *http.Request) { |
| 272 | + request.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("token %s", os.Getenv(environmentTokenKey))) |
| 273 | + request.Header.Add(http.CanonicalHeaderKey("Accept"), "application/vnd.github.v3+json") |
| 274 | +} |
| 275 | + |
| 276 | +func createRelease(client *http.Client, userInput userInputs, targetBranch string, project string, projectAPIBaseURL string) { |
| 277 | + previousComparePoint := targetBranch |
| 278 | + if !isEmpty(userInput.previousTag) { |
| 279 | + previousComparePoint = userInput.previousTag |
| 280 | + } |
| 281 | + releaseCompareBody := fmt.Sprintf(githubURL, userInput.user, project, previousComparePoint, userInput.tag) |
| 282 | + releaseRequest := release{ |
| 283 | + TagName: userInput.tag, |
| 284 | + ReleaseName: userInput.releaseName, |
| 285 | + PreRelease: userInput.preRelease, |
| 286 | + TargetBranch: targetBranch, |
| 287 | + Body: releaseCompareBody, |
| 288 | + } |
| 289 | + |
| 290 | + b, _ := json.MarshalIndent(releaseRequest, "", " ") |
| 291 | + os.Stdout.Write(b) |
| 292 | + |
| 293 | + url := fmt.Sprintf("%s/releases", projectAPIBaseURL) |
| 294 | + log.Printf("Calling URL %s to create Release %v", url, releaseRequest) |
| 295 | + bodyBytes, err := json.Marshal(releaseRequest) |
| 296 | + if err != nil { |
| 297 | + log.Fatalf("Could not create body for creating release. %v", err) |
| 298 | + } |
| 299 | + req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(bodyBytes)) |
| 300 | + if err != nil { |
| 301 | + log.Fatalf("Could not create Body Json Bytes, from release object. %v", err) |
| 302 | + } |
| 303 | + |
| 304 | + addAuthAndAcceptHeader(req) |
| 305 | + req.Header.Add(http.CanonicalHeaderKey("Content-Type"), "application/json") |
| 306 | + res, err := client.Do(req) |
| 307 | + if err != nil { |
| 308 | + log.Fatalf("Create release Failed, %v", err) |
| 309 | + } |
| 310 | + |
| 311 | + if !statusSuccess(res.StatusCode) { |
| 312 | + log.Fatalf("Create release Failed with status, %d : %s", res.StatusCode, http.StatusText(res.StatusCode)) |
| 313 | + } |
| 314 | + |
| 315 | + log.Printf("Release Tag Created : %v", res) |
| 316 | +} |
| 317 | + |
| 318 | +func getReleaseName(userInput userInputs) string { |
| 319 | + if isEmpty(userInput.releaseName) { |
| 320 | + log.Printf("Since the release name is unavailable, getting a random release name using %s", dockerNamesURL) |
| 321 | + return getRandomReleaseName() |
| 322 | + if isEmpty(userInput.releaseName) { |
| 323 | + releaseName := "Release of " + userInput.tag |
| 324 | + log.Printf("Since getting release name was not possible, using %s as release name", releaseName) |
| 325 | + return releaseName |
| 326 | + } |
| 327 | + } |
| 328 | + return userInput.releaseName |
| 329 | +} |
| 330 | + |
| 331 | +func getRandomReleaseName() string { |
| 332 | + client := http.Client{} |
| 333 | + resp, err := client.Get(dockerNamesURL) |
| 334 | + if err != nil { |
| 335 | + log.Printf("Unable to get Docker Container Names for random release name. %v", err) |
| 336 | + return "" |
| 337 | + } |
| 338 | + defer resp.Body.Close() |
| 339 | + body, err := ioutil.ReadAll(resp.Body) |
| 340 | + if err != nil { |
| 341 | + log.Printf("Error reading body for the release name : %v", err) |
| 342 | + return "" |
| 343 | + } |
| 344 | + |
| 345 | + return strings.TrimRight(fmt.Sprintf("%s", body), "\n") |
| 346 | +} |
| 347 | + |
| 348 | +func isEmpty(input string) bool { |
| 349 | + return len(input) == 0 || input == "" |
| 350 | +} |
| 351 | + |
| 352 | +func inputValidaton(userInput userInputs) { |
| 353 | + |
| 354 | + var errors []string |
| 355 | + |
| 356 | + if isEmpty(userInput.user) { |
| 357 | + errors = append(errors, "User / Organization parameter is mandatory") |
| 358 | + } |
| 359 | + |
| 360 | + if isEmpty(userInput.source) { |
| 361 | + errors = append(errors, "source parameter is mandatory and must either be a branch OR a existing TAG on Github") |
| 362 | + } |
| 363 | + |
| 364 | + if isEmpty(userInput.tag) { |
| 365 | + errors = append(errors, "tag parameter is mandatory, otherwise what are we releasing?") |
| 366 | + } |
| 367 | + |
| 368 | + if len(userInput.projects) == 0 { |
| 369 | + errors = append(errors, "Atleast provide one project, otherwise where do we create the tag?") |
| 370 | + } |
| 371 | + |
| 372 | + if len(errors) != 0 { |
| 373 | + fmt.Fprintln(flag.CommandLine.Output(), "") |
| 374 | + for index, err := range errors { |
| 375 | + fmt.Fprintf(flag.CommandLine.Output(), "%2d : %s\n", index+1, err) |
| 376 | + } |
| 377 | + fmt.Fprintln(flag.CommandLine.Output(), "") |
| 378 | + flag.Usage() |
| 379 | + } |
| 380 | + |
| 381 | +} |
0 commit comments