23 Commits
v0.2 ... v1.1

Author SHA1 Message Date
54104e61cf Added / stole some code to handle cmd execs better
It'll now use any custom environment path to lookup executables to run
as part of the project scripts
2016-04-22 21:25:33 +00:00
9891301ca4 Added better error handling.
Moved main project list columns around
2016-04-22 20:39:06 +00:00
77f2f8c5aa Build script change 2016-04-21 13:41:21 +00:00
5f776e0757 Updated build script 2016-04-20 16:42:16 +00:00
20084b9429 Post trigger test 2016-04-20 16:22:42 +00:00
b5bfd93cff Fixed issue with environment not getting set in projects 2016-04-20 16:15:53 +00:00
ead3e6ebf0 Updated build script, and addded environment option for projects 2016-04-20 16:05:59 +00:00
6e482121ed Fixed type in build script 2016-04-20 15:44:03 +00:00
f73a57d13b Build script change 2016-04-20 15:42:26 +00:00
6d7cee5f48 Added option to pass in working directory into scripts 2016-04-20 15:40:07 +00:00
e568177ea6 Working on build script 2016-04-20 15:07:58 +00:00
6d74144a3b Updated import paths to new townsourced organization 2016-04-19 19:52:55 +00:00
7f94b454a7 work on build scripts 2016-04-19 19:26:35 +00:00
e17ec20f79 Ran final bindata build, added build script 2016-04-19 15:47:47 +00:00
70c9f60d8f Everything is basically done.
It's really crude, and ugly, but it works, it's simple.
2016-04-18 21:42:21 +00:00
a754264a1d Fixed issue with retrieving release files 2016-04-18 20:31:22 +00:00
7082d69bab Finished most of the front end stuff.
Need to fix release file downloads
2016-04-18 20:11:22 +00:00
e0bc90e954 More front end work, stages and log entries 2016-04-18 19:45:16 +00:00
5f26454adf Worked on web frontend for project versions list 2016-04-17 21:14:04 -05:00
d117c3e664 More frontend work.
Added project data to version list.

Fleshing out project page
2016-04-17 20:43:27 -05:00
ae961e9dd1 More web work
rearranged how web files were loaded a bit

Started on breadcrumb handling
2016-04-15 21:57:59 +00:00
7ca04a5594 Web front end work 2016-04-14 16:29:56 +00:00
a1ced419c0 Started on web front end 2016-04-13 21:59:28 +00:00
15 changed files with 930 additions and 113 deletions

View File

@ -17,8 +17,21 @@ You'll setup a project which will need the following information:
5. Path to the release file
6. Script to set release name / version
An optional set of environment strings can be set to define the environment in which the scripts run.
```
"environment": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/local/go/bin",
"GOPATH=@dir"
]
```
Projects will be defined in a project.json file for now. I may add a web interface later.
@dir in any of the script strings or environment entries will be replaced with an absolute path to the current working directory of the specific version being worked on.
```
sh ./build.sh @dir
```
Ironsmith will take the information for the defined project above and do the following
@ -26,7 +39,8 @@ Ironsmith will take the information for the defined project above and do the fol
2. Change to that directory
2. Create a bolt DB file for the project to keep a log of all the builds
3. Run an initial pull of the repository using the pull script
4. If pull succeeds, Run the Build Scripts
4. Run version script
4. If pull is a new version, then Run the Build Scripts
5. If build succeeds, run the test scripts
6. If test succeeds, run the release scripts
7. Load the release file into project release folder with the release name

File diff suppressed because one or more lines are too long

3
build.sh Normal file
View File

@ -0,0 +1,3 @@
#!/bin/bash
go-bindata web/... && go build -a -v -o ironsmith

View File

