Skip to content

Commit 1703d23

Browse files
authored
Merge pull request #925 from 1Password/jh/approve-merge
Set up application approval workflow and logic
2 parents 945cb22 + ec435f7 commit 1703d23

File tree

6 files changed

+321
-11
lines changed

6 files changed

+321
-11
lines changed
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Approve application
2+
3+
on:
4+
issues:
5+
types: [labeled]
6+
7+
jobs:
8+
check:
9+
runs-on: ubuntu-latest
10+
outputs:
11+
is_approved: ${{ steps.check_label.outputs.is_approved }}
12+
approver_id: ${{ steps.check_label.outputs.approver_id }}
13+
approver_username: ${{ steps.check_label.outputs.approver_username }}
14+
steps:
15+
- id: check_label
16+
name: "Check if label is 'status: approved'"
17+
run: |
18+
echo "is_approved=$(echo ${{ github.event.label.name == 'status: approved' }})" >> $GITHUB_OUTPUT
19+
echo "approver_id=${{ github.event.sender.id }}" >> $GITHUB_OUTPUT
20+
echo "approver_username=${{ github.event.sender.login }}" >> $GITHUB_OUTPUT
21+
22+
approve:
23+
needs: check
24+
runs-on: ubuntu-latest
25+
if: needs.check.outputs.is_approved == 'true'
26+
steps:
27+
- name: Checkout repository
28+
uses: actions/checkout@v4
29+
30+
- name: Fetch processor
31+
uses: dsaltares/[email protected]
32+
with:
33+
file: "processor"
34+
target: "./processor"
35+
36+
- name: Automated application approval
37+
run: |
38+
chmod +x ./processor
39+
./processor approve
40+
env:
41+
APPROVER_ID: ${{ needs.check.outputs.approver_id }}
42+
APPROVER_USERNAME: ${{ needs.check.outputs.approver_username }}
43+
OP_BOT_PAT: ${{ secrets.OP_BOT_PAT }}
44+
ISSUE_NUMBER: ${{ github.event.issue.number }}
45+
REPOSITORY_OWNER: ${{ github.repository_owner }}
46+
REPOSITORY_NAME: ${{ github.event.repository.name }}

script/application.go

+59-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"encoding/json"
55
"fmt"
66
"log"
7+
"regexp"
8+
"strconv"
79
"strings"
810
"time"
911

@@ -40,13 +42,14 @@ type Application struct {
4042
sections map[string]string `json:"-"`
4143
Problems []error `json:"-"`
4244

43-
Account string `json:"account"`
44-
Project Project `json:"project"`
45-
Applicant Applicant `json:"applicant"`
46-
CanContact bool `json:"can_contact"`
47-
ApproverId int `json:"approver_id,omitempty"`
48-
IssueNumber int `json:"issue_number"`
49-
CreatedAt time.Time `json:"created_at"`
45+
Account string `json:"account"`
46+
Project Project `json:"project"`
47+
Applicant Applicant `json:"applicant"`
48+
CanContact bool `json:"can_contact"`
49+
ApproverId int `json:"approver_id,omitempty"`
50+
ApproverUsername string `json:"-"`
51+
IssueNumber int `json:"issue_number"`
52+
CreatedAt time.Time `json:"created_at"`
5053
}
5154

