Repository navigation
feat: anyr artifact publish - #110
Conversation
d5f28a7 to
7a46ee5
Compare
|
Rebased onto #108 (feat/generic-publish), conflicts in README, dispatch, commands, completion and parse.rs resolved as a union. cargo fmt, clippy -D warnings and E2E against prod with the branch build: file publish (link) and folder publish (index.html + sub/style.css; .env, .hidden/ and node_modules/ skipped) both return /a/ with 200; raw files serve 200 with the right bytes; |
duyetbot
left a comment
There was a problem hiding this comment.
Independent review of 7a46ee5 (artifact commit only, diffed against feat/generic-publish). Checked against the server on anyrouter main (packages/api-app/src/api/v1/artifacts/*, server-core/artifacts/{publish,limits,access}.ts). cargo test --all-targets is green: 289 unit tests, 11 in tests/artifact.rs, the rest pass too.
Verdict: approve. I found nothing blocking.
Checked against the server contract:
- Upload URL: the server's
putUrl()is${origin}/api/v1/artifacts/uploads/${uploadId}/files/${sha256}. The CLI buildsjoin_api(base, "/v1/artifacts/uploads/{upload}/files/{sha}")with base.../api, which is the same path. Not usingput_urlis fine, and it keeps the key on the configured--base-urlhost. - Manifest
{path,size,sha256}, the 202{upload_id, missing[]}response, the empty commit body and the 201{id,version,url,visibility}response all match. Blobs with the same hash are uploaded once, and the server dedupes by sha256 the same way. - Visibility is
private|link|public, which matchesVISIBILITIESin limits.ts. On republish,base_versioncomes fromcurrent_versionin the GET summary. A 409 readserror.current_version, which ArtifactError.toBody spreads intoerror, and exits 2. - The caps (5 MB per file, 50 files, 20 MB per publish) match ARTIFACT_LIMITS. The inline single-file read is capped at 5 MB, and the PUT streams from disk with an exact Content-Length.
- Folder walk:
DirEntry::file_type()does not follow symlinks, so symlinked files and dirs are skipped and the walk cannot loop or escape the folder. Dotfiles and node_modules are skipped. Paths are joined with/across components, so they are correct on Windows. Ids are checked before they go into the URL. - In a non-TTY,
publicandrmrefuse without--yes(via api::confirm, exit 2). A test covers this.
Non-blocking:
- src/artifact.rs:164:
--labelis only sent with--id, but the server'spublishNewalso acceptslabel. A new publish drops it with no warning. Either send it or reject it. - src/artifact.rs:281:
?limit={n}is not checked or URL-encoded. The server returns a 400 (1..100), but a local integer check would be a clearer usage error.lsalso ignoresnext_cursor, so you only get the first page. - src/artifact.rs:285: the remap from
artifactstodatais dead code, because the server already returns{object:"list",data,next_cursor}. - src/artifact.rs:467-500: every file is hashed before the 50-file and 20 MB caps are checked. A big folder hashes everything before it is rejected. Count first, or stop at MAX_FILES+1.
- src/artifact.rs:359
check(): server error text is printed without the key redaction that byok uses (redact_removes_secret_from_server_errors). The risk is low because these routes do not echo the key, but reusing the redaction would be consistent. - tests/artifact.rs: no test covers symlink skipping, and no test covers a 413 cap or the hash that fails on a size mismatch. Adding a symlinked
secret -> ~/.sshin the folder test would lock in the most important safety property. - A non-UTF-8 file name becomes U+FFFD through
to_string_lossyand fails on the server. Rejecting it locally would give a clearer error.
Publish a file (inline JSON) or a folder (manifest, missing-blob PUT, commit). --id republishes with base_version; a 409 prints the current version and exits 2. public asks for confirmation unless --yes. Adds value flags --title, --visibility, --id, --label to the parser (overlaps the parse.rs fix in #108) and http_put_file for streamed PUT. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Duyet Le <me@duyet.net> Co-authored-by: duyetbot <bot@duyet.net>
7a46ee5 to
b6e1f36
Compare
anyr size and startupStartup is wall time for a cold Size budget:
Budget check: ok raw timings[
{
"asset": "anyr-darwin-arm64",
"kind": "native",
"path": "/Users/runner/work/cli/cli/anyr-darwin-arm64",
"bytes": 3195688,
"size": "3.0 MiB",
"version": "0.1.21 (built 2026-10-23 04:33:03)",
"target": "aarch64-apple-darwin",
"os": "macos-latest",
"startup_version": {
"n": 21,
"min_ms": 3.42,
"median_ms": 3.78,
"p95_ms": 4.25,
"mean_ms": 3.8
},
"startup_help": {
"n": 21,
"min_ms": 3.21,
"median_ms": 3.5,
"p95_ms": 3.91,
"mean_ms": 3.53
}
},
{
"asset": "anyr-darwin-x86_64",
"kind": "native",
"path": "/Users/runner/work/cli/cli/anyr-darwin-x86_64",
"bytes": 3403480,
"size": "3.2 MiB",
"version": "0.1.21 (built 2026-10-23 04:34:40)",
"target": "x86_64-apple-darwin",
"os": "macos-latest",
"startup_version": {
"n": 21,
"min_ms": 32.08,
"median_ms": 35.79,
"p95_ms": 38.31,
"mean_ms": 35.71
},
"startup_help": {
"n": 21,
"min_ms": 33.03,
"median_ms": 36.58,
"p95_ms": 38.26,
"mean_ms": 36.36
}
},
{
"asset": "anyr-linux-arm64",
"kind": "native",
"path": "/home/runner/work/cli/cli/anyr-linux-arm64",
"bytes": 3151016,
"size": "3.0 MiB",
"version": "0.1.21 (built 2026-10-23 04:31:56)",
"target": "aarch64-unknown-linux-gnu",
"os": "ubuntu-24.04-arm",
"startup_version": {
"n": 21,
"min_ms": 0.64,
"median_ms": 0.71,
"p95_ms": 0.82,
"mean_ms": 0.72
},
"startup_help": {
"n": 21,
"min_ms": 0.72,
"median_ms": 0.74,
"p95_ms": 0.81,
"mean_ms": 0.75
}
},
{
"asset": "anyr-linux-x86_64",
"kind": "native",
"path": "/home/runner/work/cli/cli/anyr-linux-x86_64",
"bytes": 3497512,
"size": "3.3 MiB",
"version": "0.1.21 (built 2026-10-23 04:32:42)",
"target": "x86_64-unknown-linux-gnu",
"os": "ubuntu-latest",
"startup_version": {
"n": 21,
"min_ms": 0.96,
"median_ms": 0.99,
"p95_ms": 1.09,
"mean_ms": 1.01
},
"startup_help": {
"n": 21,
"min_ms": 0.92,
"median_ms": 0.96,
"p95_ms": 1.01,
"mean_ms": 0.97
}
},
{
"asset": "anyr-windows-x86_64.exe",
"kind": "native",
"path": "D:\\a\\cli\\cli\\anyr-windows-x86_64.exe",
"bytes": 2915840,
"size": "2.8 MiB",
"version": "0.1.21 (built 2026-10-23T04:33:16Z)",
"target": "x86_64-pc-windows-msvc",
"os": "windows-latest",
"startup_version": {
"n": 21,
"min_ms": 9.22,
"median_ms": 11.19,
"p95_ms": 13.6,
"mean_ms": 11.36
},
"startup_help": {
"n": 21,
"min_ms": 9.08,
"median_ms": 9.8,
"p95_ms": 13.08,
"mean_ms": 10.32
}
},
{
"asset": "anyr.wasm",
"kind": "wasm",
"path": "/home/runner/work/cli/cli/target/wasm-pkg/anyr_cli_bg.wasm",
"bytes": 199628,
"size": "194.9 KiB",
"version": "wasm",
"target": "wasm32-unknown-unknown",
"os": ""
}
] |
Adds
anyr artifact publish|ls|rm|visibilityfor duyet/anyrouter#6045.POST /v1/artifacts. Folder: manifest,PUTmissing blobs, commit. Dotfiles,node_modulesand.gitare skipped. Client caps match the server MVP: 5 MB per file, 50 files, 20 MB.--id:GETcurrent_version, thenPOST /v1/artifacts/<id>/versionswithbase_version. A 409 prints the current version and exits 2.publicasks for confirmation unless--yes. Stdout is the URL (or JSON with--json). Exit codes: 0 ok, 1 error, 2 usage or conflict, 3 not found, 4 auth.api::context,request,confirm,exit_for. New:http_put_file(streamed PUT).title,visibility,id,labeltoVALUE_FLAGSin parse.rs. Does not depend on feat(publish): generic anyr publish and apps, api --data @file/- #108; expect a trivial rebase conflict in parse.rs, commands.rs, help.rs, completion.rs.Depends on the server API PR (duyet/anyrouter#6045) landing. Until then the commands fail against production. The manifest flow builds upload URLs from
upload_idandsha256per DESIGN 4.1 and ignoresput_url.Release binary 3,462,224 bytes (cap 4,194,304). fmt, clippy -D warnings,
cargo test --locked --all-targets(11 new integration tests, 3 unit) pass; wasm lib check builds.🤖 Generated with Claude Code