diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml new file mode 100644 index 0000000..552a805 --- /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@3d3c42e5aac5ba805825da76410c181273ba90b1 # 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/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2ad9f2c..61a6d21 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -76,7 +76,7 @@ jobs: - name: Sign the image (keyless, by digest) env: DIGEST: ${{ steps.build.outputs.digest }} - run: cosign sign --yes ghcr.io/agentrust-io/ca2a-runtime@${DIGEST} + run: cosign sign --yes "ghcr.io/agentrust-io/ca2a-runtime@${DIGEST}" - name: Attest build provenance (SLSA) uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e6f2f53 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,78 @@ +"""Pytest configuration for this suite. + +Guards a silent failure mode rather than testing anything itself. + +pytest puts the source tree on the path, so an in-process import always finds +this tree. A test that shells out does not get that: a plain +``subprocess.run([sys.executable, ...])`` resolves the distribution the normal +way, and with a released wheel also installed it finds site-packages. The +subprocess then exercises a published version while the suite reports a pass, +which is how a tutorial test in this repo family graded against an old schema +and looked green. + +CI installs the package editable, so the subprocess resolves back into the tree +and this check is a no-op there. Locally it turns a wrong answer into a loud one. + +A package that is not importable from a subprocess at all is fine: that is the +path-only setup, not a shadowing install, and the suite still runs. +""" + +from __future__ import annotations + +import pathlib +import subprocess +import sys +import textwrap + +import pytest + +#: Packages this suite is meant to exercise from source. +_PACKAGES_UNDER_TEST = ( + "ca2a_runtime", + "ca2a_verify", +) + +#: 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}." + )