diff --git a/cmd/relui/README.md b/cmd/relui/README.md new file mode 100644 index 0000000000..eb1b5114f4 --- /dev/null +++ b/cmd/relui/README.md @@ -0,0 +1,12 @@ +#golang.org/x/build/cmd/relui + +``` + ▀▀█ ▀ + ▄ ▄▄ ▄▄▄ █ ▄ ▄ ▄▄▄ + █▀ ▀ █▀ █ █ █ █ █ + █ █▀▀▀▀ █ █ █ █ + █ ▀█▄▄▀ ▀▄▄ ▀▄▄▀█ ▄▄█▄▄ +``` + +relui is a web interface for managing the release process of Go. + diff --git a/cmd/relui/index.html b/cmd/relui/index.html new file mode 100644 index 0000000000..0c7de905eb --- /dev/null +++ b/cmd/relui/index.html @@ -0,0 +1,35 @@ + + + + Go Releases + + + + +
+
+

Workflows

+ +
+
+ + diff --git a/cmd/relui/main.go b/cmd/relui/main.go new file mode 100644 index 0000000000..57e52bb79d --- /dev/null +++ b/cmd/relui/main.go @@ -0,0 +1,22 @@ +// Copyright 2020 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 main + +import ( + "log" + "net/http" + "os" +) + +func main() { + http.Handle("/", fileServerHandler(relativeFile("./static"), http.HandlerFunc(homeHandler))) + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + log.Printf("Listening on :" + port) + log.Fatal(http.ListenAndServe(":"+port, http.DefaultServeMux)) +} diff --git a/cmd/relui/static/styles.css b/cmd/relui/static/styles.css new file mode 100644 index 0000000000..3d6217a3b9 --- /dev/null +++ b/cmd/relui/static/styles.css @@ -0,0 +1,53 @@ +/* + * Copyright 2020 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. + */ +*, +:before, +:after { + box-sizing: border-box; +} +html, +.Site { + height: 100%; +} +.Site { + display: flex; + flex-direction: column; + font-family: sans-serif; + margin: 0; +} +h1, +h2 { + font-weight: 600; + letter-spacing: 0.03rem; +} + +h3, +h4 { + font-weight: 600; + letter-spacing: 0.08rem; +} +h5, +h6 { + font-weight: 500; + letter-spacing: 0.08rem; +} +.Site-content { + flex: 1 0 auto; + padding: 0.625rem; + width: 100%; +} +.Site-header { + flex: none; +} +.Header { + background: #e0ebf5; + color: #375eab; + padding: 0.625rem; +} +.Header-title { + font-size: 1.5rem; + margin: 0; +} diff --git a/cmd/relui/testing/test.css b/cmd/relui/testing/test.css new file mode 100644 index 0000000000..ecd36b5e48 --- /dev/null +++ b/cmd/relui/testing/test.css @@ -0,0 +1 @@ +.Header { font-size: 10rem; } diff --git a/cmd/relui/web.go b/cmd/relui/web.go new file mode 100644 index 0000000000..40cd773bf8 --- /dev/null +++ b/cmd/relui/web.go @@ -0,0 +1,54 @@ +// Copyright 2020 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 main + +import ( + "html/template" + "log" + "mime" + "net/http" + "os" + "path" + "path/filepath" +) + +func fileServerHandler(root string, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + next.ServeHTTP(w, r) + return + } + if _, err := os.Stat(path.Join(root, r.URL.Path)); os.IsNotExist(err) { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(r.URL.Path))) + w.Header().Set("Cache-Control", "no-cache, private, max-age=0") + + fs := http.FileServer(http.Dir(root)) + fs.ServeHTTP(w, r) + }) +} + +var homeTemplate = template.Must(template.ParseFiles(relativeFile("index.html"))) + +func homeHandler(w http.ResponseWriter, _ *http.Request) { + if err := homeTemplate.Execute(w, nil); err != nil { + log.Printf("homeHandlerFunc: %v", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } +} + +// relativeFile returns the path to the provided file or directory, +// conditionally prepending a relative path depending on the environment. +// +// In tests the current directory is ".", but the command may be running from the module root. +func relativeFile(base string) string { + // Check to see if it is in "." first. + if _, err := os.Stat(base); err == nil { + return base + } + return filepath.Join("cmd/relui", base) +} diff --git a/cmd/relui/web_test.go b/cmd/relui/web_test.go new file mode 100644 index 0000000000..1649e2d13f --- /dev/null +++ b/cmd/relui/web_test.go @@ -0,0 +1,87 @@ +// Copyright 2020 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 main + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHomeHandler(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + + homeHandler(w, req) + resp := w.Result() + + if resp.StatusCode != http.StatusOK { + t.Errorf("rep.StatusCode = %d, wanted %d", resp.StatusCode, http.StatusOK) + } +} + +func TestFileServerHandler(t *testing.T) { + h := fileServerHandler("./testing", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Home")) + })) + + cases := []struct { + desc string + path string + wantCode int + wantBody string + wantHeaders map[string]string + }{ + { + desc: "fallback to next handler", + path: "/", + wantCode: http.StatusOK, + wantBody: "Home", + }, + { + desc: "sets headers and returns file", + path: "/test.css", + wantCode: http.StatusOK, + wantBody: ".Header { font-size: 10rem; }\n", + wantHeaders: map[string]string{ + "Content-Type": "text/css; charset=utf-8", + "Cache-Control": "no-cache, private, max-age=0", + }, + }, + { + desc: "handles missing file", + path: "/foo.js", + wantCode: http.StatusNotFound, + wantBody: "404 page not found\n", + }, + } + for _, c := range cases { + t.Run(c.desc, func(t *testing.T) { + req := httptest.NewRequest("GET", c.path, nil) + w := httptest.NewRecorder() + + h.ServeHTTP(w, req) + resp := w.Result() + defer resp.Body.Close() + + if resp.StatusCode != c.wantCode { + t.Errorf("rep.StatusCode = %d, wanted %d", resp.StatusCode, c.wantCode) + } + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("resp.Body = _, %v, wanted no error", err) + } + if string(b) != c.wantBody { + t.Errorf("resp.Body = %q, %v, wanted %q, %v", b, err, c.wantBody, nil) + } + for k, v := range c.wantHeaders { + if resp.Header.Get(k) != v { + t.Errorf("resp.Header.Get(%q) = %q, wanted %q", k, resp.Header.Get(k), v) + } + } + }) + } +}