Skip to content

feat: add AzureCliCredential fallback auth with preflight check#37

Merged
woutervanranst merged 6 commits into
masterfrom
feat/azure-cli-auth
Apr 1, 2026
Merged

feat: add AzureCliCredential fallback auth with preflight check#37
woutervanranst merged 6 commits into
masterfrom
feat/azure-cli-auth

Conversation

@woutervanranst
Copy link
Copy Markdown
Owner

@woutervanranst woutervanranst commented Apr 1, 2026

Summary

  • Add AzureCliCredential as a fallback authentication mechanism — users can now run arius archive/restore/ls without an account key as long as they've run az login with the appropriate RBAC role
  • Add a preflight connectivity check on every invocation (write+delete probe blob for archive; ExistsAsync for restore/ls) that translates auth/connectivity failures into clear, actionable error messages before the pipeline starts
  • Add a global try/catch/finally in Program.cs for unhandled exceptions using AnsiConsole.WriteException (formatted, no raw stack dumps) + guaranteed Log.CloseAndFlush()

Credential resolution order

  1. --key / -k CLI flag → StorageSharedKeyCredential
  2. ARIUS_KEY env var → StorageSharedKeyCredential
  3. dotnet user-secretsStorageSharedKeyCredential
  4. (fallback) AzureCliCredential via az login

Changes

  • CliBuilder.csPreflightMode enum, PreflightException, async BuildProductionServices, credential branching, preflight check with error translation, updated --key description
  • ArchiveVerb.cs / RestoreVerb.cs / LsVerb.cs — updated to async factory delegate, removed key-null guard, added PreflightException catch
  • Program.cs — global try/catch/finally
  • Arius.Cli.csproj / Directory.Packages.props — add Azure.Identity, remove dead FluentValidation + FluentResults dependencies
  • CliTests.cs — updated test harness to new async factory signature

Test results

All tests pass: 137 CLI tests + 122 Core tests.

Summary by CodeRabbit

  • New Features

    • Added support for Azure CLI authentication when storage account key is omitted
    • Implemented preflight validation on every command invocation for early connectivity detection
  • Bug Fixes & Improvements

    • Enhanced error messages with user-friendly formatting instead of stack traces
    • Updated --key option help text to indicate Azure CLI login as an alternative
    • Improved global exception handling with formatted console output and log flushing
  • Chores

    • Removed unused dependencies from project files

- Add Azure.Identity package; remove unused FluentValidation and FluentResults
- Implement credential resolution: key sources (flag/env/secrets) fall back to AzureCliCredential
- Add preflight connectivity check: ReadWrite (archive) uploads/deletes probe blob, ReadOnly (restore/ls) calls ExistsAsync
- Translate preflight failures (403, 404, CredentialUnavailable) into PreflightException with actionable messages
- Make BuildProductionServices async; update factory delegate to Func<..., PreflightMode, Task<IServiceProvider>>
- Add global try/catch/finally in Program.cs for unhandled exceptions via AnsiConsole.WriteException
- Update --key option description to mention az login fallback
- Update CLI tests for new async factory signature
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 1, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c02f0552-8e73-46a8-b023-672dc9be535e

📥 Commits

Reviewing files that changed from the base of the PR and between 0926a01 and 73d0469.

📒 Files selected for processing (8)
  • openspec/changes/archive/2026-04-01-azure-cli-auth/.openspec.yaml
  • openspec/changes/archive/2026-04-01-azure-cli-auth/design.md
  • openspec/changes/archive/2026-04-01-azure-cli-auth/proposal.md
  • openspec/changes/archive/2026-04-01-azure-cli-auth/specs/azure-cli-auth/spec.md
  • openspec/changes/archive/2026-04-01-azure-cli-auth/specs/cli/spec.md
  • openspec/changes/archive/2026-04-01-azure-cli-auth/tasks.md
  • openspec/specs/azure-cli-auth/spec.md
  • openspec/specs/cli/spec.md

📝 Walkthrough

Walkthrough

This PR implements Azure CLI-based authentication fallback, introduces preflight connectivity validation, and extracts blob service construction to a new BlobServiceFactory in Arius.AzureBlob. The factory asynchronously handles credential selection (shared-key or token-based), executes preflight probes, and classifies failures into structured error types.

Changes

