Skip to content

Commit

Permalink
Ability of full configuration via env variables
Browse files Browse the repository at this point in the history
Implements #1
  • Loading branch information
flexoid committed May 23, 2023
1 parent 807a956 commit c3acb64
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 37 deletions.
23 changes: 9 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,29 @@ The name "Mergentle Reminder" is a playful combination of the words "merge" and
- Supports GitLab projects and groups.
- Filters out draft merge requests.
- Retrieves approvers and additional merge request information.
- Configurable with a YAML file.
- Configurable with a YAML file or environment variables.

## Screenshot

![Screenshot](./docs/screenshot_1.png)

## Configuration

You can configure the Mergentle Reminder bot using a `config.yaml` file. The YAML file allows you to specify GitLab projects, groups, and the GitLab instance URL.
You can configure the Mergentle Reminder bot using a `config.yaml` file or by setting the environment variables.

Example `config.yaml`:
Example config can be found in `config.yaml.example`.

```yaml
gitlab:
url: https://your-gitlab-instance.com
projects:
- id: 1
- id: 2
groups:
- id: 3
```
In addition to the config.yaml file, the following environment variables should be set:
In addition to the config.yaml file, the following environment variables can be set:

* `GITLAB_URL`: The URL of your GitLab instance (defaults to https://gitlab.com).
* `GITLAB_TOKEN`: Your GitLab personal access token.
* `SLACK_WEBHOOK_URL`: The webhook URL for the Slack channel where the bot will send messages.
* `PROJECTS`: A comma-separated list of GitLab project IDs to check for merge requests.
* `GROUPS`: A comma-separated list of GitLab group IDs to check for merge requests.
* `CONFIG_PATH` (optional): The path to the config.yaml configuration file. Defaults to config.yaml.

Environment variables take precedence over the config.yaml file.

## Building and Running the Application

### Locally
Expand Down
139 changes: 139 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package main

import (
"fmt"
"os"
"strconv"
"strings"
)

type Config struct {
GitLab struct {
URL string `yaml:"url"`
Token string `yaml:"token"`
} `yaml:"gitlab"`
Slack struct {
WebhookURL string `yaml:"webhook_url"`
} `yaml:"slack"`
Projects []ConfigProject
Groups []ConfigGroup
}

type ConfigGroup struct {
ID int `yaml:"id"`
}

type ConfigProject struct {
ID int `yaml:"id"`
}

type Env interface {
Getenv(key string) string
}

type OsEnv struct{}

func (e *OsEnv) Getenv(key string) string {
return os.Getenv(key)
}

func loadConfig(env Env) (*Config, error) {
configPath := env.Getenv("CONFIG_PATH")
if configPath == "" {
configPath = "config.yaml"
}

config := &Config{}
var err error

if _, err := os.Stat(configPath); err == nil {
config, err = readConfig(configPath)
if err != nil {
return nil, fmt.Errorf("Error reading configuration file: %v\n", err)
}
}

if env := env.Getenv("PROJECTS"); env != "" {
config.Projects, err = parseIDsAsConfigProjects(env)
if err != nil {
return nil, fmt.Errorf("Error parsing PROJECTS environment variable: %v\n", err)
}
}

if env := env.Getenv("GROUPS"); env != "" {
config.Groups, err = parseIDsAsConfigGroups(env)
if err != nil {
return nil, fmt.Errorf("Error parsing GROUPS environment variable: %v\n", err)
}
}

gitlabURL := env.Getenv("GITLAB_URL")
if gitlabURL != "" {
config.GitLab.URL = gitlabURL
}
if config.GitLab.URL == "" {
config.GitLab.URL = "https://gitlab.com"
}

gitlabToken := env.Getenv("GITLAB_TOKEN")
if gitlabToken != "" {
config.GitLab.Token = gitlabToken
}
if config.GitLab.Token == "" {
return nil, fmt.Errorf("GITLAB_TOKEN environment variable is required")
}

slackWebhookURL := env.Getenv("SLACK_WEBHOOK_URL")
if slackWebhookURL != "" {
config.Slack.WebhookURL = slackWebhookURL
}
if config.Slack.WebhookURL == "" {
return nil, fmt.Errorf("SLACK_WEBHOOK_URL environment variable is required")
}

if len(config.Projects) == 0 && len(config.Groups) == 0 {
return nil, fmt.Errorf("Neither groups nor projects were provided")
}

return config, nil
}

func parseIDsAsConfigProjects(env string) ([]ConfigProject, error) {
ids, err := parseIDs(env)
if err != nil {
return nil, err
}

var projects []ConfigProject
for _, id := range ids {
projects = append(projects, ConfigProject{ID: id})
}

return projects, nil
}

func parseIDsAsConfigGroups(env string) ([]ConfigGroup, error) {
ids, err := parseIDs(env)
if err != nil {
return nil, err
}

var groups []ConfigGroup
for _, id := range ids {
groups = append(groups, ConfigGroup{ID: id})
}

return groups, nil
}

func parseIDs(env string) ([]int, error) {
var ids []int
for _, idStr := range strings.Split(env, ",") {
id, err := strconv.Atoi(idStr)
if err != nil {
return nil, err
}
ids = append(ids, id)
}
return ids, nil
}
11 changes: 11 additions & 0 deletions config.test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
gitlab:
url: https://gitlab.example.com
token: abcdef1234567890
slack:
webhook_url: https://hooks.slack.com/services/your-slack-webhook-url
projects:
- id: 123
- id: 456
groups:
- id: 1
- id: 2
3 changes: 3 additions & 0 deletions config.yaml.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
gitlab:
url: https://gitlab.com
token: your-gitlab-token
slack:
webhook_url: https://hooks.slack.com/services/your-slack-webhook-url
projects:
- id: 123
- id: 456
Expand Down
80 changes: 80 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package main

import (
"testing"

"github.com/stretchr/testify/assert"
)

type MockEnv struct {
values map[string]string
}

func (e *MockEnv) Getenv(key string) string {
return e.values[key]
}

func TestLoadConfig(t *testing.T) {
// Test default values and required environment variables
t.Run("missing required env variables", func(t *testing.T) {
env := &MockEnv{values: map[string]string{}}
_, err := loadConfig(env)
assert.Error(t, err)
assert.Contains(t, err.Error(), "GITLAB_TOKEN environment variable is required")
})

t.Run("default GitLab URL", func(t *testing.T) {
env := &MockEnv{values: map[string]string{
"GITLAB_TOKEN": "token",
"SLACK_WEBHOOK_URL": "webhook",
"CONFIG_PATH": "NONEXISTING.yaml",
"PROJECTS": "1,2,3",
}}

config, err := loadConfig(env)
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com", config.GitLab.URL)
})

// Test overriding default values with environment variables
t.Run("env variables overriding defaults", func(t *testing.T) {
env := &MockEnv{values: map[string]string{
"GITLAB_URL": "https://gitlab.example.com",
"GITLAB_TOKEN": "token",
"SLACK_WEBHOOK_URL": "webhook",
"CONFIG_PATH": "NONEXISTING.yaml",
"PROJECTS": "1,2,3",
}}

config, err := loadConfig(env)
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.example.com", config.GitLab.URL)
assert.Equal(t, []ConfigProject{
{ID: 1},
{ID: 2},
{ID: 3},
}, config.Projects)
})

// Test loading config from file
t.Run("loading from config file", func(t *testing.T) {
env := &MockEnv{values: map[string]string{
"CONFIG_PATH": "config.test.yaml",
}}

config, err := loadConfig(env)
assert.NoError(t, err)

assert.Equal(t, "https://gitlab.example.com", config.GitLab.URL)
assert.Equal(t, "abcdef1234567890", config.GitLab.Token)
assert.Equal(t, "https://hooks.slack.com/services/your-slack-webhook-url", config.Slack.WebhookURL)
assert.Equal(t, []ConfigProject{
{ID: 123},
{ID: 456},
}, config.Projects)
assert.Equal(t, []ConfigGroup{
{ID: 1},
{ID: 2},
}, config.Groups)
})
}
26 changes: 3 additions & 23 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,11 @@ import (
"gopkg.in/yaml.v2"
)

type Config struct {
GitLab struct {
URL string `yaml:"url"`
} `yaml:"gitlab"`
Projects []ConfigProject
Groups []ConfigGroup
}

type ConfigGroup struct {
ID int `yaml:"id"`
}

type ConfigProject struct {
ID int `yaml:"id"`
}

func main() {
configPath := os.Getenv("CONFIG_PATH")
if configPath == "" {
configPath = "config.yaml"
}

config, err := readConfig(configPath)
// Load configuration
config, err := loadConfig(&OsEnv{})
if err != nil {
fmt.Printf("Error reading configuration file: %v\n", err)
fmt.Printf("%v\n", err)
os.Exit(1)
}

Expand Down

0 comments on commit c3acb64

Please sign in to comment.