Repository navigation
Merge pull request #207 from lfreleng-actions/dependabot/github_actio… #553
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| # SPDX-License-Identifier: Apache-2.0 | |
| # SPDX-FileCopyrightText: 2025 The Linux Foundation | |
| # Action test/validation workflow | |
| name: "Action Tests" | |
| # yamllint disable-line rule:truthy | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: ["main"] | |
| pull_request: | |
| branches: ["main"] | |
| paths: | |
| - '**' | |
| - '!.github/**' | |
| - '!.*' | |
| - '!tox.ini' | |
| permissions: {} | |
| # Cancel any in-progress runs of this workflow on the same ref when a new | |
| # commit is pushed. Pull request updates trigger a fresh run, so this keeps | |
| # the queue tidy and ensures only the latest revision's results are visible. | |
| concurrency: | |
| group: "${{ github.workflow }}-${{ github.ref }}" | |
| cancel-in-progress: true | |
| env: | |
| python_version: "3.12" | |
| jobs: | |
| ### Test the GitHub Action in this Repository ### | |
| # | |
| # Runs the action across both x64 and arm64 ubuntu-24.04 runners as a | |
| # regression guard against a previously observed failure mode where | |
| # actions/setup-python on these runners did not reliably bootstrap pip | |
| # for Python 3.12, causing 'No module named pip' errors. The migration | |
| # from pip to uv removes the dependency on actions/setup-python's pip | |
| # provisioning, but we keep both architectures permanently in CI to | |
| # detect any future regression in the underlying setup chain. | |
| # | |
| # Original failure references: | |
| # yamllint disable rule:line-length | |
| # https://github.com/ModeSevenIndustrialSolutions/python-nss-ng/actions/runs/24763417356/job/72452664716 # arm64 | |
| # https://github.com/ModeSevenIndustrialSolutions/python-nss-ng/actions/runs/24763417356/job/72452544137 # x64 | |
| # yamllint enable rule:line-length | |
| standard-tests: | |
| name: "test-python-project (${{ matrix.runs-on }})" | |
| runs-on: ${{ matrix.runs-on }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| runs-on: | |
| - ubuntu-24.04 | |
| - ubuntu-24.04-arm | |
| permissions: | |
| contents: read | |
| timeout-minutes: 12 | |
| steps: | |
| # Harden the runner used by this workflow | |
| # yamllint disable-line rule:line-length | |
| - uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 | |
| with: | |
| egress-policy: 'audit' | |
| - name: "Checkout repository" | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # Perform setup prior to running test(s) | |
| - name: "Checkout sample project repository" | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: "lfreleng-actions/test-python-project" | |
| path: "test-python-project" | |
| persist-credentials: false | |
| # Build sample Python project | |
| - name: "Build Python project" | |
| id: build | |
| # yamllint disable-line rule:line-length | |
| uses: lfreleng-actions/python-build-action@97cf36d2a5914c94761408545d154c983024de1d # v2.0.0 | |
| with: | |
| path_prefix: "test-python-project" | |
| tox_build: false | |
| # Disambiguate artefact names across matrix legs (ubuntu-24.04 | |
| # and ubuntu-24.04-arm) to avoid actions/upload-artifact 409 | |
| # Conflict errors when both legs upload concurrently. | |
| artefact_name: "lfreleng-test-python-project-${{ matrix.runs-on }}" | |
| # Perform Python project standard tests | |
| - name: "Run action: ${{ github.repository }} [PYTEST]" | |
| uses: ./ | |
| with: | |
| python_version: ${{ env.python_version }} | |
| path_prefix: "test-python-project" | |
| tests_path: "tests" | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| # Perform Python project failing tests | |
| - name: "Run action: ${{ github.repository }} [Test Failure]" | |
| uses: ./ | |
| with: | |
| python_version: ${{ env.python_version }} | |
| path_prefix: "test-python-project" | |
| report_artefact: false | |
| # Test permit failure using action input | |
| tests_path: "tests_fail" | |
| permit_fail: true | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| # Perform Python project failing tests | |
| - name: "Run action: ${{ github.repository }} [Test Failure]" | |
| id: tests-fail | |
| uses: ./ | |
| continue-on-error: true | |
| with: | |
| python_version: ${{ env.python_version }} | |
| path_prefix: "test-python-project" | |
| report_artefact: false | |
| # Test permit failure using action input | |
| tests_path: "tests_fail" | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: "Validate previous step failure" | |
| if: steps.tests-fail.outcome == 'success' | |
| shell: bash | |
| run: | | |
| # Check previous step failure | |
| echo "Error: previous step should have failed ❌" | |
| exit 1 | |
| ### Coverage configuration regression matrix ### | |
| # | |
| # Runs the action against a curated set of small fixtures under | |
| # .fixtures/, each shaped to exercise one cell of the | |
| # (coverage source list × addopts --cov) matrix. Every fixture must | |
| # produce non-zero coverage; a regression that breaks the action's | |
| # decision about when to inject '--cov' (issue #138) will silently | |
| # report 0% for at least one cell, which this job then surfaces as | |
| # a hard failure. | |
| # | |
| # The full rationale and fixture-by-fixture explanation lives in | |
| # .fixtures/README.md. | |
| coverage-fixtures: | |
| name: "coverage-fixtures (${{ matrix.fixture }})" | |
| runs-on: ubuntu-24.04 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| fixture: | |
| # The exact regression case from issue #138: src/ layout + | |
| # addopts '--cov=mypkg' + path-based source list. Under the | |
| # broken v1.1.0 behaviour this reported 0% coverage despite | |
| # all tests passing. | |
| - src-layout-path-source | |
| # The recommended shape: package-name source list. Works | |
| # for both editable and non-editable installs because | |
| # coverage.py resolves the location via import. | |
| - src-layout-pkg-source | |
| # Zero-configuration case. The action derives the package | |
| # import name from [project].name and injects '--cov=<pkg>' | |
| # so collection works for non-editable installs (a bare | |
| # '--cov' would not, since coverage.py's default rule | |
| # excludes site-packages). Falls back to a bare '--cov' | |
| # only when [project].name cannot be extracted. | |
| - flat-layout-no-config | |
| # Middle-ground case: '--cov=mypkg' in addopts only, no | |
| # source list. The detection regex must spot the addopts | |
| # entry and skip the '--cov' injection. | |
| - addopts-cov-only | |
| permissions: | |
| contents: read | |
| timeout-minutes: 8 | |
| steps: | |
| # Harden the runner used by this workflow | |
| # yamllint disable-line rule:line-length | |
| - uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 | |
| with: | |
| egress-policy: 'audit' | |
| - name: "Checkout repository" | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # Run the action under test against the current fixture. | |
| # 'editable' is left at its default (false) so the package | |
| # executes from site-packages/ rather than from src/ - this | |
| # is the install layout that originally exposed issue #138. | |
| - name: "Run action against fixture: ${{ matrix.fixture }}" | |
| uses: ./ | |
| with: | |
| python_version: ${{ env.python_version }} | |
| path_prefix: ".fixtures/${{ matrix.fixture }}" | |
| tests_path: "tests" | |
| # yamllint disable-line rule:line-length | |
| artefact_name: "coverage-fixtures-${{ matrix.fixture }}-coverage" | |
| report_artefact: "true" | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| # Assert the produced coverage.xml reports non-zero coverage. | |
| # Tests passing on its own is not sufficient evidence that the | |
| # action wired pytest-cov correctly: the issue #138 regression | |
| # produced 'all tests passed, 0% coverage' which would slip | |
| # past a tests-only assertion. Reading the XML directly avoids | |
| # any dependency on the action's own log output format. | |
| - name: "Assert non-zero coverage from fixture" | |
| shell: bash | |
| env: | |
| PYTHON_VERSION: ${{ env.python_version }} | |
| FIXTURE: ${{ matrix.fixture }} | |
| run: | | |
| # Assert non-zero coverage from fixture | |
| set -euo pipefail | |
| # The action stores the XML at /tmp/coverage-py<XY>/coverage.xml | |
| # where <XY> is the python_version with dots stripped. Build | |
| # the same path here rather than depending on action outputs | |
| # so the assertion is decoupled from action implementation | |
| # details. | |
| py_xy="$(echo "${PYTHON_VERSION}" | tr -d '.')" | |
| xml="/tmp/coverage-py${py_xy}/coverage.xml" | |
| if [ ! -f "$xml" ]; then | |
| echo "Error: coverage XML not found at $xml ❌" | |
| ls -la /tmp/coverage-* 2>/dev/null || true | |
| exit 1 | |
| fi | |
| # Parse and validate the line-rate inside Python so the | |
| # comparison is a real numeric check (float > 0). A shell | |
| # 'case' on the raw string would only catch a fixed set of | |
| # zero-spellings ('0', '0.0', '0.00', ...) and would let | |
| # an unexpected formatting like '0.0000' or scientific | |
| # notation slip through, defeating the purpose of the | |
| # assertion. The fixtures are constructed so that 100% is | |
| # achievable, but we only require strictly > 0 here so the | |
| # assertion is not coupled to fixture details that may | |
| # legitimately change. | |
| python3 - "$xml" "$FIXTURE" <<'PY' | |
| import sys | |
| import xml.etree.ElementTree as ET | |
| xml_path, fixture = sys.argv[1], sys.argv[2] | |
| root = ET.parse(xml_path).getroot() | |
| rate_str = root.get('line-rate') | |
| if rate_str is None: | |
| print(f'FAIL: line-rate attribute missing in {xml_path}', | |
| file=sys.stderr) | |
| sys.exit(1) | |
| try: | |
| rate = float(rate_str) | |
| except ValueError: | |
| print( | |
| f'FAIL: line-rate {rate_str!r} not parseable as float', | |
| file=sys.stderr) | |
| sys.exit(1) | |
| print(f'Fixture: {fixture}') | |
| print(f'Coverage XML: {xml_path}') | |
| print(f'line-rate: {rate_str} (parsed: {rate})') | |
| if rate <= 0: | |
| print( | |
| f'FAIL: coverage line-rate is {rate} for {fixture} ❌', | |
| file=sys.stderr) | |
| print( | |
| 'This indicates the action did not configure ' | |
| 'pytest-cov correctly for this project shape.', | |
| file=sys.stderr) | |
| print( | |
| 'See .fixtures/README.md and issue #138.', | |
| file=sys.stderr) | |
| sys.exit(1) | |
| print(f'OK: {fixture} reported non-zero coverage ✅') | |
| PY | |
| ### detect_coverage.py unit test suite ### | |
| # | |
| # The detection logic that decides whether to inject '--cov', what | |
| # fallback package name to derive, and (since issue #138 follow-up) | |
| # whether the consumer's omit list excludes the install location | |
| # lives in 'scripts/detect_coverage.py'. The 'coverage-fixtures' | |
| # job above exercises each shape end-to-end by running the entire | |
| # action, but that path is expensive (per-fixture venv, real pytest, | |
| # XML parse) and slow to iterate on. The unit-test job below runs | |
| # the same script against the same fixtures plus a handful of | |
| # synthetic edge cases in well under a second, catching detection | |
| # regressions long before they could ship through the matrix job. | |
| detect-coverage-unit-tests: | |
| name: "detect_coverage.py unit tests" | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| timeout-minutes: 5 | |
| steps: | |
| # Harden the runner used by this workflow | |
| # yamllint disable-line rule:line-length | |
| - uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 | |
| with: | |
| egress-policy: 'audit' | |
| - name: "Checkout repository" | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # Use the same uv-managed interpreter the action itself uses | |
| # in production. Pinning to env.python_version keeps the unit | |
| # tests on the same Python the rest of this workflow runs | |
| # against; the script must work under 3.10-3.14 (and the | |
| # tomllib/tomli fallback for the latter) but a single version | |
| # is enough to catch logic regressions here - the matrix job | |
| # is what guarantees runtime coverage across the version grid. | |
| # | |
| # 'activate-environment: true' tells setup-uv to create a | |
| # venv at .venv/ under the workspace and prepend its bin/ | |
| # to PATH (also exporting VIRTUAL_ENV). We need that here | |
| # because Ubuntu 24.04's system Python is PEP-668 | |
| # externally-managed, so 'uv pip install --system' refuses | |
| # to install pytest. With the venv active, an unqualified | |
| # 'uv pip install' targets the venv's interpreter and 'python | |
| # -m pytest' resolves to the same one. | |
| - name: "Set up uv" | |
| # yamllint disable-line rule:line-length | |
| uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 | |
| with: | |
| python-version: ${{ env.python_version }} | |
| activate-environment: true | |
| enable-cache: true | |
| - name: "Install pytest" | |
| shell: bash | |
| run: | | |
| # Install pytest into the uv-activated venv so 'python | |
| # -m pytest' below resolves to the same interpreter. | |
| uv pip install pytest | |
| - name: "Run detect_coverage.py unit tests" | |
| shell: bash | |
| run: | | |
| # The tests live under tests/ at the repo root; the test | |
| # module prepends scripts/ to sys.path so it can import | |
| # detect_coverage directly. Run via 'python -m pytest' to | |
| # keep import resolution consistent with how the action | |
| # itself invokes the script. | |
| python -m pytest tests/test_detect_coverage.py -v | |
| ### Coverage 'omit excludes install' warning fixture ### | |
| # | |
| # The 'omit-excludes-install' fixture is structurally different | |
| # from the four cells in the coverage-fixtures matrix: it ships a | |
| # config that the action cannot rescue (the broken omit list masks | |
| # all measurement no matter what flags are passed), so the | |
| # invariant being checked is the WARNING, not the coverage value. | |
| # | |
| # The action emits the warning to console AND to | |
| # $GITHUB_STEP_SUMMARY, but $GITHUB_STEP_SUMMARY is a per-step | |
| # file (each step gets a fresh path; the runner concatenates them | |
| # at job-end into the published summary), so a follow-up | |
| # assertion step cannot read what an earlier step wrote there. | |
| # We therefore verify the warning channel via two surfaces that | |
| # DO survive across steps: | |
| # | |
| # 1. The KEY=VALUE pairs the action wrote to $GITHUB_ENV in | |
| # the 'Detect coverage configuration' step | |
| # ('coverage_omit_excludes_install', | |
| # 'coverage_problematic_omit_patterns'). If those are set | |
| # correctly it proves the bash template reached the warning | |
| # branch without crashing on a quoting accident. | |
| # 2. The generated coverage.xml's 'line-rate' attribute, which | |
| # must be exactly 0.0 for this fixture's pyproject.toml (it | |
| # reproduces the bug). A future change that silently 'fixed' | |
| # omit handling would also be caught here. | |
| # | |
| # The exact rendered markdown that goes into the step summary is | |
| # covered by the detect_coverage.py unit tests above (which check | |
| # the KEY=VALUE contract verbatim) and by manual inspection of | |
| # the per-job summary published in the GitHub UI. | |
| # | |
| # We intentionally leave coverage's '--cov-fail-under' unset in the | |
| # fixture's pyproject.toml so that 0% coverage does NOT fail the | |
| # action's pytest step - otherwise we'd be testing the failure | |
| # mode rather than the warning channel. | |
| coverage-omit-warning: | |
| name: "coverage-omit-warning fixture" | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| timeout-minutes: 8 | |
| steps: | |
| # Harden the runner used by this workflow | |
| # yamllint disable-line rule:line-length | |
| - uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 | |
| with: | |
| egress-policy: 'audit' | |
| - name: "Checkout repository" | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # Run the action against the omit fixture. editable left at | |
| # its default 'false' - that is the install layout that | |
| # triggers the regression in real consumers, and the warning | |
| # only fires for non-editable installs. | |
| - name: "Run action against fixture: omit-excludes-install" | |
| uses: ./ | |
| with: | |
| python_version: ${{ env.python_version }} | |
| path_prefix: ".fixtures/omit-excludes-install" | |
| tests_path: "tests" | |
| # yamllint disable-line rule:line-length | |
| artefact_name: "coverage-omit-warning-coverage" | |
| report_artefact: "true" | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| # Assert the detection step set the omit signals correctly. | |
| # These env vars survive across steps because the action | |
| # writes them via $GITHUB_ENV. Reaching the assertion below | |
| # with the booleans flipped (or the patterns string empty) | |
| # would mean either detect_coverage.py regressed or the | |
| # action's bash failed to forward them - either is a hard | |
| # fail. | |
| - name: "Assert omit detection signals propagated to env" | |
| shell: bash | |
| run: | | |
| # Verify env vars from the action's detection step | |
| set -euo pipefail | |
| echo "coverage_omit_excludes_install=" \ | |
| "${coverage_omit_excludes_install:-<unset>}" | |
| echo "coverage_problematic_omit_patterns=" \ | |
| "${coverage_problematic_omit_patterns:-<unset>}" | |
| if [ "${coverage_omit_excludes_install:-false}" != "true" ]; then | |
| echo "Error: coverage_omit_excludes_install was not 'true' ❌" >&2 | |
| echo "The action's detect_coverage.py step did not flag" >&2 | |
| echo "the fixture's '*/.venv/*' omit entry." >&2 | |
| exit 1 | |
| fi | |
| # The fixture sets exactly one offending pattern; assert | |
| # both that the variable is non-empty and that the | |
| # specific pattern is present. ';' is the documented | |
| # separator (see scripts/detect_coverage.py main()). | |
| if [ -z "${coverage_problematic_omit_patterns:-}" ]; then | |
| echo "Error: coverage_problematic_omit_patterns is empty ❌" >&2 | |
| exit 1 | |
| fi | |
| case ";${coverage_problematic_omit_patterns};" in | |
| *";*/.venv/*;"*) | |
| ;; | |
| *) | |
| echo "Error: '*/.venv/*' missing from patterns ❌" >&2 | |
| echo "Got: ${coverage_problematic_omit_patterns}" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| echo "OK: omit signals propagated correctly ✅" | |
| # Reproducing the bug is the whole point of this fixture, so | |
| # assert that the action did NOT manage to collect coverage. | |
| # If a future change to the action somehow caused omit | |
| # patterns to be ignored (or to be applied differently), | |
| # this assertion would surface that before the warning | |
| # channel stopped being relevant. | |
| # | |
| # When pytest-cov collects no data (which is exactly what | |
| # the broken omit list causes), it skips XML emission | |
| # entirely - the file simply does not exist. coverage.py | |
| # 7.x prints 'No data was collected.' and pytest-cov | |
| # follows up with 'Failed to generate report: No data to | |
| # report.', then writes nothing. So 'no XML at all' is the | |
| # primary symptom of the bug. We also accept the secondary | |
| # case where some pytest-cov / coverage.py future combination | |
| # might write an XML with line-rate=0 instead of skipping it, | |
| # so the assertion stays robust if that behaviour changes. | |
| - name: "Assert fixture reproduces the 0% coverage failure" | |
| shell: bash | |
| env: | |
| PYTHON_VERSION: ${{ env.python_version }} | |
| run: | | |
| # Confirm coverage was not collected - the bug is reproduced | |
| set -euo pipefail | |
| py_xy="$(echo "${PYTHON_VERSION}" | tr -d '.')" | |
| xml="/tmp/coverage-py${py_xy}/coverage.xml" | |
| if [ ! -f "$xml" ]; then | |
| echo "OK: bug reproduced - no coverage XML written ✅" | |
| echo "(pytest-cov collected no data; the omit glob" \ | |
| "masked everything, so nothing to report)." | |
| exit 0 | |
| fi | |
| # XML exists - belt-and-braces check that line-rate is | |
| # zero. Any value > 0 means coverage actually got | |
| # measured, which would mean the omit list is no longer | |
| # excluding the install location and the fixture has | |
| # stopped exercising the regression. | |
| python3 - "$xml" <<'PY' | |
| import sys | |
| import xml.etree.ElementTree as ET | |
| xml_path = sys.argv[1] | |
| root = ET.parse(xml_path).getroot() | |
| rate = float(root.get('line-rate') or '0') | |
| print(f'line-rate: {rate}') | |
| if rate != 0.0: | |
| print( | |
| f'FAIL: expected zero coverage to reproduce the ' | |
| f'omit-excludes-install bug, got {rate} ❌', | |
| file=sys.stderr) | |
| print( | |
| 'The omit pattern no longer excludes the install ' | |
| 'location; either coverage.py changed pattern ' | |
| 'matching, or the fixture needs updating.', | |
| file=sys.stderr) | |
| sys.exit(1) | |
| print('OK: bug reproduced (XML exists with line-rate=0) ✅') | |
| PY | |
| ### Real-world regression matrix: python-nss-ng ### | |
| # | |
| # Runs the action against the full python-nss-ng repository across | |
| # both x64 and arm64 runners and the full supported Python matrix | |
| # (3.10 - 3.14). python-nss-ng is one of the more complex Python | |
| # projects we exercise: it ships C extensions linking against | |
| # system NSS/NSPR libraries, builds via meson-python, and historically | |
| # surfaced two distinct classes of bug in this action that simpler | |
| # fixtures missed: | |
| # | |
| # 1. pip -> uv interpreter mismatch on real packages with | |
| # [test,dev] extras + meson-python build | |
| # (uv 0.11.x mis-detected the interpreter's patch version, | |
| # installing pytest into a site-packages path the running | |
| # python could not find). | |
| # 2. setup-python vs setup-uv patch-version disagreement on | |
| # Ubuntu noble + Python 3.12, producing a venv whose binary | |
| # advertised one patch level while loading stdlib from another; | |
| # this crashed CPython inside LogRecord.__init__. | |
| # | |
| # python-nss-ng is pinned to a known-good commit so that any | |
| # regression detected here is attributable to changes in this | |
| # action rather than upstream churn. Bump the pin deliberately | |
| # when validating against a newer python-nss-ng baseline. | |
| # | |
| # This job runs in parallel to the lightweight 'standard-tests' job | |
| # above and is intentionally heavier (full Python matrix, native | |
| # build, apt-installed NSS dependencies). It is not a replacement. | |
| regression-tests: | |
| # yamllint disable-line rule:line-length | |
| name: "python-nss-ng (${{ matrix.runs-on }} / Py${{ matrix.python-version }})" | |
| runs-on: ${{ matrix.runs-on }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| runs-on: | |
| - ubuntu-24.04 | |
| - ubuntu-24.04-arm | |
| python-version: | |
| - "3.10" | |
| - "3.11" | |
| - "3.12" | |
| - "3.13" | |
| - "3.14" | |
| permissions: | |
| contents: read | |
| timeout-minutes: 25 | |
| env: | |
| # Pinned python-nss-ng commit. Update this when validating | |
| # against a newer baseline of the downstream project. | |
| NSS_NG_REPO: "ModeSevenIndustrialSolutions/python-nss-ng" | |
| NSS_NG_REF: "01543d576ed845b22edc0369182526e64e078331" | |
| steps: | |
| # Harden the runner used by this workflow | |
| # yamllint disable-line rule:line-length | |
| - uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 | |
| with: | |
| egress-policy: 'audit' | |
| - name: "Checkout repository (action under test)" | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| path: "action" | |
| persist-credentials: false | |
| - name: "Checkout python-nss-ng at pinned commit" | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: "${{ env.NSS_NG_REPO }}" | |
| ref: "${{ env.NSS_NG_REF }}" | |
| path: "python-nss-ng" | |
| persist-credentials: false | |
| - name: "Install NSS/NSPR system dependencies" | |
| working-directory: "python-nss-ng" | |
| shell: bash | |
| run: | | |
| # Install apt-level NSS/NSPR build deps via the project's | |
| # own Makefile target. Python-level deps are deliberately | |
| # left to the action under test. | |
| make deps-nss deps-test-system env-github-actions | |
| # Refresh ldconfig cache so libnss/libnspr are discoverable | |
| sudo ldconfig | |
| # Export multi-arch library path for downstream steps. | |
| LIB_PATH="/usr/lib" | |
| LIB_PATH="${LIB_PATH}:/usr/lib/x86_64-linux-gnu" | |
| LIB_PATH="${LIB_PATH}:/usr/lib/aarch64-linux-gnu" | |
| echo "LD_LIBRARY_PATH=${LIB_PATH}" >> "$GITHUB_ENV" | |
| - name: "Verify NSS libraries are discoverable" | |
| shell: bash | |
| run: | | |
| # Verify NSS libraries are discoverable | |
| echo "=== Library Path Diagnostics ===" | |
| echo "LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" | |
| echo "" | |
| echo "=== ldconfig cache (NSS / NSPR) ===" | |
| ldconfig -p | grep -E '(nss|nspr)' || \ | |
| echo "NSS/NSPR not in ldconfig cache ⚠️" | |
| echo "" | |
| echo "=== pkg-config ===" | |
| pkg-config --exists nss && \ | |
| echo "NSS: $(pkg-config --modversion nss) ✅" || \ | |
| echo "NSS not found by pkg-config ⚠️" | |
| pkg-config --exists nspr && \ | |
| echo "NSPR: $(pkg-config --modversion nspr) ✅" || \ | |
| echo "NSPR not found by pkg-config ⚠️" | |
| # Build python-nss-ng's meson tree before running pytest. | |
| # | |
| # python-nss-ng's test suite contains bare imports such as | |
| # 'from deprecations import ...' / 'from nss_context import ...' | |
| # / 'from secure_logging import ...' at module scope (e.g. in | |
| # test/test_deprecations.py, test/test_secure_logging.py, | |
| # test/test_zzz_nss_context.py). Those modules live at | |
| # src/{deprecations,nss_context,secure_logging}.py and meson | |
| # installs them under the 'nss' subpackage at install time, so | |
| # they are NOT importable as top-level names from a plain | |
| # 'uv pip install' of the project. | |
| # | |
| # The project's own conftest.py works around this by inserting | |
| # the meson 'build/lib.<platform>-<X.Y>' directory into sys.path | |
| # via util.get_build_dir() - but that directory only exists if | |
| # an in-tree meson build has been performed. Without it, pytest | |
| # collection fails with ModuleNotFoundError on those three test | |
| # modules. | |
| # | |
| # python-nss-ng's own CI runs lfreleng-actions/python-build-action | |
| # before invoking python-test-action, which produces the build | |
| # tree as a side effect. Mirror that here so the regression job | |
| # exercises the same end-to-end shape consumers actually use. | |
| - name: "Build python-nss-ng (meson) Py${{ matrix.python-version }}" | |
| # yamllint disable-line rule:line-length | |
| uses: lfreleng-actions/python-build-action@97cf36d2a5914c94761408545d154c983024de1d # v2.0.0 | |
| with: | |
| path_prefix: "python-nss-ng" | |
| python_version: "${{ matrix.python-version }}" | |
| # Build via the project's Makefile so meson is invoked the | |
| # same way python-nss-ng's own CI invokes it. NSS apt deps | |
| # are installed in the previous step, but re-running the | |
| # idempotent target here keeps env-github-actions in sync. | |
| make: "true" | |
| make_args: "deps-nss env-github-actions" | |
| # The action under test handles its own coverage/test | |
| # artefact uploads; the build artefact is not needed here. | |
| # yamllint disable-line rule:line-length | |
| artefact_name: "regression-nss-ng-build-${{ matrix.runs-on }}-py${{ matrix.python-version }}" | |
| - name: "Run action: python-nss-ng Py${{ matrix.python-version }}" | |
| # Use the local checkout of the action under test rather than | |
| # 'uses: ./' at workflow root, since the workflow root holds | |
| # python-nss-ng's checkout in this job. | |
| # | |
| # PYTHONPATH note: python-nss-ng/test/{test_deprecations, | |
| # test_secure_logging,test_zzz_nss_context}.py contain bare | |
| # top-level imports ('from deprecations import ...' etc.) | |
| # whose targets live at python-nss-ng/src/*.py. The project's | |
| # own conftest.py works around this by walking up from CWD | |
| # looking for 'meson.build' + 'build/' and inserting the | |
| # discovered build dir into sys.path. That walk only succeeds | |
| # when CWD == project root; in this regression job CWD is | |
| # $GITHUB_WORKSPACE and meson.build lives one level below in | |
| # python-nss-ng/, so the walk never finds it. | |
| # | |
| # Setting PYTHONPATH to python-nss-ng/src directly makes the | |
| # bare imports resolve from source. We deliberately point at | |
| # 'src' rather than at the meson build dir because 'src' is | |
| # the canonical location and is always present after checkout. | |
| # | |
| uses: ./action | |
| env: | |
| PYTHONPATH: "${{ github.workspace }}/python-nss-ng/src" | |
| with: | |
| python_version: "${{ matrix.python-version }}" | |
| path_prefix: "python-nss-ng" | |
| tests_path: "test" | |
| # yamllint disable-line rule:line-length | |
| artefact_name: "regression-nss-ng-${{ matrix.runs-on }}-py${{ matrix.python-version }}-coverage" | |
| report_artefact: "true" | |
| github_token: "${{ secrets.GITHUB_TOKEN }}" | |
| - name: "Post-test diagnostics (on failure)" | |
| if: failure() | |
| shell: bash | |
| run: | | |
| # Post-test diagnostics | |
| # Surface enough state to triage interpreter / install / | |
| # link-time issues without rerunning the job manually. | |
| echo "=== POST-TEST DIAGNOSTICS ===" | |
| echo "Active VIRTUAL_ENV: ${VIRTUAL_ENV:-<unset>}" | |
| echo "python on PATH: $(command -v python || true)" | |
| if command -v python >/dev/null 2>&1; then | |
| python - <<'PY' || true | |
| import sys | |
| print(f"sys.executable = {sys.executable}") | |
| print(f"sys.version = {sys.version}") | |
| print(f"sys.prefix = {sys.prefix}") | |
| print(f"sys.base_prefix = {sys.base_prefix}") | |
| PY | |
| fi | |
| # The action provisions/activates the venv at workspace root | |
| # (.venv), not under path_prefix. Resolve the actual venv via | |
| # $VIRTUAL_ENV so this diagnostic reports useful state | |
| # regardless of where the action created it. | |
| pyvenv_cfg="${VIRTUAL_ENV:+$VIRTUAL_ENV/pyvenv.cfg}" | |
| if [ -z "$pyvenv_cfg" ]; then | |
| pyvenv_cfg=".venv/pyvenv.cfg" | |
| fi | |
| if [ -f "$pyvenv_cfg" ]; then | |
| echo "---- $pyvenv_cfg ----" | |
| cat "$pyvenv_cfg" | |
| echo "----------------------------------------" | |
| else | |
| echo "No pyvenv.cfg found at $pyvenv_cfg" | |
| fi | |
| echo "" | |
| echo "=== Installed nss/python-nss-ng artefacts ===" | |
| # The action does not install pip into the venv (uv replaces | |
| # it), so 'python -m pip' is not always available. Detect | |
| # availability first so a missing-pip case produces a | |
| # meaningful message rather than masking as a missing package. | |
| if command -v python >/dev/null 2>&1; then | |
| if python -m pip --version >/dev/null 2>&1; then | |
| python -m pip show python-nss-ng || \ | |
| echo "python-nss-ng not visible to pip" | |
| else | |
| echo "pip is not available; falling back to importlib" | |
| python - <<'PY' || true | |
| import importlib.metadata as md | |
| try: | |
| dist = md.distribution("python-nss-ng") | |
| print(f"python-nss-ng {dist.version} at {dist.locate_file('')}") | |
| except md.PackageNotFoundError: | |
| print("python-nss-ng not visible via importlib.metadata") | |
| PY | |
| fi | |
| fi | |
| echo "" | |
| echo "=== ldd of any built nss .so files ===" | |
| find python-nss-ng -name "*.so" 2>/dev/null | while read -r so; do | |
| echo "--- $so ---" | |
| ldd "$so" 2>&1 | grep -E "(nss|nspr|not found)" || true | |
| done |