pray-cli 1.6.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/CHANGELOG.md +47 -0
- data/LICENSE.md +21 -0
- data/README.md +125 -0
- data/SECURITY.md +28 -0
- data/bin/polyrun +108 -0
- data/bin/pray +26 -0
- data/lib/pray/archive.rb +68 -0
- data/lib/pray/auth_client.rb +97 -0
- data/lib/pray/cli/commands/auth.rb +42 -0
- data/lib/pray/cli/commands/distribution.rb +17 -0
- data/lib/pray/cli/commands/init.rb +62 -0
- data/lib/pray/cli/commands/meta.rb +15 -0
- data/lib/pray/cli/commands/packages.rb +107 -0
- data/lib/pray/cli/commands/trust.rb +73 -0
- data/lib/pray/cli/commands/workflow.rb +126 -0
- data/lib/pray/cli/help.rb +168 -0
- data/lib/pray/cli/helpers.rb +166 -0
- data/lib/pray/cli/parse.rb +136 -0
- data/lib/pray/cli/parse_auth.rb +91 -0
- data/lib/pray/cli/parse_trust.rb +152 -0
- data/lib/pray/cli/suggest.rb +57 -0
- data/lib/pray/cli.rb +117 -0
- data/lib/pray/confess.rb +113 -0
- data/lib/pray/config.rb +52 -0
- data/lib/pray/constraint.rb +80 -0
- data/lib/pray/destination.rb +158 -0
- data/lib/pray/dotenv.rb +46 -0
- data/lib/pray/environment.rb +41 -0
- data/lib/pray/error.rb +55 -0
- data/lib/pray/format_manifest.rb +255 -0
- data/lib/pray/format_serialize.rb +135 -0
- data/lib/pray/git_sources.rb +228 -0
- data/lib/pray/hashing.rb +59 -0
- data/lib/pray/invocation.rb +110 -0
- data/lib/pray/literal.rb +382 -0
- data/lib/pray/lockfile.rb +259 -0
- data/lib/pray/lockfile_serialize.rb +125 -0
- data/lib/pray/manifest.rb +126 -0
- data/lib/pray/manifest_formatter.rb +56 -0
- data/lib/pray/manifest_json.rb +128 -0
- data/lib/pray/manifest_parser.rb +152 -0
- data/lib/pray/manifest_parser_blocks.rb +216 -0
- data/lib/pray/manifest_parser_helpers.rb +208 -0
- data/lib/pray/materialize.rb +120 -0
- data/lib/pray/package_spec.rb +245 -0
- data/lib/pray/path_safety.rb +41 -0
- data/lib/pray/plan.rb +83 -0
- data/lib/pray/project_context.rb +67 -0
- data/lib/pray/publish.rb +162 -0
- data/lib/pray/registry.rb +355 -0
- data/lib/pray/render.rb +360 -0
- data/lib/pray/resolve.rb +396 -0
- data/lib/pray/resolve_context.rb +19 -0
- data/lib/pray/serve.rb +130 -0
- data/lib/pray/serve_federation.rb +109 -0
- data/lib/pray/session.rb +90 -0
- data/lib/pray/ssh_agent.rb +115 -0
- data/lib/pray/statement_surface.rb +262 -0
- data/lib/pray/substitute.rb +50 -0
- data/lib/pray/sync.rb +219 -0
- data/lib/pray/terminal.rb +30 -0
- data/lib/pray/trust.rb +201 -0
- data/lib/pray/trust_feed.rb +110 -0
- data/lib/pray/trust_ops.rb +202 -0
- data/lib/pray/verify.rb +257 -0
- data/lib/pray/version.rb +6 -0
- data/lib/pray-cli.rb +4 -0
- data/lib/pray.rb +46 -0
- data/pray-cli.gemspec +48 -0
- metadata +187 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
PackageExport = Struct.new(:kind, :path, :summary, :only, :except, :default_path) do
|
|
5
|
+
def initialize(kind: "fragment", path: "", summary: nil, only: [], except: [], default_path: nil)
|
|
6
|
+
super
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
PackageSkill = Struct.new(:path, :summary)
|
|
10
|
+
PackageTemplate = Struct.new(:path, :summary)
|
|
11
|
+
PackageDependency = Struct.new(:name, :constraint, :optional)
|
|
12
|
+
|
|
13
|
+
PackageSpec = Struct.new(
|
|
14
|
+
:name, :version, :summary, :description, :authors, :license, :homepage,
|
|
15
|
+
:source_code_uri, :changelog_uri, :prayfile_version, :files, :exports,
|
|
16
|
+
:skills, :templates, :adapters, :targets, :dependencies, :metadata
|
|
17
|
+
) do
|
|
18
|
+
def initialize(
|
|
19
|
+
name: "", version: "", summary: nil, description: nil, authors: [], license: nil,
|
|
20
|
+
homepage: nil, source_code_uri: nil, changelog_uri: nil, prayfile_version: nil,
|
|
21
|
+
files: [], exports: {}, skills: {}, templates: {}, adapters: {}, targets: [],
|
|
22
|
+
dependencies: [], metadata: {}
|
|
23
|
+
)
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def canonicalized
|
|
28
|
+
dup.tap do |copy|
|
|
29
|
+
copy.files = files.sort
|
|
30
|
+
copy.authors = authors.sort
|
|
31
|
+
copy.targets = targets.sort
|
|
32
|
+
copy.dependencies = dependencies.sort_by { |dependency| [dependency.name, dependency.constraint, dependency.optional] }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def tree_hash_for_root(root)
|
|
37
|
+
file_bytes = {}
|
|
38
|
+
files.each do |file|
|
|
39
|
+
path = File.join(root, file)
|
|
40
|
+
raise Error.integrity("package file missing: #{file}") unless File.exist?(path)
|
|
41
|
+
raise Error.integrity("package file is a directory: #{file}") if File.directory?(path)
|
|
42
|
+
|
|
43
|
+
file_bytes[file] = File.binread(path)
|
|
44
|
+
end
|
|
45
|
+
self.class.tree_hash_from_file_bytes(file_bytes)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.tree_hash_from_file_bytes(file_bytes)
|
|
49
|
+
entries = file_bytes.map { |path, bytes| [path, Hashing.sha256_prefixed(bytes)] }.sort_by(&:first)
|
|
50
|
+
serialized = entries.map do |path, hash|
|
|
51
|
+
"file\0regular\0#{path}\0#{hash}\n"
|
|
52
|
+
end.join
|
|
53
|
+
Hashing.sha256_prefixed(serialized)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
module PackageSpecParser
|
|
58
|
+
module_function
|
|
59
|
+
|
|
60
|
+
module ParserHelpers
|
|
61
|
+
include ManifestMethods::ParserHelpers
|
|
62
|
+
|
|
63
|
+
def parse_dependency(rest, optional)
|
|
64
|
+
values, keywords = parse_call(rest)
|
|
65
|
+
name = string_from_value(values.first)
|
|
66
|
+
constraint = values[1] ? string_from_value(values[1]) : "*"
|
|
67
|
+
PackageDependency.new(
|
|
68
|
+
name: name,
|
|
69
|
+
constraint: constraint,
|
|
70
|
+
optional: keywords["optional"]&.as_bool || optional
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def parse_exports(value)
|
|
75
|
+
map = Literal.parse_literal_map(value)
|
|
76
|
+
exports = {}
|
|
77
|
+
map.each do |name, literal|
|
|
78
|
+
entry = literal.as_map
|
|
79
|
+
raise Error.parse("prayspec", "export #{name} must be a map") unless entry
|
|
80
|
+
|
|
81
|
+
exports[name] = PackageExport.new(
|
|
82
|
+
kind: map_string(entry, "type") || "fragment",
|
|
83
|
+
path: map_string(entry, "path") || raise(Error.parse("prayspec", "export #{name} missing path")),
|
|
84
|
+
summary: map_string(entry, "summary"),
|
|
85
|
+
only: map_string_array(entry, "only"),
|
|
86
|
+
except: map_string_array(entry, "except"),
|
|
87
|
+
default_path: map_string(entry, "default_path")
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
exports
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def parse_skills(value)
|
|
94
|
+
map = Literal.parse_literal_map(value)
|
|
95
|
+
map.transform_values do |literal|
|
|
96
|
+
entry = literal.as_map
|
|
97
|
+
raise Error.parse("prayspec", "skill must be a map") unless entry
|
|
98
|
+
|
|
99
|
+
PackageSkill.new(
|
|
100
|
+
path: map_string(entry, "path") || raise(Error.parse("prayspec", "skill missing path")),
|
|
101
|
+
summary: map_string(entry, "summary")
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def parse_templates(value)
|
|
107
|
+
map = Literal.parse_literal_map(value)
|
|
108
|
+
map.transform_values do |literal|
|
|
109
|
+
entry = literal.as_map
|
|
110
|
+
raise Error.parse("prayspec", "template must be a map") unless entry
|
|
111
|
+
|
|
112
|
+
PackageTemplate.new(
|
|
113
|
+
path: map_string(entry, "path") || raise(Error.parse("prayspec", "template missing path")),
|
|
114
|
+
summary: map_string(entry, "summary")
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def parse_string_map(value)
|
|
120
|
+
Literal.parse_literal_map(value).transform_values { |literal| string_from_value(literal) }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def map_string(map, key)
|
|
124
|
+
map[key]&.as_string
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def map_string_array(map, key)
|
|
128
|
+
values = map[key]&.as_array
|
|
129
|
+
return [] unless values
|
|
130
|
+
|
|
131
|
+
values.filter_map(&:as_string)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def array_of_strings(value)
|
|
135
|
+
array = Literal.parse_literal(value)
|
|
136
|
+
values = array.as_array
|
|
137
|
+
raise Error.parse("prayspec", "expected array") unless values
|
|
138
|
+
|
|
139
|
+
values.map { |entry| string_from_value(entry) }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def string_from_value(value)
|
|
143
|
+
text = value.as_string
|
|
144
|
+
raise Error.parse("prayspec", "expected string-like literal, found #{value.inspect}") unless text
|
|
145
|
+
|
|
146
|
+
text
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def string_from_literal(value)
|
|
150
|
+
string_from_value(Literal.parse_literal(value))
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def parse_package_spec(text)
|
|
155
|
+
lines = Literal.prepare_parser_lines(text)
|
|
156
|
+
BlockParser.new(lines).parse_root
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
class BlockParser
|
|
160
|
+
include ParserHelpers
|
|
161
|
+
|
|
162
|
+
def initialize(lines)
|
|
163
|
+
@lines = lines
|
|
164
|
+
@cursor = 0
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def parse_root
|
|
168
|
+
expect_start!
|
|
169
|
+
spec = PackageSpec.new
|
|
170
|
+
while (statement = next_statement)
|
|
171
|
+
return spec.canonicalized if statement == "end"
|
|
172
|
+
|
|
173
|
+
apply_statement(spec, statement)
|
|
174
|
+
end
|
|
175
|
+
raise Error.parse("prayspec", "missing 'end'")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def expect_start!
|
|
179
|
+
statement = next_statement
|
|
180
|
+
raise Error.parse("prayspec", "empty package spec") unless statement
|
|
181
|
+
unless statement.start_with?("Package::Specification.new")
|
|
182
|
+
raise Error.parse("prayspec", "expected Package::Specification.new")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def apply_statement(spec, statement)
|
|
187
|
+
case statement
|
|
188
|
+
when /\Aspec\.add_dependency (.+)\z/
|
|
189
|
+
spec.dependencies << parse_dependency(Regexp.last_match(1), false)
|
|
190
|
+
when /\Aspec\.add_optional_dependency (.+)\z/
|
|
191
|
+
spec.dependencies << parse_dependency(Regexp.last_match(1), true)
|
|
192
|
+
when /\Aspec\.(.+) = (.+)\z/
|
|
193
|
+
apply_assignment(spec, Regexp.last_match(1).strip, Regexp.last_match(2).strip)
|
|
194
|
+
else
|
|
195
|
+
raise Error.parse("prayspec", "unrecognized statement: #{statement}")
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def apply_assignment(spec, field, value)
|
|
200
|
+
case field
|
|
201
|
+
when "name" then spec.name = string_from_literal(value)
|
|
202
|
+
when "version" then spec.version = string_from_literal(value)
|
|
203
|
+
when "summary" then spec.summary = string_from_literal(value)
|
|
204
|
+
when "description" then spec.description = string_from_literal(value)
|
|
205
|
+
when "authors" then spec.authors = array_of_strings(value)
|
|
206
|
+
when "license" then spec.license = string_from_literal(value)
|
|
207
|
+
when "homepage" then spec.homepage = string_from_literal(value)
|
|
208
|
+
when "source_code_uri" then spec.source_code_uri = string_from_literal(value)
|
|
209
|
+
when "changelog_uri" then spec.changelog_uri = string_from_literal(value)
|
|
210
|
+
when "prayfile_version" then spec.prayfile_version = string_from_literal(value)
|
|
211
|
+
when "files" then spec.files = array_of_strings(value)
|
|
212
|
+
when "targets" then spec.targets = array_of_strings(value)
|
|
213
|
+
when "exports" then spec.exports = parse_exports(value)
|
|
214
|
+
when "skills" then spec.skills = parse_skills(value)
|
|
215
|
+
when "templates" then spec.templates = parse_templates(value)
|
|
216
|
+
when "adapters" then spec.adapters = parse_string_map(value)
|
|
217
|
+
when "metadata" then spec.metadata = Literal.parse_literal_map(value)
|
|
218
|
+
else
|
|
219
|
+
raise Error.parse("prayspec", "unsupported assignment: #{field}")
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def next_statement
|
|
224
|
+
while @cursor < @lines.length
|
|
225
|
+
statement = @lines[@cursor].strip
|
|
226
|
+
@cursor += 1
|
|
227
|
+
next if statement.empty?
|
|
228
|
+
|
|
229
|
+
while !statement.end_with?(" do") && statement != "end" && @cursor < @lines.length &&
|
|
230
|
+
(statement.rstrip.end_with?(",") || !Literal.is_balanced?(statement))
|
|
231
|
+
next_line = @lines[@cursor].strip
|
|
232
|
+
@cursor += 1
|
|
233
|
+
next if next_line.empty?
|
|
234
|
+
|
|
235
|
+
statement = "#{statement} #{next_line}"
|
|
236
|
+
end
|
|
237
|
+
return statement
|
|
238
|
+
end
|
|
239
|
+
nil
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
extend PackageSpecParser
|
|
245
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Pray
|
|
6
|
+
module PathSafety
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def path_under_root?(root, candidate)
|
|
10
|
+
root_path = Pathname.new(File.expand_path(root)).cleanpath
|
|
11
|
+
candidate_path = Pathname.new(File.expand_path(candidate)).cleanpath
|
|
12
|
+
candidate_path == root_path || candidate_path.to_s.start_with?("#{root_path}#{File::SEPARATOR}")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def join_under_root(root, *segments)
|
|
16
|
+
root_path = Pathname.new(File.expand_path(root)).cleanpath
|
|
17
|
+
candidate = root_path.join(*segments).cleanpath
|
|
18
|
+
return candidate.to_s if path_under_root?(root_path.to_s, candidate.to_s)
|
|
19
|
+
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def reject_unsafe_package_name!(package_name)
|
|
24
|
+
if package_name.nil? || package_name.empty? || package_name.include?("\0") || package_name.include?("\\") ||
|
|
25
|
+
package_name.include?("..")
|
|
26
|
+
raise Error.resolution("invalid package name: #{package_name.inspect}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
package_name
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def sanitize_relative_path(path)
|
|
33
|
+
cleaned = path.to_s.delete_prefix("/").tr("\\", "/")
|
|
34
|
+
if cleaned.empty? || cleaned.include?("\0") || cleaned.split("/").include?("..")
|
|
35
|
+
raise Error.resolution("unsafe relative path: #{path.inspect}")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
cleaned
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/pray/plan.rb
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Pray
|
|
6
|
+
module Plan
|
|
7
|
+
MaterializationPreview = Struct.new(
|
|
8
|
+
:package_lines, :lockfile, :targets, :provisioned, :warnings
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def build_materialization_preview(project, rendered, lockfile, lockfile_path, previous_lockfile)
|
|
14
|
+
MaterializationPreview.new(
|
|
15
|
+
package_lines: package_summary_lines(previous_lockfile, lockfile, project),
|
|
16
|
+
lockfile: lockfile_change_status(lockfile_path, lockfile),
|
|
17
|
+
targets: rendered.map { |target| target_change(project, target) },
|
|
18
|
+
provisioned: Render.planned_provisioned_files(project).map { |file| provisioned_change(project, file) },
|
|
19
|
+
warnings: []
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def print_materialization_report(preview, mode)
|
|
24
|
+
puts (mode == :plan) ? "Plan" : "Install"
|
|
25
|
+
preview.package_lines.each { |line| puts line }
|
|
26
|
+
puts "Lockfile: #{preview.lockfile}"
|
|
27
|
+
preview.targets.each do |path, change|
|
|
28
|
+
puts "Target #{path}: #{change}"
|
|
29
|
+
end
|
|
30
|
+
preview.provisioned.each do |path, change|
|
|
31
|
+
puts "Provisioned #{path}: #{change}"
|
|
32
|
+
end
|
|
33
|
+
preview.warnings.each { |warning| puts "Warning: #{warning}" }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def package_summary_lines(previous_lockfile, lockfile, _project)
|
|
37
|
+
if previous_lockfile.nil?
|
|
38
|
+
lockfile.package.map do |entry|
|
|
39
|
+
"Package #{entry.name} #{entry.version} (new)"
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
lockfile.package.filter_map do |entry|
|
|
43
|
+
previous = previous_lockfile.package.find { |package| package.name == entry.name }
|
|
44
|
+
next "Package #{entry.name} #{entry.version} (new)" unless previous
|
|
45
|
+
next if previous.version == entry.version && previous.tree_hash == entry.tree_hash
|
|
46
|
+
|
|
47
|
+
"Package #{entry.name} #{previous.version} -> #{entry.version}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def lockfile_change_status(lockfile_path, lockfile)
|
|
53
|
+
return "create" unless File.exist?(lockfile_path)
|
|
54
|
+
|
|
55
|
+
existing = Pray.read_lockfile(lockfile_path)
|
|
56
|
+
Pray.lockfiles_equivalent?(lockfile, existing) ? "unchanged" : "update"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def target_change(project, target)
|
|
60
|
+
path = File.join(project.project_root, target.path)
|
|
61
|
+
change = if !File.exist?(path)
|
|
62
|
+
"write"
|
|
63
|
+
elsif File.read(path) == target.content
|
|
64
|
+
"unchanged"
|
|
65
|
+
else
|
|
66
|
+
"update"
|
|
67
|
+
end
|
|
68
|
+
[target.path, change]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def provisioned_change(project, file)
|
|
72
|
+
destination = File.join(project.project_root, file.path)
|
|
73
|
+
change = if !File.exist?(destination)
|
|
74
|
+
"write"
|
|
75
|
+
elsif FileUtils.compare_file(destination, file.source)
|
|
76
|
+
"unchanged"
|
|
77
|
+
else
|
|
78
|
+
"update"
|
|
79
|
+
end
|
|
80
|
+
[file.path, change]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Pray
|
|
6
|
+
ENV_PROJECT_PATH = "PRAY_PATH"
|
|
7
|
+
ENV_MANIFEST_PATH = "PRAY_FILE_PATH"
|
|
8
|
+
ENV_ENVIRONMENT = "PRAY_ENV"
|
|
9
|
+
|
|
10
|
+
ProjectInvocationContext = Struct.new(:project_root, :manifest_path, :environment) do
|
|
11
|
+
def lockfile_path
|
|
12
|
+
File.join(project_root, "Prayfile.lock")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
ProjectInvocationOptions = Struct.new(:project_root, :manifest_path, :environment)
|
|
17
|
+
|
|
18
|
+
module ProjectContext
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def from_current_directory
|
|
22
|
+
from_options(ProjectInvocationOptions.new)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def from_options(options)
|
|
26
|
+
cwd = Dir.pwd
|
|
27
|
+
dotenv = Dotenv.load_dotenv_variables(cwd)
|
|
28
|
+
project_root_hint = options.project_root ||
|
|
29
|
+
env_value(ENV_PROJECT_PATH) ||
|
|
30
|
+
dotenv[ENV_PROJECT_PATH] ||
|
|
31
|
+
cwd
|
|
32
|
+
project_root = canonicalize_path(cwd, project_root_hint)
|
|
33
|
+
manifest_hint = options.manifest_path ||
|
|
34
|
+
env_value(ENV_MANIFEST_PATH) ||
|
|
35
|
+
dotenv[ENV_MANIFEST_PATH] ||
|
|
36
|
+
"Prayfile"
|
|
37
|
+
manifest_path = if Pathname(manifest_hint).absolute?
|
|
38
|
+
canonicalize_path(cwd, manifest_hint)
|
|
39
|
+
else
|
|
40
|
+
canonicalize_path(project_root, manifest_hint)
|
|
41
|
+
end
|
|
42
|
+
environment = (options.environment || env_value(ENV_ENVIRONMENT) || dotenv[ENV_ENVIRONMENT])&.strip
|
|
43
|
+
environment = nil if environment.nil? || environment.empty?
|
|
44
|
+
|
|
45
|
+
ProjectInvocationContext.new(
|
|
46
|
+
project_root: project_root,
|
|
47
|
+
manifest_path: manifest_path,
|
|
48
|
+
environment: environment
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def env_value(key)
|
|
53
|
+
value = ENV[key]
|
|
54
|
+
return nil if value.nil? || value.strip.empty?
|
|
55
|
+
|
|
56
|
+
value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def canonicalize_path(base, path)
|
|
60
|
+
resolved = Pathname(path).absolute? ? Pathname(path) : Pathname(base).join(path)
|
|
61
|
+
resolved = resolved.expand_path
|
|
62
|
+
resolved.exist? ? resolved.realpath.to_s : resolved.cleanpath.to_s
|
|
63
|
+
rescue
|
|
64
|
+
resolved.to_s
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/pray/publish.rb
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module Pray
|
|
8
|
+
RegistryIndex = Struct.new(:spec, :packages) do
|
|
9
|
+
def initialize(spec: "prayfile-distribution-1", packages: [])
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module Publish
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def publish_to_root(project, root, signer: "local", signer_fingerprint: nil)
|
|
18
|
+
root = File.expand_path(root)
|
|
19
|
+
index = load_registry_index(root)
|
|
20
|
+
package_names = index.packages.to_set
|
|
21
|
+
|
|
22
|
+
project.packages.each do |package|
|
|
23
|
+
archive_bytes = Archive.build_package_archive_bytes(package)
|
|
24
|
+
artifact_path = registry_artifact_path(package.declaration.name, package.spec.version)
|
|
25
|
+
artifact_output_path = File.join(root, artifact_path)
|
|
26
|
+
write_output_bytes(artifact_output_path, archive_bytes)
|
|
27
|
+
|
|
28
|
+
metadata_path = registry_metadata_path(root, package.declaration.name)
|
|
29
|
+
metadata = load_registry_package_metadata(metadata_path, package.declaration.name)
|
|
30
|
+
version_entry = published_registry_package_version(
|
|
31
|
+
package,
|
|
32
|
+
signer,
|
|
33
|
+
signer_fingerprint,
|
|
34
|
+
archive_bytes,
|
|
35
|
+
artifact_path
|
|
36
|
+
)
|
|
37
|
+
metadata.versions.reject! { |entry| entry.version == version_entry.version }
|
|
38
|
+
metadata.versions << version_entry
|
|
39
|
+
write_registry_package_metadata(metadata_path, metadata)
|
|
40
|
+
package_names << package.declaration.name
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
index.packages = package_names.sort
|
|
44
|
+
write_registry_index(root, index)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def publish_to_server(project, server_url, signer: "local", signer_fingerprint: nil)
|
|
48
|
+
project.packages.each do |package|
|
|
49
|
+
archive_bytes = Archive.build_package_archive_bytes(package)
|
|
50
|
+
artifact_path = registry_artifact_path(package.declaration.name, package.spec.version)
|
|
51
|
+
Registry.http_put(join_url(server_url, artifact_path), "application/octet-stream", archive_bytes)
|
|
52
|
+
|
|
53
|
+
metadata = RegistryPackageMetadata.new(
|
|
54
|
+
name: package.declaration.name,
|
|
55
|
+
versions: [
|
|
56
|
+
published_registry_package_version(
|
|
57
|
+
package,
|
|
58
|
+
signer,
|
|
59
|
+
signer_fingerprint,
|
|
60
|
+
archive_bytes,
|
|
61
|
+
artifact_path
|
|
62
|
+
)
|
|
63
|
+
]
|
|
64
|
+
)
|
|
65
|
+
Registry.http_put(
|
|
66
|
+
join_url(server_url, "v1/packages/#{package.declaration.name}.json"),
|
|
67
|
+
"application/json",
|
|
68
|
+
JSON.pretty_generate(metadata_to_hash(metadata))
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def published_registry_package_version(package, signer, signer_fingerprint, archive_bytes, artifact_path)
|
|
74
|
+
RegistryPackageVersion.new(
|
|
75
|
+
version: package.spec.version,
|
|
76
|
+
artifact: artifact_path,
|
|
77
|
+
artifact_hash: Hashing.sha256_prefixed(archive_bytes),
|
|
78
|
+
tree_hash: package.tree_hash,
|
|
79
|
+
yanked: false,
|
|
80
|
+
targets: package.spec.targets,
|
|
81
|
+
exports: package.spec.exports.keys,
|
|
82
|
+
signer: signer,
|
|
83
|
+
signer_fingerprint: signer_fingerprint,
|
|
84
|
+
published_at: Time.now.utc.iso8601,
|
|
85
|
+
signature: Registry.registry_artifact_signature(archive_bytes, package.tree_hash, signer)
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def load_registry_index(root)
|
|
90
|
+
path = File.join(root, "v1", "index.json")
|
|
91
|
+
return RegistryIndex.new unless File.exist?(path)
|
|
92
|
+
|
|
93
|
+
data = JSON.parse(File.read(path))
|
|
94
|
+
RegistryIndex.new(spec: data["spec"], packages: data["packages"] || [])
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def write_registry_index(root, index)
|
|
98
|
+
path = File.join(root, "v1", "index.json")
|
|
99
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
100
|
+
File.write(path, JSON.pretty_generate({"spec" => index.spec, "packages" => index.packages}))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def load_registry_package_metadata(path, package_name)
|
|
104
|
+
if File.exist?(path)
|
|
105
|
+
data = JSON.parse(File.read(path))
|
|
106
|
+
RegistryPackageMetadata.new(
|
|
107
|
+
name: data["name"],
|
|
108
|
+
versions: Array(data["versions"]).map { |entry| Registry.version_from_hash(entry) }
|
|
109
|
+
)
|
|
110
|
+
else
|
|
111
|
+
RegistryPackageMetadata.new(name: package_name, versions: [])
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def write_registry_package_metadata(path, metadata)
|
|
116
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
117
|
+
File.write(path, JSON.pretty_generate(metadata_to_hash(metadata)))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def metadata_to_hash(metadata)
|
|
121
|
+
{
|
|
122
|
+
"name" => metadata.name,
|
|
123
|
+
"versions" => metadata.versions.map { |entry| version_to_hash(entry) }
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def version_to_hash(entry)
|
|
128
|
+
hash = {
|
|
129
|
+
"version" => entry.version,
|
|
130
|
+
"artifact" => entry.artifact,
|
|
131
|
+
"yanked" => entry.yanked,
|
|
132
|
+
"targets" => entry.targets,
|
|
133
|
+
"exports" => entry.exports
|
|
134
|
+
}
|
|
135
|
+
hash["artifact_hash"] = entry.artifact_hash if entry.artifact_hash
|
|
136
|
+
hash["tree_hash"] = entry.tree_hash if entry.tree_hash
|
|
137
|
+
hash["signer"] = entry.signer if entry.signer
|
|
138
|
+
hash["signer_fingerprint"] = entry.signer_fingerprint if entry.signer_fingerprint
|
|
139
|
+
hash["published_at"] = entry.published_at if entry.published_at
|
|
140
|
+
hash["signature"] = entry.signature if entry.signature
|
|
141
|
+
hash
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def registry_metadata_path(root, package_name)
|
|
145
|
+
File.join(root, "v1", "packages", "#{package_name}.json")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def registry_artifact_path(package_name, version)
|
|
149
|
+
artifact_name = "#{package_name.tr("/", "-")}-#{version}.praypkg"
|
|
150
|
+
"v1/artifacts/#{package_name}/#{version}/#{artifact_name}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def write_output_bytes(path, bytes)
|
|
154
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
155
|
+
File.binwrite(path, bytes)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def join_url(base, path)
|
|
159
|
+
Registry.join_url(base, path)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|