jara 2.0.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: dd7d9d3308929fa4978ae10bd2bd071d3a39610c
4
+ data.tar.gz: c2cf848a765a20264026e3858e6700c5923bc184
5
+ SHA512:
6
+ metadata.gz: c3e7c6e1f2a056b112606a409901252e5d14991254db3bd1b6c81a2f6f37d1b55841a3dece7ddf13426b36e8e7345df1143025f05276207ddcef83372c4f948b
7
+ data.tar.gz: 64a121477496d10d2fc695a9fa1c0ebcb229436502c9dfeaa62aa9ce18ac50ba152c388a695960faa89999761377774d12957b3f96ee75c11ea34af48edac1ba
data/LICENSE.txt ADDED
@@ -0,0 +1,12 @@
1
+ Copyright (c) 2014, Burt AB
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+
8
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
+
10
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11
+
12
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # Jarå
2
+
3
+ Jarå creates clean artifacts from Git repositories and publishes them to S3. It will check that you've pushed your code, check out a pristine copy from git, build an artifact and upload it to S3 using a name that includes the date, time and Git SHA.
4
+
5
+ In JRuby it can use [Puck](https://github.com/iconara/puck) to create a standalone JAR file that can be run with `java -jar …`.
6
+
7
+ # Installation
8
+
9
+ ```ruby
10
+ group :development do
11
+ gem 'jara'
12
+ gem 'puck', platform: 'jruby'
13
+ end
14
+ ```
15
+
16
+ Puck is optional, and only available in JRuby. If Puck is present it will be the default archiver (see below for how to configure this).
17
+
18
+ # Usage
19
+
20
+ You can use Jarå either from a `Rakefile` or from the command line. Here's an example from a `Rakefile` that builds an artifact from the master branch and uploads it to the `artifact-bucket` S3 bucket:
21
+
22
+ ```ruby
23
+ task :release do
24
+ releaser = Jarå::Releaser.new('production', 'artifact-bucket')
25
+ releaser.release
26
+ end
27
+ ```
28
+
29
+ ## Release names
30
+
31
+ The JAR artifact will be named from the project name, environment, date stamp and commit SHA, and will be uploaded with a path on S3 that also contains the environment and project name. The name of the directory that contains the code is assumed to be the project name.
32
+
33
+ For example, if you run it from a directory called "foo_bar" and sets the environment "production" it will build the artifact "foo_bar-production-YYYYmmddHHMMSS-XXXXXXXX.jar", where "YYYYmmddHHMMSS" is the current date and time and "XXXXXXXX" is the first 8 characters from the commit SHA. The artifact will be cached locally in a directory called "build/production" and then uploaded to S3 into the specified bucket, with the key "production/foo_bar/foo_bar-production-YYYYmmddHHMMSS-XXXXXXXX.jar".
34
+
35
+ If you change "production" to "staging" it will build an artifact from the staging branch instead (and all other paths and names will have "staging" where they had "production" in the description above).
36
+
37
+ You may have noticed that specifying "production" created an artifact from the master branch, and "staging" used the staging branch. Using anything but "production" means that the branch name is assumed to be the same as the environment.
38
+
39
+ Before the artifact is built Jarå will check that _branch_name_ and origin/*branch_name* point to the same commit. The reason for this is so that you don't release an artifact with a SHA that is not visible to others (this does not check that you've pulled before you release, but the important thing is to not release something that is not trackable).
40
+
41
+ ## Using the command line tool
42
+
43
+ The same can be accomplished by running this from the command line:
44
+
45
+ ```
46
+ $ jara release --environment production --bucket artifact-bucket
47
+ ```
48
+
49
+ ## Building tarballs
50
+
51
+ The primary use case for Jarå is building self contained JAR files, but it can also be used to create tarballs. This can be useful for non-Ruby projects that you want to release the same way you release your JRuby applications.
52
+
53
+ If you would rather build a tarball you can do that like this:
54
+
55
+ ```ruby
56
+ task :tarball do
57
+ releaser = Jarå::Releaser.new('production', 'artifact-bucket', archiver: :tgz)
58
+ releaser.release
59
+ end
60
+ ```
61
+
62
+ or from the command line:
63
+
64
+ ```
65
+ $ jara release --environment production --bucket artifact-bucket --archiver tgz
66
+ ```
67
+
68
+ The `tgz` archiver is the default when Puck is not installed.
69
+
70
+ Sometimes your source code isn't enough to run the application. If you're using Jarå to create a tarball of a purely client side web application you might want to minify all JavaScript and CSS files before the artifact is created. This can be done like this (assuming you have a `Makefile` with a `minify` target):
71
+
72
+ ```ruby
73
+ task :tarball do
74
+ releaser = Jarå::Releaser.new('production', 'artifact-bucket', archiver: :tgz, build_command: 'make minify')
75
+ releaser.release
76
+ end
77
+ ```
78
+
79
+ The command can also be a Ruby proc, or anything that responds to `#call`:
80
+
81
+ ```ruby
82
+ task :tarball do
83
+ build_command = lambda do
84
+ FileUtils.touch('very-important-file')
85
+ end
86
+ releaser = Jarå::Releaser.new('production', 'artifact-bucket', archiver: :tgz, build_command: build_command)
87
+ releaser.release
88
+ end
89
+ ```
90
+
91
+ or from the command line:
92
+
93
+ ```
94
+ $ jara release --environment production --bucket artifact-bucket --archiver tgz --build-command 'make minify'
95
+ ```
96
+
97
+ The command can be anything, as long as it can run from a cleanly checked out version of your repository.
98
+
99
+ ## Just building an artifact
100
+
101
+ If you don't want to release an artifact to S3 you can choose to just build one (you don't need to specify the bucket name if you're only building an artifact):
102
+
103
+ ```ruby
104
+ task :artifact do
105
+ releaser = Jarå::Releaser.new('production')
106
+ releaser.build_artifact
107
+ end
108
+ ```
109
+
110
+ and from the command line:
111
+
112
+ ```
113
+ $ jara build --environment production
114
+ ```
115
+
116
+ ## Build from the working directory
117
+
118
+ In all of the examples above Jarå has checked out a clean copy of your code before building the artifact, but in some cases you just want to pack up everything and see if it works. In those cases you can set the environment to `nil` (or leave out the `--environment` option) to build from the working directory directly. The artifact will be placed directly under the `build` directory and it will be named just after the project directory, it will not have any timestamps nor commit SHA in the name.
119
+
120
+ # Copyright
121
+
122
+ © 2014-2015 Burt AB, see LICENSE.txt (BSD 3-Clause).
data/bin/jara ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'jara/cli'
4
+
5
+ Jara::Cli.new(ARGV).run
data/lib/jara/cli.rb ADDED
@@ -0,0 +1,60 @@
1
+ # encoding: utf-8
2
+
3
+ require 'jara'
4
+ require 'optparse'
5
+
6
+ module Jara
7
+ class Cli
8
+ def initialize(argv)
9
+ @command = argv.first
10
+ @argv = argv.drop(1)
11
+ end
12
+
13
+ def run
14
+ parse_argv(@argv)
15
+ options = {}
16
+ options[:archiver] = @archiver if @archiver
17
+ options[:build_command] = @build_command if @build_command
18
+ releaser = Jara::Releaser.new(@environment, @bucket, options)
19
+ case @command
20
+ when /help|-h/
21
+ $stderr.puts(option_parser)
22
+ when 'release'
23
+ releaser.release
24
+ when 'build'
25
+ releaser.build_artifact
26
+ else
27
+ $stderr.puts('Unknown command "%s", expected "build" or "release"' % @command)
28
+ exit(1)
29
+ end
30
+ rescue OptionParser::ParseError => e
31
+ $stderr.puts(option_parser)
32
+ exit(1)
33
+ rescue JaraError => e
34
+ $stderr.puts(sprintf('Could not %s artifact: %s', @command, e.message))
35
+ exit(1)
36
+ end
37
+
38
+ private
39
+
40
+ def option_parser
41
+ @option_parser ||= OptionParser.new do |parser|
42
+ parser.banner = "Usage: jara build [options]\n jara release [options]"
43
+ parser.separator ''
44
+ parser.separator 'Common options:'
45
+ parser.on('-e', '--environment=ENV', 'Environment to release to (e.g. production, staging)') { |e| @environment = e }
46
+ parser.on('-a', '--archiver=TYPE', 'Archiver to use (jar or tgz)') { |t| @archiver = t.to_sym }
47
+ parser.on('-c', '--build-command=COMMAND', 'Command to run before creating the artifact') { |c| @build_command = c }
48
+ parser.on('-h', '--help', 'Show this message') { @command = 'help' }
49
+ parser.separator ''
50
+ parser.separator 'Release options:'
51
+ parser.on('-b', '--bucket=BUCKET', 'S3 bucket for releases') { |b| @bucket = b }
52
+ parser.separator ''
53
+ end
54
+ end
55
+
56
+ def parse_argv(argv)
57
+ option_parser.parse(argv)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,281 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+ require 'pathname'
6
+ require 'digest/md5'
7
+ require 'aws-sdk-core'
8
+ require 'socket'
9
+
10
+
11
+ module Jara
12
+ ExecError = Class.new(JaraError)
13
+
14
+ class Releaser
15
+ def initialize(environment, bucket_name=nil, options={})
16
+ @environment = environment
17
+ @bucket_name = bucket_name
18
+ @re_release = options.fetch(:re_release, false)
19
+ @extra_metadata = options[:metadata] || {}
20
+ @build_command = options[:build_command]
21
+ @shell = options[:shell] || Shell.new
22
+ @archiver = create_archiver(options[:archiver])
23
+ @file_system = options[:file_system] || FileUtils
24
+ @s3 = options[:s3]
25
+ @logger = options[:logger] || IoLogger.new($stderr)
26
+ @branch = @environment == 'production' ? 'master' : @environment
27
+ end
28
+
29
+ def build_artifact
30
+ if @environment.nil?
31
+ archive_name = "#{app_name}.#{@archiver.extension}"
32
+ Dir.chdir(project_dir) do
33
+ @archiver.create(archive_name: archive_name)
34
+ end
35
+ @logger.info('Created test artifact')
36
+ File.join(project_dir, 'build', archive_name)
37
+ elsif (artifact_path = find_local_artifact)
38
+ @logger.warn('An artifact for %s already exists: %s' % [branch_sha[0, 8], File.basename(artifact_path)])
39
+ artifact_path
40
+ else
41
+ date_stamp = Time.now.utc.strftime('%Y%m%d%H%M%S')
42
+ destination_dir = File.join(project_dir, 'build', @environment)
43
+ archive_name = [app_name, @environment, date_stamp, branch_sha[0, 8]].join('-') << '.' << @archiver.extension
44
+ Dir.mktmpdir do |path|
45
+ @shell.exec('git archive --format=tar --prefix=%s/ %s | (cd %s/ && tar xf -)' % [File.basename(path), branch_sha, File.dirname(path)])
46
+ Dir.chdir(path) do
47
+ @logger.info('Checked out %s from branch %s' % [branch_sha[0, 8], @branch])
48
+ if @build_command
49
+ if @build_command.respond_to?(:call)
50
+ @logger.info('Running build command')
51
+ @build_command.call
52
+ else
53
+ @logger.info('Running build command: %s' % @build_command)
54
+ @shell.exec(@build_command)
55
+ end
56
+ end
57
+ @archiver.create(archive_name: archive_name)
58
+ @file_system.mkdir_p(destination_dir)
59
+ @file_system.cp("build/#{archive_name}", destination_dir)
60
+ @logger.info('Created artifact %s' % archive_name)
61
+ end
62
+ end
63
+ File.join(destination_dir, archive_name)
64
+ end
65
+ end
66
+
67
+ def release
68
+ raise JaraError, 'No environment set' unless @environment
69
+ raise JaraError, 'No bucket name set' unless @bucket_name
70
+ if !@re_release && (obj = find_remote_artifact)
71
+ s3_uri = 's3://%s/%s' % [@bucket_name, obj.key]
72
+ @logger.warn('An artifact for %s already exists: %s' % [branch_sha[0, 8], s3_uri])
73
+ s3_uri
74
+ else
75
+ local_path = find_local_artifact || build_artifact
76
+ upload_artifact(local_path)
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def s3
83
+ @s3 ||= Aws::S3::Client.new
84
+ end
85
+
86
+ def app_name
87
+ File.basename(project_dir)
88
+ end
89
+
90
+ def project_dir
91
+ unless defined? @project_dir
92
+ Pathname.new(Dir.getwd).descend do |path|
93
+ if Dir.entries(path).include?('.git')
94
+ @project_dir = path
95
+ break
96
+ end
97
+ end
98
+ unless defined? @project_dir
99
+ raise JaraError, 'Could not find project directory'
100
+ end
101
+ end
102
+ @project_dir
103
+ end
104
+
105
+ def branch_sha
106
+ @branch_sha ||= begin
107
+ result = @shell.exec('git rev-parse %s && git rev-parse origin/%s' % [@branch, @branch])
108
+ local_sha, remote_sha = result.split("\n").take(2)
109
+ if local_sha == remote_sha
110
+ local_sha
111
+ else
112
+ raise JaraError, '%s and origin/%s are not in sync, did you forget to push?' % [@branch, @branch]
113
+ end
114
+ end
115
+ end
116
+
117
+ def git_remote
118
+ @git_remote ||= @shell.exec('git config --get remote.origin.url')
119
+ end
120
+
121
+ def metadata
122
+ m = {
123
+ 'packaged_by' => "#{ENV['USER']}@#{Socket.gethostname}",
124
+ 'sha' => branch_sha,
125
+ 'remote' => git_remote,
126
+ }
127
+ m.merge!(@extra_metadata)
128
+ m.merge!(@archiver.metadata)
129
+ m
130
+ end
131
+
132
+ def find_local_artifact
133
+ candidates = Dir[File.join(project_dir, 'build', @environment, "*.#{@archiver.extension}")]
134
+ candidates.select! { |path| path.include?(branch_sha[0, 8]) }
135
+ candidates.sort.last
136
+ end
137
+
138
+ def upload_artifact(local_path)
139
+ remote_path = [@environment, app_name, File.basename(local_path)].join('/')
140
+ content_md5 = Digest::MD5.file(local_path).base64digest
141
+ File.open(local_path, 'rb') do |io|
142
+ s3.put_object(
143
+ bucket: @bucket_name,
144
+ key: remote_path,
145
+ content_type: @archiver.content_type,
146
+ content_md5: content_md5,
147
+ metadata: metadata,
148
+ body: io,
149
+ )
150
+ end
151
+ s3_uri = 's3://%s/%s' % [@bucket_name, remote_path]
152
+ @logger.info('Artifact uploaded to %s' % s3_uri)
153
+ s3_uri
154
+ end
155
+
156
+ def find_remote_artifact
157
+ listing = s3.list_objects(bucket: @bucket_name, prefix: [@environment, app_name, "#{app_name}-#{@environment}-"].join('/'))
158
+ listing.contents.find { |obj| obj.key.include?(branch_sha[0, 8]) }
159
+ end
160
+
161
+ def create_archiver(archiver)
162
+ case archiver
163
+ when :puck, :jar
164
+ PuckArchiver.new(@shell)
165
+ when :tar, :tgz
166
+ Tarchiver.new(@shell)
167
+ when nil
168
+ if defined? PuckArchiver
169
+ create_archiver(:puck)
170
+ else
171
+ create_archiver(:tgz)
172
+ end
173
+ else
174
+ archiver
175
+ end
176
+ end
177
+
178
+ class Shell
179
+ def exec(command)
180
+ output = %x(#{command})
181
+ unless $?.success?
182
+ raise ExecError, %(Command `#{command}` failed with output: #{output})
183
+ end
184
+ output
185
+ rescue Errno::ENOENT => e
186
+ raise ExecError, %(Command `#{command}` failed: #{e.message})
187
+ end
188
+ end
189
+
190
+ class Archiver
191
+ def initialize(shell)
192
+ @shell = shell
193
+ end
194
+
195
+ def create(options)
196
+ end
197
+
198
+ def extension
199
+ end
200
+
201
+ def content_type
202
+ end
203
+
204
+ def metadata
205
+ {}
206
+ end
207
+ end
208
+
209
+ class Tarchiver < Archiver
210
+ def create(options)
211
+ FileUtils.mkdir_p('build')
212
+ entries = Dir['*']
213
+ entries.delete('build')
214
+ @shell.exec("tar czf build/#{options[:archive_name]} #{entries.join(' ')}")
215
+ end
216
+
217
+ def extension
218
+ 'tgz'
219
+ end
220
+
221
+ def content_type
222
+ 'application/x-gzip'
223
+ end
224
+ end
225
+
226
+ if defined? JRUBY_VERSION
227
+ begin
228
+ require 'puck'
229
+
230
+ class PuckArchiver < Archiver
231
+ def create(options)
232
+ options = options.dup
233
+ options[:jar_name] = options.delete(:archive_name)
234
+ Puck::Jar.new(options).create!
235
+ end
236
+
237
+ def extension
238
+ 'jar'
239
+ end
240
+
241
+ def content_type
242
+ 'application/java-archive'
243
+ end
244
+
245
+ def metadata
246
+ jruby_jars_path = $LOAD_PATH.grep(/\/jruby-jars/).first
247
+ jruby_version = jruby_jars_path && jruby_jars_path.scan(/\/jruby-jars-(.+)\//).flatten.first
248
+ if jruby_version
249
+ super.merge('jruby' => jruby_version)
250
+ else
251
+ super
252
+ end
253
+ end
254
+ end
255
+ rescue LoadError => e
256
+ raise unless e.message.include?('no such file to load -- puck')
257
+ end
258
+ end
259
+ end
260
+
261
+ class IoLogger
262
+ def initialize(io)
263
+ @io = io
264
+ end
265
+
266
+ def info(msg)
267
+ @io.puts(msg)
268
+ end
269
+
270
+ def warn(msg)
271
+ @io.puts(msg)
272
+ end
273
+ end
274
+
275
+ class NullLogger
276
+ def info(*); end
277
+ def warn(*); end
278
+ end
279
+
280
+ NULL_LOGGER = NullLogger.new
281
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ module Jara
4
+ VERSION = '2.0.0'.freeze
5
+ end
data/lib/jara.rb ADDED
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+
3
+ module Jara
4
+ JaraError = Class.new(StandardError)
5
+ end
6
+
7
+ Jarå = Jara
8
+
9
+ require 'jara/releaser'
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jara
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Burt Platform Team
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '>='
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ name: aws-sdk-core
20
+ prerelease: false
21
+ type: :runtime
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: |-
28
+ Jara is a tool for building artifacts from a Git
29
+ repository, and can make standalone Jar files for JRuby.
30
+ It will check out a clean copy of your code and name
31
+ the artifact from the commit it was built from.
32
+ email:
33
+ - theo@burtcorp.com
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - LICENSE.txt
39
+ - README.md
40
+ - bin/jara
41
+ - lib/jara.rb
42
+ - lib/jara/cli.rb
43
+ - lib/jara/releaser.rb
44
+ - lib/jara/version.rb
45
+ homepage: http://github.com/burtcorp/jara
46
+ licenses:
47
+ - BSD-3-Clause
48
+ metadata: {}
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - '>='
56
+ - !ruby/object:Gem::Version
57
+ version: 1.9.3
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubyforge_project:
65
+ rubygems_version: 2.2.2
66
+ signing_key:
67
+ specification_version: 4
68
+ summary: Builds and publishes project artifacts
69
+ test_files: []