Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions .Pipelines/pipeline-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# ADO release pipeline for the msal Python package.
#
# Mirrors the GitHub CD flow in .github/workflows/python-package.yml (the `cd:` job):
# - push to a `release-*` branch → publish to TestPyPI
# - push of a tag (e.g. `1.36.0`) → publish to PyPI (with manual approval)
#
# Secrets are fetched at run time from Key Vault `msidlabs` via the
# `AuthSdkResourceManager` service connection (same pattern as the lab cert in
# pipeline-unit-tests.yml). Required Key Vault secrets:
# - TestPyPiApiToken → TestPyPI API token (starts with `pypi-`)
# - PyPiApiToken → PyPI API token (starts with `pypi-`)
#
# Required ADO setup before first run:
# 1. Add the two secrets above to the `msidlabs` Key Vault.
# 2. Authorize the `AuthSdkResourceManager` SC for this pipeline (one-time
# "Permit" prompt on the first run).
# 3. Create an Environment named `msal-py-pypi` with a required approver
# (ADO → Pipelines → Environments → New environment → Approvals and checks).

trigger:
branches:
include:
- release-*
tags:
include:
- '*'

pr: none

variables:
- name: pythonBuildVersion
value: '3.12'

stages:

# ─────────────────────────────────────────────────────────────────────────────
# Stage 1 · Build sdist + wheel, publish as a pipeline artifact.
# ─────────────────────────────────────────────────────────────────────────────
- stage: Build
displayName: 'Build'
jobs:
- job: BuildDist
displayName: 'sdist + wheel'
pool:
vmImage: ubuntu-22.04
steps:
- task: UsePythonVersion@0
displayName: 'Use Python $(pythonBuildVersion)'
inputs:
versionSpec: $(pythonBuildVersion)

