Skip to content

feat(media): media upload command, SDK and agent skill - #358

Open
gregberge wants to merge 8 commits into
mainfrom
feat/media-upload
Open

feat(media): media upload command, SDK and agent skill#358
gregberge wants to merge 8 commits into
mainfrom
feat/media-upload

Conversation

@gregberge

@gregberge gregberge commented Aug 8, 2026

Copy link
Copy Markdown
Member

Client side of standalone media upload. Pairs with argos-ci/argos#2445, which adds the API.

Draft — iterating.

What's here

  • uploadMedia() in @argos-ci/core — registers each file, posts the bytes straight to storage with the signed policy, then finalizes. Sequential rather than concurrent: these are large files, and saturating an uplink makes every one of them slower while making progress output useless.
  • argos media upload|list|get|update|delete|versions, plus argos media comment … — a new subcommand group, not a change to argos upload, which already means "upload screenshots and create a build". Every media endpoint the API exposes has a command; finalize is not one of them, because upload does it.
  • WebP compression before upload — a 252 KB PNG screenshot goes out at 25 KB. Skipped when it would not help: a video, an already-efficient WebP/AVIF, an animated GIF, an image past WebP's 16383px limit (a long full-page capture reaches it), or bytes that came out no smaller. --no-compress uploads the file untouched.
  • argos-upload skill — the actual distribution channel for agents. Teaches when to attach media to a pull request (and when text is better), how staged media reaches a reviewer, and why the printed Markdown has to be pasted verbatim: a video embeds its poster frame wrapped in a link, because GitHub renders inline players only for media it hosts itself. Hand-writing a <video> tag produces a dead link.
  • skills/argos-cli — command map and auth table updated so they stay accurate.
  • packages/api-client/src/schema.ts — regenerated from the API's own OpenAPI document rather than by hand.

Following the API

The API moved while this branch was open, and three of its flags no longer existed on the other end.

  • --branch or --pr, and --comment is gone. Attaching a media to a pull request and showing it there were never two decisions. --branch is the one to reach for while working: the media is staged, and Argos publishes it and posts the comment itself once a pull request opens for that branch.
  • --slug is gone. Identity is the name, so re-uploading checkout.png adds a version and the share URL already posted to a pull request shows the new image, with nothing to edit.
  • --state, and before/after pairs. A file named checkout-before.png uploads as checkout.png labelled before, which is what lets the two halves be compared side by side. --state sets it for files not named that way, and a batch where two files would collide on one identity is refused rather than silently versioned.
  • --retention is gone — the plan decides retention.
  • media update and media versions cover the two endpoints that had no command. media feedback is gone with the endpoint behind it: a review is read as media list --branch then media comment list, and a comment carries the version it was pinned on, which is what media versions resolves.

Two things worth a look

  • The media name keeps the caller's extension (checkout.png even when WebP bytes are sent). Name + state + branch is the server-side identity, so letting the name follow the upload format would turn a --no-compress re-run — or a file the compressor skipped — into a second media instead of a second version. The storage key's extension comes from the content type anyway.
  • media upload no longer goes through the build configuration when a token is already in hand. It refused to run outside a git repository, complaining about a missing commit that uploading a screenshot has nothing to do with, and rejected the token argos login stores because it is not 40 characters. Tokenless CI auth still takes the old path, where a branch and a commit always exist.

Type of changes

enhancement

Checklist

  • The commit message follows Conventional Commits
  • Lint and unit tests pass locally
  • I have added tests if needed
  • My changes require a documentation change — argos-ci/docs PR is open

Notes

  • media upload and media update take a project token or a PAT with review access; media get, list and versions take either with access to the project; media delete needs project admin; every media comment command needs a PAT, reading included.
  • The pre-existing upload-oidc e2e test fails locally on a clean tree too — it needs a real GitHub Actions OIDC environment.

🤖 Generated with Claude Code

