From a50da6599513a014570820ffbbc95ccd25f167d0 Mon Sep 17 00:00:00 2001 From: junkataoka Date: Thu, 30 Apr 2026 18:45:15 +0900 Subject: [PATCH] feat(ci): devcontainer-based pytest integration for GH Actions and AzDO Replace ci-tests.sh with devcontainer-based CI that builds and runs pytest inside each project's actual devcontainer, ensuring CI uses the exact same environment as developers. GitHub Actions: - Use devcontainers/ci@v0.3 to build and exec inside each devcontainer - Per-project pytest runs (cpu, gpu) with junit + coverage XML - Smoke build for notebooks devcontainer Azure DevOps (ms-hosted and self-hosted): - Inline @devcontainers/cli usage (no template files) - Same per-project pytest pattern; PublishTestResults + PublishCodeCoverageResults - Self-hosted variant adds Docker prune + _work cleanup CI workarounds for non-modifiable devcontainer.json: - Pre-create .env at relative paths (./, ../, ../../) expected by devcontainer.json runArgs --env-file paths - Strip --gpus all from GPU devcontainer.json at workflow runtime since GitHub-hosted runners lack GPU device drivers (local users keep auto GPU passthrough) Other: - Add hostRequirements gpu=optional hint to GPU devcontainer.json - pyproject.toml: coverage source/relative_files and exclude_lines - Remove obsolete ci-tests.sh Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .azuredevops/ado-ci-pipeline-ms-hosted.yml | 172 ++++++++++----- .azuredevops/ado-ci-pipeline-self-hosted.yml | 207 +++++++++++------- .github/workflows/ci.yaml | 82 ++++++- ci-tests.sh | 68 ------ pyproject.toml | 9 + .../.devcontainer/devcontainer.json | 3 + 6 files changed, 326 insertions(+), 215 deletions(-) delete mode 100755 ci-tests.sh diff --git a/.azuredevops/ado-ci-pipeline-ms-hosted.yml b/.azuredevops/ado-ci-pipeline-ms-hosted.yml index 05f7d1f..295b54c 100644 --- a/.azuredevops/ado-ci-pipeline-ms-hosted.yml +++ b/.azuredevops/ado-ci-pipeline-ms-hosted.yml @@ -1,59 +1,113 @@ -# Azure DevOps pipeline for CI (Microsoft-hosted version) -# As the Microsoft-hosted agent option has a limit of 10GB of storage for disk outputs from a pipeline, -# this causes an issue when the Docker images for modules under src require more than 10GB of storage. -# If you will run into space issues (or other limitations with a Microsoft hosted agent option outlined in -# https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops&tabs=yaml#capabilities-and-limitations), -# consider using the .azuredevops/ado-ci-pipeline-self-hosted.yml version or using scale set agents, see -# this link for more info: https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/scale-set-agents?view=azure-devops -# Note that docker images will only be build for src directories that contain at least one test file, so the -# total space consumed by Docker builds will be dependent on which modules under src contain tests. -# For setting up the pipeline in ADO see: -# https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/pools-queues?view=azure-devops&tabs=yaml%2Cbrowser - - -trigger: - - main - -pool: - vmImage: 'ubuntu-latest' - -steps: - - task: UsePythonVersion@0 - displayName: "Use Python 3.11" - inputs: - versionSpec: 3.11 - - # Using pip here instead of uv/uvx because Azure DevOps doesn't have native uv support - # (unlike GitHub Actions which has astral-sh/setup-uv), and installing uv just to run - # these tools would add unnecessary overhead. - - script: | - pip install ruff pytest-azurepipelines - displayName: "Install ruff and pytest-azurepipelines" - - - bash: | - ruff check --output-format azure - displayName: "Run ruff linter" - - - task: Bash@3 - inputs: - targetType: 'filePath' - filePath: ci-tests.sh - env: - BUILD_ARTIFACTSTAGINGDIRECTORY: $(Build.ArtifactStagingDirectory) - displayName: "Run pytest in docker containers" - - - task: PublishTestResults@2 - inputs: - testResultsFiles: '**/test-results-*.xml' - searchFolder: $(Build.ArtifactStagingDirectory) - condition: succeededOrFailed() - - # Publish code coverage results - - task: PublishCodeCoverageResults@1 - inputs: - codeCoverageTool: 'Cobertura' # Available options: 'JaCoCo', 'Cobertura' - summaryFileLocation: '$(Build.ArtifactStagingDirectory)/coverage.xml' - pathToSources: src/ - #reportDirectory: # Optional - #additionalCodeCoverageFiles: # Optional - failIfCoverageEmpty: false # Optional +# Azure DevOps pipeline for CI (Microsoft-hosted version) +# As the Microsoft-hosted agent option has a limit of 10GB of storage for disk outputs from a pipeline, +# this causes an issue when the Docker images for modules under src require more than 10GB of storage. +# If you will run into space issues (or other limitations with a Microsoft hosted agent option outlined in +# https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops&tabs=yaml#capabilities-and-limitations), +# consider using the .azuredevops/ado-ci-pipeline-self-hosted.yml version or using scale set agents, see +# this link for more info: https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/scale-set-agents?view=azure-devops +# Note that docker images will only be build for src directories that contain at least one test file, so the +# total space consumed by Docker builds will be dependent on which modules under src contain tests. +# For setting up the pipeline in ADO see: +# https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/pools-queues?view=azure-devops&tabs=yaml%2Cbrowser + + +trigger: + - main + +pool: + vmImage: 'ubuntu-latest' + +steps: + - task: UsePythonVersion@0 + displayName: "Use Python 3.11" + inputs: + versionSpec: 3.11 + + # Using pip here instead of uv/uvx because Azure DevOps doesn't have native uv support + # (unlike GitHub Actions which has astral-sh/setup-uv), and installing uv just to run + # ruff would add unnecessary overhead. + - script: pip install ruff + displayName: "Install ruff" + + - bash: ruff check --output-format azure + displayName: "Run ruff linter" + + - script: | + cp .env.example .env + # devcontainer.json runArgs use relative --env-file paths (../../.env for + # src/* projects, ../.env for notebooks) that resolve from the devcontainer + # CLI's CWD (= the workspace folder). Pre-create them at the expected host + # paths so docker can find them. + cp .env.example ../.env + cp .env.example ../../.env + displayName: "Setup .env file" + + - script: npm install -g @devcontainers/cli + displayName: "Install devcontainer CLI" + + - bash: | + set -e + WORKSPACE_ROOT="$(pwd)" + devcontainer up --workspace-folder "$WORKSPACE_ROOT" \ + --config src/sample_cpu_project/.devcontainer/devcontainer.json + devcontainer exec --workspace-folder "$WORKSPACE_ROOT" \ + --config src/sample_cpu_project/.devcontainer/devcontainer.json \ + bash -c ' + cd /workspaces/repo/src/sample_cpu_project && + COVERAGE_FILE=/tmp/.coverage pytest tests/ \ + --junitxml=/tmp/test-results-cpu.xml \ + -o junit_suite_name=cpu-project \ + --doctest-modules \ + --cov=. \ + --cov-config=/workspaces/repo/pyproject.toml \ + --cov-report=xml:/tmp/coverage-cpu.xml && + sed -i "s|> $GITHUB_STEP_SUMMARY @@ -58,5 +121,6 @@ jobs: with: name: test-and-coverage-results path: | - **/test-reuslts-*.xml - coverage.xml + test-results-*.xml + coverage-*.xml + diff --git a/ci-tests.sh b/ci-tests.sh deleted file mode 100755 index d079970..0000000 --- a/ci-tests.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash -: ' -This script will run all unit tests in the repository (for all directories under src/ that -have at least one test_*.py under a tests folder). It will build a Docker image for each directory with tests, -using the Dockerfile in the .devcontainer directory. It will then run pytest in the Docker container -and save the test results and coverage report to the build artifacts directory. This script can be run -locally or also in an ADO CI pipeline or Github Actions CI pipeline. See the -.azuredevops/ado-ci-pipeline-ms-hosted.yml file for an example use in an ADO CI pipeline and the -.github/workflows/ci.yaml for an example use in Github Actions pipeline. -' - -set -eE - -repo_root="$(pwd)" - -# Find all the 'src' subdirectories with a 'tests' folder, extract the dir name as test_dir_parent -for test_dir_parent in $(find "${repo_root}/src" -type d -name 'tests' -exec dirname {} \; | sed "s|${repo_root}/src/||"); do - # Check for at least one Python file in the 'tests' subdirectory of test_dir_parent - count_test_py_files=$(find "${repo_root}/src/${test_dir_parent}/tests"/*.py 2>/dev/null | wc -l) - if [ $count_test_py_files != 0 ]; then - # Use the devcontainer Dockerfile to build a Docker image for the module to run tests - docker build "${repo_root}/src/${test_dir_parent}/.devcontainer" -t "${test_dir_parent}" - - echo "Running tests for ${test_dir_parent}, found ${count_test_py_files} test files" - - : ' - Run the tests in the built Docker container, saving the test results and coverage report to /tmp/artifact_output. - Some other key parts of the docker run command are explained here: - - The local /tmp dir is mounted to docker /tmp so that there are no permission issues with the docker user and the - pipeline user that runs this script and the user that accesses the test results and coverage report artifacts. - - The --cov-append option tells pytest coverage to append the results to the existing coverage data, instead of - overwriting it, this builds up coverage for each $test_dir_parent in a single coverage report for publishing. - - Set the .coverage location to be under /tmp so it is writable, coverage.py uses this file to store intermediate - data while measuring code coverage across multiple test runs or when combining data from multiple sources. - - exit with pytest exit code to ensure script exits with non-zero exit code if pytest fails, this ensure the CI - pipeline in ADO fails if any tests fail. - ' - docker run \ - -v "${repo_root}:/workspace" \ - -v "/tmp:/tmp" \ - --env test_dir_parent="$test_dir_parent" \ - --env COVERAGE_FILE=/tmp/artifact_output/.coverage \ - "${test_dir_parent}" \ - /bin/bash -ec ' - mkdir -p /tmp/artifact_output/$test_dir_parent; \ - env "PATH=$PATH" \ - env "PYTHONPATH=/workspace/src/$test_dir_parent:$PYTHONPATH" \ - pytest \ - --junitxml=/tmp/artifact_output/$test_dir_parent/test-results-$test_dir_parent.xml \ - -o junit_suite_name=$test_dir_parent \ - --doctest-modules \ - --cov \ - --cov-config=/workspace/pyproject.toml \ - --cov-report=xml:/tmp/artifact_output/coverage.xml \ - --cov-append \ - /workspace/src/$test_dir_parent; \ - exit $?' - fi -done - -: ' -If running CI on ADO with MS-hosted agents, copy the test and coverage results to the build artifacts directory -so that it is preserved for publishing. See the .azuredevops/ado-ci-pipeline-ms-hosted.yml file for how the -BUILD_ARTIFACTSTAGINGDIRECTORY is set. -' -if [ -n "$BUILD_ARTIFACTSTAGINGDIRECTORY" ]; then - cp -r /tmp/artifact_output/* "${BUILD_ARTIFACTSTAGINGDIRECTORY}" -fi diff --git a/pyproject.toml b/pyproject.toml index e3bfcf0..c11e52b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,9 +24,18 @@ select = ["E", "F", "B", "S", "I", "N"] pythonpath = "src" [tool.coverage.run] +source = ["src"] +relative_files = true omit = [ # ignore all notebooks in src "*/notebooks/*", # ignore all tests in src "*/tests/*", ] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "if __name__ == .__main__.:", +] diff --git a/src/sample_pytorch_gpu_project/.devcontainer/devcontainer.json b/src/sample_pytorch_gpu_project/.devcontainer/devcontainer.json index a0281ad..a3b61d7 100644 --- a/src/sample_pytorch_gpu_project/.devcontainer/devcontainer.json +++ b/src/sample_pytorch_gpu_project/.devcontainer/devcontainer.json @@ -20,6 +20,9 @@ "--env-file", "../../.env" ], + "hostRequirements": { + "gpu": "optional" + }, "remoteEnv": { "UV_PROJECT": "${containerWorkspaceFolder}/.devcontainer" },