Skip to content

Commit

Permalink
Add cron mode support
Browse files Browse the repository at this point in the history
Ability to run in a daemon mode, where the schedule is configured
with the cron-like spec.
  • Loading branch information
flexoid committed Jan 24, 2025
1 parent d23a41d commit 8270ffd
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 25 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,22 @@ In addition to the config.yaml file, the following environment variables can be
- `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.
- `CRON_SCHEDULE` (optional): The cron schedule for the bot to run. See [Run mode](#run-mode) and [supported format](https://github.com/reugn/go-quartz?tab=readme-ov-file#cron-expression-format).

Environment variables take precedence over the config.yaml file.

### Run mode

The bot can run in two modes: one-shot and cron.

When no `CRON_SCHEDULE` variable or `cron_schedule` config parameter specified, the bot will execute in one-shot mode.
In this mode, the bot will check for merge requests and send a message to the Slack channel once, then exit.
It is useful for testing or integrating with something like CI pipeline or Kubernetes CronJob.

When the cron schedule is specified, the bot will run in cron mode.
In this mode, the bot will check for merge requests and send a message to the Slack channel according to the specified cron schedule.
Well suited for running as as a container or daemon process.

## Building and Running the Application

### Locally
Expand Down Expand Up @@ -85,6 +98,8 @@ kubectl -n mergentle-reminder create secret generic mergentle-reminder-secrets -
Edit `schedule` in `k8s/cronjob.yaml` to specify the desired schedule. Set to run every hour by default.
See the [CronJob documentation](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/) for more information.

Make sure that the config does not include `cron_schedule` field, see

Apply the Kubernetes manifests:

```sh
Expand Down
27 changes: 25 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"os"
"strconv"
"strings"

"gopkg.in/yaml.v2"
)

type Config struct {
Expand All @@ -15,8 +17,9 @@ type Config struct {
Slack struct {
WebhookURL string `yaml:"webhook_url"`
} `yaml:"slack"`
Projects []ConfigProject
Groups []ConfigGroup
Projects []ConfigProject
Groups []ConfigGroup
CronSchedule string `yaml:"cron_schedule"`
}

type ConfigGroup struct {
Expand Down Expand Up @@ -91,13 +94,33 @@ func loadConfig(env Env) (*Config, error) {
return nil, fmt.Errorf("SLACK_WEBHOOK_URL environment variable is required")
}

cronSchedule := env.Getenv("CRON_SCHEDULE")
if cronSchedule != "" {
config.CronSchedule = cronSchedule
}

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

return config, nil
}

func readConfig(file string) (*Config, error) {
data, err := os.ReadFile(file)
if err != nil {
return nil, err
}

var config Config
err = yaml.Unmarshal(data, &config)
if err != nil {
return nil, err
}

return &config, nil
}

func parseIDsAsConfigProjects(env string) ([]ConfigProject, error) {
ids, err := parseIDs(env)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions config.test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ projects:
groups:
- id: 1
- id: 2
cron_schedule: "0 7,13 * * 1-5"
1 change: 1 addition & 0 deletions config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ projects:
groups:
- id: 1
- id: 2
cron_schedule: "0 7,13 * * 1-5"
3 changes: 3 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func TestLoadConfig(t *testing.T) {
"SLACK_WEBHOOK_URL": "webhook",
"CONFIG_PATH": "NONEXISTING.yaml",
"PROJECTS": "1,2,3",
"CRON_SCHEDULE": "0 1 * * *",
}}

config, err := loadConfig(env)
Expand All @@ -54,6 +55,7 @@ func TestLoadConfig(t *testing.T) {
{ID: 2},
{ID: 3},
}, config.Projects)
assert.Equal(t, "0 1 * * *", config.CronSchedule)
})