5255
func (a *Application) Parse(issue *github.Issue) {
@@ -97,7 +100,7 @@ func (a *Application) Parse(issue *github.Issue) {
97100
a.CanContact = a.boolSection("Can we contact you?", false, ParseCheckbox)
98101

99102
if isTestingIssue() {
100-
debugMessage("Application data:", a.GetData())
103+
debugMessage("Application data:", string(a.GetData()))
101104
}
102105

103106
for _, err := range a.validator.Errors {
@@ -119,13 +122,59 @@ func (a *Application) RenderProblems() string {
119122
return strings.Join(problemStrings, "\n")
120123
}
121124

122-
func (a *Application) GetData() string {
125+
func (a *Application) GetData() []byte {
123126
data, err := json.MarshalIndent(a, "", "\t")
124127
if err != nil {
125128
log.Fatalf("Could not marshal Application data: %s", err.Error())
126129
}
127130

128-
return string(data)
131+
return data
132+
}
133+
134+
// FileName takes application issue number and project name and turn it
135+
// into a file path. This will always be unique because it is relying on
136+
// GitHub's issue numbers
137+
// e.g. 782-foo.json
138+
func (a *Application) FileName() string {
139+
filename := fmt.Sprintf("%s-%s.json",
140+
strconv.FormatInt(int64(a.IssueNumber), 10),
141+
strings.ToLower(a.Project.Name),
142+
)
143+
144+
filename = strings.ReplaceAll(strings.ToLower(filename), " ", "-")
145+
filename = regexp.MustCompile(`[^\w.-]`).ReplaceAllString(filename, "")
146+
filename = regexp.MustCompile(`-+`).ReplaceAllString(filename, "-")
147+
148+
return filename
149+
}
150+
151+
func (a *Application) SetApprover() error {
152+
if isTestingIssue() {
153+
a.ApproverId = 123
154+
a.ApproverUsername = "test-username"
155+
156+
return nil
157+
}
158+
159+
approverIdValue, err := getEnv("APPROVER_ID")
160+
if err != nil {
161+
return err
162+
}
163+
164+
approverId, err := strconv.Atoi(approverIdValue)
165+
if err != nil {
166+
return err
167+
}
168+
169+
approverUsername, err := getEnv("APPROVER_USERNAME")
170+
if err != nil {
171+
return err
172+
}
173+
174+
a.ApproverId = approverId
175+
a.ApproverUsername = approverUsername
176+
177+
return nil
129178
}
130179

131180
// Take the Markdown-format body of an issue and break it down by section header

script/approver.go

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"log"
7+
"path/filepath"
8+
"strings"
9+
)
10+
11+
type Approver struct {
12+
gitHub GitHub
13+
application Application
14+
}
15+
16+
func (a *Approver) Approve() {
17+
a.gitHub = GitHub{}
18+
a.application = Application{}
19+
20+
if err := a.application.SetApprover(); err != nil {
21+
a.printErrorAndExit(err)
22+
}
23+
24+
if err := a.gitHub.Init(); err != nil {
25+
a.printErrorAndExit(err)
26+
}
27+
28+
if *a.gitHub.Issue.State == "closed" {
29+
a.printErrorAndExit(errors.New("script run on closed issue"))
30+
}
31+
32+
if !a.gitHub.IssueHasLabel(LabelStatusApproved) {
33+
a.printErrorAndExit(
34+
fmt.Errorf("script run on issue that does not have required '%s' label", LabelStatusApproved),
35+
)
36+
}
37+
38+
a.application.Parse(a.gitHub.Issue)
39+
40+
if !a.application.IsValid() {
41+
a.printErrorAndExit(
42+
fmt.Errorf("script run on issue with invalid application data:\n\n%s", a.renderProblems()),
43+
)
44+
}
45+
46+
// The reviewer may remove this label themselves, but
47+
// let's double check and remove it if they haven't
48+
if a.gitHub.IssueHasLabel(LabelStatusReviewing) {
49+
if err := a.gitHub.RemoveIssueLabel(LabelStatusReviewing); err != nil {
50+
a.printErrorAndExit(
51+
fmt.Errorf("could not remove issue label '%s': %s", LabelStatusReviewing, err.Error()),
52+
)
53+
}
54+
}
55+
56+
if err := a.gitHub.CommitNewFile(
57+
filepath.Join("data", a.application.FileName()),
58+
a.application.GetData(),
59+
fmt.Sprintf("Added \"%s\" to program", a.application.Project.Name),
60+
); err != nil {
61+
a.printErrorAndExit(
62+
fmt.Errorf("could not create commit: %s", err.Error()),
63+
)
64+
}
65+
66+
if err := a.gitHub.CreateIssueComment(a.getApprovalMessage()); err != nil {
67+
a.printErrorAndExit(
68+
fmt.Errorf("could not create issue comment: %s", err.Error()),
69+
)
70+
}
71+
72+
if err := a.gitHub.CloseIssue(); err != nil {
73+
a.printErrorAndExit(
74+
fmt.Errorf("could not close issue: %s", err.Error()),
75+
)
76+
}
77+
}
78+
79+
func (a *Approver) printErrorAndExit(err error) {
80+
log.Fatalf("Error approving application: %s\n", err.Error())
81+
}
82+
83+
func (a *Approver) renderProblems() string {
84+
var problemStrings []string
85+
86+
for _, err := range a.application.Problems {
87+
problemStrings = append(problemStrings, fmt.Sprintf("- %s", err.Error()))
88+
}
89+
90+
return strings.Join(problemStrings, "\n")
91+
}
92+
93+
func (a *Approver) getApprovalMessage() string {
94+
return strings.TrimSpace(fmt.Sprintf(`### 🎉 Your application has been approved
95+
96+
Congratulations, @%s has approved your application! A promotion will be applied to your 1Password account shortly.
97+
98+
To lower the risk of lockout, [assign at least one other person to help with account recovery](https://support.1password.com/team-recovery-plan/) in case access for a particular team member is ever lost. You may add additional core contributors as you see fit.
99+
100+
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.
101+
102+
Welcome to the program and happy coding! 🧑‍💻`, a.application.ApproverUsername))
103+
}

script/main.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
)
88

99
func printUsageAndExit() {
10-
log.Fatalf("Usage: ./processor <review> [--test-issue <issue name>]")
10+
log.Fatal("Usage: ./processor <review|approve> [--test-issue <issue name>]")
1111
}
1212

1313
func getEnv(key string) (string, error) {
@@ -38,6 +38,9 @@ func main() {
3838
case "review":
3939
reviewer := Reviewer{}
4040
reviewer.Review()
41+
case "approve":
42+
approver := Approver{}
43+
approver.Approve()
4144
default:
4245
fmt.Printf("Invalid command: %s\n", command)
4346
printUsageAndExit()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"id": 1801650328,
3+
"number": 6,
4+
"state": "open",
5+
"locked": false,
6+
"title": "Application for TestDB",
7+
"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\[email protected]\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!",
8+
"user": {
9+
"login": "wendyappleseed",
10+
"id": 38230737,
11+
"node_id": "MDQ6VXNlcjYzOTIwNDk=",
12+
"avatar_url": "https://avatars.githubusercontent.com/u/38230737?v=4",
13+
"html_url": "https://github.com/wendyappleseed",
14+
"gravatar_id": "",
15+
"type": "User",
16+
"site_admin": false,
17+
"url": "https://api.github.com/users/wendyappleseed",
18+
"events_url": "https://api.github.com/users/wendyappleseed/events{/privacy}",
19+
"following_url": "https://api.github.com/users/wendyappleseed/following{/other_user}",
20+
"followers_url": "https://api.github.com/users/wendyappleseed/followers",
21+
"gists_url": "https://api.github.com/users/wendyappleseed/gists{/gist_id}",
22+
"organizations_url": "https://api.github.com/users/wendyappleseed/orgs",
23+
"received_events_url": "https://api.github.com/users/wendyappleseed/received_events",
24+
"repos_url": "https://api.github.com/users/wendyappleseed/repos",
25+
"starred_url": "https://api.github.com/users/wendyappleseed/starred{/owner}{/repo}",
26+
"subscriptions_url": "https://api.github.com/users/wendyappleseed/subscriptions"
27+
},
28+
"labels": [
29+
{
30+
"id": 5728067083,
31+
"url": "https://api.github.com/repos/1Password/1password-teams-open-source/labels/status:%20approved",
32+
"name": "status: approved",
33+
"color": "0052CC",
34+
"description": "The application has been approved",
35+
"default": false,
36+
"node_id": "LA_kwDOJ6JE6M8AAAABVWteCw"
37+
}
38+
],
39+
"comments": 11,
40+
"closed_at": "2023-07-13T05:03:51Z",
41+
"created_at": "2023-07-12T19:49:35Z",
42+
"updated_at": "2023-07-13T05:03:51Z",
43+
"url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6",
44+
"html_url": "https://github.com/wendyappleseed/1password-teams-open-source/issues/6",
45+
"comments_url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/comments",
46+
"events_url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/events",
47+
"labels_url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/labels{/name}",
48+
"repository_url": "https://api.github.com/repos/1Password/1password-teams-open-source",
49+
"reactions": {
50+
"total_count": 0,
51+
"+1": 0,
52+
"-1": 0,
53+
"laugh": 0,
54+
"confused": 0,
55+
"heart": 0,
56+
"hooray": 0,
57+
"url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/reactions"
58+
},
59+
"node_id": "I_kwDOJ6JE6M5rYwCY"
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"id": 1801650328,
3+
"number": 6,
4+
"state": "closed",
5+
"locked": false,
6+
"title": "Application for TestDB",
7+
"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\[email protected]\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!",
8+
"user": {
9+
"login": "wendyappleseed",
10+
"id": 38230737,
11+
"node_id": "MDQ6VXNlcjYzOTIwNDk=",
12+
"avatar_url": "https://avatars.githubusercontent.com/u/38230737?v=4",
13+
"html_url": "https://github.com/wendyappleseed",
14+
"gravatar_id": "",
15+
"type": "User",
16+
"site_admin": false,
17+
"url": "https://api.github.com/users/wendyappleseed",
18+
"events_url": "https://api.github.com/users/wendyappleseed/events{/privacy}",
19+
"following_url": "https://api.github.com/users/wendyappleseed/following{/other_user}",
20+
"followers_url": "https://api.github.com/users/wendyappleseed/followers",
21+
"gists_url": "https://api.github.com/users/wendyappleseed/gists{/gist_id}",
22+
"organizations_url": "https://api.github.com/users/wendyappleseed/orgs",
23+
"received_events_url": "https://api.github.com/users/wendyappleseed/received_events",
24+
"repos_url": "https://api.github.com/users/wendyappleseed/repos",
25+
"starred_url": "https://api.github.com/users/wendyappleseed/starred{/owner}{/repo}",
26+
"subscriptions_url": "https://api.github.com/users/wendyappleseed/subscriptions"
27+
},
28+
"comments": 11,
29+
"closed_at": "2023-07-13T05:03:51Z",
30+
"created_at": "2023-07-12T19:49:35Z",
31+
"updated_at": "2023-07-13T05:03:51Z",
32+
"url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6",
33+
"html_url": "https://github.com/wendyappleseed/1password-teams-open-source/issues/6",
34+
"comments_url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/comments",
35+
"events_url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/events",
36+
"labels_url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/labels{/name}",
37+
"repository_url": "https://api.github.com/repos/1Password/1password-teams-open-source",
38+
"reactions": {
39+
"total_count": 0,
40+
"+1": 0,
41+
"-1": 0,
42+
"laugh": 0,
43+
"confused": 0,
44+
"heart": 0,
45+
"hooray": 0,
46+
"url": "https://api.github.com/repos/1Password/1password-teams-open-source/issues/6/reactions"
47+
},
48+
"node_id": "I_kwDOJ6JE6M5rYwCY"
49+
}

0 commit comments

Comments
 (0)