summaryrefslogtreecommitdiff
path: root/misc/dashboard
diff options
context:
space:
mode:
Diffstat (limited to 'misc/dashboard')
-rw-r--r--misc/dashboard/app/app.yaml20
-rw-r--r--misc/dashboard/app/build/build.go294
-rw-r--r--misc/dashboard/app/build/handler.go428
-rw-r--r--misc/dashboard/app/build/key.go62
-rw-r--r--misc/dashboard/app/build/notify.go150
-rw-r--r--misc/dashboard/app/build/notify.txt9
-rw-r--r--misc/dashboard/app/build/test.go251
-rw-r--r--misc/dashboard/app/build/ui.go313
-rw-r--r--misc/dashboard/app/build/ui.html178
-rw-r--r--misc/dashboard/app/cache/cache.go82
-rw-r--r--misc/dashboard/app/static/status_alert.gifbin0 -> 570 bytes
-rw-r--r--misc/dashboard/app/static/status_good.gifbin0 -> 328 bytes
-rw-r--r--misc/dashboard/builder/exec.go10
-rw-r--r--misc/dashboard/builder/http.go193
-rw-r--r--misc/dashboard/builder/main.go225
-rw-r--r--misc/dashboard/builder/package.go19
-rw-r--r--misc/dashboard/godashboard/app.yaml7
-rw-r--r--misc/dashboard/godashboard/fail-notify.txt3
-rw-r--r--misc/dashboard/godashboard/gobuild.py561
-rw-r--r--misc/dashboard/godashboard/main.html41
-rw-r--r--misc/dashboard/godashboard/package.html8
-rw-r--r--misc/dashboard/godashboard/static/style.css11
22 files changed, 2111 insertions, 754 deletions
diff --git a/misc/dashboard/app/app.yaml b/misc/dashboard/app/app.yaml
new file mode 100644
index 000000000..685ca6e3d
--- /dev/null
+++ b/misc/dashboard/app/app.yaml
@@ -0,0 +1,20 @@
+# Update with
+# google_appengine/appcfg.py [-V test-build] update .
+#
+# Using -V test-build will run as test-build.golang.org.
+
+application: golang-org
+version: build
+runtime: go
+api_version: 3
+
+handlers:
+- url: /static
+ static_dir: static
+- url: /log/.+
+ script: _go_app
+- url: /(|commit|packages|result|tag|todo)
+ script: _go_app
+- url: /(init|buildtest|key|_ah/queue/go/delay)
+ script: _go_app
+ login: admin
diff --git a/misc/dashboard/app/build/build.go b/misc/dashboard/app/build/build.go
new file mode 100644
index 000000000..175812a37
--- /dev/null
+++ b/misc/dashboard/app/build/build.go
@@ -0,0 +1,294 @@
+// 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.
+
+package build
+
+import (
+ "bytes"
+ "compress/gzip"
+ "crypto/sha1"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "strings"
+
+ "appengine"
+ "appengine/datastore"
+)
+
+const maxDatastoreStringLen = 500
+
+// A Package describes a package that is listed on the dashboard.
+type Package struct {
+ Name string
+ Path string // (empty for the main Go tree)
+ 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 == "" {
+ key = "go"
+ }
+ 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)
+ if err == datastore.ErrNoSuchEntity {
+ return nil, fmt.Errorf("package %q not found", path)
+ }
+ return p, err
+}
+
+// A Commit describes an individual commit in a package.
+//
+// Each Commit entity is a descendant of its associated Package entity.
+// In other words, all Commits with the same PackagePath belong to the same
+// datastore entity group.
+type Commit struct {
+ PackagePath string // (empty for Go commits)
+ Hash string
+ ParentHash string
+ Num int // Internal monotonic counter unique to this package.
+
+ User string
+ Desc string `datastore:",noindex"`
+ Time datastore.Time
+
+ // 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.
+ ResultData []string `datastore:",noindex"`
+
+ FailNotificationSent bool
+}
+
+func (com *Commit) Key(c appengine.Context) *datastore.Key {
+ if com.Hash == "" {
+ panic("tried Key on Commit with empty Hash")
+ }
+ p := Package{Path: com.PackagePath}
+ key := com.PackagePath + "|" + com.Hash
+ return datastore.NewKey(c, "Commit", key, 0, p.Key(c))
+}
+
+func (c *Commit) Valid() os.Error {
+ if !validHash(c.Hash) {
+ return os.NewError("invalid Hash")
+ }
+ if c.ParentHash != "" && !validHash(c.ParentHash) { // empty is OK
+ return os.NewError("invalid ParentHash")
+ }
+ return nil
+}
+
+// AddResult adds the denormalized Reuslt data to the Commit's Result field.
+// It must be called from inside a datastore transaction.
+func (com *Commit) AddResult(c appengine.Context, r *Result) os.Error {
+ if err := datastore.Get(c, com.Key(c), com); err != nil {
+ return fmt.Errorf("getting Commit: %v", err)
+ }
+ com.ResultData = append(com.ResultData, r.Data())
+ if _, err := datastore.Put(c, com.Key(c), com); err != nil {
+ return fmt.Errorf("putting Commit: %v", err)
+ }
+ return nil
+}
+
+// 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 nil
+}
+
+// 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[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],
+ }
+}
+
+// OK returns the Commit's build state for a specific builder and goHash.
+func (c *Commit) OK(builder, goHash string) (ok, present bool) {
+ r := c.Result(builder, goHash)
+ if r == nil {
+ return false, false
+ }
+ return r.OK, true
+}
+
+// A Result describes a build result for a Commit on an OS/architecture.
+//
+// Each Result entity is a descendant of its associated Commit entity.
+type Result struct {
+ Builder string // "arch-os[-note]"
+ Hash string
+ PackagePath string // (empty for Go commits)
+
+ // The Go Commit this was built against (empty for Go commits).
+ GoHash string
+
+ OK bool
+ Log string `datastore:"-"` // for JSON unmarshaling only
+ LogHash string `datastore:",noindex"` // Key to the Log record.
+
+ RunTime int64 // time to build+test in nanoseconds
+}
+
+func (r *Result) Key(c appengine.Context) *datastore.Key {
+ p := Package{Path: r.PackagePath}
+ key := r.Builder + "|" + r.PackagePath + "|" + r.Hash + "|" + r.GoHash
+ return datastore.NewKey(c, "Result", key, 0, p.Key(c))
+}
+
+func (r *Result) Valid() os.Error {
+ if !validHash(r.Hash) {
+ return os.NewError("invalid Hash")
+ }
+ if r.PackagePath != "" && !validHash(r.GoHash) {
+ return os.NewError("invalid GoHash")
+ }
+ 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 {
+ CompressedLog []byte
+}
+
+func (l *Log) Text() ([]byte, os.Error) {
+ d, err := gzip.NewReader(bytes.NewBuffer(l.CompressedLog))
+ if err != nil {
+ return nil, fmt.Errorf("reading log data: %v", err)
+ }
+ b, err := ioutil.ReadAll(d)
+ if err != nil {
+ return nil, fmt.Errorf("reading log data: %v", err)
+ }
+ return b, nil
+}
+
+func PutLog(c appengine.Context, text string) (hash string, err os.Error) {
+ h := sha1.New()
+ io.WriteString(h, text)
+ b := new(bytes.Buffer)
+ z, _ := gzip.NewWriterLevel(b, gzip.BestCompression)
+ io.WriteString(z, text)
+ z.Close()
+ hash = fmt.Sprintf("%x", h.Sum())
+ key := datastore.NewKey(c, "Log", hash, 0, nil)
+ _, err = datastore.Put(c, key, &Log{b.Bytes()})
+ return
+}
+
+// A Tag is used to keep track of the most recent Go weekly and release tags.
+// Typically there will be one Tag entity for each kind of hg tag.
+type Tag struct {
+ Kind string // "weekly", "release", or "tip"
+ Name string // the tag itself (for example: "release.r60")
+ Hash string
+}
+
+func (t *Tag) Key(c appengine.Context) *datastore.Key {
+ 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" {
+ return os.NewError("invalid Kind")
+ }
+ if !validHash(t.Hash) {
+ return os.NewError("invalid 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
+}
+
+// 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)
+ if _, err := t.Next(pkg); err == datastore.Done {
+ break
+ } else if err != nil {
+ return nil, err
+ }
+ if pkg.Path != "" {
+ pkgs = append(pkgs, pkg)
+ }
+ }
+ return pkgs, nil
+}
diff --git a/misc/dashboard/app/build/handler.go b/misc/dashboard/app/build/handler.go
new file mode 100644
index 000000000..b3e62ad46
--- /dev/null
+++ b/misc/dashboard/app/build/handler.go
@@ -0,0 +1,428 @@
+// 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.
+
+package build
+
+import (
+ "crypto/hmac"
+ "fmt"
+ "http"
+ "json"
+ "os"
+
+ "appengine"
+ "appengine/datastore"
+ "cache"
+)
+
+const commitsPerPage = 30
+
+// defaultPackages specifies the Package records to be created by initHandler.
+var defaultPackages = []*Package{
+ &Package{Name: "Go"},
+}
+
+// commitHandler retrieves commit data or records a new commit.
+//
+// For GET requests it returns a Commit value for the specified
+// packagePath and hash.
+//
+// For POST requests it reads a JSON-encoded Commit value from the request
+// body and creates a new Commit entity. It also updates the "tip" Tag for
+// each new commit at tip.
+//
+// This handler is used by a gobuilder process in -commit mode.
+func commitHandler(r *http.Request) (interface{}, os.Error) {
+ c := appengine.NewContext(r)
+ com := new(Commit)
+
+ if r.Method == "GET" {
+ com.PackagePath = r.FormValue("packagePath")
+ com.Hash = r.FormValue("hash")
+ if err := datastore.Get(c, com.Key(c), com); err != nil {
+ return nil, fmt.Errorf("getting Commit: %v", err)
+ }
+ return com, nil
+ }
+ if r.Method != "POST" {
+ return nil, errBadMethod(r.Method)
+ }
+
+ // POST request
+ defer r.Body.Close()
+ if err := json.NewDecoder(r.Body).Decode(com); err != nil {
+ return nil, fmt.Errorf("decoding Body: %v", err)
+ }
+ if len(com.Desc) > maxDatastoreStringLen {
+ com.Desc = com.Desc[:maxDatastoreStringLen]
+ }
+ if err := com.Valid(); err != nil {
+ return nil, fmt.Errorf("validating Commit: %v", err)
+ }
+ defer cache.Tick(c)
+ tx := func(c appengine.Context) os.Error {
+ return addCommit(c, com)
+ }
+ return nil, datastore.RunInTransaction(c, tx, nil)
+}
+
+// addCommit adds the Commit entity to the datastore and updates the tip Tag.
+// It must be run inside a datastore transaction.
+func addCommit(c appengine.Context, com *Commit) os.Error {
+ var tc Commit // temp value so we don't clobber com
+ err := datastore.Get(c, com.Key(c), &tc)
+ if err != datastore.ErrNoSuchEntity {
+ // if this commit is already in the datastore, do nothing
+ if err == nil {
+ return nil
+ }
+ return fmt.Errorf("getting Commit: %v", err)
+ }
+ // get the next commit number
+ p, err := GetPackage(c, com.PackagePath)
+ if err != nil {
+ return fmt.Errorf("GetPackage: %v", err)
+ }
+ com.Num = p.NextNum
+ p.NextNum++
+ if _, err := datastore.Put(c, p.Key(c), p); err != nil {
+ return fmt.Errorf("putting Package: %v", err)
+ }
+ // if this isn't the first Commit test the parent commit exists
+ if com.Num > 0 {
+ n, err := datastore.NewQuery("Commit").
+ Filter("Hash =", com.ParentHash).
+ Ancestor(p.Key(c)).
+ Count(c)
+ if err != nil {
+ return fmt.Errorf("testing for parent Commit: %v", err)
+ }
+ if n == 0 {
+ return os.NewError("parent commit not found")
+ }
+ }
+ // update the tip Tag if this is the Go repo
+ if p.Path == "" {
+ t := &Tag{Kind: "tip", Hash: com.Hash}
+ if _, err = datastore.Put(c, t.Key(c), t); err != nil {
+ return fmt.Errorf("putting Tag: %v", err)
+ }
+ }
+ // put the Commit
+ if _, err = datastore.Put(c, com.Key(c), com); err != nil {
+ return fmt.Errorf("putting Commit: %v", err)
+ }
+ return nil
+}
+
+// tagHandler records a new tag. It reads a JSON-encoded Tag value from the
+// request body and updates the Tag entity for the Kind of tag provided.
+//
+// This handler is used by a gobuilder process in -commit mode.
+func tagHandler(r *http.Request) (interface{}, os.Error) {
+ if r.Method != "POST" {
+ return nil, errBadMethod(r.Method)
+ }
+
+ t := new(Tag)
+ defer r.Body.Close()
+ if err := json.NewDecoder(r.Body).Decode(t); err != nil {
+ return nil, err
+ }
+ if err := t.Valid(); err != nil {
+ return nil, err
+ }
+ c := appengine.NewContext(r)
+ defer cache.Tick(c)
+ _, err := datastore.Put(c, t.Key(c), t)
+ return nil, err
+}
+
+// Todo is a todoHandler response.
+type Todo struct {
+ Kind string // "build-go-commit" or "build-package"
+ Data interface{}
+}
+
+// todoHandler returns the next action to be performed by a builder.
+// It expects "builder" and "kind" query parameters and returns a *Todo value.
+// Multiple "kind" parameters may be specified.
+func todoHandler(r *http.Request) (interface{}, os.Error) {
+ c := appengine.NewContext(r)
+ now := cache.Now(c)
+ key := "build-todo-" + r.Form.Encode()
+ var todo *Todo
+ if cache.Get(r, now, key, &todo) {
+ return todo, nil
+ }
+ var err os.Error
+ builder := r.FormValue("builder")
+ for _, kind := range r.Form["kind"] {
+ var data interface{}
+ switch kind {
+ case "build-go-commit":
+ data, err = buildTodo(c, builder, "", "")
+ case "build-package":
+ packagePath := r.FormValue("packagePath")
+ goHash := r.FormValue("goHash")
+ data, err = buildTodo(c, builder, packagePath, goHash)
+ }
+ if data != nil || err != nil {
+ todo = &Todo{Kind: kind, Data: data}
+ break
+ }
+ }
+ if err == nil {
+ cache.Set(r, now, key, todo)
+ }
+ return todo, err
+}
+
+// buildTodo returns the next Commit to be built (or nil if none available).
+//
+// If packagePath and goHash are empty, it scans the first 20 Go Commits in
+// Num-descending order and returns the first one it finds that doesn't have a
+// Result for this builder.
+//
+// If provided with non-empty packagePath and goHash args, it scans the first
+// 20 Commits in Num-descending order for the specified packagePath and
+// returns the first that doesn't have a Result for this builder and goHash.
+func buildTodo(c appengine.Context, builder, packagePath, goHash string) (interface{}, os.Error) {
+ p, err := GetPackage(c, packagePath)
+ if err != nil {
+ return nil, err
+ }
+
+ t := datastore.NewQuery("Commit").
+ Ancestor(p.Key(c)).
+ Limit(commitsPerPage).
+ Order("-Num").
+ Run(c)
+ for {
+ com := new(Commit)
+ if _, err := t.Next(com); err != nil {
+ if err == datastore.Done {
+ err = nil
+ }
+ return nil, err
+ }
+ if com.Result(builder, goHash) == nil {
+ return com, nil
+ }
+ }
+ panic("unreachable")
+}
+
+// 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)
+ now := cache.Now(c)
+ const key = "build-packages"
+ var p []*Package
+ if cache.Get(r, now, key, &p) {
+ return p, nil
+ }
+ p, err := Packages(c)
+ if err != nil {
+ return nil, err
+ }
+ cache.Set(r, now, key, p)
+ return p, nil
+}
+
+// resultHandler records a build result.
+// It reads a JSON-encoded Result value from the request body,
+// creates a new Result entity, and updates the relevant Commit entity.
+// If the Log field is not empty, resultHandler creates a new Log entity
+// and updates the LogHash field before putting the Commit entity.
+func resultHandler(r *http.Request) (interface{}, os.Error) {
+ if r.Method != "POST" {
+ return nil, errBadMethod(r.Method)
+ }
+
+ c := appengine.NewContext(r)
+ res := new(Result)
+ defer r.Body.Close()
+ if err := json.NewDecoder(r.Body).Decode(res); err != nil {
+ return nil, fmt.Errorf("decoding Body: %v", err)
+ }
+ if err := res.Valid(); err != nil {
+ return nil, fmt.Errorf("validating Result: %v", err)
+ }
+ defer cache.Tick(c)
+ // store the Log text if supplied
+ if len(res.Log) > 0 {
+ hash, err := PutLog(c, res.Log)
+ if err != nil {
+ return nil, fmt.Errorf("putting Log: %v", err)
+ }
+ res.LogHash = hash
+ }
+ tx := func(c appengine.Context) os.Error {
+ // check Package exists
+ if _, err := GetPackage(c, res.PackagePath); err != nil {
+ return fmt.Errorf("GetPackage: %v", err)
+ }
+ // put Result
+ if _, err := datastore.Put(c, res.Key(c), res); err != nil {
+ return fmt.Errorf("putting Result: %v", err)
+ }
+ // add Result to Commit
+ com := &Commit{PackagePath: res.PackagePath, Hash: res.Hash}
+ if err := com.AddResult(c, res); err != nil {
+ return fmt.Errorf("AddResult: %v", err)
+ }
+ // Send build failure notifications, if necessary.
+ // Note this must run after the call AddResult, which
+ // populates the Commit's ResultData field.
+ return notifyOnFailure(c, com, res.Builder)
+ }
+ 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) {
+ w.Header().Set("Content-type", "text/plain")
+ c := appengine.NewContext(r)
+ hash := r.URL.Path[len("/log/"):]
+ key := datastore.NewKey(c, "Log", hash, 0, nil)
+ l := new(Log)
+ if err := datastore.Get(c, key, l); err != nil {
+ logErr(w, r, err)
+ return
+ }
+ b, err := l.Text()
+ if err != nil {
+ logErr(w, r, err)
+ return
+ }
+ w.Write(b)
+}
+
+type dashHandler func(*http.Request) (interface{}, os.Error)
+
+type dashResponse struct {
+ Response interface{}
+ 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 {
+ return func(w http.ResponseWriter, r *http.Request) {
+ c := appengine.NewContext(r)
+
+ // Put the URL Query values into r.Form to avoid parsing the
+ // request body when calling r.FormValue.
+ r.Form = r.URL.Query()
+
+ var err os.Error
+ var resp interface{}
+
+ // Validate key query parameter for POST requests only.
+ key := r.FormValue("key")
+ builder := r.FormValue("builder")
+ if r.Method == "POST" && !validKey(c, key, builder) {
+ err = os.NewError("invalid key: " + key)
+ }
+
+ // Call the original HandlerFunc and return the response.
+ if err == nil {
+ resp, err = h(r)
+ }
+
+ // Write JSON response.
+ dashResp := &dashResponse{Response: resp}
+ if err != nil {
+ c.Errorf("%v", err)
+ dashResp.Error = err.String()
+ }
+ w.Header().Set("Content-Type", "application/json")
+ if err = json.NewEncoder(w).Encode(dashResp); err != nil {
+ c.Criticalf("encoding response: %v", err)
+ }
+ }
+}
+
+func initHandler(w http.ResponseWriter, r *http.Request) {
+ // TODO(adg): devise a better way of bootstrapping new packages
+ c := appengine.NewContext(r)
+ defer cache.Tick(c)
+ for _, p := range defaultPackages {
+ if err := datastore.Get(c, p.Key(c), new(Package)); err == nil {
+ continue
+ } else if err != datastore.ErrNoSuchEntity {
+ logErr(w, r, err)
+ return
+ }
+ if _, err := datastore.Put(c, p.Key(c), p); err != nil {
+ logErr(w, r, err)
+ return
+ }
+ }
+ fmt.Fprint(w, "OK")
+}
+
+func keyHandler(w http.ResponseWriter, r *http.Request) {
+ builder := r.FormValue("builder")
+ if builder == "" {
+ logErr(w, r, os.NewError("must supply builder in query string"))
+ return
+ }
+ c := appengine.NewContext(r)
+ fmt.Fprint(w, builderKey(c, builder))
+}
+
+func init() {
+ // admin handlers
+ http.HandleFunc("/init", initHandler)
+ http.HandleFunc("/key", keyHandler)
+
+ // authenticated handlers
+ http.HandleFunc("/commit", AuthHandler(commitHandler))
+ http.HandleFunc("/packages", AuthHandler(packagesHandler))
+ http.HandleFunc("/result", AuthHandler(resultHandler))
+ http.HandleFunc("/tag", AuthHandler(tagHandler))
+ http.HandleFunc("/todo", AuthHandler(todoHandler))
+
+ // public handlers
+ http.HandleFunc("/log/", logHandler)
+}
+
+func validHash(hash string) bool {
+ // TODO(adg): correctly validate a hash
+ return hash != ""
+}
+
+func validKey(c appengine.Context, key, builder string) bool {
+ if appengine.IsDevAppServer() {
+ return true
+ }
+ if key == secretKey(c) {
+ return true
+ }
+ return key == builderKey(c, builder)
+}
+
+func builderKey(c appengine.Context, builder string) string {
+ h := hmac.NewMD5([]byte(secretKey(c)))
+ h.Write([]byte(builder))
+ return fmt.Sprintf("%x", h.Sum())
+}
+
+func logErr(w http.ResponseWriter, r *http.Request, err os.Error) {
+ appengine.NewContext(r).Errorf("Error: %v", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ fmt.Fprint(w, "Error: ", err)
+}
diff --git a/misc/dashboard/app/build/key.go b/misc/dashboard/app/build/key.go
new file mode 100644
index 000000000..5306c3b6b
--- /dev/null
+++ b/misc/dashboard/app/build/key.go
@@ -0,0 +1,62 @@
+// 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.
+
+package build
+
+import (
+ "sync"
+
+ "appengine"
+ "appengine/datastore"
+)
+
+var theKey struct {
+ sync.RWMutex
+ BuilderKey
+}
+
+type BuilderKey struct {
+ Secret string
+}
+
+func (k *BuilderKey) Key(c appengine.Context) *datastore.Key {
+ return datastore.NewKey(c, "BuilderKey", "root", 0, nil)
+}
+
+func secretKey(c appengine.Context) string {
+ // check with rlock
+ theKey.RLock()
+ k := theKey.Secret
+ theKey.RUnlock()
+ if k != "" {
+ return k
+ }
+
+ // prepare to fill; check with lock and keep lock
+ theKey.Lock()
+ defer theKey.Unlock()
+ if theKey.Secret != "" {
+ return theKey.Secret
+ }
+
+ // fill
+ if err := datastore.Get(c, theKey.Key(c), &theKey.BuilderKey); err != nil {
+ if err == datastore.ErrNoSuchEntity {
+ // If the key is not stored in datastore, write it.
+ // This only happens at the beginning of a new deployment.
+ // The code is left here for SDK use and in case a fresh
+ // deployment is ever needed. "gophers rule" is not the
+ // real key.
+ if !appengine.IsDevAppServer() {
+ panic("lost key from datastore")
+ }
+ theKey.Secret = "gophers rule"
+ datastore.Put(c, theKey.Key(c), &theKey.BuilderKey)
+ return theKey.Secret
+ }
+ panic("cannot load builder key: " + err.String())
+ }
+
+ return theKey.Secret
+}
diff --git a/misc/dashboard/app/build/notify.go b/misc/dashboard/app/build/notify.go
new file mode 100644
index 000000000..826132be2
--- /dev/null
+++ b/misc/dashboard/app/build/notify.go
@@ -0,0 +1,150 @@
+// 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.
+
+package build
+
+import (
+ "appengine"
+ "appengine/datastore"
+ "appengine/delay"
+ "appengine/mail"
+ "bytes"
+ "fmt"
+ "gob"
+ "os"
+ "template"
+)
+
+const (
+ mailFrom = "builder@golang.org" // use this for sending any mail
+ failMailTo = "golang-dev@googlegroups.com"
+ domain = "build.golang.org"
+)
+
+// notifyOnFailure checks whether the supplied Commit or the subsequent
+// Commit (if present) breaks the build for this builder.
+// If either of those commits break the build an email notification is sent
+// from a delayed task. (We use a task because this way the mail won't be
+// sent if the enclosing datastore transaction fails.)
+//
+// This must be run in a datastore transaction, and the provided *Commit must
+// have been retrieved from the datastore within that transaction.
+func notifyOnFailure(c appengine.Context, com *Commit, builder string) os.Error {
+ // TODO(adg): implement notifications for packages
+ if com.PackagePath != "" {
+ return nil
+ }
+
+ p := &Package{Path: com.PackagePath}
+ var broken *Commit
+ ok, present := com.OK(builder, "")
+ if !present {
+ return fmt.Errorf("no result for %s/%s", com.Hash, builder)
+ }
+ q := datastore.NewQuery("Commit").Ancestor(p.Key(c))
+ if ok {
+ // This commit is OK. Notify if next Commit is broken.
+ next := new(Commit)
+ q.Filter("ParentHash=", com.Hash)
+ if err := firstMatch(c, q, next); err != nil {
+ if err == datastore.ErrNoSuchEntity {
+ // OK at tip, no notification necessary.
+ return nil
+ }
+ return err
+ }
+ if ok, present := next.OK(builder, ""); present && !ok {
+ broken = next
+ }
+ } else {
+ // This commit is broken. Notify if the previous Commit is OK.
+ prev := new(Commit)
+ q.Filter("Hash=", com.ParentHash)
+ if err := firstMatch(c, q, prev); err != nil {
+ if err == datastore.ErrNoSuchEntity {
+ // No previous result, let the backfill of
+ // this result trigger the notification.
+ return nil
+ }
+ return err
+ }
+ if ok, present := prev.OK(builder, ""); present && ok {
+ broken = com
+ }
+ }
+ var err os.Error
+ if broken != nil && !broken.FailNotificationSent {
+ c.Infof("%s is broken commit; notifying", broken.Hash)
+ sendFailMailLater.Call(c, broken, builder) // add task to queue
+ broken.FailNotificationSent = true
+ _, err = datastore.Put(c, broken.Key(c), broken)
+ }
+ return err
+}
+
+// firstMatch executes the query q and loads the first entity into v.
+func firstMatch(c appengine.Context, q *datastore.Query, v interface{}) os.Error {
+ t := q.Limit(1).Run(c)
+ _, err := t.Next(v)
+ if err == datastore.Done {
+ err = datastore.ErrNoSuchEntity
+ }
+ return err
+}
+
+var (
+ sendFailMailLater = delay.Func("sendFailMail", sendFailMail)
+ sendFailMailTmpl = template.Must(
+ template.New("notify").Funcs(tmplFuncs).ParseFile("build/notify.txt"),
+ )
+)
+
+func init() {
+ gob.Register(&Commit{}) // for delay
+}
+
+// sendFailMail sends a mail notification that the build failed on the
+// provided commit and builder.
+func sendFailMail(c appengine.Context, com *Commit, builder string) {
+ // TODO(adg): handle packages
+
+ // get Result
+ r := com.Result(builder, "")
+ if r == nil {
+ c.Errorf("finding result for %q: %+v", builder, com)
+ return
+ }
+
+ // get Log
+ k := datastore.NewKey(c, "Log", r.LogHash, 0, nil)
+ l := new(Log)
+ if err := datastore.Get(c, k, l); err != nil {
+ c.Errorf("finding Log record %v: %v", r.LogHash, err)
+ return
+ }
+
+ // prepare mail message
+ var body bytes.Buffer
+ err := sendFailMailTmpl.Execute(&body, map[string]interface{}{
+ "Builder": builder, "Commit": com, "Result": r, "Log": l,
+ "Hostname": domain,
+ })
+ if err != nil {
+ c.Errorf("rendering mail template: %v", err)
+ return
+ }
+ subject := fmt.Sprintf("%s broken by %s", builder, shortDesc(com.Desc))
+ msg := &mail.Message{
+ Sender: mailFrom,
+ To: []string{failMailTo},
+ ReplyTo: failMailTo,
+ Subject: subject,
+ Body: body.String(),
+ }
+
+ // send mail
+ if err := mail.Send(c, msg); err != nil {
+ c.Errorf("sending mail: %v", err)
+ }
+}
diff --git a/misc/dashboard/app/build/notify.txt b/misc/dashboard/app/build/notify.txt
new file mode 100644
index 000000000..6c9006703
--- /dev/null
+++ b/misc/dashboard/app/build/notify.txt
@@ -0,0 +1,9 @@
+Change {{shortHash .Commit.Hash}} broke the {{.Builder}} build:
+http://{{.Hostname}}/log/{{.Result.LogHash}}
+
+{{.Commit.Desc}}
+
+http://code.google.com/p/go/source/detail?r={{shortHash .Commit.Hash}}
+
+$ tail -200 < log
+{{printf "%s" .Log.Text | tail 200}}
diff --git a/misc/dashboard/app/build/test.go b/misc/dashboard/app/build/test.go
new file mode 100644
index 000000000..a923969bc
--- /dev/null
+++ b/misc/dashboard/app/build/test.go
@@ -0,0 +1,251 @@
+// 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.
+
+package build
+
+// TODO(adg): test authentication
+
+import (
+ "appengine"
+ "appengine/datastore"
+ "bytes"
+ "fmt"
+ "http"
+ "http/httptest"
+ "io"
+ "json"
+ "os"
+ "strings"
+ "time"
+ "url"
+)
+
+func init() {
+ http.HandleFunc("/buildtest", testHandler)
+}
+
+var testEntityKinds = []string{
+ "Package",
+ "Commit",
+ "Result",
+ "Log",
+}
+
+const testPkg = "code.google.com/p/go.test"
+
+var testPackage = &Package{Name: "Test", Path: testPkg}
+
+var testPackages = []*Package{
+ &Package{Name: "Go", Path: ""},
+ 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
+ req interface{}
+ res interface{}
+}{
+ // Packages
+ {"/packages", nil, nil, []*Package{testPackage}},
+
+ // Go repo
+ {"/commit", nil, tCommit("0001", "0000"), nil},
+ {"/commit", nil, tCommit("0002", "0001"), nil},
+ {"/commit", nil, tCommit("0003", "0002"), nil},
+ {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-386"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0003"}}},
+ {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-amd64"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0003"}}},
+ {"/result", nil, &Result{Builder: "linux-386", Hash: "0001", OK: true}, nil},
+ {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-386"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0003"}}},
+ {"/result", nil, &Result{Builder: "linux-386", Hash: "0002", OK: true}, nil},
+ {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-386"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0003"}}},
+
+ // multiple builders
+ {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-amd64"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0003"}}},
+ {"/result", nil, &Result{Builder: "linux-amd64", Hash: "0003", OK: true}, nil},
+ {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-386"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0003"}}},
+ {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-amd64"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0002"}}},
+
+ // branches
+ {"/commit", nil, tCommit("0004", "0003"), nil},
+ {"/commit", nil, tCommit("0005", "0002"), nil},
+ {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-386"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0005"}}},
+ {"/result", nil, &Result{Builder: "linux-386", Hash: "0005", OK: true}, nil},
+ {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-386"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0004"}}},
+ {"/result", nil, &Result{Builder: "linux-386", Hash: "0004", OK: true}, nil},
+ {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-386"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0003"}}},
+
+ // logs
+ {"/result", nil, &Result{Builder: "linux-386", Hash: "0003", OK: false, Log: "test"}, nil},
+ {"/log/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", nil, nil, "test"},
+ {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-386"}}, nil, nil},
+
+ // repeat failure (shouldn't re-send mail)
+ {"/result", nil, &Result{Builder: "linux-386", Hash: "0003", OK: false, Log: "test"}, nil},
+
+ // non-Go repos
+ {"/commit", nil, &Commit{PackagePath: testPkg, Hash: "1001", ParentHash: "1000"}, nil},
+ {"/commit", nil, &Commit{PackagePath: testPkg, Hash: "1002", ParentHash: "1001"}, nil},
+ {"/commit", nil, &Commit{PackagePath: testPkg, Hash: "1003", ParentHash: "1002"}, nil},
+ {"/todo", url.Values{"kind": {"build-package"}, "builder": {"linux-386"}, "packagePath": {testPkg}, "goHash": {"0001"}}, nil, &Todo{Kind: "build-package", Data: &Commit{Hash: "1003"}}},
+ {"/result", nil, &Result{PackagePath: testPkg, Builder: "linux-386", Hash: "1003", GoHash: "0001", OK: true}, nil},
+ {"/todo", url.Values{"kind": {"build-package"}, "builder": {"linux-386"}, "packagePath": {testPkg}, "goHash": {"0001"}}, nil, &Todo{Kind: "build-package", Data: &Commit{Hash: "1002"}}},
+ {"/result", nil, &Result{PackagePath: testPkg, Builder: "linux-386", Hash: "1002", GoHash: "0001", OK: true}, nil},
+ {"/todo", url.Values{"kind": {"build-package"}, "builder": {"linux-386"}, "packagePath": {testPkg}, "goHash": {"0001"}}, nil, &Todo{Kind: "build-package", Data: &Commit{Hash: "1001"}}},
+ {"/result", nil, &Result{PackagePath: testPkg, Builder: "linux-386", Hash: "1001", GoHash: "0001", OK: true}, nil},
+ {"/todo", url.Values{"kind": {"build-package"}, "builder": {"linux-386"}, "packagePath": {testPkg}, "goHash": {"0001"}}, nil, nil},
+ {"/todo", url.Values{"kind": {"build-package"}, "builder": {"linux-386"}, "packagePath": {testPkg}, "goHash": {"0002"}}, nil, &Todo{Kind: "build-package", Data: &Commit{Hash: "1003"}}},
+ {"/result", nil, &Result{PackagePath: testPkg, Builder: "linux-386", Hash: "1001", GoHash: "0005", OK: false, Log: "boo"}, nil},
+}
+
+func testHandler(w http.ResponseWriter, r *http.Request) {
+ if !appengine.IsDevAppServer() {
+ fmt.Fprint(w, "These tests must be run under the dev_appserver.")
+ return
+ }
+ c := appengine.NewContext(r)
+ if err := nukeEntities(c, testEntityKinds); err != nil {
+ logErr(w, r, err)
+ return
+ }
+ if r.FormValue("nukeonly") != "" {
+ fmt.Fprint(w, "OK")
+ return
+ }
+
+ for _, p := range testPackages {
+ if _, err := datastore.Put(c, p.Key(c), p); err != nil {
+ logErr(w, r, err)
+ return
+ }
+ }
+
+ for i, t := range testRequests {
+ c.Infof("running test %d %s", i, t.path)
+ errorf := func(format string, args ...interface{}) {
+ fmt.Fprintf(w, "%d %s: ", i, t.path)
+ fmt.Fprintf(w, format, args...)
+ fmt.Fprintln(w)
+ }
+ var body io.ReadWriter
+ if t.req != nil {
+ body = new(bytes.Buffer)
+ json.NewEncoder(body).Encode(t.req)
+ }
+ url := "http://" + domain + t.path
+ if t.vals != nil {
+ url += "?" + t.vals.Encode()
+ }
+ req, err := http.NewRequest("POST", url, body)
+ if err != nil {
+ logErr(w, r, err)
+ return
+ }
+ if t.req != nil {
+ req.Method = "POST"
+ }
+ req.Header = r.Header
+ rec := httptest.NewRecorder()
+
+ // Make the request
+ http.DefaultServeMux.ServeHTTP(rec, req)
+
+ if rec.Code != 0 && rec.Code != 200 {
+ errorf(rec.Body.String())
+ return
+ }
+ resp := new(dashResponse)
+
+ // If we're expecting a *Todo value,
+ // prime the Response field with a Todo and a Commit inside it.
+ if _, ok := t.res.(*Todo); ok {
+ resp.Response = &Todo{Data: &Commit{}}
+ }
+
+ if strings.HasPrefix(t.path, "/log/") {
+ resp.Response = rec.Body.String()
+ } else {
+ err := json.NewDecoder(rec.Body).Decode(resp)
+ if err != nil {
+ errorf("decoding response: %v", err)
+ return
+ }
+ }
+ if e, ok := t.res.(string); ok {
+ g, ok := resp.Response.(string)
+ if !ok {
+ errorf("Response not string: %T", resp.Response)
+ return
+ }
+ if g != e {
+ errorf("response mismatch: got %q want %q", g, e)
+ return
+ }
+ }
+ if e, ok := t.res.(*Todo); ok {
+ g, ok := resp.Response.(*Todo)
+ if !ok {
+ errorf("Response not *Todo: %T", resp.Response)
+ return
+ }
+ if e.Data == nil && g.Data != nil {
+ errorf("Response.Data should be nil, got: %v", g.Data)
+ return
+ }
+ if g.Data == nil {
+ errorf("Response.Data is nil, want: %v", e.Data)
+ return
+ }
+ gd, ok := g.Data.(*Commit)
+ if !ok {
+ errorf("Response.Data not *Commit: %T", g.Data)
+ return
+ }
+ if e.Data.(*Commit).Hash != gd.Hash {
+ errorf("hashes don't match: got %q, want %q", g, e)
+ return
+ }
+ }
+ if t.res == nil && resp.Response != nil {
+ errorf("response mismatch: got %q expected <nil>",
+ resp.Response)
+ return
+ }
+ }
+ fmt.Fprint(w, "PASS")
+}
+
+func nukeEntities(c appengine.Context, kinds []string) os.Error {
+ if !appengine.IsDevAppServer() {
+ return os.NewError("can't nuke production data")
+ }
+ var keys []*datastore.Key
+ for _, kind := range kinds {
+ q := datastore.NewQuery(kind).KeysOnly()
+ for t := q.Run(c); ; {
+ k, err := t.Next(nil)
+ if err == datastore.Done {
+ break
+ }
+ if err != nil {
+ return err
+ }
+ keys = append(keys, k)
+ }
+ }
+ return datastore.DeleteMulti(c, keys)
+}
diff --git a/misc/dashboard/app/build/ui.go b/misc/dashboard/app/build/ui.go
new file mode 100644
index 000000000..032fdbd84
--- /dev/null
+++ b/misc/dashboard/app/build/ui.go
@@ -0,0 +1,313 @@
+// 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 (
+ "bytes"
+ "exp/template/html"
+ "http"
+ "os"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "template"
+
+ "appengine"
+ "appengine/datastore"
+ "cache"
+)
+
+func init() {
+ http.HandleFunc("/", uiHandler)
+ html.Escape(uiTemplate)
+}
+
+// uiHandler draws the build status page.
+func uiHandler(w http.ResponseWriter, r *http.Request) {
+ c := appengine.NewContext(r)
+ now := cache.Now(c)
+ const key = "build-ui"
+
+ page, _ := strconv.Atoi(r.FormValue("page"))
+ if page < 0 {
+ page = 0
+ }
+
+ // Used cached version of front page, if available.
+ if page == 0 {
+ var b []byte
+ if cache.Get(r, now, key, &b) {
+ w.Write(b)
+ return
+ }
+ }
+
+ 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}
+
+ var buf bytes.Buffer
+ if err := uiTemplate.Execute(&buf, data); err != nil {
+ logErr(w, r, err)
+ return
+ }
+
+ // Cache the front page.
+ if page == 0 {
+ cache.Set(r, now, key, buf.Bytes())
+ }
+
+ buf.WriteTo(w)
+}
+
+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(tmplFuncs).ParseFile("build/ui.html"),
+)
+
+var tmplFuncs = template.FuncMap{
+ "builderOS": builderOS,
+ "builderArch": builderArch,
+ "builderArchShort": builderArchShort,
+ "builderArchChar": builderArchChar,
+ "builderTitle": builderTitle,
+ "builderSpans": builderSpans,
+ "repoURL": repoURL,
+ "shortDesc": shortDesc,
+ "shortHash": shortHash,
+ "shortUser": shortUser,
+ "tail": tail,
+}
+
+func splitDash(s string) (string, string) {
+ i := strings.Index(s, "-")
+ if i >= 0 {
+ return s[:i], s[i+1:]
+ }
+ return s, ""
+}
+
+// builderOS returns the os tag for a builder string
+func builderOS(s string) string {
+ os, _ := splitDash(s)
+ return os
+}
+
+// builderArch returns the arch tag for a builder string
+func builderArch(s string) string {
+ _, arch := splitDash(s)
+ arch, _ = splitDash(arch) // chop third part
+ return arch
+}
+
+// builderArchShort returns a short arch tag for a builder string
+func builderArchShort(s string) string {
+ arch := builderArch(s)
+ switch arch {
+ case "amd64":
+ return "x64"
+ }
+ return arch
+}
+
+// builderArchChar returns the architecture letter for a builder string
+func builderArchChar(s string) string {
+ arch := builderArch(s)
+ switch arch {
+ case "386":
+ return "8"
+ case "amd64":
+ return "6"
+ case "arm":
+ return "5"
+ }
+ return arch
+}
+
+type builderSpan struct {
+ N int
+ OS string
+}
+
+// builderSpans creates a list of tags showing
+// the builder's operating system names, spanning
+// the appropriate number of columns.
+func builderSpans(s []string) []builderSpan {
+ var sp []builderSpan
+ for len(s) > 0 {
+ i := 1
+ os := builderOS(s[0])
+ for i < len(s) && builderOS(s[i]) == os {
+ i++
+ }
+ sp = append(sp, builderSpan{i, os})
+ s = s[i:]
+ }
+ return sp
+}
+
+// builderTitle formats "linux-amd64-foo" as "linux amd64 foo".
+func builderTitle(s string) string {
+ return strings.Replace(s, "-", " ", -1)
+}
+
+// shortDesc returns the first line of a description.
+func shortDesc(desc string) string {
+ if i := strings.Index(desc, "\n"); i != -1 {
+ desc = desc[:i]
+ }
+ return desc
+}
+
+// shortHash returns a short version of a hash.
+func shortHash(hash string) string {
+ if len(hash) > 12 {
+ hash = hash[:12]
+ }
+ return hash
+}
+
+// shortUser returns a shortened version of a user string.
+func shortUser(user string) string {
+ if i, j := strings.Index(user, "<"), strings.Index(user, ">"); 0 <= i && i < j {
+ user = user[i+1 : j]
+ }
+ if i := strings.Index(user, "@"); i >= 0 {
+ return user[:i]
+ }
+ return user
+}
+
+// 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
+}
+
+// tail returns the trailing n lines of s.
+func tail(n int, s string) string {
+ lines := strings.Split(s, "\n")
+ if len(lines) < n {
+ return s
+ }
+ return strings.Join(lines[len(lines)-n:], "\n")
+}
diff --git a/misc/dashboard/app/build/ui.html b/misc/dashboard/app/build/ui.html
new file mode 100644
index 000000000..678c95238
--- /dev/null
+++ b/misc/dashboard/app/build/ui.html
@@ -0,0 +1,178 @@
+<!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.commit:nth-child(2n) {
+ background-color: #f0f0f0;
+ }
+ .build .hash {
+ font-family: monospace;
+ font-size: 9pt;
+ }
+ .build .result {
+ text-align: center;
+ width: 2em;
+ }
+ .col-hash, .col-result {
+ border-right: solid 1px #ccc;
+ }
+ .build .arch {
+ font-size: 66%;
+ font-weight: normal;
+ }
+ .build .time {
+ color: #666;
+ }
+ .build .ok {
+ font-size: 83%;
+ }
+ .build .desc, .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;
+ }
+ .fail {
+ color: #C00;
+ }
+ </style>
+ </head>
+ <body>
+
+ <h1>Go Build Status</h1>
+
+ {{if $.Commits}}
+
+ <table class="build">
+ <colgroup class="col-hash"></colgroup>
+ {{range $.Builders | builderSpans}}
+ <colgroup class="col-result" span="{{.N}}"></colgroup>
+ {{end}}
+ <colgroup class="col-user"></colgroup>
+ <colgroup class="col-time"></colgroup>
+ <colgroup class="col-desc"></colgroup>
+ <tr>
+ <!-- extra row to make alternating colors use dark for first result -->
+ </tr>
+ <tr>
+ <th>&nbsp;</th>
+ {{range $.Builders | builderSpans}}
+ <th colspan="{{.N}}">{{.OS}}</th>
+ {{end}}
+ <th></th>
+ <th></th>
+ <th></th>
+ </tr>
+ <tr>
+ <th>&nbsp;</th>
+ {{range $.Builders}}
+ <th class="result arch" title="{{.}}">{{builderArchShort .}}</th>
+ {{end}}
+ </tr>
+ {{range $c := $.Commits}}
+ <tr class="commit">
+ <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}}
+ &nbsp;
+ {{end}}
+ </td>
+ {{end}}
+ <td class="user" title="{{.User}}">{{shortUser .User}}</td>
+ <td class="time">{{.Time.Time.Format "Mon 02 Jan 15:04"}}</td>
+ <td class="desc" title="{{.Desc}}">{{shortDesc .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}}
+
+ {{if $.TipState}}
+ <h2>Other packages</h2>
+
+ <table class="packages">
+ <tr>
+ <th>State</th>
+ <th>Package</th>
+ <th>&nbsp;</th>
+ </tr>
+ {{range $state := $.TipState}}
+ <tr>
+ <td>
+ {{if .Results}}
+ <img src="/static/status_{{if .OK}}good{{else}}alert{{end}}.gif" />
+ {{else}}
+ &nbsp;
+ {{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>
+ {{if .OK}}
+ ok
+ {{else}}
+ <a href="/log/{{.LogHash}}" class="fail">failed</a>
+ {{end}}
+ on {{.Builder}}/<a href="{{repoURL .GoHash ""}}">{{shortHash .GoHash}}</a>
+ </a></div>
+ {{end}}
+ </td>
+ </tr>
+ {{end}}
+ </table>
+ {{end}}
+
+ </body>
+</html>
diff --git a/misc/dashboard/app/cache/cache.go b/misc/dashboard/app/cache/cache.go
new file mode 100644
index 000000000..d290ed416
--- /dev/null
+++ b/misc/dashboard/app/cache/cache.go
@@ -0,0 +1,82 @@
+// 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.
+
+package cache
+
+import (
+ "fmt"
+ "http"
+ "time"
+
+ "appengine"
+ "appengine/memcache"
+)
+
+const (
+ nocache = "nocache"
+ timeKey = "cachetime"
+ expiry = 600 // 10 minutes
+)
+
+func newTime() uint64 { return uint64(time.Seconds()) << 32 }
+
+// Now returns the current logical datastore time to use for cache lookups.
+func Now(c appengine.Context) uint64 {
+ t, err := memcache.Increment(c, timeKey, 0, newTime())
+ if err != nil {
+ c.Errorf("cache.Now: %v", err)
+ return 0
+ }
+ return t
+}
+
+// Tick sets the current logical datastore time to a never-before-used time
+// and returns that time. It should be called to invalidate the cache.
+func Tick(c appengine.Context) uint64 {
+ t, err := memcache.Increment(c, timeKey, 1, newTime())
+ if err != nil {
+ c.Errorf("cache.Tick: %v", err)
+ return 0
+ }
+ return t
+}
+
+// Get fetches data for name at time now from memcache and unmarshals it into
+// value. It reports whether it found the cache record and logs any errors to
+// the admin console.
+func Get(r *http.Request, now uint64, name string, value interface{}) bool {
+ if now == 0 || r.FormValue(nocache) != "" {
+ return false
+ }
+ c := appengine.NewContext(r)
+ key := fmt.Sprintf("%s.%d", name, now)
+ _, err := memcache.JSON.Get(c, key, value)
+ if err == nil {
+ c.Debugf("cache hit %q", key)
+ return true
+ }
+ c.Debugf("cache miss %q", key)
+ if err != memcache.ErrCacheMiss {
+ c.Errorf("get cache %q: %v", key, err)
+ }
+ return false
+}
+
+// Set puts value into memcache under name at time now.
+// It logs any errors to the admin console.
+func Set(r *http.Request, now uint64, name string, value interface{}) {
+ if now == 0 || r.FormValue(nocache) != "" {
+ return
+ }
+ c := appengine.NewContext(r)
+ key := fmt.Sprintf("%s.%d", name, now)
+ err := memcache.JSON.Set(c, &memcache.Item{
+ Key: key,
+ Object: value,
+ Expiration: expiry,
+ })
+ if err != nil {
+ c.Errorf("set cache %q: %v", key, err)
+ }
+}
diff --git a/misc/dashboard/app/static/status_alert.gif b/misc/dashboard/app/static/status_alert.gif
new file mode 100644
index 000000000..495d9d2e0
--- /dev/null
+++ b/misc/dashboard/app/static/status_alert.gif
Binary files differ
diff --git a/misc/dashboard/app/static/status_good.gif b/misc/dashboard/app/static/status_good.gif
new file mode 100644
index 000000000..ef9c5a8f6
--- /dev/null
+++ b/misc/dashboard/app/static/status_good.gif
Binary files differ
diff --git a/misc/dashboard/builder/exec.go b/misc/dashboard/builder/exec.go
index a042c5699..7f21abaa2 100644
--- a/misc/dashboard/builder/exec.go
+++ b/misc/dashboard/builder/exec.go
@@ -6,15 +6,15 @@ package main
import (
"bytes"
- "exec"
"io"
"log"
"os"
+ "os/exec"
"strings"
)
// run is a simple wrapper for exec.Run/Close
-func run(envv []string, dir string, argv ...string) os.Error {
+func run(envv []string, dir string, argv ...string) error {
if *verbose {
log.Println("run", argv)
}
@@ -31,7 +31,7 @@ func run(envv []string, dir string, argv ...string) os.Error {
// process combined stdout and stderr output, exit status and error.
// The error returned is nil, if process is started successfully,
// even if exit status is not 0.
-func runLog(envv []string, logfile, dir string, argv ...string) (string, int, os.Error) {
+func runLog(envv []string, logfile, dir string, argv ...string) (string, int, error) {
if *verbose {
log.Println("runLog", argv)
}
@@ -56,11 +56,11 @@ func runLog(envv []string, logfile, dir string, argv ...string) (string, int, os
err := cmd.Run()
if err != nil {
- if ws, ok := err.(*os.Waitmsg); ok {
+ if ws, ok := err.(*exec.ExitError); ok {
return b.String(), ws.ExitStatus(), nil
}
}
- return b.String(), 0, nil
+ return b.String(), 0, err
}
// useBash prefixes a list of args with 'bash' if the first argument
diff --git a/misc/dashboard/builder/http.go b/misc/dashboard/builder/http.go
index abef8faa4..b25b417e1 100644
--- a/misc/dashboard/builder/http.go
+++ b/misc/dashboard/builder/http.go
@@ -6,98 +6,129 @@ package main
import (
"bytes"
+ "encoding/json"
+ "errors"
"fmt"
- "http"
- "json"
+ "io"
"log"
- "os"
- "strconv"
- "url"
+ "net/http"
+ "net/url"
+ "time"
)
-type param map[string]string
+type obj map[string]interface{}
// dash runs the given method and command on the dashboard.
-// If args is not nil, it is the query or post parameters.
-// If resp is not nil, dash unmarshals the body as JSON into resp.
-func dash(meth, cmd string, resp interface{}, args param) os.Error {
+// If args is non-nil it is encoded as the URL query string.
+// If req is non-nil it is JSON-encoded and passed as the body of the HTTP POST.
+// If resp is non-nil the server's response is decoded into the value pointed
+// to by resp (resp must be a pointer).
+func dash(meth, cmd string, args url.Values, req, resp interface{}) error {
var r *http.Response
- var err os.Error
+ var err error
if *verbose {
- log.Println("dash", cmd, args)
+ log.Println("dash", meth, cmd, args, req)
}
cmd = "http://" + *dashboard + "/" + cmd
- vals := make(url.Values)
- for k, v := range args {
- vals.Add(k, v)
+ if len(args) > 0 {
+ cmd += "?" + args.Encode()
}
switch meth {
case "GET":
- if q := vals.Encode(); q != "" {
- cmd += "?" + q
+ if req != nil {
+ log.Panicf("%s to %s with req", meth, cmd)
}
r, err = http.Get(cmd)
case "POST":
- r, err = http.PostForm(cmd, vals)
+ var body io.Reader
+ if req != nil {
+ b, err := json.Marshal(req)
+ if err != nil {
+ return err
+ }
+ body = bytes.NewBuffer(b)
+ }
+ r, err = http.Post(cmd, "text/json", body)
default:
- return fmt.Errorf("unknown method %q", meth)
+ log.Panicf("%s: invalid method %q", cmd, meth)
+ panic("invalid method: " + meth)
}
if err != nil {
return err
}
+
defer r.Body.Close()
- var buf bytes.Buffer
- buf.ReadFrom(r.Body)
- if resp != nil {
- if err = json.Unmarshal(buf.Bytes(), resp); err != nil {
- log.Printf("json unmarshal %#q: %s\n", buf.Bytes(), err)
- return err
- }
+ body := new(bytes.Buffer)
+ if _, err := body.ReadFrom(r.Body); err != nil {
+ return err
}
- return nil
-}
-func dashStatus(meth, cmd string, args param) os.Error {
- var resp struct {
- Status string
- Error string
+ // Read JSON-encoded Response into provided resp
+ // and return an error if present.
+ var result = struct {
+ Response interface{}
+ Error string
+ }{
+ // Put the provided resp in here as it can be a pointer to
+ // some value we should unmarshal into.
+ Response: resp,
}
- err := dash(meth, cmd, &resp, args)
- if err != nil {
+ if err = json.Unmarshal(body.Bytes(), &result); err != nil {
+ log.Printf("json unmarshal %#q: %s\n", body.Bytes(), err)
return err
}
- if resp.Status != "OK" {
- return os.NewError("/build: " + resp.Error)
+ if result.Error != "" {
+ return errors.New(result.Error)
}
+
return nil
}
// todo returns the next hash to build.
-func (b *Builder) todo() (rev string, err os.Error) {
- var resp []struct {
- Hash string
+func (b *Builder) todo(kind, pkg, goHash string) (rev string, err error) {
+ args := url.Values{
+ "kind": {kind},
+ "builder": {b.name},
+ "packagePath": {pkg},
+ "goHash": {goHash},
}
- if err = dash("GET", "todo", &resp, param{"builder": b.name}); err != nil {
- return
+ var resp *struct {
+ Kind string
+ Data struct {
+ Hash string
+ }
}
- if len(resp) > 0 {
- rev = resp[0].Hash
+ if err = dash("GET", "todo", args, nil, &resp); err != nil {
+ return "", err
}
- return
+ if resp == nil {
+ return "", nil
+ }
+ if kind != resp.Kind {
+ return "", fmt.Errorf("expecting Kind %q, got %q", kind, resp.Kind)
+ }
+ return resp.Data.Hash, nil
}
// recordResult sends build results to the dashboard
-func (b *Builder) recordResult(buildLog string, hash string) os.Error {
- return dash("POST", "build", nil, param{
- "builder": b.name,
- "key": b.key,
- "node": hash,
- "log": buildLog,
- })
+func (b *Builder) recordResult(ok bool, pkg, hash, goHash, buildLog string, runTime time.Duration) error {
+ req := obj{
+ "Builder": b.name,
+ "PackagePath": pkg,
+ "Hash": hash,
+ "GoHash": goHash,
+ "OK": ok,
+ "Log": buildLog,
+ "RunTime": runTime,
+ }
+ args := url.Values{"key": {b.key}, "builder": {b.name}}
+ return dash("POST", "result", args, req, nil)
}
// packages fetches a list of package paths from the dashboard
-func packages() (pkgs []string, err os.Error) {
+func packages() (pkgs []string, err error) {
+ return nil, nil
+ /* TODO(adg): un-stub this once the new package builder design is done
var resp struct {
Packages []struct {
Path string
@@ -111,38 +142,58 @@ func packages() (pkgs []string, err os.Error) {
pkgs = append(pkgs, p.Path)
}
return
+ */
}
-// updatePackage sends package build results and info dashboard
-func (b *Builder) updatePackage(pkg string, ok bool, buildLog, info string) os.Error {
+// updatePackage sends package build results and info to the dashboard
+func (b *Builder) updatePackage(pkg string, ok bool, buildLog, info string) error {
+ return nil
+ /* TODO(adg): un-stub this once the new package builder design is done
return dash("POST", "package", nil, param{
"builder": b.name,
"key": b.key,
"path": pkg,
- "ok": strconv.Btoa(ok),
+ "ok": strconv.FormatBool(ok),
"log": buildLog,
"info": info,
})
+ */
}
-// postCommit informs the dashboard of a new commit
-func postCommit(key string, l *HgLog) os.Error {
- return dashStatus("POST", "commit", param{
- "key": key,
- "node": l.Hash,
- "date": l.Date,
- "user": l.Author,
- "parent": l.Parent,
- "desc": l.Desc,
- })
+func postCommit(key, pkg string, l *HgLog) error {
+ t, err := time.Parse(time.RFC3339, l.Date)
+ if err != nil {
+ return fmt.Errorf("parsing %q: %v", l.Date, t)
+ }
+ return dash("POST", "commit", url.Values{"key": {key}}, obj{
+ "PackagePath": pkg,
+ "Hash": l.Hash,
+ "ParentHash": l.Parent,
+ "Time": t.Unix() * 1e6, // in microseconds, yuck!
+ "User": l.Author,
+ "Desc": l.Desc,
+ }, nil)
}
-// dashboardCommit returns true if the dashboard knows about hash.
-func dashboardCommit(hash string) bool {
- err := dashStatus("GET", "commit", param{"node": hash})
- if err != nil {
- log.Printf("check %s: %s", hash, err)
- return false
+func dashboardCommit(pkg, hash string) bool {
+ err := dash("GET", "commit", url.Values{
+ "packagePath": {pkg},
+ "hash": {hash},
+ }, nil, nil)
+ return err == nil
+}
+
+func dashboardPackages() []string {
+ var resp []struct {
+ Path string
+ }
+ if err := dash("GET", "packages", nil, nil, &resp); err != nil {
+ log.Println("dashboardPackages:", err)
+ return nil
+ }
+ var pkgs []string
+ for _, r := range resp {
+ pkgs = append(pkgs, r.Path)
}
- return true
+ return pkgs
}
diff --git a/misc/dashboard/builder/main.go b/misc/dashboard/builder/main.go
index a5479846d..3556a457d 100644
--- a/misc/dashboard/builder/main.go
+++ b/misc/dashboard/builder/main.go
@@ -5,38 +5,39 @@
package main
import (
+ "encoding/xml"
+ "errors"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"path"
+ "path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
- "xml"
)
const (
codeProject = "go"
codePyScript = "misc/dashboard/googlecode_upload.py"
hgUrl = "https://go.googlecode.com/hg/"
- waitInterval = 30e9 // time to wait before checking for new revs
mkdirPerm = 0750
- pkgBuildInterval = 1e9 * 60 * 60 * 24 // rebuild packages every 24 hours
+ waitInterval = 30 * time.Second // time to wait before checking for new revs
+ pkgBuildInterval = 24 * time.Hour // rebuild packages every 24 hours
)
// These variables are copied from the gobuilder's environment
// to the envv of its subprocesses.
var extraEnv = []string{
- "GOHOSTOS",
+ "GOARM",
"GOHOSTARCH",
+ "GOHOSTOS",
"PATH",
- "DISABLE_NET_TESTS",
- "MAKEFLAGS",
- "GOARM",
+ "TMPDIR",
}
type Builder struct {
@@ -50,7 +51,7 @@ type Builder struct {
var (
buildroot = flag.String("buildroot", path.Join(os.TempDir(), "gobuilder"), "Directory under which to build")
commitFlag = flag.Bool("commit", false, "upload information about new commits")
- dashboard = flag.String("dashboard", "godashboard.appspot.com", "Go Dashboard Host")
+ dashboard = flag.String("dashboard", "build.golang.org", "Go Dashboard Host")
buildRelease = flag.Bool("release", false, "Build and upload binary release archives")
buildRevision = flag.String("rev", "", "Build specified revision and exit")
buildCmd = flag.String("cmd", "./all.bash", "Build command (specify absolute or relative to go/src/)")
@@ -92,7 +93,7 @@ func main() {
if err := os.Mkdir(*buildroot, mkdirPerm); err != nil {
log.Fatalf("Error making build root (%s): %s", *buildroot, err)
}
- if err := run(nil, *buildroot, "hg", "clone", hgUrl, goroot); err != nil {
+ if err := hgClone(hgUrl, goroot); err != nil {
log.Fatal("Error cloning repository:", err)
}
@@ -106,7 +107,7 @@ func main() {
// if specified, build revision and return
if *buildRevision != "" {
- hash, err := fullHash(*buildRevision)
+ hash, err := fullHash(goroot, *buildRevision)
if err != nil {
log.Fatal("Error finding revision: ", err)
}
@@ -130,7 +131,7 @@ func main() {
// check for new commits and build them
for {
built := false
- t := time.Nanoseconds()
+ t := time.Now()
if *parallel {
done := make(chan bool)
for _, b := range builders {
@@ -151,14 +152,14 @@ func main() {
time.Sleep(waitInterval)
}
// sleep if we're looping too fast.
- t1 := time.Nanoseconds() - t
- if t1 < waitInterval {
- time.Sleep(waitInterval - t1)
+ dt := time.Now().Sub(t)
+ if dt < waitInterval {
+ time.Sleep(waitInterval - dt)
}
}
}
-func NewBuilder(builder string) (*Builder, os.Error) {
+func NewBuilder(builder string) (*Builder, error) {
b := &Builder{name: builder}
// get goos/goarch from builder string
@@ -193,7 +194,7 @@ func NewBuilder(builder string) (*Builder, os.Error) {
// a new release tag is found.
func (b *Builder) buildExternal() {
var prevTag string
- var nextBuild int64
+ var nextBuild time.Time
for {
time.Sleep(waitInterval)
err := run(nil, goroot, "hg", "pull", "-u")
@@ -212,7 +213,7 @@ func (b *Builder) buildExternal() {
// don't rebuild if there's no new release
// and it's been less than pkgBuildInterval
// nanoseconds since the last build.
- if tag == prevTag && time.Nanoseconds() < nextBuild {
+ if tag == prevTag && time.Now().Before(nextBuild) {
continue
}
// build will also build the packages
@@ -221,7 +222,7 @@ func (b *Builder) buildExternal() {
continue
}
prevTag = tag
- nextBuild = time.Nanoseconds() + pkgBuildInterval
+ nextBuild = time.Now().Add(pkgBuildInterval)
}
}
@@ -235,7 +236,7 @@ func (b *Builder) build() bool {
log.Println(b.name, "build:", err)
}
}()
- hash, err := b.todo()
+ hash, err := b.todo("build-go-commit", "", "")
if err != nil {
log.Println(err)
return false
@@ -245,7 +246,7 @@ func (b *Builder) build() bool {
}
// Look for hash locally before running hg pull.
- if _, err := fullHash(hash[:12]); err != nil {
+ if _, err := fullHash(goroot, hash[:12]); err != nil {
// Don't have hash, so run hg pull.
if err := run(nil, goroot, "hg", "pull"); err != nil {
log.Println("hg pull failed:", err)
@@ -259,7 +260,7 @@ func (b *Builder) build() bool {
return true
}
-func (b *Builder) buildHash(hash string) (err os.Error) {
+func (b *Builder) buildHash(hash string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("%s build: %s: %s", b.name, hash, err)
@@ -283,8 +284,7 @@ func (b *Builder) buildHash(hash string) (err os.Error) {
}
// update to specified revision
- err = run(nil, path.Join(workpath, "go"),
- "hg", "update", hash)
+ err = run(nil, path.Join(workpath, "go"), "hg", "update", hash)
if err != nil {
return
}
@@ -293,7 +293,9 @@ func (b *Builder) buildHash(hash string) (err os.Error) {
// build
logfile := path.Join(workpath, "build.log")
+ startTime := time.Now()
buildLog, status, err := runLog(b.envv(), logfile, srcDir, *buildCmd)
+ runTime := time.Now().Sub(startTime)
if err != nil {
return fmt.Errorf("%s: %s", *buildCmd, err)
}
@@ -301,21 +303,24 @@ func (b *Builder) buildHash(hash string) (err os.Error) {
// if we're in external mode, build all packages and return
if *external {
if status != 0 {
- return os.NewError("go build failed")
+ return errors.New("go build failed")
}
- return b.buildPackages(workpath, hash)
+ return b.buildExternalPackages(workpath, hash)
}
if status != 0 {
// record failure
- return b.recordResult(buildLog, hash)
+ return b.recordResult(false, "", hash, "", buildLog, runTime)
}
// record success
- if err = b.recordResult("", hash); err != nil {
+ if err = b.recordResult(true, "", hash, "", "", runTime); err != nil {
return fmt.Errorf("recordResult: %s", err)
}
+ // build goinstallable packages
+ b.buildPackages(filepath.Join(workpath, "go"), hash)
+
// finish here if codeUsername and codePassword aren't set
if b.codeUsername == "" || b.codePassword == "" || !*buildRelease {
return
@@ -342,11 +347,67 @@ func (b *Builder) buildHash(hash string) (err os.Error) {
"-w", b.codePassword,
"-l", fmt.Sprintf("%s,%s", b.goos, b.goarch),
fn)
+ if err != nil {
+ return fmt.Errorf("%s: %s", codePyScript, err)
+ }
}
return
}
+func (b *Builder) buildPackages(goRoot, goHash string) {
+ for _, pkg := range dashboardPackages() {
+ // get the latest todo for this package
+ hash, err := b.todo("build-package", pkg, goHash)
+ if err != nil {
+ log.Printf("buildPackages %s: %v", pkg, err)
+ continue
+ }
+ if hash == "" {
+ continue
+ }
+
+ // goinstall the package
+ if *verbose {
+ log.Printf("buildPackages %s: installing %q", pkg, hash)
+ }
+ buildLog, err := b.goinstall(goRoot, pkg, hash)
+ ok := buildLog == ""
+ if err != nil {
+ ok = false
+ log.Printf("buildPackages %s: %v", pkg, err)
+ }
+
+ // record the result
+ err = b.recordResult(ok, pkg, hash, goHash, buildLog, 0)
+ if err != nil {
+ log.Printf("buildPackages %s: %v", pkg, err)
+ }
+ }
+}
+
+func (b *Builder) goinstall(goRoot, pkg, hash string) (string, error) {
+ bin := filepath.Join(goRoot, "bin/goinstall")
+ env := append(b.envv(), "GOROOT="+goRoot)
+
+ // fetch package and dependencies
+ log, status, err := runLog(env, "", goRoot, bin,
+ "-dashboard=false", "-install=false", pkg)
+ if err != nil || status != 0 {
+ return log, err
+ }
+
+ // hg update to the specified hash
+ pkgPath := filepath.Join(goRoot, "src/pkg", pkg)
+ if err := run(nil, pkgPath, "hg", "update", hash); err != nil {
+ return "", err
+ }
+
+ // build the package
+ log, _, err = runLog(env, "", goRoot, bin, "-dashboard=false", pkg)
+ return log, err
+}
+
// envv returns an environment for build/bench execution
func (b *Builder) envv() []string {
if runtime.GOOS == "windows" {
@@ -409,12 +470,12 @@ func (b *Builder) envvWindows() []string {
func isDirectory(name string) bool {
s, err := os.Stat(name)
- return err == nil && s.IsDirectory()
+ return err == nil && s.IsDir()
}
func isFile(name string) bool {
s, err := os.Stat(name)
- return err == nil && (s.IsRegular() || s.IsSymlink())
+ return err == nil && !s.IsDir()
}
// commitWatcher polls hg for new commits and tells the dashboard about them.
@@ -424,11 +485,16 @@ func commitWatcher() {
if err != nil {
log.Fatal(err)
}
+ key := b.key
+
for {
if *verbose {
log.Printf("poll...")
}
- commitPoll(b.key)
+ commitPoll(key, "")
+ for _, pkg := range dashboardPackages() {
+ commitPoll(key, pkg)
+ }
if *verbose {
log.Printf("sleep...")
}
@@ -436,6 +502,18 @@ func commitWatcher() {
}
}
+func hgClone(url, path string) error {
+ return run(nil, *buildroot, "hg", "clone", url, path)
+}
+
+func hgRepoExists(path string) bool {
+ fi, err := os.Stat(filepath.Join(path, ".hg"))
+ if err != nil {
+ return false
+ }
+ return fi.IsDir()
+}
+
// HgLog represents a single Mercurial revision.
type HgLog struct {
Hash string
@@ -455,18 +533,18 @@ var logByHash = map[string]*HgLog{}
// xmlLogTemplate is a template to pass to Mercurial to make
// hg log print the log in valid XML for parsing with xml.Unmarshal.
const xmlLogTemplate = `
- <log>
- <hash>{node|escape}</hash>
- <parent>{parent|escape}</parent>
- <author>{author|escape}</author>
- <date>{date}</date>
- <desc>{desc|escape}</desc>
- </log>
+ <Log>
+ <Hash>{node|escape}</Hash>
+ <Parent>{parent|escape}</Parent>
+ <Author>{author|escape}</Author>
+ <Date>{date|rfc3339date}</Date>
+ <Desc>{desc|escape}</Desc>
+ </Log>
`
// commitPoll pulls any new revisions from the hg server
// and tells the server about them.
-func commitPoll(key string) {
+func commitPoll(key, pkg string) {
// Catch unexpected panics.
defer func() {
if err := recover(); err != nil {
@@ -474,14 +552,29 @@ func commitPoll(key string) {
}
}()
- if err := run(nil, goroot, "hg", "pull"); err != nil {
+ pkgRoot := goroot
+
+ if pkg != "" {
+ pkgRoot = path.Join(*buildroot, pkg)
+ if !hgRepoExists(pkgRoot) {
+ if err := hgClone(repoURL(pkg), pkgRoot); err != nil {
+ log.Printf("%s: hg clone failed: %v", pkg, err)
+ if err := os.RemoveAll(pkgRoot); err != nil {
+ log.Printf("%s: %v", pkg, err)
+ }
+ return
+ }
+ }
+ }
+
+ if err := run(nil, pkgRoot, "hg", "pull"); err != nil {
log.Printf("hg pull: %v", err)
return
}
const N = 50 // how many revisions to grab
- data, _, err := runLog(nil, "", goroot, "hg", "log",
+ data, _, err := runLog(nil, "", pkgRoot, "hg", "log",
"--encoding=utf-8",
"--limit="+strconv.Itoa(N),
"--template="+xmlLogTemplate,
@@ -494,7 +587,7 @@ func commitPoll(key string) {
var logStruct struct {
Log []HgLog
}
- err = xml.Unmarshal(strings.NewReader("<top>"+data+"</top>"), &logStruct)
+ err = xml.Unmarshal([]byte("<Top>"+data+"</Top>"), &logStruct)
if err != nil {
log.Printf("unmarshal hg log: %v", err)
return
@@ -510,14 +603,11 @@ func commitPoll(key string) {
if l.Parent == "" && i+1 < len(logs) {
l.Parent = logs[i+1].Hash
} else if l.Parent != "" {
- l.Parent, _ = fullHash(l.Parent)
+ l.Parent, _ = fullHash(pkgRoot, l.Parent)
}
- log.Printf("hg log: %s < %s\n", l.Hash, l.Parent)
- if l.Parent == "" {
- // Can't create node without parent.
- continue
+ if *verbose {
+ log.Printf("hg log %s: %s < %s\n", pkg, l.Hash, l.Parent)
}
-
if logByHash[l.Hash] == nil {
// Make copy to avoid pinning entire slice when only one entry is new.
t := *l
@@ -527,17 +617,14 @@ func commitPoll(key string) {
for i := range logs {
l := &logs[i]
- if l.Parent == "" {
- continue
- }
- addCommit(l.Hash, key)
+ addCommit(pkg, l.Hash, key)
}
}
// addCommit adds the commit with the named hash to the dashboard.
// key is the secret key for authentication to the dashboard.
// It avoids duplicate effort.
-func addCommit(hash, key string) bool {
+func addCommit(pkg, hash, key string) bool {
l := logByHash[hash]
if l == nil {
return false
@@ -547,7 +634,7 @@ func addCommit(hash, key string) bool {
}
// Check for already added, perhaps in an earlier run.
- if dashboardCommit(hash) {
+ if dashboardCommit(pkg, hash) {
log.Printf("%s already on dashboard\n", hash)
// Record that this hash is on the dashboard,
// as must be all its parents.
@@ -559,12 +646,14 @@ func addCommit(hash, key string) bool {
}
// Create parent first, to maintain some semblance of order.
- if !addCommit(l.Parent, key) {
- return false
+ if l.Parent != "" {
+ if !addCommit(pkg, l.Parent, key) {
+ return false
+ }
}
// Create commit.
- if err := postCommit(key, l); err != nil {
+ if err := postCommit(key, pkg, l); err != nil {
log.Printf("failed to add %s to dashboard: %v", key, err)
return false
}
@@ -572,13 +661,13 @@ func addCommit(hash, key string) bool {
}
// fullHash returns the full hash for the given Mercurial revision.
-func fullHash(rev string) (hash string, err os.Error) {
+func fullHash(root, rev string) (hash string, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("fullHash: %s: %s", rev, err)
}
}()
- s, _, err := runLog(nil, "", goroot,
+ s, _, err := runLog(nil, "", root,
"hg", "log",
"--encoding=utf-8",
"--rev="+rev,
@@ -601,7 +690,7 @@ func fullHash(rev string) (hash string, err os.Error) {
var revisionRe = regexp.MustCompile(`^([^ ]+) +[0-9]+:([0-9a-f]+)$`)
// firstTag returns the hash and tag of the most recent tag matching re.
-func firstTag(re *regexp.Regexp) (hash string, tag string, err os.Error) {
+func firstTag(re *regexp.Regexp) (hash string, tag string, err error) {
o, _, err := runLog(nil, "", goroot, "hg", "tags")
for _, l := range strings.Split(o, "\n") {
if l == "" {
@@ -609,16 +698,28 @@ func firstTag(re *regexp.Regexp) (hash string, tag string, err os.Error) {
}
s := revisionRe.FindStringSubmatch(l)
if s == nil {
- err = os.NewError("couldn't find revision number")
+ err = errors.New("couldn't find revision number")
return
}
if !re.MatchString(s[1]) {
continue
}
tag = s[1]
- hash, err = fullHash(s[2])
+ hash, err = fullHash(goroot, s[2])
return
}
- err = os.NewError("no matching tag found")
+ err = errors.New("no matching tag found")
return
}
+
+var repoRe = regexp.MustCompile(`^code\.google\.com/p/([a-z0-9\-]+(\.[a-z0-9\-]+)?)(/[a-z0-9A-Z_.\-/]+)?$`)
+
+// repoURL returns the repository URL for the supplied import path.
+func repoURL(importPath string) string {
+ m := repoRe.FindStringSubmatch(importPath)
+ if len(m) < 2 {
+ log.Printf("repoURL: couldn't decipher %q", importPath)
+ return ""
+ }
+ return "https://code.google.com/p/" + m[1]
+}
diff --git a/misc/dashboard/builder/package.go b/misc/dashboard/builder/package.go
index ebf4dd3c9..dcd449ab8 100644
--- a/misc/dashboard/builder/package.go
+++ b/misc/dashboard/builder/package.go
@@ -5,6 +5,7 @@
package main
import (
+ "errors"
"fmt"
"go/doc"
"go/parser"
@@ -17,7 +18,7 @@ import (
const MaxCommentLength = 500 // App Engine won't store more in a StringProperty.
-func (b *Builder) buildPackages(workpath string, hash string) os.Error {
+func (b *Builder) buildExternalPackages(workpath string, hash string) error {
logdir := filepath.Join(*buildroot, "log")
if err := os.Mkdir(logdir, 0755); err != nil {
return err
@@ -80,14 +81,14 @@ func (b *Builder) buildPackages(workpath string, hash string) os.Error {
return nil
}
-func isGoFile(fi *os.FileInfo) bool {
- return fi.IsRegular() && // exclude directories
- !strings.HasPrefix(fi.Name, ".") && // ignore .files
- !strings.HasSuffix(fi.Name, "_test.go") && // ignore tests
- filepath.Ext(fi.Name) == ".go"
+func isGoFile(fi os.FileInfo) bool {
+ return !fi.IsDir() && // exclude directories
+ !strings.HasPrefix(fi.Name(), ".") && // ignore .files
+ !strings.HasSuffix(fi.Name(), "_test.go") && // ignore tests
+ filepath.Ext(fi.Name()) == ".go"
}
-func packageComment(pkg, pkgpath string) (info string, err os.Error) {
+func packageComment(pkg, pkgpath string) (info string, err error) {
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, pkgpath, isGoFile, parser.PackageClauseOnly|parser.ParseComments)
if err != nil {
@@ -97,12 +98,12 @@ func packageComment(pkg, pkgpath string) (info string, err os.Error) {
if name == "main" {
continue
}
- pdoc := doc.NewPackageDoc(pkgs[name], pkg)
+ pdoc := doc.New(pkgs[name], pkg, doc.AllDecls)
if pdoc.Doc == "" {
continue
}
if info != "" {
- return "", os.NewError("multiple packages with docs")
+ return "", errors.New("multiple packages with docs")
}
info = pdoc.Doc
}
diff --git a/misc/dashboard/godashboard/app.yaml b/misc/dashboard/godashboard/app.yaml
index 215c16330..8c7670437 100644
--- a/misc/dashboard/godashboard/app.yaml
+++ b/misc/dashboard/godashboard/app.yaml
@@ -1,5 +1,5 @@
application: godashboard
-version: 8
+version: 9
runtime: python
api_version: 1
@@ -21,5 +21,6 @@ handlers:
- url: /project.*
script: package.py
-- url: /.*
- script: gobuild.py
+- url: /
+ static_files: main.html
+ upload: main.html
diff --git a/misc/dashboard/godashboard/fail-notify.txt b/misc/dashboard/godashboard/fail-notify.txt
index a699005ea..f75d09aa2 100644
--- a/misc/dashboard/godashboard/fail-notify.txt
+++ b/misc/dashboard/godashboard/fail-notify.txt
@@ -4,3 +4,6 @@ http://godashboard.appspot.com/log/{{loghash}}
{{desc}}
http://code.google.com/p/go/source/detail?r={{node}}
+
+$ tail -n 100 < log
+{{log}}
diff --git a/misc/dashboard/godashboard/gobuild.py b/misc/dashboard/godashboard/gobuild.py
deleted file mode 100644
index ae8d99b3f..000000000
--- a/misc/dashboard/godashboard/gobuild.py
+++ /dev/null
@@ -1,561 +0,0 @@
-# Copyright 2009 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.
-
-# This is the server part of the continuous build system for Go. It must be run
-# by AppEngine.
-
-from django.utils import simplejson
-from google.appengine.api import mail
-from google.appengine.api import memcache
-from google.appengine.ext import db
-from google.appengine.ext import webapp
-from google.appengine.ext.webapp import template
-from google.appengine.ext.webapp.util import run_wsgi_app
-import datetime
-import hashlib
-import logging
-import os
-import re
-import bz2
-
-# local imports
-from auth import auth
-import const
-
-# The majority of our state are commit objects. One of these exists for each of
-# the commits known to the build system. Their key names are of the form
-# <commit number (%08x)> "-" <hg hash>. This means that a sorting by the key
-# name is sufficient to order the commits.
-#
-# The commit numbers are purely local. They need not match up to the commit
-# numbers in an hg repo. When inserting a new commit, the parent commit must be
-# given and this is used to generate the new commit number. In order to create
-# the first Commit object, a special command (/init) is used.
-#
-# N.B. user is a StringProperty, so it must be type 'unicode'.
-# desc is a BlobProperty, so it must be type 'string'. [sic]
-class Commit(db.Model):
- num = db.IntegerProperty() # internal, monotonic counter.
- node = db.StringProperty() # Hg hash
- parentnode = db.StringProperty() # Hg hash
- user = db.StringProperty()
- date = db.DateTimeProperty()
- desc = db.BlobProperty()
-
- # This is the list of builds. Each element is a string of the form <builder
- # name> '`' <log hash>. If the log hash is empty, then the build was
- # successful.
- builds = db.StringListProperty()
-
- fail_notification_sent = db.BooleanProperty()
-
-# A CompressedLog contains the textual build log of a failed build.
-# The key name is the hex digest of the SHA256 hash of the contents.
-# The contents is bz2 compressed.
-class CompressedLog(db.Model):
- log = db.BlobProperty()
-
-N = 30
-
-def builderInfo(b):
- f = b.split('-', 3)
- goos = f[0]
- goarch = f[1]
- note = ""
- if len(f) > 2:
- note = f[2]
- return {'name': b, 'goos': goos, 'goarch': goarch, 'note': note}
-
-def builderset():
- q = Commit.all()
- q.order('-__key__')
- results = q.fetch(N)
- builders = set()
- for c in results:
- builders.update(set(parseBuild(build)['builder'] for build in c.builds))
- return builders
-
-class MainPage(webapp.RequestHandler):
- def get(self):
- self.response.headers['Content-Type'] = 'text/html; charset=utf-8'
-
- try:
- page = int(self.request.get('p', 1))
- if not page > 0:
- raise
- except:
- page = 1
-
- try:
- num = int(self.request.get('n', N))
- if num <= 0 or num > 200:
- raise
- except:
- num = N
-
- offset = (page-1) * num
-
- q = Commit.all()
- q.order('-__key__')
- results = q.fetch(num, offset)
-
- revs = [toRev(r) for r in results]
- builders = {}
-
- for r in revs:
- for b in r['builds']:
- builders[b['builder']] = builderInfo(b['builder'])
-
- for r in revs:
- have = set(x['builder'] for x in r['builds'])
- need = set(builders.keys()).difference(have)
- for n in need:
- r['builds'].append({'builder': n, 'log':'', 'ok': False})
- r['builds'].sort(cmp = byBuilder)
-
- builders = list(builders.items())
- builders.sort()
- values = {"revs": revs, "builders": [v for k,v in builders]}
-
- values['num'] = num
- values['prev'] = page - 1
- if len(results) == num:
- values['next'] = page + 1
-
- path = os.path.join(os.path.dirname(__file__), 'main.html')
- self.response.out.write(template.render(path, values))
-
-# A DashboardHandler is a webapp.RequestHandler but provides
-# authenticated_post - called by post after authenticating
-# json - writes object in json format to response output
-class DashboardHandler(webapp.RequestHandler):
- def post(self):
- if not auth(self.request):
- self.response.set_status(403)
- return
- self.authenticated_post()
-
- def authenticated_post(self):
- return
-
- def json(self, obj):
- self.response.set_status(200)
- simplejson.dump(obj, self.response.out)
- return
-
-# Todo serves /todo. It tells the builder which commits need to be built.
-class Todo(DashboardHandler):
- def get(self):
- builder = self.request.get('builder')
- key = 'todo-%s' % builder
- response = memcache.get(key)
- if response is None:
- # Fell out of memcache. Rebuild from datastore results.
- # We walk the commit list looking for nodes that have not
- # been built by this builder.
- q = Commit.all()
- q.order('-__key__')
- todo = []
- first = None
- for c in q.fetch(N+1):
- if first is None:
- first = c
- if not built(c, builder):
- todo.append({'Hash': c.node})
- response = simplejson.dumps(todo)
- memcache.set(key, response, 3600)
- self.response.set_status(200)
- self.response.out.write(response)
-
-def built(c, builder):
- for b in c.builds:
- if b.startswith(builder+'`'):
- return True
- return False
-
-# Log serves /log/. It retrieves log data by content hash.
-class LogHandler(DashboardHandler):
- def get(self):
- self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
- hash = self.request.path[5:]
- l = CompressedLog.get_by_key_name(hash)
- if l is None:
- self.response.set_status(404)
- return
- log = bz2.decompress(l.log)
- self.response.set_status(200)
- self.response.out.write(log)
-
-# Init creates the commit with id 0. Since this commit doesn't have a parent,
-# it cannot be created by Build.
-class Init(DashboardHandler):
- def authenticated_post(self):
- date = parseDate(self.request.get('date'))
- node = self.request.get('node')
- if not validNode(node) or date is None:
- logging.error("Not valid node ('%s') or bad date (%s %s)", node, date, self.request.get('date'))
- self.response.set_status(500)
- return
-
- commit = Commit(key_name = '00000000-%s' % node)
- commit.num = 0
- commit.node = node
- commit.parentnode = ''
- commit.user = self.request.get('user')
- commit.date = date
- commit.desc = self.request.get('desc').encode('utf8')
-
- commit.put()
-
- self.response.set_status(200)
-
-# The last commit when we switched to using entity groups.
-# This is the root of the new commit entity group.
-RootCommitKeyName = '00000f26-f32c6f1038207c55d5780231f7484f311020747e'
-
-# CommitHandler serves /commit.
-# A GET of /commit retrieves information about the specified commit.
-# A POST of /commit creates a node for the given commit.
-# If the commit already exists, the POST silently succeeds (like mkdir -p).
-class CommitHandler(DashboardHandler):
- def get(self):
- node = self.request.get('node')
- if not validNode(node):
- return self.json({'Status': 'FAIL', 'Error': 'malformed node hash'})
- n = nodeByHash(node)
- if n is None:
- return self.json({'Status': 'FAIL', 'Error': 'unknown revision'})
- return self.json({'Status': 'OK', 'Node': nodeObj(n)})
-
- def authenticated_post(self):
- # Require auth with the master key, not a per-builder key.
- if self.request.get('builder'):
- self.response.set_status(403)
- return
-
- node = self.request.get('node')
- date = parseDate(self.request.get('date'))
- user = self.request.get('user')
- desc = self.request.get('desc').encode('utf8')
- parenthash = self.request.get('parent')
-
- if not validNode(node) or not validNode(parenthash) or date is None:
- return self.json({'Status': 'FAIL', 'Error': 'malformed node, parent, or date'})
-
- n = nodeByHash(node)
- if n is None:
- p = nodeByHash(parenthash)
- if p is None:
- return self.json({'Status': 'FAIL', 'Error': 'unknown parent'})
-
- # Want to create new node in a transaction so that multiple
- # requests creating it do not collide and so that multiple requests
- # creating different nodes get different sequence numbers.
- # All queries within a transaction must include an ancestor,
- # but the original datastore objects we used for the dashboard
- # have no common ancestor. Instead, we use a well-known
- # root node - the last one before we switched to entity groups -
- # as the as the common ancestor.
- root = Commit.get_by_key_name(RootCommitKeyName)
-
- def add_commit():
- if nodeByHash(node, ancestor=root) is not None:
- return
-
- # Determine number for this commit.
- # Once we have created one new entry it will be lastRooted.num+1,
- # but the very first commit created in this scheme will have to use
- # last.num's number instead (last is likely not rooted).
- q = Commit.all()
- q.order('-__key__')
- q.ancestor(root)
- last = q.fetch(1)[0]
- num = last.num+1
-
- n = Commit(key_name = '%08x-%s' % (num, node), parent = root)
- n.num = num
- n.node = node
- n.parentnode = parenthash
- n.user = user
- n.date = date
- n.desc = desc
- n.put()
- db.run_in_transaction(add_commit)
- n = nodeByHash(node)
- if n is None:
- return self.json({'Status': 'FAIL', 'Error': 'failed to create commit node'})
-
- return self.json({'Status': 'OK', 'Node': nodeObj(n)})
-
-# Build serves /build.
-# A POST to /build records a new build result.
-class Build(webapp.RequestHandler):
- def post(self):
- if not auth(self.request):
- self.response.set_status(403)
- return
-
- builder = self.request.get('builder')
- log = self.request.get('log').encode('utf8')
-
- loghash = ''
- if len(log) > 0:
- loghash = hashlib.sha256(log).hexdigest()
- l = CompressedLog(key_name=loghash)
- l.log = bz2.compress(log)
- l.put()
-
- node = self.request.get('node')
- if not validNode(node):
- logging.error('Invalid node %s' % (node))
- self.response.set_status(500)
- return
-
- n = nodeByHash(node)
- if n is None:
- logging.error('Cannot find node %s' % (node))
- self.response.set_status(404)
- return
- nn = n
-
- def add_build():
- n = nodeByHash(node, ancestor=nn)
- if n is None:
- logging.error('Cannot find hash in add_build: %s %s' % (builder, node))
- return
-
- s = '%s`%s' % (builder, loghash)
- for i, b in enumerate(n.builds):
- if b.split('`', 1)[0] == builder:
- # logging.error('Found result for %s %s already' % (builder, node))
- n.builds[i] = s
- break
- else:
- # logging.error('Added result for %s %s' % (builder, node))
- n.builds.append(s)
- n.put()
-
- db.run_in_transaction(add_build)
-
- key = 'todo-%s' % builder
- memcache.delete(key)
-
- c = getBrokenCommit(node, builder)
- if c is not None and not c.fail_notification_sent:
- notifyBroken(c, builder)
-
- self.response.set_status(200)
-
-
-def getBrokenCommit(node, builder):
- """
- getBrokenCommit returns a Commit that breaks the build.
- The Commit will be either the one specified by node or the one after.
- """
-
- # Squelch mail if already fixed.
- head = firstResult(builder)
- if broken(head, builder) == False:
- return
-
- # Get current node and node before, after.
- cur = nodeByHash(node)
- if cur is None:
- return
- before = nodeBefore(cur)
- after = nodeAfter(cur)
-
- if broken(before, builder) == False and broken(cur, builder):
- return cur
- if broken(cur, builder) == False and broken(after, builder):
- return after
-
- return
-
-def firstResult(builder):
- q = Commit.all().order('-__key__')
- for c in q.fetch(20):
- for i, b in enumerate(c.builds):
- p = b.split('`', 1)
- if p[0] == builder:
- return c
- return None
-
-def nodeBefore(c):
- return nodeByHash(c.parentnode)
-
-def nodeAfter(c):
- return Commit.all().filter('parenthash', c.node).get()
-
-def notifyBroken(c, builder):
- def send():
- n = Commit.get(c.key())
- if n is None:
- logging.error("couldn't retrieve Commit '%s'" % c.key())
- return False
- if n.fail_notification_sent:
- return False
- n.fail_notification_sent = True
- return n.put()
- if not db.run_in_transaction(send):
- return
-
- subject = const.mail_fail_subject % (builder, c.desc.split('\n')[0])
- path = os.path.join(os.path.dirname(__file__), 'fail-notify.txt')
- body = template.render(path, {
- "builder": builder,
- "node": c.node,
- "user": c.user,
- "desc": c.desc,
- "loghash": logHash(c, builder)
- })
- mail.send_mail(
- sender=const.mail_from,
- to=const.mail_fail_to,
- subject=subject,
- body=body
- )
-
-def logHash(c, builder):
- for i, b in enumerate(c.builds):
- p = b.split('`', 1)
- if p[0] == builder:
- return p[1]
- return ""
-
-def broken(c, builder):
- """
- broken returns True if commit c breaks the build for the specified builder,
- False if it is a good build, and None if no results exist for this builder.
- """
- if c is None:
- return None
- for i, b in enumerate(c.builds):
- p = b.split('`', 1)
- if p[0] == builder:
- return len(p[1]) > 0
- return None
-
-def node(num):
- q = Commit.all()
- q.filter('num =', num)
- n = q.get()
- return n
-
-def nodeByHash(hash, ancestor=None):
- q = Commit.all()
- q.filter('node =', hash)
- if ancestor is not None:
- q.ancestor(ancestor)
- n = q.get()
- return n
-
-# nodeObj returns a JSON object (ready to be passed to simplejson.dump) describing node.
-def nodeObj(n):
- return {
- 'Hash': n.node,
- 'ParentHash': n.parentnode,
- 'User': n.user,
- 'Date': n.date.strftime('%Y-%m-%d %H:%M %z'),
- 'Desc': n.desc,
- }
-
-class FixedOffset(datetime.tzinfo):
- """Fixed offset in minutes east from UTC."""
-
- def __init__(self, offset):
- self.__offset = datetime.timedelta(seconds = offset)
-
- def utcoffset(self, dt):
- return self.__offset
-
- def tzname(self, dt):
- return None
-
- def dst(self, dt):
- return datetime.timedelta(0)
-
-def validNode(node):
- if len(node) != 40:
- return False
- for x in node:
- o = ord(x)
- if (o < ord('0') or o > ord('9')) and (o < ord('a') or o > ord('f')):
- return False
- return True
-
-def parseDate(date):
- if '-' in date:
- (a, offset) = date.split('-', 1)
- try:
- return datetime.datetime.fromtimestamp(float(a), FixedOffset(0-int(offset)))
- except ValueError:
- return None
- if '+' in date:
- (a, offset) = date.split('+', 1)
- try:
- return datetime.datetime.fromtimestamp(float(a), FixedOffset(int(offset)))
- except ValueError:
- return None
- try:
- return datetime.datetime.utcfromtimestamp(float(date))
- except ValueError:
- return None
-
-email_re = re.compile('^[^<]+<([^>]*)>$')
-
-def toUsername(user):
- r = email_re.match(user)
- if r is None:
- return user
- email = r.groups()[0]
- return email.replace('@golang.org', '')
-
-def dateToShortStr(d):
- return d.strftime('%a %b %d %H:%M')
-
-def parseBuild(build):
- [builder, logblob] = build.split('`')
- return {'builder': builder, 'log': logblob, 'ok': len(logblob) == 0}
-
-def nodeInfo(c):
- return {
- "node": c.node,
- "user": toUsername(c.user),
- "date": dateToShortStr(c.date),
- "desc": c.desc,
- "shortdesc": c.desc.split('\n', 2)[0]
- }
-
-def toRev(c):
- b = nodeInfo(c)
- b['builds'] = [parseBuild(build) for build in c.builds]
- return b
-
-def byBuilder(x, y):
- return cmp(x['builder'], y['builder'])
-
-# Give old builders work; otherwise they pound on the web site.
-class Hwget(DashboardHandler):
- def get(self):
- self.response.out.write("8000\n")
-
-# This is the URL map for the server. The first three entries are public, the
-# rest are only used by the builders.
-application = webapp.WSGIApplication(
- [('/', MainPage),
- ('/hw-get', Hwget),
- ('/log/.*', LogHandler),
- ('/commit', CommitHandler),
- ('/init', Init),
- ('/todo', Todo),
- ('/build', Build),
- ], debug=True)
-
-def main():
- run_wsgi_app(application)
-
-if __name__ == "__main__":
- main()
-
diff --git a/misc/dashboard/godashboard/main.html b/misc/dashboard/godashboard/main.html
index 5390afce6..4cd98d851 100644
--- a/misc/dashboard/godashboard/main.html
+++ b/misc/dashboard/godashboard/main.html
@@ -6,8 +6,6 @@
</head>
<body>
- <a id="top"></a>
-
<ul class="menu">
<li>Build Status</li>
<li><a href="/package">Packages</a></li>
@@ -18,45 +16,8 @@
<h1>Go Dashboard</h1>
<h2>Build Status</h2>
- <table class="alternate" cellpadding="0" cellspacing="0">
- <tr>
- <th></th>
- {% for b in builders %}
- <th class="builder">{{b.goos}}<br>{{b.goarch}}<br>{{b.note}}</th>
- {% endfor %}
- <th></th>
- <th></th>
- <th></th>
- </tr>
-
- {% for r in revs %}
- <tr>
- <td class="revision"><span class="hash"><a href="https://code.google.com/p/go/source/detail?r={{r.node}}">{{r.node|slice:":12"}}</a></span></td>
- {% for b in r.builds %}
- <td class="result">
- {% if b.ok %}
- <span class="ok">ok</span>
- {% else %}
- {% if b.log %}
- <a class="fail" href="/log/{{b.log}}">fail</a>
- {% else %}
- &nbsp;
- {% endif %}
- {% endif %}
- </td>
- {% endfor %}
+ <p class="notice">The build status dashboard has moved to <a href="http://build.golang.org">build.golang.org</a>.</p>
- <td class="user">{{r.user|escape}}</td>
- <td class="date">{{r.date|escape}}</td>
- <td class="desc">{{r.shortdesc|escape}}</td>
- </tr>
- {% endfor %}
- </table>
- <div class="paginate">
- <a{% if prev %} href="?n={{num}}&p={{prev}}"{% else %} class="inactive"{% endif %}>prev</a>
- <a{% if next %} href="?n={{num}}&p={{next}}"{% else %} class="inactive"{% endif %}>next</a>
- <a{% if prev %} href="?n={{num}}&p=1"{% else %} class="inactive"{% endif %}>top</a>
- </div>
</body>
</html>
diff --git a/misc/dashboard/godashboard/package.html b/misc/dashboard/godashboard/package.html
index 8a9d0a3a0..b688af9e2 100644
--- a/misc/dashboard/godashboard/package.html
+++ b/misc/dashboard/godashboard/package.html
@@ -20,12 +20,14 @@
may or may not build or be safe to use.
</p>
+<!--
<p>
An "ok" in the <b>build</b> column indicates that the package is
<a href="http://golang.org/cmd/goinstall/">goinstallable</a>
with the latest
<a href="http://golang.org/doc/devel/release.html">release</a> of Go.
</p>
+-->
<p>
The <b>info</b> column shows the first paragraph from the
@@ -39,7 +41,7 @@
<tr>
<td class="time">{{r.last_install|date:"Y-M-d H:i"}}</td>
<td class="count">{{r.week_count}}</td>
- <td class="ok">{% if r.ok %}<a title="{{r.last_ok|date:"Y-M-d H:i"}}">ok</a>{% else %}&nbsp;{% endif %}</td>
+<!-- <td class="ok">{% if r.ok %}<a title="{{r.last_ok|date:"Y-M-d H:i"}}">ok</a>{% else %}&nbsp;{% endif %}</td> -->
<td class="path"><a href="{{r.web_url}}">{{r.path}}</a></td>
<td class="info">{% if r.info %}{{r.info|escape}}{% endif %}</td>
</tr>
@@ -53,7 +55,7 @@
<tr>
<td class="time">{{r.last_install|date:"Y-M-d H:i"}}</td>
<td class="count">{{r.count}}</td>
- <td class="ok">{% if r.ok %}<a title="{{r.last_ok|date:"Y-M-d H:i"}}">ok</a>{% else %}&nbsp;{% endif %}</td>
+<!-- <td class="ok">{% if r.ok %}<a title="{{r.last_ok|date:"Y-M-d H:i"}}">ok</a>{% else %}&nbsp;{% endif %}</td> -->
<td class="path"><a href="{{r.web_url}}">{{r.path}}</a></td>
<td class="info">{% if r.info %}{{r.info|escape}}{% endif %}</td>
</tr>
@@ -67,7 +69,7 @@
<tr>
<td class="time">{{r.last_install|date:"Y-M-d H:i"}}</td>
<td class="count">{{r.count}}</td>
- <td class="ok">{% if r.ok %}<a title="{{r.last_ok|date:"Y-M-d H:i"}}">ok</a>{% else %}&nbsp;{% endif %}</td>
+<!-- <td class="ok">{% if r.ok %}<a title="{{r.last_ok|date:"Y-M-d H:i"}}">ok</a>{% else %}&nbsp;{% endif %}</td> -->
<td class="path"><a href="{{r.web_url}}">{{r.path}}</a></td>
<td class="info">{% if r.info %}{{r.info|escape}}{% endif %}</td>
</tr>
diff --git a/misc/dashboard/godashboard/static/style.css b/misc/dashboard/godashboard/static/style.css
index d6d23b536..0ce583a54 100644
--- a/misc/dashboard/godashboard/static/style.css
+++ b/misc/dashboard/godashboard/static/style.css
@@ -116,3 +116,14 @@ div.paginate a.inactive {
td.time {
font-family: monospace;
}
+.notice {
+ padding: 10px;
+ margin: 10px;
+ border: 2px solid #FF6;
+ background: #900;
+ color: white;
+ text-align: center;
+}
+.notice a {
+ color: #FF6;
+}