Skip to content

Commit cd1ea94

Browse files
authored
Merge pull request #1330 from diggerhq/feat/cli-access-tokens
remove the need for a digger token injected to job
2 parents f0e9236 + 0ff3f9e commit cd1ea94

File tree

15 files changed

+269
-81
lines changed

15 files changed

+269
-81
lines changed

.github/workflows/tasks_run_test.yml

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Libs tests
2+
on:
3+
push:
4+
pull_request:
5+
types: [opened, reopened]
6+
7+
jobs:
8+
9+
build:
10+
name: Build
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Download Go
14+
uses: actions/setup-go@v5
15+
with:
16+
go-version: 1.21.1
17+
id: go
18+
19+
- name: Check out code into the Go module directory
20+
uses: actions/checkout@v4
21+
22+
- name: Deps
23+
run: |
24+
pwd
25+
go get -v ./...
26+
working-directory: backend/tasks
27+
28+
29+
- name: Test
30+
run: go test -v ./...
31+
working-directory: backend/tasks

backend/controllers/github.go

+15-2
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,12 @@ func handlePullRequestEvent(gh utils.GithubClientProvider, payload *github.PullR
424424
repoFullName := *payload.Repo.FullName
425425
cloneURL := *payload.Repo.CloneURL
426426
prNumber := *payload.PullRequest.Number
427+
link, err := models.DB.GetGithubAppInstallationLink(installationId)
428+
if err != nil {
429+
log.Printf("Error getting GetGithubAppInstallationLink: %v", err)
430+
return fmt.Errorf("error getting github app link")
431+
}
432+
organisationId := link.OrganisationId
427433

428434
diggerYmlStr, ghService, config, projectsGraph, branch, err := getDiggerConfigForPR(gh, installationId, repoFullName, repoOwner, repoName, cloneURL, prNumber)
429435
if err != nil {
@@ -485,7 +491,7 @@ func handlePullRequestEvent(gh utils.GithubClientProvider, payload *github.PullR
485491
}
486492

487493
batchType := getBatchType(jobsForImpactedProjects)
488-
batchId, _, err := utils.ConvertJobsToDiggerJobs(impactedJobsMap, impactedProjectsMap, projectsGraph, installationId, *branch, prNumber, repoOwner, repoName, repoFullName, commentReporter.CommentId, diggerYmlStr, batchType)
494+
batchId, _, err := utils.ConvertJobsToDiggerJobs(organisationId, impactedJobsMap, impactedProjectsMap, projectsGraph, installationId, *branch, prNumber, repoOwner, repoName, repoFullName, commentReporter.CommentId, diggerYmlStr, batchType)
489495
if err != nil {
490496
log.Printf("ConvertJobsToDiggerJobs error: %v", err)
491497
utils.InitCommentReporter(ghService, prNumber, fmt.Sprintf(":x: ConvertJobsToDiggerJobs error: %v", err))
@@ -591,6 +597,13 @@ func handleIssueCommentEvent(gh utils.GithubClientProvider, payload *github.Issu
591597
cloneURL := *payload.Repo.CloneURL
592598
issueNumber := *payload.Issue.Number
593599

600+
link, err := models.DB.GetGithubAppInstallationLink(installationId)
601+
if err != nil {
602+
log.Printf("Error getting GetGithubAppInstallationLink: %v", err)
603+
return fmt.Errorf("error getting github app link")
604+
}
605+
orgId := link.OrganisationId
606+
594607
if *payload.Action != "created" {
595608
log.Printf("comment is not of type 'created', ignoring")
596609
return nil
@@ -673,7 +686,7 @@ func handleIssueCommentEvent(gh utils.GithubClientProvider, payload *github.Issu
673686
}
674687

675688
batchType := getBatchType(jobs)
676-
batchId, _, err := utils.ConvertJobsToDiggerJobs(impactedProjectsJobMap, impactedProjectsMap, projectsGraph, installationId, *branch, issueNumber, repoOwner, repoName, repoFullName, commentReporter.CommentId, diggerYmlStr, batchType)
689+
batchId, _, err := utils.ConvertJobsToDiggerJobs(orgId, impactedProjectsJobMap, impactedProjectsMap, projectsGraph, installationId, *branch, issueNumber, repoOwner, repoName, repoFullName, commentReporter.CommentId, diggerYmlStr, batchType)
677690
if err != nil {
678691
log.Printf("ConvertJobsToDiggerJobs error: %v", err)
679692
utils.InitCommentReporter(ghService, issueNumber, fmt.Sprintf(":x: ConvertJobsToDiggerJobs error: %v", err))

backend/controllers/github_after_merge.go

+26-10
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,14 @@ func GithubAppWebHookAfterMerge(c *gin.Context) {
8686
// c.String(http.StatusInternalServerError, err.Error())
8787
// return
8888
// }
89-
//case *github.PullRequestEvent:
90-
// log.Printf("Got pull request event for %d IN APPLY AFTER MERGE", *event.PullRequest.ID)
91-
// err := handlePullRequestEvent(gh, event)
92-
// if err != nil {
93-
// log.Printf("handlePullRequestEvent error: %v", err)
94-
// c.String(http.StatusInternalServerError, err.Error())
95-
// return
96-
// }
89+
case *github.PullRequestEvent:
90+
log.Printf("Got pull request event for %d IN APPLY AFTER MERGE", *event.PullRequest.ID)
91+
err := handlePullRequestEvent(gh, event)
92+
if err != nil {
93+
log.Printf("handlePullRequestEvent error: %v", err)
94+
c.String(http.StatusInternalServerError, err.Error())
95+
return
96+
}
9797
case *github.PushEvent:
9898
log.Printf("Got push event for %d", event.Repo.URL)
9999
err := handlePushEventApplyAfterMerge(gh, event)
@@ -120,6 +120,7 @@ func handlePushEventApplyAfterMerge(gh utils.GithubClientProvider, payload *gith
120120
requestedBy := *payload.Sender.Login
121121
ref := *payload.Ref
122122
defaultBranch := *payload.Repo.DefaultBranch
123+
backendHostName := os.Getenv("HOSTNAME")
123124

124125
if strings.HasSuffix(ref, defaultBranch) {
125126
link, err := models.DB.GetGithubAppInstallationLink(installationId)
@@ -129,6 +130,7 @@ func handlePushEventApplyAfterMerge(gh utils.GithubClientProvider, payload *gith
129130
}
130131

131132
orgId := link.OrganisationId
133+
orgName := link.Organisation.Name
132134
diggerRepoName := strings.ReplaceAll(repoFullName, "/", "-")
133135
repo, err := models.DB.GetRepo(orgId, diggerRepoName)
134136
if err != nil {
@@ -208,18 +210,32 @@ func handlePushEventApplyAfterMerge(gh utils.GithubClientProvider, payload *gith
208210
planJob := planJobs[i]
209211
applyJob := applyJobs[i]
210212
projectName := planJob.ProjectName
211-
planJobSpec, err := json.Marshal(orchestrator.JobToJson(planJob, impactedProjects[i]))
213+
214+
planJobToken, err := models.DB.CreateDiggerJobToken(orgId)
215+
if err != nil {
216+
log.Printf("Error creating job token: %v %v", projectName, err)
217+
return fmt.Errorf("error creating job token")
218+
}
219+
220+
planJobSpec, err := json.Marshal(orchestrator.JobToJson(planJob, orgName, planJobToken.Value, backendHostName, impactedProjects[i]))
212221
if err != nil {
213222
log.Printf("Error creating jobspec: %v %v", projectName, err)
214223
return fmt.Errorf("error creating jobspec")
215224

216225
}
217226

218-
applyJobSpec, err := json.Marshal(orchestrator.JobToJson(applyJob, impactedProjects[i]))
227+
applyJobToken, err := models.DB.CreateDiggerJobToken(orgId)
228+
if err != nil {
229+
log.Printf("Error creating job token: %v %v", projectName, err)
230+
return fmt.Errorf("error creating job token")
231+
}
232+
233+
applyJobSpec, err := json.Marshal(orchestrator.JobToJson(applyJob, orgName, applyJobToken.Value, backendHostName, impactedProjects[i]))
219234
if err != nil {
220235
log.Printf("Error creating jobs: %v %v", projectName, err)
221236
return fmt.Errorf("error creating jobs")
222237
}
238+
223239
// create batches
224240
planBatch, err := models.DB.CreateDiggerBatch(installationId, repoOwner, repoName, repoFullName, issueNumber, diggerYmlStr, defaultBranch, scheduler.BatchTypePlan, nil)
225241
if err != nil {

backend/controllers/github_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ func setupSuite(tb testing.TB) (func(tb testing.TB), *models.Database) {
591591
// migrate tables
592592
err = gdb.AutoMigrate(&models.Policy{}, &models.Organisation{}, &models.Repo{}, &models.Project{}, &models.Token{},
593593
&models.User{}, &models.ProjectRun{}, &models.GithubAppInstallation{}, &models.GithubApp{}, &models.GithubAppInstallationLink{},
594-
&models.GithubDiggerJobLink{}, &models.DiggerJob{}, &models.DiggerJobParentLink{})
594+
&models.GithubDiggerJobLink{}, &models.DiggerJob{}, &models.DiggerJobParentLink{}, &models.JobToken{})
595595
if err != nil {
596596
log.Fatal(err)
597597
}
@@ -732,7 +732,7 @@ func TestJobsTreeWithOneJobsAndTwoProjects(t *testing.T) {
732732
graph, err := configuration.CreateProjectDependencyGraph(projects)
733733
assert.NoError(t, err)
734734

735-
_, result, err := utils.ConvertJobsToDiggerJobs(jobs, projectMap, graph, 41584295, "", 2, "diggerhq", "parallel_jobs_demo", "diggerhq/parallel_jobs_demo", 123, "test", orchestrator_scheduler.BatchTypeApply)
735+
_, result, err := utils.ConvertJobsToDiggerJobs(1, jobs, projectMap, graph, 41584295, "", 2, "diggerhq", "parallel_jobs_demo", "diggerhq/parallel_jobs_demo", 123, "test", orchestrator_scheduler.BatchTypeApply)
736736
assert.NoError(t, err)
737737
assert.Equal(t, 1, len(result))
738738
parentLinks, err := models.DB.GetDiggerJobParentLinksChildId(&result["dev"].DiggerJobID)
@@ -761,7 +761,7 @@ func TestJobsTreeWithTwoDependantJobs(t *testing.T) {
761761
projectMap["dev"] = project1
762762
projectMap["prod"] = project2
763763

764-
_, result, err := utils.ConvertJobsToDiggerJobs(jobs, projectMap, graph, 123, "", 2, "", "", "test", 123, "test", orchestrator_scheduler.BatchTypeApply)
764+
_, result, err := utils.ConvertJobsToDiggerJobs(1, jobs, projectMap, graph, 123, "", 2, "", "", "test", 123, "test", orchestrator_scheduler.BatchTypeApply)
765765
assert.NoError(t, err)
766766
assert.Equal(t, 2, len(result))
767767

@@ -794,7 +794,7 @@ func TestJobsTreeWithTwoIndependentJobs(t *testing.T) {
794794
projectMap["dev"] = project1
795795
projectMap["prod"] = project2
796796

797-
_, result, err := utils.ConvertJobsToDiggerJobs(jobs, projectMap, graph, 123, "", 2, "", "", "test", 123, "test", orchestrator_scheduler.BatchTypeApply)
797+
_, result, err := utils.ConvertJobsToDiggerJobs(1, jobs, projectMap, graph, 123, "", 2, "", "", "test", 123, "test", orchestrator_scheduler.BatchTypeApply)
798798
assert.NoError(t, err)
799799
assert.Equal(t, 2, len(result))
800800
parentLinks, err := models.DB.GetDiggerJobParentLinksChildId(&result["dev"].DiggerJobID)
@@ -839,7 +839,7 @@ func TestJobsTreeWithThreeLevels(t *testing.T) {
839839
projectMap["555"] = project5
840840
projectMap["666"] = project6
841841

842-
_, result, err := utils.ConvertJobsToDiggerJobs(jobs, projectMap, graph, 123, "", 2, "", "", "test", 123, "test", orchestrator_scheduler.BatchTypeApply)
842+
_, result, err := utils.ConvertJobsToDiggerJobs(1, jobs, projectMap, graph, 123, "", 2, "", "", "test", 123, "test", orchestrator_scheduler.BatchTypeApply)
843843
assert.NoError(t, err)
844844
assert.Equal(t, 6, len(result))
845845
parentLinks, err := models.DB.GetDiggerJobParentLinksChildId(&result["111"].DiggerJobID)

backend/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func main() {
121121
checkoutGroup.GET("/checkout", web.Checkout)
122122

123123
authorized := r.Group("/")
124-
authorized.Use(middleware.GetApiMiddleware(), middleware.AccessLevel(models.AccessPolicyType, models.AdminPolicyType))
124+
authorized.Use(middleware.GetApiMiddleware(), middleware.AccessLevel(models.CliJobAccessType, models.AccessPolicyType, models.AdminPolicyType))
125125

126126
admin := r.Group("/")
127127
admin.Use(middleware.GetApiMiddleware(), middleware.AccessLevel(models.AdminPolicyType))

backend/middleware/jwt.go

+27-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net/http"
1111
"os"
1212
"strings"
13+
"time"
1314
)
1415

1516
func SetContextParameters(c *gin.Context, auth services.Auth, token *jwt.Token) error {
@@ -193,7 +194,32 @@ func JWTBearerTokenAuth(auth services.Auth) gin.HandlerFunc {
193194
return
194195
}
195196

196-
if strings.HasPrefix(token, "t:") {
197+
if strings.HasPrefix(token, "cli:") {
198+
var dbToken models.Token
199+
200+
jobToken, err := models.DB.GetJobToken(token)
201+
if jobToken == nil {
202+
c.String(http.StatusForbidden, "Invalid bearer token")
203+
c.Abort()
204+
return
205+
}
206+
207+
if time.Now().After(jobToken.Expiry) {
208+
log.Printf("Token has already expired: %v", err)
209+
c.String(http.StatusForbidden, "Token has expired")
210+
c.Abort()
211+
return
212+
}
213+
214+
if err != nil {
215+
log.Printf("Error while fetching token from database: %v", err)
216+
c.String(http.StatusInternalServerError, "Error occurred while fetching database")
217+
c.Abort()
218+
return
219+
}
220+
c.Set(ORGANISATION_ID_KEY, dbToken.OrganisationID)
221+
c.Set(ACCESS_LEVEL_KEY, dbToken.Type)
222+
} else if strings.HasPrefix(token, "t:") {
197223
var dbToken models.Token
198224

199225
token, err := models.DB.GetToken(token)

backend/migrations/20240409161739.sql

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-- Create "job_tokens" table
2+
CREATE TABLE "public"."job_tokens" (
3+
"id" bigserial NOT NULL,
4+
"created_at" timestamptz NULL,
5+
"updated_at" timestamptz NULL,
6+
"deleted_at" timestamptz NULL,
7+
"value" text NULL,
8+
"expiry" timestamptz NULL,
9+
"organisation_id" bigint NULL,
10+
"type" text NULL,
11+
PRIMARY KEY ("id"),
12+
CONSTRAINT "fk_job_tokens_organisation" FOREIGN KEY ("organisation_id") REFERENCES "public"."organisations" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION
13+
);
14+
-- Create index "idx_job_tokens_deleted_at" to table: "job_tokens"
15+
CREATE INDEX "idx_job_tokens_deleted_at" ON "public"."job_tokens" ("deleted_at");

backend/migrations/atlas.sum

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
h1:IoMAe7wNz/XdmLMzpgmtCvO0Pu1Kw0lZsufwdmjf0dE=
1+
h1:yIWTCl8ClNyBna56KDV5hF7yq68qD8QAv8SgM/GiA7E=
22
20231227132525.sql h1:43xn7XC0GoJsCnXIMczGXWis9d504FAWi4F1gViTIcw=
33
20240115170600.sql h1:IW8fF/8vc40+eWqP/xDK+R4K9jHJ9QBSGO6rN9LtfSA=
44
20240116123649.sql h1:R1JlUIgxxF6Cyob9HdtMqiKmx/BfnsctTl5rvOqssQw=
@@ -17,3 +17,4 @@ h1:IoMAe7wNz/XdmLMzpgmtCvO0Pu1Kw0lZsufwdmjf0dE=
1717
20240404165910.sql h1:ofwrBzkvnxFz7sOrtaF3vb2xHsenPmUTSSBHvO1NEdI=
1818
20240405150942.sql h1:0JIQlXqQmfgfBcill47gAef3LnnfdwK6ry98eHraUbo=
1919
20240405160110.sql h1:8bXZtrh8ZFFuCEXWIZ4fSjca0SSk1gsa2BqK7dIZ0To=
20+
20240409161739.sql h1:x0dZOsILJhmeQ6w8JKkllXZb2oz+QqV/PGLo+8R2pWI=

backend/models/orgs.go

+1
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,5 @@ type Token struct {
123123
const (
124124
AccessPolicyType = "access"
125125
AdminPolicyType = "admin"
126+
CliJobAccessType = "cli_access"
126127
)

backend/models/scheduler.go

+10
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ type DiggerJobSummary struct {
5252
ResourcesUpdated uint
5353
}
5454

55+
// These tokens will be pre
56+
type JobToken struct {
57+
gorm.Model
58+
Value string `gorm:"uniqueJobTokenIndex:idx_token"`
59+
Expiry time.Time
60+
OrganisationID uint
61+
Organisation Organisation
62+
Type string // AccessTokenType starts with j:
63+
}
64+
5565
type DiggerJobLinkStatus int8
5666

5767
const (

backend/models/storage.go

+32
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,38 @@ func (db *Database) GetToken(tenantId any) (*Token, error) {
10371037
return token, nil
10381038
}
10391039

1040+
func (db *Database) CreateDiggerJobToken(organisationId uint) (*JobToken, error) {
1041+
1042+
// create a digger job token
1043+
// prefixing token to make easier to retire this type of tokens later
1044+
token := "cli:" + uuid.New().String()
1045+
jobToken := &JobToken{
1046+
Value: token,
1047+
OrganisationID: organisationId,
1048+
Type: CliJobAccessType,
1049+
Expiry: time.Now().Add(time.Hour * 2), // some jobs can take >30 mins (k8s cluster)
1050+
}
1051+
err := db.GormDB.Create(jobToken).Error
1052+
if err != nil {
1053+
log.Printf("failed to create token: %v", err)
1054+
return nil, err
1055+
}
1056+
return jobToken, nil
1057+
}
1058+
1059+
func (db *Database) GetJobToken(tenantId any) (*JobToken, error) {
1060+
token := &JobToken{}
1061+
result := db.GormDB.Take(token, "value = ?", tenantId)
1062+
if result.Error != nil {
1063+
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
1064+
return nil, nil
1065+
} else {
1066+
return nil, result.Error
1067+
}
1068+
}
1069+
return token, nil
1070+
}
1071+
10401072
func (db *Database) CreateGithubAppInstallation(installationId int64, githubAppId int64, login string, accountId int, repoFullName string) (*GithubAppInstallation, error) {
10411073
installation := &GithubAppInstallation{
10421074
GithubInstallationId: installationId,

backend/utils/graphs.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,31 @@ import (
1111
"github.com/dominikbraun/graph"
1212
"github.com/google/uuid"
1313
"log"
14+
"os"
1415
)
1516

1617
// ConvertJobsToDiggerJobs jobs is map with project name as a key and a Job as a value
17-
func ConvertJobsToDiggerJobs(jobsMap map[string]orchestrator.Job, projectMap map[string]configuration.Project, projectsGraph graph.Graph[string, configuration.Project], githubInstallationId int64, branch string, prNumber int, repoOwner string, repoName string, repoFullName string, commentId int64, diggerConfigStr string, batchType orchestrator_scheduler.DiggerBatchType) (*uuid.UUID, map[string]*models.DiggerJob, error) {
18+
func ConvertJobsToDiggerJobs(organisationId uint, jobsMap map[string]orchestrator.Job, projectMap map[string]configuration.Project, projectsGraph graph.Graph[string, configuration.Project], githubInstallationId int64, branch string, prNumber int, repoOwner string, repoName string, repoFullName string, commentId int64, diggerConfigStr string, batchType orchestrator_scheduler.DiggerBatchType) (*uuid.UUID, map[string]*models.DiggerJob, error) {
1819
result := make(map[string]*models.DiggerJob)
20+
organisation, err := models.DB.GetOrganisationById(organisationId)
21+
if err != nil {
22+
log.Printf("Error getting organisation: %v %v", organisationId, err)
23+
return nil, nil, fmt.Errorf("error retriving organisation")
24+
}
25+
organisationName := organisation.Name
26+
27+
backendHostName := os.Getenv("HOSTNAME")
1928

2029
log.Printf("Number of Jobs: %v\n", len(jobsMap))
2130
marshalledJobsMap := map[string][]byte{}
2231
for projectName, job := range jobsMap {
23-
marshalled, err := json.Marshal(orchestrator.JobToJson(job, projectMap[projectName]))
32+
jobToken, err := models.DB.CreateDiggerJobToken(organisationId)
33+
if err != nil {
34+
log.Printf("Error creating job token: %v %v", projectName, err)
35+
return nil, nil, fmt.Errorf("error creating job token")
36+
}
37+
38+
marshalled, err := json.Marshal(orchestrator.JobToJson(job, organisationName, jobToken.Value, backendHostName, projectMap[projectName]))
2439
if err != nil {
2540
return nil, nil, err
2641
}

cli/cmd/digger/main.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,6 @@ func gitHubCI(lock core_locking.Lock, policyChecker core_policy.Checker, backend
9696

9797
// this is used when called from api by the backend and exits in the end of if statement
9898
if wdEvent, ok := ghEvent.(github.WorkflowDispatchEvent); ok && runningMode != "manual" && runningMode != "drift-detection" {
99-
if !strings.Contains(os.Getenv("DIGGER_HOSTNAME"), "cloud.digger.dev") {
100-
usage.SendUsageRecord(githubActor, "log", "selfhosted")
101-
}
10299

103100
type Inputs struct {
104101
JobString string `json:"job"`
@@ -125,8 +122,19 @@ func gitHubCI(lock core_locking.Lock, policyChecker core_policy.Checker, backend
125122
var job orchestrator.JobJson
126123

127124
err = json.Unmarshal([]byte(inputs.JobString), &job)
125+
if err != nil {
126+
reportErrorAndExit(githubActor, fmt.Sprintf("Failed unmarshall job string: %v", err), 4)
127+
}
128128
commentId64, err := strconv.ParseInt(inputs.CommentId, 10, 64)
129129

130+
if job.BackendHostname != "" && job.BackendOrganisationName != "" && job.BackendJobToken != "" {
131+
log.Printf("Found settings sent by backend in job string, overriding backendApi and policyCheckecd r. setting: (orgName: %v BackedHost: %v token: %v)", job.BackendOrganisationName, job.BackendHostname, "****")
132+
backendApi = NewBackendApi(job.BackendHostname, job.BackendJobToken)
133+
policyChecker = NewPolicyChecker(job.BackendHostname, job.BackendOrganisationName, job.BackendJobToken)
134+
} else {
135+
reportErrorAndExit(githubActor, fmt.Sprintf("Missing values from job spec: hostname, orgName, token: %v %v", job.BackendHostname, job.BackendOrganisationName), 4)
136+
}
137+
130138
err = githubPrService.SetOutput(*job.PullRequestNumber, "DIGGER_PR_NUMBER", fmt.Sprintf("%v", *job.PullRequestNumber))
131139
if err != nil {
132140
reportErrorAndExit(githubActor, fmt.Sprintf("Failed to set job output. Exiting. %s", err), 4)

0 commit comments

Comments
 (0)