Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
18e024d
Add ADO publish pipeline and setup guide
RyAuld Mar 19, 2026
71abd77
Clarify publishTarget labels: test vs production
RyAuld Mar 19, 2026
360532b
Bump version to 1.35.2rc1
RyAuld Mar 19, 2026
302e9de
Friendly display names and fix sku.py version read
RyAuld Mar 19, 2026
90fcfd0
Fix sku.py version read: use explicit exec namespace
RyAuld Mar 19, 2026
8771d61
Fix version read: use grep/sed instead of Python exec
RyAuld Mar 19, 2026
48d84f8
Fix install deps, rename display names, skip -e in requirements
RyAuld Mar 19, 2026
b9f2f59
Update setup guide with post-setup requirements and fixes; fix test i…
RyAuld Mar 19, 2026
c5a2192
Fix editable install: add --no-build-isolation; add lab cert retrieva…
RyAuld Mar 19, 2026
4b3fbd2
Replace editable pip install with PYTHONPATH injection to avoid setup…
RyAuld Mar 19, 2026
bfb65a0
Fix sku.py syntax error: remove stray 'and' prefix on docstring
RyAuld Mar 19, 2026
d03e0f7
Keep cert retrieval infra; comment out LAB_APP_CLIENT_ID to mirror PR…
RyAuld Mar 20, 2026
3b78d35
Add Python 3.14 to publish pipeline CI matrix to match azure-pipeline…
RyAuld Mar 23, 2026
a55942d
Update .Pipelines/pipeline-publish.yml
RyAuld Mar 23, 2026
af6ec92
Fix docs and comments: align parameter values, Python version range, …
RyAuld Mar 23, 2026
7ec5fb3
Use python -m pip for twine install; fix typo in sku.py docstring
RyAuld Mar 23, 2026
d342947
Consolidate 4 step templates into shared template-pipeline-stages.yml…
RyAuld Mar 23, 2026
e171362
Add PreBuildCheck stage (PoliCheck + CredScan) to shared template, mi…
RyAuld Mar 23, 2026
5460165
Add CredScan suppression for test fixtures; wire suppressionsFile int…
RyAuld Mar 23, 2026
364eb7d
Fix PoliCheck InvalidArgumentsError (remove missing exclusion file pa…
RyAuld Mar 23, 2026
7bb5ecc
Address Copilot review comments: Node LTS, cert gate+cleanup, pipefai…
RyAuld Mar 23, 2026
d5ad7c0
Add ESRP setup steps for PyPI publishing (portal walkthrough + pipeli…
RyAuld Mar 26, 2026
08ba113
Enable e2e tests in CI pipeline + fix interactive-test skip logic for…
RyAuld Mar 26, 2026
66a42f1
Fix NodeTool versionSpec: lts/* unsupported on Windows agents, use 20.x
RyAuld Mar 26, 2026
c8662ac
Fix e2e test skip logic for undefined ADO pipeline variables
RyAuld Mar 26, 2026
ef7a76e
Add LAB_APP_TENANT_ID support; wire up RequestMSIDLAB client ID for ADO
RyAuld Mar 26, 2026
ae4b5af
Mimic MSAL.js: hardcode RequestMSIDLAB client ID in CI stage variables
RyAuld Mar 26, 2026
8e8feab
MSAL.NET pattern: hardcode LAB_APP_CLIENT_ID in source, remove from YAML
RyAuld Mar 26, 2026
91b7c2b
Re-enable test_adfs2022_fed_user - ADFS labs are back up
RyAuld Mar 27, 2026
d637dc5
Use shared template in PR/CI build
RyAuld Mar 27, 2026
e3b627d
20260327.1 remove refrances to release as we plan to move that to the…
RyAuld Mar 30, 2026
60d4080
Merge fix-e2e-tests into PyPI-ADO-PackagePublish
RyAuld Mar 30, 2026
5254163
Restore two publish targets: Preview (test.pypi.org) and ESRP Product…
RyAuld Mar 30, 2026
3bfad4f
20260330.2 Move Build/Publish stages to pipeline-publish.yml to avoid…
RyAuld Mar 30, 2026
2dd89f5
Switch ESRP Production publish stage to EsrpRelease@9 on windows-latest
RyAuld Mar 30, 2026
14789e4
Comment out TwineAuthenticate step pending MSAL-Test-Python-Upload SC…
RyAuld Mar 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
355 changes: 355 additions & 0 deletions .Pipelines/ADO-PUBLISH-SETUP.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions .Pipelines/credscan-exclusion.json
Original file line number Diff line number Diff line change
@@ -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",
Comment on lines +5 to +9
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These suppression entries won’t match the actual repo paths: the files live under tests/ (tests/certificate-with-password.pfx and tests/test_mi.py). Update the "file" values to the correct relative paths so CredScan suppression works as intended.

Suggested change
"file": "certificate-with-password.pfx",
"_justification": "Self-signed certificate used only in unit tests. Not a production credential."
},
{
"file": "test_mi.py",
"file": "tests/certificate-with-password.pfx",
"_justification": "Self-signed certificate used only in unit tests. Not a production credential."
},
{
"file": "tests/test_mi.py",

Copilot uses AI. Check for mistakes.
"_justification": "WWW-Authenticate challenge header value used as a mock HTTP response fixture in unit tests. Not a real credential."
}
]
}
179 changes: 179 additions & 0 deletions .Pipelines/pipeline-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# pipeline-publish.yml
#
# Release pipeline for the msal Python package — manually triggered only.
# Source: https://github.com/AzureAD/microsoft-authentication-library-for-python
#
# Publish targets:
# test.pypi.org (Preview / RC) — preview releases via MSAL-Test-Python-Upload SC
# (SC creation pending test.pypi.org API token)
# pypi.org (ESRP Production) — production releases via MSAL-Prod-Python-Upload SC
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header comment says production publishes use the MSAL-Prod-Python-Upload Twine service connection, but the production stage actually uses EsrpRelease@9 with the MSAL-ESRP-AME connection. Please update the comment to avoid confusing release operators.

Suggested change
# pypi.org (ESRP Production) — production releases via MSAL-Prod-Python-Upload SC
# pypi.org (ESRP Production) — production releases via ESRP (EsrpRelease@9) using MSAL-ESRP-AME SC

Copilot uses AI. Check for mistakes.
#
# 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 (ESRP Production)'

trigger: none # manual runs only — no automatic branch or tag triggers
pr: none

# Stage flow:
#
# PreBuildCheck ─► Validate ─► CI ─► Build ─► PublishMSALPython (publishTarget == Preview)
# └─► PublishPyPI (publishTarget == ESRP Production)

stages:

# PreBuildCheck, Validate, and CI stages are defined in the shared template.
- template: template-pipeline-stages.yml
parameters:
packageVersion: ${{ parameters.packageVersion }}
runPublish: true

# ══════════════════════════════════════════════════════════════════════════════
# Stage 3 · Build — build sdist + wheel
# ══════════════════════════════════════════════════════════════════════════════
- stage: Build
displayName: 'Build package'
dependsOn: CI
condition: eq(dependencies.CI.result, 'Succeeded')
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)
# Note: requires MSAL-Test-Python-Upload SC in ADO (pending test.pypi.org API token)
# ══════════════════════════════════════════════════════════════════════════════
- 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
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 twine
displayName: 'Install twine'

# TODO: create MSAL-Test-Python-Upload SC with test.pypi.org API token, then uncomment:
# - 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 test.pypi.org'

- script: echo "Publish to test.pypi.org skipped — MSAL-Test-Python-Upload SC not yet created."
displayName: 'Skip upload (SC pending)'

# ══════════════════════════════════════════════════════════════════════════════
# Stage 4b · Publish to PyPI (ESRP Production)
# Uses EsrpRelease@9 via the MSAL-ESRP-AME service connection.
# IMPORTANT: configure a required manual approval on this environment in
# ADO → Pipelines → Environments → MSAL-Python-Release → Approvals and checks.
# IMPORTANT: EsrpRelease@9 requires a Windows agent.
# ══════════════════════════════════════════════════════════════════════════════
- stage: PublishPyPI
displayName: 'Publish to PyPI (ESRP Production)'
dependsOn: Build
condition: >
and(
eq(dependencies.Build.result, 'Succeeded'),
eq('${{ parameters.publishTarget }}', 'pypi.org (ESRP Production)')
)
jobs:
- deployment: DeployPyPI
displayName: 'Upload to PyPI via ESRP'
pool:
vmImage: windows-latest
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: EsrpRelease@9
displayName: 'Publish to PyPI via ESRP'
inputs:
connectedservicename: 'MSAL-ESRP-AME'
usemanagedidentity: true
keyvaultname: 'MSALVault'
signcertname: 'MSAL-ESRP-Release-Signing'
clientid: '8650ce2b-38d4-466a-9144-bc5c19c88112'
intent: 'PackageDistribution'
contenttype: 'PyPi'
contentsource: 'Folder'
folderlocation: '$(Pipeline.Workspace)/python-dist'
waitforreleasecompletion: true
owners: 'ryauld@microsoft.com;avdunn@microsoft.com'
approvers: 'avdunn@microsoft.com;bogavril@microsoft.com'
serviceendpointurl: 'https://api.esrp.microsoft.com'
mainpublisher: 'ESRPRELPACMAN'
domaintenantid: '33e01921-4d64-4f8c-a055-5bdaffd5e33d'
192 changes: 192 additions & 0 deletions .Pipelines/template-pipeline-stages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# template-pipeline-stages.yml
#
# Shared stages template for the msal Python package.
#
# Called from:
# pipeline-publish.yml — release build (runPublish: true)
# azure-pipelines.yml — PR gate and post-merge CI (runPublish: false)
#
# Parameters:
# packageVersion - Version to validate against msal/sku.py
# Required when runPublish is true; unused otherwise.
# runPublish - When true: also runs the Validate stage before CI.
# When false (PR / merge builds): only PreBuildCheck + CI run.
#
# Stage flow:
#
# runPublish: true → PreBuildCheck ─► Validate ─► CI
# runPublish: false → PreBuildCheck ─► CI (Validate is skipped)
#
# Build and Publish stages are defined in pipeline-publish.yml (not here),
# so that the PR build never references PyPI service connections.

parameters:
- name: packageVersion
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: '20.x'

- 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).
# Matches the pattern used by MSAL.js (install-keyvault-secrets.yml) and MSAL Java.
- task: AzureKeyVault@2
displayName: 'Retrieve lab certificate from Key Vault'
inputs:
azureSubscription: 'AuthSdkResourceManager'
KeyVaultName: 'msidlabs'
SecretsFilter: 'LabAuth'
RunAsPreJob: false
Comment on lines +143 to +149
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Key Vault retrieval + cert-writing steps assume the service connection and LabAuth secret are always available. If the AzureKeyVault task is skipped/fails (common in untrusted PRs or when the service connection isn't authorized), the following bash step will still run and fail. Consider adding conditions to both steps (e.g., skip when System.PullRequest.IsFork is true) and/or gate the cert-writing step on variables['LabAuth'] being populated.

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +149
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CI stage always attempts to fetch the LabAuth secret via the AuthSdkResourceManager service connection. This will fail (and potentially expose secrets) on forked PRs or in projects where that service connection isn’t authorized. Consider adding a condition/parameter to skip the AzureKeyVault step for PR validation (e.g., forks) and let e2e tests self-skip when LAB_APP_CLIENT_CERT_PFX_PATH isn’t available.

Copilot uses AI. Check for mistakes.

- 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'
Comment on lines +154 to +157
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this bash step, using "$(LabAuth)" is unsafe: if the pipeline variable is undefined, Azure Pipelines leaves it as the literal '$(LabAuth)', which Bash treats as command substitution and the step will fail (e.g., LabAuth: command not found). Pass the secret via env: and reference it as $LABAUTH (or similar), and add a clear error if it's empty/unset before running base64 decode.

Suggested change
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'
if [ -z "${LABAUTH:-}" ] || [ "$LABAUTH" = '$(LabAuth)' ]; then
echo "Error: LabAuth secret is not set or is invalid. Ensure the LabAuth secret is defined in Azure Pipelines/Key Vault."
exit 1
fi
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'
env:
LABAUTH: $(LabAuth)

Copilot uses AI. Check for mistakes.

- 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_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: always()
Loading
Loading