Skip to content

Commit 8170ca1

Browse files
committed
Detect non-main default branches in single-branch CI checkouts
actions/checkout fetches one branch and leaves no origin/HEAD. Since 2.6.6 dropped the git fetch --all that recreated origin/HEAD, default-branch detection fell back to main/master, so scans on repos whose default branch is dev never became the branch head. When origin/HEAD is missing, read the default branch from the GitHub event payload, then from git ls-remote --symref origin HEAD, before the main/master fallback.
1 parent 8bf6f3b commit 8170ca1

6 files changed

Lines changed: 152 additions & 21 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## 2.10.6
4+
5+
### Fixed: default-branch detection in single-branch CI checkouts
6+
7+
- Repositories whose default branch isn't `main` or `master` are detected as the
8+
default branch again when the checkout has no `origin/HEAD`, as with
9+
`actions/checkout`. The CLI reads the default branch from the GitHub event
10+
payload or asks the remote, before falling back to `main`/`master`. Scans on
11+
those branches become the branch head again.
12+
313
## 2.10.5
414

515
### Changed: bump pinned @coana-tech/cli to 15.11.4

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
66

77
[project]
88
name = "socketsecurity"
9-
version = "2.10.5"
9+
version = "2.10.6"
1010
requires-python = ">= 3.11"
1111
license = {"file" = "LICENSE"}
1212
dependencies = [

‎socketsecurity/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__author__ = 'socket.dev'
2-
__version__ = '2.10.5'
2+
__version__ = '2.10.6'
33
USER_AGENT = f'SocketPythonCLI/{__version__}'

‎socketsecurity/core/git_interface.py‎

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import os
23
import re
34
import time
@@ -638,27 +639,72 @@ def get_default_branch_name(self) -> str:
638639
Returns:
639640
Default branch name (e.g., 'main', 'master')
640641
"""
642+
cached = getattr(self, "_default_branch_name", None)
643+
if cached:
644+
return cached
645+
self._default_branch_name = self._detect_default_branch_name()
646+
return self._default_branch_name
647+
648+
def _detect_default_branch_name(self) -> str:
641649
try:
642-
# Try to get the default branch from remote HEAD
643-
remote_head = self.repo.remotes.origin.refs.HEAD
644-
# Extract branch name from refs/remotes/origin/HEAD -> refs/remotes/origin/main
645-
default_branch = str(remote_head.reference).split('/')[-1]
646-
log.debug(f"Default branch detected: {default_branch}")
650+
default_branch = self.repo.remotes.origin.refs.HEAD.reference.remote_head
651+
log.debug(f"Default branch detected from origin/HEAD: {default_branch}")
647652
return default_branch
648653
except Exception as error:
649-
log.debug(f"Could not determine default branch from remote: {error}")
650-
# Fallback: check common default branch names
651-
for branch_name in ['main', 'master']:
652-
try:
653-
if f'origin/{branch_name}' in [str(ref) for ref in self.repo.remotes.origin.refs]:
654-
log.debug(f"Using fallback default branch: {branch_name}")
655-
return branch_name
656-
except Exception:
657-
continue
658-
659-
# Last fallback: assume 'main'
660-
log.debug("Using final fallback default branch: main")
661-
return 'main'
654+
log.debug(f"Could not determine default branch from origin/HEAD: {error}")
655+
656+
# CI checkouts such as actions/checkout fetch a single branch and leave no origin/HEAD.
657+
default_branch = (
658+
self._default_branch_from_github_event()
659+
or self._default_branch_from_remote()
660+
)
661+
if default_branch:
662+
return default_branch
663+
664+
for branch_name in ['main', 'master']:
665+
try:
666+
if f'origin/{branch_name}' in [str(ref) for ref in self.repo.remotes.origin.refs]:
667+
log.debug(f"Using fallback default branch: {branch_name}")
668+
return branch_name
669+
except Exception:
670+
continue
671+
672+
log.debug("Using final fallback default branch: main")
673+
return 'main'
674+
675+
@staticmethod
676+
def _default_branch_from_github_event() -> str | None:
677+
event_path = os.getenv('GITHUB_EVENT_PATH')
678+
if not event_path:
679+
return None
680+
try:
681+
with open(event_path, encoding="utf-8") as event_file:
682+
default_branch = json.load(event_file).get("repository", {}).get("default_branch")
683+
except Exception as error:
684+
log.debug(f"Could not read default branch from GitHub event payload: {error}")
685+
return None
686+
if default_branch:
687+
log.debug(f"Default branch detected from GitHub event payload: {default_branch}")
688+
return default_branch or None
689+
690+
def _default_branch_from_remote(self) -> str | None:
691+
try:
692+
output = self.repo.git.ls_remote(
693+
"--symref",
694+
"origin",
695+
"HEAD",
696+
env={"GIT_TERMINAL_PROMPT": "0"},
697+
kill_after_timeout=30,
698+
)
699+
except Exception as error:
700+
log.debug(f"Could not query origin for its default branch: {error}")
701+
return None
702+
for line in output.splitlines():
703+
match = re.match(r"ref: refs/heads/(\S+)\tHEAD$", line)
704+
if match:
705+
log.debug(f"Default branch detected from origin: {match.group(1)}")
706+
return match.group(1)
707+
return None
662708

663709
def is_commit_on_default_branch(self) -> bool:
664710
"""

‎tests/unit/test_git_interface.py‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"GITHUB_BASE_REF",
1818
"GITHUB_EVENT_BEFORE",
1919
"GITHUB_EVENT_NAME",
20+
"GITHUB_EVENT_PATH",
2021
"GITHUB_HEAD_REF",
2122
"GITHUB_REF",
2223
"GITHUB_SHA",
@@ -375,3 +376,77 @@ def test_unresolvable_base_commit_warns_and_falls_back(
375376
for record in caplog.records
376377
)
377378
fetch.assert_called_once()
379+
380+
381+
@pytest.fixture
382+
def single_branch_checkout(tmp_path):
383+
"""A repo whose default branch is dev, checked out the way actions/checkout does."""
384+
source = tmp_path / "source"
385+
source.mkdir()
386+
_git(source, "init", "-b", "dev")
387+
_git(source, "config", "user.name", "Socket Test")
388+
_git(source, "config", "user.email", "socket@example.com")
389+
(source / "package.json").write_text("{}\n", encoding="utf-8")
390+
_git(source, "add", "package.json")
391+
_git(source, "commit", "-m", "base")
392+
origin = tmp_path / "origin.git"
393+
_git(tmp_path, "clone", "--bare", str(source), str(origin))
394+
395+
checkout = tmp_path / "checkout"
396+
checkout.mkdir()
397+
_git(checkout, "init")
398+
_git(checkout, "remote", "add", "origin", str(origin))
399+
_git(checkout, "fetch", "--no-tags", "--depth=1", "origin", "+refs/heads/dev:refs/remotes/origin/dev")
400+
_git(checkout, "checkout", "-B", "dev", "refs/remotes/origin/dev")
401+
return checkout
402+
403+
404+
def test_single_branch_checkout_detects_non_main_default_branch(
405+
single_branch_checkout, monkeypatch, mocker
406+
):
407+
monkeypatch.setenv("GITHUB_REF", "refs/heads/dev")
408+
mocker.patch.object(Git, "ensure_safe_directory")
409+
410+
repository = Git(str(single_branch_checkout))
411+
412+
assert repository.get_default_branch_name() == "dev"
413+
assert repository.is_default_branch is True
414+
415+
416+
def test_github_event_payload_supplies_default_branch(
417+
single_branch_checkout, tmp_path, monkeypatch, mocker
418+
):
419+
event_path = tmp_path / "event.json"
420+
event_path.write_text('{"repository": {"default_branch": "dev"}}', encoding="utf-8")
421+
monkeypatch.setenv("GITHUB_REF", "refs/heads/dev")
422+
monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_path))
423+
mocker.patch.object(Git, "ensure_safe_directory")
424+
remote_lookup = mocker.patch.object(Git, "_default_branch_from_remote")
425+
426+
repository = Git(str(single_branch_checkout))
427+
428+
assert repository.is_default_branch is True
429+
remote_lookup.assert_not_called()
430+
431+
432+
def test_origin_head_wins_without_remote_lookup(single_branch_checkout, monkeypatch, mocker):
433+
_git(single_branch_checkout, "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/dev")
434+
monkeypatch.setenv("GITHUB_REF", "refs/heads/dev")
435+
mocker.patch.object(Git, "ensure_safe_directory")
436+
remote_lookup = mocker.patch.object(Git, "_default_branch_from_remote")
437+
438+
repository = Git(str(single_branch_checkout))
439+
440+
assert repository.is_default_branch is True
441+
remote_lookup.assert_not_called()
442+
443+
444+
def test_feature_branch_in_single_branch_checkout_is_not_default(
445+
single_branch_checkout, monkeypatch, mocker
446+
):
447+
monkeypatch.setenv("GITHUB_REF", "refs/heads/feature")
448+
mocker.patch.object(Git, "ensure_safe_directory")
449+
450+
repository = Git(str(single_branch_checkout))
451+
452+
assert repository.is_default_branch is False

‎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)