Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
is `gittuf`, so the MCP entry that `darnit install` writes gave clients
the gittuf tools. With several frameworks installed and no baseline it
now asks for `--framework` instead of guessing.
- The `gh_api` step no longer calls the platform when its endpoint needs
`$OWNER` or `$REPO` and the checkout has no repository identity (no git
remote). It used to substitute an empty owner, ask for `/repos//name`,
and report the 404 that came back; a step declaring 404 in
`fail_on_status` would have turned that into FAIL. It is now `ERROR`,
class `unavailable`, with a message that says why.
- Installs without the development dependencies work again (`uv tool install`
from a checkout, and any install from a built wheel). Two things broke
them: `tree-sitter-language-pack` 1.6.3, the newest release the old
Expand Down
17 changes: 17 additions & 0 deletions packages/darnit/src/darnit/sieve/builtin_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,23 @@ def gh_api_handler(config: dict[str, Any], context: HandlerContext) -> HandlerRe
message="No endpoint specified for gh_api handler",
error_class="evaluation",
)
# A checkout with no remote has no owner (detect_owner_repo returns "").
# Substituting it would ask the platform about "/repos//name", and the 404
# that comes back says nothing about this repository. It must not be
# reported as a platform answer, and never as a declared failure.
missing = [var for var, val in (("$OWNER", context.owner), ("$REPO", context.repo)) if var in endpoint and not val]
if missing:
message = (
f"Platform API call {endpoint} needs {' and '.join(missing)}, and this checkout has no "
"repository identity (no git remote was found)"
)
_log_environmental_failure(context.control_id, "gh_api", "unavailable", message)
return HandlerResult(
status=HandlerResultStatus.ERROR,
message=message,
evidence={"endpoint": endpoint},
error_class="unavailable",
)
for var, val in (("$OWNER", context.owner), ("$REPO", context.repo), ("$BRANCH", context.default_branch)):
endpoint = endpoint.replace(var, val or "")

Expand Down
31 changes: 31 additions & 0 deletions tests/darnit/sieve/test_gh_api_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,34 @@ def test_expr_evaluated_once_by_handler(self, recorded, caplog) -> None:
)
assert result.status == "PASS"
assert not [r for r in caplog.records if "CEL evaluation failed" in r.getMessage()]


class TestMissingRepositoryIdentity:
"""A checkout with no remote has no owner; the platform is not asked about "/repos//name"."""

@staticmethod
def _run_without_owner(config: dict) -> object:
ctx = HandlerContext(local_path="/tmp", owner="", repo="hello", default_branch="main", control_id="GH-01")
return gh_api_handler({**STEP, **config}, ctx)

def test_no_owner_is_error_and_makes_no_call(self, recorded) -> None:
responder = recorded({})
result = self._run_without_owner({})
assert responder.calls == []
assert result.status == HandlerResultStatus.ERROR
assert result.error_class == "unavailable"
assert "$OWNER" in result.message
assert result.evidence == {"endpoint": STEP["endpoint"]}

def test_no_owner_never_proves_a_declared_failure(self, recorded) -> None:
responder = recorded({"/repos//hello/branches/main/protection": {"status": 404, "error": "HTTP 404: Not Found"}})
result = self._run_without_owner({"fail_on_status": [404]})
assert responder.calls == []
assert result.status == HandlerResultStatus.ERROR

def test_endpoint_that_needs_no_identity_still_runs(self, recorded) -> None:
responder = recorded({"/user": {"status": 200, "body": {}}})
ctx = HandlerContext(local_path="/tmp", owner="", repo="", default_branch="main", control_id="GH-01")
result = gh_api_handler({"handler": "gh_api", "endpoint": "/user"}, ctx)
assert responder.calls == ["/user"]
assert result.status == HandlerResultStatus.PASS
Loading