- `uploadMedia()` in `@argos-ci/core`: registers each file, posts the bytes
  straight to storage with the signed policy, and finalizes. Sequential rather
  than concurrent — these are large files, and saturating an uplink makes every
  one of them slower.
- `argos media upload|list|get|delete`. `upload` is a new subcommand rather than
  a change to `argos upload`, which already means "upload screenshots and create
  a build".
- `argos-upload` skill: teaches when to attach media to a pull request, and why
  the printed Markdown has to be pasted verbatim — a video embeds its poster
  frame wrapped in a link, because GitHub renders inline players only for media
  it hosts itself.
- Regenerated the API client types from the OpenAPI document.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
argos-js-sdk-reference Ready Ready Preview Aug 10, 2026 11:39am

Request Review

The other half of uploading a screenshot: a human pins a comment to a spot on it,
and the agent that produced it has to be able to read that back.

`argos media feedback` is the one an agent reaches for — every open thread on a
project's media in one call, each with the file URL to go and look at and the
normalized coordinates of its pin. Open threads only by default, so what comes
back is what is left to do. `argos media comment …` covers the eleven single-
comment actions, mirroring `argos comment` for builds.

Two things were stale and would have failed at runtime:

- The generated API client predated the project re-scoping, so it still described
  `/accounts/{accountSlug}/media` and knew nothing of the comment routes.
  Regenerated; the only route it lost is the one that no longer exists.
- `media upload` and `media list` sent an account where the API now expects a
  project, so an upload with a personal access token could not resolve a target
  at all. Both now take `--project <owner/project>`.

`formatComment` only printed an anchor when a screenshot diff sat behind it, so a
media pin was invisible in the text output — the coordinates never reached the
reader they were written for.
gregberge and others added 2 commits August 10, 2026 12:13
Generated from the backend's own OpenAPI document rather than by hand, so the
client describes what argos-ci/argos#2445 actually serves.

Media is versioned now, and the shape moved with it: `slug` and the `comment`
flag are gone, `state`, `description`, `branch` and `stage` arrived, media lives
under a project, and the project-wide feedback endpoint was dropped in favour of
listing a media's comments. `MediaVersion` and `PATCH /media/{mediaId}` are new.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CLI was written against a media API that has since changed under it, and
three of its flags no longer existed on the other end.

- `--branch` is the one to reach for while working: the media is staged, and
  Argos publishes it and posts the pull request comment itself once a pull
  request opens for that branch. `--pr` publishes straight away, and `--comment`
  is gone — attaching a media to a pull request and showing it there were never
  two decisions.
- `--slug` is gone too. Identity is the name, so re-uploading `checkout.png` adds
  a version and the share URL already posted to a pull request shows the new
  image. A file named `checkout-before.png` uploads as `checkout.png` labelled
  `before`, which is what lets the two halves of a pair be compared side by side;
  `--state` sets it for files not named that way, and a batch where two files
  would collide on one identity is refused rather than silently versioned.
- Images are converted to WebP before upload — a 252 KB PNG screenshot goes out
  at 25 KB. The media keeps the name it was given, because letting it follow the
  upload format would turn a re-run with `--no-compress` into a second media
  instead of a second version. Conversion is skipped when it would not help: a
  video, an already-efficient format, an animated GIF, an image past WebP's
  16383px limit, or bytes that came out no smaller.

`media update` and `media versions` cover the two endpoints that had no command.
`media feedback` is gone with the endpoint behind it — a review is now read as
`media list --branch` then `media comment list`, and a comment carries the
version it was pinned on, which is what `media versions` resolves.

`media upload` no longer goes through the build configuration when a token is
already in hand. It refused to run outside a git repository, complaining about a
missing commit that uploading a screenshot has nothing to do with, and rejected
the token `argos login` stores because it is not 40 characters. Tokenless CI auth
still takes the old path, where a branch and a commit always exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gregberge and others added 3 commits August 10, 2026 13:27
Regenerated from the backend's OpenAPI document. `retentionDays` is gone from the
`POST /media` body — argos-ci/argos#2445 fixed retention to the plan rather than
taking it from the request — and the client stopped sending it in the commit
before this one. Advertising a field the server now silently strips was the last
thing left.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r compression

