[ARVADOS-DEV] created: 6d23fa62bd8dddf0850c3fa8e07bebfb1bafb4ee

Git user git at public.arvados.org
Thu Nov 18 20:16:56 UTC 2021


        at  6d23fa62bd8dddf0850c3fa8e07bebfb1bafb4ee (commit)


commit 6d23fa62bd8dddf0850c3fa8e07bebfb1bafb4ee
Author: Ward Vandewege <ward at curii.com>
Date:   Thu Nov 18 15:16:33 2021 -0500

    18093: add cmd/art/TASKS to the .licenseignore file
    
    Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward at curii.com>

diff --git a/.licenseignore b/.licenseignore
index ab55dbe..cc1b258 100644
--- a/.licenseignore
+++ b/.licenseignore
@@ -6,3 +6,4 @@ jenkins/packer-images/*.json
 jenkins/packer-images/1078ECD7.asc
 go.mod
 go.sum
+cmd/art/TASKS

commit 369a7040e86491570e71d55cbc59bceacab5203f
Author: Ward Vandewege <ward at curii.com>
Date:   Wed Nov 17 20:18:00 2021 -0500

    18093: add functionality to create a new release checklist ticket in
           Redmine.
    
    Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward at curii.com>

diff --git a/cmd/art/TASKS b/cmd/art/TASKS
new file mode 100644
index 0000000..4fdee2c
--- /dev/null
+++ b/cmd/art/TASKS
@@ -0,0 +1,19 @@
+Prepare release branch
+Review release branch
+Create next redmine release
+Record git commit
+Build RC packages
+Draft release notes
+Review release notes
+Test installer
+Deploy RC packages to playground
+Run test pipeline
+Sign off
+Push packages to stable
+Publish docker image, python and ruby packages
+Publish formula/installer for release
+Publish arvbox image
+Tag commits
+Update doc site
+Publish release on arvados.org
+Send out release notification
diff --git a/cmd/art/redmine.go b/cmd/art/redmine.go
index 4836428..27c14e0 100644
--- a/cmd/art/redmine.go
+++ b/cmd/art/redmine.go
@@ -5,15 +5,18 @@
 package main
 
 import (
+	"bufio"
 	"fmt"
 	"log"
 	"os"
 	"regexp"
 	"sort"
 	"strconv"
+	"time"
 
 	"git.arvados.org/arvados-dev.git/lib/redmine"
 	survey "github.com/AlecAivazis/survey/v2"
+	"github.com/Masterminds/semver"
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing"
 	"github.com/go-git/go-git/v5/plumbing/object"
@@ -56,6 +59,23 @@ func init() {
 	findAndAssociateIssuesCmd.Flags().BoolP("auto-set", "a", false, "Associate issues without existing release without prompting")
 	findAndAssociateIssuesCmd.Flags().BoolP("skip-release-change", "s", false, "Skip issues already assigned to another release (do not prompt)")
 	issuesCmd.AddCommand(findAndAssociateIssuesCmd)
+
+	createReleaseIssueCmd.Flags().StringP("new-release-version", "n", "", "Semantic version number of the new release")
+	err = createReleaseIssueCmd.MarkFlagRequired("new-release-version")
+	if err != nil {
+		log.Fatalf(err.Error())
+	}
+	createReleaseIssueCmd.Flags().IntP("sprint", "s", 0, "Redmine sprint (aka Version) ID")
+	err = createReleaseIssueCmd.MarkFlagRequired("sprint")
+	if err != nil {
+		log.Fatalf(err.Error())
+	}
+	createReleaseIssueCmd.Flags().IntP("project", "p", 0, "Redmine project ID")
+	err = createReleaseIssueCmd.MarkFlagRequired("project")
+	if err != nil {
+		log.Fatalf(err.Error())
+	}
+	issuesCmd.AddCommand(createReleaseIssueCmd)
 }
 
 var redmineCmd = &cobra.Command{
@@ -254,13 +274,13 @@ var findAndAssociateIssuesCmd = &cobra.Command{
 		}
 		sort.Ints(keys)
 
-		redmine := redmine.NewClient(conf.Endpoint, conf.Apikey)
+		r := redmine.NewClient(conf.Endpoint, conf.Apikey)
 
 		for c, k := range keys {
 			fmt.Printf("%d (%d/%d): ", k, c+1, len(keys))
 			// Look up the issue, see if it is already associated with the desired release
 
-			i, err := redmine.GetIssue(k)
+			i, err := r.GetIssue(k)
 			if err != nil {
 				fmt.Println()
 				fmt.Printf("[error] unable to retrieve issue: %s\n", err.Error())
@@ -283,7 +303,7 @@ var findAndAssociateIssuesCmd = &cobra.Command{
 						log.Fatal(err)
 					}
 					if confirm {
-						err = redmine.SetRelease(*i, releaseID)
+						err = r.SetRelease(*i, releaseID)
 						if err != nil {
 							log.Fatal(err)
 						} else {
@@ -306,7 +326,7 @@ var findAndAssociateIssuesCmd = &cobra.Command{
 					}
 				}
 				if confirm || autoSet {
-					err = redmine.SetRelease(*i, releaseID)
+					err = r.SetRelease(*i, releaseID)
 					if err != nil {
 						log.Fatal(err)
 					} else {
@@ -318,3 +338,114 @@ var findAndAssociateIssuesCmd = &cobra.Command{
 		}
 	},
 }
+
+var createReleaseIssueCmd = &cobra.Command{
+	Use:   "create-release-issue",
+	Short: "Create a release ticket with numbered subtasks for all the steps on the release checklist",
+	Long: "Create a release ticket with numbered subtasks for all the steps on the release checklist.\n" +
+		"\nThe subtask subjects are read from a file named TASKS in the current directory.\n" +
+		"\nFinally, a new Redmine release will also be created for the next release.\n" +
+		"\nThe REDMINE_ENDPOINT environment variable must be set to the base URL of your redmine server." +
+		"\nThe REDMINE_APIKEY environment variable must be set to your redmine API key.",
+	Run: func(cmd *cobra.Command, args []string) {
+		newReleaseVersion, err := cmd.Flags().GetString("new-release-version")
+		if err != nil {
+			log.Fatal(fmt.Errorf("[error] can not get new release version: %s", err))
+			return
+		}
+
+		versionID, err := cmd.Flags().GetInt("sprint")
+		if err != nil {
+			log.Fatal(fmt.Errorf("[error] can not convert Redmine sprint (version) ID to integer: %s", err))
+			return
+		}
+		projectID, err := cmd.Flags().GetInt("project")
+		if err != nil {
+			log.Fatal(fmt.Errorf("[error] can not convert Redmine project ID to integer: %s", err))
+			return
+		}
+
+		r := redmine.NewClient(conf.Endpoint, conf.Apikey)
+
+		// Does this project exist?
+		project, err := r.GetProject(projectID)
+		if err != nil {
+			log.Fatalf("[error] can not find project with id %d: %s", projectID, err)
+		}
+
+		// Is the sprint (aka "version" in redmine) in the correct state?
+		v, err := r.Version(versionID)
+		if err != nil {
+			log.Fatal(fmt.Errorf("[error] can not find sprint with id %d: %s", versionID, err))
+		}
+		if v.Status != "open" {
+			log.Fatal(fmt.Errorf("[error] the sprint must be open; the status of the sprint with id %d is '%s'", v.ID, v.Status))
+		}
+
+		i, err := r.FindOrCreateIssue("Release Arvados "+newReleaseVersion, 0, v.ID, projectID)
+		if err != nil {
+			log.Fatal(err)
+		}
+		if i.Status.Name != "New" {
+			log.Fatal(fmt.Errorf("the release ticket status must be 'New'; the status of the release issue with id %d is '%s'", i.ID, v.Status))
+		}
+
+		fmt.Printf("[ok] the release ticket is '%s' with ID #%d (%s/issues/%d)\n", i.Subject, i.ID, conf.Endpoint, i.ID)
+
+		// Get the list of subtasks from the "TASKS" file
+		tasks, err := os.Open("TASKS")
+		if err != nil {
+			log.Fatal(fmt.Errorf("[error] unable to open the \"TASKS\" file: %s", err.Error()))
+		}
+		defer tasks.Close()
+
+		scanner := bufio.NewScanner(tasks)
+		count := 0
+		for scanner.Scan() {
+			task := scanner.Text()
+			taskIssue, err := r.FindOrCreateIssue(fmt.Sprintf("%d. %s", count, task), i.ID, v.ID, projectID)
+			fmt.Printf("[ok] #%d: %d. %s\n", taskIssue.ID, count, task)
+			count++
+			if err != nil {
+				log.Fatal(fmt.Errorf("Error reading from file: %s", err))
+			}
+		}
+
+		// Create the next release in Redmine
+		version, err := semver.NewVersion(newReleaseVersion)
+		if err != nil {
+			log.Fatalf("Error parsing version: %s", err)
+		}
+		nextVersion := version.IncPatch()
+
+		var release *redmine.Release
+
+		release, err = r.FindReleaseByName(project.Name, "Arvados "+nextVersion.String())
+		if err != nil {
+			log.Fatalf("Error finding release with name %s in project with name %s: %s", release.Name, project.Name, err)
+		}
+		if release == nil {
+			// No release found, create it
+			release = &redmine.Release{}
+			release.Name = "Arvados " + nextVersion.String()
+			release.Sharing = "hierarchy"
+			release.ReleaseStartDate = time.Now().AddDate(0, 0, 7*1).Format("2006-01-02") // arbitrary choice, 1 week from today
+			release.ReleaseEndDate = time.Now().AddDate(0, 0, 7*5).Format("2006-01-02")   // also arbitrary, 5 weeks from today
+			release.ProjectID = projectID
+			release.Status = "open"
+			// Populate Project
+			tmp, err := r.GetProject(release.ProjectID)
+			if err != nil {
+				log.Fatalf("Unable to find project with ID %d: %s", release.ProjectID, err)
+			}
+			release.Project = &redmine.IDName{ID: release.ProjectID, Name: tmp.Name}
+
+			tmpRelease, err := r.CreateRelease(*release)
+			if err != nil {
+				log.Fatalf("Unable to create release: %s", err)
+			}
+			release = tmpRelease
+		}
+		fmt.Printf("[ok] the redmine release object for the next release is '%s' (%s/rb/release/%d)\n", release.Name, conf.Endpoint, release.ID)
+	},
+}
diff --git a/go.mod b/go.mod
index bf39b75..af7ce06 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.17
 
 require (
 	github.com/AlecAivazis/survey/v2 v2.3.2
+	github.com/Masterminds/semver v1.5.0
 	github.com/go-git/go-git/v5 v5.4.2
 	github.com/spf13/cobra v1.2.1
 	github.com/spf13/viper v1.9.0
diff --git a/go.sum b/go.sum
index e37a349..a4f2353 100644
--- a/go.sum
+++ b/go.sum
@@ -47,6 +47,8 @@ github.com/AlecAivazis/survey/v2 v2.3.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO
 github.com/AlecAivazis/survey/v2 v2.3.2/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
+github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
 github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
 github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
 github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
@@ -147,7 +149,6 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
diff --git a/lib/redmine/issues.go b/lib/redmine/issues.go
index 19f4c3e..e16eeba 100644
--- a/lib/redmine/issues.go
+++ b/lib/redmine/issues.go
@@ -118,14 +118,14 @@ func (c *Client) CreateIssue(issue Issue) (*Issue, error) {
 	if err != nil {
 		return nil, err
 	}
-	res, err := c.Put("/issues.json", string(s))
+	res, err := c.Post("/issues.json", string(s))
 	if err != nil {
 		return nil, err
 	}
 	defer res.Body.Close()
 
 	var r issueWrapper
-	err = responseHelper(res, &r, 200)
+	err = responseHelper(res, &r, 201)
 	if err != nil {
 		return nil, err
 	}
@@ -198,10 +198,13 @@ func (c *Client) FindOrCreateIssue(subject string, parentID int, versionID int,
 	issue.FixedVersionID = versionID
 	issue.Subject = subject
 	if parentID != 0 {
-		issue.Parent = &ID{ID: parentID}
+		issue.ParentIssueID = parentID
 	}
 
 	i, err := c.CreateIssue(issue)
+	if err != nil {
+		return Issue{}, err
+	}
 	return *i, err
 }
 
diff --git a/lib/redmine/projects.go b/lib/redmine/projects.go
new file mode 100644
index 0000000..f942c13
--- /dev/null
+++ b/lib/redmine/projects.go
@@ -0,0 +1,44 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+// Somewhat inspired by https://github.com/mattn/go-redmine (MIT licensed)
+
+package redmine
+
+import (
+	"strconv"
+)
+
+type projectWrapper struct {
+	Project Project `json:"project"`
+}
+
+type projectsResult struct {
+	Projects []Project `json:"projects"`
+}
+
+type Project struct {
+	ID          int    `json:"id"`
+	Parent      IDName `json:"parent"`
+	Name        string `json:"name"`
+	IDentifier  string `json:"identifier"`
+	Description string `json:"description"`
+	CreatedOn   string `json:"created_on"`
+	UpdatedOn   string `json:"updated_on"`
+}
+
+func (c *Client) GetProject(id int) (*Project, error) {
+	res, err := c.Get("/projects/" + strconv.Itoa(id) + ".json")
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+
+	var r projectWrapper
+	err = responseHelper(res, &r, 200)
+	if err != nil {
+		return nil, err
+	}
+	return &r.Project, nil
+}
diff --git a/lib/redmine/redmine.go b/lib/redmine/redmine.go
index c8a2677..70a93f4 100644
--- a/lib/redmine/redmine.go
+++ b/lib/redmine/redmine.go
@@ -50,6 +50,20 @@ func (c *Client) Get(url string) (*http.Response, error) {
 	return res, err
 }
 
+func (c *Client) Post(url string, payload string) (*http.Response, error) {
+	req, err := http.NewRequest("POST", c.endpoint+url, strings.NewReader(payload))
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Add("X-Redmine-API-Key", c.apikey)
+	res, err := c.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	return res, err
+}
+
 func (c *Client) Put(url string, payload string) (*http.Response, error) {
 	req, err := http.NewRequest("PUT", c.endpoint+url, strings.NewReader(payload))
 	if err != nil {
diff --git a/lib/redmine/releases.go b/lib/redmine/releases.go
new file mode 100644
index 0000000..4a13e3f
--- /dev/null
+++ b/lib/redmine/releases.go
@@ -0,0 +1,73 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package redmine
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"strings"
+)
+
+type Release struct {
+	ID               int     `json:"id"`
+	Name             string  `json:"name"`
+	Description      string  `json:"description"`
+	Sharing          string  `json:"sharing"`
+	ReleaseStartDate string  `json:"release_start_date"`
+	ReleaseEndDate   string  `json:"release_end_date"`
+	PlannedVelocity  string  `json:"planned_velocity"`
+	Status           string  `json:"status"`
+	ProjectID        int     `json:"-"`
+	Project          *IDName `json:"-"`
+}
+
+type releaseWrapper struct {
+	Release Release `json:"release"`
+}
+
+// FindReleaseByName retrieves a redmine Release object by name
+func (c *Client) FindReleaseByName(project, name string) (*Release, error) {
+	// This api call only returns the first matching release object. There is no unique index on release names.
+	res, err := c.Get("/rb/release/" + strings.ToLower(project) + "/find_by_name.json?name=" + url.QueryEscape(name))
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+
+	if res.StatusCode == 404 {
+		return nil, fmt.Errorf("Missing API call /rb/release/project_id/find_by_name.json")
+	}
+	var r releaseWrapper
+	err = responseHelper(res, &r, 200)
+	if err != nil {
+		return nil, err
+	}
+	if r.Release.ID == 0 {
+		return nil, nil
+	}
+	return &r.Release, nil
+}
+
+func (c *Client) CreateRelease(release Release) (*Release, error) {
+	var rr releaseWrapper
+	rr.Release = release
+	s, err := json.Marshal(rr)
+	if err != nil {
+		return nil, err
+	}
+	res, err := c.Post("/rb/release/"+strings.ToLower(release.Project.Name)+"/new.json", string(s))
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+
+	var r releaseWrapper
+	err = responseHelper(res, r, 200)
+	if err != nil {
+		return nil, err
+	}
+	return &r.Release, nil
+}
diff --git a/lib/redmine/version.go b/lib/redmine/version.go
new file mode 100644
index 0000000..9135dc0
--- /dev/null
+++ b/lib/redmine/version.go
@@ -0,0 +1,66 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package redmine
+
+import (
+	//	"encoding/json"
+	"errors"
+	"strconv"
+	//	"strings"
+)
+
+type versionWrapper struct {
+	Version Version `json:"version"`
+}
+
+type versionsResult struct {
+	Versions []Version `json:"versions"`
+}
+
+type Version struct {
+	ID          int    `json:"id"`
+	Project     IDName `json:"project"`
+	Name        string `json:"name"`
+	Description string `json:"description"`
+	Status      string `json:"status"`
+	DueDate     string `json:"due_date"`
+	CreatedOn   string `json:"created_on"`
+	UpdatedOn   string `json:"updated_on"`
+}
+
+func (c *Client) Version(id int) (*Version, error) {
+	res, err := c.Get("/versions/" + strconv.Itoa(id) + ".json")
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+
+	if res.StatusCode == 404 {
+		return nil, errors.New("Not Found")
+	}
+
+	var r versionWrapper
+	err = responseHelper(res, &r, 200)
+	if err != nil {
+		return nil, err
+	}
+	return &r.Version, nil
+	/*
+		decoder := json.NewDecoder(res.Body)
+		var r versionWrapper
+		if res.StatusCode != 200 {
+			var er errorsResult
+			err = decoder.Decode(&er)
+			if err == nil {
+				err = errors.New(strings.Join(er.Errors, "\n"))
+			}
+		} else {
+			err = decoder.Decode(&r)
+		}
+		if err != nil {
+			return nil, err
+		} */
+	return &r.Version, nil
+}

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list