Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
7fa3553da6
|
|||
14581dbe83
|
|||
3053c62fa8
|
|||
880ad68663
|
|||
91f4676118
|
|||
e315c37bb6
|
|||
c4ff0acc8a
|
|||
cedbc80a6e
|
|||
c8411ff6e8
|
|||
7bc47cf428
|
|||
5c4a169040
|
|||
0018c79b89
|
24
.drone.yml
Normal file
24
.drone.yml
Normal 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
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
*DS_Store*
|
||||
.idea
|
||||
.vscode
|
||||
config.yaml
|
||||
release
|
||||
|
@@ -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}
|
||||
|
@@ -16,6 +16,8 @@ linters:
|
||||
- testpackage
|
||||
# Crashes a lot.
|
||||
- gci
|
||||
# Deprecated.
|
||||
- exhaustivestruct
|
||||
linters-settings:
|
||||
lll:
|
||||
line-length: 128
|
||||
|
16
CHANGELOG.md
16
CHANGELOG.md
@@ -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
|
||||
|
@@ -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
7
LICENSE
Normal 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.
|
5
Makefile
5
Makefile
@@ -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
|
||||
|
@@ -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
2
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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 {
|
||||
executionStartTimestamp time.Time
|
||||
client *gitlab.Client
|
||||
|
||||
projectID int
|
||||
title string
|
||||
body string
|
||||
tags []string
|
||||
|
||||
executionStartTimestamp time.Time
|
||||
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,
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
14
main.go
@@ -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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user