Skip to content

v0.8.7 - The Fail-Loud Pass: a Bounded Subprocess, Three Client Lies, and the Two Halves of D6

Choose a tag to compare

@github-actions github-actions released this 12 Oct 02:19
· 105 commits to main since this release

What's Changed in v0.8.7

A patch release: every change in it is a defect rather than a capability. The theme is the one the
fail-closed principle has always stated and the code did not always follow — a check that cannot
answer must not be read as an answer, and a command that never finished must not be read as one that
did.

Three of the four groups below are that sentence in different clothes. Nothing bounded a subprocess, so
a wedged git repack held the remote lock past its own expiry while another client lawfully took over.
A download compared nothing against its Content-Length, so a truncated packfile was reported as a
success. A dir_exists() answered "does not exist" for every non-200, including the ones that mean "I
could not tell". And the shipped source cited twenty-three documents that are not in the repository, so
no reader could follow a single one of them.

The fourth is the two halves of D6 that the v0.8.6 pass deliberately left undone — and the reason it
left them is the reason they were worth doing properly: a type check that is red on arrival and a
coverage report nothing produces are both worse than nothing, because they look like enforcement.

Changed

  • Every git invocation is bounded (H-24). The tree had timeouts on its HTTP calls and none at all
    on its twenty-five subprocess sites. git_util.run_git, gc's runner and the snapshot tool's
    runner now bound every call and fail loudly, naming the command and the limit.

    The bound is a constant rather than a configuration key, on purpose: a repository must not be able to
    raise the ceiling that protects its own concurrency guarantee. The failure it prevents is specific —
    a gc whose git repack never returns holds the remote lease open past expiry, at which point
    another client lawfully takes over the lock this one still believes it holds.

    filter_existing_objects is the one deliberate exception: a timeout there warns on stderr and answers
    "assume no objects are present", because the cost of a false negative is a re-upload rather than a
    lost object.

  • dir_exists() no longer answers "does not exist" for a transient error (H-7). It returned False
    for every non-200, while list_dir() beside it raised for anything that was not 200 or 404 — the
    two disagreed about the same state, and the ref-pruning path acted on the optimistic reading. Only an
    explicit 404 is a False now. The pruning loop already caught the raise and stopped; mkdir_p now
    aborts with the client's own message instead of proceeding to a creation it could not confirm.

  • .gitignore gained .coverage.*. Parallel mode — which pyproject.toml turns on for the whole
    suite, so that a worker cannot overwrite the driver's data — writes one data file per process, and the
    plain .coverage rule matched none of them.

Fixed

  • A truncated download is a failure, not a success (A12.10). download_file_to read
    Content-Length into the progress callback and never compared it against what it had written, so a
    short body returned True. A packfile that is quietly incomplete is a corrupt object store, found
    later and far from the cause; a mismatch now raises.

  • A server time that was never learned is announced (H-8). get_server_time() fell back to the
    local clock whenever no Date header had been seen, which rested the lock's ordering on a clock the
    protocol does not trust. It still falls back — a server that sends no date must not make the lock
    unusable — but it now says so once, on stderr.

  • The shipped source stops citing documents a reader cannot open (C8). Twenty-three comments across
    seven files named REVIEW.*.md, archive/…, (N-16) and (Qwen …); every one of them lives in the
    gitignored archive/, so none could be followed from a checkout. The reasoning is inlined, and an
    identifier survives only where it names a fact about the review rather than standing in for the
    explanation.

Added

  • A typecheck CI job (D6 residual). mypy 2.4.0, pinned, run bare so that the scope and the
    strictness come from [tool.mypy] in pyproject.toml rather than from the workflow — a local mypy
    and CI cannot disagree about what is checked. The package and tools/ are clean.

    tests/ is deliberately out of scope: it is a namespace package, so mypy resolves each module under
    two names and stops before checking anything, and its test doubles are MagicMocks whose attributes
    no annotation can describe. A check that has to exclude a third of what it sees is not one people
    trust. warn_unused_ignores is on, so an ignore that has stopped being necessary fails the job
    instead of absorbing the next real error on its line.

  • A coverage CI job (D6 residual). It runs the whole suite under coverage and publishes the
    per-file table to the job summary, because a workflow log needs admin rights to download and a job
    summary does not.

    [tool.coverage.*] holds the configuration, and two settings there are load-bearing rather than
    tidiness: parallel keeps each process's data in its own file instead of letting the last one
    overwrite it, and patch = ["subprocess"] installs a startup hook and puts the setting in the
    environment — the hook lives in site-packages and the variable is inherited through git, so the
    helper is measured as well as the workers. Without both, the report would describe the driver process
    alone: a figure that looks like coverage and is not.

    There is deliberately no threshold. A fail_under chosen to make today's build green is one that
    gets raised for the wrong reason or lowered for a worse one; what the job is for is that a drop is
    visible in a review. At this release it reads 91% of the package (2944 statements, 264 missed) —
    a dated observation, not a target. The report is also a measurement rather than a promise: it counts
    the paths a stub exercises, and
    DESIGN.md §4.3.1 says plainly
    that none of this has been validated against a real Seafile instance.

Tests & Tooling

Twelve new tests in four classes cover the changes above: the three client.py defects, and the
subprocess timeout in git_util, in gc and in the snapshot tool. Suite: 681 → 693 tests / 134 → 138
targets. ROADMAP.md is re-targeted: the six rows a patch release could carry are closed and recorded
in §2, and every row that is left still names v0.9.0, v1.0.0 or won't do.


Full Changelog: v0.8.6...v0.8.7