cartage 1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.autotest +8 -0
- data/.gemtest +1 -0
- data/.minitest.rb +2 -0
- data/.travis.yml +36 -0
- data/Cartage.yml.rdoc +271 -0
- data/Contributing.rdoc +66 -0
- data/Gemfile +9 -0
- data/History.rdoc +5 -0
- data/Licence.rdoc +27 -0
- data/Manifest.txt +24 -0
- data/README.rdoc +192 -0
- data/Rakefile +62 -0
- data/bin/cartage +8 -0
- data/cartage.yml.sample +172 -0
- data/lib/cartage.rb +496 -0
- data/lib/cartage/command.rb +59 -0
- data/lib/cartage/config.rb +193 -0
- data/lib/cartage/manifest.rb +218 -0
- data/lib/cartage/manifest/commands.rb +106 -0
- data/lib/cartage/pack_command.rb +14 -0
- data/lib/cartage/plugin.rb +80 -0
- data/test/minitest_config.rb +72 -0
- data/test/test_cartage.rb +261 -0
- data/test/test_cartage_config.rb +47 -0
- metadata +348 -0
data/README.rdoc
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
= Cartage by Kinetic Cafe
|
2
|
+
|
3
|
+
code :: https://github.com/KineticCafe/cartage/
|
4
|
+
issues :: https://github.com/KineticCafe/cartage/issues
|
5
|
+
docs :: http://www.rubydoc.info/github/KineticCafe/cartage/master
|
6
|
+
continuous integration :: {<img src="https://travis-ci.org/KineticCafe/cartage.svg?branch=master" alt="Build Status" />}[https://travis-ci.org/KineticCafe/cartage]
|
7
|
+
|
8
|
+
== Description
|
9
|
+
|
10
|
+
Cartage provides a plug-in based tool to reliably create a package for a
|
11
|
+
Bundler-based Ruby application that can be used in deployment with a
|
12
|
+
configuration tool like Ansible, Chef, Puppet, or Salt. The package is created
|
13
|
+
with its dependencies bundled in +vendor/bundle+, so it can be deployed in
|
14
|
+
environments with strict access control rules and without requiring development
|
15
|
+
tool access.
|
16
|
+
|
17
|
+
Cartage has learned its tricks from Heroku, Capistrano, and Hoe. From Hoe, it
|
18
|
+
learned to keep a manifest to control what is packaged (as well as its plug-in
|
19
|
+
system). From Heroku, it learned to keep a simple ignore file. From Capistrano,
|
20
|
+
it learned to mark the Git hashref as a file in its built package, and to
|
21
|
+
timestamp the packages.
|
22
|
+
|
23
|
+
Cartage follows a relatively simple set of steps when creating a package:
|
24
|
+
|
25
|
+
1. Copy the application files to the work area. The application’s files are
|
26
|
+
specified in +Manifest.txt+ and filtered against the exclusion list
|
27
|
+
(+.cartignore+). If there is no +.cartignore+, try to use +.slugignore+. If
|
28
|
+
there is no +.slugignore+, Cartage will use a sensible default exclusion
|
29
|
+
list. To override the use of this exclusion list, an empty +.cartignore+
|
30
|
+
file must be present.
|
31
|
+
|
32
|
+
2. The Git hashref is written to the work area (as +release_hashref+) and to
|
33
|
+
the package staging area.
|
34
|
+
|
35
|
+
3. Files that have been modified are restored to pristine condition in the
|
36
|
+
work area. The source files are not touched. (This ensures that
|
37
|
+
+config/database.yml+, for example, will not be the version used by a
|
38
|
+
continuous integration system.)
|
39
|
+
|
40
|
+
4. Bundler is fetched into the work area, and the bundle is installed into the
|
41
|
+
work area’s +vendor/bundle+ without the +development+ and +test+
|
42
|
+
environments. If a bundle cache is kept (by default, one is), the resulting
|
43
|
+
+vendor/bundle+ will be put into a bundle cache so that future bundle
|
44
|
+
installs are faster.
|
45
|
+
|
46
|
+
5. A timestamped tarball is created from the contents of the work area. It can
|
47
|
+
then be copied to a more permanent or accessible location.
|
48
|
+
|
49
|
+
Cartage is extremely opinionated about its tools and environment:
|
50
|
+
|
51
|
+
* The packages are created with +tar+ and +bzip2+ using <tt>tar cfj</tt>.
|
52
|
+
* Cartage only understands +git+, which is used for creating
|
53
|
+
<tt>release_hashref</tt>s, +Manifest.txt+ creation and comparison, and even
|
54
|
+
default application name detection (from the name of the origin remote).
|
55
|
+
|
56
|
+
== Synopsis
|
57
|
+
|
58
|
+
# Build a package from the current machine, using the Manifest.txt.
|
59
|
+
cartage pack
|
60
|
+
|
61
|
+
# Create or update a Manifest.txt from the current repository.
|
62
|
+
cartage manifest
|
63
|
+
# Check the current Manifest against the files that should be there.
|
64
|
+
cartage check
|
65
|
+
|
66
|
+
# Create a .cartignore file for use.
|
67
|
+
cartage install-ignore
|
68
|
+
# Overwrite the current .cartignore with the default.
|
69
|
+
cartage install-ignore --force
|
70
|
+
# Merge the current .cartignore with the default. Merging automatically
|
71
|
+
# removes any comments.
|
72
|
+
cartage install-ignore --merge
|
73
|
+
|
74
|
+
== Install
|
75
|
+
|
76
|
+
Add cartage to your Gemfile:
|
77
|
+
|
78
|
+
gem 'cartage', '~> 1.0', groups: [ :development, :test ]
|
79
|
+
|
80
|
+
Or manually install:
|
81
|
+
|
82
|
+
% gem install cartage
|
83
|
+
|
84
|
+
== Cartage Plug-Ins
|
85
|
+
|
86
|
+
Cartage is extensible by plug-ins. This version of the plug-in system provides
|
87
|
+
one integration point, subcommands. Cartage is implemented with
|
88
|
+
{cmdparse}[http://cmdparse.gettalong.org/], and plug-ins implement a class that
|
89
|
+
performs the work (nested under the Cartage namespace) and one or more commands
|
90
|
+
that execute on the work. Plug-in files are discovered immediately relative to
|
91
|
+
the +cartage+ directory, and are registered on inheritance from
|
92
|
+
Cartage::Plugin. This plug-in would be found in <tt>'cartage/info.rb'</tt>.
|
93
|
+
Each plug-in will get a lazy-instantiation constructor added to Cartage itself
|
94
|
+
that provides a reference to the owning Cartage instance.
|
95
|
+
|
96
|
+
Below is an example of a Cartage plug-in to provide a <tt>cartage info</tt>
|
97
|
+
command. This command isn’t very useful, since most values used in Cartage
|
98
|
+
packaging are lazily resolved, but it demonstrates how simple a plug-in can be.
|
99
|
+
|
100
|
+
require 'cartage/plugin'
|
101
|
+
require 'cartage/command'
|
102
|
+
|
103
|
+
# An instance of this will be created lazily by calling Cartage#info.
|
104
|
+
class Cartage::Info < Cartage::Plugin
|
105
|
+
# It does not matter what this method is. It’s just a public instance
|
106
|
+
# method used by the command.
|
107
|
+
def run
|
108
|
+
@cartage.instance_variables.sort.each { |ivar|
|
109
|
+
puts "#{ivar}: #{@cartage.instance_variable_get(ivar)}"
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
# This will create a CmdParse command that responds to 'info' and takes
|
114
|
+
# no subcommands. If the plug-in provides a number of features, it is
|
115
|
+
# recommended that subcommands be used.
|
116
|
+
class InfoCommand < Cartage::Command
|
117
|
+
# The Cartage::Command objects are initialized with the cartage
|
118
|
+
# instance. Set the command name with the string here.
|
119
|
+
def initialize(cartage)
|
120
|
+
super(cartage, 'info', takes_commands: false)
|
121
|
+
short_desc('Shows configuration information about Cartage.')
|
122
|
+
end
|
123
|
+
|
124
|
+
# This is run by Cartage::Command#execute to make the command happen.
|
125
|
+
# An exception should be thrown on failure, as return values do not
|
126
|
+
# affect the status code of the application.
|
127
|
+
def perform
|
128
|
+
@cartage.info.run
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# This returns the command(s) created by this plug-in.
|
133
|
+
def self.commands
|
134
|
+
[ Cartage::Info::InfoCommand ]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
For a more comprehensive example, see the implementation of Manifest
|
139
|
+
(<tt>lib/cartage/manifest.rb</tt>) and its commands
|
140
|
+
(<tt>lib/cartage/manifest/commands.rb</tt>).
|
141
|
+
|
142
|
+
Future releases of Cartage will offer additional ways to extend Cartage.
|
143
|
+
|
144
|
+
== Alternate Projects
|
145
|
+
|
146
|
+
The closest project to Cartage is {pkgr}[https://github.com/crohr/pkgr]. Pkgr
|
147
|
+
will create a distribution package for Ubuntu (14.04 and 12.02, Debian 7),
|
148
|
+
CentOS 6, SLES 12, and Fedora 20.
|
149
|
+
|
150
|
+
Both Cartage and Pkgr provide isolated builds with all in-application
|
151
|
+
dependencies included.
|
152
|
+
|
153
|
+
Pkgr offers several advantages over Cartage:
|
154
|
+
|
155
|
+
* It supports more languages than just Ruby (suport for Go and Node.js are
|
156
|
+
confirmed and more are in progress). Cartage will probably have more language
|
157
|
+
support in the future, but it is not an immediate priority. For Ruby and
|
158
|
+
Node.js, the interpreters are included locally to the application.
|
159
|
+
|
160
|
+
* It creates a distribution package, meaning that you can just use +apt-get+ to
|
161
|
+
install or upgrade your package.
|
162
|
+
|
163
|
+
* It reuses Heroku buildpacks (this requires that your application behave a bit
|
164
|
+
more like a Heroku application, which may be a bit more
|
165
|
+
|
166
|
+
Cartage offers advantages over Pkgr:
|
167
|
+
|
168
|
+
* Cartage offers a plug-in based extension system. While the plug-in system is
|
169
|
+
currently limited to adding new +cartage+ commands, this is not its only
|
170
|
+
possible use. Existing plug-ins are +cartage-s3+ (upload the resulting
|
171
|
+
package to S3), +cartage-remote+ (build on a remote machine), and
|
172
|
+
+cartage-rack+ (provide a Rack application to read the +release_hashref+ over
|
173
|
+
an API call).
|
174
|
+
|
175
|
+
* Cartage makes it easier to integrate into a workflow translated from
|
176
|
+
Capistrano, as it essentially replaces the source control checkout stage.
|
177
|
+
This process makes it really easy to integrate into an Ansible playbook (as
|
178
|
+
we have done at Kinetic Cafe).
|
179
|
+
|
180
|
+
== Cartage Semantic Versioning
|
181
|
+
|
182
|
+
Cartage uses a {Semantic Versioning}[http://semver.org/] scheme with one
|
183
|
+
significant change:
|
184
|
+
|
185
|
+
* When PATCH is zero (+0+), it will be omitted from version references.
|
186
|
+
|
187
|
+
Additionally, the major version will generally be reserved for plug-in
|
188
|
+
infrastructure changes.
|
189
|
+
|
190
|
+
:include: Contributing.rdoc
|
191
|
+
|
192
|
+
:include: Licence.rdoc
|
data/Rakefile
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require 'pathname'
|
6
|
+
|
7
|
+
Hoe.plugin :doofus
|
8
|
+
Hoe.plugin :email unless ENV['CI'] or ENV['TRAVIS']
|
9
|
+
Hoe.plugin :gemspec2
|
10
|
+
Hoe.plugin :git
|
11
|
+
Hoe.plugin :minitest
|
12
|
+
Hoe.plugin :rubygems
|
13
|
+
Hoe.plugin :travis
|
14
|
+
|
15
|
+
spec = Hoe.spec 'cartage' do
|
16
|
+
developer('Austin Ziegler', 'aziegler@kineticcafe.com')
|
17
|
+
|
18
|
+
self.history_file = 'History.rdoc'
|
19
|
+
self.readme_file = 'README.rdoc'
|
20
|
+
self.extra_rdoc_files = FileList['*.rdoc'].to_a
|
21
|
+
|
22
|
+
license 'MIT'
|
23
|
+
|
24
|
+
self.extra_deps << ['cmdparse', '~> 3.0']
|
25
|
+
|
26
|
+
self.extra_dev_deps << ['rake', '~> 10.0']
|
27
|
+
self.extra_dev_deps << ['hoe-doofus', '~> 1.0']
|
28
|
+
self.extra_dev_deps << ['hoe-gemspec2', '~> 1.1']
|
29
|
+
self.extra_dev_deps << ['hoe-git', '~> 1.5']
|
30
|
+
self.extra_dev_deps << ['hoe-geminabox', '~> 0.3']
|
31
|
+
self.extra_dev_deps << ['hoe-travis', '~> 1.2']
|
32
|
+
self.extra_dev_deps << ['minitest', '~> 5.4']
|
33
|
+
self.extra_dev_deps << ['minitest-autotest', '~> 1.0']
|
34
|
+
self.extra_dev_deps << ['minitest-bisect', '~> 1.2']
|
35
|
+
self.extra_dev_deps << ['minitest-focus', '~> 1.1']
|
36
|
+
self.extra_dev_deps << ['minitest-moar', '~> 0.0']
|
37
|
+
self.extra_dev_deps << ['minitest-pretty_diff', '~> 0.1']
|
38
|
+
self.extra_dev_deps << ['simplecov', '~> 0.7']
|
39
|
+
end
|
40
|
+
|
41
|
+
module Hoe::Publish
|
42
|
+
alias_method :original_make_rdoc_cmd, :make_rdoc_cmd
|
43
|
+
|
44
|
+
def make_rdoc_cmd(*extra_args) # :nodoc:
|
45
|
+
spec.extra_rdoc_files.reject! { |f| f == 'Manifest.txt' }
|
46
|
+
original_make_rdoc_cmd(*extra_args)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
namespace :test do
|
51
|
+
task :coverage do
|
52
|
+
prelude = <<-EOS
|
53
|
+
require "simplecov"
|
54
|
+
SimpleCov.start("test_frameworks") { command_name "Minitest" }
|
55
|
+
gem "minitest"
|
56
|
+
EOS
|
57
|
+
spec.test_prelude = prelude.split($/).join('; ')
|
58
|
+
Rake::Task['test'].execute
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# vim: syntax=ruby
|
data/bin/cartage
ADDED
data/cartage.yml.sample
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
---
|
2
|
+
# The name of the application. Optional, defaults to the basename of the origin
|
3
|
+
# Git URL. Overridden with <tt>cartage --name NAME</tt>.
|
4
|
+
# ---
|
5
|
+
# name: my-application
|
6
|
+
|
7
|
+
# The target path for the Cartage package. Optional and defaults to
|
8
|
+
# <tt>./tmp</tt>. Overridden with <tt>cartage --target PATH</tt>.
|
9
|
+
# ---
|
10
|
+
# target: tmp/cartage
|
11
|
+
|
12
|
+
# The root path of the application. Optional, defaults to the top of the Git
|
13
|
+
# repository (<tt>git rev-parse --show-cdup</tt>). Overridden with <tt>cartage
|
14
|
+
# --root-path ROOT_PATH</tt>.
|
15
|
+
# ---
|
16
|
+
# root_path: .
|
17
|
+
|
18
|
+
# The timestamp for the final package (which is
|
19
|
+
# <tt><em>name</em>-<em>timestamp</em></tt>). Optional, defaults to the current
|
20
|
+
# time in UTC. Overridden with <tt>cartage --timestamp TIMESTAMP</tt>. This
|
21
|
+
# value is *not* validated to be a time value when supplied.
|
22
|
+
# ---
|
23
|
+
# timestamp: not-a-timestamp
|
24
|
+
|
25
|
+
# The bundle cache path, where the package’s <tt>vendor-bundle.tar.bz2</tt>
|
26
|
+
# will be stored between builds. Optional, defaults to <tt>./tmp</tt>.
|
27
|
+
# Overridden with <tt>cartage --bundle-cache BUNDLE_CACHE</tt>.
|
28
|
+
# ---
|
29
|
+
# bundle_cache: tmp/cache
|
30
|
+
|
31
|
+
# The groups to exclude from <tt>bundle install</tt>. Optional, defaults to
|
32
|
+
# <tt>[ "development", "test"]</tt>. Overridden with <tt>cartage --without
|
33
|
+
# group1,group2</tt>.
|
34
|
+
# ---
|
35
|
+
# without:
|
36
|
+
# - development
|
37
|
+
# - test
|
38
|
+
# - other
|
39
|
+
|
40
|
+
# This section is for all plug-in configuration.
|
41
|
+
plugins:
|
42
|
+
# This section is for cartage-s3.
|
43
|
+
s3:
|
44
|
+
# The path to the cartage S3 bucket or directory (for another service that
|
45
|
+
# Fog::Storage supports). This has no default and is overridden with
|
46
|
+
# <tt>cartage s3 --path PATH</tt>.
|
47
|
+
# ---
|
48
|
+
# path: cartage-bucket
|
49
|
+
|
50
|
+
# The credentials dictionary passed to Fog::Storage for uploads. Each
|
51
|
+
# provider has different keys. If present, this will dictionary be used in
|
52
|
+
# preference to <tt>cartage s3</tt> flags <tt>--key-id</tt>,
|
53
|
+
# <tt>--secret-key</tt>, and <tt>--region</tt> as those work *only* with
|
54
|
+
# Amazon AWS S3.
|
55
|
+
credentials:
|
56
|
+
# The name of the provider.
|
57
|
+
# ---
|
58
|
+
# provider: AWS
|
59
|
+
# provider: Rackspace
|
60
|
+
# provider: Google
|
61
|
+
|
62
|
+
# The name of the access user. The name of this key varies per provider.
|
63
|
+
# ---
|
64
|
+
# aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID
|
65
|
+
# rackspace_username: RACKSPACE_USERNAME
|
66
|
+
# google_storage_access_key_id: YOUR_SECRET_ACCESS_KEY_ID
|
67
|
+
|
68
|
+
# The authentication key. The name of this key varies per provider.
|
69
|
+
# ---
|
70
|
+
# aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY
|
71
|
+
# rackspace_api_key: RACKSPACE_API_KEY
|
72
|
+
# google_storage_secret_access_key: YOUR_SECRET_ACCESS_KEY
|
73
|
+
|
74
|
+
# Other keys can be provided as needed for the provider. AWS generally
|
75
|
+
# wants to know the +region+, and Rackspace needs to know if you are
|
76
|
+
# using its European datacentre.
|
77
|
+
# ---
|
78
|
+
# region: us-west-2
|
79
|
+
# rackspace_auth_url: lon.auth.api.rackspacecloud.com
|
80
|
+
|
81
|
+
# This section is for cartage-remote. cartage-remote will not work without a
|
82
|
+
# configuration section at this point because remote build scripts tend to be
|
83
|
+
# heavily customized. This may change in the future.
|
84
|
+
remote:
|
85
|
+
# The name of the build server. This field is required and can show up in
|
86
|
+
# two different formats.
|
87
|
+
# ---
|
88
|
+
# server: build@my-build-machine:2222
|
89
|
+
# server:
|
90
|
+
# user: build
|
91
|
+
# host: my-build-machine
|
92
|
+
# port: 2222
|
93
|
+
|
94
|
+
# The SSH key(s) used to connect to the server. Optional, and
|
95
|
+
# cartage-remote will use <tt>~/.ssh/*id_[rd]sa</tt> to find keys if this
|
96
|
+
# is not provided. Three formats are available.
|
97
|
+
#
|
98
|
+
# As a dictionary (first form), the private keys are embedded directly in
|
99
|
+
# the configuration file. As a string or an array, provides glob patterns
|
100
|
+
# to find key files on disk.
|
101
|
+
# ---
|
102
|
+
# keys:
|
103
|
+
# custom: |
|
104
|
+
# -----BEGIN RSA PRIVATE KEY-----
|
105
|
+
# ...
|
106
|
+
# -----END RSA PRIVATE KEY-----
|
107
|
+
# keys:
|
108
|
+
# - "config/secrets/*id_[rd]sa"
|
109
|
+
# - "~/.ssh/*id_[rd]sa"
|
110
|
+
# keys: "config/secrets/*id_[rd]sa"
|
111
|
+
|
112
|
+
# The build script that will be run on the remote server. This is optional
|
113
|
+
# with a reasonable default.
|
114
|
+
#
|
115
|
+
# #!/bin/bash
|
116
|
+
# set -e
|
117
|
+
# if [ -f Gemfile ]; then
|
118
|
+
# bundle install --path %<remote_bundle>s
|
119
|
+
# bundle exec cartage build \
|
120
|
+
# --config-file %<config_file>s \
|
121
|
+
# --target %<project_path>s
|
122
|
+
# else
|
123
|
+
# cartage build --config-file %<config_file>s \
|
124
|
+
# --target %<project_path>s
|
125
|
+
# fi
|
126
|
+
# ---
|
127
|
+
# build: |
|
128
|
+
# #!/bin/bash
|
129
|
+
# set -e
|
130
|
+
# if [ -f Gemfile ]; then
|
131
|
+
# bundle install --path %<remote_bundle>s
|
132
|
+
# bundle exec cartage s3 \
|
133
|
+
# --config-file %<config_file>s \
|
134
|
+
# --target %<project_path>s \
|
135
|
+
# --verbose
|
136
|
+
# else
|
137
|
+
# cartage build \
|
138
|
+
# --config-file %<config_file>s \
|
139
|
+
# --target %<project_path>s \
|
140
|
+
# --verbose
|
141
|
+
# fi
|
142
|
+
|
143
|
+
# The prebuild script that will be run on the local system. This is
|
144
|
+
# optional with a reasonable default.
|
145
|
+
#
|
146
|
+
# #!/bin/bash
|
147
|
+
# ssh-keyscan -H %<remote_host>s >> ~/.ssh/known_hosts
|
148
|
+
# ---
|
149
|
+
# prebuild: |
|
150
|
+
# #!/bin/bash
|
151
|
+
# ssh-keyscan -H %<remote_host>s >> ~/.ssh/known_hosts
|
152
|
+
# echo 'Prebuild complete'
|
153
|
+
|
154
|
+
# The postbuild script that will be run on the local system. This is
|
155
|
+
# optional with no default (no post-build actions).
|
156
|
+
# ---
|
157
|
+
# postbuild: |
|
158
|
+
# #!/bin/bash
|
159
|
+
# t="token=SLACK_TOKEN"
|
160
|
+
# c="channel=%23ci"
|
161
|
+
# d=MYDOMAIN
|
162
|
+
# u="https://${d}.slack.com/services/hooks/slackbot?${t}&${c}"
|
163
|
+
# curl --data "Build %<name>s-%<timestamp>s complete." ${u}
|
164
|
+
|
165
|
+
# This must not be indented for it to work.
|
166
|
+
<% if File.exist?('config/ansible/cartage.yml') %>
|
167
|
+
<%= File.read('config/ansible/cartage.yml') %>
|
168
|
+
<% end %>
|
169
|
+
<% if File.exist?('config/local/cartage.yml') %>
|
170
|
+
<%= File.read('config/local/cartage.yml') %>
|
171
|
+
<% end %>
|
172
|
+
|
data/lib/cartage.rb
ADDED
@@ -0,0 +1,496 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
# Cartage, a package builder.
|
4
|
+
class Cartage
|
5
|
+
VERSION = '1.0' #:nodoc:
|
6
|
+
|
7
|
+
# Plug-in commands that want to return a non-zero exit code should raise
|
8
|
+
# Cartage::QuietError.new(exitstatus).
|
9
|
+
class QuietError < StandardError
|
10
|
+
# Initialize the exception with +exitstatus+.
|
11
|
+
def initialize(exitstatus)
|
12
|
+
@exitstatus = exitstatus
|
13
|
+
end
|
14
|
+
|
15
|
+
# The exit status to be returned from this exception.
|
16
|
+
attr_reader :exitstatus
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# :attr_accessor: name
|
21
|
+
#
|
22
|
+
# The name of the package to create. Defaults to #default_name.
|
23
|
+
|
24
|
+
##
|
25
|
+
# :method: default_name
|
26
|
+
#
|
27
|
+
# The default name of the package to be created, derived from the
|
28
|
+
# repository's Git URL.
|
29
|
+
|
30
|
+
##
|
31
|
+
# :attr_accessor: root_path
|
32
|
+
#
|
33
|
+
# The root path for the application.
|
34
|
+
|
35
|
+
##
|
36
|
+
# :method: default_root_path
|
37
|
+
#
|
38
|
+
# The default root path of the package, the top-level path of the Git
|
39
|
+
# repository.
|
40
|
+
|
41
|
+
##
|
42
|
+
# :attr_accessor: target
|
43
|
+
#
|
44
|
+
# The target where the final Cartage package will be created.
|
45
|
+
|
46
|
+
##
|
47
|
+
# :method: default_target
|
48
|
+
#
|
49
|
+
# The default target of the package, './tmp'.
|
50
|
+
|
51
|
+
##
|
52
|
+
# :attr_accessor: timestamp
|
53
|
+
#
|
54
|
+
# The timestamp to be applied to the final package name.
|
55
|
+
|
56
|
+
##
|
57
|
+
# :method: default_timestamp
|
58
|
+
#
|
59
|
+
# The default timestamp.
|
60
|
+
|
61
|
+
##
|
62
|
+
# :attr_accessor: without_groups
|
63
|
+
#
|
64
|
+
# The environments to exclude from a bundle install.
|
65
|
+
|
66
|
+
##
|
67
|
+
# :method: default_without_environments
|
68
|
+
#
|
69
|
+
# The default environments to exclude from a bundle install. The default is
|
70
|
+
# <tt>[ 'test', 'development' ]</tt>.
|
71
|
+
|
72
|
+
# Commands that normally output data will have that output suppressed.
|
73
|
+
attr_accessor :quiet
|
74
|
+
|
75
|
+
# Commands will be run with extra information.
|
76
|
+
attr_accessor :verbose
|
77
|
+
|
78
|
+
# The environment to be used when resolving configuration options from a
|
79
|
+
# configuration file. Cartage configuration files do not usually have
|
80
|
+
# environment partitions, but if they do, use this to select the environment
|
81
|
+
# partition.
|
82
|
+
attr_accessor :environment
|
83
|
+
|
84
|
+
# The Config object. If +with_environment+ is +true+ (the default) and
|
85
|
+
# #environment has been set, only the subset of the config matching
|
86
|
+
# #environment will be returned. If +with_environment+ is +false+, the full
|
87
|
+
# configuration will be returned.
|
88
|
+
#
|
89
|
+
# If +for_plugin+ is specified, only the subset of the config for the named
|
90
|
+
# plug-in will be returned.
|
91
|
+
#
|
92
|
+
# # Assume that #environment is 'development'.
|
93
|
+
# cartage.config
|
94
|
+
# # => base_config[:development]
|
95
|
+
# cartage.config(with_environment: false)
|
96
|
+
# # => base_config
|
97
|
+
# cartage.config(for_plugin: 's3')
|
98
|
+
# # => base_config[:development][:plugins][:s3]
|
99
|
+
# cartage.config(for_plugin: 's3', with_environment: false)
|
100
|
+
# # => base_config[:plugins][:s3]
|
101
|
+
def config(with_environment: true, for_plugin: nil)
|
102
|
+
env = environment.to_sym if with_environment && environment
|
103
|
+
plug = for_plugin.to_sym if for_plugin
|
104
|
+
|
105
|
+
cfg = if env
|
106
|
+
base_config[env]
|
107
|
+
else
|
108
|
+
base_config
|
109
|
+
end
|
110
|
+
|
111
|
+
cfg = cfg.plugins[plug] if plug && cfg.plugins
|
112
|
+
cfg
|
113
|
+
end
|
114
|
+
|
115
|
+
# The configuration file to read. This should not be used by clients.
|
116
|
+
attr_writer :load_config #:nodoc:
|
117
|
+
|
118
|
+
# The base config file. This should not be used by clients.
|
119
|
+
attr_accessor :base_config #:nodoc:
|
120
|
+
|
121
|
+
def initialize #:nodoc:
|
122
|
+
@load_config = :default
|
123
|
+
end
|
124
|
+
|
125
|
+
# Create the package.
|
126
|
+
def pack
|
127
|
+
timestamp # Force the timestamp to be set now.
|
128
|
+
prepare_work_area
|
129
|
+
save_release_hashref
|
130
|
+
fetch_bundler
|
131
|
+
install_vendor_bundle
|
132
|
+
restore_modified_files
|
133
|
+
build_final_tarball
|
134
|
+
end
|
135
|
+
|
136
|
+
# Set or return the bundle cache. The bundle cache is a compressed tarball of
|
137
|
+
# <tt>vendor/bundle</tt> in the working path.
|
138
|
+
#
|
139
|
+
# If it exists, it will be extracted into <tt>vendor/bundle</tt> before
|
140
|
+
# <tt>bundle install --deployment</tt> is run, and it will be created after
|
141
|
+
# the bundle has been installed. In this way, bundle installation works
|
142
|
+
# almost the same way as Capistrano’s shared bundle concept as long as the
|
143
|
+
# path to the bundle_cache has been set to a stable location.
|
144
|
+
#
|
145
|
+
# On Semaphore CI, this should be created relative to
|
146
|
+
# <tt>$SEMAPHORE_CACHE</tt>.
|
147
|
+
#
|
148
|
+
# cartage pack --bundle-cache $SEMAPHORE_CACHE
|
149
|
+
def bundle_cache(location = nil)
|
150
|
+
if location || !defined?(@bundle_cache)
|
151
|
+
@bundle_cache = Pathname(location || tmp_path).
|
152
|
+
join('vendor-bundle.tar.bz2').expand_path
|
153
|
+
end
|
154
|
+
@bundle_cache
|
155
|
+
end
|
156
|
+
|
157
|
+
# Return the release hashref. If the optional +save_to+ parameter is
|
158
|
+
# provided, the release hashref will be written to the specified file.
|
159
|
+
def release_hashref(save_to: nil)
|
160
|
+
@release_hashref ||= %x(git rev-parse HEAD).chomp
|
161
|
+
File.open(save_to, 'w') { |f| f.write @release_hashref } if save_to
|
162
|
+
@release_hashref
|
163
|
+
end
|
164
|
+
|
165
|
+
# The repository URL.
|
166
|
+
def repo_url
|
167
|
+
unless defined? @repo_url
|
168
|
+
origin = %x(git remote show -n origin)
|
169
|
+
match = origin.match(%r{\n\s+Fetch URL: (?<fetch>[^\n]+)})
|
170
|
+
@repo_url = match[:fetch]
|
171
|
+
end
|
172
|
+
@repo_url
|
173
|
+
end
|
174
|
+
|
175
|
+
# The path to the resulting package.
|
176
|
+
def final_tarball
|
177
|
+
@final_tarball ||= Pathname("#{final_name}.tar.bz2")
|
178
|
+
end
|
179
|
+
|
180
|
+
# The path to the resulting release_hashref.
|
181
|
+
def final_release_hashref
|
182
|
+
@final_release_hashref ||=
|
183
|
+
Pathname("#{final_name}-release-hashref.txt")
|
184
|
+
end
|
185
|
+
|
186
|
+
# A utility method for Cartage plug-ins to display a message only if verbose
|
187
|
+
# is on. Unless the command implemented by the plug-in is output only, this
|
188
|
+
# should be used.
|
189
|
+
def display(message)
|
190
|
+
__display(message)
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
def resolve_config!(*with_plugins)
|
195
|
+
return unless @load_config
|
196
|
+
@base_config = Cartage::Config.load(@load_config)
|
197
|
+
|
198
|
+
cfg = config
|
199
|
+
maybe_assign :target, cfg.target
|
200
|
+
maybe_assign :name, cfg.name
|
201
|
+
maybe_assign :root_path, cfg.root_path
|
202
|
+
maybe_assign :timestamp, cfg.timestamp
|
203
|
+
maybe_assign :without_groups, cfg.without
|
204
|
+
|
205
|
+
bundle_cache(cfg.bundle_cache) unless cfg.bundle_cache.nil? ||
|
206
|
+
cfg.bundle_cache.empty?
|
207
|
+
|
208
|
+
with_plugins.each do |name|
|
209
|
+
next unless respond_to? name
|
210
|
+
plugin = send(name)
|
211
|
+
|
212
|
+
next unless plugin
|
213
|
+
plugin.send(:resolve_config!, config(for_plugin: name))
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def maybe_assign(name, value)
|
218
|
+
return if value.nil? || value.empty? ||
|
219
|
+
instance_variable_defined?(:"@#{name}")
|
220
|
+
send(:"#{name}=", value)
|
221
|
+
end
|
222
|
+
|
223
|
+
def __display(message, partial: false, verbose: verbose())
|
224
|
+
return unless verbose && !quiet
|
225
|
+
|
226
|
+
if partial
|
227
|
+
print message
|
228
|
+
else
|
229
|
+
puts message
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def run(command)
|
234
|
+
display command.join(' ')
|
235
|
+
|
236
|
+
IO.popen(command + [ err: [ :child, :out ] ]) do |io|
|
237
|
+
__display(io.read(128), partial: true, verbose: true) until io.eof?
|
238
|
+
end
|
239
|
+
|
240
|
+
unless $?.success?
|
241
|
+
raise StandardError, "Error running '#{command.join(' ')}'"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def prepare_work_area
|
246
|
+
display "Preparing cartage work area..."
|
247
|
+
|
248
|
+
work_path.rmtree if work_path.exist?
|
249
|
+
work_path.mkpath
|
250
|
+
|
251
|
+
xf_status = cf_status = nil
|
252
|
+
|
253
|
+
manifest.resolve(root_path) do |file_list|
|
254
|
+
tar_cf_cmd = [
|
255
|
+
'tar', 'cf', '-', '-C', parent, '-h', '-T', file_list
|
256
|
+
].map(&:to_s)
|
257
|
+
|
258
|
+
tar_xf_cmd = [
|
259
|
+
'tar', 'xf', '-', '-C', work_path, '--strip-components=1'
|
260
|
+
].map(&:to_s)
|
261
|
+
|
262
|
+
IO.popen(tar_cf_cmd) do |cf|
|
263
|
+
IO.popen(tar_xf_cmd, 'w') do |xf|
|
264
|
+
xf.write cf.read
|
265
|
+
end
|
266
|
+
|
267
|
+
unless $?.success?
|
268
|
+
raise StandardError, "Error running #{tar_xf_cmd.join(' ')}"
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
unless $?.success?
|
273
|
+
raise StandardError, "Error running #{tar_cf_cmd.join(' ')}"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def save_release_hashref
|
279
|
+
display 'Saving release hashref...'
|
280
|
+
release_hashref save_to: work_path.join('release_hashref')
|
281
|
+
release_hashref save_to: final_release_hashref
|
282
|
+
end
|
283
|
+
|
284
|
+
def extract_bundle_cache
|
285
|
+
run %W(tar xfj #{bundle_cache} -C #{work_path}) if bundle_cache.exist?
|
286
|
+
end
|
287
|
+
|
288
|
+
def install_vendor_bundle
|
289
|
+
extract_bundle_cache
|
290
|
+
|
291
|
+
Bundler.with_clean_env do
|
292
|
+
Dir.chdir(work_path) do
|
293
|
+
run %w(bundle install --jobs=4 --deployment --clean --without) +
|
294
|
+
without_groups
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
create_bundle_cache
|
299
|
+
end
|
300
|
+
|
301
|
+
def restore_modified_files
|
302
|
+
%x(git status -s).split($/).map(&:split).map(&:last).each { |file|
|
303
|
+
restore_modified_file file
|
304
|
+
}
|
305
|
+
end
|
306
|
+
|
307
|
+
def fetch_bundler
|
308
|
+
Dir.chdir(work_path) do
|
309
|
+
run %w(gem fetch bundler)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def build_final_tarball
|
314
|
+
run %W(tar cfj #{final_tarball} -C #{tmp_path} #{name})
|
315
|
+
end
|
316
|
+
|
317
|
+
def work_path
|
318
|
+
@work_path ||= tmp_path.join(name)
|
319
|
+
end
|
320
|
+
|
321
|
+
def clean
|
322
|
+
[ work_path ] + final
|
323
|
+
end
|
324
|
+
|
325
|
+
def final
|
326
|
+
[ final_tarball, final_release_hashref ]
|
327
|
+
end
|
328
|
+
|
329
|
+
def parent
|
330
|
+
@parent ||= root_path.parent
|
331
|
+
end
|
332
|
+
|
333
|
+
def restore_modified_file(filename)
|
334
|
+
command = [
|
335
|
+
'git', 'show', "#{release_hashref}:#{filename}"
|
336
|
+
]
|
337
|
+
|
338
|
+
IO.popen(command) do |show|
|
339
|
+
work_path.join(filename).open('w') { |f| f.write show.read }
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def tmp_path
|
344
|
+
@tmp_path ||= root_path.join('tmp')
|
345
|
+
end
|
346
|
+
|
347
|
+
def final_name
|
348
|
+
@final_name ||= tmp_path.join("#{name}-#{timestamp}")
|
349
|
+
end
|
350
|
+
|
351
|
+
class << self
|
352
|
+
private
|
353
|
+
|
354
|
+
def lazy_accessor(sym, default: nil, setter: nil, &block)
|
355
|
+
ivar = :"@#{sym}"
|
356
|
+
wsym = :"#{sym}="
|
357
|
+
dsym = :"default_#{sym}"
|
358
|
+
|
359
|
+
if default.nil? && block.nil?
|
360
|
+
raise ArgumentError, "No default provided."
|
361
|
+
end
|
362
|
+
|
363
|
+
if setter && !setter.respond_to?(:call)
|
364
|
+
raise ArgumentError, "setter must be callable"
|
365
|
+
end
|
366
|
+
|
367
|
+
setter ||= ->(v) { v }
|
368
|
+
|
369
|
+
dblk = if default.respond_to?(:call)
|
370
|
+
default
|
371
|
+
elsif default.nil?
|
372
|
+
block
|
373
|
+
else
|
374
|
+
-> { default }
|
375
|
+
end
|
376
|
+
|
377
|
+
define_method(sym) do
|
378
|
+
instance_variable_get(ivar) || send(dsym)
|
379
|
+
end
|
380
|
+
|
381
|
+
define_method(wsym) do |value|
|
382
|
+
instance_variable_set(ivar, setter.call(value || send(dsym)))
|
383
|
+
end
|
384
|
+
|
385
|
+
define_method(dsym, &dblk)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
lazy_accessor :name, default: -> { File.basename(repo_url, '.git') }
|
390
|
+
lazy_accessor :root_path, setter: ->(v) { Pathname(v).expand_path },
|
391
|
+
default: -> { Pathname(%x(git rev-parse --show-cdup).chomp).expand_path }
|
392
|
+
lazy_accessor :target, setter: ->(v) { Pathname(v) },
|
393
|
+
default: -> { Pathname('tmp') }
|
394
|
+
lazy_accessor :timestamp, default: -> {
|
395
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
396
|
+
}
|
397
|
+
lazy_accessor :without_groups, setter: ->(v) { Array(v) },
|
398
|
+
default: -> { %w(development test) }
|
399
|
+
lazy_accessor :base_config, default: -> { OpenStruct.new }
|
400
|
+
end
|
401
|
+
|
402
|
+
class << Cartage
|
403
|
+
# Run the Cartage command-line program.
|
404
|
+
def run(args) #:nodoc:
|
405
|
+
require_relative 'cartage/plugin'
|
406
|
+
Cartage::Plugin.load
|
407
|
+
Cartage::Plugin.decorate(Cartage)
|
408
|
+
|
409
|
+
cartage = Cartage.new
|
410
|
+
|
411
|
+
cli = CmdParse::CommandParser.new(handle_exceptions: true)
|
412
|
+
cli.main_options.program_name = 'cartage'
|
413
|
+
cli.main_options.version = Cartage::VERSION.split(/\./)
|
414
|
+
cli.main_options.banner = 'Manage releaseable packages.'
|
415
|
+
|
416
|
+
cli.global_options do |opts|
|
417
|
+
# opts.on('--[no-]quiet', 'Silence normal command output.') { |q|
|
418
|
+
# cartage.quiet = !!q
|
419
|
+
# }
|
420
|
+
opts.on('--[no-]verbose', 'Show verbose output.') { |v|
|
421
|
+
cartage.verbose = !!v
|
422
|
+
}
|
423
|
+
opts.on(
|
424
|
+
'-E', '--environment [ENVIRONMENT]', <<-desc
|
425
|
+
Set the environment to be used when necessary. If an environment name is not
|
426
|
+
provided, it will check the values of $RAILS_ENV and RACK_ENV. If neither is
|
427
|
+
set, this option is ignored.
|
428
|
+
desc
|
429
|
+
) { |e| cartage.environment = e || ENV['RAILS_ENV'] || ENV['RACK_ENV'] }
|
430
|
+
opts.on(
|
431
|
+
'-C', '--[no-]config-file load_config', <<-desc
|
432
|
+
Configure Cartage from a default configuration file or a specified
|
433
|
+
configuration file.
|
434
|
+
desc
|
435
|
+
) { |c| cartage.load_config = c }
|
436
|
+
end
|
437
|
+
|
438
|
+
cli.add_command(CmdParse::HelpCommand.new)
|
439
|
+
cli.add_command(CmdParse::VersionCommand.new)
|
440
|
+
cli.add_command(Cartage::PackCommand.new(cartage))
|
441
|
+
|
442
|
+
Cartage::Plugin.registered.each do |plugin|
|
443
|
+
if plugin.respond_to?(:commands)
|
444
|
+
Array(plugin.commands).flatten.each do |command|
|
445
|
+
registered_commands << command
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
registered_commands.uniq.each { |cmd| cli.add_command(cmd.new(cartage)) }
|
451
|
+
cli.parse
|
452
|
+
0
|
453
|
+
rescue Cartage::QuietError => qe
|
454
|
+
qe.exitstatus
|
455
|
+
rescue StandardError => e
|
456
|
+
$stderr.puts "Error:\n " + e.message
|
457
|
+
$stderr.puts e.backtrace.join("\n") if cartage.verbose
|
458
|
+
2
|
459
|
+
end
|
460
|
+
|
461
|
+
# Set options common to anything that builds a package (that is, it calls
|
462
|
+
# Cartage#pack).
|
463
|
+
def common_build_options(opts, cartage)
|
464
|
+
opts.on(
|
465
|
+
'-t', '--target PATH',
|
466
|
+
'The build package will be placed in PATH, which defaults to \'tmp\'.'
|
467
|
+
) { |t| cartage.target = t }
|
468
|
+
opts.on(
|
469
|
+
'-n', '--name NAME',
|
470
|
+
"Set the package name. Defaults to '#{cartage.default_name}'."
|
471
|
+
) { |n| cartage.name = n }
|
472
|
+
opts.on(
|
473
|
+
'-r', '--root-path PATH',
|
474
|
+
'Set the root path. Defaults to the repository root.'
|
475
|
+
) { |r| cartage.root_path = r }
|
476
|
+
opts.on(
|
477
|
+
'--timestamp TIMESTAMP',
|
478
|
+
'The timestamp used for the final package.'
|
479
|
+
) { |t| cartage.timestamp = t }
|
480
|
+
opts.on(
|
481
|
+
'--bundle-cache PATH',
|
482
|
+
'Set the bundle cache path.'
|
483
|
+
) { |b| cartage.bundle_cache(b) }
|
484
|
+
opts.on(
|
485
|
+
'--without GROUP1,GROUP2', Array,
|
486
|
+
'Set the groups to be excluded from bundle installation.',
|
487
|
+
) { |w| cartage.without_environments = w }
|
488
|
+
end
|
489
|
+
|
490
|
+
private
|
491
|
+
def registered_commands
|
492
|
+
@registered_commands ||= []
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
require_relative 'cartage/config'
|