diff --git a/.azure-pipelines/macos-standalone-release.yml b/.azure-pipelines/mac-and-linux-standalone-release.yml similarity index 73% rename from .azure-pipelines/macos-standalone-release.yml rename to .azure-pipelines/mac-and-linux-standalone-release.yml index 31aee2ba575..d5412dd3af3 100644 --- a/.azure-pipelines/macos-standalone-release.yml +++ b/.azure-pipelines/mac-and-linux-standalone-release.yml @@ -1,19 +1,21 @@ -# Azure CLI - macOS Release Pipeline (Build → Sign → Test → Publish) +# Azure CLI - macOS and Linux Standalone Validation Pipeline # -# Purpose: Complete end-to-end macOS release pipeline +# Purpose: Manually validate macOS and Linux standalone tarballs and the Homebrew Cask. +# This is an ad hoc testing pipeline, not the production release pipeline. # Architecture: Chains 4 job templates in sequence # # Pipeline Flow: # 1. macos-build-jobs.yml → Build unsigned tarballs (ARM64 + Intel) # 2. macos-sign-notarize-jobs.yml → Sign and notarize via ESRP -# 3. macos-test-jobs.yml → Test cask (local file://) + offline install -# 4. macos-publish-jobs.yml → GitHub release + Homebrew cask update +# 3. cask-generation-and-tests.yml → Test cask (local file://) + offline install +# 4. publish-jobs.yml → GitHub release + Homebrew cask update # # Output Artifacts: -# - cli-build-unsigned-arm64, cli-build-unsigned-x86_64 (intermediate) -# - cli-signed-notarized-arm64, cli-signed-notarized-x86_64 (final) +# - macos-cli-signed-notarized-arm64, macos-cli-signed-notarized-x86_64 (final) +# - linux-cli-final-arm64, linux-cli-final-x86_64 (final) trigger: none +pr: none parameters: # Build parameters @@ -49,11 +51,6 @@ parameters: type: string default: 'Azure/homebrew-azure-cli' - - name: GitHubServiceConnection - displayName: 'GitHub Service Connection' - type: string - default: 'Azure' - - name: ESRPServiceConnection displayName: 'ESRP Service Connection' type: string @@ -76,6 +73,8 @@ variables: - template: templates/variables.yml - ${{ if eq(variables['System.TeamProject'], 'release') }}: - group: 'AME ESRP Variable Group' + - group: 'esrp-signing-codes' + - group: 'get-github-pat-token' - name: GitHubRepo value: ${{ parameters.GitHubRepo }} @@ -100,10 +99,10 @@ variables: - name: skipNugetSecurityAnalysis value: true -name: macos-release-$(Build.BuildId) +name: standalone-release-$(Build.BuildId) # ============================================================================ -# JOBS: End-to-end macOS release flow +# JOBS: End-to-end standalone release flow # ============================================================================ jobs: # ============================================================================ @@ -115,6 +114,12 @@ jobs: MacosArm64Image: ${{ variables.macos_arm64_pool }} MacosIntelImage: ${{ variables.macos_intel_pool }} +- template: templates/linux/linux-build-jobs.yml + parameters: + PythonVersion: ${{ parameters.PythonVersion }} + LinuxArm64Pool: ${{ variables.ubuntu_arm64_pool }} + LinuxX64Pool: ${{ variables.ubuntu_pool }} + # Jobs included: # - BuildMacOSCli (matrix: ARM64 + Intel) # - VerifyMacOSCli (matrix: ARM64 + Intel) @@ -140,46 +145,58 @@ jobs: # - SignBinaries (matrix: ARM64 + Intel) # - CreateNotarizeBundle (matrix: ARM64 + Intel) # - Notarize (matrix: ARM64 + Intel) -# - CreateFinalTarball (matrix: ARM64 + Intel) +# - CreateMacOSFinalTarball (matrix: ARM64 + Intel) # Artifacts: cli-signed-notarized-arm64, cli-signed-notarized-x86_64 # ============================================================================ # PHASE 3a: TEST (local file:// cask + offline install) # ============================================================================ - ${{ if eq(variables['System.TeamProject'], 'release') }}: - - template: templates/macos/macos-cask-generation-and-tests.yml + - template: templates/standalone/cask-generation-and-tests.yml parameters: MacosArm64Image: ${{ variables.macos_arm64_pool }} MacosIntelImage: ${{ variables.macos_intel_pool }} + LinuxArm64Pool: ${{ variables.ubuntu_arm64_pool }} + LinuxX64Pool: ${{ variables.ubuntu_pool }} PythonVersion: ${{ parameters.PythonVersion }} GitHubRepo: $(GitHubRepo) Debug: ${{ parameters.Debug }} - dependsOn: - - CreateFinalTarball + CaskDependsOn: + - CreateMacOSFinalTarball + - BuildLinuxCli + MacosDependsOn: + - CreateMacOSFinalTarball + LinuxDependsOn: + - BuildLinuxCli # Jobs included: -# - TestTempTapCask (matrix: ARM64 + Intel) - tests cask with local file:// URLs -# - TestOfflineInstall (matrix: ARM64 + Intel) - tests direct tarball install +# - TestMacOSTempTapCask (matrix: ARM64 + Intel) - tests the macOS cask +# - TestLinuxTempTapCask (matrix: ARM64 + x86_64) - tests the Linux cask +# - TestMacOSOfflineInstall (matrix: ARM64 + Intel) - tests macOS tarballs +# - TestLinuxOfflineInstall (matrix: ARM64 + x86_64) - tests Linux tarballs # ============================================================================ # PHASE 3b: PUBLISH (GitHub + Homebrew tap) # ============================================================================ - ${{ if eq(variables['System.TeamProject'], 'release') }}: - - template: templates/macos/macos-publish-jobs.yml + - template: templates/standalone/publish-jobs.yml parameters: PublishToGitHub: ${{ parameters.PublishToGitHub }} UpdateHomebrew: ${{ parameters.UpdateHomebrew }} TestAfterPublish: false GitHubRepo: $(GitHubRepo) - GitHubServiceConnection: ${{ parameters.GitHubServiceConnection }} HomebrewTapRepo: ${{ parameters.HomebrewTapRepo }} MacosArm64Image: ${{ variables.macos_arm64_pool }} MacosIntelImage: ${{ variables.macos_intel_pool }} + LinuxArm64Pool: ${{ variables.ubuntu_arm64_pool }} + LinuxX64Pool: ${{ variables.ubuntu_pool }} PythonVersion: ${{ parameters.PythonVersion }} Debug: ${{ parameters.Debug }} dependsOn: - - TestTempTapCask - - TestOfflineInstall + - TestMacOSTempTapCask + - TestLinuxTempTapCask + - TestMacOSOfflineInstall + - TestLinuxOfflineInstall # Jobs included: # - CreateGitHubRelease (conditional) diff --git a/.azure-pipelines/templates/linux/linux-build-jobs.yml b/.azure-pipelines/templates/linux/linux-build-jobs.yml new file mode 100644 index 00000000000..864f94e2aa8 --- /dev/null +++ b/.azure-pipelines/templates/linux/linux-build-jobs.yml @@ -0,0 +1,95 @@ +# Linux tarball build jobs for the Homebrew Cask release. + +parameters: + - name: PythonVersion + type: string + default: '3.14' + - name: LinuxArm64Pool + type: string + default: 'pool-ubuntu-latest-arm64' + - name: LinuxX64Pool + type: string + default: 'pool-ubuntu-2204' + - name: condition + type: string + default: 'succeeded()' + - name: dependsOn + type: object + default: [] + +jobs: +- job: BuildLinuxCli + displayName: 'Linux | Build and verify CLI' + condition: ${{ parameters.condition }} + dependsOn: ${{ parameters.dependsOn }} + strategy: + matrix: + ARM64: + Architecture: 'arm64' + PoolName: ${{ parameters.LinuxArm64Pool }} + X64: + Architecture: 'x86_64' + PoolName: ${{ parameters.LinuxX64Pool }} + pool: + name: $(PoolName) + timeoutInMinutes: 60 + + steps: + - checkout: self + fetchDepth: 1 + + - template: setup-homebrew.yml + + - bash: | + set -euo pipefail + + brew install python@${{ parameters.PythonVersion }} + PYTHON_PATH="$(brew --prefix python@${{ parameters.PythonVersion }})/libexec/bin/python" + "$PYTHON_PATH" --version + echo "##vso[task.setvariable variable=PythonPath]$PYTHON_PATH" + displayName: 'Install Homebrew Python' + + - bash: | + set -euo pipefail + + ARCH="$(Architecture)" + OUTPUT_DIR="$(Build.ArtifactStagingDirectory)/cli-build" + "$(PythonPath)" scripts/release/standalone/build_binary_tar_gz.py \ + --platform-tag "linux-$ARCH" \ + --output-dir "$OUTPUT_DIR" + + SOURCE_TARBALL=$(find "$OUTPUT_DIR" -name "azure-cli-*-linux-$ARCH-nopython.tar.gz" -print -quit) + VERSION=$(basename "$SOURCE_TARBALL" | sed -E "s/^azure-cli-(.*)-linux-$ARCH-nopython\.tar\.gz$/\1/") + RELEASE_TARBALL="$OUTPUT_DIR/azure-cli-$VERSION-linux-$ARCH.tar.gz" + cp "$SOURCE_TARBALL" "$RELEASE_TARBALL" + sha256sum "$RELEASE_TARBALL" > "$RELEASE_TARBALL.sha256" + rm "$SOURCE_TARBALL" "$SOURCE_TARBALL.sha256" + + echo "Release tarball: $RELEASE_TARBALL" + cat "$RELEASE_TARBALL.sha256" + displayName: 'Build release tarball' + env: + PYTHON_MAJOR_MINOR: ${{ parameters.PythonVersion }} + + - bash: | + set -euo pipefail + + ARCH="$(Architecture)" + TARBALL=$(find "$(Build.ArtifactStagingDirectory)/cli-build" -name "azure-cli-*-linux-$ARCH.tar.gz" -print -quit) + EXTRACT_DIR=$(mktemp -d) + trap 'rm -rf "$EXTRACT_DIR" || true' EXIT + + tar -xzf "$TARBALL" -C "$EXTRACT_DIR" + export AZ_PYTHON="$(PythonPath)" + export AZURE_CORE_COLLECT_TELEMETRY=False + "$EXTRACT_DIR/bin/az" version + displayName: 'Smoke test release tarball' + + - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 + displayName: 'Generate SBOM' + inputs: + BuildDropPath: $(Build.ArtifactStagingDirectory)/cli-build + + - publish: $(Build.ArtifactStagingDirectory)/cli-build + artifact: 'linux-cli-final-$(Architecture)' + displayName: 'Publish Linux CLI ($(Architecture))' \ No newline at end of file diff --git a/.azure-pipelines/templates/linux/setup-homebrew.yml b/.azure-pipelines/templates/linux/setup-homebrew.yml new file mode 100644 index 00000000000..a07eca315d0 --- /dev/null +++ b/.azure-pipelines/templates/linux/setup-homebrew.yml @@ -0,0 +1,14 @@ +parameters: [] + +steps: +- bash: | + set -euo pipefail + + if ! command -v brew >/dev/null 2>&1; then + NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + fi + + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" + echo "##vso[task.prependpath]/home/linuxbrew/.linuxbrew/bin" + brew --version + displayName: 'Install Linuxbrew' \ No newline at end of file diff --git a/.azure-pipelines/templates/macos/macos-build-jobs.yml b/.azure-pipelines/templates/macos/macos-build-jobs.yml index e67fb574b4f..aa727104a5d 100644 --- a/.azure-pipelines/templates/macos/macos-build-jobs.yml +++ b/.azure-pipelines/templates/macos/macos-build-jobs.yml @@ -80,7 +80,7 @@ jobs: echo "Architecture: $ARCH" $PYTHON --version - $PYTHON scripts/release/macos/build_binary_tar_gz.py \ + $PYTHON scripts/release/standalone/build_binary_tar_gz.py \ --platform-tag macos-$ARCH \ --output-dir dist/binary_tar_gz diff --git a/.azure-pipelines/templates/macos/macos-sign-notarize-jobs.yml b/.azure-pipelines/templates/macos/macos-sign-notarize-jobs.yml index 3eda6928aa1..5b009388419 100644 --- a/.azure-pipelines/templates/macos/macos-sign-notarize-jobs.yml +++ b/.azure-pipelines/templates/macos/macos-sign-notarize-jobs.yml @@ -235,7 +235,7 @@ jobs: inlineOperation: | [ { - "KeyCode": "CP-401337-Apple", + "KeyCode": "$(ESRPAppleKeyCode)", "OperationCode": "MacAppDeveloperSign", "ToolName": "sign", "ToolVersion": "1.0", @@ -454,7 +454,7 @@ jobs: inlineOperation: | [ { - "KeyCode": "CP-401337-Apple", + "KeyCode": "$(ESRPAppleKeyCode)", "OperationCode": "MacAppNotarize", "ToolName": "sign", "ToolVersion": "1.0", @@ -475,7 +475,7 @@ jobs: # ============================================================================ # JOB: CREATE FINAL TARBALL (Both Architectures via Matrix) # ============================================================================ -- job: CreateFinalTarball +- job: CreateMacOSFinalTarball displayName: 'macOS | Create Final Tarball' dependsOn: - DownloadAnalyze diff --git a/.azure-pipelines/templates/macos/macos-cask-generation-and-tests.yml b/.azure-pipelines/templates/standalone/cask-generation-and-tests.yml similarity index 58% rename from .azure-pipelines/templates/macos/macos-cask-generation-and-tests.yml rename to .azure-pipelines/templates/standalone/cask-generation-and-tests.yml index ce34c4f3ff9..1bfe5431a07 100644 --- a/.azure-pipelines/templates/macos/macos-cask-generation-and-tests.yml +++ b/.azure-pipelines/templates/standalone/cask-generation-and-tests.yml @@ -1,4 +1,4 @@ -# macOS Cask Generation and Test Jobs Template +# Standalone Cask Generation and Test Jobs Template # # Purpose: Generate cask definition, test installation (using local file:// URLs), # and test offline tarball install @@ -7,16 +7,21 @@ # Parameters: # - MacosArm64Image: VM image for ARM64 tests # - MacosIntelImage: VM image for Intel tests +# - LinuxArm64Pool: pool for ARM64 Linux tests +# - LinuxX64Pool: pool for x86_64 Linux tests # - PythonVersion: Homebrew Python version for testing # - GitHubRepo: GitHub repository (for cask generation) # - Debug: Enable debug diagnostics -# - dependsOn: Jobs this depends on (typically CreateFinalTarball) +# - CaskDependsOn: Jobs required to generate the cross-platform Cask +# - MacosDependsOn: Jobs required by macOS offline-install tests +# - LinuxDependsOn: Jobs required by Linux offline-install tests # # Requires: -# - Artifacts: macos-cli-signed-notarized-arm64, macos-cli-signed-notarized-x86_64 +# - Artifacts: macos-cli-signed-notarized-arm64, macos-cli-signed-notarized-x86_64, +# linux-cli-final-arm64, linux-cli-final-x86_64 # # Produces: -# - Artifact: macos-cask-definition (contains azure-cli.rb with GitHub URLs) +# - Artifact: cask-definition (contains azure-cli.rb with GitHub URLs) parameters: - name: MacosArm64Image @@ -25,6 +30,12 @@ parameters: - name: MacosIntelImage type: string default: 'macos-15' + - name: LinuxArm64Pool + type: string + default: 'pool-ubuntu-latest-arm64' + - name: LinuxX64Pool + type: string + default: 'pool-ubuntu-2204' - name: PythonVersion type: string default: '3.14' @@ -37,17 +48,23 @@ parameters: - name: condition type: string default: 'succeeded()' - - name: dependsOn + - name: CaskDependsOn + type: object + default: ['CreateMacOSFinalTarball', 'BuildLinuxCli'] + - name: MacosDependsOn type: object - default: ['CreateFinalTarball'] + default: ['CreateMacOSFinalTarball'] + - name: LinuxDependsOn + type: object + default: ['BuildLinuxCli'] jobs: # ============================================================================ # JOB: GENERATE CASK AND TEST VIA TEMP HOMEBREW TAP (Both Architectures) # ============================================================================ -- job: TestTempTapCask +- job: TestMacOSTempTapCask displayName: 'macOS | Generate Cask and Test' - dependsOn: ${{ parameters.dependsOn }} + dependsOn: ${{ parameters.CaskDependsOn }} condition: ${{ parameters.condition }} strategy: matrix: @@ -72,6 +89,14 @@ jobs: artifact: 'macos-cli-signed-notarized-x86_64' displayName: 'Download signed tarball (Intel)' + - download: current + artifact: 'linux-cli-final-arm64' + displayName: 'Download Linux tarball (ARM64)' + + - download: current + artifact: 'linux-cli-final-x86_64' + displayName: 'Download Linux tarball (x86_64)' + - bash: | echo "=== Environment Info ($(Architecture)) ===" echo "HOMEBREW_CASK_OPTS: $HOMEBREW_CASK_OPTS" @@ -101,18 +126,22 @@ jobs: # Get tarballs ARM64_TARBALL=$(find $(Pipeline.Workspace)/macos-cli-signed-notarized-arm64 -name "azure-cli-*-macos-arm64.tar.gz" | head -1) X86_64_TARBALL=$(find $(Pipeline.Workspace)/macos-cli-signed-notarized-x86_64 -name "azure-cli-*-macos-x86_64.tar.gz" | head -1) + ARM64_LINUX_TARBALL=$(find $(Pipeline.Workspace)/linux-cli-final-arm64 -name "azure-cli-*-linux-arm64.tar.gz" | head -1) + X86_64_LINUX_TARBALL=$(find $(Pipeline.Workspace)/linux-cli-final-x86_64 -name "azure-cli-*-linux-x86_64.tar.gz" | head -1) VERSION=$(basename "$ARM64_TARBALL" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+([a-z0-9]+)?' | head -1) - ARM64_SHA256=$(cat ${ARM64_TARBALL}.sha256 | cut -d' ' -f1) - X86_64_SHA256=$(cat ${X86_64_TARBALL}.sha256 | cut -d' ' -f1) + ARM64_MACOS_SHA256=$(cat ${ARM64_TARBALL}.sha256 | cut -d' ' -f1) + X86_64_MACOS_SHA256=$(cat ${X86_64_TARBALL}.sha256 | cut -d' ' -f1) + ARM64_LINUX_SHA256=$(cat ${ARM64_LINUX_TARBALL}.sha256 | cut -d' ' -f1) + X86_64_LINUX_SHA256=$(cat ${X86_64_LINUX_TARBALL}.sha256 | cut -d' ' -f1) echo "=== Generating Cask using template ===" echo "Version: $VERSION" echo "ARM64 tarball: $ARM64_TARBALL" - echo "ARM64 SHA256: $ARM64_SHA256" + echo "macOS ARM64 SHA256: $ARM64_MACOS_SHA256" echo "x86_64 tarball: $X86_64_TARBALL" - echo "x86_64 SHA256: $X86_64_SHA256" + echo "macOS x86_64 SHA256: $X86_64_MACOS_SHA256" # Resolve repo root REPO_NAME="$(Build.Repository.Name)" @@ -134,22 +163,24 @@ jobs: fi # Generate cask using template with GitHub URLs (for publishing) - mkdir -p $(Pipeline.Workspace)/macos-cask-definition - python3 "$REPO_ROOT/scripts/release/macos/cask_generate.py" \ + mkdir -p $(Pipeline.Workspace)/cask-definition + python3 "$REPO_ROOT/scripts/release/standalone/cask_generate.py" \ --version "$VERSION" \ - --arm64-sha "$ARM64_SHA256" \ - --x86-64-sha "$X86_64_SHA256" \ + --arm64-macos-sha "$ARM64_MACOS_SHA256" \ + --x86-64-macos-sha "$X86_64_MACOS_SHA256" \ + --arm64-linux-sha "$ARM64_LINUX_SHA256" \ + --x86-64-linux-sha "$X86_64_LINUX_SHA256" \ --github-repo "${{ parameters.GitHubRepo }}" \ --python-version "${{ parameters.PythonVersion }}" \ - --template "$REPO_ROOT/scripts/release/macos/templates/azure-cli.rb.in" \ - --output "$(Pipeline.Workspace)/macos-cask-definition/azure-cli.rb" + --template "$REPO_ROOT/scripts/release/standalone/templates/azure-cli.rb.in" \ + --output "$(Pipeline.Workspace)/cask-definition/azure-cli.rb" echo "=== Generated Cask (GitHub URLs - for publishing) ===" - cat $(Pipeline.Workspace)/macos-cask-definition/azure-cli.rb + cat $(Pipeline.Workspace)/cask-definition/azure-cli.rb # Copy to staging and modify for local file:// testing mkdir -p $(Pipeline.Workspace)/cask-staging - cp $(Pipeline.Workspace)/macos-cask-definition/azure-cli.rb $(Pipeline.Workspace)/cask-staging/azure-cli.rb + cp $(Pipeline.Workspace)/cask-definition/azure-cli.rb $(Pipeline.Workspace)/cask-staging/azure-cli.rb # Replace GitHub URL with file:// URL for local testing WORKSPACE_PATH="$(Pipeline.Workspace)" @@ -175,6 +206,7 @@ jobs: # Create a temporary tap (named distinctly to avoid conflict with real Azure/homebrew-azure-cli) brew tap-new test/azure-cli-pipeline-test + brew trust test/azure-cli-pipeline-test # Get tap path TAP_PATH="$(brew --repository)/Library/Taps/test/homebrew-azure-cli-pipeline-test" @@ -202,6 +234,11 @@ jobs: set -ev echo "=== Installing Cask from Temporary Tap ($(Architecture)) ===" + + # Ensure neither the formula nor an existing cask can provide az. + brew uninstall azure-cli 2>/dev/null || true + brew uninstall --cask azure-cli 2>/dev/null || true + hash -r # Ensure Python is available if ! brew list python@${{ parameters.PythonVersion }} &>/dev/null; then @@ -214,6 +251,8 @@ jobs: # Verify installation echo "=== Verifying Installation ===" + brew info --cask azure-cli + brew info --cask azure-cli | grep -F "github.com/test/homebrew-azure-cli-pipeline-test/blob/HEAD/Casks/azure-cli.rb" which az az --version @@ -242,15 +281,83 @@ jobs: displayName: 'Publish cask definition artifact' condition: and(succeeded(), eq(variables['Architecture'], 'arm64')) inputs: - targetPath: '$(Pipeline.Workspace)/macos-cask-definition' - artifactName: 'macos-cask-definition' + targetPath: '$(Pipeline.Workspace)/cask-definition' + artifactName: 'cask-definition' + +# ============================================================================ +# JOB: TEST LINUX CASK VIA TEMP HOMEBREW TAP (Both Architectures) +# ============================================================================ +- job: TestLinuxTempTapCask + displayName: 'Linux | Generate Cask and Test' + dependsOn: TestMacOSTempTapCask + condition: ${{ parameters.condition }} + strategy: + matrix: + ARM64: + Architecture: 'arm64' + PoolName: ${{ parameters.LinuxArm64Pool }} + X64: + Architecture: 'x86_64' + PoolName: ${{ parameters.LinuxX64Pool }} + pool: + name: $(PoolName) + + steps: + - checkout: none + + - template: ../linux/setup-homebrew.yml + + - download: current + artifact: 'cask-definition' + displayName: 'Download cask definition' + + - download: current + artifact: 'linux-cli-final-$(Architecture)' + displayName: 'Download Linux tarball' + + - bash: | + set -euo pipefail + + brew uninstall azure-cli 2>/dev/null || true + brew uninstall --cask azure-cli 2>/dev/null || true + hash -r + + mkdir -p "$(Pipeline.Workspace)/cask-staging" + cp "$(Pipeline.Workspace)/cask-definition/azure-cli.rb" "$(Pipeline.Workspace)/cask-staging/azure-cli.rb" + sed -i.bak "s|url \"https://github.com/[^\"]*\"|url \"file://$(Pipeline.Workspace)/linux-cli-final-#{arch}/azure-cli-#{version}-linux-#{arch}.tar.gz\"|" \ + "$(Pipeline.Workspace)/cask-staging/azure-cli.rb" + sed -i.bak '/livecheck do/,/^ end$/d' "$(Pipeline.Workspace)/cask-staging/azure-cli.rb" + + git config --global user.email "test@pipeline.local" + git config --global user.name "Test Pipeline" + brew tap-new test/azure-cli-pipeline-test + brew trust test/azure-cli-pipeline-test + TAP_PATH="$(brew --repository)/Library/Taps/test/homebrew-azure-cli-pipeline-test" + mkdir -p "$TAP_PATH/Casks" + cp "$(Pipeline.Workspace)/cask-staging/azure-cli.rb" "$TAP_PATH/Casks/" + cd "$TAP_PATH" + git add Casks/azure-cli.rb + git commit -m "Add azure-cli cask for testing" + + brew install python@${{ parameters.PythonVersion }} + brew install --cask test/azure-cli-pipeline-test/azure-cli + brew info --cask azure-cli + brew info --cask azure-cli | grep -F "github.com/test/homebrew-azure-cli-pipeline-test/blob/HEAD/Casks/azure-cli.rb" + az version + displayName: 'Install and test Linux cask' + + - bash: | + brew uninstall --cask azure-cli 2>/dev/null || true + brew untap test/azure-cli-pipeline-test 2>/dev/null || true + displayName: 'Cleanup' + condition: always() # ============================================================================ # JOB: TEST OFFLINE/TARBALL INSTALLATION (Both Architectures) # ============================================================================ -- job: TestOfflineInstall +- job: TestMacOSOfflineInstall displayName: 'macOS | Test Offline Installation' - dependsOn: ${{ parameters.dependsOn }} + dependsOn: ${{ parameters.MacosDependsOn }} condition: ${{ parameters.condition }} strategy: matrix: @@ -316,3 +423,44 @@ jobs: rm -rf "$TEST_DIR" displayName: 'Test offline install' + +# ============================================================================ +# JOB: TEST LINUX OFFLINE/TARBALL INSTALLATION (Both Architectures) +# ============================================================================ +- job: TestLinuxOfflineInstall + displayName: 'Linux | Test Offline Installation' + dependsOn: ${{ parameters.LinuxDependsOn }} + condition: ${{ parameters.condition }} + strategy: + matrix: + ARM64: + Architecture: 'arm64' + PoolName: ${{ parameters.LinuxArm64Pool }} + X64: + Architecture: 'x86_64' + PoolName: ${{ parameters.LinuxX64Pool }} + pool: + name: $(PoolName) + + steps: + - checkout: none + + - template: ../linux/setup-homebrew.yml + + - download: current + artifact: 'linux-cli-final-$(Architecture)' + displayName: 'Download Linux tarball' + + - bash: | + set -euo pipefail + + TARBALL=$(find "$(Pipeline.Workspace)/linux-cli-final-$(Architecture)" -name "azure-cli-*-linux-$(Architecture).tar.gz" -print -quit) + TEST_DIR=$(mktemp -d) + trap 'rm -rf "$TEST_DIR" || true' EXIT + + tar -xzf "$TARBALL" -C "$TEST_DIR" + brew install python@${{ parameters.PythonVersion }} + export AZ_PYTHON="$(brew --prefix python@${{ parameters.PythonVersion }})/libexec/bin/python" + export AZURE_CORE_COLLECT_TELEMETRY=False + "$TEST_DIR/bin/az" version + displayName: 'Test Linux offline install' diff --git a/.azure-pipelines/templates/standalone/get-github-pat-token.yml b/.azure-pipelines/templates/standalone/get-github-pat-token.yml new file mode 100644 index 00000000000..7bee09d4da4 --- /dev/null +++ b/.azure-pipelines/templates/standalone/get-github-pat-token.yml @@ -0,0 +1,46 @@ +# Fetch a pre-minted GitHub PAT from Azure Key Vault for later steps. +# The Azure service connection needs Key Vault Secrets User (RBAC) or Get secret permission. +parameters: +- name: azureSubscription + type: string +- name: keyVaultName + type: string +- name: secretName + type: string +- name: outputVariable + type: string + default: GH_TOKEN +- name: execCondition + type: string + default: succeeded() + +steps: +- task: AzurePowerShell@5 + displayName: Get GitHub PAT from Key Vault + condition: ${{ parameters.execCondition }} + inputs: + azureSubscription: ${{ parameters.azureSubscription }} + azurePowerShellVersion: LatestVersion + pwsh: true + ScriptType: InlineScript + Inline: | + $keyVaultName = '${{ parameters.keyVaultName }}' + $secretName = '${{ parameters.secretName }}' + $outputVariable = '${{ parameters.outputVariable }}' + + if ($outputVariable -notmatch '^[A-Za-z_][A-Za-z0-9_.]*$') { + throw "outputVariable must be an Azure Pipelines variable name. Got: '$outputVariable'" + } + + try { + $ghToken = Get-AzKeyVaultSecret -VaultName $keyVaultName -Name $secretName -AsPlainText + } + catch { + throw "Failed to retrieve GitHub PAT secret '$secretName' from vault '$keyVaultName'. $($_.Exception.Message)" + } + + if ([string]::IsNullOrWhiteSpace($ghToken)) { + throw "GitHub PAT secret '$secretName' in vault '$keyVaultName' was empty or not found." + } + + Write-Host "##vso[task.setvariable variable=$outputVariable;issecret=true]$ghToken" diff --git a/.azure-pipelines/templates/macos/macos-publish-jobs.yml b/.azure-pipelines/templates/standalone/publish-jobs.yml similarity index 69% rename from .azure-pipelines/templates/macos/macos-publish-jobs.yml rename to .azure-pipelines/templates/standalone/publish-jobs.yml index 6640ef48283..4a8af7e7250 100644 --- a/.azure-pipelines/templates/macos/macos-publish-jobs.yml +++ b/.azure-pipelines/templates/standalone/publish-jobs.yml @@ -1,4 +1,4 @@ -# macOS Publish Jobs Template +# Standalone Publish Jobs Template # # Purpose: Publish signed tarballs to GitHub and update Homebrew cask # Usage: Called after test jobs pass @@ -11,12 +11,15 @@ # - HomebrewTapRepo: Homebrew tap repository # - MacosArm64Image: VM image for ARM64 tests # - MacosIntelImage: VM image for Intel tests +# - LinuxArm64Pool: pool for ARM64 Linux tests +# - LinuxX64Pool: pool for x86_64 Linux tests # - PythonVersion: Homebrew Python version for testing # - Debug: Enable debug diagnostics # - dependsOn: Jobs this depends on (typically test jobs) # # Requires: -# - Artifacts: macos-cli-signed-notarized-arm64, macos-cli-signed-notarized-x86_64, macos-cask-definition +# - Artifacts: macos-cli-signed-notarized-arm64, macos-cli-signed-notarized-x86_64, +# linux-cli-final-arm64, linux-cli-final-x86_64, cask-definition # - Repository resource: homebrewtap (for Homebrew cask updates) parameters: @@ -32,9 +35,6 @@ parameters: - name: GitHubRepo type: string default: '' - - name: GitHubServiceConnection - type: string - default: 'Azure' - name: HomebrewTapRepo type: string default: '' @@ -44,6 +44,12 @@ parameters: - name: MacosIntelImage type: string default: 'macos-15' + - name: LinuxArm64Pool + type: string + default: 'pool-ubuntu-latest-arm64' + - name: LinuxX64Pool + type: string + default: 'pool-ubuntu-2204' - name: PythonVersion type: string default: '3.14' @@ -55,7 +61,7 @@ parameters: default: 'succeeded()' - name: dependsOn type: object - default: ['TestTempTapCask', 'TestOfflineInstall'] + default: ['TestMacOSTempTapCask', 'TestLinuxTempTapCask', 'TestMacOSOfflineInstall', 'TestLinuxOfflineInstall'] jobs: # ============================================================================ @@ -63,7 +69,7 @@ jobs: # ============================================================================ - ${{ if eq(parameters.PublishToGitHub, true) }}: - job: CreateGitHubRelease - displayName: 'macOS | Create GitHub Release' + displayName: 'Standalone | Create GitHub Release' dependsOn: ${{ parameters.dependsOn }} condition: succeeded() pool: @@ -71,6 +77,12 @@ jobs: steps: - checkout: none + + - template: get-github-pat-token.yml + parameters: + azureSubscription: $(AzureSubscription) + keyVaultName: $(GitHubPatKeyVault) + secretName: $(GitHubPatSecretName) - download: current artifact: 'macos-cli-signed-notarized-arm64' @@ -79,6 +91,14 @@ jobs: - download: current artifact: 'macos-cli-signed-notarized-x86_64' displayName: 'Download signed tarball (Intel)' + + - download: current + artifact: 'linux-cli-final-arm64' + displayName: 'Download Linux tarball (ARM64)' + + - download: current + artifact: 'linux-cli-final-x86_64' + displayName: 'Download Linux tarball (x86_64)' - task: CopyFiles@2 displayName: 'Copy ARM64 release files' @@ -99,6 +119,26 @@ jobs: azure-cli-*.tar.gz.sha256 TargetFolder: '$(Build.ArtifactStagingDirectory)/release' flattenFolders: true + + - task: CopyFiles@2 + displayName: 'Copy Linux ARM64 release files' + inputs: + SourceFolder: '$(Pipeline.Workspace)/linux-cli-final-arm64' + Contents: | + azure-cli-*.tar.gz + azure-cli-*.tar.gz.sha256 + TargetFolder: '$(Build.ArtifactStagingDirectory)/release' + flattenFolders: true + + - task: CopyFiles@2 + displayName: 'Copy Linux x86_64 release files' + inputs: + SourceFolder: '$(Pipeline.Workspace)/linux-cli-final-x86_64' + Contents: | + azure-cli-*.tar.gz + azure-cli-*.tar.gz.sha256 + TargetFolder: '$(Build.ArtifactStagingDirectory)/release' + flattenFolders: true - bash: | ARM64_TARBALL=$(find "$(Build.ArtifactStagingDirectory)/release" -name "azure-cli-*-macos-arm64.tar.gz" | head -1) @@ -113,51 +153,48 @@ jobs: displayName: 'Prepare release files' - - task: GitHubRelease@1 - displayName: 'Delete existing release if present' - continueOnError: true - inputs: - gitHubConnection: '${{ parameters.GitHubServiceConnection }}' - repositoryName: '${{ parameters.GitHubRepo }}' - action: 'delete' - tag: '$(ReleaseTag)' - - - task: GitHubRelease@1 - displayName: 'Create GitHub Release' - inputs: - gitHubConnection: '${{ parameters.GitHubServiceConnection }}' - repositoryName: '${{ parameters.GitHubRepo }}' - action: 'create' - target: 'main' - tagSource: 'userSpecifiedTag' - tag: '$(ReleaseTag)' - title: 'Azure CLI $(AzureCliVersion) - macOS (Homebrew Python)' - releaseNotesSource: 'inline' - releaseNotesInline: | - ## Azure CLI $(AzureCliVersion) - macOS (Homebrew Python) - - ### Available Architectures - - **Apple Silicon (arm64)**: azure-cli-$(AzureCliVersion)-macos-arm64.tar.gz - - **Intel (x86_64)**: azure-cli-$(AzureCliVersion)-macos-x86_64.tar.gz - - ### Requirements - - macOS 11+ (Big Sur or later) - - Homebrew Python ${{ parameters.PythonVersion }} - - ### Installation via Homebrew Cask - ```bash - brew tap ${{ parameters.HomebrewTapRepo }} - brew install --cask azure-cli - ``` - assets: '$(Build.ArtifactStagingDirectory)/release/*' - addChangeLog: false + - bash: | + set -euo pipefail + + release_tag='$(ReleaseTag)' + repository='${{ parameters.GitHubRepo }}' + notes_file='$(Agent.TempDirectory)/release-notes.md' + + cat > "$notes_file" <<'EOF' + ## Azure CLI $(AzureCliVersion) - Homebrew Cask + + ### Available Architectures + - **Apple Silicon (arm64)**: azure-cli-$(AzureCliVersion)-macos-arm64.tar.gz + - **Intel (x86_64)**: azure-cli-$(AzureCliVersion)-macos-x86_64.tar.gz + - **Linux ARM64**: azure-cli-$(AzureCliVersion)-linux-arm64.tar.gz + - **Linux x86_64**: azure-cli-$(AzureCliVersion)-linux-x86_64.tar.gz + + ### Requirements + - Homebrew Python ${{ parameters.PythonVersion }} on macOS or Linux + + ### Installation via Homebrew Cask + ```bash + brew tap ${{ parameters.HomebrewTapRepo }} + brew install --cask azure-cli + ``` + EOF + + gh release delete "$release_tag" --repo "$repository" --yes || true + gh release create "$release_tag" "$(Build.ArtifactStagingDirectory)/release/"* \ + --repo "$repository" \ + --target main \ + --title "Azure CLI $(AzureCliVersion) - Homebrew Cask" \ + --notes-file "$notes_file" + displayName: 'Create GitHub Release with PAT' + env: + GH_TOKEN: $(GH_TOKEN) # ============================================================================ # JOB: UPDATE HOMEBREW CASK # ============================================================================ - ${{ if eq(parameters.UpdateHomebrew, true) }}: - job: UpdateHomebrewCask - displayName: 'macOS | Update Homebrew Cask' + displayName: 'Standalone | Update Homebrew Cask' dependsOn: - ${{ parameters.dependsOn }} - ${{ if eq(parameters.PublishToGitHub, true) }}: @@ -172,7 +209,7 @@ jobs: path: homebrew-tap - download: current - artifact: 'macos-cask-definition' + artifact: 'cask-definition' displayName: 'Download cask definition' - bash: | @@ -191,7 +228,7 @@ jobs: mkdir -p "$CASK_DIR" # Copy the pre-generated cask from artifact - cp $(Pipeline.Workspace)/macos-cask-definition/azure-cli.rb "$CASK_DIR/azure-cli.rb" + cp $(Pipeline.Workspace)/cask-definition/azure-cli.rb "$CASK_DIR/azure-cli.rb" echo "=== Cask Definition ===" cat "$CASK_DIR/azure-cli.rb" @@ -274,11 +311,43 @@ jobs: brew uninstall --cask azure-cli displayName: 'Test published cask install' + - job: TestPublishedLinuxCask + displayName: 'Linux | Test Published Cask' + dependsOn: UpdateHomebrewCask + condition: succeeded() + strategy: + matrix: + ARM64: + Architecture: 'arm64' + PoolName: ${{ parameters.LinuxArm64Pool }} + X64: + Architecture: 'x86_64' + PoolName: ${{ parameters.LinuxX64Pool }} + pool: + name: $(PoolName) + + steps: + - checkout: none + + - template: ../linux/setup-homebrew.yml + + - bash: | + set -euo pipefail + + brew uninstall azure-cli 2>/dev/null || true + brew uninstall --cask azure-cli 2>/dev/null || true + brew install python@${{ parameters.PythonVersion }} + brew tap ${{ parameters.HomebrewTapRepo }} + brew install --cask azure-cli + az version + brew uninstall --cask azure-cli + displayName: 'Test published Linux cask install' + # ============================================================================ # JOB: PRINT SUMMARY # ============================================================================ - job: PrintSummary - displayName: 'macOS | Print Summary' + displayName: 'Standalone | Print Summary' dependsOn: - ${{ parameters.dependsOn }} - ${{ if eq(parameters.PublishToGitHub, true) }}: @@ -287,6 +356,7 @@ jobs: - UpdateHomebrewCask - ${{ if and(eq(parameters.TestAfterPublish, true), eq(parameters.UpdateHomebrew, true)) }}: - TestPublishedCask + - TestPublishedLinuxCask condition: always() pool: vmImage: 'ubuntu-latest' @@ -296,7 +366,7 @@ jobs: - bash: | echo "╔══════════════════════════════════════════════════════════════════╗" - echo "║ AZURE CLI MACOS PUBLISH SUMMARY ║" + echo "║ AZURE CLI STANDALONE PUBLISH SUMMARY ║" echo "╠══════════════════════════════════════════════════════════════════╣" echo "║ Architectures: ARM64 (Apple Silicon) + x86_64 (Intel) ║" echo "║ GitHub Release: ${{ parameters.PublishToGitHub }} ║" @@ -307,6 +377,8 @@ jobs: echo "╠══════════════════════════════════════════════════════════════════╣" echo "║ • macos-cli-signed-notarized-arm64 (tarballs) ║" echo "║ • macos-cli-signed-notarized-x86_64 (tarballs) ║" - echo "║ • macos-cask-definition (azure-cli.rb) ║" + echo "║ • linux-cli-final-arm64 (tarballs) ║" + echo "║ • linux-cli-final-x86_64 (tarballs) ║" + echo "║ • cask-definition (azure-cli.rb) ║" echo "╚══════════════════════════════════════════════════════════════════╝" displayName: 'Print Summary' diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8a268fa3f6a..75ce83fc57f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -18,13 +18,14 @@ variables: - template: ${{ variables.Pipeline.Workspace }}/.azure-pipelines/templates/variables.yml - ${{ if eq(variables['System.TeamProject'], 'release') }}: - group: 'AME ESRP Variable Group' + - group: 'esrp-signing-codes' - name: Codeql.Enabled value: false - name: ComponentDetection.ForceScan value: eq(variables['Build.SourceBranch'], 'refs/heads/release') -# macOS Cask parameters -- name: macos_cask_python_version +# Standalone tarball and Cask parameters +- name: standalone_python_version value: '3.14' parameters: @@ -757,12 +758,20 @@ jobs: # Phase 1: Build unsigned tarballs (ARM64 + Intel) - template: .azure-pipelines/templates/macos/macos-build-jobs.yml parameters: - PythonVersion: $(macos_cask_python_version) + PythonVersion: $(standalone_python_version) MacosArm64Image: ${{ variables.macos_arm64_pool }} MacosIntelImage: ${{ variables.macos_intel_pool }} condition: and(succeeded(), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'Manual', 'Schedule')) dependsOn: ['ExtractMetadata'] +- template: .azure-pipelines/templates/linux/linux-build-jobs.yml + parameters: + PythonVersion: $(standalone_python_version) + LinuxArm64Pool: ${{ variables.ubuntu_arm64_pool }} + LinuxX64Pool: ${{ variables.ubuntu_pool }} + condition: and(succeeded(), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'Manual', 'Schedule')) + dependsOn: ['ExtractMetadata'] + # Phase 2: Sign and notarize via ESRP - ${{ if eq(variables['System.TeamProject'], 'release') }}: - template: .azure-pipelines/templates/macos/macos-sign-notarize-jobs.yml @@ -770,21 +779,25 @@ jobs: MacosArm64Image: ${{ variables.macos_arm64_pool }} MacosIntelImage: ${{ variables.macos_intel_pool }} BundleId: 'com.microsoft.azure.cli' - PythonVersion: $(macos_cask_python_version) + PythonVersion: $(standalone_python_version) ESRPServiceConnection: 'ame_esrp_connection' UseCurrentPipelineArtifacts: true dependsOn: ['VerifyMacOSCli'] # Phase 3: Generate cask and test via temp tap - ${{ if eq(variables['System.TeamProject'], 'release') }}: - - template: .azure-pipelines/templates/macos/macos-cask-generation-and-tests.yml + - template: .azure-pipelines/templates/standalone/cask-generation-and-tests.yml parameters: MacosArm64Image: ${{ variables.macos_arm64_pool }} MacosIntelImage: ${{ variables.macos_intel_pool }} - PythonVersion: $(macos_cask_python_version) + LinuxArm64Pool: ${{ variables.ubuntu_arm64_pool }} + LinuxX64Pool: ${{ variables.ubuntu_pool }} + PythonVersion: $(standalone_python_version) GitHubRepo: $(Build.Repository.Name) Debug: false - dependsOn: ['CreateFinalTarball'] + CaskDependsOn: ['CreateMacOSFinalTarball', 'BuildLinuxCli'] + MacosDependsOn: ['CreateMacOSFinalTarball'] + LinuxDependsOn: ['BuildLinuxCli'] - job: BuildRpmPackagesAzureLinux displayName: Build Rpm Package diff --git a/scripts/release/macos/build_binary_tar_gz.py b/scripts/release/standalone/build_binary_tar_gz.py similarity index 89% rename from scripts/release/macos/build_binary_tar_gz.py rename to scripts/release/standalone/build_binary_tar_gz.py index c673d3b7754..1007351c4e5 100644 --- a/scripts/release/macos/build_binary_tar_gz.py +++ b/scripts/release/standalone/build_binary_tar_gz.py @@ -15,8 +15,8 @@ Output Structure: ``` dist/binary_tar_gz/ - azure-cli-{VERSION}-macos-arm64-nopython.tar.gz - azure-cli-{VERSION}-macos-arm64-nopython.tar.gz.sha256 + azure-cli-{VERSION}-{PLATFORM_TAG}-nopython.tar.gz + azure-cli-{VERSION}-{PLATFORM_TAG}-nopython.tar.gz.sha256 ``` Archive Contents: @@ -48,8 +48,9 @@ Usage: python build_binary_tar_gz.py --help python build_binary_tar_gz.py --platform-tag macos-arm64 + python build_binary_tar_gz.py --platform-tag linux-x86_64 python build_binary_tar_gz.py --platform-tag macos-arm64 --output-dir ./dist/custom - python build_binary_tar_gz.py --platform-tag macos-arm64 --keep-temp + python build_binary_tar_gz.py --platform-tag macos-arm64 --keep-temp Requirements: - Homebrew python@x.yz installed: brew install python@x.yz @@ -61,6 +62,7 @@ import argparse import hashlib import os +import platform import shutil import subprocess import sys @@ -72,12 +74,16 @@ PROJECT_ROOT = Path(__file__).resolve().parents[3] SRC_DIR = PROJECT_ROOT / "src" AZURE_CLI_CORE_DIR = SRC_DIR / "azure-cli-core" -REQUIREMENTS_FILE = SRC_DIR / "azure-cli" / "requirements.py3.Darwin.txt" +REQUIREMENTS_FILES = { + "linux": SRC_DIR / "azure-cli" / "requirements.py3.Linux.txt", + "macos": SRC_DIR / "azure-cli" / "requirements.py3.Darwin.txt", +} # Package configuration APP_NAME = "azure-cli" CLI_EXECUTABLE_NAME = "az" TARBALL_NAME_TEMPLATE_DEFAULT = "{APP_NAME}-{VERSION}-{PLATFORM_TAG}-nopython.tar.gz" +PLATFORM_TAGS = ("linux-arm64", "linux-x86_64", "macos-arm64", "macos-x86_64") # Python version we're building for (must match Homebrew python@X.Y) # Can be overridden via PYTHON_MAJOR_MINOR env var @@ -140,8 +146,10 @@ def find_homebrew_python() -> Path: candidates = [ Path(f"/opt/homebrew/opt/python@{PYTHON_MAJOR_MINOR}/libexec/bin/python"), Path(f"/usr/local/opt/python@{PYTHON_MAJOR_MINOR}/libexec/bin/python"), + Path(f"/home/linuxbrew/.linuxbrew/opt/python@{PYTHON_MAJOR_MINOR}/libexec/bin/python"), Path(f"/opt/homebrew/bin/python{PYTHON_MAJOR_MINOR}"), Path(f"/usr/local/bin/python{PYTHON_MAJOR_MINOR}"), + Path(f"/home/linuxbrew/.linuxbrew/bin/python{PYTHON_MAJOR_MINOR}"), ] for python_path in candidates: @@ -182,12 +190,12 @@ def create_venv(python_path: Path, venv_dir: Path) -> Path: return venv_python -def install_azure_cli(venv_python: Path) -> None: +def install_azure_cli(venv_python: Path, requirements_file: Path) -> None: """Install Azure CLI components from source, then install pinned dependencies. Mirrors the run.sh approach: 1. Install all src packages with --no-deps (local source code takes precedence) - 2. Install pinned dependencies from requirements.py3.Darwin.txt + 2. Install pinned dependencies from the platform requirements file """ # Step 1: install every package found under SRC_DIR from source, without pulling # transitive deps from PyPI (--no-deps). This ensures the locally-built wheels @@ -212,12 +220,12 @@ def install_azure_cli(venv_python: Path) -> None: # resolves to the exact version recorded in the requirements file. print("\n=== Step 2: Installing pinned dependencies from requirements file ===") - if not REQUIREMENTS_FILE.exists(): - raise BuildError(f"Requirements file not found: {REQUIREMENTS_FILE}") + if not requirements_file.exists(): + raise BuildError(f"Requirements file not found: {requirements_file}") - print(f" Using: {REQUIREMENTS_FILE}") + print(f" Using: {requirements_file}") subprocess.run( - [str(venv_python), "-m", "pip", "install", "-r", str(REQUIREMENTS_FILE)], + [str(venv_python), "-m", "pip", "install", "-r", str(requirements_file)], check=True, ) @@ -428,13 +436,34 @@ def create_tarball( return tarball_path +def _platform_from_tag(platform_tag: str) -> str: + return platform_tag.split("-", maxsplit=1)[0] + + +def _validate_build_architecture(platform_tag: str) -> None: + """Reject releases whose tag does not match the builder's architecture.""" + expected_architecture = platform_tag.split("-", maxsplit=1)[1] + actual_architecture = platform.machine().lower() + aliases = { + "aarch64": "arm64", + "amd64": "x86_64", + "x64": "x86_64", + } + actual_architecture = aliases.get(actual_architecture, actual_architecture) + if actual_architecture != expected_architecture: + raise BuildError( + f"Platform tag {platform_tag} requires {expected_architecture}, " + f"but the builder is running on {actual_architecture}" + ) + + def main() -> int: """Main entry point.""" parser = argparse.ArgumentParser( description="Build Azure CLI tar.gz using Homebrew Python (no bundled Python)" ) parser.add_argument( - "--platform-tag", required=True, choices=["macos-arm64", "macos-x86_64"], help="Platform tag for the build" + "--platform-tag", required=True, choices=PLATFORM_TAGS, help="Platform tag for the build" ) parser.add_argument( "--output-dir", @@ -459,6 +488,8 @@ def main() -> int: print() try: + _validate_build_architecture(args.platform_tag) + requirements_file = REQUIREMENTS_FILES[_platform_from_tag(args.platform_tag)] version = get_cli_version() print(f"Azure CLI version: {version}") @@ -470,7 +501,7 @@ def main() -> int: install_dir = temp_path / "install" venv_python = create_venv(python_path=python_path, venv_dir=venv_dir) - install_azure_cli(venv_python=venv_python) + install_azure_cli(venv_python=venv_python, requirements_file=requirements_file) create_install_structure( venv_dir=venv_dir, diff --git a/scripts/release/macos/cask_generate.py b/scripts/release/standalone/cask_generate.py similarity index 61% rename from scripts/release/macos/cask_generate.py rename to scripts/release/standalone/cask_generate.py index 6520ad4612f..c9088f5cf2c 100644 --- a/scripts/release/macos/cask_generate.py +++ b/scripts/release/standalone/cask_generate.py @@ -6,24 +6,26 @@ """Generate the Homebrew cask from a template. Usage: - python3 scripts/release/macos/cask_generate.py \ + python3 scripts/release/standalone/cask_generate.py \ --version "2.x.y" \ - --arm64-sha "" \ - --x86-64-sha "" \ + --arm64-macos-sha "" \ + --x86-64-macos-sha "" \ + --arm64-linux-sha "" \ + --x86-64-linux-sha "" \ --github-repo "Azure/azure-cli" \ - --template "scripts/release/macos/templates/azure-cli.rb.in" \ + --template "scripts/release/standalone/templates/azure-cli.rb.in" \ --output "azure-cli.rb" Environment variable fallbacks (used when a CLI flag is omitted): - VERSION, ARM64_SHA, X86_64_SHA, GITHUB_REPO, TEMPLATE, OUTPUT + VERSION, ARM64_MACOS_SHA, X86_64_MACOS_SHA, ARM64_LINUX_SHA, X86_64_LINUX_SHA, + GITHUB_REPO, TEMPLATE, OUTPUT """ import argparse import os from pathlib import Path -from typing import Dict -DEFAULT_TEMPLATE = "scripts/release/macos/templates/azure-cli.rb.in" +DEFAULT_TEMPLATE = "scripts/release/standalone/templates/azure-cli.rb.in" DEFAULT_OUTPUT = "azure-cli.rb" @@ -37,7 +39,7 @@ def _require(value: str, name: str) -> str: return value -def _render_template(template_path: Path, replacements: Dict[str, str]) -> str: +def _render_template(template_path: Path, replacements: dict[str, str]) -> str: content = template_path.read_text() for key, value in replacements.items(): content = content.replace(key, value) @@ -51,8 +53,10 @@ def _render_template(template_path: Path, replacements: Dict[str, str]) -> str: def generate_cask(args: argparse.Namespace) -> None: version = _require(_env_or_arg(args.version, "VERSION"), "version") - arm64_sha = _require(_env_or_arg(args.arm64_sha, "ARM64_SHA"), "arm64_sha") - x86_64_sha = _require(_env_or_arg(args.x86_64_sha, "X86_64_SHA"), "x86_64_sha") + arm64_macos_sha = _require(_env_or_arg(args.arm64_macos_sha, "ARM64_MACOS_SHA"), "arm64_macos_sha") + x86_64_macos_sha = _require(_env_or_arg(args.x86_64_macos_sha, "X86_64_MACOS_SHA"), "x86_64_macos_sha") + arm64_linux_sha = _require(_env_or_arg(args.arm64_linux_sha, "ARM64_LINUX_SHA"), "arm64_linux_sha") + x86_64_linux_sha = _require(_env_or_arg(args.x86_64_linux_sha, "X86_64_LINUX_SHA"), "x86_64_linux_sha") github_repo = _require(_env_or_arg(args.github_repo, "GITHUB_REPO"), "github_repo") python_version = _require(_env_or_arg(args.python_version, "PYTHON_VERSION"), "python_version") @@ -61,8 +65,10 @@ def generate_cask(args: argparse.Namespace) -> None: replacements = { "{{ version }}": version, - "{{ arm64_sha }}": arm64_sha, - "{{ x86_64_sha }}": x86_64_sha, + "{{ arm64_macos_sha }}": arm64_macos_sha, + "{{ x86_64_macos_sha }}": x86_64_macos_sha, + "{{ arm64_linux_sha }}": arm64_linux_sha, + "{{ x86_64_linux_sha }}": x86_64_linux_sha, "{{ github_repo }}": github_repo, "{{ python_version }}": python_version, } @@ -74,8 +80,10 @@ def generate_cask(args: argparse.Namespace) -> None: def main() -> None: parser = argparse.ArgumentParser(prog="cask_generate.py") parser.add_argument("--version", dest="version", help="Azure CLI version") - parser.add_argument("--arm64-sha", dest="arm64_sha", help="ARM64 tarball SHA256") - parser.add_argument("--x86-64-sha", dest="x86_64_sha", help="x86_64 tarball SHA256") + parser.add_argument("--arm64-macos-sha", dest="arm64_macos_sha", help="macOS ARM64 tarball SHA256") + parser.add_argument("--x86-64-macos-sha", dest="x86_64_macos_sha", help="macOS x86_64 tarball SHA256") + parser.add_argument("--arm64-linux-sha", dest="arm64_linux_sha", help="Linux ARM64 tarball SHA256") + parser.add_argument("--x86-64-linux-sha", dest="x86_64_linux_sha", help="Linux x86_64 tarball SHA256") parser.add_argument("--github-repo", dest="github_repo", help="GitHub repo, e.g. Azure/azure-cli") parser.add_argument("--python-version", dest="python_version", help="Python major.minor version, e.g. 3.14") parser.add_argument("--template", dest="template", help="Template path (.rb.in)") diff --git a/scripts/release/macos/resources/ThirdPartyNotices.txt b/scripts/release/standalone/resources/ThirdPartyNotices.txt similarity index 100% rename from scripts/release/macos/resources/ThirdPartyNotices.txt rename to scripts/release/standalone/resources/ThirdPartyNotices.txt diff --git a/scripts/release/macos/templates/az_launcher.sh.in b/scripts/release/standalone/templates/az_launcher.sh.in similarity index 79% rename from scripts/release/macos/templates/az_launcher.sh.in rename to scripts/release/standalone/templates/az_launcher.sh.in index f11971e0f6d..8e221e321ea 100644 --- a/scripts/release/macos/templates/az_launcher.sh.in +++ b/scripts/release/standalone/templates/az_launcher.sh.in @@ -4,7 +4,7 @@ set -euo pipefail # Azure CLI Launcher (Homebrew Cask & Offline/Tarball) # # Install Mode: -# - Cask path detected: use Homebrew Python +# - Homebrew Cask path detected: use Homebrew Python # - Other paths: treat as tarball and require AZ_PYTHON PY_VER="{PYTHON_MAJOR_MINOR}" @@ -28,7 +28,9 @@ INSTALL_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" AZURE_CLI_SITE_PACKAGES="$INSTALL_DIR/libexec/lib/python${PY_VER}/site-packages" -if [[ "$SCRIPT_PATH" == /opt/homebrew/Caskroom/* ]] || [[ "$SCRIPT_PATH" == /usr/local/Caskroom/* ]]; then +if [[ "$SCRIPT_PATH" == /opt/homebrew/Caskroom/* ]] || \ + [[ "$SCRIPT_PATH" == /usr/local/Caskroom/* ]] || \ + [[ "$SCRIPT_PATH" == /home/linuxbrew/.linuxbrew/Caskroom/* ]]; then INSTALLER="HOMEBREW_CASK" else INSTALLER="TARBALL" @@ -43,6 +45,10 @@ if [[ "$INSTALLER" == "HOMEBREW_CASK" ]]; then PYTHON="/opt/homebrew/bin/python${PY_VER}" elif [[ -x "/usr/local/bin/python${PY_VER}" ]]; then PYTHON="/usr/local/bin/python${PY_VER}" + elif [[ -x "/home/linuxbrew/.linuxbrew/opt/python@${PY_VER}/libexec/bin/${PY_BIN}" ]]; then + PYTHON="/home/linuxbrew/.linuxbrew/opt/python@${PY_VER}/libexec/bin/${PY_BIN}" + elif [[ -x "/home/linuxbrew/.linuxbrew/bin/python${PY_VER}" ]]; then + PYTHON="/home/linuxbrew/.linuxbrew/bin/python${PY_VER}" else echo "Error: Python ${PY_VER} not found." >&2 echo "Install via Homebrew: brew install python@${PY_VER}" >&2 diff --git a/scripts/release/macos/templates/azure-cli.rb.in b/scripts/release/standalone/templates/azure-cli.rb.in similarity index 65% rename from scripts/release/macos/templates/azure-cli.rb.in rename to scripts/release/standalone/templates/azure-cli.rb.in index 6941ba1fe62..15449ae347a 100644 --- a/scripts/release/macos/templates/azure-cli.rb.in +++ b/scripts/release/standalone/templates/azure-cli.rb.in @@ -1,12 +1,14 @@ cask "azure-cli" do arch arm: "arm64", intel: "x86_64" + os macos: "macos", linux: "linux" version "{{ version }}" - sha256 arm: "{{ arm64_sha }}", - intel: "{{ x86_64_sha }}" + sha256 arm: "{{ arm64_macos_sha }}", + intel: "{{ x86_64_macos_sha }}", + arm64_linux: "{{ arm64_linux_sha }}", + x86_64_linux: "{{ x86_64_linux_sha }}" - url "https://github.com/{{ github_repo }}/releases/download/azure-cli-#{version}/azure-cli-#{version}-macos-#{arch}.tar.gz", - verified: "github.com/{{ github_repo }}/" + url "https://github.com/{{ github_repo }}/releases/download/azure-cli-#{version}/azure-cli-#{version}-#{os}-#{arch}.tar.gz" name "Azure CLI" desc "Microsoft Azure CLI 2.0" homepage "https://docs.microsoft.com/cli/azure/overview"