Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ stages:
# fixed cross-language output contract. Downstream release stages read outputs as:
# condition: dependencies.AutoReleasePrepare.outputs['ResolveAutoReleasePackages.resolve.<var>']
# variables: stageDependencies.AutoReleasePrepare.ResolveAutoReleasePackages.outputs['resolve.<var>']
# Emitted variables: AutoReleasePrNumber, AutoReleaseLabelPresent, HasAutoReleaseArtifacts,
# AutoReleaseArtifactsJson, and ReleaseArtifact_<safeName> per declared artifact.
# Emitted variables: HasAutoReleaseArtifacts (the single eligibility gate), AutoReleaseArtifactsJson,
# and ReleaseArtifact_<safeName> per declared artifact.
- stage: AutoReleasePrepare
displayName: Auto-release prepare
dependsOn: ${{ parameters.DependsOn }}
Expand Down
47 changes: 21 additions & 26 deletions eng/common/scripts/Resolve-AutoReleasePackages.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ commit, this script:
for pipelines that iterate the releasable set at runtime (e.g. Java).

The script FAILS CLOSED: on any error, or if no qualifying labeled PR / changed package is found, it
emits AutoReleaseLabelPresent=false, HasAutoReleaseArtifacts=false, AutoReleaseArtifactsJson=[] and
ReleaseArtifact_<safeName>=false for every artifact, and exits 0 so the CI run is not failed.
emits HasAutoReleaseArtifacts=false, AutoReleaseArtifactsJson=[] and ReleaseArtifact_<safeName>=false
for every artifact, and exits 0 so the CI run is not failed.

.PARAMETER CommitSha
The build source version (merge commit) to resolve the pull request from. Typically $(Build.SourceVersion).
Expand All @@ -49,11 +49,11 @@ The base branch a PR must have been merged into to qualify. Defaults to 'main'.

.OUTPUTS
Azure DevOps output variables (reference cross-stage via dependencies.<stage>.outputs['<job>.<step>.<name>']):
- AutoReleasePrNumber : the resolved PR number, or empty
- AutoReleaseLabelPresent : 'true' if the resolved merged PR has the auto-release label
- HasAutoReleaseArtifacts : 'true' if at least one declared package is releasable
- AutoReleaseArtifactsJson : JSON array of the matched declared-artifact objects (or '[]')
- ReleaseArtifact_<safeName> : 'true'/'false' per declared artifact
HasAutoReleaseArtifacts is the single eligibility gate: it is 'true' only when a merged, auto-release-labeled
PR changed at least one declared package, and it is emitted last so any earlier failure fails closed.
#>
#Requires -Version 7.0
[CmdletBinding()]
Expand Down Expand Up @@ -86,12 +86,10 @@ try {
if ($null -ne $parsed) { $declaredArtifacts = @($parsed) }
}
catch {
Write-Host "##[warning]Failed to parse -Artifacts JSON; treating as empty. $($_.Exception.Message)"
LogWarning "Failed to parse -Artifacts JSON; treating as empty. $($_.Exception.Message)"
}

# Fail-closed defaults: nothing releases unless we positively determine otherwise below.
Set-PipelineVariable -Name 'AutoReleasePrNumber' -Value '' -IsOutput
Set-PipelineVariable -Name 'AutoReleaseLabelPresent' -Value 'false' -IsOutput
Set-PipelineVariable -Name 'HasAutoReleaseArtifacts' -Value 'false' -IsOutput
Set-PipelineVariable -Name 'AutoReleaseArtifactsJson' -Value '[]' -IsOutput
foreach ($artifact in $declaredArtifacts) {
Expand All @@ -109,30 +107,28 @@ function Invoke-AutoReleaseResolution {
-RequiredLabel $AutoReleaseLabel `
-AuthToken $AuthToken

if ($release.PullRequestNumber) {
Set-PipelineVariable -Name 'AutoReleasePrNumber' -Value "$($release.PullRequestNumber)" -IsOutput
}

if (-not $release.IsEligible) {
Write-Host "Skipping auto-release: $($release.SkipReason)"
return
}

$pr = $release.PullRequest
Write-Host "PR #$($pr.number) is eligible for auto-release (merged into '$BaseBranch' with the '$AutoReleaseLabel' label)."
Set-PipelineVariable -Name 'AutoReleaseLabelPresent' -Value 'true' -IsOutput
# Prefer the PR's canonical html_url; fall back to constructing it so the log link stays clickable even
# if the field is absent from the payload.
$prLink = if ($pr.PSObject.Properties['html_url'] -and $pr.html_url) { "$($pr.html_url)" } else { "https://github.com/$RepoId/pull/$($pr.number)" }
Write-Host "PR $prLink is eligible for auto-release (merged into '$BaseBranch' with the '$AutoReleaseLabel' label)."

if ($declaredArtifacts.Count -eq 0) {
Write-Host "No declared artifacts for this pipeline. Nothing to auto-release."
LogWarning "PR $prLink has the '$AutoReleaseLabel' label but this pipeline declares no artifacts; nothing will be auto-released."
return
}

# Turn the PR's changed files into a diff object (Generate-PR-Diff.ps1 shape) and reuse the repo's
# package-detection logic to identify the changed packages.
Write-Host "Fetching changed files for PR #$($pr.number)..."
Write-Host "Fetching changed files for PR $prLink..."
$files = @(Get-GitHubPullRequestFiles -RepoId $RepoId -PullRequestNumber $pr.number -AuthToken $AuthToken)
$diff = New-GitHubPullRequestDiffObject -PullRequestNumber $pr.number -PullRequestFiles $files
Write-Host "PR #$($pr.number) changed $($diff.ChangedFiles.Count) file(s) and deleted $($diff.DeletedFiles.Count) file(s)."
Write-Host "PR $prLink changed $($diff.ChangedFiles.Count) file(s) and deleted $($diff.DeletedFiles.Count) file(s)."

$diffPath = Join-Path ([System.IO.Path]::GetTempPath()) ("autorelease-diff-" + [System.Guid]::NewGuid().ToString('N') + ".json")
$diff | ConvertTo-Json -Depth 10 | Set-Content -Path $diffPath -Encoding utf8
Expand Down Expand Up @@ -187,40 +183,39 @@ function Invoke-AutoReleaseResolution {
}

if ($isMatch) {
Write-Host " [$name] changed by PR #$($pr.number) -> releasable."
Write-Host " [$name] changed by PR $prLink -> releasable."
Set-PipelineVariable -Name "ReleaseArtifact_$safeName" -Value 'true' -IsOutput
$matchedArtifacts += $artifact
}
else {
Write-Host " [$name] not changed by PR #$($pr.number)."
Write-Host " [$name] not changed by PR $prLink."
}
}
catch {
Write-Host "##[warning]Failed to evaluate an artifact; treating as not releasable. $($_.Exception.Message)"
LogWarning "Failed to evaluate an artifact; treating as not releasable. $($_.Exception.Message)"
}
}

if ($matchedArtifacts.Count -gt 0) {
# Pipe (not -InputObject) with -AsArray so a single match still serializes as a JSON array, '[{...}]'.
$artifactsJson = $matchedArtifacts | ConvertTo-Json -Depth 100 -Compress -AsArray
Set-PipelineVariable -Name 'AutoReleaseArtifactsJson' -Value $artifactsJson -IsOutput
Write-Host "Auto-release packages from PR #$($pr.number): $((@($matchedArtifacts | ForEach-Object { $_.name })) -join ', ')"
Write-Host "Auto-release packages from PR ${prLink}: $((@($matchedArtifacts | ForEach-Object { $_.name })) -join ', ')"
Set-PipelineVariable -Name 'HasAutoReleaseArtifacts' -Value 'true' -IsOutput
}
else {
Write-Host "PR #$($pr.number) changed no releasable package in this pipeline."
LogWarning "PR $prLink has the '$AutoReleaseLabel' label but changed no releasable package in this pipeline; nothing will be auto-released."
}
}

try {
Invoke-AutoReleaseResolution
}
catch {
# Re-emit the fail-closed defaults so a failure after any positive signal was set (e.g. after
# AutoReleaseLabelPresent or a ReleaseArtifact_<safeName> flag was flipped to 'true') cannot leak a
# partial "release" decision to downstream stages, regardless of how each consumer gates on the outputs.
Write-Host "##[warning]Auto-release resolution failed; skipping auto-release. $($_.Exception.Message)"
Set-PipelineVariable -Name 'AutoReleaseLabelPresent' -Value 'false' -IsOutput
# Re-emit the fail-closed defaults so a failure after any positive signal was set (e.g. after a
# ReleaseArtifact_<safeName> flag was flipped to 'true') cannot leak a partial "release" decision to
# downstream stages, regardless of how each consumer gates on the outputs.
LogWarning "Auto-release resolution failed; skipping auto-release. $($_.Exception.Message)"
Set-PipelineVariable -Name 'HasAutoReleaseArtifacts' -Value 'false' -IsOutput
Set-PipelineVariable -Name 'AutoReleaseArtifactsJson' -Value '[]' -IsOutput
foreach ($artifact in $declaredArtifacts) {
Expand Down
8 changes: 8 additions & 0 deletions sdk/spring/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ This section includes changes in `spring-cloud-azure-autoconfigure` module.

- Upgrade to Jackson 3 to align with Spring Boot 4 ([#49538](https://github.com/Azure/azure-sdk-for-java/issues/49538)).

### Spring Cloud Azure Service

This section includes changes in `spring-cloud-azure-service` module.

#### Bugs Fixed

- Fixed the Service Bus producer, consumer and processor sub-client builders overwriting configuration already set on the underlying `ServiceBusClientBuilder` through an `AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder>` (for example the `ClientOptions` carrying `TracingOptions`). The customizers are now applied to the underlying builder as the last step, so their configuration is preserved ([#49742](https://github.com/Azure/azure-sdk-for-java/issues/49742)).

### Spring Messaging Azure

This section includes changes in `spring-messaging-azure` module.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ public class AzureMessagingListenerAutoConfiguration {
@Configuration
@ConditionalOnBean(EventHubsProcessorFactory.class)
static class EventHubsConfiguration {
@Bean(name = "azureEventHubsListenerContainerFactory")
@ConditionalOnMissingBean(name = "azureEventHubsListenerContainerFactory")
@Bean(
name = EventHubsListenerAnnotationBeanPostProcessor.DEFAULT_EVENT_HUBS_LISTENER_CONTAINER_FACTORY_BEAN_NAME)
@ConditionalOnMissingBean(
name = EventHubsListenerAnnotationBeanPostProcessor.DEFAULT_EVENT_HUBS_LISTENER_CONTAINER_FACTORY_BEAN_NAME)
public MessageListenerContainerFactory<? extends MessageListenerContainer> azureEventHubsListenerContainerFactory(
EventHubsProcessorFactory eventHubsProcessorFactory,
ObjectProvider<AzureMessageConverter<EventData, EventData>> messageConverterProvider) {
Expand All @@ -73,8 +75,10 @@ static class EnableEventHubsConfiguration {
@Configuration
@ConditionalOnBean(ServiceBusProcessorFactory.class)
static class ServiceBusConfiguration {
@Bean(name = "azureServiceBusListenerContainerFactory")
@ConditionalOnMissingBean(name = "azureServiceBusListenerContainerFactory")
@Bean(
name = ServiceBusListenerAnnotationBeanPostProcessor.DEFAULT_SERVICE_BUS_LISTENER_CONTAINER_FACTORY_BEAN_NAME)
@ConditionalOnMissingBean(
name = ServiceBusListenerAnnotationBeanPostProcessor.DEFAULT_SERVICE_BUS_LISTENER_CONTAINER_FACTORY_BEAN_NAME)
public MessageListenerContainerFactory<? extends MessageListenerContainer> azureServiceBusListenerContainerFactory(
ServiceBusProcessorFactory serviceBusProcessorFactory,
ObjectProvider<AzureMessageConverter<ServiceBusReceivedMessage, ServiceBusMessage>> messageConverterProvider) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ abstract class AbstractServiceBusSubClientBuilderFactory<T, P extends ServiceBus
private ServiceBusClientBuilder serviceBusClientBuilder;
private final boolean shareServiceBusClientBuilder;
private ServiceBusClientBuilderFactory serviceBusClientBuilderFactory;
private final List<AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder>> serviceBusClientBuilderCustomizers;

/**
* Create a {@link AbstractServiceBusSubClientBuilderFactory} instance with the properties and the collection of
* @{link ServiceBusClientBuilder} customizers.
* {@link AzureServiceClientBuilderCustomizer} customizers.
* @param properties the properties describing the service bus sub client, which could be a sender, a receiver or
* a processor.
* @param serviceClientBuilderCustomizers the collection of customizers for the service bus client builder.
Expand Down Expand Up @@ -70,11 +71,16 @@ protected AbstractServiceBusSubClientBuilderFactory(ServiceBusClientBuilder serv
this.serviceBusClientBuilder = serviceBusClientBuilder;
this.shareServiceBusClientBuilder = true;
this.serviceBusClientBuilderFactory = null;
// The shared ServiceBusClientBuilder is configured and customized by its own factory, so there are no
// customizers for this sub-client builder factory to re-apply.
this.serviceBusClientBuilderCustomizers = null;
} else {
this.serviceBusClientBuilderFactory = new ServiceBusClientBuilderFactory(properties);
if (serviceBusClientBuilderCustomizers != null) {
serviceBusClientBuilderCustomizers.forEach(this.serviceBusClientBuilderFactory::addBuilderCustomizer);
}
// Remember the customizers instead of adding them to the nested factory, so they are applied to the
// underlying ServiceBusClientBuilder as the last step of each build() call. This ensures they win over
// the property-derived configuration this factory redirects onto the shared builder (for example
// ClientOptions carrying TracingOptions). See #49742.
this.serviceBusClientBuilderCustomizers = serviceBusClientBuilderCustomizers;
// Don't build yet - defer until first use when ApplicationContext is available
this.serviceBusClientBuilder = null;
this.shareServiceBusClientBuilder = false;
Expand All @@ -90,6 +96,23 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
}
}

@Override
public T build() {
// super.build() creates the sub-client builder and, in the non-shared path, lazily builds and configures
// the underlying ServiceBusClientBuilder from the properties (including the Spring identifier).
// The customizers are deliberately not applied during that step.
T builder = super.build();
// Apply the ServiceBusClientBuilder customizers as the last step of this build() call,
// so they take precedence over the property-derived configuration redirected onto the underlying builder.
// See #49742.
if (!isShareServiceBusClientBuilder() && this.serviceBusClientBuilderCustomizers != null) {
ServiceBusClientBuilder parentServiceBusClientBuilder = getServiceBusClientBuilder();
this.serviceBusClientBuilderCustomizers.forEach(customizer ->
customizer.customize(parentServiceBusClientBuilder));
}
return builder;
}

@Override
protected void configureCredential(T builder) {
// skip to avoid overriding the parent builder's credentials.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@

/**
* Helper class to control the lifecycle of a {@link ServiceBusProcessorClient}.
* This class implements {@code SmartLifecycle} to auto start {@link ServiceBusProcessorClient} when the Spring Application Context starts,
* And stop the {@link ServiceBusProcessorClient} when the Spring Application Context stops.
* NOTE, there is not need to call {@link #start()} or {@link #stop()} explicitly, as the {@link ServiceBusProcessorClient} will be started and stopped automatically.
* And since the {@link ServiceBusProcessorClient} is a {@link AutoCloseable}, there is no need to call {@link ServiceBusProcessorClient#close()} explicitly.
* This class implements {@code SmartLifecycle} to automatically start {@link ServiceBusProcessorClient} when the
* Spring Application Context starts and stops it when the Spring Application Context stops.
* NOTE: There is no need to call {@link #start()} or {@link #stop()} explicitly, as the
* {@link ServiceBusProcessorClient} will be started and stopped automatically.
* Since {@link ServiceBusProcessorClient} is {@link AutoCloseable}, there is no need to call
* {@link ServiceBusProcessorClient#close()} explicitly.
*/
public class ServiceBusProcessorClientLifecycleManager implements SmartLifecycle {
private final Logger logger = LoggerFactory.getLogger(ServiceBusProcessorClientLifecycleManager.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@

package com.azure.spring.cloud.service.implementation.servicebus.factory;

import com.azure.core.util.ClientOptions;
import com.azure.messaging.servicebus.ServiceBusClientBuilder;
import com.azure.spring.cloud.core.customizer.AzureServiceClientBuilderCustomizer;
import com.azure.spring.cloud.service.implementation.servicebus.properties.ServiceBusSenderClientTestProperties;
import com.azure.spring.cloud.service.servicebus.properties.ServiceBusEntityType;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.InOrder;
import org.mockito.Mockito;

import java.util.Collections;
import java.util.List;

import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doReturn;
Expand Down Expand Up @@ -34,6 +42,29 @@ void queueConfigured() {
verify(builder, times(1)).queueName("test-queue");
}

@Test
void serviceBusClientBuilderCustomizerAppliedAsLastStep() {
ServiceBusSenderClientTestProperties properties = createMinimalServiceProperties();
properties.setShareServiceBusClientBuilder(false);

ServiceBusClientBuilder rootBuilder = mock(ServiceBusClientBuilder.class);
@SuppressWarnings("unchecked")
AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder> customizer =
mock(AzureServiceClientBuilderCustomizer.class);

ServiceBusSenderClientBuilderFactory factory =
new CustomizerTestFactory(properties, Collections.singletonList(customizer), rootBuilder);

factory.build();

InOrder inOrder = Mockito.inOrder(rootBuilder, customizer);
inOrder.verify(rootBuilder, atLeast(1))
.clientOptions(ArgumentMatchers.any(ClientOptions.class));
inOrder.verify(customizer, times(1)).customize(rootBuilder);
inOrder.verify(rootBuilder, Mockito.never())
.clientOptions(ArgumentMatchers.any(ClientOptions.class));
}

@Override
protected ServiceBusSenderClientTestProperties createMinimalServiceProperties() {
ServiceBusSenderClientTestProperties properties = new ServiceBusSenderClientTestProperties();
Expand Down Expand Up @@ -100,4 +131,24 @@ protected ServiceBusClientBuilder getServiceBusClientBuilder() {
return this.serviceBusClientBuilder;
}
}

static class CustomizerTestFactory extends ServiceBusSenderClientBuilderFactory {
private final ServiceBusClientBuilder serviceBusClientBuilder;
CustomizerTestFactory(ServiceBusSenderClientTestProperties properties,
List<AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder>> customizers,
ServiceBusClientBuilder serviceBusClientBuilder) {
super(properties, customizers);
this.serviceBusClientBuilder = serviceBusClientBuilder;
}

@Override
public ServiceBusClientBuilder.ServiceBusSenderClientBuilder createBuilderInstance() {
return mock(ServiceBusClientBuilder.ServiceBusSenderClientBuilder.class);
}

@Override
protected ServiceBusClientBuilder getServiceBusClientBuilder() {
return this.serviceBusClientBuilder;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* specified {@link #destination}. The {@link #containerFactory} identifies the
* {@link MessageListenerContainerFactory} to use to build
* the Azure listener container. If not set, a <em>default</em> container factory is
* assumed to be available with a bean name of {@code eventHubsListenerContainerFactory}
* assumed to be available with a bean name of {@code azureEventHubsListenerContainerFactory}
* unless an explicit default has been provided through configuration.
*
* <p>Processing of {@code @EventHubsListener} annotations is performed by registering a
Expand Down
Loading
Loading