diff --git a/cli/azd/extensions/azure.ai.agents/extension.yaml b/cli/azd/extensions/azure.ai.agents/extension.yaml index 939c5e33bde..7e8ce5a957f 100644 --- a/cli/azd/extensions/azure.ai.agents/extension.yaml +++ b/cli/azd/extensions/azure.ai.agents/extension.yaml @@ -5,7 +5,7 @@ displayName: Foundry agents (Preview) description: Ship agents with Microsoft Foundry from your terminal. (Preview) usage: azd ai agent [options] # NOTE: Make sure version.txt is in sync with this version. -version: 0.1.21-preview +version: 0.1.22-preview requiredAzdVersion: ">1.23.13" language: go capabilities: diff --git a/cli/azd/extensions/azure.ai.agents/version.txt b/cli/azd/extensions/azure.ai.agents/version.txt index 84c8f2d9e45..440ea160368 100644 --- a/cli/azd/extensions/azure.ai.agents/version.txt +++ b/cli/azd/extensions/azure.ai.agents/version.txt @@ -1 +1 @@ -0.1.21-preview +0.1.22-preview diff --git a/cli/azd/extensions/azure.ai.finetune/extension.yaml b/cli/azd/extensions/azure.ai.finetune/extension.yaml index afcac71f338..2eea05b6047 100644 --- a/cli/azd/extensions/azure.ai.finetune/extension.yaml +++ b/cli/azd/extensions/azure.ai.finetune/extension.yaml @@ -3,7 +3,7 @@ namespace: ai.finetuning displayName: Foundry Fine Tuning (Preview) description: Extension for Foundry Fine Tuning. (Preview) usage: azd ai finetuning [options] -version: 0.0.17-preview +version: 0.0.18-preview language: go capabilities: - custom-commands diff --git a/cli/azd/extensions/azure.ai.finetune/version.txt b/cli/azd/extensions/azure.ai.finetune/version.txt index 4a615f30a4e..575667c628c 100644 --- a/cli/azd/extensions/azure.ai.finetune/version.txt +++ b/cli/azd/extensions/azure.ai.finetune/version.txt @@ -1 +1 @@ -0.0.17-preview +0.0.18-preview diff --git a/cli/azd/extensions/azure.ai.models/extension.yaml b/cli/azd/extensions/azure.ai.models/extension.yaml index 43f17641435..27029d82468 100644 --- a/cli/azd/extensions/azure.ai.models/extension.yaml +++ b/cli/azd/extensions/azure.ai.models/extension.yaml @@ -3,7 +3,7 @@ namespace: ai.models displayName: Foundry Custom Models (Preview) description: Extension for managing custom models in Azure AI Foundry. (Preview) usage: azd ai models [options] -version: 0.0.5-preview +version: 0.0.6-preview language: go capabilities: - custom-commands diff --git a/cli/azd/extensions/azure.ai.models/version.txt b/cli/azd/extensions/azure.ai.models/version.txt index f23e8a72a0f..a2c0ee7a53c 100644 --- a/cli/azd/extensions/azure.ai.models/version.txt +++ b/cli/azd/extensions/azure.ai.models/version.txt @@ -1 +1 @@ -0.0.5-preview +0.0.6-preview diff --git a/cli/azd/extensions/azure.appservice/extension.yaml b/cli/azd/extensions/azure.appservice/extension.yaml index b9f0e67be76..3f7d5cda966 100644 --- a/cli/azd/extensions/azure.appservice/extension.yaml +++ b/cli/azd/extensions/azure.appservice/extension.yaml @@ -5,7 +5,7 @@ displayName: Azure App Service description: Extension for managing Azure App Service resources. usage: azd appservice [options] # NOTE: Make sure version.txt is in sync with this version. -version: 0.1.0 +version: 0.1.1 language: go capabilities: - custom-commands diff --git a/cli/azd/extensions/azure.appservice/version.txt b/cli/azd/extensions/azure.appservice/version.txt index 6e8bf73aa55..17e51c385ea 100644 --- a/cli/azd/extensions/azure.appservice/version.txt +++ b/cli/azd/extensions/azure.appservice/version.txt @@ -1 +1 @@ -0.1.0 +0.1.1 diff --git a/cli/azd/extensions/azure.coding-agent/extension.yaml b/cli/azd/extensions/azure.coding-agent/extension.yaml index 0c42c3de738..f84701e809f 100644 --- a/cli/azd/extensions/azure.coding-agent/extension.yaml +++ b/cli/azd/extensions/azure.coding-agent/extension.yaml @@ -8,4 +8,4 @@ language: go namespace: coding-agent usage: azd coding-agent [options] # NOTE: Make sure version.txt is in sync with this version. -version: 0.6.1 +version: 0.6.2 diff --git a/cli/azd/extensions/azure.coding-agent/version.txt b/cli/azd/extensions/azure.coding-agent/version.txt index ee6cdce3c29..b6160487433 100644 --- a/cli/azd/extensions/azure.coding-agent/version.txt +++ b/cli/azd/extensions/azure.coding-agent/version.txt @@ -1 +1 @@ -0.6.1 +0.6.2 diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/extension.yaml b/cli/azd/extensions/microsoft.azd.ai.builder/extension.yaml index 4271102ea81..d58b8acfc84 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/extension.yaml +++ b/cli/azd/extensions/microsoft.azd.ai.builder/extension.yaml @@ -4,7 +4,7 @@ namespace: ai.builder displayName: azd AI Builder description: This extension provides custom commands for building AI applications using Azure Developer CLI. usage: azd ai builder [options] -version: 0.2.0 +version: 0.2.1 language: go capabilities: - custom-commands diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/version.txt b/cli/azd/extensions/microsoft.azd.ai.builder/version.txt index 341cf11faf9..0c62199f16a 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/version.txt +++ b/cli/azd/extensions/microsoft.azd.ai.builder/version.txt @@ -1 +1 @@ -0.2.0 \ No newline at end of file +0.2.1 diff --git a/cli/azd/extensions/microsoft.azd.concurx/extension.yaml b/cli/azd/extensions/microsoft.azd.concurx/extension.yaml index 35b5e66c1fb..3f3cc37d552 100644 --- a/cli/azd/extensions/microsoft.azd.concurx/extension.yaml +++ b/cli/azd/extensions/microsoft.azd.concurx/extension.yaml @@ -7,4 +7,4 @@ id: microsoft.azd.concurx language: go namespace: concurx usage: azd concurx [options] -version: 0.1.0 +version: 0.1.1 diff --git a/cli/azd/extensions/microsoft.azd.concurx/version.txt b/cli/azd/extensions/microsoft.azd.concurx/version.txt index 6c6aa7cb091..17e51c385ea 100644 --- a/cli/azd/extensions/microsoft.azd.concurx/version.txt +++ b/cli/azd/extensions/microsoft.azd.concurx/version.txt @@ -1 +1 @@ -0.1.0 \ No newline at end of file +0.1.1 diff --git a/cli/azd/extensions/microsoft.azd.demo/extension.yaml b/cli/azd/extensions/microsoft.azd.demo/extension.yaml index 05e570efb95..96d5a942b61 100644 --- a/cli/azd/extensions/microsoft.azd.demo/extension.yaml +++ b/cli/azd/extensions/microsoft.azd.demo/extension.yaml @@ -4,7 +4,7 @@ namespace: demo displayName: Demo Extension description: This extension provides examples of the azd extension framework. usage: azd demo [options] -version: 0.6.0 +version: 0.6.1 language: go capabilities: - custom-commands diff --git a/cli/azd/extensions/microsoft.azd.demo/version.txt b/cli/azd/extensions/microsoft.azd.demo/version.txt index 09a3acfa138..ee6cdce3c29 100644 --- a/cli/azd/extensions/microsoft.azd.demo/version.txt +++ b/cli/azd/extensions/microsoft.azd.demo/version.txt @@ -1 +1 @@ -0.6.0 \ No newline at end of file +0.6.1 diff --git a/cli/azd/extensions/microsoft.azd.extensions/extension.yaml b/cli/azd/extensions/microsoft.azd.extensions/extension.yaml index 51317a17e2a..b978c255986 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/extension.yaml +++ b/cli/azd/extensions/microsoft.azd.extensions/extension.yaml @@ -5,7 +5,7 @@ language: go displayName: azd extensions Developer Kit description: This extension provides a set of tools for azd extension developers to test and debug their extensions. usage: azd x [options] -version: 0.10.0 +version: 0.10.1 capabilities: - custom-commands - metadata diff --git a/cli/azd/extensions/microsoft.azd.extensions/version.txt b/cli/azd/extensions/microsoft.azd.extensions/version.txt index 2774f8587f4..571215736a6 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/version.txt +++ b/cli/azd/extensions/microsoft.azd.extensions/version.txt @@ -1 +1 @@ -0.10.0 \ No newline at end of file +0.10.1 diff --git a/eng/pipelines/release-ext-microsoft-azd-demo.yml b/eng/pipelines/release-ext-microsoft-azd-demo.yml index a5995f1602e..8e938c8c7bf 100644 --- a/eng/pipelines/release-ext-microsoft-azd-demo.yml +++ b/eng/pipelines/release-ext-microsoft-azd-demo.yml @@ -29,6 +29,8 @@ extends: stages: - template: /eng/pipelines/templates/stages/release-azd-extension.yml parameters: + # TODO: Revert before merging + SkipTests: true AzdExtensionId: microsoft.azd.demo SanitizedExtensionId: microsoft-azd-demo AzdExtensionDirectory: cli/azd/extensions/microsoft.azd.demo diff --git a/eng/pipelines/templates/jobs/build-azd-extension.yml b/eng/pipelines/templates/jobs/build-azd-extension.yml index a12ae98d977..e77e8385eb6 100644 --- a/eng/pipelines/templates/jobs/build-azd-extension.yml +++ b/eng/pipelines/templates/jobs/build-azd-extension.yml @@ -40,6 +40,10 @@ jobs: - template: /eng/pipelines/templates/steps/setup-go.yml + - template: /eng/pipelines/templates/steps/set-extension-version-cd.yml + parameters: + AzdExtensionDirectory: ${{ parameters.AzdExtensionDirectory }} + - task: PowerShell@2 inputs: pwsh: true diff --git a/eng/pipelines/templates/jobs/cross-build-azd-extension.yml b/eng/pipelines/templates/jobs/cross-build-azd-extension.yml index b8a711bfa19..db2c8a303a1 100644 --- a/eng/pipelines/templates/jobs/cross-build-azd-extension.yml +++ b/eng/pipelines/templates/jobs/cross-build-azd-extension.yml @@ -49,6 +49,10 @@ jobs: parameters: Condition: false + - template: /eng/pipelines/templates/steps/set-extension-version-cd.yml + parameters: + AzdExtensionDirectory: ${{ parameters.AzdExtensionDirectory }} + - task: PowerShell@2 displayName: Set extension version variable inputs: diff --git a/eng/pipelines/templates/stages/build-and-test-azd-extension.yml b/eng/pipelines/templates/stages/build-and-test-azd-extension.yml index 703f6e822e0..5a913e61b2c 100644 --- a/eng/pipelines/templates/stages/build-and-test-azd-extension.yml +++ b/eng/pipelines/templates/stages/build-and-test-azd-extension.yml @@ -95,7 +95,10 @@ stages: OSVmImage: ${{ build.value.OSVmImage }} OS: ${{ build.value.OS }} Variables: ${{ build.value.Variables }} - ValidateCrossCompile: ${{ build.value.ValidateCrossCompile }} + # TODO: Revert this before merge + # ValidateCrossCompile: ${{ build.value.ValidateCrossCompile }} + # TODO: Remove this line before merge + ValidateCrossCompile: false ValidateVm: ${{ build.value.ValidateVm }} ValidationTask: ${{ build.value.ValidationTask }} ValidationScript: ${{ build.value.ValidationScript }} @@ -110,6 +113,10 @@ stages: steps: - checkout: self + - template: /eng/pipelines/templates/steps/set-extension-version-cd.yml + parameters: + AzdExtensionDirectory: ${{ parameters.AzdExtensionDirectory }} + - task: PowerShell@2 displayName: Set extension version variable inputs: @@ -157,6 +164,18 @@ stages: Copy-Item NOTICE.txt release-metadata/NOTICE.txt displayName: Copy NOTICE.txt to release-metadata + - pwsh: | + Copy-Item eng/templates/extensions-registry-daily.json.template release-metadata/extensions-registry-daily.json.template + displayName: Copy registry template to release-metadata + + - pwsh: | + Copy-Item eng/scripts/Update-ExtensionDailyRegistry.ps1 release-metadata/Update-ExtensionDailyRegistry.ps1 + displayName: Copy Update-ExtensionDailyRegistry.ps1 to release-metadata + + - pwsh: | + Copy-Item eng/common/scripts/Helpers/PSModule-Helpers.ps1 release-metadata/PSModule-Helpers.ps1 + displayName: Copy PSModule-Helpers.ps1 to release-metadata + templateContext: outputs: - output: pipelineArtifact diff --git a/eng/pipelines/templates/stages/publish-extension-daily.yml b/eng/pipelines/templates/stages/publish-extension-daily.yml new file mode 100644 index 00000000000..1d367d1b781 --- /dev/null +++ b/eng/pipelines/templates/stages/publish-extension-daily.yml @@ -0,0 +1,63 @@ +parameters: + - name: SanitizedExtensionId + type: string + - name: AzdExtensionId + type: string + +stages: + - stage: PublishDaily + dependsOn: Sign + condition: >- + and( + succeeded(), + ne(variables['Skip.Release'], 'true'), + or( + in(variables['BuildReasonOverride'], 'IndividualCI', 'BatchedCI'), + and( + eq('', variables['BuildReasonOverride']), + in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI') + ) + ) + ) + + variables: + - template: /eng/pipelines/templates/variables/image.yml + - template: /eng/pipelines/templates/variables/globals.yml + + jobs: + - deployment: Publish_Daily + environment: none + + pool: + name: azsdk-pool + image: ubuntu-22.04 + os: linux + + templateContext: + type: releaseJob + isProduction: false + inputs: + - input: pipelineArtifact + artifactName: release + targetPath: release + - input: pipelineArtifact + artifactName: release-metadata + targetPath: release-metadata + + strategy: + runOnce: + deploy: + steps: + - template: /eng/pipelines/templates/steps/extension-set-metadata-variables.yml + parameters: + Use1ESArtifactTask: true + + - template: /eng/pipelines/templates/steps/publish-extension.yml + parameters: + PublishUploadLocations: ${{ parameters.SanitizedExtensionId }}/daily;${{ parameters.SanitizedExtensionId }}/daily/archive/$(EXT_VERSION) + CreateGitHubRelease: false + + - template: /eng/pipelines/templates/steps/update-extension-daily-registry.yml + parameters: + SanitizedExtensionId: ${{ parameters.SanitizedExtensionId }} + AzdExtensionId: ${{ parameters.AzdExtensionId }} diff --git a/eng/pipelines/templates/stages/publish-extension-pr.yml b/eng/pipelines/templates/stages/publish-extension-pr.yml new file mode 100644 index 00000000000..84a0048db69 --- /dev/null +++ b/eng/pipelines/templates/stages/publish-extension-pr.yml @@ -0,0 +1,120 @@ +parameters: + - name: SanitizedExtensionId + type: string + - name: AzdExtensionId + type: string + +stages: + - stage: PublishForPR + dependsOn: Sign + condition: >- + and( + succeeded(), + ne(variables['Skip.Release'], 'true'), + or( + eq('PullRequest', variables['BuildReasonOverride']), + and( + eq('', variables['BuildReasonOverride']), + eq(variables['Build.Reason'], 'PullRequest') + ) + ) + ) + + variables: + - template: /eng/pipelines/templates/variables/image.yml + - template: /eng/pipelines/templates/variables/globals.yml + + jobs: + - deployment: Publish_Extension_For_PR + environment: none + + pool: + name: azsdk-pool + image: ubuntu-22.04 + os: linux + + templateContext: + type: releaseJob + isProduction: false + inputs: + - input: pipelineArtifact + artifactName: release + targetPath: release + - input: pipelineArtifact + artifactName: release-metadata + targetPath: release-metadata + + strategy: + runOnce: + deploy: + steps: + - pwsh: | + $PRNumber = '$(System.PullRequest.PullRequestNumber)' + if ($env:PRNUMBEROVERRIDE) { + Write-Host "PR Number override: $($env:PRNUMBEROVERRIDE)" + $PRNumber = "$($env:PRNUMBEROVERRIDE)" + } + if (-not $PRNumber -or $PRNumber -match '^\$\(') { + Write-Error "PR number could not be determined. Ensure this runs in a PR build or set PRNUMBEROVERRIDE." + exit 1 + } + Write-Host "##vso[task.setvariable variable=PRNumber]$PRNumber" + displayName: Set PR Number Variable + + - template: /eng/pipelines/templates/steps/extension-set-metadata-variables.yml + parameters: + Use1ESArtifactTask: true + + - template: /eng/pipelines/templates/steps/publish-extension.yml + parameters: + PublishUploadLocations: ${{ parameters.SanitizedExtensionId }}/pr/$(PRNumber) + CreateGitHubRelease: false + + - template: /eng/pipelines/templates/steps/update-extension-pr-registry.yml + parameters: + SanitizedExtensionId: ${{ parameters.SanitizedExtensionId }} + AzdExtensionId: ${{ parameters.AzdExtensionId }} + PRNumber: $(PRNumber) + + - pwsh: | + $storageHost = "$(publish-storage-static-host)" + $extId = "${{ parameters.AzdExtensionId }}" + $sanitizedId = "${{ parameters.SanitizedExtensionId }}" + $prNumber = "$(PRNumber)" + $registryUrl = "$storageHost/azd/extensions/pr-registry-entries/$prNumber/$extId.json" + $binaryBase = "$storageHost/azd/extensions/$sanitizedId/pr/$prNumber" + + $content = @" + + ## azd Extension Install Instructions — ``$extId`` + + > :warning: **These are unsigned PR builds for testing only.** Do not use in production. + + ### Install from PR build + + ``````bash + azd ext source add -n pr-$prNumber -t url -l "$registryUrl" + azd ext install $extId --source pr-$prNumber + `````` + + ### Standalone Binaries + + | Platform | Download | + |----------|----------| + | Linux x86_64 | $binaryBase/$sanitizedId-linux-amd64.tar.gz | + | Linux ARM64 | $binaryBase/$sanitizedId-linux-arm64.tar.gz | + | macOS x86_64 | $binaryBase/$sanitizedId-darwin-amd64.zip | + | macOS ARM64 | $binaryBase/$sanitizedId-darwin-arm64.zip | + | Windows x86_64 | $binaryBase/$sanitizedId-windows-amd64.zip | + | Windows ARM64 | $binaryBase/$sanitizedId-windows-arm64.zip | + "@ + $file = New-TemporaryFile + Set-Content -Path $file -Value $content + Write-Host "##vso[task.setvariable variable=CommentBodyFile]$file" + displayName: Generate PR comment body + + - template: /eng/pipelines/templates/steps/update-prcomment.yml + parameters: + PrNumber: $(PRNumber) + BodyFile: $(CommentBodyFile) + Tag: '' diff --git a/eng/pipelines/templates/stages/publish-extension.yml b/eng/pipelines/templates/stages/publish-extension.yml index 704d4e2e076..f355ca9a5c5 100644 --- a/eng/pipelines/templates/stages/publish-extension.yml +++ b/eng/pipelines/templates/stages/publish-extension.yml @@ -67,5 +67,6 @@ stages: - template: /eng/pipelines/templates/steps/publish-extension.yml parameters: PublishUploadLocations: $(StorageUploadLocations) + CreateGitHubRelease: true TagPrefix: azd-ext-${{ parameters.SanitizedExtensionId }} TagVersion: $(EXT_VERSION) diff --git a/eng/pipelines/templates/stages/release-azd-extension.yml b/eng/pipelines/templates/stages/release-azd-extension.yml index 11442c20342..6ba8cb45252 100644 --- a/eng/pipelines/templates/stages/release-azd-extension.yml +++ b/eng/pipelines/templates/stages/release-azd-extension.yml @@ -76,7 +76,7 @@ stages: AZURE_DEV_CI_OS: mac-arm64 # Only sign and release on manual builds from internal - - ${{ if and(eq(variables['System.TeamProject'], 'internal'), eq(variables['Build.Reason'], 'Manual')) }}: + - ${{ if and(eq(variables['System.TeamProject'], 'internal'), in(variables['Build.Reason'], 'Manual', 'IndividualCI', 'BatchedCI')) }}: - template: /eng/pipelines/templates/stages/sign-extension.yml parameters: SanitizedExtensionId: ${{ parameters.SanitizedExtensionId }} @@ -84,3 +84,19 @@ stages: - template: /eng/pipelines/templates/stages/publish-extension.yml parameters: SanitizedExtensionId: ${{ parameters.SanitizedExtensionId }} + + - template: /eng/pipelines/templates/stages/publish-extension-daily.yml + parameters: + SanitizedExtensionId: ${{ parameters.SanitizedExtensionId }} + AzdExtensionId: ${{ parameters.AzdExtensionId }} + + # Publish PR builds to storage and post install instructions as PR comment + - ${{ if and(eq(variables['System.TeamProject'], 'internal'), eq(variables['Build.Reason'], 'PullRequest')) }}: + - template: /eng/pipelines/templates/stages/sign-extension.yml + parameters: + SanitizedExtensionId: ${{ parameters.SanitizedExtensionId }} + + - template: /eng/pipelines/templates/stages/publish-extension-pr.yml + parameters: + SanitizedExtensionId: ${{ parameters.SanitizedExtensionId }} + AzdExtensionId: ${{ parameters.AzdExtensionId }} diff --git a/eng/pipelines/templates/steps/publish-extension.yml b/eng/pipelines/templates/steps/publish-extension.yml index 6608941e932..f024e21674a 100644 --- a/eng/pipelines/templates/steps/publish-extension.yml +++ b/eng/pipelines/templates/steps/publish-extension.yml @@ -6,63 +6,72 @@ parameters: default: '`$web' - name: TagPrefix type: string + default: '' - name: TagVersion type: string + default: '' + - name: CreateGitHubRelease + type: boolean + default: false steps: - # This step must run first because a duplicated tag means we don't need to - # continue with any of the subsequent steps. + # Remove _manifest folder unconditionally before upload/release - pwsh: | - $tag = "${{ parameters.TagPrefix }}_${{ parameters.TagVersion}}" - Write-Host "Release tag: $tag" - - # Check for tag using gh API - $existingTag = gh api /repos/$(Build.Repository.Name)/tags | ConvertFrom-Json | Where-Object { $_.name -eq $tag } - if ($existingTag) { - Write-Host "Tag $tag already exists. Exiting." - exit 1 + if (Test-Path release/_manifest) { + Remove-Item -Path release/_manifest -Recurse -Force } + Write-Host "Release:" + Get-ChildItem -Recurse release/ | Select-Object -Property Length,FullName + displayName: Remove _manifest folder - gh release view $tag --repo $(Build.Repository.Name) - if ($LASTEXITCODE -eq 0) { - Write-Host "Release ($tag) already exists. Exiting." - exit 1 - } + - ${{ if and(eq(parameters.CreateGitHubRelease, true), ne(parameters.TagPrefix, ''), ne(parameters.TagVersion, '')) }}: + # This step must run first because a duplicated tag means we don't need to + # continue with any of the subsequent steps. + - pwsh: | + $tag = "${{ parameters.TagPrefix }}_${{ parameters.TagVersion}}" + Write-Host "Release tag: $tag" - Write-Host "##vso[task.setvariable variable=GH_RELEASE_TAG;]$tag" + # Check for tag using gh API + $existingTag = gh api /repos/$(Build.Repository.Name)/tags | ConvertFrom-Json | Where-Object { $_.name -eq $tag } + if ($existingTag) { + Write-Host "Tag $tag already exists. Exiting." + exit 1 + } - # Exit with 0 (otherwise $LASTEXITCODE will not be 0 and the pipeline - # will fail) - exit 0 - displayName: Check for existing GitHub release - env: - GH_TOKEN: $(azuresdk-github-pat) + gh release view $tag --repo $(Build.Repository.Name) + if ($LASTEXITCODE -eq 0) { + Write-Host "Release ($tag) already exists. Exiting." + exit 1 + } - - pwsh: | - Remove-Item -Path release/_manifest -Recurse -Force - Write-Host "Release:" - Get-ChildItem -Recurse release/ | Select-Object -Property Length,FullName - displayName: Remove _manifest folder + Write-Host "##vso[task.setvariable variable=GH_RELEASE_TAG;]$tag" - - pwsh: | - $version = "${{ parameters.TagVersion }}" - $createArgs = @( - "$(GH_RELEASE_TAG)", - "--title", "$(GH_RELEASE_TAG)", - "--notes-file", "changelog/CHANGELOG.md", - "--repo", "$(Build.Repository.Name)" - ) + # Exit with 0 (otherwise $LASTEXITCODE will not be 0 and the pipeline + # will fail) + exit 0 + displayName: Check for existing GitHub release + env: + GH_TOKEN: $(azuresdk-github-pat) - if ($version -match "^0\." -or $version -match "-(alpha|beta|preview)") { - $createArgs += "--prerelease" - } + - pwsh: | + $version = "${{ parameters.TagVersion }}" + $createArgs = @( + "$(GH_RELEASE_TAG)", + "--title", "$(GH_RELEASE_TAG)", + "--notes-file", "changelog/CHANGELOG.md", + "--repo", "$(Build.Repository.Name)" + ) + + if ($version -match "^0\." -or $version -match "-(alpha|beta|preview)") { + $createArgs += "--prerelease" + } - gh release create @createArgs + gh release create @createArgs - gh release upload $(GH_RELEASE_TAG) release/* --repo $(Build.Repository.Name) - displayName: Create GitHub Release and upload artifacts - env: - GH_TOKEN: $(azuresdk-github-pat) + gh release upload $(GH_RELEASE_TAG) release/* --repo $(Build.Repository.Name) + displayName: Create GitHub Release and upload artifacts + env: + GH_TOKEN: $(azuresdk-github-pat) - task: AzurePowerShell@5 displayName: Upload release to storage account @@ -77,7 +86,7 @@ steps: Get-ChildItem release/ foreach ($folder in $uploadLocations) { Write-Host "Upload to ${{ parameters.StorageContainerName }}/azd/extensions/$folder" - azcopy copy "release/*" "$(publish-storage-location)/${{ parameters.StorageContainerName }}/azd/extensions/$folder" + azcopy copy "release/*" "$(publish-storage-location)/${{ parameters.StorageContainerName }}/azd/extensions/$folder" --overwrite=true if ($LASTEXITCODE) { Write-Error "Upload failed" exit 1 diff --git a/eng/pipelines/templates/steps/set-extension-version-cd.yml b/eng/pipelines/templates/steps/set-extension-version-cd.yml new file mode 100644 index 00000000000..2e402aeca06 --- /dev/null +++ b/eng/pipelines/templates/steps/set-extension-version-cd.yml @@ -0,0 +1,15 @@ +parameters: + - name: AzdExtensionDirectory + type: string + +steps: + - task: PowerShell@2 + displayName: Set extension version for CD release + inputs: + pwsh: true + targetType: filePath + filePath: eng/scripts/Set-ExtensionVersionInBuild.ps1 + arguments: >- + -ExtensionDirectory ${{ parameters.AzdExtensionDirectory }} + -BuildReason ($env:BUILDREASONOVERRIDE ?? '$(Build.Reason)') + -BuildId $(Build.BuildId) diff --git a/eng/pipelines/templates/steps/update-extension-daily-registry.yml b/eng/pipelines/templates/steps/update-extension-daily-registry.yml new file mode 100644 index 00000000000..f27f29a27a7 --- /dev/null +++ b/eng/pipelines/templates/steps/update-extension-daily-registry.yml @@ -0,0 +1,31 @@ +parameters: + - name: SanitizedExtensionId + type: string + - name: AzdExtensionId + type: string + +steps: + - task: AzurePowerShell@5 + displayName: Upload per-extension daily registry entry + inputs: + azureSubscription: 'Azure SDK Artifacts' + azurePowerShellVersion: LatestVersion + pwsh: true + ScriptType: InlineScript + Inline: | + $storageHost = "$(publish-storage-static-host)" + $dailyBaseUrl = "$storageHost/azd/extensions/${{ parameters.SanitizedExtensionId }}/daily" + $entryBlobPath = "$(publish-storage-location)/`$web/azd/extensions/daily-registry-entries/${{ parameters.AzdExtensionId }}.json" + $templatePath = "release-metadata/extensions-registry-daily.json.template" + + & "release-metadata/Update-ExtensionDailyRegistry.ps1" ` + -SanitizedExtensionId "${{ parameters.SanitizedExtensionId }}" ` + -AzdExtensionId "${{ parameters.AzdExtensionId }}" ` + -Version "$(EXT_VERSION)" ` + -StorageBaseUrl $dailyBaseUrl ` + -RegistryEntryBlobPath $entryBlobPath ` + -TemplatePath $templatePath ` + -ReleasePath "release" ` + -MetadataPath "release-metadata" + env: + AZCOPY_AUTO_LOGIN_TYPE: 'PSCRED' diff --git a/eng/pipelines/templates/steps/update-extension-pr-registry.yml b/eng/pipelines/templates/steps/update-extension-pr-registry.yml new file mode 100644 index 00000000000..bc01049db1c --- /dev/null +++ b/eng/pipelines/templates/steps/update-extension-pr-registry.yml @@ -0,0 +1,33 @@ +parameters: + - name: SanitizedExtensionId + type: string + - name: AzdExtensionId + type: string + - name: PRNumber + type: string + +steps: + - task: AzurePowerShell@5 + displayName: Upload per-extension PR registry entry + inputs: + azureSubscription: 'Azure SDK Artifacts' + azurePowerShellVersion: LatestVersion + pwsh: true + ScriptType: InlineScript + Inline: | + $storageHost = "$(publish-storage-static-host)" + $prBaseUrl = "$storageHost/azd/extensions/${{ parameters.SanitizedExtensionId }}/pr/${{ parameters.PRNumber }}" + $entryBlobPath = "$(publish-storage-location)/`$web/azd/extensions/pr-registry-entries/${{ parameters.PRNumber }}/${{ parameters.AzdExtensionId }}.json" + $templatePath = "release-metadata/extensions-registry-daily.json.template" + + & "release-metadata/Update-ExtensionDailyRegistry.ps1" ` + -SanitizedExtensionId "${{ parameters.SanitizedExtensionId }}" ` + -AzdExtensionId "${{ parameters.AzdExtensionId }}" ` + -Version "$(EXT_VERSION)" ` + -StorageBaseUrl $prBaseUrl ` + -RegistryEntryBlobPath $entryBlobPath ` + -TemplatePath $templatePath ` + -ReleasePath "release" ` + -MetadataPath "release-metadata" + env: + AZCOPY_AUTO_LOGIN_TYPE: 'PSCRED' diff --git a/eng/scripts/Set-ExtensionVersionInBuild.ps1 b/eng/scripts/Set-ExtensionVersionInBuild.ps1 new file mode 100644 index 00000000000..fbe5cf5307d --- /dev/null +++ b/eng/scripts/Set-ExtensionVersionInBuild.ps1 @@ -0,0 +1,56 @@ +<# + .SYNOPSIS + Appends a prerelease suffix to the extension's version.txt for CI/PR builds. + Skips for Manual (release) builds. + + .PARAMETER ExtensionDirectory + Path to the extension directory containing version.txt. + + .PARAMETER BuildReason + The build reason from CI (e.g. Build.Reason). + + .PARAMETER BuildId + A unique build ID from CI (e.g. Build.BuildId). + #> +param( + [Parameter(Mandatory)] [string] $ExtensionDirectory, + [Parameter(Mandatory)] [string] $BuildReason, + [Parameter(Mandatory)] [string] $BuildId +) + +Write-Host "Build reason: $BuildReason" + +$prereleaseCategory = "" + +if ($BuildReason -eq "Manual") { + Write-Host "Skipping prerelease tagging for release build." + exit 0 +} +elseif ($BuildReason -eq "PullRequest") { + $prereleaseCategory = "pr" +} +else { + $prereleaseCategory = "daily" +} + +$versionFile = Join-Path $ExtensionDirectory "version.txt" +if (!(Test-Path $versionFile)) { + Write-Error "version.txt not found at $versionFile" + exit 1 +} +$version = (Get-Content $versionFile).Trim() +if ([string]::IsNullOrWhiteSpace($version)) { + Write-Error "version.txt is empty at $versionFile" + exit 1 +} + +# Guard against pipeline retries — skip if suffix already applied +if ($version -match "-${prereleaseCategory}\.\d+$") { + Write-Host "Version '$version' already has $prereleaseCategory suffix, skipping." + exit 0 +} + +$newVersion = "$version-$prereleaseCategory.$BuildId" + +Set-Content $versionFile -Value $newVersion +Write-Host "Set version.txt contents to: $newVersion" diff --git a/eng/scripts/Set-ExtensionVersionVariable.ps1 b/eng/scripts/Set-ExtensionVersionVariable.ps1 index 34277f57182..36b2a348758 100644 --- a/eng/scripts/Set-ExtensionVersionVariable.ps1 +++ b/eng/scripts/Set-ExtensionVersionVariable.ps1 @@ -2,6 +2,6 @@ param( [string] $ExtensionDirectory ) -$extVersion = Get-Content "$ExtensionDirectory/version.txt" +$extVersion = (Get-Content "$ExtensionDirectory/version.txt").Trim() Write-Host "Extension Version: $extVersion" Write-Host "##vso[task.setvariable variable=EXT_VERSION;]$extVersion" diff --git a/eng/scripts/Update-ExtensionDailyRegistry.ps1 b/eng/scripts/Update-ExtensionDailyRegistry.ps1 new file mode 100644 index 00000000000..f080ef0ffeb --- /dev/null +++ b/eng/scripts/Update-ExtensionDailyRegistry.ps1 @@ -0,0 +1,228 @@ +<# +.SYNOPSIS + Writes a per-extension daily registry entry to Azure Storage. + +.DESCRIPTION + 1. Computes checksums from signed release artifacts + 2. Reads extension.yaml for metadata (id, namespace, displayName, etc.) + 3. Loads the JSON template, replaces placeholders with actual values + 4. Uploads the entry as a standalone per-extension JSON blob + + Each extension writes its own file to avoid race conditions when multiple + extension pipelines run concurrently. The azd CLI reads each per-extension + entry directly via the registry source URL. + +.PARAMETER SanitizedExtensionId + Hyphenated extension id (e.g. azure-ai-agents) + +.PARAMETER AzdExtensionId + Dotted extension id (e.g. azure.ai.agents) + +.PARAMETER Version + Extension version from version.txt + +.PARAMETER StorageBaseUrl + Static storage host URL for daily artifacts + +.PARAMETER RegistryEntryBlobPath + Full blob path for the per-extension entry JSON + (e.g. .../azd/extensions/daily-registry-entries/azure.ai.agents.json) + +.PARAMETER ReleasePath + Path to the signed release artifacts + +.PARAMETER MetadataPath + Path to the release-metadata directory containing extension.yaml + +.PARAMETER TemplatePath + Path to the extension-registry-daily-template.json +#> + +param( + [Parameter(Mandatory)] [string] $SanitizedExtensionId, + [Parameter(Mandatory)] [string] $AzdExtensionId, + [Parameter(Mandatory)] [string] $Version, + [Parameter(Mandatory)] [string] $StorageBaseUrl, + [Parameter(Mandatory)] [string] $RegistryEntryBlobPath, + [Parameter(Mandatory)] [string] $TemplatePath, + [string] $ReleasePath = "release", + [string] $MetadataPath = "release-metadata" +) + +$ErrorActionPreference = 'Stop' + +# Validate required files exist +$extYamlPath = Join-Path $MetadataPath "extension.yaml" +if (!(Test-Path $extYamlPath)) { + Write-Error "extension.yaml not found at $extYamlPath" + exit 1 +} +if (!(Test-Path $TemplatePath)) { + Write-Error "Template not found at $TemplatePath" + exit 1 +} + +# Compute checksums from signed artifacts +$checksums = @{} +$missingArtifacts = @() +$artifactFiles = @( + @{ key = "DARWIN_AMD64"; file = "$SanitizedExtensionId-darwin-amd64.zip" }, + @{ key = "DARWIN_ARM64"; file = "$SanitizedExtensionId-darwin-arm64.zip" }, + @{ key = "LINUX_AMD64"; file = "$SanitizedExtensionId-linux-amd64.tar.gz" }, + @{ key = "LINUX_ARM64"; file = "$SanitizedExtensionId-linux-arm64.tar.gz" }, + @{ key = "WINDOWS_AMD64"; file = "$SanitizedExtensionId-windows-amd64.zip" }, + @{ key = "WINDOWS_ARM64"; file = "$SanitizedExtensionId-windows-arm64.zip" } +) + +foreach ($artifact in $artifactFiles) { + $filePath = Join-Path $ReleasePath $artifact.file + if (Test-Path $filePath) { + try { + $hash = (Get-FileHash -Path $filePath -Algorithm SHA256).Hash.ToLower() + $checksums[$artifact.key] = $hash + Write-Host "Checksum $($artifact.key): $hash" + } catch { + Write-Error "Failed to compute checksum for ${filePath}: $_" + exit 1 + } + } else { + $missingArtifacts += $artifact.file + } +} + +if ($missingArtifacts.Count -gt 0) { + Write-Error "Missing release artifacts: $($missingArtifacts -join ', ')" + exit 1 +} + +# Install powershell-yaml for proper YAML parsing +$psModuleHelpers = Join-Path $PSScriptRoot "PSModule-Helpers.ps1" +if (!(Test-Path $psModuleHelpers)) { + # Fallback to repo location when running from source checkout + $psModuleHelpers = Join-Path $PSScriptRoot "../common/scripts/Helpers/PSModule-Helpers.ps1" +} +if (!(Test-Path $psModuleHelpers)) { + Write-Error "PSModule-Helpers.ps1 not found at $PSScriptRoot or repo fallback path" + exit 1 +} +. $psModuleHelpers +Install-ModuleIfNotInstalled "powershell-yaml" "0.4.7" | Import-Module + +# Parse extension.yaml +$extData = ConvertFrom-Yaml (Get-Content $extYamlPath -Raw) +if ($null -eq $extData) { + Write-Error "Failed to parse extension.yaml at $extYamlPath — file may be empty or malformed" + exit 1 +} + +$extMeta = @{} +foreach ($key in @('namespace', 'displayName', 'description', 'usage', 'requiredAzdVersion')) { + if ($extData.ContainsKey($key)) { + $extMeta[$key] = $extData[$key] + } +} + +$capabilities = if ($extData.ContainsKey('capabilities')) { @($extData['capabilities']) } else { @() } + +$providers = @() +if ($extData.ContainsKey('providers')) { + foreach ($p in $extData['providers']) { + $provider = [ordered]@{} + foreach ($k in $p.Keys) { $provider[$k] = $p[$k] } + $providers += $provider + } +} + +# Validate required fields were parsed +$requiredFields = @('namespace', 'displayName', 'description', 'usage') +foreach ($field in $requiredFields) { + if (-not $extMeta[$field] -or $extMeta[$field] -eq '') { + Write-Error "Required field '$field' missing or empty in extension.yaml" + exit 1 + } +} + +Write-Host "Parsed extension metadata:" +Write-Host " namespace: $($extMeta.namespace)" +Write-Host " displayName: $($extMeta.displayName)" +Write-Host " description: $($extMeta.description)" +Write-Host " usage: $($extMeta.usage)" +Write-Host " capabilities: $($capabilities -join ', ')" +Write-Host " providers: $($providers.Count)" + +# Load template and replace placeholders +# JSON-escape string values that are inserted into JSON string literals. +# This prevents characters like " \ and control chars from producing invalid JSON. +function ConvertTo-JsonSafeString([string]$value) { + # Use ConvertTo-Json to get a properly escaped JSON string, then strip the surrounding quotes + $escaped = $value | ConvertTo-Json + return $escaped.Substring(1, $escaped.Length - 2) +} + +$template = Get-Content $TemplatePath -Raw +$replacements = @{ + '${EXT_VERSION}' = ConvertTo-JsonSafeString $Version + '${REQUIRED_AZD_VERSION}' = ConvertTo-JsonSafeString ($(if ($extMeta.requiredAzdVersion) { $extMeta.requiredAzdVersion } else { "" })) + '${USAGE}' = ConvertTo-JsonSafeString $extMeta.usage + '${SANITIZED_ID}' = ConvertTo-JsonSafeString $SanitizedExtensionId + '${STORAGE_BASE_URL}' = ConvertTo-JsonSafeString $StorageBaseUrl + '${CHECKSUM_DARWIN_AMD64}' = $checksums["DARWIN_AMD64"] + '${CHECKSUM_DARWIN_ARM64}' = $checksums["DARWIN_ARM64"] + '${CHECKSUM_LINUX_AMD64}' = $checksums["LINUX_AMD64"] + '${CHECKSUM_LINUX_ARM64}' = $checksums["LINUX_ARM64"] + '${CHECKSUM_WINDOWS_AMD64}' = $checksums["WINDOWS_AMD64"] + '${CHECKSUM_WINDOWS_ARM64}' = $checksums["WINDOWS_ARM64"] +} + +foreach ($placeholder in $replacements.Keys) { + $template = $template.Replace($placeholder, $replacements[$placeholder]) +} + +# Verify all placeholders were replaced +if ($template -match '\$\{[A-Za-z0-9_]+\}') { + Write-Error "Unreplaced placeholder found: $($matches[0])" + exit 1 +} + +$versionEntry = $template | ConvertFrom-Json + +# Add capabilities and providers (can't template arrays/objects easily) +$versionEntry.capabilities = $capabilities +if ($providers.Count -gt 0) { + $versionEntry | Add-Member -NotePropertyName "providers" -NotePropertyValue $providers +} + +# Build the per-extension entry wrapped in registry format +# azd ext source add -t url expects { "extensions": [...] } +$extEntry = [ordered]@{ + id = $AzdExtensionId + namespace = $extMeta.namespace + displayName = $extMeta.displayName + description = $extMeta.description + versions = @($versionEntry) +} + +$registry = [ordered]@{ extensions = @($extEntry) } + +# Write registry entry and validate JSON +$entryFile = "$AzdExtensionId.json" +$registry | ConvertTo-Json -Depth 20 | Set-Content $entryFile -Encoding utf8 + +try { + $null = Get-Content $entryFile -Raw | ConvertFrom-Json -Depth 20 +} catch { + Write-Error "Generated entry JSON is invalid: $_" + exit 1 +} + +Write-Host "Extension entry:" +Get-Content $entryFile + +# Upload per-extension entry to storage +azcopy copy $entryFile $RegistryEntryBlobPath --overwrite=true +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to upload entry to $RegistryEntryBlobPath (exit code $LASTEXITCODE)" + exit 1 +} + +Write-Host "Entry uploaded to $RegistryEntryBlobPath" diff --git a/eng/templates/extensions-registry-daily.json.template b/eng/templates/extensions-registry-daily.json.template new file mode 100644 index 00000000000..d27d134075b --- /dev/null +++ b/eng/templates/extensions-registry-daily.json.template @@ -0,0 +1,38 @@ +{ + "version": "${EXT_VERSION}", + "requiredAzdVersion": "${REQUIRED_AZD_VERSION}", + "capabilities": [], + "usage": "${USAGE}", + "artifacts": { + "darwin/amd64": { + "checksum": { "algorithm": "sha256", "value": "${CHECKSUM_DARWIN_AMD64}" }, + "entryPoint": "${SANITIZED_ID}-darwin-amd64", + "url": "${STORAGE_BASE_URL}/${SANITIZED_ID}-darwin-amd64.zip" + }, + "darwin/arm64": { + "checksum": { "algorithm": "sha256", "value": "${CHECKSUM_DARWIN_ARM64}" }, + "entryPoint": "${SANITIZED_ID}-darwin-arm64", + "url": "${STORAGE_BASE_URL}/${SANITIZED_ID}-darwin-arm64.zip" + }, + "linux/amd64": { + "checksum": { "algorithm": "sha256", "value": "${CHECKSUM_LINUX_AMD64}" }, + "entryPoint": "${SANITIZED_ID}-linux-amd64", + "url": "${STORAGE_BASE_URL}/${SANITIZED_ID}-linux-amd64.tar.gz" + }, + "linux/arm64": { + "checksum": { "algorithm": "sha256", "value": "${CHECKSUM_LINUX_ARM64}" }, + "entryPoint": "${SANITIZED_ID}-linux-arm64", + "url": "${STORAGE_BASE_URL}/${SANITIZED_ID}-linux-arm64.tar.gz" + }, + "windows/amd64": { + "checksum": { "algorithm": "sha256", "value": "${CHECKSUM_WINDOWS_AMD64}" }, + "entryPoint": "${SANITIZED_ID}-windows-amd64.exe", + "url": "${STORAGE_BASE_URL}/${SANITIZED_ID}-windows-amd64.zip" + }, + "windows/arm64": { + "checksum": { "algorithm": "sha256", "value": "${CHECKSUM_WINDOWS_ARM64}" }, + "entryPoint": "${SANITIZED_ID}-windows-arm64.exe", + "url": "${STORAGE_BASE_URL}/${SANITIZED_ID}-windows-arm64.zip" + } + } +}