Conversation
- Introduced new inputs for artifact distribution: `distribute` and `distribute_type`. - Implemented logic to parse distribution input and distribute artifacts via the Fly API. - Added output for distribution results and updated job summary to include distributed artifacts. - Bumped version to 1.4.2.
- Added tests to verify rendering of distributed artifacts in job summary. - Implemented logic to handle both populated and empty distributed results. - Included warning for malformed JSON in distributed results. - Introduced `buildDistributedTable` function to format distributed artifacts for display.
- Added `distribute` sub-action to handle generic artifacts distribution. - Updated action inputs and constants to reflect new distribution logic. - Enhanced job summary to read distributed results from environment variables. - Refactored tests to accommodate changes in distributed artifacts handling.
Integrate buildArtifactsTable, formatSize, and template simplification from #43 alongside the distribute feature additions. Made-with: Cursor
There was a problem hiding this comment.
🤖 Review comment by Cursor AI
Folks, I reviewed this PR as part of the full distribution feature — together with fly-service #1009 and fly-desktop #189. Three repos, one big-league review.
Code Review Findings
✅ All findings have been resolved.
- BLOCKER (contract mismatch): fly-service was updated (commit ce808098) to include
public_urlanddownload_urlin the distribute response. Contract now matches.
sverdlov93
left a comment
There was a problem hiding this comment.
🤖 Review comment by Cursor AI (Round 2)
Folks, I compared this PR with PR #35 (upload/download) — the gold standard for sub-actions in this repo. And the pattern is COMPLETELY different. Not even close!
🟠 Architecture Mismatch: Direct HTTP API calls instead of Fly CLI
Upload/download pattern (PR #35):
action.yml → upload.ts → transfer.ts → execFlyCLI() → fly CLI binary → Artifactory
- Sub-action calls the fly CLI binary via
execFlyCLI() - CLI handles auth, retries, checksum verification, structured JSON output
transfer.tsis a shared orchestrator — DRY across upload and download- Input is simple:
name,version,files(multiline, one per line) - Output follows
FlyClientResponse→results: [{name, status, message}] - Results accumulate via
ENV_FLY_TRANSFER_RESULTSas JSON lines
Distribute pattern (PR #42):
action.yml → distribute-action.ts → distribute.ts → createHttpClient().post() → fly-service REST API
- Sub-action makes direct HTTP calls to the fly-service backend
- Bypasses the fly CLI entirely — duplicates auth header construction, error handling, response parsing
- Introduces its own input format (
"name:version"comma-separated) instead of declarativename/versioninputs - Returns a different response shape (
DistributeResponsevsFlyClientResponse) - Accumulates results via its own env var pattern (
ENV_FLY_DISTRIBUTE_RESULTS)
What should change
- Add
fly distributecommand to fly-client — the CLI should own the API call, just likefly uploadandfly downloadown theirs - Use
execFlyCLI()in the action — callfly distribute --name my-app --version 1.0.0instead ofhttpClient.post() - Use
transfer.tsor a similar shared orchestrator — follow therunTransfer()pattern - Align input format — use
nameandversionas separate inputs (like upload/download), not comma-separated"name:version"pairs - Align output format — return
FlyClientResponsewithresults: [{name, status, message}], not a customDistributeResponse
This keeps all sub-actions consistent: same CLI, same auth flow, same error handling, same output contract.
sverdlov93
left a comment
There was a problem hiding this comment.
🤖 Review comment by Cursor AI (Round 2 — corrected)
Updated after verifying the full pattern across both repos. The core issue: no fly distribute CLI command exists yet, and both the desktop MCP tool (#189) and this action make direct HTTP calls instead of going through the CLI like upload/download do.
Pattern comparison
fly-action:
| Upload/Download (PR #35) | Distribute (this PR) | |
|---|---|---|
| Calls | execFlyCLI() → fly CLI binary |
createHttpClient().post() → direct HTTP |
| Input | Separate name, version, files inputs |
Comma-separated "name:version" pairs |
| Response | FlyClientResponse → {results: [{name, status, message}]} |
Custom DistributeResponse → {public_url, download_url} |
| Auth | CLI via env vars | Manual Authorization header + X-JFROG-FLY-TENANT-HOST |
| Results accumulation | ENV_FLY_TRANSFER_RESULTS JSON lines |
ENV_FLY_DISTRIBUTE_RESULTS (separate mechanism) |
fly-desktop:
| Upload/Download (existing) | Distribute (#189) | |
|---|---|---|
| Calls | runFlyClientCommand() → fly CLI binary |
fetchWithRetry() → direct HTTP |
| Response | FlyClientResponse |
Custom DistributeResponse |
Recommended path
- Add
fly distributecommand tofly-client(client/cmd/distribute.go) — the CLI command doesn't exist yet and is the prerequisite for both PRs - Action calls
execFlyCLI(["distribute", "--name", name, "--version", version])— matches upload/download - Desktop MCP tool calls
runFlyClientCommand("distribute --name ...")— matches upload/download - Input format — separate
nameandversioninputs, not comma-separated - Output —
FlyClientResponsewithresults[], not custom types
Terminology
"Distribute" collides with Artifactory's existing "Distribution" feature (edge node replication). Consider aligning on a better term with the team — e.g., "publish", "share", "make-public".
- Simplified output logging in `runDistribute` by removing unnecessary line breaks. - Removed deprecated `setOutput` function and related constants from the codebase. - Updated job summary to read distributed results from environment variables instead of state. - Cleaned up test files for consistency and readability.
- package.json: include both distribute-action and go-publish in build - release.yml: add distribute/action.yml to node20 GHES compat tag Made-with: Cursor
Addresses PR #42 review comment on src/distribute-action.ts:50 — verifies appendDistributeResults accumulates across invocations and createJobSummary parser handles the accumulated newline-separated JSON arrays. Made-with: Cursor
sverdlov93
left a comment
There was a problem hiding this comment.
🤖 Review comment by Cursor AI (Round 3)
Folks, I'm back. Third round. And I went cross-repo — traced the contracts through fly-service #1009, fly-desktop #189, and this action. The alignment is solid, believe me. But I found something new. Very important.
✅ Resolved from Round 1 + 2
| # | Finding | Status |
|---|---|---|
| R1-1 | Response contract mismatch | ✅ Resolved — fly-service updated |
| R1-2 | Sequential distribution | Acknowledged — Phase 1 |
| R2-1 | Direct HTTP instead of CLI | Deferred — Phase 2 |
| R2-2 | Input format inconsistency | Intentional design |
| R2-3 | Custom response type | Follows from Phase 1 |
| R2-4 | Missing multi-step accumulation test | ✅ Fixed in dfbe047 |
New Findings (Round 3)
| # | Severity | File | Finding |
|---|---|---|---|
| 1 | 🟠 | distribute-action.ts |
Multi-artifact partial failure: successful results lost |
| 2 | 🟡 | types.ts |
DistributeRequest type exported but unused |
| 3 | 🔵 | .prettierrc |
Unrelated formatting config added in feature PR |
Cross-Repo Alignment ✅
Verified against fly-service handler (http_handler_public_distribution.go) and fly-desktop MCP tool (#189):
- Request body: All three repos send
package_name,package_version,package_type✅ - Response body:
public_url,download_url,download_count— aligned ✅ - Endpoint: All use
POST /fly/api/v1/artifacts/distribute✅ - URL construction: fly-service uses
GetPublicHostFromRequestwhich checksX-JFROG-FLY-TENANT-HOST→X-Forwarded-Host→Host. This action sends it explicitly ✅
Round 3 tally: 🟠 1 | 🟡 1 | 🔵 1 | ✅ 6 resolved | Cross-repo: clean
sverdlov93
left a comment
There was a problem hiding this comment.
🤖 Review comment by Cursor AI (Round 3 — Neighbor Pattern Validation)
I compared distribute against its closest neighbors in this repo — upload, download, and go-publish — checking naming, file structure, input/output shapes, error handling, and flow.
Comparison Table
| Dimension | upload | download | go-publish | distribute |
|---|---|---|---|---|
| Entry point | upload.ts |
download.ts |
go-publish.ts |
distribute-action.ts |
| Logic module | transfer.ts (shared) |
transfer.ts (shared) |
inline | distribute.ts |
| Compiled file | lib/upload.js |
lib/download.js |
lib/go-publish.js |
lib/distribute-action.js |
| Input style | Separate: name, version, files |
Separate: name, version, files |
Separate: path, version |
Packed: artifacts ("name:version, ...") |
| Output shape | FlyClientResult[] |
FlyClientResult[] |
FlyClientResult[] |
DistributeResponse[] |
| Output key | results |
results |
results |
results |
| Execution | execFlyCLI() |
execFlyCLI() |
execFlyCLI() |
httpClient.post() |
| Error strategy | Per-file {status: "error"}, continues |
Per-file {status: "error"}, continues |
Per-file {status: "error"}, continues |
Throws on first failure |
| Results env var | FLY_TRANSFER_RESULTS |
FLY_TRANSFER_RESULTS |
FLY_TRANSFER_RESULTS |
FLY_DISTRIBUTE_RESULTS |
require.main guard |
✅ | ✅ | ✅ | ✅ |
core.setSecret(token) |
✅ (via transfer.ts) |
✅ (via transfer.ts) |
✅ | ✅ |
| Auth | getAuthEnv() |
getAuthEnv() |
getAuthEnv() |
getAuthEnv() ✅ |
Findings
🟡 N1 — Entry point file naming breaks neighbor convention
All sub-actions use <name>.ts as the entry point: upload.ts, download.ts, go-publish.ts. Distribute uses distribute-action.ts and takes the plain name distribute.ts for the logic module.
The neighbor pattern would be: rename entry → distribute.ts, logic → distribute-core.ts or distribute-api.ts. This also fixes the compiled output (lib/distribute.js instead of lib/distribute-action.js), matching lib/upload.js, lib/download.js, lib/go-publish.js.
🟡 N2 — Output shape diverges from all neighbors
The results output is DistributeResponse[] ({package_name, package_version, public_url, download_url, download_count}) while every other sub-action returns FlyClientResult[] ({name, status, message}). Users consuming results from different sub-actions will need different parsing logic for distribute.
When fly distribute CLI command is added (Phase 2), this will naturally align to FlyClientResult[] — but document this divergence in the distribute/action.yml outputs description so users aren't surprised.
🟡 N3 — Error handling doesn't collect partial results like neighbors
transfer.ts (upload/download) and go-publish.ts always output results — even when some items fail. They report per-item {status: "error", message: "..."} alongside successes. distribute throws on first failure and discards all prior successes. (This reinforces the Round 3 inline finding on distribute-action.ts:37.)
| Scenario: 3 items, 2nd fails | upload/download | distribute |
|---|---|---|
Output results |
[{status: "success"}, {status: "error"}, ...] |
❌ empty |
| Job summary | Shows 1 success + 1 failure | Shows nothing |
core.setFailed |
Yes, with per-file details | Yes, with single error |
| Server-side state | 1 file uploaded | 1 artifact distributed but action doesn't know |
🔵 N4 — Description style differs from neighbors
Neighbors use <Verb> <object> to/from Fly. pattern:
- "Upload files to Fly generic storage."
- "Download files from Fly generic storage."
- "Publish a Go module to Fly."
Distribute uses: "Make generic artifact versions publicly downloadable." — different sentence structure, doesn't mention "Fly" in the verb phrase.
Suggested: "Distribute generic artifacts publicly via Fly."
What matches well ✅
- Auth flow via
getAuthEnv()— identical core.setSecret(token)— presentrequire.mainguard — present- Results accumulation via env var + JSON lines — pattern matches (different var name is fine)
- Test structure
<source>.spec.ts— consistent - Copyright header and author field — consistent
Neighbor pattern tally: 🟡 3 (N1 naming, N2 output shape, N3 error handling) | 🔵 1 (N4 description style) | ✅ 6 patterns match
|
please update pr desc |
* Partial-failure bug: distributeArtifacts now collects per-entry successes/failures instead of throwing on the first non-2xx. runDistribute always emits `results` output and FLY_DISTRIBUTE_RESULTS before calling setFailed, so workflows can see which artifacts were actually made public even when some entries fail. * DistributeRequest type is now applied to the inline POST payload in distribute-core.ts (previously exported-but-unused). * Rename distribute-action.ts -> distribute.ts and distribute.ts -> distribute-core.ts to match the upload/download/go-publish neighbor convention (entry point file matches sub-action name; compiled output is lib/distribute.js). * Expanded `results` description in distribute/action.yml to call out the schema divergence from upload/download (DistributeResponse[] vs FlyClientResult[]) so workflow authors aren't surprised. Tests: added distribute-core partial-failure coverage (preserves earlier successes when a later entry fails) and distribute.spec partial-success assertion (output emitted before setFailed). All 271 tests pass. Made-with: Cursor

Overview
Adds a new
jfrog/fly-action/distributesub-action that makes generic artifact versions publicly downloadable directly from a GitHub Actions workflow — no extra scripts, no manual API calls.It's a separate composite entry point (like
upload,download,go-publish), used after the mainjfrog/fly-actionsets up OIDC auth.How it works
Given one or more
name:versionpairs, the sub-action callsPOST /fly/api/v1/artifacts/distributeon the Fly backend for each entry and collects per-entry results. On partial failure, the successful entries are still emitted on theresultsoutput and inFLY_DISTRIBUTE_RESULTS(for the job summary) before the step fails — so downstream steps and the post-job summary always see the artifacts that actually became public on the server.The job summary template is extended with a Distributed Artifacts table that renders
public_url/download_urllinks.Usage
Inputs
artifactsname:versionpairstypegeneric)Outputs
results[{ package_name, package_version, package_type, public_url, download_url, download_count }]. On partial failure this still contains successful entries — inspect the step's exit status to detect failures. Schema intentionally differs from upload/download (FlyClientResult[]).Flow
flowchart LR Setup["jfrog/fly-action (OIDC + env setup)"] --> Sub["jfrog/fly-action/distribute"] Sub --> Parse["Parse name:version pairs"] Parse --> Loop["POST /fly/api/v1/artifacts/distribute (per entry, per-entry try/catch)"] Loop --> Emit["Always setOutput(results) + FLY_DISTRIBUTE_RESULTS"] Emit --> Decide{"Any failures?"} Decide -- no --> Done["Done"] Decide -- yes --> Fail["core.setFailed with failed list"] Fail --> DoneChanges
distribute/action.yml— new composite sub-action (artifacts,typeinputs;resultsoutput)src/distribute.ts— action entry point (runDistribute,appendDistributeResults). Always emits output beforesetFailed.src/distribute-core.ts— input parser + HTTP client (parseDistributeInput,distributeArtifacts). Returns{ successes, failures }— per-entry try/catch, never discards earlier successes.src/distribute.spec.ts/src/distribute-core.spec.ts— unit tests including partial-failure preservation and multi-step accumulation ofFLY_DISTRIBUTE_RESULTSsrc/job-summary.ts—buildDistributedTable()+ template placeholder, multi-line JSON accumulationsrc/types.ts—DistributeEntry,DistributeRequest,DistributeResponseinterfacessrc/constants.ts— input/output/env constants for distributetemplates/job-summary.md—{{DISTRIBUTED_TABLE}}placeholderpackage.json/.github/workflows/release.yml— build + node20 compat-tag wiring fordistribute/action.ymllib/— bundled JS output (lib/distribute.js)Cross-repo contract
Verified against fly-service (
http_handler_public_distribution.go) and fly-desktop (#189):POST /fly/api/v1/artifacts/distribute{ package_name, package_version, package_type }{ package_name, package_version, package_type, public_url, download_url, download_count, files[] }X-JFROG-FLY-TENANT-HOST(derived fromFLY_URL)GetPublicHostFromRequestX-JFrog-Fly-Tenant-Id)Made with Cursor