Skip to content

fix(pkg): resolve --rpm-file packages per source package, not per name - #358

Draft
Andrew Phelps (anphel31) wants to merge 2 commits into
microsoft:mainfrom
anphel31:anphel/rpm-file-resolve-per-source
Draft

Andrew Phelps (anphel31) wants to merge 2 commits into
microsoft:mainfrom
anphel31:anphel/rpm-file-resolve-per-source

Conversation

@anphel31

@anphel31 Andrew Phelps (anphel31) commented Sep 18, 2026

Copy link
Copy Markdown
Member

Summary

azldev package list --rpm-file now reports one entry per (packageName, sourcePackageName) pair, instead of one entry per package name chosen by position in the file.

The problem

loadRPMFile kept the first mapping for a binary package name and discarded any later entry naming a different source package:

if existingSource, exists := rpmCompOf[packageName]; exists {
    // First mapping wins.
    ...
    slog.Warn("RPM source map contains conflicting source package mappings; first mapping wins", ...)
    continue
}

So the component — and therefore the publish channel — for such a name depended on the order of entries in the file.

A binary name produced by more than one component is legitimate, not a malformed input. rubygem-bundler is emitted by both ruby (rpm-base) and the standalone rubygem-bundler component (rpm-sdk). Listing both producers gave one answer, picked by position:

$ azldev package list --rpm-file bundler-first.json -O json --project base/project.toml
  ... "packageName": "rubygem-bundler", "component": "rubygem-bundler", "publishChannel": "rpm-sdk"

$ azldev package list --rpm-file ruby-first.json -O json --project base/project.toml
  WRN RPM source map contains conflicting source package mappings; first mapping wins
      packageName=rubygem-bundler keptSourcePackageName=ruby skippedSourcePackageName=rubygem-bundler
  ... "packageName": "rubygem-bundler", "component": "ruby", "publishChannel": "rpm-base"

Same question, two answers.

Why it mattered

Control Tower builds its RPM source map from an unordered collection, so entry order varies with the contents of the batch. Its whole-corpus routing pass and its per-batch publish pass therefore received different channels for the same package: publishing placed the RPMs in the repos that rpm-sdk maps to while the presence check expected the ones rpm-base maps to. (In Azure Linux those are the builddeps and base PMC repositories respectively — the channel name and the repository name differ, which is worth keeping straight when reading the logs.)

The component never satisfied its own presence check and was re-queued every six hours for weeks — uploading nothing and reporting success each time. The warning above was emitted on every call and never surfaced, because the caller only logged subprocess stderr on a non-zero exit.

The change

  • Each (packageName, sourcePackageName) pair is preserved. A package with two producers yields one entry per producer, each resolved against its own component.
  • The component is taken from the source package being expanded, not looked up by package name. resolveFromRPMFile already iterates by source, so the producing component is known at the call site and the order dependence disappears.
  • resolvePackageListResult now takes an already-resolved component name. Both call paths state where the component came from: resolveComponentName for project-driven listings, the source package for --rpm-file.
  • loadRPMFile returns only srpmMap; the rpmCompOf index was the structure that could not represent two producers.
  • Only exact duplicate pairs are collapsed.
  • The conflict warning and its TODO are removed — the condition it reported is now handled rather than tolerated.

The existing sort in listPackagesFromRPMFile already tie-breaks on Component for entries sharing a package name, so output remains deterministic.

Compatibility

Callers that send one producer per name see no change. Callers that send several now receive an entry per producer rather than one arbitrary entry, and must key results by (packageName, sourcePackageName) rather than by name alone. That is the intent: a name alone is not a unique key.

Tests

  • The validation case that asserted first-mapping-wins now asserts one entry per source.
  • New TestListPackages_RPMFile_MultipleProducersResolvePerSource checks that two producers of one name resolve to their own components' channels, and that reversing the order of the map does not change the result.

go build ./... clean. go test ./internal/app/azldev/cmds/pkg/... passes.

go test ./... has one unrelated failure in internal/utils/fileutils (TestEmbedFSWithSubdir/ReadFileInSubdir, CRLF vs LF) which reproduces on an unmodified checkout — an artefact of checking the repo out on Windows, not of this change.

golangci-lint was not available in my environment; gofmt reports no diff on the changed files once CRLF line endings are normalised.

Note

Filed as a draft. The behaviour change is small but it is a contract change for anyone consuming --rpm-file output, so it is worth an owner's read before it goes further.

Copilot AI lite review requested due to automatic review settings September 18, 2026 18:49

Copilot AI 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.

🟡 Changes recommended

Document the changed --rpm-file output contract and row uniqueness for scripting users.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Updates --rpm-file resolution to preserve multiple producers for the same binary package and resolve each by source component.

Changes:

  • Deduplicates exact (packageName, sourcePackageName) pairs.
  • Resolves RPM channels per source package.
  • Adds multi-producer and order-independence tests.
File summaries
File Description
internal/app/azldev/cmds/pkg/list.go Implements per-source RPM resolution and pair deduplication.
internal/app/azldev/cmds/pkg/list_test.go Tests duplicate handling and multi-producer resolution.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/app/azldev/cmds/pkg/list.go
Copilot AI review requested due to automatic review settings September 18, 2026 19:58

Copilot AI 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.

🟢 Approval recommended

Reviewed changes are covered by tests and documentation.

Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

loadRPMFile kept the first mapping for a binary package name and discarded any
later entry naming a different source package, warning that it had done so. The
result was one entry per packageName, whose component - and therefore publish
channel - depended on the order of entries in the file.

A binary name produced by more than one component is legitimate. rubygem-bundler
is emitted by both 'ruby' (rpm-base) and the standalone 'rubygem-bundler'
component (rpm-sdk). A caller that listed both got a single answer chosen by
position, so the same package resolved differently in different invocations
depending on what else was in the map.

That reached production. Control Tower builds its source map from an unordered
collection, so its whole-corpus routing pass and its per-batch publish pass
received different channels for rubygem-bundler. Publishing placed the RPMs in
the repos that rpm-sdk maps to while the presence check expected the ones
rpm-base maps to, so the component was re-queued every six hours for weeks,
uploading nothing and reporting success.

Each (packageName, sourcePackageName) pair is now preserved and reported, so a
package with two producers yields one entry per producer, each resolved against
its own component. The component is taken from the source package being expanded
rather than looked up by name, which removes the order dependence entirely.
resolvePackageListResult now receives an already-resolved component name, so
both call paths state where the component came from.

Only exact duplicate pairs are collapsed. The conflict warning and its TODO are
gone: the condition it reported is handled rather than tolerated.

Tests: the validation case that asserted first-mapping-wins now asserts one
entry per source, and a new case checks that two producers resolve to their own
channels and that reversing the order of the map does not change the result.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The change to report one row per (packageName, sourcePackageName) is user-visible:
a binary name produced by several components now appears once per producer. A
script keying results by package name alone silently drops a producer, and which
one survives depends on iteration order - the same failure this change removes,
reintroduced one layer up.

Adds a 'Row uniqueness' section to the inspect-package-config how-to with a
worked rubygem-bundler example, and corrects the Component-column note, which
said Component never means 'the component whose spec produces this package' -
true for -a and -p, but the opposite of what it means under --rpm-file.

Also states the contract in the command's own help text, and regenerates
docs/user/reference/cli/azldev_package_list.md from it.

Raised in review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@anphel31
Andrew Phelps (anphel31) force-pushed the anphel/rpm-file-resolve-per-source branch from 5a4487e to bdae30a Compare September 18, 2026 20:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants