pray-cli 1.12.0 → 1.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68bb4b372a260d62b3cfce3bdc74811b85c2cff0446adf0bd551065b0214fbb1
4
- data.tar.gz: 3bfc7db4cd0b6227a554199f6c8cbf8fd2e4c1e2715be48f7d102903e7a26bb2
3
+ metadata.gz: 55a5d9fd2741f9ecd2d6d41c18acf6c7358b8bedb15e874d3686ce4ac88c7898
4
+ data.tar.gz: 489b7b073d8194c8f90fbbbab0b757804cc05a9ee309d7e596ce8a894ead1666
5
5
  SHA512:
6
- metadata.gz: '095670c5e563280d0610d1eefa2ef2a825e2676aaff537c54eda62ad97ee11a631dc5328648614bfdd69b6dca744c7377fea9fef3b5cc0c4d3341ad511adb4be'
7
- data.tar.gz: a820d50b02742e1b8a7c79fe9075f48e6ae7c90f3551dba8e8fbb37c7ed28d2e7089ac9fe81b82c2db7e8bf7d20f926840084d841e30bb3d2a733efa68e2892f
6
+ metadata.gz: 005a627e411946e40ae7b782feb62737b8d49a3eef411fda42ecd90c705c252414e504f231f94f24f5e687b035562233ba68f424abe49d2d541700b281493406
7
+ data.tar.gz: 80f998bf51a0fa32bf40d36b38f9a1101affdd988a0cb938e34b637a8225d3903d5c98bb217a88dab2ceb99849cd11957748d22d612e4f593f082f0d40122132
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.13.0 (2026-09-08)
6
+
7
+ - Record and verify package upstream pins in `.prayspec` and `Prayfile.lock`. Refuse path-fork updates until this CLI can refresh the path tree safely (RFC 0114).
8
+
9
+ ## 1.12.1 (2026-09-08)
10
+
11
+ - Refresh a locked git catalog during `pray install` when a newly declared package is missing from the pinned revision, and name that revision with `pray update` if it is still missing.
12
+
5
13
  ## 1.12.0 (2026-09-07)
6
14
 
7
15
  - Speed up planning for large package trees and reuse the resolved lock during installation.
@@ -100,6 +100,8 @@ module Pray
100
100
  raise Error.manifest("package #{package} not found")
101
101
  end
102
102
  end
103
+ current = resolve_current_project(ResolveOptions.new(offline: offline))
104
+ Upstream.ensure_update_supported!(current.packages, package)
103
105
  install_command({locked: false, frozen: false, offline: offline, refresh: true})
104
106
  end
105
107
 
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module GitRefresh
5
+ module_function
6
+
7
+ def resolution_may_benefit_from_git_source_refresh?(error)
8
+ error.category == :resolution && catalog_miss_message?(error.message)
9
+ end
10
+
11
+ def catalog_miss_message?(message)
12
+ message.include?("no registry version") ||
13
+ message.include?("not found in distribution") ||
14
+ message.include?("not found in git source") ||
15
+ message.include?("v1/packages/")
16
+ end
17
+
18
+ def annotate_missing_git_catalog(error, package_name:, source_name:, revision:)
19
+ return error unless error.category == :resolution
20
+ return error if revision.to_s.empty?
21
+ return error unless catalog_miss_message?(error.message)
22
+ return error if error.message.include?("not found in git source")
23
+
24
+ Error.resolution(
25
+ "package #{package_name} was not found in git source #{source_name} at revision #{revision}. " \
26
+ "Run `pray update` to advance the source pin."
27
+ )
28
+ end
29
+
30
+ def annotate_failed_refresh(lockfile_path, error)
31
+ return error unless error.category == :resolution
32
+ return error unless catalog_miss_message?(error.message)
33
+
34
+ pins = locked_git_revision_guidance(lockfile_path)
35
+ return error if pins.nil?
36
+
37
+ package = package_name_from_catalog_miss(error.message) || "the declared package"
38
+ Error.resolution(
39
+ "package #{package} was not found in locked git source (#{pins}). " \
40
+ "Run `pray update` to advance the source pin."
41
+ )
42
+ end
43
+
44
+ def locked_git_revision_guidance(lockfile_path)
45
+ return unless File.exist?(lockfile_path)
46
+
47
+ lockfile = LockfileIO.read_lockfile(lockfile_path)
48
+ pins = lockfile.source.filter_map do |source|
49
+ next unless source.kind == "git" && source.revision
50
+
51
+ "#{source.name} at #{source.revision}"
52
+ end
53
+ pins.empty? ? nil : pins.join(", ")
54
+ rescue Error
55
+ nil
56
+ end
57
+
58
+ def package_name_from_catalog_miss(message)
59
+ rest = message.delete_prefix("package ")
60
+ return if rest == message
61
+
62
+ ending = rest.index(" was not found") || rest.index(" not found")
63
+ return unless ending
64
+
65
+ name = rest[0, ending].strip
66
+ name.empty? ? nil : name
67
+ end
68
+ end
69
+ end
@@ -49,10 +49,14 @@ module Pray
49
49
  if allow_git_refresh_fallback &&
