diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac2801d6..a28119c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ Agents provisioned before this release need `Agent365.Observability.OtelWrite` g
**Option B — CLI** (`a365 setup admin`) has been removed in this release. Use Option A above, or copy the PowerShell instructions printed in the `a365 setup all` summary output.
### Added
+- Version check: stable-channel users now see an informational notice when a newer preview release exists above the current stable version, without triggering the update-required banner.
- `setup requirements` Global Administrator path: when the well-known CLI client app is not found in a new tenant, Global Admins are prompted to create the app and grant admin consent automatically (enter an app ID or type `C` to create).
- `--authmode obo|s2s|both` option on `setup all` — controls how the agent identity service principal receives permissions:
- `obo` (default): principal-scoped delegated grants (`consentType: "Principal"`); no Global Administrator required.
@@ -38,6 +39,10 @@ Agents provisioned before this release need `Agent365.Observability.OtelWrite` g
- Messaging endpoint row added to `a365 setup all` summary output, with "registered"/"reused"/"skipped (non-M365)"/"manual config required"/"failed" states. When registration can't complete, the summary surfaces an "Action Required" entry with the Teams Developer Portal URL so the user knows exactly what to do next.
- Defensive fallback when the server rejects the new request with a known contract-mismatch signature — the CLI logs `"Automated messaging endpoint registration is not available for this tenant yet. You'll need to configure it manually."` and directs the user to the Teams Developer Portal. Same user-facing path is reused when registration fails because the signed-in user is not a blueprint owner.
+### Fixed
+- `setup all --agent-name` re-runs no longer create a duplicate agent registration: the CLI now reads `agentRegistrationId` from `a365.generated.config.json` (when present) and checks for an existing registration before posting a new one.
+- `setup all` now skips agent registration with a clear warning when the agent identity ID is not available, instead of silently sending an invalid request. Retry with `a365 setup all --agent-registration-only` once the identity is ready.
+
### Removed
- `a365 config` command family (`config init`, `config display`, `config permissions`) — replaced by `a365 setup all --agent-name` and `a365 setup permissions custom`.
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/AllSubcommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/AllSubcommand.cs
index 3ba09fa9..84304820 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/AllSubcommand.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/AllSubcommand.cs
@@ -258,6 +258,28 @@ public static Command CreateCommand(
else
await WriteBootstrapConfigFileAsync(nonDwConfig, config.FullName, logger);
}
+
+ // Merge AgentRegistrationId from the existing generated config (if present) so
+ // re-running with --agent-name does not create a duplicate registration. Blueprint
+ // and identity are found by display name, but registration has no lookup endpoint —
+ // the stored ID is the only idempotency key available for the registration step.
+ var bootstrapGenPath = Path.Combine(
+ config.DirectoryName ?? Environment.CurrentDirectory,
+ "a365.generated.config.json");
+ if (File.Exists(bootstrapGenPath) && string.IsNullOrWhiteSpace(nonDwConfig.AgentRegistrationId))
+ {
+ try
+ {
+ var genConfig = await configService.LoadAsync(config.FullName, bootstrapGenPath);
+ if (!string.IsNullOrWhiteSpace(genConfig.AgentRegistrationId))
+ nonDwConfig.AgentRegistrationId = genConfig.AgentRegistrationId;
+ }
+ catch (OperationCanceledException) { throw; }
+ catch (Exception ex)
+ {
+ logger.LogDebug(ex, "Could not merge generated config in bootstrap mode; proceeding without stored registration ID.");
+ }
+ }
}
}
else
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/NonDwBlueprintSetupOrchestrator.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/NonDwBlueprintSetupOrchestrator.cs
index a900330e..2a550fef 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/NonDwBlueprintSetupOrchestrator.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/NonDwBlueprintSetupOrchestrator.cs
@@ -445,6 +445,19 @@ private static async Task ExecuteAgentIdentityAndRegistrationAsync(
ctx.Logger.LogInformation("");
ctx.Logger.LogInformation("Registering agent...");
+ if (string.IsNullOrWhiteSpace(ctx.Config.AgenticAppId))
+ {
+ var registrationSkippedMessage =
+ "Agent registration failed: agent identity ID is not available. " +
+ "Ensure the agent identity was created successfully, then retry with: a365 setup all --agent-registration-only";
+ ctx.Results.Warnings.Add(registrationSkippedMessage);
+ using (ctx.Logger.Indent())
+ ctx.Logger.LogWarning(registrationSkippedMessage);
+ ctx.Results.AgentRegistrationFailed = true;
+ }
+ else
+ {
+
// If a registration ID is already stored, verify it still exists before skipping creation.
string? registrationId = null;
bool registrationAlreadyExisted = false;
@@ -521,6 +534,8 @@ private static async Task ExecuteAgentIdentityAndRegistrationAsync(
ctx.Logger.LogWarning("Agent registration failed via Graph copilot/agentRegistrations API.");
}
+ } // end else (AgenticAppId present)
+
// Step 6.5: Messaging endpoint registration — --m365 gated; no-op for non-M365 agents.
await AllSubcommand.ExecuteMessagingEndpointStepAsync(ctx);
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Models/VersionCheckModels.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Models/VersionCheckModels.cs
index e5be9467..d7c40d10 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Models/VersionCheckModels.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Models/VersionCheckModels.cs
@@ -15,9 +15,10 @@ public record VersionCheckResult(
bool UpdateAvailable,
string? CurrentVersion,
string? LatestVersion,
- string? UpdateCommand);
+ string? UpdateCommand,
+ string? NewerPreviewVersion = null);
///
/// On-disk cache envelope for the version check result, keyed by fetch timestamp.
///
-public record VersionCheckCache(DateTimeOffset CachedAt, string? LatestVersion);
+public record VersionCheckCache(DateTimeOffset CachedAt, string? LatestVersion, string? NewerPreviewVersion = null);
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Program.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Program.cs
index 75b5c1d0..e5341a3d 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Program.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Program.cs
@@ -105,6 +105,13 @@ await Task.WhenAll(
startupLogger.LogWarning("To update, run: {Command}", result.UpdateCommand);
startupLogger.LogWarning("");
}
+ else if (result.NewerPreviewVersion is not null)
+ {
+ startupLogger.LogInformation("");
+ startupLogger.LogInformation("A preview release is also available: {Preview}", result.NewerPreviewVersion);
+ startupLogger.LogInformation("To try it: {Command}", Services.Internal.VersionCheckHelper.GetUpdateCommand(result.NewerPreviewVersion));
+ startupLogger.LogInformation("");
+ }
}
catch (OperationCanceledException)
{
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/Internal/VersionCheckHelper.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/Internal/VersionCheckHelper.cs
index caaddd89..df6b258f 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/Internal/VersionCheckHelper.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/Internal/VersionCheckHelper.cs
@@ -115,4 +115,54 @@ internal static string GetUpdateCommand(string version)
? $"{baseCommand} --prerelease"
: baseCommand;
}
+
+ ///
+ /// Selects the primary latest version and an optional newer preview version from a list of
+ /// all NuGet versions, applying channel-aware filtering based on the current version.
+ ///
+ /// Stable users see only stable versions as their primary update target. If a newer preview
+ /// exists above the latest stable, it is returned separately as an informational nudge.
+ /// Preview users see the globally highest version (preview or stable) with no secondary nudge.
+ ///
+ ///
+ internal static (string? Primary, string? NewerPreview) SelectLatestVersions(
+ IEnumerable allVersions, string currentVersion)
+ {
+ bool currentIsPreview = currentVersion.Contains("preview", StringComparison.OrdinalIgnoreCase);
+
+ var allParsed = allVersions
+ .Select(v => new { Original = v, Parsed = TryParseVersion(v) })
+ .Where(v => v.Parsed != null)
+ .OrderByDescending(v => v.Parsed)
+ .ToList();
+
+ // Primary: stable-only for stable users; unrestricted for preview users
+ var primary = allParsed
+ .Where(v => currentIsPreview || !v.Original.Contains("preview", StringComparison.OrdinalIgnoreCase))
+ .FirstOrDefault()?.Original;
+
+ // Informational nudge: surface the latest preview when a stable user is already on the
+ // latest stable but a newer preview exists above it.
+ // Use base-version comparison (not TryParseVersion) so that a preview of the same base
+ // (e.g., "1.1.0-preview.50") is never treated as newer than its GA ("1.1.0").
+ string? newerPreview = null;
+ if (!currentIsPreview && primary != null)
+ {
+ var latestPreview = allParsed
+ .Where(v => v.Original.Contains("preview", StringComparison.OrdinalIgnoreCase))
+ .FirstOrDefault()?.Original;
+
+ if (latestPreview != null)
+ {
+ var primaryBase = TryParseVersion(primary.Split('-')[0]);
+ var previewBase = TryParseVersion(latestPreview.Split('-')[0]);
+ // Only nudge when the preview's base version is strictly higher than the GA base.
+ // This prevents "1.1.0-preview.50" from appearing as newer than "1.1.0".
+ if (primaryBase != null && previewBase != null && previewBase > primaryBase)
+ newerPreview = latestPreview;
+ }
+ }
+
+ return (primary, newerPreview);
+ }
}
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/VersionCheckService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/VersionCheckService.cs
index 1de90fd4..f6bbcf7f 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/VersionCheckService.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/VersionCheckService.cs
@@ -43,12 +43,12 @@ public async Task CheckForUpdatesAsync(CancellationToken can
_logger.LogDebug("Checking for updates...");
- var latestVersion = GetCachedLatestVersion();
+ var (latestVersion, newerPreviewVersion) = GetCachedLatestVersion();
if (latestVersion == null)
{
- latestVersion = await GetLatestVersionFromNuGetAsync(cancellationToken);
+ (latestVersion, newerPreviewVersion) = await GetLatestVersionFromNuGetAsync(cancellationToken);
if (latestVersion != null)
- SaveCache(new VersionCheckCache(DateTimeOffset.UtcNow, latestVersion));
+ SaveCache(new VersionCheckCache(DateTimeOffset.UtcNow, latestVersion, newerPreviewVersion));
}
if (latestVersion == null)
@@ -65,7 +65,7 @@ public async Task CheckForUpdatesAsync(CancellationToken can
_logger.LogDebug("Running latest version: {Current}", _currentVersion);
return new VersionCheckResult(updateAvailable, _currentVersion, latestVersion,
- VersionCheckHelper.GetUpdateCommand(latestVersion));
+ VersionCheckHelper.GetUpdateCommand(latestVersion), newerPreviewVersion);
}
catch (OperationCanceledException)
{
@@ -79,7 +79,7 @@ public async Task CheckForUpdatesAsync(CancellationToken can
}
}
- private async Task GetLatestVersionFromNuGetAsync(CancellationToken cancellationToken)
+ private async Task<(string? Primary, string? NewerPreview)> GetLatestVersionFromNuGetAsync(CancellationToken cancellationToken)
{
try
{
@@ -89,7 +89,7 @@ public async Task CheckForUpdatesAsync(CancellationToken can
if (!response.IsSuccessStatusCode)
{
_logger.LogDebug("NuGet API returned {StatusCode}", response.StatusCode);
- return null;
+ return (null, null);
}
var content = await response.Content.ReadAsStringAsync(cancellationToken);
@@ -99,22 +99,15 @@ public async Task CheckForUpdatesAsync(CancellationToken can
if (versionResponse?.Versions == null || versionResponse.Versions.Length == 0)
{
_logger.LogDebug("No versions found in NuGet response");
- return null;
+ return (null, null);
}
- // Sort semantically — NuGet returns chronological order, but we sort to be safe
- var sorted = versionResponse.Versions
- .Select(v => new { Original = v, Parsed = VersionCheckHelper.TryParseVersion(v) })
- .Where(v => v.Parsed != null)
- .OrderByDescending(v => v.Parsed)
- .ToList();
-
- return sorted.Count == 0 ? null : sorted[0].Original;
+ return VersionCheckHelper.SelectLatestVersions(versionResponse.Versions, _currentVersion);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogDebug(ex, "Failed to query NuGet API");
- return null;
+ return (null, null);
}
}
@@ -131,33 +124,33 @@ private bool IsNewerVersion(string current, string latest)
}
}
- private string? GetCachedLatestVersion()
+ private (string? Primary, string? NewerPreview) GetCachedLatestVersion()
{
try
{
var path = GetCacheFilePath();
if (!File.Exists(path))
- return null;
+ return (null, null);
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var cache = JsonSerializer.Deserialize(File.ReadAllText(path), options);
if (cache == null)
- return null;
+ return (null, null);
if (DateTimeOffset.UtcNow - cache.CachedAt >= TimeSpan.FromHours(CacheTtlHours))
{
_logger.LogDebug("Version cache expired (cached at {CachedAt})", cache.CachedAt);
- return null;
+ return (null, null);
}
_logger.LogDebug("Using cached version {Version} (cached at {CachedAt})", cache.LatestVersion, cache.CachedAt);
- return cache.LatestVersion;
+ return (cache.LatestVersion, cache.NewerPreviewVersion);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Could not load version cache");
- return null;
+ return (null, null);
}
}
diff --git a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/VersionCheckServiceTests.cs b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/VersionCheckServiceTests.cs
index c9c79151..f1d6d608 100644
--- a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/VersionCheckServiceTests.cs
+++ b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/VersionCheckServiceTests.cs
@@ -105,6 +105,33 @@ public void ParseVersion_ComparesVersionsCorrectly(string current, string latest
isNewer.Should().Be(expectedNewerAvailable);
}
+ [Theory]
+ // Stable user — no preview above GA: latest stable is 1.2.0, preview is older → no nudge
+ [InlineData("1.2.0", new[] { "1.1.165-preview", "1.2.0" }, "1.2.0", null)]
+ // Stable user — newer preview above GA: 1.3.0-preview.1 exists → nudge with preview version
+ [InlineData("1.2.0", new[] { "1.2.0", "1.3.0-preview.1" }, "1.2.0", "1.3.0-preview.1")]
+ // Preview user — GA is above preview: pick GA, no secondary nudge
+ [InlineData("1.1.165-preview", new[] { "1.1.165-preview", "1.2.0" }, "1.2.0", null)]
+ // Preview user — newer preview above GA: pick highest preview, no secondary nudge
+ [InlineData("1.1.165-preview", new[] { "1.1.165-preview", "1.2.0", "1.3.0-preview.1" }, "1.3.0-preview.1", null)]
+ // Stable user — preview of same base as GA exists (e.g., "1.1.0-preview.50" alongside "1.1.0")
+ // → GA is primary, no nudge (same-base preview must not sort above its own GA)
+ [InlineData("1.1.0", new[] { "1.1.0", "1.1.0-preview.50" }, "1.1.0", null)]
+ // Stable user on GA with both same-base preview and a higher-base preview → nudge the higher-base preview only
+ [InlineData("1.1.0", new[] { "1.1.0", "1.1.0-preview.50", "1.2.0-preview.1" }, "1.1.0", "1.2.0-preview.1")]
+ public void SelectLatestVersions_AppliesChannelAwareFiltering(
+ string currentVersion, string[] nugetVersions, string expectedPrimary, string? expectedNewerPreview)
+ {
+ // Act
+ var (primary, newerPreview) = VersionCheckHelper.SelectLatestVersions(nugetVersions, currentVersion);
+
+ // Assert
+ primary.Should().Be(expectedPrimary,
+ because: "the primary latest should respect the channel of the current version");
+ newerPreview.Should().Be(expectedNewerPreview,
+ because: "the informational preview nudge should only appear for stable users when a newer preview exists");
+ }
+
[Theory]
[InlineData("CI")]
[InlineData("TF_BUILD")]
diff --git a/src/version.json b/src/version.json
index 3a3b3829..a301f6e9 100644
--- a/src/version.json
+++ b/src/version.json
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
- "version": "1.1-preview",
+ "version": "1.1",
"assemblyVersion": {
"precision": "revision"
},