commit a942e23b393c2ab7f819d1234153682add139d48
Author: Přemysl Janouch
Date: Sat Dec 2 22:07:25 2017 +0100
Initial commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5ea369e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/gitea-obs
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c949ca2
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright (c) 2017, Přemysl Janouch
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/README.adoc b/README.adoc
new file mode 100644
index 0000000..54fabc2
--- /dev/null
+++ b/README.adoc
@@ -0,0 +1,49 @@
+gitea-obs
+=========
+
+'gitea-obs' is a webhook endpoint to trigger the services in Open Build Service,
+meant as a replacement for the Obs service on GitHub.
+
+Unfortunately someone thought I wasn't being funny with my project names and
+blocked my OBS account before I could test it. Therefore it comes as is.
+
+Usage
+-----
+
+ $ go build
+ $ mkdir gitea-obs-workdir
+ $ cd gitea-obs-workdir
+ $ ../gitea-obs :3000 secret
+
+Then, in Gitea, assuming it's running on the same machine as the program,
+use a 'Payload URL' like the following:
+
+ http://localhost:3000/?token=TOKEN
+
+Optional arguments:
+
+ * 'project' is the OBS project name (when not implied by the token itself)
+ * 'package' is the OBS package name (when not implied by the token itself)
+ * 'refs' is a colon-separated list of Go filepath patterns to filter branches,
+ for example 'refs/heads/*'; defaults to 'refs/heads/master'
+ * 'obs' specifies a different URL for the OBS instance
+
+The program uses the current working directory as a dispatch queue, and it must
+be run in a dedicated directory.
+
+Contributing and Support
+------------------------
+Use https://git.janouch.name/p/gitea-obs to report any bugs, request features,
+or submit pull requests. If you want to discuss this project, or maybe just
+hang out with the developer, feel free to join me at irc://irc.janouch.name,
+channel #dev.
+
+License
+-------
+'gitea-obs' is written by Přemysl Janouch .
+
+You may use the software under the terms of the ISC license, the text of which
+is included within the package, or, at your option, you may relicense the work
+under the MIT or the Modified BSD License, as listed at the following site:
+
+http://www.gnu.org/licenses/license-list.html
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..395497a
--- /dev/null
+++ b/main.go
@@ -0,0 +1,232 @@
+// Open Build System Gitea trigger webhook
+package main
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "os/signal"
+ "path"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "time"
+)
+
+var programName = path.Base(os.Args[0])
+var secretToken = ""
+var apiClient *http.Client = &http.Client{Timeout: 60 * time.Second}
+
+// We're on a relatively short timeout (10s), just queue the events for later
+func handler(w http.ResponseWriter, r *http.Request) {
+ data, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // Presence of the header marks the format of hook data
+ event := r.Header.Get("X-Gitea-Event")
+ if event == "" {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ // React to new commits being added
+ if event != "push" {
+ return
+ }
+
+ buf := new(bytes.Buffer)
+ if err := json.Indent(buf, data, "", " "); err != nil {
+ log.Printf("req: %s", err)
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ // Mark our files as such, use the filesystem as a simple database
+ base := programName + "&" + r.URL.RawQuery
+ if err := func() error {
+ f, err := os.OpenFile(base+".tmp",
+ os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_SYNC, 0644)
+ if err != nil {
+ return err
+ }
+ if _, err := f.Write(buf.Bytes()); err != nil {
+ f.Close()
+ return err
+ }
+ if err := f.Close(); err != nil {
+ return err
+ }
+ // Ready for processing -- needs to be the last step here
+ if err := os.Rename(base+".tmp", base); err != nil {
+ return err
+ }
+ return nil
+ }(); err != nil {
+ log.Printf("req: %s", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ }
+}
+
+func process(ctx context.Context, values url.Values, data []byte) error {
+ pkg := values.Get("package")
+ project := values.Get("project")
+ token := values.Get("token")
+ if token == "" {
+ return fmt.Errorf("no token given")
+ }
+
+ obs := "https://build.opensuse.org"
+ if v := values.Get("obs"); v != "" {
+ obs = v
+ }
+
+ refs := "refs/heads/master"
+ if v := values.Get("refs"); v != "" {
+ refs = v
+ }
+
+ // No need for the whole "code.gitea.io/sdk/gitea" package so far
+ push := struct {
+ Secret string `json:"secret"`
+ Ref string `json:"ref"`
+ }{}
+ if err := json.Unmarshal(data, &push); err != nil {
+ return err
+ }
+ if push.Secret != secretToken {
+ return fmt.Errorf("invalid secret: %s", push.Secret)
+ }
+
+ // This roughly matches the behaviour of the GitHub service
+ matches := false
+ for _, ref := range strings.Split(refs, ":") {
+ if m, err := filepath.Match(ref, push.Ref); m {
+ matches = true
+ break
+ } else if err != nil {
+ return fmt.Errorf("%s: %s", ref, err)
+ }
+ }
+
+ // Trigger the OBS source service to make it rebuild the package
+ if matches {
+ uri := obs + "/trigger/runservice"
+ if pkg != "" && project != "" {
+ escape := url.QueryEscape
+ uri += "?package=" + escape(pkg) + "&project=" + escape(project)
+ }
+ req, err := http.NewRequest("POST", uri, nil)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Authorization", "Token "+token)
+ resp, err := apiClient.Do(req.WithContext(ctx))
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ }
+ return nil
+}
+
+func consume(ctx context.Context) {
+ list, err := ioutil.ReadDir(".")
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ for _, info := range list {
+ base := info.Name()
+ if strings.HasSuffix(base, ".tmp") || !info.Mode().IsRegular() {
+ continue
+ }
+ data, err := ioutil.ReadFile(base)
+ if err != nil {
+ log.Println(err)
+ continue
+ }
+
+ values, err := url.ParseQuery(base)
+ if err != nil {
+ log.Printf("%s: %s\n", base, err)
+ continue
+ }
+ if _, ok := values[programName]; !ok {
+ continue
+ }
+
+ if err := process(ctx, values, data); err != nil {
+ log.Printf("%s: %s\n", base, err)
+ continue
+ }
+ // XXX minor race condition
+ if err := os.Remove(base); err != nil {
+ log.Println(err)
+ continue
+ }
+ }
+}
+
+func main() {
+ if len(os.Args) < 2 || len(os.Args) > 3 {
+ fmt.Fprintf(os.Stderr, "Usage: %s LISTEN-ADDR [SECRET]\n", os.Args[0])
+ os.Exit(1)
+ }
+
+ listenAddr := os.Args[1]
+ if len(os.Args) == 3 {
+ secretToken = os.Args[2]
+ }
+
+ http.HandleFunc("/", handler)
+ server := &http.Server{Addr: listenAddr}
+
+ // Run the producer
+ go func() {
+ if err := server.ListenAndServe(); err != nil &&
+ err != http.ErrServerClosed {
+ log.Fatalln(err)
+ }
+ }()
+
+ // Run the consumer
+ ctx, ctxCancel := context.WithCancel(context.Background())
+ consumerFinished := make(chan struct{})
+ // Process the currently available batch and retry after a few seconds
+ go func() {
+ defer close(consumerFinished)
+ for {
+ consume(ctx)
+ select {
+ case <-time.After(3 * time.Second):
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+
+ // Wait for a termination signal
+ sigs := make(chan os.Signal, 1)
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+ <-sigs
+
+ // Stop the producer gracefully
+ ctxSd, ctxSdCancel := context.WithTimeout(context.Background(), 5)
+ if err := server.Shutdown(ctxSd); err != nil {
+ log.Println(err)
+ }
+ ctxSdCancel()
+
+ // Stop the consumer gracefully
+ ctxCancel()
+ <-consumerFinished
+}