50
50
  !options.offline &&
51
51
  !options.refresh &&
52
- Resolve.resolution_may_benefit_from_git_source_refresh?(error)
52
+ GitRefresh.resolution_may_benefit_from_git_source_refresh?(error)
53
53
  refreshed = options.dup
54
54
  refreshed.refresh = true
55
- resolve_current_project(refreshed)
55
+ begin
56
+ resolve_current_project(refreshed)
57
+ rescue Error => retry_error
58
+ raise GitRefresh.annotate_failed_refresh(lockfile_path, retry_error)
59
+ end
56
60
  else
57
61
  raise
58
62
  end
data/lib/pray/lockfile.rb CHANGED
@@ -8,14 +8,15 @@ module Pray
8
8
  LockSource = Struct.new(:name, :kind, :url, :revision, :host_key_fingerprint)
9
9
  LockedPackage = Struct.new(
10
10
  :name, :version, :source, :path, :tree_hash, :artifact_hash, :artifact,
11
- :exports, :dependencies, :signer_fingerprint
11
+ :exports, :dependencies, :signer_fingerprint, :upstream
12
12
  ) do
13
- def initialize(exports: [], dependencies: [], signer_fingerprint: nil, source: nil, **kwargs)
14
- super(**kwargs, exports: exports, dependencies: dependencies, signer_fingerprint: signer_fingerprint, source: source)
13
+ def initialize(exports: [], dependencies: [], signer_fingerprint: nil, source: nil, upstream: nil, **kwargs)
14
+ super(**kwargs, exports: exports, dependencies: dependencies, signer_fingerprint: signer_fingerprint, source: source, upstream: upstream)
15
15
  end
16
16
  end
17
17
 
18
18
  LockedTarget = Struct.new(:name, :outputs)
19
+ LockedUpstream = Struct.new(:name, :version, :source, :tree_hash, :artifact_hash, keyword_init: true)
19
20
  ManagedSpanRecord = Struct.new(
20
21
  :id, :target, :open_line, :close_line, :ideal_checksum, :package, :export,
21
22
  :source_checksum, :silenced
@@ -98,6 +99,7 @@ module Pray
98
99
  }
99
100
  hash["source"] = entry.source unless entry.source.nil?
100
101
  hash["signer_fingerprint"] = entry.signer_fingerprint if entry.signer_fingerprint
102
+ hash["upstream"] = Upstream.to_lock_hash(entry.upstream) if entry.upstream
101
103
  hash
102
104
  end
103
105
 
@@ -185,7 +187,8 @@ module Pray
185
187
  artifact: normalize_lockfile_artifact(project_root, package.artifact, package.root),
