pray-cli 1.9.1 → 1.10.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 +25 -0
- data/bin/pray +0 -1
- data/lib/pray/archive.rb +18 -16
- data/lib/pray/archive_unpack.rb +68 -0
- data/lib/pray/cli/commands/workflow.rb +1 -1
- data/lib/pray/cli/helpers.rb +2 -1
- data/lib/pray/compose_dest.rb +59 -0
- data/lib/pray/format_serialize.rb +6 -1
- data/lib/pray/http_body.rb +34 -0
- data/lib/pray/lockfile.rb +32 -6
- data/lib/pray/lockfile_serialize.rb +15 -0
- data/lib/pray/manifest.rb +26 -9
- data/lib/pray/manifest_json.rb +4 -4
- data/lib/pray/manifest_parser.rb +1 -0
- data/lib/pray/manifest_parser_blocks.rb +5 -2
- data/lib/pray/manifest_parser_helpers.rb +34 -5
- data/lib/pray/materialize.rb +5 -3
- data/lib/pray/package_spec.rb +19 -0
- data/lib/pray/path_safety.rb +43 -0
- data/lib/pray/plan.rb +5 -11
- data/lib/pray/registry.rb +33 -70
- data/lib/pray/registry_install.rb +30 -0
- data/lib/pray/registry_integrity.rb +63 -0
- data/lib/pray/render.rb +43 -65
- data/lib/pray/render_content.rb +36 -0
- data/lib/pray/render_dest.rb +215 -0
- data/lib/pray/render_layout.rb +21 -0
- data/lib/pray/render_patch.rb +107 -0
- data/lib/pray/resolve.rb +11 -54
- data/lib/pray/resolve_exports.rb +79 -0
- data/lib/pray/resolve_source.rb +28 -0
- data/lib/pray/resource_limits.rb +14 -0
- data/lib/pray/serve.rb +46 -4
- data/lib/pray/session.rb +36 -4
- data/lib/pray/tar_validation.rb +157 -0
- data/lib/pray/verify.rb +2 -0
- data/lib/pray/verify_provisioned.rb +55 -0
- data/lib/pray/version.rb +2 -2
- data/lib/pray.rb +12 -0
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d3cea2d193f4cf496417c5098ad4ff57b0a4b55bc7ee26664f72293c2fcbd06
|
|
4
|
+
data.tar.gz: 89d8dc8a3c5c65062531e175587d3f86f27ca9630679dac925593be231062e8c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa2d115c1300f177cd4c9dc8c8e4ab2802baf700be3b7124a644f6889ba4138cafe3a6be16d37005f5c2aba95b803740084139171466ebf7ba126e22f82e7d9c
|
|
7
|
+
data.tar.gz: 1d0c8da737a56bcb21ad6798f6cf59c3fd6ad3384e2d423bb492b37ddf42ccb2c6231b6afebf2b2fc9dc8b7a44e748dd505910a0ceb6835b87006870ae3e68d9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 1.10.0 (2026-09-02)
|
|
4
|
+
|
|
5
|
+
- Refuse to overwrite exclusive `file:` and `tree:` destinations that already exist with other bytes.
|
|
6
|
+
- Fail when a provisioned or compose destination is a symbolic link.
|
|
7
|
+
- Record provisioned leaves in `Prayfile.lock` and delete a dropped leaf only when on-disk bytes still match the locked hash.
|
|
8
|
+
- Keep the previous lock when a provisioned destination write fails, so retry still has its ownership record.
|
|
9
|
+
- Reject destination paths that start with `~`.
|
|
10
|
+
- Print every provisioned path in `pray plan`.
|
|
11
|
+
- BREAKING: Drop unused `render` fields `section_markers` and `line_endings`; reject them at parse.
|
|
12
|
+
- Inline a UTF-8 `file` export into `compose` as a marked span. Exclusive `file:` stays unmarked.
|
|
13
|
+
- Write the Agent context banner on `AGENTS.md` by default. Other compose destinations opt in with `header: true`.
|
|
14
|
+
- Fail compose of JSON, binary, or an unknown file type and name `file:` as the unmarked path.
|
|
15
|
+
- Warn when Prayfile uses deprecated `skills`, export type `skill`, or `spec.skills`; prefer `tree` / `folder`. These forms will be removed in version 2.
|
|
16
|
+
|
|
17
|
+
## 1.9.2 (2026-09-01)
|
|
18
|
+
|
|
19
|
+
- Fix `.praypkg` unpack for git and registry installs when the Ruby process forces UTF-8 internal encoding.
|
|
20
|
+
- Treat empty or corrupt `.pray/cache/registry` directories as not ready so the next install unpacks again.
|
|
21
|
+
- Reject archive members that escape the package root or exceed size limits; unpack through staging into cache.
|
|
22
|
+
- Resolve packages from a matching source namespace or sole source without an explicit `source:`.
|
|
23
|
+
- Move login sessions to the user Pray home with owner-only permissions and migrate legacy repository sessions.
|
|
24
|
+
- Reject manifest paths outside the project; require registry hashes and recheck cached package trees.
|
|
25
|
+
- Bound server requests, headers, connections, and timeouts; expose a readiness endpoint.
|
|
26
|
+
- Cap registry downloads at 64 MiB; unpack `.praypkg` tar members without system tar; reject absolute artifact URLs.
|
|
27
|
+
|
|
3
28
|
## 1.9.1 (2026-08-29)
|
|
4
29
|
|
|
5
30
|
- Rewrite every matching `pray` line when a package constraint is updated, keeping indent and extra keywords.
|
data/bin/pray
CHANGED
data/lib/pray/archive.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "open3"
|
|
5
5
|
require "fileutils"
|
|
6
|
-
|
|
6
|
+
require_relative "archive_unpack"
|
|
7
7
|
|
|
8
8
|
module Pray
|
|
9
9
|
module Archive
|
|
@@ -23,15 +23,20 @@ module Pray
|
|
|
23
23
|
File.binwrite(destination, File.binread(File.join(package.root, file)))
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
with_binary_process_encoding do
|
|
27
|
+
tar_bytes, status = Open3.capture2(
|
|
28
|
+
{"COPYFILE_DISABLE" => "1"},
|
|
29
|
+
"tar", "-cf", "-", "-C", staging, "."
|
|
30
|
+
)
|
|
31
|
+
raise Error.integrity("failed to build package tar archive") unless status.success?
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
zstd_bytes, status = Open3.capture2("zstd", "-q", "-c", stdin_data: tar_bytes)
|
|
34
|
+
unless status.success?
|
|
35
|
+
raise Error.unsupported("zstd is required to build package archives")
|
|
36
|
+
end
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
zstd_bytes
|
|
39
|
+
end
|
|
35
40
|
end
|
|
36
41
|
end
|
|
37
42
|
|
|
@@ -41,14 +46,7 @@ module Pray
|
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
def unpack_praypkg(artifact_bytes, output_directory)
|
|
44
|
-
|
|
45
|
-
tar_bytes, status = Open3.capture2("zstd", "-d", "-q", "-c", stdin_data: artifact_bytes)
|
|
46
|
-
unless status.success?
|
|
47
|
-
raise Error.unsupported("zstd is required to unpack package archives")
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
_stdout, _stderr, status = Open3.capture3("tar", "-xf", "-", "-C", output_directory, stdin_data: tar_bytes)
|
|
51
|
-
raise Error.integrity("failed to unpack package archive") unless status.success?
|
|
49
|
+
ArchiveUnpack.unpack_praypkg(artifact_bytes, output_directory)
|
|
52
50
|
end
|
|
53
51
|
|
|
54
52
|
def package_archive_path(package_name, version)
|
|
@@ -64,5 +62,9 @@ module Pray
|
|
|
64
62
|
"exports" => package.selected_exports
|
|
65
63
|
)
|
|
66
64
|
end
|
|
65
|
+
|
|
66
|
+
def with_binary_process_encoding(&block)
|
|
67
|
+
ArchiveUnpack.with_binary_process_encoding(&block)
|
|
68
|
+
end
|
|
67
69
|
end
|
|
68
70
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "find"
|
|
6
|
+
require_relative "path_safety"
|
|
7
|
+
require_relative "resource_limits"
|
|
8
|
+
require_relative "tar_validation"
|
|
9
|
+
|
|
10
|
+
module Pray
|
|
11
|
+
module ArchiveUnpack
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def unpack_praypkg(artifact_bytes, output_directory)
|
|
15
|
+
if artifact_bytes.bytesize > ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES
|
|
16
|
+
raise Error.integrity(
|
|
17
|
+
"package archive exceeds #{ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES} bytes"
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
tar_bytes = decompress_zstd(artifact_bytes)
|
|
22
|
+
if tar_bytes.bytesize > ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES
|
|
23
|
+
raise Error.integrity(
|
|
24
|
+
"package archive exceeds #{ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES} decompressed bytes"
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
TarValidation.extract!(tar_bytes, output_directory)
|
|
29
|
+
reject_unsafe_extracted_tree!(output_directory)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def with_binary_process_encoding
|
|
33
|
+
previous = Encoding.default_internal
|
|
34
|
+
verbose = $VERBOSE
|
|
35
|
+
$VERBOSE = nil
|
|
36
|
+
Encoding.default_internal = nil
|
|
37
|
+
yield
|
|
38
|
+
ensure
|
|
39
|
+
Encoding.default_internal = previous
|
|
40
|
+
$VERBOSE = verbose
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def decompress_zstd(artifact_bytes)
|
|
44
|
+
with_binary_process_encoding do
|
|
45
|
+
tar_bytes, status = Open3.capture2("zstd", "-d", "-q", "-c", stdin_data: artifact_bytes)
|
|
46
|
+
unless status.success?
|
|
47
|
+
raise Error.unsupported("zstd is required to unpack package archives")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
tar_bytes
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def reject_unsafe_extracted_tree!(output_directory)
|
|
55
|
+
Find.find(output_directory) do |path|
|
|
56
|
+
next if path == output_directory
|
|
57
|
+
next if File.directory?(path)
|
|
58
|
+
|
|
59
|
+
if File.symlink?(path)
|
|
60
|
+
raise Error.integrity("unsupported package archive entry type")
|
|
61
|
+
end
|
|
62
|
+
unless PathSafety.path_under_root?(output_directory, path)
|
|
63
|
+
raise Error.integrity("package path escapes package root: #{path}")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -107,8 +107,8 @@ module Pray
|
|
|
107
107
|
rendered = Render.render_project(project)
|
|
108
108
|
updated_lockfile = build_lockfile(project, rendered)
|
|
109
109
|
merged_lockfile = merge_selected_package_update(previous_lockfile, updated_lockfile, name)
|
|
110
|
+
Render.write_rendered_targets(project, rendered, previous_lockfile)
|
|
110
111
|
Pray.write_lockfile(lockfile_path, merged_lockfile)
|
|
111
|
-
Render.write_rendered_targets(project, rendered)
|
|
112
112
|
puts "Unlocked #{name}"
|
|
113
113
|
end
|
|
114
114
|
|
data/lib/pray/cli/helpers.rb
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
module ComposeDest
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
HTML_COMMENT_EXTENSIONS = %w[.md .markdown .html .htm].freeze
|
|
8
|
+
BINARY_EXTENSIONS = %w[
|
|
9
|
+
.png .jpg .jpeg .gif .webp .ico .pdf .zip .gz .tgz .tar .wasm .bin
|
|
10
|
+
.woff .woff2 .exe .dylib .so
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
def compose_writes_header?(target, output, project_header)
|
|
14
|
+
return target.header unless target.header.nil?
|
|
15
|
+
|
|
16
|
+
project_header && agents_markdown?(output)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def header_text(target, output, project_header)
|
|
20
|
+
return unless compose_writes_header?(target, output, project_header)
|
|
21
|
+
|
|
22
|
+
name = File.basename(output)
|
|
23
|
+
guidance = if agents_markdown?(output)
|
|
24
|
+
"Do not edit managed blocks in `#{name}` or provisioned files under `.agents/`."
|
|
25
|
+
else
|
|
26
|
+
"Do not edit managed blocks in `#{name}`."
|
|
27
|
+
end
|
|
28
|
+
<<~HEADER.rstrip
|
|
29
|
+
<!-- pray:0 ignore-comments -->
|
|
30
|
+
|
|
31
|
+
# Agent context
|
|
32
|
+
|
|
33
|
+
#{guidance}
|
|
34
|
+
To change shared guidance, update `Prayfile` and run `pray install`.
|
|
35
|
+
HEADER
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ensure_html_comment_dest!(output)
|
|
39
|
+
dest = output.tr("\\", "/")
|
|
40
|
+
extension = File.extname(output).downcase
|
|
41
|
+
return if HTML_COMMENT_EXTENSIONS.include?(extension)
|
|
42
|
+
if extension == ".json"
|
|
43
|
+
raise Error.render("compose cannot write JSON; use file: \"#{dest}\" for unmarked bytes")
|
|
44
|
+
end
|
|
45
|
+
if BINARY_EXTENSIONS.include?(extension)
|
|
46
|
+
raise Error.render(
|
|
47
|
+
"compose cannot write a binary file; use file: \"#{dest}\" for unmarked bytes"
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
raise Error.render(
|
|
51
|
+
"compose cannot write this file type; use file: \"#{dest}\" for unmarked bytes"
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def agents_markdown?(output)
|
|
56
|
+
File.basename(output) == "AGENTS.md"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -28,7 +28,12 @@ module Pray
|
|
|
28
28
|
case target.mode
|
|
29
29
|
when "compose"
|
|
30
30
|
path = target.outputs.first || ""
|
|
31
|
-
|
|
31
|
+
header = case target.header
|
|
32
|
+
when true then ", header: true"
|
|
33
|
+
when false then ", header: false"
|
|
34
|
+
else ""
|
|
35
|
+
end
|
|
36
|
+
lines << %(compose "#{path}"#{header} do)
|
|
32
37
|
target.entries.each do |entry|
|
|
33
38
|
lines << " #{format_destination_entry(entry, manifest)}"
|
|
34
39
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "resource_limits"
|
|
4
|
+
|
|
5
|
+
module Pray
|
|
6
|
+
module HttpBody
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def reject_oversized_content_length!(content_length, max_bytes = ResourceLimits::MAX_HTTP_RESPONSE_BYTES)
|
|
10
|
+
return if content_length.nil?
|
|
11
|
+
return if content_length <= max_bytes
|
|
12
|
+
|
|
13
|
+
raise Error.resolution("HTTP response exceeds #{max_bytes} bytes")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def append_chunk!(body, chunk, max_bytes = ResourceLimits::MAX_HTTP_RESPONSE_BYTES)
|
|
17
|
+
body << chunk
|
|
18
|
+
return body if body.bytesize <= max_bytes
|
|
19
|
+
|
|
20
|
+
raise Error.resolution("HTTP response exceeds #{max_bytes} bytes")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def read_response!(response, max_bytes = ResourceLimits::MAX_HTTP_RESPONSE_BYTES)
|
|
24
|
+
length = response["content-length"]
|
|
25
|
+
parsed = length && Integer(length, exception: false)
|
|
26
|
+
reject_oversized_content_length!(parsed, max_bytes)
|
|
27
|
+
body = +"".b
|
|
28
|
+
response.read_body do |chunk|
|
|
29
|
+
append_chunk!(body, chunk, max_bytes)
|
|
30
|
+
end
|
|
31
|
+
body
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/pray/lockfile.rb
CHANGED
|
@@ -20,14 +20,16 @@ module Pray
|
|
|
20
20
|
:id, :target, :open_line, :close_line, :ideal_checksum, :package, :export,
|
|
21
21
|
:source_checksum, :silenced
|
|
22
22
|
)
|
|
23
|
+
ProvisionedFileRecord = Struct.new(:path, :content_hash, :package, :export)
|
|
23
24
|
|
|
24
25
|
Lockfile = Struct.new(
|
|
25
26
|
:prayfile_lock, :spec, :generated_by, :manifest_hash, :environment, :source, :package,
|
|
26
|
-
:target, :managed_span
|
|
27
|
+
:target, :managed_span, :provisioned
|
|
27
28
|
) do
|
|
28
29
|
def initialize(
|
|
29
30
|
prayfile_lock: "1", spec: "0.1", generated_by: Pray::GENERATED_BY,
|
|
30
|
-
manifest_hash: "", environment: nil, source: [], package: [], target: [], managed_span: []
|
|
31
|
+
manifest_hash: "", environment: nil, source: [], package: [], target: [], managed_span: [],
|
|
32
|
+
provisioned: []
|
|
31
33
|
)
|
|
32
34
|
super
|
|
33
35
|
end
|
|
@@ -38,6 +40,7 @@ module Pray
|
|
|
38
40
|
copy.package = package.sort_by { |entry| [entry.name, entry.source.to_s, entry.version] }
|
|
39
41
|
copy.target = target.sort_by(&:name)
|
|
40
42
|
copy.managed_span = managed_span.sort_by { |span| [span.target, span.open_line, span.id] }
|
|
43
|
+
copy.provisioned = provisioned.sort_by { |entry| [entry.path, entry.package] }
|
|
41
44
|
end
|
|
42
45
|
end
|
|
43
46
|
|
|
@@ -70,7 +73,8 @@ module Pray
|
|
|
70
73
|
"source" => lockfile.source.map { |entry| source_to_hash(entry) },
|
|
71
74
|
"package" => lockfile.package.map { |entry| package_to_hash(entry) },
|
|
72
75
|
"target" => lockfile.target.map { |entry| {"name" => entry.name, "outputs" => entry.outputs} },
|
|
73
|
-
"managed_span" => lockfile.managed_span.map { |entry| managed_span_to_hash(entry) }
|
|
76
|
+
"managed_span" => lockfile.managed_span.map { |entry| managed_span_to_hash(entry) },
|
|
77
|
+
"provisioned" => lockfile.provisioned.map { |entry| provisioned_to_hash(entry) }
|
|
74
78
|
}
|
|
75
79
|
end
|
|
76
80
|
|
|
@@ -111,6 +115,15 @@ module Pray
|
|
|
111
115
|
}
|
|
112
116
|
end
|
|
113
117
|
|
|
118
|
+
def provisioned_to_hash(entry)
|
|
119
|
+
{
|
|
120
|
+
"path" => entry.path,
|
|
121
|
+
"content_hash" => entry.content_hash,
|
|
122
|
+
"package" => entry.package,
|
|
123
|
+
"export" => entry.export
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
114
127
|
def parse_lockfile(text)
|
|
115
128
|
data = TomlRB.parse(text)
|
|
116
129
|
from_hash(data)
|
|
@@ -147,8 +160,9 @@ module Pray
|
|
|
147
160
|
left.equivalent_to?(right)
|
|
148
161
|
end
|
|
149
162
|
|
|
150
|
-
def build_lockfile(manifest_hash, environment, project_root, manifest_sources, manifest_targets, rendered, packages, source_revisions, source_host_keys)
|
|
151
|
-
|
|
163
|
+
def build_lockfile(manifest_hash, environment, project_root, manifest_sources, manifest_targets, rendered, packages, source_revisions, source_host_keys, project = nil)
|
|
164
|
+
rendered = Render.layout_rendered_targets(project, rendered) if project
|
|
165
|
+
lockfile = Lockfile.new(
|
|
152
166
|
manifest_hash: manifest_hash,
|
|
153
167
|
environment: environment,
|
|
154
168
|
source: manifest_sources.map do |source|
|
|
@@ -176,7 +190,11 @@ module Pray
|
|
|
176
190
|
end,
|
|
177
191
|
target: manifest_targets.map { |target| LockedTarget.new(name: target.name, outputs: target.outputs) },
|
|
178
192
|
managed_span: rendered.flat_map(&:managed_spans)
|
|
179
|
-
)
|
|
193
|
+
)
|
|
194
|
+
if project
|
|
195
|
+
lockfile.provisioned = RenderDest.provisioned_records(project)
|
|
196
|
+
end
|
|
197
|
+
lockfile.canonicalized
|
|
180
198
|
end
|
|
181
199
|
|
|
182
200
|
def from_hash(data)
|
|
@@ -222,6 +240,14 @@ module Pray
|
|
|
222
240
|
source_checksum: entry["source_checksum"],
|
|
223
241
|
silenced: entry["silenced"]
|
|
224
242
|
)
|
|
243
|
+
end,
|
|
244
|
+
provisioned: Array(data["provisioned"]).map do |entry|
|
|
245
|
+
ProvisionedFileRecord.new(
|
|
246
|
+
path: entry["path"],
|
|
247
|
+
content_hash: entry["content_hash"],
|
|
248
|
+
package: entry["package"],
|
|
249
|
+
export: entry["export"]
|
|
250
|
+
)
|
|
225
251
|
end
|
|
226
252
|
)
|
|
227
253
|
end
|
|
@@ -22,6 +22,7 @@ module Pray
|
|
|
22
22
|
lockfile.package.each { |entry| append_section(lines, format_package(entry)) }
|
|
23
23
|
lockfile.target.each { |entry| append_section(lines, format_target(entry)) }
|
|
24
24
|
lockfile.managed_span.each { |entry| append_section(lines, format_managed_span(entry)) }
|
|
25
|
+
lockfile.provisioned.each { |entry| append_section(lines, format_provisioned(entry)) }
|
|
25
26
|
|
|
26
27
|
lines.pop while lines.last == ""
|
|
27
28
|
"#{lines.join("\n")}\n"
|
|
@@ -94,6 +95,20 @@ module Pray
|
|
|
94
95
|
lines
|
|
95
96
|
end
|
|
96
97
|
|
|
98
|
+
def format_provisioned(entry)
|
|
99
|
+
lines = ["[[provisioned]]"]
|
|
100
|
+
append_scalars(
|
|
101
|
+
lines,
|
|
102
|
+
[
|
|
103
|
+
["path", entry.path],
|
|
104
|
+
["content_hash", entry.content_hash],
|
|
105
|
+
["package", entry.package],
|
|
106
|
+
["export", entry.export]
|
|
107
|
+
]
|
|
108
|
+
)
|
|
109
|
+
lines
|
|
110
|
+
end
|
|
111
|
+
|
|
97
112
|
def append_scalars(lines, entries)
|
|
98
113
|
entries.each do |key, value|
|
|
99
114
|
lines << case value
|
data/lib/pray/manifest.rb
CHANGED
|
@@ -5,30 +5,29 @@ require_relative "manifest_formatter"
|
|
|
5
5
|
require_relative "manifest_parser_helpers"
|
|
6
6
|
require_relative "manifest_parser_blocks"
|
|
7
7
|
require_relative "manifest_parser"
|
|
8
|
+
require_relative "path_safety"
|
|
8
9
|
|
|
9
10
|
module Pray
|
|
10
11
|
RenderPolicy = Struct.new(
|
|
11
|
-
:mode, :conflict, :churn, :header
|
|
12
|
+
:mode, :conflict, :churn, :header
|
|
12
13
|
) do
|
|
13
14
|
def self.default
|
|
14
15
|
new(
|
|
15
16
|
mode: "managed",
|
|
16
17
|
conflict: "fail",
|
|
17
18
|
churn: "minimal",
|
|
18
|
-
header: true
|
|
19
|
-
section_markers: true,
|
|
20
|
-
line_endings: "lf"
|
|
19
|
+
header: true
|
|
21
20
|
)
|
|
22
21
|
end
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
ManifestSource = Struct.new(:name, :kind, :url, :subdir, :rev, :tag)
|
|
26
25
|
ManifestTarget = Struct.new(
|
|
27
|
-
:name, :outputs, :skills, :commands, :rules, :max_bytes, :mode, :scoped, :entries
|
|
26
|
+
:name, :outputs, :skills, :commands, :rules, :max_bytes, :mode, :scoped, :entries, :header
|
|
28
27
|
) do
|
|
29
28
|
def initialize(
|
|
30
29
|
name:, outputs: [], skills: [], commands: [], rules: [], max_bytes: nil,
|
|
31
|
-
mode: "legacy", scoped: false, entries: []
|
|
30
|
+
mode: "legacy", scoped: false, entries: [], header: nil
|
|
32
31
|
)
|
|
33
32
|
super
|
|
34
33
|
end
|
|
@@ -72,7 +71,7 @@ module Pray
|
|
|
72
71
|
end
|
|
73
72
|
|
|
74
73
|
def note_deprecated_keyword(keyword)
|
|
75
|
-
return unless %w[target output agent].include?(keyword)
|
|
74
|
+
return unless %w[target output agent skills].include?(keyword)
|
|
76
75
|
self.deprecated_keywords ||= []
|
|
77
76
|
deprecated_keywords << keyword unless deprecated_keywords.include?(keyword)
|
|
78
77
|
end
|
|
@@ -81,7 +80,10 @@ module Pray
|
|
|
81
80
|
replacements = {
|
|
82
81
|
"target" => "compose` / `tree",
|
|
83
82
|
"output" => "compose",
|
|
84
|
-
"agent" => "pray"
|
|
83
|
+
"agent" => "pray",
|
|
84
|
+
"skills" => "tree` / `folder",
|
|
85
|
+
"skill" => "folder",
|
|
86
|
+
"spec.skills" => "a folder export"
|
|
85
87
|
}
|
|
86
88
|
(deprecated_keywords || []).filter_map do |keyword|
|
|
87
89
|
replacement = replacements[keyword]
|
|
@@ -118,7 +120,22 @@ module Pray
|
|
|
118
120
|
|
|
119
121
|
def parse_manifest(text)
|
|
120
122
|
lines = Literal.prepare_parser_lines(text)
|
|
121
|
-
BlockParser.new(lines).parse_root
|
|
123
|
+
manifest = BlockParser.new(lines).parse_root
|
|
124
|
+
validate_manifest_paths!(manifest)
|
|
125
|
+
manifest
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def validate_manifest_paths!(manifest)
|
|
129
|
+
manifest.targets.each do |target|
|
|
130
|
+
(target.outputs + target.skills + target.commands + target.rules).each do |path|
|
|
131
|
+
PathSafety.validate_destination_path!(path)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
manifest.packages.each do |package|
|
|
135
|
+
PathSafety.validate_project_relative_path!(package.path) if package.path
|
|
136
|
+
PathSafety.validate_destination_path!(package.file) if package.file
|
|
137
|
+
end
|
|
138
|
+
manifest.local.each { |local| PathSafety.validate_project_relative_path!(local.path) }
|
|
122
139
|
end
|
|
123
140
|
end
|
|
124
141
|
|
data/lib/pray/manifest_json.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Pray
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def target_fields(target)
|
|
37
|
-
{
|
|
37
|
+
fields = {
|
|
38
38
|
"name" => target.name,
|
|
39
39
|
"outputs" => target.outputs,
|
|
40
40
|
"skills" => target.skills,
|
|
@@ -45,6 +45,8 @@ module Pray
|
|
|
45
45
|
"scoped" => target.scoped || false,
|
|
46
46
|
"entries" => (target.entries || []).map { |entry| entry_fields(entry) }
|
|
47
47
|
}
|
|
48
|
+
fields["header"] = target.header unless target.header.nil?
|
|
49
|
+
fields
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
def entry_fields(entry)
|
|
@@ -93,9 +95,7 @@ module Pray
|
|
|
93
95
|
"mode" => render.mode,
|
|
94
96
|
"conflict" => render.conflict,
|
|
95
97
|
"churn" => render.churn,
|
|
96
|
-
"header" => render.header
|
|
97
|
-
"section_markers" => render.section_markers,
|
|
98
|
-
"line_endings" => render.line_endings
|
|
98
|
+
"header" => render.header
|
|
99
99
|
}
|
|
100
100
|
end
|
|
101
101
|
|
data/lib/pray/manifest_parser.rb
CHANGED
|
@@ -104,6 +104,7 @@ module Pray
|
|
|
104
104
|
if statement.start_with?("folder ", "skills ") && !statement.end_with?(" do")
|
|
105
105
|
raise Error.parse("manifest", "top-level folder/skills must use a tree block")
|
|
106
106
|
end
|
|
107
|
+
manifest.note_deprecated_keyword("skills") if statement.start_with?("skills ")
|
|
107
108
|
parse_destination_block(manifest, rest, "tree")
|
|
108
109
|
end
|
|
109
110
|
|
|
@@ -55,11 +55,13 @@ module Pray
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
header = rest.sub(/\s*do\z/, "").strip
|
|
58
|
-
values, = parse_call(header)
|
|
58
|
+
values, keywords = parse_call(header)
|
|
59
59
|
raise Error.parse("manifest", "destination missing path") if values.empty?
|
|
60
60
|
|
|
61
61
|
path = string_from_value(values.first)
|
|
62
|
-
|
|
62
|
+
target = Destination.new_destination_target(mode, path)
|
|
63
|
+
target.header = destination_header_keyword(mode, keywords)
|
|
64
|
+
manifest.targets << target
|
|
63
65
|
index = manifest.targets.length - 1
|
|
64
66
|
parse_destination_body(manifest, index, mode)
|
|
65
67
|
end
|
|
@@ -207,6 +209,7 @@ module Pray
|
|
|
207
209
|
raise Error.manifest("target index out of range") unless target
|
|
208
210
|
|
|
209
211
|
manifest.note_deprecated_keyword("output") if statement.start_with?("output ")
|
|
212
|
+
manifest.note_deprecated_keyword("skills") if statement.start_with?("skills ")
|
|
210
213
|
apply_target_statement(target, statement)
|
|
211
214
|
end
|
|
212
215
|
raise Error.parse("manifest", "missing 'end' for target block")
|
|
@@ -170,23 +170,52 @@ module Pray
|
|
|
170
170
|
|
|
171
171
|
def parse_render_policy(rest)
|
|
172
172
|
_, keywords = parse_call(rest)
|
|
173
|
+
keywords.each_key do |key|
|
|
174
|
+
unless %w[mode conflict churn header].include?(key)
|
|
175
|
+
raise Error.parse("manifest", "render does not accept #{key}")
|
|
176
|
+
end
|
|
177
|
+
end
|
|
173
178
|
conflict = keywords["conflict"]&.as_string || "fail"
|
|
174
179
|
unless conflict == "fail"
|
|
175
180
|
raise Error.unsupported(
|
|
176
181
|
"render conflict :#{conflict} is not implemented; only :fail is supported"
|
|
177
182
|
)
|
|
178
183
|
end
|
|
184
|
+
mode = keywords["mode"]&.as_string || "managed"
|
|
185
|
+
unless mode == "managed"
|
|
186
|
+
raise Error.unsupported("render mode :#{mode} is not implemented")
|
|
187
|
+
end
|
|
188
|
+
churn = keywords["churn"]&.as_string || "minimal"
|
|
189
|
+
unless churn == "minimal"
|
|
190
|
+
raise Error.unsupported("render churn :#{churn} is not implemented")
|
|
191
|
+
end
|
|
179
192
|
|
|
180
193
|
RenderPolicy.new(
|
|
181
|
-
mode:
|
|
194
|
+
mode: mode,
|
|
182
195
|
conflict: conflict,
|
|
183
|
-
churn:
|
|
184
|
-
header: keyword_bool(keywords, "header", true)
|
|
185
|
-
section_markers: keyword_bool(keywords, "section_markers", true),
|
|
186
|
-
line_endings: keywords["line_endings"]&.as_string || "lf"
|
|
196
|
+
churn: churn,
|
|
197
|
+
header: keyword_bool(keywords, "header", true)
|
|
187
198
|
)
|
|
188
199
|
end
|
|
189
200
|
|
|
201
|
+
def destination_header_keyword(mode, keywords)
|
|
202
|
+
label = (mode == "compose") ? "compose" : "tree"
|
|
203
|
+
keywords.each_key do |key|
|
|
204
|
+
unless key == "header"
|
|
205
|
+
raise Error.parse("manifest", "#{label} does not accept #{key}")
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
return if keywords["header"].nil?
|
|
209
|
+
unless mode == "compose"
|
|
210
|
+
raise Error.parse("manifest", "#{label} does not accept header")
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
value = keywords["header"].as_bool
|
|
214
|
+
raise Error.parse("manifest", "header must be true or false") if value.nil?
|
|
215
|
+
|
|
216
|
+
value
|
|
217
|
+
end
|
|
218
|
+
|
|
190
219
|
def keyword_bool(keywords, key, default)
|
|
191
220
|
value = keywords[key]
|
|
192
221
|
return default if value.nil?
|
data/lib/pray/materialize.rb
CHANGED
|
@@ -40,6 +40,7 @@ module Pray
|
|
|
40
40
|
end
|
|
41
41
|
rendered = Render.render_project(project)
|
|
42
42
|
lockfile_path = default_lockfile_path(project.project_root)
|
|
43
|
+
previous_lockfile = File.exist?(lockfile_path) ? Pray.read_lockfile(lockfile_path) : nil
|
|
43
44
|
next_lockfile = LockfileIO.build_lockfile(
|
|
44
45
|
project.manifest_hash,
|
|
45
46
|
project.environment,
|
|
@@ -49,7 +50,8 @@ module Pray
|
|
|
49
50
|
rendered,
|
|
50
51
|
project.packages,
|
|
51
52
|
project.source_revisions,
|
|
52
|
-
project.source_host_keys
|
|
53
|
+
project.source_host_keys,
|
|
54
|
+
project
|
|
53
55
|
)
|
|
54
56
|
|
|
55
57
|
if locked
|
|
@@ -61,7 +63,7 @@ module Pray
|
|
|
61
63
|
raise Error.verify("lockfile needs update; rerun install to refresh Prayfile.lock")
|
|
62
64
|
end
|
|
63
65
|
unless frozen
|
|
64
|
-
Render.write_rendered_targets(project, rendered)
|
|
66
|
+
Render.write_rendered_targets(project, rendered, existing)
|
|
65
67
|
end
|
|
66
68
|
if frozen
|
|
67
69
|
rendered.each do |target|
|
|
@@ -89,8 +91,8 @@ module Pray
|
|
|
89
91
|
end
|
|
90
92
|
end
|
|
91
93
|
|
|
94
|
+
Render.write_rendered_targets(project, rendered, previous_lockfile)
|
|
92
95
|
Pray.write_lockfile_if_changed(lockfile_path, next_lockfile)
|
|
93
|
-
Render.write_rendered_targets(project, rendered)
|
|
94
96
|
end
|
|
95
97
|
|
|
96
98
|
def default_manifest_path(working_directory = Dir.pwd)
|