Five things the upload path got wrong, found reviewing it.

`resolveAuth` went *around* the configuration rather than through it, because
`readConfig` insists on a branch and a commit — which uploading a screenshot has
nothing to do with — and because `mustBeArgosToken` demands exactly 40 characters,
which an `argos login` token (`argos_oat_` plus 40) is not. Both are now fixed at
the layer that owns them: `readConfig` takes `requireGitContext`, and the token
format knows the OAuth family. So media resolves its token, API and project the
same way every other command does, and a malformed `ARGOS_API_BASE_URL` fails with
"Invalid Argos API base URL" again instead of an opaque `TypeError` from fetch.

That also closes a leak. The old fallback handed the whole parameter bag to the
config layer, where `branch` means the branch of a *build* — and the CI tokenless
exchange authorizes against it. `argos media upload --branch feat/x` from a job
running on `main` asked to be authorized for `feat/x`. Only the three fields being
asked about are passed now.

Compression is an optimization, so it no longer refuses uploads:

- Any failure — a file sharp declines to decode, an unwritable `$TMPDIR` — falls
  back to the original bytes, which is what the docstring always claimed. Before,
  a PNG that GitHub renders fine could fail a command that used to work.
- The temporary file is deleted once the bytes are up, and its descriptor is never
  opened. `promisify(tmp.file)` drops both the fd and the remove callback, and
  `tmp` only sweeps at exit if asked to, so a few hundred screenshots meant a few
  hundred held descriptors and a few hundred WebP files left behind for good.
- An animated PNG is left alone. Only the first frame would have survived, and the
  "is it smaller?" check cannot notice: one frame always is. libvips reports no
  page count for APNG, so the `acTL` chunk is read off the bytes.
- Pixels are capped at the 80 Mpx the screenshot path already uses. The per-side
  WebP limit is not a bound on work — 16383x16383 clears it and is a gigabyte of
  raw RGBA, enough to take a CI container down over one upload.

And two smaller ones: a batch now checks every file is readable before creating
any media, instead of discovering a typo in file three with the first two already
uploaded and billed; and the collision guard's key is written with an escape,
because the literal NUL byte it held made the whole file binary to grep and to
GitHub's diff view.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`argos media comment list` is how an agent reads the feedback a human left on a
screenshot, and it was answering with neither of the two things that feedback
consists of. The list rendering printed author and text only, so the pin — the
whole content of "this button is misaligned", the part that says *which* button —
was visible in `--json` and nowhere else, along with the version the pin was
placed on. Both are printed now, for build and test comments too.

It also listed settled threads alongside open ones. `media feedback` used to send
`resolved=false` by default and the endpoint that replaced it takes no filter, so
the default moved into the CLI: open threads only, replies to a resolved thread
included in what is dropped, and `--all` to see the rest. An agent asked to act on
a review was otherwise being handed work somebody had already done.

Two smaller ones: `Version: 3 of 2` was reachable, because `version` counts every
version created while `versionCount` counts only the ones whose bytes landed — it
reads `Version: 3 (2 uploaded)` now. And `media update --name ""` was PATCHed
verbatim for the server to reject; an empty value means "clear this" everywhere
else in that command, so it is refused locally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gregberge
gregberge requested a review from jsfez August 10, 2026 11:33
@gregberge
gregberge marked this pull request as ready for review August 10, 2026 11:35
`packages/cli/e2e` uploads the whole of the repo's `__fixtures__` to Argos as a
visual test, so the animated PNG added for the compression test arrived as a
snapshot — a 2x2 image nobody has any reason to review. It lives in the core
package now, next to the only test that reads it.

The next e2e build reports `media/animated.png` as removed; approving that once
settles it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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