From df71edc2c25e6abd8e503570d910616d9ee086bf Mon Sep 17 00:00:00 2001 From: David Pine Date: Wed, 27 May 2026 10:05:13 -0500 Subject: [PATCH 1/3] Add embedded Aspire skills fallback (#17537) Embed a checked-in Aspire skills bundle snapshot for CLI fallback, make agent init warn instead of fail when bundle acquisition is unavailable, and add automation to refresh the snapshot via a draft PR. Co-authored-by: IEvangelist <7679720+IEvangelist@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../actions/create-pull-request/action.yml | 11 ++ .../workflows/update-aspire-skills-bundle.yml | 77 ++++++++ .../workflows/verify-aspire-skills-bundle.yml | 26 +++ eng/scripts/update-aspire-skills-bundle.ps1 | 164 +++++++++++++++++ eng/scripts/verify-aspire-skills-bundle.ps1 | 63 +++++++ .../Agents/AspireSkills/AspireSkillsBundle.cs | 2 +- .../AspireSkills/AspireSkillsInstaller.cs | 144 ++++++++++++++- .../Embedded/aspire-skills-v0.0.1.tgz | Bin 0 -> 95516 bytes .../Embedded/aspire-skills.metadata.json | 7 + .../EmbeddedAspireSkillsBundleProvider.cs | 80 ++++++++ .../AspireSkills/SkillBundleManifest.cs | 17 ++ src/Aspire.Cli/Aspire.Cli.csproj | 6 + src/Aspire.Cli/Commands/AgentInitCommand.cs | 81 ++++++-- src/Aspire.Cli/Program.cs | 1 + .../Resources/AgentCommandStrings.Designer.cs | 2 +- .../Resources/AgentCommandStrings.resx | 2 +- .../Resources/xlf/AgentCommandStrings.cs.xlf | 4 +- .../Resources/xlf/AgentCommandStrings.de.xlf | 4 +- .../Resources/xlf/AgentCommandStrings.es.xlf | 4 +- .../Resources/xlf/AgentCommandStrings.fr.xlf | 4 +- .../Resources/xlf/AgentCommandStrings.it.xlf | 4 +- .../Resources/xlf/AgentCommandStrings.ja.xlf | 4 +- .../Resources/xlf/AgentCommandStrings.ko.xlf | 4 +- .../Resources/xlf/AgentCommandStrings.pl.xlf | 4 +- .../xlf/AgentCommandStrings.pt-BR.xlf | 4 +- .../Resources/xlf/AgentCommandStrings.ru.xlf | 4 +- .../Resources/xlf/AgentCommandStrings.tr.xlf | 4 +- .../xlf/AgentCommandStrings.zh-Hans.xlf | 4 +- .../xlf/AgentCommandStrings.zh-Hant.xlf | 4 +- .../Agents/AspireSkillsInstallerTests.cs | 174 +++++++++++++++++- .../Commands/AgentInitCommandTests.cs | 11 +- 31 files changed, 862 insertions(+), 58 deletions(-) create mode 100644 .github/workflows/update-aspire-skills-bundle.yml create mode 100644 .github/workflows/verify-aspire-skills-bundle.yml create mode 100644 eng/scripts/update-aspire-skills-bundle.ps1 create mode 100644 eng/scripts/verify-aspire-skills-bundle.ps1 create mode 100644 src/Aspire.Cli/Agents/AspireSkills/Embedded/aspire-skills-v0.0.1.tgz create mode 100644 src/Aspire.Cli/Agents/AspireSkills/Embedded/aspire-skills.metadata.json create mode 100644 src/Aspire.Cli/Agents/AspireSkills/EmbeddedAspireSkillsBundleProvider.cs diff --git a/.github/actions/create-pull-request/action.yml b/.github/actions/create-pull-request/action.yml index c211663474f..d58dd17aeb8 100644 --- a/.github/actions/create-pull-request/action.yml +++ b/.github/actions/create-pull-request/action.yml @@ -29,6 +29,10 @@ inputs: description: 'Set to true if the branch is already pushed remotely (skips commit/push)' required: false default: 'false' + draft: + description: 'Create the pull request as a draft' + required: false + default: 'false' outputs: pull-request-number: description: 'The pull request number' @@ -91,6 +95,7 @@ runs: PR_TITLE: ${{ inputs.title }} PR_BODY: ${{ inputs.body }} LABELS: ${{ inputs.labels }} + DRAFT: ${{ inputs.draft }} run: | # Check if a PR already exists for this branch EXISTING_PR=$(gh pr list --head "$BRANCH" --base "$BASE" --json number,url --jq '.[0] // empty') @@ -133,12 +138,18 @@ runs: trap 'rm -f "$BODY_FILE"' EXIT printf '%s\n' "$PR_BODY" > "$BODY_FILE" + DRAFT_ARGS=() + if [ "$DRAFT" = "true" ]; then + DRAFT_ARGS+=(--draft) + fi + # Create the pull request without eval — all args are properly quoted PR_URL=$(gh pr create \ --title "$PR_TITLE" \ --body-file "$BODY_FILE" \ --base "$BASE" \ --head "$BRANCH" \ + "${DRAFT_ARGS[@]}" \ "${LABEL_ARGS[@]}") rm -f "$BODY_FILE" diff --git a/.github/workflows/update-aspire-skills-bundle.yml b/.github/workflows/update-aspire-skills-bundle.yml new file mode 100644 index 00000000000..ac28ca9dad8 --- /dev/null +++ b/.github/workflows/update-aspire-skills-bundle.yml @@ -0,0 +1,77 @@ +name: Update Aspire Skills Bundle + +on: + workflow_dispatch: + inputs: + version: + description: "Aspire skills release version to embed. Defaults to the currently pinned version." + required: false + type: string + schedule: + - cron: '0 17 * * *' # 9am PT / 17:00 UTC + +permissions: + contents: write + pull-requests: write + +jobs: + update-and-pr: + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'microsoft' }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Generate GitHub App Token for bundle update + id: update-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + with: + app-id: ${{ secrets.ASPIRE_BOT_APP_ID }} + private-key: ${{ secrets.ASPIRE_BOT_PRIVATE_KEY }} + + - name: Update embedded Aspire skills bundle + shell: pwsh + env: + GH_TOKEN: ${{ steps.update-token.outputs.token }} + VERSION: ${{ inputs.version }} + run: | + if ([string]::IsNullOrWhiteSpace($env:VERSION)) { + ./eng/scripts/update-aspire-skills-bundle.ps1 + } + else { + ./eng/scripts/update-aspire-skills-bundle.ps1 -Version $env:VERSION + } + + - name: Restore solution + run: ./restore.sh + + - name: Test Aspire skills installer + run: > + dotnet test --project tests/Aspire.Cli.Tests/Aspire.Cli.Tests.csproj + --no-launch-profile + -- + --filter-class "*.AspireSkillsInstallerTests" + --filter-class "*.AspireSkillsBundleTests" + --filter-class "*.AgentInitCommandTests" + --filter-not-trait "quarantined=true" + --filter-not-trait "outerloop=true" + + - name: Generate GitHub App Token for pull request + id: pr-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + with: + app-id: ${{ secrets.ASPIRE_BOT_APP_ID }} + private-key: ${{ secrets.ASPIRE_BOT_PRIVATE_KEY }} + + - name: Create or update pull request + uses: ./.github/actions/create-pull-request + with: + token: ${{ steps.pr-token.outputs.token }} + branch: update-aspire-skills-bundle + base: main + draft: true + commit-message: "[Automated] Update Aspire skills bundle" + labels: | + area-cli + area-engineering-systems + title: "[Automated] Update Aspire skills bundle" + body: "Auto-generated update to refresh the embedded Aspire skills bundle fallback used by the Aspire CLI." diff --git a/.github/workflows/verify-aspire-skills-bundle.yml b/.github/workflows/verify-aspire-skills-bundle.yml new file mode 100644 index 00000000000..db82170a3dc --- /dev/null +++ b/.github/workflows/verify-aspire-skills-bundle.yml @@ -0,0 +1,26 @@ +name: Verify Aspire Skills Bundle + +on: + workflow_dispatch: + pull_request: + paths: + - 'src/Aspire.Cli/Agents/AspireSkills/Embedded/**' + - 'eng/scripts/verify-aspire-skills-bundle.ps1' + - '.github/workflows/verify-aspire-skills-bundle.yml' + +permissions: + contents: read + attestations: read + +jobs: + verify: + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'microsoft' }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Verify embedded Aspire skills bundle attestation + shell: pwsh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./eng/scripts/verify-aspire-skills-bundle.ps1 diff --git a/eng/scripts/update-aspire-skills-bundle.ps1 b/eng/scripts/update-aspire-skills-bundle.ps1 new file mode 100644 index 00000000000..94c2212c641 --- /dev/null +++ b/eng/scripts/update-aspire-skills-bundle.ps1 @@ -0,0 +1,164 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param( + [string]$Version, + [string]$Repository = 'microsoft/aspire-skills' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +$scriptDir = $PSScriptRoot +$repoRoot = (Resolve-Path (Join-Path $scriptDir '..\..')).Path +$embeddedDir = Join-Path $repoRoot 'src\Aspire.Cli\Agents\AspireSkills\Embedded' +$metadataPath = Join-Path $embeddedDir 'aspire-skills.metadata.json' +$installerPath = Join-Path $repoRoot 'src\Aspire.Cli\Agents\AspireSkills\AspireSkillsInstaller.cs' +$cliProjectPath = Join-Path $repoRoot 'src\Aspire.Cli\Aspire.Cli.csproj' + +function Invoke-GitHubCli { + param( + [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)] + [string[]]$Arguments + ) + + & gh @Arguments + if ($LASTEXITCODE -ne 0) { + throw "gh $($Arguments -join ' ') failed with exit code $LASTEXITCODE." + } +} + +function Get-UnprefixedVersion([string]$Value) { + if ([string]::IsNullOrWhiteSpace($Value)) { + throw 'A version is required.' + } + + if ($Value.StartsWith('v', [System.StringComparison]::OrdinalIgnoreCase)) { + return $Value.Substring(1) + } + + return $Value +} + +function Get-CurrentEmbeddedVersion { + if (-not (Test-Path $metadataPath)) { + throw "Embedded Aspire skills metadata was not found at '$metadataPath'. Pass -Version to choose the initial version." + } + + $metadata = Get-Content -Raw -Path $metadataPath | ConvertFrom-Json + return Get-UnprefixedVersion $metadata.version +} + +function Set-TextFile { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][string]$Content + ) + + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText($Path, $Content.TrimEnd("`r", "`n") + [System.Environment]::NewLine, $utf8NoBom) +} + +function Get-GitHubRelease([string]$NormalizedVersion) { + $tagCandidates = @("v$NormalizedVersion", $NormalizedVersion) | Select-Object -Unique + + foreach ($tag in $tagCandidates) { + try { + $json = Invoke-GitHubCli release view $tag --repo $Repository --json 'tagName,assets' + return $json | ConvertFrom-Json + } + catch { + Write-Host "Release '$tag' was not found in '$Repository'." + } + } + + throw "Could not find an Aspire skills release for version '$NormalizedVersion' in '$Repository'." +} + +function Get-ReleaseAsset($Release, [string]$NormalizedVersion) { + $assetNameCandidates = foreach ($archiveExtension in @('.zip', '.tar.gz', '.tgz')) { + "aspire-skills-v$NormalizedVersion$archiveExtension" + "aspire-skills-$NormalizedVersion$archiveExtension" + } + + foreach ($assetName in $assetNameCandidates) { + $asset = $Release.assets | Where-Object { $_.name -ieq $assetName } | Select-Object -First 1 + if ($null -ne $asset) { + return $asset + } + } + + throw "Release '$($Release.tagName)' does not contain a supported Aspire skills archive asset for version '$NormalizedVersion'." +} + +if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + throw "The GitHub CLI ('gh') is required to update the embedded Aspire skills bundle." +} + +$normalizedVersion = if ([string]::IsNullOrWhiteSpace($Version)) { + Get-CurrentEmbeddedVersion +} +else { + Get-UnprefixedVersion $Version +} + +New-Item -ItemType Directory -Force -Path $embeddedDir | Out-Null + +Write-Host "Resolving Aspire skills release '$normalizedVersion' from '$Repository'..." +$release = Get-GitHubRelease $normalizedVersion +$asset = Get-ReleaseAsset $release $normalizedVersion + +$tempDir = [System.IO.Directory]::CreateTempSubdirectory('aspire-skills-update-').FullName +try { + Write-Host "Downloading '$($asset.name)' from '$Repository' release '$($release.tagName)'..." + Invoke-GitHubCli release download $release.tagName --repo $Repository --pattern $asset.name --dir $tempDir --clobber + + $archivePath = Join-Path $tempDir $asset.name + if (-not (Test-Path $archivePath)) { + throw "Expected downloaded asset '$archivePath' was not found." + } + + $certIdentity = "https://github.com/$Repository/.github/workflows/publish.yml@refs/tags/$($release.tagName)" + Write-Host "Verifying GitHub artifact attestation for '$($asset.name)'..." + Invoke-GitHubCli attestation verify $archivePath --repo $Repository --cert-identity $certIdentity --cert-oidc-issuer 'https://token.actions.githubusercontent.com' + + $hash = (Get-FileHash -Algorithm SHA256 $archivePath).Hash.ToLowerInvariant() + $targetArchivePath = Join-Path $embeddedDir $asset.name + + Get-ChildItem -Path $embeddedDir -File -Force | + Where-Object { $_.Name -match '^aspire-skills-.*\.(zip|tar\.gz|tgz)$' -and $_.Name -ne $asset.name } | + Remove-Item -Force + + Copy-Item -Path $archivePath -Destination $targetArchivePath -Force + + $metadata = [ordered]@{ + version = $normalizedVersion + repository = $Repository + tag = $release.tagName + assetName = $asset.name + sha256 = $hash + } + Set-TextFile -Path $metadataPath -Content ($metadata | ConvertTo-Json) + + $installerContent = Get-Content -Raw -Path $installerPath + $installerContent = [regex]::Replace( + $installerContent, + 'internal const string Version = "[^"]+";', + "internal const string Version = ""$normalizedVersion"";") + Set-TextFile -Path $installerPath -Content $installerContent + + $cliProjectContent = Get-Content -Raw -Path $cliProjectPath + $cliProjectContent = [regex]::Replace( + $cliProjectContent, + 'Agents\\AspireSkills\\Embedded\\aspire-skills-[^"]+\.(zip|tar\.gz|tgz)', + "Agents\AspireSkills\Embedded\$($asset.name)") + Set-TextFile -Path $cliProjectPath -Content $cliProjectContent + + Write-Host "Embedded Aspire skills bundle updated to '$($asset.name)' with SHA-256 '$hash'." +} +finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force + } +} diff --git a/eng/scripts/verify-aspire-skills-bundle.ps1 b/eng/scripts/verify-aspire-skills-bundle.ps1 new file mode 100644 index 00000000000..550a5e34b03 --- /dev/null +++ b/eng/scripts/verify-aspire-skills-bundle.ps1 @@ -0,0 +1,63 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param( + [string]$Repository = 'microsoft/aspire-skills' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +$scriptDir = $PSScriptRoot +$repoRoot = (Resolve-Path (Join-Path $scriptDir '..\..')).Path +$embeddedDir = Join-Path $repoRoot 'src\Aspire.Cli\Agents\AspireSkills\Embedded' +$metadataPath = Join-Path $embeddedDir 'aspire-skills.metadata.json' + +if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + throw "The GitHub CLI ('gh') is required to verify the embedded Aspire skills bundle." +} + +if (-not (Test-Path $metadataPath)) { + throw "Embedded Aspire skills metadata was not found at '$metadataPath'." +} + +$metadata = Get-Content -Raw -Path $metadataPath | ConvertFrom-Json + +if ($metadata.repository -ne $Repository) { + throw "Unexpected embedded bundle repository '$($metadata.repository)'. Expected '$Repository'." +} + +if ([string]::IsNullOrWhiteSpace($metadata.tag)) { + throw "Embedded Aspire skills metadata must specify a GitHub release tag." +} + +if ([string]::IsNullOrWhiteSpace($metadata.assetName)) { + throw "Embedded Aspire skills metadata must specify a release asset name." +} + +if ($metadata.assetName -ne [System.IO.Path]::GetFileName($metadata.assetName)) { + throw "Embedded Aspire skills asset name '$($metadata.assetName)' must not contain path separators." +} + +if ([string]::IsNullOrWhiteSpace($metadata.sha256)) { + throw "Embedded Aspire skills metadata must specify the release asset SHA-256 hash." +} + +$archivePath = Join-Path $embeddedDir $metadata.assetName +if (-not (Test-Path $archivePath)) { + throw "Embedded Aspire skills archive was not found at '$archivePath'." +} + +$actualHash = (Get-FileHash -Algorithm SHA256 $archivePath).Hash.ToLowerInvariant() +if ($actualHash -ne $metadata.sha256) { + throw "Embedded bundle SHA-256 mismatch. Expected '$($metadata.sha256)', got '$actualHash'." +} + +$certIdentity = "https://github.com/$($metadata.repository)/.github/workflows/publish.yml@refs/tags/$($metadata.tag)" +gh attestation verify $archivePath ` + --repo $metadata.repository ` + --cert-identity $certIdentity ` + --cert-oidc-issuer 'https://token.actions.githubusercontent.com' + +Write-Host "Embedded Aspire skills bundle '$($metadata.assetName)' verified against GitHub artifact attestation." diff --git a/src/Aspire.Cli/Agents/AspireSkills/AspireSkillsBundle.cs b/src/Aspire.Cli/Agents/AspireSkills/AspireSkillsBundle.cs index dc47c324cce..49c726ea960 100644 --- a/src/Aspire.Cli/Agents/AspireSkills/AspireSkillsBundle.cs +++ b/src/Aspire.Cli/Agents/AspireSkills/AspireSkillsBundle.cs @@ -302,7 +302,7 @@ internal static string NormalizeRelativePath(string? relativePath) return Path.Combine(segments); } - private static string NormalizeSha256(string sha256) + internal static string NormalizeSha256(string sha256) { const string prefix = "sha256-"; return sha256.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) diff --git a/src/Aspire.Cli/Agents/AspireSkills/AspireSkillsInstaller.cs b/src/Aspire.Cli/Agents/AspireSkills/AspireSkillsInstaller.cs index dbc2f1cfdd5..dec2cca8e41 100644 --- a/src/Aspire.Cli/Agents/AspireSkills/AspireSkillsInstaller.cs +++ b/src/Aspire.Cli/Agents/AspireSkills/AspireSkillsInstaller.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.IO.Compression; using System.Net; +using System.Security.Cryptography; using System.Text.Json; using Aspire.Cli.Interaction; using Aspire.Cli.Resources; @@ -21,6 +22,7 @@ namespace Aspire.Cli.Agents.AspireSkills; internal sealed class AspireSkillsInstaller( IGitHubArtifactAttestationVerifier githubArtifactAttestationVerifier, IHttpClientFactory httpClientFactory, + IEmbeddedAspireSkillsBundleProvider embeddedBundleProvider, IInteractionService interactionService, CliExecutionContext executionContext, IConfiguration configuration, @@ -46,7 +48,6 @@ public Task InstallAsync(CancellationToken cancellati AgentCommandStrings.AspireSkillsInstaller_InstallingStatus, () => InstallCoreAsync(cancellationToken)); } - private async Task InstallCoreAsync(CancellationToken cancellationToken) { using var activity = telemetry.StartReportedActivity("AspireSkillsInstaller.Install"); @@ -80,12 +81,24 @@ private async Task InstallCoreAsync(CancellationToken if (githubResult.Status == AcquisitionStatus.Failed) { - activity?.SetStatus(ActivityStatusCode.Error, githubResult.Message); - return AspireSkillsInstallResult.Failed(githubResult.Message ?? AgentCommandStrings.AspireSkillsInstaller_InvalidBundle); + logger.LogDebug("Aspire skills GitHub acquisition failed for version {Version}; falling back to embedded snapshot. Failure: {Failure}", effectiveVersion, githubResult.Message); + } + + var embeddedResult = await InstallFromEmbeddedAsync(cacheRoot, effectiveVersion, activity, cancellationToken).ConfigureAwait(false); + if (embeddedResult.Status == AcquisitionStatus.Installed) + { + CleanupStaleCacheEntries(cacheRoot, effectiveVersion); + return AspireSkillsInstallResult.Installed(embeddedResult.Bundle!); } - activity?.SetStatus(ActivityStatusCode.Error, "GitHub acquisition is unavailable."); - return AspireSkillsInstallResult.Failed(AgentCommandStrings.AspireSkillsInstaller_GitHubUnavailable); + var failureMessage = embeddedResult.Status == AcquisitionStatus.Failed + ? embeddedResult.Message ?? AgentCommandStrings.AspireSkillsInstaller_GitHubUnavailable + : githubResult.Status == AcquisitionStatus.Failed + ? githubResult.Message ?? AgentCommandStrings.AspireSkillsInstaller_GitHubUnavailable + : AgentCommandStrings.AspireSkillsInstaller_GitHubUnavailable; + + activity?.SetStatus(ActivityStatusCode.Error, failureMessage); + return AspireSkillsInstallResult.Failed(failureMessage); } private async Task InstallFromGitHubAsync( @@ -167,6 +180,127 @@ private async Task InstallFromGitHubAsync( } } + private async Task InstallFromEmbeddedAsync( + string cacheRoot, + string version, + Activity? activity, + CancellationToken cancellationToken) + { + var metadata = embeddedBundleProvider.Metadata; + if (metadata is null) + { + logger.LogDebug("No embedded Aspire skills bundle metadata is available."); + return AcquisitionResult.Unavailable(); + } + + if (ValidateEmbeddedMetadata(metadata) is { } metadataError) + { + return AcquisitionResult.Failed($"Embedded Aspire skills bundle metadata is invalid: {metadataError}"); + } + + if (!string.Equals(metadata.Version, version, StringComparison.OrdinalIgnoreCase)) + { + logger.LogDebug( + "Embedded Aspire skills bundle version {EmbeddedVersion} does not match requested version {Version}.", + metadata.Version, + version); + return AcquisitionResult.Unavailable(); + } + + var tempDir = Path.Combine(cacheRoot, $".embedded-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + + try + { + var archivePath = Path.Combine(tempDir, GetSafeFileName(metadata.AssetName!)); + var archiveStream = embeddedBundleProvider.OpenArchive(); + if (archiveStream is null) + { + logger.LogDebug("Embedded Aspire skills archive is unavailable for version {Version}.", version); + return AcquisitionResult.Unavailable(); + } + + await using (archiveStream) + { + await using var fileStream = File.Create(archivePath); + await archiveStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); + } + + ValidateArchiveSha256(archivePath, metadata.Sha256!); + + try + { + var bundle = await CacheArchiveAsync(cacheRoot, archivePath, version, cancellationToken).ConfigureAwait(false); + activity?.SetTag("aspire.skills.source", "embedded"); + activity?.SetTag("aspire.skills.cache_hit", false); + return AcquisitionResult.Installed(bundle); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException or InvalidOperationException) + { + logger.LogWarning(ex, "Embedded Aspire skills bundle {AssetName} is invalid.", metadata.AssetName); + return AcquisitionResult.Failed(string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.AspireSkillsInstaller_InvalidBundle, ex.Message)); + } + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException) + { + logger.LogDebug(ex, "Embedded Aspire skills bundle could not be staged for version {Version}.", version); + return AcquisitionResult.Failed(string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.AspireSkillsInstaller_InvalidBundle, ex.Message)); + } + finally + { + TryDeleteDirectory(tempDir); + } + } + + private static string? ValidateEmbeddedMetadata(EmbeddedAspireSkillsBundleMetadata metadata) + { + if (string.IsNullOrWhiteSpace(metadata.Version)) + { + return "Embedded Aspire skills metadata must specify a version."; + } + + if (!string.Equals(metadata.Repository, GitHubRepository, StringComparison.OrdinalIgnoreCase)) + { + return string.Format(CultureInfo.InvariantCulture, "Embedded Aspire skills metadata repository '{0}' does not match expected repository '{1}'.", metadata.Repository, GitHubRepository); + } + + if (string.IsNullOrWhiteSpace(metadata.Tag)) + { + return "Embedded Aspire skills metadata must specify a GitHub release tag."; + } + + if (string.IsNullOrWhiteSpace(metadata.AssetName)) + { + return "Embedded Aspire skills metadata must specify a release asset name."; + } + + if (string.IsNullOrWhiteSpace(metadata.Sha256)) + { + return "Embedded Aspire skills metadata must specify the release asset SHA-256 hash."; + } + + return null; + } + + private static void ValidateArchiveSha256(string archivePath, string expectedSha256) + { + var expectedHash = AspireSkillsBundle.NormalizeSha256(expectedSha256); + string actualHash; + using (var stream = File.OpenRead(archivePath)) + { + actualHash = Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant(); + } + + if (!string.Equals(expectedHash, actualHash, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException(string.Format( + CultureInfo.InvariantCulture, + "Embedded Aspire skills archive failed SHA-256 verification. Expected '{0}', got '{1}'.", + expectedHash, + actualHash)); + } + } + private async Task TryGetGitHubReleaseAsync(HttpClient httpClient, string version, CancellationToken cancellationToken) { foreach (var tag in GetGitHubTagCandidates(version)) diff --git a/src/Aspire.Cli/Agents/AspireSkills/Embedded/aspire-skills-v0.0.1.tgz b/src/Aspire.Cli/Agents/AspireSkills/Embedded/aspire-skills-v0.0.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..e25af3228df2f9c81ea780a9e1853e5532e9fd0d GIT binary patch literal 95516 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%0DN48lW(Q`5sWFG4 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 b!_SAG4?iD%KK%UW|M`CbRI7({0H6f`v6bx@ literal 0 HcmV?d00001 diff --git a/src/Aspire.Cli/Agents/AspireSkills/Embedded/aspire-skills.metadata.json b/src/Aspire.Cli/Agents/AspireSkills/Embedded/aspire-skills.metadata.json new file mode 100644 index 00000000000..4b76e028392 --- /dev/null +++ b/src/Aspire.Cli/Agents/AspireSkills/Embedded/aspire-skills.metadata.json @@ -0,0 +1,7 @@ +{ + "version": "0.0.1", + "repository": "microsoft/aspire-skills", + "tag": "v0.0.1", + "assetName": "aspire-skills-v0.0.1.tgz", + "sha256": "8f0aa535917bb6d2589acbf8f986c7b0d622ee7744e39c526bb7166c0664b53c" +} diff --git a/src/Aspire.Cli/Agents/AspireSkills/EmbeddedAspireSkillsBundleProvider.cs b/src/Aspire.Cli/Agents/AspireSkills/EmbeddedAspireSkillsBundleProvider.cs new file mode 100644 index 00000000000..3b7d70cadd1 --- /dev/null +++ b/src/Aspire.Cli/Agents/AspireSkills/EmbeddedAspireSkillsBundleProvider.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Microsoft.Extensions.Logging; + +namespace Aspire.Cli.Agents.AspireSkills; + +/// +/// Provides access to the Aspire skills bundle snapshot embedded in the CLI assembly. +/// +internal interface IEmbeddedAspireSkillsBundleProvider +{ + /// + /// Gets metadata for the embedded Aspire skills bundle snapshot. + /// + EmbeddedAspireSkillsBundleMetadata? Metadata { get; } + + /// + /// Opens the embedded Aspire skills bundle archive. + /// + Stream? OpenArchive(); +} + +internal sealed class EmbeddedAspireSkillsBundleProvider : IEmbeddedAspireSkillsBundleProvider +{ + private const string ArchiveResourceName = "aspire-skills.bundle.tgz"; + private const string MetadataResourceName = "aspire-skills.metadata.json"; + + private readonly ILogger _logger; + private readonly Lazy _metadata; + + public EmbeddedAspireSkillsBundleProvider(ILogger logger) + { + _logger = logger; + _metadata = new Lazy(LoadMetadata); + } + + public EmbeddedAspireSkillsBundleMetadata? Metadata => _metadata.Value; + + public Stream? OpenArchive() + { + var stream = typeof(EmbeddedAspireSkillsBundleProvider).Assembly.GetManifestResourceStream(ArchiveResourceName); + if (stream is null) + { + _logger.LogDebug("Embedded Aspire skills archive resource {ResourceName} was not found.", ArchiveResourceName); + } + + return stream; + } + + private EmbeddedAspireSkillsBundleMetadata? LoadMetadata() + { + using var stream = typeof(EmbeddedAspireSkillsBundleProvider).Assembly.GetManifestResourceStream(MetadataResourceName); + if (stream is null) + { + _logger.LogDebug("Embedded Aspire skills metadata resource {ResourceName} was not found.", MetadataResourceName); + return null; + } + + try + { + var metadata = JsonSerializer.Deserialize( + stream, + AspireSkillsJsonSerializerContext.Default.EmbeddedAspireSkillsBundleMetadata); + + if (metadata is null) + { + _logger.LogDebug("Embedded Aspire skills metadata resource {ResourceName} was empty.", MetadataResourceName); + } + + return metadata; + } + catch (JsonException ex) + { + _logger.LogWarning(ex, "Embedded Aspire skills metadata resource {ResourceName} could not be parsed.", MetadataResourceName); + return null; + } + } +} diff --git a/src/Aspire.Cli/Agents/AspireSkills/SkillBundleManifest.cs b/src/Aspire.Cli/Agents/AspireSkills/SkillBundleManifest.cs index a41777944c1..5e48ed3f573 100644 --- a/src/Aspire.Cli/Agents/AspireSkills/SkillBundleManifest.cs +++ b/src/Aspire.Cli/Agents/AspireSkills/SkillBundleManifest.cs @@ -56,6 +56,22 @@ internal sealed class SkillBundleFile public string? Sha256 { get; init; } } +/// +/// Describes the Aspire skills bundle archive embedded in the CLI. +/// +internal sealed class EmbeddedAspireSkillsBundleMetadata +{ + public string? Version { get; init; } + + public string? Repository { get; init; } + + public string? Tag { get; init; } + + public string? AssetName { get; init; } + + public string? Sha256 { get; init; } +} + /// /// Source-generation context for Aspire skills bundle JSON. /// @@ -65,6 +81,7 @@ internal sealed class SkillBundleFile PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true)] [JsonSerializable(typeof(SkillBundleManifest))] +[JsonSerializable(typeof(EmbeddedAspireSkillsBundleMetadata))] internal sealed partial class AspireSkillsJsonSerializerContext : JsonSerializerContext { } diff --git a/src/Aspire.Cli/Aspire.Cli.csproj b/src/Aspire.Cli/Aspire.Cli.csproj index 096c95408cf..afd4bd73f5e 100644 --- a/src/Aspire.Cli/Aspire.Cli.csproj +++ b/src/Aspire.Cli/Aspire.Cli.csproj @@ -297,6 +297,12 @@ + + false + + + false + diff --git a/src/Aspire.Cli/Commands/AgentInitCommand.cs b/src/Aspire.Cli/Commands/AgentInitCommand.cs index bfb45024e53..5788f880841 100644 --- a/src/Aspire.Cli/Commands/AgentInitCommand.cs +++ b/src/Aspire.Cli/Commands/AgentInitCommand.cs @@ -311,6 +311,8 @@ private async Task ExecuteAgentInitAsync(DirectoryInfo } } + var installedSkills = new List(); + foreach (var location in selectedLocations) { context.AddSkillBaseDirectory(location.RelativeSkillDirectory); @@ -328,27 +330,39 @@ private async Task ExecuteAgentInitAsync(DirectoryInfo continue; } - hasErrors |= !await InstallSkillAsync( + var installResult = await InstallSkillAsync( workspaceRoot, location.RelativeSkillDirectory, skill, aspireSkillsBundle, isUserLevel: false, cancellationToken); + hasErrors |= !installResult.Succeeded; + if (installResult.UpdatedSkill is not null) + { + installedSkills.Add(installResult.UpdatedSkill); + } if (location.IncludeUserLevel) { - hasErrors |= !await InstallSkillAsync( + installResult = await InstallSkillAsync( ExecutionContext.HomeDirectory, location.RelativeSkillDirectory, skill, aspireSkillsBundle, isUserLevel: true, cancellationToken); + hasErrors |= !installResult.Succeeded; + if (installResult.UpdatedSkill is not null) + { + installedSkills.Add(installResult.UpdatedSkill); + } } } } + DisplayInstalledSkillsSummary(installedSkills); + // --- Phase 4: Handle Playwright CLI (installs binary + mirrors skill files to registered directories) --- var selectedSkillDirs = selectedLocations.Select(l => l.RelativeSkillDirectory).ToHashSet(StringComparer.OrdinalIgnoreCase); if (selectedSkills.Contains(SkillDefinition.PlaywrightCli) && selectedLocations.Count > 0) @@ -424,8 +438,8 @@ private async Task ExecuteAgentInitAsync(DirectoryInfo /// /// Installs the files for a skill at the specified location, creating or updating them as needed. /// - /// true if successful, false if an error occurred. - private async Task InstallSkillAsync( + /// The install result, including the skill/location pair when files were updated. + private async Task InstallSkillAsync( DirectoryInfo rootDirectory, string relativeSkillDirectory, SkillDefinition skill, @@ -465,23 +479,60 @@ private async Task InstallSkillAsync( if (!anyFileUpdated) { - return true; + return new(Succeeded: true, UpdatedSkill: null); } - var displayRelativeSkillPath = relativeSkillPath - .Replace(Path.DirectorySeparatorChar, '/') - .Replace(Path.AltDirectorySeparatorChar, '/'); - var displayPath = isUserLevel ? $"~/{displayRelativeSkillPath}" : displayRelativeSkillPath; - _interactionService.DisplayMessage(KnownEmojis.Robot, - string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkill, skill.Name, displayPath)); - return true; + var displayLocation = GetDisplaySkillDirectory(relativeSkillDirectory, isUserLevel); + return new(Succeeded: true, new InstalledSkillSummaryItem(skill.Name, displayLocation)); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException) { _interactionService.DisplayError( string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_FailedToInstallSkill, skill.Name, fullSkillDirectoryPath, ex.Message)); - return false; + return new(Succeeded: false, UpdatedSkill: null); + } + } + + private void DisplayInstalledSkillsSummary(IReadOnlyList installedSkills) + { + if (installedSkills.Count == 0) + { + return; + } + + var skillNames = string.Join(", ", GetUniqueValues(installedSkills.Select(static installedSkill => installedSkill.SkillName))); + var locations = string.Join(", ", GetUniqueValues(installedSkills.Select(static installedSkill => installedSkill.DisplayLocation))); + var message = string.Join(Environment.NewLine, + AgentCommandStrings.InitCommand_InstalledSkillsSummary, + $" {string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummarySkills, skillNames)}", + $" {string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummaryLocations, locations)}"); + + _interactionService.DisplayMessage(KnownEmojis.Robot, message); + } + + private static IReadOnlyList GetUniqueValues(IEnumerable values) + { + var uniqueValues = new List(); + var seenValues = new HashSet(StringComparer.Ordinal); + + foreach (var value in values) + { + if (seenValues.Add(value)) + { + uniqueValues.Add(value); + } } + + return uniqueValues; + } + + private static string GetDisplaySkillDirectory(string relativeSkillDirectory, bool isUserLevel) + { + var displayRelativeSkillDirectory = relativeSkillDirectory + .Replace(Path.DirectorySeparatorChar, '/') + .Replace(Path.AltDirectorySeparatorChar, '/'); + + return isUserLevel ? $"~/{displayRelativeSkillDirectory}" : displayRelativeSkillDirectory; } private static async Task> GetSkillFilesAsync(SkillDefinition skill, AspireSkillsBundle? aspireSkillsBundle, CancellationToken cancellationToken) @@ -509,6 +560,10 @@ private enum AgentInitErrorMode Strict, BestEffort } + + private sealed record InstalledSkillSummaryItem(string SkillName, string DisplayLocation); + + private readonly record struct SkillInstallResult(bool Succeeded, InstalledSkillSummaryItem? UpdatedSkill); } internal readonly record struct AgentInitExecutionResult( diff --git a/src/Aspire.Cli/Program.cs b/src/Aspire.Cli/Program.cs index dc8df361491..8d6cc53c443 100644 --- a/src/Aspire.Cli/Program.cs +++ b/src/Aspire.Cli/Program.cs @@ -464,6 +464,7 @@ internal static async Task BuildApplicationAsync(string[] args, CliStartu builder.Services.AddSingleton(); builder.Services.AddHttpClient(); builder.Services.AddHttpClient(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs index f1e80b58211..d0273f09185 100644 --- a/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs @@ -358,7 +358,7 @@ internal static string AspireSkillsInstaller_InstallingStatus { } /// - /// Looks up a localized string similar to Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available.. + /// Looks up a localized string similar to Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available.. /// internal static string AspireSkillsInstaller_GitHubUnavailable { get { diff --git a/src/Aspire.Cli/Resources/AgentCommandStrings.resx b/src/Aspire.Cli/Resources/AgentCommandStrings.resx index 275a332b141..6535f3d6835 100644 --- a/src/Aspire.Cli/Resources/AgentCommandStrings.resx +++ b/src/Aspire.Cli/Resources/AgentCommandStrings.resx @@ -160,7 +160,7 @@ Installing Aspire skills... - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. The Aspire skills bundle is invalid: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf index 4c3fa5cbf55..9061cb5a136 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf @@ -3,8 +3,8 @@ - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf index 9083254de4e..dce8ab8f751 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf @@ -3,8 +3,8 @@ - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf index d3657b160c0..c3bd6b71ef0 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf @@ -3,8 +3,8 @@ - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf index 297ceb6f134..75c7086d777 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf @@ -3,8 +3,8 @@ - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf index 0735f52225e..7ce842b0ab5 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf @@ -3,8 +3,8 @@ - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf index 922fd845ae4..120037928ed 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf @@ -3,8 +3,8 @@ - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf index 87a7da1cb2e..3aa39fab40a 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf @@ -3,8 +3,8 @@ - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf index 09c9a59599b..bbf05bf0c65 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf @@ -3,8 +3,8 @@ - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf index cddc1e8fedd..8247914edb3 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf @@ -3,8 +3,8 @@ - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf index f42eb379f17..194d522626a 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf @@ -3,8 +3,8 @@ - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf index 8559bf134d6..b6ae61c8a33 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf @@ -3,8 +3,8 @@ - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf index 2e2531cc0ad..99db98a885c 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf @@ -3,8 +3,8 @@ - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf index 177fbc55ac9..0074ecf6427 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf @@ -3,8 +3,8 @@ - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. - Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. + Aspire skills could not be downloaded from the verified GitHub release asset, and no valid cached or embedded bundle is available. diff --git a/tests/Aspire.Cli.Tests/Agents/AspireSkillsInstallerTests.cs b/tests/Aspire.Cli.Tests/Agents/AspireSkillsInstallerTests.cs index f17750521e0..3b418f7cc99 100644 --- a/tests/Aspire.Cli.Tests/Agents/AspireSkillsInstallerTests.cs +++ b/tests/Aspire.Cli.Tests/Agents/AspireSkillsInstallerTests.cs @@ -31,12 +31,14 @@ public async Task InstallAsync_WhenValidBundleIsCached_UsesCacheWithoutNetwork() var executionContext = TestExecutionContextHelper.CreateExecutionContext(new DirectoryInfo(rootDirectory)); var cachedBundleDirectory = Path.Combine(executionContext.CacheDirectory.FullName, "aspire-skills", AspireSkillsInstaller.Version); await CreateCachedBundleAsync(cachedBundleDirectory); - var installer = CreateInstaller(executionContext); + var embeddedBundleProvider = await CreateEmbeddedBundleProviderAsync(); + var installer = CreateInstaller(executionContext, embeddedBundleProvider: embeddedBundleProvider); var result = await installer.InstallAsync(CancellationToken.None); Assert.Equal(AspireSkillsInstallStatus.Installed, result.Status); Assert.NotNull(result.Bundle); + Assert.False(embeddedBundleProvider.OpenArchiveCalled); } finally { @@ -89,6 +91,47 @@ public async Task InstallAsync_WhenGitHubReleaseIsUnavailableAndNoCache_ReturnsF } } + [Fact] + public async Task InstallAsync_WhenGitHubReleaseIsUnavailableAndEmbeddedBundleMatches_UsesEmbeddedBundle() + { + var rootDirectory = CreateTempDirectory(); + + try + { + var executionContext = TestExecutionContextHelper.CreateExecutionContext(new DirectoryInfo(rootDirectory)); + var embeddedBundleProvider = await CreateEmbeddedBundleProviderAsync(); + var installer = CreateInstaller(executionContext, embeddedBundleProvider: embeddedBundleProvider); + + var result = await installer.InstallAsync(CancellationToken.None); + + Assert.Equal(AspireSkillsInstallStatus.Installed, result.Status); + Assert.NotNull(result.Bundle); + Assert.True(embeddedBundleProvider.OpenArchiveCalled); + } + finally + { + Directory.Delete(rootDirectory, recursive: true); + } + } + + [Fact] + public void EmbeddedAspireSkillsBundleProvider_OpensSnapshotResource() + { + var provider = new EmbeddedAspireSkillsBundleProvider(NullLogger.Instance); + + var metadata = Assert.IsType(provider.Metadata); + using var archiveStream = Assert.IsAssignableFrom(provider.OpenArchive()); + + Assert.Equal(AspireSkillsInstaller.Version, metadata.Version); + Assert.Equal(AspireSkillsInstaller.GitHubRepository, metadata.Repository); + Assert.Equal(metadata.Sha256, ComputeSha256(archiveStream)); + } + + private static string ComputeSha256(Stream stream) + { + return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant(); + } + [Fact] public async Task InstallAsync_WhenGitHubReleaseAssetIsAvailable_UsesGitHub() { @@ -115,7 +158,12 @@ public async Task InstallAsync_WhenGitHubReleaseAssetIsAvailable_UsesGitHub() }); var executionContext = TestExecutionContextHelper.CreateExecutionContext(new DirectoryInfo(rootDirectory)); var attestationVerifier = new TestGitHubArtifactAttestationVerifier(); - var installer = CreateInstaller(executionContext, httpMessageHandler: handler, githubArtifactAttestationVerifier: attestationVerifier); + var embeddedBundleProvider = await CreateEmbeddedBundleProviderAsync(); + var installer = CreateInstaller( + executionContext, + httpMessageHandler: handler, + githubArtifactAttestationVerifier: attestationVerifier, + embeddedBundleProvider: embeddedBundleProvider); var result = await installer.InstallAsync(CancellationToken.None); @@ -127,6 +175,7 @@ public async Task InstallAsync_WhenGitHubReleaseAssetIsAvailable_UsesGitHub() Assert.Equal(AspireSkillsInstaller.ExpectedWorkflowPath, attestationVerifier.ExpectedWorkflowPath); Assert.Equal(GitHubReleaseAssetBuildType, attestationVerifier.ExpectedBuildType); Assert.Equal(AspireSkillsInstaller.Version, attestationVerifier.ExpectedVersion); + Assert.False(embeddedBundleProvider.OpenArchiveCalled); Assert.NotNull(releaseRequestUri); Assert.NotNull(assetRequestUri); Assert.Contains("/microsoft/aspire-skills/releases/tags/v0.0.1", releaseRequestUri.AbsolutePath); @@ -139,7 +188,7 @@ public async Task InstallAsync_WhenGitHubReleaseAssetIsAvailable_UsesGitHub() } [Fact] - public async Task InstallAsync_WhenGitHubAttestationFails_ReturnsFailure() + public async Task InstallAsync_WhenGitHubAttestationFails_FallsBackToEmbeddedBundle() { var rootDirectory = CreateTempDirectory(); @@ -163,13 +212,85 @@ public async Task InstallAsync_WhenGitHubAttestationFails_ReturnsFailure() { Result = new ProvenanceVerificationResult { Outcome = ProvenanceVerificationOutcome.WorkflowMismatch } }; - var installer = CreateInstaller(executionContext, httpMessageHandler: handler, githubArtifactAttestationVerifier: attestationVerifier); + var embeddedBundleProvider = await CreateEmbeddedBundleProviderAsync(); + var installer = CreateInstaller( + executionContext, + httpMessageHandler: handler, + githubArtifactAttestationVerifier: attestationVerifier, + embeddedBundleProvider: embeddedBundleProvider); + + var result = await installer.InstallAsync(CancellationToken.None); + + Assert.Equal(AspireSkillsInstallStatus.Installed, result.Status); + Assert.NotNull(result.Bundle); + Assert.True(attestationVerifier.VerifyCalled); + Assert.True(embeddedBundleProvider.OpenArchiveCalled); + } + finally + { + Directory.Delete(rootDirectory, recursive: true); + } + } + + [Fact] + public async Task InstallAsync_WhenVersionOverrideDoesNotMatchEmbeddedBundle_DoesNotUseEmbeddedBundle() + { + var rootDirectory = CreateTempDirectory(); + + try + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [AspireSkillsInstaller.VersionOverrideKey] = "9.9.9" + }) + .Build(); + var executionContext = TestExecutionContextHelper.CreateExecutionContext(new DirectoryInfo(rootDirectory)); + var embeddedBundleProvider = await CreateEmbeddedBundleProviderAsync(); + var installer = CreateInstaller( + executionContext, + configuration: configuration, + embeddedBundleProvider: embeddedBundleProvider); + + var result = await installer.InstallAsync(CancellationToken.None); + + Assert.Equal(AspireSkillsInstallStatus.Failed, result.Status); + Assert.False(embeddedBundleProvider.OpenArchiveCalled); + } + finally + { + Directory.Delete(rootDirectory, recursive: true); + } + } + + [Fact] + public async Task InstallAsync_WhenEmbeddedArchiveHashDoesNotMatch_ReturnsFailure() + { + var rootDirectory = CreateTempDirectory(); + + try + { + var embeddedBundleProvider = await CreateEmbeddedBundleProviderAsync(); + embeddedBundleProvider.Metadata = new EmbeddedAspireSkillsBundleMetadata + { + Version = AspireSkillsInstaller.Version, + Repository = AspireSkillsInstaller.GitHubRepository, + Tag = $"v{AspireSkillsInstaller.Version}", + AssetName = $"aspire-skills-v{AspireSkillsInstaller.Version}.tgz", + Sha256 = "0000000000000000000000000000000000000000000000000000000000000000" + }; + var executionContext = TestExecutionContextHelper.CreateExecutionContext(new DirectoryInfo(rootDirectory)); + var installer = CreateInstaller( + executionContext, + embeddedBundleProvider: embeddedBundleProvider); var result = await installer.InstallAsync(CancellationToken.None); Assert.Equal(AspireSkillsInstallStatus.Failed, result.Status); Assert.NotNull(result.Message); - Assert.Contains("Provenance", result.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("SHA-256", result.Message, StringComparison.Ordinal); + Assert.Contains("0000000000000000000000000000000000000000000000000000000000000000", result.Message, StringComparison.Ordinal); + Assert.True(embeddedBundleProvider.OpenArchiveCalled); } finally { @@ -180,14 +301,17 @@ public async Task InstallAsync_WhenGitHubAttestationFails_ReturnsFailure() private static AspireSkillsInstaller CreateInstaller( CliExecutionContext executionContext, HttpMessageHandler? httpMessageHandler = null, - TestGitHubArtifactAttestationVerifier? githubArtifactAttestationVerifier = null) + TestGitHubArtifactAttestationVerifier? githubArtifactAttestationVerifier = null, + IConfiguration? configuration = null, + IEmbeddedAspireSkillsBundleProvider? embeddedBundleProvider = null) { return new AspireSkillsInstaller( githubArtifactAttestationVerifier ?? new TestGitHubArtifactAttestationVerifier(), new MockHttpClientFactory(httpMessageHandler ?? new MockHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.NotFound))), + embeddedBundleProvider ?? new TestEmbeddedAspireSkillsBundleProvider(), new TestInteractionService(), executionContext, - new ConfigurationBuilder().Build(), + configuration ?? new ConfigurationBuilder().Build(), TestTelemetryHelper.CreateInitializedTelemetry(), NullLogger.Instance); } @@ -273,6 +397,28 @@ private static string ComputeSha256(string path) return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant(); } + private static string ComputeSha256(byte[] bytes) + { + return Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(); + } + + private static async Task CreateEmbeddedBundleProviderAsync() + { + var archiveBytes = await CreateBundleArchiveBytesAsync(); + return new TestEmbeddedAspireSkillsBundleProvider + { + Metadata = new EmbeddedAspireSkillsBundleMetadata + { + Version = AspireSkillsInstaller.Version, + Repository = AspireSkillsInstaller.GitHubRepository, + Tag = $"v{AspireSkillsInstaller.Version}", + AssetName = $"aspire-skills-v{AspireSkillsInstaller.Version}.tgz", + Sha256 = ComputeSha256(archiveBytes) + }, + ArchiveBytes = archiveBytes + }; + } + private static HttpResponseMessage CreateJsonResponse(string json) { return new HttpResponseMessage(HttpStatusCode.OK) @@ -341,4 +487,18 @@ public Task VerifyAsync( } } + private sealed class TestEmbeddedAspireSkillsBundleProvider : IEmbeddedAspireSkillsBundleProvider + { + public EmbeddedAspireSkillsBundleMetadata? Metadata { get; set; } + + public byte[]? ArchiveBytes { get; init; } + + public bool OpenArchiveCalled { get; private set; } + + public Stream? OpenArchive() + { + OpenArchiveCalled = true; + return ArchiveBytes is null ? null : new MemoryStream(ArchiveBytes, writable: false); + } + } } diff --git a/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs index 6c7d792f38d..2492777c06c 100644 --- a/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs @@ -222,7 +222,7 @@ public async Task AgentInitCommand_NonInteractive_WithoutWorkspaceRoot_UsesWorki } [Fact] - public async Task AgentInitCommand_NonInteractive_WithUnavailableAspireSkillsBundle_Fails() + public async Task AgentInitCommand_NonInteractive_WithUnavailableAspireSkillsBundle_WarnsAndSucceedsWithoutSelectedAspireSkills() { using var workspace = TemporaryWorkspace.Create(outputHelper); const string installFailureMessage = "Aspire skills bundle is unavailable."; @@ -242,9 +242,12 @@ public async Task AgentInitCommand_NonInteractive_WithUnavailableAspireSkillsBun var exitCode = await result.InvokeAsync().DefaultTimeout(); - Assert.Equal(CliExitCodes.InvalidCommand, exitCode); - Assert.Contains(installFailureMessage, interactionService.DisplayedErrors); - Assert.Empty(interactionService.DisplayedSuccess); + Assert.Equal(CliExitCodes.Success, exitCode); + Assert.DoesNotContain(installFailureMessage, interactionService.DisplayedErrors); + Assert.Contains( + interactionService.DisplayedMessages, + message => message.Emoji.Equals(KnownEmojis.Warning) && message.Message == installFailureMessage); + Assert.Contains(McpCommandStrings.InitCommand_ConfigurationComplete, interactionService.DisplayedSuccess); } [Fact] From 7fe84b82a39d7106124d117ee5293d47d78c9249 Mon Sep 17 00:00:00 2001 From: IEvangelist <7679720+IEvangelist@users.noreply.github.com> Date: Wed, 27 May 2026 10:56:56 -0500 Subject: [PATCH 2/3] Fix release backport resource strings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Resources/AgentCommandStrings.Designer.cs | 24 ++++++- .../Resources/AgentCommandStrings.resx | 10 ++- .../Resources/xlf/AgentCommandStrings.cs.xlf | 16 ++++- .../Resources/xlf/AgentCommandStrings.de.xlf | 16 ++++- .../Resources/xlf/AgentCommandStrings.es.xlf | 16 ++++- .../Resources/xlf/AgentCommandStrings.fr.xlf | 16 ++++- .../Resources/xlf/AgentCommandStrings.it.xlf | 16 ++++- .../Resources/xlf/AgentCommandStrings.ja.xlf | 16 ++++- .../Resources/xlf/AgentCommandStrings.ko.xlf | 16 ++++- .../Resources/xlf/AgentCommandStrings.pl.xlf | 16 ++++- .../xlf/AgentCommandStrings.pt-BR.xlf | 16 ++++- .../Resources/xlf/AgentCommandStrings.ru.xlf | 16 ++++- .../Resources/xlf/AgentCommandStrings.tr.xlf | 16 ++++- .../xlf/AgentCommandStrings.zh-Hans.xlf | 16 ++++- .../xlf/AgentCommandStrings.zh-Hant.xlf | 16 ++++- .../Commands/AgentInitCommandTests.cs | 68 +++++++++++++++---- 16 files changed, 254 insertions(+), 56 deletions(-) diff --git a/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs index d0273f09185..6133c22ac5e 100644 --- a/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs @@ -205,11 +205,29 @@ internal static string InitCommand_PlaywrightCliSkipped { } /// - /// Looks up a localized string similar to Installed {0} skill ({1}).. + /// Looks up a localized string similar to Installed Aspire agent skills:. /// - internal static string InitCommand_InstalledSkill { + internal static string InitCommand_InstalledSkillsSummary { get { - return ResourceManager.GetString("InitCommand_InstalledSkill", resourceCulture); + return ResourceManager.GetString("InitCommand_InstalledSkillsSummary", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Skills: {0}. + /// + internal static string InitCommand_InstalledSkillsSummarySkills { + get { + return ResourceManager.GetString("InitCommand_InstalledSkillsSummarySkills", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Locations: {0}. + /// + internal static string InitCommand_InstalledSkillsSummaryLocations { + get { + return ResourceManager.GetString("InitCommand_InstalledSkillsSummaryLocations", resourceCulture); } } diff --git a/src/Aspire.Cli/Resources/AgentCommandStrings.resx b/src/Aspire.Cli/Resources/AgentCommandStrings.resx index 6535f3d6835..2364ff91d8d 100644 --- a/src/Aspire.Cli/Resources/AgentCommandStrings.resx +++ b/src/Aspire.Cli/Resources/AgentCommandStrings.resx @@ -108,8 +108,14 @@ Playwright CLI requires npm, which was not found on PATH. Skipping installation. - - Installed {0} skill ({1}). + + Installed Aspire agent skills: + + + Skills: {0} + + + Locations: {0} Failed to install {0} skill at {1}: {2} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf index 9061cb5a136..7139b9c497a 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf index dce8ab8f751..a09a22f0b5d 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf index c3bd6b71ef0..416a73644e6 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf index 75c7086d777..38117b59c88 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf index 7ce842b0ab5..670db6c9e77 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf index 120037928ed..297b0f9eb54 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf index 3aa39fab40a..05a8359420d 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf index bbf05bf0c65..d87eccc448a 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf index 8247914edb3..66b4e8820f1 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf index 194d522626a..76dc2665fa2 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf index b6ae61c8a33..65b85538393 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf index 99db98a885c..f4c08201732 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf index 0074ecf6427..62de972aa38 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs index 2492777c06c..ace28bcd5c4 100644 --- a/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs @@ -17,7 +17,7 @@ namespace Aspire.Cli.Tests.Commands; public class AgentInitCommandTests(ITestOutputHelper outputHelper) { [Fact] - public async Task AgentInitCommand_UsesNormalizedDisplayPath_WhenInstallingUserLevelSkill() + public async Task AgentInitCommand_SummarizesNormalizedDisplayPath_WhenInstallingUserLevelSkill() { using var workspace = TemporaryWorkspace.Create(outputHelper); var homeDirectory = workspace.CreateDirectory("fake-home"); @@ -44,13 +44,60 @@ public async Task AgentInitCommand_UsesNormalizedDisplayPath_WhenInstallingUserL var exitCode = await result.InvokeAsync().DefaultTimeout(); Assert.Equal(0, exitCode); + var expectedSummary = string.Join(Environment.NewLine, + AgentCommandStrings.InitCommand_InstalledSkillsSummary, + $" {string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummarySkills, SkillDefinition.Aspire.Name)}", + $" {string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummaryLocations, ".agents/skills, ~/.agents/skills")}"); + Assert.Contains( interactionService.DisplayedMessages, - displayedMessage => displayedMessage.Message == string.Format( + displayedMessage => displayedMessage.Emoji.Equals(KnownEmojis.Robot) && displayedMessage.Message == expectedSummary); + Assert.DoesNotContain( + interactionService.DisplayedMessages, + displayedMessage => displayedMessage.Message.Contains("Installed aspire skill", StringComparison.Ordinal)); + } + + [Fact] + public async Task AgentInitCommand_SummarizesDefaultSkillsOnce() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var homeDirectory = workspace.CreateDirectory("fake-home"); + var interactionService = new TestInteractionService(); + interactionService.SetupStringPromptResponse(workspace.WorkspaceRoot.FullName); + interactionService.PromptForSelectionsCallback = (_, choices, _, _) => choices.Cast() + .Where(choice => choice switch + { + SkillLocation location => location == SkillLocation.Standard, + SkillDefinition skill => skill.IsDefault, + _ => false + }) + .ToList(); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.InteractionServiceFactory = _ => interactionService; + options.CliExecutionContextFactory = _ => CreateExecutionContext(workspace.WorkspaceRoot, homeDirectory); + }); + + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + var result = command.Parse("agent init"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(0, exitCode); + + var expectedSummary = string.Join(Environment.NewLine, + AgentCommandStrings.InitCommand_InstalledSkillsSummary, + $" {string.Format( CultureInfo.CurrentCulture, - AgentCommandStrings.InitCommand_InstalledSkill, - SkillDefinition.Aspire.Name, - "~/.agents/skills/aspire")); + AgentCommandStrings.InitCommand_InstalledSkillsSummarySkills, + string.Join(", ", SkillDefinition.All.Where(static skill => skill.IsDefault).Select(static skill => skill.Name)))}", + $" {string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummaryLocations, ".agents/skills, ~/.agents/skills")}"); + var message = Assert.Single(interactionService.DisplayedMessages, displayedMessage => displayedMessage.Emoji.Equals(KnownEmojis.Robot)); + Assert.Equal(expectedSummary, message.Message); + Assert.DoesNotContain( + interactionService.DisplayedMessages, + displayedMessage => displayedMessage.Message.Contains("Installed aspire skill", StringComparison.Ordinal)); } [Fact] @@ -222,7 +269,7 @@ public async Task AgentInitCommand_NonInteractive_WithoutWorkspaceRoot_UsesWorki } [Fact] - public async Task AgentInitCommand_NonInteractive_WithUnavailableAspireSkillsBundle_WarnsAndSucceedsWithoutSelectedAspireSkills() + public async Task AgentInitCommand_NonInteractive_WithUnavailableAspireSkillsBundle_Fails() { using var workspace = TemporaryWorkspace.Create(outputHelper); const string installFailureMessage = "Aspire skills bundle is unavailable."; @@ -242,12 +289,9 @@ public async Task AgentInitCommand_NonInteractive_WithUnavailableAspireSkillsBun var exitCode = await result.InvokeAsync().DefaultTimeout(); - Assert.Equal(CliExitCodes.Success, exitCode); - Assert.DoesNotContain(installFailureMessage, interactionService.DisplayedErrors); - Assert.Contains( - interactionService.DisplayedMessages, - message => message.Emoji.Equals(KnownEmojis.Warning) && message.Message == installFailureMessage); - Assert.Contains(McpCommandStrings.InitCommand_ConfigurationComplete, interactionService.DisplayedSuccess); + Assert.Equal(CliExitCodes.InvalidCommand, exitCode); + Assert.Contains(installFailureMessage, interactionService.DisplayedErrors); + Assert.Empty(interactionService.DisplayedSuccess); } [Fact] From ad335b86ed148660c0644daba2a12fac9f5f23b2 Mon Sep 17 00:00:00 2001 From: IEvangelist <7679720+IEvangelist@users.noreply.github.com> Date: Wed, 27 May 2026 11:17:52 -0500 Subject: [PATCH 3/3] Fix Docker compose prepare test runtime dependency Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DockerComposePublisherTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs b/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs index 7ac8fdec09f..17ef7854130 100644 --- a/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs +++ b/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs @@ -2,11 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREPIPELINES003 +#pragma warning disable ASPIRECONTAINERRUNTIME001 using System.Text.RegularExpressions; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Docker.Resources.ComposeNodes; using Aspire.Hosting.Publishing; +using Aspire.Hosting.Tests.Publishing; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; @@ -812,6 +814,8 @@ public async Task PrepareStep_ResolvesContainerImageReferenceViaIValueProvider() var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, tempDir.Path, step: "prepare-docker-compose"); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => (IContainerRuntimeResolver)sp.GetRequiredService()); builder.AddDockerComposeEnvironment("docker-compose");