diff --git a/constants/repo.go b/constants/repo.go index b4368f98..c6e8277d 100644 --- a/constants/repo.go +++ b/constants/repo.go @@ -16,3 +16,21 @@ const ( // in Vela to control their pipeline being compiled as Starlark templates. PipelineTypeStarlark = "starlark" ) + +// Repo ApproveBuild types. +const ( + // ApproveForkAlways defines the CI strategy of having a repo administrator approve + // all builds triggered from a forked PR. + ApproveForkAlways = "fork-always" + + // ApproveForkNoWrite defines the CI strategy of having a repo administrator approve + // all builds triggered from a forked PR where the author does not have write access. + ApproveForkNoWrite = "fork-no-write" + + // ApproveOnce defines the CI strategy of having a repo administrator approve + // all builds triggered from an outside contributor if this is their first time contributing. + ApproveOnce = "first-time" + + // ApproveNever defines the CI strategy of never having to approve CI builds from outside contributors. + ApproveNever = "never" +) diff --git a/constants/status.go b/constants/status.go index af206a67..489f4a7d 100644 --- a/constants/status.go +++ b/constants/status.go @@ -19,6 +19,9 @@ const ( // StatusPending defines the status type for build and step pending statuses. StatusPending = "pending" + // StatusPendingApproval defines the status type for a build waiting to be approved to run. + StatusPendingApproval = "pending approval" + // StatusRunning defines the status type for build and step running statuses. StatusRunning = "running" diff --git a/database/build.go b/database/build.go index 204a5e77..d8a7e49b 100644 --- a/database/build.go +++ b/database/build.go @@ -62,6 +62,8 @@ type Build struct { Host sql.NullString `sql:"host"` Runtime sql.NullString `sql:"runtime"` Distribution sql.NullString `sql:"distribution"` + ApprovedAt sql.NullInt64 `sql:"approved_at"` + ApprovedBy sql.NullString `sql:"approved_by"` } // Crop prepares the Build type for inserting into the database by @@ -92,7 +94,7 @@ func (b *Build) Crop() *Build { // value for the field, the valid flag is set to // false causing it to be NULL in the database. // -//nolint:gocyclo // ignore cyclomatic complexity due to number of fields +//nolint:gocyclo,funlen // ignore cyclomatic complexity due to number of fields func (b *Build) Nullify() *Build { if b == nil { return nil @@ -248,6 +250,16 @@ func (b *Build) Nullify() *Build { b.Distribution.Valid = false } + // check if the ApprovedAt field should be false + if b.ApprovedAt.Int64 == 0 { + b.ApprovedAt.Valid = false + } + + // check if the ApprovedBy field should be false + if len(b.ApprovedBy.String) == 0 { + b.ApprovedBy.Valid = false + } + return b } @@ -287,6 +299,8 @@ func (b *Build) ToLibrary() *library.Build { build.SetHost(b.Host.String) build.SetRuntime(b.Runtime.String) build.SetDistribution(b.Distribution.String) + build.SetApprovedAt(b.ApprovedAt.Int64) + build.SetApprovedBy(b.ApprovedBy.String) return build } @@ -328,6 +342,7 @@ func (b *Build) Validate() error { b.Host = sql.NullString{String: sanitize(b.Host.String), Valid: b.Host.Valid} b.Runtime = sql.NullString{String: sanitize(b.Runtime.String), Valid: b.Runtime.Valid} b.Distribution = sql.NullString{String: sanitize(b.Distribution.String), Valid: b.Distribution.Valid} + b.ApprovedBy = sql.NullString{String: sanitize(b.ApprovedBy.String), Valid: b.ApprovedBy.Valid} return nil } @@ -367,6 +382,8 @@ func BuildFromLibrary(b *library.Build) *Build { Host: sql.NullString{String: b.GetHost(), Valid: true}, Runtime: sql.NullString{String: b.GetRuntime(), Valid: true}, Distribution: sql.NullString{String: b.GetDistribution(), Valid: true}, + ApprovedAt: sql.NullInt64{Int64: b.GetApprovedAt(), Valid: true}, + ApprovedBy: sql.NullString{String: b.GetApprovedBy(), Valid: true}, } return build.Nullify() diff --git a/database/build_test.go b/database/build_test.go index 972b4cf2..90458c44 100644 --- a/database/build_test.go +++ b/database/build_test.go @@ -139,6 +139,8 @@ func TestDatabase_Build_ToLibrary(t *testing.T) { want.SetRuntime("docker") want.SetDistribution("linux") want.SetDeployPayload(raw.StringSliceMap{"foo": "test1", "bar": "test2"}) + want.SetApprovedAt(1563474076) + want.SetApprovedBy("OctoCat") // run test got := testBuild().ToLibrary() @@ -228,6 +230,8 @@ func TestDatabase_BuildFromLibrary(t *testing.T) { b.SetRuntime("docker") b.SetDistribution("linux") b.SetDeployPayload(raw.StringSliceMap{"foo": "test1", "bar": "test2"}) + b.SetApprovedAt(1563474076) + b.SetApprovedBy("OctoCat") want := testBuild() @@ -286,5 +290,7 @@ func testBuild() *Build { Host: sql.NullString{String: "example.company.com", Valid: true}, Runtime: sql.NullString{String: "docker", Valid: true}, Distribution: sql.NullString{String: "linux", Valid: true}, + ApprovedAt: sql.NullInt64{Int64: 1563474076, Valid: true}, + ApprovedBy: sql.NullString{String: "OctoCat", Valid: true}, } } diff --git a/database/repo.go b/database/repo.go index a0e12c35..1c185b68 100644 --- a/database/repo.go +++ b/database/repo.go @@ -68,6 +68,7 @@ type Repo struct { AllowComment sql.NullBool `sql:"allow_comment"` PipelineType sql.NullString `sql:"pipeline_type"` PreviousName sql.NullString `sql:"previous_name"` + ApproveBuild sql.NullString `sql:"approve_build"` } // Decrypt will manipulate the existing repo hash by @@ -198,6 +199,11 @@ func (r *Repo) Nullify() *Repo { r.PreviousName.Valid = false } + // check if the ApproveForkBuild field should be false + if len(r.ApproveBuild.String) == 0 { + r.ApproveBuild.Valid = false + } + return r } @@ -230,6 +236,7 @@ func (r *Repo) ToLibrary() *library.Repo { repo.SetAllowComment(r.AllowComment.Bool) repo.SetPipelineType(r.PipelineType.String) repo.SetPreviousName(r.PreviousName.String) + repo.SetApproveBuild(r.ApproveBuild.String) return repo } @@ -325,6 +332,7 @@ func RepoFromLibrary(r *library.Repo) *Repo { AllowComment: sql.NullBool{Bool: r.GetAllowComment(), Valid: true}, PipelineType: sql.NullString{String: r.GetPipelineType(), Valid: true}, PreviousName: sql.NullString{String: r.GetPreviousName(), Valid: true}, + ApproveBuild: sql.NullString{String: r.GetApproveBuild(), Valid: true}, } return repo.Nullify() diff --git a/database/repo_test.go b/database/repo_test.go index f5f24966..cf7d2436 100644 --- a/database/repo_test.go +++ b/database/repo_test.go @@ -7,6 +7,7 @@ import ( "reflect" "testing" + "github.com/go-vela/types/constants" "github.com/go-vela/types/library" ) @@ -118,6 +119,7 @@ func TestDatabase_Repo_Nullify(t *testing.T) { Timeout: sql.NullInt64{Int64: 0, Valid: false}, Visibility: sql.NullString{String: "", Valid: false}, PipelineType: sql.NullString{String: "", Valid: false}, + ApproveBuild: sql.NullString{String: "", Valid: false}, } // setup tests @@ -177,6 +179,7 @@ func TestDatabase_Repo_ToLibrary(t *testing.T) { want.SetAllowComment(false) want.SetPipelineType("yaml") want.SetPreviousName("oldName") + want.SetApproveBuild(constants.ApproveNever) // run test got := testRepo().ToLibrary() @@ -330,6 +333,7 @@ func TestDatabase_RepoFromLibrary(t *testing.T) { r.SetAllowComment(false) r.SetPipelineType("yaml") r.SetPreviousName("oldName") + r.SetApproveBuild(constants.ApproveNever) want := testRepo() @@ -369,5 +373,6 @@ func testRepo() *Repo { AllowComment: sql.NullBool{Bool: false, Valid: true}, PipelineType: sql.NullString{String: "yaml", Valid: true}, PreviousName: sql.NullString{String: "oldName", Valid: true}, + ApproveBuild: sql.NullString{String: constants.ApproveNever, Valid: true}, } } diff --git a/library/build.go b/library/build.go index 34edee9d..ca091267 100644 --- a/library/build.go +++ b/library/build.go @@ -46,6 +46,8 @@ type Build struct { Host *string `json:"host,omitempty"` Runtime *string `json:"runtime,omitempty"` Distribution *string `json:"distribution,omitempty"` + ApprovedAt *int64 `json:"approved_at,omitempty"` + ApprovedBy *string `json:"approved_by,omitempty"` } // Duration calculates and returns the total amount of @@ -83,6 +85,8 @@ func (b *Build) Duration() string { // provided from the fields of the Build type. func (b *Build) Environment(workspace, channel string) map[string]string { envs := map[string]string{ + "VELA_BUILD_APPROVED_AT": ToString(b.GetApprovedAt()), + "VELA_BUILD_APPROVED_BY": ToString(b.GetApprovedBy()), "VELA_BUILD_AUTHOR": ToString(b.GetAuthor()), "VELA_BUILD_AUTHOR_EMAIL": ToString(b.GetEmail()), "VELA_BUILD_BASE_REF": ToString(b.GetBaseRef()), @@ -600,6 +604,32 @@ func (b *Build) GetDistribution() string { return *b.Distribution } +// GetApprovedAt returns the ApprovedAt field. +// +// When the provided Build type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *Build) GetApprovedAt() int64 { + // return zero value if Build type or ApprovedAt field is nil + if b == nil || b.ApprovedAt == nil { + return 0 + } + + return *b.ApprovedAt +} + +// GetApprovedBy returns the ApprovedBy field. +// +// When the provided Build type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *Build) GetApprovedBy() string { + // return zero value if Build type or ApprovedBy field is nil + if b == nil || b.ApprovedBy == nil { + return "" + } + + return *b.ApprovedBy +} + // SetID sets the ID field. // // When the provided Build type is nil, it @@ -1003,11 +1033,39 @@ func (b *Build) SetDistribution(v string) { b.Distribution = &v } +// SetApprovedAt sets the ApprovedAt field. +// +// When the provided Build type is nil, it +// will set nothing and immediately return. +func (b *Build) SetApprovedAt(v int64) { + // return if Build type is nil + if b == nil { + return + } + + b.ApprovedAt = &v +} + +// SetApprovedBy sets the ApprovedBy field. +// +// When the provided Build type is nil, it +// will set nothing and immediately return. +func (b *Build) SetApprovedBy(v string) { + // return if Build type is nil + if b == nil { + return + } + + b.ApprovedBy = &v +} + // String implements the Stringer interface for the Build type. // //nolint:dupl // this is duplicated in the test func (b *Build) String() string { return fmt.Sprintf(`{ + ApprovedAt: %d, + ApprovedBy: %s, Author: %s, BaseRef: %s, Branch: %s, @@ -1040,6 +1098,8 @@ func (b *Build) String() string { Status: %s, Title: %s, }`, + b.GetApprovedAt(), + b.GetApprovedBy(), b.GetAuthor(), b.GetBaseRef(), b.GetBranch(), diff --git a/library/build_test.go b/library/build_test.go index 64e4c5ca..339daa90 100644 --- a/library/build_test.go +++ b/library/build_test.go @@ -86,6 +86,8 @@ func TestLibrary_Build_Environment(t *testing.T) { { build: testBuild(), want: map[string]string{ + "VELA_BUILD_APPROVED_AT": "1563474076", + "VELA_BUILD_APPROVED_BY": "OctoCat", "VELA_BUILD_AUTHOR": "OctoKitty", "VELA_BUILD_AUTHOR_EMAIL": "OctoKitty@github.com", "VELA_BUILD_BASE_REF": "", @@ -138,6 +140,8 @@ func TestLibrary_Build_Environment(t *testing.T) { { build: _comment, want: map[string]string{ + "VELA_BUILD_APPROVED_AT": "1563474076", + "VELA_BUILD_APPROVED_BY": "OctoCat", "VELA_BUILD_AUTHOR": "OctoKitty", "VELA_BUILD_AUTHOR_EMAIL": "OctoKitty@github.com", "VELA_BUILD_BASE_REF": "", @@ -193,6 +197,8 @@ func TestLibrary_Build_Environment(t *testing.T) { { build: _deploy, want: map[string]string{ + "VELA_BUILD_APPROVED_AT": "1563474076", + "VELA_BUILD_APPROVED_BY": "OctoCat", "VELA_BUILD_AUTHOR": "OctoKitty", "VELA_BUILD_AUTHOR_EMAIL": "OctoKitty@github.com", "VELA_BUILD_BASE_REF": "", @@ -250,6 +256,8 @@ func TestLibrary_Build_Environment(t *testing.T) { { build: _deployTag, want: map[string]string{ + "VELA_BUILD_APPROVED_AT": "1563474076", + "VELA_BUILD_APPROVED_BY": "OctoCat", "VELA_BUILD_AUTHOR": "OctoKitty", "VELA_BUILD_AUTHOR_EMAIL": "OctoKitty@github.com", "VELA_BUILD_BASE_REF": "", @@ -309,6 +317,8 @@ func TestLibrary_Build_Environment(t *testing.T) { { build: _pull, want: map[string]string{ + "VELA_BUILD_APPROVED_AT": "1563474076", + "VELA_BUILD_APPROVED_BY": "OctoCat", "VELA_BUILD_AUTHOR": "OctoKitty", "VELA_BUILD_AUTHOR_EMAIL": "OctoKitty@github.com", "VELA_BUILD_BASE_REF": "", @@ -366,6 +376,8 @@ func TestLibrary_Build_Environment(t *testing.T) { { build: _tag, want: map[string]string{ + "VELA_BUILD_APPROVED_AT": "1563474076", + "VELA_BUILD_APPROVED_BY": "OctoCat", "VELA_BUILD_AUTHOR": "OctoKitty", "VELA_BUILD_AUTHOR_EMAIL": "OctoKitty@github.com", "VELA_BUILD_BASE_REF": "", @@ -570,6 +582,14 @@ func TestLibrary_Build_Getters(t *testing.T) { if test.build.GetDistribution() != test.want.GetDistribution() { t.Errorf("GetDistribution is %v, want %v", test.build.GetDistribution(), test.want.GetDistribution()) } + + if test.build.GetApprovedAt() != test.want.GetApprovedAt() { + t.Errorf("GetApprovedAt is %v, want %v", test.build.GetApprovedAt(), test.want.GetApprovedAt()) + } + + if test.build.GetApprovedBy() != test.want.GetApprovedBy() { + t.Errorf("GetApprovedBy is %v, want %v", test.build.GetApprovedBy(), test.want.GetApprovedBy()) + } } } @@ -625,6 +645,8 @@ func TestLibrary_Build_Setters(t *testing.T) { test.build.SetHost(test.want.GetHost()) test.build.SetRuntime(test.want.GetRuntime()) test.build.SetDistribution(test.want.GetDistribution()) + test.build.SetApprovedAt(test.want.GetApprovedAt()) + test.build.SetApprovedBy(test.want.GetApprovedBy()) if test.build.GetID() != test.want.GetID() { t.Errorf("SetID is %v, want %v", test.build.GetID(), test.want.GetID()) @@ -749,6 +771,14 @@ func TestLibrary_Build_Setters(t *testing.T) { if test.build.GetDistribution() != test.want.GetDistribution() { t.Errorf("SetDistribution is %v, want %v", test.build.GetDistribution(), test.want.GetDistribution()) } + + if test.build.GetApprovedAt() != test.want.GetApprovedAt() { + t.Errorf("SetApprovedAt is %v, want %v", test.build.GetApprovedAt(), test.want.GetApprovedAt()) + } + + if test.build.GetApprovedBy() != test.want.GetApprovedBy() { + t.Errorf("SetApprovedBy is %v, want %v", test.build.GetApprovedBy(), test.want.GetApprovedBy()) + } } } @@ -757,6 +787,8 @@ func TestLibrary_Build_String(t *testing.T) { b := testBuild() want := fmt.Sprintf(`{ + ApprovedAt: %d, + ApprovedBy: %s, Author: %s, BaseRef: %s, Branch: %s, @@ -789,6 +821,8 @@ func TestLibrary_Build_String(t *testing.T) { Status: %s, Title: %s, }`, + b.GetApprovedAt(), + b.GetApprovedBy(), b.GetAuthor(), b.GetBaseRef(), b.GetBranch(), @@ -865,6 +899,8 @@ func testBuild() *Build { b.SetHost("example.company.com") b.SetRuntime("docker") b.SetDistribution("linux") + b.SetApprovedAt(1563474076) + b.SetApprovedBy("OctoCat") return b } diff --git a/library/repo.go b/library/repo.go index 57a3b74e..054316a1 100644 --- a/library/repo.go +++ b/library/repo.go @@ -35,6 +35,7 @@ type Repo struct { AllowComment *bool `json:"allow_comment,omitempty"` PipelineType *string `json:"pipeline_type,omitempty"` PreviousName *string `json:"previous_name,omitempty"` + ApproveBuild *string `json:"approve_build,omitempty"` } // Environment returns a list of environment variables @@ -60,6 +61,7 @@ func (r *Repo) Environment() map[string]string { "VELA_REPO_TRUSTED": ToString(r.GetTrusted()), "VELA_REPO_VISIBILITY": ToString(r.GetVisibility()), "VELA_REPO_PIPELINE_TYPE": ToString(r.GetPipelineType()), + "VELA_REPO_APPROVE_BUILD": ToString(r.GetApproveBuild()), // deprecated environment variables "REPOSITORY_ACTIVE": ToString(r.GetActive()), @@ -393,6 +395,19 @@ func (r *Repo) GetPreviousName() string { return *r.PreviousName } +// GetApproveBuild returns the ApproveBuild field. +// +// When the provided Repo type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *Repo) GetApproveBuild() string { + // return zero value if Repo type or ApproveBuild field is nil + if r == nil || r.ApproveBuild == nil { + return "" + } + + return *r.ApproveBuild +} + // SetID sets the ID field. // // When the provided Repo type is nil, it @@ -705,6 +720,19 @@ func (r *Repo) SetPreviousName(v string) { r.PreviousName = &v } +// SetApproveBuild sets the ApproveBuild field. +// +// When the provided Repo type is nil, it +// will set nothing and immediately return. +func (r *Repo) SetApproveBuild(v string) { + // return if Repo type is nil + if r == nil { + return + } + + r.ApproveBuild = &v +} + // String implements the Stringer interface for the Repo type. func (r *Repo) String() string { return fmt.Sprintf(`{ @@ -714,6 +742,7 @@ func (r *Repo) String() string { AllowPull: %t, AllowPush: %t, AllowTag: %t, + ApproveBuild: %s, Branch: %s, BuildLimit: %d, Clone: %s, @@ -738,6 +767,7 @@ func (r *Repo) String() string { r.GetAllowPull(), r.GetAllowPush(), r.GetAllowTag(), + r.GetApproveBuild(), r.GetBranch(), r.GetBuildLimit(), r.GetClone(), diff --git a/library/repo_test.go b/library/repo_test.go index 0dc48830..e1b38afd 100644 --- a/library/repo_test.go +++ b/library/repo_test.go @@ -6,6 +6,8 @@ import ( "fmt" "reflect" "testing" + + "github.com/go-vela/types/constants" ) func TestLibrary_Repo_Environment(t *testing.T) { @@ -30,6 +32,7 @@ func TestLibrary_Repo_Environment(t *testing.T) { "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "public", "VELA_REPO_PIPELINE_TYPE": "", + "VELA_REPO_APPROVE_BUILD": "never", "REPOSITORY_ACTIVE": "true", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", @@ -165,6 +168,10 @@ func TestLibrary_Repo_Getters(t *testing.T) { if !reflect.DeepEqual(test.repo.GetPreviousName(), test.want.GetPreviousName()) { t.Errorf("GetPreviousName is %v, want %v", test.repo.GetPreviousName(), test.want.GetPreviousName()) } + + if test.repo.GetApproveBuild() != test.want.GetApproveBuild() { + t.Errorf("GetApproveForkBuild is %v, want %v", test.repo.GetApproveBuild(), test.want.GetApproveBuild()) + } } } @@ -213,6 +220,7 @@ func TestLibrary_Repo_Setters(t *testing.T) { test.repo.SetAllowComment(test.want.GetAllowComment()) test.repo.SetPipelineType(test.want.GetPipelineType()) test.repo.SetPreviousName(test.want.GetPreviousName()) + test.repo.SetApproveBuild(test.want.GetApproveBuild()) if test.repo.GetID() != test.want.GetID() { t.Errorf("SetID is %v, want %v", test.repo.GetID(), test.want.GetID()) @@ -305,6 +313,10 @@ func TestLibrary_Repo_Setters(t *testing.T) { if !reflect.DeepEqual(test.repo.GetPreviousName(), test.want.GetPreviousName()) { t.Errorf("SetPreviousName is %v, want %v", test.repo.GetPreviousName(), test.want.GetPreviousName()) } + + if test.repo.GetApproveBuild() != test.want.GetApproveBuild() { + t.Errorf("SetApproveForkBuild is %v, want %v", test.repo.GetApproveBuild(), test.want.GetApproveBuild()) + } } } @@ -319,6 +331,7 @@ func TestLibrary_Repo_String(t *testing.T) { AllowPull: %t, AllowPush: %t, AllowTag: %t, + ApproveBuild: %s, Branch: %s, BuildLimit: %d, Clone: %s, @@ -343,6 +356,7 @@ func TestLibrary_Repo_String(t *testing.T) { r.GetAllowPull(), r.GetAllowPush(), r.GetAllowTag(), + r.GetApproveBuild(), r.GetBranch(), r.GetBuildLimit(), r.GetClone(), @@ -397,6 +411,7 @@ func testRepo() *Repo { r.SetAllowComment(false) r.SetPipelineType("") r.SetPreviousName("") + r.SetApproveBuild(constants.ApproveNever) return r } diff --git a/webhook.go b/webhook.go index 838adca9..aaddd5c4 100644 --- a/webhook.go +++ b/webhook.go @@ -14,15 +14,22 @@ var ( skipDirectiveMsg = "skip ci directive found in commit title/message" ) +// PullRequest defines the data pulled from PRs while +// processing a webhook. +type PullRequest struct { + Comment string + Number int + IsFromFork bool +} + // Webhook defines a struct that is used to return // the required data when processing webhook event // a for a source provider event. type Webhook struct { - Comment string - PRNumber int - Hook *library.Hook - Repo *library.Repo - Build *library.Build + Hook *library.Hook + Repo *library.Repo + Build *library.Build + PullRequest PullRequest } // ShouldSkip uses the build information