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
157 changes: 84 additions & 73 deletions src/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,72 @@ import (
"go.uber.org/zap"
)

type GithubRepo struct {
FullName string `json:"full_name"`
}

type GithubWorkflow struct {
Workflow struct {
ID int `json:"id"`
Status string `json:"status"`
RunID int `json:"run_id"`
Name string `json:"name"`
Branch string `json:"head_branch"`
Repository GithubRepo `json:"repository"`
Conclusion string `json:"conclusion"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
HTMLURL string `json:"html_url"`
} `json:"workflow_run"`
}
type GithubJob struct {
Job struct {
ID int `json:"id"`
Status string `json:"status"`
Name string `json:"name"`
Branch string `json:"head_branch"`
Repository GithubRepo `json:"repository"`
RunnerName string `json:"runner_name"`
Conclusion string `json:"conclusion"`
StartedAt string `json:"started_at"`
CompletedAt string `json:"completed_at"`
WorkflowName string `json:"workflow_name"`
HTMLURL string `json:"html_url"`
} `json:"workflow_job"`
}

type GithubCommit struct {
Repository GithubRepo `json:"repository"`
Commits []struct {
ID string `json:"id"`
Author struct {
Name string `json:"name"`
Email string `json:"email"`
} `json:"author"`
} `json:"commits"`
Ref string `json:"ref"`
}

type GithubPullRequest struct {
Action string `json:"action"`
PullRequest struct {
ID int `json:"id"`
State string `json:"state"`
Title string `json:"title"`
Base struct {
Ref string `json:"ref"`
} `json:"base"`
Head struct {
Ref string `json:"ref"`
} `json:"head"`
User struct {
Login string `json:"login"`
Email string `json:"email"`
} `json:"user"`
} `json:"pull_request"`
Repository GithubRepo `json:"repository"`
}

func validateHMAC(body []byte, signature string, secret []byte) bool {
h := hmac.New(sha256.New, secret)
h.Write(body)
Expand Down Expand Up @@ -54,22 +120,8 @@ func githubEventsHandler(w http.ResponseWriter, r *http.Request) {
}

func updateWorkflowMetrics(body []byte) {
var payload struct {
Workflow struct {
ID int `json:"id"`
Status string `json:"status"`
RunID int `json:"run_id"`
Name string `json:"name"`
Branch string `json:"head_branch"`
Repository struct {
FullName string `json:"full_name"`
} `json:"repository"`
Conclusion string `json:"conclusion"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
HTMLURL string `json:"html_url"`
} `json:"workflow_run"`
}
var payload GithubWorkflow

