diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml new file mode 100644 index 0000000..bbf8e00 --- /dev/null +++ b/.github/workflows/actionlint.yml @@ -0,0 +1,41 @@ +name: Workflow lint + +# actionlint catches what a YAML parser cannot: duplicate mapping keys that +# make GitHub refuse to load a workflow, invalid ${{ }} expressions, and shell +# problems inside run: blocks. A duplicate env: key silently broke this repo +# family's release workflow once; yaml.safe_load keeps the last value without +# complaining, so local validation passed while Actions rejected the file. +on: + push: + branches: [main] + paths: ['.github/workflows/**'] + pull_request: + paths: ['.github/workflows/**'] + workflow_dispatch: + +permissions: + contents: read + +jobs: + actionlint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.1 + + # Fetched and checksum-verified rather than run as a third-party action, + # so this check adds no new action to the supply chain it exists to guard. + - name: Install actionlint + env: + ACTIONLINT_VERSION: 1.7.12 + ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 + run: | + set -euo pipefail + archive="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" + curl -sSfL -o "$archive" \ + "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/${archive}" + echo "${ACTIONLINT_SHA256} ${archive}" | sha256sum -c - + tar -xzf "$archive" actionlint + install -m 0755 actionlint /usr/local/bin/actionlint + + - name: Lint workflows + run: actionlint -color diff --git a/tests/conftest.py b/tests/conftest.py index 18468c8..24abf89 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,9 @@ import base64 import json import pathlib +import subprocess +import sys +import textwrap import time import pytest @@ -206,3 +209,53 @@ def attestation_report(trust_record: dict) -> dict: "timestamp": trust_record["trace"]["iat"], "cnf_key_x": trust_record["trace"]["cnf"]["jwk"]["x"], } + + +# --- environment guard --- +#: Packages this suite is meant to exercise from source. +_PACKAGES_UNDER_TEST = ("trace_tests",) + +#: Repository root, resolved from this file. +_REPO_ROOT = pathlib.Path(__file__).resolve().parents[1] + +_PROBE = textwrap.dedent( + """ + import importlib, sys + try: + m = importlib.import_module(sys.argv[1]) + except Exception: + print("") + else: + print(getattr(m, "__file__", "") or "") + """ +) + + +def _subprocess_origin(package: str) -> pathlib.Path | None: + """Where a fresh interpreter finds ``package``, or None if it cannot.""" + try: + done = subprocess.run( + [sys.executable, "-c", _PROBE, package], + capture_output=True, + text=True, + timeout=60, + check=False, # a non-zero exit just means "cannot import", handled below + ) + except (OSError, subprocess.SubprocessError): + return None + origin = done.stdout.strip() + return pathlib.Path(origin).resolve() if origin else None + + +def pytest_configure(config: pytest.Config) -> None: + for name in _PACKAGES_UNDER_TEST: + origin = _subprocess_origin(name) + if origin is None: + continue # not importable from a subprocess; nothing can shadow + if _REPO_ROOT not in origin.parents: + raise pytest.UsageError( + f"{name} resolves to {origin} in a subprocess, outside " + f"{_REPO_ROOT}. Any test that shells out would exercise that " + "installed distribution instead of this working tree. Install " + f"editable (pip install -e .) or uninstall the shadowing {name}." + )