diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..da21c28 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.18-alpine + +# Install migrate tool +RUN apk add --no-cache curl bash && \ + curl -L https://github.com/golang-migrate/migrate/releases/download/v4.15.2/migrate.linux-amd64.tar.gz | \ + tar xvz && mv migrate.linux-amd64 /usr/local/bin/migrate + +# Set working directory +WORKDIR /app + +# Copy go mod and sum files +COPY go.mod go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy the source code +COPY . . + +# Expose port +EXPOSE 8080 + +# Run migrations and start the application +CMD ["sh", "-c", "./scripts/migrate.sh up && go run cmd/server/main.go"] diff --git a/README.md b/README.md index 465a748..5a62442 100644 --- a/README.md +++ b/README.md @@ -1 +1,244 @@ -# github-actions-aggregator \ No newline at end of file +# GitHub Actions Aggregator Service + +A Go-based service to aggregate and analyze data from GitHub Actions workflows across multiple repositories. This application provides insights into workflow runs, success rates, failure rates, and other statistics over customizable time ranges. + +## Table of Contents + +- [Features](#features) +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Configuration](#configuration) +- [Usage](#usage) +- [API Endpoints](#api-endpoints) +- [Authentication](#authentication) +- [Testing](#testing) +- [Contributing](#contributing) +- [License](#license) + +## Features + +- **OAuth 2.0 Authentication**: Securely authenticate users via GitHub OAuth. +- **Data Collection**: + - **Webhooks**: Receive real-time updates on workflow events. + - **Polling**: Periodically poll GitHub API to ensure data completeness. +- **Data Aggregation**: Compute statistics like success rates, failure rates, and more. +- **API Endpoints**: Expose RESTful APIs for accessing aggregated data. +- **Background Processing**: Use worker pools to handle asynchronous tasks. +- **Configurable**: Easily adjust settings like polling intervals and webhook secrets. +- **Secure**: Validate webhook payloads and protect routes with authentication middleware. + +## Prerequisites + +- **Go**: Version 1.18 or higher. +- **GitHub Account**: For OAuth authentication and API access. +- **PostgreSQL**: For storing data. +- **Redis** (optional): For caching (if implemented). +- **Docker** (optional): For containerization and deployment. + +## Installation + +1. **Clone the Repository** + + ```bash + git clone https://github.com/yourusername/github-actions-aggregator.git + cd github-actions-aggregator + ``` + +2. **Install Dependencies** + + ```bash + go mod download + ``` + +3. **Set Up Environment Variables** + + Create a `.env` file or export the required environment variables: + + ```bash + export GITHUB_CLIENT_ID="your_github_client_id" + export GITHUB_CLIENT_SECRET="your_github_client_secret" + export GITHUB_ACCESS_TOKEN="your_github_access_token" + export GITHUB_WEBHOOK_SECRET="your_webhook_secret" + export DATABASE_URL="postgres://username:password@localhost:5432/yourdbname?sslmode=disable" + export SERVER_PORT="8080" + ``` + +## Configuration + +Configuration can be managed via a `config.yaml` file in the `configs/` directory or through environment variables. + +**Example `config.yaml`:** + +```yaml +server: + port: "8080" + +log: + level: "info" + +github: + client_id: "your_github_client_id" + client_secret: "your_github_client_secret" + access_token: "your_github_access_token" + webhook_secret: "your_webhook_secret" +``` + +**Note:** Environment variables override values in the configuration file. + +## Usage + +### Running the Application + +1. **Run Database Migrations** + + ```bash + ./scripts/migrate.sh + ``` + +2. **Start the Application** + + ```bash + go run cmd/server/main.go + ``` + +### Accessing the Application + +- **Login with GitHub**: Navigate to `http://localhost:8080/login` to authenticate via GitHub. +- **API Requests**: Use tools like `curl` or Postman to interact with the API endpoints. + +## API Endpoints + +### Authentication + +- `GET /login`: Redirects the user to GitHub for OAuth authentication. +- `GET /callback`: Handles the OAuth callback from GitHub. + +### Workflow Statistics + +- `GET /workflows/:id/stats`: Retrieves statistics for a specific workflow. + + **Query Parameters:** + + - `start_time` (optional): Start of the time range (ISO 8601 format). + - `end_time` (optional): End of the time range (ISO 8601 format). + + **Example Request:** + + ```http + GET /workflows/123/stats?start_time=2023-09-01T00:00:00Z&end_time=2023-09-30T23:59:59Z + ``` + + **Example Response:** + + ```json + { + "workflow_id": 123, + "workflow_name": "CI Build and Test", + "total_runs": 200, + "success_count": 150, + "failure_count": 30, + "cancelled_count": 10, + "timed_out_count": 5, + "action_required_count": 5, + "success_rate": 75.0, + "failure_rate": 15.0, + "cancelled_rate": 5.0, + "timed_out_rate": 2.5, + "action_required_rate": 2.5, + "start_time": "2023-09-01T00:00:00Z", + "end_time": "2023-09-30T23:59:59Z" + } + ``` + +## Authentication + +### Setting Up OAuth with GitHub + +1. **Register a New OAuth Application** + + - Go to [GitHub Developer Settings](https://github.com/settings/developers). + - Click on **"New OAuth App"**. + - Fill in the application details: + - **Application Name** + - **Homepage URL**: `http://localhost:8080` + - **Authorization Callback URL**: `http://localhost:8080/callback` + - Obtain your **Client ID** and **Client Secret**. + +2. **Configure Application Credentials** + + Set your `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` in your environment variables or `config.yaml`. + +### Permissions and Scopes + +Ensure that your GitHub OAuth application has the necessary scopes: + +- `read:user` +- `repo` +- `workflow` + +## Testing + +### Running Unit Tests + +```bash +go test ./tests/unit/... +``` + +### Running Integration Tests + +```bash +go test ./tests/integration/... +``` + +### Test Coverage + +You can generate a test coverage report using: + +```bash +go test -coverprofile=coverage.out ./... +go tool cover -html=coverage.out +``` + +## Contributing + +Contributions are welcome! Please follow these steps: + +1. **Fork the Repository** + + Click on the "Fork" button at the top right of the repository page. + +2. **Clone Your Fork** + + ```bash + git clone https://github.com/yourusername/github-actions-aggregator.git + ``` + +3. **Create a Feature Branch** + + ```bash + git checkout -b feature/your-feature-name + ``` + +4. **Commit Your Changes** + + ```bash + git commit -am "Add new feature" + ``` + +5. **Push to Your Fork** + + ```bash + git push origin feature/your-feature-name + ``` + +6. **Create a Pull Request** + + Go to the original repository and open a pull request. + +## License + +This project is licensed under the [MIT License](LICENSE). + +--- + +**Disclaimer:** This project is not affiliated with GitHub. Ensure compliance with GitHub's [Terms of Service](https://docs.github.com/en/github/site-policy/github-terms-of-service) when using their APIs. \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index 8ae73fa..688b438 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -3,43 +3,44 @@ package main import ( "log" "os" - "os/signal" - "syscall" - - "github.com/mooshe3/github-actions-aggregator/pkg/api" - "github.com/mooshe3/github-actions-aggregator/pkg/config" - "github.com/mooshe3/github-actions-aggregator/pkg/db" - "github.com/mooshe3/github-actions-aggregator/pkg/logger" - "github.com/mooshe3/github-actions-aggregator/pkg/worker" + "os/exec" + + "github.com/moosh3/github-actions-aggregator/pkg/api" + "github.com/moosh3/github-actions-aggregator/pkg/config" + "github.com/moosh3/github-actions-aggregator/pkg/db" + "github.com/moosh3/github-actions-aggregator/pkg/github" + "github.com/moosh3/github-actions-aggregator/pkg/logger" ) func main() { - // Initialize configurations + // Load configuration cfg := config.LoadConfig() // Initialize logger logger.Init(cfg.LogLevel) + // Run migrations + err := runMigrations() + if err != nil { + log.Fatalf("Failed to run migrations: %v", err) + } + // Initialize database database, err := db.InitDB(cfg) if err != nil { log.Fatalf("Failed to connect to database: %v", err) } - // Start the worker pool - wp := worker.NewWorkerPool(database, 5) // Adjust the number of workers as needed - wp.Start() + // Initialize GitHub client + githubClient := github.NewClient(cfg.GitHub.AccessToken) // Start the API server - go api.StartServer(cfg, database) - - // Wait for interrupt signal to gracefully shut down the worker pool - quit := make(chan os.Signal, 1) - signal.Notify(quit, os.Interrupt, syscall.SIGTERM) - <-quit - - log.Println("Shutting down worker pool...") - wp.Stop() + api.StartServer(cfg, database, githubClient) +} - log.Println("Server exiting") +func runMigrations() error { + cmd := exec.Command("./scripts/migrate.sh", "up") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() } diff --git a/configs/config.yaml b/configs/config.yaml new file mode 100644 index 0000000..a9619ec --- /dev/null +++ b/configs/config.yaml @@ -0,0 +1,11 @@ +server: + port: "8080" + +log: + level: "info" + +github: + client_id: "your_github_client_id" + client_secret: "your_github_client_secret" + access_token: "your_github_access_token" + webhook_secret: "your_webhook_secret" \ No newline at end of file diff --git a/migrations/20231015010101_create_users_table.down.sql b/migrations/20231015010101_create_users_table.down.sql new file mode 100644 index 0000000..365a210 --- /dev/null +++ b/migrations/20231015010101_create_users_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/migrations/20231015010101_create_users_table.up.sql b/migrations/20231015010101_create_users_table.up.sql new file mode 100644 index 0000000..87628b4 --- /dev/null +++ b/migrations/20231015010101_create_users_table.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) NOT NULL UNIQUE, + email VARCHAR(255) NOT NULL UNIQUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/pkg/api/handlers.go b/pkg/api/handlers.go index a9add4c..2b9036a 100644 --- a/pkg/api/handlers.go +++ b/pkg/api/handlers.go @@ -2,15 +2,128 @@ package api import ( "net/http" + "strconv" + "time" "github.com/gin-gonic/gin" + "github.com/moosh3/github-actions-aggregator/pkg/db/models" + "gorm.io/gorm" ) -func GetStats(c *gin.Context) { - // Placeholder for actual logic - stats := map[string]interface{}{ - "total_runs": 100, - "success_rate": 95.0, +func GetWorkflowStats(c *gin.Context) { + workflowIDParam := c.Param("id") + startTimeParam := c.Query("start_time") + endTimeParam := c.Query("end_time") + + // Convert workflowID to integer + workflowID, err := strconv.ParseInt(workflowIDParam, 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid workflow ID"}) + return + } + + // Default start and end times + defaultStartTime := time.Now().AddDate(0, 0, -30) // Default to 30 days ago + defaultEndTime := time.Now() + + // Parse start_time + startTime, err := parseTimeParameter(startTimeParam, defaultStartTime) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Parse end_time + endTime, err := parseTimeParameter(endTimeParam, defaultEndTime) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Access the database + db, ok := c.MustGet("db").(*gorm.DB) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not found"}) + return + } + + // Check if the workflow exists + var workflow models.Workflow + err = db.First(&workflow, "id = ?", workflowID).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "Workflow not found"}) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve workflow"}) + } + return } - c.JSON(http.StatusOK, stats) + + // Query workflow runs + var runs []models.WorkflowRun + err = db.Where("workflow_id = ?", workflowID). + Where("created_at BETWEEN ? AND ?", startTime, endTime). + Find(&runs).Error + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve workflow runs"}) + return + } + + // Ensure startTime is before endTime + if !startTime.Before(endTime) { + c.JSON(http.StatusBadRequest, gin.H{"error": "start_time must be before end_time"}) + return + } + // Initialize counters + totalRuns := len(runs) + successCount := 0 + failureCount := 0 + cancelledCount := 0 + timedOutCount := 0 + actionRequiredCount := 0 + + for _, run := range runs { + switch run.Conclusion { + case "success": + successCount++ + case "failure": + failureCount++ + case "cancelled": + cancelledCount++ + case "timed_out": + timedOutCount++ + case "action_required": + actionRequiredCount++ + } + } + + // Calculate percentages + var successRate, failureRate, cancelledRate, timedOutRate, actionRequiredRate float64 + if totalRuns > 0 { + successRate = float64(successCount) / float64(totalRuns) * 100 + failureRate = float64(failureCount) / float64(totalRuns) * 100 + cancelledRate = float64(cancelledCount) / float64(totalRuns) * 100 + timedOutRate = float64(timedOutCount) / float64(totalRuns) * 100 + actionRequiredRate = float64(actionRequiredCount) / float64(totalRuns) * 100 + } + + // Respond with extended statistics + c.JSON(http.StatusOK, gin.H{ + "workflow_id": workflowID, + "workflow_name": workflow.Name, + "total_runs": totalRuns, + "success_count": successCount, + "failure_count": failureCount, + "cancelled_count": cancelledCount, + "timed_out_count": timedOutCount, + "action_required_count": actionRequiredCount, + "success_rate": successRate, + "failure_rate": failureRate, + "cancelled_rate": cancelledRate, + "timed_out_rate": timedOutRate, + "action_required_rate": actionRequiredRate, + "start_time": startTime.Format(time.RFC3339), + "end_time": endTime.Format(time.RFC3339), + }) + } diff --git a/pkg/api/helpers.go b/pkg/api/helpers.go new file mode 100644 index 0000000..cbf9cdd --- /dev/null +++ b/pkg/api/helpers.go @@ -0,0 +1,84 @@ +package api + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +func parseTimeParameter(param string, defaultTime time.Time) (time.Time, error) { + if param == "" { + return defaultTime, nil + } + + // Handle special keywords + if param == "now" { + return time.Now(), nil + } + + // Check for relative time expressions + if strings.HasSuffix(param, "_ago") { + return parseRelativeTimeAgo(param) + } else if strings.Contains(param, "_") { + return parseRelativeTime(param) + } else { + // Try to parse as an explicit date-time + t, err := time.Parse(time.RFC3339, param) + if err != nil { + return time.Time{}, fmt.Errorf("invalid time format: %v", param) + } + return t, nil + } +} + +func parseRelativeTime(param string) (time.Time, error) { + parts := strings.Split(param, "_") + if len(parts) != 2 { + return time.Time{}, fmt.Errorf("invalid relative time format: %v", param) + } + + value, err := strconv.Atoi(parts[0]) + if err != nil { + return time.Time{}, fmt.Errorf("invalid number in relative time: %v", param) + } + + unit := parts[1] + + return calculateRelativeTime(value, unit) +} + +func parseRelativeTimeAgo(param string) (time.Time, error) { + parts := strings.Split(param, "_") + if len(parts) != 3 || parts[2] != "ago" { + return time.Time{}, fmt.Errorf("invalid relative time format: %v", param) + } + + value, err := strconv.Atoi(parts[0]) + if err != nil { + return time.Time{}, fmt.Errorf("invalid number in relative time: %v", param) + } + + unit := parts[1] + + return calculateRelativeTime(value, unit) +} + +func calculateRelativeTime(value int, unit string) (time.Time, error) { + switch unit { + case "minute", "minutes": + return time.Now().Add(-time.Duration(value) * time.Minute), nil + case "hour", "hours": + return time.Now().Add(-time.Duration(value) * time.Hour), nil + case "day", "days": + return time.Now().AddDate(0, 0, -value), nil + case "week", "weeks": + return time.Now().AddDate(0, 0, -value*7), nil + case "month", "months": + return time.Now().AddDate(0, -value, 0), nil + case "year", "years": + return time.Now().AddDate(-value, 0, 0), nil + default: + return time.Time{}, fmt.Errorf("invalid time unit in relative time: %v", unit) + } +} diff --git a/pkg/api/router.go b/pkg/api/router.go index b73e427..5ce24b6 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -4,10 +4,11 @@ import ( "github.com/gin-gonic/gin" "github.com/moosh3/github-actions-aggregator/pkg/auth" "github.com/moosh3/github-actions-aggregator/pkg/config" + "github.com/moosh3/github-actions-aggregator/pkg/db" "github.com/moosh3/github-actions-aggregator/pkg/github" ) -func StartServer(cfg *config.Config) { +func StartServer(cfg *config.Config, db *db.Database, githubClient *github.Client) { r := gin.Default() // Public routes @@ -15,15 +16,13 @@ func StartServer(cfg *config.Config) { r.GET("/callback", auth.GitHubCallback) // Webhook route (exclude middleware that could interfere) - webhookHandler := github.NewWebhookHandler(db, cfg.GitHub.WebhookSecret) + webhookHandler := github.NewWebhookHandler(db, githubClient, cfg.Github.WebhookSecret) r.POST("/webhook", webhookHandler.HandleWebhook) // Protected routes protected := r.Group("/", auth.AuthMiddleware()) { - protected.GET("/dashboard", dashboardHandler) - protected.GET("/stats", statsHandler) - // Add other protected routes + protected.GET("/workflows/:id/stats", GetWorkflowStats) } r.Run(":" + cfg.ServerPort) diff --git a/pkg/auth/middleware.go b/pkg/auth/middleware.go index d321266..be15586 100644 --- a/pkg/auth/middleware.go +++ b/pkg/auth/middleware.go @@ -1,32 +1,35 @@ package auth import ( - "net/http" + "net/http" + "strconv" "github.com/gin-gonic/gin" "github.com/moosh3/github-actions-aggregator/pkg/db/models" ) func AuthMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - user := getUserFromSession(c) - if user == nil { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return func(c *gin.Context) { + user := getUserFromSession(c) + if user == nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } c.Set("user", user) c.Next() - } + } } func getUserFromSession(c *gin.Context) *models.GitHubUser { - // Retrieve user information from session or cookie - // For example, using a secure cookie: - userID, err := c.Cookie("user_id") - if err != nil { - return nil - } - // Fetch user from database using userID - // Return the user object - return &GitHubUser{ID: /* fetched ID */} + // Retrieve user information from session or cookie + // For example, using a secure cookie: + userID, err := c.Cookie("user_id") + if err != nil { + return nil + } + // Convert userID to int64 + id, _ := strconv.ParseInt(userID, 10, 64) + // Fetch user from database using userID + // Return the user object + return &models.GitHubUser{ID: id} } diff --git a/pkg/auth/oauth.go b/pkg/auth/oauth.go index 67b7bef..142b990 100644 --- a/pkg/auth/oauth.go +++ b/pkg/auth/oauth.go @@ -7,6 +7,7 @@ import ( "encoding/json" "net/http" "os" + "strconv" "github.com/gin-gonic/gin" "golang.org/x/oauth2" @@ -54,6 +55,7 @@ func GitHubCallback(c *gin.Context) { // Save or update user in database (implement saveOrUpdateUser) // Set user session (implement setUserSession) + setUserSession(c, user) c.Redirect(http.StatusFound, "/dashboard") } @@ -108,3 +110,7 @@ func getEnv(key, fallback string) string { } return fallback } + +func setUserSession(c *gin.Context, user *GitHubUser) { + c.SetCookie("user_id", strconv.FormatInt(user.ID, 10), 300, "/", "", false, true) +} diff --git a/pkg/config/config.go b/pkg/config/config.go index ae9de10..800c6c4 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -6,10 +6,25 @@ import ( "github.com/spf13/viper" ) +type GitHubConfig struct { + ClientID string + ClientSecret string + AccessToken string + WebhookSecret string +} + +type DatabaseConfig struct { + Host string + Port string + User string + Password string +} + type Config struct { ServerPort string LogLevel string - // Add other configuration fields + GitHub GitHubConfig + Database DatabaseConfig } func LoadConfig() *Config { @@ -25,6 +40,17 @@ func LoadConfig() *Config { return &Config{ ServerPort: viper.GetString("server.port"), LogLevel: viper.GetString("log.level"), - // Initialize other fields + GitHub: GitHubConfig{ + ClientID: viper.GetString("github.client_id"), + ClientSecret: viper.GetString("github.client_secret"), + AccessToken: viper.GetString("github.access_token"), + WebhookSecret: viper.GetString("github.webhook_secret"), + }, + Database: DatabaseConfig{ + Host: viper.GetString("database.host"), + Port: viper.GetString("database.port"), + User: viper.GetString("database.user"), + Password: viper.GetString("database.password"), + }, } } diff --git a/pkg/github/client.go b/pkg/github/client.go index 2f4182d..b8aa51f 100644 --- a/pkg/github/client.go +++ b/pkg/github/client.go @@ -1,5 +1,51 @@ package github -type GitHubClient interface { - GetWorkflowRuns(repo string) ([]WorkflowRun, error) +import ( + "context" + + gh "github.com/google/go-github/v50/github" + "golang.org/x/oauth2" +) + +type Client struct { + ghClient *gh.Client + ctx context.Context +} + +func NewClient(token string) *Client { + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + tc := oauth2.NewClient(ctx, ts) + client := gh.NewClient(tc) + + return &Client{ + ghClient: client, + ctx: ctx, + } +} + +func (c *Client) ListWorkflows(owner, repo string) ([]*gh.Workflow, error) { + workflows, _, err := c.ghClient.Actions.ListWorkflows(c.ctx, owner, repo, nil) + if err != nil { + return nil, err + } + return workflows.Workflows, nil +} + +func (c *Client) ListWorkflowRuns(owner, repo string, workflowID int64) ([]*gh.WorkflowRun, error) { + runs, _, err := c.ghClient.Actions.ListWorkflowRunsByID(c.ctx, owner, repo, workflowID, nil) + if err != nil { + return nil, err + } + return runs.WorkflowRuns, nil +} + +func (c *Client) GetWorkflowRun(owner, repo string, runID int64) (*gh.WorkflowRun, error) { + run, _, err := c.ghClient.Actions.GetWorkflowRunByID(c.ctx, owner, repo, runID) + if err != nil { + return nil, err + } + return run, nil } diff --git a/pkg/github/client_impl.go b/pkg/github/client_impl.go deleted file mode 100644 index 0d9149d..0000000 --- a/pkg/github/client_impl.go +++ /dev/null @@ -1,9 +0,0 @@ -package github - -type clientImpl struct { - // ... -} - -func (c *clientImpl) GetWorkflowRuns(repo string) ([]WorkflowRun, error) { - // Actual implementation -} diff --git a/pkg/github/polling.go b/pkg/github/polling.go index c517936..f1c39a8 100644 --- a/pkg/github/polling.go +++ b/pkg/github/polling.go @@ -12,6 +12,8 @@ import ( "golang.org/x/oauth2" ) +const maxConcurrentPolls = 10 + type Poller struct { db *db.Database ghClient *gh.Client diff --git a/pkg/github/webhook.go b/pkg/github/webhook.go index 6a6159c..d1837d5 100644 --- a/pkg/github/webhook.go +++ b/pkg/github/webhook.go @@ -9,21 +9,25 @@ import ( "github.com/gin-gonic/gin" "github.com/google/go-github/v50/github" - "github.com/mooshe3/github-actions-aggregator/pkg/db" + "github.com/moosh3/github-actions-aggregator/pkg/db" + "github.com/moosh3/github-actions-aggregator/pkg/worker" ) type WebhookHandler struct { - db *db.Database - webhookSecret []byte + db *db.Database + client *Client + whSecret []byte + worker *worker.WorkerPool } -func NewWebhookHandler(db *db.Database, secret string) *WebhookHandler { +func NewWebhookHandler(db *db.Database, client *Client, secret string, worker *worker.WorkerPool) *WebhookHandler { return &WebhookHandler{ - db: db, - webhookSecret: []byte(secret), + db: db, + client: client, + whSecret: []byte(secret), + worker: worker, } } - func (wh *WebhookHandler) HandleWebhook(c *gin.Context) { payload, err := ioutil.ReadAll(c.Request.Body) if err != nil { @@ -59,7 +63,7 @@ func (wh *WebhookHandler) HandleWebhook(c *gin.Context) { } func (wh *WebhookHandler) verifySignature(signature string, payload []byte) bool { - mac := hmac.New(sha256.New, wh.webhookSecret) + mac := hmac.New(sha256.New, wh.whSecret) mac.Write(payload) expectedSignature := "sha256=" + hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(signature), []byte(expectedSignature)) @@ -78,7 +82,7 @@ func (wh *WebhookHandler) handleWorkflowRunEvent(event *github.WorkflowRunEvent) } // Enqueue a job to aggregate data after a new run is saved - wh.workerPool.JobQueue <- worker.Job{ + wh.worker.JobQueue <- worker.Job{ Type: "aggregate_data", } diff --git a/scripts/migrate.sh b/scripts/migrate.sh new file mode 100644 index 0000000..cd02c85 --- /dev/null +++ b/scripts/migrate.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +set -e + +# Configuration +DB_HOST=${DB_HOST:-"localhost"} +DB_PORT=${DB_PORT:-5432} +DB_USER=${DB_USER:-"postgres"} +DB_PASSWORD=${DB_PASSWORD:-"password"} +DB_NAME=${DB_NAME:-"github_actions_aggregator"} +MIGRATIONS_DIR=${MIGRATIONS_DIR:-"./migrations"} + +# Command-line arguments +COMMAND=$1 + +# Check if the migrate tool is installed +if ! [ -x "$(command -v migrate)" ]; then + echo 'Error: migrate is not installed.' >&2 + echo 'You can install it by running:' + echo ' curl -L https://github.com/golang-migrate/migrate/releases/download/v4.15.2/migrate.linux-amd64.tar.gz | tar xvz' + echo ' sudo mv migrate.linux-amd64 /usr/local/bin/migrate' + exit 1 +fi + +# Build the database URL +DB_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable" + +case $COMMAND in + "up") + echo "Applying all up migrations..." + migrate -path "${MIGRATIONS_DIR}" -database "${DB_URL}" up + ;; + "down") + echo "Reverting the last migration..." + migrate -path "${MIGRATIONS_DIR}" -database "${DB_URL}" down 1 + ;; + "force") + VERSION=$2 + if [ -z "$VERSION" ]; then + echo "Please specify the version to force." + exit 1 + fi + echo "Forcing migration version to ${VERSION}..." + migrate -path "${MIGRATIONS_DIR}" -database "${DB_URL}" force "${VERSION}" + ;; + "version") + echo "Current migration version:" + migrate -path "${MIGRATIONS_DIR}" -database "${DB_URL}" version + ;; + *) + echo "Usage: $0 {up|down|force |version}" + exit 1 + ;; +esac + +echo "Migration command '${COMMAND}' completed successfully."