Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.60.3
version: v1.64.3

test:
name: Unit Test
Expand Down
10 changes: 6 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Build stage
FROM docker.io/golang:1.22-alpine3.20 as builder
FROM --platform=$BUILDPLATFORM docker.io/golang:1.24.2-alpine3.21 AS builder
ARG TARGETARCH
ENV CGO_ENABLED=0 \
GOOS=linux
GOOS=linux \
GOARCH=${TARGETARCH}

WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags "-s -w" -o /build/radix-github-webhook
RUN go build -ldflags="-s -w" -o /build/radix-github-webhook

# Final stage, ref https://github.com/GoogleContainerTools/distroless/blob/main/base/README.md for distroless
FROM gcr.io/distroless/static
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ docker-build: $(addsuffix -image,$(IMAGES))
docker-push: $(addsuffix -push,$(IMAGES))

%-push:
az acr login --name $(DOCKER_REGISTRY)
docker push $(DOCKER_REGISTRY)/$*:$(IMAGE_TAG)

.PHONY: deploy
deploy: docker-build docker-push

.PHONY: mocks
mocks: bootstrap
mockgen -source ./radix/api_server.go -destination ./radix/api_server_mock.go -package radix
Expand All @@ -55,7 +59,7 @@ HAS_MOCKGEN := $(shell command -v mockgen;)
.PHONY: bootstrap
bootstrap:
ifndef HAS_GOLANGCI_LINT
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.58.2
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin v1.64.3
endif
ifndef HAS_MOCKGEN
go install github.com/golang/mock/mockgen@v1.6.0
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module github.com/equinor/radix-github-webhook

go 1.22.0
go 1.24.0

toolchain go1.23.0
toolchain go1.24.2