// Test loading config from file
Expand All @@ -76,5 +78,6 @@ func TestLoadConfig(t *testing.T) {
{ID: 1},
{ID: 2},
}, config.Groups)
assert.Equal(t, "0 7,13 * * 1-5", config.CronSchedule)
})
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ require (
gopkg.in/yaml.v2 v2.4.0
)

require github.com/reugn/go-quartz v0.13.0

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/reugn/go-quartz v0.13.0 h1:0eMxvj28Qu1npIDdN9Mzg9hwyksGH6XJt4Cz0QB8EUk=
github.com/reugn/go-quartz v0.13.0/go.mod h1:0ghKksELp8MJ4h84T203aTHRF3Kug5BrxEW3ErBvhzY=
github.com/slack-go/slack v0.13.1 h1:6UkM3U1OnbhPsYeb1IMkQ6HSNOSikWluwOncJt4Tz/o=
github.com/slack-go/slack v0.13.1/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
Expand Down
72 changes: 49 additions & 23 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,81 @@
package main

import (
"context"
"fmt"
"io/ioutil"
"log"
"os"
"strings"

"github.com/reugn/go-quartz/job"
"github.com/reugn/go-quartz/quartz"
"github.com/xanzy/go-gitlab"
"gopkg.in/yaml.v2"
)

func main() {
// Load configuration
config, err := loadConfig(&OsEnv{})
if err != nil {
fmt.Printf("%v\n", err)
log.Printf("Error loading configuration: %v", err)
os.Exit(1)
}

if config.CronSchedule == "" {
log.Printf("Running in one-shot mode")
execute(config)
return
}

runScheduler(config)
}

func runScheduler(config *Config) {
log.Printf("Running in cron mode with schedule: %s\n", config.CronSchedule)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

sched := quartz.NewStdScheduler()
sched.Start(ctx)

cronTrigger, err := quartz.NewCronTrigger(config.CronSchedule)
if err != nil {
log.Printf("Error creating cron trigger: %v\n", err)
os.Exit(1)
}

executeJob := job.NewFunctionJob(func(_ context.Context) (int, error) {
execute(config)
return 0, nil
})

err = sched.ScheduleJob(quartz.NewJobDetail(executeJob, quartz.NewJobKey("executeJob")), cronTrigger)
if err != nil {
log.Printf("Error scheduling job: %v\n", err)
os.Exit(1)
}

<-ctx.Done()
}

func execute(config *Config) {
glClient, err := gitlab.NewClient(os.Getenv("GITLAB_TOKEN"),
gitlab.WithBaseURL(config.GitLab.URL))
if err != nil {
fmt.Printf("Error creating GitLab client: %v\n", err)
log.Printf("Error creating GitLab client: %v\n", err)
os.Exit(1)
}

gitlabClient := &gitLabClient{client: glClient}

mrs, err := fetchOpenedMergeRequests(config, gitlabClient)
if err != nil {
fmt.Printf("Error fetching opened merge requests: %v\n", err)
log.Printf("Error fetching opened merge requests: %v\n", err)
os.Exit(1)
}

if len(mrs) == 0 {
fmt.Println("No opened merge requests found.")
log.Println("No opened merge requests found.")
os.Exit(0)
}

Expand All @@ -43,26 +84,11 @@ func main() {
slackClient := &slackClient{webhookURL: os.Getenv("SLACK_WEBHOOK_URL")}
err = sendSlackMessage(slackClient, summary)
if err != nil {
fmt.Printf("Error sending Slack message: %v\n", err)
log.Printf("Error sending Slack message: %v\n", err)
os.Exit(1)
}

fmt.Println("Successfully sent merge request summary to Slack.")
}

func readConfig(file string) (*Config, error) {
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}

var config Config
err = yaml.Unmarshal(data, &config)
if err != nil {
return nil, err
}

return &config, nil
log.Println("Successfully sent merge request summary to Slack.")
}

func formatMergeRequestsSummary(mrs []*MergeRequestWithApprovals) string {
Expand Down

0 comments on commit 8270ffd

Please sign in to comment.