186
188
  exports: package.selected_exports,
187
189
  dependencies: package.spec.dependencies.map(&:name),
188
- signer_fingerprint: package.signer_fingerprint
190
+ signer_fingerprint: package.signer_fingerprint,
191
+ upstream: package.upstream
189
192
  )
190
193
  end,
191
194
  target: manifest_targets.map { |target| LockedTarget.new(name: target.name, outputs: target.outputs) },
@@ -224,7 +227,8 @@ module Pray
224
227
  artifact: entry["artifact"],
225
228
  exports: entry["exports"] || [],
226
229
  dependencies: entry["dependencies"] || [],
227
- signer_fingerprint: entry["signer_fingerprint"]
230
+ signer_fingerprint: entry["signer_fingerprint"],
231
+ upstream: Upstream.from_lock_hash(entry["upstream"])
228
232
  )
229
233
  end,
230
234
  target: Array(data["target"]).map { |entry| LockedTarget.new(name: entry["name"], outputs: entry["outputs"] || []) },
@@ -66,6 +66,25 @@ module Pray
66
66
  lines << "exports = #{format_string_array(entry.exports)}"
67
67
  lines << "dependencies = #{format_string_array(entry.dependencies)}"
68
68
  lines << "signer_fingerprint = #{format_string(entry.signer_fingerprint)}" if entry.signer_fingerprint
69
+ if entry.upstream
70
+ lines << ""
71
+ lines << "[package.upstream]"
72
+ append_scalars(
73
+ lines,
74
+ [
75
+ ["name", entry.upstream.name],
76
+ ["version", entry.upstream.version]
77
+ ].tap do |scalars|
78
+ scalars << ["source", entry.upstream.source] unless entry.upstream.source.nil?
79
+ scalars.concat(
80
+ [
81
+ ["tree_hash", entry.upstream.tree_hash],
82
+ ["artifact_hash", entry.upstream.artifact_hash]
83
+ ]
84
+ )
85
+ end
86
+ )
87
+ end
69
88
  lines
70
89
  end
71
90
 
@@ -34,10 +34,17 @@ module Pray
34
34
  if allow_git_refresh_fallback &&
35
35
  !offline &&
36
36
  !refresh &&
37
- Resolve.resolution_may_benefit_from_git_source_refresh?(error)
37
+ GitRefresh.resolution_may_benefit_from_git_source_refresh?(error)
38
38
  refreshed = options.dup
39
39
  refreshed.refresh = true
40
- Resolve.resolve_project_in_context(manifest_path, project_root, refreshed)
40
+ begin
41
+ Resolve.resolve_project_in_context(manifest_path, project_root, refreshed)
42
+ rescue Error => retry_error
43
+ raise GitRefresh.annotate_failed_refresh(
44
+ default_lockfile_path(project_root),
45
+ retry_error
46
+ )
47
+ end
41
48
  else
42
49
  raise
43
50
  end
@@ -9,17 +9,18 @@ module Pray
9
9
  PackageSkill = Struct.new(:path, :summary)
10
10
  PackageTemplate = Struct.new(:path, :summary)
11
11
  PackageDependency = Struct.new(:name, :constraint, :optional)
12
+ PackageUpstream = Struct.new(:name, :constraint, keyword_init: true)
12
13
 
13
14
  PackageSpec = Struct.new(
14
15
  :name, :version, :summary, :description, :authors, :license, :homepage,
15
16
  :source_code_uri, :changelog_uri, :prayfile_version, :files, :exports,
16
- :skills, :templates, :adapters, :targets, :dependencies, :metadata
17
+ :skills, :templates, :adapters, :targets, :dependencies, :metadata, :upstream
17
18
  ) do
