Compare commits

...

81 Commits
v0.2 ... master

Author SHA1 Message Date
Tim Shannon eaf4d9c4f5 gobs webhook test 2016-07-05 12:19:56 -05:00
Tim Shannon cb9cea887e Merge branch 'master' of git.townsourced.com:townsourced/ironsmith 2016-07-05 12:17:42 -05:00
Tim Shannon 74c85da725 Merge branch 'master' of https://git.townsourced.com/townsourced/ironsmith 2016-06-23 16:50:37 -05:00
Tim Shannon 29412a68ff Merge branch 'master' of https://git.townsourced.com/townsourced/ironsmith 2016-06-23 16:41:42 -05:00
Tim Shannon 2bc13eca6c Merge branch 'master' of https://git.townsourced.com/townsourced/ironsmith 2016-06-23 16:36:23 -05:00
Tim Shannon f8d5ddb469 removed verbose from build script 2016-05-05 10:51:57 -05:00
Tim Shannon 3bbb8d7c20 removed verbose from build script 2016-05-05 10:51:57 -05:00
Tim Shannon c7745beb28 removed verbose from build script 2016-05-05 10:51:57 -05:00
Tim Shannon f4dea30265 Changed forced build to be only off of trigger
instead of anytime that it's not polled
2016-05-05 10:30:50 -05:00
Tim Shannon 479175baa0 Changed forced build to be only off of trigger
instead of anytime that it's not polled
2016-05-05 10:30:50 -05:00
Tim Shannon dbf41e948b Changed forced build to be only off of trigger
instead of anytime that it's not polled
2016-05-05 10:30:50 -05:00
Tim Shannon 1e3eb54ea2 Added max version limit for projects 2016-05-04 15:39:56 -05:00
Tim Shannon b1c259640e Added max version limit for projects 2016-05-04 15:39:56 -05:00
Tim Shannon 35ec632334 Added max version limit for projects 2016-05-04 15:39:56 -05:00
Tim Shannon 52750bad48 Changed behavior so triggered builds ignore version
Version checks are only done on polling projects.
2016-05-04 08:35:36 -05:00
Tim Shannon f75d91d4c0 Changed behavior so triggered builds ignore version
Version checks are only done on polling projects.
2016-05-04 08:35:36 -05:00
Tim Shannon 22db2cfdad Changed behavior so triggered builds ignore version
Version checks are only done on polling projects.
2016-05-04 08:35:36 -05:00
Tim Shannon 85d8ad8221 Added max project size starting point 2016-05-03 21:04:06 -05:00
Tim Shannon f0e7c794c6 Added max project size starting point 2016-05-03 21:04:06 -05:00
Tim Shannon d0b745cc9f Added max project size starting point 2016-05-03 21:04:06 -05:00
Tim Shannon 6e3ddacbf4 Fixed small issue with logs in table formatting 2016-04-28 15:12:50 +00:00
Tim Shannon 0ab64982da Fixed small issue with logs in table formatting 2016-04-28 15:12:50 +00:00
Tim Shannon a5e982f508 Fixed small issue with logs in table formatting 2016-04-28 15:12:50 +00:00
Tim Shannon ddbcb8fc6e Fixed last release file issue
trimmed logs in tables some more
2016-04-25 21:39:11 +00:00
Tim Shannon 1a161d9554 Fixed last release file issue
trimmed logs in tables some more
2016-04-25 21:39:11 +00:00
Tim Shannon a3a7c4f66b Fixed last release file issue
trimmed logs in tables some more
2016-04-25 21:39:11 +00:00
Tim Shannon 6bca816d11 Changed log view formatting in tables a bit 2016-04-25 21:09:16 +00:00
Tim Shannon 07d499456b Changed log view formatting in tables a bit 2016-04-25 21:09:16 +00:00
Tim Shannon 21ebb286a6 Changed log view formatting in tables a bit 2016-04-25 21:09:16 +00:00
Tim Shannon a4be249b2c Added time took to release log statement
Changed release file name handling to only include the base of the file
path.
2016-04-25 21:02:01 +00:00
Tim Shannon 8d79ded661 Added time took to release log statement
Changed release file name handling to only include the base of the file
path.
2016-04-25 21:02:01 +00:00
Tim Shannon 24b541801a Added time took to release log statement
Changed release file name handling to only include the base of the file
path.
2016-04-25 21:02:01 +00:00
Tim Shannon 0f165681ac Added pre-version version
No fetch and load errors can be properly seen without having to run
ironsmith manually.
2016-04-25 19:41:03 +00:00
Tim Shannon 04060ee63d Added pre-version version
No fetch and load errors can be properly seen without having to run
ironsmith manually.
2016-04-25 19:41:03 +00:00
Tim Shannon 345062d93a Added pre-version version
No fetch and load errors can be properly seen without having to run
ironsmith manually.
2016-04-25 19:41:03 +00:00
Tim Shannon 219efef570 Another small change in the same code 2016-04-25 16:56:48 +00:00
Tim Shannon 7a0b821be4 Another small change in the same code 2016-04-25 16:56:48 +00:00
Tim Shannon e586626a69 Another small change in the same code 2016-04-25 16:56:48 +00:00
Tim Shannon 92a8045e13 Fixed issue with new projects
If they haven't done a single poll the UI was filing to find a last
version.
2016-04-25 16:51:01 +00:00
Tim Shannon 2066c68257 Fixed issue with new projects
If they haven't done a single poll the UI was filing to find a last
version.
2016-04-25 16:51:01 +00:00
Tim Shannon 6afd073f57 Fixed issue with new projects
If they haven't done a single poll the UI was filing to find a last
version.
2016-04-25 16:51:01 +00:00
Tim Shannon 1368f8aa50 Testing git hooks with ironsmith 2016-04-25 15:44:04 +00:00
Tim Shannon 4785efdfc2 Testing git hooks with ironsmith 2016-04-25 15:44:04 +00:00
Tim Shannon dd12b9dfbf Testing git hooks with ironsmith 2016-04-25 15:44:04 +00:00
Tim Shannon 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
Tim Shannon 78d160247d 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
Tim Shannon 01f655a2a1 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
Tim Shannon 9891301ca4 Added better error handling.
Moved main project list columns around
2016-04-22 20:39:06 +00:00
Tim Shannon 6ca5b8668a Added better error handling.
Moved main project list columns around
2016-04-22 20:39:06 +00:00
Tim Shannon 0cabc10fd7 Added better error handling.
Moved main project list columns around
2016-04-22 20:39:06 +00:00
Tim Shannon 77f2f8c5aa Build script change 2016-04-21 13:41:21 +00:00
Tim Shannon 358b2069d6 Build script change 2016-04-21 13:41:21 +00:00
Tim Shannon 9cb1f8c347 Build script change 2016-04-21 13:41:21 +00:00
Tim Shannon 5f776e0757 Updated build script 2016-04-20 16:42:16 +00:00
Tim Shannon 066ae7aa32 Updated build script 2016-04-20 16:42:16 +00:00
Tim Shannon 0908c91a7d Updated build script 2016-04-20 16:42:16 +00:00
Tim Shannon 20084b9429 Post trigger test 2016-04-20 16:22:42 +00:00
Tim Shannon 9632d7f2b6 Post trigger test 2016-04-20 16:22:42 +00:00
Tim Shannon 19d11b456b Post trigger test 2016-04-20 16:22:42 +00:00
Tim Shannon b5bfd93cff Fixed issue with environment not getting set in projects 2016-04-20 16:15:53 +00:00
Tim Shannon f0735abb32 Fixed issue with environment not getting set in projects 2016-04-20 16:15:53 +00:00
Tim Shannon b1ee0c640e Fixed issue with environment not getting set in projects 2016-04-20 16:15:53 +00:00
Tim Shannon ead3e6ebf0 Updated build script, and addded environment option for projects 2016-04-20 16:05:59 +00:00
Tim Shannon 6e482121ed Fixed type in build script 2016-04-20 15:44:03 +00:00
Tim Shannon f73a57d13b Build script change 2016-04-20 15:42:26 +00:00
Tim Shannon 6d7cee5f48 Added option to pass in working directory into scripts 2016-04-20 15:40:07 +00:00
Tim Shannon e568177ea6 Working on build script 2016-04-20 15:07:58 +00:00
Tim Shannon cf681a83f5 Updated build script, and addded environment option for projects 2016-04-20 18:38:53 +05:00
Tim Shannon 481c2574a6 Updated build script, and addded environment option for projects 2016-04-20 06:38:53 +05:00
Tim Shannon 6d74144a3b Updated import paths to new townsourced organization 2016-04-19 19:52:55 +00:00
Tim Shannon 7f94b454a7 work on build scripts 2016-04-19 19:26:35 +00:00
Tim Shannon e17ec20f79 Ran final bindata build, added build script 2016-04-19 15:47:47 +00:00
Tim Shannon 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
Tim Shannon a754264a1d Fixed issue with retrieving release files 2016-04-18 20:31:22 +00:00
Tim Shannon 7082d69bab Finished most of the front end stuff.
Need to fix release file downloads
2016-04-18 20:11:22 +00:00
Tim Shannon e0bc90e954 More front end work, stages and log entries 2016-04-18 19:45:16 +00:00
Tim Shannon 5f26454adf Worked on web frontend for project versions list 2016-04-17 21:14:04 -05:00
Tim Shannon d117c3e664 More frontend work.
Added project data to version list.

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

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

View File

@ -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

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 -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"
)
/*
@ -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)
}

View File

@ -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)
})
}

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"
@ -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
}

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{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
}

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
@ -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)
}()
}

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.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>

View File

@ -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();
}
});
}

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
@ -240,6 +259,6 @@ func triggerPost(w http.ResponseWriter, r *http.Request) {
}
go func() {
project.load()
project.load(true)
}()
}