shanty 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 472f2d824a07db1b5c47ec13ddca184cad7478db
4
+ data.tar.gz: 6a45c1b9a654584efbd06d87d2c7202667095b43
5
+ SHA512:
6
+ metadata.gz: 92abe04b4e6d65338f17a637c03ff0079297b3777c3e23726d4d8f85438121ece70a02806feac64e149e24103816f734518247e6f07ee53c460b0778a802b31a
7
+ data.tar.gz: 7b7e75add8f40176c1510ffeb96453678194a8e4f7f6a6fafb9f266d1e3a211d00d5d144285ee50d72ebd0e63d8c3276d9931d6017121705ca2ab8b2feecf3f0
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # Shanty [![Build Status](https://travis-ci.org/shantytown/shanty.svg?branch=master)](https://travis-ci.org/shantytown/shanty)[![Coverage Status](https://coveralls.io/repos/shantytown/shanty/badge.png?branch=master)](https://coveralls.io/r/shantytown/shanty?branch=master)
2
+
3
+ **Shanty** is a project orchestration tool. It is designed to make it easy for you to build dependencies between projects, and execute tasks across this tree of relationships, regardless of what language or technology these projects use. The aim is consistency in the way you manage, build, test and deploy your projects.
4
+
5
+ * Shanty is **language agnostic**. Shanty config files must be written in Ruby, but can be written for any type of project written in any language.
6
+ * Shanty **is not a build tool**. It is designed to work with your existing build tool, instead living a layer up where it manages tasks to call to your build tool.
7
+ * Shanty **is great for people with single repository, many subproject setups**. It makes it trivial to use your repository with many well known Continuous Integration tools and get benefits like building only changed projects, and depedency management.
8
+ * Shanty **supports plugins**. You can hook into the lifecycle of Shanty at any point to achieve the task you want. Plugins can discover projects, add support for a new VCS, add actions that will be executed when a command is run, and work out whether a project has changed or not based on more complex dependencies.
9
+
10
+ ## Why Would I Want To Use Shanty Instead Of `<Insert Build Tool Here>`?
11
+
12
+ Shanty is designed to make it easy to run tasks consistently across projects of many different types or written in many different languages.
13
+
14
+ A great example use case of Shanty is integrating with Docker:
15
+
16
+ ```
17
+ Foo (Java) -> Bar (Java) -> Bar Container (Docker)
18
+ ```
19
+
20
+ Lets say you have a Java project `Bar` that builds an artifact. This Java project has a dependency on the artifact of another Java project called `Foo`. In order to dockerise the `Bar` project, you need to build the `Foo` dependency, then build the `Bar` project, and then build the Docker container.
21
+
22
+ Managing this in existing setups often requires cobbling together shell scripts and the build tool of your choice. Shanty is designed to manage these relationships for you, abstracting the tasks of building and linking these projects into plugins that get auto-triggered based on these relationships (think `make`). Projects like `Gradle` go a long way towards supporting this model with their multi-project support, but the expressiveness of Ruby is missed as soon as you need to do something menial like read in a JSON options file for a project.
23
+
24
+ So, with Shanty, building the Docker container would be as simple as `shanty build` in the Docker container project folder, and the rest is taken care of for you. This is because Shanty is designed to give you the freedom to choose any tool for any project, written in any language, by allowing you to plug in functionality where you need it.
25
+
26
+ ## Contibuting
27
+
28
+ We welcome any contribution, whether big or small! We're busy importing our planned work as GitHub issues so people can join in the fun, we also have a [Huboard](https://huboard.com/shantytown/shanty) to make following the progress easier. If you have any questions, please hit us up on our freenode IRC channel `#shantytown`.
29
+
30
+ ## License
31
+
32
+ The MIT License (MIT)
33
+
34
+ Copyright (c) 2014 Chris Jansen, Nathan Kleyn
35
+
36
+ Permission is hereby granted, free of charge, to any person obtaining a copy
37
+ of this software and associated documentation files (the "Software"), to deal
38
+ in the Software without restriction, including without limitation the rights
39
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
40
+ copies of the Software, and to permit persons to whom the Software is
41
+ furnished to do so, subject to the following conditions:
42
+
43
+ The above copyright notice and this permission notice shall be included in
44
+ all copies or substantial portions of the Software.
45
+
46
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
47
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
48
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
49
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
50
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
51
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
52
+ THE SOFTWARE.
data/bin/shanty ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'shanty'
3
+ Shanty::Shanty.new.start!
data/lib/shanty/cli.rb ADDED
@@ -0,0 +1,83 @@
1
+ require 'commander'
2
+ require 'i18n'
3
+ require 'shanty/task'
4
+
5
+ module Shanty
6
+ # Public: Handle the CLI interface between the user and the registered tasks
7
+ # and plugins.
8
+ class Cli
9
+ include Commander::Methods
10
+
11
+ def initialize(graph)
12
+ @graph = graph
13
+ end
14
+
15
+ def tasks
16
+ Task.tasks.reduce({}) do |acc, task|
17
+ acc.merge(task.task_definitions)
18
+ end
19
+ end
20
+
21
+ def run
22
+ program :name, 'Shanty'
23
+ program :version, '0.1.0'
24
+ program :description, 'Something'
25
+
26
+ setup_tasks
27
+ run!
28
+ end
29
+
30
+ private
31
+
32
+ def setup_tasks
33
+ tasks.each do |name, task|
34
+ setup_task(name, task)
35
+ end
36
+ end
37
+
38
+ def setup_task(name, task)
39
+ command name do |c|
40
+ c.description = I18n.t(task[:desc], default: task[:desc])
41
+
42
+ task[:options].each do |option_name, option|
43
+ c.option(syntax_for_option(option_name, option), I18n.t(option[:desc], default: option[:desc]))
44
+ end
45
+
46
+ c.action do |args, options|
47
+ execute_task(name, args, options)
48
+ end
49
+ end
50
+ end
51
+
52
+ def execute_task(name, args, options)
53
+ task = tasks[name]
54
+
55
+ default_option_pairs = task[:options].map do |option_name, option|
56
+ [option_name, default_for_type(option)]
57
+ end
58
+ options.default(Hash[default_option_pairs])
59
+
60
+ task[:klass].new.send(name, options, @graph, *args)
61
+ end
62
+
63
+ def syntax_for_option(name, option)
64
+ syntax = case option[:type]
65
+ when :boolean
66
+ "--#{name}"
67
+ else
68
+ "--#{name} #{option[:type].upcase}"
69
+ end
70
+
71
+ option[:required] ? "[#{syntax}]" : syntax
72
+ end
73
+
74
+ def default_for_type(option)
75
+ case option[:type]
76
+ when :boolean
77
+ option[:default] || false
78
+ else
79
+ option[:default]
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,31 @@
1
+ require 'shanty/util'
2
+ require 'shanty/project_template'
3
+
4
+ module Shanty
5
+ # Public: Enables discovery of different types of project
6
+ # utilises inherited class method to find all implementing
7
+ # classes
8
+ class Discoverer
9
+ class << self
10
+ attr_reader :discoverers
11
+ end
12
+
13
+ def self.inherited(discoverer)
14
+ Util.logger.debug("Detected project discoverer #{discoverer}")
15
+ @discoverers ||= []
16
+ @discoverers << discoverer
17
+ end
18
+
19
+ def discover_all
20
+ self.class.discoverers.flat_map do |discoverer|
21
+ discoverer.new.discover
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def create_project(*args)
28
+ ProjectTemplate.new(*args)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ require 'shanty/discoverer'
2
+ require 'shanty/projects/static'
3
+
4
+ module Shanty
5
+ # Gradle discoverer
6
+ class ShantyfileDiscoverer < Discoverer
7
+ def discover
8
+ Dir['**/build.gradle'].map do |path|
9
+ create_project(
10
+ File.dirname(path),
11
+ type: GradleProject,
12
+ options: { foo: 'bar' },
13
+ plugins: [GradlePlugin], parents: ['']
14
+ )
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ require 'shanty/discoverer'
2
+ require 'shanty/projects/static'
3
+
4
+ module Shanty
5
+ # Public: Discoverer for Shantyfiles
6
+ # will create a a project for every Shantyfile it finds in
7
+ # a directory
8
+ class ShantyfileDiscoverer < Discoverer
9
+ def discover
10
+ Dir['**/Shantyfile'].map do |path|
11
+ create_project(File.absolute_path(File.dirname(path)))
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,49 @@
1
+ require 'yaml'
2
+ require 'deep_merge'
3
+
4
+ module Shanty
5
+ # Global configuration variables for shanty
6
+ module Global
7
+ module_function
8
+
9
+ def config_file
10
+ '.shanty.yml'
11
+ end
12
+
13
+ def environment
14
+ @environment = ENV['SHANTY_ENV'] || 'local'
15
+ end
16
+
17
+ def default_config
18
+ @default_config ||= {}
19
+ end
20
+
21
+ def add_default_config(new_config)
22
+ @default_config.merge!(new_config)
23
+ end
24
+
25
+ def root
26
+ @root ||= find_root
27
+ end
28
+
29
+ def config
30
+ file_config = YAML.load_file("#{root}/#{config_file}") || {}
31
+ @config ||= default_config.deep_merge!(file_config[environment]) || default_config
32
+ end
33
+
34
+ def find_root
35
+ if root_dir.nil?
36
+ fail "Could not find a #{Global.config_file} file in this or any parent directories. \
37
+ Please run `shanty init` in the directory you want to be the root of your project structure."
38
+ end
39
+
40
+ root_dir
41
+ end
42
+
43
+ def root_dir
44
+ Pathname.new(Dir.pwd).ascend do |d|
45
+ return d if d.join(Global.config_file).exist?
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,122 @@
1
+ require 'algorithms'
2
+
3
+ module Shanty
4
+ # Public: Represents the link graph of projects in the repository. This class is
5
+ # responsible for collecting up all the information the projects we have,
6
+ # making parent/child dependency links between them, and then calculating which
7
+ # projects need to be build by combining a Git diff with the dependency graph
8
+ # to resolve to a list of projects required to be built.
9
+ class Graph
10
+ attr_reader :projects
11
+
12
+ # Public: Initialise a ProjectLinkGraph.
13
+ #
14
+ # projects - An array of project instances to take and link together into
15
+ # a graph structure of dependencies.
16
+ def initialize(projects)
17
+ @projects = sort_projects(link_projects(projects))
18
+
19
+ @project_path_trie = Containers::Trie.new
20
+
21
+ @projects.each do |project|
22
+ @project_path_trie[project.path] = project
23
+ end
24
+ end
25
+
26
+ # Public: All the changed projects in the current repository.
27
+ #
28
+ # Returns an Array of Project subclasses, one for each project in the
29
+ # repository.
30
+ def changed
31
+ @projects.select { |project| project.changed? }
32
+ end
33
+
34
+ # Public: Returns a project, if any, with the given name.
35
+ #
36
+ # name - The name to filter by.
37
+ #
38
+ # Returns an instance of a Project subclass if found, otherwise nil.
39
+ def by_name(name)
40
+ @projects.find { |project| project.name == name }
41
+ end
42
+
43
+ # Public: Returns all projects of the given types.
44
+ #
45
+ # *types - One or more types to filter by.
46
+ #
47
+ # Returns an Array of Project subclasses, one for each project in the
48
+ # repository.
49
+ def all_of_type(*types)
50
+ @projects.select { |project| types.include?(project.class) }
51
+ end
52
+
53
+ # Public: Returns all the changed projects of the given types.
54
+ #
55
+ # *types - One or more types to filter by.
56
+ #
57
+ # Returns an Array of Project subclasses, one for each project in the
58
+ # repository.
59
+ def changed_of_type(*types)
60
+ changed.select { |project| types.include?(project.class) }
61
+ end
62
+
63
+ # Public: Returns the project, if any, that the current working directory
64
+ # belongs to.
65
+ #
66
+ # Returns an instance of a Project subclass if found, otherwise nil.
67
+ def current
68
+ owner_of_file(Dir.pwd)
69
+ end
70
+
71
+ # Public: Given a path to a file or directory (normally a path obtained
72
+ # while looking at a Git diff), find the project that owns this file. This
73
+ # works by finding the project with the longest path in common with the
74
+ # file, and is very efficient because this search is backed using a Trie
75
+ # data structure. This data structure allows worst case matching of O(m)
76
+ # complexity.
77
+ #
78
+ # path - The path to the file to find the owner project.
79
+ #
80
+ # Returns a Project subclass if any found, otherwise nil.
81
+ def owner_of_file(path)
82
+ key = @project_path_trie.longest_prefix(path)
83
+ @project_path_trie[key]
84
+ end
85
+
86
+ private
87
+
88
+ # Private: Given a list of projects, construct the parent/child
89
+ # relationships between them given a list of their parents/children by name
90
+ # as defined on the project instances.
91
+ #
92
+ # projects - An array of Project subclasses to link together.
93
+ #
94
+ # Returns an Array of linked projects.
95
+ def link_projects(projects)
96
+ projects_by_name = projects.each_with_object({}) { |project, acc| acc[project.name] = project }
97
+
98
+ projects.each do |project|
99
+ parent_dependencies = project.parents_by_name.map { |parent_name| projects_by_name[parent_name] }.compact
100
+
101
+ parent_dependencies.each do |parent_dependency|
102
+ project.add_parent(parent_dependency)
103
+ parent_dependency.add_child(project)
104
+ end
105
+ end
106
+
107
+ projects
108
+ end
109
+
110
+ # Private: Given a list of Project subclasses, sort them by their distance
111
+ # from the root node (that is, the topmost node). This order is important
112
+ # because it matches the order in which things should be build to avoid
113
+ # missing dependencies.
114
+ #
115
+ # projects - An array of Project subclasses to sort.
116
+ #
117
+ # Returns a sorted Array of Project subclasses.
118
+ def sort_projects(projects)
119
+ projects.sort { |a, b| a.distance_from_root <=> b.distance_from_root }
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,81 @@
1
+ module Shanty
2
+ module Mixins
3
+ # A mixin module enabling classes to have parents and children. It provides
4
+ # convenience methods for determining dependencies, depdenants, and a distance
5
+ # from the root node. Note that in contrast to a tree, this link graph module
6
+ # allows multiple parents.
7
+ module ActsAsLinkGraphNode
8
+ # The self.included idiom. This is described in great detail in a
9
+ # fantastic blog post here:
10
+ #
11
+ # http://www.railstips.org/blog/archives/2009/05/15/include-vs-extend-in-ruby/
12
+ #
13
+ # Basically, this idiom allows us to add both instance *and* class methods
14
+ # to the class that is mixing this module into itself without forcing them
15
+ # to call extend and include for this mixin. You'll see this idiom everywhere
16
+ # in the Ruby/Rails world, so we use it too.
17
+ def self.included(cls)
18
+ cls.extend(ClassMethods)
19
+ end
20
+
21
+ # Common methods inherited by all classes
22
+ module ClassMethods
23
+ attr_writer :parents, :children
24
+ end
25
+
26
+ # Public: The parents linked to this instance.
27
+ #
28
+ # Returns an Array of Objects.
29
+ def parents
30
+ @parents ||= []
31
+ end
32
+
33
+ # Public: The children linked to this instance.
34
+ #
35
+ # Returns an Array of Objects.
36
+ def children
37
+ @children ||= []
38
+ end
39
+
40
+ # Public: Add a node to the parents linked to this instance.
41
+ #
42
+ # node - The Object to add.
43
+ def add_parent(node)
44
+ parents << node
45
+ end
46
+
47
+ # Public: Add a node to the children linked to this instance.
48
+ #
49
+ # node - The Object to add.
50
+ def add_child(node)
51
+ children << node
52
+ end
53
+
54
+ # Public: Convenience method to return all of the children from this node in
55
+ # the tree downwards to the leaves of the tree.
56
+ #
57
+ # Returns an Array of child Objects.
58
+ def all_children
59
+ children + children.map(&:all_children).flatten
60
+ end
61
+
62
+ # Public: Convenience method to return all of the parents from this node in
63
+ # the tree upwards to the root of the tree.
64
+ #
65
+ # Returns an Array of parent Objects.
66
+ def all_parents
67
+ parents + parents.map(&:all_parents).flatten
68
+ end
69
+
70
+ # Public: Calculate the maximum number of traverses that need to be made to
71
+ # reach the root from this node.
72
+ #
73
+ # Returns a Fixnum representing the traverses to the root. Note that a return
74
+ # value of 0 means this is the root (ie. it has no parents).
75
+ def distance_from_root
76
+ return 0 if parents.empty?
77
+ parents.map(&:distance_from_root).max + 1
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,24 @@
1
+ module Shanty
2
+ module Mixins
3
+ # Additional accessor utility
4
+ module AttrCombinedAccessor
5
+ # Creates an invariant accessor that allows getting and setting from the
6
+ # same endpoint. It will operate in getter mode if you don't pass any
7
+ # arguments when calling it, otherwise it will work in setter mode. Useful
8
+ # when needing to chain methods (you can't chain standard attr_writer
9
+ # methods because of the `= something` part) or when trying to create a
10
+ # nice looking DSL.
11
+ def attr_combined_accessor(*syms)
12
+ syms.each do |sym|
13
+ define_method(sym) do |*args|
14
+ if args.empty?
15
+ instance_variable_get(:"@#{sym}")
16
+ else
17
+ instance_variable_set(:"@#{sym}", *args)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,39 @@
1
+ require 'hooks'
2
+
3
+ module Shanty
4
+ module Mixins
5
+ # A mixin to implement publish/subscribe style callbacks in the class that
6
+ # includes this.
7
+ module Callbacks
8
+ # The self.included idiom. This is described in great detail in a
9
+ # fantastic blog post here:
10
+ #
11
+ # http://www.railstips.org/blog/archives/2009/05/15/include-vs-extend-in-ruby/
12
+ #
13
+ # Basically, this idiom allows us to add both instance *and* class methods
14
+ # to the class that is mixing this module into itself without forcing them
15
+ # to call extend and include for this mixin. You'll see this idiom everywhere
16
+ # in the Ruby/Rails world, so we use it too.
17
+ def self.included(cls)
18
+ cls.include(Hooks)
19
+ cls.extend(ClassMethods)
20
+ end
21
+
22
+ # Common methods inherited by all classes
23
+ module ClassMethods
24
+ def subscribe(*names, sym)
25
+ names.each do |name|
26
+ define_hook(name, halts_on_falsey: true) unless respond_to?(name)
27
+ send(name, sym)
28
+ end
29
+ end
30
+ end
31
+
32
+ def publish(name, *args)
33
+ return true if self.class.callbacks_for_hook(name).nil?
34
+ results = run_hook(name, *args)
35
+ !results.halted?
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,23 @@
1
+ require 'shanty/util'
2
+
3
+ module Shanty
4
+ # Public: enables mutation of the project graph
5
+ # Common usage would be to set changed flags on projects
6
+ class Mutator
7
+ class << self
8
+ attr_reader :mutators
9
+ end
10
+
11
+ def self.inherited(mutator)
12
+ Util.logger.debug("Detected mutator #{mutator}")
13
+ @mutators ||= []
14
+ @mutators << mutator
15
+ end
16
+
17
+ def apply_mutations(graph)
18
+ self.class.mutators.reduce(graph) do |acc, mutator|
19
+ mutator.new.mutate(acc)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ require 'shanty/mutator'
2
+ require 'shanty/vcs_ranges/local_git'
3
+
4
+ module Shanty
5
+ # Git VCS mutator
6
+ class Git < Mutator
7
+ def initialize
8
+ @vcs_range = VCSRange.new
9
+ end
10
+
11
+ def mutate(graph)
12
+ git_root = `git rev-parse --show-toplevel`.strip
13
+ diff_files = `git diff --name-only #{@vcs_range.from_commit} #{@vcs_range.to_commit}`.split("\n")
14
+ diff_files.each do |path|
15
+ next if path.nil?
16
+ path = File.join(git_root, path)
17
+ project = graph.owner_of_file(path)
18
+ project.changed = true unless project.nil?
19
+ end
20
+
21
+ graph
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ require 'shanty/util'
2
+ require 'shanty/project_template'
3
+
4
+ module Shanty
5
+ # Some basic functionality for every plugin.
6
+ module Plugin
7
+ def self.extended(mod)
8
+ mod.module_eval do
9
+ def self.included(cls)
10
+ copy_subscribes_to_class(cls)
11
+ end
12
+ end
13
+ end
14
+
15
+ def copy_subscribes_to_class(cls)
16
+ @subscriptions.each do |args|
17
+ cls.subscribe(*args)
18
+ end
19
+ end
20
+
21
+ def subscribe(*args)
22
+ @subscriptions ||= []
23
+ @subscriptions.push(args)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ require 'shanty/plugin'
2
+
3
+ module Shanty
4
+ # Public: Bundler plugin for building ruby projects.
5
+ module BundlerPlugin
6
+ extend Plugin
7
+
8
+ subscribe :build, :bundle_install
9
+
10
+ def bundle_install
11
+ # FIXME: Add support for the --jobs argument to parallelise the bundler run.
12
+ system 'bundle install --quiet'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ require 'shanty/plugin'
2
+
3
+ module Shanty
4
+ # Public: Rspec plugin for testing ruby projects.
5
+ module RspecPlugin
6
+ extend Plugin
7
+
8
+ subscribe :test, :rspec
9
+
10
+ def rspec
11
+ system 'rspec'
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ require 'shanty/plugin'
2
+
3
+ module Shanty
4
+ # Public: Rubocop plugin for style checking ruby projects.
5
+ module RubocopPlugin
6
+ extend Plugin
7
+
8
+ subscribe :test, :rubocop
9
+
10
+ def rubocop
11
+ system 'rubocop'
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,71 @@
1
+ require 'shanty/mixins/acts_as_link_graph_node'
2
+ require 'shanty/mixins/callbacks'
3
+
4
+ module Shanty
5
+ # Public: Represents a project in the current repository.
6
+ class Project
7
+ include Mixins::ActsAsLinkGraphNode
8
+ include Mixins::Callbacks
9
+
10
+ attr_accessor :name, :path, :options, :parents_by_name, :changed
11
+ attr_reader :changed
12
+ alias_method :changed?, :changed
13
+
14
+ # Public: Initialise the Project instance.
15
+ #
16
+ # project_template - An instance of ProjectTemplate from which to
17
+ # instantiate this project.
18
+ def initialize(project_template)
19
+ @name = project_template.name
20
+ @path = project_template.path
21
+ @options = project_template.options
22
+ @parents_by_name = project_template.parents
23
+ @changed = false
24
+
25
+ project_template.plugins.each do |plugin|
26
+ self.class.include plugin
27
+ end
28
+
29
+ instance_eval(&project_template.after_create) unless project_template.after_create.nil?
30
+ end
31
+
32
+ # Public: A list of the external dependencies this project has by name
33
+ # and version. This is used in dependency tree generation.
34
+ #
35
+ # Returns an Array of Strings representing external dependencies by name
36
+ # and version.
37
+ def externals_by_name
38
+ []
39
+ end
40
+
41
+ # Public: The absolute path to the artifact that would be created by this
42
+ # project when built, if any. This is expected to be overriden in subclasses.
43
+ #
44
+ # Returns a String representing the absolute path to the artifact.
45
+ def artifact_path
46
+ nil
47
+ end
48
+
49
+ # Public: Overriden String conversion method to return a simplified
50
+ # representation of this instance that doesn't include the cyclic
51
+ # parent/children attributes as defined by the ActsAsLinkGraphNode mixin.
52
+ #
53
+ # Returns a simple String representation of this instance.
54
+ def to_s
55
+ "Name: #{name}"
56
+ end
57
+
58
+ # Public: Overriden String conversion method to return a more detailed
59
+ # representation of this instance that doesn't include the cyclic
60
+ # parent/children attributes as defined by the ActsAsLinkGraphNode mixin.
61
+ #
62
+ # Returns more detailed String representation of this instance.
63
+ def inspect
64
+ {
65
+ name: name,
66
+ path: path,
67
+ options: options
68
+ }.inspect
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,53 @@
1
+ require 'shanty/mixins/attr_combined_accessor'
2
+ require 'shanty/projects/static'
3
+
4
+ module Shanty
5
+ # Public: Allows creation of a project using a discoverer
6
+ class ProjectTemplate
7
+ extend Mixins::AttrCombinedAccessor
8
+
9
+ attr_combined_accessor :name, :type, :plugins, :parents, :options
10
+ attr_reader :path
11
+
12
+ def initialize(path, args = {})
13
+ fail 'Path to project must be a directory.' unless File.directory?(path)
14
+
15
+ @path = path
16
+ @name = File.basename(path)
17
+ @type = args[:type] || StaticProject
18
+ @plugins = args[:plugins] || []
19
+ @parents = args[:parents] || []
20
+ @options = args[:options] || {}
21
+
22
+ execute_shantyfile
23
+ end
24
+
25
+ def execute_shantyfile
26
+ shantyfile_path = File.join(@path, 'Shantyfile')
27
+
28
+ return unless File.exist?(shantyfile_path)
29
+
30
+ eval(File.read(shantyfile_path))
31
+ end
32
+
33
+ def plugin(plugin)
34
+ @plugins << plugin
35
+ end
36
+
37
+ def parent(parent)
38
+ @parents << parent
39
+ end
40
+
41
+ def option(key, value)
42
+ @options[key] = value
43
+ end
44
+
45
+ def after_create(&block)
46
+ if block.nil?
47
+ @after_create
48
+ else
49
+ @after_create = block
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,12 @@
1
+ require 'shanty/project'
2
+
3
+ module Shanty
4
+ # Public: Represents a projects created with the Ruby language.
5
+ class RubyProject < Project
6
+ subscribe :build, :on_build
7
+
8
+ def on_build
9
+ system 'gem build *.gemspec'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ require 'shanty/project'
2
+
3
+ module Shanty
4
+ # Public: Base type of project, simply creates a tarball of the directory
5
+ class StaticProject < Project
6
+ subscribe :build, :on_build
7
+
8
+ def on_build
9
+ # FIXME: Create a tarball of the current project.
10
+ true
11
+ end
12
+
13
+ # Public: The absolute path to the artifact that would be created by this
14
+ # project when built.
15
+ #
16
+ # Returns a String representing the absolute path to the artifact.
17
+ def artifact_path
18
+ "#{@root_dir}/build/#{@options['artifact_name'] || name}-#{@build_number}.tgz"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,45 @@
1
+ module Shanty
2
+ # Public: Discover shanty tasks
3
+ class Task
4
+ class << self
5
+ attr_reader :tasks
6
+ attr_reader :task_definitions, :task_definiton
7
+ end
8
+
9
+ # This method is auto-triggred by Ruby whenever a class inherits from
10
+ # Shanty::Task. This means we can build up a list of all the tasks
11
+ # without requiring them to register with us - neat!
12
+ def self.inherited(task)
13
+ @tasks ||= []
14
+ @tasks << task
15
+ end
16
+
17
+ def self.desc(desc)
18
+ task_definition[:desc] = desc
19
+ end
20
+
21
+ def self.param(name, options = {})
22
+ task_definition[:params][name] = options
23
+ end
24
+
25
+ def self.option(name, options = {})
26
+ task_definition[:options][name] = options
27
+ end
28
+
29
+ def self.method_added(name)
30
+ @task_definitions ||= {}
31
+ @task_definitions[name] = task_definition.merge(klass: self)
32
+
33
+ # Now reset the task definition.
34
+ @task_definition = {}
35
+ end
36
+
37
+ def self.task_definition
38
+ @task_definition ||= {}
39
+ @task_definition[:params] ||= {}
40
+ @task_definition[:options] ||= {}
41
+
42
+ @task_definition
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,41 @@
1
+ require 'i18n'
2
+ require 'shanty/task'
3
+ require 'shanty/util'
4
+
5
+ module Shanty
6
+ # Public: A set of basic tasks that can be applied to all projects and that
7
+ # ship with the core of Shanty.
8
+ class BasicTasks < Shanty::Task
9
+ desc 'tasks.projects.desc'
10
+ option :changed, type: :boolean, desc: 'tasks.common.options.changed'
11
+ option :types, type: :array, desc: 'tasks.common.options.types'
12
+ def projects(options, graph)
13
+ graph.projects.each do |project|
14
+ next if options.changed? && !project.changed?
15
+ puts project
16
+ end
17
+ end
18
+
19
+ desc 'tasks.build.desc'
20
+ option :changed, type: :boolean, desc: 'tasks.common.options.changed'
21
+ option :watch, type: :boolean, desc: 'tasks.common.options.watch'
22
+ option :types, type: :array, desc: 'tasks.common.options.types'
23
+ def build(options, graph)
24
+ graph.projects.each do |project|
25
+ next if options.changed? && !project.changed?
26
+ fail I18n.t('tasks.build.failed', project: project) unless project.publish(:build)
27
+ end
28
+ end
29
+
30
+ desc 'tasks.test.desc'
31
+ option :changed, type: :boolean, desc: 'tasks.common.options.changed'
32
+ option :watch, type: :boolean, desc: 'tasks.common.options.watch'
33
+ option :types, type: :array, desc: 'tasks.common.options.types'
34
+ def test(options, graph)
35
+ graph.projects.each do |project|
36
+ next if options.changed? && !project.changed?
37
+ fail I18n.t('tasks.test.failed', project: project) unless project.publish(:test)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,12 @@
1
+ require 'logger'
2
+
3
+ module Shanty
4
+ # Utility module for common tasks
5
+ module Util
6
+ module_function
7
+
8
+ def logger
9
+ @logger ||= Logger.new(STDOUT)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,31 @@
1
+ require 'shanty/util'
2
+
3
+ module Shanty
4
+ # Public: Enables discovery of rangeerent types of CI provider
5
+ # utilises inherited class method to find all implementing
6
+ # classes
7
+ class VCSRange
8
+ class << self
9
+ attr_reader :vcs_range
10
+ end
11
+
12
+ def self.inherited(vcs_range)
13
+ Util.logger.debug("Detected VCS range #{vcs_range}")
14
+ @vcs_range = vcs_range
15
+ end
16
+
17
+ def from_commit
18
+ vcs_range.from_commit
19
+ end
20
+
21
+ def to_commit
22
+ vcs_range.to_commit
23
+ end
24
+
25
+ private
26
+
27
+ def vcs_range
28
+ @vcs_range ||= self.class.vcs_range.new
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ require 'shanty/vcs_range'
2
+
3
+ module Shanty
4
+ # Use range of local vs remote changes
5
+ class LocalGit < VCSRange
6
+ def from_commit
7
+ "origin/#{branch}"
8
+ end
9
+
10
+ def to_commit
11
+ 'HEAD'
12
+ end
13
+
14
+ private
15
+
16
+ def branch
17
+ `git rev-parse --abbrev-ref HEAD`.strip
18
+ end
19
+ end
20
+ end
data/lib/shanty.rb ADDED
@@ -0,0 +1,47 @@
1
+ require 'i18n'
2
+ require 'pathname'
3
+ require 'pry'
4
+
5
+ require 'shanty/cli'
6
+ require 'shanty/discoverers/shantyfile'
7
+ require 'shanty/graph'
8
+ require 'shanty/global'
9
+ require 'shanty/mutators/git'
10
+ require 'shanty/tasks/basic'
11
+
12
+ module Shanty
13
+ # Main shanty class
14
+ class Shanty
15
+ GEM_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
16
+
17
+ def start!
18
+ setup_i18n
19
+ Cli.new(graph).run
20
+ end
21
+
22
+ def graph
23
+ @graph ||= construct_project_graph
24
+ end
25
+
26
+ private
27
+
28
+ def setup_i18n
29
+ I18n.enforce_available_locales = true
30
+ I18n.load_path = Dir[File.join(GEM_ROOT, 'translations', '*.yml')]
31
+ end
32
+
33
+ def construct_project_graph
34
+ project_templates = Dir.chdir(Global.root) do
35
+ Discoverer.new.discover_all
36
+ end
37
+
38
+ projects = project_templates.map do |project_template|
39
+ project_template.type.new(project_template)
40
+ end
41
+
42
+ graph = Graph.new(projects)
43
+
44
+ Mutator.new.apply_mutations(graph)
45
+ end
46
+ end
47
+ end
metadata ADDED
@@ -0,0 +1,315 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shanty
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Jansen
8
+ - Nathan Kleyn
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-10-31 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: algorithms
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '0.6'
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.6.1
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - "~>"
29
+ - !ruby/object:Gem::Version
30
+ version: '0.6'
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.6.1
34
+ - !ruby/object:Gem::Dependency
35
+ name: commander
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.2'
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 4.2.1
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - "~>"
49
+ - !ruby/object:Gem::Version
50
+ version: '4.2'
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 4.2.1
54
+ - !ruby/object:Gem::Dependency
55
+ name: deep_merge
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 1.0.1
64
+ type: :runtime
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: '1.0'
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 1.0.1
74
+ - !ruby/object:Gem::Dependency
75
+ name: graph
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '2.6'
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 2.6.0
84
+ type: :runtime
85
+ prerelease: false
86
+ version_requirements: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '2.6'
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: 2.6.0
94
+ - !ruby/object:Gem::Dependency
95
+ name: hooks
96
+ requirement: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '0.4'
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 0.4.0
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.4'
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: 0.4.0
114
+ - !ruby/object:Gem::Dependency
115
+ name: i18n
116
+ requirement: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - "~>"
119
+ - !ruby/object:Gem::Version
120
+ version: '0.6'
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: 0.6.11
124
+ type: :runtime
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '0.6'
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: 0.6.11
134
+ - !ruby/object:Gem::Dependency
135
+ name: pry-byebug
136
+ requirement: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - "~>"
139
+ - !ruby/object:Gem::Version
140
+ version: '1.3'
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: 1.3.3
144
+ type: :development
145
+ prerelease: false
146
+ version_requirements: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - "~>"
149
+ - !ruby/object:Gem::Version
150
+ version: '1.3'
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: 1.3.3
154
+ - !ruby/object:Gem::Dependency
155
+ name: rspec
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - "~>"
159
+ - !ruby/object:Gem::Version
160
+ version: '3.0'
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: 3.0.0
164
+ type: :development
165
+ prerelease: false
166
+ version_requirements: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - "~>"
169
+ - !ruby/object:Gem::Version
170
+ version: '3.0'
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: 3.0.0
174
+ - !ruby/object:Gem::Dependency
175
+ name: rubocop
176
+ requirement: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '0.24'
181
+ - - ">="
182
+ - !ruby/object:Gem::Version
183
+ version: 0.24.1
184
+ type: :development
185
+ prerelease: false
186
+ version_requirements: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - "~>"
189
+ - !ruby/object:Gem::Version
190
+ version: '0.24'
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: 0.24.1
194
+ - !ruby/object:Gem::Dependency
195
+ name: rubocop-rspec
196
+ requirement: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - "~>"
199
+ - !ruby/object:Gem::Version
200
+ version: '1.1'
201
+ - - ">="
202
+ - !ruby/object:Gem::Version
203
+ version: 1.1.0
204
+ type: :development
205
+ prerelease: false
206
+ version_requirements: !ruby/object:Gem::Requirement
207
+ requirements:
208
+ - - "~>"
209
+ - !ruby/object:Gem::Version
210
+ version: '1.1'
211
+ - - ">="
212
+ - !ruby/object:Gem::Version
213
+ version: 1.1.0
214
+ - !ruby/object:Gem::Dependency
215
+ name: rake
216
+ requirement: !ruby/object:Gem::Requirement
217
+ requirements:
218
+ - - "~>"
219
+ - !ruby/object:Gem::Version
220
+ version: '10.3'
221
+ - - ">="
222
+ - !ruby/object:Gem::Version
223
+ version: 10.3.2
224
+ type: :development
225
+ prerelease: false
226
+ version_requirements: !ruby/object:Gem::Requirement
227
+ requirements:
228
+ - - "~>"
229
+ - !ruby/object:Gem::Version
230
+ version: '10.3'
231
+ - - ">="
232
+ - !ruby/object:Gem::Version
233
+ version: 10.3.2
234
+ - !ruby/object:Gem::Dependency
235
+ name: coveralls
236
+ requirement: !ruby/object:Gem::Requirement
237
+ requirements:
238
+ - - "~>"
239
+ - !ruby/object:Gem::Version
240
+ version: '0.7'
241
+ - - ">="
242
+ - !ruby/object:Gem::Version
243
+ version: 0.7.1
244
+ type: :development
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - "~>"
249
+ - !ruby/object:Gem::Version
250
+ version: '0.7'
251
+ - - ">="
252
+ - !ruby/object:Gem::Version
253
+ version: 0.7.1
254
+ description: Pluggable, destributed continous delivery framework. Works with your
255
+ existing build, test, CI, deployment, or indeed any tools to provide a cohesive
256
+ way to do continuous delivery.
257
+ email:
258
+ - nathan@nathankleyn.com
259
+ executables:
260
+ - shanty
261
+ extensions: []
262
+ extra_rdoc_files: []
263
+ files:
264
+ - README.md
265
+ - bin/shanty
266
+ - lib/shanty.rb
267
+ - lib/shanty/cli.rb
268
+ - lib/shanty/discoverer.rb
269
+ - lib/shanty/discoverers/gradle.rb
270
+ - lib/shanty/discoverers/shantyfile.rb
271
+ - lib/shanty/global.rb
272
+ - lib/shanty/graph.rb
273
+ - lib/shanty/mixins/acts_as_link_graph_node.rb
274
+ - lib/shanty/mixins/attr_combined_accessor.rb
275
+ - lib/shanty/mixins/callbacks.rb
276
+ - lib/shanty/mutator.rb
277
+ - lib/shanty/mutators/git.rb
278
+ - lib/shanty/plugin.rb
279
+ - lib/shanty/plugins/bundler.rb
280
+ - lib/shanty/plugins/rspec.rb
281
+ - lib/shanty/plugins/rubocop.rb
282
+ - lib/shanty/project.rb
283
+ - lib/shanty/project_template.rb
284
+ - lib/shanty/projects/ruby.rb
285
+ - lib/shanty/projects/static.rb
286
+ - lib/shanty/task.rb
287
+ - lib/shanty/tasks/basic.rb
288
+ - lib/shanty/util.rb
289
+ - lib/shanty/vcs_range.rb
290
+ - lib/shanty/vcs_ranges/local_git.rb
291
+ homepage: https://github.com/shantytown/shanty
292
+ licenses:
293
+ - MIT
294
+ metadata: {}
295
+ post_install_message:
296
+ rdoc_options: []
297
+ require_paths:
298
+ - lib
299
+ required_ruby_version: !ruby/object:Gem::Requirement
300
+ requirements:
301
+ - - ">="
302
+ - !ruby/object:Gem::Version
303
+ version: '0'
304
+ required_rubygems_version: !ruby/object:Gem::Requirement
305
+ requirements:
306
+ - - ">="
307
+ - !ruby/object:Gem::Version
308
+ version: '0'
309
+ requirements: []
310
+ rubyforge_project:
311
+ rubygems_version: 2.2.2
312
+ signing_key:
313
+ specification_version: 4
314
+ summary: Pluggable, destributed continous delivery framework.
315
+ test_files: []