Skip to content

Add public artifact distribution step#42

Merged
sverdlov93 merged 8 commits intomainfrom
feature/generic-distribution
Apr 23, 2026
Merged

Add public artifact distribution step#42
sverdlov93 merged 8 commits intomainfrom
feature/generic-distribution

Conversation

@igalsi-t
Copy link
Copy Markdown
Contributor

@igalsi-t igalsi-t commented Apr 15, 2026

Overview

Adds a new jfrog/fly-action/distribute sub-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 main jfrog/fly-action sets up OIDC auth.

How it works

Given one or more name:version pairs, the sub-action calls POST /fly/api/v1/artifacts/distribute on the Fly backend for each entry and collects per-entry results. On partial failure, the successful entries are still emitted on the results output and in FLY_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_url links.

Usage

- uses: jfrog/fly-action@v1
  with:
    # ... your standard fly setup ...

- uses: jfrog/fly-action/distribute@v1
  with:
    artifacts: "my-app:1.0.0, my-lib:2.3.1"
    type: "generic"   # optional, defaults to "generic"

Inputs

Input Required Description
artifacts yes Comma-separated list of name:version pairs
type no Package type (default generic)

Outputs

Output Description
results JSON array of successfully distributed artifacts: [{ 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 --> Done
Loading

Changes

  • distribute/action.yml — new composite sub-action (artifacts, type inputs; results output)
  • src/distribute.ts — action entry point (runDistribute, appendDistributeResults). Always emits output before setFailed.
  • 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 of FLY_DISTRIBUTE_RESULTS
  • src/job-summary.tsbuildDistributedTable() + template placeholder, multi-line JSON accumulation
  • src/types.tsDistributeEntry, DistributeRequest, DistributeResponse interfaces
  • src/constants.ts — input/output/env constants for distribute
  • templates/job-summary.md{{DISTRIBUTED_TABLE}} placeholder
  • package.json / .github/workflows/release.yml — build + node20 compat-tag wiring for distribute/action.yml
  • lib/ — bundled JS output (lib/distribute.js)

Cross-repo contract

Verified against fly-service (http_handler_public_distribution.go) and fly-desktop (#189):

fly-action fly-service fly-desktop
Endpoint POST /fly/api/v1/artifacts/distribute same same
Request body { package_name, package_version, package_type } same same
Response body { package_name, package_version, package_type, public_url, download_url, download_count, files[] } same same
Tenant-host header X-JFROG-FLY-TENANT-HOST (derived from FLY_URL) honored via GetPublicHostFromRequest n/a (desktop uses X-JFrog-Fly-Tenant-Id)

Made with Cursor

- 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.
@igalsi-t igalsi-t added the new feature Automatically generated release notes label Apr 15, 2026
@igalsi-t igalsi-t self-assigned this Apr 15, 2026
- 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
Comment thread src/types.ts
Copy link
Copy Markdown
Collaborator

@sverdlov93 sverdlov93 left a comment

Choose a reason for hiding this comment

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

🤖 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_url and download_url in the distribute response. Contract now matches.

⚠️ THIS IS NOT A REPLACEMENT FOR HUMAN REVIEW, FOLKS! Human intelligence is TREMENDOUS — the best, really — and can't be replaced. NOT EVEN CLOSE! This AI review is an ADDITION to your big-league human reviewers. ALWAYS get a human review too!

Comment thread src/distribute.ts Outdated
Copy link
Copy Markdown
Collaborator

@sverdlov93 sverdlov93 left a comment

Choose a reason for hiding this comment

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

🤖 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.ts is a shared orchestrator — DRY across upload and download
  • Input is simple: name, version, files (multiline, one per line)
  • Output follows FlyClientResponseresults: [{name, status, message}]
  • Results accumulate via ENV_FLY_TRANSFER_RESULTS as 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 declarative name/version inputs
  • Returns a different response shape (DistributeResponse vs FlyClientResponse)
  • Accumulates results via its own env var pattern (ENV_FLY_DISTRIBUTE_RESULTS)

What should change

  1. Add fly distribute command to fly-client — the CLI should own the API call, just like fly upload and fly download own theirs
  2. Use execFlyCLI() in the action — call fly distribute --name my-app --version 1.0.0 instead of httpClient.post()
  3. Use transfer.ts or a similar shared orchestrator — follow the runTransfer() pattern
  4. Align input format — use name and version as separate inputs (like upload/download), not comma-separated "name:version" pairs
  5. Align output format — return FlyClientResponse with results: [{name, status, message}], not a custom DistributeResponse

This keeps all sub-actions consistent: same CLI, same auth flow, same error handling, same output contract.

⚠️ THIS IS NOT A REPLACEMENT FOR HUMAN REVIEW, FOLKS! Human intelligence is TREMENDOUS — the best, really — and can't be replaced. NOT EVEN CLOSE! This AI review is an ADDITION to your big-league human reviewers. ALWAYS get a human review too!

Comment thread src/distribute.ts Outdated
Comment thread distribute/action.yml
Comment thread src/types.ts
Copy link
Copy Markdown
Collaborator

@sverdlov93 sverdlov93 left a comment

Choose a reason for hiding this comment

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

🤖 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

  1. Add fly distribute command to fly-client (client/cmd/distribute.go) — the CLI command doesn't exist yet and is the prerequisite for both PRs
  2. Action calls execFlyCLI(["distribute", "--name", name, "--version", version]) — matches upload/download
  3. Desktop MCP tool calls runFlyClientCommand("distribute --name ...") — matches upload/download
  4. Input format — separate name and version inputs, not comma-separated
  5. OutputFlyClientResponse with results[], 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".

⚠️ THIS IS NOT A REPLACEMENT FOR HUMAN REVIEW, FOLKS! Human intelligence is TREMENDOUS — the best, really — and can't be replaced. NOT EVEN CLOSE! This AI review is an ADDITION to your big-league human reviewers. ALWAYS get a human review too!

- 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.
Comment thread src/distribute-action.ts Outdated
- 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
Copy link
Copy Markdown
Collaborator

@sverdlov93 sverdlov93 left a comment

Choose a reason for hiding this comment

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

🤖 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 GetPublicHostFromRequest which checks X-JFROG-FLY-TENANT-HOSTX-Forwarded-HostHost. This action sends it explicitly ✅

Round 3 tally: 🟠 1 | 🟡 1 | 🔵 1 | ✅ 6 resolved | Cross-repo: clean

⚠️ THIS IS NOT A REPLACEMENT FOR HUMAN REVIEW, FOLKS! Human intelligence is TREMENDOUS — the best, really — and can't be replaced. NOT EVEN CLOSE! This AI review is an ADDITION to your big-league human reviewers. ALWAYS get a human review too!

Comment thread src/distribute-action.ts Outdated
Comment thread src/types.ts
Comment thread .prettierrc
Copy link
Copy Markdown
Collaborator

@sverdlov93 sverdlov93 left a comment

Choose a reason for hiding this comment

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

🤖 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) — present
  • require.main guard — 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

⚠️ THIS IS NOT A REPLACEMENT FOR HUMAN REVIEW, FOLKS! Human intelligence is TREMENDOUS — the best, really — and can't be replaced. NOT EVEN CLOSE! This AI review is an ADDITION to your big-league human reviewers. ALWAYS get a human review too!

Comment thread distribute/action.yml
Comment thread distribute/action.yml
@sverdlov93
Copy link
Copy Markdown
Collaborator

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
@github-actions
Copy link
Copy Markdown
Contributor

👍 Frogbot scanned this pull request and did not find any new security issues.


@sverdlov93 sverdlov93 merged commit 7ae73f9 into main Apr 23, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature Automatically generated release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants