Skip to content

Commit 3331a53

Browse files
authored
Improved acceptance action (#137)
- made `*boilerplate.Boilerplate` public - changed slack notification API - restructured acceptance running code into a type
1 parent e28ae5b commit 3331a53

File tree

7 files changed

+134
-85
lines changed

7 files changed

+134
-85
lines changed

.github/workflows/nightly-go-libs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
go-version: 1.21
2525

2626
- name: Acceptance
27-
uses: databrickslabs/sandbox/acceptance@acceptance/v0.2.0
27+
uses: databrickslabs/sandbox/acceptance@acceptance/v0.2.1
2828
with:
2929
directory: go-libs
3030
vault_uri: ${{ secrets.VAULT_URI }}

acceptance/boilerplate/actions.go

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"github.com/sethvargo/go-githubactions"
1616
)
1717

18-
func New(ctx context.Context, opts ...githubactions.Option) (*boilerplate, error) {
18+
func New(ctx context.Context, opts ...githubactions.Option) (*Boilerplate, error) {
1919
opts = append(opts, githubactions.WithGetenv(func(key string) string {
2020
return env.Get(ctx, key)
2121
}))
@@ -25,7 +25,7 @@ func New(ctx context.Context, opts ...githubactions.Option) (*boilerplate, error
2525
return nil, err
2626
}
2727
logger.DefaultLogger = &actionsLogger{a}
28-
return &boilerplate{
28+
return &Boilerplate{
2929
Action: a,
3030
context: context,
3131
GitHub: github.NewClient(&github.GitHubConfig{
@@ -35,14 +35,14 @@ func New(ctx context.Context, opts ...githubactions.Option) (*boilerplate, error
3535
}, nil
3636
}
3737

38-
type boilerplate struct {
38+
type Boilerplate struct {
3939
Action *githubactions.Action
4040
context *githubactions.GitHubContext
4141
GitHub *github.GitHubClient
4242
uploader *artifactUploader
4343
}
4444

45-
func (a *boilerplate) PrepareArtifacts() (string, error) {
45+
func (a *Boilerplate) PrepareArtifacts() (string, error) {
4646
tempDir, err := os.MkdirTemp(os.TempDir(), "artifacts-*")
4747
if err != nil {
4848
return "", fmt.Errorf("tmp: %w", err)
@@ -61,7 +61,7 @@ func (a *boilerplate) PrepareArtifacts() (string, error) {
6161
return tempDir, nil
6262
}
6363

64-
func (a *boilerplate) Upload(ctx context.Context, folder string) error {
64+
func (a *Boilerplate) Upload(ctx context.Context, folder string) error {
6565
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
6666
suffix := make([]byte, 12)
6767
for i := range suffix {
@@ -75,7 +75,7 @@ func (a *boilerplate) Upload(ctx context.Context, folder string) error {
7575
return nil
7676
}
7777

78-
func (a *boilerplate) RunURL(ctx context.Context) (string, error) {
78+
func (a *Boilerplate) RunURL(ctx context.Context) (string, error) {
7979
org, repo := a.context.Repo()
8080
workflowJobs := a.GitHub.ListWorkflowJobs(ctx, org, repo, a.context.RunID)
8181
for workflowJobs.HasNext(ctx) {
@@ -92,27 +92,32 @@ func (a *boilerplate) RunURL(ctx context.Context) (string, error) {
9292
return "", fmt.Errorf("id not found for current run: %s", a.context.Job)
9393
}
9494

95-
func (a *boilerplate) CreateIssueIfNotOpen(ctx context.Context, newIssue github.NewIssue) error {
95+
func (a *Boilerplate) CreateOrCommentOnIssue(ctx context.Context, newIssue github.NewIssue) error {
9696
org, repo := a.context.Repo()
9797
it := a.GitHub.ListRepositoryIssues(ctx, org, repo, &github.ListIssues{
9898
State: "open",
9999
})
100-
created := map[string]bool{}
100+
created := map[string]int{}
101101
for it.HasNext(ctx) {
102102
issue, err := it.Next(ctx)
103103
if err != nil {
104104
return fmt.Errorf("issue: %w", err)
105105
}
106-
created[issue.Title] = true
107-
}
108-
if created[newIssue.Title] {
109-
return nil
106+
created[issue.Title] = issue.Number
110107
}
108+
// with the tagged comment, which has the workflow ref, we can link to the run
111109
body, err := a.taggedComment(ctx, newIssue.Body)
112110
if err != nil {
113111
return fmt.Errorf("tagged comment: %w", err)
114112
}
115-
// with the tagged comment, which has the workflow ref, we can link to the run
113+
number, ok := created[newIssue.Title]
114+
if ok {
115+
_, err = a.GitHub.CreateIssueComment(ctx, org, repo, number, body)
116+
if err != nil {
117+
return fmt.Errorf("new comment: %w", err)
118+
}
119+
return nil
120+
}
116121
issue, err := a.GitHub.CreateIssue(ctx, org, repo, github.NewIssue{
117122
Title: newIssue.Title,
118123
Assignees: newIssue.Assignees,
@@ -122,17 +127,17 @@ func (a *boilerplate) CreateIssueIfNotOpen(ctx context.Context, newIssue github.
122127
if err != nil {
123128
return fmt.Errorf("new issue: %w", err)
124129
}
125-
logger.Infof(ctx, "Created issue: https://github.com/%s/%s/issues/%d", issue.Number)
130+
logger.Infof(ctx, "Created new issue: https://github.com/%s/%s/issues/%d", org, repo, issue.Number)
126131
return nil
127132
}
128133

129-
func (a *boilerplate) tag() string {
134+
func (a *Boilerplate) tag() string {
130135
// The ref path to the workflow. For example,
131136
// octocat/hello-world/.github/workflows/my-workflow.yml@refs/heads/my_branch.
132137
return fmt.Sprintf("\n<!-- workflow:%s -->", a.Action.Getenv("GITHUB_WORKFLOW_REF"))
133138
}
134139

135-
func (a *boilerplate) taggedComment(ctx context.Context, body string) (string, error) {
140+
func (a *Boilerplate) taggedComment(ctx context.Context, body string) (string, error) {
136141
runUrl, err := a.RunURL(ctx)
137142
if err != nil {
138143
return "", fmt.Errorf("run url: %w", err)
@@ -141,11 +146,11 @@ func (a *boilerplate) taggedComment(ctx context.Context, body string) (string, e
141146
body, a.WorkflowRunName(), runUrl, a.tag()), nil
142147
}
143148

144-
func (a *boilerplate) WorkflowRunName() string {
149+
func (a *Boilerplate) WorkflowRunName() string {
145150
return fmt.Sprintf("%s #%d", a.context.Workflow, a.context.RunNumber)
146151
}
147152

148-
func (a *boilerplate) currentPullRequest(ctx context.Context) (*github.PullRequest, error) {
153+
func (a *Boilerplate) currentPullRequest(ctx context.Context) (*github.PullRequest, error) {
149154
if a.context.Event == nil {
150155
return nil, fmt.Errorf("missing actions event")
151156
}
@@ -163,7 +168,7 @@ func (a *boilerplate) currentPullRequest(ctx context.Context) (*github.PullReque
163168
return event.PullRequest, nil
164169
}
165170

166-
func (a *boilerplate) Comment(ctx context.Context, commentText string) error {
171+
func (a *Boilerplate) AddOrUpdateComment(ctx context.Context, commentText string) error {
167172
pr, err := a.currentPullRequest(ctx)
168173
if err != nil {
169174
return fmt.Errorf("pr: %w", err)

acceptance/main.go

Lines changed: 100 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package main
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
7+
"net/url"
68
"os"
79
"path/filepath"
810
"strings"
@@ -11,107 +13,142 @@ import (
1113
"github.com/databrickslabs/sandbox/acceptance/boilerplate"
1214
"github.com/databrickslabs/sandbox/acceptance/ecosystem"
1315
"github.com/databrickslabs/sandbox/acceptance/notify"
16+
"github.com/databrickslabs/sandbox/acceptance/redaction"
1417
"github.com/databrickslabs/sandbox/acceptance/testenv"
1518
"github.com/databrickslabs/sandbox/go-libs/env"
1619
"github.com/databrickslabs/sandbox/go-libs/github"
20+
"github.com/databrickslabs/sandbox/go-libs/slack"
1721
"github.com/sethvargo/go-githubactions"
1822
)
1923

24+
func main() {
25+
err := run(context.Background())
26+
if err != nil {
27+
githubactions.Fatalf("failed: %s", err)
28+
}
29+
}
30+
2031
func run(ctx context.Context, opts ...githubactions.Option) error {
21-
b, err := boilerplate.New(ctx)
32+
b, err := boilerplate.New(ctx, opts...)
2233
if err != nil {
2334
return fmt.Errorf("boilerplate: %w", err)
2435
}
25-
timeoutRaw := b.Action.GetInput("timeout")
26-
if timeoutRaw == "" {
27-
timeoutRaw = "1h"
28-
}
29-
timeout, err := time.ParseDuration(timeoutRaw)
36+
a := &acceptance{Boilerplate: b}
37+
alert, err := a.trigger(ctx)
3038
if err != nil {
31-
return fmt.Errorf("timeout: %w", err)
39+
return fmt.Errorf("trigger: %w", err)
3240
}
33-
ctx, cancel := context.WithTimeout(ctx, timeout)
34-
defer cancel()
35-
vaultURI := b.Action.GetInput("vault_uri")
36-
directory := b.Action.GetInput("directory")
37-
project := b.Action.GetInput("project")
38-
if project == "" {
39-
abs, err := filepath.Abs(directory)
40-
if err != nil {
41-
return fmt.Errorf("absolute path: %w", err)
42-
}
43-
project = filepath.Base(abs)
41+
return a.notifyIfNeeded(ctx, alert)
42+
}
43+
44+
type acceptance struct {
45+
*boilerplate.Boilerplate
46+
}
47+
48+
func (a *acceptance) trigger(ctx context.Context) (*notify.Notification, error) {
49+
vaultURI := a.Action.GetInput("vault_uri")
50+
directory, project, err := a.getProject()
51+
if err != nil {
52+
return nil, fmt.Errorf("project: %w", err)
4453
}
45-
artifactDir, err := b.PrepareArtifacts()
54+
artifactDir, err := a.PrepareArtifacts()
4655
if err != nil {
47-
return fmt.Errorf("prepare artifacts: %w", err)
56+
return nil, fmt.Errorf("prepare artifacts: %w", err)
4857
}
4958
defer os.RemoveAll(artifactDir)
50-
testEnv := testenv.NewWithGitHubOIDC(b.Action, vaultURI)
59+
testEnv := testenv.NewWithGitHubOIDC(a.Action, vaultURI)
5160
loaded, err := testEnv.Load(ctx)
5261
if err != nil {
53-
return fmt.Errorf("load: %w", err)
62+
return nil, fmt.Errorf("load: %w", err)
5463
}
5564
ctx, stop, err := loaded.Start(ctx)
5665
if err != nil {
57-
return fmt.Errorf("start: %w", err)
66+
return nil, fmt.Errorf("start: %w", err)
5867
}
5968
defer stop()
6069
// make sure that test logs leave their artifacts somewhere we can pickup
6170
ctx = env.Set(ctx, ecosystem.LogDirEnv, artifactDir)
6271
redact := loaded.Redaction()
63-
// detect and run all tests
64-
report, err := ecosystem.RunAll(ctx, redact, directory)
72+
report, err := a.runWithTimeout(ctx, redact, directory)
6573
if err != nil {
66-
return fmt.Errorf("unknown: %w", err)
74+
return nil, fmt.Errorf("run: %w", err)
6775
}
6876
err = report.WriteReport(project, filepath.Join(artifactDir, "test-report.json"))
6977
if err != nil {
70-
return fmt.Errorf("report: %w", err)
78+
return nil, fmt.Errorf("report: %w", err)
7179
}
7280
// better be redacting twice, right?
7381
summary := redact.ReplaceAll(report.StepSummary())
74-
b.Action.AddStepSummary(summary)
75-
err = b.Comment(ctx, summary)
82+
a.Action.AddStepSummary(summary)
83+
err = a.AddOrUpdateComment(ctx, summary)
84+
if err != nil {
85+
return nil, fmt.Errorf("comment: %w", err)
86+
}
87+
err = a.Upload(ctx, artifactDir)
88+
if err != nil {
89+
return nil, fmt.Errorf("upload artifact: %w", err)
90+
}
91+
runUrl, err := a.RunURL(ctx)
92+
if err != nil {
93+
return nil, fmt.Errorf("run url: %w", err)
94+
}
95+
kvStoreURL, err := url.Parse(vaultURI)
7696
if err != nil {
77-
return fmt.Errorf("comment: %w", err)
97+
return nil, fmt.Errorf("vault uri: %w", err)
98+
}
99+
runName := strings.TrimSuffix(kvStoreURL.Host, ".vault.azure.net")
100+
return &notify.Notification{
101+
Project: project,
102+
Report: report,
103+
Cloud: loaded.Cloud(),
104+
RunName: runName,
105+
RunURL: runUrl,
106+
}, nil
107+
}
108+
109+
func (a *acceptance) runWithTimeout(
110+
ctx context.Context, redact redaction.Redaction, directory string,
111+
) (ecosystem.TestReport, error) {
112+
timeoutRaw := a.Action.GetInput("timeout")
113+
if timeoutRaw == "" {
114+
timeoutRaw = "50m"
78115
}
79-
err = b.Upload(ctx, artifactDir)
116+
timeout, err := time.ParseDuration(timeoutRaw)
80117
if err != nil {
81-
return fmt.Errorf("upload artifact: %w", err)
118+
return nil, fmt.Errorf("timeout: %w", err)
82119
}
83-
slackWebhook := b.Action.GetInput("slack_webhook")
84-
createIssues := strings.ToLower(b.Action.GetInput("create_issues"))
120+
ctx, cancel := context.WithTimeout(ctx, timeout)
121+
defer cancel()
122+
// detect and run all tests
123+
report, err := ecosystem.RunAll(ctx, redact, directory)
124+
if err == nil || errors.Is(err, context.DeadlineExceeded) {
125+
return report, nil
126+
}
127+
return nil, fmt.Errorf("unknown: %w", err)
128+
}
129+
130+
func (a *acceptance) notifyIfNeeded(ctx context.Context, alert *notify.Notification) error {
131+
slackWebhook := a.Action.GetInput("slack_webhook")
132+
createIssues := strings.ToLower(a.Action.GetInput("create_issues"))
85133
needsSlack := slackWebhook != ""
86134
needsIssues := createIssues == "true" || createIssues == "yes"
87135
needsNotification := needsSlack || needsIssues
88-
if !report.Pass() && needsNotification {
89-
runUrl, err := b.RunURL(ctx)
90-
if err != nil {
91-
return fmt.Errorf("run url: %w", err)
92-
}
93-
alert := notify.Notification{
94-
Project: project,
95-
Report: report,
96-
Cloud: loaded.Cloud(),
97-
RunName: b.WorkflowRunName(),
98-
WebHook: slackWebhook,
99-
RunURL: runUrl,
100-
}
136+
if !alert.Report.Pass() && needsNotification {
101137
if needsSlack {
102-
err = alert.ToSlack()
138+
hook := slack.Webhook(slackWebhook)
139+
err := alert.ToSlack(hook)
103140
if err != nil {
104141
return fmt.Errorf("slack: %w", err)
105142
}
106143
}
107144
if needsIssues {
108-
for _, v := range report {
145+
for _, v := range alert.Report {
109146
if !v.Failed() {
110147
continue
111148
}
112-
err = b.CreateIssueIfNotOpen(ctx, github.NewIssue{
113-
Title: fmt.Sprintf("Test failure: `%s`", v.Name),
114-
Body: v.Summary(),
149+
err := a.CreateOrCommentOnIssue(ctx, github.NewIssue{
150+
Title: fmt.Sprintf("Test failure: `%s`", v.Name),
151+
Body: v.Summary(),
115152
Labels: []string{"bug"},
116153
})
117154
if err != nil {
@@ -120,12 +157,18 @@ func run(ctx context.Context, opts ...githubactions.Option) error {
120157
}
121158
}
122159
}
123-
return report.Failed()
160+
return alert.Report.Failed()
124161
}
125162

126-
func main() {
127-
err := run(context.Background())
128-
if err != nil {
129-
githubactions.Fatalf("failed: %s", err)
163+
func (a *acceptance) getProject() (string, string, error) {
164+
directory := a.Action.GetInput("directory")
165+
project := a.Action.GetInput("project")
166+
if project == "" {
167+
abs, err := filepath.Abs(directory)
168+
if err != nil {
169+
return "", "", fmt.Errorf("absolute path: %w", err)
170+
}
171+
project = filepath.Base(abs)
130172
}
173+
return directory, project, nil
131174
}

acceptance/notify/slack.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ type Notification struct {
1616
Cloud config.Cloud
1717
RunName string
1818
Report ecosystem.TestReport
19-
WebHook string
2019
RunURL string
2120
}
2221

@@ -26,7 +25,7 @@ var icons = map[config.Cloud]string{
2625
config.CloudGCP: "https://cloud.google.com/favicon.ico",
2726
}
2827

29-
func (n Notification) ToSlack() error {
28+
func (n Notification) ToSlack(hook slack.Webhook) error {
3029
var failures, flakes []string
3130
for _, v := range n.Report {
3231
if v.Skip {
@@ -62,7 +61,6 @@ func (n Notification) ToSlack() error {
6261
if n.RunName == "" {
6362
n.RunName = string(n.Cloud)
6463
}
65-
hook := slack.Webhook(n.WebHook)
6664
return hook.Notify(slack.Message{
6765
Text: n.Report.String(),
6866
UserName: n.Project,

0 commit comments

Comments
 (0)