@ -7,6 +7,7 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
@ -14,7 +15,7 @@ import (
"strings"
"time"
"git.townsourced.com/ironsmith/datastore"
"git.townsourced.com/townsourced/ironsmith/datastore"
)
/*
@ -93,13 +94,13 @@ func (p *Project) fetch() {
}
//fetch project
fetchResult, err := runCmd(p.Fetch, tempDir)
fetchResult, err := runCmd(p.Fetch, tempDir, p.Environment)
if p.errHandled(err) {
return
}
// fetched succesfully, determine version
version, err := runCmd(p.Version, tempDir)
version, err := runCmd(p.Version, tempDir, p.Environment)
if p.errHandled(err) {
return
@ -107,12 +108,13 @@ func (p *Project) fetch() {
p.setVersion(strings.TrimSpace(string(version)))
lVer, err := p.ds.LastVersion("")
// check if this specific version has attempted a build yet
lVer, err := p.ds.LastVersion(stageBuild)
if err != datastore.ErrNotFound && p.errHandled(err) {
return
}
if p.version == "" || p.version == lVer {
if p.version == "" || p.version == lVer.Version {
// no new build clean up temp dir
p.errHandled(os.RemoveAll(tempDir))
@ -150,7 +152,7 @@ func (p *Project) build() {
return
}
output, err := runCmd(p.Build, p.workingDir())
output, err := runCmd(p.Build, p.workingDir(), p.Environment)
if p.errHandled(err) {
return
@ -171,7 +173,7 @@ func (p *Project) test() {
if p.Test == "" {
return
}
output, err := runCmd(p.Test, p.workingDir())
output, err := runCmd(p.Test, p.workingDir(), p.Environment)
if p.errHandled(err) {
return
@ -193,7 +195,7 @@ func (p *Project) release() {
return
}
output, err := runCmd(p.Release, p.workingDir())
output, err := runCmd(p.Release, p.workingDir(), p.Environment)
if p.errHandled(err) {
return
@ -218,8 +220,15 @@ func (p *Project) release() {
return
}
p.setStage(stageReleased)
if p.errHandled(p.ds.AddLog(p.version, p.stage,
fmt.Sprintf("Project %s Version %s built, tested, and released successfully.\n", p.id(), p.version))) {
return
}
//build successfull, remove working dir
p.errHandled(os.RemoveAll(p.workingDir()))
vlog("Project: %s Version %s built, tested, and released successfully.\n", p.id(), p.version)
vlog("Project %s Version %s built, tested, and released successfully.\n", p.id(), p.version)
}

View File

@ -6,10 +6,8 @@
package datastore
import (
"bytes"
"encoding/json"
"errors"
"io"
"time"
"github.com/boltdb/bolt"
@ -77,15 +75,6 @@ func (ds *Store) get(bucket string, key []byte, result interface{}) error {
return ErrNotFound
}
if value, ok := result.([]byte); ok {
buff := bytes.NewBuffer(value)
_, err := io.Copy(buff, bytes.NewReader(dsValue))
if err != nil {
return err
}
return nil
}
return json.Unmarshal(dsValue, result)
})
}

View File

@ -37,8 +37,8 @@ func (ds *Store) AddLog(version, stage, entry string) error {
// LastVersion returns the last version in the log for the given stage. If stage is blank,
// then it returns the last of any stage
func (ds *Store) LastVersion(stage string) (string, error) {
version := ""
func (ds *Store) LastVersion(stage string) (*Log, error) {
last := &Log{}
err := ds.bolt.View(func(tx *bolt.Tx) error {
c := tx.Bucket([]byte(bucketLog)).Cursor()
@ -52,7 +52,7 @@ func (ds *Store) LastVersion(stage string) (string, error) {
if l.Version != "" {
if stage == "" || l.Stage == stage {
version = l.Version
last = l
return nil
}
}
@ -62,10 +62,10 @@ func (ds *Store) LastVersion(stage string) (string, error) {
})
if err != nil {
return "", err
return nil, err
}
return version, nil
return last, nil
}
// Versions lists the versions in a given project, including the last stage that version got to
@ -86,7 +86,6 @@ func (ds *Store) Versions() ([]*Log, error) {
// capture the newest entry for each version
if l.Version != current {
l.Log = "" // only care about date, ver and stage
vers = append(vers, l)
current = l.Version
}

View File

@ -5,7 +5,9 @@
package datastore
import (
"bytes"
"encoding/json"
"io"
"time"
"github.com/boltdb/bolt"
@ -52,13 +54,27 @@ func (ds *Store) AddRelease(version, fileName string, fileData []byte) error {
// ReleaseFile returns a specific file from a release for the given file key
func (ds *Store) ReleaseFile(fileKey TimeKey) ([]byte, error) {
var fileData []byte
err := ds.get(bucketFiles, fileKey.Bytes(), fileData)
var fileData bytes.Buffer
err := ds.bolt.View(func(tx *bolt.Tx) error {
dsValue := tx.Bucket([]byte(bucketFiles)).Get(fileKey.Bytes())
if dsValue == nil {
return ErrNotFound
}
_, err := io.Copy(&fileData, bytes.NewReader(dsValue))
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
return fileData, nil
return fileData.Bytes(), nil
}
// Release gets the release record for a specific version

View File

@ -11,7 +11,7 @@ import (
"log"
"net/http"
"git.townsourced.com/ironsmith/datastore"
"git.townsourced.com/townsourced/ironsmith/datastore"
)
const (

75
exec.go
View File

@ -6,12 +6,18 @@ package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
func runCmd(cmd, dir string) ([]byte, error) {
s := strings.Fields(cmd)
func runCmd(cmd, dir string, env []string) ([]byte, error) {
s := strings.Fields(strings.Replace(cmd, "@dir", dir, -1))
for i := range env {
env[i] = strings.Replace(env[i], "@dir", dir, -1)
}
var args []string
@ -19,15 +25,74 @@ func runCmd(cmd, dir string) ([]byte, error) {
args = s[1:]
}
ec := exec.Command(s[0], args...)
name := s[0]
ec := &exec.Cmd{
Path: name,
Args: append([]string{name}, args...),
Dir: dir,
Env: env,
}
ec.Dir = dir
if filepath.Base(name) == name {
lp, err := lookPath(name, env)
if err != nil {
return nil, err
}
ec.Path = lp
}
vlog("Executing command: %s in dir %s\n", cmd, dir)
result, err := ec.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("%s:\n%s", err, result)
return nil, fmt.Errorf("%s\n%s", err, result)
}
return result, nil
}
// similar to os/exec.LookPath, except it checks if the passed in
// custom environment includes a path definitions and uses that path instead
// note this probably only works on unix, that's all I care about for now
func lookPath(file string, env []string) (string, error) {
if strings.Contains(file, "/") {
err := findExecutable(file)
if err == nil {
return file, nil
}
return "", &exec.Error{file, err}
}
for i := range env {
if strings.HasPrefix(env[i], "PATH=") {
pathenv := env[i][5:]
if pathenv == "" {
return "", &exec.Error{file, exec.ErrNotFound}
}
for _, dir := range strings.Split(pathenv, ":") {
if dir == "" {
// Unix shell semantics: path element "" means "."
dir = "."
}
path := dir + "/" + file
if err := findExecutable(path); err == nil {
return path, nil
}
}
return "", &exec.Error{file, exec.ErrNotFound}
}
}
return exec.LookPath(file)
}
func findExecutable(file string) error {
d, err := os.Stat(file)
if err != nil {
return err
}
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
return nil
}
return os.ErrPermission
}

View File

@ -11,7 +11,7 @@ import (
"os/signal"
"path/filepath"
"git.townsourced.com/config"
"git.townsourced.com/townsourced/config"
)
//settings

View File

@ -15,7 +15,7 @@ import (
"sync"
"time"
"git.townsourced.com/ironsmith/datastore"
"git.townsourced.com/townsourced/ironsmith/datastore"
)
const (
@ -25,12 +25,13 @@ const (
//stages
const (
stageLoad = "loading"
stageFetch = "fetching"
stageBuild = "building"
stageTest = "testing"
stageRelease = "releasing"
stageWait = "waiting"
stageLoad = "loading"
stageFetch = "fetching"
stageBuild = "building"
stageTest = "testing"
stageRelease = "releasing"
stageReleased = "released"
stageWait = "waiting"
)
const projectFilePoll = 30 * time.Second
@ -47,6 +48,8 @@ The project lifecycle goes like this, each step calling the next if successful
type Project struct {
Name string `json:"name"` // name of the project
Environment []string `json:"environment"` // Environment for each of the scripts below, if empty will use the current processes environment
Fetch string `json:"fetch"` //Script to fetch the latest project code into the current directory
Build string `json:"build"` //Script to build the latest project code
Test string `json:"test"` //Script to test the latest project code
@ -185,11 +188,11 @@ func (p *Project) setStage(stage string) {
}
type webProject struct {
ID string `json:"id"`
Name string `json:"name"`
ReleaseVersion string `json:"releaseVersion"` //last successfully released version
LastVersion string `json:"lastVersion"` //last version success or otherwise
Stage string `json:"stage"` // current stage
ID string `json:"id"`
Name string `json:"name"`
ReleaseVersion string `json:"releaseVersion"` //last successfully released version
Stage string `json:"stage"` // current stage
LastLog *datastore.Log `json:"lastLog"`
}
func (p *Project) webData() (*webProject, error) {
@ -209,9 +212,9 @@ func (p *Project) webData() (*webProject, error) {
d := &webProject{
Name: p.Name,
ID: p.id(),
LastVersion: last,
ReleaseVersion: release,
ReleaseVersion: release.Version,
Stage: p.stage,
LastLog: last,
}
return d, nil
@ -272,6 +275,7 @@ func (p *Project) setData(new *Project) {
defer p.Unlock()
p.Name = new.Name
p.Environment = new.Environment
p.Fetch = new.Fetch
p.Build = new.Build

View File

@ -103,6 +103,14 @@ func routes() {
get: rootGet,
})
webRoot.Handle("/js/", &methodHandler{
get: assetGet,
})
webRoot.Handle("/css/", &methodHandler{
get: assetGet,
})
webRoot.Handle("/log/", &methodHandler{
get: logGet,
})
@ -118,12 +126,11 @@ func routes() {
}
func rootGet(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
//send index.html
serveAsset(w, r, "web/index.html")
return
}
//send index.html
serveAsset(w, r, "web/index.html")
}
func assetGet(w http.ResponseWriter, r *http.Request) {
serveAsset(w, r, path.Join("web", r.URL.Path))
}

View File

@ -10,12 +10,255 @@
<link rel="stylesheet" href="/css/pure-min.css">
<style>
.container {
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 768px) {
.container {
padding-right: 40px;
padding-left: 40px;
}
}
.center-block {
display: block;
margin-left: auto;
margin-right: auto;
}
.text-center {
text-align: center;
}
/*tables*/
.table-responsive {
margin-left: auto;
margin-right: auto;
overflow-x: auto;
}
.table-responsive table {
width: 100%;
max-width: 100%;
}
/* error */
.error {
display: inline-block;
background-color: red;
color: white;
border-radius: 4px;
padding: .5em 1em;
margin: 10px;
}
/* breadcrumbs */
#breadcrumbs {
margin-bottom: 10px;
}
.breadcrumb-separator {
color: #ccc;
font-weight: bold;
font-size: 2em;
}
.pull-left {
float: left;
}
.pull-right {
float: right;
}
.timestamp {
font-size: .75em;
color: #777;
margin-left: 10px;
}
.log {
margin-left: 15px;
margin-right: 15px;
}
.log > pre {
margin-left: 15px;
}
</style>
</head>
<body>
<script id="tMain" type="text/ractive">
<div class="container pure-g">
<div class="pure-u-1">
<h3 class="text-center">Iron Smith</h3>
{{#if error}}
<div class="text-center">
<span class="error">{{error}}</span>
</div>
{{/if}}
<div id="breadcrumbs" class="pure-menu pure-menu-horizontal text-center">
<ul class="pure-menu-list">
<li class="pure-menu-item">
<a href="/" class="pure-menu-link">Project List</a>
</li>
{{#if project}}
<li class="pure-menu-item">
<span class="breadcrumb-separator">/</span>
</li>
{{#if !version && !currentStage}}
<li class="pure-menu-item pure-menu-has-children" decorator="menu">
<a href="#" id="projectMenu" class="pure-menu-link">{{project.name}}</a>
<ul class="pure-menu-children">
<li class="pure-menu-item">
<a href="#" class="pure-menu-link" on-click="triggerBuild">Trigger Build</a>
</li>
</ul>
</li>
{{else}}
<li class="pure-menu-item">
<a href="/project/{{project.id}}" class="pure-menu-link">{{project.name}}</a>
</li>
{{/if}}
{{/if}}
{{#if project && version}}
<li class="pure-menu-item">
<span class="breadcrumb-separator">/</span>
</li>
<li class="pure-menu-item">
<a href="/project/{{project.id}}/{{version}}" class="pure-menu-link">{{version}}</a>
</li>
{{/if}}
{{#if project && version && currentStage}}
<li class="pure-menu-item">
<span class="breadcrumb-separator">/</span>
</li>
<li class="pure-menu-item">
<a href="/project/{{project.id}}/{{version}}/{{currentStage}}" class="pure-menu-link">{{currentStage}}</a>
</li>
{{/if}}
</ul>
</div>
{{#if !project}}
{{>projects}}
{{elseif !version}}
{{>project}}
{{else}}
{{>version}}
{{/if}}
</div>
</div>
{{#partial projects}}
<div class="table-responsive">
<table class="pure-table pure-table-striped">
<thead>
<tr>
<th>Project</th>
<th>Status</th>
<th>Last Version</th>
<th>Last Log</th>
<th>Last Release</th>
<th>Last Release File</th>
</tr>
</thead>
<tbody>
{{#projects:i}}
<tr title="{{formatDate(.lastLog.when)}}">
<td><a href="/project/{{.id}}/">{{.name}}</a></td>
<td>{{.status}}</td>
<td>
<a href="/project/{{.id}}/{{.lastLog.version}}">{{.lastLog.version}}</a>
</td>
<td title="{{.lastLog.log}}">{{#if .lastLog.log}}{{.lastLog.log.substring(0,100)}}{{/if}}</td>
<td>
<a href="/project/{{.id}}/{{.releaseVersion}}">{{.releaseVersion}}</a>
</td>
<td>
{{#if releases[.id]}}
<a href="/release/{{.id}}?file">{{releases[id].fileName}}</a>
{{else}}
No release file available
{{/if}}
</td>
</tr>
{{/projects}}
</tbody>
</table>
</div>
{{/partial}}
{{#partial project}}
<div class="table-responsive">
<table class="pure-table pure-table-striped">
<thead>
<tr>
<th>Version</th>
<th>Stage</th>
<th>Last Log</th>
<th>Release File</th>
</tr>
</thead>
<tbody>
{{#project.versions:i}}
<tr title="{{formatDate(.when)}}">
<td>
<a href="/project/{{project.id}}/{{.version}}">{{.version}}</a>
</td>
<td>{{.stage}}</td>
<td title="{{.log}}">{{#if .log}}{{.log.substring(0,100)}}{{/if}}</td>
<td>
{{#if releases[project.id + .version]}}
<a href="/release/{{project.id}}/{{.version}}?file">{{releases[project.id + .version].fileName}}</a>
{{/if}}
</td>
</tr>
{{/versions}}
</tbody>
</table>
</div>
{{/partial}}
{{#partial version}}
<hr>
{{#if releases[project.id + .version]}}
<a href="/release/{{project.id}}/{{.version}}?file" class="pull-right pure-button pure-button-primary">Download Release</a>
{{/if}}
<div class="pure-menu pure-menu-horizontal">
<ul class="pure-menu-list">
<li class="pure-menu-item {{#if !currentStage}}pure-menu-selected{{/if}}">
<a href="/project/{{project.id}}/{{.version}}/" class="pure-menu-link">All</a>
</li>
{{#stages:i}}
<li class="pure-menu-item {{#if currentStage && currentStage == .stage}}pure-menu-selected{{/if}}">
<a href="/project/{{project.id}}/{{version}}/{{.stage}}" class="pure-menu-link">{{.stage}}</a>
</li>
{{/stages}}
</ul>
</div>
<hr>
<div class="log">
{{#if currentStage}}
<h3>{{currentStage}}<small class="timestamp">{{formatDate(logs.when)}}</small></h3>
<pre><samp>{{logs.log}}</samp></pre>
{{else}}
{{#stages:i}}
<h3>{{.stage}}<small class="timestamp">{{formatDate(.when)}}</small></h3>
<pre><samp>{{.log}}</samp></pre>
{{/stages}}
{{/if}}
</div>
{{/partial}}
</script>
<script src="/js/ractive.min.js"></script>
<script src="/js/index.js"></script>

View File

@ -3,17 +3,417 @@
// that can be found in the LICENSE file.
/* jshint strict: true */
Ractive.DEBUG = false;
(function() {
"use strict";
var r = new Ractive({
var r = new Ractive({
el: "body",
template: "#tMain",
data: function() {
return {
data: function() {
return {
project: null,
version: null,
stages: null,
currentStage: null,
logs: null,
projects: [],
error: null,
formatDate: formatDate,
releases: {},
};
},
decorators: {
menu: function(node) {
new PureDropdown(node);
return {
teardown: function() {
return;
},
};
},
},
});
};
},
});
setPaths();
r.on({
"triggerBuild": function(event) {
event.original.preventDefault();
var secret = window.prompt("Please enter the trigger secret for this project:");
triggerBuild(r.get("project.id"), secret);
},
});
function triggerBuild(projectID, secret) {
ajax("POST", "/trigger/" + projectID, {
secret: secret
},
function(result) {
window.location = "/";
},
function(result) {
r.set("error", err(result).message);
});
}
function setPaths() {
var paths = window.location.pathname.split("/");
if (paths.length <= 1) {
getProjects();
return;
}
if (!paths[1]) {
getProjects();
return;
}
if (paths[1] == "project") {
if (paths[2]) {
if (paths[3]) {
if (paths[4]) {
getStage(paths[2], paths[3], paths[4]);
}
getVersion(paths[2], paths[3]);
}
getProject(paths[2]);
}
getProjects();
return;
}
r.set("error", "Path Not found!");
}
function getProjects() {
get("/log/",
function(result) {
for (var i = 0; i < result.data.length; i++) {
setStatus(result.data[i]);
hasRelease(result.data[i].id, "");
}
result.data.sort(function(a, b) {
if (a.name > b.name) {
return 1;
}
if (a.name < b.name) {
return -1;
}
return 0;
});
r.set("projects", result.data);
window.setTimeout(getProjects, 10000);
},
function(result) {
r.set("error", err(result).message);
});
}
function getProject(id) {
get("/log/" + id,
function(result) {
r.set("project", result.data);
if (result.data.versions) {
for (var i = 0; i < result.data.versions.length; i++) {
hasRelease(result.data.id, result.data.versions[i].version);
}
}
},
function(result) {
r.set("error", err(result).message);
});
}
function getVersion(id, version) {
get("/log/" + id + "/" + version,
function(result) {
r.set("version", version);
r.set("stages", result.data);
},
function(result) {
r.set("error", err(result).message);
});
}
function getStage(id, version, stage) {
get("/log/" + id + "/" + version + "/" + stage,
function(result) {
r.set("logs", result.data);
r.set("currentStage", stage);
},
function(result) {
r.set("error", err(result).message);
});
}
function hasRelease(id, version) {
/*/release/<project-id>/<version>*/
get("/release/" + id + "/" + version,
function(result) {
r.set("releases." + id + version, result.data);
},
function(result) {
r.set("releases." + id + version, undefined);
});
}
function setStatus(project) {
//statuses
if (project.stage != "waiting") {
project.status = project.stage;
} else if (project.lastLog.version.trim() == project.releaseVersion.trim()) {
project.status = "Successfully Released";
} else {
if (project.lastLog.stage == "loading") {
project.status = "Load Failing";
} else if (project.lastLog.stage == "fetching") {
project.status = "Fetch Failing";
} else if (project.lastLog.stage == "building") {
project.status = "Build Failing";
} else if (project.lastLog.stage == "testing") {
project.status = "Tests Failing";
} else if (project.lastLog.stage == "releasing") {
project.status = "Release Failing";
} else {
project.status = "Failing";
}
}
}
})();
function ajax(type, url, data, success, error) {
"use strict";
var req = new XMLHttpRequest();
req.open(type, url);
if (success || error) {
req.onload = function() {
if (req.status >= 200 && req.status < 400) {
if (success && typeof success === 'function') {
var result;
try {
result = JSON.parse(req.responseText);
} catch (e) {
result = "";
}
success(result);
}
return;
}
//failed
if (error && typeof error === 'function') {
error(req);
}
};
req.onerror = function() {
if (error && typeof error === 'function') {
error(req);
}
};
}
var sendData;
if (type != "get") {
req.setRequestHeader("Content-Type", "application/json");
sendData = JSON.stringify(data);
}
req.send(sendData);
}
function get(url, success, error) {
"use strict";
ajax("GET", url, null, success, error);
}
function err(response) {
"use strict";
var error = {
message: "An error occurred",
};
if (typeof response === "string") {
error.message = response;
} else {
error.message = JSON.parse(response.responseText).message;
}
return error;
}
function formatDate(strDate) {
"use strict";
var date = new Date(strDate);
if (!date) {
return "";
}
return date.toLocaleDateString() + " at " + date.toLocaleTimeString();
}
function PureDropdown(dropdownParent) {
"use strict";
var PREFIX = 'pure-',
ACTIVE_CLASS_NAME = PREFIX + 'menu-active',
ARIA_ROLE = 'role',
ARIA_HIDDEN = 'aria-hidden',
MENU_OPEN = 0,
MENU_CLOSED = 1,
MENU_PARENT_CLASS_NAME = 'pure-menu-has-children',
MENU_ACTIVE_SELECTOR = '.pure-menu-active',
MENU_LINK_SELECTOR = '.pure-menu-link',
MENU_SELECTOR = '.pure-menu-children',
DISMISS_EVENT = (window.hasOwnProperty &&
window.hasOwnProperty('ontouchstart')) ?
'touchstart' : 'mousedown',
ARROW_KEYS_ENABLED = true,
ddm = this; // drop down menu
this._state = MENU_CLOSED;
this.show = function() {
if (this._state !== MENU_OPEN) {
this._dropdownParent.classList.add(ACTIVE_CLASS_NAME);
this._menu.setAttribute(ARIA_HIDDEN, false);
this._state = MENU_OPEN;
}
};
this.hide = function() {
if (this._state !== MENU_CLOSED) {
this._dropdownParent.classList.remove(ACTIVE_CLASS_NAME);
this._menu.setAttribute(ARIA_HIDDEN, true);
this._link.focus();
this._state = MENU_CLOSED;
}
};
this.toggle = function() {
this[this._state === MENU_CLOSED ? 'show' : 'hide']();
};
this.halt = function(e) {
e.stopPropagation();
e.preventDefault();
};
this._dropdownParent = dropdownParent;
this._link = this._dropdownParent.querySelector(MENU_LINK_SELECTOR);
this._menu = this._dropdownParent.querySelector(MENU_SELECTOR);
this._firstMenuLink = this._menu.querySelector(MENU_LINK_SELECTOR);
// Set ARIA attributes
this._link.setAttribute('aria-haspopup', 'true');
this._menu.setAttribute(ARIA_ROLE, 'menu');
this._menu.setAttribute('aria-labelledby', this._link.getAttribute('id'));
this._menu.setAttribute('aria-hidden', 'true');
[].forEach.call(
this._menu.querySelectorAll('li'),
function(el) {
el.setAttribute(ARIA_ROLE, 'presentation');
}
);
[].forEach.call(
this._menu.querySelectorAll('a'),
function(el) {
el.setAttribute(ARIA_ROLE, 'menuitem');
}
);
// Toggle on click
this._link.addEventListener('click', function(e) {
e.stopPropagation();
e.preventDefault();
ddm.toggle();
});
// Keyboard navigation
document.addEventListener('keydown', function(e) {
var currentLink,
previousSibling,
nextSibling,
previousLink,
nextLink;
// if the menu isn't active, ignore
if (ddm._state !== MENU_OPEN) {
return;
}
// if the menu is the parent of an open, active submenu, ignore
if (ddm._menu.querySelector(MENU_ACTIVE_SELECTOR)) {
return;
}
currentLink = ddm._menu.querySelector(':focus');
// Dismiss an open menu on ESC
if (e.keyCode === 27) {
/* Esc */
ddm.halt(e);
ddm.hide();
}
// Go to the next link on down arrow
else if (ARROW_KEYS_ENABLED && e.keyCode === 40) {
/* Down arrow */
ddm.halt(e);
// get the nextSibling (an LI) of the current link's LI
nextSibling = (currentLink) ? currentLink.parentNode.nextSibling : null;
// if the nextSibling is a text node (not an element), go to the next one
while (nextSibling && nextSibling.nodeType !== 1) {
nextSibling = nextSibling.nextSibling;
}
nextLink = (nextSibling) ? nextSibling.querySelector('.pure-menu-link') : null;
// if there is no currently focused link, focus the first one
if (!currentLink) {
ddm._menu.querySelector('.pure-menu-link').focus();
} else if (nextLink) {
nextLink.focus();
}
}
// Go to the previous link on up arrow
else if (ARROW_KEYS_ENABLED && e.keyCode === 38) {
/* Up arrow */
ddm.halt(e);
// get the currently focused link
previousSibling = (currentLink) ? currentLink.parentNode.previousSibling : null;
while (previousSibling && previousSibling.nodeType !== 1) {
previousSibling = previousSibling.previousSibling;
}
previousLink = (previousSibling) ? previousSibling.querySelector('.pure-menu-link') : null;
// if there is no currently focused link, focus the last link
if (!currentLink) {
ddm._menu.querySelector('.pure-menu-item:last-child .pure-menu-link').focus();
}
// else if there is a previous item, go to the previous item
else if (previousLink) {
previousLink.focus();
}
}
});
// Dismiss an open menu on outside event
document.addEventListener(DISMISS_EVENT, function(e) {
var target = e.target;
if (target !== ddm._link && !ddm._menu.contains(target)) {
ddm.hide();
ddm._link.blur();
}
});
}

View File

@ -10,6 +10,8 @@ import (
"net/url"
"strings"
"time"
"git.townsourced.com/townsourced/ironsmith/datastore"
)
// /path/<project-id>/<version>/<stage>
@ -73,9 +75,21 @@ func logGet(w http.ResponseWriter, r *http.Request) {
if errHandled(err, w, r) {
return
}
prjData, err := project.webData()
if errHandled(err, w, r) {
return
}
respondJsend(w, &JSend{
Status: statusSuccess,
Data: vers,
Data: struct {
*webProject
Versions []*datastore.Log `json:"versions"`
}{
webProject: prjData,
Versions: vers,
},
})
return
}
@ -226,6 +240,11 @@ func triggerPost(w http.ResponseWriter, r *http.Request) {
return
}
if strings.TrimSpace(project.TriggerSecret) == "" {
four04(w, r)
return
}
input := &triggerInput{}
if errHandled(parseInput(r, input), w, r) {
return