Compare commits
81 Commits
Author | SHA1 | Date |
---|---|---|
Tim Shannon | eaf4d9c4f5 | |
Tim Shannon | cb9cea887e | |
Tim Shannon | 74c85da725 | |
Tim Shannon | 29412a68ff | |
Tim Shannon | 2bc13eca6c | |
Tim Shannon | f8d5ddb469 | |
Tim Shannon | 3bbb8d7c20 | |
Tim Shannon | c7745beb28 | |
Tim Shannon | f4dea30265 | |
Tim Shannon | 479175baa0 | |
Tim Shannon | dbf41e948b | |
Tim Shannon | 1e3eb54ea2 | |
Tim Shannon | b1c259640e | |
Tim Shannon | 35ec632334 | |
Tim Shannon | 52750bad48 | |
Tim Shannon | f75d91d4c0 | |
Tim Shannon | 22db2cfdad | |
Tim Shannon | 85d8ad8221 | |
Tim Shannon | f0e7c794c6 | |
Tim Shannon | d0b745cc9f | |
Tim Shannon | 6e3ddacbf4 | |
Tim Shannon | 0ab64982da | |
Tim Shannon | a5e982f508 | |
Tim Shannon | ddbcb8fc6e | |
Tim Shannon | 1a161d9554 | |
Tim Shannon | a3a7c4f66b | |
Tim Shannon | 6bca816d11 | |
Tim Shannon | 07d499456b | |
Tim Shannon | 21ebb286a6 | |
Tim Shannon | a4be249b2c | |
Tim Shannon | 8d79ded661 | |
Tim Shannon | 24b541801a | |
Tim Shannon | 0f165681ac | |
Tim Shannon | 04060ee63d | |
Tim Shannon | 345062d93a | |
Tim Shannon | 219efef570 | |
Tim Shannon | 7a0b821be4 | |
Tim Shannon | e586626a69 | |
Tim Shannon | 92a8045e13 | |
Tim Shannon | 2066c68257 | |
Tim Shannon | 6afd073f57 | |
Tim Shannon | 1368f8aa50 | |
Tim Shannon | 4785efdfc2 | |
Tim Shannon | dd12b9dfbf | |
Tim Shannon | 54104e61cf | |
Tim Shannon | 78d160247d | |
Tim Shannon | 01f655a2a1 | |
Tim Shannon | 9891301ca4 | |
Tim Shannon | 6ca5b8668a | |
Tim Shannon | 0cabc10fd7 | |
Tim Shannon | 77f2f8c5aa | |
Tim Shannon | 358b2069d6 | |
Tim Shannon | 9cb1f8c347 | |
Tim Shannon | 5f776e0757 | |
Tim Shannon | 066ae7aa32 | |
Tim Shannon | 0908c91a7d | |
Tim Shannon | 20084b9429 | |
Tim Shannon | 9632d7f2b6 | |
Tim Shannon | 19d11b456b | |
Tim Shannon | b5bfd93cff | |
Tim Shannon | f0735abb32 | |
Tim Shannon | b1ee0c640e | |
Tim Shannon | ead3e6ebf0 | |
Tim Shannon | 6e482121ed | |
Tim Shannon | f73a57d13b | |
Tim Shannon | 6d7cee5f48 | |
Tim Shannon | e568177ea6 | |
Tim Shannon | cf681a83f5 | |
Tim Shannon | 481c2574a6 | |
Tim Shannon | 6d74144a3b | |
Tim Shannon | 7f94b454a7 | |
Tim Shannon | e17ec20f79 | |
Tim Shannon | 70c9f60d8f | |
Tim Shannon | a754264a1d | |
Tim Shannon | 7082d69bab | |
Tim Shannon | e0bc90e954 | |
Tim Shannon | 5f26454adf | |
Tim Shannon | d117c3e664 | |
Tim Shannon | ae961e9dd1 | |
Tim Shannon | 7ca04a5594 | |
Tim Shannon | a1ced419c0 |
18
README.md
18
README.md
|
@ -14,11 +14,24 @@ You'll setup a project which will need the following information:
|
|||
2. Script to build the repository
|
||||
3. Script to test the repository
|
||||
4. Script to build the release file
|
||||
5. Path to the release file
|
||||
5. Path to the release file / can also be a script that returns a file name
|
||||
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
|
||||
|
|
147
bindata.go
147
bindata.go
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
go-bindata web/... && go build -a -o ironsmith
|
63
cycle.go
63
cycle.go
|
@ -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"
|
||||
)
|
||||
|
||||
/*
|
||||
|
@ -26,12 +27,13 @@ Project life cycle:
|
|||
|
||||
// load is the beginning of the cycle. Loads / reloads the project file to make sure that the scripts are up-to-date
|
||||
// call's fetch and triggers the next poll if one exists
|
||||
func (p *Project) load() {
|
||||
func (p *Project) load(forceBuild bool) {
|
||||
p.processing.Lock() // ensure only one cycle is running at a time per project
|
||||
defer p.processing.Unlock()
|
||||
|
||||
p.setStage(stageLoad)
|
||||
p.setVersion("")
|
||||
p.setVersion("Version not yet set")
|
||||
p.start = time.Time{}
|
||||
|
||||
if p.filename == "" {
|
||||
p.errHandled(errors.New("Invalid project file name"))
|
||||
|
@ -62,25 +64,27 @@ func (p *Project) load() {
|
|||
|
||||
p.setData(new)
|
||||
|
||||
p.fetch()
|
||||
p.fetch(forceBuild)
|
||||
|
||||
p.setStage(stageWait)
|
||||
|
||||
//full cycle completed
|
||||
p.errHandled(p.ds.TrimVersions(p.MaxVersions))
|
||||
|
||||
if p.poll > 0 {
|
||||
//start polling
|
||||
go func() {
|
||||
time.AfterFunc(p.poll, p.load)
|
||||
}()
|
||||
time.AfterFunc(p.poll, func() {
|
||||
p.load(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// fetch first runs the fetch script into a temporary directory
|
||||
// then it runs the version script in the temp directory to see if there is a newer version of the
|
||||
// fetched code, if there is then the temp dir is renamed to the version name
|
||||
func (p *Project) fetch() {
|
||||
func (p *Project) fetch(forceBuild bool) {
|
||||
p.setStage(stageFetch)
|
||||
p.start = time.Now()
|
||||
|
||||
if p.Fetch == "" {
|
||||
return
|
||||
|
@ -93,13 +97,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,17 +111,20 @@ func (p *Project) fetch() {
|
|||
|
||||
p.setVersion(strings.TrimSpace(string(version)))
|
||||
|
||||
lVer, err := p.ds.LastVersion("")
|
||||
if err != datastore.ErrNotFound && p.errHandled(err) {
|
||||
return
|
||||
}
|
||||
if !forceBuild {
|
||||
// if not forced build, then 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 {
|
||||
// no new build clean up temp dir
|
||||
p.errHandled(os.RemoveAll(tempDir))
|
||||
if p.version == "" || p.version == lVer.Version {
|
||||
// no new build clean up temp dir
|
||||
p.errHandled(os.RemoveAll(tempDir))
|
||||
|
||||
vlog("No new version found for Project: %s Version: %s.\n", p.id(), p.version)
|
||||
return
|
||||
vlog("No new version found for Project: %s Version: %s.\n", p.id(), p.version)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//remove any existing data that matches version hash
|
||||
|
@ -150,7 +157,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 +178,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 +200,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
|
||||
|
@ -214,12 +221,20 @@ func (p *Project) release() {
|
|||
return
|
||||
}
|
||||
|
||||
if p.errHandled(p.ds.AddRelease(p.version, p.ReleaseFile, buff)) {
|
||||
if p.errHandled(p.ds.AddRelease(p.version, filepath.Base(p.ReleaseFile), buff)) {
|
||||
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 and took %s.\n", p.id(), p.version,
|
||||
time.Now().Sub(p.start)))) {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -6,10 +6,8 @@
|
|||
package datastore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
|
@ -69,6 +67,83 @@ func (ds *Store) Close() error {
|
|||
return ds.bolt.Close()
|
||||
}
|
||||
|
||||
// TrimVersions Removes versions from the datastore file until it reaches the maxVersions count
|
||||
func (ds *Store) TrimVersions(maxVersions int) error {
|
||||
if maxVersions <= 0 {
|
||||
// no max set
|
||||
return nil
|
||||
}
|
||||
|
||||
versions, err := ds.Versions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(versions) <= maxVersions {
|
||||
return nil
|
||||
}
|
||||
|
||||
remove := versions[maxVersions:]
|
||||
|
||||
for i := range remove {
|
||||
err = ds.deleteVersion(remove[i].Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removes the earliest instance of a specific version
|
||||
func (ds *Store) deleteVersion(version string) error {
|
||||
return ds.bolt.Update(func(tx *bolt.Tx) error {
|
||||
// remove all logs for this version
|
||||
c := tx.Bucket([]byte(bucketLog)).Cursor()
|
||||
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
lg := &Log{}
|
||||
|
||||
err := json.Unmarshal(v, lg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if lg.Version != version {
|
||||
break
|
||||
}
|
||||
|
||||
err = c.Delete()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// remove all releases for this version
|
||||
release, err := ds.Release(version)
|
||||
if err == ErrNotFound {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Bucket([]byte(bucketReleases)).Delete(release.FileKey.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove release file for this version
|
||||
err = tx.Bucket([]byte(bucketFiles)).Delete(release.FileKey.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (ds *Store) get(bucket string, key []byte, result interface{}) error {
|
||||
return ds.bolt.View(func(tx *bolt.Tx) error {
|
||||
dsValue := tx.Bucket([]byte(bucket)).Get(key)
|
||||
|
@ -77,15 +152,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)
|
||||
})
|
||||
}
|
||||
|
@ -104,9 +170,3 @@ func (ds *Store) put(bucket string, key []byte, value interface{}) error {
|
|||
return tx.Bucket([]byte(bucket)).Put(key, dsValue)
|
||||
})
|
||||
}
|
||||
|
||||
func (ds *Store) delete(bucket string, key []byte) error {
|
||||
return ds.bolt.Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket([]byte(bucket)).Delete(key)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
package datastore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
|
@ -26,13 +28,13 @@ const (
|
|||
|
||||
// AddRelease adds a new Release
|
||||
func (ds *Store) AddRelease(version, fileName string, fileData []byte) error {
|
||||
fileKey := NewTimeKey()
|
||||
key := NewTimeKey()
|
||||
|
||||
r := &Release{
|
||||
When: fileKey.Time(),
|
||||
When: key.Time(),
|
||||
Version: version,
|
||||
FileName: fileName,
|
||||
FileKey: fileKey,
|
||||
FileKey: key,
|
||||
}
|
||||
|
||||
dsValue, err := json.Marshal(r)
|
||||
|
@ -41,33 +43,64 @@ func (ds *Store) AddRelease(version, fileName string, fileData []byte) error {
|
|||
}
|
||||
|
||||
return ds.bolt.Update(func(tx *bolt.Tx) error {
|
||||
err = tx.Bucket([]byte(bucketReleases)).Put([]byte(version), dsValue)
|
||||
err = tx.Bucket([]byte(bucketReleases)).Put(key.Bytes(), dsValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Bucket([]byte(bucketFiles)).Put(fileKey.Bytes(), fileData)
|
||||
return tx.Bucket([]byte(bucketFiles)).Put(key.Bytes(), fileData)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
func (ds *Store) Release(version string) (*Release, error) {
|
||||
r := &Release{}
|
||||
err := ds.get(bucketReleases, []byte(version), r)
|
||||
err := ds.bolt.View(func(tx *bolt.Tx) error {
|
||||
c := tx.Bucket([]byte(bucketReleases)).Cursor()
|
||||
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
err := json.Unmarshal(v, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Version == version {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return ErrNotFound
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
|
@ -103,9 +136,8 @@ func (ds *Store) LastRelease() (*Release, error) {
|
|||
r := &Release{}
|
||||
|
||||
err := ds.bolt.View(func(tx *bolt.Tx) error {
|
||||
c := tx.Bucket([]byte(bucketReleases)).Cursor()
|
||||
_, v := tx.Bucket([]byte(bucketReleases)).Cursor().Last()
|
||||
|
||||
_, v := c.Last()
|
||||
if v == nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
|
2
error.go
2
error.go
|
@ -11,7 +11,7 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.townsourced.com/ironsmith/datastore"
|
||||
"git.townsourced.com/townsourced/ironsmith/datastore"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
75
exec.go
75
exec.go
|
@ -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{Name: file, Err: err}
|
||||
}
|
||||
|
||||
for i := range env {
|
||||
if strings.HasPrefix(env[i], "PATH=") {
|
||||
pathenv := env[i][5:]
|
||||
if pathenv == "" {
|
||||
return "", &exec.Error{Name: file, Err: 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{Name: file, Err: 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
|
||||
}
|
||||
|
|
2
main.go
2
main.go
|
@ -11,7 +11,7 @@ import (
|
|||
"os/signal"
|
||||
"path/filepath"
|
||||
|
||||
"git.townsourced.com/config"
|
||||
"git.townsourced.com/townsourced/config"
|
||||
)
|
||||
|
||||
//settings
|
||||
|
|
39
project.go
39
project.go
|
@ -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
|
||||
|
@ -57,6 +60,7 @@ type Project struct {
|
|||
ReleaseFile string `json:"releaseFile"`
|
||||
PollInterval string `json:"pollInterval,omitempty"` // if not poll interval is specified, this project is trigger only
|
||||
TriggerSecret string `json:"triggerSecret,omitempty"` //secret to be included with a trigger call
|
||||
MaxVersions int `json:"maxVersions,omitempty"` // Max number of versions to keep in the project datastore
|
||||
|
||||
filename string
|
||||
poll time.Duration
|
||||
|
@ -65,6 +69,7 @@ type Project struct {
|
|||
status string
|
||||
version string
|
||||
hash string
|
||||
start time.Time // the last start time of the latest cycle
|
||||
|
||||
sync.RWMutex
|
||||
processing sync.Mutex
|
||||
|
@ -185,11 +190,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 +214,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 +277,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
|
||||
|
@ -282,6 +288,7 @@ func (p *Project) setData(new *Project) {
|
|||
p.ReleaseFile = new.ReleaseFile
|
||||
p.PollInterval = new.PollInterval
|
||||
p.TriggerSecret = new.TriggerSecret
|
||||
p.MaxVersions = new.MaxVersions
|
||||
|
||||
if p.PollInterval != "" {
|
||||
var err error
|
||||
|
@ -319,6 +326,8 @@ var projectTemplate = &Project{
|
|||
|
||||
ReleaseFile: "release.tar.gz",
|
||||
PollInterval: "15m",
|
||||
|
||||
MaxVersions: 100,
|
||||
}
|
||||
|
||||
func prepTemplateProject() error {
|
||||
|
@ -418,7 +427,7 @@ func (p *projectList) add(name string) {
|
|||
log.Printf("Error opening datastore for Project: %s Error: %s\n", prj.id(), err)
|
||||
return
|
||||
}
|
||||
prj.load()
|
||||
prj.load(false)
|
||||
}()
|
||||
}
|
||||
|
||||
|
|
17
server.go
17
server.go
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
245
web/index.html
245
web/index.html
|
@ -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.length > 150}}{{.lastLog.log.substring(0,150)}}...{{else}}{{.lastLog.log}}{{/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.length > 150}}{{.log.substring(0,150)}}...{{else}}{{.log}}{{/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>
|
||||
|
|
418
web/js/index.js
418
web/js/index.js
|
@ -3,17 +3,423 @@
|
|||
// 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) {
|
||||
if (!result.data || !result.data.length || !result.data[0].version) {
|
||||
r.set("version", version);
|
||||
} else {
|
||||
r.set("version", result.data[0].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 || !project.lastLog.version) {
|
||||
project.status = "waiting";
|
||||
} 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();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -240,6 +259,6 @@ func triggerPost(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
go func() {
|
||||
project.load()
|
||||
project.load(true)
|
||||
}()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue