Skip to content

Add ADO publish pipeline for PyPI releases#890

Open
RyAuld wants to merge 21 commits intodevfrom
RyAuld/PyPI-ADO-PackagePublish
Open

Add ADO publish pipeline for PyPI releases#890
RyAuld wants to merge 21 commits intodevfrom
RyAuld/PyPI-ADO-PackagePublish

Conversation

@RyAuld
Copy link

@RyAuld RyAuld commented Mar 20, 2026

Summary

Adds a manually-triggered ADO release pipeline (.Pipelines/) that validates, tests, builds, and publishes the msal package to test.pypi.org or pypi.org.
Following the same template convention as MSAL.NET, all pipeline logic lives in a single shared template (template-pipeline-stages.yml) called from a thin top-level pipeline wrapper. The template is designed to be reused by future PR-gate and post-merge CI pipelines (with runPublish: false).

Pipeline files

File Purpose
pipeline-publish.yml Thin wrapper — triggers, parameters, calls shared template with runPublish: true
template-pipeline-stages.yml Shared stages template — all stages for release, PR-gate, and post-merge CI pipelines
credscan-exclusion.json CredScan suppression file — suppresses known false positives for test fixture files (certificate-with-password.pfx, test_mi.py)
ADO-PUBLISH-SETUP.md One-time setup guide and release walkthrough

Pipeline stages

  1. PreBuildCheck — PoliCheck + CredScan SDL security scans (mirrors MSAL.NET; runs in all 3 pipeline scenarios). CredScan suppressions are defined in credscan-exclusion.json.
  2. Validate — confirms packageVersion parameter matches msal/sku.py __version__ (release only)
  3. CI — runs the full pytest matrix across Python 3.9–3.14 (mirrors existing azure-pipelines.yml build)
  4. Build — builds sdist + wheel, verifies with twine check, publishes as pipeline artifact (release only)
  5. Publish — uploads to test.pypi.org (Preview/RC) or pypi.org (Production) depending on publishTarget; production stage uses the MSAL-Python-Release environment for approval gating (release only)

Other changes

  • msal/sku.py — bumped version to 1.35.2rc1; fixed typo in module docstring

@RyAuld RyAuld changed the base branch from main to dev March 20, 2026 22:03
@RyAuld RyAuld requested a review from a team as a code owner March 20, 2026 22:03
Copy link

@gladjohn gladjohn left a comment

Choose a reason for hiding this comment

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

Approved with comments. Please Add Python 3.14 to the matrix in .Pipelines/pipeline-publish.yml

Copilot AI review requested due to automatic review settings March 23, 2026 16:43
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a manually triggered Azure DevOps publish pipeline (via .Pipelines/) intended to validate the release version, run the existing test matrix, build sdist/wheel, and publish msal to either test.pypi.org or pypi.org with environment-based approval gating for production releases.

Changes:

  • Added a new multi-stage ADO pipeline (pipeline-publish.yml) composed of reusable step templates for test, build, and publish.
  • Added documentation (ADO-PUBLISH-SETUP.md) describing one-time Azure DevOps setup and release walkthrough steps.
  • Bumped msal package version in msal/sku.py to 1.35.2rc1.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
msal/sku.py Updates the library version used by packaging metadata (setup.cfg reads msal.__version__).
.Pipelines/pipeline-publish.yml Defines Validate → CI → Build → Publish stages and routes publish target via a parameter.
.Pipelines/template-run-tests.yml Installs dependencies and runs pytest, publishing JUnit results.
.Pipelines/template-build-package.yml Builds sdist/wheel, runs twine check, and publishes dist/ as a pipeline artifact.
.Pipelines/template-publish-package.yml Authenticates to a PyPI-compatible feed and uploads built artifacts via twine.
.Pipelines/template-install-lab-cert.yml Retrieves and materializes a lab certificate from Key Vault (currently always invoked by test template).
.Pipelines/ADO-PUBLISH-SETUP.md Documents required ADO service connections/environments and how to run the manual publish pipeline.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 23, 2026 16:55
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

# steps:
# - template: .Pipelines/template-run-tests.yml

steps:
Copy link
Member

Choose a reason for hiding this comment

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

Do we need these templates? Can we have 1 file with all steps? Templates are only useful if reused. I don't think it is the case here.


