Skip to content

Commit ea6b8b1

Browse files
authored
fix: pin-freshness audit normalises sub-path actions before API call (#83)
1 parent 63f5da8 commit ea6b8b1

9 files changed

Lines changed: 77 additions & 17 deletions

File tree

.github/scripts/check_pin_freshness.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,22 @@ def _fetch_json(url: str, token: str) -> dict[str, object] | None:
109109
return payload if isinstance(payload, dict) else None
110110

111111

112+
def _action_repo(action: str) -> str:
113+
"""Return `owner/repo` for an action string that may carry a sub-path.
114+
115+
Action references can be `owner/repo` or `owner/repo/path/to/subaction`
116+
(e.g. `github/codeql-action/init`). Only the first two slash-segments
117+
name the GitHub repository — the trailing segments are paths within
118+
the repo's tree (containing per-subaction `action.yml` files). The
119+
REST API endpoint we hit (`/repos/<owner>/<repo>/git/...`) only
120+
accepts the `owner/repo` form; passing the full action string would
121+
404 on every sub-path action and surface as a false-positive
122+
"tag no longer resolves" finding.
123+
"""
124+
parts = action.split("/", 2)
125+
return "/".join(parts[:2]) if len(parts) >= 2 else action
126+
127+
112128
def _resolve_tag_sha(action: str, tag: str, token: str) -> str | None:
113129
"""Return the commit SHA the tag points at, or None on missing/error.
114130
@@ -117,7 +133,8 @@ def _resolve_tag_sha(action: str, tag: str, token: str) -> str | None:
117133
commit. Lightweight tags resolve in one GET (the ref's `object.sha`
118134
is the commit directly).
119135
"""
120-
ref = _fetch_json(f"{_API_BASE}/repos/{action}/git/refs/tags/{tag}", token)
136+
repo = _action_repo(action)
137+
ref = _fetch_json(f"{_API_BASE}/repos/{repo}/git/refs/tags/{tag}", token)
121138
if ref is None:
122139
return None
123140
obj = ref.get("object")
@@ -131,7 +148,7 @@ def _resolve_tag_sha(action: str, tag: str, token: str) -> str | None:
131148
return obj_sha
132149
if obj_type == "tag":
133150
# Annotated tag — dereference to the commit it points at.
134-
annotated = _fetch_json(f"{_API_BASE}/repos/{action}/git/tags/{obj_sha}", token)
151+
annotated = _fetch_json(f"{_API_BASE}/repos/{repo}/git/tags/{obj_sha}", token)
135152
if annotated is None:
136153
return None
137154
inner = annotated.get("object")

.github/workflows/ci.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
runs-on: ubuntu-latest
1919
steps:
2020
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
21-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
21+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
2222
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
2323
with:
2424
python-version: "3.14"
@@ -31,7 +31,7 @@ jobs:
3131
runs-on: ubuntu-latest
3232
steps:
3333
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
34-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
34+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
3535
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
3636
with:
3737
python-version: "3.14"
@@ -44,7 +44,7 @@ jobs:
4444
# Pure in-process tests — completes fast so PR authors get quick feedback.
4545
steps:
4646
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
47-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
47+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
4848
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
4949
with:
5050
python-version: "3.14"
@@ -57,7 +57,7 @@ jobs:
5757
# Enforces [tool.coverage.report].fail_under from pyproject.toml (75%).
5858
steps:
5959
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
60-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
60+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
6161
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
6262
with:
6363
python-version: "3.14"
@@ -69,7 +69,7 @@ jobs:
6969
runs-on: ubuntu-latest
7070
steps:
7171
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
72-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
72+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
7373
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
7474
with:
7575
python-version: "3.14"
@@ -84,7 +84,7 @@ jobs:
8484
# secret past the first defence layer.
8585
steps:
8686
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
87-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
87+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
8888
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
8989
with:
9090
python-version: "3.14"
@@ -218,7 +218,7 @@ jobs:
218218
# actual workflow jobs on disk.
219219
steps:
220220
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
221-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
221+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
222222
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
223223
with:
224224
python-version: "3.14"
@@ -234,7 +234,7 @@ jobs:
234234
# while PR titles fail in CI (or vice versa).
235235
steps:
236236
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
237-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
237+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
238238
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
239239
with:
240240
python-version: "3.14"

.github/workflows/codeql.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ jobs:
4444
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
4545

4646
- name: Initialize CodeQL
47-
uses: github/codeql-action/init@v3
47+
uses: github/codeql-action/init@v4
4848
with:
4949
languages: ${{ matrix.language }}
5050
build-mode: ${{ matrix.build-mode }}
5151

5252
- name: Perform CodeQL Analysis
53-
uses: github/codeql-action/analyze@v3
53+
uses: github/codeql-action/analyze@v4
5454
with:
5555
category: "/language:${{ matrix.language }}"

.github/workflows/eval-nightly.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
runs-on: ubuntu-latest
4040
steps:
4141
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
42-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
42+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
4343
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
4444
with:
4545
python-version: ${{ inputs.python_version || '3.14' }}

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
# annotation when a new release lands and you've reviewed the diff.
3030
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
3131

32-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
32+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
3333

3434
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
3535
with:

.github/workflows/security.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
runs-on: ubuntu-latest
4545
steps:
4646
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
47-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
47+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
4848
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
4949
with:
5050
python-version: "3.14"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "harness-python-react"
3-
version = "0.2.9"
3+
version = "0.2.10"
44
description = "Production-quality LLM-driven coding harness — Python (FastAPI) backend, Vite + React + TypeScript frontend."
55
readme = "README.md"
66
requires-python = ">=3.14"

tests/test_check_pin_freshness.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,49 @@ def test_resolve_returns_none_on_malformed_payload() -> None:
7676
assert cpf._resolve_tag_sha("foo/bar", "v1.0.0", "fake") is None
7777

7878

79+
# ---------- _action_repo (sub-path normalisation) ----------
80+
81+
82+
def test_action_repo_passthrough_for_owner_repo() -> None:
83+
assert cpf._action_repo("actions/checkout") == "actions/checkout"
84+
85+
86+
def test_action_repo_strips_subpath() -> None:
87+
"""`github/codeql-action/init` → `github/codeql-action` (subpath isn't a repo)."""
88+
assert cpf._action_repo("github/codeql-action/init") == "github/codeql-action"
89+
90+
91+
def test_action_repo_strips_deep_subpath() -> None:
92+
"""Deeply nested sub-actions still strip back to owner/repo."""
93+
assert cpf._action_repo("owner/repo/path/to/sub-action") == "owner/repo"
94+
95+
96+
def test_resolve_tag_sha_uses_owner_repo_for_subpath_action(
97+
monkeypatch: pytest.MonkeyPatch,
98+
) -> None:
99+
"""Regression for the false-positive 404 on sub-path actions.
100+
101+
Before this fix, _resolve_tag_sha passed `github/codeql-action/init` as
102+
the API path segment, hitting `/repos/github/codeql-action/init/...`
103+
which 404s (init is a tree path, not a repo). The audit then reported
104+
`init@v4 — upstream tag no longer resolves` even though `v4` resolves
105+
fine on `github/codeql-action`.
106+
"""
107+
seen_urls: list[str] = []
108+
109+
def fake_fetch(url: str, _token: str) -> dict[str, object] | None:
110+
seen_urls.append(url)
111+
return {"object": {"type": "commit", "sha": "deadbeef" * 5}}
112+
113+
monkeypatch.setattr(cpf, "_fetch_json", fake_fetch)
114+
sha = cpf._resolve_tag_sha("github/codeql-action/init", "v4", "fake")
115+
assert sha == "deadbeef" * 5
116+
assert (
117+
seen_urls[0]
118+
== "https://api.github.com/repos/github/codeql-action/git/refs/tags/v4"
119+
), seen_urls
120+
121+
79122
# ---------- _check_tag_pin ----------
80123

81124

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)