add webhook mode

This commit is contained in:
Ronmi Ren 2025-01-12 21:01:37 +08:00
commit a82b00b191
3 changed files with 290 additions and 1 deletions

100
cmd/listen.go Normal file
View file

@ -0,0 +1,100 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
package cmd
import (
"context"
"fmt"
"net/url"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"git.ronmi.tw/ronmi/forgejo-pages/lib"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// listenCmd represents the listen command
var listenCmd = &cobra.Command{
Use: "listen",
Short: "Start webhook listener",
Run: func(cmd *cobra.Command, args []string) {
viper.BindPFlags(cmd.Flags())
// check flags
bind := viper.GetString("bind")
server := viper.GetString("server")
user := viper.GetString("user")
token := viper.GetString("token")
dir := viper.GetString("dir")
branch := viper.GetString("branch")
if bind == "" || server == "" || user == "" || token == "" || branch == "" || dir == "" {
fmt.Println("bind, server, user, token, branch and dir are required")
fmt.Println("dumping flags:")
fmt.Println(" bind: ", bind)
fmt.Println(" server: ", server)
fmt.Println(" user: ", user)
fmt.Println(" token: ", token)
fmt.Println(" branch: ", branch)
fmt.Println(" dir: ", dir)
return
}
serverUrl, err := url.Parse(server)
if err != nil {
fmt.Println("invalid server url: ", err)
return
}
cfg := &lib.WebhookCFG{
Forgejo: lib.Forgejo{
Server: *serverUrl,
Token: token,
Branch: branch,
},
GitDir: filepath.Join(dir, "git"),
PageDir: filepath.Join(dir, "pages"),
GitUser: user,
GitPass: token,
}
s, err := lib.UseWebhook(bind, cfg)
if err != nil {
fmt.Println("cannot create server: ", err)
return
}
ctx, stop := signal.NotifyContext(
context.TODO(),
os.Interrupt,
syscall.SIGTERM,
os.Kill,
)
defer stop()
fmt.Println("starting server")
go func() {
<-ctx.Done()
stop()
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
s.Shutdown(ctx)
}()
s.ListenAndServe()
},
}
func init() {
rootCmd.AddCommand(listenCmd)
f := listenCmd.Flags()
f.StringP("bind", "a", ":8080", "bind address")
f.StringP("server", "s", "", "Forgejo server address")
f.StringP("user", "u", "", "Forgejo user")
f.StringP("token", "k", "", "Forgejo api token or password")
f.StringP("branch", "b", "static-pages", "branch to use")
f.StringP("dir", "d", "", "directory to store data, must be writable")
}

View file

@ -23,6 +23,7 @@ var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the static page server.",
Run: func(cmd *cobra.Command, args []string) {
viper.BindPFlags(cmd.Flags())
// check flags
bind := viper.GetString("bind")
server := viper.GetString("server")
@ -83,5 +84,4 @@ func init() {
f.StringP("token", "k", "", "Forgejo api token")
f.StringP("branch", "b", "static-pages", "branch to use")
f.StringP("well-known", "w", "/.well-known", "well-known path, used by LetsEncrypt")
viper.BindPFlags(f)
}

189
lib/hook.go Normal file
View file

@ -0,0 +1,189 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
package lib
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
)
type WebhookCFG struct {
Forgejo
PageDir string
GitDir string
GitUser string
GitPass string
}
func (c *WebhookCFG) pageDir(user, repo string) string {
return filepath.Join(c.PageDir, user, repo)
}
func (c *WebhookCFG) gitDir(user, repo string) string {
return filepath.Join(c.GitDir, user, repo)
}
func (c *WebhookCFG) checkout(user, repo string) (err error) {
pageDir := c.pageDir(user, repo)
gitDir := c.gitDir(user, repo)
git := exec.Command(
"git",
"--git-dir", gitDir,
"--work-tree", pageDir,
"checkout", "origin/"+c.Branch,
)
output, err := git.CombinedOutput()
if err != nil {
fmt.Println("git checkout failed: ", err)
fmt.Println("=========== Dump output: ============")
fmt.Println(string(output))
fmt.Println("=====================================")
}
return
}
func (c *WebhookCFG) fetch(user, repo string) (err error) {
gitDir := c.gitDir(user, repo)
git := exec.Command(
"git",
"--git-dir", gitDir,
"fetch", "origin", c.Branch,
)
output, err := git.CombinedOutput()
if err != nil {
fmt.Println("git fetch failed: ", err)
fmt.Println("=========== Dump output: ============")
fmt.Println(string(output))
fmt.Println("=====================================")
}
return
}
func (c *WebhookCFG) clone(user, repo string) (err error) {
pageDir := c.pageDir(user, repo)
err = os.MkdirAll(filepath.Dir(pageDir), 0755)
if err != nil {
return
}
gitDir := c.gitDir(user, repo)
err = os.MkdirAll(filepath.Dir(gitDir), 0700)
if err != nil {
return
}
uri := c.Server
uri.Path = "/" + path.Join(user, repo) + ".git"
uri.User = url.UserPassword(c.GitUser, c.GitPass)
git := exec.Command(
"git",
"clone",
"-b", c.Branch,
"--single-branch",
"--no-tags",
"--separate-git-dir", gitDir,
uri.String(),
pageDir,
)
output, err := git.CombinedOutput()
if err != nil {
fmt.Println("git clone failed: ", err)
fmt.Println("=========== Dump output: ============")
fmt.Println(string(output))
fmt.Println("=====================================")
}
os.Remove(filepath.Join(pageDir, ".git"))
return
}
func (c *WebhookCFG) download(user, repo string) (err error) {
gitDir := c.gitDir(user, repo)
if _, err = os.Stat(gitDir); os.IsNotExist(err) {
err = c.clone(user, repo)
} else {
err = c.fetch(user, repo)
}
if err != nil {
return
}
err = c.checkout(user, repo)
return
}
type webhookPayload struct {
Ref string `json:"ref"`
Repository struct {
FullName string `json:"full_name"`
} `json:"repository"`
}
func (c *WebhookCFG) handle(w http.ResponseWriter, r *http.Request) {
fmt.Println("Received webhook event")
ev := r.Header.Get("X-GitHub-Event")
if ev != "push" {
// skip non-push events
fmt.Println("Skip non-push event: ", ev)
return
}
defer r.Body.Close()
data, err := io.ReadAll(r.Body)
if err != nil {
fmt.Println("Failed to read request body: ", err)
return
}
var payload webhookPayload
err = json.Unmarshal(data, &payload)
if err != nil {
fmt.Println("Failed to parse request body: ", err)
fmt.Println("=========== Dump body: ============")
fmt.Println(string(data))
fmt.Println("===================================")
return
}
if payload.Ref != "refs/heads/"+c.Branch {
// skip non-branch events
fmt.Println("Skip different branch: ", payload.Ref)
return
}
user, repo := path.Split(payload.Repository.FullName)
user = user[:len(user)-1]
fmt.Println("Pulling ", user, repo)
err = c.download(user, repo)
if err != nil {
fmt.Println("Failed to download repository: ", err)
return
}
}
func UseWebhook(bind string, cfg *WebhookCFG) (*http.Server, error) {
if cfg == nil {
return nil, errors.New("webhook config is nil")
}
s := &http.Server{
Addr: bind,
Handler: http.HandlerFunc(cfg.handle),
}
return s, nil
}