diff --git a/script/application.go b/script/application.go index 1a988a30..22d75048 100644 --- a/script/application.go +++ b/script/application.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "log" + "regexp" + "strconv" "strings" "time" @@ -40,13 +42,14 @@ type Application struct { sections map[string]string `json:"-"` Problems []error `json:"-"` - Account string `json:"account"` - Project Project `json:"project"` - Applicant Applicant `json:"applicant"` - CanContact bool `json:"can_contact"` - ApproverId int `json:"approver_id,omitempty"` - IssueNumber int `json:"issue_number"` - CreatedAt time.Time `json:"created_at"` + Account string `json:"account"` + Project Project `json:"project"` + Applicant Applicant `json:"applicant"` + CanContact bool `json:"can_contact"` + ApproverId int `json:"approver_id,omitempty"` + ApproverUsername string `json:"-"` + IssueNumber int `json:"issue_number"` + CreatedAt time.Time `json:"created_at"` } func (a *Application) Parse(issue *github.Issue) { @@ -97,7 +100,7 @@ func (a *Application) Parse(issue *github.Issue) { a.CanContact = a.boolSection("Can we contact you?", false, ParseCheckbox) if isTestingIssue() { - debugMessage("Application data:", a.GetData()) + debugMessage("Application data:", string(a.GetData())) } for _, err := range a.validator.Errors { @@ -119,13 +122,59 @@ func (a *Application) RenderProblems() string { return strings.Join(problemStrings, "\n") } -func (a *Application) GetData() string { +func (a *Application) GetData() []byte { data, err := json.MarshalIndent(a, "", "\t") if err != nil { log.Fatalf("Could not marshal Application data: %s", err.Error()) } - return string(data) + return data +} + +// Take the application issue number and project name and turn it into +// a file path. This will always be unique because it is relying on +// github's issue numbers +// e.g. 782-foo.json +func (a *Application) FileName() string { + filename := fmt.Sprintf("%s-%s.json", + strconv.FormatInt(int64(a.IssueNumber), 10), + strings.ToLower(a.Project.Name), + ) + + filename = strings.ReplaceAll(strings.ToLower(filename), " ", "-") + filename = regexp.MustCompile(`[^\w.-]`).ReplaceAllString(filename, "") + filename = regexp.MustCompile(`-+`).ReplaceAllString(filename, "-") + + return filename +} + +func (a *Application) SetApprover() error { + if isTestingIssue() { + a.ApproverId = 123 + a.ApproverUsername = "test-username" + + return nil + } + + approverIdValue, err := getEnv("APPROVER_ID") + if err != nil { + return err + } + + approverId, err := strconv.Atoi(approverIdValue) + if err != nil { + return err + } + + approverUsername, err := getEnv("APPROVER_USERNAME") + if err != nil { + return err + } + + a.ApproverId = approverId + a.ApproverUsername = approverUsername + + return nil } // Take the Markdown-format body of an issue and break it down by section header diff --git a/script/approver.go b/script/approver.go new file mode 100644 index 00000000..65652616 --- /dev/null +++ b/script/approver.go @@ -0,0 +1,102 @@ +package main + +import ( + "fmt" + "log" + "path/filepath" + "strings" +) + +type Approver struct { + gitHub GitHub + application Application +} + +func (a *Approver) Approve() { + a.gitHub = GitHub{} + a.application = Application{} + + if err := a.application.SetApprover(); err != nil { + a.printErrorAndExit(err) + } + + if err := a.gitHub.Init(); err != nil { + a.printErrorAndExit(err) + } + + if *a.gitHub.Issue.State == "closed" { + a.printErrorAndExit(fmt.Errorf("script run on closed issue")) + } + + if !a.gitHub.IssueHasLabel(LabelStatusApproved) { + a.printErrorAndExit( + fmt.Errorf("script run on issue that does not have required '%s' label", LabelStatusApproved), + ) + } + + a.application.Parse(a.gitHub.Issue) + + if !a.application.IsValid() { + a.printErrorAndExit( + fmt.Errorf("script run on issue with invalid application data:\n\n%s", a.renderProblems()), + ) + } + + // The reviewer may remove this label themselves, but + // let's double check and remove it if they haven't + if a.gitHub.IssueHasLabel(LabelStatusReviewing) { + if err := a.gitHub.RemoveIssueLabel(LabelStatusReviewing); err != nil { + a.printErrorAndExit( + fmt.Errorf("could not remove issue label '%s': %s", LabelStatusReviewing, err.Error()), + ) + } + } + + if err := a.gitHub.CommitNewFile( + filepath.Join("data", a.application.FileName()), + a.application.GetData(), + fmt.Sprintf("Added \"%s\" to program", a.application.Project.Name), + ); err != nil { + a.printErrorAndExit( + fmt.Errorf("could not create commit: %s", err.Error()), + ) + } + + if err := a.gitHub.CreateIssueComment(a.getApprovalMessage()); err != nil { + a.printErrorAndExit( + fmt.Errorf("could not create issue comment: %s", err.Error()), + ) + } + + if err := a.gitHub.CloseIssue(); err != nil { + a.printErrorAndExit( + fmt.Errorf("could not close issue: %s", err.Error()), + ) + } +} + +func (a *Approver) printErrorAndExit(err error) { + log.Fatalf("Error approving application: %s\n", err.Error()) +} + +func (a *Approver) renderProblems() string { + var problemStrings []string + + for _, err := range a.application.Problems { + problemStrings = append(problemStrings, fmt.Sprintf("- %s", err.Error())) + } + + return strings.Join(problemStrings, "\n") +} + +func (a *Approver) getApprovalMessage() string { + return strings.TrimSpace(fmt.Sprintf(`### 🎉 Your application has been approved + +Congratulations, @%s has approved your application! A promotion will be applied to your 1Password account shortly. + +If you haven't done so already, we highly recommend implementing a [recovery plan for your team](https://support.1password.com/team-recovery-plan/) in case access for a particular contributor is ever lost. You may add additional core contributors as you see fit. + +Finally, we'd love to hear more about your experience using 1Password in your development workflows! Feel free to join us over on the [1Password Developers Slack](https://join.slack.com/t/1password-devs/shared_invite/zt-15k6lhima-GRb5Ga~fo7mjS9xPzDaF2A) workspace. + +Welcome to the program and happy coding! 🧑‍💻`, a.application.ApproverUsername)) +} diff --git a/script/main.go b/script/main.go index d9346581..f6a2786f 100644 --- a/script/main.go +++ b/script/main.go @@ -7,7 +7,7 @@ import ( ) func printUsageAndExit() { - log.Fatalf("Usage: ./processor [--test-issue ]") + log.Fatalf("Usage: ./processor [--test-issue ]") } func getEnv(key string) (string, error) { @@ -38,6 +38,9 @@ func main() { case "review": reviewer := Reviewer{} reviewer.Review() + case "approve": + approver := Approver{} + approver.Approve() default: fmt.Printf("Invalid command: %s\n", command) printUsageAndExit() diff --git a/script/test-issues/valid-project-approved.json b/script/test-issues/valid-project-approved.json new file mode 100644 index 00000000..e1a48a73 --- /dev/null +++ b/script/test-issues/valid-project-approved.json @@ -0,0 +1,60 @@ +{ + "id": 1801650328, + "number": 6, + "state": "open", + "locked": false, + "title": "Application for TestDB", + "body": "### Account URL\n\ntestdb.1password.com\n\n### Non-commercial confirmation\n\n- [X] No, this account won't be used for commercial activity\n\n### Team application\n\n- [ ] Yes, this application is for a team\n\n### Event application\n\n- [ ] Yes, this application is for an event\n\n### Project name\n\nTestDB\n\n### Short description\n\nTestDB is a free and open source, community-based forum software project.\n\n### Number of team members/core contributors\n\n1\n\n### Homepage URL\n\nhttps://github.com/wendyappleed/test-db\n\n### Repository URL\n\nhttps://github.com/wendyappleed/test-db\n\n### License type\n\nMIT\n\n### License URL\n\nhttps://github.com/wendyappleed/test-db/blob/main/LICENSE.md\n\n### Age confirmation\n\n- [X] Yes, this project is at least 30 days old\n\n### Name\n\nWendy Appleseed\n\n### Email\n\nwendyappleseed@example.com\n\n### Project role\n\nCore Maintainer\n\n### Profile or website\n\nhttps://github.com/wendyappleseed/\n\n### Can we contact you?\n\n- [X] Yes, you may contact me\n\n### Additional comments\n\nThank you!", + "user": { + "login": "wendyappleseed", + "id": 38230737, + "node_id": "MDQ6VXNlcjYzOTIwNDk=", + "avatar_url": "https://avatars.githubusercontent.com/u/38230737?v=4", + "html_url": "https://github.com/wendyappleseed", + "gravatar_id": "", + "type": "User", + "site_admin": false, + "url": "https://api.github.com/users/wendyappleseed", + "events_url": "https://api.github.com/users/wendyappleseed/events{/privacy}", + "following_url": "https://api.github.com/users/wendyappleseed/following{/other_user}", + "followers_url": "https://api.github.com/users/wendyappleseed/followers", + "gists_url": "https://api.github.com/users/wendyappleseed/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/wendyappleseed/orgs", + "received_events_url": "https://api.github.com/users/wendyappleseed/received_events", + "repos_url": "https://api.github.com/users/wendyappleseed/repos", + "starred_url": "https://api.github.com/users/wendyappleseed/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wendyappleseed/subscriptions" + }, + "labels": [ + { + "id": 5728067083, + "url": "https://api.github.com/repos/1Password/1password-teams-open-source/labels/status:%20approved", + "name": "status: approved", + "color": "0052CC", + "description": "The application has been approved", + "default": false, + "node_id": "LA_kwDOJ6JE6M8AAAABVWteCw" + } + ], + "comments": 11, + "closed_at": "2023-07-13T05:03:51Z", + "created_at": "2023-07-12T19:49:35Z", + "updated_at": "2023-07-13T05:03:51Z", + "url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6", + "html_url": "https://github.com/wendyappleseed/1password-teams-open-source/issues/6", + "comments_url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/comments", + "events_url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/events", + "labels_url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/labels{/name}", + "repository_url": "https://api.github.com/repos/1Password/1password-teams-open-source", + "reactions": { + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "confused": 0, + "heart": 0, + "hooray": 0, + "url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/reactions" + }, + "node_id": "I_kwDOJ6JE6M5rYwCY" +} diff --git a/script/test-issues/valid-project-closed.json b/script/test-issues/valid-project-closed.json new file mode 100644 index 00000000..60958930 --- /dev/null +++ b/script/test-issues/valid-project-closed.json @@ -0,0 +1,49 @@ +{ + "id": 1801650328, + "number": 6, + "state": "closed", + "locked": false, + "title": "Application for TestDB", + "body": "### Account URL\n\ntestdb.1password.com\n\n### Non-commercial confirmation\n\n- [X] No, this account won't be used for commercial activity\n\n### Team application\n\n- [ ] Yes, this application is for a team\n\n### Event application\n\n- [ ] Yes, this application is for an event\n\n### Project name\n\nTestDB\n\n### Short description\n\nTestDB is a free and open source, community-based forum software project.\n\n### Number of team members/core contributors\n\n1\n\n### Homepage URL\n\nhttps://github.com/wendyappleed/test-db\n\n### Repository URL\n\nhttps://github.com/wendyappleed/test-db\n\n### License type\n\nMIT\n\n### License URL\n\nhttps://github.com/wendyappleed/test-db/blob/main/LICENSE.md\n\n### Age confirmation\n\n- [X] Yes, this project is at least 30 days old\n\n### Name\n\nWendy Appleseed\n\n### Email\n\nwendyappleseed@example.com\n\n### Project role\n\nCore Maintainer\n\n### Profile or website\n\nhttps://github.com/wendyappleseed/\n\n### Can we contact you?\n\n- [X] Yes, you may contact me\n\n### Additional comments\n\nThank you!", + "user": { + "login": "wendyappleseed", + "id": 38230737, + "node_id": "MDQ6VXNlcjYzOTIwNDk=", + "avatar_url": "https://avatars.githubusercontent.com/u/38230737?v=4", + "html_url": "https://github.com/wendyappleseed", + "gravatar_id": "", + "type": "User", + "site_admin": false, + "url": "https://api.github.com/users/wendyappleseed", + "events_url": "https://api.github.com/users/wendyappleseed/events{/privacy}", + "following_url": "https://api.github.com/users/wendyappleseed/following{/other_user}", + "followers_url": "https://api.github.com/users/wendyappleseed/followers", + "gists_url": "https://api.github.com/users/wendyappleseed/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/wendyappleseed/orgs", + "received_events_url": "https://api.github.com/users/wendyappleseed/received_events", + "repos_url": "https://api.github.com/users/wendyappleseed/repos", + "starred_url": "https://api.github.com/users/wendyappleseed/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wendyappleseed/subscriptions" + }, + "comments": 11, + "closed_at": "2023-07-13T05:03:51Z", + "created_at": "2023-07-12T19:49:35Z", + "updated_at": "2023-07-13T05:03:51Z", + "url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6", + "html_url": "https://github.com/wendyappleseed/1password-teams-open-source/issues/6", + "comments_url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/comments", + "events_url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/events", + "labels_url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/labels{/name}", + "repository_url": "https://api.github.com/repos/1Password/1password-teams-open-source", + "reactions": { + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "confused": 0, + "heart": 0, + "hooray": 0, + "url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/reactions" + }, + "node_id": "I_kwDOJ6JE6M5rYwCY" +}