-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
2,143 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, &lify.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, &lify.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, &lify.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, &lify.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() | ||
} |
Oops, something went wrong.