cartage 1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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'
|