require (
github.com/equinor/radix-common v1.9.7
Expand Down
33 changes: 22 additions & 11 deletions handler/webhook_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ var (
createPipelineJobErrorMessage = func(appName string, apiError error) string {
return fmt.Sprintf("Failed to create pipeline job for Radix application %s. ApiError was: %s", appName, apiError)
}
createPipelineJobSuccessMessage = func(jobName, appName, branch, commitID string) string {
return fmt.Sprintf("Pipeline job %s created for Radix application %s on branch %s for commit %s", jobName, appName, branch, commitID)
createPipelineJobSuccessMessage = func(jobName, appName, gitRefs, gitRefsType, commitID string) string {
return fmt.Sprintf("Pipeline job %s created for Radix application %s on %s %s for commit %s", jobName, appName, gitRefsType, gitRefs, commitID)
}
)

Expand Down Expand Up @@ -116,12 +116,12 @@ func (wh *webhookHandler) HandleFunc(c *gin.Context) {

switch e := payload.(type) {
case *github.PushEvent:
branch := getBranch(e)
gitRef, gitRefType := getGitRefWithType(e)
commitID := getCommitID(e)
sshURL := e.Repo.GetSSHURL()
triggeredBy := getPushTriggeredBy(e)

metrics.IncreasePushGithubEventTypeCounter(sshURL, branch, commitID)
metrics.IncreasePushGithubEventTypeCounter(sshURL, gitRef, gitRefType, commitID)

if isPushEventForRefDeletion(e) {
writeSuccessResponse(http.StatusAccepted, refDeletionPushEventUnsupportedMessage(*e.Ref))
Expand All @@ -135,19 +135,19 @@ func (wh *webhookHandler) HandleFunc(c *gin.Context) {
return
}

metrics.IncreasePushGithubEventTypeTriggerPipelineCounter(sshURL, branch, commitID, applicationSummary.Name)
jobSummary, err := wh.apiServer.TriggerPipeline(c.Request.Context(), applicationSummary.Name, branch, commitID, triggeredBy)
metrics.IncreasePushGithubEventTypeTriggerPipelineCounter(sshURL, gitRef, gitRefType, commitID, applicationSummary.Name)
jobSummary, err := wh.apiServer.TriggerPipeline(c.Request.Context(), applicationSummary.Name, gitRef, gitRefType, commitID, triggeredBy)
if err != nil {
if e, ok := err.(*radix.ApiError); ok && e.Code == 400 {
writeSuccessResponse(http.StatusAccepted, createPipelineJobErrorMessage(applicationSummary.Name, err))
return
}
metrics.IncreasePushGithubEventTypeFailedTriggerPipelineCounter(sshURL, branch, commitID)
metrics.IncreasePushGithubEventTypeFailedTriggerPipelineCounter(sshURL, gitRef, gitRefType, commitID)
writeErrorResponse(http.StatusBadRequest, errors.New(createPipelineJobErrorMessage(applicationSummary.Name, err)))
return
}

writeSuccessResponse(http.StatusOK, createPipelineJobSuccessMessage(jobSummary.Name, jobSummary.AppName, jobSummary.Branch, jobSummary.CommitID))
writeSuccessResponse(http.StatusOK, createPipelineJobSuccessMessage(jobSummary.Name, jobSummary.AppName, jobSummary.GetGitRefOrDefault(), jobSummary.GetGitRefTypeOrDefault(), jobSummary.CommitID))

case *github.PingEvent:
// sshURL := getSSHUrlFromPingURL(*e.Hook.URL)
Expand All @@ -170,6 +170,16 @@ func (wh *webhookHandler) HandleFunc(c *gin.Context) {
}
}

func getApiGitRefType(gitRefsType string) string {
switch gitRefsType {
case "heads":
return "branch"
case "tags":
return "tag"
}
return ""
}

func getCommitID(e *github.PushEvent) string {
if e.Ref != nil && strings.HasPrefix(*e.Ref, "refs/tags/") && e.BaseRef == nil {
// The property After has not an existing commit-ID, but other object ID
Expand Down Expand Up @@ -254,10 +264,11 @@ func getPushTriggeredBy(pushEvent *github.PushEvent) string {
return ""
}

func getBranch(pushEvent *github.PushEvent) string {
// Remove refs/heads from ref
func getGitRefWithType(pushEvent *github.PushEvent) (string, string) {
ref := strings.Split(*pushEvent.Ref, "/")
return strings.Join(ref[2:], "/")
gitRef := strings.Join(ref[2:], "/") // Remove refs/heads from ref
gitRefType := ref[1]
return gitRef, getApiGitRefType(gitRefType)
}

func isPushEventForRefDeletion(pushEvent *github.PushEvent) bool {
Expand Down
29 changes: 19 additions & 10 deletions handler/webhook_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ func (s *handlerTestSuite) Test_PushEventUnmatchedRepo() {
Times(1)
jobSummary := models.JobSummary{Name: "jobname", AppName: expectAppDetail.appName, Branch: "master", CommitID: commitID, TriggeredBy: ""}
s.apiServer.EXPECT().
TriggerPipeline(gomock.Any(), expectAppDetail.appName, "master", commitID, "").
TriggerPipeline(gomock.Any(), expectAppDetail.appName, "master", "branch", commitID, "").
Return(&jobSummary, nil).
Times(1)
}
Expand Down Expand Up @@ -457,7 +457,7 @@ func (s *handlerTestSuite) Test_PushEventTriggerPipelineReturnsError() {

s.apiServer.EXPECT().ShowApplications(gomock.Any(), "git@github.com:equinor/repo-4.git").Return([]*models.ApplicationSummary{&appSummary}, nil).Times(1)
s.apiServer.EXPECT().GetApplication(gomock.Any(), appName).Return(appDetail, nil).Times(1)
s.apiServer.EXPECT().TriggerPipeline(gomock.Any(), appName, "master", commitID, "").Return(nil, scenario.apiError).Times(1)
s.apiServer.EXPECT().TriggerPipeline(gomock.Any(), appName, "master", "branch", commitID, "").Return(nil, scenario.apiError).Times(1)

sut := NewWebHookHandler(s.apiServer)
req, _ := http.NewRequest("POST", "/", bytes.NewReader(payload))
Expand Down Expand Up @@ -490,7 +490,7 @@ func (s *handlerTestSuite) Test_PushEventCorrectSecret() {
jobSummary := models.JobSummary{Name: "jobname", AppName: "jobappname", Branch: "jobbranchname", CommitID: "jobcommitID", TriggeredBy: "anyuser"}
s.apiServer.EXPECT().ShowApplications(gomock.Any(), "git@github.com:equinor/repo-4.git").Return([]*models.ApplicationSummary{&appSummary}, nil).Times(1)
s.apiServer.EXPECT().GetApplication(gomock.Any(), appName).Return(appDetail, nil).Times(1)
s.apiServer.EXPECT().TriggerPipeline(gomock.Any(), appName, "master", commitID, "").Return(&jobSummary, nil).Times(1)
s.apiServer.EXPECT().TriggerPipeline(gomock.Any(), appName, "master", "branch", commitID, "").Return(&jobSummary, nil).Times(1)

sut := NewWebHookHandler(s.apiServer)
req, _ := http.NewRequest("POST", "/", bytes.NewReader(payload))
Expand All @@ -502,7 +502,7 @@ func (s *handlerTestSuite) Test_PushEventCorrectSecret() {
var res response
err := json.Unmarshal(s.w.Body.Bytes(), &res)
require.NoError(s.T(), err)
s.Equal(createPipelineJobSuccessMessage(jobSummary.Name, jobSummary.AppName, jobSummary.Branch, jobSummary.CommitID), res.Message)
s.Equal(createPipelineJobSuccessMessage(jobSummary.Name, jobSummary.AppName, jobSummary.Branch, "branch", jobSummary.CommitID), res.Message)
s.ctrl.Finish()
}

Expand All @@ -528,9 +528,18 @@ func (s *handlerTestSuite) Test_PushEventWithRefDeleted() {
}

func Test_GetBranch_RemovesRefsHead(t *testing.T) {
assert.Equal(t, "master", getBranch(&github.PushEvent{Ref: strPtr("refs/heads/master")}))
assert.Equal(t, "feature/RA-326-TestBranch", getBranch(&github.PushEvent{Ref: strPtr("refs/heads/feature/RA-326-TestBranch")}))
assert.Equal(t, "hotfix/api/refs/heads/fix1", getBranch(&github.PushEvent{Ref: strPtr("refs/heads/hotfix/api/refs/heads/fix1")}))
gitRef, gitRefType := getGitRefWithType(&github.PushEvent{Ref: strPtr("refs/tags/v1.0.2")})
assert.Equal(t, "v1.0.2", gitRef)
assert.Equal(t, "tag", gitRefType)
gitRef, gitRefType = getGitRefWithType(&github.PushEvent{Ref: strPtr("refs/heads/master")})
assert.Equal(t, "master", gitRef)
assert.Equal(t, "branch", gitRefType)
gitRef, gitRefType = getGitRefWithType(&github.PushEvent{Ref: strPtr("refs/heads/feature/RA-326-TestBranch")})
assert.Equal(t, "feature/RA-326-TestBranch", gitRef)
assert.Equal(t, "branch", gitRefType)
gitRef, gitRefType = getGitRefWithType(&github.PushEvent{Ref: strPtr("refs/heads/hotfix/api/refs/heads/fix1")})
assert.Equal(t, "hotfix/api/refs/heads/fix1", gitRef)
assert.Equal(t, "branch", gitRefType)
}

func (s *handlerTestSuite) Test_PushEventWithAnnotatedTag() {
Expand All @@ -550,7 +559,7 @@ func (s *handlerTestSuite) Test_PushEventWithAnnotatedTag() {
jobSummary := models.JobSummary{Name: "jobname", AppName: "jobappname", Branch: "jobbranchname", CommitID: headCommitID, TriggeredBy: "anyuser"}
s.apiServer.EXPECT().ShowApplications(gomock.Any(), "git@github.com:equinor/repo-1.git").Return([]*models.ApplicationSummary{&appSummary}, nil).Times(1)
s.apiServer.EXPECT().GetApplication(gomock.Any(), appName).Return(appDetail, nil).Times(1)
s.apiServer.EXPECT().TriggerPipeline(gomock.Any(), appName, tag, headCommitID, "").Return(&jobSummary, nil).Times(1)
s.apiServer.EXPECT().TriggerPipeline(gomock.Any(), appName, tag, "tag", headCommitID, "").Return(&jobSummary, nil).Times(1)

sut := NewWebHookHandler(s.apiServer)
req, _ := http.NewRequest("POST", "/", bytes.NewReader(payload))
Expand All @@ -562,7 +571,7 @@ func (s *handlerTestSuite) Test_PushEventWithAnnotatedTag() {
var res response
err := json.Unmarshal(s.w.Body.Bytes(), &res)
require.NoError(s.T(), err)
s.Equal(createPipelineJobSuccessMessage(jobSummary.Name, jobSummary.AppName, jobSummary.Branch, jobSummary.CommitID), res.Message)
s.Equal(createPipelineJobSuccessMessage(jobSummary.Name, jobSummary.AppName, jobSummary.Branch, "branch", jobSummary.CommitID), res.Message)
s.ctrl.Finish()
}

Expand All @@ -571,7 +580,7 @@ type response struct {
Error string `json:"error"`
}

// GitHubPayloadBuilder Handles construction of github payload
// GitHubPayloadBuilder Handles construction of GitHub payload
type GitHubPayloadBuilder interface {
withRef(refs string) GitHubPayloadBuilder
withAfter(after string) GitHubPayloadBuilder
Expand Down
27 changes: 14 additions & 13 deletions metrics/custom_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import (
)

const (
sshURLLabel = "radix_webhook_request_ssh_url"
branchLabel = "radix_webhook_request_branch"
commitIDLabel = "radix_webhook_request_commit_id"
appNameLabel = "radix_webhook_request_app_name"
sshURLLabel = "radix_webhook_request_ssh_url"
gitRefsLabel = "radix_webhook_request_branch"
gitRefsTypeLabel = "radix_webhook_request_refs_type"
commitIDLabel = "radix_webhook_request_commit_id"
appNameLabel = "radix_webhook_request_app_name"
)

var (
Expand Down Expand Up @@ -55,21 +56,21 @@ var (
Name: "radix_webhook_request_push_github_event_type_counter",
Help: "Counter for push GitHub event type requests",
},
[]string{sshURLLabel, branchLabel, commitIDLabel},
[]string{sshURLLabel, gitRefsLabel, gitRefsTypeLabel, commitIDLabel},
)
pushEventTypeTriggerPipelineCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "radix_webhook_request_push_github_event_type_trigger_pipeline_counter",
Help: "Counter for push GitHub event type trigger pipeline requests",
},
[]string{sshURLLabel, branchLabel, commitIDLabel, appNameLabel},
[]string{sshURLLabel, gitRefsLabel, gitRefsTypeLabel, commitIDLabel, appNameLabel},
)
pushEventTypeFailedTriggerPipelineCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "radix_webhook_request_push_github_event_type_failed_trigger_pipeline_counter",
Help: "Counter for push GitHub event type failed trigger pipeline requests",
},
[]string{sshURLLabel, branchLabel, commitIDLabel},
[]string{sshURLLabel, gitRefsLabel, gitRefsTypeLabel, commitIDLabel},
)
)

Expand Down Expand Up @@ -116,16 +117,16 @@ func IncreaseFailedCloneURLValidationCounter(sshURL string) {
}

// IncreasePushGithubEventTypeCounter increases all GitHub push event type request counter
func IncreasePushGithubEventTypeCounter(sshURL, branch, commitID string) {
pushEventTypeCounter.With(prometheus.Labels{sshURLLabel: sshURL, branchLabel: branch, commitIDLabel: commitID}).Inc()
func IncreasePushGithubEventTypeCounter(sshURL, gitRefs, gitRefsType, commitID string) {
pushEventTypeCounter.With(prometheus.Labels{sshURLLabel: sshURL, gitRefsLabel: gitRefs, gitRefsTypeLabel: gitRefsType, commitIDLabel: commitID}).Inc()
}

// IncreasePushGithubEventTypeTriggerPipelineCounter increases GitHub push event type trigger pipeline request counter
func IncreasePushGithubEventTypeTriggerPipelineCounter(sshURL, branch, commitID, appName string) {
pushEventTypeTriggerPipelineCounter.With(prometheus.Labels{sshURLLabel: sshURL, branchLabel: branch, commitIDLabel: commitID, appNameLabel: appName}).Inc()
func IncreasePushGithubEventTypeTriggerPipelineCounter(sshURL, gitRefs, gitRefsType, commitID, appName string) {
pushEventTypeTriggerPipelineCounter.With(prometheus.Labels{sshURLLabel: sshURL, gitRefsLabel: gitRefs, gitRefsTypeLabel: gitRefsType, commitIDLabel: commitID, appNameLabel: appName}).Inc()
}

// IncreasePushGithubEventTypeFailedTriggerPipelineCounter increases GitHub push event type failed trigger pipeline request counter
func IncreasePushGithubEventTypeFailedTriggerPipelineCounter(sshURL, branch, commitID string) {
pushEventTypeFailedTriggerPipelineCounter.With(prometheus.Labels{sshURLLabel: sshURL, branchLabel: branch, commitIDLabel: commitID}).Inc()
func IncreasePushGithubEventTypeFailedTriggerPipelineCounter(sshURL, gitRefs, gitRefsType, commitID string) {
pushEventTypeFailedTriggerPipelineCounter.With(prometheus.Labels{sshURLLabel: sshURL, gitRefsLabel: gitRefs, gitRefsTypeLabel: gitRefsType, commitIDLabel: commitID}).Inc()
}
31 changes: 30 additions & 1 deletion models/job_summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,41 @@ type JobSummary struct {
// AppName of the application
AppName string `json:"appName"`

// Branch branch to build from
// Branch to build from
Branch string `json:"branch"`

// GitRef Branch or tag to build from
//
// example: master
GitRef string `json:"gitRef,omitempty"`

// GitRefType When the pipeline job should be built from branch or tag specified in GitRef:
// - branch
// - tag
// - <empty> - either branch or tag
//
// example: "branch"
GitRefType string `json:"gitRefType,omitempty"`

// CommitID the commit ID of the branch to build
CommitID string `json:"commitID"`

// TriggeredBy of the job
TriggeredBy string `json:"triggeredBy"`
}

// GetGitRefOrDefault returns the GitRef if set, otherwise returns the Branch
func (jobSummary JobSummary) GetGitRefOrDefault() string {
if jobSummary.GitRef != "" {
return jobSummary.GitRef
}
return jobSummary.Branch
}

// GetGitRefTypeOrDefault returns the GitRefType if set, otherwise returns the Branch
func (jobSummary JobSummary) GetGitRefTypeOrDefault() string {
if jobSummary.GitRefType != "" {
return jobSummary.GitRefType
}
return "branch"
}
16 changes: 16 additions & 0 deletions models/pipeline_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,20 @@ type PipelineParameters struct {
// required: true
// example: 4faca8595c5283a9d0f17a623b9255a0d9866a2e
TriggeredBy string `json:"triggeredBy"`

// GitRef Branch or tag to build from
//
// required: false
// example: master
GitRef string `json:"gitRef,omitempty"`

// GitRefType When the pipeline job should be built from branch or tag specified in GitRef:
// - branch
// - tag
// - <empty> - either branch or tag
//
// required false
// enum: branch,tag,""
// example: "branch"
GitRefType string `json:"gitRefType,omitempty"`
}
2 changes: 1 addition & 1 deletion radix/api_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ import (
type APIServer interface {
ShowApplications(ctx context.Context, sshURL string) ([]*models.ApplicationSummary, error)
GetApplication(ctx context.Context, appName string) (*models.Application, error)
TriggerPipeline(ctx context.Context, appName, branch, commitID, triggeredBy string) (*models.JobSummary, error)
TriggerPipeline(ctx context.Context, appName, gitRef, gitRefType, commitID, triggeredBy string) (*models.JobSummary, error)
}
8 changes: 4 additions & 4 deletions radix/api_server_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions radix/api_server_stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ func (api *APIServerStub) GetApplication(ctx context.Context, appName string) (*
}

// TriggerPipeline Implementation
func (api *APIServerStub) TriggerPipeline(ctx context.Context, appName, branch, commitID, triggeredBy string) (*models.JobSummary, error) {
func (api *APIServerStub) TriggerPipeline(ctx context.Context, appName, gitRef, gitRefType, commitID, triggeredBy string) (*models.JobSummary, error) {
url := fmt.Sprintf(api.apiServerEndPoint+startPipelineEndPointPattern, appName, buildDeployPipeline)
parameters := models.PipelineParameters{Branch: branch, CommitID: commitID, TriggeredBy: triggeredBy}
parameters := models.PipelineParameters{GitRef: gitRef, GitRefType: gitRefType, CommitID: commitID, TriggeredBy: triggeredBy}

body, err := json.Marshal(parameters)
if err != nil {
Expand Down