Cohort / File(s) Summary
CLI Core Architecture
src/Arius.Cli/CliBuilder.cs, src/Arius.Cli/Program.cs
Updated service-provider factory delegate signature to async Func<string, string?, string?, string, PreflightMode, Task<IServiceProvider>> with nullable key/passphrase parameters; refactored BuildProductionServices to delegate blob service creation to BlobServiceFactory.CreateAsync and introduced credential fallback logic (shared-key when key present, otherwise AzureCliCredential); added top-level exception handling in Program.cs with Spectre.Console rendering and Serilog flushing.
CLI Verb Builders
src/Arius.Cli/Commands/Archive/ArchiveVerb.cs, src/Arius.Cli/Commands/Ls/LsVerb.cs, src/Arius.Cli/Commands/Restore/RestoreVerb.cs
Updated all three verbs to accept async factory delegate; removed immediate key-null validation; wrapped service provider creation in try/catch to handle PreflightException; added structured error formatting based on PreflightErrorKind and AuthMode; returns exit code 1 on preflight failure.
Azure Blob Service Layer
src/Arius.AzureBlob/BlobServiceFactory.cs, src/Arius.AzureBlob/Preflight.cs
Introduced new BlobServiceFactory static class with async CreateAsync method handling credential validation, preflight probes (ReadWrite uploads/deletes probe blob; ReadOnly checks container existence), and error classification; added PreflightMode enum and PreflightException with structured fields (ErrorKind, AuthMode, AccountName, ContainerName, StatusCode, inner exception) for domain-specific error handling.
CLI Tests
src/Arius.Cli.Tests/CliTests.cs
Updated test harness to expect async factory with trailing parameter; replaced integration test for missing key with direct unit assertion on CliBuilder.ResolveKey(null, "acct") returning null; adjusted Task.FromResult wrapping in factory delegate.
Project Dependencies
src/Arius.Core/Arius.Core.csproj, src/Arius.AzureBlob/Arius.AzureBlob.csproj, src/Directory.Packages.props
Removed unused FluentResults and FluentValidation from Arius.Core; added Azure.Identity (1.13.2) to centralized version management; added Azure.Identity package reference to Arius.AzureBlob.
Architecture Tests
src/Arius.Architecture.Tests/DependencyTests.cs
Widened existing Core_Should_Not_Reference_Azure test to forbid all Azure.* namespaces (not just Azure.Storage); added new Cli_Should_Not_Reference_Azure test enforcing same restriction on CLI assembly.
Documentation & Specifications
openspec/changes/archive/2026-04-01-blob-service-factory/*, openspec/changes/archive/2026-04-01-azure-cli-auth/*, openspec/specs/*
Added comprehensive design documents, proposals, and specifications covering: blob service factory behavior with preflight modes and error classification; Azure CLI authentication fallback and credential precedence; architecture boundary enforcement; CLI error formatting requirements; DI registration patterns; and task breakdowns for implementation.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/CLI
    participant CliBuilder
    participant BlobServiceFactory
    participant Azure as Azure SDK
    participant Storage as BlobStorageService

    Client->>CliBuilder: BuildProductionServices(accountName, key?, passphrase, containerName, preflightMode)
    activate CliBuilder
    
    alt key provided
        CliBuilder->>BlobServiceFactory: CreateAsync(StorageSharedKeyCredential, ...)
    else no key
        CliBuilder->>BlobServiceFactory: CreateAsync(AzureCliCredential, ...)
    end
    
    activate BlobServiceFactory
    BlobServiceFactory->>Azure: new BlobServiceClient(credential)
    BlobServiceFactory->>Azure: container = GetBlobContainerClient()
    
    alt preflightMode == ReadWrite
        BlobServiceFactory->>Azure: Upload probe blob
        BlobServiceFactory->>Azure: Delete probe blob
    else preflightMode == ReadOnly
        BlobServiceFactory->>Azure: container.ExistsAsync()
    end
    
    alt Preflight succeeds
        BlobServiceFactory-->>Storage: new AzureBlobStorageService(containerClient)
    else Credential unavailable
        BlobServiceFactory-->>BlobServiceFactory: throw PreflightException(CredentialUnavailable)
    else 403 Forbidden
        BlobServiceFactory-->>BlobServiceFactory: throw PreflightException(AccessDenied)
    else 404 Not Found
        BlobServiceFactory-->>BlobServiceFactory: throw PreflightException(ContainerNotFound)
    end
    
    deactivate BlobServiceFactory
    
    alt Preflight exception caught
        CliBuilder-->>Client: Return exit code 1 with formatted error
    else Success
        CliBuilder->>CliBuilder: Build DI container with IBlobStorageService
        CliBuilder-->>Client: Return Task<IServiceProvider>
    end
    
    deactivate CliBuilder
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #34: Updates Archive/Ls/Restore verb extractors and service-provider factory signature—the main PR further evolves that factory to async with preflight awareness and credential fallback.
  • PR #15: Modifies CliBuilder and CliTests harness setup—the main PR updates both the factory delegate signature and test expectations for the now-asynchronous factory.
  • PR #27: Refactors CliBuilder.BuildProductionServices DI construction—the main PR introduces async construction, credential fallback, and delegation to BlobServiceFactory within that same method.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding AzureCliCredential as a fallback authentication mechanism with a preflight connectivity check, which is the primary objective of this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/azure-cli-auth

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 1, 2026

Codecov Report

❌ Patch coverage is 13.47150% with 167 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.24%. Comparing base (67aea3c) to head (73d0469).
⚠️ Report is 9 commits behind head on master.

Files with missing lines Patch % Lines
src/Arius.AzureBlob/BlobServiceFactory.cs 0.00% 64 Missing ⚠️
src/Arius.Cli/Commands/Archive/ArchiveVerb.cs 22.85% 27 Missing ⚠️
src/Arius.Cli/Commands/Ls/LsVerb.cs 22.85% 27 Missing ⚠️
src/Arius.Cli/Commands/Restore/RestoreVerb.cs 22.85% 27 Missing ⚠️
src/Arius.AzureBlob/Preflight.cs 0.00% 9 Missing ⚠️
src/Arius.Cli/CliBuilder.cs 20.00% 8 Missing ⚠️
src/Arius.Cli/Program.cs 0.00% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #37      +/-   ##
==========================================
- Coverage   82.61%   79.24%   -3.37%     
==========================================
  Files          33       35       +2     
  Lines        3595     3763     +168     
  Branches      432      443      +11     
==========================================
+ Hits         2970     2982      +12     
- Misses        526      684     +158     
+ Partials       99       97       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/Arius.Cli.Tests/CliTests.cs (1)

359-367: Consider adding an integration test for PreflightException handling.

The original Archive_MissingKeyFromAllSources_ReturnsExitCode1 test exercised the full CLI path. The replacement unit test only verifies that ResolveKey returns null—it no longer tests the PreflightException catch block in the verb handlers.

While the unit test is valuable, consider adding a test that exercises the preflight failure path by configuring the harness factory to throw PreflightException:

[Test]
public async Task Archive_PreflightFailure_ReturnsExitCode1()
{
    var rootCommand = CliBuilder.BuildRootCommand(
        serviceProviderFactory: (_, _, _, _, _) => 
            throw new PreflightException("Simulated preflight failure"));
    
    var exitCode = await rootCommand.Parse("archive /data -a acct -c ctr").InvokeAsync();
    exitCode.ShouldBe(1);
}

As per coding guidelines, **/*.Tests/**: Focus on test coverage gaps.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli.Tests/CliTests.cs` around lines 359 - 367, Add an integration
test that exercises the CLI preflight failure path by constructing the root
command with a serviceProviderFactory that throws PreflightException (similar
intent to the removed Archive_MissingKeyFromAllSources_ReturnsExitCode1 test);
call CliBuilder.BuildRootCommand with a serviceProviderFactory delegate that
throws new PreflightException("Simulated preflight failure"), then parse and
InvokeAsync the archive command (e.g., rootCommand.Parse("archive /data -a acct
-c ctr").InvokeAsync()) and assert the returned exit code equals 1 so the
PreflightException catch block in the verb handlers is exercised.
openspec/changes/azure-cli-auth/design.md (1)

29-34: Add language specifier to fenced code block.

The code block documenting credential resolution precedence is missing a language identifier. Adding one improves rendering and addresses the markdownlint warning.

📝 Suggested fix
-```
+```text
 1. --key / -k CLI flag         → StorageSharedKeyCredential
 2. ARIUS_KEY env var           → StorageSharedKeyCredential
 3. User secrets                → StorageSharedKeyCredential
 4. AzureCliCredential          → TokenCredential (from az login)
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @openspec/changes/azure-cli-auth/design.md around lines 29 - 34, The fenced
code block listing credential resolution precedence is missing a language
identifier; update the code fence that contains the four lines starting with
"--key / -k CLI flag → StorageSharedKeyCredential" to include a language
specifier (use "text") so the block becomes text ... which will satisfy
markdownlint and improve rendering.


</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @src/Arius.Cli/Program.cs:

  • Around line 9-14: Exceptions thrown during command construction/parsing are
    logged via AnsiConsole.WriteException but Log.Fatal is a no-op because
    Log.Logger isn't initialized until ConfigureAuditLogging runs inside command
    handlers; to fix, initialize a minimal fallback Serilog logger (e.g., set
    Log.Logger via LoggerConfiguration) before entering the try block in
    Program.Main (so Log.Fatal will record pre-handler exceptions), or call a
    lightweight initialization method that sets a default sink and level and is
    later replaced by ConfigureAuditLogging; reference Log.Fatal, Log.Logger,
    ConfigureAuditLogging, BuildRootCommand and Parse to locate where to add the
    fallback initialization.

In @src/Directory.Packages.props:

  • Line 7: Update the PackageVersion entry for Azure.Identity in
    Directory.Packages.props by changing the PackageVersion element with
    Include="Azure.Identity" from Version="1.13.2" to Version="1.20.0"; after
    updating, run a package restore/build and run the test suite to catch any API
    changes, and review Azure.Identity 1.20.0 release notes for breaking changes or
    migration steps before merging.

Nitpick comments:
In @openspec/changes/azure-cli-auth/design.md:

  • Around line 29-34: The fenced code block listing credential resolution
    precedence is missing a language identifier; update the code fence that contains
    the four lines starting with "--key / -k CLI flag → StorageSharedKeyCredential"
    to include a language specifier (use "text") so the block becomes ```text ...

In `@src/Arius.Cli.Tests/CliTests.cs`:
- Around line 359-367: Add an integration test that exercises the CLI preflight
failure path by constructing the root command with a serviceProviderFactory that
throws PreflightException (similar intent to the removed
Archive_MissingKeyFromAllSources_ReturnsExitCode1 test); call
CliBuilder.BuildRootCommand with a serviceProviderFactory delegate that throws
new PreflightException("Simulated preflight failure"), then parse and
InvokeAsync the archive command (e.g., rootCommand.Parse("archive /data -a acct
-c ctr").InvokeAsync()) and assert the returned exit code equals 1 so the
PreflightException catch block in the verb handlers is exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1e1877b6-87c5-4501-865d-d358016e4fc9

📥 Commits

Reviewing files that changed from the base of the PR and between 67aea3c and 78034cf.

📒 Files selected for processing (15)
  • openspec/changes/azure-cli-auth/.openspec.yaml
  • openspec/changes/azure-cli-auth/design.md
  • openspec/changes/azure-cli-auth/proposal.md
  • openspec/changes/azure-cli-auth/specs/azure-cli-auth/spec.md
  • openspec/changes/azure-cli-auth/specs/cli/spec.md
  • openspec/changes/azure-cli-auth/tasks.md
  • src/Arius.Cli.Tests/CliTests.cs
  • src/Arius.Cli/Arius.Cli.csproj
  • src/Arius.Cli/CliBuilder.cs
  • src/Arius.Cli/Commands/Archive/ArchiveVerb.cs
  • src/Arius.Cli/Commands/Ls/LsVerb.cs
  • src/Arius.Cli/Commands/Restore/RestoreVerb.cs
  • src/Arius.Cli/Program.cs
  • src/Arius.Core/Arius.Core.csproj
  • src/Directory.Packages.props
💤 Files with no reviewable changes (1)
  • src/Arius.Core/Arius.Core.csproj

Comment thread src/Arius.Cli/Program.cs
Comment on lines +9 to +14
catch (Exception ex)
{
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks);
Log.Fatal(ex, "Unhandled exception");
return 1;
}
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.

⚠️ Potential issue | 🟡 Minor

Log.Fatal may be a no-op for exceptions occurring before command handler execution.

Based on the code flow in CliBuilder.cs and the verb handlers, ConfigureAuditLogging (which initializes Log.Logger) is called inside command handlers—after BuildRootCommand() and Parse() complete. If an exception occurs during command construction or parsing, Log.Logger remains in its default no-op state, and this Log.Fatal call will silently discard the log entry.

The user still sees the exception via AnsiConsole.WriteException, so this is a minor observability gap rather than a functional bug.

💡 Potential fix: Initialize a minimal fallback logger before the try block
 using Arius.Cli;
 using Serilog;
 using Spectre.Console;

+// Minimal bootstrap logger for exceptions before command handlers configure audit logging
+Log.Logger = new LoggerConfiguration()
+    .WriteTo.Console(restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Error)
+    .CreateBootstrapLogger();
+
 try
 {
     return await CliBuilder.BuildRootCommand().Parse(args).InvokeAsync();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli/Program.cs` around lines 9 - 14, Exceptions thrown during
command construction/parsing are logged via AnsiConsole.WriteException but
Log.Fatal is a no-op because Log.Logger isn't initialized until
ConfigureAuditLogging runs inside command handlers; to fix, initialize a minimal
fallback Serilog logger (e.g., set Log.Logger via LoggerConfiguration) before
entering the try block in Program.Main (so Log.Fatal will record pre-handler
exceptions), or call a lightweight initialization method that sets a default
sink and level and is later replaced by ConfigureAuditLogging; reference
Log.Fatal, Log.Logger, ConfigureAuditLogging, BuildRootCommand and Parse to
locate where to add the fallback initialization.

<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Azure.Identity" Version="1.13.2" />
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

What is the latest version of Azure.Identity NuGet package and are there any known vulnerabilities in version 1.13.2?

💡 Result:

The latest version of the Azure.Identity NuGet package is 1.20.0, released on 2026-03-30. There are no known vulnerabilities in version 1.13.2. Known CVEs affecting Azure.Identity for .NET (CVE-2023-36414 fixed in 1.10.2+, CVE-2024-29992 fixed in 1.11.0+) predate 1.13.2 (released 2025-01-15), and CVE-2024-35255 affects versions before 1.11.4 but scanners report false positives for later versions like 1.13.2; scans of 1.14.0 show no vulnerabilities, and NuGet does not flag 1.13.2.

Citations:


Consider upgrading Azure.Identity to version 1.20.0.

Version 1.13.2 is secure with no known vulnerabilities, but version 1.20.0 (released 2026-03-30) is the latest available release. Evaluate whether upgrading would benefit the project, as it may include performance improvements, new features, or additional security enhancements beyond the baseline.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Directory.Packages.props` at line 7, Update the PackageVersion entry for
Azure.Identity in Directory.Packages.props by changing the PackageVersion
element with Include="Azure.Identity" from Version="1.13.2" to Version="1.20.0";
after updating, run a package restore/build and run the test suite to catch any
API changes, and review Azure.Identity 1.20.0 release notes for breaking changes
or migration steps before merging.

Move PreflightMode, PreflightException (now with structured fields), and
preflight probe logic from Arius.Cli into a static BlobServiceFactory in
Arius.AzureBlob. Add Azure.Identity to AzureBlob; remove it from Cli.
Slim CliBuilder.BuildProductionServices to credential resolution + DI wiring.
Update verb catch blocks to switch on PreflightErrorKind for per-host formatting.
Widen Core architecture test to all Azure namespaces; add Cli_Should_Not_Reference_Azure.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/Arius.Architecture.Tests/DependencyTests.cs (1)

28-30: Consider adding Tests exclusion for consistency.

Lines 42, 52, 53, 65, and 66 exclude test assemblies with && !a.FullName.Contains("Tests"), but line 29 does not. For consistency and to guard against future test projects, consider adding the same exclusion:

         IArchRule rule = Classes().That().ResideInAssembly(
-                Architecture.Assemblies.First(a => a.FullName.Contains("Arius.Core")))
+                Architecture.Assemblies.First(a => a.FullName.Contains("Arius.Core") && !a.FullName.Contains("Tests")))
             .Should().NotDependOnAnyTypesThat().ResideInNamespace("Azure");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Architecture.Tests/DependencyTests.cs` around lines 28 - 30, The
rule creation using IArchRule and
Classes().That().ResideInAssembly(Architecture.Assemblies.First(a =>
a.FullName.Contains("Arius.Core"))) should mirror other exclusions by ignoring
test assemblies; update the predicate inside ResideInAssembly (the First lambda)
to also exclude assemblies whose FullName contains "Tests" (e.g., change
a.FullName.Contains("Arius.Core") to a.FullName.Contains("Arius.Core") &&
!a.FullName.Contains("Tests")) so the rule consistently skips test projects.
src/Arius.AzureBlob/BlobServiceFactory.cs (1)

83-89: Probe blob cleanup is best-effort; consider documenting or handling edge case.

If the upload succeeds but delete fails (e.g., network interruption), the .arius-preflight-probe blob remains in the container. This is mitigated by:

  • The blob is 0 bytes (negligible storage cost)
  • overwrite: true ensures the next preflight will succeed

This is acceptable, but consider adding a comment documenting this behavior, or catching delete failures separately to avoid confusing users with a preflight error when write access was actually confirmed.

Optional: Document or handle delete failure gracefully
             if (preflightMode == PreflightMode.ReadWrite)
             {
                 var probeBlob = containerClient.GetBlobClient(PreflightProbeBlobName);
                 using var emptyStream = new MemoryStream();
                 await probeBlob.UploadAsync(emptyStream, overwrite: true).ConfigureAwait(false);
-                await probeBlob.DeleteAsync().ConfigureAwait(false);
+                // Best-effort cleanup; if this fails, the 0-byte probe blob remains
+                // and will be overwritten on the next invocation.
+                try
+                {
+                    await probeBlob.DeleteAsync().ConfigureAwait(false);
+                }
+                catch (Azure.RequestFailedException)
+                {
+                    // Ignore delete failures — write access was verified
+                }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.AzureBlob/BlobServiceFactory.cs` around lines 83 - 89, The
preflight probe cleanup in the PreflightMode.ReadWrite branch (using
PreflightProbeBlobName and the probeBlob UploadAsync/DeleteAsync calls in
BlobServiceFactory) can leave a zero-byte blob if DeleteAsync fails; update the
block to handle delete failures gracefully by catching exceptions from
probeBlob.DeleteAsync() and logging a non-fatal warning (or documenting the
best-effort cleanup with an explanatory comment) so a delete error doesn't
surface as a preflight write failure while the upload succeeded.
openspec/changes/blob-service-factory/proposal.md (1)

31-31: Consider direct unit tests for BlobServiceFactory instead of relying solely on transitive testing.

While CLI tests will exercise the factory, direct unit tests would provide:

  • Faster feedback when factory logic breaks
  • Better isolation of failure causes
  • Easier testing of edge cases (network errors, malformed credentials, etc.)
  • Documentation of factory behavior through test names
  • Protection against future CLI refactoring that might reduce coverage

Consider adding a small test suite for BlobServiceFactory.CreateAsync that mocks Azure SDK interactions and verifies:

  • Credential type validation
  • Error classification (403 → AccessDenied, 404 → ContainerNotFound, etc.)
  • Preflight probe behavior for both ReadOnly and ReadWrite modes
  • Structured exception field population
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/blob-service-factory/proposal.md` at line 31, Add a focused
unit test suite for BlobServiceFactory.CreateAsync rather than relying only on
transitive CLI tests: create tests that mock the Azure SDK interactions (e.g.,
mock container client/credential behavior) to assert credential type validation,
map HTTP errors to the correct classifications (403 → AccessDenied, 404 →
ContainerNotFound, etc.), exercise preflight probe behavior for ReadOnly and
ReadWrite modes, and verify PreflightException structured fields are populated;
update existing Arius.Cli.Tests catch-block assertions if they assume different
exception shapes so they match the new structured PreflightException
representation.
openspec/changes/blob-service-factory/specs/cli/spec.md (1)

23-23: DI registration ordering requirement may be fragile.

The requirement "Handler registrations MUST be placed after AddMediator() to override the source generator's auto-registrations" relies on call order, which is error-prone and may break silently if someone refactors the registration sequence.

Consider adding:

  1. A runtime assertion or startup validation check that verifies handler registrations are correct
  2. An architecture test that validates the registration order
  3. Code comments at both call sites warning against reordering
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/blob-service-factory/specs/cli/spec.md` at line 23, Add a
startup-time validation to fail fast when handler registrations are not
overriding the source-generator defaults: in the AddArius() extension (where
ICommandHandler<TCommand,TResult> handlers are registered) add a
post-registration check that queries the IServiceCollection (or built
IServiceProvider) to assert that each expected ICommandHandler<TCommand,TResult>
is registered with the factory delegate signature taking
accountName/containerName and that AddMediator() registrations have been
replaced; if the assertion fails throw an InvalidOperationException with a clear
message explaining the required ordering and how to fix it. Also add a short
comment in AddMediator() and AddArius() noting the ordering requirement and add
the same runtime assertion in CliBuilder.BuildProductionServices before calling
BlobServiceFactory.CreateAsync to ensure the DI state is correct at startup;
keep messages referencing AddArius, AddMediator, ICommandHandler<,>,
CliBuilder.BuildProductionServices, BlobServiceFactory.CreateAsync and
IBlobStorageService so maintainers can find the checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openspec/changes/blob-service-factory/specs/cli/spec.md`:
- Around line 38-56: Add spec scenarios for the missing PreflightErrorKind.Other
and the ls verb: include a scenario for "Other" where WHEN a verb catches
PreflightException with ErrorKind = Other and THEN the CLI SHALL display a
generic error message that includes the structured fields and an instruction to
open an issue; and add AccessDenied token-auth scenarios for the ls verb
mirroring archive/restore (WHEN the ls verb catches PreflightException with
ErrorKind = AccessDenied and AuthMode = "token" THEN suggest the appropriate
RBAC role or minimal permission guidance consistent with archive (Storage Blob
Data Contributor) or restore (Storage Blob Data Reader) as applicable).
- Line 4: Add a new test scenario to the CLI spec that verifies the resolved key
value is never logged or printed: describe running Arius.Cli with a sensitive
key provided via --key/-k, via ARIUS_KEY, and via user secrets and assert no
process output, error logs, or telemetry contains the raw key; mention that
credential resolution remains in Arius.Cli and the resolved credential
(StorageSharedKeyCredential or TokenCredential) is passed to
BlobServiceFactory.CreateAsync, and explicitly state that test must check
console/stdout, logger outputs, and any serialized diagnostic/telemetry for
absence of the key.

---

Nitpick comments:
In `@openspec/changes/blob-service-factory/proposal.md`:
- Line 31: Add a focused unit test suite for BlobServiceFactory.CreateAsync
rather than relying only on transitive CLI tests: create tests that mock the
Azure SDK interactions (e.g., mock container client/credential behavior) to
assert credential type validation, map HTTP errors to the correct
classifications (403 → AccessDenied, 404 → ContainerNotFound, etc.), exercise
preflight probe behavior for ReadOnly and ReadWrite modes, and verify
PreflightException structured fields are populated; update existing
Arius.Cli.Tests catch-block assertions if they assume different exception shapes
so they match the new structured PreflightException representation.

In `@openspec/changes/blob-service-factory/specs/cli/spec.md`:
- Line 23: Add a startup-time validation to fail fast when handler registrations
are not overriding the source-generator defaults: in the AddArius() extension
(where ICommandHandler<TCommand,TResult> handlers are registered) add a
post-registration check that queries the IServiceCollection (or built
IServiceProvider) to assert that each expected ICommandHandler<TCommand,TResult>
is registered with the factory delegate signature taking
accountName/containerName and that AddMediator() registrations have been
replaced; if the assertion fails throw an InvalidOperationException with a clear
message explaining the required ordering and how to fix it. Also add a short
comment in AddMediator() and AddArius() noting the ordering requirement and add
the same runtime assertion in CliBuilder.BuildProductionServices before calling
BlobServiceFactory.CreateAsync to ensure the DI state is correct at startup;
keep messages referencing AddArius, AddMediator, ICommandHandler<,>,
CliBuilder.BuildProductionServices, BlobServiceFactory.CreateAsync and
IBlobStorageService so maintainers can find the checks.

In `@src/Arius.Architecture.Tests/DependencyTests.cs`:
- Around line 28-30: The rule creation using IArchRule and
Classes().That().ResideInAssembly(Architecture.Assemblies.First(a =>
a.FullName.Contains("Arius.Core"))) should mirror other exclusions by ignoring
test assemblies; update the predicate inside ResideInAssembly (the First lambda)
to also exclude assemblies whose FullName contains "Tests" (e.g., change
a.FullName.Contains("Arius.Core") to a.FullName.Contains("Arius.Core") &&
!a.FullName.Contains("Tests")) so the rule consistently skips test projects.

In `@src/Arius.AzureBlob/BlobServiceFactory.cs`:
- Around line 83-89: The preflight probe cleanup in the PreflightMode.ReadWrite
branch (using PreflightProbeBlobName and the probeBlob UploadAsync/DeleteAsync
calls in BlobServiceFactory) can leave a zero-byte blob if DeleteAsync fails;
update the block to handle delete failures gracefully by catching exceptions
from probeBlob.DeleteAsync() and logging a non-fatal warning (or documenting the
best-effort cleanup with an explanatory comment) so a delete error doesn't
surface as a preflight write failure while the upload succeeded.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7a56926f-7a45-475b-acb8-918e2ba73894

📥 Commits

Reviewing files that changed from the base of the PR and between 78034cf and 8a35430.

📒 Files selected for processing (15)
  • openspec/changes/blob-service-factory/.openspec.yaml
  • openspec/changes/blob-service-factory/design.md
  • openspec/changes/blob-service-factory/proposal.md
  • openspec/changes/blob-service-factory/specs/architecture-tests/spec.md
  • openspec/changes/blob-service-factory/specs/blob-service-factory/spec.md
  • openspec/changes/blob-service-factory/specs/cli/spec.md
  • openspec/changes/blob-service-factory/tasks.md
  • src/Arius.Architecture.Tests/DependencyTests.cs
  • src/Arius.AzureBlob/Arius.AzureBlob.csproj
  • src/Arius.AzureBlob/BlobServiceFactory.cs
  • src/Arius.AzureBlob/Preflight.cs
  • src/Arius.Cli/CliBuilder.cs
  • src/Arius.Cli/Commands/Archive/ArchiveVerb.cs
  • src/Arius.Cli/Commands/Ls/LsVerb.cs
  • src/Arius.Cli/Commands/Restore/RestoreVerb.cs
✅ Files skipped from review due to trivial changes (4)
  • openspec/changes/blob-service-factory/.openspec.yaml
  • src/Arius.AzureBlob/Arius.AzureBlob.csproj
  • openspec/changes/blob-service-factory/design.md
  • openspec/changes/blob-service-factory/specs/blob-service-factory/spec.md

## MODIFIED Requirements

### Requirement: Account key resolution
The CLI SHALL resolve the account key in the following order: (1) `--key` / `-k` CLI parameter, (2) `ARIUS_KEY` environment variable, (3) `Microsoft.Extensions.Configuration.UserSecrets` (hidden, developer use only). The key SHALL NEVER be logged or displayed in output. The `--key` option description SHALL NOT mention user secrets. Credential resolution (key lookup and fallback to `AzureCliCredential`) SHALL remain in `Arius.Cli`. The resolved credential SHALL be passed to `BlobServiceFactory.CreateAsync` as either a `StorageSharedKeyCredential` or a `TokenCredential`.
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.

🛠️ Refactor suggestion | 🟠 Major

Add a test scenario to verify keys are never logged.

The requirement states "The key SHALL NEVER be logged or displayed in output," which is critical for security. However, there is no corresponding test scenario to verify this constraint.

💡 Suggested additional scenario

Add after line 21:

+#### Scenario: Account key is never logged
+- **WHEN** any command is executed with a key from any source
+- **THEN** the key SHALL NOT appear in console output, log files, or error messages
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/blob-service-factory/specs/cli/spec.md` at line 4, Add a new
test scenario to the CLI spec that verifies the resolved key value is never
logged or printed: describe running Arius.Cli with a sensitive key provided via
--key/-k, via ARIUS_KEY, and via user secrets and assert no process output,
error logs, or telemetry contains the raw key; mention that credential
resolution remains in Arius.Cli and the resolved credential
(StorageSharedKeyCredential or TokenCredential) is passed to
BlobServiceFactory.CreateAsync, and explicitly state that test must check
console/stdout, logger outputs, and any serialized diagnostic/telemetry for
absence of the key.

Comment on lines +38 to +56
#### Scenario: ContainerNotFound error formatted
- **WHEN** a verb catches `PreflightException` with `ErrorKind = ContainerNotFound`
- **THEN** the CLI SHALL display a message including the container name and account name from the structured fields

#### Scenario: AccessDenied with key auth formatted
- **WHEN** a verb catches `PreflightException` with `ErrorKind = AccessDenied` and `AuthMode = "key"`
- **THEN** the CLI SHALL display a message suggesting the account key may be incorrect

#### Scenario: AccessDenied with token auth formatted for archive
- **WHEN** the archive verb catches `PreflightException` with `ErrorKind = AccessDenied` and `AuthMode = "token"`
- **THEN** the CLI SHALL display a message suggesting the "Storage Blob Data Contributor" RBAC role

#### Scenario: AccessDenied with token auth formatted for restore
- **WHEN** the restore verb catches `PreflightException` with `ErrorKind = AccessDenied` and `AuthMode = "token"`
- **THEN** the CLI SHALL display a message suggesting the "Storage Blob Data Reader" RBAC role

#### Scenario: CredentialUnavailable error formatted
- **WHEN** a verb catches `PreflightException` with `ErrorKind = CredentialUnavailable`
- **THEN** the CLI SHALL display a message listing the credential resolution options (--key, ARIUS_KEY, user secrets, az login)
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.

🛠️ Refactor suggestion | 🟠 Major

Missing scenarios for PreflightErrorKind.Other and ls verb.

The specification defines scenarios for ContainerNotFound, AccessDenied, and CredentialUnavailable, but PreflightErrorKind.Other (mentioned in tasks.md) has no formatting scenario. Additionally, the ls verb is mentioned in PR objectives but only archive and restore have AccessDenied token auth scenarios.

📝 Suggested additional scenarios

Add after line 56:

+#### Scenario: Other error formatted
+- **WHEN** a verb catches `PreflightException` with `ErrorKind = Other`
+- **THEN** the CLI SHALL display a message including the status code, account name, container name, and inner exception details
+
+#### Scenario: AccessDenied with token auth formatted for ls
+- **WHEN** the ls verb catches `PreflightException` with `ErrorKind = AccessDenied` and `AuthMode = "token"`
+- **THEN** the CLI SHALL display a message suggesting the "Storage Blob Data Reader" RBAC role
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/blob-service-factory/specs/cli/spec.md` around lines 38 -
56, Add spec scenarios for the missing PreflightErrorKind.Other and the ls verb:
include a scenario for "Other" where WHEN a verb catches PreflightException with
ErrorKind = Other and THEN the CLI SHALL display a generic error message that
includes the structured fields and an instruction to open an issue; and add
AccessDenied token-auth scenarios for the ls verb mirroring archive/restore
(WHEN the ls verb catches PreflightException with ErrorKind = AccessDenied and
AuthMode = "token" THEN suggest the appropriate RBAC role or minimal permission
guidance consistent with archive (Storage Blob Data Contributor) or restore
(Storage Blob Data Reader) as applicable).

Sync delta specs to main: create openspec/specs/blob-service-factory/spec.md,
create openspec/specs/architecture-tests/spec.md, update openspec/specs/cli/spec.md
with account key resolution, DI handler delegation, and PreflightException formatting.
Archive change to openspec/changes/archive/2026-04-01-blob-service-factory/.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
openspec/changes/archive/2026-04-01-blob-service-factory/proposal.md (1)

31-31: Consider adding direct factory-focused tests.

On Line 31, relying only on transitive CLI coverage may miss classification/probe edge cases in BlobServiceFactory. A small direct test set for 403/404/credential-unavailable mapping would make this boundary safer to evolve.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/archive/2026-04-01-blob-service-factory/proposal.md` at line
31, Tests currently rely only on transitive coverage through Arius.Cli.Tests
which can miss edge cases in BlobServiceFactory; add a small direct unit test
suite that exercises BlobServiceFactory to validate mapping of
403/404/credential-unavailable scenarios and ensure PreflightException
classification/probe behavior remains correct. Create tests that instantiate
BlobServiceFactory (or its test-friendly constructor) and simulate Azure client
responses for 403 and 404 and a credential-unavailable error, then assert the
factory returns the expected mapped result or throws the structured
PreflightException as expected; add focused cases for each mapping and update
Arius.Cli.Tests catch-block assertions if the factory test reveals any
behavioral differences.
openspec/changes/archive/2026-04-01-blob-service-factory/design.md (3)

7-19: Goals section uses future tense for completed work.

The Goals list uses imperative/future phrasing ("Extract a BlobServiceFactory", "Move PreflightMode", "Make PreflightException carry") when describing changes that are already implemented in the codebase (per context snippets 1-3). This reinforces the timeline confusion noted in the Context section.

Consider either:

  1. Rewriting in past tense ("Extracted...", "Moved...", "Made...") to document what was accomplished, or
  2. Reframing this as an "Architecture Decision Record" (ADR) with a header like ## Decision and ## Status: Implemented to clarify this captures decisions that have been made and executed.
♻️ Example reframe as past-tense achievements
-## Goals / Non-Goals
+## Changes Implemented
 
-**Goals:**
-- Extract a `BlobServiceFactory` into `Arius.AzureBlob` that any host can call to get a preflight-validated `IBlobStorageService`
-- Move `PreflightMode` and `PreflightException` to `Arius.AzureBlob` so both hosts share the same types
-- Make `PreflightException` carry structured data (status code, auth mode, account, container) instead of pre-formatted strings, so each host can format its own user-facing messages
-- Keep `CliBuilder.BuildProductionServices` as a thin wrapper: resolve credential, call factory, wire DI
+**What was done:**
+- Extracted `BlobServiceFactory` into `Arius.AzureBlob` so any host can call it to get a preflight-validated `IBlobStorageService`
+- Moved `PreflightMode` and `PreflightException` to `Arius.AzureBlob` so all hosts share the same types
+- Made `PreflightException` carry structured data (status code, auth mode, account, container) instead of pre-formatted strings, so each host formats its own messages
+- Kept `CliBuilder.BuildProductionServices` as a thin wrapper: resolve credential, call factory, wire DI
 
-**Non-Goals:**
+**Out of scope for this change:**
 - Changing the credential resolution logic (CLI flag > env var > user secrets > `AzureCliCredential`) -- that stays in CLI
 - Adding a second host in this change -- that is a separate future change
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/archive/2026-04-01-blob-service-factory/design.md` around
lines 7 - 19, The Goals section in design.md uses future/imperative tense for
work that's already done; update that section to reflect completed work by
changing phrases like "Extract a `BlobServiceFactory`", "Move `PreflightMode`",
and "Make `PreflightException` carry" to past tense (e.g., "Extracted
`BlobServiceFactory`", "Moved `PreflightMode`", "Made `PreflightException`
carry") or convert the document into an ADR-style entry with headers such as "##
Decision" and "## Status: Implemented" so the intent and status of symbols like
BlobServiceFactory, PreflightMode, and PreflightException are clearly recorded
as completed.

78-84: Risks section describes past trade-offs as future concerns.

The Risks section warns about "breaking changes" and proposes "mitigations" as if they're future concerns. However, since the implementation is complete and the PR objectives confirm "All tests pass — 137 CLI tests and 122 Core tests," these trade-offs have already been accepted and addressed.

For instance:

  • "Breaking change for test helpers: Mitigation: Straightforward find-and-replace" → This implies a future action, but the tests already pass, so the change has been made.
  • "Azure.Identity transitive dependency size" → The dependency is already added (the factory in snippet 1 catches CredentialUnavailableException from Azure.Identity).

Consider reframing this section to document the trade-offs that were made and how they were handled, rather than warning about future risks.

♻️ Example reframe as completed trade-offs
-## Risks / Trade-offs
+## Trade-offs Accepted
 
-- **Breaking change for test helpers**: `Arius.Cli.Tests` uses a factory delegate typed `Func<string, string?, string?, string, PreflightMode, Task<IServiceProvider>>`. The `PreflightMode` namespace changes from `Arius.Cli` to `Arius.AzureBlob`, requiring a `using` update. No behavioral change. **Mitigation**: Straightforward find-and-replace.
+- **Breaking change for test helpers**: `Arius.Cli.Tests` factory delegates required a `using` update when `PreflightMode` moved from `Arius.Cli` to `Arius.AzureBlob`. This was a straightforward namespace change with no behavioral impact (test suite remains at 137 passing tests).
 
-- **`Azure.Identity` transitive dependency size**: Adding `Azure.Identity` to `AzureBlob` pulls in MSAL and related packages at the library level. However, `Arius.Cli` already depends on `Azure.Identity`, and AzureBlob is only consumed by CLI (and the future second host, which will also need it). **Mitigation**: No actual binary size increase since the dependency is already transitively present.
+- **`Azure.Identity` transitive dependency size**: Adding `Azure.Identity` to `Arius.AzureBlob` pulled in MSAL and related packages. This was acceptable because `Arius.Cli` already had this dependency, so there was no binary size increase for the current host.
 
-- **Enum evolution**: Adding a new `PreflightErrorKind` value in the future requires updating all host switch blocks. **Mitigation**: There are only two hosts planned, and the enum is small (4 values). Exhaustive switch with a default throw will catch missing cases at compile time.
+- **Enum evolution**: Adding a new `PreflightErrorKind` value will require updating all host switch blocks. This was acceptable because only two hosts are planned, the enum is small (4 values), and exhaustive switch statements with a default throw will catch missing cases at compile time.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/archive/2026-04-01-blob-service-factory/design.md` around
lines 78 - 84, Reword the "Risks / Trade-offs" section under the "## Risks /
Trade-offs" heading to present the items as completed trade-offs rather than
future risks: change wording around "Breaking change for test helpers" to state
that the PreflightMode namespace move (affecting Arius.Cli.Tests and the factory
delegate typed Func<string, string?, string?, string, PreflightMode,
Task<IServiceProvider>>) was updated (mention the required using change) and
that tests were updated/passed; change the Azure.Identity item to note the
dependency was already present and caught (e.g., CredentialUnavailableException
handled by the factory) and therefore caused no binary size regression; and
update the PreflightErrorKind item to state that enum evolution is handled via
exhaustive switches with a default throw and that hosts were updated as needed.
Ensure you reference the symbols PreflightMode, Arius.Cli.Tests, Azure.Identity,
CredentialUnavailableException, and PreflightErrorKind in the revised text.

21-77: Decisions section framed as proposals rather than completed choices.

The Decisions section accurately describes the implemented architecture (confirmed by context snippets), but uses present/imperative tense ("lives", "is", "Add") as if proposing design choices to be made. This compounds the timeline ambiguity throughout the document.

For example:

  • Decision 1: "The factory is a static CreateAsync method" → ✅ matches snippet 1
  • Decision 3: Lists PreflightException fields → ✅ matches snippet 2 exactly
  • Decision 4: "Add Azure.Identity to Arius.AzureBlob" → imperative suggests future work

If this document is meant to record the design rationale for the implemented refactoring (which is a valuable practice), consider reframing each decision in past tense to reflect that these choices have been made and executed.

♻️ Example reframe for Decision 1
-### 1. Factory lives in `Arius.AzureBlob` as a static async method
+### 1. Factory was implemented in `Arius.AzureBlob` as a static async method
 
-The factory is a static `CreateAsync` method on a new `BlobServiceFactory` class. It takes a `TokenCredential` or `StorageSharedKeyCredential` (via the common base -- both are passed as typed overloads or a discriminated parameter), account name, container name, and `PreflightMode`. It returns an `IBlobStorageService`.
+The factory was implemented as a static `CreateAsync` method on `BlobServiceFactory`. It accepts a `TokenCredential` or `StorageSharedKeyCredential` (via an `object` parameter with runtime type check), account name, container name, and `PreflightMode`. It returns an `IBlobStorageService`.
 
-**Why static**: The factory has no instance state. It builds the client chain, runs the preflight probe, and returns the service. No reason to instantiate it.
+**Rationale for static**: The factory has no instance state. Making it static avoids unnecessary instantiation for what is purely a construction pipeline.

Apply similar changes to decisions 2-6.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/archive/2026-04-01-blob-service-factory/design.md` around
lines 21 - 77, Reword the Decisions section so each item records completed
choices in past tense rather than proposing actions: e.g., change Decision 1 to
state "The factory was implemented as a static CreateAsync method on
BlobServiceFactory" and similarly update Decision 2 (credential parameter was
accepted as object with runtime type checks), Decision 3 (PreflightException
carries structured fields), Decision 4 (Azure.Identity was added to
Arius.AzureBlob), Decision 5 (added ArchUnit test Cli_Should_Not_Reference_Azure
and widened Core_Should_Not_Reference_Azure), and Decision 6 (CLI error
formatting was moved to verb catch blocks); preserve the same facts, symbols
(BlobServiceFactory.CreateAsync, PreflightException, PreflightErrorKind,
Cli_Should_Not_Reference_Azure, Core_Should_Not_Reference_Azure,
Azure.Identity), and rationale but convert imperative/present wording into
past-tense statements summarizing what was done.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openspec/changes/archive/2026-04-01-blob-service-factory/design.md`:
- Around line 1-6: Update the "Context" paragraph to describe the refactor as
completed rather than planned: state that PreflightMode, PreflightErrorKind, and
PreflightException are defined in src/Arius.AzureBlob/Preflight.cs, and that
BlobServiceFactory.CreateAsync now encapsulates the client construction,
preflight probe and error classification; also note that
CliBuilder.BuildProductionServices delegates to BlobServiceFactory.CreateAsync
(lines referenced for reviewers only) so no duplication is required for the
non-CLI host. Ensure the wording clearly communicates the current architecture
and where each responsibility now lives.

In `@openspec/changes/archive/2026-04-01-blob-service-factory/proposal.md`:
- Around line 12-13: Reword the sentence about PreflightException.Message to
clarify that PreflightException still sets a base, preformatted Message in
src/Arius.AzureBlob/Preflight.cs, but the important change is that hosts no
longer rely on that Message for user-facing UX and instead use the structured
fields (status code, account, container, auth mode) to format errors; update the
line to explicitly state "exception retains a base Message but hosts must ignore
it for UX and format errors from structured fields" and keep the note about
adding Azure.Identity separate.

In
`@openspec/changes/archive/2026-04-01-blob-service-factory/specs/architecture-tests/spec.md`:
- Line 4: Update the spec text to reflect current tests: state that the
architecture tests enforce only the Arius.AzureBlob assembly may reference Azure
SDK namespaces and that both Core and Cli boundaries are already enforced by
tests in DependencyTests.cs using ResideInNamespace("Azure") (i.e., the existing
Core_Should_Not_Reference_Azure check was broadened and a
Cli_Should_Not_Reference_Azure test exists), replacing the stale wording about
planned expansion with this current enforcement description and referencing the
DependencyTests.cs test names (Core_Should_Not_Reference_Azure,
Cli_Should_Not_Reference_Azure) and the ResideInNamespace("Azure") assertion.

---

Nitpick comments:
In `@openspec/changes/archive/2026-04-01-blob-service-factory/design.md`:
- Around line 7-19: The Goals section in design.md uses future/imperative tense
for work that's already done; update that section to reflect completed work by
changing phrases like "Extract a `BlobServiceFactory`", "Move `PreflightMode`",
and "Make `PreflightException` carry" to past tense (e.g., "Extracted
`BlobServiceFactory`", "Moved `PreflightMode`", "Made `PreflightException`
carry") or convert the document into an ADR-style entry with headers such as "##
Decision" and "## Status: Implemented" so the intent and status of symbols like
BlobServiceFactory, PreflightMode, and PreflightException are clearly recorded
as completed.
- Around line 78-84: Reword the "Risks / Trade-offs" section under the "## Risks
/ Trade-offs" heading to present the items as completed trade-offs rather than
future risks: change wording around "Breaking change for test helpers" to state
that the PreflightMode namespace move (affecting Arius.Cli.Tests and the factory
delegate typed Func<string, string?, string?, string, PreflightMode,
Task<IServiceProvider>>) was updated (mention the required using change) and
that tests were updated/passed; change the Azure.Identity item to note the
dependency was already present and caught (e.g., CredentialUnavailableException
handled by the factory) and therefore caused no binary size regression; and
update the PreflightErrorKind item to state that enum evolution is handled via
exhaustive switches with a default throw and that hosts were updated as needed.
Ensure you reference the symbols PreflightMode, Arius.Cli.Tests, Azure.Identity,
CredentialUnavailableException, and PreflightErrorKind in the revised text.
- Around line 21-77: Reword the Decisions section so each item records completed
choices in past tense rather than proposing actions: e.g., change Decision 1 to
state "The factory was implemented as a static CreateAsync method on
BlobServiceFactory" and similarly update Decision 2 (credential parameter was
accepted as object with runtime type checks), Decision 3 (PreflightException
carries structured fields), Decision 4 (Azure.Identity was added to
Arius.AzureBlob), Decision 5 (added ArchUnit test Cli_Should_Not_Reference_Azure
and widened Core_Should_Not_Reference_Azure), and Decision 6 (CLI error
formatting was moved to verb catch blocks); preserve the same facts, symbols
(BlobServiceFactory.CreateAsync, PreflightException, PreflightErrorKind,
Cli_Should_Not_Reference_Azure, Core_Should_Not_Reference_Azure,
Azure.Identity), and rationale but convert imperative/present wording into
past-tense statements summarizing what was done.

In `@openspec/changes/archive/2026-04-01-blob-service-factory/proposal.md`:
- Line 31: Tests currently rely only on transitive coverage through
Arius.Cli.Tests which can miss edge cases in BlobServiceFactory; add a small
direct unit test suite that exercises BlobServiceFactory to validate mapping of
403/404/credential-unavailable scenarios and ensure PreflightException
classification/probe behavior remains correct. Create tests that instantiate
BlobServiceFactory (or its test-friendly constructor) and simulate Azure client
responses for 403 and 404 and a credential-unavailable error, then assert the
factory returns the expected mapped result or throws the structured
PreflightException as expected; add focused cases for each mapping and update
Arius.Cli.Tests catch-block assertions if the factory test reveals any
behavioral differences.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: df159d7a-930e-46b2-b591-dd773ccd193c

📥 Commits

Reviewing files that changed from the base of the PR and between 8a35430 and 0926a01.

📒 Files selected for processing (10)
  • openspec/changes/archive/2026-04-01-blob-service-factory/.openspec.yaml
  • openspec/changes/archive/2026-04-01-blob-service-factory/design.md
  • openspec/changes/archive/2026-04-01-blob-service-factory/proposal.md
  • openspec/changes/archive/2026-04-01-blob-service-factory/specs/architecture-tests/spec.md
  • openspec/changes/archive/2026-04-01-blob-service-factory/specs/blob-service-factory/spec.md
  • openspec/changes/archive/2026-04-01-blob-service-factory/specs/cli/spec.md
  • openspec/changes/archive/2026-04-01-blob-service-factory/tasks.md
  • openspec/specs/architecture-tests/spec.md
  • openspec/specs/blob-service-factory/spec.md
  • openspec/specs/cli/spec.md
✅ Files skipped from review due to trivial changes (7)
  • openspec/changes/archive/2026-04-01-blob-service-factory/.openspec.yaml
  • openspec/specs/architecture-tests/spec.md
  • openspec/changes/archive/2026-04-01-blob-service-factory/tasks.md
  • openspec/specs/blob-service-factory/spec.md
  • openspec/changes/archive/2026-04-01-blob-service-factory/specs/blob-service-factory/spec.md
  • openspec/specs/cli/spec.md
  • openspec/changes/archive/2026-04-01-blob-service-factory/specs/cli/spec.md

Comment on lines +1 to +6
## Context

`CliBuilder.BuildProductionServices` (lines 147-264 of `CliBuilder.cs`) currently owns the entire chain from credential to validated `IBlobStorageService`: it builds a `BlobServiceClient`, obtains a `BlobContainerClient`, runs a preflight connectivity probe, classifies errors into `PreflightException`, and constructs `AzureBlobStorageService`. The `PreflightMode` enum and `PreflightException` class also live in `Arius.Cli`.

A second (non-CLI) host is planned. Without extraction, that host must duplicate all of this logic. The `Arius.AzureBlob` project is the natural home: it already owns `AzureBlobStorageService` and has the `Azure.Storage.Blobs` dependency. `Arius.Core` must remain Azure-free (enforced by ArchUnit tests).

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.

⚠️ Potential issue | 🟠 Major

Documentation describes completed refactoring as if it's pending.

The "Context" section states that PreflightMode and PreflightException "also live in Arius.Cli" and that BuildProductionServices "currently owns the entire chain" of preflight logic. However, the provided context snippets show the refactoring is already complete:

  • Preflight.cs (context snippet 2): PreflightMode, PreflightErrorKind, and PreflightException already exist in src/Arius.AzureBlob/Preflight.cs, not in Arius.Cli.
  • BlobServiceFactory.cs (context snippet 1): BlobServiceFactory.CreateAsync already contains the full chain (client construction, preflight probe, error classification).
  • CliBuilder.cs (context snippet 3): BuildProductionServices already delegates to BlobServiceFactory.CreateAsync at lines 125-129.

This creates a timeline mismatch: the document reads like a pre-implementation design plan, but it's being added in the same PR that already implemented the changes. Future readers will be confused about whether this describes planned, in-progress, or completed work.

📝 Suggested fix: rewrite Context to describe existing architecture
 ## Context
 
-`CliBuilder.BuildProductionServices` (lines 147-264 of `CliBuilder.cs`) currently owns the entire chain from credential to validated `IBlobStorageService`: it builds a `BlobServiceClient`, obtains a `BlobContainerClient`, runs a preflight connectivity probe, classifies errors into `PreflightException`, and constructs `AzureBlobStorageService`. The `PreflightMode` enum and `PreflightException` class also live in `Arius.Cli`.
+As of PR `#37`, `BlobServiceFactory.CreateAsync` in `Arius.AzureBlob` encapsulates the full chain from credential to validated `IBlobStorageService`: it builds a `BlobServiceClient`, obtains a `BlobContainerClient`, runs a preflight connectivity probe based on `PreflightMode`, classifies errors into `PreflightException`, and constructs `AzureBlobStorageService`. The `PreflightMode` enum, `PreflightErrorKind` enum, and `PreflightException` class are defined in `Arius.AzureBlob/Preflight.cs`.
 
-A second (non-CLI) host is planned. Without extraction, that host must duplicate all of this logic. The `Arius.AzureBlob` project is the natural home: it already owns `AzureBlobStorageService` and has the `Azure.Storage.Blobs` dependency. `Arius.Core` must remain Azure-free (enforced by ArchUnit tests).
+This extraction enables planned second (non-CLI) hosts to reuse the factory without duplicating credential and preflight logic. The `Arius.AzureBlob` project is the natural home: it already owns `AzureBlobStorageService` and has the `Azure.Storage.Blobs` dependency. `Arius.Core` remains Azure-free (enforced by ArchUnit tests).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/archive/2026-04-01-blob-service-factory/design.md` around
lines 1 - 6, Update the "Context" paragraph to describe the refactor as
completed rather than planned: state that PreflightMode, PreflightErrorKind, and
PreflightException are defined in src/Arius.AzureBlob/Preflight.cs, and that
BlobServiceFactory.CreateAsync now encapsulates the client construction,
preflight probe and error classification; also note that
CliBuilder.BuildProductionServices delegates to BlobServiceFactory.CreateAsync
(lines referenced for reviewers only) so no duplication is required for the
non-CLI host. Ensure the wording clearly communicates the current architecture
and where each responsibility now lives.

Comment on lines +12 to +13
- `PreflightException` changes from carrying a pre-formatted user-facing message to carrying structured fields (status code, account, container, auth mode) — each host formats its own error messages
- Add `Azure.Identity` package reference to `Arius.AzureBlob` (needed for `CredentialUnavailableException` catch)
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.

⚠️ Potential issue | 🟡 Minor

Tighten wording about PreflightException.Message.

On Line 12, the text says the exception changed from carrying a pre-formatted user-facing message to structured fields. In current code (src/Arius.AzureBlob/Preflight.cs), the exception still sets a preformatted base Message; the key change is that hosts no longer rely on it for UX formatting. Consider rewording to avoid ambiguity.

Suggested doc tweak
-- `PreflightException` changes from carrying a pre-formatted user-facing message to carrying structured fields (status code, account, container, auth mode) — each host formats its own error messages
+- `PreflightException` now exposes structured fields (status code, account, container, auth mode) as the primary contract, and hosts format their own user-facing messages instead of relying on exception text
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- `PreflightException` changes from carrying a pre-formatted user-facing message to carrying structured fields (status code, account, container, auth mode) — each host formats its own error messages
- Add `Azure.Identity` package reference to `Arius.AzureBlob` (needed for `CredentialUnavailableException` catch)
- `PreflightException` now exposes structured fields (status code, account, container, auth mode) as the primary contract, and hosts format their own user-facing messages instead of relying on exception text
- Add `Azure.Identity` package reference to `Arius.AzureBlob` (needed for `CredentialUnavailableException` catch)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/archive/2026-04-01-blob-service-factory/proposal.md` around
lines 12 - 13, Reword the sentence about PreflightException.Message to clarify
that PreflightException still sets a base, preformatted Message in
src/Arius.AzureBlob/Preflight.cs, but the important change is that hosts no
longer rely on that Message for user-facing UX and instead use the structured
fields (status code, account, container, auth mode) to format errors; update the
line to explicitly state "exception retains a base Message but hosts must ignore
it for UX and format errors from structured fields" and keep the note about
adding Azure.Identity separate.

## ADDED Requirements

### Requirement: Only Arius.AzureBlob may depend on Azure SDK namespaces
The architecture tests SHALL enforce that only the `Arius.AzureBlob` assembly may depend on types in Azure SDK namespaces. Specifically, `Arius.Core` and `Arius.Cli` SHALL NOT depend on types in `Azure.Storage`, `Azure.Identity`, `Azure.Core`, or any other `Azure.*` namespace. The existing `Core_Should_Not_Reference_Azure` test SHALL be widened from checking only `Azure.Storage` to checking all `Azure` namespaces. A new `Cli_Should_Not_Reference_Azure` test SHALL be added to enforce the same boundary for the CLI project.
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.

⚠️ Potential issue | 🟡 Minor

Spec text is stale about what tests are being introduced/expanded.

This line says Core is being widened from Azure.Storage and CLI test is new, but both are already implemented as ResideInNamespace("Azure") in src/Arius.Architecture.Tests/DependencyTests.cs (Lines 24–47). Please update this requirement to describe current enforcement rather than planned changes.

Suggested wording update
-The architecture tests SHALL enforce that only the `Arius.AzureBlob` assembly may depend on types in Azure SDK namespaces. Specifically, `Arius.Core` and `Arius.Cli` SHALL NOT depend on types in `Azure.Storage`, `Azure.Identity`, `Azure.Core`, or any other `Azure.*` namespace. The existing `Core_Should_Not_Reference_Azure` test SHALL be widened from checking only `Azure.Storage` to checking all `Azure` namespaces. A new `Cli_Should_Not_Reference_Azure` test SHALL be added to enforce the same boundary for the CLI project.
+The architecture tests SHALL enforce that only the `Arius.AzureBlob` assembly may depend on types in Azure SDK namespaces. Specifically, `Arius.Core` and `Arius.Cli` SHALL NOT depend on types in `Azure.Storage`, `Azure.Identity`, `Azure.Core`, or any other `Azure.*` namespace. This boundary is enforced by the existing `Core_Should_Not_Reference_Azure` and `Cli_Should_Not_Reference_Azure` tests, both checking `Azure` namespaces broadly.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
The architecture tests SHALL enforce that only the `Arius.AzureBlob` assembly may depend on types in Azure SDK namespaces. Specifically, `Arius.Core` and `Arius.Cli` SHALL NOT depend on types in `Azure.Storage`, `Azure.Identity`, `Azure.Core`, or any other `Azure.*` namespace. The existing `Core_Should_Not_Reference_Azure` test SHALL be widened from checking only `Azure.Storage` to checking all `Azure` namespaces. A new `Cli_Should_Not_Reference_Azure` test SHALL be added to enforce the same boundary for the CLI project.
The architecture tests SHALL enforce that only the `Arius.AzureBlob` assembly may depend on types in Azure SDK namespaces. Specifically, `Arius.Core` and `Arius.Cli` SHALL NOT depend on types in `Azure.Storage`, `Azure.Identity`, `Azure.Core`, or any other `Azure.*` namespace. This boundary is enforced by the existing `Core_Should_Not_Reference_Azure` and `Cli_Should_Not_Reference_Azure` tests, both checking `Azure` namespaces broadly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@openspec/changes/archive/2026-04-01-blob-service-factory/specs/architecture-tests/spec.md`
at line 4, Update the spec text to reflect current tests: state that the
architecture tests enforce only the Arius.AzureBlob assembly may reference Azure
SDK namespaces and that both Core and Cli boundaries are already enforced by
tests in DependencyTests.cs using ResideInNamespace("Azure") (i.e., the existing
Core_Should_Not_Reference_Azure check was broadened and a
Cli_Should_Not_Reference_Azure test exists), replacing the stale wording about
planned expansion with this current enforcement description and referencing the
DependencyTests.cs test names (Core_Should_Not_Reference_Azure,
Cli_Should_Not_Reference_Azure) and the ResideInNamespace("Azure") assertion.

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.

1 participant