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,
"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
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/server"
"github.com/Theodor-Springmann-Stiftung/lenz-web/views"
@@ -9,11 +10,21 @@ import (
)
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{
Level: compress.LevelBestSpeed,
}))
server.Server.Use(ASSETS_URL, middleware.StaticHandler(&views.StaticFS))
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"
"fmt"
"os"
"sync"
"time"
"github.com/go-git/go-git/v5"
@@ -16,6 +17,9 @@ var NoURLProvidedError = errors.New("Missing URL.")
var NoPathProvidedError = errors.New("Missing path.")
var NoBranchProvidedError = errors.New("Missing branch name.")
var mu sync.Mutex
var repo *git.Repository
type Commit struct {
Path string
URL string
@@ -29,7 +33,11 @@ func IsValidRepository(path, url, branch string) *Commit {
return commit
}
// WARNING: Only OpenOrClone() and Pull() should be used externally.
func OpenOrClone(path, url, branch string) (*Commit, error) {
mu.Lock()
defer mu.Unlock()
commit := IsValidRepository(path, url, branch)
if commit == 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) {
mu.Lock()
defer mu.Unlock()
if url == "" {
return nil, NoURLProvidedError
}
@@ -63,12 +74,19 @@ func Pull(path, url, branch string) (*Commit, error) {
}
br := plumbing.NewBranchReferenceName(branch)
repo, err := git.PlainOpen(path)
if err != nil {
return nil, err
var r *git.Repository
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 {
return nil, err
}
@@ -82,6 +100,8 @@ func Pull(path, url, branch string) (*Commit, error) {
}
defer wt.Clean(&git.CleanOptions{Dir: true})
repo = r
return latestCommit(repo, path, branch, url)
}
@@ -98,14 +118,20 @@ func Read(path, branch, url string) (*Commit, error) {
return nil, NoURLProvidedError
}
repo, err := git.PlainOpen(path)
if err != nil {
return nil, err
var r *git.Repository
if repo == nil {
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)
wt, err := repo.Worktree()
wt, err := r.Worktree()
if err != nil {
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) {
@@ -141,7 +168,7 @@ func Clone(path, url, branch string) (*Commit, error) {
br := plumbing.NewBranchReferenceName(branch)
repo, err := git.PlainClone(path, false, &git.CloneOptions{
r, err := git.PlainClone(path, false, &git.CloneOptions{
URL: url,
Progress: os.Stdout,
})
@@ -150,7 +177,7 @@ func Clone(path, url, branch string) (*Commit, error) {
return nil, err
}
wt, err := repo.Worktree()
wt, err := r.Worktree()
if err != nil {
return nil, err
}
@@ -163,7 +190,8 @@ func Clone(path, url, branch string) (*Commit, error) {
return nil, err
}
return latestCommit(repo, path, branch, url)
repo = r
return latestCommit(r, path, branch, url)
}
func (g Commit) String() string {

View File

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

View File

@@ -9,6 +9,37 @@
{{ range $l := $letters -}}
<div>{{ $l.Letter }}</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 -}}
</div>
{{- end -}}

View File

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