From 1bc3c4a6555ecbd4f59ae050132874463913e9e1 Mon Sep 17 00:00:00 2001 From: David Zager Date: Wed, 31 Jan 2024 09:56:15 -0500 Subject: [PATCH] :sparkles: repo + labels + milestone config yaml (#55) What we want is to make it a bit easier for the uninitiated to get their bearings by: 1. Prefixing workflows for this repo with `_` to make the reusable ones more obvious. 2. Merging the labels and milestone config files into one `config.yaml`. This should make it a bit easier for us to see what the automation is doing, allow us to grow this config later, and enable further automation down the road. There are minor changes somewhat hidden in here, like putting a due date on `v0.4.0` milestone that I lazily decided to keep in this PR. Signed-off-by: David Zager --- .github/workflows/README.md | 6 + .github/workflows/{main.yml => _main.yml} | 0 .../{pr-closed.yaml => _pr-closed.yaml} | 0 .../workflows/{verifyPR.yml => _verifyPR.yml} | 0 .github/workflows/manage-labels.yaml | 25 -- .github/workflows/manage-milestone.yaml | 48 ---- README.md | 19 +- cmd/labels/action.yml | 31 --- cmd/labels/labels.yaml | 58 ----- cmd/labels/main.go | 232 +++++++---------- cmd/milestones/action.yml | 53 ---- cmd/milestones/main.go | 245 +++++++++++++----- pkg/config/config.go | 23 ++ pkg/config/config.yaml | 135 ++++++++++ pkg/config/types.go | 50 ++++ 15 files changed, 499 insertions(+), 426 deletions(-) create mode 100644 .github/workflows/README.md rename .github/workflows/{main.yml => _main.yml} (100%) rename .github/workflows/{pr-closed.yaml => _pr-closed.yaml} (100%) rename .github/workflows/{verifyPR.yml => _verifyPR.yml} (100%) delete mode 100644 .github/workflows/manage-labels.yaml delete mode 100644 .github/workflows/manage-milestone.yaml delete mode 100644 cmd/labels/action.yml delete mode 100644 cmd/labels/labels.yaml delete mode 100644 cmd/milestones/action.yml create mode 100644 pkg/config/config.go create mode 100644 pkg/config/config.yaml create mode 100644 pkg/config/types.go diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..fb03f43 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,6 @@ +GitHub Workflows for Release Tools +================================== + +Here you will find workflows for the administration of this repository, +prefixed like `_main.yaml` as well as [reusable workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows) +for use across Konveyor. diff --git a/.github/workflows/main.yml b/.github/workflows/_main.yml similarity index 100% rename from .github/workflows/main.yml rename to .github/workflows/_main.yml diff --git a/.github/workflows/pr-closed.yaml b/.github/workflows/_pr-closed.yaml similarity index 100% rename from .github/workflows/pr-closed.yaml rename to .github/workflows/_pr-closed.yaml diff --git a/.github/workflows/verifyPR.yml b/.github/workflows/_verifyPR.yml similarity index 100% rename from .github/workflows/verifyPR.yml rename to .github/workflows/_verifyPR.yml diff --git a/.github/workflows/manage-labels.yaml b/.github/workflows/manage-labels.yaml deleted file mode 100644 index 9417915..0000000 --- a/.github/workflows/manage-labels.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: Manage Labels - -on: - workflow_dispatch: - -jobs: - sync-labels: - name: Synchronize Labels - runs-on: ubuntu-latest - strategy: - matrix: - repos: - - org: konveyor - repo: release-tools - fail-fast: true - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Sync Labels - uses: ./cmd/labels - with: - github_token: ${{ secrets.GH_TOKEN }} - organization: ${{ matrix.repos.org }} - repository: ${{ matrix.repos.repo }} - diff --git a/.github/workflows/manage-milestone.yaml b/.github/workflows/manage-milestone.yaml deleted file mode 100644 index d26b57f..0000000 --- a/.github/workflows/manage-milestone.yaml +++ /dev/null @@ -1,48 +0,0 @@ -name: Manage Milestone - -on: - workflow_dispatch: - inputs: - title: - description: Title of the milestone - required: true - type: string - state: - description: The state (open|closed) of the milestone - required: false - default: "open" - type: string - description: - description: Description of the milestone - required: false - default: "" - type: string - due: - description: Due date (DateOnly format https://pkg.go.dev/time#pkg-constants) - required: false - default: "" - type: string - -jobs: - sync-labels: - name: Synchronize Labels - runs-on: ubuntu-latest - strategy: - matrix: - repos: - - org: konveyor - repo: release-tools - fail-fast: true - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Manage Milestone - uses: ./cmd/milestones - with: - github_token: ${{ secrets.GH_TOKEN }} - organization: ${{ matrix.repos.org }} - repository: ${{ matrix.repos.repo }} - title: ${{ inputs.title }} - state: ${{ inputs.state }} - description: ${{ inputs.description }} - due: ${{ inputs.due }} diff --git a/README.md b/README.md index 55c8578..b4a0f80 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,21 @@ Release Tools ============= -This project contains tooling for creating and managing releases for the Konveyor organization. +This project contains, or should contain, all of the configuration and +automation to maintain, build, and release Konveyor. -## Available Workflows -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fkonveyor%2Frelease-tools.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fkonveyor%2Frelease-tools?ref=badge_shield) +Check out the [config.yaml](./pkg/config/config.yaml) to see: + +1. The repositories we are managing +1. The Labels we are configuring in repositories +1. The milestones we are configuring in repositories + +This allows us to have a single source of truth to make sure that, as we create +enhancments, issues, and pull requests, they can be tracked properly. +You can find our reusable GitHub Workflows in [./.github/workflows](./.github/workflows). + +## Available Workflows ### Prepare repository for release @@ -39,6 +49,5 @@ for more information on how to get started. Refer to Konveyor's Code of Conduct [here](https://github.com/konveyor/community/blob/main/CODE_OF_CONDUCT.md). - ## License -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fkonveyor%2Frelease-tools.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fkonveyor%2Frelease-tools?ref=badge_large) \ No newline at end of file +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fkonveyor%2Frelease-tools.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fkonveyor%2Frelease-tools?ref=badge_shield) diff --git a/cmd/labels/action.yml b/cmd/labels/action.yml deleted file mode 100644 index 287aaba..0000000 --- a/cmd/labels/action.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: 'Synchronize Labels' -description: 'Synchronize Labels in Repository' -inputs: - github_token: - description: "the github_token provided by the actions runner" - required: true - type: string - organization: - description: "The organization" - required: true - type: string - repository: - description: "The repository" - required: true - type: string -runs: - using: composite - steps: - - name: Set up Go - uses: actions/setup-go@v3 - - name: See environment - run: env - shell: bash - - name: Run action - env: - GITHUB_TOKEN: ${{ inputs.github_token }} - run: | - cd ${GITHUB_ACTION_PATH} - go mod download - go run main.go -org ${{ inputs.organization }} -repo ${{ inputs.repository }} - shell: bash diff --git a/cmd/labels/labels.yaml b/cmd/labels/labels.yaml deleted file mode 100644 index 360d4d0..0000000 --- a/cmd/labels/labels.yaml +++ /dev/null @@ -1,58 +0,0 @@ -# https://github.com/kubernetes/test-infra/blob/master/label_sync/labels.yaml -# default: global configuration to be applied to all repos -# labels: list of labels - keys for each item: color, description, name, target, deleteAfter, previously -# deleteAfter: 2006-01-02T15:04:05Z (rfc3339) -# previously: list of previous labels (color name deleteAfter, previously) -# target: one of issues, prs, or both (also TBD) -# addedBy: human? prow plugin? other? -default: - labels: - # Triage - - color: ededed - description: Indicates an issue or PR lacks a `triage/foo` label and requires one. - name: needs-triage - - color: 8fc951 - description: Indicates an issue or PR is ready to be actively worked on. - name: triage/accepted - - color: d455d0 - description: Indicates an issue is a duplicate of other open issue. - name: triage/duplicate - - color: d455d0 - description: Indicates an issue needs more information in order to work on it. - name: triage/needs-information - - color: d455d0 - description: Indicates an issue can not be reproduced as described. - name: triage/not-reproducible - - color: d455d0 - description: Indicates an issue that is a support question. - name: triage/support - # Kind - - color: e11d21 - description: Categorizes issue or PR as related to a bug. - name: kind/bug - - color: c7def8 - description: Categorizes issue or PR as related to documentation. - name: kind/documentation - - color: c7def8 - description: Categorizes issue or PR as related to a new feature. - name: kind/feature - # Priority - - color: fef2c0 - description: Lowest priority. Possibly useful, but not yet enough support to actually get it done. # These are mostly place-holders for potentially good ideas, so that they don't get completely forgotten, and can be referenced /deduped every time they come up. - name: priority/awaiting-more-evidence - - color: fbca04 - description: Higher priority than priority/awaiting-more-evidence. # There appears to be general agreement that this would be good to have, but we may not have anyone available to work on it right now or in the immediate future. Community contributions would be most welcome in the mean time (although it might take a while to get them reviewed if reviewers are fully occupied with higher priority issues, for example immediately before a release). - name: priority/backlog - - color: e11d21 - description: Highest priority. Must be actively worked on as someone's top priority right now. # Stuff is burning. If it's not being actively worked on, someone is expected to drop what they're doing immediately to work on it. Team leaders are responsible for making sure that all the issues, labeled with this priority, in their area are being actively worked on. Examples include user-visible bugs in core features, broken builds or tests and critical security issues. - name: priority/critical-urgent - - color: eb6420 - description: Important over the long term, but may not be staffed and/or may need multiple releases to complete. - name: priority/important-longterm - - color: eb6420 - description: Must be staffed and worked on either currently, or very soon, ideally in time for the next release. - name: priority/important-soon - # Etcetera - - color: 15dd18 - description: Indicates that a PR is ready to be merged. - name: lgtm diff --git a/cmd/labels/main.go b/cmd/labels/main.go index 570092e..138d261 100644 --- a/cmd/labels/main.go +++ b/cmd/labels/main.go @@ -6,182 +6,140 @@ package main import ( "context" "flag" - "fmt" "log" "os" "strings" "github.com/google/go-github/v55/github" "github.com/konveyor/release-tools/pkg/action" + "github.com/konveyor/release-tools/pkg/config" "sigs.k8s.io/yaml" ) -const ( - CONFIG = "labels.yaml" -) - -// LabelTarget specifies the intent of the label (PR or issue) -// type LabelTarget string -// -// const ( -// prTarget LabelTarget = "prs" -// issueTarget LabelTarget = "issues" -// bothTarget LabelTarget = "both" -// ) - -// Label holds declarative data about the label. -type Label struct { - // Name is the current name of the label - Name string `json:"name"` - // Color is rrggbb or color - Color string `json:"color"` - // Description is brief text explaining its meaning, who can apply it - Description string `json:"description"` - // TODO(djzager): Consider using these if/when we need it - // // Target specifies whether it targets PRs, issues or both - // Target LabelTarget `json:"target"` - // // ProwPlugin specifies which prow plugin add/removes this label - // ProwPlugin string `json:"prowPlugin"` - // // IsExternalPlugin specifies if the prow plugin is external or not - // IsExternalPlugin bool `json:"isExternalPlugin"` - // // AddedBy specifies whether human/munger/bot adds the label - // AddedBy string `json:"addedBy"` - // // Previously lists deprecated names for this label - // Previously []Label `json:"previously,omitempty"` - // // DeleteAfter specifies the label is retired and a safe date for deletion - // DeleteAfter *time.Time `json:"deleteAfter,omitempty"` - // parent *Label // Current name for previous labels (used internally) -} - -// Configuratio ... for now ... there is only the default list of labels -// to be applied to all repositories -type Configuration struct { - Default RepoConfig `json:"default"` -} - -// RepoConfig contains only labels for the moment -type RepoConfig struct { - Labels []Label `json:"labels"` -} - -func (label *Label) Print() { - fmt.Printf("Name: %s\n", label.Name) - fmt.Printf("Color: %s\n", label.Color) - fmt.Printf("Description: %s\n", label.Description) - // fmt.Printf("Target: %s\n", label.Target) - // fmt.Printf("ProwPlugin: %s\n", label.ProwPlugin) - // fmt.Printf("IsExternalPlugin: %v\n", label.IsExternalPlugin) - // fmt.Printf("AddedBy: %s\n", label.AddedBy) - // if len(label.Previously) > 0 { - // fmt.Println("Previously:") - // for _, prevLabel := range label.Previously { - // fmt.Printf(" Name: %s\n", prevLabel.Name) - // // Add other fields as needed - // } - // } - // if label.DeleteAfter != nil { - // fmt.Printf("DeleteAfter: %s\n", label.DeleteAfter.Format(time.RFC3339)) - // } - fmt.Println("------------------------") +// Update a label in a repo +type Update struct { + Org string + Repo string + Why string + Wanted *config.Label `json:"wanted,omitempty"` + Current *config.Label `json:"current,omitempty"` } func main() { - orgPtr := flag.String("org", "", "The organization for the repo") - repoPtr := flag.String("repo", "", "The repository") - + configPtr := flag.String("config", "", "Path to config.yaml") + confirmPtr := flag.Bool("confirm", false, "Make mutating changes to labels via GitHub API") flag.Parse() - org := *orgPtr - if org == "" { - action.ErrorCommand("input 'organization' not defined") - os.Exit(1) - } - repo := *repoPtr - if repo == "" { - action.ErrorCommand("input 'repository' not defined") - os.Exit(1) - } + configPath := *configPtr + confirm := *confirmPtr - data, err := os.ReadFile(CONFIG) + c, err := config.LoadConfig(configPath) if err != nil { - action.ErrorCommand("Failed reading config") log.Fatal(err) } - // TODO(djzager): Should we validate this config? - var c Configuration - if err = yaml.Unmarshal(data, &c); err != nil { - action.ErrorCommand("Failed to unmarshal config") - log.Fatal(err) - } - action.NoticeCommand("Labels in Configuration:") - for _, label := range c.Default.Labels { - label.Print() - } - fmt.Println("#######################################") - fmt.Println() + defaultLabels := c.Labels // Instantiate the client and get the current labels on the repo client := action.GetClient() opt := &github.ListOptions{ PerPage: 100, } - var currentLabels []*github.Label - for { - labels, resp, err := client.Issues.ListLabels(context.Background(), org, repo, opt) - if err != nil { - action.ErrorCommand("Failed to get repo labels") - log.Fatal(err) + + updates := []Update{} + for _, r := range c.Repos { + // TODO(djzager): maybe have repo specific labels in the future + // repoLabels := append(defaultLabels, r.AddLabels...) + repoLabels := defaultLabels + + var currentLabels []*github.Label + for { + labels, resp, err := client.Issues.ListLabels(context.Background(), r.Org, r.Repo, opt) + if err != nil { + action.ErrorCommand("Failed to get repo labels") + log.Fatal(err) + } + currentLabels = append(currentLabels, labels...) + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage } - currentLabels = append(currentLabels, labels...) - if resp.NextPage == 0 { - break + + currentLabelsMap := make(map[string]*github.Label) + for _, label := range currentLabels { + currentLabelsMap[label.GetName()] = label + } + + // Compare labels + for _, l := range repoLabels { + label := l + existingLabel, exists := currentLabelsMap[l.Name] + if !exists { + updates = append(updates, Update{ + Org: r.Org, + Repo: r.Repo, + Why: "missing", + Wanted: &label, + Current: nil, + }) + continue + } + + if strings.ToLower(existingLabel.GetColor()) != strings.ToLower(l.Color) || + existingLabel.GetDescription() != l.Description { + + updates = append(updates, Update{ + Org: r.Org, + Repo: r.Repo, + Why: "changed", + Wanted: &label, + Current: &config.Label{ + Name: existingLabel.GetName(), + Color: existingLabel.GetColor(), + Description: existingLabel.GetDescription(), + }, + }) + } } - opt.Page = resp.NextPage } - currentLabelsMap := make(map[string]*github.Label) - action.NoticeCommand("Labels in the repository:") - for _, label := range currentLabels { - fmt.Printf("Name: %40s\tColor: %10s\tDescription: %30s\n", label.GetName(), label.GetColor(), label.GetDescription()) - currentLabelsMap[label.GetName()] = label + if len(updates) == 0 { + action.NoticeCommand("Yay, there are no changes to be made") + os.Exit(0) + } + y, _ := yaml.Marshal(updates) + + log.Print(string(y)) + + if !confirm { + action.NoticeCommand("Running without confirm, no mutations will be made") + os.Exit(0) } - fmt.Println("#######################################") - fmt.Println() - - // Compare labels and: - // 1 - create ones that do not exist - // 2 - modify ones that do but have the wrong color || description - action.NoticeCommand("Synchronizing labels") - for _, label := range c.Default.Labels { - existingLabel, exists := currentLabelsMap[label.Name] - if !exists { - action.NoticeCommand("Creating missing label") - label.Print() - _, _, err := client.Issues.CreateLabel(context.Background(), org, repo, &github.Label{ - Name: github.String(label.Name), - Color: github.String(label.Color), - Description: github.String(label.Description), + + for _, update := range updates { + switch update.Why { + case "missing": + _, _, err := client.Issues.CreateLabel(context.Background(), update.Org, update.Repo, &github.Label{ + Name: github.String(update.Wanted.Name), + Color: github.String(update.Wanted.Color), + Description: github.String(update.Wanted.Description), }) if err != nil { action.ErrorCommand("Error creating label") log.Fatal(err) } - action.NoticeCommand("Label " + label.Name + " created") - os.Exit(0) - } - - if strings.ToLower(existingLabel.GetColor()) != strings.ToLower(label.Color) || - existingLabel.GetDescription() != label.Description { - action.NoticeCommand("Modifying label") - _, _, err := client.Issues.EditLabel(context.Background(), org, repo, label.Name, &github.Label{ - Name: github.String(label.Name), - Color: github.String(label.Color), - Description: github.String(label.Description), + case "changed": + _, _, err := client.Issues.EditLabel(context.Background(), update.Org, update.Repo, update.Wanted.Name, &github.Label{ + Name: github.String(update.Wanted.Name), + Color: github.String(update.Wanted.Color), + Description: github.String(update.Wanted.Description), }) if err != nil { action.ErrorCommand("Error modifying label") log.Fatal(err) } + default: + panic("Should not happen") } } diff --git a/cmd/milestones/action.yml b/cmd/milestones/action.yml deleted file mode 100644 index e7eb076..0000000 --- a/cmd/milestones/action.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Manage Milestone -description: Manage a Milestone in a Repository -inputs: - github_token: - description: "the github_token provided by the actions runner" - required: true - type: string - organization: - description: "The organization" - required: true - type: string - repository: - description: "The repository" - required: true - type: string - title: - description: Title of the milestone - required: true - type: string - state: - description: The state (open|closed) of the milestone - required: true - type: string - description: - description: Description of the milestone - required: true - type: string - due: - description: Due date (DateOnly format https://pkg.go.dev/time#pkg-constants) - required: true - type: string -runs: - using: composite - steps: - - name: Set up Go - uses: actions/setup-go@v3 - - name: See environment - run: env - shell: bash - - name: Run action - env: - GITHUB_TOKEN: ${{ inputs.github_token }} - ORGANIZATION: ${{ inputs.organization }} - REPOSITORY: ${{ inputs.repository }} - TITLE: ${{ inputs.title }} - STATE: ${{ inputs.state }} - DESCRIPTION: ${{ inputs.description }} - DUE: ${{ inputs.due }} - run: | - cd ${GITHUB_ACTION_PATH} - go mod download - go run main.go - shell: bash diff --git a/cmd/milestones/main.go b/cmd/milestones/main.go index c3b68eb..b99cb7b 100644 --- a/cmd/milestones/main.go +++ b/cmd/milestones/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "flag" "fmt" "log" "os" @@ -9,94 +10,200 @@ import ( "github.com/google/go-github/v55/github" "github.com/konveyor/release-tools/pkg/action" + "github.com/konveyor/release-tools/pkg/config" + "gopkg.in/yaml.v2" ) +// Update a milestone in a repo +type Update struct { + Org string + Repo string + Why string + Wanted *config.Milestone `json:"wanted,omitempty"` + Current *config.Milestone `json:"current,omitempty"` + Issues []int +} + func main() { - org := os.Getenv("ORGANIZATION") - repo := os.Getenv("REPOSITORY") - title := os.Getenv("TITLE") - state := os.Getenv("STATE") - desc := os.Getenv("DESCRIPTION") - due := os.Getenv("DUE") - - if org == "" { - action.ErrorCommand("input 'organization' not defined") - os.Exit(1) - } - if repo == "" { - action.ErrorCommand("input 'repository' not defined") - os.Exit(1) + configPtr := flag.String("config", "", "Path to config.yaml") + confirmPtr := flag.Bool("confirm", false, "Make mutating changes to labels via GitHub API") + flag.Parse() + configPath := *configPtr + confirm := *confirmPtr + + c, err := config.LoadConfig(configPath) + if err != nil { + log.Fatal(err) } - if title == "" { - action.ErrorCommand("input 'title' not defined") - os.Exit(1) + wantedMilestones := c.Milestones + + // Instantiate the client and get the current labels on the repo + client := action.GetClient() + listOptions := github.ListOptions{ + PerPage: 100, } - if state != "open" && state != "closed" { - action.ErrorCommand("input 'state' must be 'open' or 'closed'") - os.Exit(1) + milestoneListOptions := &github.MilestoneListOptions{ + State: "all", + ListOptions: listOptions, } - var dueTime *github.Timestamp - if due != "" { - parsedTime, err := time.Parse(time.DateOnly, due) - if err != nil { - fmt.Println("Error: " + err.Error()) - action.ErrorCommand("input 'due' not parseable as DateOnly time") - os.Exit(1) + updates := []Update{} + for _, r := range c.Repos { + var currentMilestones []*github.Milestone + for { + milestones, resp, err := client.Issues.ListMilestones(context.Background(), r.Org, r.Repo, milestoneListOptions) + if err != nil { + action.ErrorCommand("Failed to get repo milestones") + log.Fatal(err) + } + currentMilestones = append(currentMilestones, milestones...) + if resp.NextPage == 0 { + break + } + milestoneListOptions.Page = resp.NextPage + } + + currentMilestonesMap := make(map[string]*github.Milestone) + for _, m := range currentMilestones { + currentMilestonesMap[m.GetTitle()] = m + } + + for _, m := range wantedMilestones { + wantMilestone := m + + repoIssues := []int{} + if wantMilestone.Replaces != "" { + oldMilestone, oldMilestoneExists := currentMilestonesMap[wantMilestone.Replaces] + if oldMilestoneExists { + issueListByRepoOpts := &github.IssueListByRepoOptions{ + Milestone: fmt.Sprintf("%x", oldMilestone.GetNumber()), + ListOptions: listOptions, + } + + // just need their number + for { + issues, resp, err := client.Issues.ListByRepo(context.Background(), r.Org, r.Repo, issueListByRepoOpts) + if err != nil { + action.ErrorCommand("Failed to get repo issues") + log.Fatal(err) + } + + for _, i := range issues { + repoIssues = append(repoIssues, i.GetNumber()) + } + if resp.NextPage == 0 { + break + } + issueListByRepoOpts.Page = resp.NextPage + } + } + } + + existingMilestone, exists := currentMilestonesMap[wantMilestone.Title] + if !exists { + updates = append(updates, Update{ + Org: r.Org, + Repo: r.Repo, + Why: "missing", + Wanted: &wantMilestone, + Current: nil, + Issues: repoIssues, + }) + continue + } + + // Counting on this to return empty string if unset + existingMilestoneDue := existingMilestone.GetDueOn().Time.Format(time.DateOnly) + if existingMilestoneDue == "0001-01-01" { + existingMilestoneDue = "" + } + if existingMilestone.GetDescription() != wantMilestone.Description || + existingMilestoneDue != wantMilestone.Due || + existingMilestone.GetState() != wantMilestone.State || + len(repoIssues) > 0 { + updates = append(updates, Update{ + Org: r.Org, + Repo: r.Repo, + Why: "changed", + Wanted: &wantMilestone, + Current: &config.Milestone{ + Title: existingMilestone.GetTitle(), + Description: existingMilestone.GetDescription(), + State: existingMilestone.GetState(), + Due: existingMilestoneDue, + Number: existingMilestone.GetNumber(), + }, + Issues: repoIssues, + }) + } } - dueOn := github.Timestamp{Time: parsedTime} - dueTime = &dueOn } - desiredMilestone := github.Milestone{ - Title: &title, - State: &state, - Description: &desc, - DueOn: dueTime, + if len(updates) == 0 { + action.NoticeCommand("Yay, there are no changes to be made") + os.Exit(0) } + y, _ := yaml.Marshal(updates) - // Instantiate the client and get milestones - client := action.GetClient() - opt := &github.MilestoneListOptions{ - ListOptions: github.ListOptions{ - PerPage: 100, - }, + log.Print(string(y)) + + if !confirm { + action.NoticeCommand("Running without confirm, no mutations will be made") + os.Exit(0) } - // Make sure we get all the milestones - var currentMilestones []*github.Milestone - for { - labels, resp, err := client.Issues.ListMilestones(context.Background(), org, repo, opt) - if err != nil { - action.ErrorCommand("Failed to get repo labels") - log.Fatal(err) + for _, update := range updates { + var dueOn *github.Timestamp + if update.Wanted.Due != "" { + parsedTime, err := time.Parse(time.DateOnly, update.Wanted.Due) + if err != nil { + log.Fatal(err) + } + // add some time to make sure it registers as correct day + dueOn = &github.Timestamp{Time: parsedTime.Add(12 * time.Hour)} } - currentMilestones = append(currentMilestones, labels...) - if resp.NextPage == 0 { - break - } - opt.Page = resp.NextPage - } - // Grab the milestone number if it already exists - for _, milestone := range currentMilestones { - if *milestone.Title == *desiredMilestone.Title { - desiredMilestone.Number = milestone.Number + milestone := &github.Milestone{} + switch update.Why { + case "missing": + milestone, _, err = client.Issues.CreateMilestone(context.Background(), update.Org, update.Repo, &github.Milestone{ + Title: github.String(update.Wanted.Title), + Description: github.String(update.Wanted.Description), + State: github.String(update.Wanted.State), + DueOn: dueOn, + }) + if err != nil { + action.ErrorCommand("Error creating milestone") + log.Fatal(err) + } + log.Printf("[%v/%v] Milestone %v created", update.Org, update.Repo, update.Wanted) + case "changed": + milestone, _, err = client.Issues.EditMilestone(context.Background(), update.Org, update.Repo, update.Current.Number, &github.Milestone{ + Title: github.String(update.Wanted.Title), + Description: github.String(update.Wanted.Description), + State: github.String(update.Wanted.State), + DueOn: dueOn, + }) + if err != nil { + action.ErrorCommand("Error modifying milestone") + log.Fatal(err) + } + log.Printf("[%v/%v] Milestone %v updated", update.Org, update.Repo, update.Wanted) + default: + panic("Should not happen") } - } - if desiredMilestone.Number == nil { - _, _, err := client.Issues.CreateMilestone(context.Background(), org, repo, &desiredMilestone) - if err != nil { - action.ErrorCommand("Error creating milestone") - log.Fatal(err) + for _, i := range update.Issues { + milestoneNumber := milestone.GetNumber() + _, _, err := client.Issues.Edit(context.Background(), update.Org, update.Repo, i, &github.IssueRequest{ + Milestone: &milestoneNumber, + }) + if err != nil { + action.ErrorCommand("Failed to move issue to milestone") + log.Fatal(err) + } + log.Printf("[%v/%v] Issue %v added to milestone %v", update.Org, update.Repo, i, update.Wanted.Title) } - return - } - _, _, err := client.Issues.EditMilestone(context.Background(), org, repo, *desiredMilestone.Number, &desiredMilestone) - if err != nil { - action.ErrorCommand("Error editingmilestone") - log.Fatal(err) } action.NoticeCommand("Yay") diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..5b367ee --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,23 @@ +package config + +import ( + "os" + + "github.com/konveyor/release-tools/pkg/action" + "gopkg.in/yaml.v2" +) + +func LoadConfig(path string) (*Configuration, error) { + data, err := os.ReadFile(path) + if err != nil { + action.ErrorCommand("Failed reading config") + return nil, err + } + + var c Configuration + if err = yaml.Unmarshal(data, &c); err != nil { + action.ErrorCommand("Failed to unmarshal config") + return nil, err + } + return &c, nil +} diff --git a/pkg/config/config.yaml b/pkg/config/config.yaml new file mode 100644 index 0000000..2ecc162 --- /dev/null +++ b/pkg/config/config.yaml @@ -0,0 +1,135 @@ +# This configuration is specifically for managing the repos listed below and their: +# - labels +# - milestones +# - ... + +# Repos +# List of repositories we are managing. We may, in the future, worry about +# repositories in konveyor-ecosystem. +# +# repos: +# - org: the organization of the repo +# repo: the repo +repos: + - org: konveyor + repo: enhancements + - org: konveyor + repo: release-tools + - org: konveyor + repo: operator + - org: konveyor + repo: java-analyzer-bundle + - org: konveyor + repo: analyzer-lsp + - org: konveyor + repo: windup-shim + - org: konveyor + repo: tackle2-addon-analyzer + - org: konveyor + repo: tackle2-hub + - org: konveyor + repo: tackle2-seed + - org: konveyor + repo: tackle2-ui + - org: konveyor + repo: tackle2-addon + - org: konveyor + repo: static-report + - org: konveyor + repo: kantra + +# Labels +# List of labels, their color and description, that should exist in the specified repositories. +# +# labels: +# - color: the color of the label +# description: what does it mean? +# name: the name of the label +labels: + # Triage + - color: ededed + description: Indicates an issue or PR lacks a `triage/foo` label and requires one. + name: needs-triage + - color: 8fc951 + description: Indicates an issue or PR is ready to be actively worked on. + name: triage/accepted + - color: d455d0 + description: Indicates an issue is a duplicate of other open issue. + name: triage/duplicate + - color: d455d0 + description: Indicates an issue needs more information in order to work on it. + name: triage/needs-information + - color: d455d0 + description: Indicates an issue can not be reproduced as described. + name: triage/not-reproducible + - color: d455d0 + description: Indicates an issue that is a support question. + name: triage/support + # Kind + - color: e11d21 + description: Categorizes issue or PR as related to a bug. + name: kind/bug + - color: c7def8 + description: Categorizes issue or PR as related to documentation. + name: kind/documentation + - color: c7def8 + description: Categorizes issue or PR as related to a new feature. + name: kind/feature + # Priority + - color: fef2c0 + description: Lowest priority. Possibly useful, but not yet enough support to actually get it done. # These are mostly place-holders for potentially good ideas, so that they don't get completely forgotten, and can be referenced /deduped every time they come up. + name: priority/awaiting-more-evidence + - color: fbca04 + description: Higher priority than priority/awaiting-more-evidence. # There appears to be general agreement that this would be good to have, but we may not have anyone available to work on it right now or in the immediate future. Community contributions would be most welcome in the mean time (although it might take a while to get them reviewed if reviewers are fully occupied with higher priority issues, for example immediately before a release). + name: priority/backlog + - color: eb6420 + description: Important over the long term, but may not be staffed and/or may need multiple releases to complete. + name: priority/important-longterm + - color: eb6420 + description: Must be staffed and worked on either currently, or very soon, ideally in time for the next release. + name: priority/important-soon + - color: e11d21 + description: Must be staffed and worked in time for the next release. + name: priority/release-blocker + - color: e11d21 + description: Highest priority. Must be actively worked on as someone's top priority right now. # Stuff is burning. If it's not being actively worked on, someone is expected to drop what they're doing immediately to work on it. Team leaders are responsible for making sure that all the issues, labeled with this priority, in their area are being actively worked on. Examples include user-visible bugs in core features, broken builds or tests and critical security issues. + name: priority/critical-urgent + # Etcetera + - color: 15dd18 + description: Indicates that a PR is ready to be merged. + name: lgtm + # CherryPick + - color: fef2c0 + description: This PR should be cherry-picked to release-0.3 branch. + name: cherry-pick/release-0.3 + + +# Milestones +# List of milestones, and their state, that should exist in the specified repositories. +# +# milestones: +# - title: the title for the milestone +# description: the description +# state: open/closed +# due: +# +milestones: + - title: 0.3-beta.2 + description: The second beta for v0.3.0 release cycle + state: closed + due: 2023-11-02 + - title: v0.3.0 + description: The v0.3.0 release of Konveyor + state: closed + due: 2024-01-24 + replaces: 0.3-beta.2 + - title: v0.3.1 + description: The v0.3.1 release of Konveyor + state: open + - title: v0.4.0 + description: The v0.4.0 release of Konveyor + state: open + due: 2024-04-30 + - title: Next + description: Bucket for work we want to accomplish in the next release + state: open diff --git a/pkg/config/types.go b/pkg/config/types.go new file mode 100644 index 0000000..0033bd6 --- /dev/null +++ b/pkg/config/types.go @@ -0,0 +1,50 @@ +package config + +// Configuration is a representation of the repositories we will manage +// + their Labels +// + their Milestons +type Configuration struct { + Repos []Repo `json:"repos"` + Labels []Label `json:"labels"` + Milestones []Milestone `json:"milestone"` +} + +// Repo represents the "coordinates" to a repository +type Repo struct { + Org string `json:"org"` + Repo string `json:"repo"` +} + +// Label holds declarative data about the label. +type Label struct { + // Name is the current name of the label + Name string `json:"name"` + // Color is rrggbb or color + Color string `json:"color"` + // Description is brief text explaining its meaning, who can apply it + Description string `json:"description"` + // TODO(djzager): Consider using these if/when we need it + // // Target specifies whether it targets PRs, issues or both + // Target LabelTarget `json:"target"` + // // ProwPlugin specifies which prow plugin add/removes this label + // ProwPlugin string `json:"prowPlugin"` + // // IsExternalPlugin specifies if the prow plugin is external or not + // IsExternalPlugin bool `json:"isExternalPlugin"` + // // AddedBy specifies whether human/munger/bot adds the label + // AddedBy string `json:"addedBy"` + // // Previously lists deprecated names for this label + // Previously []Label `json:"previously,omitempty"` + // // DeleteAfter specifies the label is retired and a safe date for deletion + // DeleteAfter *time.Time `json:"deleteAfter,omitempty"` + // parent *Label // Current name for previous labels (used internally) +} + +// Milestone holds declarative data about the milestone. +type Milestone struct { + Number int `json:"number,omitempty"` + Title string `json:"title"` + Description string `json:"description"` + State string `json:"state"` + Due string `json:"due"` + Replaces string `json:"replaces"` +}