feat(actions): add artifact list + download subcommands (DEV-410) #71

Merged
nrupard merged 2 commits from feat/dev-410-actions-artifacts into main 2026-07-09 18:31:08 +02:00
Member

Implements DEV-410.

What

Adds artifact access to fj actions, so a failing Playwright e2e trace/report can be pulled and opened locally without the web UI (the recurring blocker on the chromium SPA-login races: PMS-592/595/605, BUNYIP-294).

  • fj actions artifacts <run> - list each artifact's name, human-readable size, and status.
  • fj actions artifact download <run> [--name <artifact>] [--output <dir>] - write the artifact zip(s) to disk (default: all artifacts for the run into the current directory).

How

Like fj actions logs, artifacts are served only by Forgejo web routes, not /api/v1, so forgejo-api wraps none of them. New crates/fj-client/src/actions_artifacts.rs talks to the web routes with our own reqwest client, reusing the host URL and bearer token.

Verified against Forgejo v14 (routers/web/repo/actions/view.go, routers/web/web.go), the version dev.a8n.run runs:

  • List: GET /{owner}/{repo}/actions/runs/{run}/artifacts -> {"artifacts":[{name,size,status}]}.
  • Download: GET .../artifacts/{name} -> streams {name}.zip (application/zip).

Details:

  • List disables redirects so a sign-in redirect surfaces as a clear auth error rather than an HTML body we would fail to parse.
  • Download follows redirects (v4 artifact storage may 302), with the Authorization header marked sensitive so reqwest drops it on a cross-origin hop.
  • On-disk filenames neutralize / and \ in artifact names so a crafted name cannot escape --output.

Layers touched

  • crates/fj-client/src/actions_artifacts.rs (new): list_artifacts, download_artifact, Artifact.
  • crates/fj-client/src/lib.rs, crates/fj-core/src/actions.rs: re-exports.
  • crates/fj/src/actions.rs: Artifacts + Artifact { Download } subcommands, view_artifacts / download_artifacts, human_size, artifact_zip_filename.
  • crates/fj/localization/en-US/messages.ftl: two new messages.

Tests

just pre-commit green (fmt, clippy -D warnings, build, full test suite). New unit tests: artifacts JSON shape, web-URL construction, human_size boundaries, filename sanitization.

Verified live against dev.a8n.run: artifacts on an empty run, --json, and a bad run number all behave; download error paths (no artifacts, unknown --name) behave. A successful zip write could not be exercised live because no failed e2e run with a non-expired artifact existed in retention at implementation time (CI is fully green right now); the download path is contract-verified against Forgejo source and shares the live-verified client/auth/URL construction.

AC status

  • fj actions artifacts <run> lists artifact name + size (+ status).
  • fj actions artifact download <run> [--name] [--output] writes the zip(s) (contract-verified; see note above).
  • Both honor --host; list honors --json.
  • Documented in fj help output. Wiki (external codeberg forgejo-cli wiki) is a separate repo - follow-up to add the two commands there.
Implements DEV-410. ## What Adds artifact access to `fj actions`, so a failing Playwright e2e trace/report can be pulled and opened locally without the web UI (the recurring blocker on the chromium SPA-login races: PMS-592/595/605, BUNYIP-294). - `fj actions artifacts <run>` - list each artifact's name, human-readable size, and status. - `fj actions artifact download <run> [--name <artifact>] [--output <dir>]` - write the artifact zip(s) to disk (default: all artifacts for the run into the current directory). ## How Like `fj actions logs`, artifacts are served only by Forgejo **web** routes, not `/api/v1`, so `forgejo-api` wraps none of them. New `crates/fj-client/src/actions_artifacts.rs` talks to the web routes with our own reqwest client, reusing the host URL and bearer token. Verified against Forgejo v14 (`routers/web/repo/actions/view.go`, `routers/web/web.go`), the version dev.a8n.run runs: - List: `GET /{owner}/{repo}/actions/runs/{run}/artifacts` -> `{"artifacts":[{name,size,status}]}`. - Download: `GET .../artifacts/{name}` -> streams `{name}.zip` (`application/zip`). Details: - List disables redirects so a sign-in redirect surfaces as a clear auth error rather than an HTML body we would fail to parse. - Download follows redirects (v4 artifact storage may 302), with the `Authorization` header marked sensitive so reqwest drops it on a cross-origin hop. - On-disk filenames neutralize `/` and `\` in artifact names so a crafted name cannot escape `--output`. ## Layers touched - `crates/fj-client/src/actions_artifacts.rs` (new): `list_artifacts`, `download_artifact`, `Artifact`. - `crates/fj-client/src/lib.rs`, `crates/fj-core/src/actions.rs`: re-exports. - `crates/fj/src/actions.rs`: `Artifacts` + `Artifact { Download }` subcommands, `view_artifacts` / `download_artifacts`, `human_size`, `artifact_zip_filename`. - `crates/fj/localization/en-US/messages.ftl`: two new messages. ## Tests `just pre-commit` green (fmt, clippy `-D warnings`, build, full test suite). New unit tests: artifacts JSON shape, web-URL construction, `human_size` boundaries, filename sanitization. Verified live against dev.a8n.run: `artifacts` on an empty run, `--json`, and a bad run number all behave; download error paths (no artifacts, unknown `--name`) behave. A successful zip write could not be exercised live because no failed e2e run with a non-expired artifact existed in retention at implementation time (CI is fully green right now); the download path is contract-verified against Forgejo source and shares the live-verified client/auth/URL construction. ## AC status - [x] `fj actions artifacts <run>` lists artifact name + size (+ status). - [x] `fj actions artifact download <run> [--name] [--output]` writes the zip(s) (contract-verified; see note above). - [x] Both honor `--host`; list honors `--json`. - [x] Documented in `fj` help output. Wiki (external codeberg forgejo-cli wiki) is a separate repo - follow-up to add the two commands there.
feat(actions): add artifact list + download subcommands
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 52s
a487974e2b
`fj actions` exposed `tasks` and `logs` but no way to reach a run's artifacts, so a failing Playwright e2e `trace.zip` / report could not be pulled and opened locally without the web UI (the recurring blocker on the chromium SPA-login races). Add two subcommands mirroring the `logs` UX:

- `fj actions artifacts <run>` lists each artifact's name, human-readable size, and status.
- `fj actions artifact download <run> [--name <artifact>] [--output <dir>]` writes the artifact zip(s) to disk (default: all artifacts for the run into the current directory).

Like `logs`, artifacts are served only by Forgejo web routes, not `/api/v1`, so the fetch lives in the connection layer (`fj-client/src/actions_artifacts.rs`) using our own reqwest client that reuses the host URL and bearer token. Verified against Forgejo v14 (`routers/web/repo/actions/view.go`, `web.go`): list is `GET /{owner}/{repo}/actions/runs/{run}/artifacts` returning `{"artifacts":[{name,size,status}]}`; download is `GET .../artifacts/{name}` streaming `{name}.zip`. The list call disables redirects so a sign-in redirect surfaces as an auth error instead of an HTML body; the download call follows redirects so instances that offload artifact storage still resolve, with the token marked sensitive so reqwest drops it on a cross-origin hop. On-disk filenames neutralize path separators in artifact names so a crafted name cannot escape the output directory.

Both honor `--host` and, for the list, `--json`. Added unit tests for the artifacts JSON shape, the web URL construction, the human-size formatter, and the filename sanitization.

Verified live against dev.a8n.run: list (empty run), `--json`, and a bad run number all behave; download error paths (no artifacts, unknown `--name`) behave. A successful zip write could not be exercised live because no failed e2e run with a non-expired artifact existed in retention at implementation time; the download path is contract-verified against Forgejo source and shares the live-verified client/auth/URL construction.

#DEV-410

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
refactor(actions): address artifact-download review findings
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 23s
Create release / Create release from merged PR (pull_request) Has been skipped
219316a286
- Guard the artifact download against a followed sign-in redirect: on a private repo without a valid token the download resolves to the HTML login page with a 200, which would otherwise be written out as a .zip. Detect a text/html content-type or a /user/login final URL and surface it as an auth error instead.
- Refuse to clobber an existing output file: write with create_new like the issue-attachment downloader, erroring with a clear "already exists" message rather than silently overwriting.
- Hoist the duplicated web-route HTTP client builder and URL joiner into a shared fj-client `web` module; actions_logs, attachments, and actions_artifacts now all use `build_web_client` / `web_url` instead of three private copies.
- Document that download_artifacts writes each artifact as its fetch completes, so a mid-run failure leaves earlier artifacts on disk.
- Make the "no artifact named" lookup error lazy (ok_or_else) so the message is not formatted on the success path.

`just pre-commit` green (fmt, clippy -D warnings, build, tests). Re-verified live against dev.a8n.run that `actions logs` (which shares the refactored client), `actions artifacts`, and the download bail path all still behave.

#DEV-410

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch feat/dev-410-actions-artifacts 2026-07-09 18:31:08 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
pandoras-box/forgejo-cli!71
No description provided.