Skip to content

feat: anyr artifact publish - #110

Merged
duyet merged 1 commit into
mainfrom
feat/artifact-publish
Oct 23, 2026
Merged

duyet merged 1 commit into
mainfrom
feat/artifact-publish

Conversation

@duyet

@duyet duyet commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Adds anyr artifact publish|ls|rm|visibility for duyet/anyrouter#6045.

  • File: inline JSON POST /v1/artifacts. Folder: manifest, PUT missing blobs, commit. Dotfiles, node_modules and .git are skipped. Client caps match the server MVP: 5 MB per file, 50 files, 20 MB.
  • --id: GET current_version, then POST /v1/artifacts/<id>/versions with base_version. A 409 prints the current version and exits 2.
  • public asks 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.
  • Reuses api::context, request, confirm, exit_for. New: http_put_file (streamed PUT).
  • Overlap with feat(publish): generic anyr publish and apps, api --data @file/- #108: adds value flags title, visibility, id, label to VALUE_FLAGS in 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_id and sha256 per DESIGN 4.1 and ignores put_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

@duyet
duyet force-pushed the feat/artifact-publish branch from d5f28a7 to 7a46ee5 Compare October 23, 2026 04:25
@duyet

duyet commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

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 cargo test --locked --all-targets pass.

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; ls lists both; visibility private makes the anonymous raw URL 404; public without --yes exits 2, with --yes succeeds; --id --label v2 publishes version 2; rm without --yes exits 2, with --yes deletes and raw 404s; a second rm exits 3.

@duyetbot duyetbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 builds join_api(base, "/v1/artifacts/uploads/{upload}/files/{sha}") with base .../api, which is the same path. Not using put_url is fine, and it keeps the key on the configured --base-url host.
  • 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 matches VISIBILITIES in limits.ts. On republish, base_version comes from current_version in the GET summary. A 409 reads error.current_version, which ArtifactError.toBody spreads into error, 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, public and rm refuse without --yes (via api::confirm, exit 2). A test covers this.

Non-blocking:

  1. src/artifact.rs:164: --label is only sent with --id, but the server's publishNew also accepts label. A new publish drops it with no warning. Either send it or reject it.
  2. 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. ls also ignores next_cursor, so you only get the first page.
  3. src/artifact.rs:285: the remap from artifacts to data is dead code, because the server already returns {object:"list",data,next_cursor}.
  4. 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.
  5. 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.
  6. 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 -> ~/.ssh in the folder test would lock in the most important safety property.
  7. A non-UTF-8 file name becomes U+FFFD through to_string_lossy and 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>
@duyet

duyet commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

#108 merged; rebased onto main. The artifact diff is unchanged from the approved 7a46ee5 (only blob hashes and hunk offsets differ). fmt, clippy and tests pass.

@github-actions

Copy link
Copy Markdown
Contributor

anyr size and startup

Startup is wall time for a cold anyr --version / anyr --help (median of 21 runs). Size is the stripped release binary, or the .wasm for the browser demo.

Size budget: anyr-linux-x86_64 must stay ≤ 4.0 MiB (4194304 bytes) stripped.

Asset Kind Size --version median --help median
anyr-darwin-arm64 native 3.0 MiB 3.78 ms 3.5 ms
anyr-darwin-x86_64 native 3.2 MiB 35.79 ms 36.58 ms
anyr-linux-arm64 native 3.0 MiB 0.71 ms 0.74 ms
anyr-linux-x86_64 native 3.3 MiB 0.99 ms 0.96 ms
anyr-windows-x86_64.exe native 2.8 MiB 11.19 ms 9.8 ms
anyr.wasm wasm 194.9 KiB — —

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": ""
  }
]

duyet added a commit that referenced this pull request Oct 23, 2026
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
@duyet
duyet deleted the feat/artifact-publish branch October 25, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants