puppet_forge 1.0.6 → 2.0.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.
- data/CHANGELOG.md +23 -0
- data/MAINTAINERS +13 -0
- data/README.md +48 -6
- data/lib/puppet_forge.rb +4 -0
- data/lib/puppet_forge/connection.rb +81 -0
- data/lib/puppet_forge/connection/connection_failure.rb +26 -0
- data/lib/puppet_forge/error.rb +34 -0
- data/lib/{her → puppet_forge}/lazy_accessors.rb +20 -27
- data/lib/{her → puppet_forge}/lazy_relations.rb +28 -9
- data/lib/puppet_forge/middleware/symbolify_json.rb +72 -0
- data/lib/puppet_forge/tar.rb +10 -0
- data/lib/puppet_forge/tar/mini.rb +81 -0
- data/lib/puppet_forge/unpacker.rb +68 -0
- data/lib/puppet_forge/v3.rb +11 -0
- data/lib/puppet_forge/v3/base.rb +106 -73
- data/lib/puppet_forge/v3/base/paginated_collection.rb +23 -14
- data/lib/puppet_forge/v3/metadata.rb +197 -0
- data/lib/puppet_forge/v3/module.rb +2 -1
- data/lib/puppet_forge/v3/release.rb +33 -8
- data/lib/puppet_forge/v3/user.rb +2 -0
- data/lib/puppet_forge/version.rb +1 -1
- data/puppet_forge.gemspec +6 -3
- data/spec/fixtures/v3/modules/puppetlabs-apache.json +21 -1
- data/spec/fixtures/v3/releases/puppetlabs-apache-0.0.1.json +4 -1
- data/spec/integration/forge/v3/module_spec.rb +79 -0
- data/spec/integration/forge/v3/release_spec.rb +75 -0
- data/spec/integration/forge/v3/user_spec.rb +70 -0
- data/spec/spec_helper.rb +15 -8
- data/spec/unit/forge/connection/connection_failure_spec.rb +30 -0
- data/spec/unit/forge/connection_spec.rb +53 -0
- data/spec/unit/{her → forge}/lazy_accessors_spec.rb +20 -13
- data/spec/unit/{her → forge}/lazy_relations_spec.rb +60 -46
- data/spec/unit/forge/middleware/symbolify_json_spec.rb +63 -0
- data/spec/unit/forge/tar/mini_spec.rb +85 -0
- data/spec/unit/forge/tar_spec.rb +9 -0
- data/spec/unit/forge/unpacker_spec.rb +58 -0
- data/spec/unit/forge/v3/base/paginated_collection_spec.rb +68 -46
- data/spec/unit/forge/v3/base_spec.rb +1 -1
- data/spec/unit/forge/v3/metadata_spec.rb +300 -0
- data/spec/unit/forge/v3/module_spec.rb +14 -36
- data/spec/unit/forge/v3/release_spec.rb +9 -30
- data/spec/unit/forge/v3/user_spec.rb +7 -7
- metadata +127 -41
- checksums.yaml +0 -7
- data/lib/puppet_forge/middleware/json_for_her.rb +0 -37
@@ -2,22 +2,31 @@ module PuppetForge
|
|
2
2
|
module V3
|
3
3
|
class Base
|
4
4
|
|
5
|
-
#
|
6
|
-
|
7
|
-
|
5
|
+
# Enables navigation of the Forge API's paginated datasets.
|
6
|
+
class PaginatedCollection < Array
|
7
|
+
|
8
|
+
# Default pagination limit for API request
|
9
|
+
LIMIT = 20
|
8
10
|
|
9
|
-
# In addition to the standard Her::Collection arguments, this
|
10
|
-
# constructor requires a reference to the paginated class. This enables
|
11
|
-
# the collection to load related pages.
|
12
|
-
#
|
13
11
|
# @api private
|
14
|
-
# @param klass [
|
12
|
+
# @param klass [PuppetForge::V3::Base] the class to page over
|
15
13
|
# @param data [Array] the current data page
|
16
14
|
# @param metadata [Hash<(:limit, :total, :offset)>] page metadata
|
17
15
|
# @param errors [Object] errors for the page request
|
18
|
-
def initialize(klass, data, metadata, errors)
|
19
|
-
super(
|
16
|
+
def initialize(klass, data = [], metadata = {:total => 0, :offset => 0, :limit => LIMIT}, errors = nil)
|
17
|
+
super()
|
18
|
+
@metadata = metadata
|
19
|
+
@errors = errors
|
20
20
|
@klass = klass
|
21
|
+
|
22
|
+
data.each do |item|
|
23
|
+
self << @klass.new(item)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# For backwards compatibility, all returns the current object.
|
28
|
+
def all
|
29
|
+
self
|
21
30
|
end
|
22
31
|
|
23
32
|
# An enumerator that iterates over the entire collection, independent
|
@@ -26,7 +35,7 @@ module PuppetForge
|
|
26
35
|
#
|
27
36
|
# @return [Enumerator] an iterator for the entire collection
|
28
37
|
def unpaginated
|
29
|
-
page = @klass.get_collection(metadata[:first])
|
38
|
+
page = @klass.get_collection(@metadata[:first])
|
30
39
|
Enumerator.new do |emitter|
|
31
40
|
loop do
|
32
41
|
page.each { |x| emitter << x }
|
@@ -42,7 +51,7 @@ module PuppetForge
|
|
42
51
|
# @!method offset
|
43
52
|
# @return [Integer] the offset for the current page
|
44
53
|
[ :total, :limit, :offset ].each do |info|
|
45
|
-
define_method(info) { metadata[info] }
|
54
|
+
define_method(info) { @metadata[info] }
|
46
55
|
end
|
47
56
|
|
48
57
|
[ :next, :previous ].each do |link|
|
@@ -53,7 +62,7 @@ module PuppetForge
|
|
53
62
|
# Returns the previous page if a previous page exists.
|
54
63
|
# @return [PaginatedCollection, nil] the previous page
|
55
64
|
define_method(link) do
|
56
|
-
return unless path = metadata[link]
|
65
|
+
return unless path = @metadata[link]
|
57
66
|
@klass.get_collection(path)
|
58
67
|
end
|
59
68
|
|
@@ -64,7 +73,7 @@ module PuppetForge
|
|
64
73
|
# Returns the url of the previous page if a previous page exists.
|
65
74
|
# @return [String, nil] the previous page's url
|
66
75
|
define_method("#{link}_url") do
|
67
|
-
metadata[link]
|
76
|
+
@metadata[link]
|
68
77
|
end
|
69
78
|
end
|
70
79
|
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'json'
|
3
|
+
require 'set'
|
4
|
+
require 'semantic_puppet/version'
|
5
|
+
|
6
|
+
module PuppetForge
|
7
|
+
module V3
|
8
|
+
# This class provides a data structure representing a module's metadata.
|
9
|
+
# @api private
|
10
|
+
class Metadata
|
11
|
+
|
12
|
+
attr_accessor :module_name
|
13
|
+
|
14
|
+
DEFAULTS = {
|
15
|
+
'name' => nil,
|
16
|
+
'version' => nil,
|
17
|
+
'author' => nil,
|
18
|
+
'summary' => nil,
|
19
|
+
'license' => 'Apache-2.0',
|
20
|
+
'source' => '',
|
21
|
+
'project_page' => nil,
|
22
|
+
'issues_url' => nil,
|
23
|
+
'dependencies' => Set.new.freeze,
|
24
|
+
}
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@data = DEFAULTS.dup
|
28
|
+
@data['dependencies'] = @data['dependencies'].dup
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns a filesystem-friendly version of this module name.
|
32
|
+
def dashed_name
|
33
|
+
PuppetForge::V3.normalize_name(@data['name']) if @data['name']
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns a string that uniquely represents this version of this module.
|
37
|
+
def release_name
|
38
|
+
return nil unless @data['name'] && @data['version']
|
39
|
+
[ dashed_name, @data['version'] ].join('-')
|
40
|
+
end
|
41
|
+
|
42
|
+
alias :name :module_name
|
43
|
+
alias :full_module_name :dashed_name
|
44
|
+
|
45
|
+
# Merges the current set of metadata with another metadata hash. This
|
46
|
+
# method also handles the validation of module names and versions, in an
|
47
|
+
# effort to be proactive about module publishing constraints.
|
48
|
+
def update(data, with_dependencies = true)
|
49
|
+
process_name(data) if data['name']
|
50
|
+
process_version(data) if data['version']
|
51
|
+
process_source(data) if data['source']
|
52
|
+
merge_dependencies(data) if with_dependencies && data['dependencies']
|
53
|
+
|
54
|
+
@data.merge!(data)
|
55
|
+
return self
|
56
|
+
end
|
57
|
+
|
58
|
+
# Validates the name and version_requirement for a dependency, then creates
|
59
|
+
# the Dependency and adds it.
|
60
|
+
# Returns the Dependency that was added.
|
61
|
+
def add_dependency(name, version_requirement=nil, repository=nil)
|
62
|
+
validate_name(name)
|
63
|
+
validate_version_range(version_requirement) if version_requirement
|
64
|
+
|
65
|
+
if dup = @data['dependencies'].find { |d| d.full_module_name == name && d.version_requirement != version_requirement }
|
66
|
+
raise ArgumentError, "Dependency conflict for #{full_module_name}: Dependency #{name} was given conflicting version requirements #{version_requirement} and #{dup.version_requirement}. Verify that there are no duplicates in the metadata.json or the Modulefile."
|
67
|
+
end
|
68
|
+
|
69
|
+
dep = Dependency.new(name, version_requirement, repository)
|
70
|
+
@data['dependencies'].add(dep)
|
71
|
+
|
72
|
+
dep
|
73
|
+
end
|
74
|
+
|
75
|
+
# Provides an accessor for the now defunct 'description' property. This
|
76
|
+
# addresses a regression in Puppet 3.6.x where previously valid templates
|
77
|
+
# refering to the 'description' property were broken.
|
78
|
+
# @deprecated
|
79
|
+
def description
|
80
|
+
@data['description']
|
81
|
+
end
|
82
|
+
|
83
|
+
def dependencies
|
84
|
+
@data['dependencies'].to_a
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns a hash of the module's metadata. Used by Puppet's automated
|
88
|
+
# serialization routines.
|
89
|
+
#
|
90
|
+
# @see Puppet::Network::FormatSupport#to_data_hash
|
91
|
+
def to_hash
|
92
|
+
@data
|
93
|
+
end
|
94
|
+
alias :to_data_hash :to_hash
|
95
|
+
|
96
|
+
def to_json
|
97
|
+
data = @data.dup.merge('dependencies' => dependencies)
|
98
|
+
|
99
|
+
contents = data.keys.map do |k|
|
100
|
+
value = (JSON.pretty_generate(data[k]) rescue data[k].to_json)
|
101
|
+
"#{k.to_json}: #{value}"
|
102
|
+
end
|
103
|
+
|
104
|
+
"{\n" + contents.join(",\n").gsub(/^/, ' ') + "\n}\n"
|
105
|
+
end
|
106
|
+
|
107
|
+
# Expose any metadata keys as callable reader methods.
|
108
|
+
def method_missing(name, *args)
|
109
|
+
return @data[name.to_s] if @data.key? name.to_s
|
110
|
+
super
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
# Do basic validation and parsing of the name parameter.
|
116
|
+
def process_name(data)
|
117
|
+
validate_name(data['name'])
|
118
|
+
author, @module_name = data['name'].split(/[-\/]/, 2)
|
119
|
+
|
120
|
+
data['author'] ||= author if @data['author'] == DEFAULTS['author']
|
121
|
+
end
|
122
|
+
|
123
|
+
# Do basic validation on the version parameter.
|
124
|
+
def process_version(data)
|
125
|
+
validate_version(data['version'])
|
126
|
+
end
|
127
|
+
|
128
|
+
# Do basic parsing of the source parameter. If the source is hosted on
|
129
|
+
# GitHub, we can predict sensible defaults for both project_page and
|
130
|
+
# issues_url.
|
131
|
+
def process_source(data)
|
132
|
+
if data['source'] =~ %r[://]
|
133
|
+
source_uri = URI.parse(data['source'])
|
134
|
+
else
|
135
|
+
source_uri = URI.parse("http://#{data['source']}")
|
136
|
+
end
|
137
|
+
|
138
|
+
if source_uri.host =~ /^(www\.)?github\.com$/
|
139
|
+
source_uri.scheme = 'https'
|
140
|
+
source_uri.path.sub!(/\.git$/, '')
|
141
|
+
data['project_page'] ||= @data['project_page'] || source_uri.to_s
|
142
|
+
data['issues_url'] ||= @data['issues_url'] || source_uri.to_s.sub(/\/*$/, '') + '/issues'
|
143
|
+
end
|
144
|
+
|
145
|
+
rescue URI::Error
|
146
|
+
return
|
147
|
+
end
|
148
|
+
|
149
|
+
# Validates and parses the dependencies.
|
150
|
+
def merge_dependencies(data)
|
151
|
+
data['dependencies'].each do |dep|
|
152
|
+
add_dependency(dep['name'], dep['version_requirement'], dep['repository'])
|
153
|
+
end
|
154
|
+
|
155
|
+
# Clear dependencies so @data dependencies are not overwritten
|
156
|
+
data.delete 'dependencies'
|
157
|
+
end
|
158
|
+
|
159
|
+
# Validates that the given module name is both namespaced and well-formed.
|
160
|
+
def validate_name(name)
|
161
|
+
return if name =~ /\A[a-z0-9]+[-\/][a-z][a-z0-9_]*\Z/i
|
162
|
+
|
163
|
+
namespace, modname = name.split(/[-\/]/, 2)
|
164
|
+
modname = :namespace_missing if namespace == ''
|
165
|
+
|
166
|
+
err = case modname
|
167
|
+
when nil, '', :namespace_missing
|
168
|
+
"the field must be a namespaced module name"
|
169
|
+
when /[^a-z0-9_]/i
|
170
|
+
"the module name contains non-alphanumeric (or underscore) characters"
|
171
|
+
when /^[^a-z]/i
|
172
|
+
"the module name must begin with a letter"
|
173
|
+
else
|
174
|
+
"the namespace contains non-alphanumeric characters"
|
175
|
+
end
|
176
|
+
|
177
|
+
raise ArgumentError, "Invalid 'name' field in metadata.json: #{err}"
|
178
|
+
end
|
179
|
+
|
180
|
+
# Validates that the version string can be parsed by SemanticPuppet.
|
181
|
+
def validate_version(version)
|
182
|
+
return if SemanticPuppet::Version.valid?(version)
|
183
|
+
|
184
|
+
err = "version string cannot be parsed as a valid Semantic Version"
|
185
|
+
raise ArgumentError, "Invalid 'version' field in metadata.json: #{err}"
|
186
|
+
end
|
187
|
+
|
188
|
+
# Validates that the version range can be parsed by SemanticPuppet.
|
189
|
+
def validate_version_range(version_range)
|
190
|
+
SemanticPuppet::VersionRange.parse(version_range)
|
191
|
+
rescue ArgumentError => e
|
192
|
+
raise ArgumentError, "Invalid 'version_range' field in metadata.json: #{e}"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
@@ -6,8 +6,8 @@ module PuppetForge
|
|
6
6
|
|
7
7
|
# Models a specific release version of a Puppet Module on the Forge.
|
8
8
|
class Release < Base
|
9
|
-
lazy :module
|
10
|
-
|
9
|
+
lazy :module, 'Module'
|
10
|
+
|
11
11
|
# Returns a fully qualified URL for downloading this release from the Forge.
|
12
12
|
#
|
13
13
|
# @return [String] fully qualified download URL for release
|
@@ -21,15 +21,40 @@ module PuppetForge
|
|
21
21
|
|
22
22
|
# Downloads the Release tarball to the specified file path.
|
23
23
|
#
|
24
|
-
# @
|
25
|
-
# @param file [String] the file to create
|
24
|
+
# @param path [Pathname]
|
26
25
|
# @return [void]
|
27
|
-
def download(
|
28
|
-
self.class.
|
29
|
-
|
26
|
+
def download(path)
|
27
|
+
resp = self.class.conn.get(file_url)
|
28
|
+
path.open('wb') { |fh| fh.write(resp.body) }
|
29
|
+
rescue Faraday::ResourceNotFound => e
|
30
|
+
raise PuppetForge::ReleaseNotFound, "The module release #{slug} does not exist on #{conn.url_prefix}.", e.backtrace
|
31
|
+
end
|
32
|
+
|
33
|
+
# Verify that a downloaded module matches the checksum in the metadata for this release.
|
34
|
+
#
|
35
|
+
# @param path [Pathname]
|
36
|
+
# @return [void]
|
37
|
+
def verify(path)
|
38
|
+
expected_md5 = file_md5
|
39
|
+
file_md5 = Digest::MD5.file(path).hexdigest
|
40
|
+
if expected_md5 != file_md5
|
41
|
+
raise ChecksumMismatch.new("Expected #{path} checksum to be #{expected_md5}, got #{file_md5}")
|
30
42
|
end
|
31
|
-
nil
|
32
43
|
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def file_url
|
48
|
+
"/v3/files/#{slug}.tar.gz"
|
49
|
+
end
|
50
|
+
|
51
|
+
def resource_url
|
52
|
+
"/v3/releases/#{slug}"
|
53
|
+
end
|
54
|
+
|
55
|
+
class ChecksumMismatch < StandardError
|
56
|
+
end
|
57
|
+
|
33
58
|
end
|
34
59
|
end
|
35
60
|
end
|
data/lib/puppet_forge/v3/user.rb
CHANGED
data/lib/puppet_forge/version.rb
CHANGED
data/puppet_forge.gemspec
CHANGED
@@ -8,7 +8,8 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = PuppetForge::VERSION
|
9
9
|
spec.authors = ["Puppet Labs"]
|
10
10
|
spec.email = ["forge-team+api@puppetlabs.com"]
|
11
|
-
spec.summary = "Access
|
11
|
+
spec.summary = "Access the Puppet Forge API from Ruby for resource information and to download releases."
|
12
|
+
spec.description = %q{Tools that can be used to access Forge API information on Modules, Users, and Releases. As well as download, unpack, and install Releases to a directory.}
|
12
13
|
spec.homepage = "https://github.com/puppetlabs/forge-ruby"
|
13
14
|
spec.license = "Apache-2.0"
|
14
15
|
|
@@ -19,8 +20,10 @@ Gem::Specification.new do |spec|
|
|
19
20
|
|
20
21
|
spec.required_ruby_version = '>= 1.9.3'
|
21
22
|
|
22
|
-
spec.add_runtime_dependency "
|
23
|
-
spec.add_runtime_dependency "
|
23
|
+
spec.add_runtime_dependency "faraday", "~> 0.9.0"
|
24
|
+
spec.add_runtime_dependency "faraday_middleware", "~> 0.9.0"
|
25
|
+
spec.add_dependency 'semantic_puppet', '~> 0.1.0'
|
26
|
+
spec.add_dependency 'minitar'
|
24
27
|
|
25
28
|
spec.add_development_dependency "bundler", "~> 1.6"
|
26
29
|
spec.add_development_dependency "rake"
|
@@ -1,16 +1,19 @@
|
|
1
1
|
{
|
2
2
|
"uri": "/v3/modules/puppetlabs-apache",
|
3
|
+
"slug": "puppetlabs-apache",
|
3
4
|
"name": "apache",
|
4
5
|
"downloads": 81387,
|
5
6
|
"created_at": "2010-05-20 22:43:19 -0700",
|
6
7
|
"updated_at": "2014-01-06 14:42:07 -0800",
|
7
8
|
"owner": {
|
8
9
|
"uri": "/v3/users/puppetlabs",
|
10
|
+
"slug": "puppetlabs",
|
9
11
|
"username": "puppetlabs",
|
10
12
|
"gravatar_id": "fdd009b7c1ec96e088b389f773e87aec"
|
11
13
|
},
|
12
14
|
"current_release": {
|
13
15
|
"uri": "/v3/releases/puppetlabs-apache-0.10.0",
|
16
|
+
"slug": "puppetlabs-apache-0.10.0",
|
14
17
|
"module": {
|
15
18
|
"uri": "/v3/modules/puppetlabs-apache",
|
16
19
|
"name": "apache",
|
@@ -318,73 +321,90 @@
|
|
318
321
|
"releases": [
|
319
322
|
{
|
320
323
|
"uri": "/v3/releases/puppetlabs-apache-0.10.0",
|
324
|
+
"slug": "puppetlabs-apache-0.10.0",
|
321
325
|
"version": "0.10.0"
|
322
326
|
},
|
323
327
|
{
|
324
328
|
"uri": "/v3/releases/puppetlabs-apache-0.9.0",
|
329
|
+
"slug": "puppetlabs-apache-0.9.0",
|
325
330
|
"version": "0.9.0"
|
326
331
|
},
|
327
332
|
{
|
328
333
|
"uri": "/v3/releases/puppetlabs-apache-0.8.1",
|
334
|
+
"slug": "puppetlabs-apache-0.8.1",
|
329
335
|
"version": "0.8.1"
|
330
336
|
},
|
331
337
|
{
|
332
338
|
"uri": "/v3/releases/puppetlabs-apache-0.8.0",
|
339
|
+
"slug": "puppetlabs-apache-0.8.0",
|
333
340
|
"version": "0.8.0"
|
334
341
|
},
|
335
342
|
{
|
336
343
|
"uri": "/v3/releases/puppetlabs-apache-0.7.0",
|
344
|
+
"slug": "puppetlabs-apache-0.7.0",
|
337
345
|
"version": "0.7.0"
|
338
346
|
},
|
339
347
|
{
|
340
348
|
"uri": "/v3/releases/puppetlabs-apache-0.6.0",
|
349
|
+
"slug": "puppetlabs-apache-0.6.0",
|
341
350
|
"version": "0.6.0"
|
342
351
|
},
|
343
352
|
{
|
344
353
|
"uri": "/v3/releases/puppetlabs-apache-0.5.0-rc1",
|
354
|
+
"slug": "puppetlabs-apache-0.5.0-rc1",
|
345
355
|
"version": "0.5.0-rc1"
|
346
356
|
},
|
347
357
|
{
|
348
358
|
"uri": "/v3/releases/puppetlabs-apache-0.4.0",
|
359
|
+
"slug": "puppetlabs-apache-0.4.0",
|
349
360
|
"version": "0.4.0"
|
350
361
|
},
|
351
362
|
{
|
352
363
|
"uri": "/v3/releases/puppetlabs-apache-0.3.0",
|
364
|
+
"slug": "puppetlabs-apache-0.3.0",
|
353
365
|
"version": "0.3.0"
|
354
366
|
},
|
355
367
|
{
|
356
368
|
"uri": "/v3/releases/puppetlabs-apache-0.2.2",
|
369
|
+
"slug": "puppetlabs-apache-0.2.2",
|
357
370
|
"version": "0.2.2"
|
358
371
|
},
|
359
372
|
{
|
360
373
|
"uri": "/v3/releases/puppetlabs-apache-0.2.1",
|
374
|
+
"slug": "puppetlabs-apache-0.2.1",
|
361
375
|
"version": "0.2.1"
|
362
376
|
},
|
363
377
|
{
|
364
378
|
"uri": "/v3/releases/puppetlabs-apache-0.2.0",
|
379
|
+
"slug": "puppetlabs-apache-0.2.0",
|
365
380
|
"version": "0.2.0"
|
366
381
|
},
|
367
382
|
{
|
368
383
|
"uri": "/v3/releases/puppetlabs-apache-0.1.1",
|
384
|
+
"slug": "puppetlabs-apache-0.1.1",
|
369
385
|
"version": "0.1.1"
|
370
386
|
},
|
371
387
|
{
|
372
388
|
"uri": "/v3/releases/puppetlabs-apache-0.0.4",
|
389
|
+
"slug": "puppetlabs-apache-0.0.4",
|
373
390
|
"version": "0.0.4"
|
374
391
|
},
|
375
392
|
{
|
376
393
|
"uri": "/v3/releases/puppetlabs-apache-0.0.3",
|
394
|
+
"slug": "puppetlabs-apache-0.0.3",
|
377
395
|
"version": "0.0.3"
|
378
396
|
},
|
379
397
|
{
|
380
398
|
"uri": "/v3/releases/puppetlabs-apache-0.0.2",
|
399
|
+
"slug": "puppetlabs-apache-0.0.2",
|
381
400
|
"version": "0.0.2"
|
382
401
|
},
|
383
402
|
{
|
384
403
|
"uri": "/v3/releases/puppetlabs-apache-0.0.1",
|
404
|
+
"slug": "puppetlabs-apache-0.0.1",
|
385
405
|
"version": "0.0.1"
|
386
406
|
}
|
387
407
|
],
|
388
408
|
"homepage_url": "https://github.com/puppetlabs/puppetlabs-apache",
|
389
409
|
"issues_url": "https://tickets.puppetlabs.com"
|
390
|
-
}
|
410
|
+
}
|