Skip to content

Commit 48f96aa

Browse files
Add non-blocking startup version check for preview releases (#168)
* Initial plan * Add version check service with tests Co-authored-by: sellakumaran <147754920+sellakumaran@users.noreply.github.com> * Address code review feedback: improve test and version sorting Co-authored-by: sellakumaran <147754920+sellakumaran@users.noreply.github.com> * Make ParseVersion internal and improve documentation Co-authored-by: sellakumaran <147754920+sellakumaran@users.noreply.github.com> * Improve update messaging & add release-drafter workflow - Enhance update notification in Program.cs for clarity, conciseness, and changelog link - Dynamically generate update command with --prerelease flag for preview versions in VersionCheckService - Refine version parsing to handle multiple preview formats - Add and configure release-drafter.yml for automated GitHub release notes and draft releases, including workflow triggers and label-based categorization - Improve code comments and documentation for maintainability * Disable test parallelization, improve disposal and parsing - Updated copilot-instructions.md with guidance on disabling test parallelization for tests that modify environment variables or shared resources, and expanded resource management instructions for IDisposable objects. - Refactored VersionCheckService to properly dispose HttpResponseMessage using a using statement, improved preview version parsing with a ternary operator, and streamlined version component padding logic. - Marked VersionCheckServiceTests with [CollectionDefinition] and [Collection] to ensure tests run sequentially, preventing race conditions when modifying global environment variables. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sellakumaran <147754920+sellakumaran@users.noreply.github.com> Co-authored-by: Sellakumaran Kanagarathnam <sellak@microsoft.com>
1 parent ae5347c commit 48f96aa

8 files changed

Lines changed: 618 additions & 0 deletions

File tree

.github/copilot-instructions.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,56 @@
4141
- Even in mock/test handlers, follow proper disposal patterns
4242
- Consider using `using` statements or ensure test handlers dispose responses
4343
- This applies to all `IDisposable` test objects to avoid analyzer warnings
44+
- **Disable parallel execution for tests with shared state**:
45+
- Tests that modify environment variables must disable parallelization
46+
- Tests that access shared file system resources must run sequentially
47+
- Use `[CollectionDefinition("TestName", DisableParallelization = true)]` pattern
48+
- Add `[Collection("TestName")]` attribute to test class
49+
- **Pattern to follow**:
50+
```csharp
51+
/// <summary>
52+
/// Tests must run sequentially because they modify environment variables.
53+
/// </summary>
54+
[CollectionDefinition("EnvTests", DisableParallelization = true)]
55+
public class EnvTestCollection { }
56+
57+
[Collection("EnvTests")]
58+
public class MyTests
59+
{
60+
[Fact]
61+
public void Test_ModifiesEnvironmentVariable()
62+
{
63+
Environment.SetEnvironmentVariable("VAR", "value");
64+
try
65+
{
66+
// Test logic
67+
}
68+
finally
69+
{
70+
Environment.SetEnvironmentVariable("VAR", null);
71+
}
72+
}
73+
}
74+
```
75+
76+
### Resource Management
77+
- **Always dispose IDisposable objects** to prevent resource leaks:
78+
- `HttpResponseMessage` returned by `HttpClient.GetAsync()`, `PostAsync()`, etc. must be disposed
79+
- Use `using` statements for automatic disposal: `using var response = await httpClient.GetAsync(...);`
80+
- Even when checking `IsSuccessStatusCode` or reading content, wrap in `using`
81+
- This applies to all HTTP responses, streams, file handles, and other disposable resources
82+
- **Pattern to follow**:
83+
```csharp
84+
// CORRECT - Dispose HttpResponseMessage
85+
using var response = await httpClient.GetAsync(url, cancellationToken);
86+
if (!response.IsSuccessStatusCode) { return null; }
87+
var content = await response.Content.ReadAsStringAsync(cancellationToken);
88+
89+
// INCORRECT - Resource leak
90+
var response = await httpClient.GetAsync(url, cancellationToken);
91+
if (!response.IsSuccessStatusCode) { return null; }
92+
var content = await response.Content.ReadAsStringAsync(cancellationToken);
93+
```
4494

4595
### Output and Logging
4696
- No emojis or special characters in logs, output, or comments

.github/release-drafter.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name-template: 'v$RESOLVED_VERSION'
2+
tag-template: 'v$RESOLVED_VERSION'
3+
prerelease: true
4+
5+
categories:
6+
- title: '🚀 Features'
7+
labels:
8+
- 'feature'
9+
- 'enhancement'
10+
- title: '🐛 Bug Fixes'
11+
labels:
12+
- 'bug'
13+
- 'fix'
14+
- title: '🔧 Maintenance'
15+
labels:
16+
- 'chore'
17+
- 'dependencies'
18+
- 'refactor'
19+
- title: '📚 Documentation'
20+
labels:
21+
- 'documentation'
22+
- 'docs'
23+
- title: '⚡ Performance'
24+
labels:
25+
- 'performance'
26+
- 'perf'
27+
28+
change-template: '- $TITLE (#$NUMBER)'
29+
change-title-escapes: '\<*_&'
30+
31+
version-resolver:
32+
minor:
33+
labels:
34+
- 'feature'
35+
- 'enhancement'
36+
patch:
37+
labels:
38+
- 'bug'
39+
- 'fix'
40+
- 'chore'
41+
- 'dependencies'
42+
- 'documentation'
43+
- 'performance'
44+
default: patch
45+
46+
autolabeler:
47+
- label: 'documentation'
48+
files:
49+
- '*.md'
50+
- 'docs/**/*'
51+
- label: 'bug'
52+
branch:
53+
- '/fix\/.+/'
54+
title:
55+
- '/fix/i'
56+
- '/bug/i'
57+
- label: 'feature'
58+
branch:
59+
- '/feature\/.+/'
60+
title:
61+
- '/feat/i'
62+
- '/feature/i'
63+
64+
template: |
65+
## What's Changed
66+
67+
$CHANGES
68+
69+
**Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Release Drafter
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
types: [opened, reopened, synchronize, closed]
9+
10+
permissions:
11+
contents: write
12+
pull-requests: read
13+
14+
jobs:
15+
update_release_draft:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: release-drafter/release-drafter@v6
19+
env:
20+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.Agents.A365.DevTools.Cli.Models;
5+
6+
/// <summary>
7+
/// Response from NuGet V3 API containing available versions.
8+
/// </summary>
9+
public record NuGetVersionResponse(string[] Versions);
10+
11+
/// <summary>
12+
/// Result of a version check operation.
13+
/// </summary>
14+
public record VersionCheckResult(
15+
bool UpdateAvailable,
16+
string? CurrentVersion,
17+
string? LatestVersion,
18+
string? UpdateCommand);

src/Microsoft.Agents.A365.DevTools.Cli/Program.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,34 @@ static async Task<int> Main(string[] args)
4848
ConfigureServices(services, logLevel, logFilePath);
4949
var serviceProvider = services.BuildServiceProvider();
5050

51+
// Check for updates (non-blocking, with timeout)
52+
try
53+
{
54+
var versionCheckService = serviceProvider.GetRequiredService<IVersionCheckService>();
55+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
56+
var result = await versionCheckService.CheckForUpdatesAsync(cts.Token);
57+
58+
if (result.UpdateAvailable)
59+
{
60+
startupLogger.LogWarning("");
61+
startupLogger.LogWarning("A newer version is available with bug fixes and improvements.");
62+
startupLogger.LogWarning(" Current: {Current}", result.CurrentVersion);
63+
startupLogger.LogWarning(" Latest: {Latest}", result.LatestVersion);
64+
startupLogger.LogWarning("");
65+
startupLogger.LogWarning("What's new: https://github.com/microsoft/Agent365-devTools/releases");
66+
startupLogger.LogWarning("To update, run: {Command}", result.UpdateCommand);
67+
startupLogger.LogWarning("");
68+
}
69+
}
70+
catch (OperationCanceledException)
71+
{
72+
startupLogger.LogDebug("Version check timed out");
73+
}
74+
catch (Exception ex)
75+
{
76+
startupLogger.LogDebug(ex, "Version check failed: {Message}", ex.Message);
77+
}
78+
5179
// Create root command
5280
var rootCommand = new RootCommand($"Agent 365 Developer Tools CLI v{version} – Build, deploy, and manage AI agents for Microsoft 365.");
5381

@@ -167,6 +195,7 @@ private static void ConfigureServices(IServiceCollection services, LogLevel mini
167195
services.AddSingleton<CommandExecutor>();
168196
services.AddSingleton<AuthenticationService>();
169197
services.AddSingleton<IClientAppValidator, ClientAppValidator>();
198+
services.AddSingleton<IVersionCheckService, VersionCheckService>();
170199

171200
// Add Microsoft Agent 365 Tooling Service with environment detection
172201
services.AddSingleton<IAgent365ToolingService>(provider =>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Agents.A365.DevTools.Cli.Models;
5+
6+
namespace Microsoft.Agents.A365.DevTools.Cli.Services;
7+
8+
/// <summary>
9+
/// Service for checking if newer versions of the CLI are available.
10+
/// </summary>
11+
public interface IVersionCheckService
12+
{
13+
/// <summary>
14+
/// Checks if a newer version of the CLI is available on NuGet.
15+
/// </summary>
16+
/// <param name="cancellationToken">Cancellation token to abort the check.</param>
17+
/// <returns>Version check result indicating if an update is available.</returns>
18+
Task<VersionCheckResult> CheckForUpdatesAsync(CancellationToken cancellationToken = default);
19+
}

0 commit comments

Comments
 (0)