diff --git a/.github/scripts/check_pin_freshness.py b/.github/scripts/check_pin_freshness.py index f901bdc..85ac943 100644 --- a/.github/scripts/check_pin_freshness.py +++ b/.github/scripts/check_pin_freshness.py @@ -109,6 +109,22 @@ def _fetch_json(url: str, token: str) -> dict[str, object] | None: return payload if isinstance(payload, dict) else None +def _action_repo(action: str) -> str: + """Return `owner/repo` for an action string that may carry a sub-path. + + Action references can be `owner/repo` or `owner/repo/path/to/subaction` + (e.g. `github/codeql-action/init`). Only the first two slash-segments + name the GitHub repository — the trailing segments are paths within + the repo's tree (containing per-subaction `action.yml` files). The + REST API endpoint we hit (`/repos///git/...`) only + accepts the `owner/repo` form; passing the full action string would + 404 on every sub-path action and surface as a false-positive + "tag no longer resolves" finding. + """ + parts = action.split("/", 2) + return "/".join(parts[:2]) if len(parts) >= 2 else action + + def _resolve_tag_sha(action: str, tag: str, token: str) -> str | None: """Return the commit SHA the tag points at, or None on missing/error. @@ -117,7 +133,8 @@ def _resolve_tag_sha(action: str, tag: str, token: str) -> str | None: commit. Lightweight tags resolve in one GET (the ref's `object.sha` is the commit directly). """ - ref = _fetch_json(f"{_API_BASE}/repos/{action}/git/refs/tags/{tag}", token) + repo = _action_repo(action) + ref = _fetch_json(f"{_API_BASE}/repos/{repo}/git/refs/tags/{tag}", token) if ref is None: return None obj = ref.get("object") @@ -131,7 +148,7 @@ def _resolve_tag_sha(action: str, tag: str, token: str) -> str | None: return obj_sha if obj_type == "tag": # Annotated tag — dereference to the commit it points at. - annotated = _fetch_json(f"{_API_BASE}/repos/{action}/git/tags/{obj_sha}", token) + annotated = _fetch_json(f"{_API_BASE}/repos/{repo}/git/tags/{obj_sha}", token) if annotated is None: return None inner = annotated.get("object") diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c9bf08..2a22fb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.14" @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.14" @@ -44,7 +44,7 @@ jobs: # Pure in-process tests — completes fast so PR authors get quick feedback. steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.14" @@ -57,7 +57,7 @@ jobs: # Enforces [tool.coverage.report].fail_under from pyproject.toml (75%). steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.14" @@ -69,7 +69,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.14" @@ -84,7 +84,7 @@ jobs: # secret past the first defence layer. steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.14" @@ -218,7 +218,7 @@ jobs: # actual workflow jobs on disk. steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.14" @@ -234,7 +234,7 @@ jobs: # while PR titles fail in CI (or vice versa). steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.14" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a06c133..85c73ad 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -44,12 +44,12 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/eval-nightly.yml b/.github/workflows/eval-nightly.yml index 3b069a9..9020446 100644 --- a/.github/workflows/eval-nightly.yml +++ b/.github/workflows/eval-nightly.yml @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: ${{ inputs.python_version || '3.14' }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 577bef4..60d3df2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: # annotation when a new release lands and you've reviewed the diff. - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 04c6894..8a01886 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -44,7 +44,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.14" diff --git a/pyproject.toml b/pyproject.toml index c1f5158..a01deaa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "harness-python-react" -version = "0.2.9" +version = "0.2.10" description = "Production-quality LLM-driven coding harness — Python (FastAPI) backend, Vite + React + TypeScript frontend." readme = "README.md" requires-python = ">=3.14" diff --git a/tests/test_check_pin_freshness.py b/tests/test_check_pin_freshness.py index 4f62ee2..6aabccb 100644 --- a/tests/test_check_pin_freshness.py +++ b/tests/test_check_pin_freshness.py @@ -76,6 +76,49 @@ def test_resolve_returns_none_on_malformed_payload() -> None: assert cpf._resolve_tag_sha("foo/bar", "v1.0.0", "fake") is None +# ---------- _action_repo (sub-path normalisation) ---------- + + +def test_action_repo_passthrough_for_owner_repo() -> None: + assert cpf._action_repo("actions/checkout") == "actions/checkout" + + +def test_action_repo_strips_subpath() -> None: + """`github/codeql-action/init` → `github/codeql-action` (subpath isn't a repo).""" + assert cpf._action_repo("github/codeql-action/init") == "github/codeql-action" + + +def test_action_repo_strips_deep_subpath() -> None: + """Deeply nested sub-actions still strip back to owner/repo.""" + assert cpf._action_repo("owner/repo/path/to/sub-action") == "owner/repo" + + +def test_resolve_tag_sha_uses_owner_repo_for_subpath_action( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Regression for the false-positive 404 on sub-path actions. + + Before this fix, _resolve_tag_sha passed `github/codeql-action/init` as + the API path segment, hitting `/repos/github/codeql-action/init/...` + which 404s (init is a tree path, not a repo). The audit then reported + `init@v4 — upstream tag no longer resolves` even though `v4` resolves + fine on `github/codeql-action`. + """ + seen_urls: list[str] = [] + + def fake_fetch(url: str, _token: str) -> dict[str, object] | None: + seen_urls.append(url) + return {"object": {"type": "commit", "sha": "deadbeef" * 5}} + + monkeypatch.setattr(cpf, "_fetch_json", fake_fetch) + sha = cpf._resolve_tag_sha("github/codeql-action/init", "v4", "fake") + assert sha == "deadbeef" * 5 + assert ( + seen_urls[0] + == "https://api.github.com/repos/github/codeql-action/git/refs/tags/v4" + ), seen_urls + + # ---------- _check_tag_pin ---------- diff --git a/uv.lock b/uv.lock index 8fe0b4f..cdc1eae 100644 --- a/uv.lock +++ b/uv.lock @@ -328,7 +328,7 @@ wheels = [ [[package]] name = "harness-python-react" -version = "0.2.9" +version = "0.2.10" source = { virtual = "." } dependencies = [ { name = "fastapi" },