Finished first pass on the full cycle
Need to do some testing, then start on the web frontend
This commit is contained in:
parent
6f53ea70c0
commit
0472b31877
@ -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
283
cycle.go
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
92
project.go
92
project.go
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user