commit a7f800bb144e2f70eb20cb3220234322d486de7d Author: Chris Sexton Date: Mon Apr 13 17:35:47 2015 -0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e72462 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go +yoctobuild diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ac21b0 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# yoctoBuild + +yoctoBuild is a bare-bones build service for your personal projects. Since many +hosted build services do not work outside of GitHub and BitBucket, this was +made to work with Gogs for light usage where hosting Jenkins would be absurd. + +yoctoBuild is unaware of git, Mercurial, scss, etc. It depends on a bash +executable and proper configuration. + +## Usage + +Run `yoctobuild` in its directory or copy the badges and a config file to a +working directory. Provide a `-secret` on startup, and then set your Gogs/Git +hooks/etc to send a request to `/projects//build?secret=`. + +The config file consists of a map of projects and their steps. The build server +does nothing but create a working directory for the build so the build steps +must check out code, perform tests, and manage any dependencies. The config +file should only be modified by owners of the server it lives on as it may +execute arbitrary commands on your behalf. + +This build service is very simple. It does not watch for branches or anything +of the like. You must configure a project for each individual item that you +wish to watch. + +Badges may be served by embedding `/projects//badge` as an image +in your page. diff --git a/badges/failing.png b/badges/failing.png new file mode 100644 index 0000000..3705228 Binary files /dev/null and b/badges/failing.png differ diff --git a/badges/passing.png b/badges/passing.png new file mode 100644 index 0000000..1110a78 Binary files /dev/null and b/badges/passing.png differ diff --git a/badges/pending.png b/badges/pending.png new file mode 100644 index 0000000..1210a98 Binary files /dev/null and b/badges/pending.png differ diff --git a/config.json b/config.json new file mode 100644 index 0000000..c9e3b96 --- /dev/null +++ b/config.json @@ -0,0 +1,6 @@ +{ + "test": { + "before": "sleep 5; touch y", + "after": "touch x" + } +} diff --git a/yoctobuild.go b/yoctobuild.go new file mode 100644 index 0000000..7d2c397 --- /dev/null +++ b/yoctobuild.go @@ -0,0 +1,172 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/gorilla/mux" +) + +var ( + configPath = flag.String("config", "./config.json", "Path to config file") + badgePath = flag.String("badges", "./badges/", "Path to badges") + addr = flag.String("addr", ":3001", "Address to serve on") + secret = flag.String("secret", "12345", "Secret to authorize builds") + + projects map[string]*project +) + +type project struct { + Before string + After string + out string + err error + time time.Time +} + +func runBuild(name string) { + steps := fmt.Sprintf("mkdir -p %s; cd %s; %s", + name, name, projects[name].Before) + script := bytes.NewBufferString(steps) + projects[name].time = time.Time{} + + bash := exec.Command("bash") + stdin, _ := bash.StdinPipe() + + io.Copy(stdin, script) + stdin.Close() + + out, err := bash.CombinedOutput() + + projects[name].out = string(out) + projects[name].err = err + projects[name].time = time.Now() + + if err == nil { + runPostBuild(name) + } +} + +func runPostBuild(name string) { + steps := fmt.Sprintf("cd %s; %s", + name, projects[name].After) + script := bytes.NewBufferString(steps) + + bash := exec.Command("bash") + stdin, _ := bash.StdinPipe() + + io.Copy(stdin, script) + stdin.Close() + + if out, err := bash.CombinedOutput(); err != nil { + projects[name].out = string(out) + projects[name].err = err + } +} + +func readConfig() { + if f, err := ioutil.ReadFile(*configPath); err != nil { + log.Fatal("Could not access configuration.", err) + } else { + if err := json.Unmarshal(f, &projects); err != nil { + log.Fatal("Could not read configuration.", err) + } + } +} + +func getProject(r *http.Request) string { + vars := mux.Vars(r) + return vars["project"] +} + +// TODO: Make these templates or something +func writeHeader(w http.ResponseWriter, title string) { + fmt.Fprintf(w, "%s - yoctobuild", title) +} + +func writeFooter(w http.ResponseWriter) { + fmt.Fprintf(w, "") +} + +func projectIndex(w http.ResponseWriter, r *http.Request) { + writeHeader(w, "index") + fmt.Fprintf(w, "

Projects:

    ") + for name := range projects { + fmt.Fprintf(w, `
  • %s
  • `, name, name) + } + fmt.Fprintf(w, "
") + writeFooter(w) +} + +func projectStatus(w http.ResponseWriter, r *http.Request) { + name := getProject(r) + writeHeader(w, name) + fmt.Fprintf(w, `

`, name) + if p, ok := projects[name]; ok && p.err != nil { + fmt.Fprintf(w, "Last built: %s
\nError:
%s

\nOutput:
\n
%s
\n", p.time, p.err, p.out) + } else if ok && !p.time.IsZero() { + fmt.Fprintf(w, "Last built: %s
\nOutput:
\n
%s
\n", p.time, p.out) + } + writeFooter(w) +} + +func projectBadge(w http.ResponseWriter, r *http.Request) { + name, file := getProject(r), "" + + if p, ok := projects[name]; ok && p.err != nil { + file = "failing.png" + } else if ok && !p.time.IsZero() { + file = "passing.png" + } else { + file = "pending.png" + } + + // http.ServeFile is neat, but it likes to write its own status codes that are wrong + if f, err := os.Open(filepath.Join(*badgePath, file)); err != nil { + http.NotFound(w, r) + } else { + w.Header().Set("Cache-Control", "no-cache, private") + w.Header().Set("Content-Type", "image/png") + w.WriteHeader(200) + io.Copy(w, f) + } +} + +func projectBuild(w http.ResponseWriter, r *http.Request) { + name := getProject(r) + + get, err := url.ParseQuery(r.URL.RawQuery) + if err != nil || get.Get("secret") != *secret { + w.WriteHeader(401) + return + } + + go runBuild(name) + fmt.Fprintf(w, "Build scheduled.\n") +} + +func main() { + flag.Parse() + + readConfig() + + r := mux.NewRouter() + r.Handle("/", http.RedirectHandler("/projects", 301)) + r.HandleFunc("/projects", projectIndex) + r.HandleFunc("/projects/{project}", projectStatus) + r.HandleFunc("/projects/{project}/badge", projectBadge) + r.HandleFunc("/projects/{project}/build", projectBuild) + + log.Fatal(http.ListenAndServe(*addr, r)) +}