initial commit

This commit is contained in:
Chris Sexton 2015-04-13 17:35:47 -04:00 committed by Chris Sexton
commit a7f800bb14
7 changed files with 227 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@ -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

28
README.md Normal file
View File

@ -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/<your project>/build?secret=<your
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/<your project>/badge` as an image
in your page.

BIN
badges/failing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
badges/passing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
badges/pending.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

6
config.json Normal file
View File

@ -0,0 +1,6 @@
{
"test": {
"before": "sleep 5; touch y",
"after": "touch x"
}
}

172
yoctobuild.go Normal file
View File

@ -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, "<html><head><title>%s - yoctobuild</title></head><body>", title)
}
func writeFooter(w http.ResponseWriter) {
fmt.Fprintf(w, "<body></html>")
}
func projectIndex(w http.ResponseWriter, r *http.Request) {
writeHeader(w, "index")
fmt.Fprintf(w, "<p>Projects:</p><ul>")
for name := range projects {
fmt.Fprintf(w, `<li><a href="/projects/%s">%s</a></li>`, name, name)
}
fmt.Fprintf(w, "</ul>")
writeFooter(w)
}
func projectStatus(w http.ResponseWriter, r *http.Request) {
name := getProject(r)
writeHeader(w, name)
fmt.Fprintf(w, `<p><img src="/projects/%s/badge" /></p>`, name)
if p, ok := projects[name]; ok && p.err != nil {
fmt.Fprintf(w, "Last built: %s<br>\nError: <pre>%s</pre><br>\nOutput:<br>\n<pre>%s</pre>\n", p.time, p.err, p.out)
} else if ok && !p.time.IsZero() {
fmt.Fprintf(w, "Last built: %s<br>\nOutput:<br>\n<pre>%s</pre>\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))
}