diff --git a/.Pipelines/CI-AND-RELEASE-PIPELINES.md b/.Pipelines/CI-AND-RELEASE-PIPELINES.md index d8d18d00..e7c5ad1a 100644 --- a/.Pipelines/CI-AND-RELEASE-PIPELINES.md +++ b/.Pipelines/CI-AND-RELEASE-PIPELINES.md @@ -7,16 +7,16 @@ including what each pipeline does, when it runs, and how to trigger a release. ## Pipeline Files -| File | Purpose | -|------|---------| -| [`azure-pipelines.yml`](../azure-pipelines.yml) | PR gate and post-merge CI — calls the shared template with `runPublish: false` | -| [`pipeline-publish.yml`](pipeline-publish.yml) | Release pipeline — manually queued, builds and publishes to PyPI | -| [`template-pipeline-stages.yml`](template-pipeline-stages.yml) | Shared stages template — PreBuildCheck, Validate, and CI stages reused by both pipelines | -| [`credscan-exclusion.json`](credscan-exclusion.json) | CredScan suppression file for known test fixtures | +| File | ADO Pipeline | Purpose | +|------|-------------|---------| +| [`azure-pipelines.yml`](../azure-pipelines.yml) | [MSAL.Python-PR-OneBranch-Official (3064)](https://dev.azure.com/IdentityDivision/IDDP/_build?definitionId=3064) | PR gate and post-merge CI — calls the shared template with `runPublish: false` | +| [`pipeline-publish.yml`](pipeline-publish.yml) | [MSAL.Python-Publish (3067)](https://dev.azure.com/IdentityDivision/IDDP/_build?definitionId=3067) | Release pipeline — manually queued, builds and publishes to PyPI | +| [`template-pipeline-stages.yml`](template-pipeline-stages.yml) | — | Shared stages template — PreBuildCheck, Validate, and CI stages reused by both pipelines | +| [`credscan-exclusion.json`](credscan-exclusion.json) | — | CredScan suppression file for known test fixtures | --- -## PR / CI Pipeline (`azure-pipelines.yml`) +## PR / CI Pipeline — [MSAL.Python-PR-OneBranch-Official (3064)](https://dev.azure.com/IdentityDivision/IDDP/_build?definitionId=3064) ### Triggers @@ -35,7 +35,7 @@ PreBuildCheck ─► CI | Stage | What it does | |-------|-------------| | **PreBuildCheck** | Runs SDL security scans: PoliCheck (policy/offensive content), CredScan (leaked credentials), and PostAnalysis (breaks the build on findings) | -| **CI** | Runs the full test suite on Python 3.9, 3.10, 3.11, 3.12, 3.13, and 3.14 | +| **CI** | Runs the full test suite on Python 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, and 3.14 | The Validate stage is **skipped** on PR/CI runs (it only applies to release builds). @@ -45,7 +45,7 @@ The Validate stage is **skipped** on PR/CI runs (it only applies to release buil --- -## Release Pipeline (`pipeline-publish.yml`) +## Release Pipeline — [MSAL.Python-Publish (3067)](https://dev.azure.com/IdentityDivision/IDDP/_build?definitionId=3067) ### Triggers @@ -70,7 +70,7 @@ PreBuildCheck ─► Validate ─► CI ─► Build ─┬─► PublishMSALPyt |-------|-------------|-----------| | **PreBuildCheck** | PoliCheck + CredScan scans | Always | | **Validate** | Asserts the `packageVersion` parameter matches `msal/sku.py __version__` | Always (release runs only) | -| **CI** | Full test matrix (Python 3.9–3.14) | After Validate passes | +| **CI** | Full test matrix (Python 3.8–3.14) | After Validate passes | | **Build** | Builds `sdist` and `wheel` via `python -m build`; publishes `python-dist` artifact | After CI passes | | **PublishMSALPython** | Uploads to test.pypi.org | `publishTarget == test.pypi.org (Preview / RC)` | | **PublishPyPI** | Uploads to PyPI via ESRP; requires manual approval | `publishTarget == pypi.org (ESRP Production)` | diff --git a/.Pipelines/template-pipeline-stages.yml b/.Pipelines/template-pipeline-stages.yml index 972121c3..b3a88741 100644 --- a/.Pipelines/template-pipeline-stages.yml +++ b/.Pipelines/template-pipeline-stages.yml @@ -68,6 +68,16 @@ stages: GdnBreakGdnToolCredScan: true GdnBreakGdnToolPoliCheck: true + - task: securedevelopmentteam.vss-secure-development-tools.build-task-publishsecurityanalysislogs.PublishSecurityAnalysisLogs@3 + displayName: 'Publish Security Analysis Logs (TSA)' + condition: succeededOrFailed() + inputs: + tsaConfigFile: '$(Build.SourcesDirectory)/.Pipelines/tsaConfig.json' + + - task: mspremier.PostBuildCleanup.PostBuildCleanup-task.PostBuildCleanup@3 + displayName: 'Clean agent directories' + condition: always() + # ══════════════════════════════════════════════════════════════════════════════ # Stage 1 · Validate — verify packageVersion matches msal/sku.py __version__ # Skipped when runPublish is false (PR / merge builds). @@ -127,6 +137,8 @@ stages: vmImage: ubuntu-latest strategy: matrix: + Python38: + python.version: '3.8' Python39: python.version: '3.9' Python310: @@ -187,6 +199,7 @@ stages: set -o pipefail pytest -vv --junitxml=test-results/junit.xml 2>&1 | tee test-results/pytest.log displayName: 'Run tests' + retryCountOnTaskFailure: 3 env: LAB_APP_CLIENT_CERT_PFX_PATH: $(LAB_APP_CLIENT_CERT_PFX_PATH) diff --git a/.Pipelines/tsaConfig.json b/.Pipelines/tsaConfig.json new file mode 100644 index 00000000..f20b3111 --- /dev/null +++ b/.Pipelines/tsaConfig.json @@ -0,0 +1,17 @@ +{ + "codebaseName": "MSAL Python", + "notificationAliases": [ + "IdentityDevExDotnet@microsoft.com" + ], + "codebaseAdmins": [ + "EUROPE\\aadidagt" + ], + "instanceUrl": "https://identitydivision.visualstudio.com", + "projectName": "IDDP", + "areaPath": "IDDP\\DevEx-Client-SDK\\Python", + "iterationPath": "IDDP\\Unscheduled", + "tools": [ + "credscan", + "policheck" + ] +} diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml deleted file mode 100644 index 6c1626b6..00000000 --- a/.github/workflows/python-package.yml +++ /dev/null @@ -1,129 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: CI/CD - -on: - push: - pull_request: - branches: [ dev ] - - # This guards against unknown PR until a community member vet it and label it. - types: [ labeled ] - -jobs: - ci: - env: - # Fake a TRAVIS env so that the pre-existing test cases would behave like before - TRAVIS: true - LAB_APP_CLIENT_ID: ${{ secrets.LAB_APP_CLIENT_ID }} - LAB_APP_CLIENT_CERT_BASE64: ${{ secrets.LAB_APP_CLIENT_CERT_BASE64 }} - LAB_APP_CLIENT_CERT_PFX_PATH: lab_cert.pfx - - # Derived from https://docs.github.com/en/actions/guides/building-and-testing-python#starting-with-the-python-workflow-template - runs-on: ubuntu-22.04 - strategy: - matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] - - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - # It automatically takes care of pip cache, according to - # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#about-caching-workflow-dependencies - with: - python-version: ${{ matrix.python-version }} - cache: 'pip' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Populate lab cert.pfx - # https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#storing-base64-binary-blobs-as-secrets - run: echo $LAB_APP_CLIENT_CERT_BASE64 | base64 -d > $LAB_APP_CLIENT_CERT_PFX_PATH - - name: Test with pytest - run: pytest --benchmark-skip - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - #flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - #flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - cb: - # Benchmark only after the correctness has been tested by CI, - # and then run benchmark only once (sampling with only one Python version). - needs: ci - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - name: Set up Python 3.9 - uses: actions/setup-python@v5 - with: - python-version: 3.9 - cache: 'pip' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Setup an updatable cache for Performance Baselines - uses: actions/cache@v4 - with: - path: .perf.baseline - key: ${{ runner.os }}-performance-${{ hashFiles('tests/test_benchmark.py') }} - restore-keys: ${{ runner.os }}-performance- - - name: Run benchmark - run: pytest --benchmark-only --benchmark-json benchmark.json --log-cli-level INFO tests/test_benchmark.py - - name: Render benchmark result - uses: benchmark-action/github-action-benchmark@v1 - with: - tool: 'pytest' - output-file-path: benchmark.json - fail-on-alert: true - - name: Publish Gibhub Pages - run: git push origin gh-pages - - cd: - needs: ci - # Note: github.event.pull_request.draft == false WON'T WORK in "if" statement, - # because the triggered event is a push, not a pull_request. - # This means each commit will trigger a release on TestPyPI. - # Those releases will only succeed when each push has a new version number: a1, a2, a3, etc. - if: | - github.event_name == 'push' && - ( - startsWith(github.ref, 'refs/tags') || - startsWith(github.ref, 'refs/heads/release-') - ) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python 3.9 - uses: actions/setup-python@v5 - with: - python-version: 3.9 - cache: 'pip' - - name: Build a package for release - run: | - python -m pip install build --user - python -m build --sdist --wheel --outdir dist/ . - - name: | - Publish to TestPyPI when pushing to release-* branch. - You better test with a1, a2, b1, b2 releases first. - uses: pypa/gh-action-pypi-publish@v1.13.0 - if: startsWith(github.ref, 'refs/heads/release-') - with: - user: __token__ - password: ${{ secrets.TEST_PYPI_API_TOKEN }} - repository_url: https://test.pypi.org/legacy/ - - name: Publish to PyPI when tagged - if: startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@v1.13.0 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b800165e..a56a2428 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -24,3 +24,51 @@ stages: - template: .Pipelines/template-pipeline-stages.yml parameters: runPublish: false + +- stage: Benchmark + displayName: 'Run benchmarks' + dependsOn: CI + # Only run on post-merge pushes to dev — not on PRs or scheduled runs. + # Benchmarks are noisy and the baseline cache is only meaningful on a stable branch. + condition: | + and( + succeeded('CI'), + eq(variables['Build.SourceBranch'], 'refs/heads/dev'), + or( + eq(variables['Build.Reason'], 'IndividualCI'), + eq(variables['Build.Reason'], 'BatchedCI'), + eq(variables['Build.Reason'], 'Manual') + ) + ) + jobs: + - job: Benchmark + displayName: 'Performance benchmarks (Python 3.9)' + pool: + vmImage: ubuntu-latest + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.9' + displayName: 'Set up Python 3.9' + + - script: | + python -m pip install --upgrade pip + pip install -r requirements.txt + displayName: 'Install dependencies' + + - task: Cache@2 + displayName: 'Restore performance baseline cache' + inputs: + key: 'perf-baseline | "$(Agent.OS)" | tests/test_benchmark.py' + path: .perf.baseline + + - bash: | + pytest --benchmark-only --benchmark-json benchmark.json --log-cli-level INFO tests/test_benchmark.py + displayName: 'Run benchmarks' + + - task: PublishPipelineArtifact@1 + displayName: 'Publish benchmark results' + condition: succeededOrFailed() + inputs: + targetPath: 'benchmark.json' + artifact: 'benchmark-results'