Skip to content

Propagate has_shell_shebang's Read Failures Instead of Swallowing Them - #956

Merged
ptr727 merged 2 commits into
developfrom
propagate-read-failure
Aug 23, 2026
Merged

Propagate has_shell_shebang's Read Failures Instead of Swallowing Them#956
ptr727 merged 2 commits into
developfrom
propagate-read-failure

Conversation

@ptr727

@ptr727ptr727 commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Propagate has_shell_shebang's Read Failures Instead of Swallowing Them

Fixes one real finding from coderabbitai on PR #952 (declines the
other; see below).

Propagate shell-file read failures

has_shell_shebang caught every OSError from path.open()/
readline() and returned False, the same value it returns for a
file that legitimately isn't a shell script. Unlike the CI bash side
(where read fails at true EOF even after filling the variable),
Python's readline() never raises for EOF, an empty read is just
b'' with no exception, so every OSError this caught was a genuine
failure (permission denied, the file vanishing between git ls-files
and the read, disk I/O). Swallowing it meant a tracked file this
couldn't open silently dropped out of the lint target list, and
lint() could report success having never actually checked it.

  • scripts/docker_lint.py: has_shell_shebang now raises
    CommandFailed on a genuine read OSError, matching the pattern
    ls_files already uses for its own I/O failures. The deliberate
    False cases (a symlink, invalid UTF-8) are unchanged.
  • scripts/tests/test_docker_lint.py: added
    test_has_shell_shebang_raises_rather_than_swallowing_a_read_failure,
    confirming a mocked PermissionError surfaces as CommandFailed
    instead of a silent False.

Declined: reject symlinks in every shell-discovery path

The *.sh-glob-matched branch (ls_files(root, linter.patterns))
never reads file content on the host at all, before or after this
chain's own symlink fix (#955): it only builds a path list and passes
it to docker run ... -- files. Confirmed empirically that a symlink
processed inside the container cannot escape to the host filesystem
regardless of target: docker run -v "$PWD":/mnt alpine sh -c 'cat /mnt/link-to-etc-shadow' reads the container's own /etc/shadow
(byte-identical to reading it directly), and a symlink to a real host
tmp file that exists on the host but not in the container's own
filesystem tree fails with "No such file or directory" (i.e., the
container's own root, not the host's, is what a bind-mounted symlink
resolves against). The host-side read this chain actually guards
against is specific to extensionless_shell_scripts' shebang peek,
which already rejects symlinks (#955); the glob-matched branch has no
equivalent host-side read to guard.

Verified

Full test suite (798 tests), ruff, mypy, repo_gate.py,
prose_lint.py --diff origin/develop, and the complete
docker_lint.py run (all 7 linters) all pass clean.

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of symbolic links during shell-file detection.
    • File read failures now report a clear error with the affected path instead of being silently ignored.
  • Tests

    • Added coverage to verify that permission-related read failures are surfaced correctly.

Fixes one real finding from coderabbitai on PR #952 (declines the
other; see below).
## Propagate shell-file read failures
`has_shell_shebang` caught every `OSError` from `path.open()`/
`readline()` and returned `False`, the same value it returns for a
file that legitimately isn't a shell script. Unlike the CI bash side
(where `read` fails at true EOF even after filling the variable),
Python's `readline()` never raises for EOF, an empty read is just
`b''` with no exception, so every `OSError` this caught was a genuine
failure (permission denied, the file vanishing between `git ls-files`
and the read, disk I/O). Swallowing it meant a tracked file this
couldn't open silently dropped out of the lint target list, and
`lint()` could report success having never actually checked it.
- `scripts/docker_lint.py`: `has_shell_shebang` now raises
`CommandFailed` on a genuine read `OSError`, matching the pattern
`ls_files` already uses for its own I/O failures. The deliberate
`False` cases (a symlink, invalid UTF-8) are unchanged.
- `scripts/tests/test_docker_lint.py`: added
`test_has_shell_shebang_raises_rather_than_swallowing_a_read_failure`,
confirming a mocked `PermissionError` surfaces as `CommandFailed`
instead of a silent `False`.
## Declined: reject symlinks in every shell-discovery path
The `*.sh`-glob-matched branch (`ls_files(root, linter.patterns)`)
never reads file content on the host at all, before or after this
chain's own symlink fix (#955): it only builds a path list and passes
it to `docker run ... -- files`. Confirmed empirically that a symlink
processed *inside* the container cannot escape to the host filesystem
regardless of target: `docker run -v "$PWD":/mnt alpine sh -c 'cat
/mnt/link-to-etc-shadow'` reads the container's own `/etc/shadow`
(byte-identical to reading it directly), and a symlink to a real host
tmp file that exists on the host but not in the container's own
filesystem tree fails with "No such file or directory" (i.e., the
container's own root, not the host's, is what a bind-mounted symlink
resolves against). The host-side read this chain actually guards
against is specific to `extensionless_shell_scripts`' shebang peek,
which already rejects symlinks (#955); the glob-matched branch has no
equivalent host-side read to guard.
## Verified
Full test suite (798 tests), ruff, mypy, `repo_gate.py`,
`prose_lint.py --diff origin/develop`, and the complete
`docker_lint.py` run (all 7 linters) all pass clean.
CopilotAI lite review requested due to automatic review settings August 23, 2026 17:09
@coderabbitai

coderabbitaiBot commented Aug 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 991163b6-abab-4854-a3c6-7aeb614b6c4c

📥 Commits

Reviewing files that changed from the base of the PR and between c4e8524 and 48486be.

📒 Files selected for processing (1)
  • scripts/docker_lint.py

Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

has_shell_shebang now rejects symlinks and raises CommandFailed when it cannot read a file. A regression test verifies propagation of PermissionError with the expected message.

Changes

Shell shebang handling

Layer / File(s)Summary
Shebang validation and regression coverage
scripts/docker_lint.py, scripts/tests/test_docker_lint.py
has_shell_shebang rejects symlinks before reading and converts read failures into CommandFailed. The test verifies the PermissionError message.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk:🟡 Moderate · up to 48486

The change makes shell-file read failures surface instead of being silently ignored, but the path-safety check only covers the final component; a symlinked parent directory or race before opening could make lint read outside the checkout. Merge should wait for that boundary to be fixed or explicitly accepted by the appropriate owner.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely describes the main change: propagating read failures from has_shell_shebang instead of returning False.
Docstring Coverage✅ PassedDocstring check was indeterminate for this PR — some files could not be analyzed in time. Not blocking.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch propagate-read-failure

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0)📘 Rule violations (0)📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display preferences

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Propagate shell-shebang read failures in docker_lint target discovery