if err := json.Unmarshal(body, &payload); err != nil {
logger.Error("Failed to unmarshal workflow_run payload", zap.Error(err))
return
Expand Down Expand Up @@ -132,22 +184,9 @@ func updateWorkflowMetrics(body []byte) {
}

func updateJobMetrics(body []byte) {
var payload struct {
Job struct {
ID int `json:"id"`
Status string `json:"status"`
Name string `json:"name"`
Branch string `json:"head_branch"`
Repository struct {
FullName string `json:"full_name"`
} `json:"repository"`
RunnerName string `json:"runner_name"`
Conclusion string `json:"conclusion"`
StartedAt string `json:"started_at"`
CompletedAt string `json:"completed_at"`
HTMLURL string `json:"html_url"`
} `json:"workflow_job"`
}

var payload GithubJob

if err := json.Unmarshal(body, &payload); err != nil {
logger.Error("Failed to unmarshal workflow_job payload", zap.Error(err))
return
Expand All @@ -157,7 +196,7 @@ func updateJobMetrics(body []byte) {
"runner": payload.Job.RunnerName,
"repository": payload.Job.Repository.FullName,
"branch": payload.Job.Branch,
"workflow_name": payload.Job.Name,
"workflow_name": payload.Job.WorkflowName,
"job_name": payload.Job.Name,
"job_status": payload.Job.Status,
"job_conclusion": payload.Job.Conclusion,
Expand All @@ -171,37 +210,37 @@ func updateJobMetrics(body []byte) {
"runner": payload.Job.RunnerName,
"repository": payload.Job.Repository.FullName,
"branch": payload.Job.Branch,
"workflow_name": payload.Job.Name,
"workflow_name": payload.Job.WorkflowName,
"job_name": payload.Job.Name,
}).Inc()
case "in_progress":
jobInProgressGauge.With(prometheus.Labels{
"runner": payload.Job.RunnerName,
"repository": payload.Job.Repository.FullName,
"branch": payload.Job.Branch,
"workflow_name": payload.Job.Name,
"workflow_name": payload.Job.WorkflowName,
"job_name": payload.Job.Name,
}).Inc()
jobQueuedGauge.With(prometheus.Labels{
"runner": payload.Job.RunnerName,
"repository": payload.Job.Repository.FullName,
"branch": payload.Job.Branch,
"workflow_name": payload.Job.Name,
"workflow_name": payload.Job.WorkflowName,
"job_name": payload.Job.Name,
}).Dec()
case "completed":
jobCompletedGauge.With(prometheus.Labels{
"runner": payload.Job.RunnerName,
"repository": payload.Job.Repository.FullName,
"branch": payload.Job.Branch,
"workflow_name": payload.Job.Name,
"workflow_name": payload.Job.WorkflowName,
"job_name": payload.Job.Name,
}).Inc()
jobInProgressGauge.With(prometheus.Labels{
"runner": payload.Job.RunnerName,
"repository": payload.Job.Repository.FullName,
"branch": payload.Job.Branch,
"workflow_name": payload.Job.Name,
"workflow_name": payload.Job.WorkflowName,
"job_name": payload.Job.Name,
}).Dec()

Expand All @@ -214,7 +253,7 @@ func updateJobMetrics(body []byte) {
"runner": payload.Job.RunnerName,
"repository": payload.Job.Repository.FullName,
"branch": payload.Job.Branch,
"workflow_name": payload.Job.Name,
"workflow_name": payload.Job.WorkflowName,
"job_name": payload.Job.Name,
"job_status": payload.Job.Status,
"job_conclusion": payload.Job.Conclusion,
Expand All @@ -224,19 +263,9 @@ func updateJobMetrics(body []byte) {
}

func updateCommitMetrics(body []byte) {
var payload struct {
Repository struct {
FullName string `json:"full_name"`
} `json:"repository"`
Commits []struct {
ID string `json:"id"`
Author struct {
Name string `json:"name"`
Email string `json:"email"`
} `json:"author"`
} `json:"commits"`
Ref string `json:"ref"`
}

var payload GithubCommit

if err := json.Unmarshal(body, &payload); err != nil {
logger.Error("Failed to unmarshal push payload", zap.Error(err))
return
Expand All @@ -253,27 +282,9 @@ func updateCommitMetrics(body []byte) {
}

func updatePullRequestMetrics(body []byte) {
var payload struct {
Action string `json:"action"`
PullRequest struct {
ID int `json:"id"`
State string `json:"state"`
Title string `json:"title"`
Base struct {
Ref string `json:"ref"`
} `json:"base"`
Head struct {
Ref string `json:"ref"`
} `json:"head"`
User struct {
Login string `json:"login"`
Email string `json:"email"`
} `json:"user"`
} `json:"pull_request"`
Repository struct {
FullName string `json:"full_name"`
} `json:"repository"`
}

var payload GithubPullRequest

if err := json.Unmarshal(body, &payload); err != nil {
logger.Error("Failed to unmarshal pull_request payload", zap.Error(err))
return
Expand Down
38 changes: 31 additions & 7 deletions src/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"encoding/hex"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -39,14 +41,30 @@ func TestValidateHMAC(t *testing.T) {
assert.True(t, valid)
}

func TestValidPayload(t *testing.T) {
body := []byte(`{"workflow_run": {"id": 1, "status": "completed", "run_id": 1001, "name": "CI", "head_branch": "main", "repository": {"full_name": "user/repo"}, "conclusion": "success", "created_at": "2023-01-01T00:00:00Z", "updated_at": "2023-01-01T01:00:00Z"}}`)
recorder := sendTestRequest(body, "workflow_run")
assert.Equal(t, http.StatusOK, recorder.Code)
func TestValidWorkflowPayload(t *testing.T) {
dir, err := os.ReadDir("../test_data")
if err != nil {
t.Fatalf("Failed to read test data directory: %v", err)
}
for _, file := range dir {
if file.IsDir() {
continue
}
body, err := os.ReadFile("../test_data/" + file.Name())
if err != nil {
t.Fatalf("Failed to read test data file: %v", err)
}
eventType := strings.TrimSuffix(file.Name(), ".json")
recorder := sendTestRequest(body, eventType)
assert.Equal(t, http.StatusOK, recorder.Code)
}
}

func TestInvalidSignature(t *testing.T) {
body := []byte(`{"workflow_run": {"id": 1, "status": "completed", "run_id": 1001, "name": "CI", "head_branch": "main", "repository": {"full_name": "user/repo"}, "conclusion": "success", "created_at": "2023-01-01T00:00:00Z", "updated_at": "2023-01-01T01:00:00Z"}}`)
body, err := os.ReadFile("../test_data/workflow_run.json")
if err != nil {
t.Fatalf("Failed to read test data file: %v", err)
}
recorder := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewBuffer(body))
req.Header.Set("X-Hub-Signature-256", "invalid_signature")
Expand All @@ -59,7 +77,10 @@ func TestInvalidSignature(t *testing.T) {
}

func TestMissingSignature(t *testing.T) {
body := []byte(`{"workflow_run": {"id": 1, "status": "completed", "run_id": 1001, "name": "CI", "head_branch": "main", "repository": {"full_name": "user/repo"}, "conclusion": "success", "created_at": "2023-01-01T00:00:00Z", "updated_at": "2023-01-01T01:00:00Z"}}`)
body, err := os.ReadFile("../test_data/workflow_run.json")
if err != nil {
t.Fatalf("Failed to read test data file: %v", err)
}
recorder := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewBuffer(body))
req.Header.Set("X-GitHub-Event", "workflow_run")
Expand All @@ -71,7 +92,10 @@ func TestMissingSignature(t *testing.T) {
}

func TestUnknownEvent(t *testing.T) {
body := []byte(`{"workflow_run": {"id": 1, "status": "completed", "run_id": 1001, "name": "CI", "head_branch": "main", "repository": {"full_name": "user/repo"}, "conclusion": "success", "created_at": "2023-01-01T00:00:00Z", "updated_at": "2023-01-01T01:00:00Z"}}`)
body, err := os.ReadFile("../test_data/workflow_run.json")
if err != nil {
t.Fatalf("Failed to read test data file: %v", err)
}
recorder := sendTestRequest(body, "unknown_event")
assert.Equal(t, http.StatusOK, recorder.Code)
}
32 changes: 18 additions & 14 deletions src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,25 +108,12 @@ func init() {
defer logger.Sync()
}

func main() {
port := strings.TrimSpace(os.Getenv("PROMGITHUB_SERVICE_PORT"))
if port == "" {
port = "8080"
}

ghWebhookSecretEnv := strings.TrimSpace(os.Getenv("PROMGITHUB_WEBHOOK_SECRET"))
if ghWebhookSecretEnv == "" {
logger.Fatal("Environment variable PROMGITHUB_WEBHOOK_SECRET is not set")
}
githubWebhookSecret = []byte(ghWebhookSecretEnv)

func setupRouter(logger *zap.Logger) *mux.Router {
r := mux.NewRouter()
r.Use(APIHandler(logger))

r.HandleFunc("/health", healthCheck).Methods("GET")

r.Handle("/metrics", promhttp.Handler())

r.HandleFunc("/webhook", githubEventsHandler).Methods("POST")

// Profiling endpoints
Expand All @@ -138,6 +125,23 @@ func main() {
r.HandleFunc("/debug/pprof/trace", pprof.Trace)
}

return r
}

func main() {
port := strings.TrimSpace(os.Getenv("PROMGITHUB_SERVICE_PORT"))
if port == "" {
port = "8080"
}

ghWebhookSecretEnv := strings.TrimSpace(os.Getenv("PROMGITHUB_WEBHOOK_SECRET"))
if ghWebhookSecretEnv == "" {
logger.Fatal("Environment variable PROMGITHUB_WEBHOOK_SECRET is not set")
}
githubWebhookSecret = []byte(ghWebhookSecretEnv)

r := setupRouter(logger)

logger.Info("Starting server", zap.String("port", port))
if err := http.ListenAndServe(":"+port, r); err != nil {
logger.Fatal("Error starting server", zap.Error(err))
Expand Down
Loading