pray-cli 1.21.0 → 1.22.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 +4 -4
- data/CHANGELOG.md +18 -0
- data/lib/pray/cli/commands/distribution.rb +34 -3
- data/lib/pray/cli/commands/packages.rb +9 -7
- data/lib/pray/cli/commands/update_latest.rb +11 -4
- data/lib/pray/cli/commands/workflow.rb +4 -2
- data/lib/pray/cli/help.rb +7 -0
- data/lib/pray/cli.rb +14 -0
- data/lib/pray/derived_metadata.rb +194 -0
- data/lib/pray/git_cache.rb +46 -19
- data/lib/pray/git_legacy.rb +36 -0
- data/lib/pray/git_materialize.rb +204 -38
- data/lib/pray/git_store.rb +101 -14
- data/lib/pray/package_spec.rb +6 -1
- data/lib/pray/package_spec_paths.rb +18 -0
- data/lib/pray/publish.rb +49 -5
- data/lib/pray/registry.rb +10 -7
- data/lib/pray/sync.rb +1 -4
- data/lib/pray/tar_validation.rb +2 -0
- data/lib/pray/terminal.rb +28 -0
- data/lib/pray/upstream_merge.rb +6 -2
- data/lib/pray/upstream_plan.rb +84 -0
- data/lib/pray/version.rb +2 -2
- data/lib/pray.rb +3 -0
- metadata +6 -2
data/lib/pray/git_materialize.rb
CHANGED
|
@@ -5,7 +5,10 @@ require "open3"
|
|
|
5
5
|
require_relative "error"
|
|
6
6
|
require_relative "git_clone"
|
|
7
7
|
require_relative "git_run"
|
|
8
|
+
require_relative "git_store"
|
|
8
9
|
require_relative "path_safety"
|
|
10
|
+
require_relative "resource_limits"
|
|
11
|
+
require_relative "tar_validation"
|
|
9
12
|
|
|
10
13
|
module Pray
|
|
11
14
|
module GitMaterialize
|
|
@@ -14,56 +17,129 @@ module Pray
|
|
|
14
17
|
|
|
15
18
|
module_function
|
|
16
19
|
|
|
17
|
-
def read_local_artifact_bytes(source_root, artifact)
|
|
18
|
-
if artifact.start_with?("file
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return File.binread(path)
|
|
23
|
-
end
|
|
24
|
-
if artifact.match?(%r{\A[a-z][a-z0-9+.-]*:}i)
|
|
25
|
-
raise Error.integrity("remote artifact path must be relative: #{artifact}")
|
|
20
|
+
def read_local_artifact_bytes(source_root, artifact, offline: false)
|
|
21
|
+
if artifact.include?("://") || artifact.start_with?("file:")
|
|
22
|
+
raise Error.integrity(
|
|
23
|
+
"local and git catalog artifacts must be relative paths under the distribution root, not URLs: #{artifact}"
|
|
24
|
+
)
|
|
26
25
|
end
|
|
27
26
|
|
|
28
27
|
path = PathSafety.join_under_root(source_root, artifact)
|
|
29
28
|
raise Error.resolution("package artifact path escapes distribution root") unless path
|
|
30
29
|
|
|
31
|
-
materialize_catalog_file(source_root, artifact) unless File.exist?(path)
|
|
30
|
+
materialize_catalog_file(source_root, artifact, offline: offline) unless File.exist?(path)
|
|
32
31
|
raise Error.resolution("package artifact missing at #{path}") unless File.exist?(path)
|
|
33
32
|
|
|
34
33
|
File.binread(path)
|
|
35
34
|
end
|
|
36
35
|
|
|
37
|
-
def materialize_catalog_tree(git_dir, dest, revision, subdir, refresh)
|
|
38
|
-
|
|
36
|
+
def materialize_catalog_tree(git_dir, dest, revision, subdir, refresh, offline: false)
|
|
37
|
+
require_revision_present!(git_dir, revision, offline: offline)
|
|
38
|
+
GitStore.with_git_cache_lock(dest) do
|
|
39
|
+
return if !refresh && catalog_matches?(dest, git_dir, revision)
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
staging = "#{dest}.partial"
|
|
42
|
+
FileUtils.rm_rf(staging)
|
|
43
|
+
FileUtils.mkdir_p(staging)
|
|
44
|
+
unpacked = GitClone.catalog_sparse_cones(subdir).any? do |prefix|
|
|
45
|
+
unpack_archive_prefix(git_dir, staging, revision, prefix, offline: offline)
|
|
46
|
+
end
|
|
47
|
+
unless unpacked
|
|
48
|
+
FileUtils.rm_rf(staging)
|
|
49
|
+
if offline
|
|
50
|
+
raise Error.resolution(
|
|
51
|
+
"git catalog at revision #{revision} is not materializable locally and offline mode is enabled"
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
raise Error.resolution("no pray distribution root in git source at revision #{revision}")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
File.write(File.join(staging, REVISION_MARKER), revision)
|
|
58
|
+
File.write(File.join(staging, GIT_DIR_MARKER), git_dir)
|
|
59
|
+
publish_catalog_tree!(dest, staging)
|
|
44
60
|
end
|
|
45
|
-
|
|
61
|
+
end
|
|
46
62
|
|
|
47
|
-
|
|
48
|
-
|
|
63
|
+
def publish_catalog_tree!(dest, staging)
|
|
64
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
65
|
+
if File.exist?(dest)
|
|
66
|
+
backup = "#{dest}.previous"
|
|
67
|
+
FileUtils.rm_rf(backup)
|
|
68
|
+
File.rename(dest, backup)
|
|
69
|
+
begin
|
|
70
|
+
File.rename(staging, dest)
|
|
71
|
+
rescue
|
|
72
|
+
File.rename(backup, dest) if File.exist?(backup)
|
|
73
|
+
FileUtils.rm_rf(staging)
|
|
74
|
+
raise
|
|
75
|
+
end
|
|
76
|
+
FileUtils.rm_rf(backup)
|
|
77
|
+
else
|
|
78
|
+
File.rename(staging, dest)
|
|
79
|
+
end
|
|
49
80
|
end
|
|
81
|
+
private_class_method :publish_catalog_tree!
|
|
50
82
|
|
|
51
|
-
def materialize_catalog_file(source_root, relative)
|
|
52
|
-
|
|
83
|
+
def materialize_catalog_file(source_root, relative, offline: false)
|
|
84
|
+
full_path = File.join(source_root, relative)
|
|
85
|
+
return if File.file?(full_path)
|
|
53
86
|
|
|
54
87
|
markers = find_catalog_markers(source_root)
|
|
55
88
|
return unless markers
|
|
56
89
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
markers.fetch(:git_dir),
|
|
61
|
-
"
|
|
62
|
-
markers.fetch(:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
GitStore.with_git_cache_lock(markers.fetch(:catalog_root)) do
|
|
91
|
+
return if File.file?(full_path)
|
|
92
|
+
|
|
93
|
+
require_revision_present!(markers.fetch(:git_dir), markers.fetch(:revision), offline: offline)
|
|
94
|
+
repo_relative = full_path.delete_prefix(markers.fetch(:catalog_root)).delete_prefix("/")
|
|
95
|
+
object = "#{markers.fetch(:revision)}:#{repo_relative}"
|
|
96
|
+
require_blob_within_limit!(markers.fetch(:git_dir), object, offline: offline)
|
|
97
|
+
env = GitRun.git_env(offline_git_env(offline))
|
|
98
|
+
stdout, status = Open3.capture2(
|
|
99
|
+
env,
|
|
100
|
+
"git",
|
|
101
|
+
"-c",
|
|
102
|
+
"protocol.file.allow=always",
|
|
103
|
+
"-C",
|
|
104
|
+
markers.fetch(:git_dir),
|
|
105
|
+
"show",
|
|
106
|
+
object,
|
|
107
|
+
stdin_data: ""
|
|
108
|
+
)
|
|
109
|
+
unless status.success?
|
|
110
|
+
if offline
|
|
111
|
+
raise Error.resolution(
|
|
112
|
+
"git catalog blob #{repo_relative} at revision #{markers.fetch(:revision)} is not available locally and offline mode is enabled"
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
annotate_git_materialize_failure!(
|
|
116
|
+
markers.fetch(:git_dir),
|
|
117
|
+
markers.fetch(:revision),
|
|
118
|
+
"git show failed"
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
write_catalog_blob_atomic!(full_path, stdout)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def require_revision_present!(git_dir, revision, offline: false)
|
|
126
|
+
return if GitRun.try_run_git(git_dir, "cat-file", "-e", revision)
|
|
127
|
+
|
|
128
|
+
if offline
|
|
129
|
+
raise Error.resolution(
|
|
130
|
+
"git revision #{revision} is not available in #{git_dir} and offline mode is enabled (git_revision_unfetched)."
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
raise Error.resolution(
|
|
134
|
+
"git revision #{revision} is not available in #{git_dir} (git_revision_unfetched). " \
|
|
135
|
+
"Run `pray install --locked`, or clear the project git cache and shared bare then install again."
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def annotate_git_materialize_failure!(git_dir, revision, detail)
|
|
140
|
+
raise Error.resolution(
|
|
141
|
+
"git catalog materialize failed for revision #{revision} in #{git_dir} (git_cache_stale): #{detail}. " \
|
|
142
|
+
"Run `pray install --locked`, or clear `.pray/cache/git` and the shared bare then install again."
|
|
67
143
|
)
|
|
68
144
|
end
|
|
69
145
|
|
|
@@ -73,6 +149,7 @@ module Pray
|
|
|
73
149
|
return false unless File.file?(revision_path) && File.file?(git_dir_path)
|
|
74
150
|
return false unless File.read(revision_path).strip == revision
|
|
75
151
|
return false unless File.read(git_dir_path).strip == git_dir
|
|
152
|
+
return false unless GitRun.try_run_git(git_dir, "cat-file", "-e", revision)
|
|
76
153
|
|
|
77
154
|
File.directory?(File.join(dest, "v1", "packages")) ||
|
|
78
155
|
File.directory?(File.join(dest, "prayers", "v1", "packages")) ||
|
|
@@ -81,8 +158,10 @@ module Pray
|
|
|
81
158
|
false
|
|
82
159
|
end
|
|
83
160
|
|
|
84
|
-
def unpack_archive_prefix(git_dir, dest, revision, prefix)
|
|
85
|
-
|
|
161
|
+
def unpack_archive_prefix(git_dir, dest, revision, prefix, offline: false)
|
|
162
|
+
return false unless tree_prefix_within_limits?(git_dir, revision, prefix, offline: offline)
|
|
163
|
+
|
|
164
|
+
env = GitRun.git_env(offline_git_env(offline).merge("GIT_TERMINAL_PROMPT" => "0"))
|
|
86
165
|
stdout, _stderr, status = Open3.capture3(
|
|
87
166
|
env,
|
|
88
167
|
"git",
|
|
@@ -98,13 +177,78 @@ module Pray
|
|
|
98
177
|
stdin_data: ""
|
|
99
178
|
)
|
|
100
179
|
return false unless status.success? && !stdout.empty?
|
|
180
|
+
if stdout.bytesize > ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES
|
|
181
|
+
raise Error.integrity(
|
|
182
|
+
"git catalog archive exceeds #{ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES} bytes"
|
|
183
|
+
)
|
|
184
|
+
end
|
|
101
185
|
|
|
102
|
-
|
|
103
|
-
raise Error.resolution("failed to unpack git catalog archive") unless extract_status.success?
|
|
104
|
-
|
|
186
|
+
TarValidation.extract!(stdout, dest)
|
|
105
187
|
true
|
|
106
188
|
end
|
|
107
189
|
|
|
190
|
+
def require_blob_within_limit!(git_dir, object, offline: false)
|
|
191
|
+
env = GitRun.git_env(offline_git_env(offline))
|
|
192
|
+
stdout, status = Open3.capture2(
|
|
193
|
+
env, "git", "-c", "protocol.file.allow=always", "-C", git_dir, "cat-file", "-s", object,
|
|
194
|
+
stdin_data: ""
|
|
195
|
+
)
|
|
196
|
+
return unless status.success?
|
|
197
|
+
|
|
198
|
+
size = stdout.strip.to_i
|
|
199
|
+
return if size <= ResourceLimits::MAX_ARCHIVE_ENTRY_BYTES
|
|
200
|
+
|
|
201
|
+
raise Error.integrity(
|
|
202
|
+
"git catalog blob exceeds #{ResourceLimits::MAX_ARCHIVE_ENTRY_BYTES} bytes"
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def tree_prefix_within_limits?(git_dir, revision, prefix, offline: false)
|
|
207
|
+
env = GitRun.git_env(offline_git_env(offline))
|
|
208
|
+
stdout, status = Open3.capture2(
|
|
209
|
+
env, "git", "-c", "protocol.file.allow=always", "-C", git_dir,
|
|
210
|
+
"ls-tree", "-r", "-l", revision, "--", prefix, stdin_data: ""
|
|
211
|
+
)
|
|
212
|
+
return false unless status.success? && !stdout.strip.empty?
|
|
213
|
+
|
|
214
|
+
entry_count = 0
|
|
215
|
+
total_bytes = 0
|
|
216
|
+
stdout.each_line do |line|
|
|
217
|
+
parts = line.split
|
|
218
|
+
next unless parts[1] == "blob"
|
|
219
|
+
|
|
220
|
+
size = parts[3].to_i
|
|
221
|
+
entry_count += 1
|
|
222
|
+
if entry_count > ResourceLimits::MAX_ARCHIVE_ENTRIES
|
|
223
|
+
raise Error.integrity(
|
|
224
|
+
"git catalog archive exceeds #{ResourceLimits::MAX_ARCHIVE_ENTRIES} entries"
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
if size > ResourceLimits::MAX_ARCHIVE_ENTRY_BYTES
|
|
228
|
+
raise Error.integrity(
|
|
229
|
+
"git catalog archive entry exceeds #{ResourceLimits::MAX_ARCHIVE_ENTRY_BYTES} bytes"
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
total_bytes += size
|
|
233
|
+
if total_bytes > ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES
|
|
234
|
+
raise Error.integrity(
|
|
235
|
+
"git catalog archive exceeds #{ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES} bytes"
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
entry_count.positive?
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def write_catalog_blob_atomic!(full_path, bytes)
|
|
243
|
+
FileUtils.mkdir_p(File.dirname(full_path))
|
|
244
|
+
partial = "#{full_path}.partial"
|
|
245
|
+
File.binwrite(partial, bytes)
|
|
246
|
+
File.rename(partial, full_path)
|
|
247
|
+
rescue
|
|
248
|
+
FileUtils.rm_f(partial)
|
|
249
|
+
raise
|
|
250
|
+
end
|
|
251
|
+
|
|
108
252
|
def find_catalog_markers(start)
|
|
109
253
|
current = start
|
|
110
254
|
8.times do
|
|
@@ -113,9 +257,9 @@ module Pray
|
|
|
113
257
|
if File.file?(revision_path) && File.file?(git_dir_path)
|
|
114
258
|
revision = File.read(revision_path).strip
|
|
115
259
|
git_dir = File.read(git_dir_path).strip
|
|
116
|
-
return
|
|
260
|
+
return unless acceptable_revision?(revision) && acceptable_shared_bare?(git_dir)
|
|
117
261
|
|
|
118
|
-
return {git_dir: git_dir, revision: revision,
|
|
262
|
+
return {git_dir: git_dir, revision: revision, catalog_root: current}
|
|
119
263
|
end
|
|
120
264
|
parent = File.dirname(current)
|
|
121
265
|
return if parent == current
|
|
@@ -124,5 +268,27 @@ module Pray
|
|
|
124
268
|
end
|
|
125
269
|
nil
|
|
126
270
|
end
|
|
271
|
+
|
|
272
|
+
def acceptable_revision?(revision)
|
|
273
|
+
return false if revision.empty? || revision.start_with?("-")
|
|
274
|
+
return false unless [40, 64].include?(revision.length)
|
|
275
|
+
|
|
276
|
+
revision.match?(/\A[0-9a-fA-F]+\z/)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def acceptable_shared_bare?(git_dir)
|
|
280
|
+
return false unless File.file?(File.join(git_dir, "HEAD"))
|
|
281
|
+
return false unless File.directory?(File.join(git_dir, "objects"))
|
|
282
|
+
|
|
283
|
+
root = GitStore.global_cache_root
|
|
284
|
+
return false unless root
|
|
285
|
+
|
|
286
|
+
expected = File.join(root, "git")
|
|
287
|
+
PathSafety.path_under_root?(expected, git_dir)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def offline_git_env(offline)
|
|
291
|
+
offline ? {"GIT_NO_LAZY_FETCH" => "1"} : {}
|
|
292
|
+
end
|
|
127
293
|
end
|
|
128
294
|
end
|
data/lib/pray/git_store.rb
CHANGED
|
@@ -8,27 +8,28 @@ require_relative "git_clone"
|
|
|
8
8
|
|
|
9
9
|
module Pray
|
|
10
10
|
module GitStore
|
|
11
|
+
READY_MARKER = ".pray-ready"
|
|
12
|
+
|
|
11
13
|
module_function
|
|
12
14
|
|
|
13
15
|
def ensure_global_git_db(clone_url, pinned_revision:, refresh:, offline:, working_directory:)
|
|
14
16
|
db = global_git_cache_directory(clone_url)
|
|
15
17
|
raise Error.resolution("git object cache is not configured") unless db
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
with_git_cache_lock(db) do
|
|
20
|
+
unless global_git_cache_ready?(db)
|
|
21
|
+
raise Error.resolution(offline_git_source_uncached(clone_url)) if offline
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
ensure_revision_in_db(db, clone_url, pinned_revision, !offline)
|
|
28
|
-
return [db, pinned_revision]
|
|
29
|
-
end
|
|
23
|
+
install_bare_clone(working_directory, clone_url, db)
|
|
24
|
+
end
|
|
25
|
+
fetch_origin_tip(db, clone_url) if refresh && !offline
|
|
26
|
+
if pinned_revision
|
|
27
|
+
ensure_revision_in_db(db, clone_url, pinned_revision, !offline)
|
|
28
|
+
return [db, pinned_revision]
|
|
29
|
+
end
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
[db, git_head_revision(db)]
|
|
32
|
+
end
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
def global_cache_root
|
|
@@ -45,13 +46,98 @@ module Pray
|
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
def global_git_cache_ready?(global_cache)
|
|
48
|
-
|
|
49
|
+
return false unless File.file?(File.join(global_cache, "HEAD"))
|
|
50
|
+
return false unless File.directory?(File.join(global_cache, "objects"))
|
|
51
|
+
return true if File.file?(File.join(global_cache, READY_MARKER))
|
|
52
|
+
|
|
53
|
+
begin
|
|
54
|
+
git_head_revision(global_cache)
|
|
55
|
+
true
|
|
56
|
+
rescue Error
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def mark_git_cache_ready(global_cache)
|
|
62
|
+
File.write(File.join(global_cache, READY_MARKER), "1")
|
|
49
63
|
end
|
|
50
64
|
|
|
51
65
|
def offline_git_source_uncached(clone_url)
|
|
52
66
|
"git source #{clone_url} is not cached locally and offline mode is enabled"
|
|
53
67
|
end
|
|
54
68
|
|
|
69
|
+
def install_bare_clone(working_directory, clone_url, db)
|
|
70
|
+
FileUtils.mkdir_p(File.dirname(db))
|
|
71
|
+
staging = "#{db}.partial"
|
|
72
|
+
FileUtils.rm_rf(staging) if File.exist?(staging)
|
|
73
|
+
GitClone.clone_bare_git_db(working_directory, clone_url, staging, quiet: false)
|
|
74
|
+
ensure_git_remote_origin(staging, clone_url)
|
|
75
|
+
git_head_revision(staging)
|
|
76
|
+
mark_git_cache_ready(staging)
|
|
77
|
+
if File.exist?(db)
|
|
78
|
+
if global_git_cache_ready?(db)
|
|
79
|
+
FileUtils.rm_rf(staging)
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
FileUtils.rm_rf(db)
|
|
83
|
+
end
|
|
84
|
+
FileUtils.mv(staging, db)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
LOCK_ATTEMPTS = 2_400
|
|
88
|
+
LOCK_RETRY_SECONDS = 0.025
|
|
89
|
+
STALE_LOCK_SECONDS = 600
|
|
90
|
+
|
|
91
|
+
def with_git_cache_lock(db)
|
|
92
|
+
lock_directory = "#{db}.lock.d"
|
|
93
|
+
FileUtils.mkdir_p(File.dirname(lock_directory))
|
|
94
|
+
acquired = false
|
|
95
|
+
LOCK_ATTEMPTS.times do
|
|
96
|
+
Dir.mkdir(lock_directory)
|
|
97
|
+
write_lock_owner!(lock_directory)
|
|
98
|
+
acquired = true
|
|
99
|
+
break
|
|
100
|
+
rescue Errno::EEXIST
|
|
101
|
+
clear_stale_lock!(lock_directory)
|
|
102
|
+
sleep LOCK_RETRY_SECONDS
|
|
103
|
+
end
|
|
104
|
+
unless acquired
|
|
105
|
+
raise Error.resolution(
|
|
106
|
+
"timed out waiting for git cache lock #{lock_directory}. " \
|
|
107
|
+
"If no other pray process is using that cache, remove the lock directory and retry."
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
begin
|
|
112
|
+
yield
|
|
113
|
+
ensure
|
|
114
|
+
FileUtils.rm_rf(lock_directory)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def write_lock_owner!(lock_directory)
|
|
119
|
+
File.write(File.join(lock_directory, "owner"), "#{Process.pid}\n#{Time.now.to_i}\n")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def clear_stale_lock!(lock_directory)
|
|
123
|
+
age = lock_age_seconds(lock_directory)
|
|
124
|
+
return if age < STALE_LOCK_SECONDS
|
|
125
|
+
|
|
126
|
+
FileUtils.rm_rf(lock_directory)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def lock_age_seconds(lock_directory)
|
|
130
|
+
owner = File.join(lock_directory, "owner")
|
|
131
|
+
if File.file?(owner)
|
|
132
|
+
stamp = File.read(owner).lines[1].to_s.strip.to_i
|
|
133
|
+
return Time.now.to_i - stamp if stamp.positive?
|
|
134
|
+
end
|
|
135
|
+
Time.now.to_i - File.mtime(lock_directory).to_i
|
|
136
|
+
rescue Errno::ENOENT
|
|
137
|
+
0
|
|
138
|
+
end
|
|
139
|
+
private_class_method :write_lock_owner!, :clear_stale_lock!, :lock_age_seconds
|
|
140
|
+
|
|
55
141
|
def ensure_revision_in_db(db, clone_url, revision, allow_fetch)
|
|
56
142
|
return if GitRun.try_run_git(db, "cat-file", "-e", revision)
|
|
57
143
|
unless allow_fetch
|
|
@@ -110,6 +196,7 @@ module Pray
|
|
|
110
196
|
GitRun.run_git(db, "fetch", "--depth", "1", "origin")
|
|
111
197
|
end
|
|
112
198
|
GitRun.run_git(db, "update-ref", "HEAD", "FETCH_HEAD")
|
|
199
|
+
mark_git_cache_ready(db)
|
|
113
200
|
end
|
|
114
201
|
|
|
115
202
|
def fetch_unshallow(db)
|
data/lib/pray/package_spec.rb
CHANGED
|
@@ -55,6 +55,7 @@ module Pray
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def tree_hash_for_root(root)
|
|
58
|
+
validate_relative_paths!
|
|
58
59
|
file_bytes = {}
|
|
59
60
|
files.each do |file|
|
|
60
61
|
path = File.join(root, file)
|
|
@@ -196,7 +197,11 @@ module Pray
|
|
|
196
197
|
expect_start!
|
|
197
198
|
spec = PackageSpec.new
|
|
198
199
|
while (statement = next_statement)
|
|
199
|
-
|
|
200
|
+
if statement == "end"
|
|
201
|
+
package = spec.canonicalized
|
|
202
|
+
package.validate_relative_paths!
|
|
203
|
+
return package
|
|
204
|
+
end
|
|
200
205
|
|
|
201
206
|
apply_statement(spec, statement)
|
|
202
207
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "path_safety"
|
|
4
|
+
|
|
5
|
+
module Pray
|
|
6
|
+
class PackageSpec
|
|
7
|
+
def validate_relative_paths!
|
|
8
|
+
files.each { |file| PathSafety.validate_archive_member_path!(file) }
|
|
9
|
+
exports.each_value do |export|
|
|
10
|
+
PathSafety.validate_archive_member_path!(export.path)
|
|
11
|
+
PathSafety.validate_archive_member_path!(export.default_path) if export.default_path
|
|
12
|
+
end
|
|
13
|
+
skills.each_value { |skill| PathSafety.validate_archive_member_path!(skill.path) }
|
|
14
|
+
templates.each_value { |template| PathSafety.validate_archive_member_path!(template.path) }
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/pray/publish.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "time"
|
|
6
6
|
require "tmpdir"
|
|
7
|
+
require_relative "terminal"
|
|
7
8
|
|
|
8
9
|
module Pray
|
|
9
10
|
RegistryIndex = Struct.new(:spec, :packages) do
|
|
@@ -15,11 +16,14 @@ module Pray
|
|
|
15
16
|
module Publish
|
|
16
17
|
module_function
|
|
17
18
|
|
|
18
|
-
def publish_to_root(project, root, signer: "local", signer_fingerprint: nil)
|
|
19
|
+
def publish_to_root(project, root, signer: "local", signer_fingerprint: nil, dest_name: nil, target: nil)
|
|
19
20
|
root = File.expand_path(root)
|
|
21
|
+
dest_name ||= root
|
|
22
|
+
target ||= root
|
|
20
23
|
index = load_registry_index(root)
|
|
21
24
|
distribution = Distribution.read_settings(root)
|
|
22
25
|
package_names = index.packages.to_set
|
|
26
|
+
stats = {published: 0, unchanged: 0}
|
|
23
27
|
|
|
24
28
|
project.packages.each do |package|
|
|
25
29
|
package.spec.require_release_version!
|
|
@@ -31,6 +35,8 @@ module Pray
|
|
|
31
35
|
if stored_artifact && stored_publish_matches?(stored_artifact, package, existing)
|
|
32
36
|
package_names << package.declaration.name
|
|
33
37
|
write_registry_package_metadata(metadata_path, metadata)
|
|
38
|
+
report_unchanged(package, dest_name, target)
|
|
39
|
+
stats[:unchanged] += 1
|
|
34
40
|
next
|
|
35
41
|
end
|
|
36
42
|
|
|
@@ -56,14 +62,20 @@ module Pray
|
|
|
56
62
|
metadata.versions << version_entry
|
|
57
63
|
write_registry_package_metadata(metadata_path, metadata)
|
|
58
64
|
package_names << package.declaration.name
|
|
65
|
+
report_published(package, dest_name, target, artifact_path)
|
|
66
|
+
stats[:published] += 1
|
|
59
67
|
end
|
|
60
68
|
|
|
61
69
|
index.packages = package_names.sort
|
|
62
70
|
write_registry_index(root, index)
|
|
71
|
+
stats
|
|
63
72
|
end
|
|
64
73
|
|
|
65
|
-
def publish_to_server(project, server_url, signer: "local", signer_fingerprint: nil)
|
|
74
|
+
def publish_to_server(project, server_url, signer: "local", signer_fingerprint: nil, dest_name: nil, target: nil)
|
|
75
|
+
dest_name ||= server_url
|
|
76
|
+
target ||= server_url
|
|
66
77
|
distribution = Distribution.fetch_settings(server_url)
|
|
78
|
+
stats = {published: 0, unchanged: 0}
|
|
67
79
|
project.packages.each do |package|
|
|
68
80
|
archive_bytes = Archive.build_package_archive_bytes(package)
|
|
69
81
|
artifact_path = registry_artifact_path(package.declaration.name, package.spec.version)
|
|
@@ -99,9 +111,38 @@ module Pray
|
|
|
99
111
|
"application/json",
|
|
100
112
|
JSON.pretty_generate(metadata_to_hash(metadata))
|
|
101
113
|
)
|
|
114
|
+
report_published(package, dest_name, target, artifact_path)
|
|
115
|
+
stats[:published] += 1
|
|
102
116
|
end
|
|
117
|
+
stats
|
|
103
118
|
end
|
|
104
119
|
|
|
120
|
+
def report_published(package, dest_name, target, artifact_path)
|
|
121
|
+
Terminal.println_stdout(
|
|
122
|
+
"published #{package.declaration.name} #{package.spec.version} -> #{redact_publish_target(dest_name)} (#{redact_publish_target(target)})"
|
|
123
|
+
)
|
|
124
|
+
Terminal.println_verbose(" artifact #{artifact_path}")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def report_unchanged(package, dest_name, target)
|
|
128
|
+
Terminal.println_stdout(
|
|
129
|
+
"unchanged #{package.declaration.name} #{package.spec.version} -> #{redact_publish_target(dest_name)} (#{redact_publish_target(target)})"
|
|
130
|
+
)
|
|
131
|
+
Terminal.println_verbose(" skip reason: stored publish matches")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def redact_publish_target(target)
|
|
135
|
+
text = target.to_s
|
|
136
|
+
return text unless text.include?("://")
|
|
137
|
+
|
|
138
|
+
scheme, remainder = text.split("://", 2)
|
|
139
|
+
return text unless remainder&.include?("@")
|
|
140
|
+
|
|
141
|
+
_userinfo, host_and_path = remainder.split("@", 2)
|
|
142
|
+
"#{scheme}://***@#{host_and_path}"
|
|
143
|
+
end
|
|
144
|
+
private_class_method :report_published, :report_unchanged
|
|
145
|
+
|
|
105
146
|
def published_registry_package_version(package, signer, signer_fingerprint, archive_bytes, artifact_path)
|
|
106
147
|
RegistryPackageVersion.new(
|
|
107
148
|
version: package.spec.version,
|
|
@@ -114,7 +155,8 @@ module Pray
|
|
|
114
155
|
signer: signer,
|
|
115
156
|
signer_fingerprint: signer_fingerprint,
|
|
116
157
|
published_at: Time.now.to_i,
|
|
117
|
-
signature: Registry.registry_artifact_signature(archive_bytes, package.tree_hash, signer)
|
|
158
|
+
signature: Registry.registry_artifact_signature(archive_bytes, package.tree_hash, signer),
|
|
159
|
+
derived_metadata: DerivedMetadata.from_package_root(package.root, package.spec)
|
|
118
160
|
)
|
|
119
161
|
end
|
|
120
162
|
|
|
@@ -163,6 +205,7 @@ module Pray
|
|
|
163
205
|
|
|
164
206
|
version_entry.yanked = existing.yanked
|
|
165
207
|
version_entry.published_at = existing.published_at if stored_artifact
|
|
208
|
+
version_entry.derived_metadata ||= existing.derived_metadata
|
|
166
209
|
end
|
|
167
210
|
|
|
168
211
|
def load_registry_index(root)
|
|
@@ -181,7 +224,7 @@ module Pray
|
|
|
181
224
|
|
|
182
225
|
def load_registry_package_metadata(path, package_name)
|
|
183
226
|
if File.exist?(path)
|
|
184
|
-
data = JSON.parse(File.read(path))
|
|
227
|
+
data = JSON.parse(File.read(path, encoding: "UTF-8"))
|
|
185
228
|
RegistryPackageMetadata.new(
|
|
186
229
|
name: data["name"],
|
|
187
230
|
versions: Array(data["versions"]).map { |entry| Registry.version_from_hash(entry) }
|
|
@@ -193,7 +236,7 @@ module Pray
|
|
|
193
236
|
|
|
194
237
|
def write_registry_package_metadata(path, metadata)
|
|
195
238
|
FileUtils.mkdir_p(File.dirname(path))
|
|
196
|
-
File.write(path, JSON.pretty_generate(metadata_to_hash(metadata)))
|
|
239
|
+
File.write(path, JSON.pretty_generate(metadata_to_hash(metadata)), encoding: "UTF-8")
|
|
197
240
|
end
|
|
198
241
|
|
|
199
242
|
def metadata_to_hash(metadata)
|
|
@@ -217,6 +260,7 @@ module Pray
|
|
|
217
260
|
hash["signer_fingerprint"] = entry.signer_fingerprint if entry.signer_fingerprint
|
|
218
261
|
hash["published_at"] = entry.published_at if entry.published_at
|
|
219
262
|
hash["signature"] = entry.signature if entry.signature
|
|
263
|
+
hash["derived_metadata"] = entry.derived_metadata if entry.derived_metadata
|
|
220
264
|
hash
|
|
221
265
|
end
|
|
222
266
|
|