diff --git a/.Pipelines/ADO-PUBLISH-SETUP.md b/.Pipelines/ADO-PUBLISH-SETUP.md
new file mode 100644
index 00000000..adc133a4
--- /dev/null
+++ b/.Pipelines/ADO-PUBLISH-SETUP.md
@@ -0,0 +1,243 @@
+# 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) | Thin top-level wrapper — triggers, parameters, calls `template-pipeline-stages.yml` with `runPublish: true` |
+| [`template-pipeline-stages.yml`](template-pipeline-stages.yml) | Shared stages template — Validate, CI, Build, Publish stages; reusable by PR-gate and post-merge CI pipelines |
+| [`credscan-exclusion.json`](credscan-exclusion.json) | CredScan suppression file — suppresses known false positives for test fixture files (`certificate-with-password.pfx`, `test_mi.py`) |
+
+---
+
+## 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 |
+|-------|---------|--------|
+| **PreBuildCheck** (PoliCheck + CredScan) | always | SDL security scans |
+| **Validate** | release runs only (`runPublish: true`) | asserts `packageVersion` matches `msal/sku.py` |
+| **CI** (tests on Py 3.9–3.14) | after Validate (or immediately on PR/merge runs) | — |
+| **Build** (sdist + wheel) | after CI, release runs only | 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** |
+| [Secure Development Tools](https://marketplace.visualstudio.com/items?itemName=securedevelopmentteam.vss-secure-development-tools) extension | Must be installed in the ADO organization — required for the PreBuildCheck stage (PoliCheck, CredScan, PostAnalysis tasks) |
+| 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 |
+| `AuthSdkResourceManager` Azure service connection *(optional)* | Required only if `LAB_APP_CLIENT_ID` is set to enable e2e tests. ARM service connection with **Get** access to the `LabAuth` secret in the `msidlabs` Key Vault. When not set, the Key Vault steps are automatically skipped. |
+
+---
+
+## 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 = test.pypi.org (Preview / RC))
+ └─► PreBuildCheck ─► Validate ─► CI ─► Build ─► PublishMSALPython
+ (test.pypi.org (Preview / RC), auto)
+
+Manual queue (publishTarget = pypi.org (Production))
+ └─► PreBuildCheck ─► Validate ─► CI ─► Build ─► PublishPyPI
+ (pypi.org (Production), requires approval)
+
+PR / merge build (runPublish: false)
+ └─► PreBuildCheck ─► CI
+```
+
+---
+
+## 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 .` (editable local install of `msal`). `azure-identity` does not depend on `msal`, so no PyPI version is pulled in as a transitive dependency and the local package is not overwritten. The template installs dependencies with `pip install -r requirements.txt`, which installs the editable local copy directly.
+- 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 | Missing or broken dependency | Run `pip install -r requirements.txt` locally and confirm `msal` resolves to the local editable install (check `pip show msal`) |
+| `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/credscan-exclusion.json b/.Pipelines/credscan-exclusion.json
new file mode 100644
index 00000000..598dbc62
--- /dev/null
+++ b/.Pipelines/credscan-exclusion.json
@@ -0,0 +1,13 @@
+{
+ "tool": "Credential Scanner",
+ "suppressions": [
+ {
+ "file": "certificate-with-password.pfx",
+ "_justification": "Self-signed certificate used only in unit tests. Not a production credential."
+ },
+ {
+ "file": "test_mi.py",
+ "_justification": "WWW-Authenticate challenge header value used as a mock HTTP response fixture in unit tests. Not a real credential."
+ }
+ ]
+}
diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml
new file mode 100644
index 00000000..a55ab2a2
--- /dev/null
+++ b/.Pipelines/pipeline-publish.yml
@@ -0,0 +1,30 @@
+# pipeline-publish.yml
+#
+# Release pipeline for the msal Python package — manually triggered only.
+# Source: https://github.com/AzureAD/microsoft-authentication-library-for-python
+#
+# Delegates all stages to template-pipeline-stages.yml, which is shared with
+# the (future) PR gate and post-merge CI pipelines.
+# For one-time ADO setup, see ADO-PUBLISH-SETUP.md.
+
+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)'
+ - 'pypi.org (Production)'
+
+trigger: none # manual runs only — no automatic branch or tag triggers
+pr: none
+
+stages:
+- template: template-pipeline-stages.yml
+ parameters:
+ packageVersion: ${{ parameters.packageVersion }}
+ publishTarget: ${{ parameters.publishTarget }}
+ runPublish: true
diff --git a/.Pipelines/template-pipeline-stages.yml b/.Pipelines/template-pipeline-stages.yml
new file mode 100644
index 00000000..8a52314b
--- /dev/null
+++ b/.Pipelines/template-pipeline-stages.yml
@@ -0,0 +1,346 @@
+# template-pipeline-stages.yml
+#
+# Unified pipeline stages template for the msal Python package.
+#
+# Called from:
+# pipeline-publish.yml — release build (runPublish: true)
+# (future) pipeline-ci.yml — post-merge CI (runPublish: false)
+# (future) pipeline-pullrequest.yml — PR gate (runPublish: false)
+#
+# Parameters:
+# packageVersion - Version to validate against msal/sku.py
+# Required when runPublish is true; unused otherwise.
+# publishTarget - 'test.pypi.org (Preview / RC)' or 'pypi.org (Production)'
+# Required when runPublish is true; unused otherwise.
+# runPublish - When true: also run Validate, Build, and Publish stages.
+# When false (PR / merge builds): only the CI stage runs.
+#
+# Stage flow:
+#
+# runPublish: true → PreBuildCheck ─► Validate ─► CI ─► Build ─► PublishMSALPython
+# └─► PublishPyPI
+# runPublish: false → PreBuildCheck ─► CI (Validate / Build / Publish are skipped)
+
+parameters:
+- name: packageVersion
+ type: string
+ default: ''
+- name: publishTarget
+ type: string
+ default: ''
+- name: runPublish
+ type: boolean
+ default: false
+
+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: 'lts/*'
+
+ - 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
+
+# ══════════════════════════════════════════════════════════════════════════════
+# 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'))
+ 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 across all supported Python versions.
+# Always runs. Waits for Validate when runPublish is true;
+# runs immediately when Validate is skipped (PR / merge builds).
+# ══════════════════════════════════════════════════════════════════════════════
+- stage: CI
+ displayName: 'Run tests'
+ dependsOn:
+ - PreBuildCheck
+ - Validate
+ condition: |
+ and(
+ eq(dependencies.PreBuildCheck.result, 'Succeeded'),
+ in(dependencies.Validate.result, 'Succeeded', 'Skipped')
+ )
+ 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'
+ Python314:
+ python.version: '3.14'
+ steps:
+ # Retrieve the MSID Lab certificate from Key Vault (via AuthSdkResourceManager SC).
+ # Gated on LAB_APP_CLIENT_ID being non-empty — if e2e tests are not enabled (the default),
+ # both steps are skipped and the pipeline has no Key Vault dependency.
+ - task: AzureKeyVault@2
+ displayName: 'Retrieve lab certificate from Key Vault'
+ condition: and(succeeded(), ne(variables['LAB_APP_CLIENT_ID'], ''))
+ inputs:
+ azureSubscription: 'AuthSdkResourceManager'
+ KeyVaultName: 'msidlabs'
+ SecretsFilter: 'LabAuth'
+ RunAsPreJob: false
+
+ - bash: |
+ set -euo pipefail
+ CERT_PATH="$(Agent.TempDirectory)/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'
+ condition: and(succeeded(), ne(variables['LAB_APP_CLIENT_ID'], ''))
+
+ - 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: explicitly; set -o pipefail so that pytest failures aren't hidden by the pipe to tee.
+ # Without pipefail, tee exits 0 and the step can succeed even when tests fail.
+ # (set -o pipefail also works in script: steps, but bash: makes the shell choice explicit.)
+ - 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 to match the PR gate build
+ # behaviour (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 to enable full e2e 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)'
+
+ - bash: rm -f "$(Agent.TempDirectory)/lab-auth.pfx"
+ displayName: 'Clean up lab certificate'
+ condition: and(always(), ne(variables['LAB_APP_CLIENT_ID'], ''))
+
+# ══════════════════════════════════════════════════════════════════════════════
+# Stage 3 · Build — build sdist + wheel (release only)
+# ══════════════════════════════════════════════════════════════════════════════
+- stage: Build
+ displayName: 'Build package'
+ dependsOn: CI
+ condition: and(eq(dependencies.CI.result, 'Succeeded'), eq(${{ parameters.runPublish }}, true))
+ 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)
+# Runs when: runPublish is true AND publishTarget == '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:
+ vmImage: ubuntu-latest
+ # Optional: add approval checks in ADO → Pipelines → Environments → MSAL-Python
+ 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
+ python -m pip install twine
+ displayName: 'Install twine'
+
+ - 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 MSAL-Test-Python-Upload (skip existing)'
+
+# ══════════════════════════════════════════════════════════════════════════════
+# Stage 4b · Publish to PyPI (Production)
+# Runs when: runPublish is true AND publishTarget == 'pypi.org (Production)'
+# ══════════════════════════════════════════════════════════════════════════════
+- stage: PublishPyPI
+ displayName: 'Publish to PyPI (Production)'
+ dependsOn: Build
+ condition: >
+ and(
+ eq(dependencies.Build.result, '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:
+ - 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
+ python -m pip install twine
+ displayName: 'Install twine'
+
+ - task: TwineAuthenticate@1
+ displayName: 'Authenticate with MSAL-Prod-Python-Upload'
+ inputs:
+ pythonUploadServiceConnection: MSAL-Prod-Python-Upload
+
+ - script: |
+ python -m twine upload \
+ -r "MSAL-Prod-Python-Upload" \
+ --config-file $(PYPIRC_PATH) \
+ $(Pipeline.Workspace)/python-dist/*
+ displayName: 'Upload to MSAL-Prod-Python-Upload'
diff --git a/msal/sku.py b/msal/sku.py
index 01751048..19ff0138 100644
--- a/msal/sku.py
+++ b/msal/sku.py
@@ -1,6 +1,6 @@
-"""This module is from where we recieve the client sku name and version.
+"""This module is from where we receive the client sku name and version.
"""
# The __init__.py will import this. Not the other way around.
-__version__ = "1.35.0"
+__version__ = "1.35.2rc1"
SKU = "MSAL.Python"