diff --git a/.Pipelines/ADO-PUBLISH-SETUP.md b/.Pipelines/ADO-PUBLISH-SETUP.md new file mode 100644 index 00000000..a57ed748 --- /dev/null +++ b/.Pipelines/ADO-PUBLISH-SETUP.md @@ -0,0 +1,238 @@ +# ADO Pipeline Setup Guide — MSAL Python → PyPI + +This document describes every step needed to create an Azure DevOps (ADO) +pipeline that checks out the GitHub repo, runs tests, builds distributions, +and publishes to test.pypi.org (via the MSAL-Python environment) and PyPI. + +The `.Pipelines/` folder follows the same template convention as [MSAL.NET](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/tree/main/build): + +| File | Purpose | +|------|---------| +| [`pipeline-publish.yml`](pipeline-publish.yml) | Top-level orchestrator — triggers, variables, stage wiring | +| [`template-run-tests.yml`](template-run-tests.yml) | Reusable step template — pytest across Python version matrix | +| [`template-build-package.yml`](template-build-package.yml) | Reusable step template — `python -m build` + `twine check` + artifact publish | +| [`template-publish-package.yml`](template-publish-package.yml) | Reusable step template — `TwineAuthenticate` + `twine upload` (parameterized for MSAL-Python/PyPI) | + +--- + +## Overview + +This pipeline is **manually triggered only** — no automatic branch or tag triggers. +Every publish requires explicitly entering a version and selecting a destination. + +| Stage | Trigger | Target | +|-------|---------|--------| +| **Validate** | always | asserts `packageVersion` matches `msal/sku.py` | +| **CI** (tests on Py 3.9–3.13) | after Validate | — | +| **Build** (sdist + wheel) | after CI | dist artifact | +| **PublishMSALPython** | `publishTarget = test.pypi.org (Preview / RC)` | test.pypi.org | +| **PublishPyPI** | `publishTarget = pypi.org (Production)` | PyPI (production) | + +--- + +## Step 1 — Prerequisites + +| Requirement | Notes | +|-------------|-------| +| ADO Organization | [Create one](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/create-organization) if you don't have one | +| ADO Project | Under the org; enable **Pipelines** and **Artifacts** | +| GitHub account with admin rights | Needed to authorize the ADO GitHub App | +| PyPI API token | Scoped to the `msal` project — generate at | +| MSAL-Python (test.pypi.org) API token | Scoped to the `msal` project on test.pypi.org | + +--- + +## Step 2 — Connect ADO to the GitHub Repository + +1. In your ADO project go to **Project Settings → Service connections → New service connection**. +2. Choose **GitHub** and click **Next**. +3. Under **Authentication**, select **Grant authorization** (OAuth) — do **not** use Personal Access Token. + - Click **Authorize** — a GitHub OAuth popup will open. + - Sign in with a GitHub account that has admin rights on the `AzureAD` organization. + - Grant access to `microsoft-authentication-library-for-python`. + - This installs the Azure Pipelines GitHub App and enables webhook and repository listing. + + > **Why OAuth and not PAT:** PAT-based connections cannot install the GitHub webhook + > required for pipeline creation via CLI or API. The OAuth/GitHub App flow installs the + > webhook using the browser's authenticated GitHub session. + +4. Set **Service connection name**: `github-msal-python` +5. Check **Grant access permission to all pipelines**, click **Save**. + +--- + +## Step 3 — Create PyPI Service Connections (Twine) + +The `TwineAuthenticate@1` task uses "Python package upload" service connections +for external registries. + +### 3a — MSAL-Python (test.pypi.org) connection + +1. **Project Settings → Service connections → New service connection** +2. Choose **Python package upload**, click **Next**. +3. Fill in: + | Field | Value | + |-------|-------| + | **Twine repository URL** | `https://test.pypi.org/legacy/` | + | **EndpointName** (`-r` value) | `MSAL-Test-Python-Upload` | + | **Authentication method** | **Authentication Token** | + | **Token** | *(your test.pypi.org API token, full value including `pypi-` prefix)* | + | **Service connection name** | `MSAL-Test-Python-Upload` | +4. Check **Grant access permission to all pipelines**, click **Save**. + +### 3b — PyPI (production) connection + +1. **Project Settings → Service connections → New service connection** +2. Choose **Python package upload**, click **Next**. +3. Fill in: + | Field | Value | + |-------|-------| + | **Twine repository URL** | `https://upload.pypi.org/legacy/` | + | **EndpointName** (`-r` value) | `MSAL-Prod-Python-Upload` | + | **Authentication method** | **Authentication Token** | + | **Token** | *(your PyPI API token, full value including `pypi-` prefix)* | + | **Service connection name** | `MSAL-Prod-Python-Upload` | +4. Check **Grant access permission to all pipelines**, click **Save**. + +> **Security note:** Never commit API tokens to source control. All secrets +> are stored in ADO service connections and injected by `TwineAuthenticate@1` +> via the ephemeral `$(PYPIRC_PATH)` file at pipeline runtime. + +--- + +## Step 4 — Create ADO Environments + +Environments let you add approval gates before the deployment jobs run. + +1. Go to **Pipelines → Environments → New environment**. +2. Create two environments: + + | Name | Description | + |------|-------------| + | `MSAL-Python` | Staging — test.pypi.org uploads | + | `MSAL-Python-Release` | Production — PyPI uploads (**add approval check**) | + +3. For the `MSAL-Python-Release` environment: + - Click the `MSAL-Python-Release` environment → **Approvals and checks → +** + - Add **Approvals** → add the release approver(s) (e.g., release manager). + - This ensures a human must approve before the wheel is pushed to production. + +--- + +## Step 5 — Create the Pipeline in ADO + +1. Go to **Pipelines → New pipeline**. +2. Select **GitHub** as the code source. +3. Pick the repository **AzureAD/microsoft-authentication-library-for-python**. + - ADO will use the `github-msal-python` service connection created in Step 2. +4. Choose **Existing Azure Pipelines YAML file**. +5. Set the path to: `/.Pipelines/pipeline-publish.yml` +6. Click **Continue** → review the YAML → click **Save** (not *Run*). +7. Rename the pipeline to something descriptive, e.g. + `msal-python · publish`. + +> **Note:** The existing `azure-pipelines.yml` (CI-only, runs on `dev`) is a +> separate pipeline and is not affected. + +--- + +## Step 6 — Authorize Pipelines to use Service Connections + +When the pipeline first uses a service connection you may be prompted to +authorize it. To pre-authorize: + +1. **Project Settings → Service connections** → click a connection → + **Security** tab. +2. Set the **Pipeline permissions** to include the new publish pipeline. + +Repeat for all three connections: `github-msal-python`, `MSAL-Test-Python-Upload`, +`MSAL-Prod-Python-Upload`. + +--- + +## Step 7 — Pipeline Parameters (Run Pipeline UI) + +This pipeline is **always manually queued**. Both fields are required — the Validate stage fails if either is missing or the version doesn’t match `msal/sku.py`: + +| Parameter | Required | Description | Example values | +|-----------|----------|-------------|----------------| +| **Package version to publish** | Yes | Must exactly match `msal/sku.py __version__`. PEP 440 format only — no `-Preview` suffix. | `1.36.0` (release), `1.36.0rc1` (RC), `1.36.0b1` (beta) | +| **Publish target** | Yes | Explicit destination — no auto-routing. | `test.pypi.org (Preview / RC)` or `pypi.org (Production)` | + +> **Version format:** PyPI enforces [PEP 440](https://peps.python.org/pep-0440/). Versions with `-` (e.g. `1.36.0-Preview`) are rejected. Use `rc1`, `b1`, or `a1` suffixes instead. + +> **Version must be in sync:** Before queuing, update `msal/sku.py __version__` to the target version and push the change. The Validate stage checks the value on the branch the run is sourced from, not the pipeline default branch. + +--- + +## Step 8 — End-to-End Release Walkthrough + +### Publishing a preview / release candidate to test.pypi.org + +1. Set `msal/sku.py __version__ = "1.36.0rc1"` and push the change +2. Go to **Pipelines → MSAL-Python · Publish → Run pipeline** +3. Select the branch/tag to run from (e.g. the release branch) +4. Enter **Package version to publish**: `1.36.0rc1` +5. Select **Publish target**: `test.pypi.org (Preview / RC)` +6. Click **Run** — pipeline runs: Validate → CI → Build → Publish to test.pypi.org +7. Verify at + +### Publishing a production release to PyPI + +1. Set `msal/sku.py __version__ = "1.36.0"` and push to the release branch +2. Go to **Pipelines → MSAL-Python · Publish → Run pipeline** +3. Select the release branch +4. Enter **Package version to publish**: `1.36.0` +5. Select **Publish target**: `pypi.org (Production)` +6. Click **Run** — pipeline runs: Validate → CI → Build → Publish to PyPI (Production) +7. Verify: `pip install msal==1.36.0` or check + +## Pipeline Trigger Reference + +``` +Manual queue (publishTarget = MSAL-Python) + └─► Validate ─► CI ─► Build ─► PublishMSALPython + (test.pypi.org, auto) + +Manual queue (publishTarget = pypi) + └─► Validate ─► CI ─► Build ─► PublishPyPI + (PyPI, requires approval) +``` + +--- + +## Known Requirements + +The following requirements were identified during initial setup and testing: + +- The GitHub service connection **must** be created via OAuth (Grant authorization) in the ADO UI, not via CLI or PAT. The CLI `az pipelines create` command requires webhook installation on the GitHub repo, which requires org admin rights not available to service accounts. +- The pipeline **must** be created via the ADO REST API (`/_apis/build/definitions`) or UI — not via `az pipelines create` — when using an OAuth GitHub service connection without org-level admin rights. +- The `msal/sku.py __version__` must be updated and pushed to the source branch **before** the pipeline run is queued. The Validate stage reads the file from the checked-out branch at runtime. +- The `requirements.txt` file includes `-e .` which causes pip to install `msal` from PyPI as a transitive dependency of `azure-identity`, overwriting the local editable install. The template handles this by removing the `-e .` line and reinstalling the local package last with `--no-deps`. +- The `1.35.1` version bump (hotfix) was released from `origin/release-1.35.0` and was never merged back into `dev`. Before the next release from `dev`, this should be backfilled via PR: `https://github.com/AzureAD/microsoft-authentication-library-for-python/compare/dev...release-1.35.0` + +--- + +## Troubleshooting + +| Symptom | Likely cause | Fix | +|---------|-------------|-----| +| `403` on twine upload | Token expired or wrong scope | Regenerate API token on pypi.org; update the service connection | +| `File already exists` error | Version already published; PyPI does not allow overwriting | Bump version in `msal/sku.py` | +| Validate stage: `msal/sku.py ''` (empty version) | Python import silently failed | The template uses `grep`/`sed` to read the version — verify `msal/sku.py` contains a `__version__ = "..."` line | +| Validate stage: version mismatch | `sku.py` on the source branch doesn't match the parameter entered | Update `msal/sku.py` on the branch the run is sourced from, not just the pipeline default branch | +| Tests: collection failure across all modules | PyPI `msal` installed over the local editable install | Ensure the template installs local package last with `--no-deps` | +| `az pipelines create` fails with webhook error | GitHub service connection PAT/account lacks org admin rights | Create the pipeline via the ADO UI using a browser session with org admin GitHub access | +| Pipeline creation fails: `Value cannot be null. Parameter name: Connection` | GitHub SC ID is wrong or SC was recreated | Re-query the SC ID with `az devops service-endpoint list` and use the current ID | +| Service connection shows `Authentication: PersonalAccessToken` | SC was created via CLI with a PAT | Delete and recreate via UI using OAuth (Grant authorization) so repos are enumerable | +| `TwineAuthenticate` says endpoint not found | Service connection name mismatch | Ensure `pythonUploadServiceConnection` value exactly matches the service connection name | + +--- + +## References + +- [Publish Python packages with Azure Pipelines](https://learn.microsoft.com/en-us/azure/devops/pipelines/artifacts/pypi?view=azure-devops) +- [TwineAuthenticate@1 task reference](https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/twine-authenticate-v1?view=azure-devops) +- [Publish and download Python packages with Azure Artifacts](https://learn.microsoft.com/en-us/azure/devops/artifacts/quickstarts/python-packages?view=azure-devops) +- [Python package upload service connection](https://learn.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints#python-package-upload-service-connection) +- [ADO Environments – approvals and checks](https://learn.microsoft.com/en-us/azure/devops/pipelines/process/approvals?view=azure-devops) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml new file mode 100644 index 00000000..88615e3a --- /dev/null +++ b/.Pipelines/pipeline-publish.yml @@ -0,0 +1,179 @@ +# pipeline-publish.yml +# +# Publish pipeline for the msal Python package. +# Source: https://github.com/AzureAD/microsoft-authentication-library-for-python +# +# Composes reusable templates from this folder: +# template-run-tests.yml - pytest across Python version matrix +# template-build-package.yml - sdist + wheel build + twine check +# template-publish-package.yml - TwineAuthenticate + twine upload (parameterized) +# +# Trigger logic: +# This pipeline is MANUALLY TRIGGERED ONLY. +# Both packageVersion and publishTarget must be explicitly set at queue time. +# +# One-time ADO setup: see ADO-PUBLISH-SETUP.md + +# ── Pipeline parameters ──────────────────────────────────────────────────────── +# Both fields are shown as required inputs in the ADO "Run pipeline" UI. +# Neither has a default — the Validate stage will fail if either is empty or +# if packageVersion does not match msal/sku.py __version__. +parameters: +- name: packageVersion + displayName: 'Package version to publish (must match msal/sku.py, e.g. 1.36.0 or 1.36.0rc1)' + type: string + +- name: publishTarget + displayName: 'Publish target' + type: string + values: + - 'test.pypi.org (Preview / RC)' # publishes to test.pypi.org (staging / preview) + - 'pypi.org (Production)' # publishes to PyPI (production) + +trigger: none # manual runs only — no automatic branch or tag triggers +pr: none + +variables: + pythonBuildVersion: '3.12' # single version used for build + publish jobs + +# ══════════════════════════════════════════════════════════════════════════════ +# Stage 1 · Validate — verify packageVersion matches msal/sku.py before +# anything else runs. +# ══════════════════════════════════════════════════════════════════════════════ +stages: +- stage: Validate + displayName: 'Validate version' + jobs: + - job: ValidateVersion + displayName: 'Check version matches source' + pool: + vmImage: ubuntu-latest + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.12' + displayName: 'Set up Python' + + - bash: | + PARAM_VER="${{ parameters.packageVersion }}" + SKU_VER=$(grep '__version__' msal/sku.py | sed 's/.*"\(.*\)".*/\1/') + + if [ -z "$PARAM_VER" ]; then + echo "##vso[task.logissue type=error]packageVersion is required. Enter the version to publish (must match msal/sku.py __version__)." + exit 1 + elif [ "$PARAM_VER" != "$SKU_VER" ]; then + echo "##vso[task.logissue type=error]Version mismatch: parameter '$PARAM_VER' != msal/sku.py '$SKU_VER'" + echo "Update msal/sku.py __version__ to match the packageVersion parameter, or correct the parameter." + exit 1 + else + echo "Version validated: $PARAM_VER" + fi + displayName: 'Verify version parameter matches msal/sku.py' + +# ══════════════════════════════════════════════════════════════════════════════ +# Stage 2 · CI — run the full test matrix +# ══════════════════════════════════════════════════════════════════════════════ +- stage: CI + displayName: 'Run tests' + dependsOn: Validate + condition: succeeded() + jobs: + - job: Test + displayName: 'Run unit tests' + pool: + vmImage: ubuntu-latest + 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' + steps: + - template: template-run-tests.yml # python.version resolved from matrix + +# ══════════════════════════════════════════════════════════════════════════════ +# Stage 3 · Build — compile sdist + wheel (single Python version) +# ══════════════════════════════════════════════════════════════════════════════ +- stage: Build + displayName: 'Build package' + dependsOn: CI + condition: succeeded() + jobs: + - job: BuildDist + displayName: 'Build sdist + wheel (Python 3.12)' + pool: + vmImage: ubuntu-latest + steps: + - template: template-build-package.yml + parameters: + pythonVersion: '3.12' # must be a literal — template params resolve at compile time + artifactName: python-dist + +# ══════════════════════════════════════════════════════════════════════════════ +# Stage 4a · Publish to MSAL-Python (test.pypi.org) +# Runs when: publishTarget == 'MSAL-Python' +# ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +- stage: PublishMSALPython + displayName: 'Publish to test.pypi.org (Preview)' + dependsOn: Build + condition: > + and( + succeeded(), + eq('${{ parameters.publishTarget }}', 'test.pypi.org (Preview / RC)') + ) + jobs: + - deployment: DeployMSALPython + displayName: 'Upload to test.pypi.org' + pool: + vmImage: ubuntu-latest + # Optional: add approval checks in ADO → Pipelines → Environments → MSAL-Python + environment: MSAL-Python + strategy: + runOnce: + deploy: + steps: + - template: template-publish-package.yml + parameters: + serviceConnectionName: MSAL-Test-Python-Upload + repositoryName: MSAL-Test-Python-Upload + artifactName: python-dist + pythonVersion: '3.12' # must be a literal — template params resolve at compile time + skipExisting: true + +# ══════════════════════════════════════════════════════════════════════════════ +# Stage 4b · Publish to PyPI +# Runs when: publishTarget == 'pypi' +# ══════════════════════════════════════════════════════════════════════════════ +- stage: PublishPyPI + displayName: 'Publish to PyPI (Production)' + dependsOn: Build + condition: > + and( + succeeded(), + eq('${{ parameters.publishTarget }}', 'pypi.org (Production)') + ) + jobs: + - deployment: DeployPyPI + displayName: 'Upload to pypi.org' + pool: + vmImage: ubuntu-latest + # IMPORTANT: configure a required manual approval on this environment in + # ADO → Pipelines → Environments → MSAL-Python-Release → Approvals and checks. + environment: MSAL-Python-Release + strategy: + runOnce: + deploy: + steps: + - template: template-publish-package.yml + parameters: + serviceConnectionName: MSAL-Prod-Python-Upload + repositoryName: MSAL-Prod-Python-Upload + artifactName: python-dist + pythonVersion: '3.12' # must be a literal — template params resolve at compile time + skipExisting: false diff --git a/.Pipelines/template-build-package.yml b/.Pipelines/template-build-package.yml new file mode 100644 index 00000000..06c04f21 --- /dev/null +++ b/.Pipelines/template-build-package.yml @@ -0,0 +1,49 @@ +# template-build-package.yml +# +# Reusable step template: build sdist + wheel, verify with twine check, +# and publish the dist/ folder as a pipeline artifact for downstream jobs. +# +# Parameters: +# pythonVersion - Python version to use for the build (default: '3.12') +# artifactName - Name of the published pipeline artifact (default: 'python-dist') +# +# Usage: +# +# steps: +# - template: .Pipelines/template-build-package.yml +# parameters: +# pythonVersion: '3.12' +# artifactName: 'python-dist' + +parameters: +- name: pythonVersion + type: string + default: '3.12' +- name: artifactName + type: string + default: 'python-dist' + +steps: +- task: UsePythonVersion@0 + inputs: + versionSpec: ${{ parameters.pythonVersion }} + displayName: 'Use Python ${{ parameters.pythonVersion }}' + +- script: | + python -m pip install --upgrade pip build twine + displayName: 'Install build toolchain' + +- script: | + python -m build + displayName: 'Build sdist and wheel' + +# Verify metadata and packaging integrity before any upload attempt +- script: | + python -m twine check dist/* + displayName: 'Verify distribution (twine check)' + +- task: PublishPipelineArtifact@1 + displayName: 'Publish dist/ as pipeline artifact' + inputs: + targetPath: dist/ + artifact: ${{ parameters.artifactName }} diff --git a/.Pipelines/template-install-lab-cert.yml b/.Pipelines/template-install-lab-cert.yml new file mode 100644 index 00000000..ddb1e72b --- /dev/null +++ b/.Pipelines/template-install-lab-cert.yml @@ -0,0 +1,32 @@ +# template-install-lab-cert.yml +# +# Retrieves the MSID Lab authentication certificate from Key Vault and writes +# it to disk as a PFX file, then exposes the path as a pipeline variable so +# the test step can pass it via LAB_APP_CLIENT_CERT_PFX_PATH. +# +# Prerequisites (one-time ADO setup): +# - Service connection 'AuthSdkResourceManager' must exist in the project and +# have 'Get' and 'List' access to the 'msidlabs' Key Vault. +# - Pipeline variable 'LAB_APP_CLIENT_ID' must be set on the pipeline +# (ADO UI: Pipelines -> MSAL-Python Publish -> Edit -> Variables). +# +# The 'LabAuth' secret in msidlabs Key Vault is a base64-encoded PFX +# certificate used to authenticate to both the msidlabs and id4skeyvault +# Key Vaults during e2e tests. + +steps: +- task: AzureKeyVault@2 + displayName: 'Retrieve lab certificate from Key Vault' + inputs: + azureSubscription: 'AuthSdkResourceManager' + KeyVaultName: 'msidlabs' + SecretsFilter: 'LabAuth' + RunAsPreJob: false + +- bash: | + set -euo pipefail + CERT_PATH="$(Build.SourcesDirectory)/lab-auth.pfx" + printf '%s' "$(LabAuth)" | 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: 'Write lab certificate to disk' diff --git a/.Pipelines/template-publish-package.yml b/.Pipelines/template-publish-package.yml new file mode 100644 index 00000000..f2cf3f56 --- /dev/null +++ b/.Pipelines/template-publish-package.yml @@ -0,0 +1,68 @@ +# template-publish-package.yml +# +# Reusable step template: authenticate with a PyPI-compatible registry via +# TwineAuthenticate and upload the pre-built distributions from the pipeline +# artifact produced by template-build-package.yml. +# +# Parameters: +# serviceConnectionName - Name of the ADO "Python package upload" service connection +# repositoryName - Value passed to twine -r (must match the service connection name) +# artifactName - Pipeline artifact name containing the dist files (default: 'python-dist') +# pythonVersion - Python version used to run twine (default: '3.12') +# skipExisting - Pass --skip-existing to twine upload (default: false) +# +# Usage inside a deployment job's runOnce.deploy.steps: +# +# - template: .Pipelines/template-publish-package.yml +# parameters: +# serviceConnectionName: MSAL-Test-Python-Upload +# repositoryName: MSAL-Test-Python-Upload +# skipExisting: true + +parameters: +- name: serviceConnectionName + type: string +- name: repositoryName + type: string +- name: artifactName + type: string + default: 'python-dist' +- name: pythonVersion + type: string + default: '3.12' +- name: skipExisting + type: boolean + default: false + +steps: +- task: UsePythonVersion@0 + inputs: + versionSpec: ${{ parameters.pythonVersion }} + displayName: 'Use Python ${{ parameters.pythonVersion }}' + +- script: pip install twine + displayName: 'Install twine' + +- task: TwineAuthenticate@1 + displayName: 'Authenticate with ${{ parameters.repositoryName }}' + inputs: + pythonUploadServiceConnection: ${{ parameters.serviceConnectionName }} + +# Compile-time conditional: ${{ if }} / ${{ else }} so the correct twine variant +# is baked in at queue time; no shell conditionals needed at runtime. +- ${{ if eq(parameters.skipExisting, true) }}: + - script: | + python -m twine upload \ + -r "${{ parameters.repositoryName }}" \ + --config-file $(PYPIRC_PATH) \ + --skip-existing \ + $(Pipeline.Workspace)/${{ parameters.artifactName }}/* + displayName: 'Upload to ${{ parameters.repositoryName }} (skip existing)' + +- ${{ else }}: + - script: | + python -m twine upload \ + -r "${{ parameters.repositoryName }}" \ + --config-file $(PYPIRC_PATH) \ + $(Pipeline.Workspace)/${{ parameters.artifactName }}/* + displayName: 'Upload to ${{ parameters.repositoryName }}' diff --git a/.Pipelines/template-run-tests.yml b/.Pipelines/template-run-tests.yml new file mode 100644 index 00000000..47f5da78 --- /dev/null +++ b/.Pipelines/template-run-tests.yml @@ -0,0 +1,53 @@ +# template-run-tests.yml +# +# Reusable step template: install dependencies and run pytest. +# The caller job is expected to set a 'python.version' matrix variable +# (or any runtime variable) that UsePythonVersion will resolve. +# +# Usage (from a job that has a matrix strategy): +# +# steps: +# - template: .Pipelines/template-run-tests.yml + +steps: +# Retrieve the MSID Lab certificate from Key Vault (via AuthSdkResourceManager SC). +# The cert is written to disk and LAB_APP_CLIENT_CERT_PFX_PATH is set as a variable. +# This is kept here for when e2e tests are fully enabled on a lab-capable agent pool. +- template: template-install-lab-cert.yml + +- task: UsePythonVersion@0 + inputs: + versionSpec: '$(python.version)' + displayName: 'Set up Python' + +- script: | + python -m pip install --upgrade pip + pip install -r requirements.txt + displayName: 'Install dependencies' + +# Use bash: (not script:) so set -o pipefail works — script: uses /bin/sh on Linux +# which does not support pipefail; without it, tee always exits 0 masking test failures. +- bash: | + pip install pytest pytest-azurepipelines + mkdir -p test-results + set -o pipefail + pytest -vv --junitxml=test-results/junit.xml 2>&1 | tee test-results/pytest.log + displayName: 'Run tests' + env: + # LAB_APP_CLIENT_ID is intentionally omitted here to match the behaviour of + # the existing PR gate build (pipeline 2708 / azure-pipelines.yml). + # Without it, _get_credential() in lab_config.py raises EnvironmentError and + # all e2e tests skip or error gracefully — identical to the PR build result. + # Uncomment and set this variable in the pipeline to enable full e2e test runs + # on a lab-capable agent pool (requires CA-exempt network / internal agent). + # LAB_APP_CLIENT_ID: $(LAB_APP_CLIENT_ID) + LAB_APP_CLIENT_CERT_PFX_PATH: $(LAB_APP_CLIENT_CERT_PFX_PATH) + +- task: PublishTestResults@2 + displayName: 'Publish test results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'test-results/junit.xml' + failTaskOnFailedTests: true + testRunTitle: 'Python $(python.version)' diff --git a/msal/sku.py b/msal/sku.py index 01751048..8b30cc9c 100644 --- a/msal/sku.py +++ b/msal/sku.py @@ -2,5 +2,5 @@ """ # The __init__.py will import this. Not the other way around. -__version__ = "1.35.0" +__version__ = "1.35.2rc1" SKU = "MSAL.Python"