boshify 0.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/bin/boshify +23 -0
- data/lib/boshify/command_line.rb +81 -0
- data/lib/boshify/downloader.rb +22 -0
- data/lib/boshify/filesystem.rb +44 -0
- data/lib/boshify/package_converter.rb +22 -0
- data/lib/boshify/release_creator.rb +154 -0
- data/lib/boshify/ubuntu_packages.rb +111 -0
- data/lib/boshify.rb +6 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 301facaa8c05504dbec23d0896f196a1714a7551
|
4
|
+
data.tar.gz: c37f62978214b529975c01c8c1ef3a84549ed5a0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3fe9bb319022d84106081a18ad7e341c90cb256d3e670b5c9eb9f70d4c56ee7c2012d35eb2e8eb68c8fc702e31ef22b3021c44b764d3ab4f8ed91fa156749cb7
|
7
|
+
data.tar.gz: 813e29e859b7945191cc061298038923ac9f76153610f886b6303a1c684748e2a463237ae001350d6191f957b8ba656f5a955f9a63678897bc9ec7a9fd9af4ed
|
data/bin/boshify
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'boshify'
|
3
|
+
require 'bosh_lite_helpers'
|
4
|
+
|
5
|
+
# Boshify generates BOSH releases
|
6
|
+
module Boshify
|
7
|
+
cr = BoshLiteHelpers::CommandRunner.new
|
8
|
+
fs = Filesystem.new
|
9
|
+
dl = Downloader.new(filesystem: fs)
|
10
|
+
release_creator = ReleaseCreator.new(filesystem: fs,
|
11
|
+
release_dir: Dir.pwd,
|
12
|
+
cmd_runner: cr)
|
13
|
+
pkg_source = UbuntuPackages.new(downloader: dl, cmd_runner: cr)
|
14
|
+
converter = PackageConverter.new(package_source: pkg_source,
|
15
|
+
downloader: dl,
|
16
|
+
release_creator: release_creator)
|
17
|
+
cmd_line = CommandLine.new(program_name: $PROGRAM_NAME,
|
18
|
+
package_converter: converter)
|
19
|
+
result = cmd_line.run(ARGV)
|
20
|
+
puts result[:stdout] unless result[:stdout].empty?
|
21
|
+
STDERR.puts result[:stderr] unless result[:stderr].empty?
|
22
|
+
exit result[:exit_code]
|
23
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module Boshify
|
5
|
+
# Command line argument processing
|
6
|
+
class CommandLine
|
7
|
+
def initialize(options)
|
8
|
+
unless options[:program_name]
|
9
|
+
fail ArgumentError, 'Program name must be specified'
|
10
|
+
end
|
11
|
+
unless options[:package_converter]
|
12
|
+
fail ArgumentError, 'Package converter must be specified'
|
13
|
+
end
|
14
|
+
@program_name = Pathname.new(options[:program_name]).basename
|
15
|
+
@package_converter = options[:package_converter]
|
16
|
+
end
|
17
|
+
|
18
|
+
def run(args)
|
19
|
+
with_options(args) do |options|
|
20
|
+
begin
|
21
|
+
use_mirror_if_specified(options[:mirror])
|
22
|
+
@package_converter.create_release_for(name: options[:package])
|
23
|
+
{ exit_code: 0, stdout: "Package #{options[:package]} converted",
|
24
|
+
stderr: '' }
|
25
|
+
rescue => e
|
26
|
+
{ exit_code: 1, stdout: '', stderr: e.message }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def with_options(args)
|
34
|
+
options = parse(args)
|
35
|
+
if options[:package]
|
36
|
+
yield options
|
37
|
+
else
|
38
|
+
{ exit_code: 0, stdout: @parser.help, stderr: '' }
|
39
|
+
end
|
40
|
+
rescue OptionParser::MissingArgument
|
41
|
+
{ exit_code: 1, stdout: @parser.help, stderr: '' }
|
42
|
+
end
|
43
|
+
|
44
|
+
def use_mirror_if_specified(mirror_url)
|
45
|
+
return unless mirror_url
|
46
|
+
@package_converter.package_source.mirror_url = mirror_url
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse(args)
|
50
|
+
options = {}
|
51
|
+
@parser = OptionParser.new do |cmd_opts|
|
52
|
+
cmd_opts.banner = "#{@program_name} [options]"
|
53
|
+
add_package_option(cmd_opts, options)
|
54
|
+
add_mirror_option(cmd_opts, options)
|
55
|
+
add_help_option(cmd_opts, options)
|
56
|
+
end
|
57
|
+
@parser.parse!(args)
|
58
|
+
options
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_package_option(cmd_opts, options)
|
62
|
+
cmd_opts.on('-p', '--package PACKAGE', 'Ubuntu source package') do |p|
|
63
|
+
options[:package] = p
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_mirror_option(cmd_opts, options)
|
68
|
+
cmd_opts.on('-m',
|
69
|
+
'--mirror [MIRROR]',
|
70
|
+
'Alternate Ubuntu mirror URL') do |m|
|
71
|
+
options[:mirror] = m
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def add_help_option(cmd_opts, options)
|
76
|
+
cmd_opts.on('-h', '--help', 'Print help') do
|
77
|
+
options[:help] = true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Boshify
|
5
|
+
class DownloadError < StandardError; end
|
6
|
+
|
7
|
+
# Downloads remote resources to disk
|
8
|
+
class Downloader
|
9
|
+
def initialize(options = {})
|
10
|
+
@filesystem = options[:filesystem]
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(url)
|
14
|
+
bn = Pathname.new(URI.parse(url.to_s).path).basename
|
15
|
+
r = HTTParty.get(url)
|
16
|
+
unless r.ok?
|
17
|
+
fail DownloadError, "The resource could not be retrieved: #{url}"
|
18
|
+
end
|
19
|
+
@filesystem.write_file(basename: bn, content: r.body)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tmpdir'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module Boshify
|
6
|
+
# Wrapper around filesystem operations
|
7
|
+
class Filesystem
|
8
|
+
def copy(from, to)
|
9
|
+
FileUtils.copy(from, to)
|
10
|
+
end
|
11
|
+
|
12
|
+
def mkdir_p(path)
|
13
|
+
path.mkpath
|
14
|
+
end
|
15
|
+
|
16
|
+
def write_file(options)
|
17
|
+
check_file_options!(options)
|
18
|
+
file = determine_file_path(options)
|
19
|
+
File.open(file.cleanpath, 'w') { |f| f.write(options[:content]) }
|
20
|
+
file
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def check_file_options!(options)
|
26
|
+
unless options[:content]
|
27
|
+
fail ArgumentError, 'File content must be specified'
|
28
|
+
end
|
29
|
+
|
30
|
+
# rubocop:disable GuardClause
|
31
|
+
unless options[:path] || options[:basename]
|
32
|
+
fail ArgumentError, 'Either basename or path must be specified'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def determine_file_path(options)
|
37
|
+
if options[:path]
|
38
|
+
options[:path]
|
39
|
+
else
|
40
|
+
Pathname.new(Dir.mktmpdir) + options[:basename].basename
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Boshify
|
2
|
+
# Converts an operating system package to a BOSH release
|
3
|
+
class PackageConverter
|
4
|
+
attr_reader :package_source
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
@package_source = options[:package_source]
|
8
|
+
@downloader = options[:downloader]
|
9
|
+
@release_creator = options[:release_creator]
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_release_for(package = {})
|
13
|
+
@package_source.refresh
|
14
|
+
local_path = @downloader.get(
|
15
|
+
@package_source.source_tarball_url(package[:name]))
|
16
|
+
@release_creator.create_release(name: package[:name], packages: [
|
17
|
+
name: package[:name],
|
18
|
+
source_tarball: local_path
|
19
|
+
])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Boshify
|
5
|
+
# Responsible for generating BOSH releases
|
6
|
+
# rubocop:disable ClassLength
|
7
|
+
class ReleaseCreator
|
8
|
+
def initialize(options)
|
9
|
+
check_options!(options)
|
10
|
+
@fs = options[:filesystem]
|
11
|
+
@release_dir = Pathname.new(options[:release_dir])
|
12
|
+
@cmd_runner = options[:cmd_runner]
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_release(release)
|
16
|
+
create_empty_release
|
17
|
+
create_placeholder_blobstore_config(release)
|
18
|
+
create_job(release)
|
19
|
+
create_packages(release)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def check_options!(options)
|
25
|
+
unless options[:filesystem]
|
26
|
+
fail ArgumentError, 'Filesystem must be provided'
|
27
|
+
end
|
28
|
+
unless options[:release_dir]
|
29
|
+
fail ArgumentError, 'Release directory must be provided'
|
30
|
+
end
|
31
|
+
# rubocop:disable GuardClause
|
32
|
+
unless options[:cmd_runner]
|
33
|
+
fail ArgumentError, 'Command runner must be provided'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_empty_release
|
38
|
+
create_release_dirs
|
39
|
+
generate_empty_blobs_yaml
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_release_dirs
|
43
|
+
%w(blobs config jobs packages src).each do |dir|
|
44
|
+
@fs.mkdir_p(@release_dir + dir)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def generate_empty_blobs_yaml
|
49
|
+
@fs.write_file(path: @release_dir + 'config' + 'blobs.yml',
|
50
|
+
content: YAML.dump({}))
|
51
|
+
end
|
52
|
+
|
53
|
+
# rubocop:disable MethodLength
|
54
|
+
def create_placeholder_blobstore_config(release)
|
55
|
+
@fs.write_file(path: @release_dir + 'config' + 'final.yml',
|
56
|
+
content: YAML.dump(
|
57
|
+
'blobstore' => {
|
58
|
+
'provider' => 's3',
|
59
|
+
'options' => {
|
60
|
+
'bucket_name' => "#{release[:name]}-release",
|
61
|
+
'access_key_id' => 'MY_ACCESS_KEY_ID',
|
62
|
+
'secret_acces_key' => 'MY_SECRET_ACCESS_KEY',
|
63
|
+
'encryption_key' => 'MY_ENCRYPTION_KEY'
|
64
|
+
}
|
65
|
+
},
|
66
|
+
'final_name' => release[:name]
|
67
|
+
))
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_job(release)
|
71
|
+
job_dir = @release_dir + 'jobs' + release[:name]
|
72
|
+
@fs.mkdir_p(job_dir)
|
73
|
+
@fs.write_file(path: job_dir + 'monit', content: '')
|
74
|
+
@fs.write_file(path: job_dir + 'spec', content: job_spec(release))
|
75
|
+
end
|
76
|
+
|
77
|
+
def job_spec(release)
|
78
|
+
YAML.dump(
|
79
|
+
'name' => release[:name],
|
80
|
+
'packages' => release[:packages].map { |p| p[:name] },
|
81
|
+
'templates' => {}
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_path(source_tarball)
|
86
|
+
files = @cmd_runner.run("tar -ztf #{source_tarball}",
|
87
|
+
quiet: true)[:stdout].split("\n")
|
88
|
+
directory_with_configure(files.map { |p| Pathname.new(p) })
|
89
|
+
end
|
90
|
+
|
91
|
+
def directory_with_configure(paths)
|
92
|
+
paths.find { |p| p.basename == Pathname.new('configure') }.dirname
|
93
|
+
end
|
94
|
+
|
95
|
+
def create_packages(release)
|
96
|
+
release[:packages].each do |pkg|
|
97
|
+
pkg_dir = make_package_dir(pkg[:name])
|
98
|
+
bp = blob_path(pkg)
|
99
|
+
|
100
|
+
generate_package_spec(pkg, pkg_dir, bp)
|
101
|
+
copy_blob_into_place(pkg, bp)
|
102
|
+
generate_package_script(pkg_dir, bp, pkg[:source_tarball])
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def generate_package_script(pkg_dir, blob_path, source_tarball)
|
107
|
+
pkg_script = packaging_script(blob_path, build_path(source_tarball))
|
108
|
+
@fs.write_file(path: pkg_dir + 'packaging', content: pkg_script)
|
109
|
+
end
|
110
|
+
|
111
|
+
def blob_path(pkg)
|
112
|
+
"#{pkg[:name]}/#{pkg[:source_tarball].basename}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def make_package_dir(pkg_name)
|
116
|
+
pkg_dir = @release_dir + 'packages' + pkg_name
|
117
|
+
@fs.mkdir_p(pkg_dir)
|
118
|
+
pkg_dir
|
119
|
+
end
|
120
|
+
|
121
|
+
def generate_package_spec(pkg, pkg_dir, blob_path)
|
122
|
+
@fs.write_file(path: pkg_dir + 'spec',
|
123
|
+
content: package_spec(pkg, blob_path))
|
124
|
+
end
|
125
|
+
|
126
|
+
def copy_blob_into_place(pkg, blob_path)
|
127
|
+
@fs.mkdir_p(@release_dir + 'blobs' + pkg[:name])
|
128
|
+
@fs.copy(pkg[:source_tarball],
|
129
|
+
Pathname.new(@release_dir + 'blobs' + blob_path))
|
130
|
+
end
|
131
|
+
|
132
|
+
def packaging_script(blob_path, build_dir_path)
|
133
|
+
"#!/bin/bash
|
134
|
+
set -e
|
135
|
+
set -u
|
136
|
+
|
137
|
+
tar zxvf #{blob_path}
|
138
|
+
cd #{build_dir_path}
|
139
|
+
|
140
|
+
./configure --prefix=${BOSH_INSTALL_TARGET}
|
141
|
+
|
142
|
+
make
|
143
|
+
make install"
|
144
|
+
end
|
145
|
+
|
146
|
+
def package_spec(pkg, blob_path)
|
147
|
+
YAML.dump(
|
148
|
+
'name' => pkg[:name],
|
149
|
+
'dependencies' => [],
|
150
|
+
'files' => [blob_path]
|
151
|
+
)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'uri'
|
2
|
+
module Boshify
|
3
|
+
class PackageNotFoundError < StandardError; end
|
4
|
+
class InvalidPackageMetadataError < StandardError; end
|
5
|
+
|
6
|
+
# Ubuntu package source
|
7
|
+
class UbuntuPackages
|
8
|
+
attr_reader :mirror_url
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@downloader = options[:downloader]
|
12
|
+
@cmd_runner = options[:cmd_runner]
|
13
|
+
self.mirror_url = options[:mirror_url] || 'http://us.archive.ubuntu.com/ubuntu'
|
14
|
+
end
|
15
|
+
|
16
|
+
def mirror_url=(mirror)
|
17
|
+
@mirror_url = URI.parse("#{mirror}/")
|
18
|
+
end
|
19
|
+
|
20
|
+
def refresh
|
21
|
+
@all_packages = packages_hash(
|
22
|
+
parse(decompress(download_sources_metadata)))
|
23
|
+
end
|
24
|
+
|
25
|
+
def source_tarball_url(package_name)
|
26
|
+
unless @all_packages[package_name]
|
27
|
+
fail PackageNotFoundError, "Package #{package_name} was not found"
|
28
|
+
end
|
29
|
+
pkg = @all_packages[package_name]
|
30
|
+
mirror_url + "#{pkg['Directory']}/#{original_tarball(pkg)}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse(input)
|
34
|
+
group_package_values(
|
35
|
+
as_a_hash(
|
36
|
+
parse_multiline_values(
|
37
|
+
group_by_whether_pairs(
|
38
|
+
split_key_value_pairs(input)))))
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def original_tarball(pkg)
|
44
|
+
pkg['Files'].find { |f| f[:name].end_with?('orig.tar.gz') }[:name]
|
45
|
+
end
|
46
|
+
|
47
|
+
def download_sources_metadata
|
48
|
+
@downloader.get(mirror_url + 'dists/lucid/main/source/Sources.bz2')
|
49
|
+
end
|
50
|
+
|
51
|
+
def decompress(local_path)
|
52
|
+
result = @cmd_runner.run("bzcat #{local_path}", quiet: true)
|
53
|
+
if result[:exit_code] != 0
|
54
|
+
fail InvalidPackageMetadataError,
|
55
|
+
"Could not decompress: #{result[:stderr]}"
|
56
|
+
end
|
57
|
+
result[:stdout]
|
58
|
+
end
|
59
|
+
|
60
|
+
FILE_KEYS = %w(Files Checksums-Sha1 Checksums-Sha256)
|
61
|
+
|
62
|
+
def split_key_value_pairs(input)
|
63
|
+
input.lines.map { |line| line.split(':', 2).map { |f| f.strip } }
|
64
|
+
end
|
65
|
+
|
66
|
+
def group_by_whether_pairs(pairs)
|
67
|
+
pairs.chunk { |p| p.size == 2 }.to_a
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_multiline_values(pairs)
|
71
|
+
# rubocop:disable Next
|
72
|
+
pairs.each_with_index do |v, i|
|
73
|
+
if !v[0] && FILE_KEYS.include?(pairs[i - 1][1].last[0])
|
74
|
+
pairs[i - 1][1].last[1] = remove_empty(v[1]).map do |line|
|
75
|
+
file = line[0].split(' ')
|
76
|
+
{ name: file[2], size_bytes: file[1].to_i, checksum: file[0] }
|
77
|
+
end
|
78
|
+
pairs[i] = nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
pairs.compact
|
82
|
+
end
|
83
|
+
|
84
|
+
def remove_empty(lines)
|
85
|
+
lines.reject { |line| line[0].empty? }
|
86
|
+
end
|
87
|
+
|
88
|
+
def as_a_hash(pairs)
|
89
|
+
pairs.map { |b, p| Hash[p] if b }.compact
|
90
|
+
end
|
91
|
+
|
92
|
+
def group_package_values(pkgs)
|
93
|
+
last_pkg_index = -1
|
94
|
+
pkgs.each_with_index do |pkg, i|
|
95
|
+
if pkg.key?('Package')
|
96
|
+
last_pkg_index = i
|
97
|
+
else
|
98
|
+
pkgs[last_pkg_index].merge!(pkg)
|
99
|
+
pkgs[i] = nil
|
100
|
+
end
|
101
|
+
end
|
102
|
+
pkgs.compact
|
103
|
+
end
|
104
|
+
|
105
|
+
def packages_hash(pkgs)
|
106
|
+
Hash[pkgs.map do |pkg|
|
107
|
+
[pkg['Package'], pkg]
|
108
|
+
end]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/lib/boshify.rb
ADDED
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: boshify
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew Crump
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-08-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: httparty
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.13.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.13.1
|
27
|
+
description: Generates BOSH releases
|
28
|
+
email: andrew@cloudcredo.com
|
29
|
+
executables:
|
30
|
+
- boshify
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- bin/boshify
|
35
|
+
- lib/boshify.rb
|
36
|
+
- lib/boshify/command_line.rb
|
37
|
+
- lib/boshify/downloader.rb
|
38
|
+
- lib/boshify/filesystem.rb
|
39
|
+
- lib/boshify/package_converter.rb
|
40
|
+
- lib/boshify/release_creator.rb
|
41
|
+
- lib/boshify/ubuntu_packages.rb
|
42
|
+
homepage: https://github.com/cloudcredo/boshify
|
43
|
+
licenses:
|
44
|
+
- Apache
|
45
|
+
metadata: {}
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 2.2.2
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: Generates BOSH releases
|
66
|
+
test_files: []
|