gitlab.com/jfprevost/gitlab-runner-notlscheck@v11.11.4+incompatible/scripts/prepare-release-checklist-issue/main.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "encoding/json" 7 "flag" 8 "fmt" 9 "io/ioutil" 10 "net/http" 11 "net/url" 12 "os" 13 "regexp" 14 "strconv" 15 "strings" 16 "text/template" 17 "time" 18 19 "gopkg.in/yaml.v2" 20 ) 21 22 type ReleaseMetadata struct { 23 Major int 24 Minor int 25 ReleaseManagerHandle string 26 ReleaseBlogPostMR int 27 ReleaseBlogPostDeadline string 28 HelmChartMajor int 29 HelmChartMinor int 30 } 31 32 const ( 33 GitLabRunnerProjectID = "gitlab-org/gitlab-runner" 34 GitLabRunnerHelmChartProjectID = "charts/gitlab-runner" 35 WWWGitlabComProjectID = "gitlab-com/www-gitlab-com" 36 37 ReleasePostLabel = "release post" 38 39 ReleaseManagerHandleEnvVariable = "GITLAB_RUNNER_RELEASE_MANAGER_HANDLE" 40 41 VersionFile = "./VERSION" 42 HelmChartVersionFile = "Chart.yaml" 43 44 LayoutDay = "2006-01-02" 45 ) 46 47 var ( 48 reader *bufio.Reader 49 releaseMetadata ReleaseMetadata 50 51 detectedVersion []string 52 detectedMergeRequest []string 53 detectedHelmChartVersion []string 54 55 templateFilePath = flag.String("issue-template-file", ".gitlab/issue_templates/Release Checklist.md", "Path to a file with issue template") 56 57 dryRun = flag.Bool("dry-run", false, "Show issue content instead of creating it in GitLab") 58 noInteractive = flag.Bool("no-interactive", false, "Don't ask, just try to work!") 59 60 major = flag.String("major", detectVersion()[0], "Major version number") 61 minor = flag.String("minor", detectVersion()[1], "Minor version number") 62 releaseManagerHandle = flag.String("release-manager-handle", defaultReleaseManagerHandle(), "GitLab.com handle of the release manager") 63 releaseBlogPostMR = flag.String("release-blog-post-mr", detectBlogPostMergeRequest()[0], "ID of the Release Blog Post MR") 64 releaseBlogPostDeadline = flag.String("release-blog-post-deadline", detectReleaseMergeRequestDeadline(), "Deadline for adding Runner specific content to the Release Blog Post") 65 66 helmChartMajor = flag.String("helm-chart-major", detectHelmChartVersion()[0], "Major version number of GitLab Runner Helm Chart") 67 helmChartMinor = flag.String("helm-chart-minor", detectHelmChartVersion()[1], "Minor version number of GitLab Runner Helm Chart") 68 ) 69 70 func detectVersion() []string { 71 if len(detectedVersion) > 0 { 72 return detectedVersion 73 } 74 75 fmt.Println("Auto-detecting version...") 76 77 content, err := ioutil.ReadFile(VersionFile) 78 if err != nil { 79 fmt.Printf("Error while reading version file %q: %v", VersionFile, err) 80 81 return []string{"", ""} 82 } 83 84 fmt.Printf("Found: %s\n", content) 85 86 detectedVersion = strings.Split(string(content), ".") 87 88 return detectedVersion 89 } 90 91 type HelmChartData struct { 92 Version string `yaml:"version"` 93 } 94 95 func detectHelmChartVersion() []string { 96 if len(detectedHelmChartVersion) > 0 { 97 return detectedHelmChartVersion 98 } 99 100 fmt.Println("Auto-detecting Helm Chart version...") 101 102 charVersionFileURL := fmt.Sprintf("https://gitlab.com/%s/raw/master/%s", GitLabRunnerHelmChartProjectID, HelmChartVersionFile) 103 req, err := http.NewRequest(http.MethodGet, charVersionFileURL, nil) 104 if err != nil { 105 panic(fmt.Errorf("error while creating helm chart version detection request: %v", err)) 106 } 107 108 resp, err := http.DefaultClient.Do(req) 109 if err != nil { 110 panic(fmt.Errorf("error while requesting helm chart version: %v", err)) 111 } 112 113 defer resp.Body.Close() 114 content, err := ioutil.ReadAll(resp.Body) 115 if err != nil { 116 panic(fmt.Errorf("error while reading helm chart version response: %v", err)) 117 } 118 119 helmChartData := new(HelmChartData) 120 err = yaml.Unmarshal(content, helmChartData) 121 if err != nil { 122 fmt.Printf("Error while parsing Helm Chart version file: %v", err) 123 124 return []string{"", "", ""} 125 } 126 127 fmt.Printf("Found: %s\n", helmChartData.Version) 128 129 versionRx := regexp.MustCompile(`([0-9]+)\.([0-9]+)\.([0-9]+)`) 130 versions := versionRx.FindAllStringSubmatch(helmChartData.Version, -1) 131 132 if len(versions) != 1 { 133 fmt.Printf("Couldn't parse the version string") 134 135 return []string{"", "", ""} 136 } 137 138 detectedHelmChartVersion = []string{ 139 versions[0][1], 140 versions[0][2], 141 versions[0][3], 142 } 143 144 return detectedHelmChartVersion 145 } 146 147 func defaultReleaseManagerHandle() string { 148 fmt.Println("Auto-detecting Release Manager handle...") 149 150 handle := os.Getenv(ReleaseManagerHandleEnvVariable) 151 fmt.Printf("Found: %s\n", handle) 152 153 return handle 154 } 155 156 type listMergeRequestsResponse []listMergeRequestsResponseEntry 157 158 type listMergeRequestsResponseEntry struct { 159 ID int `json:"iid"` 160 WebURL string `json:"web_url"` 161 Title string `json:"title"` 162 Description string `json:"description"` 163 } 164 165 func detectBlogPostMergeRequest() []string { 166 if len(detectedMergeRequest) > 0 { 167 return detectedMergeRequest 168 } 169 170 fmt.Println("Auto-detecting Release Post merge request...") 171 172 version := detectVersion() 173 174 mergeRequests := listBlogPostMergeRequests(version) 175 if mergeRequests == nil { 176 return []string{"", ""} 177 } 178 179 printEntry := func(entry listMergeRequestsResponseEntry) { 180 fmt.Printf("\t%-40q %s\n", entry.Title, entry.WebURL) 181 } 182 183 fmt.Println("Found following www-gitlab-com merge requests:") 184 for _, entry := range mergeRequests { 185 printEntry(entry) 186 } 187 188 for _, chosen := range mergeRequests { 189 r := regexp.MustCompile("gitlab.com/gitlab-com/www-gitlab-com/blob/release-\\d+-\\d+/data/release_posts/(\\d+)_(\\d+)_(\\d+)_gitlab_\\d+_\\d+_released.yml") 190 dateParts := r.FindStringSubmatch(chosen.Description) 191 192 if len(dateParts) < 1 { 193 continue 194 } 195 196 fmt.Println("Choosing:") 197 printEntry(chosen) 198 199 detectedMergeRequest = []string{ 200 strconv.Itoa(chosen.ID), 201 fmt.Sprintf("%s-%s-%s", dateParts[1], dateParts[2], dateParts[3]), 202 } 203 204 return detectedMergeRequest 205 } 206 207 fmt.Println("Release Post merge request was not auto-detected. Please enter the ID manually") 208 209 return []string{"", ""} 210 } 211 212 func listBlogPostMergeRequests(version []string) listMergeRequestsResponse { 213 q := url.Values{} 214 q.Add("labels", ReleasePostLabel) 215 q.Add("state", "opened") 216 q.Add("milestone", fmt.Sprintf("%s.%s", version[0], version[1])) 217 218 rawURL := fmt.Sprintf("https://gitlab.com/api/v4/projects/%s/merge_requests?%s", url.QueryEscape(WWWGitlabComProjectID), q.Encode()) 219 220 findMergeRequestURL, err := url.Parse(rawURL) 221 if err != nil { 222 fmt.Printf("Error while parsing findMergeRequestURL: %v", err) 223 224 return nil 225 } 226 227 req, err := http.NewRequest(http.MethodGet, findMergeRequestURL.String(), nil) 228 if err != nil { 229 fmt.Printf("Error while creating HTTP Request: %v", err) 230 231 return nil 232 } 233 234 resp, err := http.DefaultClient.Do(req) 235 if err != nil { 236 fmt.Printf("Error while requesting API endpoint: %v", err) 237 238 return nil 239 } 240 241 defer resp.Body.Close() 242 body, err := ioutil.ReadAll(resp.Body) 243 if err != nil { 244 fmt.Printf("Error while reading response body: %v", err) 245 246 return nil 247 } 248 249 var response listMergeRequestsResponse 250 251 err = json.Unmarshal(body, &response) 252 if err != nil { 253 fmt.Printf("Error while parsing response JSON: %v", err) 254 255 return nil 256 } 257 258 return response 259 } 260 261 func detectReleaseMergeRequestDeadline() string { 262 fmt.Println("Auto-detecting Release Post entry deadline...") 263 264 offsetMap := map[time.Weekday]int{ 265 time.Monday: -11, 266 time.Tuesday: -11, 267 time.Wednesday: -9, 268 time.Thursday: -9, 269 time.Friday: -9, 270 time.Saturday: -9, 271 time.Sunday: -10, 272 } 273 274 date := detectBlogPostMergeRequest()[1] 275 if len(date) < 1 { 276 fmt.Println("Could not detect the date of Release...") 277 278 return "" 279 } 280 281 releaseDate, err := time.Parse(LayoutDay, date) 282 if err != nil { 283 fmt.Printf("Could not parse detected date %q: %v", date, err) 284 285 return "" 286 } 287 288 offset := offsetMap[releaseDate.Weekday()] 289 290 deadlineTime := releaseDate.Add(time.Duration(24*offset) * time.Hour) 291 deadline := deadlineTime.Format(LayoutDay) 292 293 fmt.Printf("Decided to use %q. Please adjust if required!\n", deadline) 294 295 return deadline 296 } 297 298 func main() { 299 fmt.Println() 300 fmt.Println("\nGitLab Runner release checklist issue generator") 301 fmt.Println() 302 303 flag.Usage = func() { 304 fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 305 fmt.Fprintf(os.Stderr, "\n %s [OPTIONS]\n\nOptions:\n", os.Args[0]) 306 flag.PrintDefaults() 307 } 308 flag.Parse() 309 310 if *noInteractive { 311 fmt.Println("Running in non-interactive mode.") 312 } 313 prepareMetadata() 314 315 content := prepareIssueContent() 316 title := prepareIssueTitle() 317 318 if *dryRun { 319 fmt.Println("Running in dry-run mode. No real changes will be done") 320 printIssue(title, content) 321 } else { 322 fmt.Println("Running in standard mode. A new issue will be created") 323 postIssue(title, content) 324 } 325 } 326 327 func prepareMetadata() { 328 var err error 329 330 askOnce("Major version number", major) 331 releaseMetadata.Major, err = strconv.Atoi(*major) 332 if err != nil { 333 panic(err) 334 } 335 336 askOnce("Minor version number", minor) 337 releaseMetadata.Minor, err = strconv.Atoi(*minor) 338 if err != nil { 339 panic(err) 340 } 341 342 askOnce("Helm Chart Major version number", helmChartMajor) 343 releaseMetadata.HelmChartMajor, err = strconv.Atoi(*helmChartMajor) 344 if err != nil { 345 panic(err) 346 } 347 348 askOnce("Helm Chart Minor version number", helmChartMinor) 349 releaseMetadata.HelmChartMinor, err = strconv.Atoi(*helmChartMinor) 350 if err != nil { 351 panic(err) 352 } 353 354 askOnce("GitLab.com handle of the release manager", releaseManagerHandle) 355 releaseMetadata.ReleaseManagerHandle = *releaseManagerHandle 356 357 askOnce("ID of the Release Blog Post MR", releaseBlogPostMR) 358 releaseMetadata.ReleaseBlogPostMR, err = strconv.Atoi(*releaseBlogPostMR) 359 if err != nil { 360 panic(err) 361 } 362 363 askOnce("Deadline for adding Runner specific content to the Release Blog Post", releaseBlogPostDeadline) 364 releaseMetadata.ReleaseBlogPostDeadline = *releaseBlogPostDeadline 365 } 366 367 func askOnce(prompt string, result *string) { 368 fmt.Printf("%s [%s]: ", prompt, *result) 369 if *noInteractive { 370 fmt.Println() 371 372 return 373 } 374 375 if reader == nil { 376 reader = bufio.NewReader(os.Stdin) 377 } 378 379 data, _, err := reader.ReadLine() 380 if err != nil { 381 panic(err) 382 } 383 384 newResult := string(data) 385 newResult = strings.TrimSpace(newResult) 386 387 if newResult != "" { 388 *result = newResult 389 } 390 391 if *result == "" { 392 panic("Can't be left empty!") 393 } 394 } 395 396 func prepareIssueContent() string { 397 data, err := ioutil.ReadFile(*templateFilePath) 398 if err != nil { 399 panic(err) 400 } 401 402 tpl := template.New("release-issue") 403 tpl.Funcs(template.FuncMap{ 404 "inc": func(i int) int { 405 return i + 1 406 }, 407 "dec": func(i int) int { 408 return i - 1 409 }, 410 }) 411 412 tpl, err = tpl.Parse(string(data)) 413 if err != nil { 414 panic(err) 415 } 416 417 var output []byte 418 buffer := bytes.NewBuffer(output) 419 err = tpl.Execute(buffer, releaseMetadata) 420 if err != nil { 421 panic(err) 422 } 423 424 return buffer.String() 425 } 426 427 func prepareIssueTitle() string { 428 return fmt.Sprintf("GitLab Runner %d.%d release checklist", releaseMetadata.Major, releaseMetadata.Minor) 429 } 430 431 func printIssue(title, content string) { 432 fmt.Println() 433 fmt.Println("====================================") 434 fmt.Printf(" Title: %s\n", title) 435 fmt.Printf("Content:\n\n%s\n", content) 436 fmt.Println("====================================") 437 fmt.Println() 438 } 439 440 type createIssueOptions struct { 441 Title string `json:"title"` 442 Description string `json:"description"` 443 } 444 445 type createIssueResponse struct { 446 WebURL string `json:"web_url"` 447 } 448 449 func postIssue(title, content string) { 450 newIssueURL := fmt.Sprintf("https://gitlab.com/api/v4/projects/%s/issues", url.QueryEscape(GitLabRunnerProjectID)) 451 452 options := &createIssueOptions{ 453 Title: title, 454 Description: content, 455 } 456 457 jsonBody, err := json.Marshal(options) 458 if err != nil { 459 panic(err) 460 } 461 462 req, err := http.NewRequest(http.MethodPost, newIssueURL, bytes.NewBuffer(jsonBody)) 463 if err != nil { 464 panic(err) 465 } 466 467 token := os.Getenv("GITLAB_API_PRIVATE_TOKEN") 468 req.Header.Set("Private-Token", token) 469 req.Header.Set("Content-Type", "application/json") 470 471 resp, err := http.DefaultClient.Do(req) 472 if err != nil { 473 panic(err) 474 } 475 476 defer resp.Body.Close() 477 body, err := ioutil.ReadAll(resp.Body) 478 if err != nil { 479 panic(err) 480 } 481 482 var response createIssueResponse 483 err = json.Unmarshal(body, &response) 484 if err != nil { 485 panic(err) 486 } 487 488 fmt.Printf("Created new issue: %s", response.WebURL) 489 }