Skip to content

Commit

Permalink
atlasaction: allow config approver users on the action
Browse files Browse the repository at this point in the history
  • Loading branch information
giautm committed Sep 18, 2024
1 parent 1449122 commit b48c456
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 3 deletions.
99 changes: 96 additions & 3 deletions atlasaction/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"time"

"ariga.io/atlas-go-sdk/atlasexec"
"github.com/shurcooL/githubv4"
"golang.org/x/oauth2"
)

Expand Down Expand Up @@ -526,6 +527,10 @@ func (a *Actions) SchemaPlan(ctx context.Context) error {
return nil
// Branch context with pending plan
case tc.PullRequest == nil && len(planFiles) == 1:
// Check the approval checks for the schema plan.
if err = a.ApprovalChecks(ctx, tc); err != nil {
return err
}
result, err := a.Atlas.SchemaPlanApprove(ctx, &atlasexec.SchemaPlanApproveParams{
ConfigURL: params.ConfigURL,
Env: params.Env,
Expand Down Expand Up @@ -665,6 +670,37 @@ func (a *Actions) SchemaApply(ctx context.Context) error {
return nil
}

// ApprovalChecks checks the approval checks for the schema plan.
// It will fail the action if the approval checks are not satisfied.
func (a *Actions) ApprovalChecks(ctx context.Context, tc *TriggerContext) error {
if !a.GetBoolInput("require-approval") {
return nil
}
triggerComment := "/approved"
approvers := a.GetArrayInput("approvers")
c := a.GithubClient(tc.Repo, tc.SCM.APIURL)
pr, err := c.MergedPullRequest(ctx, tc.Commit)
if err != nil {
return fmt.Errorf("failed to get the pull request: %w", err)
}
comments, err := c.getIssueComments(ctx, pr)
if err != nil {
return fmt.Errorf("failed to get the pull request comments: %w", err)
}
approved := slices.IndexFunc(comments, func(c githubIssueComment) bool {
if c.AuthorHasAccess() && c.HasCommand(triggerComment) {
return len(approvers) == 0 || slices.Contains(approvers, c.Author())
}
return false
})
if approved == -1 {
a.Errorf("Please comment %q on the pull request to approve the action: %s", triggerComment, pr.URL)
return errors.New("Approval checks failed")
}
a.Infof("The action approved by %q", comments[approved].Author())
return nil
}

// WorkingDir returns the working directory for the action.
func (a *Actions) WorkingDir() string {
return a.GetInput("working-directory")
Expand Down Expand Up @@ -1025,10 +1061,15 @@ func RenderTemplate(name string, data any) (string, error) {

type (
githubIssueComment struct {
ID int `json:"id"`
Body string `json:"body"`
ID int `json:"id"`
Body string `json:"body"`
AuthorAssociation string `json:"author_association"`
User githubUser `json:"user"`
}
githubUser struct {
ID int `json:"id"`
Login string `json:"login"`
}

pullRequestComment struct {
ID int `json:"id,omitempty"`
Body string `json:"body"`
Expand All @@ -1046,11 +1087,48 @@ type (
baseURL string
repo string
client *http.Client
gql *githubv4.Client
}
)

const defaultGHApiUrl = "https://api.github.com"

func (g *githubAPI) MergedPullRequest(ctx context.Context, commitID string) (*PullRequest, error) {
owner, repo, err := g.ownerRepo()
if err != nil {
return nil, err
}
var resp struct {
Repository struct {
Object struct {
Commit struct {
AssociatedPullRequests struct {
Nodes []struct {
Number int
URL string
}
} `graphql:"associatedPullRequests(first: 1)"`
} `graphql:"... on Commit"`
} `graphql:"object(expression: $commit)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}
err = g.gql.Query(ctx, &resp, map[string]any{
"owner": githubv4.String(owner),
"name": githubv4.String(repo),
"commit": githubv4.String(commitID),
})
if err != nil {
return nil, err
}
if prs := resp.Repository.Object.Commit.AssociatedPullRequests.Nodes; len(prs) > 0 {
return &PullRequest{
Number: prs[0].Number,
URL: prs[0].URL,
}, nil
}
return nil, nil
}

func (g *githubAPI) UpsertComment(ctx context.Context, pr *PullRequest, id, comment string) error {
comments, err := g.getIssueComments(ctx, pr)
if err != nil {
Expand Down Expand Up @@ -1325,6 +1403,21 @@ func (g *githubAPI) ownerRepo() (string, string, error) {
return s[0], s[1], nil
}

// AuthorHasAccess returns true if the author has write access to the repository.
func (c githubIssueComment) AuthorHasAccess() bool {
return slices.Contains([]string{"OWNER", "MEMBER", "COLLABORATOR"}, c.AuthorAssociation)
}

// Author returns the author of the comment.
func (c githubIssueComment) Author() string {
return c.User.Login
}

// HasCommand returns true if the comment has the given command.
func (c githubIssueComment) HasCommand(cmd string) bool {
return strings.Contains(c.Body, cmd)
}

// commentMarker creates a hidden marker to identify the comment as one created by this action.
func commentMarker(id string) string {
return fmt.Sprintf(`<!-- generated by ariga/atlas-action for %v -->`, id)
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/mitchellh/mapstructure v1.1.2
github.com/rogpeppe/go-internal v1.12.1-0.20240709150035-ccf4b4329d21
github.com/sethvargo/go-githubactions v1.3.0
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7
github.com/stretchr/testify v1.8.4
golang.org/x/oauth2 v0.22.0
)
Expand All @@ -23,6 +24,7 @@ require (
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
github.com/zclconf/go-cty v1.14.1 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.14.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sethvargo/go-githubactions v1.3.0 h1:Kg633LIUV2IrJsqy2MfveiED/Ouo+H2P0itWS0eLh8A=
github.com/sethvargo/go-githubactions v1.3.0/go.mod h1:7/4WeHgYfSz9U5vwuToCK9KPnELVHAhGtRwLREOQV80=
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkvclm+pWm1Lk4YrREb4IOIb/YdFO0p2M=
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA=
Expand Down
9 changes: 9 additions & 0 deletions schema/plan/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ inputs:
description: |
The name of the plan. By default, Atlas will generate a name based on the schema changes.
required: false
require-approval:
description: |
Whether to require approval before applying the plan. By default, Atlas will not require approval.
default: 'false'
required: false
approvers:
description: |
A list of approvers for the plan. By default, all users with write access to the repository are approvers.
required: false
outputs:
plan:
description: The plan to be applied or generated. (ig. `atlas://<schema>/plans/<id>`)
Expand Down

0 comments on commit b48c456

Please sign in to comment.