Finished first pass on the full cycle

Need to do some testing, then start on the web frontend
This commit is contained in:
Tim Shannon 2016-04-01 19:30:17 +00:00
parent 6f53ea70c0
commit 0472b31877
5 changed files with 323 additions and 95 deletions

View File

@ -16,7 +16,6 @@ You'll setup a project which will need the following information:
4. Script to build the release file 4. Script to build the release file
5. Path to the release file 5. Path to the release file
6. Script to set release name / version 6. Script to set release name / version
* If script doesn't return a unique name, ironsmith will append a timestamp
Projects will be defined in a project.json file for now. I may add a web interface later. Projects will be defined in a project.json file for now. I may add a web interface later.

283
cycle.go Normal file
View File

@ -0,0 +1,283 @@
// Copyright 2016 Tim Shannon. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
package main
import (
"encoding/json"
"errors"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"git.townsourced.com/ironsmith/datastore"
)
func (p *Project) errHandled(err error) bool {
if err == nil {
return false
}
if p.ds == nil {
log.Printf("Error in project %s: %s", p.id(), err)
return true
}
defer func() {
err = p.ds.Close()
if err != nil {
log.Printf("Error closing the datastore for project %s: %s", p.id(), err)
}
p.ds = nil
//clean up version folder if it exists
if p.version != "" {
err = os.RemoveAll(p.verDir())
log.Printf("Error deleting the version directory project %s version %s: %s",
p.id(), p.version, err)
}
}()
lerr := p.ds.AddLog(p.version, p.stage, err.Error())
if lerr != nil {
log.Printf("Error logging an error in project %s: Original error %s, Logging Error: %s",
p.id(), err, lerr)
}
return true
}
func (p *Project) id() string {
if p.filename == "" {
panic("invalid project filename")
}
return strings.TrimSuffix(p.filename, filepath.Ext(p.filename))
}
func (p *Project) dir() string {
return filepath.Join(dataDir, p.id())
}
func (p *Project) verDir() string {
return filepath.Join(p.dir(), p.version)
}
// prepData makes sure the project's data folder and data store is created
/*
folder structure
projectDataFolder/<project-name>/<project-version>
*/
func (p *Project) prepData() error {
err := os.MkdirAll(p.dir(), 0777)
if err != nil {
return err
}
p.ds, err = datastore.Open(filepath.Join(p.dir(), p.id()+".ironsmith"))
if err != nil {
return err
}
return nil
}
/*
Project life cycle:
(Load Project file) -> (Fetch) -> (Build) -> (Test) -> (Release) - > (Sleep for polling period) ->
(Reload Project File) -> (Fetch) -> etc...
*/
// 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() {
p.version = ""
if p.filename == "" {
p.errHandled(errors.New("Invalid project file name"))
return
}
if !projects.exists(p.filename) {
// project has been deleted
// don't continue polling
// move project data to deleted folder
p.errHandled(os.Rename(p.dir(), filepath.Join(dataDir, deletedProjectDir, p.id())))
return
}
data, err := ioutil.ReadFile(filepath.Join(projectDir, enabledProjectDir, p.filename))
if p.errHandled(err) {
return
}
if p.errHandled(json.Unmarshal(data, p)) {
return
}
p.stage = stageLoad
if p.errHandled(p.prepData()) {
return
}
if p.PollInterval != "" {
p.poll, err = time.ParseDuration(p.PollInterval)
if p.errHandled(err) {
p.poll = 0
}
}
p.fetch()
//full cycle completed
if p.poll > 0 {
//start polling
time.AfterFunc(p.poll, p.load)
}
}
// fetch first runs the version script and checks the returned version against the latest version in the
// project database. If the version hasn't changed, then it breaks out of the cycle early doing nothing
// if the version has changed, then it runs the fetch script
func (p *Project) fetch() {
p.stage = stageFetch
verCmd := &exec.Cmd{
Path: p.Version,
Dir: p.dir(),
}
version, err := verCmd.Output()
if p.errHandled(err) {
return
}
p.version = string(version)
lVer, err := p.ds.LatestVersion()
if err != datastore.ErrNotFound && p.errHandled(err) {
return
}
if p.version == lVer {
// no new build
return
}
if p.errHandled(os.MkdirAll(p.verDir(), 0777)) {
return
}
//fetch project
fetchCmd := &exec.Cmd{
Path: p.Fetch,
Dir: p.verDir(),
}
fetchResult, err := fetchCmd.Output()
if p.errHandled(err) {
return
}
if p.errHandled(p.ds.AddLog(p.stage, p.version, string(fetchResult))) {
return
}
// fetched succesfully, onto the build stage
p.build()
}
// build runs the build scripts to build the project which should result in the a single file
// configured in the ReleaseFile section of the project file
func (p *Project) build() {
p.stage = stageBuild
buildCmd := &exec.Cmd{
Path: p.Build,
Dir: p.verDir(),
}
output, err := buildCmd.Output()
if p.errHandled(err) {
return
}
if p.errHandled(p.ds.AddLog(p.stage, p.version, string(output))) {
return
}
// built successfully, onto test stage
p.test()
}
// test runs the test scripts
func (p *Project) test() {
p.stage = stageTest
testCmd := &exec.Cmd{
Path: p.Test,
Dir: p.verDir(),
}
output, err := testCmd.Output()
if p.errHandled(err) {
return
}
if p.errHandled(p.ds.AddLog(p.stage, p.version, string(output))) {
return
}
// Tests passed, onto release
p.release()
}
// release runs the release scripts and builds the release file
func (p *Project) release() {
p.stage = stageRelease
releaseCmd := &exec.Cmd{
Path: p.Release,
Dir: p.verDir(),
}
output, err := releaseCmd.Output()
if p.errHandled(err) {
return
}
if p.errHandled(p.ds.AddLog(p.stage, p.version, string(output))) {
return
}
//get release file
f, err := os.Open(filepath.Join(p.verDir(), p.ReleaseFile))
if p.errHandled(err) {
return
}
buff, err := ioutil.ReadAll(f)
if p.errHandled(err) {
return
}
if p.errHandled(p.ds.AddRelease(p.version, buff)) {
return
}
}