18
19
  def initialize(
19
20
  name: "", version: "", summary: nil, description: nil, authors: [], license: nil,
20
21
  homepage: nil, source_code_uri: nil, changelog_uri: nil, prayfile_version: nil,
21
22
  files: [], exports: {}, skills: {}, templates: {}, adapters: {}, targets: [],
22
- dependencies: [], metadata: {}
23
+ dependencies: [], metadata: {}, upstream: nil
23
24
  )
24
25
  super
25
26
  end
@@ -90,6 +91,13 @@ module Pray
90
91
  )
91
92
  end
92
93
 
94
+ def parse_upstream(rest)
95
+ values, _keywords = parse_call(rest)
96
+ name = string_from_value(values.first)
97
+ constraint = values[1] ? string_from_value(values[1]) : "*"
98
+ PackageUpstream.new(name: name, constraint: constraint)
99
+ end
100
+
93
101
  def parse_exports(value)
94
102
  map = Literal.parse_literal_map(value)
95
103
  exports = {}
@@ -208,6 +216,10 @@ module Pray
208
216
  spec.dependencies << parse_dependency(Regexp.last_match(1), false)
209
217
  when /\Aspec\.add_optional_dependency (.+)\z/
210
218
  spec.dependencies << parse_dependency(Regexp.last_match(1), true)
219
+ when /\Aspec\.upstream (.+)\z/
220
+ raise Error.parse("prayspec", "upstream may only be declared once") if spec.upstream
221
+
222
+ spec.upstream = parse_upstream(Regexp.last_match(1))
211
223
  when /\Aspec\.(.+) = (.+)\z/
212
224
  apply_assignment(spec, Regexp.last_match(1).strip, Regexp.last_match(2).strip)
213
225
  else
data/lib/pray/resolve.rb CHANGED
@@ -13,7 +13,7 @@ module Pray
13
13
  ResolvedPackage = Struct.new(
14
14
  :declaration, :root, :spec, :tree_hash, :artifact_hash, :artifact,
15
15
  :selected_exports, :source_checksum, :export_bodies, :skill_files,
16
- :signer_fingerprint, :registry_latest_version
16
+ :signer_fingerprint, :registry_latest_version, :upstream
17
17
  )
18
18
 
19
19
  ResolvedLocalFile = Struct.new(
@@ -75,7 +75,7 @@ module Pray
75
75
  user_config,
76
76
  declaration,
77
77
  lockfile_hints,
78
- offline: options.offline
78
+ options: options
79
79
  )
80
80
  if seen[package.declaration.name]
81
81
  raise Error.resolution("duplicate package declaration: #{package.declaration.name}")
@@ -127,14 +127,21 @@ module Pray
127
127
  !refresh &&
128
128
  resolution_may_benefit_from_git_source_refresh?(error)
129
129
  refreshed = ResolveOptions.new(offline: offline, refresh: true, environment: environment)
130
- resolve_project_with_options(manifest_path, refreshed)
130
+ begin
131
+ resolve_project_with_options(manifest_path, refreshed)
132
+ rescue Error => retry_error
133
+ raise GitRefresh.annotate_failed_refresh(
134
+ File.join(File.dirname(File.expand_path(manifest_path)), "Prayfile.lock"),
135
+ retry_error
136
+ )
137
+ end
131
138
  else
132
139
  raise
133
140
  end
134
141
  end
135
142
 
136
143
  def resolution_may_benefit_from_git_source_refresh?(error)
137
- error.category == :resolution && error.message.include?("no registry version")
144
+ GitRefresh.resolution_may_benefit_from_git_source_refresh?(error)
138
145
  end
139
146
 
140
147
  def missing_local_embed_guidance(path)
@@ -142,9 +149,10 @@ module Pray
142
149
  "Create the file or remove the entry from Prayfile, then run `pray install`."
143
150
  end
144
151
 