## Overview

This pipeline is **manually triggered only** — no automatic branch or tag triggers.
Copy link
Member

@bgavrilMS bgavrilMS Mar 23, 2026

Choose a reason for hiding this comment

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

THerea are 3 sceanrios:

  • pipeline which runs on PRs
  • pipeline which runs after a merge
  • release pipeline

In MSAL.NET I believe these are all 1 pipeline, whith optional config.

Copilot AI review requested due to automatic review settings March 23, 2026 17:07
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +28 to +29
# 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.
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The comment claiming that script: uses /bin/sh on Linux (and thus doesn’t support pipefail) is misleading in Azure Pipelines; this repo’s existing azure-pipelines.yml uses script: with set -o pipefail successfully. Either remove/reword the comment to avoid spreading incorrect guidance, or keep the switch to bash: but explain it as making the shell explicit rather than implying script: can’t handle pipefail.

Suggested change
# 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.
# Use bash: here so the shell is explicit and set -o pipefail is reliably honored;
# without pipefail, tee can exit 0 and mask pytest failures.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +60
SKU_VER=$(grep '__version__' msal/sku.py | sed 's/.*"\(.*\)".*/\1/')

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The version check parses msal/sku.py with grep/sed, which is brittle (it assumes a specific quoting/format and could break if the assignment line is reformatted). A more robust approach is to have Python load the module and print __version__ (without importing the full package dependency graph), and compare against the parameter.

Suggested change
SKU_VER=$(grep '__version__' msal/sku.py | sed 's/.*"\(.*\)".*/\1/')
SKU_VER=$(python - << 'PYCODE'
import importlib.util
import pathlib
sku_path = pathlib.Path("msal") / "sku.py"
spec = importlib.util.spec_from_file_location("msal.sku", sku_path)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
print(module.__version__)
PYCODE
)

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 23, 2026 18:22
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- stage: Build
displayName: 'Build package'
dependsOn: CI
condition: and(eq(dependencies.CI.result, 'Succeeded'), eq('${{ parameters.runPublish }}', 'true'))
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The Build stage condition compares a boolean template parameter to the string 'true'. Template boolean parameters typically evaluate as booleans (and may render as True/False depending on context), which can cause this condition to evaluate false and skip Build even when runPublish: true. Use a boolean comparison instead (e.g., directly reference the boolean parameter in the condition) to ensure Build runs on publish executions.

Suggested change
condition: and(eq(dependencies.CI.result, 'Succeeded'), eq('${{ parameters.runPublish }}', 'true'))
condition: and(eq(dependencies.CI.result, 'Succeeded'), eq(${{ parameters.runPublish }}, true))

Copilot uses AI. Check for mistakes.
Comment on lines +268 to +283
- 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/*
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The publish deployment jobs upload from $(Pipeline.Workspace)/python-dist/*, but there is no explicit artifact download step in the Publish stages. In multi-stage YAML (and especially deployment jobs), pipeline artifacts from earlier stages are not reliably available unless you add an explicit download (e.g., download: current / DownloadPipelineArtifact) for the python-dist artifact. Add a download step before the twine upload in both publish stages to ensure the dist files are present.

Copilot uses AI. Check for mistakes.
Comment on lines +150 to +164
- 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'
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

These steps run unconditionally in the CI stage, which makes the pipeline dependent on an ADO service connection + Key Vault secret even though the comments say this is 'kept for when e2e tests are fully enabled'. If LabAuth is missing/unavailable (or the service connection isn’t authorized), CI will fail before unit tests run. Make the Key Vault + write-cert steps conditional (e.g., behind a parameter like enableLabAuth / a variable toggle), and only set LAB_APP_CLIENT_CERT_PFX_PATH when the secret is actually retrieved.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
displayName: 'Install NPM'
inputs:
versionSpec: '16.x'
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

Pinning Node.js to 16.x is risky because Node 16 is end-of-life and may be removed from hosted agents over time, causing the pre-build stage to break unexpectedly. Consider moving to a currently supported LTS (and update the display name to reflect that this installs Node.js, not NPM).

Suggested change
displayName: 'Install NPM'
inputs:
versionSpec: '16.x'
displayName: 'Install Node.js'
inputs:
versionSpec: 'lts/*'

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 23, 2026 18:44
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +145 to +156
# 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.
# Kept for when e2e tests are fully enabled on a lab-capable agent pool.
# Prerequisites: service connection 'AuthSdkResourceManager' with Get/List access
# to the 'msidlabs' Key Vault. The 'LabAuth' secret is a base64-encoded PFX.
- task: AzureKeyVault@2
displayName: 'Retrieve lab certificate from Key Vault'
inputs:
azureSubscription: 'AuthSdkResourceManager'
KeyVaultName: 'msidlabs'
SecretsFilter: 'LabAuth'
RunAsPreJob: false
Copy link

Copilot AI Mar 23, 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 runs the AzureKeyVault@2 step to fetch the LabAuth secret via the 'AuthSdkResourceManager' service connection. That makes the pipeline depend on a Key Vault + service connection even though LAB_APP_CLIENT_ID is intentionally not set (so e2e tests won’t run). Consider gating the Key Vault fetch + cert write behind an explicit parameter/variable (or only when LAB_APP_CLIENT_ID is provided) so the publish pipeline doesn’t fail due to missing Key Vault wiring.

Copilot uses AI. Check for mistakes.
Comment on lines +158 to +165
- 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'

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The lab certificate is written to $(Build.SourcesDirectory)/lab-auth.pfx but never removed. Since it originates from Key Vault, it should be treated as sensitive material; delete it in a final always() step (or write to a temp dir and clean up) to reduce risk of accidental exposure or reuse across steps.

Copilot uses AI. Check for mistakes.
Comment on lines +176 to +178
# 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.
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The comment claims "script: uses /bin/sh on Linux which does not support pipefail", but this repo’s existing azure-pipelines.yml uses a script: step with set -o pipefail successfully. This comment is likely inaccurate/misleading; please update it (or remove it) to match actual Azure Pipelines behavior.

Suggested change
# 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.
# Use set -o pipefail so that pytest failures are not hidden by the pipe to tee.
# Without pipefail, tee exits 0 and the step can succeed even when tests fail.
# This step uses bash: explicitly, but set -o pipefail also works in script: steps.

Copilot uses AI. Check for mistakes.
| 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 <https://pypi.org/manage/account/token/> |
| MSAL-Python (test.pypi.org) API token | Scoped to the `msal` project on test.pypi.org |
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

This setup guide doesn’t mention the required 'AuthSdkResourceManager' Azure service connection / Key Vault access needed by the CI stage to fetch the LabAuth secret (AzureKeyVault@2 in template-pipeline-stages.yml). Without documenting/setting this up, the pipeline will fail before tests run.

Suggested change
| MSAL-Python (test.pypi.org) API token | Scoped to the `msal` project on test.pypi.org |
| MSAL-Python (test.pypi.org) API token | Scoped to the `msal` project on test.pypi.org |
| Azure service connection `AuthSdkResourceManager` | Azure Resource Manager service connection with access to the subscription / resource group that contains the Key Vault used by the pipeline. The service principal behind this connection must have at least **Get** permission on **secrets** in that Key Vault. This connection name is referenced by the `AzureKeyVault@2` task in `template-pipeline-stages.yml`. |
| Key Vault secret `LabAuth` | In the Key Vault referenced by `template-pipeline-stages.yml`, create a secret named `LabAuth` containing the lab authentication credentials required by the CI stage. The `AuthSdkResourceManager` service connection must be able to read this secret for the pipeline to succeed. |

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +28
| 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) |
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The pipeline uses PoliCheck/CredScan/PostAnalysis tasks from the Secure Development Tools extension, but the prerequisites/setup steps don’t call out installing/enabling that extension in the ADO organization/project. Add this requirement so new projects can run PreBuildCheck successfully.

Copilot uses AI. Check for mistakes.
Codeql.SkipTaskAutoInjection: true
steps:
- task: NodeTool@0
displayName: 'Install NPM'
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

NodeTool@0 installs Node.js, not NPM specifically; the display name "Install NPM" is misleading. Consider renaming to "Install Node.js" (or clarify why this step is needed for PoliCheck/CredScan).

Suggested change
displayName: 'Install NPM'
displayName: 'Install Node.js (includes npm)'

Copilot uses AI. Check for mistakes.
…l comment, Build condition, explicit artifact download, prereq docs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants