Skip to content

Fix resolveArmResources incorrectly merging cross-scope LegacyOperations into a single resource#4072

Open
Copilot wants to merge 3 commits intomainfrom
copilot/fix-merge-cross-scope-legacyoperations
Open

Fix resolveArmResources incorrectly merging cross-scope LegacyOperations into a single resource#4072
Copilot wants to merge 3 commits intomainfrom
copilot/fix-merge-cross-scope-legacyoperations

Conversation

Copy link
Contributor

Copilot AI commented Mar 20, 2026

  • Analyze the issue: resolveArmResources merges cross-scope LegacyOperations into a single resource when they share the same model
  • Understand the code flow: isResourceOperationMatch matches by resource name + provider + types but ignores scope prefix
  • Add scope prefix comparison to isResourceOperationMatch when resource names are not explicitly provided
  • Update ResolvedResourceOperations and ResolvedResourceInfo interfaces to track resourceNameIsExplicit
  • Update resolveArmResourceOperations to pass explicit name flag through matching
  • Add test: cross-scope LegacyOperations with default names should produce separate resources
  • Add test: cross-scope LegacyOperations with explicit same name should merge into one resource
  • Add unit tests for isResourceOperationMatch with scope prefix comparison
  • Verify all existing tests still pass (274 tests across 44 files)
  • Merge latest from main branch
  • Format code
  • Add changeset
  • Run code review and CodeQL checks (no issues found)
Original prompt

This section details on the original issue you should resolve

<issue_title>resolveArmResources incorrectly merges cross-scope LegacyOperations into a single resource</issue_title>
<issue_description>## Description

resolveArmResources in @azure-tools/typespec-azure-resource-manager incorrectly merges cross-scope LegacyOperations that share the same model into a single resource, instead of separating them into distinct resources per scope.

This affects real-world patterns like the Support SDK, where SupportTicketDetails (decorated with @subscriptionResource) is used by both subscription-scoped (SupportTickets) and tenant-scoped (SupportTicketsNoSubscription) interfaces via LegacyOperations.

Reproduction

@subscriptionResource
model SupportTicketDetails is ProxyResource<SupportTicketProperties> {
  ...ResourceNameParameter<
    Resource = SupportTicketDetails,
    KeyName = "supportTicketName",
    SegmentName = "supportTickets",
    NamePattern = ""
  >;
}

model SupportTicketProperties {
  description?: string;
}

// Subscription-scoped operations (includes SubscriptionIdParameter)
alias SubTicketOps = Azure.ResourceManager.Legacy.LegacyOperations<
  { ...ApiVersionParameter; ...SubscriptionIdParameter; ...Azure.ResourceManager.Legacy.Provider; },
  { @segment("supportTickets") @key @TypeSpec.Http.path supportTicketName: string; }
>;

// Tenant-scoped operations (no SubscriptionIdParameter)
alias TenantTicketOps = Azure.ResourceManager.Legacy.LegacyOperations<
  { ...ApiVersionParameter; ...Azure.ResourceManager.Legacy.Provider; },
  { @segment("supportTickets") @key @TypeSpec.Http.path supportTicketName: string; }
>;

@armResourceOperations
interface SupportTickets {
  get is SubTicketOps.Read<SupportTicketDetails>;
  list is SubTicketOps.List<SupportTicketDetails>;
}

@armResourceOperations
interface SupportTicketsNoSubscription {
  get is TenantTicketOps.Read<SupportTicketDetails>;
  list is TenantTicketOps.List<SupportTicketDetails>;
}

Expected behavior

resolveArmResources should produce 2 separate resources (one per scope):

  1. Subscription-scoped resource with resourceInstancePath: "/subscriptions/{subscriptionId}/providers/.../supportTickets/{supportTicketName}", scope: "Subscription", and its own lifecycle read + list operations
  2. Tenant-scoped resource with resourceInstancePath: "/providers/.../supportTickets/{supportTicketName}", scope: "Tenant", and its own lifecycle read + list operations

Actual behavior

resolveArmResources returns 1 merged resource:

resources.length: 1

Resource:
  typeName: SupportTicketDetails
  resourceInstancePath: /subscriptions/{subscriptionId}/providers/Microsoft.ContosoProviderHub/supportTickets/{supportTicketName}
  resourceName: SupportTickets
  scope: Subscription

  lifecycle.read[0]:
    kind=read, operationGroup=SupportTickets
    path=/subscriptions/{subscriptionId}/providers/.../supportTickets/{supportTicketName}

  lifecycle.read[1]:
    kind=read, operationGroup=SupportTicketsNoSubscription
    path=/providers/.../supportTickets/{supportTicketName}

  lists[0]:
    kind=list, operationGroup=SupportTickets
    path=/subscriptions/{subscriptionId}/providers/.../supportTickets

  lists[1]:
    kind=list, operationGroup=SupportTicketsNoSubscription
    path=/providers/.../supportTickets

The operations from both scopes are merged into a single resource. The individual operations do retain their correct paths (e.g., lifecycle.read[1] has the tenant path /providers/.../supportTickets/{name}), but the resource-level scope is "Subscription" and only the subscription resourceInstancePath is used.

Affected package

packages/typespec-azure-resource-manager/src/resource.tsresolveArmResources function
</issue_description>

<agent_instructions>@copilot The matching of operations with a ResolvedResource in resolveArmResources should use the resourceName whenever it is explicitly provided as a parameter to the operation template. However, if the resourceName is simply the default (derived from the resource model), matching should depend on the scope. SPecifically, operations at the tenant level should not be matched with subscription-level operations.

Please add some tests that check these rules, ensure that they fail in the mentioned case, and then update the logic for matching in resolveArmResources to make the succeed. As always, make sure that all tests are passing, that there are no formatting errors and no misspellings and that you update your pr with the latest from the main rbanch.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@markcowl @ArcturusZhang This is unexpected, but there is a workaround until the logic is fixed. You can explicitly supply the resource name, which ch...

💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.

Add scope prefix comparison to isResourceOperationMatch when resource names
are not explicitly provided. This prevents operations at different scopes
(e.g., subscription vs tenant) from being incorrectly merged into a single
resource when they share the same model.

When a resource name is explicitly provided via the ResourceName parameter
of LegacyOperations, matching continues to use the name for grouping,
allowing intentional cross-scope merging.

Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com>
@microsoft-github-policy-service microsoft-github-policy-service bot added the lib:azure-resource-manager Issues for @azure-tools/typespec-azure-core library label Mar 20, 2026
Copilot AI changed the title [WIP] Fix merging of cross-scope LegacyOperations in resolveArmResources Fix resolveArmResources incorrectly merging cross-scope LegacyOperations into a single resource Mar 20, 2026
Copilot AI requested a review from markcowl March 20, 2026 02:35
@markcowl
Copy link
Member

/azp run typespec-azure - PR Tools

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@azure-sdk
Copy link
Collaborator

azure-sdk commented Mar 20, 2026

All changed packages have been documented.

  • @azure-tools/typespec-azure-resource-manager
Show changes

@azure-tools/typespec-azure-resource-manager - fix ✏️

Fix resolveArmResources incorrectly merging cross-scope LegacyOperations into a single resource. Operations at different scopes (e.g., subscription vs tenant) with the same model but no explicit resource name are now resolved as separate resources.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 20, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@azure-tools/typespec-azure-resource-manager@4072

commit: 5a60dd5

@markcowl
Copy link
Member

@copilot the consistency check is reporting a divergence between your branch and main. Please pull the latest, fix any conflicts and push the results.

Copy link
Contributor Author

Copilot AI commented Mar 20, 2026

@copilot the consistency check is reporting a divergence between your branch and main. Please pull the latest, fix any conflicts and push the results.

Merged latest from main (commit e055de3) into the branch at 5a60dd5. The merge commit has 2 parents, all 274 tests pass, and the build succeeds.

@azure-sdk
Copy link
Collaborator

azure-sdk commented Mar 20, 2026

You can try these changes here

🛝 Playground 🌐 Website

@markcowl
Copy link
Member

/azp run typespec-azure - PR Tools

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@markcowl markcowl added the int:azure-specs Run integration tests against azure-rest-api-specs label Mar 20, 2026
@markcowl markcowl marked this pull request as ready for review March 21, 2026 07:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

int:azure-specs Run integration tests against azure-rest-api-specs lib:azure-resource-manager Issues for @azure-tools/typespec-azure-core library

Projects

None yet

Development

Successfully merging this pull request may close these issues.

resolveArmResources incorrectly merges cross-scope LegacyOperations into a single resource

4 participants