mirror of
https://github.com/thegeeklab/github-releases-notifier.git
synced 2024-11-13 01:30:40 +00:00
Add all Go source files
This commit is contained in:
parent
c3bcf09882
commit
19b8b3382e
80
main.go
Normal file
80
main.go
Normal file
@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alexflint/go-arg"
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/go-kit/kit/log/level"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/shurcooL/githubql"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Config of env and args
|
||||
type Config struct {
|
||||
GithubToken string `arg:"env:GITHUB_TOKEN"`
|
||||
Interval time.Duration `arg:"env:INTERVAL"`
|
||||
LogLevel string `arg:"env:LOG_LEVEL"`
|
||||
Repositories []string `arg:"-r,separate"`
|
||||
SlackHook string `arg:"env:SLACK_HOOK"`
|
||||
}
|
||||
|
||||
// Token returns an oauth2 token or an error.
|
||||
func (c Config) Token() *oauth2.Token {
|
||||
return &oauth2.Token{AccessToken: c.GithubToken}
|
||||
}
|
||||
|
||||
func main() {
|
||||
_ = godotenv.Load()
|
||||
|
||||
c := Config{
|
||||
Interval: time.Hour,
|
||||
LogLevel: "info",
|
||||
}
|
||||
arg.MustParse(&c)
|
||||
|
||||
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
|
||||
logger = log.With(logger,
|
||||
"ts", log.DefaultTimestampUTC,
|
||||
"caller", log.Caller(5),
|
||||
)
|
||||
|
||||
level.SetKey("severity")
|
||||
switch strings.ToLower(c.LogLevel) {
|
||||
case "debug":
|
||||
logger = level.NewFilter(logger, level.AllowDebug())
|
||||
case "warn":
|
||||
logger = level.NewFilter(logger, level.AllowWarn())
|
||||
case "error":
|
||||
logger = level.NewFilter(logger, level.AllowError())
|
||||
default:
|
||||
logger = level.NewFilter(logger, level.AllowInfo())
|
||||
}
|
||||
|
||||
tokenSource := oauth2.StaticTokenSource(c.Token())
|
||||
client := oauth2.NewClient(context.Background(), tokenSource)
|
||||
checker := &Checker{
|
||||
logger: logger,
|
||||
client: githubql.NewClient(client),
|
||||
}
|
||||
|
||||
releases := make(chan Repository)
|
||||
go checker.Run(c.Interval, c.Repositories, releases)
|
||||
|
||||
slack := SlackSender{Hook: c.SlackHook}
|
||||
|
||||
level.Info(logger).Log("msg", "waiting for new releases")
|
||||
for repository := range releases {
|
||||
if err := slack.Send(repository); err != nil {
|
||||
level.Warn(logger).Log(
|
||||
"msg", "failed to send release to messenger",
|
||||
"err", err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
15
release.go
Normal file
15
release.go
Normal file
@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Release of a repository tagged via GitHub.
|
||||
type Release struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
URL url.URL
|
||||
PublishedAt time.Time
|
||||
}
|
135
releasechecker.go
Normal file
135
releasechecker.go
Normal file
@ -0,0 +1,135 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/go-kit/kit/log/level"
|
||||
"github.com/shurcooL/githubql"
|
||||
)
|
||||
|
||||
// Checker has a githubql client to run queries and also knows about
|
||||
// the current repositories releases to compare against.
|
||||
type Checker struct {
|
||||
logger log.Logger
|
||||
client *githubql.Client
|
||||
releases map[string]Repository
|
||||
}
|
||||
|
||||
// Run the queries and comparisons for the given repositories in a given interval.
|
||||
func (c *Checker) Run(interval time.Duration, repositories []string, releases chan<- Repository) {
|
||||
if c.releases == nil {
|
||||
c.releases = make(map[string]Repository)
|
||||
}
|
||||
|
||||
for {
|
||||
for _, repoName := range repositories {
|
||||
s := strings.Split(repoName, "/")
|
||||
owner, name := s[0], s[1]
|
||||
|
||||
nextRepo, err := c.query(owner, name)
|
||||
if err != nil {
|
||||
level.Warn(c.logger).Log(
|
||||
"msg", "failed to query the repository's releases",
|
||||
"owner", owner,
|
||||
"name", name,
|
||||
"err", err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// For debugging uncomment this next line
|
||||
//releases <- nextRepo
|
||||
|
||||
currRepo, ok := c.releases[repoName]
|
||||
|
||||
// We've queried the repository for the first time.
|
||||
// Saving the current state to compare with the next iteration.
|
||||
if !ok {
|
||||
c.releases[repoName] = nextRepo
|
||||
continue
|
||||
}
|
||||
|
||||
if nextRepo.Release.PublishedAt.After(currRepo.Release.PublishedAt) {
|
||||
releases <- nextRepo
|
||||
c.releases[repoName] = nextRepo
|
||||
} else {
|
||||
level.Debug(c.logger).Log(
|
||||
"msg", "no new release for repository",
|
||||
"owner", owner,
|
||||
"name", name,
|
||||
)
|
||||
}
|
||||
}
|
||||
time.Sleep(interval)
|
||||
}
|
||||
}
|
||||
|
||||
// This should be improved in the future to make batch requests for all watched repositories at once
|
||||
// TODO: https://github.com/shurcooL/githubql/issues/17
|
||||
|
||||
func (c *Checker) query(owner, name string) (Repository, error) {
|
||||
var query struct {
|
||||
Repository struct {
|
||||
ID githubql.ID
|
||||
Name githubql.String
|
||||
Description githubql.String
|
||||
URL githubql.URI
|
||||
|
||||
Releases struct {
|
||||
Edges []struct {
|
||||
Node struct {
|
||||
ID githubql.ID
|
||||
Name githubql.String
|
||||
Description githubql.String
|
||||
URL githubql.URI
|
||||
PublishedAt githubql.DateTime
|
||||
}
|
||||
}
|
||||
} `graphql:"releases(last: 1)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": githubql.String(owner),
|
||||
"name": githubql.String(name),
|
||||
}
|
||||
|
||||
if err := c.client.Query(context.Background(), &query, variables); err != nil {
|
||||
return Repository{}, err
|
||||
}
|
||||
|
||||
repositoryID, ok := query.Repository.ID.(string)
|
||||
if !ok {
|
||||
return Repository{}, fmt.Errorf("can't convert repository id to string: %v", query.Repository.ID)
|
||||
}
|
||||
|
||||
if len(query.Repository.Releases.Edges) == 0 {
|
||||
return Repository{}, fmt.Errorf("can't find any releases for %s/%s", owner, name)
|
||||
}
|
||||
latestRelease := query.Repository.Releases.Edges[0].Node
|
||||
|
||||
releaseID, ok := latestRelease.ID.(string)
|
||||
if !ok {
|
||||
return Repository{}, fmt.Errorf("can't convert release id to string: %v", query.Repository.ID)
|
||||
}
|
||||
|
||||
return Repository{
|
||||
ID: repositoryID,
|
||||
Name: string(query.Repository.Name),
|
||||
Owner: owner,
|
||||
Description: string(query.Repository.Description),
|
||||
URL: *query.Repository.URL.URL,
|
||||
|
||||
Release: Release{
|
||||
ID: releaseID,
|
||||
Name: string(latestRelease.Name),
|
||||
Description: string(latestRelease.Description),
|
||||
URL: *latestRelease.URL.URL,
|
||||
PublishedAt: latestRelease.PublishedAt.Time,
|
||||
},
|
||||
}, nil
|
||||
}
|
13
repository.go
Normal file
13
repository.go
Normal file
@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import "net/url"
|
||||
|
||||
// Repository on GitHub.
|
||||
type Repository struct {
|
||||
ID string
|
||||
Name string
|
||||
Owner string
|
||||
Description string
|
||||
URL url.URL
|
||||
Release Release
|
||||
}
|
62
slack.go
Normal file
62
slack.go
Normal file
@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SlackSender struct {
|
||||
Hook string
|
||||
}
|
||||
|
||||
type slackPayload struct {
|
||||
Username string `json:"username"`
|
||||
IconEmoji string `json:"icon_emoji"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func (s *SlackSender) Send(repository Repository) error {
|
||||
payload := slackPayload{
|
||||
Username: "GitHub Releases",
|
||||
IconEmoji: ":github:",
|
||||
Text: fmt.Sprintf(
|
||||
"<%s|%s/%s>: <%s|%s> released",
|
||||
repository.URL.String(),
|
||||
repository.Owner,
|
||||
repository.Name,
|
||||
repository.Release.URL.String(),
|
||||
repository.Release.Name,
|
||||
),
|
||||
}
|
||||
|
||||
payloadData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, s.Hook, bytes.NewReader(payloadData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond)
|
||||
defer cancel()
|
||||
req.WithContext(ctx)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
return fmt.Errorf("request didn't respond with 200 OK: %s, %s", resp.Status, body)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user