@microck/raft 0.6.0 → 0.7.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.
- package/README.md +13 -4
- package/deploy/build-image.py +4 -2
- package/deploy/verify-controls.py +5 -5
- package/deploy/verify-features.py +35 -2
- package/deploy/verify-jobs.py +264 -0
- package/deploy/verify-limits.py +20 -2
- package/deploy/verify-live.py +12 -3
- package/deploy/verify-output.py +208 -0
- package/deploy/verify-release.py +33 -4
- package/deploy/verify-run.py +571 -0
- package/deploy/verify-self-update.py +9 -1
- package/deploy/verify-sync.py +122 -0
- package/docs/benchmarks/read-latency-2026-10-29.json +70 -0
- package/docs/boat-capabilities.md +60 -7
- package/docs/boat-parity-design.md +1 -0
- package/docs/boot-performance.mdx +21 -0
- package/docs/contract.md +76 -2
- package/docs/first-box.mdx +22 -19
- package/docs/guides/background-jobs.mdx +38 -31
- package/docs/guides/backup-recovery.mdx +3 -2
- package/docs/guides/ci-jobs.mdx +209 -0
- package/docs/guides/files.mdx +2 -2
- package/docs/guides/index.mdx +31 -0
- package/docs/guides/long-running-services.mdx +1 -1
- package/docs/guides/meta.json +7 -2
- package/docs/guides/project-setup.mdx +1 -1
- package/docs/guides/scripting-ci.mdx +28 -9
- package/docs/guides/snapshots-forks.mdx +3 -1
- package/docs/guides/upgrades.mdx +7 -7
- package/docs/index.mdx +26 -28
- package/docs/maintainers/index.mdx +18 -0
- package/docs/maintainers/meta.json +12 -0
- package/docs/meta.json +5 -9
- package/docs/operations.mdx +7 -0
- package/docs/publishing.md +14 -4
- package/docs/reference/capacity.mdx +2 -1
- package/docs/reference/cli.md +85 -19
- package/docs/reference/meta.json +3 -1
- package/docs/releases/0.2.0.md +1 -0
- package/docs/releases/0.2.1.md +1 -0
- package/docs/releases/0.3.0.md +1 -0
- package/docs/releases/0.4.0.md +1 -0
- package/docs/releases/0.5.0.md +1 -0
- package/docs/releases/0.6.0.md +1 -0
- package/docs/releases/0.7.0.md +23 -0
- package/docs/releases/index.mdx +21 -0
- package/docs/releases/meta.json +15 -0
- package/docs/requirements.mdx +1 -1
- package/docs/reviews/docs-audit-2026-10-24.md +92 -0
- package/docs/reviews/performance-audit-2026-10-29.md +90 -0
- package/docs/site-development.md +1 -0
- package/docs/verification.mdx +21 -3
- package/package.json +3 -1
- package/raft.py +380 -179
- package/raft_files.py +13 -1
- package/raft_jobs.py +390 -0
- package/raft_output.py +166 -0
- package/raft_run.py +330 -0
- package/skills/raft-cli/SKILL.md +78 -197
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Verify JSONL bytes, errors, completion discovery and native command status."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import base64
|
|
5
|
+
import importlib.util
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
|
|
13
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
14
|
+
sys.path.insert(0, str(ROOT))
|
|
15
|
+
import raft # noqa: E402
|
|
16
|
+
import raft_output # noqa: E402
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def events(command, env):
|
|
20
|
+
response = subprocess.run(command, env=env, capture_output=True)
|
|
21
|
+
records = [json.loads(line) for line in response.stdout.splitlines()]
|
|
22
|
+
assert not response.stderr, response.stderr
|
|
23
|
+
assert records[-1] == {"type": "exit", "code": response.returncode}, records
|
|
24
|
+
return response, records
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def stream(records, kind):
|
|
28
|
+
return b"".join(base64.b64decode(r["data"]) for r in records if r["type"] == kind)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def verify():
|
|
32
|
+
with tempfile.TemporaryDirectory(prefix="raft-output-") as directory:
|
|
33
|
+
env = {**os.environ, "HOME": directory, "RAFT_NO_UPDATE_CHECK": "1"}
|
|
34
|
+
command = [sys.executable, str(ROOT / "raft.py"), "--json"]
|
|
35
|
+
response, records = events(command + ["--help"], env)
|
|
36
|
+
assert response.returncode == 0 and b"usage:" in stream(records, "stdout")
|
|
37
|
+
for arguments in [["invalid"], ["new", "--ttl", "no"], ["new", "--unknown"]]:
|
|
38
|
+
response, records = events(command + arguments, env)
|
|
39
|
+
assert response.returncode == 2 and records[0]["code"] == "usage", records
|
|
40
|
+
response, records = events(command + ["locations"], env)
|
|
41
|
+
assert response.returncode == 1 and records[0]["code"] == "operation", records
|
|
42
|
+
response, records = events(command + ["run", "--", "true"], env)
|
|
43
|
+
failures = [record for record in records if record["type"] == "error"]
|
|
44
|
+
assert response.returncode == 1 and len(failures) == 1, records
|
|
45
|
+
assert failures[0]["code"] == "operation" and "incus.json" in failures[0]["message"]
|
|
46
|
+
assert b"raft run: finished" in stream(records, "stderr"), records
|
|
47
|
+
plain = subprocess.run(
|
|
48
|
+
[sys.executable, str(ROOT / "raft.py"), "run", "--", "true"],
|
|
49
|
+
env=env,
|
|
50
|
+
capture_output=True,
|
|
51
|
+
)
|
|
52
|
+
assert plain.returncode == 1 and not plain.stdout
|
|
53
|
+
assert plain.stderr.startswith(b"raft run: ") and b"incus.json" in plain.stderr
|
|
54
|
+
inventory = Path(directory) / ".config/raft"
|
|
55
|
+
inventory.mkdir(parents=True)
|
|
56
|
+
(inventory / "incus.json").write_text('{"fixture":{"ssh":"offline.invalid","image":""}}')
|
|
57
|
+
response, records = events(command + ["locations"], env)
|
|
58
|
+
assert records[0] == {
|
|
59
|
+
"type": "data",
|
|
60
|
+
"value": {"location": "fixture", "default": True, "note": ""},
|
|
61
|
+
}
|
|
62
|
+
# Real subprocess descriptors, including binary bytes and more than a pipe
|
|
63
|
+
# buffer on both channels. Reconstruction must be exact and cannot hang.
|
|
64
|
+
program = """
|
|
65
|
+
import os, subprocess, sys
|
|
66
|
+
import raft_output
|
|
67
|
+
out = raft_output.JsonOutput().start()
|
|
68
|
+
subprocess.run([sys.executable, '-c', 'import os; os.write(1, bytes(range(256))*512); os.write(2,b"error"*32768)'], check=True)
|
|
69
|
+
out.finish(7)
|
|
70
|
+
sys.exit(7)
|
|
71
|
+
"""
|
|
72
|
+
response, records = events(
|
|
73
|
+
[sys.executable, "-c", program], {**env, "PYTHONPATH": str(ROOT)}
|
|
74
|
+
)
|
|
75
|
+
assert response.returncode == 7
|
|
76
|
+
assert stream(records, "stdout") == bytes(range(256)) * 512
|
|
77
|
+
assert stream(records, "stderr") == b"error" * 32768
|
|
78
|
+
for number in (9, 15):
|
|
79
|
+
# The event and shell status must agree after real child termination.
|
|
80
|
+
signalled = f"""
|
|
81
|
+
import subprocess, sys
|
|
82
|
+
import raft, raft_output
|
|
83
|
+
out = raft_output.JsonOutput().start()
|
|
84
|
+
child = subprocess.run([sys.executable, '-c', 'import os, signal; os.kill(os.getpid(), {number})'])
|
|
85
|
+
status = raft.exit_status(child.returncode)
|
|
86
|
+
out.finish(status)
|
|
87
|
+
sys.exit(status)
|
|
88
|
+
"""
|
|
89
|
+
response, records = events(
|
|
90
|
+
[sys.executable, "-c", signalled], {**env, "PYTHONPATH": str(ROOT)}
|
|
91
|
+
)
|
|
92
|
+
assert response.returncode == 128 + number, records
|
|
93
|
+
parser = raft.parser()
|
|
94
|
+
parser.add_argument("--future-flag", action="store_true")
|
|
95
|
+
assert raft_output.candidates(parser, ["--future"]) == ["--future-flag"]
|
|
96
|
+
assert "--location" in raft_output.candidates(parser, ["new", "--lo"])
|
|
97
|
+
assert "set" in raft_output.candidates(parser, ["config", ""])
|
|
98
|
+
assert raft_output.candidates(parser, ["exec", "box", "--", ""]) == []
|
|
99
|
+
assert raft_output.candidates(parser, ["completions", "z"]) == ["zsh"]
|
|
100
|
+
for shell in ("bash", "zsh", "fish"):
|
|
101
|
+
script = subprocess.run(
|
|
102
|
+
[sys.executable, str(ROOT / "raft.py"), "completions", shell],
|
|
103
|
+
env=env,
|
|
104
|
+
capture_output=True,
|
|
105
|
+
check=True,
|
|
106
|
+
)
|
|
107
|
+
assert b"_complete" in script.stdout and not script.stderr
|
|
108
|
+
setup = raft_output.completions("bash")
|
|
109
|
+
completed = subprocess.run(
|
|
110
|
+
[
|
|
111
|
+
"bash",
|
|
112
|
+
"-c",
|
|
113
|
+
setup
|
|
114
|
+
+ "\nraft() { "
|
|
115
|
+
+ sys.executable
|
|
116
|
+
+ " "
|
|
117
|
+
+ str(ROOT / "raft.py")
|
|
118
|
+
+ ' "$@"; }\nCOMP_WORDS=(raft config ""); COMP_CWORD=2; _raft_complete; printf "%s\\n" "${COMPREPLY[@]}"',
|
|
119
|
+
],
|
|
120
|
+
env=env,
|
|
121
|
+
capture_output=True,
|
|
122
|
+
check=True,
|
|
123
|
+
)
|
|
124
|
+
assert "set" in completed.stdout.decode().splitlines(), completed
|
|
125
|
+
print("JSONL bytes, subprocess streams, structured errors, reports and Bash completion passed")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def native(location):
|
|
129
|
+
spec = importlib.util.spec_from_file_location("live", ROOT / "deploy/verify-live.py")
|
|
130
|
+
live = importlib.util.module_from_spec(spec)
|
|
131
|
+
spec.loader.exec_module(live)
|
|
132
|
+
box = live.raft(
|
|
133
|
+
"new",
|
|
134
|
+
"--location",
|
|
135
|
+
location,
|
|
136
|
+
"--ttl",
|
|
137
|
+
"600",
|
|
138
|
+
"--disposable",
|
|
139
|
+
"--purpose",
|
|
140
|
+
"JSON output verification",
|
|
141
|
+
"--owner",
|
|
142
|
+
"verify-output",
|
|
143
|
+
).stdout.strip()
|
|
144
|
+
try:
|
|
145
|
+
live.wait_running(box)
|
|
146
|
+
response, records = events(
|
|
147
|
+
[
|
|
148
|
+
sys.executable,
|
|
149
|
+
str(ROOT / "raft.py"),
|
|
150
|
+
"--json",
|
|
151
|
+
"exec",
|
|
152
|
+
box,
|
|
153
|
+
"--",
|
|
154
|
+
"sh",
|
|
155
|
+
"-c",
|
|
156
|
+
"printf hello; printf error >&2; exit 7",
|
|
157
|
+
],
|
|
158
|
+
live.CONTROLLER_ENV,
|
|
159
|
+
)
|
|
160
|
+
assert response.returncode == 7
|
|
161
|
+
assert stream(records, "stdout") == b"hello" and stream(records, "stderr") == b"error", (
|
|
162
|
+
records
|
|
163
|
+
)
|
|
164
|
+
response, records = events(
|
|
165
|
+
[sys.executable, str(ROOT / "raft.py"), "--json", "info", box], live.CONTROLLER_ENV
|
|
166
|
+
)
|
|
167
|
+
assert records[0]["value"]["name"] == box.partition(":")[2], records
|
|
168
|
+
with tempfile.TemporaryDirectory(prefix="raft-output-artifacts-") as artifacts:
|
|
169
|
+
response, records = events(
|
|
170
|
+
[
|
|
171
|
+
sys.executable,
|
|
172
|
+
str(ROOT / "raft.py"),
|
|
173
|
+
"--json",
|
|
174
|
+
"run",
|
|
175
|
+
"--location",
|
|
176
|
+
location,
|
|
177
|
+
"--purpose",
|
|
178
|
+
"JSON artifact failure verification",
|
|
179
|
+
"--owner",
|
|
180
|
+
"verify-output",
|
|
181
|
+
"--pull",
|
|
182
|
+
"/missing-raft-artifacts",
|
|
183
|
+
str(Path(artifacts) / "collected"),
|
|
184
|
+
"--",
|
|
185
|
+
"true",
|
|
186
|
+
],
|
|
187
|
+
live.CONTROLLER_ENV,
|
|
188
|
+
)
|
|
189
|
+
assert response.returncode == 1, records
|
|
190
|
+
assert any(
|
|
191
|
+
record["type"] == "error"
|
|
192
|
+
and record["code"] == "operation"
|
|
193
|
+
and "could not collect /missing-raft-artifacts" in record["message"]
|
|
194
|
+
for record in records
|
|
195
|
+
), records
|
|
196
|
+
assert b"cleanup confirmed" in stream(records, "stderr"), records
|
|
197
|
+
print(f"{location}: JSON exec, reports, run artifact failure and cleanup passed")
|
|
198
|
+
finally:
|
|
199
|
+
live.raft("destroy", box)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
cli = argparse.ArgumentParser(description=__doc__)
|
|
204
|
+
cli.add_argument("--location")
|
|
205
|
+
args = cli.parse_args()
|
|
206
|
+
verify()
|
|
207
|
+
if args.location:
|
|
208
|
+
native(args.location)
|
package/deploy/verify-release.py
CHANGED
|
@@ -68,12 +68,21 @@ def verify_npm():
|
|
|
68
68
|
content = archive.extractfile(member).read()
|
|
69
69
|
inspect(member.name, content)
|
|
70
70
|
names.add(member.name)
|
|
71
|
-
if member.name in {
|
|
71
|
+
if member.name in {
|
|
72
|
+
"package/raft.py",
|
|
73
|
+
"package/raft_files.py",
|
|
74
|
+
"package/raft_run.py",
|
|
75
|
+
"package/raft_jobs.py",
|
|
76
|
+
"package/raft_output.py",
|
|
77
|
+
}:
|
|
72
78
|
if content != (ROOT / PurePosixPath(member.name).name).read_bytes():
|
|
73
79
|
raise ValueError("npm runtime differs from canonical Python source")
|
|
74
80
|
required = {
|
|
75
81
|
"package/raft.py",
|
|
76
82
|
"package/raft_files.py",
|
|
83
|
+
"package/raft_run.py",
|
|
84
|
+
"package/raft_jobs.py",
|
|
85
|
+
"package/raft_output.py",
|
|
77
86
|
"package/LICENSE",
|
|
78
87
|
"package/deploy/deploy.py",
|
|
79
88
|
"package/images/incus/build.sh",
|
|
@@ -105,12 +114,28 @@ def main():
|
|
|
105
114
|
raise ValueError("Unexpected non-file member: " + member.name)
|
|
106
115
|
inspect(member.name, archive.extractfile(member).read())
|
|
107
116
|
names = {PurePosixPath(member.name).parts[1] for member in members}
|
|
108
|
-
if
|
|
117
|
+
if (
|
|
118
|
+
not {
|
|
119
|
+
"raft.py",
|
|
120
|
+
"raft_files.py",
|
|
121
|
+
"raft_run.py",
|
|
122
|
+
"raft_jobs.py",
|
|
123
|
+
"raft_output.py",
|
|
124
|
+
"deploy",
|
|
125
|
+
"docs",
|
|
126
|
+
"images",
|
|
127
|
+
"skills",
|
|
128
|
+
"LICENSE",
|
|
129
|
+
}
|
|
130
|
+
<= names
|
|
131
|
+
):
|
|
109
132
|
raise ValueError("Source archive missing installation resources")
|
|
110
133
|
with zipfile.ZipFile(wheels[0]) as archive:
|
|
111
134
|
for name in archive.namelist():
|
|
112
135
|
inspect(name, archive.read(name))
|
|
113
|
-
if not {"raft.py", "raft_files.py"} <= set(
|
|
136
|
+
if not {"raft.py", "raft_files.py", "raft_run.py", "raft_jobs.py", "raft_output.py"} <= set(
|
|
137
|
+
archive.namelist()
|
|
138
|
+
):
|
|
114
139
|
raise ValueError("Wheel missing CLI module")
|
|
115
140
|
# A new environment avoids both editable checkouts and uv's cached same-version
|
|
116
141
|
# tool environments. Run the installed entry point outside the source tree.
|
|
@@ -123,7 +148,11 @@ def main():
|
|
|
123
148
|
check=True,
|
|
124
149
|
)
|
|
125
150
|
verify_cli(environment / "bin/raft", work)
|
|
126
|
-
subprocess.run(
|
|
151
|
+
subprocess.run(
|
|
152
|
+
[str(python), "-I", "-c", "import raft, raft_files, raft_run, raft_jobs, raft_output"],
|
|
153
|
+
cwd=work,
|
|
154
|
+
check=True,
|
|
155
|
+
)
|
|
127
156
|
verify_npm()
|
|
128
157
|
print("Release archives and isolated wheel installation passed")
|
|
129
158
|
|