12 Commits

17 changed files with 159 additions and 53 deletions

24
.drone.yml Normal file
View File

@@ -0,0 +1,24 @@
---
kind: pipeline
type: docker
name: build
steps:
- name: lint
image: code.pztrn.name/containers/mirror/golangci/golangci-lint:v1.46.2
environment:
CGO_ENABLED: 0
commands:
- golangci-lint run
- name: docker
image: code.pztrn.name/containers/mirror/plugins/docker:20.13.0
when:
branch: ["master"]
settings:
registry: code.pztrn.name
username: drone
password:
from_secret: drone_secret
repo: code.pztrn.name/apps/periodicator
auto_tag: true

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
*DS_Store*
.idea
.vscode
config.yaml
release

View File

@@ -38,7 +38,7 @@ build_master_image:
- apk add --no-cache git bash
- scripts/get_version.sh generate
- docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
- docker build --pull -t ${REGISTRY_IMAGE_LATEST} --build-arg CI_COMMIT_TAG=${CI_COMMIT_TAG} --build-arg CI_COMMIT_REF_NAME=${CI_COMMIT_REF_NAME} --build-arg CI_COMMIT_SHA=${CI_COMMIT_SHA} --build-arg CI_PROJECT_NAME=${CI_PROJECT_NAME} .
- docker build --pull -t ${REGISTRY_IMAGE_LATEST} .
- docker push ${REGISTRY_IMAGE_LATEST}
build_tag_image:
@@ -52,5 +52,7 @@ build_tag_image:
- apk add --no-cache git bash
- scripts/get_version.sh generate
- docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
- docker build -t ${REGISTRY_IMAGE_TAGGED} --build-arg CI_COMMIT_TAG=${CI_COMMIT_TAG} --build-arg CI_COMMIT_REF_NAME=${CI_COMMIT_REF_NAME} --build-arg CI_COMMIT_SHA=${CI_COMMIT_SHA} --build-arg CI_PROJECT_NAME=${CI_PROJECT_NAME} .
- docker build -t ${REGISTRY_IMAGE_TAGGED} .
- docker build -t ${REGISTRY_IMAGE_LATEST} .
- docker push ${REGISTRY_IMAGE_TAGGED}
- docker push ${REGISTRY_IMAGE_LATEST}

View File

@@ -16,6 +16,8 @@ linters:
- testpackage
# Crashes a lot.
- gci
# Deprecated.
- exhaustivestruct
linters-settings:
lll:
line-length: 128

View File

@@ -6,6 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.2.0]
### Added
- License information. We're on MIT.
- Ability to show next issue/task creation timestamp using ``-show-next-creation-ts`` flag.
### Fixed
- "latest" Docker tag is now always latest, even when building images for tags.
- Update github.com/robfig/cron to v3.
## [0.1.0] - 2021-09-29
- Initial release with ability to create issues on Gitlab (both hosted and self-hosted).
[Unreleased]: https://gitlab.pztrn.name/pztrn/periodicator/-/compare/v0.1.0...master
[0.2.0]: https://gitlab.pztrn.name/pztrn/periodicator/-/compare/v0.1.0...v0.2.0
[0.1.0]: https://gitlab.pztrn.name/pztrn/periodicator/-/releases/v0.1.0

View File

@@ -1,4 +1,4 @@
FROM golang:1.17.1-alpine AS build
FROM code.pztrn.name/containers/mirror/golang:1.18.3-alpine AS build
WORKDIR /go/src/go.dev.pztrn.name/periodicator
COPY . .
@@ -6,7 +6,7 @@ COPY . .
ENV CGO_ENABLED=0
RUN apk add make && make build
FROM alpine:latest
FROM code.pztrn.name/containers/mirror/alpine:3.16.0
LABEL maintainer="Stanislav N. <pztrn@pztrn.name>"
COPY --from=build /go/src/go.dev.pztrn.name/periodicator/periodicator /usr/local/bin/periodicator

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2021, Stanislav N. aka pztrn and Periodicator developers.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -12,6 +12,11 @@ run:
GPT_CONFIG=$(CONFIG) ./periodicator
rm periodicator
run-show-next-creation-ts:
go build -ldflags $(LINKER_FLAGS) -o periodicator .
GPT_CONFIG=$(CONFIG) ./periodicator -show-next-creation-ts
rm periodicator
run-version:
go build -ldflags $(LINKER_FLAGS) -o periodicator .
GPT_CONFIG=$(CONFIG) ./periodicator -version

View File

@@ -17,7 +17,7 @@ Head over releases page, grab your binary and configure your system to start bin
Compose a configuration file (read below) and add this to your cron:
```shell
docker run --rm -v ./config.yaml:/periodicator.yaml registry.gitlab.pztrn.name/pztrn/periodicator:latest
docker run --rm -v ./config.yaml:/periodicator.yaml pztrn/periodicator:latest
```
## Configuring

2
go.mod
View File

@@ -3,7 +3,7 @@ module go.dev.pztrn.name/periodicator
go 1.17
require (
github.com/robfig/cron v1.2.0
github.com/robfig/cron/v3 v3.0.1
github.com/xanzy/go-gitlab v0.51.1
gopkg.in/yaml.v2 v2.2.2
)

4
go.sum
View File

@@ -125,8 +125,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=

View File

@@ -45,12 +45,12 @@ func Parse() *Config {
panic("Failed to read configuration file data: " + readErr.Error())
}
// nolint:exhaustivestruct
c := &Config{}
// nolint:exhaustruct
cfg := &Config{}
if err := yaml.Unmarshal(data, c); err != nil {
if err := yaml.Unmarshal(data, cfg); err != nil {
panic("Failed to unmarshal YAML data: " + err.Error())
}
return c
return cfg
}

View File

@@ -16,7 +16,7 @@ type Client struct {
// NewGitlabClient creates new Gitlab's client controlling structure.
func NewGitlabClient(cfg *Config) *Client {
// nolint:exhaustivestruct
// nolint:exhaustruct
c := &Client{
config: cfg,
}
@@ -43,7 +43,7 @@ func (c *Client) GetClient() *gitlab.Client {
// GetIssuesByTitle returns list of issues that matches passed title in specific
// project.
func (c *Client) GetIssuesByTitle(projectID int, title string) ([]*gitlab.Issue, error) {
// nolint:exhaustivestruct
// nolint:exhaustruct
issues, resp, err := c.client.Issues.ListProjectIssues(projectID, &gitlab.ListProjectIssuesOptions{
ListOptions: gitlab.ListOptions{
PerPage: 1000,

View File

@@ -4,22 +4,20 @@ import (
"log"
"time"
"github.com/robfig/cron"
"github.com/robfig/cron/v3"
g "github.com/xanzy/go-gitlab"
"go.dev.pztrn.name/periodicator/internal/gitlab"
)
// BaseTask is a base task structure.
type BaseTask struct {
client *gitlab.Client
projectID int
title string
body string
tags []string
executionStartTimestamp time.Time
client *gitlab.Client
title string
body string
cron string
tags []string
projectID int
dueIn time.Duration
}
@@ -39,6 +37,37 @@ func (b *BaseTask) checkIfOpenedTaskExists(issues []*g.Issue) bool {
return foundAndNotClosed
}
func (b *BaseTask) getIssues() ([]*g.Issue, error) {
// nolint:wrapcheck
return b.client.GetIssuesByTitle(b.projectID, b.title)
}
func (b *BaseTask) getLastCreationTimestamp(issues []*g.Issue) time.Time {
lastTaskCreationTS := b.executionStartTimestamp
for _, issue := range issues {
if issue.ClosedAt != nil && issue.CreatedAt.After(lastTaskCreationTS) {
lastTaskCreationTS = *issue.CreatedAt
}
}
return lastTaskCreationTS
}
func (b *BaseTask) getNextCreationTimestamp(lastTaskCreationTS time.Time) time.Time {
// Set up cron job parser.
cp := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
schedule, err := cp.Parse(b.cron)
if err != nil {
b.log("Failed to parse cron string: " + err.Error())
return lastTaskCreationTS
}
return schedule.Next(lastTaskCreationTS)
}
func (b *BaseTask) log(message string) {
log.Println("Task '" + b.title + "': " + message)
}
@@ -46,7 +75,7 @@ func (b *BaseTask) log(message string) {
// Run executes task procedure.
func (b *BaseTask) Run() {
// Get similar tasks.
issues, err := b.client.GetIssuesByTitle(b.projectID, b.title)
issues, err := b.getIssues()
if err != nil {
b.log("Error while getting issues from Gitlab: " + err.Error())
@@ -65,29 +94,16 @@ func (b *BaseTask) Run() {
b.log("No still opened tasks found, checking if we should create new one...")
// Get latest task creation timestamp from Gitlab.
lastTaskCreationTS := b.executionStartTimestamp
for _, issue := range issues {
if issue.ClosedAt != nil && issue.CreatedAt.After(lastTaskCreationTS) {
lastTaskCreationTS = *issue.CreatedAt
}
}
lastTaskCreationTS := b.getLastCreationTimestamp(issues)
b.log("Last task creation timestamp: " + lastTaskCreationTS.String())
// Set up cron job parser.
cp := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
schedule, err := cp.Parse(b.cron)
if err != nil {
b.log("Failed to parse cron string: " + err.Error())
// Figure out next task creation and deadline timestamps.
nextCreationTS := b.getNextCreationTimestamp(lastTaskCreationTS)
if nextCreationTS.Equal(lastTaskCreationTS) {
return
}
// Figure out next task creation and deadline timestamps.
nextCreationTS := schedule.Next(lastTaskCreationTS)
// Check if task should be created and create if so.
if nextCreationTS.Before(time.Now()) {
// Deadlines should be calculated until first one will appear AFTER today.
@@ -107,7 +123,7 @@ func (b *BaseTask) Run() {
b.log("Found no opened tasks and task should be created, doing so. Task deadline: " + nextDeadlineTS.String())
// nolint:exhaustivestruct
// nolint:exhaustruct
err := b.client.CreateIssue(b.projectID, &g.CreateIssueOptions{
Title: &b.title,
Description: &b.body,

View File

@@ -5,12 +5,12 @@ import "time"
// Config is a task's configuration as should be defined in configuration file.
// nolint:tagliatelle
type Config struct {
ProjectID int `yaml:"project_id"`
ExecutionStart TaskStartTime `yaml:"execution_start"`
Title string `yaml:"title"`
Body string `yaml:"body"`
Tags []string `yaml:"tags"`
ExecutionStart TaskStartTime `yaml:"execution_start"`
Cron string `yaml:"cron"`
Tags []string `yaml:"tags"`
ProjectID int `yaml:"project_id"`
DueIn time.Duration `yaml:"due_in"`
}
@@ -30,14 +30,14 @@ func (tts *TaskStartTime) UnmarshalYAML(unmarshal func(interface{}) error) error
return err
}
t, err := time.Parse("2006-01-02 15:04:05", timeData)
timeField, err := time.Parse("2006-01-02 15:04:05", timeData)
if err != nil {
// ToDo: fix it!
// nolint:wrapcheck
return err
}
tts.ts = t
tts.ts = timeField
return nil
}

View File

@@ -4,10 +4,10 @@ import (
"go.dev.pztrn.name/periodicator/internal/gitlab"
)
// Process processes passed tasks.
func Process(client *gitlab.Client, tasks []Config) {
// PrintCreationTSes prints tasks creation timestamps.
func PrintCreationTSes(client *gitlab.Client, tasks []Config) {
for _, task := range tasks {
t := &BaseTask{
taskData := &BaseTask{
client: client,
projectID: task.ProjectID,
title: task.Title,
@@ -18,6 +18,31 @@ func Process(client *gitlab.Client, tasks []Config) {
dueIn: task.DueIn,
}
t.Run()
// Get similar tasks.
// ToDo: refactor?
issues, err := taskData.getIssues()
if err != nil {
panic("Error while getting issues from Gitlab: " + err.Error())
}
taskData.log(taskData.getNextCreationTimestamp(taskData.getLastCreationTimestamp(issues)).String())
}
}
// Process processes passed tasks.
func Process(client *gitlab.Client, tasks []Config) {
for _, task := range tasks {
taskData := &BaseTask{
client: client,
projectID: task.ProjectID,
title: task.Title,
body: task.Body,
tags: task.Tags,
executionStartTimestamp: task.ExecutionStart.GetTimestamp(),
cron: task.Cron,
dueIn: task.DueIn,
}
taskData.Run()
}
}

14
main.go
View File

@@ -11,7 +11,10 @@ import (
"go.dev.pztrn.name/periodicator/internal/tasks"
)
var showVersion = flag.Bool("version", false, "Show version information and exit")
var (
showNextCreationTS = flag.Bool("show-next-creation-ts", false, "Show tasks next creation timestamps")
showVersion = flag.Bool("version", false, "Show version information and exit")
)
func main() {
flag.Parse()
@@ -26,7 +29,12 @@ func main() {
cfg := config.Parse()
c := gitlab.NewGitlabClient(&cfg.Gitlab)
gitlabClient := gitlab.NewGitlabClient(&cfg.Gitlab)
tasks.Process(c, cfg.Tasks)
if *showNextCreationTS {
tasks.PrintCreationTSes(gitlabClient, cfg.Tasks)
os.Exit(0)
}
tasks.Process(gitlabClient, cfg.Tasks)
}