diff --git a/.Pipelines/pipeline-release.yml b/.Pipelines/pipeline-release.yml new file mode 100644 index 00000000..e0848daa --- /dev/null +++ b/.Pipelines/pipeline-release.yml @@ -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) diff --git a/.Pipelines/pipeline-unit-tests.yml b/.Pipelines/pipeline-unit-tests.yml new file mode 100644 index 00000000..187ce1d5 --- /dev/null +++ b/.Pipelines/pipeline-unit-tests.yml @@ -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 + + - 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()