Skip to content

Rteco 1539 cargo implementation phase 1 - #510

Open
reshmifrog wants to merge 20 commits into
mainfrom
RTECO-1539-cargo-implementation-phase-1
Open

Rteco 1539 cargo implementation phase 1#510
reshmifrog wants to merge 20 commits into
mainfrom
RTECO-1539-cargo-implementation-phase-1

Conversation

@reshmifrog

@reshmifrog reshmifrog commented Jul 17, 2026

Copy link
Copy Markdown
Contributor
  • All tests passed. If this feature is not already covered by the tests, I added new tests.
  • All static analysis checks passed.
  • Appropriate label is added to auto generate release notes.
  • I used gofmt for formatting the code before submitting the pull request.
  • PR description is clear and concise, and it includes the proposed solution/fix.

Summary by CodeRabbit

  • New Features
    • Added Cargo package-manager support for dependency resolution, publishing, and build-info collection.
    • Added native Artifactory registry setup, including sparse indexes, crates.io replacement, deployment repositories, and credential handling.
    • Added support for token, basic, and anonymous authentication.
    • Added Cargo artifact discovery, repository mapping, checksum enrichment, and build artifact metadata.
    • Added workspace, dry-run, package selection, registry configuration, and module override support.

reshmifrog and others added 16 commits June 30, 2026 23:36
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…h env

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… fails

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… in build-info

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- close a non-nil response body when Aql returns a transport error (M1)
- apply partial page results before surfacing a mid-batch error, since
  enrichment is best-effort (M3)
- recompute the still-missing count from the build-info so it reflects
  unique crates and can never go negative across modules (M2)
- add tests for the transport-error and partial-apply-on-error paths (M4)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CLI layer (getCommandName) splits the sub-command out of the args into
commandName, but runNativeCargo forwarded only the remaining args, so
'jf cargo build' invoked bare 'cargo' with no sub-command. Prepend the
sub-command via cargoInvocationArgs before exec. Verified: 'jf cargo build'
now runs 'cargo build'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@reshmifrog
reshmifrog requested review from a team, agrasth, bhanurp, fluxxBot and itsmeleela July 17, 2026 04:22
@reshmifrog reshmifrog added the new feature Automatically generated release notes label Jul 17, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds native Cargo support for Artifactory repository setup, authenticated command execution, Cargo metadata collection, crate artifact mapping, build-info publication, and checksum enrichment.

Changes

Cargo integration

Layer / File(s) Summary
Registry setup and repository selection
artifactory/commands/cargo/setup.go, artifactory/commands/setup/setup.go, artifactory/commands/cargo/setup_test.go, artifactory/commands/setup/setup_test.go
Cargo setup selects resolution and deployment repositories, writes sparse registry configuration and credentials, preserves TOML settings, and supports token, basic, and anonymous authentication.
Command execution and authentication
artifactory/commands/cargo/command.go, artifactory/commands/cargo/exec.go, artifactory/commands/cargo/login.go, artifactory/commands/cargo/*_test.go
Cargo commands restore subcommands, run natively with child-scoped environment variables, classify build-info operations, and inject matching registry credentials.
Checksum enrichment
artifactory/commands/cargo/checksums.go, artifactory/commands/cargo/checksums_test.go
Cargo dependencies and artifacts receive missing checksums from paginated Artifactory AQL queries. Parsing, deduplication, partial results, and no-op cases are covered.
Build-info collection and publication
artifactory/commands/cargo/publish.go, artifactory/commands/cargo/artifacts.go, artifactory/commands/cargo/repo.go, artifactory/commands/cargo/*_test.go, go.mod
Cargo metadata and packaged crates are converted into build-info records. The flow resolves repositories, routes modules, applies properties, handles dry runs and overrides, saves build-info, and uses local development module replacements.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested reviewers: itsmeleela, naveenku-jfrog, bhanurp

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Description check ✅ Passed The PR summary clearly describes the Cargo support added and matches the reported changes.
Title check ✅ Passed The title identifies the Cargo implementation and phase, which matches the main change in the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch RTECO-1539-cargo-implementation-phase-1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (14)
artifactory/commands/cargo/repo_test.go (1)

36-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a case for the GetRepository error path.

resolveDeploymentRepo returns the input repo unchanged when GetRepository fails. fakeRepoGetter already supports err, but no test sets it.

🧪 Proposed test case
 	// virtual without default -> ""
 	if got := resolveDeploymentRepo("virt", fakeRepoGetter{rclass: "virtual"}); got != "" {
 		t.Errorf("virtual no-default: got %q, want empty", got)
 	}
+	// lookup failure -> repo used as-is
+	if got := resolveDeploymentRepo("unknown", fakeRepoGetter{err: errors.New("404")}); got != "unknown" {
+		t.Errorf("lookup error: got %q, want unknown", got)
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/repo_test.go` around lines 36 - 49, The
TestResolveDeploymentRepo test should cover the GetRepository failure path by
configuring fakeRepoGetter with a non-nil err and asserting
resolveDeploymentRepo returns the original repository name unchanged.
artifactory/commands/cargo/publish.go (3)

293-316: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Create the services manager once and reuse it.

targetRepo, setBuildProperties, and enrichChecksumsAndFetch each call artutils.CreateServiceManager. A single publish creates up to three managers, each with its own HTTP client and connection pool. Cache one instance on CargoCommand and reuse it.

♻️ Proposed helper
// serviceManager returns a lazily created, cached services manager.
func (c *CargoCommand) serviceManager() (artifactory.ArtifactoryServicesManager, error) {
	if c.servicesManager != nil {
		return c.servicesManager, nil
	}
	sm, err := artutils.CreateServiceManager(c.serverDetails, -1, 0, false)
	if err != nil {
		return nil, err
	}
	c.servicesManager = sm
	return sm, nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/publish.go` around lines 293 - 316, Cache a single
services manager on CargoCommand and reuse it across targetRepo,
setBuildProperties, and enrichChecksumsAndFetch. Add a lazy serviceManager
helper that returns the cached instance or creates and stores one, then replace
each direct artutils.CreateServiceManager call with this helper while preserving
existing error handling.

442-450: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the loop variable that shadows the path package.

The file imports path and uses it in dirOf. The loop variable path shadows that import inside this block. Rename it to reqPath for clarity and to satisfy shadow linters.

♻️ Proposed change
 	for di := range bi.Modules[idx].Dependencies {
-		for _, path := range bi.Modules[idx].Dependencies[di].RequestedBy {
-			for ei := range path {
-				if path[ei] == oldId {
-					path[ei] = moduleName
+		for _, reqPath := range bi.Modules[idx].Dependencies[di].RequestedBy {
+			for ei := range reqPath {
+				if reqPath[ei] == oldId {
+					reqPath[ei] = moduleName
 				}
 			}
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/publish.go` around lines 442 - 450, Rename the
inner loop variable in the dependency update block to reqPath, and update its
indexed accesses accordingly, so the imported path package remains unshadowed
for uses such as dirOf.

41-59: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Drop a value flag that has no value.

When a value flag is the last argument, the function emits the flag alone. cargo metadata --features then fails with a missing-value error, which aborts build-info collection. Omit the flag instead.

♻️ Proposed change
 		case metadataValueFlags[a]:
-			out = append(out, a)
 			if i+1 < len(args) {
-				out = append(out, args[i+1])
+				out = append(out, a, args[i+1])
 				i++
 			}

Update the matching expectation in artifactory/commands/cargo/publish_test.go Lines 220-224.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/publish.go` around lines 41 - 59, Update
metadataFlagsFromArgs so metadata value flags are appended only when a following
argument exists; when such a flag is the final argument, omit it entirely.
Adjust the corresponding expectation in the metadata flag test in
publish_test.go to reflect the omitted flag.
artifactory/commands/cargo/artifacts_test.go (1)

9-20: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a case for a crate filename without a version.

crateRepoPath has a distinct branch when no hyphen is followed by a digit. That branch returns an empty version and uses the whole base as the name. No test covers it.

🧪 Proposed test case
 	if path != "crates/my-crate/my-crate-0.2.0.crate" || name != "my-crate" || version != "0.2.0" {
 		t.Errorf("hyphenated: got (%q,%q,%q)", path, name, version)
 	}
+	// no version token
+	path, name, version = crateRepoPath("mycrate.crate")
+	if path != "crates/mycrate/mycrate.crate" || name != "mycrate" || version != "" {
+		t.Errorf("no version: got (%q,%q,%q)", path, name, version)
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/artifacts_test.go` around lines 9 - 20, Add a test
case in TestCrateRepoPath for a crate filename without a version, such as a base
name with no hyphen followed by a digit. Assert that crateRepoPath returns an
empty version, uses the full base name as the crate name, and constructs the
corresponding crates/<name>/<filename> path.
artifactory/commands/cargo/checksums_test.go (1)

229-316: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the extraNames path of enrichAndLookup.

The tests only exercise enrichMissingChecksums, which passes nil extras. The publish flow depends on enrichAndLookup folding the published crate name into the same AQL and returning its checksum. Add a test that passes a non-empty extraNames and asserts both the query content and the returned map entry.

🧪 Proposed test
func TestEnrichAndLookup_IncludesExtraNames(t *testing.T) {
	bi := &entities.BuildInfo{Modules: []entities.Module{
		{Dependencies: []entities.Dependency{{Id: "serde-1.0.197.crate"}}},
	}}
	resp := `{"results":[
		{"name":"serde-1.0.197.crate","actual_sha1":"a","sha256":"b","actual_md5":"c"},
		{"name":"mycrate-0.1.0.crate","actual_sha1":"d","sha256":"e","actual_md5":"f"}]}`
	fake := &fakeAql{responses: []string{resp}}

	byName, err := enrichAndLookup(bi, "cargo-local", fake, []string{"mycrate-0.1.0.crate"})
	require.NoError(t, err)
	require.Len(t, fake.queries, 1)
	assert.Contains(t, fake.queries[0], "mycrate-0.1.0.crate")
	assert.Equal(t, "e", byName["mycrate-0.1.0.crate"].Sha256)
	assert.Equal(t, "b", bi.Modules[0].Dependencies[0].Sha256)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/checksums_test.go` around lines 229 - 316, Add a
test for enrichAndLookup that supplies a non-empty extraNames list, verifies the
extra crate name is included in the generated AQL query, and confirms its
checksum is returned in the lookup map. Also assert that the dependency checksum
from the same response is applied to BuildInfo, matching the existing fakeAql
response pattern.
artifactory/commands/cargo/publish_test.go (1)

51-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for repeated -p selectors.

packageNamesFromArgs exists specifically to collect several -p values, and newCollector passes the full list to the collector. Only the singular packageNameFromArgs is tested here.

🧪 Proposed test
func TestPackageNamesFromArgs(t *testing.T) {
	got := packageNamesFromArgs([]string{"build", "-p", "a", "--package=b", "-p=c"})
	want := []string{"a", "b", "c"}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("got %v, want %v", got, want)
	}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/publish_test.go` around lines 51 - 67, Add a
TestPackageNamesFromArgs test covering multiple package selectors in one
argument list, including separated and equals forms such as -p, --package=b, and
-p=c. Call packageNamesFromArgs and assert the collected names preserve input
order and match []string{"a", "b", "c"}, using the appropriate deep-equality
assertion.
artifactory/commands/cargo/checksums.go (1)

154-163: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Internal review discussion is left in shipped code comments. Both comments name a reviewer and describe the history of the change instead of the current behavior. Rewrite them to describe what the code does.

  • artifactory/commands/cargo/checksums.go#L154-L163: remove the sentence about the previous two-AQL implementation and the reviewer name; keep the description of the single batched query and its return value.
  • artifactory/commands/cargo/publish.go#L141-L148: remove "(Naveen: avoid the extra AQL query)" and keep the explanation that the published-crate name joins the same AQL batch.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/checksums.go` around lines 154 - 163, Rewrite the
comments for enrichAndLookup in artifactory/commands/cargo/checksums.go:154-163
to describe only the single batched AQL behavior and returned checksum map,
removing implementation history and the reviewer reference. In
artifactory/commands/cargo/publish.go:141-148, remove the “Naveen: avoid the
extra AQL query” text while retaining the explanation that the published-crate
name is included in the same AQL batch.
artifactory/commands/cargo/setup.go (1)

113-116: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

global-credential-providers overwrites any user list.

setNested replaces the whole value. If a user configured global-credential-providers = ["cargo:libsecret", "cargo:token"], this run reduces it to ["cargo:token"] and disables their keyring provider for all registries.

Merge instead: keep the existing entries and append cargo:token only when it is absent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/setup.go` around lines 113 - 116, Update the cargo
configuration setup around setNested’s registry global-credential-providers
assignment to preserve existing user-configured providers, append cargo:token
only if absent, and avoid overwriting the list. Keep the resulting configuration
compatible with the existing setNested flow and retain all other registry
settings unchanged.
artifactory/commands/cargo/setup_test.go (1)

97-120: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the stale deploy-registry cleanup.

ConfigureNativeRegistry deletes [registries.jfrog-local] from both files when deployRepo is empty. No test exercises that path. Add a case that runs with a deploy repo and then re-runs without one, and assert that jfrog-local is absent from config.toml and credentials.toml.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/setup_test.go` around lines 97 - 120, Extend
TestConfigureNativeRegistry_WithDeployRepo to re-run ConfigureNativeRegistry
with an empty deployRepo after the initial deploy-repository setup, then decode
both config.toml and credentials.toml and assert that jfrogDeployRegistryName is
absent from their registries maps while the existing resolution registry remains
covered.
artifactory/commands/cargo/login_test.go (1)

180-195: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for resolveAuthEnv.

The helpers are tested individually, but resolveAuthEnv holds the branch logic: config-discovered matches, the --registry fallback, and the conditional CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS entry. Add cases for a matching registry, a non-matching registry with --registry supplied, and an existing provider override in the environment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/login_test.go` around lines 180 - 195, Extend
TestBuildAuthEnv or the relevant test suite with coverage for resolveAuthEnv,
exercising a config-discovered registry match, a non-matching configured
registry when --registry is supplied as fallback, and an existing
CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS environment override. Assert each
result includes the expected resolved authentication environment and preserves
the conditional provider entry.
artifactory/commands/setup/setup.go (2)

209-213: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Success message omits the deploy repository.

For Cargo the run can configure two repositories. The message reports only sc.repoName, so the user does not learn which local repository was configured for publishing. Include sc.deployRepoName when it is not empty.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/setup/setup.go` around lines 209 - 213, Update the
success message construction around sc.repoName and sc.deployRepoName so Cargo
configurations report both repositories: retain the existing repository text and
append the deploy repository when sc.deployRepoName is non-empty. Preserve the
current output for configurations without a deploy repository and for Docker or
Podman.

217-221: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid matching SelectRepositoryInteractively errors by message text.

SelectRepositoryInteractively returns len(filteredRepos) == 0 as the message no repositories were found that match the following criteria: ...; changing that wording makes the Cargo fallback path fall through as an unrecoverable setup error. Query the repository list (or use an exported sentinel/typed error from jfrog-cli-core) and branch on zero matches instead of on strings.Contains(err.Error(), ...).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/setup/setup.go` around lines 217 - 221, Replace the
message-based no-repository detection around the Cargo fallback with a
structured zero-match check: query or reuse the filtered repository list, or use
an exported sentinel/typed error from SelectRepositoryInteractively, and branch
when no repositories match. Remove reliance on
noMatchingRepositoriesErrSubstring and strings.Contains while preserving the
manual repository-name prompt for zero matches and existing error handling for
other failures.
artifactory/commands/cargo/exec_test.go (1)

11-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that stdin is wired through.

The GetCmd doc states that cmd.Stdin = os.Stdin fixes interactive cargo login. No test covers it, so a future change can drop the line without failing the suite. Add if cmd.Stdin != os.Stdin { ... }.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/exec_test.go` around lines 11 - 52, Extend
TestCargoRunConfigGetCmd to assert that GetCmd wires standard input through by
checking cmd.Stdin equals os.Stdin, alongside the existing command configuration
assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@artifactory/commands/cargo/command.go`:
- Around line 110-116: Update runNativeCargo so its log.Debug call does not
expose secret values contained in cargo arguments, including tokens passed to
cargo login and cargo owner --token; redact those known secret-bearing arguments
before logging while preserving the actual unredacted args passed to runCmd.
- Around line 17-25: Update applyEnv to avoid leaving credential values in the
parent process environment: have it track each variable’s prior state and return
a restoration closure that uses os.Unsetenv or os.Setenv as appropriate, then
defer that cleanup immediately after the build-info collection call. Prefer
passing the auth environment explicitly if the collector API supports it, and
preserve existing handling for non-credential variables.

In `@artifactory/commands/cargo/login.go`:
- Around line 70-81: The doc comment for needsRemoteAccess must reflect that
commandBucket maps both install and build into the "deps" bucket, so remote
access is enabled for install, build, and publish. Update the comment’s command
list and related wording while leaving the function logic unchanged.
- Around line 113-130: Update registryHostMatches to require both a
case-insensitive host match and an Artifactory URL path-prefix match, using the
parsed artifactoryURL path as the base path for the index URL. Preserve
sparse+/git+ stripping and reject invalid or hostless URLs, ensuring unrelated
paths on the same host do not match.

In `@artifactory/commands/cargo/publish.go`:
- Around line 484-527: Update parseCargoRegistries to traverse from workingDir
upward through its parent directories, reading both .cargo/config.toml and
legacy .cargo/config at each level, while preserving global $CARGO_HOME
configuration as the lowest-precedence source. Apply discovered project
configurations from ancestors to the current directory so deeper entries
override parent entries, and ensure both filenames use readRegistriesInto.
- Around line 347-358: Update the property construction around the props
assignment before SetProps to use the repository’s properties builder or
escaping mechanism for build.name, build.number, build.timestamp, and
build.project values. Ensure values containing semicolons or equals signs remain
single property values, while preserving the existing optional project inclusion
and SetProps call.

In `@artifactory/commands/cargo/setup.go`:
- Around line 162-189: Update mergeTomlFile so TOML encoding completes
successfully before replacing the existing file; avoid calling os.Create on the
target before toml.NewEncoder.Encode can fail. Encode into an in-memory buffer
or same-directory temporary file, then atomically write or rename the result
into place while preserving the existing error handling and file permissions.
- Around line 138-155: Update mergeTomlFile to accept an os.FileMode parameter
and create files with os.OpenFile using the requested permissions before
writing. Pass 0600 at the credentials.toml call site in the setup flow, remove
the post-write os.Chmod handling, and update the config.toml call site to pass
0644.

In `@artifactory/commands/setup/setup.go`:
- Around line 288-299: Update the local repository selection flow around
SelectRepositoryInteractively so users can decline the optional publishing
configuration even when matching repositories exist. Add an explicit skip choice
or a preceding confirmation prompt, and leave sc.deployRepoName unset when
skipped while preserving the existing no-matching-repositories handling.

In `@go.mod`:
- Around line 206-211: Remove both LOCAL-ONLY replace directives for
build-info-go and jfrog-cli-core/v2 from go.mod, then update their require
entries to the intended released versions so builds no longer depend on
unreleased pseudo-versions.

---

Nitpick comments:
In `@artifactory/commands/cargo/artifacts_test.go`:
- Around line 9-20: Add a test case in TestCrateRepoPath for a crate filename
without a version, such as a base name with no hyphen followed by a digit.
Assert that crateRepoPath returns an empty version, uses the full base name as
the crate name, and constructs the corresponding crates/<name>/<filename> path.

In `@artifactory/commands/cargo/checksums_test.go`:
- Around line 229-316: Add a test for enrichAndLookup that supplies a non-empty
extraNames list, verifies the extra crate name is included in the generated AQL
query, and confirms its checksum is returned in the lookup map. Also assert that
the dependency checksum from the same response is applied to BuildInfo, matching
the existing fakeAql response pattern.

In `@artifactory/commands/cargo/checksums.go`:
- Around line 154-163: Rewrite the comments for enrichAndLookup in
artifactory/commands/cargo/checksums.go:154-163 to describe only the single
batched AQL behavior and returned checksum map, removing implementation history
and the reviewer reference. In artifactory/commands/cargo/publish.go:141-148,
remove the “Naveen: avoid the extra AQL query” text while retaining the
explanation that the published-crate name is included in the same AQL batch.

In `@artifactory/commands/cargo/exec_test.go`:
- Around line 11-52: Extend TestCargoRunConfigGetCmd to assert that GetCmd wires
standard input through by checking cmd.Stdin equals os.Stdin, alongside the
existing command configuration assertions.

In `@artifactory/commands/cargo/login_test.go`:
- Around line 180-195: Extend TestBuildAuthEnv or the relevant test suite with
coverage for resolveAuthEnv, exercising a config-discovered registry match, a
non-matching configured registry when --registry is supplied as fallback, and an
existing CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS environment override. Assert
each result includes the expected resolved authentication environment and
preserves the conditional provider entry.

In `@artifactory/commands/cargo/publish_test.go`:
- Around line 51-67: Add a TestPackageNamesFromArgs test covering multiple
package selectors in one argument list, including separated and equals forms
such as -p, --package=b, and -p=c. Call packageNamesFromArgs and assert the
collected names preserve input order and match []string{"a", "b", "c"}, using
the appropriate deep-equality assertion.

In `@artifactory/commands/cargo/publish.go`:
- Around line 293-316: Cache a single services manager on CargoCommand and reuse
it across targetRepo, setBuildProperties, and enrichChecksumsAndFetch. Add a
lazy serviceManager helper that returns the cached instance or creates and
stores one, then replace each direct artutils.CreateServiceManager call with
this helper while preserving existing error handling.
- Around line 442-450: Rename the inner loop variable in the dependency update
block to reqPath, and update its indexed accesses accordingly, so the imported
path package remains unshadowed for uses such as dirOf.
- Around line 41-59: Update metadataFlagsFromArgs so metadata value flags are
appended only when a following argument exists; when such a flag is the final
argument, omit it entirely. Adjust the corresponding expectation in the metadata
flag test in publish_test.go to reflect the omitted flag.

In `@artifactory/commands/cargo/repo_test.go`:
- Around line 36-49: The TestResolveDeploymentRepo test should cover the
GetRepository failure path by configuring fakeRepoGetter with a non-nil err and
asserting resolveDeploymentRepo returns the original repository name unchanged.

In `@artifactory/commands/cargo/setup_test.go`:
- Around line 97-120: Extend TestConfigureNativeRegistry_WithDeployRepo to
re-run ConfigureNativeRegistry with an empty deployRepo after the initial
deploy-repository setup, then decode both config.toml and credentials.toml and
assert that jfrogDeployRegistryName is absent from their registries maps while
the existing resolution registry remains covered.

In `@artifactory/commands/cargo/setup.go`:
- Around line 113-116: Update the cargo configuration setup around setNested’s
registry global-credential-providers assignment to preserve existing
user-configured providers, append cargo:token only if absent, and avoid
overwriting the list. Keep the resulting configuration compatible with the
existing setNested flow and retain all other registry settings unchanged.

In `@artifactory/commands/setup/setup.go`:
- Around line 209-213: Update the success message construction around
sc.repoName and sc.deployRepoName so Cargo configurations report both
repositories: retain the existing repository text and append the deploy
repository when sc.deployRepoName is non-empty. Preserve the current output for
configurations without a deploy repository and for Docker or Podman.
- Around line 217-221: Replace the message-based no-repository detection around
the Cargo fallback with a structured zero-match check: query or reuse the
filtered repository list, or use an exported sentinel/typed error from
SelectRepositoryInteractively, and branch when no repositories match. Remove
reliance on noMatchingRepositoriesErrSubstring and strings.Contains while
preserving the manual repository-name prompt for zero matches and existing error
handling for other failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3850195d-753e-4def-bc65-3efc6707dfec

📥 Commits

Reviewing files that changed from the base of the PR and between 4fd5a71 and dc4fa47.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (19)
  • artifactory/commands/cargo/artifacts.go
  • artifactory/commands/cargo/artifacts_test.go
  • artifactory/commands/cargo/checksums.go
  • artifactory/commands/cargo/checksums_test.go
  • artifactory/commands/cargo/command.go
  • artifactory/commands/cargo/command_test.go
  • artifactory/commands/cargo/exec.go
  • artifactory/commands/cargo/exec_test.go
  • artifactory/commands/cargo/login.go
  • artifactory/commands/cargo/login_test.go
  • artifactory/commands/cargo/publish.go
  • artifactory/commands/cargo/publish_test.go
  • artifactory/commands/cargo/repo.go
  • artifactory/commands/cargo/repo_test.go
  • artifactory/commands/cargo/setup.go
  • artifactory/commands/cargo/setup_test.go
  • artifactory/commands/setup/setup.go
  • artifactory/commands/setup/setup_test.go
  • go.mod

Comment on lines +17 to +25
func applyEnv(env []string) {
for _, kv := range env {
if i := strings.IndexByte(kv, '='); i > 0 {
if err := os.Setenv(kv[:i], kv[i+1:]); err != nil {
log.Debug("cargo: could not set env " + kv[:i] + ": " + err.Error())
}
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

applyEnv writes credentials into the parent process environment.

applyEnv calls os.Setenv for each entry, including CARGO_REGISTRIES_<NAME>_TOKEN. The value stays in the jf process environment after collection completes. Every later child process in the same run inherits the token, and any code that dumps the environment can expose it.

Restrict the scope: pass the auth variables to the build-info collector explicitly if its API accepts an environment, or restore the previous values with os.Unsetenv/os.Setenv in a defer right after collection.

🔒 Minimal containment
-	applyEnv(extraEnv)
+	restore := applyEnv(extraEnv)
+	defer restore()

applyEnv returns a closure that restores each key to its prior value.

Also applies to: 84-86

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 20-20: A log/format call (log.Print/Printf/Println, the Fatal/Panic variants, fmt.Sprintf, or a structured logger's Info/Warn/Error/Debug method) is given a message built by concatenating a string literal with a non-literal value such as request data. Unsanitized, attacker-controlled input written to logs enables log forging / CRLF injection: an attacker can inject newlines to spoof log entries or break log parsers. Do not concatenate raw input into the log message; pass it as a separate structured field/argument (e.g. 'log.Printf("user: %s", user)' or 'logger.Info("login", "user", user)') and strip or escape newline characters first.
Context: log.Debug("cargo: could not set env " + kv[:i] + ": " + err.Error())
Note: [CWE-117] Improper Output Neutralization for Logs.

(log-injection-request-data-concat-go)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/command.go` around lines 17 - 25, Update applyEnv
to avoid leaving credential values in the parent process environment: have it
track each variable’s prior state and return a restoration closure that uses
os.Unsetenv or os.Setenv as appropriate, then defer that cleanup immediately
after the build-info collection call. Prefer passing the auth environment
explicitly if the collector API supports it, and preserve existing handling for
non-credential variables.

Comment on lines +110 to +116
func (c *CargoCommand) runNativeCargo(extraEnv []string) error {
cargoExe := "cargo"
args := cargoInvocationArgs(c.commandName, c.args)
cfg := &CargoRunConfig{Exe: cargoExe, Args: args, Dir: c.workingDir, ExtraEnv: extraEnv}
log.Debug(fmt.Sprintf("cargo: running '%s %v'", cargoExe, args))
return runCmd(cfg)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

The debug log can print a secret.

log.Debug prints the full argument list for every cargo sub-command. cargo login <token> and cargo owner --token <token> pass secrets as arguments, so the token reaches the debug log verbatim. Redact known secret-bearing arguments before logging, or log only the sub-command name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/command.go` around lines 110 - 116, Update
runNativeCargo so its log.Debug call does not expose secret values contained in
cargo arguments, including tokens passed to cargo login and cargo owner --token;
redact those known secret-bearing arguments before logging while preserving the
actual unredacted args passed to runCmd.

Comment on lines +70 to +81
// needsRemoteAccess reports whether jf should inject registry auth for the command. Only the two
// build-info-collecting commands (install, publish) are jf-integrated and get token injection so
// they can resolve/upload against Artifactory. All other commands are pass-throughs and rely on the
// user's cargo credentials (e.g. from `jf setup cargo`).
func needsRemoteAccess(cmd string) bool {
switch commandBucket(cmd) {
case "deps", "publish":
return true
default:
return false
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The doc comment does not match the code.

The comment says "Only the two build-info-collecting commands (install, publish)". commandBucket maps build to deps, so needsRemoteAccess returns true for three commands: install, build, and publish. Update the comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/login.go` around lines 70 - 81, The doc comment
for needsRemoteAccess must reflect that commandBucket maps both install and
build into the "deps" bucket, so remote access is enabled for install, build,
and publish. Update the comment’s command list and related wording while leaving
the function logic unchanged.

Comment on lines +113 to +130
// registryHostMatches reports whether a cargo registry index URL points at the same
// host as the configured Artifactory server URL. Strips cargo's "sparse+"/"git+" prefixes.
func registryHostMatches(indexURL, artifactoryURL string) bool {
strip := func(s string) string {
s = strings.TrimPrefix(s, "sparse+")
s = strings.TrimPrefix(s, "git+")
return s
}
iu, err := url.Parse(strip(indexURL))
if err != nil || iu.Host == "" {
return false
}
au, err := url.Parse(artifactoryURL)
if err != nil || au.Host == "" {
return false
}
return strings.EqualFold(iu.Host, au.Host)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Host-only matching sends the credential to unrelated registries.

registryHostMatches compares only the host. If a user hosts another Cargo index on the same host as the Artifactory server (a different path or a reverse-proxied service), that registry also receives the Artifactory token. Compare the URL path prefix of ArtifactoryUrl as well, so only indexes under the Artifactory base path match.

Also applies to: 149-154

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/login.go` around lines 113 - 130, Update
registryHostMatches to require both a case-insensitive host match and an
Artifactory URL path-prefix match, using the parsed artifactoryURL path as the
base path for the index URL. Preserve sparse+/git+ stripping and reject invalid
or hostless URLs, ensuring unrelated paths on the same host do not match.

Comment on lines +347 to +358
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
props := fmt.Sprintf("build.name=%s;build.number=%s;build.timestamp=%s", name, number, timestamp)
if c.buildConfiguration != nil {
if projectKey := c.buildConfiguration.GetProject(); projectKey != "" {
props += fmt.Sprintf(";build.project=%s", projectKey)
}
}

_, err = sm.SetProps(services.PropsParams{Reader: reader, Props: props, UseDebugLogs: true, IsRecursive: true})
if err != nil {
return fmt.Errorf("set properties: %w", err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find existing property-building helpers and how other commands build build.* props.
rg -nP --type=go -C3 'build\.name=|build\.timestamp=' -g '!**/cargo/**'
fd -t f -e go . --exec rg -nP 'func (NewProperties|ParseProperties|\(.*Properties\) (AddProperty|ToEncodedString))' {} \;

Repository: jfrog/jfrog-cli-artifactory

Length of output: 165


🏁 Script executed:

#!/bin/bash
set -e

echo "Repository files:"
git ls-files | sed -n '1,80p'

echo
echo "Locate publish.go:"
fd -t f 'publish\.go$' .

echo
echo "Inspect relevant lines:"
if [ -f artifactory/commands/cargo/publish.go ]; then
  nl -ba artifactory/commands/cargo/publish.go | sed -n '320,370p'
fi

echo
echo "Find Build* helpers/usages in codebase:"
rg -n --type=go 'Build(Props|Properties|Info|Details)|PropsParams|ParseProperties|NewProperties|SetProps|build\.name=|build\.number=|build\.timestamp=' .

Repository: jfrog/jfrog-cli-artifactory

Length of output: 50383


🏁 Script executed:

#!/bin/bash
set -e

echo "cargo publish lines:"
awk 'NR>=320 && NR<=370 {printf "%6d\t%s\n", NR, $0}' artifactory/commands/cargo/publish.go

echo
echo "cargo publish imports:"
awk 'NR>=1 && NR<=60 {printf "%6d\t%s\n", NR, $0}' artifactory/commands/cargo/publish.go

echo
echo "createBuildPropsForArtifact definition:"
awk 'NR>=280 && NR<=310 {printf "%6d\t%s\n", NR, $0}' artifactory/commands/cargo/publish.go

echo
echo "CreateBuildProperties helper:"
rg -n --type=go -C5 'func CreateBuildProperties|func BuildProps' artifactory -g '!**/cargo/**' | head -n 200

echo
echo "ParseProperties helpers/usages:"
rg -n --type=go 'func ParseProperties|ParseProperties\(' . | head -n 120

echo
echo "git diff stat:"
git diff --stat -- artifactory/commands/cargo/publish.go || true

Repository: jfrog/jfrog-cli-artifactory

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -u

echo "cargo publish imports:"
awk '/^import \(/,/^\)/{print}' artifactory/commands/cargo/publish.go
awk 'NR>=1 && NR<=60 {printf "%6d\t%s\n", NR, $0}' artifactory/commands/cargo/publish.go

echo
echo "cargo publish SetProps block:"
awk 'NR>=320 && NR<=370 {printf "%6d\t%s\n", NR, $0}' artifactory/commands/cargo/publish.go

echo
echo "cargo publish createBuildPropsForArtifact:"
awk 'NR>=280 && NR<=310 {printf "%6d\t%s\n", NR, $0}' artifactory/commands/cargo/publish.go

echo
echo "CreateBuildProperties helper definitions:"
rg -n --type=go -C4 'func CreateBuildProperties|func CreateBuildProps|BuildProps.*configuration|CreateBuildPropsFromConfiguration' artifactory | head -n 160

echo
echo "ParseProperties helpers/usages:"
rg -n --type=go 'func ParseProperties|func \*Properties ToEncodedString|func NewProperties|ParseProperties\(' . | head -n 140

echo
echo "Command flag definitions:"
rg -n --type=go -C4 'build-name|build-number|build\.name|build\.number' artifactory/commands/cargo artifactory/cli | head -n 220

Repository: jfrog/jfrog-cli-artifactory

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -u

printf 'files:\n'
git ls-files artifactory/commands/cargo/publish.go artifactory/commands/cargo | sort | sed -n '1,80p'

printf '\nfile size:\n'
wc -l artifactory/commands/cargo/publish.go

printf '\nimports:\n'
sed -n '1,70p' artifactory/commands/cargo/publish.go

printf '\nSetProps block:\n'
sed -n '330,370p' artifactory/commands/cargo/publish.go

printf '\ncreate/props functions:\n'
sed -n '270,310p' artifactory/commands/cargo/publish.go

printf '\nhelper references:\n'
rg -n --type=go 'CreateBuildProperties|CreateBuildPropsFromConfiguration|ParseProperties|NewProperties|ToEncodedString' artifactory | head -n 120

printf '\nflags:\n'
rg -n --type=go -C3 'BuildName|BuildNumber|build-name|build-number' artifactory/commands/cargo/artifactory/cli | head -n 220

Repository: jfrog/jfrog-cli-artifactory

Length of output: 157


Escape build property values before passing them to SetProps.

build.name, build.number, and build.project come from command flags/configuration. Values containing ; or = split or overwrite properties, so generate these with a properties builder that escapes values before calling sm.SetProps.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/publish.go` around lines 347 - 358, Update the
property construction around the props assignment before SetProps to use the
repository’s properties builder or escaping mechanism for build.name,
build.number, build.timestamp, and build.project values. Ensure values
containing semicolons or equals signs remain single property values, while
preserving the existing optional project inclusion and SetProps call.

Comment on lines +484 to +527
// parseCargoRegistries returns registry name -> index URL, merging cargo's config sources the way
// cargo resolves them: the user-global $CARGO_HOME/config.toml (default ~/.cargo/config.toml) — this
// is what `jf setup cargo` writes — as a base, overlaid by the project-local
// <workingDir>/.cargo/config.toml (project entries win). This lets `jf cargo` locate registries
// whether they came from `jf setup cargo` (global) or a project-committed .cargo/config.toml.
func parseCargoRegistries(workingDir string) map[string]string {
out := map[string]string{}
// Global (lowest precedence) — written by `jf setup cargo`.
if home, err := cargoHome(); err == nil && home != "" {
readRegistriesInto(filepath.Join(home, "config.toml"), out)
}
// Project-local (highest precedence) overlays the global entries.
readRegistriesInto(filepath.Join(workingDir, ".cargo", "config.toml"), out)
return out
}

// readRegistriesInto parses one cargo config.toml and merges its [registries.<name>] index URLs
// into out (existing keys are overwritten). Missing/invalid files are skipped (debug-logged).
func readRegistriesInto(configPath string, out map[string]string) {
data, err := os.ReadFile(configPath)
if err != nil {
log.Debug("cargo: could not read " + configPath + ": " + err.Error())
return
}
var cfg cargoConfigToml
if err := toml.Unmarshal(data, &cfg); err != nil {
log.Debug("cargo: could not parse " + configPath + ": " + err.Error())
return
}
for name, reg := range cfg.Registries {
if reg.Index != "" {
out[name] = reg.Index
}
}
}

// cargoRegistryIndexURL reads <workingDir>/.cargo/config.toml and returns the
// index URL of [registries.<registryName>]. Returns "" on any error or if absent.
func cargoRegistryIndexURL(workingDir, registryName string) string {
if registryName == "" {
return ""
}
return parseCargoRegistries(workingDir)[registryName]
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Repository files matching publish.go/config.toml related names:\n'
fd -a 'publish\.go$|config\.toml$|.*cargo.*\.go$' . | sed 's#^\./##' | head -200

printf '\nRelevant symbol references:\n'
rg -n "parseCargoRegistries|readRegistriesInto|cargoRegistryIndexURL|cargoHome|Registries|registries" -S .

printf '\nTarget file outline:\n'
ast-grep outline artifactory/commands/cargo/publish.go --match parseCargoRegistries --view expanded || true

printf '\nRelevant target file sections:\n'
sed -n '440,540p' artifactory/commands/cargo/publish.go

printf '\nCargo config-related implementation sections:\n'
rg -n -C 3 "func (parseCargoRegistries|readRegistriesInto|cargoRegistryIndexURL|cargoHome)" -S .

Repository: jfrog/jfrog-cli-artifactory

Length of output: 15609


🌐 Web query:

Cargo documentation cargo config file paths .cargo/config.toml .cargo/config current directory parent directories

💡 Result:

Cargo searches for configuration files by traversing upward from the current working directory, including the current directory and all its parent directories [1][2]. The lookup process is as follows: 1. Cargo probes for files named.cargo/config.toml or.cargo/config in the current directory and every parent directory, continuing until it reaches the root directory [1][3]. 2. It then checks the CARGO_HOME directory ($HOME/.cargo/config.toml on Unix or %USERPROFILE%.cargo\config.toml on Windows) [1][2]. Important rules and behaviors: - Hierarchical Merging: Cargo unifies the configuration files found in these locations. Settings defined closer to the current working directory take precedence over those found in parent directories, while the global CARGO_HOME configuration has the lowest precedence [1][3]. - File Naming: Both.cargo/config.toml and.cargo/config are valid. If both exist in the same.cargo directory, Cargo will use the file without the.toml extension [1][2]. - Workspace Limitations: When Cargo is invoked from a workspace root, it does not automatically read configuration files located within individual workspace member crates [1][3]. - Probing Behavior: Because the search is based on the current working directory, invoking Cargo with --manifest-path or from a different directory than the project root can result in different configuration files being discovered [4][5]. For managing complex projects, the official recommendation is to keep extra configuration files at the same level as the discovered.cargo/config.toml within your project or to use a new directory at the same level, such as.config/ [1][2].

Citations:


🌐 Web query:

site:doc.rust-lang.org/cargo/reference/config.html default-config "search" ".cargo/config.toml" ".cargo/config"

💡 Result:

Cargo searches for configuration files by traversing upward from the current working directory, as well as checking a global location [1]. Lookup Order and Merging Cargo probes for configuration files in the following order, merging them as it goes: 1. All.cargo/config.toml files in the current directory and every parent directory, starting from the current directory and moving toward the root [1]. 2. The global configuration file located at $CARGO_HOME/config.toml (which defaults to $HOME/.cargo/config.toml on Unix systems or %USERPROFILE%.cargo\config.toml on Windows) [1]. Precedence and Conflicts - For scalars (numbers, strings, booleans), configuration files found in deeper directories take precedence over those in ancestor directories, with the global home directory having the lowest priority [1]. - For arrays, values are joined together, with higher-precedence items placed later in the merged array [1]. File Naming Conventions Cargo supports both.cargo/config.toml and.cargo/config [1]. While.cargo/config.toml is the preferred extension introduced in version 1.39, Cargo will prioritize the older.cargo/config file if both exist at the same location [1].

Citations:


Walk parent Cargo configs and support the legacy config filename.

parseCargoRegistries only reads $CARGO_HOME/config.toml and <workingDir>/.cargo/config.toml. Cargo searches upward from the current directory for both .cargo/config.toml and .cargo/config, then reads global config, with deeper/local entries overriding ancestors/global entries. This lookup misses workspace-root configs when running from a member crate and any legacy .cargo/config, so registry indices can resolve to "" and disable repository resolution, checksum enrichment, and build properties. Add parent-directory traversal that reads both file names and applies highest-precedence local entries last.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 504-504: A log/format call (log.Print/Printf/Println, the Fatal/Panic variants, fmt.Sprintf, or a structured logger's Info/Warn/Error/Debug method) is given a message built by concatenating a string literal with a non-literal value such as request data. Unsanitized, attacker-controlled input written to logs enables log forging / CRLF injection: an attacker can inject newlines to spoof log entries or break log parsers. Do not concatenate raw input into the log message; pass it as a separate structured field/argument (e.g. 'log.Printf("user: %s", user)' or 'logger.Info("login", "user", user)') and strip or escape newline characters first.
Context: log.Debug("cargo: could not read " + configPath + ": " + err.Error())
Note: [CWE-117] Improper Output Neutralization for Logs.

(log-injection-request-data-concat-go)


[warning] 509-509: A log/format call (log.Print/Printf/Println, the Fatal/Panic variants, fmt.Sprintf, or a structured logger's Info/Warn/Error/Debug method) is given a message built by concatenating a string literal with a non-literal value such as request data. Unsanitized, attacker-controlled input written to logs enables log forging / CRLF injection: an attacker can inject newlines to spoof log entries or break log parsers. Do not concatenate raw input into the log message; pass it as a separate structured field/argument (e.g. 'log.Printf("user: %s", user)' or 'logger.Info("login", "user", user)') and strip or escape newline characters first.
Context: log.Debug("cargo: could not parse " + configPath + ": " + err.Error())
Note: [CWE-117] Improper Output Neutralization for Logs.

(log-injection-request-data-concat-go)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/publish.go` around lines 484 - 527, Update
parseCargoRegistries to traverse from workingDir upward through its parent
directories, reading both .cargo/config.toml and legacy .cargo/config at each
level, while preserving global $CARGO_HOME configuration as the
lowest-precedence source. Apply discovered project configurations from ancestors
to the current directory so deeper entries override parent entries, and ensure
both filenames use readRegistriesInto.

Comment on lines +138 to +155
credsPath := filepath.Join(home, "credentials.toml")
if err = mergeTomlFile(credsPath, func(m map[string]interface{}) {
for _, reg := range registries {
setNested(m, []string{"registries", reg, "token"}, credential)
}
if deployRepo == "" {
// Mirror the config.toml cleanup: drop any stale jfrog-local credentials so the two
// files stay in sync (a leftover token here would otherwise reference a registry
// that no longer exists in config.toml).
deleteNested(m, []string{"registries", jfrogDeployRegistryName})
}
}); err != nil {
return fmt.Errorf("failed to write cargo credentials %q: %w", credsPath, err)
}
// credentials.toml holds a secret — restrict permissions (best-effort).
if err = os.Chmod(credsPath, 0600); err != nil {
log.Debug("cargo: could not chmod credentials.toml: " + err.Error())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Create credentials.toml with 0600 instead of chmod after write.

mergeTomlFile uses os.Create, which creates the file with mode 0666 masked by umask. The token is written before os.Chmod runs, so the secret exists on disk with world-readable permissions during that window. If os.Chmod fails, the file stays readable and only a debug log is emitted.

Pass the wanted mode into mergeTomlFile and create the file with os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm).

🔒 Proposed fix
-	if err = mergeTomlFile(credsPath, func(m map[string]interface{}) {
+	if err = mergeTomlFile(credsPath, 0600, func(m map[string]interface{}) {
 		for _, reg := range registries {
 			setNested(m, []string{"registries", reg, "token"}, credential)
 		}
@@
 	}); err != nil {
 		return fmt.Errorf("failed to write cargo credentials %q: %w", credsPath, err)
 	}
-	// credentials.toml holds a secret — restrict permissions (best-effort).
-	if err = os.Chmod(credsPath, 0600); err != nil {
-		log.Debug("cargo: could not chmod credentials.toml: " + err.Error())
-	}
+	// Existing files keep their previous mode, so still narrow them (best-effort).
+	if err = os.Chmod(credsPath, 0600); err != nil {
+		log.Debug("cargo: could not chmod credentials.toml: " + err.Error())
+	}

Outside the selected range, update the helper:

func mergeTomlFile(path string, perm os.FileMode, apply func(map[string]interface{})) error {
	...
	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
	...
}

The config.toml call site then passes 0644.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
credsPath := filepath.Join(home, "credentials.toml")
if err = mergeTomlFile(credsPath, func(m map[string]interface{}) {
for _, reg := range registries {
setNested(m, []string{"registries", reg, "token"}, credential)
}
if deployRepo == "" {
// Mirror the config.toml cleanup: drop any stale jfrog-local credentials so the two
// files stay in sync (a leftover token here would otherwise reference a registry
// that no longer exists in config.toml).
deleteNested(m, []string{"registries", jfrogDeployRegistryName})
}
}); err != nil {
return fmt.Errorf("failed to write cargo credentials %q: %w", credsPath, err)
}
// credentials.toml holds a secret — restrict permissions (best-effort).
if err = os.Chmod(credsPath, 0600); err != nil {
log.Debug("cargo: could not chmod credentials.toml: " + err.Error())
}
credsPath := filepath.Join(home, "credentials.toml")
if err = mergeTomlFile(credsPath, 0600, func(m map[string]interface{}) {
for _, reg := range registries {
setNested(m, []string{"registries", reg, "token"}, credential)
}
if deployRepo == "" {
// Mirror the config.toml cleanup: drop any stale jfrog-local credentials so the two
// files stay in sync (a leftover token here would otherwise reference a registry
// that no longer exists in config.toml).
deleteNested(m, []string{"registries", jfrogDeployRegistryName})
}
}); err != nil {
return fmt.Errorf("failed to write cargo credentials %q: %w", credsPath, err)
}
// Existing files keep their previous mode, so still narrow them (best-effort).
if err = os.Chmod(credsPath, 0600); err != nil {
log.Debug("cargo: could not chmod credentials.toml: " + err.Error())
}
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 153-153: A log/format call (log.Print/Printf/Println, the Fatal/Panic variants, fmt.Sprintf, or a structured logger's Info/Warn/Error/Debug method) is given a message built by concatenating a string literal with a non-literal value such as request data. Unsanitized, attacker-controlled input written to logs enables log forging / CRLF injection: an attacker can inject newlines to spoof log entries or break log parsers. Do not concatenate raw input into the log message; pass it as a separate structured field/argument (e.g. 'log.Printf("user: %s", user)' or 'logger.Info("login", "user", user)') and strip or escape newline characters first.
Context: log.Debug("cargo: could not chmod credentials.toml: " + err.Error())
Note: [CWE-117] Improper Output Neutralization for Logs.

(log-injection-request-data-concat-go)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/setup.go` around lines 138 - 155, Update
mergeTomlFile to accept an os.FileMode parameter and create files with
os.OpenFile using the requested permissions before writing. Pass 0600 at the
credentials.toml call site in the setup flow, remove the post-write os.Chmod
handling, and update the config.toml call site to pass 0644.

Comment on lines +162 to +189
func mergeTomlFile(path string, apply func(map[string]interface{})) error {
m := map[string]interface{}{}
data, err := os.ReadFile(path)
switch {
case err == nil && len(data) > 0:
if uerr := toml.Unmarshal(data, &m); uerr != nil {
return fmt.Errorf("parse existing TOML: %w", uerr)
}
case err != nil && !os.IsNotExist(err):
return err
}

apply(m)

if err = os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); cerr != nil {
log.Debug("cargo: close " + path + ": " + cerr.Error())
}
}()
return toml.NewEncoder(f).Encode(m)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Encode failure leaves a truncated config file.

os.Create truncates the target before the encode runs. If toml.NewEncoder(f).Encode(m) fails, the user keeps an empty or partial config.toml or credentials.toml, and their unrelated settings are lost.

Encode into a buffer first, then write the buffer, or write to a temp file in the same directory and rename it into place.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 184-184: A log/format call (log.Print/Printf/Println, the Fatal/Panic variants, fmt.Sprintf, or a structured logger's Info/Warn/Error/Debug method) is given a message built by concatenating a string literal with a non-literal value such as request data. Unsanitized, attacker-controlled input written to logs enables log forging / CRLF injection: an attacker can inject newlines to spoof log entries or break log parsers. Do not concatenate raw input into the log message; pass it as a separate structured field/argument (e.g. 'log.Printf("user: %s", user)' or 'logger.Info("login", "user", user)') and strip or escape newline characters first.
Context: log.Debug("cargo: close " + path + ": " + cerr.Error())
Note: [CWE-117] Improper Output Neutralization for Logs.

(log-injection-request-data-concat-go)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/cargo/setup.go` around lines 162 - 189, Update
mergeTomlFile so TOML encoding completes successfully before replacing the
existing file; avoid calling os.Create on the target before
toml.NewEncoder.Encode can fail. Encode into an in-memory buffer or
same-directory temporary file, then atomically write or rename the result into
place while preserving the existing error handling and file permissions.

Comment on lines +288 to +299
local, err := utils.SelectRepositoryInteractively(
sc.serverDetails,
services.RepositoriesFilterParams{RepoType: utils.Local.String(), PackageType: packageType, ProjectKey: sc.projectKey},
"Select a local repository for publishing crates (optional):")
if err != nil {
if !strings.Contains(err.Error(), noMatchingRepositoriesErrSubstring) {
return err
}
log.Info(fmt.Sprintf("No local %s repository was found; configuring resolution only (publishing not configured).", packageType))
return nil
}
sc.deployRepoName = local

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The "optional" local repository cannot be declined.

The prompt text says the local repository is optional, but SelectRepositoryInteractively returns an error only when no repository matches. If one or more local Cargo repositories exist, the user must select one and cannot skip publishing configuration. Add an explicit skip entry, or ask a yes/no question before the selection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@artifactory/commands/setup/setup.go` around lines 288 - 299, Update the local
repository selection flow around SelectRepositoryInteractively so users can
decline the optional publishing configuration even when matching repositories
exist. Add an explicit skip choice or a preceding confirmation prompt, and leave
sc.deployRepoName unset when skipped while preserving the existing
no-matching-repositories handling.

Comment thread go.mod
Comment on lines +206 to +211

// LOCAL-ONLY (uncommitted, REVERT before PR): use local build-info-go for cargo flexpack work
replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.13.1-0.20260728061823-0ba531230559

// LOCAL-ONLY (uncommitted, REVERT before PR): use local jfrog-cli-core for the project.Cargo enum (jf setup cargo)
replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260717063801-dfcd55d10923

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP 'jfrog/(build-info-go|jfrog-cli-core)' go.mod
rg -nP 'jfrog/(build-info-go|jfrog-cli-core)' go.sum | head -20

Repository: jfrog/jfrog-cli-artifactory

Length of output: 1185


Remove the LOCAL-ONLY replace directives before merge.

The manifest still contains the uncommitted local replace directives for github.com/jfrog/build-info-go and github.com/jfrog/jfrog-cli-core/v2. These override the declared require entries and make CI builds depend on unreleased pseudo-versions. Delete both directives and update the require versions to the intended releases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go.mod` around lines 206 - 211, Remove both LOCAL-ONLY replace directives for
build-info-go and jfrog-cli-core/v2 from go.mod, then update their require
entries to the intended released versions so builds no longer depend on
unreleased pseudo-versions.

@g3n35i5 g3n35i5 mentioned this pull request Aug 6, 2026
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.

1 participant