Skip to content

Commit 639917f

Browse files
Gladwin JohnsonGladwin Johnson
authored andcommitted
MSI v2: mTLS Proof-of-Possession with KeyGuard Attestation
1 parent e4e692c commit 639917f

22 files changed

+3587
-92
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# CI/CD Pipelines
2+
3+
This document describes the pipeline structure for the `msal` Python package,
4+
including what each pipeline does, when it runs, and how to trigger a release.
5+
6+
---
7+
8+
## Pipeline Files
9+
10+
| File | Purpose |
11+
|------|---------|
12+
| [`azure-pipelines.yml`](../azure-pipelines.yml) | PR gate and post-merge CI — calls the shared template with `runPublish: false` |
13+
| [`pipeline-publish.yml`](pipeline-publish.yml) | Release pipeline — manually queued, builds and publishes to PyPI |
14+
| [`template-pipeline-stages.yml`](template-pipeline-stages.yml) | Shared stages template — PreBuildCheck, Validate, and CI stages reused by both pipelines |
15+
| [`credscan-exclusion.json`](credscan-exclusion.json) | CredScan suppression file for known test fixtures |
16+
17+
---
18+
19+
## PR / CI Pipeline (`azure-pipelines.yml`)
20+
21+
### Triggers
22+
23+
| Event | Branches |
24+
|-------|----------|
25+
| Pull request opened / updated | all branches |
26+
| Push / merge | `dev`, `azure-pipelines` |
27+
| Scheduled | Daily at 11:45 PM Pacific, `dev` branch (only when there are new changes) |
28+
29+
### Stages
30+
31+
```
32+
PreBuildCheck ─► CI
33+
```
34+
35+
| Stage | What it does |
36+
|-------|-------------|
37+
| **PreBuildCheck** | Runs SDL security scans: PoliCheck (policy/offensive content), CredScan (leaked credentials), and PostAnalysis (breaks the build on findings) |
38+
| **CI** | Runs the full test suite on Python 3.9, 3.10, 3.11, 3.12, 3.13, and 3.14 |
39+
40+
The Validate stage is **skipped** on PR/CI runs (it only applies to release builds).
41+
42+
> **SDL coverage:** The PreBuildCheck stage satisfies the OneBranch SDL requirement.
43+
> It runs on every PR, every merge to `dev`, and on the daily schedule — ensuring
44+
> continuous security scanning without a separate dedicated SDL pipeline.
45+
46+
---
47+
48+
## Release Pipeline (`pipeline-publish.yml`)
49+
50+
### Triggers
51+
52+
**Manual only** — no automatic branch or tag triggers. Must be queued explicitly
53+
with both parameters filled in.
54+
55+
### Parameters
56+
57+
| Parameter | Description | Example values |
58+
|-----------|-------------|----------------|
59+
| **Package version to publish** | Must exactly match `msal/sku.py __version__`. [PEP 440](https://peps.python.org/pep-0440/) format. | `1.36.0`, `1.36.0rc1`, `1.36.0b1` |
60+
| **Publish target** | Destination for this release. | `test.pypi.org (Preview / RC)` or `pypi.org (ESRP Production)` |
61+
62+
### Stage Flow
63+
64+
```
65+
PreBuildCheck ─► Validate ─► CI ─► Build ─┬─► PublishMSALPython (publishTarget == 'test.pypi.org (Preview / RC)')
66+
└─► PublishPyPI (publishTarget == 'pypi.org (ESRP Production)')
67+
```
68+
69+
| Stage | What it does | Condition |
70+
|-------|-------------|-----------|
71+
| **PreBuildCheck** | PoliCheck + CredScan scans | Always |
72+
| **Validate** | Asserts the `packageVersion` parameter matches `msal/sku.py __version__` | Always (release runs only) |
73+
| **CI** | Full test matrix (Python 3.9–3.14) | After Validate passes |
74+
| **Build** | Builds `sdist` and `wheel` via `python -m build`; publishes `python-dist` artifact | After CI passes |
75+
| **PublishMSALPython** | Uploads to test.pypi.org | `publishTarget == test.pypi.org (Preview / RC)` |
76+
| **PublishPyPI** | Uploads to PyPI via ESRP; requires manual approval | `publishTarget == pypi.org (ESRP Production)` |
77+
78+
---
79+
80+
## How to Publish a Release
81+
82+
### Step 1 — Update the version
83+
84+
Edit `msal/sku.py` and set `__version__` to the target version:
85+
86+
```python
87+
__version__ = "1.36.0rc1" # RC / preview
88+
__version__ = "1.36.0" # production release
89+
```
90+
91+
Push the change to the branch you intend to release from.
92+
93+
### Step 2 — Queue the pipeline
94+
95+
1. Go to the **MSAL.Python-Publish** pipeline in ADO.
96+
2. Click **Run pipeline**.
97+
3. Select the branch to release from.
98+
4. Enter the **Package version to publish** (must match `msal/sku.py` exactly).
99+
5. Select the **Publish target**:
100+
- `test.pypi.org (Preview / RC)` — for release candidates and previews
101+
- `pypi.org (ESRP Production)` — for final releases (requires approval gate)
102+
6. Click **Run**.
103+
104+
### Step 3 — Approve (production releases only)
105+
106+
The `pypi.org (ESRP Production)` path includes a required manual approval before
107+
the package is uploaded. An approver must review and approve in the ADO
108+
**Environments** panel before the `PublishPyPI` stage proceeds.
109+
110+
### Step 4 — Verify
111+
112+
- **test.pypi.org:** https://test.pypi.org/project/msal/
113+
- **PyPI:** https://pypi.org/project/msal/
114+
115+
---
116+
117+
## Version Format
118+
119+
PyPI enforces [PEP 440](https://peps.python.org/pep-0440/). Versions with `-` (e.g. `1.36.0-Preview`) are rejected at upload time. Use standard suffixes:
120+
121+
| Release type | Format |
122+
|-------------|--------|
123+
| Production | `1.36.0` |
124+
| Release candidate | `1.36.0rc1` |
125+
| Beta | `1.36.0b1` |
126+
| Alpha | `1.36.0a1` |

.Pipelines/credscan-exclusion.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"tool": "Credential Scanner",
3+
"suppressions": [
4+
{
5+
"file": "tests/certificate-with-password.pfx",
6+
"_justification": "Self-signed certificate used only in unit tests. Not a production credential."
7+
},
8+
{
9+
"file": "tests/test_mi.py",
10+
"_justification": "WWW-Authenticate challenge header value used as a mock HTTP response fixture in unit tests. Not a real credential."
11+
}
12+
]
13+
}

.Pipelines/pipeline-publish.yml

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# pipeline-publish.yml
2+
#
3+
# Release pipeline for the msal Python package — manually triggered only.
4+
# Source: https://github.com/AzureAD/microsoft-authentication-library-for-python
5+
#
6+
# Publish targets:
7+
# test.pypi.org (Preview / RC) — preview releases via MSAL-Test-Python-Upload SC
8+
# (SC creation pending test.pypi.org API token)
9+
# pypi.org (ESRP Production) — production releases via ESRP (EsrpRelease@9) using MSAL-ESRP-AME SC
10+
#
11+
# For pipeline documentation, see .Pipelines/CI-AND-RELEASE-PIPELINES.md.
12+
13+
parameters:
14+
- name: packageVersion
15+
displayName: 'Package version to publish (must match msal/sku.py, e.g. 1.36.0 or 1.36.0rc1)'
16+
type: string
17+
18+
- name: publishTarget
19+
displayName: 'Publish target'
20+
type: string
21+
values:
22+
- 'test.pypi.org (Preview / RC)'
23+
- 'pypi.org (ESRP Production)'
24+
25+
trigger: none # manual runs only — no automatic branch or tag triggers
26+
pr: none
27+
28+
# Stage flow:
29+
#
30+
# PreBuildCheck ─► Validate ─► CI ─► Build ─► PublishMSALPython (publishTarget == Preview)
31+
# └─► PublishPyPI (publishTarget == ESRP Production)
32+
33+
stages:
34+
35+
# PreBuildCheck, Validate, and CI stages are defined in the shared template.
36+
- template: template-pipeline-stages.yml
37+
parameters:
38+
packageVersion: ${{ parameters.packageVersion }}
39+
runPublish: true
40+
41+
# ══════════════════════════════════════════════════════════════════════════════
42+
# Stage 3 · Build — build sdist + wheel
43+
# ══════════════════════════════════════════════════════════════════════════════
44+
- stage: Build
45+
displayName: 'Build package'
46+
dependsOn: CI
47+
condition: eq(dependencies.CI.result, 'Succeeded')
48+
jobs:
49+
- job: BuildDist
50+
displayName: 'Build sdist + wheel (Python 3.12)'
51+
pool:
52+
vmImage: ubuntu-latest
53+
steps:
54+
- task: UsePythonVersion@0
55+
inputs:
56+
versionSpec: '3.12'
57+
displayName: 'Use Python 3.12'
58+
59+
- script: |
60+
python -m pip install --upgrade pip build twine
61+
displayName: 'Install build toolchain'
62+
63+
- script: |
64+
python -m build
65+
displayName: 'Build sdist and wheel'
66+
67+
- script: |
68+
python -m twine check dist/*
69+
displayName: 'Verify distribution (twine check)'
70+
71+
- task: PublishPipelineArtifact@1
72+
displayName: 'Publish dist/ as pipeline artifact'
73+
inputs:
74+
targetPath: dist/
75+
artifact: python-dist
76+
77+
# ══════════════════════════════════════════════════════════════════════════════
78+
# Stage 4a · Publish to test.pypi.org (Preview / RC)
79+
# Note: requires MSAL-Test-Python-Upload SC in ADO (pending test.pypi.org API token)
80+
# ══════════════════════════════════════════════════════════════════════════════
81+
- stage: PublishMSALPython
82+
displayName: 'Publish to test.pypi.org (Preview)'
83+
dependsOn: Build
84+
condition: >
85+
and(
86+
eq(dependencies.Build.result, 'Succeeded'),
87+
eq('${{ parameters.publishTarget }}', 'test.pypi.org (Preview / RC)')
88+
)
89+
jobs:
90+
- deployment: DeployMSALPython
91+
displayName: 'Upload to test.pypi.org'
92+
pool:
93+
vmImage: ubuntu-latest
94+
environment: MSAL-Python
95+
strategy:
96+
runOnce:
97+
deploy:
98+
steps:
99+
- task: DownloadPipelineArtifact@2
100+
displayName: 'Download python-dist artifact'
101+
inputs:
102+
artifactName: python-dist
103+
targetPath: $(Pipeline.Workspace)/python-dist
104+
105+
- task: UsePythonVersion@0
106+
inputs:
107+
versionSpec: '3.12'
108+
displayName: 'Use Python 3.12'
109+
110+
- script: |
111+
python -m pip install --upgrade pip twine
112+
displayName: 'Install twine'
113+
114+
# TODO: create MSAL-Test-Python-Upload SC with test.pypi.org API token, then uncomment:
115+
# - task: TwineAuthenticate@1
116+
# displayName: 'Authenticate with MSAL-Test-Python-Upload'
117+
# inputs:
118+
# pythonUploadServiceConnection: MSAL-Test-Python-Upload
119+
120+
# - script: |
121+
# python -m twine upload \
122+
# -r "MSAL-Test-Python-Upload" \
123+
# --config-file $(PYPIRC_PATH) \
124+
# --skip-existing \
125+
# $(Pipeline.Workspace)/python-dist/*
126+
# displayName: 'Upload to test.pypi.org'
127+
128+
- script: echo "Publish to test.pypi.org skipped — MSAL-Test-Python-Upload SC not yet created."
129+
displayName: 'Skip upload (SC pending)'
130+
131+
# ══════════════════════════════════════════════════════════════════════════════
132+
# Stage 4b · Publish to PyPI (ESRP Production)
133+
# Uses EsrpRelease@9 via the MSAL-ESRP-AME service connection.
134+
# IMPORTANT: configure a required manual approval on this environment in
135+
# ADO → Pipelines → Environments → MSAL-Python-Release → Approvals and checks.
136+
# IMPORTANT: EsrpRelease@9 requires a Windows agent.
137+
# ══════════════════════════════════════════════════════════════════════════════
138+
- stage: PublishPyPI
139+
displayName: 'Publish to PyPI (ESRP Production)'
140+
dependsOn: Build
141+
condition: >
142+
and(
143+
eq(dependencies.Build.result, 'Succeeded'),
144+
eq('${{ parameters.publishTarget }}', 'pypi.org (ESRP Production)')
145+
)
146+
jobs:
147+
- deployment: DeployPyPI
148+
displayName: 'Upload to PyPI via ESRP'
149+
pool:
150+
vmImage: windows-latest
151+
environment: MSAL-Python-Release
152+
strategy:
153+
runOnce:
154+
deploy:
155+
steps:
156+
- task: DownloadPipelineArtifact@2
157+
displayName: 'Download python-dist artifact'
158+
inputs:
159+
artifactName: python-dist
160+
targetPath: $(Pipeline.Workspace)/python-dist
161+
162+
- task: EsrpRelease@9
163+
displayName: 'Publish to PyPI via ESRP'
164+
inputs:
165+
connectedservicename: 'MSAL-ESRP-AME'
166+
usemanagedidentity: true
167+
keyvaultname: 'MSALVault'
168+
signcertname: 'MSAL-ESRP-Release-Signing'
169+
clientid: '8650ce2b-38d4-466a-9144-bc5c19c88112'
170+
intent: 'PackageDistribution'
171+
contenttype: 'PyPi'
172+
contentsource: 'Folder'
173+
folderlocation: '$(Pipeline.Workspace)/python-dist'
174+
waitforreleasecompletion: true
175+
owners: 'ryauld@microsoft.com,avdunn@microsoft.com'
176+
approvers: 'avdunn@microsoft.com,bogavril@microsoft.com'
177+
serviceendpointurl: 'https://api.esrp.microsoft.com'
178+
mainpublisher: 'ESRPRELPACMAN'
179+
domaintenantid: '33e01921-4d64-4f8c-a055-5bdaffd5e33d'

0 commit comments

Comments
 (0)