From 416e840a5d8b769cc473fe6a554502f2988c0494 Mon Sep 17 00:00:00 2001 From: Ryan Auld Date: Fri, 5 Jun 2026 13:50:40 -0700 Subject: [PATCH 01/13] Add OneBranch extends wrappers to CI and release pipelines - azure-pipelines.yml: extends v2/OneBranch.NonOfficial.CrossPlat.yml - pipeline-publish.yml: extends v2/OneBranch.Official.CrossPlat.yml - template-pipeline-stages.yml: remove PreBuildCheck stage (SDL now via globalSdl), convert pools to OneBranch syntax (type: linux, isCustom, ob_outputDirectory) OneBranch provides: TSA, CredScan, PoliCheck, CodeQL (Python), and compliance tracking. Release pipeline adds ManualValidation@1 approval gate before ESRP publish. Also includes ESRP client ID fix (38d4 -> 384d). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .Pipelines/pipeline-publish.yml | 361 ++++++++++++++---------- .Pipelines/template-pipeline-stages.yml | 98 ++----- azure-pipelines.yml | 143 ++++++---- 3 files changed, 325 insertions(+), 277 deletions(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index 12133d72..80bd74b4 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -1,12 +1,13 @@ # pipeline-publish.yml # -# Release pipeline for the msal Python package — manually triggered only. +# Release pipeline for the msal Python package - manually triggered only. # Source: https://github.com/AzureAD/microsoft-authentication-library-for-python # +# Extends OneBranch Official template for compliance (SDL, TSA, CodeQL). +# # Publish targets: -# test.pypi.org (Preview / RC) — preview releases via MSAL-Test-Python-Upload SC -# (SC creation pending test.pypi.org API token) -# pypi.org (ESRP Production) — production releases via ESRP (EsrpRelease@12) using MSAL-ESRP-AME SC +# test.pypi.org (Preview / RC) - preview releases (SC creation pending) +# pypi.org (ESRP Production) - production releases via ESRP (EsrpRelease@12) # # For pipeline documentation, see .Pipelines/CI-AND-RELEASE-PIPELINES.md. @@ -22,158 +23,210 @@ parameters: - 'test.pypi.org (Preview / RC)' - 'pypi.org (ESRP Production)' -trigger: none # manual runs only — no automatic branch or tag triggers +trigger: none # manual runs only - no automatic branch or tag triggers pr: none -# Stage flow: -# -# PreBuildCheck ─► Validate ─► UnitTests ─► E2ETests ─► Build ─► PublishMSALPython (publishTarget == Preview) -# └─► PublishPyPI (publishTarget == ESRP Production) +variables: + CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] -stages: +resources: + repositories: + - repository: templates + type: git + name: OneBranch.Pipelines/GovernedTemplates + ref: refs/heads/main -# PreBuildCheck, Validate, UnitTests, and E2ETests stages are defined in the shared template. -- template: template-pipeline-stages.yml +extends: + template: v2/OneBranch.Official.CrossPlat.yml@templates parameters: - packageVersion: ${{ parameters.packageVersion }} - runPublish: true - -# ══════════════════════════════════════════════════════════════════════════════ -# Stage 3 · Build — build sdist + wheel -# ══════════════════════════════════════════════════════════════════════════════ -- stage: Build - displayName: 'Build package' - dependsOn: E2ETests - condition: eq(dependencies.E2ETests.result, 'Succeeded') - jobs: - - job: BuildDist - displayName: 'Build sdist + wheel (Python 3.12)' - pool: - vmImage: ubuntu-latest - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '3.12' - displayName: 'Use Python 3.12' - - - script: | - python -m pip install --upgrade pip build twine - displayName: 'Install build toolchain' - - - script: | - python -m build - displayName: 'Build sdist and wheel' - - - script: | - python -m twine check dist/* - displayName: 'Verify distribution (twine check)' - - - task: PublishPipelineArtifact@1 - displayName: 'Publish dist/ as pipeline artifact' - inputs: - targetPath: dist/ - artifact: python-dist - -# ══════════════════════════════════════════════════════════════════════════════ -# Stage 4a · Publish to test.pypi.org (Preview / RC) -# Note: requires MSAL-Test-Python-Upload SC in ADO (pending test.pypi.org API token) -# ══════════════════════════════════════════════════════════════════════════════ -- stage: PublishMSALPython - displayName: 'Publish to test.pypi.org (Preview)' - dependsOn: Build - condition: > - and( - eq(dependencies.Build.result, 'Succeeded'), - eq('${{ parameters.publishTarget }}', 'test.pypi.org (Preview / RC)') - ) - jobs: - - deployment: DeployMSALPython - displayName: 'Upload to test.pypi.org' - pool: - vmImage: ubuntu-latest - environment: MSAL-Python - strategy: - runOnce: - deploy: - steps: - - task: DownloadPipelineArtifact@2 - displayName: 'Download python-dist artifact' - inputs: - artifactName: python-dist - targetPath: $(Pipeline.Workspace)/python-dist - - - task: UsePythonVersion@0 - inputs: - versionSpec: '3.12' - displayName: 'Use Python 3.12' - - - script: | - python -m pip install --upgrade pip twine - displayName: 'Install twine' - - # TODO: create MSAL-Test-Python-Upload SC with test.pypi.org API token, then uncomment: - # - task: TwineAuthenticate@1 - # displayName: 'Authenticate with MSAL-Test-Python-Upload' - # inputs: - # pythonUploadServiceConnection: MSAL-Test-Python-Upload - - # - script: | - # python -m twine upload \ - # -r "MSAL-Test-Python-Upload" \ - # --config-file $(PYPIRC_PATH) \ - # --skip-existing \ - # $(Pipeline.Workspace)/python-dist/* - # displayName: 'Upload to test.pypi.org' - - - script: echo "Publish to test.pypi.org skipped — MSAL-Test-Python-Upload SC not yet created." - displayName: 'Skip upload (SC pending)' - -# ══════════════════════════════════════════════════════════════════════════════ -# Stage 4b · Publish to PyPI (ESRP Production) -# Uses EsrpRelease@12 via the MSAL-ESRP-AME service connection. -# IMPORTANT: configure a required manual approval on this environment in -# ADO → Pipelines → Environments → MSAL-Python-Release → Approvals and checks. -# IMPORTANT: EsrpRelease@12 requires a Windows agent. -# ══════════════════════════════════════════════════════════════════════════════ -- stage: PublishPyPI - displayName: 'Publish to PyPI (ESRP Production)' - dependsOn: Build - condition: > - and( - eq(dependencies.Build.result, 'Succeeded'), - eq('${{ parameters.publishTarget }}', 'pypi.org (ESRP Production)') - ) - jobs: - - deployment: DeployPyPI - displayName: 'Upload to PyPI via ESRP' - pool: - vmImage: windows-latest - environment: MSAL-Python-Release - strategy: - runOnce: - deploy: - steps: - - task: DownloadPipelineArtifact@2 - displayName: 'Download python-dist artifact' - inputs: - artifactName: python-dist - targetPath: $(Pipeline.Workspace)/python-dist - - - task: EsrpRelease@12 - displayName: 'Publish to PyPI via ESRP' - inputs: - connectedservicename: 'MSAL-ESRP-AME' - usemanagedidentity: true - keyvaultname: 'MSALVault' - signcertname: 'MSAL-ESRP-Release-Signing' - clientid: '8650ce2b-38d4-466a-9144-bc5c19c88112' - intent: 'PackageDistribution' - contenttype: 'PyPi' - contentsource: 'Folder' - folderlocation: '$(Pipeline.Workspace)/python-dist' - waitforreleasecompletion: true - owners: 'ryauld@microsoft.com,avdunn@microsoft.com' - approvers: 'avdunn@microsoft.com,bogavril@microsoft.com' - serviceendpointurl: 'https://api.esrp.microsoft.com' - mainpublisher: 'ESRPRELPACMAN' - domaintenantid: '33e01921-4d64-4f8c-a055-5bdaffd5e33d' + globalSdl: + asyncSdl: + enabled: false + tsa: + enabled: true + breakOnFailure: true + credscan: + suppressionsFile: $(Build.SourcesDirectory)/.Pipelines/credscan-exclusion.json + policheck: + break: true + codeql: + compiled: + enabled: false + interpretedLanguages: python + tsaEnabled: true + featureFlags: + EnableCDPxPAT: false + + stages: + + # Validate, UnitTests, and E2ETests stages from the shared template. + - template: .Pipelines/template-pipeline-stages.yml + parameters: + packageVersion: ${{ parameters.packageVersion }} + runPublish: true + + # ======================================================================== + # Stage 4 - Build - build sdist + wheel + # ======================================================================== + - stage: Build + displayName: 'Build package' + dependsOn: E2ETests + condition: eq(dependencies.E2ETests.result, 'Succeeded') + jobs: + - job: BuildDist + displayName: 'Build sdist + wheel (Python 3.12)' + pool: + type: linux + isCustom: true + vmImage: 'ubuntu-latest' + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.12' + displayName: 'Use Python 3.12' + + - script: | + python -m pip install --upgrade pip build twine + displayName: 'Install build toolchain' + + - script: | + python -m build + displayName: 'Build sdist and wheel' + + - script: | + python -m twine check dist/* + displayName: 'Verify distribution (twine check)' + + - task: PublishPipelineArtifact@1 + displayName: 'Publish dist/ as pipeline artifact' + inputs: + targetPath: dist/ + artifact: python-dist + + # ======================================================================== + # Stage 5a - Publish to test.pypi.org (Preview / RC) + # ======================================================================== + - stage: PublishMSALPython + displayName: 'Publish to test.pypi.org (Preview)' + dependsOn: Build + condition: > + and( + eq(dependencies.Build.result, 'Succeeded'), + eq('${{ parameters.publishTarget }}', 'test.pypi.org (Preview / RC)') + ) + jobs: + - deployment: DeployMSALPython + displayName: 'Upload to test.pypi.org' + pool: + type: linux + isCustom: true + vmImage: 'ubuntu-latest' + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + environment: MSAL-Python + strategy: + runOnce: + deploy: + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download python-dist artifact' + inputs: + artifactName: python-dist + targetPath: $(Pipeline.Workspace)/python-dist + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.12' + displayName: 'Use Python 3.12' + + - script: | + python -m pip install --upgrade pip twine + displayName: 'Install twine' + + # TODO: create MSAL-Test-Python-Upload SC with test.pypi.org API token, then uncomment: + # - task: TwineAuthenticate@1 + # displayName: 'Authenticate with MSAL-Test-Python-Upload' + # inputs: + # pythonUploadServiceConnection: MSAL-Test-Python-Upload + + # - script: | + # python -m twine upload \ + # -r "MSAL-Test-Python-Upload" \ + # --config-file $(PYPIRC_PATH) \ + # --skip-existing \ + # $(Pipeline.Workspace)/python-dist/* + # displayName: 'Upload to test.pypi.org' + + - script: echo "Publish to test.pypi.org skipped - MSAL-Test-Python-Upload SC not yet created." + displayName: 'Skip upload (SC pending)' + + # ======================================================================== + # Stage 5b - Approval gate for production PyPI release + # ======================================================================== + - stage: Approval + displayName: 'Pre-release approval' + dependsOn: Build + condition: > + and( + eq(dependencies.Build.result, 'Succeeded'), + eq('${{ parameters.publishTarget }}', 'pypi.org (ESRP Production)') + ) + jobs: + - job: RequireApproval + displayName: 'Manual approval required' + timeoutInMinutes: 1440 + pool: + type: agentless + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + steps: + - task: ManualValidation@1 + displayName: 'Approve PyPI release' + inputs: + notifyUsers: | + ryauld@microsoft.com + avdunn@microsoft.com + instructions: 'Approve publishing msal ${{ parameters.packageVersion }} to PyPI via ESRP.' + + # ======================================================================== + # Stage 5c - Publish to PyPI (ESRP Production) + # EsrpRelease@12 requires a Windows agent. + # ======================================================================== + - stage: PublishPyPI + displayName: 'Publish to PyPI (ESRP Production)' + dependsOn: Approval + condition: eq(dependencies.Approval.result, 'Succeeded') + jobs: + - job: EsrpPublish + displayName: 'Upload to PyPI via ESRP' + pool: + type: windows + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download python-dist artifact' + inputs: + artifactName: python-dist + targetPath: $(Pipeline.Workspace)/python-dist + + - task: EsrpRelease@12 + displayName: 'Publish to PyPI via ESRP' + inputs: + connectedservicename: 'MSAL-ESRP-AME' + usemanagedidentity: true + keyvaultname: 'MSALVault' + signcertname: 'MSAL-ESRP-Release-Signing' + clientid: '8650ce2b-384d-466a-9144-bc5c19c88112' + intent: 'PackageDistribution' + contenttype: 'PyPi' + contentsource: 'Folder' + folderlocation: '$(Pipeline.Workspace)/python-dist' + waitforreleasecompletion: true + owners: 'ryauld@microsoft.com,avdunn@microsoft.com' + approvers: 'avdunn@microsoft.com,bogavril@microsoft.com' + serviceendpointurl: 'https://api.esrp.microsoft.com' + mainpublisher: 'ESRPRELPACMAN' + domaintenantid: '33e01921-4d64-4f8c-a055-5bdaffd5e33d' diff --git a/.Pipelines/template-pipeline-stages.yml b/.Pipelines/template-pipeline-stages.yml index e6b68438..bf3586ac 100644 --- a/.Pipelines/template-pipeline-stages.yml +++ b/.Pipelines/template-pipeline-stages.yml @@ -3,22 +3,22 @@ # Shared stages template for the msal Python package. # # Called from: -# pipeline-publish.yml — release build (runPublish: true) -# azure-pipelines.yml — PR gate and post-merge CI (runPublish: false) +# pipeline-publish.yml - release build (runPublish: true) +# azure-pipelines.yml - PR gate and post-merge CI (runPublish: false) # # Parameters: # packageVersion - Version to validate against msal/sku.py # Required when runPublish is true; unused otherwise. # runPublish - When true: also runs the Validate stage before UnitTests. -# When false (PR / merge builds): only PreBuildCheck + tests run. +# When false (PR / merge builds): only UnitTests + E2ETests run. # # Stage flow: # -# runPublish: true → PreBuildCheck ─► Validate ─► UnitTests ─► E2ETests -# runPublish: false → PreBuildCheck ─► UnitTests ─► E2ETests (Validate is skipped) +# runPublish: true -> Validate -> UnitTests -> E2ETests +# runPublish: false -> UnitTests -> E2ETests # -# Build and Publish stages are defined in pipeline-publish.yml (not here), -# so that the PR build never references PyPI service connections. +# Note: SDL security scans (PoliCheck, CredScan, CodeQL) are handled by OneBranch +# globalSdl configuration in the parent pipeline. No explicit PreBuildCheck stage needed. parameters: - name: packageVersion @@ -31,66 +31,21 @@ parameters: stages: # ══════════════════════════════════════════════════════════════════════════════ -# Stage 0 · PreBuildCheck — SDL security scans (PoliCheck + CredScan) -# Always runs, mirrors MSAL.NET pre-build analysis. -# ══════════════════════════════════════════════════════════════════════════════ -- stage: PreBuildCheck - displayName: 'Pre-build security checks' - jobs: - - job: SecurityScan - displayName: 'PoliCheck + CredScan' - pool: - vmImage: windows-latest - variables: - Codeql.SkipTaskAutoInjection: true - steps: - - task: NodeTool@0 - displayName: 'Install Node.js (includes npm)' - inputs: - versionSpec: '20.x' - - - task: securedevelopmentteam.vss-secure-development-tools.build-task-policheck.PoliCheck@2 - displayName: 'Run PoliCheck' - inputs: - targetType: F - continueOnError: true - - - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@3 - displayName: 'Run CredScan' - inputs: - suppressionsFile: '$(Build.SourcesDirectory)/.Pipelines/credscan-exclusion.json' - toolMajorVersion: V2 - debugMode: false - - - task: securedevelopmentteam.vss-secure-development-tools.build-task-postanalysis.PostAnalysis@2 - displayName: 'Post Analysis' - inputs: - GdnBreakGdnToolCredScan: true - GdnBreakGdnToolPoliCheck: true - - - task: securedevelopmentteam.vss-secure-development-tools.build-task-publishsecurityanalysislogs.PublishSecurityAnalysisLogs@3 - displayName: 'Publish Security Analysis Logs (TSA)' - condition: succeededOrFailed() - inputs: - tsaConfigFile: '$(Build.SourcesDirectory)/.Pipelines/tsaConfig.json' - - - task: mspremier.PostBuildCleanup.PostBuildCleanup-task.PostBuildCleanup@3 - displayName: 'Clean agent directories' - condition: always() - -# ══════════════════════════════════════════════════════════════════════════════ -# Stage 1 · Validate — verify packageVersion matches msal/sku.py __version__ +# Stage 1 - Validate - verify packageVersion matches msal/sku.py __version__ # Skipped when runPublish is false (PR / merge builds). # ══════════════════════════════════════════════════════════════════════════════ - stage: Validate displayName: 'Validate version' - dependsOn: PreBuildCheck - condition: and(${{ parameters.runPublish }}, eq(dependencies.PreBuildCheck.result, 'Succeeded')) + condition: ${{ parameters.runPublish }} jobs: - job: ValidateVersion displayName: 'Check version matches source' pool: - vmImage: ubuntu-latest + type: linux + isCustom: true + vmImage: 'ubuntu-latest' + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' steps: - task: UsePythonVersion@0 inputs: @@ -116,7 +71,7 @@ stages: displayName: 'Verify version parameter matches msal/sku.py' # ══════════════════════════════════════════════════════════════════════════════ -# Stage 2 · UnitTests — run unit tests across all supported Python versions. +# Stage 2 - UnitTests - run unit tests across all supported Python versions. # No Key Vault or service connection needed. # Waits for Validate when runPublish is true; # runs immediately when Validate is skipped (PR / merge builds). @@ -124,19 +79,19 @@ stages: - stage: UnitTests displayName: 'Unit test' dependsOn: - - PreBuildCheck - Validate condition: | - and( - eq(dependencies.PreBuildCheck.result, 'Succeeded'), - in(dependencies.Validate.result, 'Succeeded', 'Skipped') - ) + in(dependencies.Validate.result, 'Succeeded', 'Skipped') jobs: - job: Pytest displayName: 'pytest' pool: - vmImage: ubuntu-22.04 + type: linux + isCustom: true + vmImage: 'ubuntu-22.04' timeoutInMinutes: 30 + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' strategy: matrix: Python39: { python.version: '3.9' } @@ -216,9 +171,8 @@ stages: testRunTitle: 'Cryptography ceiling check $(python.version)' # ══════════════════════════════════════════════════════════════════════════════ -# Stage 3 · E2ETests — runs only if unit tests pass. Fetches the MSID Lab -# certificate from Key Vault (mirrors MSAL.NET's -# build/template-install-keyvault-secrets.yaml). +# Stage 3 - E2ETests - runs only if unit tests pass. Fetches the MSID Lab +# certificate from Key Vault. # Fork behaviour: the stage still runs on forked PRs, but the # Key Vault retrieval and certificate decoding steps are skipped # via `ne(System.PullRequest.IsFork, 'True')`. The pytest step then @@ -235,8 +189,12 @@ stages: - job: Pytest displayName: 'pytest' pool: - vmImage: ubuntu-22.04 + type: linux + isCustom: true + vmImage: 'ubuntu-22.04' timeoutInMinutes: 60 + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' strategy: matrix: Python39: { python.version: '3.9' } diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f155db7f..9c048090 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,7 +1,9 @@ -# PR gate and branch CI for the msal Python package. -# Runs on pushes to dev, on all PRs to dev, and on a daily schedule. -# Delegates test stages to .Pipelines/template-pipeline-stages.yml with -# runPublish: false — PreBuildCheck (SDL scans) + UnitTests + E2ETests only. +################################################################################# +# OneBranch Pipelines - PR Gate and CI # +# Runs on pushes to dev, on all PRs to dev, and on a daily schedule. # +# Documentation: https://aka.ms/obpipelines # +# Yaml Schema: https://aka.ms/obpipelines/yaml/schema # +################################################################################# trigger: branches: @@ -22,59 +24,94 @@ schedules: - dev always: false # only run when there are new changes -stages: -- template: .Pipelines/template-pipeline-stages.yml - parameters: - runPublish: false +variables: + CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] -- stage: Benchmark - displayName: 'Run benchmarks' - dependsOn: E2ETests - # Only run on post-merge pushes to dev — not on PRs or scheduled runs. - condition: | - and( - succeeded('E2ETests'), - eq(variables['Build.SourceBranch'], 'refs/heads/dev'), - or( - eq(variables['Build.Reason'], 'IndividualCI'), - eq(variables['Build.Reason'], 'BatchedCI'), - eq(variables['Build.Reason'], 'Manual') - ) - ) - jobs: - - job: Benchmark - displayName: 'Performance benchmarks (Python 3.9)' - pool: - vmImage: ubuntu-latest - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '3.9' - displayName: 'Set up Python 3.9' +resources: + repositories: + - repository: templates + type: git + name: OneBranch.Pipelines/GovernedTemplates + ref: refs/heads/main - - script: | - python -m pip install --upgrade pip - pip install pytest -r requirements.txt - displayName: 'Install dependencies' +extends: + template: v2/OneBranch.NonOfficial.CrossPlat.yml@templates + parameters: + globalSdl: + asyncSdl: + enabled: false + tsa: + enabled: true + breakOnFailure: true + credscan: + suppressionsFile: $(Build.SourcesDirectory)/.Pipelines/credscan-exclusion.json + policheck: + break: true + codeql: + compiled: + enabled: false + interpretedLanguages: python + tsaEnabled: true + featureFlags: + EnableCDPxPAT: false - - task: Cache@2 - displayName: 'Restore performance baseline cache' - inputs: - key: 'perf-baseline | "$(Agent.OS)" | tests/test_benchmark.py' - path: .perf.baseline + stages: + - template: .Pipelines/template-pipeline-stages.yml + parameters: + runPublish: false - - bash: | - pytest --benchmark-only --benchmark-json benchmark.json --log-cli-level INFO tests/test_benchmark.py + - stage: Benchmark displayName: 'Run benchmarks' + dependsOn: E2ETests + # Only run on post-merge pushes to dev - not on PRs or scheduled runs. + condition: | + and( + succeeded('E2ETests'), + eq(variables['Build.SourceBranch'], 'refs/heads/dev'), + or( + eq(variables['Build.Reason'], 'IndividualCI'), + eq(variables['Build.Reason'], 'BatchedCI'), + eq(variables['Build.Reason'], 'Manual') + ) + ) + jobs: + - job: Benchmark + displayName: 'Performance benchmarks (Python 3.9)' + pool: + type: linux + isCustom: true + vmImage: 'ubuntu-latest' + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.9' + displayName: 'Set up Python 3.9' + + - script: | + python -m pip install --upgrade pip + pip install pytest -r requirements.txt + displayName: 'Install dependencies' + + - task: Cache@2 + displayName: 'Restore performance baseline cache' + inputs: + key: 'perf-baseline | "$(Agent.OS)" | tests/test_benchmark.py' + path: .perf.baseline + + - bash: | + pytest --benchmark-only --benchmark-json benchmark.json --log-cli-level INFO tests/test_benchmark.py + displayName: 'Run benchmarks' - - bash: | - [ -f benchmark.json ] && echo "##vso[task.setvariable variable=benchmarkFileExists]true" - displayName: 'Check benchmark output exists' - condition: succeededOrFailed() + - bash: | + [ -f benchmark.json ] && echo "##vso[task.setvariable variable=benchmarkFileExists]true" + displayName: 'Check benchmark output exists' + condition: succeededOrFailed() - - task: PublishPipelineArtifact@1 - displayName: 'Publish benchmark results' - condition: and(succeededOrFailed(), eq(variables['benchmarkFileExists'], 'true')) - inputs: - targetPath: 'benchmark.json' - artifact: 'benchmark-results' + - task: PublishPipelineArtifact@1 + displayName: 'Publish benchmark results' + condition: and(succeededOrFailed(), eq(variables['benchmarkFileExists'], 'true')) + inputs: + targetPath: 'benchmark.json' + artifact: 'benchmark-results' From b9fd2b7320e8d99d9ec38cea3410c2880a0539e7 Mon Sep 17 00:00:00 2001 From: Ryan Auld Date: Fri, 5 Jun 2026 14:08:13 -0700 Subject: [PATCH 02/13] fix: OneBranch validation - artifact naming and parameter defaults - Rename PublishPipelineArtifact artifacts to drop__ convention - Add default values for packageVersion and publishTarget parameters - Fixes pipeline queue validation errors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .Pipelines/pipeline-publish.yml | 8 +++++--- azure-pipelines.yml | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index 80bd74b4..222ede74 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -15,10 +15,12 @@ parameters: - name: packageVersion displayName: 'Package version to publish (must match msal/sku.py, e.g. 1.36.0 or 1.36.0rc1)' type: string + default: '1.37.0' - name: publishTarget displayName: 'Publish target' type: string + default: 'test.pypi.org (Preview / RC)' values: - 'test.pypi.org (Preview / RC)' - 'pypi.org (ESRP Production)' @@ -103,7 +105,7 @@ extends: displayName: 'Publish dist/ as pipeline artifact' inputs: targetPath: dist/ - artifact: python-dist + artifact: drop_Build_BuildDist # ======================================================================== # Stage 5a - Publish to test.pypi.org (Preview / RC) @@ -133,7 +135,7 @@ extends: - task: DownloadPipelineArtifact@2 displayName: 'Download python-dist artifact' inputs: - artifactName: python-dist + artifactName: drop_Build_BuildDist targetPath: $(Pipeline.Workspace)/python-dist - task: UsePythonVersion@0 @@ -209,7 +211,7 @@ extends: - task: DownloadPipelineArtifact@2 displayName: 'Download python-dist artifact' inputs: - artifactName: python-dist + artifactName: drop_Build_BuildDist targetPath: $(Pipeline.Workspace)/python-dist - task: EsrpRelease@12 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9c048090..b5882ecf 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -114,4 +114,4 @@ extends: condition: and(succeededOrFailed(), eq(variables['benchmarkFileExists'], 'true')) inputs: targetPath: 'benchmark.json' - artifact: 'benchmark-results' + artifact: 'drop_Benchmark_Benchmark' From f7d3d096e23637c712cb5beb36fbab3675e0d3c5 Mon Sep 17 00:00:00 2001 From: Ryan Auld Date: Fri, 5 Jun 2026 14:09:11 -0700 Subject: [PATCH 03/13] fix: use root-relative path for template reference in publish pipeline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .Pipelines/pipeline-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index 222ede74..b7f28f73 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -62,7 +62,7 @@ extends: stages: # Validate, UnitTests, and E2ETests stages from the shared template. - - template: .Pipelines/template-pipeline-stages.yml + - template: /.Pipelines/template-pipeline-stages.yml parameters: packageVersion: ${{ parameters.packageVersion }} runPublish: true From 10c382eeec087a2e9df94bdede9891d4f06d2a25 Mon Sep 17 00:00:00 2001 From: Ryan Auld Date: Fri, 5 Jun 2026 14:10:12 -0700 Subject: [PATCH 04/13] fix: convert deployment job to regular job for OneBranch compatibility OneBranch custom Linux pools don't support deployment strategy (runOnce). Convert to a regular job with explicit artifact download. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .Pipelines/pipeline-publish.yml | 70 ++++++++++++++++----------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index b7f28f73..e74904d1 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -119,7 +119,7 @@ extends: eq('${{ parameters.publishTarget }}', 'test.pypi.org (Preview / RC)') ) jobs: - - deployment: DeployMSALPython + - job: DeployMSALPython displayName: 'Upload to test.pypi.org' pool: type: linux @@ -127,42 +127,38 @@ extends: vmImage: 'ubuntu-latest' variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' - environment: MSAL-Python - strategy: - runOnce: - deploy: - steps: - - task: DownloadPipelineArtifact@2 - displayName: 'Download python-dist artifact' - inputs: - artifactName: drop_Build_BuildDist - targetPath: $(Pipeline.Workspace)/python-dist - - - task: UsePythonVersion@0 - inputs: - versionSpec: '3.12' - displayName: 'Use Python 3.12' - - - script: | - python -m pip install --upgrade pip twine - displayName: 'Install twine' - - # TODO: create MSAL-Test-Python-Upload SC with test.pypi.org API token, then uncomment: - # - task: TwineAuthenticate@1 - # displayName: 'Authenticate with MSAL-Test-Python-Upload' - # inputs: - # pythonUploadServiceConnection: MSAL-Test-Python-Upload - - # - script: | - # python -m twine upload \ - # -r "MSAL-Test-Python-Upload" \ - # --config-file $(PYPIRC_PATH) \ - # --skip-existing \ - # $(Pipeline.Workspace)/python-dist/* - # displayName: 'Upload to test.pypi.org' - - - script: echo "Publish to test.pypi.org skipped - MSAL-Test-Python-Upload SC not yet created." - displayName: 'Skip upload (SC pending)' + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download build artifact' + inputs: + artifactName: drop_Build_BuildDist + targetPath: $(Pipeline.Workspace)/python-dist + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.12' + displayName: 'Use Python 3.12' + + - script: | + python -m pip install --upgrade pip twine + displayName: 'Install twine' + + # TODO: create MSAL-Test-Python-Upload SC with test.pypi.org API token, then uncomment: + # - task: TwineAuthenticate@1 + # displayName: 'Authenticate with MSAL-Test-Python-Upload' + # inputs: + # pythonUploadServiceConnection: MSAL-Test-Python-Upload + + # - script: | + # python -m twine upload \ + # -r "MSAL-Test-Python-Upload" \ + # --config-file $(PYPIRC_PATH) \ + # --skip-existing \ + # $(Pipeline.Workspace)/python-dist/* + # displayName: 'Upload to test.pypi.org' + + - script: echo "Publish to test.pypi.org skipped - MSAL-Test-Python-Upload SC not yet created." + displayName: 'Skip upload (SC pending)' # ======================================================================== # Stage 5b - Approval gate for production PyPI release From 75a24caee758059e70ad2dda621ea0a3843caf7c Mon Sep 17 00:00:00 2001 From: Ryan Auld Date: Fri, 5 Jun 2026 14:45:44 -0700 Subject: [PATCH 05/13] add .config/tsaoptions.json for OneBranch TSA upload Required by OneBranch SDL pipeline to upload security scan results to TSA. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .config/tsaoptions.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .config/tsaoptions.json diff --git a/.config/tsaoptions.json b/.config/tsaoptions.json new file mode 100644 index 00000000..9c697602 --- /dev/null +++ b/.config/tsaoptions.json @@ -0,0 +1,14 @@ +{ + "codebaseName": "MSAL Python AUTH SDK", + "notificationAliases": [ + "IdentityDevExPython@microsoft.com" + ], + "codebaseAdmins": [ + "EUROPE\\aadidagt" + ], + "instanceUrl": "https://identitydivision.visualstudio.com", + "projectName": "Engineering", + "areaPath": "Engineering\\Auth Client\\Libraries\\Client-SDKs\\MSAL", + "iterationPath": "Engineering\\Unscheduled", + "allTools": true +} From 336d74487b5f763654075e5b39a29624c247148a Mon Sep 17 00:00:00 2001 From: Ryan Auld Date: Mon, 8 Jun 2026 14:09:37 -0700 Subject: [PATCH 06/13] fix: remove isCustom pool config, use OneBranch managed pools 1ES PT requires 1ES Hosted Pools for Official templates. Remove isCustom/vmImage and let OneBranch manage pool selection with just 'type: linux'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .Pipelines/pipeline-publish.yml | 4 ---- .Pipelines/template-pipeline-stages.yml | 6 ------ azure-pipelines.yml | 2 -- 3 files changed, 12 deletions(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index e74904d1..ee14d4b0 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -79,8 +79,6 @@ extends: displayName: 'Build sdist + wheel (Python 3.12)' pool: type: linux - isCustom: true - vmImage: 'ubuntu-latest' variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' steps: @@ -123,8 +121,6 @@ extends: displayName: 'Upload to test.pypi.org' pool: type: linux - isCustom: true - vmImage: 'ubuntu-latest' variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' steps: diff --git a/.Pipelines/template-pipeline-stages.yml b/.Pipelines/template-pipeline-stages.yml index bf3586ac..5191f232 100644 --- a/.Pipelines/template-pipeline-stages.yml +++ b/.Pipelines/template-pipeline-stages.yml @@ -42,8 +42,6 @@ stages: displayName: 'Check version matches source' pool: type: linux - isCustom: true - vmImage: 'ubuntu-latest' variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' steps: @@ -87,8 +85,6 @@ stages: displayName: 'pytest' pool: type: linux - isCustom: true - vmImage: 'ubuntu-22.04' timeoutInMinutes: 30 variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' @@ -190,8 +186,6 @@ stages: displayName: 'pytest' pool: type: linux - isCustom: true - vmImage: 'ubuntu-22.04' timeoutInMinutes: 60 variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b5882ecf..e6ee410a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -79,8 +79,6 @@ extends: displayName: 'Performance benchmarks (Python 3.9)' pool: type: linux - isCustom: true - vmImage: 'ubuntu-latest' variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' steps: From d57526d94fffa9b2aa1e3ba5817e93a033fefd21 Mon Sep 17 00:00:00 2001 From: Ryan Auld Date: Mon, 8 Jun 2026 14:13:53 -0700 Subject: [PATCH 07/13] fix: replace PublishPipelineArtifact with ob_outputDirectory copy OneBranch auto-publishes artifacts from ob_outputDirectory at job end. Explicit PublishPipelineArtifact tasks are not allowed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .Pipelines/pipeline-publish.yml | 8 +++----- azure-pipelines.yml | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index ee14d4b0..8fc263b6 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -99,11 +99,9 @@ extends: python -m twine check dist/* displayName: 'Verify distribution (twine check)' - - task: PublishPipelineArtifact@1 - displayName: 'Publish dist/ as pipeline artifact' - inputs: - targetPath: dist/ - artifact: drop_Build_BuildDist + - script: | + cp -r dist/* $(ob_outputDirectory)/ + displayName: 'Copy dist to output directory' # ======================================================================== # Stage 5a - Publish to test.pypi.org (Preview / RC) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e6ee410a..a006fa48 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -107,9 +107,7 @@ extends: displayName: 'Check benchmark output exists' condition: succeededOrFailed() - - task: PublishPipelineArtifact@1 - displayName: 'Publish benchmark results' + - bash: | + [ -f benchmark.json ] && cp benchmark.json $(ob_outputDirectory)/ + displayName: 'Copy benchmark results to output' condition: and(succeededOrFailed(), eq(variables['benchmarkFileExists'], 'true')) - inputs: - targetPath: 'benchmark.json' - artifact: 'drop_Benchmark_Benchmark' From f6d8ebd89791ebc09e9df462152b4d5149028f26 Mon Sep 17 00:00:00 2001 From: Ryan Auld Date: Mon, 8 Jun 2026 14:23:46 -0700 Subject: [PATCH 08/13] fix: restore isCustom pool config - required for MS-hosted agents Without isCustom, OneBranch tries to run in a container using LinuxContainerImage variable which isn't configured. isCustom: true tells OneBranch to use MS-hosted agents directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .Pipelines/pipeline-publish.yml | 4 ++++ .Pipelines/template-pipeline-stages.yml | 6 ++++++ azure-pipelines.yml | 2 ++ 3 files changed, 12 insertions(+) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index 8fc263b6..8f5ab7b1 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -79,6 +79,8 @@ extends: displayName: 'Build sdist + wheel (Python 3.12)' pool: type: linux + isCustom: true + vmImage: 'ubuntu-latest' variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' steps: @@ -119,6 +121,8 @@ extends: displayName: 'Upload to test.pypi.org' pool: type: linux + isCustom: true + vmImage: 'ubuntu-latest' variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' steps: diff --git a/.Pipelines/template-pipeline-stages.yml b/.Pipelines/template-pipeline-stages.yml index 5191f232..be26d3f4 100644 --- a/.Pipelines/template-pipeline-stages.yml +++ b/.Pipelines/template-pipeline-stages.yml @@ -42,6 +42,8 @@ stages: displayName: 'Check version matches source' pool: type: linux + isCustom: true + vmImage: 'ubuntu-latest' variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' steps: @@ -85,6 +87,8 @@ stages: displayName: 'pytest' pool: type: linux + isCustom: true + vmImage: 'ubuntu-latest' timeoutInMinutes: 30 variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' @@ -186,6 +190,8 @@ stages: displayName: 'pytest' pool: type: linux + isCustom: true + vmImage: 'ubuntu-latest' timeoutInMinutes: 60 variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a006fa48..6aa5d838 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -79,6 +79,8 @@ extends: displayName: 'Performance benchmarks (Python 3.9)' pool: type: linux + isCustom: true + vmImage: 'ubuntu-latest' variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' steps: From 645c76d1f442ac7ef58be12c8fd8ff6b51818d66 Mon Sep 17 00:00:00 2001 From: Ryan Auld Date: Mon, 8 Jun 2026 15:43:36 -0700 Subject: [PATCH 09/13] retrigger CI From 8f52f9fe7ce70363042cc61a9e71f84b1832073d Mon Sep 17 00:00:00 2001 From: Ryan Auld Date: Mon, 8 Jun 2026 15:44:16 -0700 Subject: [PATCH 10/13] retrigger CI From 0fac0d3b47aeaaed618d9ce49a45dacd56b7ee6b Mon Sep 17 00:00:00 2001 From: Ryan Auld Date: Mon, 8 Jun 2026 15:53:04 -0700 Subject: [PATCH 11/13] Add debug parameter for system.debug verbose logging --- .Pipelines/pipeline-publish.yml | 6 ++++++ azure-pipelines.yml | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index 6df164d6..c5b4faa5 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -25,11 +25,17 @@ parameters: - 'test.pypi.org (Preview / RC)' - 'pypi.org (ESRP Production)' +- name: debug + displayName: 'Enable debug output (system.debug)' + type: boolean + default: false + trigger: none # manual runs only - no automatic branch or tag triggers pr: none variables: CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] + system.debug: ${{ parameters.debug }} resources: repositories: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 43e50989..8aef40a4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -24,8 +24,15 @@ schedules: - dev always: false # only run when there are new changes +parameters: +- name: debug + displayName: 'Enable debug output (system.debug)' + type: boolean + default: false + variables: CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] + system.debug: ${{ parameters.debug }} resources: repositories: From 2a101f1cc2c167f3b5f66cc10742c8fac25e9277 Mon Sep 17 00:00:00 2001 From: Ryan Auld Date: Mon, 8 Jun 2026 16:02:58 -0700 Subject: [PATCH 12/13] Fix packageVersion default: use empty string instead of space --- .Pipelines/pipeline-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index c5b4faa5..e4d709b2 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -15,7 +15,7 @@ parameters: - name: packageVersion displayName: 'Package version to publish (must match msal/sku.py, e.g. 1.36.0 or 1.36.0rc1)' type: string - default: ' ' + default: '' - name: publishTarget displayName: 'Publish target' From c1f0ea7138e08aadd8052276f26947ee970ce02c Mon Sep 17 00:00:00 2001 From: Ryan Auld Date: Mon, 8 Jun 2026 16:39:06 -0700 Subject: [PATCH 13/13] Add PostAnalysis task to break build on PoliCheck failures --- .Pipelines/template-pipeline-stages.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.Pipelines/template-pipeline-stages.yml b/.Pipelines/template-pipeline-stages.yml index 42e5b0fc..d62d670f 100644 --- a/.Pipelines/template-pipeline-stages.yml +++ b/.Pipelines/template-pipeline-stages.yml @@ -170,6 +170,14 @@ stages: failTaskOnFailedTests: false testRunTitle: 'Cryptography ceiling check $(python.version)' + - task: PostAnalysis@2 + displayName: 'Guardian Break' + inputs: + GdnBreakFast: false + GdnBreakAllTools: false + GdnBreakGdnToolPoliCheck: true + GdnBreakGdnToolPoliCheckSeverity: Error + # ══════════════════════════════════════════════════════════════════════════════ # Stage 3 - E2ETests - runs only if unit tests pass. Fetches the MSID Lab # certificate from Key Vault.