Skip to content

[RAPTOR-20140] fix(filesapi): name the File Registry entry a sync creates - #913

Open
wojtekwdr wants to merge 1 commit into
datarobot-oss:mainfrom
wojtekwdr:wojtekw/RAPTOR-20140-filesapi-name
Open

[RAPTOR-20140] fix(filesapi): name the File Registry entry a sync creates#913
wojtekwdr wants to merge 1 commit into
datarobot-oss:mainfrom
wojtekwdr:wojtekw/RAPTOR-20140-filesapi-name

Conversation

@wojtekwdr

@wojtekwdr wojtekwdr commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

RATIONALE

Code pushed to the File Registry showed up as Untitled Dataset, or as wapi-sync.zip when the change set was big enough to take the zip route, because the CLI never sent a name: one route falls back to a class constant, the other titles the entry after the file it was built from, which is our temp archive. The Registry accepts duplicate names silently, so a tenant ends up with a column of identical rows and no way to tell whose code is whose.

The Files API gained an optional name on its create routes in DataRobot#158141. This sends it.

CHANGES

Entries are named Artifact: <artifact id> now, on both upload routes: JSON on POST /files/, a multipart form field on POST /files/fromFile/, since that API binds a POST from the parsed body and drops query parameters.

The id rather than the artifact's name, because naming is create-time only: the entry keeps whatever it was created with, so a name the project is free to change would go stale in place, and artifact names are not unique anyway (up derives one from the directory). The artifact's own id rather than its repository's, since a repository is a different entity. The cost of that is the id names the artifact that first pushed the code rather than the one deploying from it later, which keeps resolving because locked artifacts are retained.

Existing entries keep their names. No rename on sync, deliberately: it would mean writing over a name someone set by hand.

Checked on staging against both routes, before and after.


Note

Low Risk
Label-only change on catalog create for code upload paths, with tests and no rename of existing entries; behavior depends on the platform accepting the new name parameter.

Overview
dr artifact code sync and dr workload up now set a meaningful File Registry title on first catalog creation instead of leaving Untitled Dataset (stage path) or wapi-sync.zip (large zip uploads).

New entries are named Artifact: <artifact-id>. The sync engine’s newCatalogName uses the artifact id (not the renamable artifact name or repo id) because Files API naming is create-only; later syncs do not rename existing rows.

The Files API client sends that name on both create paths: JSON name on CreateCatalog, and a name multipart field before the file on UploadFromZipNew (query params are ignored). Names are clamped to 255 characters via clampCatalogName / CatalogNameMaxLen. UploadFromZipNew now takes separate catalogName and zip filename arguments.

Docs (CHANGELOG, artifact.md) and tests cover JSON body, zip multipart, omission when empty, clamping, and engine stage vs zip first-sync behavior.

Reviewed by Cursor Bugbot for commit eb420ba. Configure here.

@datarobot-pr-review-router

Copy link
Copy Markdown

🎫 Jira: RAPTOR-20140 — Send name parameter when uploading files to filesAPI through CLI

@datarobot-pr-review-router

Copy link
Copy Markdown

Code Ownership

Cli Maintainers

  • CHANGELOG.md
  • docs/commands/artifact.md
  • internal/drapi/filesapi/catalogname.go
  • internal/drapi/filesapi/client.go
  • internal/drapi/filesapi/client_test.go
  • internal/drapi/filesapi/fromfile.go
  • internal/drapi/filesapi/multipart.go
  • internal/drapi/filesapi/stage.go
  • internal/drapi/filesapi/types.go

Workload Cli

  • cmd/artifact/code/checkout/cmd_test.go
  • cmd/artifact/code/versions/cmd_test.go
  • internal/workload/sync/engine.go
  • internal/workload/sync/engine_test.go
  • internal/workload/sync/upload_stage.go
  • internal/workload/sync/upload_zip.go

Review requested from the teams above. Labels will be removed automatically upon approval.

@adamalpi adamalpi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reasoning and tests are strong; four inline notes, one of which is a rollout decision worth settling before merge.

// sends a name, and one against a server predating the parameter fails on
// the unknown key rather than degrading.
type CreateCatalogReq struct {
Name string `json:"name,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[High] This makes the first sync of every project fail against a pre-#158141 Files API, for a cosmetic label

This doc states the consequence plainly — "one against a server predating the parameter fails on the unknown key rather than degrading" — and I think that decision is worth surfacing outside a code comment, because it is a new hard version coupling between the CLI and the platform on a core path.

Why it bites in practice rather than in theory:

  • omitempty does not save it. newCatalogName returns "" only when e.artifact == nil, and the engine's own comment says that never happens ("Phase 1 always fetches the artifact before Phase 5 uploads"), so the key is always sent on a real sync.
  • A rejection is fatal, not degraded: drapi.Post (post.go:71-73) turns any non-2xx into ErrFromResp. dr workload up on a fresh project dies at catalog creation with a raw API error, and the user has no way to connect that to a label they never asked for.
  • The CLI documents self-managed/on-prem instances as a first-class target (README.md:60, docs/commands/auth.md:429, the "Custom/On-Prem" option in dr auth set-url), and those clusters routinely trail cloud. I grepped for any feature detection or minimum-version check in the repo and there is none — docs/commands/start.md version-checks the CLI against a template, never the platform against the CLI.

I could not test an old server from here, so whether it 422s or quietly ignores the key is unverified — which is exactly why this is worth confirming rather than assuming. Note the asymmetry while you do: an unknown JSON key is far more likely to be rejected than an unknown multipart field, so CreateCatalog is the exposed route and the zip path probably is not.

Three placements:

  • Retry without the name on rejection, localized to CreateCatalog — every cluster keeps working, costs one wasted request on old servers, and the label falls back to Untitled Dataset, i.e. exactly today's behaviour, so nothing regresses.
  • Document a minimum platform version in the CHANGELOG and docs/commands/artifact.md — zero code, but turns a cosmetic improvement into a support constraint operators discover from release notes.
  • Ship as-is — correct only if every instance this CLI is expected to reach is already past #158141. That is a product call.

My read is that hard failure is the wrong default when the benefit is a label, but it is your call; it just should not be made only here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is the other way round, which changes the fix.

POST /files/ had no validator_class before #158141, just a bare post() that never read the body, so an unknown key there is ignored and the entry keeps the platform default. /files/fromFile/ already had FormValidationMixin and a validator without name, so that is the one that 422s. I put a junk key through both against staging and they reject unknown keys identically today, so the strictness is real; what differs is only which of them was strict back then.

That makes retry the worst option for the route that actually breaks, since re-sending means streaming the whole archive a second time. So the zip path now creates its catalog first and uploads into it, and the name only ever travels on the JSON create. There is no version coupling left to document or retry around. It costs one extra request on a first sync, and the empty-catalog-if-the-upload-fails case is what the stage path has always done, so the two paths are now one shape.

// That covers the upload's overwrite mode and the name a newly created
// catalog gets, both of which are silently lost from a query string.
//
// useArchiveContents on the fromFile routes reads like a counter-example

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Medium] This documents a live silent-failure dependency and defers the one-line fix into code the PR is already rewriting

First: this is a better answer than the one I gave on #898. "The query rule is over-general" was wrong in an interesting way — the query parameter really is dropped, and extraction survives only on a server-side default. Good dig.

But that makes the deferral the problem. By this comment's own account, extraction happens "only because the server's declared form default for that field is already true … a flip of that default would stop extraction with no error." The failure mode is a catalog holding a wapi-sync.zip blob instead of extracted source, an image built from that blob, and a zero exit code — quieter than the overwrite bug #898 just fixed.

And the fix is in code this PR is editing. UploadFromZipNew now builds a fields map at fromfile.go:39 to carry name, and UploadFromZipExisting has had one since #898, so this is fields.Set("useArchiveContents", "true") in two functions already being rewritten, next to a parameter being moved for the identical reason.

"Moving it into the form is a separate change" is the only record that it needs doing — I grepped the repo and there is no ticket, TODO, or issue reference anywhere. Either do it here, or file it and cite the id in this comment so it does not rest on a sentence in a doc block.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

RAPTOR-19915, filed before this PR. It is not in the comment because we do not put ticket ids in code comments, which is why that line reads as bare as it does.

Leaving it rather than folding it in: it is a separate defect with its own ticket, and unlike name it is safe against every server version, since use_archive_contents has been a declared form field for years. Nothing about it is blocked on this change.

Comment thread internal/workload/sync/engine.go Outdated
Comment thread internal/drapi/filesapi/fromfile.go Outdated
…ates

The Files API takes an entry's name only when the entry is created, and
the CLI was never sending one, so what the Registry showed was an
accident of which upload route ran. A small change set went through the
stage route and landed as 'Untitled Dataset', a class-constant fallback;
anything over 20 files or 50 MB went through the zip route and landed as
'wapi-sync.zip', the CLI's own temporary filename, because the platform
titles an entry after the file it was built from when nothing else names
it. Neither says whose code it holds, and the Registry accepts duplicate
names without complaint, so they stacked up as identical rows.

Both routes now send 'Artifact: <artifact id>', as JSON on the create
call and as a multipart form field on the zip one, since the Files API
reads a POST's parameters from the parsed body and drops them from the
query.

The id and not the artifact's name, because naming is create-time only:
the entry keeps whatever it was created with, so a name the project is
free to change would go stale in place, and artifact names are not
unique to begin with. The artifact's own id and not its repository's,
since a repository is a separate entity in the Workload API.

Verified on staging across both routes.
@wojtekwdr
wojtekwdr force-pushed the wojtekw/RAPTOR-20140-filesapi-name branch from eb420ba to 2b4cd56 Compare September 11, 2026 11:00
@wojtekwdr

Copy link
Copy Markdown
Contributor Author

/approve-smoke-tests

@github-actions

Copy link
Copy Markdown
Contributor

🔐 Fork PR smoke tests triggered by @wojtekwdr

⚠️ Security Notice: This will run tests with access to repository secrets.

What happens next:

  1. Security scans will run automatically (Trivy, gosec)
  2. If security scans pass, smoke tests will run
  3. Results will be posted as PR comments

⚠️ Important: Review the PR code carefully before approving!

@github-actions

Copy link
Copy Markdown
Contributor

🔐 Fork smoke tests started by maintainer

⏳ Security scans passed. Running smoke tests...

Commit: 2b4cd562a9e57aee9d8a6329fe564f00203d766d
View run

@github-actions

Copy link
Copy Markdown
Contributor

All smoke tests passed! (Fork PR)

✅ Security Scan: success
✅ Linux: success
✅ Windows: success

View run details

@wojtekwdr
wojtekwdr requested a review from adamalpi September 11, 2026 14:14

@adamalpi adamalpi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the correction on the route strictness — deleting the create-from-file route rather than retrying around it is a better fix than any of the three I offered. Two small leftovers from the restructure inline, neither blocking.

func postZip(e *Engine, body io.Reader, size int64) (*filesapi.FromFileResp, error) {
if id := resolveExistingCatalogID(e); id != "" {
return e.files.UploadFromZipExisting(id, "wapi-sync.zip", filesapi.OverwriteReplace, size, body)
catalogID, err := ensureCatalog(e)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Low] The last asymmetry: the zip path holds the catalog id and then returns the server's echo instead

Now that postZip gets catalogID from ensureCatalog, line 72 still returns resp.CatalogID, resp.CatalogVersionID, while the stage path returns its own id and takes only the version from the response (upload_stage.go:52). Returning catalogID here would make the two genuinely one shape — which is the stated goal of this change — and drop a dependency on /files/<id>/fromFile/ echoing catalogId back.

That echo now matters on a first sync, where it did not before: previously the first sync went through the create-from-file route, whose response necessarily carried the id it had just minted, so the existing route's response only ever had to be right for subsequent syncs.

I checked before raising it and this is not a live break — phase5_execute.go:282 already assigns newCatalogID from that same field on every subsequent zip sync in production, so the field is populated. It is robustness and consistency, not a regression.

One note on the test: engine_test.go:466 sets zipResp.CatalogID to "cid-zip", the same value as fake.catalogID, so both implementations pass identically. Making the two differ would pin which one the code actually uses.

// second time. The JSON route ignores what it does not recognize, so
// there the same server just leaves its own default on the entry.
//
// The cost is an extra round trip on first sync and, if the upload then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Low] The empty-catalog case is parity with the stage path, but it is new to this one

"Both are what the stage path has always done" is accurate about the stage path and worth stating — the wrinkle is that UploadFromZipNew was atomic. No catalog existed unless the upload succeeded, so the zip path is acquiring this failure mode rather than matching one it already had.

phase6_state.go:38 writes cfg.CatalogID only after phase 5 succeeds, so a failed first sync leaves an entry nothing has recorded, and the retry's resolveExistingCatalogID comes back empty and creates another.

What makes it worth a line in this PR specifically: those leftovers now carry an identical Artifact: <id> name, so repeated failures reproduce exactly the indistinguishable column of rows this change exists to remove. Previously they would at least have been the same Untitled Dataset nobody expected to be unique.

Not worth blocking on, and I would not pin the id before the upload — that trades a leak for a wrong binding, which is worse. Just that the trade reads better stated as what it is than as parity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants