pray-cli 1.13.0 → 1.14.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: 55a5d9fd2741f9ecd2d6d41c18acf6c7358b8bedb15e874d3686ce4ac88c7898
4
- data.tar.gz: 489b7b073d8194c8f90fbbbab0b757804cc05a9ee309d7e596ce8a894ead1666
3
+ metadata.gz: cc753f65405f7052d882da07b3293b66c69e60cb26b8fe865919000323f2da1a
4
+ data.tar.gz: 91586f374cc5a677b0f5d2b722a4ba5da5912d8bd3664eac0ccd48b864889c65
5
5
  SHA512:
6
- metadata.gz: 005a627e411946e40ae7b782feb62737b8d49a3eef411fda42ecd90c705c252414e504f231f94f24f5e687b035562233ba68f424abe49d2d541700b281493406
7
- data.tar.gz: 80f998bf51a0fa32bf40d36b38f9a1101affdd988a0cb938e34b637a8225d3903d5c98bb217a88dab2ceb99849cd11957748d22d612e4f593f082f0d40122132
6
+ metadata.gz: a84c2945dba3386f31dfdfc08d603ecee88d2be1e9898c20a993cd9dba9149ae4966b9cf9e02ba8c28905b7a1c81e2f5158bdf2eff8b36fe7f470e0bbd82ab6a
7
+ data.tar.gz: 48f107b5721d69c62b83db0f99247f9c4821eea42f85d3459cac09b7441badc3fa092965f877b1d3ae3fb0d38f391e8f65ae76cc21a6fd1595799dd2de0c6324
data/CHANGELOG.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # CHANGELOG
2
2
 
3
- ## Unreleased
3
+ ## 1.14.0 (2026-09-14)
4
+
5
+ - Keep an unchanged package version's artifact, first-publish time, and yank when `pray publish` runs again (RFC 0061).
6
+ - Write `published_at` as UTC Unix seconds in registry and federation JSON.
7
+ - Collapse trailing blank lines when composing so a composed file ends with a single newline, matching the other CLIs.
8
+ - Order `symbols` before `render` in the canonical manifest JSON so `manifest_hash` matches the other CLIs.
4
9
 
5
10
  ## 1.13.0 (2026-09-08)
6
11
 
@@ -14,10 +14,10 @@ module Pray
14
14
  "sources" => manifest.sources.map { |source| source_fields(source) },
15
15
  "targets" => manifest.targets.map { |target| target_fields(target) },
16
16
  "packages" => manifest.packages.map { |package| package_fields(package) },
17
- "local" => manifest.local.map { |entry| local_fields(entry) },
18
- "render" => render_fields(manifest.render)
17
+ "local" => manifest.local.map { |entry| local_fields(entry) }
19
18
  }
20
19
  fields["symbols"] = manifest.symbols.sort.to_h unless manifest.symbols.empty?
20
+ fields["render"] = render_fields(manifest.render)
21
21
  fields
22
22
  end
23
23
 
data/lib/pray/publish.rb CHANGED
@@ -20,13 +20,21 @@ module Pray
20
20
  package_names = index.packages.to_set
21
21
 
22
22
  project.packages.each do |package|
23
- archive_bytes = Archive.build_package_archive_bytes(package)
24
23
  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
24
  metadata_path = registry_metadata_path(root, package.declaration.name)
29
25
  metadata = load_registry_package_metadata(metadata_path, package.declaration.name)
26
+ existing = metadata.versions.find { |entry| entry.version == package.spec.version }
27
+ stored_artifact = stored_package_artifact(root, artifact_path, package, existing)
28
+ if stored_artifact && stored_publish_matches?(
29
+ stored_artifact, package, signer, signer_fingerprint, existing
30
+ )
31
+ package_names << package.declaration.name
32
+ write_registry_package_metadata(metadata_path, metadata)
33
+ next
34
+ end
35
+
36
+ archive_bytes = Archive.build_package_archive_bytes(package)
37
+ write_output_bytes(File.join(root, artifact_path), archive_bytes)
30
38
  version_entry = published_registry_package_version(
31
39
  package,
32
40
  signer,
@@ -34,6 +42,7 @@ module Pray
34
42
  archive_bytes,
35
43
  artifact_path
36
44
  )
45
+ preserve_existing_publish_metadata(metadata, version_entry, stored_artifact)
37
46
  metadata.versions.reject! { |entry| entry.version == version_entry.version }
38
47
  metadata.versions << version_entry
39
48
  write_registry_package_metadata(metadata_path, metadata)
@@ -81,11 +90,50 @@ module Pray
81
90
  exports: package.spec.exports.keys,
82
91
  signer: signer,
83
92
  signer_fingerprint: signer_fingerprint,
84
- published_at: Time.now.utc.iso8601,
93
+ published_at: Time.now.to_i,
85
94
  signature: Registry.registry_artifact_signature(archive_bytes, package.tree_hash, signer)
86
95
  )
87
96
  end
88
97
 
98
+ def stored_package_artifact(root, artifact_path, package, existing)
99
+ return unless existing&.artifact == artifact_path && existing.tree_hash == package.tree_hash
100
+
101
+ stored_path = File.join(root, artifact_path)
102
+ return unless File.file?(stored_path)
103
+
104
+ artifact_bytes = File.binread(stored_path)
105
+ return unless existing.artifact_hash == Hashing.sha256_prefixed(artifact_bytes)
106
+ return unless stored_prayspec_matches?(artifact_bytes, package.root)
107
+
108
+ artifact_bytes
109
+ end
110
+
111
+ def stored_publish_matches?(artifact_bytes, package, signer, signer_fingerprint, existing)
112
+ existing.signer == signer &&
113
+ existing.signer_fingerprint == signer_fingerprint &&
114
+ existing.signature == Registry.registry_artifact_signature(artifact_bytes, package.tree_hash, signer)
115
+ end
116
+
117
+ def stored_prayspec_matches?(artifact_bytes, package_root)
118
+ Dir.mktmpdir("pray-publish-check-") do |directory|
119
+ Archive.unpack_praypkg(artifact_bytes, directory)
120
+ stored_path = Resolve.find_prayspec_file(directory)
121
+ current_path = Resolve.find_prayspec_file(package_root)
122
+ File.basename(stored_path) == File.basename(current_path) &&
123
+ File.binread(stored_path) == File.binread(current_path)
124
+ end
125
+ rescue Error, SystemCallError
126
+ false
127
+ end
128
+
129
+ def preserve_existing_publish_metadata(metadata, version_entry, stored_artifact)
130
+ existing = metadata.versions.find { |entry| entry.version == version_entry.version }
131
+ return unless existing
132
+
133
+ version_entry.yanked = existing.yanked
134
+ version_entry.published_at = existing.published_at if stored_artifact
135
+ end
136
+
89
137
  def load_registry_index(root)
90
138
  path = File.join(root, "v1", "index.json")
91
139
  return RegistryIndex.new unless File.exist?(path)
data/lib/pray/registry.rb CHANGED
@@ -5,6 +5,7 @@ require "net/http"
5
5
  require "uri"
6
6
  require "fileutils"
7
7
  require "pathname"
8
+ require "time"
8
9
  require_relative "path_safety"
9
10
  require_relative "http_body"
10
11
 
@@ -145,11 +146,26 @@ module Pray
145
146
  exports: entry["exports"] || [],
146
147
  signer: entry["signer"],
147
148
  signer_fingerprint: entry["signer_fingerprint"],
148
- published_at: entry["published_at"],
149
+ published_at: entry.key?("published_at") ? publish_timestamp_from_value(entry["published_at"]) : nil,
149
150
  signature: entry["signature"]
150
151
  )
151
152
  end
152
153
 
154
+ def publish_timestamp_from_value(value)
155
+ timestamp = case value
156
+ when Integer then value
157
+ when String
158
+ value.match?(/\A\d+\z/) ? value.to_i : Time.iso8601(value).to_i
159
+ end
160
+ unless timestamp&.between?(0, 253_402_300_799)
161
+ raise Error.integrity("registry published_at must be whole UTC Unix seconds")
162
+ end
163
+
164
+ timestamp
165
+ rescue ArgumentError
166
+ raise Error.integrity("registry published_at must be whole UTC Unix seconds")
167
+ end
168
+
153
169
  def select_package_version(metadata, constraint, preferred_version)
154
170
  if preferred_version
155
171
  version = metadata.versions.find { |entry| entry.version == preferred_version && !entry.yanked }
@@ -27,7 +27,7 @@ module Pray
27
27
  end
28
28
 
29
29
  def finish
30
- @content.sub(/\n\n+\z/, "\n")
30
+ @content.sub!(/\n\n+\z/, "\n")
31
31
  @content << "\n" unless @content.end_with?("\n")
32
32
  @content
33
33
  end
@@ -34,7 +34,7 @@ module Pray
34
34
  )
35
35
  next if metadata.versions.empty?
36
36
 
37
- updated_at = metadata.versions.map { |version| version.published_at.to_s }.max || "0"
37
+ updated_at = (metadata.versions.filter_map(&:published_at).max || 0).to_s
38
38
  {
39
39
  "name" => name,
40
40
  "updated_at" => updated_at,
@@ -57,7 +57,7 @@ module Pray
57
57
  metadata = Publish.load_registry_package_metadata(metadata_path, name)
58
58
  body = {
59
59
  "name" => metadata.name,
60
- "updated_at" => metadata.versions.map { |version| version.published_at.to_s }.max || "0",
60
+ "updated_at" => (metadata.versions.filter_map(&:published_at).max || 0).to_s,
61
61
  "versions" => metadata.versions.map { |version| transport_version(version) }
62
62
  }
63
63
  Serve.ok_response("application/json", JSON.pretty_generate(body))
@@ -78,9 +78,9 @@ module Pray
78
78
  "tree_hash" => version.tree_hash.to_s,
79
79
  "yanked" => version.yanked,
80
80
  "targets" => version.targets,
81
- "exports" => version.exports,
82
- "published_at" => version.published_at.to_s
81
+ "exports" => version.exports
83
82
  }
83
+ hash["published_at"] = version.published_at if version.published_at
84
84
  if version.signer || version.signer_fingerprint
85
85
  hash["publisher"] = {
86
86
  "id" => version.signer.to_s,
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.13.0"
5
- GENERATED_BY = "pray 1.13.0"
4
+ VERSION = "1.14.0"
5
+ GENERATED_BY = "pray 1.14.0"
6
6
  end
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.13.0
4
+ version: 1.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov