[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