fix(ci): add test CI, repair auto-tag releases, harden action pins (2.11.4)

The repo had no CI: ruff/ty/pytest ran only in local prek pre-push hooks, so
a commit pushed without hooks installed reached main unvalidated -- and
auto-tag would then cut a release from it. Adds ci.yml (ruff, ty, pytest on
3.13) and gates tagging on it.

Auto-tagged versions also never produced a release. The tag is pushed with
the default GITHUB_TOKEN, and GitHub suppresses workflow triggers for
GITHUB_TOKEN-created events, so the tag-triggered release.yml never fired --
v2.11.3 was tagged with no release. auto-tag now creates the release itself,
idempotently; release.yml stays as the manual-tag path.

Also fixes version logic that matched the `!` breaking marker but only ever
bumped minor (and never saw BREAKING CHANGE: footers, since only subjects
were inspected), serializes concurrent merges that both computed the same
tag, and repins softprops/action-gh-release from an arbitrary master commit
to v2.6.2 with all workflows on one actions/checkout version.

Clears the pre-existing ty error in notify/tailer.py that would have made
the new type-check step red: _read_new_lines read self._file_pos
(int | None) while its only None-guard lived in the caller, so the position
is passed in explicitly as an int. Behavior unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Bollinger
2026-07-24 16:39:59 -04:00
co-authored by Claude
parent a0dc56ce08
commit 665c8ab47c
7 changed files with 245 additions and 28 deletions
+43
View File
@@ -190,6 +190,49 @@ class TestCrackTailerStop:
tailer.stop()
class TestCrackTailerPollOnceInternals:
"""Direct unit tests for _poll_once edge cases via the internal API."""
def _make_uninitialised(self, out_path: Path):
"""Return a tailer whose _file_pos is still None (seek never called)."""
tailer, notify, aggregate = _make_tailer(out_path)
# Do NOT call start() or _seek_to_eof(); leave _file_pos = None.
return tailer, notify, aggregate
def test_first_poll_with_file_pos_none_seeds_position(
self, tmp_path: Path
) -> None:
"""_poll_once with _file_pos=None must record size and return without notifying."""
out = tmp_path / "hashes.out"
out.write_text("alice:hash:plain\n")
tailer, notify, aggregate = self._make_uninitialised(out)
assert tailer._file_pos is None
tailer._poll_once()
# Position seeded to current size; no notification fired.
assert tailer._file_pos == out.stat().st_size
assert notify.call_count == 0
assert aggregate.call_count == 0
def test_truncation_resets_position_directly(self, tmp_path: Path) -> None:
"""_poll_once must reset position and buffer when the file shrinks."""
out = tmp_path / "hashes.out"
content = "alice:hash:plain\nbob:hash:plain\n"
out.write_text(content)
tailer, notify, aggregate = _make_tailer(out)
tailer._file_pos = len(content.encode()) # pretend we've read it all
tailer._buffer = b"leftover"
# Truncate to a smaller file.
out.write_text("c:h:p\n")
tailer._poll_once()
# Position must have been reset and buffer cleared.
assert tailer._file_pos == len(b"c:h:p\n")
assert tailer._buffer == b""
# One new line notified.
assert notify.call_count == 1
class TestCrackTailerFileHandling:
def test_missing_file_then_appearing(self, tmp_path: Path) -> None:
out = tmp_path / "missing.out"