- bash: |
set -euo pipefail
python -m pip install --upgrade pip build twine
python -m build --sdist --wheel --outdir dist/ .
python -m twine check dist/*
ls -la dist/
displayName: 'Build + twine check'

- task: PublishPipelineArtifact@1
displayName: 'Publish dist/ as pipeline artifact'
inputs:
targetPath: dist/
artifact: python-dist

# ─────────────────────────────────────────────────────────────────────────────
# Stage 2a · Publish to TestPyPI — runs on push to a release-* branch.
# No approval gate (matches GitHub flow today).
# ─────────────────────────────────────────────────────────────────────────────
- stage: PublishTestPyPI
displayName: 'Publish to TestPyPI'
dependsOn: Build
condition: |
and(
succeeded(),
startsWith(variables['Build.SourceBranch'], 'refs/heads/release-')
)
jobs:
- job: Upload
displayName: 'twine upload → test.pypi.org'
pool:
vmImage: ubuntu-22.04
steps:
- checkout: none

- task: DownloadPipelineArtifact@2
displayName: 'Download dist/ artifact'
inputs:
artifactName: python-dist
targetPath: dist/

- task: UsePythonVersion@0
displayName: 'Use Python $(pythonBuildVersion)'
inputs:
versionSpec: $(pythonBuildVersion)

- task: AzureKeyVault@2
displayName: 'Fetch TestPyPI API token from Key Vault'
inputs:
azureSubscription: 'AuthSdkResourceManager'
KeyVaultName: 'msidlabs'
SecretsFilter: 'TestPyPiApiToken'
RunAsPreJob: false

- bash: |
set -euo pipefail
python -m pip install --upgrade pip twine
python -m twine upload \
--repository-url https://test.pypi.org/legacy/ \
--username __token__ \
--password "$TWINE_PASSWORD" \
--skip-existing \
dist/*
displayName: 'twine upload → test.pypi.org'
env:
TWINE_PASSWORD: $(TestPyPiApiToken)

# ─────────────────────────────────────────────────────────────────────────────
# Stage 2b · Publish to PyPI — runs on push of a tag.
# Manual approval enforced via the `msal-py-pypi` Environment.
# ─────────────────────────────────────────────────────────────────────────────
- stage: PublishPyPI
displayName: 'Publish to PyPI'
dependsOn: Build
condition: |
and(
succeeded(),
startsWith(variables['Build.SourceBranch'], 'refs/tags/')
)
jobs:
- deployment: Upload
displayName: 'twine upload → pypi.org'
pool:
vmImage: ubuntu-22.04
environment: 'msal-py-pypi'
strategy:
runOnce:
deploy:
steps:
- task: DownloadPipelineArtifact@2
displayName: 'Download dist/ artifact'
inputs:
artifactName: python-dist
targetPath: dist/

- task: UsePythonVersion@0
displayName: 'Use Python $(pythonBuildVersion)'
inputs:
versionSpec: $(pythonBuildVersion)

- task: AzureKeyVault@2
displayName: 'Fetch PyPI API token from Key Vault'
inputs:
azureSubscription: 'AuthSdkResourceManager'
KeyVaultName: 'msidlabs'
SecretsFilter: 'PyPiApiToken'
RunAsPreJob: false

- bash: |
set -euo pipefail
python -m pip install --upgrade pip twine
python -m twine upload \
--username __token__ \
--password "$TWINE_PASSWORD" \
dist/*
displayName: 'twine upload → pypi.org'
env:
TWINE_PASSWORD: $(PyPiApiToken)
159 changes: 159 additions & 0 deletions .Pipelines/pipeline-unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# ADO pipeline for the msal Python test suite.
# Two stages → two GitHub stage checks (Unit test, E2E tests). Each stage
# fans out the Python matrix as separate jobs so every Python version runs
# on its own agent in parallel with its own timeout.

trigger:
branches:
include:
- 4gust/ado-pipeline
- dev

pr:
branches:
include:
- dev
drafts: false

stages:

# ─────────────────────────────────────────────────────────────────────────────
# Stage 1 · Unit test — no Key Vault, no service connection.
# ─────────────────────────────────────────────────────────────────────────────
- stage: UnitTests
displayName: 'Unit test'
jobs:
- job: Pytest
displayName: 'pytest'
pool:
vmImage: ubuntu-22.04
timeoutInMinutes: 30
strategy:
matrix:
Python39: { python.version: '3.9' }
Python310: { python.version: '3.10' }
Python311: { python.version: '3.11' }
Python312: { python.version: '3.12' }
Python313: { python.version: '3.13' }
Python314: { python.version: '3.14' }
maxParallel: 6
steps:
- task: UsePythonVersion@0
displayName: 'Use Python $(python.version)'
inputs:
versionSpec: '$(python.version)'

- bash: |
set -euo pipefail
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-azurepipelines
displayName: 'Install Python dependencies'

- bash: |
set -o pipefail
mkdir -p test-results
pytest -vv \
--junitxml=test-results/junit-unit.xml \
--ignore=tests/test_e2e.py \
--ignore=tests/test_e2e_manual.py \
--ignore=tests/test_fmi_e2e.py \
2>&1 | tee test-results/pytest-unit.log
displayName: 'Run pytest (unit)'

- task: PublishTestResults@2
displayName: 'Publish JUnit test results'
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: 'test-results/junit-unit.xml'
failTaskOnFailedTests: true
testRunTitle: 'Unit · Python $(python.version)'

# ─────────────────────────────────────────────────────────────────────────────
# Stage 2 · E2E tests — runs only if unit tests pass. Fetches the MSID Lab
# certificate from Key Vault (mirrors MSAL.NET's
# build/template-install-keyvault-secrets.yaml).
# Skipped on forked PRs — service connections / Key Vault are not
# available to forks. E2E tests self-skip when LAB_APP_CLIENT_CERT_PFX_PATH
# is unset (matches the pattern in template-pipeline-stages.yml).
# ─────────────────────────────────────────────────────────────────────────────
- stage: E2ETests
displayName: 'E2E tests'
dependsOn: UnitTests
condition: succeeded()
jobs:
- job: Pytest
displayName: 'pytest'
pool:
vmImage: ubuntu-22.04
timeoutInMinutes: 60
strategy:
matrix:
Python39: { python.version: '3.9' }
Python310: { python.version: '3.10' }
Python311: { python.version: '3.11' }
Python312: { python.version: '3.12' }
Python313: { python.version: '3.13' }
Python314: { python.version: '3.14' }
maxParallel: 6
steps:
- task: UsePythonVersion@0
displayName: 'Use Python $(python.version)'
inputs:
versionSpec: '$(python.version)'

- task: AzureKeyVault@2
displayName: 'Fetch MSID Lab certificate from Key Vault'
condition: ne(variables['System.PullRequest.IsFork'], 'True')
inputs:
azureSubscription: 'AuthSdkResourceManager'
KeyVaultName: 'msidlabs'
SecretsFilter: 'LabAuth'
RunAsPreJob: false
Comment on lines +106 to +113

- bash: |
set -euo pipefail
if [ -z "${LAB_AUTH_B64:-}" ]; then
echo "##vso[task.logissue type=error]LabAuth secret is empty — Key Vault retrieval failed."
exit 1
fi
CERT_PATH="$(Agent.TempDirectory)/lab-auth.pfx"
printf '%s' "$LAB_AUTH_B64" | base64 -d > "$CERT_PATH"
echo "##vso[task.setvariable variable=LAB_APP_CLIENT_CERT_PFX_PATH]$CERT_PATH"
echo "Lab cert written to: $CERT_PATH ($(wc -c < "$CERT_PATH") bytes)"
displayName: 'Decode lab certificate to PFX'
condition: ne(variables['System.PullRequest.IsFork'], 'True')
env:
LAB_AUTH_B64: $(LabAuth)

- bash: |
set -euo pipefail
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-azurepipelines
displayName: 'Install Python dependencies'

- bash: |
set -o pipefail
mkdir -p test-results
pytest -vv \
--junitxml=test-results/junit-e2e.xml \
tests/test_e2e.py tests/test_fmi_e2e.py \
2>&1 | tee test-results/pytest-e2e.log
displayName: 'Run pytest (E2E)'
env:
LAB_APP_CLIENT_CERT_PFX_PATH: $(LAB_APP_CLIENT_CERT_PFX_PATH)

- task: PublishTestResults@2
displayName: 'Publish JUnit test results'
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: 'test-results/junit-e2e.xml'
failTaskOnFailedTests: true
testRunTitle: 'E2E · Python $(python.version)'

- bash: rm -f "$(Agent.TempDirectory)/lab-auth.pfx"
displayName: 'Remove lab certificate from agent'
condition: always()
Loading