Example of Azure Pipeline yaml file to configure pytest with code coverage and Nexus IQ / SonarQube scanners for Python application.
Additional yaml file features:
- trigger CI on merge to master, or each tag
- pip download folder caching
- freeze requirements and add marker for selected platform as recommended in Nexux docs
- publish test result
- publish code coverage report using Cobertura tool
Notes:
- overall time save is neglible compared to regular pip install for small requirements file
- azure pipeline file grows quickly, so if using similar in multiple repositories it is worth to implement pipeline templates
- Sonar and Nexus are configured to run only on merge to master
Azure Devops configuration for SonarQube and Sonata Nexus:
Example app structure:
src\
core\
core2\
utils\
resources\
tests\
testcore\
testsutils\
requirements.txt
Example Azure CI pipeline file:
# azure-pipeline.yml
trigger:
branches:
include:
- master
tags:
include:
- '*'
resources:
repositories:
- repository: self
type: git
name: <put-here-name-of-repository>
variables:
pythonVersion: '3.11'
pipDownloadDir: $(Pipeline.Workspace)/.pip
jobs:
- job: Test
pool:
name: 'Azure Pipelines'
vmImage: ubuntu-latest
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: $(pythonVersion)
- task: Cache@2
displayName: Load cache
inputs:
key: 'pip | "$(Agent.OS)" | requirements.txt'
path: $(pipDownloadDir)
cacheHitVar: cacheRestored
- script: pip download -r requirements.txt --dest=$(pipDownloadDir)
displayName: 'Download requirements'
condition: eq(variables.cacheRestored, 'false')
- script: pip install -r requirements.txt --no-index --find-links=$(pipDownloadDir)
displayName: 'Install requirements'
- script: |
mkdir -p dist
pip freeze > frozen_req.txt
cat frozen_req.txt | while read line; do echo ${line}'; sys_platform="linux"' >> frozen_req_sys.txt; done
mv frozen_req_sys.txt dist/requirements.txt
displayName: 'Prepare files for Nexus scanning'
- script: pytest -vs --junitxml=junit/test-results.xml --cov=src --cov-report=xml tests/
displayName: 'Run tests with coverage'
- script: cp coverage.xml dist/coverage.xml
displayName: 'Copy coverage report for Sonar'
- task: PublishTestResults@1
condition: succeededOrFailed()
inputs:
testResultsFiles: 'junit/**.xml'
testRunTitle: 'Publish test results'
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml'
- publish: $(System.DefaultWorkingDirectory)/dist
artifact: dist
displayName: 'Publish files for scanning'
- job: Sonar
dependsOn: Test
condition: and(succeded(), eq(variables['build.sourceBranch'], 'refs/heads/master'))
pool:
name: 'Azure Pipelines'
vmImage: ubuntu-latest
steps:
- task: DownloadPipelineArtifact@2
inputs:
source: current
artifact: dist
path: $(System.DefaultWorkingDirectory)
- task: SonarQubePrepare@5
inputs:
SonarQube: 'YourSonarqubeServerEndpoint'
scannerMode: 'Other'
extraProperties: |
sonar.projectKey=<your-application-name>.$(Build.Repository.Name)
sonar.verbose=true
sonar.sources=src/
sonar.tests=tests/
sonar.exclusions=src/noncore/**,src/resources/**
sonar.python.version=$(pythonVersion)
sonar.python.coverage.reportPaths=coverage.xml
sonar.coverage.exclusions=src/noncore/**/*,src/resources/**/*
# Run Code Analysis task
- task: SonarQubeAnalyze@5
# Publish Quality Gate Result task
- task: SonarQubePublish@5
inputs:
pollingTimeoutSec: '300'
- job: NexusIQ
dependsOn: Test
condition: and(succeded(), eq(variables['build.sourceBranch'], 'refs/heads/master'))
pool:
name: 'Azure Pipelines'
vmImage: ubuntu-latest
steps:
- task: DownloadPipelineArtifact@2
inputs:
source: current
artifact: dist
path: $(System.DefaultWorkingDirectory)
- checkout: none
- bash: ls -lRH
displayName: Show local files
- task: NexusIqPipelineTask@1
displayName: Nexus IQ Scan
continueOnError: true
inputs:
nexusIqService: 'Nexus Iq'
applicationId: <your-application-name>.$(Build.Repository.Name)
stage: 'Build'
scanTargets: '$(System.DefaultWorkingDirectory)/**/*.*'