🐞 Bug fix🧪 Tests🕐 20-40 Minutes

Grey Divider

AI Description

• Raise CommandFailed when shebang probing can’t read a tracked file.
• Prevent unreadable tracked files from silently dropping out of shell lint targets.
• Add a regression test covering PermissionError propagation.
Diagram

graph TD
A["lint()"] --> B["tracked_files()"] --> C["extensionless_shell_scripts()"] --> D["has_shell_shebang()"] --> E[("Filesystem read")]
D -. "OSError" .-> F["CommandFailed"] --> A
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Warn and skip unreadable files (best-effort linting)
  • ➕ Lints the rest of the repo even if a subset of files are unreadable
  • ➕ Less disruptive for transient local filesystem issues
  • ➖ Can still miss checks unless overall status is forced to fail
  • ➖ More complex UX/reporting to avoid a misleading success signal
2. Aggregate read errors and fail at end
  • ➕ Reports all unreadable files in one run
  • ➕ Still ensures lint fails overall
  • ➖ More plumbing/complexity (collecting and formatting multiple failures)
  • ➖ Less consistent with existing fail-fast discovery behavior (e.g., ls_files raising CommandFailed)

Recommendation: Fail-fast via CommandFailed is the right default for correctness: unreadable tracked files indicate discovery is incomplete, and treating that as a hard error prevents false-green lint runs while matching the existing ls_files failure pattern.

Files changed (2) +16 / -4

Bug fix (1) +8 / -4
docker_lint.pyRaise CommandFailed when has_shell_shebang cannot read a tracked file+8/-4

Raise CommandFailed when has_shell_shebang cannot read a tracked file

• Moves the symlink fast-path before attempting to open the file, and changes OSError handling to raise CommandFailed instead of returning False. This preserves intentional False cases (symlink, invalid UTF-8) while ensuring discovery failures don’t silently exclude tracked files from lint targets.

scripts/docker_lint.py

Tests (1) +8 / -0
test_docker_lint.pyAdd regression test for shebang read failure propagation+8/-0

Add regression test for shebang read failure propagation

• Adds a test that mocks Path.open to raise PermissionError and asserts has_shell_shebang raises CommandFailed containing 'could not read'. This locks in the new behavior and prevents regressions that would silently skip unreadable tracked files.

scripts/tests/test_docker_lint.py

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The behavioral change is narrowly scoped, aligns with existing error handling in target discovery, and is covered by a focused regression test.

Pull request overview

This PR makes shell-script discovery failures in scripts/docker_lint.py explicit by propagating host-side read errors from has_shell_shebang() as CommandFailed, preventing unreadable tracked files from being silently skipped during lint target selection.

Changes:

  • Raise CommandFailed when has_shell_shebang() encounters an OSError while opening/reading a tracked file.
  • Add a regression test asserting that a read failure (e.g., PermissionError) is not swallowed as False.
File summaries
FileDescription
scripts/docker_lint.pyPropagates shebang-peek read failures as CommandFailed during extensionless shell-script discovery.
scripts/tests/test_docker_lint.pyAdds coverage ensuring read failures in has_shell_shebang() raise rather than returning False.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadscripts/docker_lint.py
Fixes a Copilot nitpick on PR #956: the function returns False, it doesn't "read" anything as False. Wording only, no behavior change.
CopilotAI review requested due to automatic review settings August 23, 2026 17:13

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The change is narrowly scoped, aligns error handling with existing discovery behavior, and includes a focused regression test for the new failure mode.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@ptr727
ptr727 merged commit 031d8a2 into developAug 23, 2026
8 of 9 checks passed
@ptr727
ptr727 deleted the propagate-read-failure branch August 23, 2026 17:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ptr727