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"