145
- def resolve_package(project_root, sources, git_sources, user_config, declaration, lockfile, offline: false)
152
+ def resolve_package(project_root, sources, git_sources, user_config, declaration, lockfile, offline: false, options: nil)
153
+ options ||= ResolveOptions.new(offline: offline)
146
154
  root, registry_latest_version = resolve_package_root_with_metadata(
147
- project_root, sources, git_sources, user_config, declaration, lockfile, offline: offline
155
+ project_root, sources, git_sources, user_config, declaration, lockfile, offline: options.offline
148
156
  )
149
157
  spec_path = find_prayspec_file(root)
150
158
  spec_text = File.read(spec_path)
@@ -178,7 +186,10 @@ module Pray
178
186
  export_bodies: export_bodies,
179
187
  skill_files: skill_files,
180
188
  signer_fingerprint: nil,
181
- registry_latest_version: registry_latest_version
189
+ registry_latest_version: registry_latest_version,
190
+ upstream: Upstream.lock_path(
191
+ project_root, sources, git_sources, user_config, declaration, spec, lockfile, options
192
+ )
182
193
  )
183
194
  end
184
195
 
@@ -228,14 +239,23 @@ module Pray
228
239
  clone_url = source.url.delete_prefix("git+")
229
240
  distribution_root = GitSources.resolve_distribution_root(checkout.cache_directory, checkout.subdir)
230
241
  source_key = checkout.revision.to_s.empty? ? clone_url : "#{clone_url}@#{checkout.revision}"
231
- resolved = Registry.resolve_local_registry_package_root(
232
- project_root,
233
- source_key,
234
- distribution_root,
235
- declaration,
236
- preferred_version: lockfile_preferred_version(lockfile, declaration.name),
237
- offline: offline
238
- )
242
+ resolved = begin
243
+ Registry.resolve_local_registry_package_root(
244
+ project_root,
245
+ source_key,
246
+ distribution_root,
247
+ declaration,
248
+ preferred_version: lockfile_preferred_version(lockfile, declaration.name),
249
+ offline: offline
250
+ )
251
+ rescue Error => error
252
+ raise GitRefresh.annotate_missing_git_catalog(
253
+ error,
254
+ package_name: declaration.name,
255
+ source_name: source.name,
256
+ revision: checkout.revision
257
+ )
258
+ end
239
259
  return [resolved.root, resolved.registry_latest_version]
240
260
  else
