Skip to content

Commit

Permalink
Merge pull request #925 from 1Password/jh/approve-merge
Browse files Browse the repository at this point in the history
Set up application approval workflow and logic
  • Loading branch information
jodyheavener authored Mar 28, 2024
2 parents 945cb22 + ec435f7 commit 1703d23
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 11 deletions.
46 changes: 46 additions & 0 deletions .github/workflows/approve-application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Approve application

on:
issues:
types: [labeled]

jobs:
check:
runs-on: ubuntu-latest
outputs:
is_approved: ${{ steps.check_label.outputs.is_approved }}
approver_id: ${{ steps.check_label.outputs.approver_id }}
approver_username: ${{ steps.check_label.outputs.approver_username }}
steps:
- id: check_label
name: "Check if label is 'status: approved'"
run: |
echo "is_approved=$(echo ${{ github.event.label.name == 'status: approved' }})" >> $GITHUB_OUTPUT
echo "approver_id=${{ github.event.sender.id }}" >> $GITHUB_OUTPUT
echo "approver_username=${{ github.event.sender.login }}" >> $GITHUB_OUTPUT
approve:
needs: check
runs-on: ubuntu-latest
if: needs.check.outputs.is_approved == 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Fetch processor
uses: dsaltares/fetch-gh-release-asset@1.1.1
with:
file: "processor"
target: "./processor"

- name: Automated application approval
run: |
chmod +x ./processor
./processor approve
env:
APPROVER_ID: ${{ needs.check.outputs.approver_id }}
APPROVER_USERNAME: ${{ needs.check.outputs.approver_username }}
OP_BOT_PAT: ${{ secrets.OP_BOT_PAT }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
REPOSITORY_OWNER: ${{ github.repository_owner }}
REPOSITORY_NAME: ${{ github.event.repository.name }}
69 changes: 59 additions & 10 deletions script/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/json"
"fmt"
"log"
"regexp"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}

// FileName takes 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
Expand Down
103 changes: 103 additions & 0 deletions script/approver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package main

import (
"errors"
"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(errors.New("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.
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.
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))
}
5 changes: 4 additions & 1 deletion script/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
)

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

func getEnv(key string) (string, error) {
Expand Down Expand Up @@ -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()
Expand Down
60 changes: 60 additions & 0 deletions script/test-issues/valid-project-approved.json
Original file line number Diff line number Diff line change
@@ -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"
}
49 changes: 49 additions & 0 deletions script/test-issues/valid-project-closed.json
Original file line number Diff line number Diff line change
@@ -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"
}

0 comments on commit 1703d23

Please sign in to comment.