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"
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{
}
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())
log.Fatal(err)
}
if confirm {
- err = redmine.SetRelease(*i, releaseID)
+ err = r.SetRelease(*i, releaseID)
if err != nil {
log.Fatal(err)
} else {
}
}
if confirm || autoSet {
- err = redmine.SetRelease(*i, releaseID)
+ err = r.SetRelease(*i, releaseID)
if err != nil {
log.Fatal(err)
} else {
}
},
}
+
+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)
+ },
+}
--- /dev/null
+// 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
+}