View File

@ -64,11 +64,7 @@ func Open(filename string) (*Store, error) {
// Close closes the bolt datastore // Close closes the bolt datastore
func (ds *Store) Close() error { func (ds *Store) Close() error {
if ds != nil {
return ds.Close() return ds.Close()
}
return nil
} }
func (ds *Store) get(bucket string, key TimeKey, result interface{}) error { func (ds *Store) get(bucket string, key TimeKey, result interface{}) error {

View File

@ -4,7 +4,12 @@
package datastore package datastore
import "time" import (
"encoding/json"
"time"
"github.com/boltdb/bolt"
)
type log struct { type log struct {
When time.Time `json:"when"` When time.Time `json:"when"`
@ -28,3 +33,32 @@ func (ds *Store) AddLog(version, stage, entry string) error {
return ds.put(bucketLog, key, data) return ds.put(bucketLog, key, data)
} }
// LatestVersion returns the latest version for the current project
func (ds *Store) LatestVersion() (string, error) {
version := ""
err := ds.bolt.View(func(tx *bolt.Tx) error {
c := tx.Bucket([]byte(bucketLog)).Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() {
l := &log{}
err := json.Unmarshal(v, l)
if err != nil {
return err
}
if l.Version != "" {
version = l.Version
return nil
}
}
return ErrNotFound
})
if err != nil {
return "", err
}
return version, nil
}

View File

@ -6,19 +6,18 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"io/ioutil"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"time" "time"
"git.townsourced.com/ironsmith/datastore" "git.townsourced.com/ironsmith/datastore"
) )
const enabledProjectDir = "enabled" const (
enabledProjectDir = "enabled"
deletedProjectDir = "deleted"
)
//stages //stages
const ( const (
@ -105,88 +104,6 @@ func prepTemplateProject() error {
return nil return nil
} }
func (p *Project) errHandled(err error) bool {
if err == nil {
return false
}
if p.ds == nil {
log.Printf("Error in project %s: %s", p.filename, err)
return true
}
p.ds.AddLog(p.version, p.stage, err.Error())
return true
}
func (p *Project) load() {
if p.filename == "" {
p.errHandled(errors.New("Invalid project file name"))
return
}
if !projects.exists(p.filename) {
// project has been deleted
// don't continue polling
// TODO: Clean up Project data folder?
return
}
data, err := ioutil.ReadFile(filepath.Join(projectDir, enabledProjectDir, p.filename))
if p.errHandled(err) {
return
}
if p.errHandled(json.Unmarshal(data, p)) {
return
}
p.stage = stageLoad
if p.errHandled(p.prepData()) {
return
}
//TODO: call fetch
if p.PollInterval != "" {
p.poll, err = time.ParseDuration(p.PollInterval)
if p.errHandled(err) {
p.poll = 0
}
}
if p.poll > 0 {
//start polling
}
}
// prepData makes sure the project's data folder and data store is created
/*
folder structure
projectDataFolder/<project-name>/<project-version>
*/
func (p *Project) prepData() error {
var name = strings.TrimSuffix(p.filename, filepath.Ext(p.filename))
var dir = filepath.Join(dataDir, name)
err := os.MkdirAll(dir, 0777)
if err != nil {
return err
}
p.ds, err = datastore.Open(filepath.Join(dir, name+".ironsmith"))
if err != nil {
return err
}
return nil
}
type projectList struct { type projectList struct {
sync.RWMutex sync.RWMutex
data map[string]*Project data map[string]*Project
@ -221,7 +138,6 @@ func (p *projectList) load() error {
prj := &Project{ prj := &Project{
filename: files[i].Name(), filename: files[i].Name(),
Name: files[i].Name(), Name: files[i].Name(),
version: "starting up",
stage: stageLoad, stage: stageLoad,
} }
p.data[files[i].Name()] = prj p.data[files[i].Name()] = prj