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  }