diff --git a/scripts/docker_lint.py b/scripts/docker_lint.py index f1f9d1fa..88ef902e 100755 --- a/scripts/docker_lint.py +++ b/scripts/docker_lint.py @@ -197,15 +197,19 @@ def has_shell_shebang(root: Path, relative_path: str) -> bool: """Report whether a tracked file's shebang directly names bash or sh. Never follows a tracked symlink: `is_symlink()` uses `lstat`, keeping the target unreached. + A read failure raises `CommandFailed` instead of returning `False`. + That keeps a tracked file this cannot open from silently dropping out of the target list. """ path = root / relative_path + if path.is_symlink(): + return False try: - if path.is_symlink(): - return False with path.open("rb") as handle: first_line = handle.readline(256) - except OSError: - return False + except OSError as error: + raise CommandFailed( + f"target discovery failed: could not read {relative_path}: {error}" + ) from error try: text = first_line.decode("utf-8").rstrip("\n") except UnicodeDecodeError: diff --git a/scripts/tests/test_docker_lint.py b/scripts/tests/test_docker_lint.py index 065d020b..12d2fc7a 100755 --- a/scripts/tests/test_docker_lint.py +++ b/scripts/tests/test_docker_lint.py @@ -180,6 +180,14 @@ def test_has_shell_shebang_reports_false_for_a_symlink_without_reading_it(self) with mock.patch.object(Path, "open", side_effect=AssertionError("symlink target opened")): self.assertFalse(docker_lint.has_shell_shebang(self.root, "ops/evil-symlink")) + def test_has_shell_shebang_raises_rather_than_swallowing_a_read_failure(self) -> None: + self.track("ops/unreadable") + with ( + mock.patch.object(Path, "open", side_effect=PermissionError("denied")), + self.assertRaisesRegex(docker_lint.CommandFailed, "could not read"), + ): + docker_lint.has_shell_shebang(self.root, "ops/unreadable") + def test_extensionless_untracked_shebang_script_is_not_picked_up(self) -> None: path = self.root / "ops" / "vps-backup-pull" path.parent.mkdir(parents=True, exist_ok=True)