Skip to content

Commit

Permalink
Go version of GHA job
Browse files Browse the repository at this point in the history
  • Loading branch information
taraspos committed Dec 11, 2024
2 parents be2b917 + cf9c32a commit 5e63c93
Show file tree
Hide file tree
Showing 9 changed files with 2,143 additions and 0 deletions.
69 changes: 69 additions & 0 deletions libs/github/comment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package github

import (
"context"
"errors"
"strings"

"github.com/google/go-github/v63/github"
)

var (
ErrCommentNotFound = errors.New("comment not found")
)

type IssueIdentifier struct {
Owner string
Repo string
Number int
}

// every trait is treated as AND
type CommentTraits struct {
BodyContains *string
UserLogin *string
}

func (c *Client) FindCommentByTraits(ctx context.Context, issue IssueIdentifier, targetComment CommentTraits) (*github.IssueComment, error) {
comments, _, err := c.client.Issues.ListComments(ctx, issue.Owner, issue.Repo, issue.Number, nil)
if err != nil {
return nil, err
}

for _, c := range comments {
matcher := true
if targetComment.UserLogin != nil {
matcher = matcher &&
c.User != nil && c.User.Login != nil &&
*c.User.Login == *targetComment.UserLogin
}

if targetComment.BodyContains != nil {
matcher = matcher &&
c.Body != nil &&
strings.Contains(*c.Body, *targetComment.BodyContains)
}

if matcher {
return c, nil
}
}

return nil, ErrCommentNotFound
}

func (c *Client) CreateComment(ctx context.Context, issue IssueIdentifier, commentBody string) error {
_, _, err := c.client.Issues.CreateComment(ctx, issue.Owner, issue.Repo, issue.Number, &github.IssueComment{
Body: &commentBody,
})

return err
}

func (c *Client) UpdateComment(ctx context.Context, issue IssueIdentifier, commentId int64, commentBody string) error {
_, _, err := c.client.Issues.EditComment(ctx, issue.Owner, issue.Repo, commentId, &github.IssueComment{
Body: &commentBody,
})

return err
}
3 changes: 3 additions & 0 deletions libs/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ require (
github.com/cli/go-gh/v2 v2.9.0
github.com/google/go-github/v63 v63.0.0
github.com/gravitational/trace v1.4.0
github.com/stretchr/testify v1.8.3
golang.org/x/oauth2 v0.21.0
)

