api_version: 3
handlers:
+- url: /static
+ static_dir: static
- url: /log/.+
script: _go_app
-- url: /(commit|packages|result|tag|todo)
+- url: /(|commit|packages|result|tag|todo)
script: _go_app
- url: /(init|buildtest)
script: _go_app
NextNum int // Num of the next head Commit
}
+func (p *Package) String() string {
+ return fmt.Sprintf("%s: %q", p.Path, p.Name)
+}
+
func (p *Package) Key(c appengine.Context) *datastore.Key {
key := p.Path
if key == "" {
return datastore.NewKey(c, "Package", key, 0, nil)
}
+// LastCommit returns the most recent Commit for this Package.
+func (p *Package) LastCommit(c appengine.Context) (*Commit, os.Error) {
+ var commits []*Commit
+ _, err := datastore.NewQuery("Commit").
+ Ancestor(p.Key(c)).
+ Order("-Time").
+ Limit(1).
+ GetAll(c, &commits)
+ if err != nil {
+ return nil, err
+ }
+ if len(commits) != 1 {
+ return nil, datastore.ErrNoSuchEntity
+ }
+ return commits[0], nil
+}
+
+// GetPackage fetches a Package by path from the datastore.
func GetPackage(c appengine.Context, path string) (*Package, os.Error) {
p := &Package{Path: path}
err := datastore.Get(c, p.Key(c), p)
Desc string `datastore:",noindex"`
Time datastore.Time
- // Result is the Data string of each build Result for this Commit.
+ // ResultData is the Data string of each build Result for this Commit.
// For non-Go commits, only the Results for the current Go tip, weekly,
// and release Tags are stored here. This is purely de-normalized data.
// The complete data set is stored in Result entities.
- Result []string `datastore:",noindex"`
+ ResultData []string `datastore:",noindex"`
}
func (com *Commit) Key(c appengine.Context) *datastore.Key {
if err := datastore.Get(c, com.Key(c), com); err != nil {
return err
}
- com.Result = append(com.Result, r.Data())
+ com.ResultData = append(com.ResultData, r.Data())
_, err := datastore.Put(c, com.Key(c), com)
return err
}
-func (com *Commit) HasResult(builder string) bool {
- for _, r := range com.Result {
- if strings.SplitN(r, "|", 2)[0] == builder {
- return true
+// Result returns the build Result for this Commit for the given builder/goHash.
+func (c *Commit) Result(builder, goHash string) *Result {
+ for _, r := range c.ResultData {
+ p := strings.SplitN(r, "|", 4)
+ if len(p) != 4 || p[0] != builder || p[3] != goHash {
+ continue
}
+ return partsToHash(c, p)
}
- return false
+ return nil
}
-func (com *Commit) HasGoHashResult(builder, goHash string) bool {
- for _, r := range com.Result {
+// Results returns the build Results for this Commit for the given goHash.
+func (c *Commit) Results(goHash string) (results []*Result) {
+ for _, r := range c.ResultData {
p := strings.SplitN(r, "|", 4)
- if len(p) == 4 && p[0] == builder && p[3] == goHash {
- return true
+ if len(p) != 4 || p[3] != goHash {
+ continue
}
+ results = append(results, partsToHash(c, p))
+ }
+ return
+}
+
+// partsToHash converts a Commit and ResultData substrings to a Result.
+func partsToHash(c *Commit, p []string) *Result {
+ return &Result{
+ Builder: p[0],
+ Hash: c.Hash,
+ PackagePath: c.PackagePath,
+ GoHash: p[3],
+ OK: p[1] == "true",
+ LogHash: p[2],
}
- return false
}
// A Result describes a build result for a Commit on an OS/architecture.
return datastore.NewKey(c, "Result", key, 0, p.Key(c))
}
-func (r *Result) Data() string {
- return fmt.Sprintf("%v|%v|%v|%v", r.Builder, r.OK, r.LogHash, r.GoHash)
-}
-
func (r *Result) Valid() os.Error {
if !validHash(r.Hash) {
return os.NewError("invalid Hash")
return nil
}
+// Data returns the Result in string format
+// to be stored in Commit's ResultData field.
+func (r *Result) Data() string {
+ return fmt.Sprintf("%v|%v|%v|%v", r.Builder, r.OK, r.LogHash, r.GoHash)
+}
+
// A Log is a gzip-compressed log file stored under the SHA1 hash of the
// uncompressed log text.
type Log struct {
}
func (t *Tag) Key(c appengine.Context) *datastore.Key {
- p := &Package{Path: ""}
+ p := &Package{}
return datastore.NewKey(c, "Tag", t.Kind, 0, p.Key(c))
}
func (t *Tag) Valid() os.Error {
- if t.Kind != "weekly" || t.Kind != "release" || t.Kind != "tip" {
+ if t.Kind != "weekly" && t.Kind != "release" && t.Kind != "tip" {
return os.NewError("invalid Kind")
}
if !validHash(t.Hash) {
return nil
}
+// GetTag fetches a Tag by name from the datastore.
+func GetTag(c appengine.Context, tag string) (*Tag, os.Error) {
+ t := &Tag{Kind: tag}
+ if err := datastore.Get(c, t.Key(c), t); err != nil {
+ if err == datastore.ErrNoSuchEntity {
+ return nil, os.NewError("tag not found: " + tag)
+ }
+ return nil, err
+ }
+ if err := t.Valid(); err != nil {
+ return nil, err
+ }
+ return t, nil
+}
+
// commitHandler retrieves commit data or records a new commit.
//
// For GET requests it returns a Commit value for the specified
}
return nil, err
}
- var hasResult bool
- if goHash != "" {
- hasResult = com.HasGoHashResult(builder, goHash)
- } else {
- hasResult = com.HasResult(builder)
- }
- if !hasResult {
+ if com.Result(builder, goHash) == nil {
return com.Hash, nil
}
}
// packagesHandler returns a list of the non-Go Packages monitored
// by the dashboard.
func packagesHandler(r *http.Request) (interface{}, os.Error) {
- c := appengine.NewContext(r)
+ return Packages(appengine.NewContext(r))
+}
+
+// Packages returns all non-Go packages.
+func Packages(c appengine.Context) ([]*Package, os.Error) {
var pkgs []*Package
for t := datastore.NewQuery("Package").Run(c); ; {
pkg := new(Package)
return nil, datastore.RunInTransaction(c, tx, nil)
}
+// logHandler displays log text for a given hash.
+// It handles paths like "/log/hash".
func logHandler(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
h := r.URL.Path[len("/log/"):]
}
}
-type errBadMethod string
-
-func (e errBadMethod) String() string {
- return "bad method: " + string(e)
-}
-
type dashHandler func(*http.Request) (interface{}, os.Error)
type dashResponse struct {
Error string
}
+// errBadMethod is returned by a dashHandler when
+// the request has an unsuitable method.
+type errBadMethod string
+
+func (e errBadMethod) String() string {
+ return "bad method: " + string(e)
+}
+
// AuthHandler wraps a http.HandlerFunc with a handler that validates the
// supplied key and builder query parameters.
func AuthHandler(h dashHandler) http.HandlerFunc {
// Validate key query parameter for POST requests only.
key := r.FormValue("key")
- if r.Method == "POST" && key != secretKey {
+ if r.Method == "POST" && key != secretKey &&
+ !appengine.IsDevAppServer() {
h := sha1.New()
h.Write([]byte(r.FormValue("builder") + secretKey))
if key != fmt.Sprintf("%x", h.Sum()) {
func initHandler(w http.ResponseWriter, r *http.Request) {
// TODO(adg): devise a better way of bootstrapping new packages
var pkgs = []*Package{
- &Package{Name: "Go", Path: ""},
+ &Package{Name: "Go"},
&Package{Name: "Test", Path: "code.google.com/p/go.test"},
}
c := appengine.NewContext(r)
"json"
"os"
"strings"
+ "time"
"url"
)
testPackage,
}
+var tCommitTime = time.Seconds() - 60*60*24*7
+
+func tCommit(hash, parentHash string) *Commit {
+ tCommitTime += 60 * 60 * 12 // each commit should have a different time
+ return &Commit{
+ Hash: hash,
+ ParentHash: parentHash,
+ Time: datastore.Time(tCommitTime * 1e6),
+ User: "adg",
+ Desc: "change description",
+ }
+}
+
var testRequests = []struct {
path string
vals url.Values
{"/packages", nil, nil, []*Package{testPackage}},
// Go repo
- {"/commit", nil, &Commit{Hash: "0001", ParentHash: "0000"}, nil},
- {"/commit", nil, &Commit{Hash: "0002", ParentHash: "0001"}, nil},
- {"/commit", nil, &Commit{Hash: "0003", ParentHash: "0002"}, nil},
+ {"/commit", nil, tCommit("0001", "0000"), nil},
+ {"/commit", nil, tCommit("0002", "0001"), nil},
+ {"/commit", nil, tCommit("0003", "0002"), nil},
{"/todo", url.Values{"builder": {"linux-386"}}, nil, "0003"},
{"/todo", url.Values{"builder": {"linux-amd64"}}, nil, "0003"},
{"/result", nil, &Result{Builder: "linux-386", Hash: "0001", OK: true}, nil},
{"/todo", url.Values{"builder": {"linux-amd64"}}, nil, "0002"},
// branches
- {"/commit", nil, &Commit{Hash: "0004", ParentHash: "0003"}, nil},
- {"/commit", nil, &Commit{Hash: "0005", ParentHash: "0002"}, nil},
+ {"/commit", nil, tCommit("0004", "0003"), nil},
+ {"/commit", nil, tCommit("0005", "0002"), nil},
{"/todo", url.Values{"builder": {"linux-386"}}, nil, "0005"},
{"/result", nil, &Result{Builder: "linux-386", Hash: "0005", OK: true}, nil},
{"/todo", url.Values{"builder": {"linux-386"}}, nil, "0004"},
{"/result", nil, &Result{PackagePath: testPkg, Builder: "linux-386", Hash: "1001", GoHash: "0001", OK: true}, nil},
{"/todo", url.Values{"builder": {"linux-386"}, "packagePath": {testPkg}, "goHash": {"0001"}}, nil, nil},
{"/todo", url.Values{"builder": {"linux-386"}, "packagePath": {testPkg}, "goHash": {"0002"}}, nil, "1003"},
+ {"/result", nil, &Result{PackagePath: testPkg, Builder: "linux-386", Hash: "1001", GoHash: "0005", OK: false, Log: []byte("boo")}, nil},
}
func testHandler(w http.ResponseWriter, r *http.Request) {
--- /dev/null
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// TODO(adg): packages at weekly/release
+// TODO(adg): some means to register new packages
+
+package build
+
+import (
+ "appengine"
+ "appengine/datastore"
+ "exp/template/html"
+ "http"
+ "os"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "template"
+)
+
+func init() {
+ http.HandleFunc("/", uiHandler)
+ html.Escape(uiTemplate)
+}
+
+// uiHandler draws the build status page.
+func uiHandler(w http.ResponseWriter, r *http.Request) {
+ // TODO(adg): put the HTML in memcache and invalidate on updates
+ c := appengine.NewContext(r)
+
+ page, _ := strconv.Atoi(r.FormValue("page"))
+ if page < 0 {
+ page = 0
+ }
+
+ commits, err := goCommits(c, page)
+ if err != nil {
+ logErr(w, r, err)
+ return
+ }
+ builders := commitBuilders(commits)
+
+ tipState, err := TagState(c, "tip")
+ if err != nil {
+ logErr(w, r, err)
+ return
+ }
+
+ p := &Pagination{}
+ if len(commits) == commitsPerPage {
+ p.Next = page + 1
+ }
+ if page > 0 {
+ p.Prev = page - 1
+ p.HasPrev = true
+ }
+ data := &uiTemplateData{commits, builders, tipState, p}
+ if err := uiTemplate.Execute(w, data); err != nil {
+ logErr(w, r, err)
+ }
+}
+
+type Pagination struct {
+ Next, Prev int
+ HasPrev bool
+}
+
+// goCommits gets a slice of the latest Commits to the Go repository.
+// If page > 0 it paginates by commitsPerPage.
+func goCommits(c appengine.Context, page int) ([]*Commit, os.Error) {
+ q := datastore.NewQuery("Commit").
+ Ancestor((&Package{}).Key(c)).
+ Order("-Time").
+ Limit(commitsPerPage).
+ Offset(page * commitsPerPage)
+ var commits []*Commit
+ _, err := q.GetAll(c, &commits)
+ return commits, err
+}
+
+// commitBuilders returns the names of the builders that provided
+// Results for the provided commits.
+func commitBuilders(commits []*Commit) []string {
+ builders := make(map[string]bool)
+ for _, commit := range commits {
+ for _, r := range commit.Results("") {
+ builders[r.Builder] = true
+ }
+ }
+ return keys(builders)
+}
+
+func keys(m map[string]bool) (s []string) {
+ for k := range m {
+ s = append(s, k)
+ }
+ sort.Strings(s)
+ return
+}
+
+// PackageState represents the state of a Package at a tag.
+type PackageState struct {
+ *Package
+ *Commit
+ Results []*Result
+ OK bool
+}
+
+// TagState fetches the results for all non-Go packages at the specified tag.
+func TagState(c appengine.Context, name string) ([]*PackageState, os.Error) {
+ tag, err := GetTag(c, name)
+ if err != nil {
+ return nil, err
+ }
+ pkgs, err := Packages(c)
+ if err != nil {
+ return nil, err
+ }
+ var states []*PackageState
+ for _, pkg := range pkgs {
+ commit, err := pkg.LastCommit(c)
+ if err != nil {
+ c.Errorf("no Commit found: %v", pkg)
+ continue
+ }
+ results := commit.Results(tag.Hash)
+ ok := len(results) > 0
+ for _, r := range results {
+ ok = ok && r.OK
+ }
+ states = append(states, &PackageState{
+ pkg, commit, results, ok,
+ })
+ }
+ return states, nil
+}
+
+type uiTemplateData struct {
+ Commits []*Commit
+ Builders []string
+ TipState []*PackageState
+ Pagination *Pagination
+}
+
+var uiTemplate = template.Must(
+ template.New("ui").
+ Funcs(template.FuncMap{
+ "builderTitle": builderTitle,
+ "shortHash": shortHash,
+ "repoURL": repoURL,
+ }).
+ ParseFile("build/ui.html"),
+)
+
+// builderTitle formats "linux-amd64-foo" as "linux amd64 foo".
+func builderTitle(s string) string {
+ return strings.Replace(s, "-", " ", -1)
+}
+
+// shortHash returns a the short version of a hash.
+func shortHash(hash string) string {
+ if len(hash) > 12 {
+ hash = hash[:12]
+ }
+ return hash
+}
+
+// repoRe matches Google Code repositories and subrepositories (without paths).
+var repoRe = regexp.MustCompile(`^code\.google\.com/p/([a-z0-9\-]+)(\.[a-z0-9\-]+)?$`)
+
+// repoURL returns the URL of a change at a Google Code repository or subrepo.
+func repoURL(hash, packagePath string) (string, os.Error) {
+ if packagePath == "" {
+ return "https://code.google.com/p/go/source/detail?r=" + hash, nil
+ }
+ m := repoRe.FindStringSubmatch(packagePath)
+ if m == nil {
+ return "", os.NewError("unrecognized package: " + packagePath)
+ }
+ url := "https://code.google.com/p/" + m[1] + "/source/detail?r=" + hash
+ if len(m) > 2 {
+ url += "&repo=" + m[2][1:]
+ }
+ return url, nil
+}
--- /dev/null
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Go Build Dashboard</title>
+ <style>
+ body {
+ font-family: sans-serif;
+ padding: 0; margin: 0;
+ }
+ h1, h2 {
+ margin: 0;
+ padding: 5px;
+ }
+ h1 {
+ background: #eee;
+ }
+ h2 {
+ margin-top: 10px;
+ }
+ .build, .packages {
+ margin: 5px;
+ border-collapse: collapse;
+ }
+ .build td, .build th, .packages td, .packages th {
+ vertical-align: top;
+ padding: 2px 4px;
+ font-size: 10pt;
+ }
+ .build tr:nth-child(2n) {
+ background-color: #f0f0f0;
+ }
+ .build .result {
+ text-align: center;
+ width: 50px;
+ }
+ .build .time {
+ color: #666;
+ }
+ .build .descr, .build .time, .build .user {
+ white-space: nowrap;
+ }
+ .paginate {
+ padding: 0.5em;
+ }
+ .paginate a {
+ padding: 0.5em;
+ background: #eee;
+ color: blue;
+ }
+ .paginate a.inactive {
+ color: #999;
+ }
+ </style>
+ </head>
+ <body>
+
+ <h1>Go Build Status</h1>
+
+ <h2>Go</h2>
+
+ {{if $.Commits}}
+
+ <table class="build">
+ <tr>
+ <th> </th>
+ {{range $.Builders}}
+ <th class="result">{{builderTitle .}}</th>
+ {{end}}
+ </tr>
+ {{range $c := $.Commits}}
+ <tr>
+ <td class="hash"><a href="{{repoURL .Hash ""}}">{{shortHash .Hash}}</a></td>
+ {{range $.Builders}}
+ <td class="result">
+ {{with $c.Result . ""}}
+ {{if .OK}}
+ <span class="ok">ok</span>
+ {{else}}
+ <a href="/log/{{.LogHash}}" class="fail">fail</a>
+ {{end}}
+ {{else}}
+
+ {{end}}
+ </td>
+ {{end}}
+ <td class="user">{{.User}}</td>
+ <td class="time">{{.Time.Time}}</td>
+ <td class="desc">{{.Desc}}</td>
+ </tr>
+ {{end}}
+ </table>
+
+ {{with $.Pagination}}
+ <div class="paginate">
+ <a {{if .HasPrev}}href="?page={{.Prev}}"{{else}}class="inactive"{{end}}>prev</a>
+ <a {{if .Next}}href="?page={{.Next}}"{{else}}class="inactive"{{end}}>next</a>
+ <a {{if .HasPrev}}href="?page=0}"{{else}}class="inactive"{{end}}>top</a>
+ </div>
+ {{end}}
+
+ {{else}}
+ <p>No commits to display. Hm.</p>
+ {{end}}
+
+ <h2>Other packages</h2>
+
+ <table class="packages">
+ <tr>
+ <th>State</th>
+ <th>Package</th>
+ <th> </th>
+ </tr>
+ {{range $state := $.TipState}}
+ <tr>
+ <td>
+ {{if .Results}}
+ <img src="/static/status_{{if .OK}}good{{else}}alert{{end}}.gif" />
+ {{else}}
+
+ {{end}}
+ </td>
+ <td><a title="{{.Package.Path}}">{{.Package.Name}}</a></td>
+ <td>
+ {{range .Results}}
+ <div>
+ {{$h := $state.Commit.Hash}}
+ <a href="{{repoURL $h $state.Commit.PackagePath}}">{{shortHash $h}}</a>
+ <a href="/log/{{.LogHash}}">failed</a>
+ on {{.Builder}}/<a href="{{repoURL .GoHash ""}}">{{shortHash .GoHash}}</a>
+ </a></div>
+ {{end}}
+ </td>
+ </tr>
+ {{end}}
+ </table>
+
+ </body>
+</html>