GitHub Webhooks integration

This commit is contained in:
Simon Martens
2025-03-26 18:53:27 +01:00
parent 052f21e87a
commit a224d31c47
8 changed files with 174 additions and 21 deletions

View File

@@ -1,5 +1,6 @@
{ {
"debug": true, "debug": true,
"watch": true, "watch": true,
"git_url": "git@github.com:Theodor-Springmann-Stiftung/lenz-briefe.git" "git_url": "git@github.com:Theodor-Springmann-Stiftung/lenz-briefe.git",
"webhook_secret": "test_secret"
} }

View File

@@ -1,6 +1,7 @@
package controllers package controllers
import ( import (
"github.com/Theodor-Springmann-Stiftung/lenz-web/config"
"github.com/Theodor-Springmann-Stiftung/lenz-web/helpers/middleware" "github.com/Theodor-Springmann-Stiftung/lenz-web/helpers/middleware"
"github.com/Theodor-Springmann-Stiftung/lenz-web/server" "github.com/Theodor-Springmann-Stiftung/lenz-web/server"
"github.com/Theodor-Springmann-Stiftung/lenz-web/views" "github.com/Theodor-Springmann-Stiftung/lenz-web/views"
@@ -9,11 +10,21 @@ import (
) )
const ASSETS_URL = "/assets" const ASSETS_URL = "/assets"
const WBHOOK_URL = "/webhook"
func Register(server server.Server) { func Register(server server.Server, cfg config.Config) {
server.Server.Use(ASSETS_URL, compress.New(compress.Config{ server.Server.Use(ASSETS_URL, compress.New(compress.Config{
Level: compress.LevelBestSpeed, Level: compress.LevelBestSpeed,
})) }))
server.Server.Use(ASSETS_URL, middleware.StaticHandler(&views.StaticFS)) server.Server.Use(ASSETS_URL, middleware.StaticHandler(&views.StaticFS))
server.Server.Get("/", GetIndex) server.Server.Get("/", GetIndex)
if cfg.WebHookSecret != "" {
whurl := WBHOOK_URL
if cfg.WebHookEndpoint != "" {
whurl = cfg.WebHookEndpoint
}
server.Server.Post(whurl, PostWebhook(cfg))
}
} }

68
controllers/webhook.go Normal file
View File

@@ -0,0 +1,68 @@
package controllers
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"path/filepath"
"strings"
"github.com/Theodor-Springmann-Stiftung/lenz-web/config"
gitprovider "github.com/Theodor-Springmann-Stiftung/lenz-web/git"
"github.com/Theodor-Springmann-Stiftung/lenz-web/xmlmodels"
"github.com/gofiber/fiber/v2"
)
const SIGNATURE_PREFIX = "sha256="
func PostWebhook(cfg config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
body := c.Body()
if !verifySignature256([]byte(cfg.WebHookSecret), body, c.Get("X-Hub-Signature-256")) {
return c.SendStatus(fiber.StatusUnauthorized)
}
if c.Get("X-GitHub-Event") == "" {
return c.SendStatus(fiber.StatusBadRequest)
}
dir := filepath.Join(cfg.BaseDIR, cfg.GITPath)
commit, err := gitprovider.Pull(dir, cfg.GitURL, cfg.GitBranch)
if err != nil {
panic(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
_, err = xmlmodels.Parse(dir, commit.Hash)
if err != nil {
panic(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.SendStatus(fiber.StatusOK)
}
}
func sign256(secret, body []byte) []byte {
computed := hmac.New(sha256.New, secret)
computed.Write(body)
return []byte(computed.Sum(nil))
}
func verifySignature256(secret, payload []byte, header string) bool {
if !strings.HasPrefix(header, SIGNATURE_PREFIX) {
return false
}
sig, err := hex.DecodeString(header[len(SIGNATURE_PREFIX):])
if err != nil {
return false
}
mac := hmac.New(sha256.New, secret)
mac.Write(payload)
expected := mac.Sum(nil)
return hmac.Equal(expected, sig)
}

View File

@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"sync"
"time" "time"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
@@ -16,6 +17,9 @@ var NoURLProvidedError = errors.New("Missing URL.")
var NoPathProvidedError = errors.New("Missing path.") var NoPathProvidedError = errors.New("Missing path.")
var NoBranchProvidedError = errors.New("Missing branch name.") var NoBranchProvidedError = errors.New("Missing branch name.")
var mu sync.Mutex
var repo *git.Repository
type Commit struct { type Commit struct {
Path string Path string
URL string URL string
@@ -29,7 +33,11 @@ func IsValidRepository(path, url, branch string) *Commit {
return commit return commit
} }
// WARNING: Only OpenOrClone() and Pull() should be used externally.
func OpenOrClone(path, url, branch string) (*Commit, error) { func OpenOrClone(path, url, branch string) (*Commit, error) {
mu.Lock()
defer mu.Unlock()
commit := IsValidRepository(path, url, branch) commit := IsValidRepository(path, url, branch)
if commit == nil { if commit == nil {
if _, err := os.Stat(path); err == nil { if _, err := os.Stat(path); err == nil {
@@ -50,6 +58,9 @@ func OpenOrClone(path, url, branch string) (*Commit, error) {
} }
func Pull(path, url, branch string) (*Commit, error) { func Pull(path, url, branch string) (*Commit, error) {
mu.Lock()
defer mu.Unlock()
if url == "" { if url == "" {
return nil, NoURLProvidedError return nil, NoURLProvidedError
} }
@@ -63,12 +74,19 @@ func Pull(path, url, branch string) (*Commit, error) {
} }
br := plumbing.NewBranchReferenceName(branch) br := plumbing.NewBranchReferenceName(branch)
repo, err := git.PlainOpen(path)
if err != nil { var r *git.Repository
return nil, err if repo == nil {
rep, err := git.PlainOpen(path)
if err != nil {
return nil, err
}
r = rep
} else {
r = repo
} }
wt, err := repo.Worktree() wt, err := r.Worktree()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -82,6 +100,8 @@ func Pull(path, url, branch string) (*Commit, error) {
} }
defer wt.Clean(&git.CleanOptions{Dir: true}) defer wt.Clean(&git.CleanOptions{Dir: true})
repo = r
return latestCommit(repo, path, branch, url) return latestCommit(repo, path, branch, url)
} }
@@ -98,14 +118,20 @@ func Read(path, branch, url string) (*Commit, error) {
return nil, NoURLProvidedError return nil, NoURLProvidedError
} }
repo, err := git.PlainOpen(path) var r *git.Repository
if err != nil { if repo == nil {
return nil, err rep, err := git.PlainOpen(path)
if err != nil {
return nil, err
}
r = rep
} else {
r = repo
} }
if err := ValidateBranch(repo, branch); err != nil { if err := ValidateBranch(r, branch); err != nil {
br := plumbing.NewBranchReferenceName(branch) br := plumbing.NewBranchReferenceName(branch)
wt, err := repo.Worktree() wt, err := r.Worktree()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -123,7 +149,8 @@ func Read(path, branch, url string) (*Commit, error) {
} }
} }
return latestCommit(repo, path, branch, url) repo = r
return latestCommit(r, path, branch, url)
} }
func Clone(path, url, branch string) (*Commit, error) { func Clone(path, url, branch string) (*Commit, error) {
@@ -141,7 +168,7 @@ func Clone(path, url, branch string) (*Commit, error) {
br := plumbing.NewBranchReferenceName(branch) br := plumbing.NewBranchReferenceName(branch)
repo, err := git.PlainClone(path, false, &git.CloneOptions{ r, err := git.PlainClone(path, false, &git.CloneOptions{
URL: url, URL: url,
Progress: os.Stdout, Progress: os.Stdout,
}) })
@@ -150,7 +177,7 @@ func Clone(path, url, branch string) (*Commit, error) {
return nil, err return nil, err
} }
wt, err := repo.Worktree() wt, err := r.Worktree()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -163,7 +190,8 @@ func Clone(path, url, branch string) (*Commit, error) {
return nil, err return nil, err
} }
return latestCommit(repo, path, branch, url) repo = r
return latestCommit(r, path, branch, url)
} }
func (g Commit) String() string { func (g Commit) String() string {

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"fmt"
"log/slog" "log/slog"
"path/filepath" "path/filepath"
"time" "time"
@@ -37,6 +38,7 @@ func main() {
dir := filepath.Join(cfg.BaseDIR, cfg.GITPath) dir := filepath.Join(cfg.BaseDIR, cfg.GITPath)
fmt.Printf("Starting Lenz with config: %v", cfg)
commit, err := gitprovider.OpenOrClone(dir, cfg.GitURL, cfg.GitBranch) commit, err := gitprovider.OpenOrClone(dir, cfg.GitURL, cfg.GitBranch)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -60,7 +62,7 @@ func main() {
} }
server := server.New(engine, storage, cfg.Debug) server := server.New(engine, storage, cfg.Debug)
controllers.Register(server) controllers.Register(server, cfg)
server.Start(cfg.Address + ":" + cfg.Port) server.Start(cfg.Address + ":" + cfg.Port)
} }

View File

@@ -49,7 +49,9 @@ func New(engine *templating.Engine, storage fiber.Storage, debug bool) Server {
server.Use(logger.New()) server.Use(logger.New())
} }
server.Use(recover.New()) if !debug {
server.Use(recover.New())
}
if debug { if debug {
server.Use(cache.New(cache.Config{ server.Use(cache.New(cache.Config{

View File

@@ -9,6 +9,37 @@
{{ range $l := $letters -}} {{ range $l := $letters -}}
<div>{{ $l.Letter }}</div> <div>{{ $l.Letter }}</div>
<div>{{ $l.Earliest.Text -}}</div> <div>{{ $l.Earliest.Text -}}</div>
{{- range $sr := $l.SendReceivedPairs -}}
<div>
<div>
{{- range $i, $p := $sr.Sent.Persons -}}
<div>
{{- if $i -}}
,
{{ end -}}
{{- $person := Person $p.Reference -}}
{{- $person.Name -}}
</div>
{{- end -}}
</div>
<div>an</div>
{{- if $sr.Received -}}
<div>
{{- range $i, $p := $sr.Received.Persons -}}
<div>
{{- if $i -}}
,
{{ end -}}
{{- $person := Person $p.Reference -}}
{{- $person.Name -}}
</div>
{{- end -}}
</div>
{{- else -}}
<div>Unbekannt</div>
{{- end -}}
</div>
{{- end -}}
{{- end -}} {{- end -}}
</div> </div>
{{- end -}} {{- end -}}

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"iter" "iter"
"slices"
xmlparsing "github.com/Theodor-Springmann-Stiftung/lenz-web/xml" xmlparsing "github.com/Theodor-Springmann-Stiftung/lenz-web/xml"
) )
@@ -15,7 +16,7 @@ type Meta struct {
IsProofread xmlparsing.OptionalBool `xml:"isProofread"` IsProofread xmlparsing.OptionalBool `xml:"isProofread"`
IsDraft xmlparsing.OptionalBool `xml:"isDraft"` IsDraft xmlparsing.OptionalBool `xml:"isDraft"`
Sent []Action `xml:"sent"` Sent []Action `xml:"sent"`
Recieved []Action `xml:"recieved"` Recieved []Action `xml:"received"`
} }
func (m *Meta) Earliest() *Date { func (m *Meta) Earliest() *Date {
@@ -56,14 +57,23 @@ func (m Meta) String() string {
return string(json) return string(json)
} }
func (m Meta) SendRecieved() iter.Seq2[*Action, *Action] { type SendRecievedPair struct {
return func(yield func(*Action, *Action) bool) { Sent *Action
Received *Action
}
func (m Meta) SendReceivedPairs() []SendRecievedPair {
return slices.Collect(m.SendRecieved())
}
func (m Meta) SendRecieved() iter.Seq[SendRecievedPair] {
return func(yield func(SendRecievedPair) bool) {
for i, sent := range m.Sent { for i, sent := range m.Sent {
var rec *Action var rec *Action
if i < len(m.Recieved) { if i < len(m.Recieved) {
rec = &m.Recieved[i] rec = &m.Recieved[i]
} }
if !yield(&sent, rec) { if !yield(SendRecievedPair{Sent: &sent, Received: rec}) {
return return
} }
} }