|
| 1 | +# ADO Pipeline Setup Guide — MSAL Python → PyPI |
| 2 | + |
| 3 | +This document describes every step needed to create an Azure DevOps (ADO) |
| 4 | +pipeline that checks out the GitHub repo, runs tests, builds distributions, |
| 5 | +and publishes to test.pypi.org (via the MSAL-Python environment) and PyPI. |
| 6 | + |
| 7 | +The `.Pipelines/` folder follows the same template convention as [MSAL.NET](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/tree/main/build): |
| 8 | + |
| 9 | +| File | Purpose | |
| 10 | +|------|---------| |
| 11 | +| [`pipeline-publish.yml`](pipeline-publish.yml) | Top-level orchestrator — triggers, variables, stage wiring | |
| 12 | +| [`template-run-tests.yml`](template-run-tests.yml) | Reusable step template — pytest across Python version matrix | |
| 13 | +| [`template-build-package.yml`](template-build-package.yml) | Reusable step template — `python -m build` + `twine check` + artifact publish | |
| 14 | +| [`template-publish-package.yml`](template-publish-package.yml) | Reusable step template — `TwineAuthenticate` + `twine upload` (parameterized for MSAL-Python/PyPI) | |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## Overview |
| 19 | + |
| 20 | +This pipeline is **manually triggered only** — no automatic branch or tag triggers. |
| 21 | +Every publish requires explicitly entering a version and selecting a destination. |
| 22 | + |
| 23 | +| Stage | Trigger | Target | |
| 24 | +|-------|---------|--------| |
| 25 | +| **Validate** | always | asserts `packageVersion` matches `msal/sku.py` | |
| 26 | +| **CI** (tests on Py 3.9–3.13) | after Validate | — | |
| 27 | +| **Build** (sdist + wheel) | after CI | dist artifact | |
| 28 | +| **PublishMSALPython** | `publishTarget = MSAL-Python` | test.pypi.org | |
| 29 | +| **PublishPyPI** | `publishTarget = pypi` | PyPI (production) | |
| 30 | + |
| 31 | +--- |
| 32 | + |
| 33 | +## Step 1 — Prerequisites |
| 34 | + |
| 35 | +| Requirement | Notes | |
| 36 | +|-------------|-------| |
| 37 | +| ADO Organization | [Create one](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/create-organization) if you don't have one | |
| 38 | +| ADO Project | Under the org; enable **Pipelines** and **Artifacts** | |
| 39 | +| GitHub account with admin rights | Needed to authorize the ADO GitHub App | |
| 40 | +| PyPI API token | Scoped to the `msal` project — generate at <https://pypi.org/manage/account/token/> | |
| 41 | +| MSAL-Python (test.pypi.org) API token | Scoped to the `msal` project on test.pypi.org | |
| 42 | + |
| 43 | +--- |
| 44 | + |
| 45 | +## Step 2 — Connect ADO to the GitHub Repository |
| 46 | + |
| 47 | +1. In your ADO project go to **Project Settings → Service connections → New service connection**. |
| 48 | +2. Choose **GitHub** and click **Next**. |
| 49 | +3. Select **GitHub App** (recommended) or **Personal Access Token**. |
| 50 | + - GitHub App: follow the OAuth flow to install the ADO GitHub App on the |
| 51 | + `AzureAD` organization and grant repository access to |
| 52 | + `microsoft-authentication-library-for-python`. |
| 53 | + - PAT: create a GitHub PAT with `repo` scope and paste it here. |
| 54 | +4. Set **Service connection name**: `github-msal-python` |
| 55 | +5. Check **Grant access permission to all pipelines**, click **Save**. |
| 56 | + |
| 57 | +--- |
| 58 | + |
| 59 | +## Step 3 — Create PyPI Service Connections (Twine) |
| 60 | + |
| 61 | +The `TwineAuthenticate@1` task uses "Python package upload" service connections |
| 62 | +for external registries. |
| 63 | + |
| 64 | +### 3a — MSAL-Python (test.pypi.org) connection |
| 65 | + |
| 66 | +1. **Project Settings → Service connections → New service connection** |
| 67 | +2. Choose **Python package upload**, click **Next**. |
| 68 | +3. Fill in: |
| 69 | + | Field | Value | |
| 70 | + |-------|-------| |
| 71 | + | **Twine repository URL** | `https://test.pypi.org/legacy/` | |
| 72 | + | **EndpointName** (`-r` value) | `MSAL-Test-Python-Upload` | |
| 73 | + | **Username** | `__token__` | |
| 74 | + | **Password** | *(your test.pypi.org API token, e.g. `pypi-AgA...`)* | |
| 75 | + | **Service connection name** | `MSAL-Test-Python-Upload` | |
| 76 | +4. Check **Grant access permission to all pipelines**, click **Save**. |
| 77 | + |
| 78 | +### 3b — PyPI (production) connection |
| 79 | + |
| 80 | +1. **Project Settings → Service connections → New service connection** |
| 81 | +2. Choose **Python package upload**, click **Next**. |
| 82 | +3. Fill in: |
| 83 | + | Field | Value | |
| 84 | + |-------|-------| |
| 85 | + | **Twine repository URL** | `https://upload.pypi.org/legacy/` | |
| 86 | + | **EndpointName** (`-r` value) | `MSAL-Prod-Python-Upload` | |
| 87 | + | **Username** | `__token__` | |
| 88 | + | **Password** | *(your PyPI API token)* | |
| 89 | + | **Service connection name** | `MSAL-Prod-Python-Upload` | |
| 90 | +4. Check **Grant access permission to all pipelines**, click **Save**. |
| 91 | + |
| 92 | +> **Security note:** Never commit API tokens to source control. All secrets |
| 93 | +> are stored in ADO service connections and injected by `TwineAuthenticate@1` |
| 94 | +> via the ephemeral `$(PYPIRC_PATH)` file at pipeline runtime. |
| 95 | +
|
| 96 | +--- |
| 97 | + |
| 98 | +## Step 4 — Create ADO Environments |
| 99 | + |
| 100 | +Environments let you add approval gates before the deployment jobs run. |
| 101 | + |
| 102 | +1. Go to **Pipelines → Environments → New environment**. |
| 103 | +2. Create two environments: |
| 104 | + |
| 105 | + | Name | Description | |
| 106 | + |------|-------------| |
| 107 | + | `MSAL-Python` | Staging — test.pypi.org uploads | |
| 108 | + | `MSAL-Python-Release` | Production — PyPI uploads (**add approval check**) | |
| 109 | + |
| 110 | +3. For the `MSAL-Python-Release` environment: |
| 111 | + - Click the `MSAL-Python-Release` environment → **Approvals and checks → +** |
| 112 | + - Add **Approvals** → add the release approver(s) (e.g., release manager). |
| 113 | + - This ensures a human must approve before the wheel is pushed to production. |
| 114 | + |
| 115 | +--- |
| 116 | + |
| 117 | +## Step 5 — Create the Pipeline in ADO |
| 118 | + |
| 119 | +1. Go to **Pipelines → New pipeline**. |
| 120 | +2. Select **GitHub** as the code source. |
| 121 | +3. Pick the repository **AzureAD/microsoft-authentication-library-for-python**. |
| 122 | + - ADO will use the `github-msal-python` service connection created in Step 2. |
| 123 | +4. Choose **Existing Azure Pipelines YAML file**. |
| 124 | +5. Set the path to: `/.Pipelines/pipeline-publish.yml` |
| 125 | +6. Click **Continue** → review the YAML → click **Save** (not *Run*). |
| 126 | +7. Rename the pipeline to something descriptive, e.g. |
| 127 | + `msal-python · publish`. |
| 128 | + |
| 129 | +> **Note:** The existing `azure-pipelines.yml` (CI-only, runs on `dev`) is a |
| 130 | +> separate pipeline and is not affected. |
| 131 | +
|
| 132 | +--- |
| 133 | + |
| 134 | +## Step 6 — Authorize Pipelines to use Service Connections |
| 135 | + |
| 136 | +When the pipeline first uses a service connection you may be prompted to |
| 137 | +authorize it. To pre-authorize: |
| 138 | + |
| 139 | +1. **Project Settings → Service connections** → click a connection → |
| 140 | + **Security** tab. |
| 141 | +2. Set the **Pipeline permissions** to include the new publish pipeline. |
| 142 | + |
| 143 | +Repeat for all three connections: `github-msal-python`, `MSAL-Test-Python-Upload`, |
| 144 | +`MSAL-Prod-Python-Upload`. |
| 145 | + |
| 146 | +--- |
| 147 | + |
| 148 | +## Step 7 — Pipeline Parameters (Run Pipeline UI) |
| 149 | + |
| 150 | +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`: |
| 151 | + |
| 152 | +| Parameter | Required | Description | Example values | |
| 153 | +|-----------|----------|-------------|----------------| |
| 154 | +| **Package version to publish** | ✅ Yes | Must exactly match `msal/sku.py __version__`. | `1.36.0` (release), `1.36.0rc1` (preview) | |
| 155 | +| **Publish target** | ✅ Yes | Explicit destination — no auto-routing. | `MSAL-Python` (test.pypi.org) or `pypi` (production) | |
| 156 | + |
| 157 | +--- |
| 158 | + |
| 159 | +## Step 8 — End-to-End Release Walkthrough |
| 160 | + |
| 161 | +### Publishing a preview / release candidate to test.pypi.org |
| 162 | + |
| 163 | +1. Set `msal/sku.py __version__ = "1.36.0rc1"` |
| 164 | +2. Go to **Pipelines → Run pipeline** |
| 165 | +3. Enter `packageVersion = 1.36.0rc1`, select `publishTarget = MSAL-Python` |
| 166 | +4. Click **Run** — pipeline runs: Validate → CI → Build → PublishMSALPython |
| 167 | +5. Verify at <https://test.pypi.org/project/msal/> |
| 168 | + |
| 169 | +### Publishing a production release to PyPI |
| 170 | + |
| 171 | +1. Set `msal/sku.py __version__ = "1.36.0"` and merge to the release branch |
| 172 | +2. Go to **Pipelines → Run pipeline** |
| 173 | +3. Enter `packageVersion = 1.36.0`, select `publishTarget = pypi` |
| 174 | +4. Click **Run** — pipeline runs: Validate → CI → Build → PublishPyPI (approval gate) |
| 175 | +5. Go to **Pipelines → Environments → MSAL-Python-Release** and approve the deployment |
| 176 | +6. Verify: `pip install msal==1.36.0` or check <https://pypi.org/project/msal/> |
| 177 | + |
| 178 | +## Pipeline Trigger Reference |
| 179 | + |
| 180 | +``` |
| 181 | +Manual queue (publishTarget = MSAL-Python) |
| 182 | + └─► Validate ─► CI ─► Build ─► PublishMSALPython |
| 183 | + (test.pypi.org, auto) |
| 184 | +
|
| 185 | +Manual queue (publishTarget = pypi) |
| 186 | + └─► Validate ─► CI ─► Build ─► PublishPyPI |
| 187 | + (PyPI, requires approval) |
| 188 | +``` |
| 189 | + |
| 190 | +--- |
| 191 | + |
| 192 | +## Troubleshooting |
| 193 | + |
| 194 | +| Symptom | Likely cause | Fix | |
| 195 | +|---------|-------------|-----| |
| 196 | +| `403` on twine upload | Token expired or wrong scope | Regenerate API token on pypi.org; update the service connection | |
| 197 | +| `File already exists` error | Version already published; PyPI does not allow overwriting | Bump version in `msal/sku.py` | |
| 198 | +| Pipeline not triggered by tag | ADO only picks up tags after the pipeline is saved with the `tags:` trigger | Re-save the pipeline in ADO after adding the trigger | |
| 199 | +| `TwineAuthenticate` says endpoint not found | Service connection name mismatch | Ensure `pythonUploadServiceConnection` value exactly matches the service connection name | |
| 200 | + |
| 201 | +--- |
| 202 | + |
| 203 | +## References |
| 204 | + |
| 205 | +- [Publish Python packages with Azure Pipelines](https://learn.microsoft.com/en-us/azure/devops/pipelines/artifacts/pypi?view=azure-devops) |
| 206 | +- [TwineAuthenticate@1 task reference](https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/twine-authenticate-v1?view=azure-devops) |
| 207 | +- [Publish and download Python packages with Azure Artifacts](https://learn.microsoft.com/en-us/azure/devops/artifacts/quickstarts/python-packages?view=azure-devops) |
| 208 | +- [Python package upload service connection](https://learn.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints#python-package-upload-service-connection) |
| 209 | +- [ADO Environments – approvals and checks](https://learn.microsoft.com/en-us/azure/devops/pipelines/process/approvals?view=azure-devops) |
0 commit comments