241
261
  raise Error.unsupported("source kind #{source.kind} not implemented yet")
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module Upstream
5
+ module_function
6
+
7
+ def ensure_update_supported!(packages, selected = nil)
8
+ unsupported = packages.find do |package|
9
+ package.declaration.path && package.spec.upstream &&
10
+ (selected.nil? || package.declaration.name == selected)
11
+ end
12
+ return unless unsupported
13
+
14
+ raise Error.unsupported(
15
+ "this installation cannot refresh upstream package #{unsupported.declaration.name}; " \
16
+ "install pray with Cargo and retry"
17
+ )
18
+ end
19
+
20
+ def to_lock_hash(entry)
21
+ hash = {
22
+ "name" => entry.name,
23
+ "version" => entry.version,
24
+ "tree_hash" => entry.tree_hash,
25
+ "artifact_hash" => entry.artifact_hash
26
+ }
27
+ hash["source"] = entry.source unless entry.source.nil?
28
+ hash
29
+ end
30
+
31
+ def from_lock_hash(entry)
32
+ return nil if entry.nil?
33
+
34
+ LockedUpstream.new(
35
+ name: entry["name"],
36
+ version: entry["version"],
37
+ source: entry["source"],
38
+ tree_hash: entry["tree_hash"],
39
+ artifact_hash: entry["artifact_hash"]
40
+ )
41
+ end
42
+
43
+ def lock_path(project_root, sources, git_sources, user_config, declaration, spec, lockfile, options)
44
+ return nil unless spec.upstream
45
+ if spec.name == spec.upstream.name
46
+ raise Error.resolution("package #{spec.name} cannot use itself as upstream")
47
+ end
48
+ return nil unless declaration.path
49
+
50
+ refresh = options.ignore_locked_versions || options.unlocked_packages.include?(declaration.name)
51
+ locked = lockfile&.package&.find { |entry| entry.name == declaration.name }&.upstream
52
+ name, constraint, source = resolution_input(declaration, spec, sources, locked, refresh)
53
+ resolved = Resolve.resolve_package(
54
+ project_root,
55
+ sources,
56
+ git_sources,
57
+ user_config,
58
+ ManifestPackage.new(
59
+ name: name,
60
+ constraint: constraint,
61
+ source: source
62
+ ),
63
+ lockfile,
64
+ options: options
65
+ )
66
+ resolved_upstream = LockedUpstream.new(
67
+ name: name,
68
+ version: resolved.spec.version,
69
+ source: source,
70
+ tree_hash: resolved.tree_hash,
71
+ artifact_hash: resolved.artifact_hash
72
+ )
73
+ ensure_locked_match!(locked, resolved_upstream) if !refresh && locked
74
+ resolved_upstream
75
+ end
76
+
77
+ def resolution_input(declaration, spec, sources, locked, refresh)
78
+ if locked && !refresh
79
+ if locked.name != spec.upstream.name
80
+ raise Error.integrity(
81
+ "locked upstream name mismatch for #{declaration.name}: " \
82
+ "expected #{locked.name}, found #{spec.upstream.name}"
83
+ )
84
+ end
85
+ return [locked.name, "= #{locked.version}", locked.source]
86
+ end
87
+
88
+ name = spec.upstream.name
89
+ source = ResolveSource.implied_source_name(ManifestPackage.new(name: name), sources)
90
+ [name, spec.upstream.constraint, source]
91
+ end
92
+
93
+ def ensure_locked_match!(locked, resolved)
94
+ if locked.name != resolved.name || locked.version != resolved.version
95
+ raise Error.integrity("locked upstream identity mismatch")
96
+ end
97
+ raise Error.integrity("locked upstream source mismatch") if locked.source != resolved.source
98
+ raise Error.integrity("locked upstream tree hash mismatch") if locked.tree_hash != resolved.tree_hash
99
+ if locked.artifact_hash != resolved.artifact_hash
100
+ raise Error.integrity("locked upstream artifact hash mismatch")
101
+ end
102
+ end
103
+ end
104
+ end
data/lib/pray/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pray
4
- VERSION = "1.12.0"
5
- GENERATED_BY = "pray 1.12.0"
4
+ VERSION = "1.13.0"
5
+ GENERATED_BY = "pray 1.13.0"
6
6
  end
data/lib/pray.rb CHANGED
@@ -22,7 +22,9 @@ require_relative "pray/resolve_context"
22
22
  require_relative "pray/resolve_source"
23
23
  require_relative "pray/resolve_exports"
24
24
  require_relative "pray/invocation"
25
+ require_relative "pray/git_refresh"
25
26
  require_relative "pray/resolve"
27
+ require_relative "pray/upstream"
26
28
  require_relative "pray/compose_dest"
27
29
  require_relative "pray/render_patch"
28
30
  require_relative "pray/render"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pray-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.12.0
4
+ version: 1.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -130,6 +130,7 @@ files:
130
130
  - lib/pray/error.rb
131
131
  - lib/pray/format_manifest.rb
132
132
  - lib/pray/format_serialize.rb
133
+ - lib/pray/git_refresh.rb
133
134
  - lib/pray/git_sources.rb
134
135
  - lib/pray/hashing.rb
135
136
  - lib/pray/http_body.rb
@@ -179,6 +180,7 @@ files:
179
180
  - lib/pray/trust.rb
180
181
  - lib/pray/trust_feed.rb
181
182
  - lib/pray/trust_ops.rb
183
+ - lib/pray/upstream.rb
182
184
  - lib/pray/verify.rb
183
185
  - lib/pray/verify_position.rb
184
186
  - lib/pray/verify_provisioned.rb