require (
github.com/cli/safeexec v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
1 change: 1 addition & 0 deletions libs/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,7 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
Expand Down
35 changes: 35 additions & 0 deletions tools/amplify-preview/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Amplify Preview
description: Prepare Amplify Preview URL and post it in PR comments
inputs:
app_ids:
description: "Comma separated list of Amplify App IDs"
required: true
create_branches:
description: 'Create preview branches using this actions instead of "auto-build" feature on AWS Amplify'
required: false
default: "false"
github_token:
required: true
description: "Github token with permissions to read/write comments in pull request"
runs:
using: composite
steps:
- name: Extract branch name
shell: bash
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
id: extract_branch

- uses: actions/setup-go@v5
with:
go-version-file: tools/amplify-preview/go.mod
cache-dependency-path: tools/amplify-preview/go.sum

- name: Amplify Preview
env:
AMPLIFY_APP_IDS: ${{ inputs.app_ids }}
GIT_BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
CREATE_BRANCHES: ${{ inputs.create_branches }}
GITHUB_TOKEN: ${{ inputs.github_token }}
shell: bash
run: |
pushd ./tools/amplify-preview/; go run ./; popd
244 changes: 244 additions & 0 deletions tools/amplify-preview/amplify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package main

import (
"context"
"errors"
"fmt"
"strings"
"sync"
"time"

"github.com/aws/aws-sdk-go-v2/service/amplify"
"github.com/aws/aws-sdk-go-v2/service/amplify/types"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/arn"
)

var (
errBranchNotFound = errors.New("Branch not found")
errNoJobForBranch = errors.New("Current branch has no jobs")
)

const (
logKeyAppID = "appID"
logKeyBranchName = "branchName"
logKeyJobID = "jobID"

amplifyMarkdownHeader = "Amplify deployment status"
amplifyDefaultDomain = "amplifyapp.com"
)

type AmplifyPreview struct {
appIDs []string
client *amplify.Client
}

type aggregatedError struct {
perAppErr map[string]error
message string
}

func (amp *AmplifyPreview) FindExistingBranch(ctx context.Context, branchName string) (*types.Branch, error) {
type resp struct {
appID string
data *amplify.GetBranchOutput
err error
}
var wg sync.WaitGroup
wg.Add(len(amp.appIDs))
resultCh := make(chan resp, len(amp.appIDs))

for _, appID := range amp.appIDs {
go func() {
defer wg.Done()
branch, err := amp.client.GetBranch(ctx, &amplify.GetBranchInput{
AppId: aws.String(appID),
BranchName: aws.String(branchName),
})
resultCh <- resp{
appID: appID,
data: branch,
err: err,
}
}()
}

wg.Wait()
close(resultCh)

failedResp := aggregatedError{
perAppErr: map[string]error{},
message: "failed to fetch branch",
}

for resp := range resultCh {
var errNotFound *types.NotFoundException
if errors.As(resp.err, &errNotFound) {
logger.Debug("Branch not found", logKeyAppID, resp.appID, logKeyBranchName, branchName)
continue
} else if resp.err != nil {
failedResp.perAppErr[resp.appID] = resp.err
continue
}

if resp.data != nil {
return resp.data.Branch, nil
}
}

if err := failedResp.Error(); err != nil {
return nil, err
}

return nil, errBranchNotFound
}

func (amp *AmplifyPreview) CreateBranch(ctx context.Context, branchName string) (*types.Branch, error) {
failedResp := aggregatedError{
perAppErr: map[string]error{},
message: "failed to create branch",
}

for _, appID := range amp.appIDs {
resp, err := amp.client.CreateBranch(ctx, &amplify.CreateBranchInput{
AppId: aws.String(appID),
BranchName: aws.String(branchName),
Description: aws.String("Branch generated for PR TODO"),
Stage: types.StagePullRequest,
EnableAutoBuild: aws.Bool(true),
})

var errLimitExceeded *types.LimitExceededException
if errors.As(err, &errLimitExceeded) {
logger.Debug("Reached branches limit", logKeyAppID, appID)
} else if err != nil {
failedResp.perAppErr[appID] = err
}

if resp != nil {
logger.Info("Successfully created branch", logKeyAppID, appID, logKeyBranchName, *resp.Branch.BranchName, logKeyJobID, resp.Branch.ActiveJobId)
return resp.Branch, nil
}
}

return nil, failedResp.Error()
}

func (amp *AmplifyPreview) StartJob(ctx context.Context, branch *types.Branch) (*types.JobSummary, error) {
appID, err := appIDFromBranchARN(*branch.BranchArn)
if err != nil {
return nil, err
}

resp, err := amp.client.StartJob(ctx, &amplify.StartJobInput{
AppId: &appID,
BranchName: branch.BranchName,
JobType: types.JobTypeRelease,
JobReason: aws.String("Initial job from GHA"),
})

if err != nil {
return nil, err
}

logger.Info("Successfully started job", logKeyAppID, appID, logKeyBranchName, *branch.BranchName, logKeyJobID, *resp.JobSummary.JobId)

return resp.JobSummary, nil

}

func (amp *AmplifyPreview) GetJob(ctx context.Context, branch *types.Branch, jobID *string) (*types.JobSummary, error) {
appID, err := appIDFromBranchARN(*branch.BranchArn)
if err != nil {
return nil, err
}

if jobID == nil {
jobID = branch.ActiveJobId
}

if jobID != nil {
resp, err := amp.client.GetJob(ctx, &amplify.GetJobInput{
AppId: aws.String(appID),
BranchName: branch.BranchName,
JobId: jobID,
})
if err != nil {
return nil, err
}

return resp.Job.Summary, nil
}

return nil, errNoJobForBranch
}

func appIDFromBranchARN(branchArn string) (string, error) {
parsedArn, err := arn.Parse(branchArn)
if err != nil {
return "", err
}

if arnParts := strings.Split(parsedArn.Resource, "/"); len(arnParts) > 2 {
return arnParts[1], nil
}

return "", fmt.Errorf("Invalid branch ARN")
}

func (err aggregatedError) Error() error {
if len(err.perAppErr) == 0 {
return nil
}

var msg strings.Builder
for k, v := range err.perAppErr {
msg.WriteString(fmt.Sprintf("%s: %s\n", k, v))
}

return fmt.Errorf("%s for apps:\n\t%s", err.message, msg.String())
}

func amplifyJobToMarkdown(job *types.JobSummary, branch *types.Branch) string {
var mdTableHeader = [...]string{"Branch", "Commit", "Status", "Preview", "Updated (UTC)"}
var commentBody strings.Builder
var jobStatusToEmoji = map[types.JobStatus]rune{
types.JobStatusFailed: '❌',
types.JobStatusRunning: '🔄',
types.JobStatusPending: '⏳',
types.JobStatusProvisioning: '⏳',
types.JobStatusSucceed: '✅',
}

appID, _ := appIDFromBranchARN(*branch.BranchArn)

updateTime := job.StartTime
if job.EndTime != nil {
updateTime = job.EndTime
}
if updateTime == nil {
updateTime = branch.CreateTime
}

commentBody.WriteString(amplifyMarkdownHeader)
commentBody.WriteByte('\n')

// Markdown table header
commentBody.WriteString(strings.Join(mdTableHeader[:], " | "))
commentBody.WriteByte('\n')
commentBody.WriteString(strings.TrimSuffix(strings.Repeat("---------|", len(mdTableHeader)), "|"))
commentBody.WriteByte('\n')
// Markdown table content
commentBody.WriteString(*branch.BranchName)
commentBody.WriteString(" | ")
commentBody.WriteString(*job.CommitId)
commentBody.WriteString(" | ")
commentBody.WriteString(fmt.Sprintf("%c%s", jobStatusToEmoji[job.Status], job.Status))
commentBody.WriteString(" | ")
commentBody.WriteString(fmt.Sprintf("https://%s.%s.%s", *branch.DisplayName, appID, amplifyDefaultDomain))
commentBody.WriteString(" | ")
commentBody.WriteString(updateTime.Format(time.DateTime))
commentBody.WriteByte('\n')

return commentBody.String()
}
Loading

0 comments on commit 5e63c93

Please sign in to comment.