Repository navigation
fix(vendor): make main() exit non-zero when any crawler target raises (#872) - #873
Conversation
|
NEEDS CHANGES on 1. The test guarding the new So delete the 2. Needs a decision from you — this changes what a broken crawler costs. GitHub Actions runs Two ways to go, and it is your call: (a) accept it — a red cron and no partial commits until a human fixes the crawler; or (b) defer the exit in the workflow so the good crawls still land — Good news on the risk: nothing is broken today. The latest successful cron run ( Verified and not disputed: Non-blocking omission for a follow-up: a mixed list like |
58f883c to
54639e1
Compare
|
NEEDS CHANGES on Round 1's defect is repaired and now genuinely pinned. With The remaining item — # The regression this issue is about: pre-fix, main() ends on a bare
# `return 0` with no accounting at all, so this assertion is the one
# that fails (0 != 0) against origin/main.Both specifics are stale residue of round 2's own edit. Verified and not disputed: the round-2 diff ( Still open and not mine to settle: the |
54639e1 to
08d0fea
Compare
|
GOOD TO GO on
The record for this PR, carried forward: the production change in This verdict covers the change as it stands. It does not settle the scope question still open on this PR: under Non-blocking, for a follow-up issue rather than this PR: a mixed target list ( |
|
What needs my decision? |
Whether this PR should also change The code itself is settled — Today Two options:
For what it is worth: nothing is broken right now. Cron run My recommendation is (b): the whole point of |
|
I take (b). And only discard changes made by a non-zero sub-vendor. zero-exited sub-vendor's updates should be kept. So the idea is for cron jobs, at most earn about failed sub-vendors, but never fail the entire job. |
|
Recorded, verbatim:
So the target shape is per-target granularity, and a cron that warns rather than fails — which is more than option (b) as I framed it. Three requirements, and I have relayed them:
|
08d0fea to
a0748fb
Compare
|
NEEDS CHANGES on 1. 2. The
Only true at umask 022. Practical impact here is near zero (git records just the exec bit, CI is umask 022), so fix the comment or 3. Cosmetic, and I would take the second: Confirmed and closed out: per-target restore is genuinely unnecessary. |
73fc66a to
16a73b4
Compare
|
NEEDS CHANGES on 1. Deleting the Two other mutations are caught (deleting
2. There is a second capability regression, and only one is disclosed. A writable file inside a non-writable directory: pre-fix regenerated it, this head fails the target because Confirmed clean: the Cosmetic: add the |
…#872) run() swallowed every crawler exception as a filterable VendorRuntimeWarning, and main() always returned 0 regardless -- so a crawler failure looked identical to a successful regeneration at the only boundary CI or a script checks. Vendor.__init__ fetches and renders before it ever opens the const file for writing, so a failed crawler leaves the previous file untouched: a no-op indistinguishable from success (hit for real in #870's docstring-propagation bug). - run() now returns whether its target succeeded; main() attempts every target regardless of earlier failures (a dead crawler should not block the rest) and returns 1 if any of them raised. - A raising target's qualified name and exception repr now also go to stderr unconditionally via print(), since VendorRuntimeWarning is filterable and PCAPKIT_VERBOSE defaults false. - run() now wraps every vendor() call in pcapkit.vendor.__main__._snapshot_and_restore, a @contextlib.contextmanager that copies a target's existing const file aside before the crawler runs and restores it if the run raises, dropping the copy on success (via contextlib.suppress, not a bare call, so a cleanup failure after a successful run cannot itself turn that success into run() -> False). Per the owner's ruling ("I prefer we use contextlib over manually manage the temp file deletion based on pure best intent (try-finally) and for atomic writing, an easier path is simply keep a copy before running the sub-vendor and revert if anything failed"), this supersedes rounds 4-8's atomic write inside Vendor.__init__ entirely -- _write_atomic is gone, and __init__'s last line is once again a plain open(const_file, 'w'). Snapshot-and-restore is a *wider* guarantee: it covers any way a crawler's own code could touch its const file, not just one open()/print() pair, and it puts the discarding exactly at the per-target boundary the owner's earlier (b) ruling asked for. The except/else split is structural, not a comment: an except clause that always re-raises is followed by an else that only runs on success, rather than an unindented statement after the whole try/except relying on "raise never falls through" -- a mutation test (deleting the raise) proved the fallthrough form lets the exception escape the generator entirely undetected once an else exists to gate the cleanup, whereas the comment-only version let a *different*, wrong exception (FileNotFoundError from the orphaned backup) reach run() instead of the crawler's real one, with every existing test still green either way. - Two capability regressions relative to `main`, both measured and disclosed rather than assumed: a 0o444 (read-only) destination now fails to regenerate (a plain open() needs file write permission, unlike os.replace()'s directory-only requirement), and a writable file inside a non-writable directory also now fails (mkstemp needs directory write permission to create the backup, which open() on an existing file never needed). Both leave the previous file untouched -- fail-safe -- and neither is new relative to rounds 4-8's os.replace()-based design, only relative to `main`. The two failure modes are asymmetric: an unresolvable _dest_path skips the snapshot and lets the target proceed (it fails loudly anyway, since Vendor.__init__ calls _dest_path itself), while a mkstemp failure fails the target outright before vendor() is ever attempted. - Vendor._dest_path is now a classmethod rather than an instance method -- it only ever consulted type(self), so cls is exactly that, with no instance required. This lets _snapshot_and_restore resolve a crawler's destination via vendor._dest_path() before instantiating it at all. No crawler overrides _dest_path, so this is a behaviour-preserving refactor for all of them; confirmed against tests/vendor/test_vendor_dest_path_unit.py (not part of this PR's file set, run read-only for verification), which still passes unchanged. - .github/workflows/cron-vendor.yml's "Update Vendor" step is unchanged this round; re-verified against the snapshot/restore rewrite. - tests/vendor/test_vendor_exit_code_unit.py unchanged this round (still 5 methods) -- its stub crawlers don't override _dest_path, so they hit the "skip the snapshot" fallback and behave exactly as before. - tests/vendor/test_vendor_atomic_write_unit.py renamed to tests/vendor/test_vendor_snapshot_restore_unit.py (git mv), since the mechanism it tests is _snapshot_and_restore, not an atomic write. Its regression pin now asserts on the error run() actually *reported* (assertWarnsRegex against the crawler's own RuntimeError text), not merely that run() returned falsy -- the mutation above is exactly what that distinction catches, and a bare assertFalse(run(...)) passed on it. It also asserts the destination's mode (0o640 in, 0o640 out) alongside content, which is the measurement that actually justifies collapsing rounds 6-8's three mode-permutation tests into one. Verified this round: tests/vendor/test_vendor_snapshot_restore_unit.py (3 methods) and tests/vendor/test_vendor_exit_code_unit.py (5 methods), 8/8 together, under plain unittest. All three mutations are now caught: deleting the `raise` fails 2 of 3 methods, deleting os.replace() fails 1 of 3, and removing the `with` from run() fails 1 of 3. Against both of __main__.py and default.py taken from origin/main together, 2 of 3 methods fail (the byte-for-byte pin with '' != 'GOOD CONTENT\n', and the happy path with None is not true, since pre-fix run() returns nothing); the 0o444 method passes there too, since it is a measurement of a case main() already left untouched, not a regression pin.
16a73b4 to
89e5996
Compare
|
GOOD TO GO on The Four non-blocking items, and I am asking for the first three in one short round: 1. 2. A false clause in 3. The 4. Not asking: deleting Also confirmed: |
…lause GitHub issue #875, a follow-up to #872/#873 (merged as 6877210). - Pin pcapkit.vendor.__main__._snapshot_and_restore's `except BaseException:` itself, not just its effect. Narrowing it to `except Exception:` left the existing suite fully green, because nothing had raised anything BaseException-but-not-Exception during a run. Added a test whose stub raises KeyboardInterrupt after the destination has already been truncated (via the same print()-patch technique the other failure tests use); it asserts the content is restored byte-for-byte, that no backup file survives in the directory, and that the KeyboardInterrupt itself still propagates out of run() rather than being converted into a plain False return. - Corrected a false clause in _snapshot_and_restore's own docstring (pcapkit/vendor/__main__.py). It claimed a crawler whose _dest_path cannot be resolved without instance state "goes on to fail loudly anyway" once Vendor.__init__ calls the same method -- true of the first trigger listed (a crawler outside the real pcapkit.vendor tree, which fails the identical way whether or not the snapshot ran) but false of the second: an instance-method _dest_path raises TypeError on the classmethod-style call this function makes, but resolves fine on the bound instance call inside __init__, so that crawler actually runs unprotected and succeeds silently -- run() returns True, the file is replaced, nothing warns that no snapshot was ever taken. The clause is narrowed to the first trigger and the second is now stated plainly, matching what the test module's own docstring already said correctly. - The `# pylint: disable=no-else-raise` pragma at _snapshot_and_restore's `try:` line is kept, not dropped. Re-measured with the Makefile's own PYLINT_FLAGS (what CI actually runs): R1720 does fire on this try/except/else without the pragma (9.87 -> 9.74), because pylint 4 extends no-else-raise to a try/except whose except ends in raise. The structure it flags is deliberate -- the comment at :153-163 already explains why the else: is not a style default pylint would pick -- so the finding is suppressed rather than the structure changed. Verified: tests/vendor/test_vendor_snapshot_restore_unit.py (4 methods) and tests/vendor/test_vendor_exit_code_unit.py (5 methods), 9/9 together, under plain unittest. The new test_a_keyboard_interrupt_still_restores_and_propagates method passes on this code (content and directory listing restored, the KeyboardInterrupt still propagates). Mutated `except BaseException:` to `except Exception:` at its exact line (content of that line asserted before mutating, since `os.replace(backup, const_file)` also appears in prose elsewhere in the same file and a string-index mutation would otherwise silently hit that instead): the method then fails on its content assertion ('' != 'GOOD CONTENT\n'); a direct follow-up check confirmed the predicted orphaned backup file ('.const.py.<random>.bak') left in the directory afterwards. pylint $(PYLINT_FLAGS) pcapkit/vendor/__main__.py, pragma present: no R1720, 9.87/10 (only W1203, pre-existing). Pragma absent (checked and reverted, not shipped): R1720 at the try: line, 9.74/10.
#719 Swept tests/ for owner-ruling quotes attributed to the wrong GitHub thread, the same defect class #719 fixed under pcapkit/. Confirmed each by grepping the quote's distinctive text against the cited thread's body/comments; a hit elsewhere is a re-quote or a different thread's own words, not the source. - test_sentinel_exports_unit.py: the already-reported #937->#719 fix for AbsentType's privacy ruling. - test_enum_lookup_reparent_930_unit.py (4 sites) and test_mh_unit.py: "I prefer (2) directly" and the question that drew it are in pull request #940's thread, not issue #935 -- #935 only carries the first ruling ("I lean on 1"). - test_vendor_snapshot_restore_unit.py: the contextlib/atomic-write ruling is in pull request #873's thread; issue #872 has zero comments. - test_vendor_reg_apptype_generator_unit.py (2 sites): the "undefined direct uses 0" ruling is in pull request #874's thread, not issue #860 or #770. - test_const_enum_no_mint.py (2 sites): the mint/unmint criterion was settled on pull request #847 and confirmed on #775 -- the reverse of what the text said, per #861's own description of the same ruling; and "Q1 - bare it is." is pull request #838's thread, not #775's. One occurrence left unresolved rather than guessed at: the "Preserve each branch's existing name argument..." quote (4 sites in test_const_enum_no_mint.py, attributed to "#775's final round") does not appear verbatim in #775, #847, or #878 (the implementing PR) by body, comments, review comments, or commit message -- only a paraphrase in #878's own PR description/commit message, which is the author's prose rather than a quoted ruling. Flagged for the owner rather than fixed. tests/corekit/, tests/vendor/, tests/const/ pass (400/16, 118, 299 respectively, pcapkit.__file__ confirmed inside this worktree); tests/protocols/internet/test_mh_unit.py passes standalone (52/0) -- the full directory has 5 unrelated pre-existing failures from ungenerated examples/captures/ fixtures, untouched by this change. Refs #719
…quest (#719) (#982) * docs(pcapkit,ci): cite the issue a defect belongs to, not the pull request (#719) Per the owner's ruling on #719, replace every reference to a pull-request number in pcapkit/** and .github/workflows/** comments and docstrings with the issue it closed, or a description where no issue covers it. - 92 real PR citations in pcapkit/ (93 was the estimate; the gap is RFC packet-diagram and hex-format-spec false positives, plus one cross-repo issue citation that only coincidentally matched a PyPCAPKit PR number). - 13 PR citations in .github/workflows/, matching the estimate exactly. - Several citations named two or three numbers for one claim where a PR closed several issues, or several PRs closed the same issue; deduplicated rather than left reading "#425 and #425". Four review rounds caught the same category error recurring: several sites had relocated a verbatim quote or a specific finding into the issue number rather than describing where the ruling was actually given, so the quote no longer existed where the sentence pointed. Fixed each by naming the issue while locating the ruling honestly -- "a ruling given in review of the work for #N" -- the same shape already used on this repo's conventions docs. Two sites needed the inverse correction instead: the #923 quote in enum.py/exceptions.py genuinely is recorded on #923's own thread, just attributed there to the pull request that implemented it, so those read "a ruling recorded on GitHub issue #923" rather than pointing elsewhere. Also fixed a lost conjunction and an ordinal/number mismatch in corekit/enum.py, a self-contradicting below/above pointer repeated across three internet/ files, and a number collision in http.py where one issue ended up naming both a defect and the change that closed it. Final sweep: grepped the whole tree for the word "verbatim" -- the marker that makes a quote-attribution claim falsifiable -- across all 30 files under pcapkit/ that carry it, and checked every quote this way names against the actual issue thread. Caught two more of the same defect: vendor/__main__.py's #872 citation (the quote is in the implementing pull request's review, not #872 itself) and four sites across mh.py attributing to #935 a ruling that only exists in the review of the pull request that implemented it -- #935's own thread holds just the superseded widen-not-delete proposal. Both fixed the same way. Every other quote-bearing claim the sweep found -- #911, #937, three distinct #877 quotes, both #842 quotes, and the #860/#808/#806/#886/#917 rewrites from earlier in this pass -- resolves to the thread it names. Verified: targeted pytest across every touched module passes, including the test that pins the vendor/const apptype.py get() region as byte-identical, reconfirmed after each amendment. Both edited workflow YAML files parse before and after with unchanged key counts. * docs(corekit): cite #719, not #937, for the AbsentType ruling AbsentType's docstring attributed the owner's "document it as private type/class... not for public use is enough" quote to #937. #937 itself quotes that ruling verbatim under "The owner's ruling, verbatim (from #719)" -- it re-attributes rather than originates it. Per the house rule to cite the issue a ruling was settled on (docs/source/contributing/conventions/documentation.rst:196-200), point the attribution at #719 and re-wrap the paragraph to the file's existing ~78-column width. The neighbouring, unrelated #937 citation describing what #937 did to sentinel naming is untouched. tests/corekit/ passes (400 passed, 16 skipped) against this worktree's own pcapkit (confirmed via pcapkit.__file__); pylint on the file is 9.77/10, unchanged by this edit -- the one finding is a pre-existing, unrelated too-few-public-methods warning on NoValueType. * docs(tests): re-point six ruling citations at their actual threads, per #719 Swept tests/ for owner-ruling quotes attributed to the wrong GitHub thread, the same defect class #719 fixed under pcapkit/. Confirmed each by grepping the quote's distinctive text against the cited thread's body/comments; a hit elsewhere is a re-quote or a different thread's own words, not the source. - test_sentinel_exports_unit.py: the already-reported #937->#719 fix for AbsentType's privacy ruling. - test_enum_lookup_reparent_930_unit.py (4 sites) and test_mh_unit.py: "I prefer (2) directly" and the question that drew it are in pull request #940's thread, not issue #935 -- #935 only carries the first ruling ("I lean on 1"). - test_vendor_snapshot_restore_unit.py: the contextlib/atomic-write ruling is in pull request #873's thread; issue #872 has zero comments. - test_vendor_reg_apptype_generator_unit.py (2 sites): the "undefined direct uses 0" ruling is in pull request #874's thread, not issue #860 or #770. - test_const_enum_no_mint.py (2 sites): the mint/unmint criterion was settled on pull request #847 and confirmed on #775 -- the reverse of what the text said, per #861's own description of the same ruling; and "Q1 - bare it is." is pull request #838's thread, not #775's. One occurrence left unresolved rather than guessed at: the "Preserve each branch's existing name argument..." quote (4 sites in test_const_enum_no_mint.py, attributed to "#775's final round") does not appear verbatim in #775, #847, or #878 (the implementing PR) by body, comments, review comments, or commit message -- only a paraphrase in #878's own PR description/commit message, which is the author's prose rather than a quoted ruling. Flagged for the owner rather than fixed. tests/corekit/, tests/vendor/, tests/const/ pass (400/16, 118, 299 respectively, pcapkit.__file__ confirmed inside this worktree); tests/protocols/internet/test_mh_unit.py passes standalone (52/0) -- the full directory has 5 unrelated pre-existing failures from ungenerated examples/captures/ fixtures, untouched by this change. Refs #719 * docs(tests): paraphrase four fabricated or altered owner quotations (#719) Per #719's citation ruling (de-quote, never reproduce a verbatim quote that may have come from outside GitHub): - test_const_enum_no_mint.py (4 sites): a quotation attributed to "the owner's ruling, verbatim" never appears in #775, #847, #861 or #878 (or anywhere in the repo's comment corpus). Replaced with a paraphrase attributed to PR #878's own body, which carries the real design note in different words. - test_sentinel_exports_unit.py / test_const_registry_protocol.py: a quote attributed to #911 silently dropped half of what the owner wrote on #719 and swapped `__all__` for "users". Replaced with a paraphrase naming #719 as where it was settled and #911 as the issue that carried it out. - test_const_enum_no_mint.py / test_const_enum_builtin_parity.py (4 sites): a "verbatim" quote of #860 silently corrected the owner's typo ("entires" -> "entries"). Paraphrased, which drops the question of reproducing or flagging the typo. - test_enum_lookup_reparent_930_unit.py: "the owner's final ruling there" had #935 as its nearest antecedent instead of #940; named #940 explicitly and paraphrased the adjacent quote. Verified: ast.parse and reST markup pairing clean on every touched file; tests/const (299 tests) and the targeted pytest sweep of all touched files (261 passed, 2013 subtests) are green. tests/corekit's full discover run shows 5 pre-existing failures in test_sentinel_exports_unit.py, confirmed identical on the unedited originals -- a cross-file test-order dependency unrelated to this change. * test(vendor,corekit): fix a surviving fabricated ruling and a wrong citation (#719) - tests/vendor/test_ipx_socket_unit.py: the "owner's ruling" attribution for keeping the hex-suffixed Xerox name survived in this file after the prior commit removed the same false attribution from four sites in test_const_enum_no_mint.py. Reworded to credit PR #878's own design note, matching the wording already used at the repaired sites. - tests/corekit/test_sentinel_exports_unit.py: the docstring cited the #719 export ruling ("only export objects, not types") as grounds for keeping ABSENT out of __all__, but ABSENT is an object, so that ruling argues for including it, not excluding it. Re-grounded the sentence on the privacy ruling already quoted ~15 lines below instead, without re-quoting it. Both changes are prose-only: tokenizing each file before and after with comments and docstrings stripped produces identical token sequences. tests/vendor passes 118/118 except one pre-existing, test-order-dependent flake in test_vendor_snapshot_restore_unit.py (reproduces identically on the pre-edit tree); tests/project/test_conventions_doc_claims.py passes 38/38. * test(corekit,const): narrow the blanket paraphrase, restoring quotations that cite correctly (#719) The last two commits paraphrased every disputed owner quotation away. That was right for one case and wrong for two: a quotation that exists nowhere has to be paraphrased, but a quotation that is real and was only cited to the wrong thread lost its audit trail for nothing, since the defect was the pointer, not the words. Per the owner's ruling, narrow the fix to match. Restored as quotations, correctly cited: - tests/corekit/test_sentinel_exports_unit.py (~L4-7) and tests/const/test_const_registry_protocol.py (~L1363): the sentinel export rule, split back into its two real sources instead of one spliced sentence -- #719's "we should ONLY export the objects ... and leave the types ... out", and #911's own "we only expose the final objects to users", with #911 noted as both executor and source. - tests/const/test_const_enum_no_mint.py (~L88, ~L1876, ~L2363) and tests/const/test_const_enum_builtin_parity.py (~L655): the #860 minting ruling, including its load-bearing first sentence ("I think we should not mint on get still actually") and the owner's own "entires" typo, marked [sic] rather than silently corrected. Left alone: the four #878 fabricated-quote sites in test_const_enum_no_mint.py, which cite no real thread and stay paraphrased, and the ABSENT privacy sentence, which is a correct paraphrase of a different ruling. Verified: ast.parse and reST markup clean on all four files; code token sequences (docstrings/comments stripped) identical before and after; each restored quotation substring-matches its source comment after whitespace/markup normalisation. tests/const: 299 OK. tests/ project/test_conventions_doc_claims: 38 OK, 1 skipped. * test(corekit,const): convert restored quotations to statements with context, per #719 The previous commit restored eight verbatim quotations to fix a narrowing that had dropped their context. The owner has since ruled that neither form is right: a narrowed paraphrase without context does not help a reader who was not in the thread, but a verbatim quotation makes the docstring read as a discussion rather than documentation. - Sentinel export rule (corekit/test_sentinel_exports_unit.py, const/test_const_registry_protocol.py): state that a module's `__all__` lists a sentinel's object but deliberately leaves its type out, and why (the type is not part of the public surface), citing #719 as where it was settled and #911 as where the implementing work belongs. - #860 minting rule, four sites (const/test_const_enum_no_mint.py x3, const/test_const_enum_builtin_parity.py): state that `get()` must not mint and only `register()` creates a new entry, and why (only IANA-registered values are legitimate and `get()` lacks the information to construct one), citing #860. Each site is fitted to its own surrounding prose rather than one paragraph pasted four times. Drops the `[sic]` each quotation carried, since there is nothing left to reproduce. - Fixed two sentences left orphaned by the quotations' removal: an antecedent ("the three") that depended on the deleted quote's wording, and a sentence whose "get() as well as _missing_" had the emphasis backwards relative to the rule's own subject. The four PR #878 paraphrase sites in test_const_enum_no_mint.py were already in this third form and are unchanged. Verified: ast.parse on all four files; tokenize with comments and docstrings stripped shows an identical token sequence before/after (prose-only); tests/const (299) and tests/project/test_conventions_doc_claims.py (38, 1 skip) pass; tests/corekit (400, 5 failures, 16 skipped) matches the documented pre-existing sentinel-identity failures. * test(corekit,const): state the remaining owner rulings in our own words, per #719 The four files still carried owner-attributed quotations beside the eight converted earlier, so each read half as documentation and half as a thread. - Replace each quoted ruling with a statement of the rule, the reason a reader needs, and the issue where it was given (#842, #864, #775, #860, #911, #719, #647, #808, #759, #857). - Rename the dangling "privacy ruling quoted below" reference to point at the statement that replaced the quotation. - Leave RFC text, code literals and ordinary prose untouched. Prose only: tokens with comments and docstrings stripped are identical before and after; tests/const 299 OK, tests/corekit unchanged (5 known). * test(const): restore ruling citations to the pull requests that carry them, per #719 tests/ is exempt from the issue-citation rule (documentation.rst, ruled on #719): the fact cited lives in the pull request, not the issue. - PR #836 restored for the TransportProtocol-extension refusal, the |-composite decoding retirement, and the stale-comment deletion; the rulings are not on #808 at all. - PR #783 for the f-string convention; PR #847 for the mint criterion. - The de-quotation stands: wording stays as statements, no quotation marks.
tests/vendor/test_vendor_snapshot_restore_unit.py:14 and :58 carried :meth: roles pointing at Vendor._write_atomic. Nothing defines it: grep finds no `def _write_atomic` anywhere in the tree, and pcapkit/vendor/ default.py has no such method. Line 14 is the odder of the two, since the sentence it sits in says the method no longer exists while using a role that asserts it does. The history is sharper than "removed". `def _write_atomic` appears only in 6fde283 and a0748fb, both intermediate revisions of #873 that the squash dropped, so the method never reached main at all. Under pcapkit/ the only commit touching the name is that squash, 6877210, and what it added was the dangling role in __main__.py rather than the method -- #991 fixes that one. - Both roles become plain inline literals, so the prose still names the method without claiming a resolvable target. tests/ sits outside the Sphinx source tree, which is why neither emitted a build warning. A sweep of all 346 roles across tests/vendor/ found no other unresolved target. Prose only: both sites are inside the module docstring, which spans lines 2-165. Token sequences are identical with strings masked, the AST with docstrings blanked compares equal, and maximum line length stays 99.
Please follow the guide below
make pylint,make mypy,make isort)make testpasses, and a test case covers the changedocs/source/changelog/and regeneratedCHANGELOG.md, if the change is user-visible — N/A, changelog centralised in docs(changelog): shared 1.5.0 changelog — long-lived, merges last (#610, #616, #617, #618, #620) #657What is the purpose of your pull request?
fix— corrects a defectDescription of your pull request and other information
Fixes #872.
python -m pcapkit.vendor <target>could not fail:run()swallowed every crawler exception as a filterableVendorRuntimeWarning, andmain()alwaysreturn 0. This grew into three pieces after review, following the owner's ruling (quoted verbatim):1. Exit code (
pcapkit/vendor/__main__.py) —run()returns whether its target succeeded;main()attempts every target regardless of earlier failures and returns1if any raised. A raising target's qualified name and exceptionrepralso reach stderr unconditionally viaprint(), since the warning is filterable andPCAPKIT_VERBOSEdefaults false. This exit code stays load-bearing for every consumer — including a manual run — and is unchanged from earlier rounds.2. Atomic write (
pcapkit/vendor/default.py) —Vendor.__init__'s previous last line,open(const_file, 'w'), truncates the file immediately, before any content is written. A failure during the write itself (not just fetch or render, which already left the file untouched) could therefore destroy a good file and leave a truncated one to be committed._write_atomic()now renders to a temp file in the same directory andos.replace()s onto the target — atomic on the same filesystem — so every failure mode leaves the previous file bit-for-bit untouched.On per-target restore: I concluded the workflow does not need a
git checkout -- <path>step per failed target, and did not add one. With the atomic write, a failed target's own const file is never touched at all — not modified, not truncated — sogit diff/git statusshows no change for that path in the first place; there is nothing to discard. Consequently I also did not add a formal, machine-parseable failed-target contract to the CLI: the workflow'sgrep ' failed: 'against the existing human-readable stderr line (added in round 1) is best-effort only, used to enrich the$GITHUB_STEP_SUMMARY/::warning::text — if the format ever drifts, the workflow falls back to a generic "see the step log" annotation rather than depending on the grep for correctness.3. Workflow (
.github/workflows/cron-vendor.yml) — the "Update Vendor" step no longer aborts underbash -ewhenpcapkit-vendorexits non-zero: the exit code is captured explicitly (set +e/set -earound the one command, not a bare|| true), so targets that succeeded still getisorted, diffed, committed and pushed. Failures are surfaced loudly: the full stderr is replayed into the step log, a### :warning:block naming the failed target(s) is appended to$GITHUB_STEP_SUMMARY, and a::warning::annotation is emitted per failed target — visible on the run without opening the log. Verified directly against stubpcapkit-vendor/isortexecutables in a scratch sandbox: multi-failure, clean success, and a non-zero exit with no parseable target line all behave as intended, and the step's own exit code is always0so later steps proceed.Tests:
tests/vendor/test_vendor_exit_code_unit.py(5 methods) andtests/vendor/test_vendor_atomic_write_unit.py(2 methods), both through the real entrypoints with stubVendorsubclasses — no network calls. The atomic-write regression method simulates a failure during the write by makingprint()raise once the destination is already open; run against pre-fixdefault.pyit fails (the previous file is truncated to''), and passes with the fix. 7/7 methods green together under plainunittest.