personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(release): bind advisory policy runs to live snapshots

The advisory check previously failed closed on every real invocation: the generated config used a duration string rejected by the cargo-deny 0.20.2 pin, and the command ran from a working directory with no Cargo.toml. It also measured advisory identity at db_root rather than the tree cargo-deny actually scanned, so a db_root inside the checkout could git-walk-up and record solstone's own commit as the advisory identity.

Measure identity at the single cargo-deny snapshot under db_root instead. The snapshot must be its own git top level, and the scanned path parsed from -L debug must equal the measured tree. Freshness now comes from measured values: FETCH_HEAD mtime within 24h and HEAD commit timestamp within 14 days, because cargo-deny's own staleness bound reads FETCH_HEAD mtime only and is silently skipped when that file is absent.

Record a measured advisory count so a zero-advisory snapshot cannot pass. Remove RELEASE_ADVISORY_MODE and RELEASE_ADVISORY_ACQUIRED_AT, along with the refresh-once network fetch, so no advisory acquisition is reachable from candidate entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

+830 -470
+2 -1
AGENTS.md
··· 159 159 stale payload/evidence. It verifies the expected source commit and lock state, 160 160 gathers target evidence through configured build/proof-host channels, 161 161 pair-promotes payload and evidence, and prints canonical local readiness JSON. 162 - This is candidate evidence only, not publication authorization. 162 + This is candidate evidence only, not publication authorization. Advisory acquisition 163 + is a separate operator operation documented in `scripts/release_advisory_policy.py`. 163 164 164 165 `bash scripts/release.sh --recover <version> <source-commit>` is 165 166 retained-byte-only, read-only validation. It preserves retained payload, ledger,
+3
scripts/check_rust_release_manifest.py
··· 2174 2174 ] 2175 2175 policy_run = PolicyRun( 2176 2176 advisory_source_id="fixture-advisories", 2177 + db_snapshot_basename="advisory-db-fixture00000000", 2177 2178 db_commit="a" * 40, 2178 2179 db_archive_sha256="b" * 64, 2180 + advisory_count=1, 2179 2181 advisory_acquired_at="2026-07-20T11:00:00Z", 2182 + db_commit_timestamp="2026-07-19T12:00:00Z", 2180 2183 policy_checked_at="2026-07-20T12:00:00Z", 2181 2184 result="pass", 2182 2185 )
+2 -3
scripts/release.sh
··· 60 60 --candidate: 61 61 EXPECTED_RELEASE_COMMIT expected lowercase source commit 62 62 RELEASE_MODEL_PACKAGES include or exclude 63 - RELEASE_ADVISORY_MODE refresh-once or caller-provisioned 64 63 RELEASE_ADVISORY_SOURCE_NAME public advisory source id 65 64 RELEASE_ADVISORY_DB_URL explicit non-GitHub advisory DB source 65 + RELEASE_ADVISORY_DB_ROOT cargo-deny advisory db parent; see 66 + scripts/release_advisory_policy.py 66 67 RELEASE_BUILD_HOST_CHANNEL external build-host adapter command 67 68 RELEASE_PROOF_HOST_LINUX_X86_64_MUSL_CHANNEL 68 69 external proof-host adapter command ··· 70 71 external proof-host adapter command 71 72 RELEASE_PROOF_HOST_MACOS_ARM64_CHANNEL 72 73 external proof-host adapter command 73 - RELEASE_ADVISORY_DB_ROOT caller-provisioned mode only 74 - RELEASE_ADVISORY_ACQUIRED_AT caller-provisioned mode only 75 74 76 75 --recover: 77 76 VERSION and SOURCE_COMMIT are required positional selectors. Recovery does
+395 -128
scripts/release_advisory_policy.py
··· 2 2 # SPDX-License-Identifier: AGPL-3.0-only 3 3 # Copyright (c) 2026 sol pbc 4 4 5 - """Advisory snapshot binding for release candidates.""" 5 + """Advisory snapshot binding for release candidates. 6 + 7 + Release candidates consume an operator-provisioned advisory database root. The root 8 + is a plain, non-git parent directory containing exactly one cargo-deny snapshot 9 + directory plus cargo-deny's parent-level ``db.lock``. The release rail measures git 10 + identity inside that snapshot only: the snapshot's git top level must resolve to the 11 + snapshot itself, and the receipt records the snapshot basename, HEAD commit, archive 12 + digest, advisory count, FETCH_HEAD mtime, HEAD commit timestamp, check time, and pass 13 + result. Absolute paths are never recorded. 14 + 15 + Freshness has two independent bounds. Fetch recency is the measured 16 + ``.git/FETCH_HEAD`` mtime and must be within 24 hours. Content age is the measured 17 + HEAD commit timestamp and must be within 14 days. FETCH_HEAD mtime is mutable 18 + filesystem metadata; the commit timestamp is derived from the commit object named by 19 + ``db_commit``. 20 + 21 + To acquire a conforming snapshot, write a cargo-deny config that sets the same 22 + ``db-path`` and non-GitHub ``db-urls`` supplied to the release rail, then run 23 + ``cargo-deny --config <cfg> --manifest-path core/Cargo.toml fetch db`` twice. The 24 + second run is intentional: cargo-deny 0.20.2 does not write ``.git/FETCH_HEAD`` on the 25 + first clone into an empty db root, but it does on subsequent fetches. Do not run 26 + manual ``git fetch`` or ``git reset``. ``make audit`` is not this acquisition 27 + operation; it uses cargo-deny's default db path, not a controlled release db root. 28 + """ 6 29 7 30 from __future__ import annotations 8 31 9 32 import hashlib 10 33 import json 11 34 import re 12 - import shutil 13 35 import subprocess 14 36 import tempfile 15 37 from collections.abc import Callable, Sequence ··· 18 40 from pathlib import Path 19 41 from urllib.parse import urlparse 20 42 21 - from scripts.check_rust_release_manifest import SHA256_RE, SOURCE_COMMIT_RE, Failure 43 + from scripts.check_rust_release_manifest import ( 44 + RFC3339_UTC_RE, 45 + SHA256_RE, 46 + SOURCE_COMMIT_RE, 47 + Failure, 48 + ) 22 49 from scripts.release_tool_pins import CARGO_DENY_PIN 23 50 24 - PolicyMode = str 25 51 Runner = Callable[..., subprocess.CompletedProcess[str]] 26 52 TempPathFactory = Callable[[str], Path] 27 53 Clock = Callable[[], datetime] ··· 29 55 PathRemover = Callable[[Path], None] 30 56 31 57 ADVISORY_TABLE_RE = re.compile(r"(?m)^\s*\[\s*advisories\s*\]\s*(?:#.*)?$") 58 + ADVISORY_DB_DEBUG_RE = re.compile(r"Opening advisory database at '(?P<path>[^']+)'") 32 59 SOURCE_ID_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$") 33 - MAXIMUM_DB_STALENESS = "24 hours" 34 - MAXIMUM_DB_STALENESS_DELTA = timedelta(hours=24) 60 + MAXIMUM_DB_FETCH_STALENESS_DELTA = timedelta(hours=24) 61 + MAXIMUM_DB_CONTENT_AGE_DELTA = timedelta(days=14) 35 62 ARCHIVE_PREFIX = "advisory-db/" 36 63 37 64 38 65 @dataclass(frozen=True) 39 66 class PolicyRun: 40 67 advisory_source_id: str 68 + db_snapshot_basename: str 41 69 db_commit: str 42 70 db_archive_sha256: str 71 + advisory_count: int 43 72 advisory_acquired_at: str 73 + db_commit_timestamp: str 44 74 policy_checked_at: str 45 75 result: str 46 76 47 77 def __post_init__(self) -> None: 48 - failures = validate_snapshot_identity( 49 - "policy_run", 50 - db_commit=self.db_commit, 51 - db_archive_sha256=self.db_archive_sha256, 52 - ) 78 + failures = [ 79 + *validate_snapshot_identity( 80 + "policy_run", 81 + db_commit=self.db_commit, 82 + db_archive_sha256=self.db_archive_sha256, 83 + ), 84 + *_validate_policy_run_receipt(self), 85 + ] 53 86 if failures: 54 87 raise ReleasePolicyError(failures) 55 88 ··· 61 94 } 62 95 63 96 97 + def _validate_policy_run_receipt(policy_run: PolicyRun) -> list[Failure]: 98 + failures: list[Failure] = [] 99 + if not _safe_snapshot_basename(policy_run.db_snapshot_basename): 100 + failures.append( 101 + _failure( 102 + "policy_run.db_snapshot_basename is invalid", 103 + expected="safe snapshot directory basename", 104 + actual=repr(policy_run.db_snapshot_basename), 105 + repair="python3 scripts/check_rust_release_manifest.py", 106 + ) 107 + ) 108 + if type(policy_run.advisory_count) is not int or policy_run.advisory_count <= 0: 109 + failures.append( 110 + _failure( 111 + "policy_run.advisory_count is invalid", 112 + expected="positive integer advisory count", 113 + actual=repr(policy_run.advisory_count), 114 + repair="python3 scripts/check_rust_release_manifest.py", 115 + ) 116 + ) 117 + for key in ( 118 + "advisory_acquired_at", 119 + "db_commit_timestamp", 120 + "policy_checked_at", 121 + ): 122 + value = getattr(policy_run, key) 123 + if not _is_normalized_utc_timestamp(value): 124 + failures.append( 125 + _failure( 126 + f"policy_run.{key} is invalid", 127 + expected="RFC3339 UTC timestamp normalized with Z", 128 + actual=repr(value), 129 + repair="python3 scripts/check_rust_release_manifest.py", 130 + ) 131 + ) 132 + if policy_run.result != "pass": 133 + failures.append( 134 + _failure( 135 + "policy_run.result is invalid", 136 + expected="pass", 137 + actual=repr(policy_run.result), 138 + repair="python3 scripts/check_rust_release_manifest.py", 139 + ) 140 + ) 141 + return failures 142 + 143 + 144 + def _safe_snapshot_basename(value: object) -> bool: 145 + return ( 146 + isinstance(value, str) 147 + and bool(value) 148 + and value not in {".", ".."} 149 + and Path(value).name == value 150 + and "/" not in value 151 + and "\\" not in value 152 + ) 153 + 154 + 64 155 class ReleasePolicyError(RuntimeError): 65 156 def __init__(self, failures: Sequence[Failure]) -> None: 66 157 self.failures = tuple(failures) ··· 121 212 path.unlink() 122 213 123 214 124 - def _remove_tree(path: Path) -> None: 125 - shutil.rmtree(path) 126 - 127 - 128 215 def _remove_dir(path: Path) -> None: 129 216 path.rmdir() 130 217 ··· 133 220 return json.dumps(value, ensure_ascii=False) 134 221 135 222 223 + def _cargo_deny_duration(delta: timedelta) -> str: 224 + total_seconds = delta.total_seconds() 225 + if total_seconds <= 0 or not float(total_seconds).is_integer(): 226 + raise AssertionError("cargo-deny duration must be a positive whole second") 227 + seconds = int(total_seconds) 228 + if seconds % 3600 == 0: 229 + return f"PT{seconds // 3600}H" 230 + if seconds % 60 == 0: 231 + return f"PT{seconds // 60}M" 232 + return f"PT{seconds}S" 233 + 234 + 235 + def _duration_label(delta: timedelta) -> str: 236 + seconds = int(delta.total_seconds()) 237 + if seconds % 86400 == 0: 238 + return f"{seconds // 86400}d" 239 + if seconds % 3600 == 0: 240 + return f"{seconds // 3600}h" 241 + return f"{seconds}s" 242 + 243 + 136 244 def _advisory_host(value: str) -> str | None: 137 245 parsed = urlparse(value) 138 246 if parsed.scheme and parsed.hostname: ··· 231 339 f"db-path = {_toml_string(str(db_root))}\n" 232 340 f"db-urls = [{urls}]\n" 233 341 "git-fetch-with-cli = true\n" 234 - f"maximum-db-staleness = {_toml_string(MAXIMUM_DB_STALENESS)}\n" 342 + "maximum-db-staleness = " 343 + f"{_toml_string(_cargo_deny_duration(MAXIMUM_DB_FETCH_STALENESS_DELTA))}\n" 235 344 ) 236 345 return prefix + block.encode("utf-8") 237 346 ··· 286 395 return result 287 396 288 397 289 - def _git_stdout(runner: Runner, db_root: Path, args: Sequence[str]) -> str: 290 - return _run(runner, ["git", "-C", str(db_root), *args]).stdout.strip() 398 + def _git_stdout(runner: Runner, git_root: Path, args: Sequence[str]) -> str: 399 + return _run(runner, ["git", "-C", str(git_root), *args]).stdout.strip() 400 + 401 + 402 + def _realpath(path: Path) -> Path: 403 + return path.resolve(strict=False) 404 + 405 + 406 + def _locate_advisory_snapshot(db_root: Path) -> Path: 407 + try: 408 + entries = sorted(db_root.iterdir(), key=lambda path: path.name) 409 + except OSError as exc: 410 + raise ReleasePolicyError( 411 + [ 412 + _failure( 413 + "advisory db root could not be inspected", 414 + expected="existing advisory db root containing one snapshot", 415 + actual=type(exc).__name__, 416 + repair="provision RELEASE_ADVISORY_DB_ROOT with cargo-deny fetch db", 417 + ) 418 + ] 419 + ) from None 420 + visible_entries = [path for path in entries if path.name != "db.lock"] 421 + unexpected = [ 422 + path.name for path in visible_entries if path.is_symlink() or not path.is_dir() 423 + ] 424 + if unexpected: 425 + raise ReleasePolicyError( 426 + [ 427 + _failure( 428 + "advisory db root contains unexpected entries", 429 + expected="one snapshot directory plus db.lock", 430 + actual=", ".join(unexpected), 431 + repair="provision a clean RELEASE_ADVISORY_DB_ROOT with cargo-deny fetch db", 432 + ) 433 + ] 434 + ) 435 + snapshots = [path for path in visible_entries if path.is_dir()] 436 + if len(snapshots) != 1: 437 + raise ReleasePolicyError( 438 + [ 439 + _failure( 440 + "advisory db snapshot count is invalid", 441 + expected="exactly one snapshot directory under RELEASE_ADVISORY_DB_ROOT", 442 + actual=str(len(snapshots)), 443 + repair="provision a clean RELEASE_ADVISORY_DB_ROOT with cargo-deny fetch db", 444 + ) 445 + ] 446 + ) 447 + return snapshots[0] 448 + 449 + 450 + def _assert_snapshot_git_top_level(runner: Runner, snapshot: Path) -> None: 451 + try: 452 + top_level = _git_stdout(runner, snapshot, ["rev-parse", "--show-toplevel"]) 453 + except ReleasePolicyError as exc: 454 + raise ReleasePolicyError( 455 + [ 456 + _failure( 457 + "advisory db snapshot git root could not be resolved", 458 + expected="snapshot directory is a git checkout root", 459 + actual="git rev-parse failed", 460 + repair="reacquire RELEASE_ADVISORY_DB_ROOT with cargo-deny fetch db", 461 + ) 462 + ] 463 + ) from exc 464 + if _realpath(Path(top_level)) != _realpath(snapshot): 465 + raise ReleasePolicyError( 466 + [ 467 + _failure( 468 + "advisory db snapshot is not an isolated git checkout", 469 + expected=str(_realpath(snapshot)), 470 + actual=str(_realpath(Path(top_level))), 471 + repair="set RELEASE_ADVISORY_DB_ROOT to cargo-deny's non-git parent directory", 472 + ) 473 + ] 474 + ) 475 + 476 + 477 + def _assert_snapshot_clean(runner: Runner, snapshot: Path) -> None: 478 + clean = _git_stdout( 479 + runner, 480 + snapshot, 481 + [ 482 + "status", 483 + "--porcelain=v1", 484 + "--untracked-files=all", 485 + "--ignored=matching", 486 + ], 487 + ) 488 + if clean: 489 + raise ReleasePolicyError( 490 + [ 491 + _failure( 492 + "advisory db snapshot has uncommitted or ignored material", 493 + expected="empty git status including ignored and untracked files", 494 + actual=clean, 495 + repair=( 496 + "git -C <advisory-db-snapshot> status --porcelain=v1 " 497 + "--untracked-files=all --ignored=matching" 498 + ), 499 + ) 500 + ] 501 + ) 502 + 503 + 504 + def _count_advisories(snapshot: Path) -> int: 505 + return sum( 506 + 1 507 + for path in snapshot.glob("crates/**/RUSTSEC-*.md") 508 + if path.is_file() and not path.is_symlink() 509 + ) 510 + 511 + 512 + def _validate_advisory_count(count: int) -> None: 513 + if count <= 0: 514 + raise ReleasePolicyError( 515 + [ 516 + _failure( 517 + "advisory db snapshot contains no advisories", 518 + expected="at least one crates/**/RUSTSEC-*.md advisory", 519 + actual="0", 520 + repair="reacquire RELEASE_ADVISORY_DB_ROOT from a populated advisory mirror", 521 + ) 522 + ] 523 + ) 524 + 525 + 526 + def _fetch_head_mtime(snapshot: Path) -> datetime: 527 + fetch_head = snapshot / ".git" / "FETCH_HEAD" 528 + if fetch_head.is_symlink() or not fetch_head.is_file(): 529 + raise ReleasePolicyError( 530 + [ 531 + _failure( 532 + "advisory db FETCH_HEAD is missing", 533 + expected="snapshot .git/FETCH_HEAD written by cargo-deny fetch db", 534 + actual="<missing>", 535 + repair="run cargo-deny --config <cfg> --manifest-path core/Cargo.toml fetch db twice", 536 + ) 537 + ] 538 + ) 539 + return datetime.fromtimestamp(fetch_head.stat().st_mtime, UTC) 291 540 292 541 293 542 def _strip_one_trailing_newline(value: str) -> str: ··· 311 560 return value 312 561 313 562 563 + def _git_db_commit_timestamp(runner: Runner, snapshot: Path) -> datetime: 564 + value = _git_stdout(runner, snapshot, ["show", "-s", "--format=%cI", "HEAD"]) 565 + return _parse_utc(value, label="advisory db commit timestamp") 566 + 567 + 568 + def _advisory_check_argv(cargo_deny: str, config_path: Path, root: Path) -> list[str]: 569 + return [ 570 + cargo_deny, 571 + "--config", 572 + str(config_path), 573 + "--manifest-path", 574 + str(root / "core" / "Cargo.toml"), 575 + "-L", 576 + "debug", 577 + "--locked", 578 + "--offline", 579 + "check", 580 + "advisories", 581 + ] 582 + 583 + 584 + def _scanned_advisory_db(stderr: str) -> Path: 585 + matches = [match.group("path") for match in ADVISORY_DB_DEBUG_RE.finditer(stderr)] 586 + if len(matches) != 1: 587 + raise ReleasePolicyError( 588 + [ 589 + _failure( 590 + "cargo-deny advisory database debug line is missing", 591 + expected="exactly one Opening advisory database at '<path>' debug line", 592 + actual=str(len(matches)), 593 + repair="run the pinned cargo-deny with -L debug and inspect stderr", 594 + ) 595 + ] 596 + ) 597 + return Path(matches[0]) 598 + 599 + 600 + def _assert_scanned_snapshot(stderr: str, snapshot: Path) -> None: 601 + scanned = _scanned_advisory_db(stderr) 602 + if _realpath(scanned) != _realpath(snapshot): 603 + raise ReleasePolicyError( 604 + [ 605 + _failure( 606 + "cargo-deny scanned a different advisory database", 607 + expected=str(_realpath(snapshot)), 608 + actual=str(_realpath(scanned)), 609 + repair="provision RELEASE_ADVISORY_DB_ROOT with exactly one cargo-deny snapshot", 610 + ) 611 + ] 612 + ) 613 + 614 + 314 615 def _default_archive_hasher(db_root: Path) -> str: 315 616 result = subprocess.run( 316 617 [ ··· 341 642 return hashlib.sha256(result.stdout).hexdigest() 342 643 343 644 344 - def _parse_utc(value: str) -> datetime: 645 + def _parse_utc(value: str, *, label: str = "advisory acquisition time") -> datetime: 345 646 try: 346 647 parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) 347 - except ValueError as exc: 648 + except (AttributeError, ValueError) as exc: 348 649 raise ReleasePolicyError( 349 650 [ 350 651 _failure( 351 - "advisory acquisition time is not RFC3339", 652 + f"{label} is not RFC3339", 352 653 expected="RFC3339 timestamp with UTC offset", 353 - actual=value or "<empty>", 654 + actual=str(value) if value else "<empty>", 354 655 repair="python3 scripts/check_rust_release_manifest.py", 355 656 ) 356 657 ] ··· 359 660 raise ReleasePolicyError( 360 661 [ 361 662 _failure( 362 - "advisory acquisition time is missing an offset", 663 + f"{label} is missing an offset", 363 664 expected="RFC3339 timestamp with UTC offset", 364 - actual=value, 665 + actual=str(value), 365 666 repair="python3 scripts/check_rust_release_manifest.py", 366 667 ) 367 668 ] ··· 375 676 ) 376 677 377 678 679 + def _is_normalized_utc_timestamp(value: object) -> bool: 680 + if not isinstance(value, str) or not RFC3339_UTC_RE.fullmatch(value): 681 + return False 682 + try: 683 + return _format_utc(_parse_utc(value)) == value 684 + except ReleasePolicyError: 685 + return False 686 + 687 + 378 688 def _cleanup_temp( 379 689 temp_root: Path, 380 690 config_path: Path | None, 381 691 *, 382 - remove_tree: bool, 383 692 unlink_path: PathRemover = _unlink_path, 384 - remove_tree_path: PathRemover = _remove_tree, 385 693 remove_dir: PathRemover = _remove_dir, 386 694 ) -> None: 387 695 try: 388 - if remove_tree: 389 - if temp_root.exists(): 390 - remove_tree_path(temp_root) 391 - return 392 696 if config_path is not None and config_path.exists(): 393 697 unlink_path(config_path) 394 698 if temp_root.exists(): 395 699 remove_dir(temp_root) 396 700 except OSError as exc: 397 - operation = ( 398 - "refresh temp removal" if remove_tree else "materialized config removal" 399 - ) 400 701 raise ReleasePolicyError( 401 702 [ 402 703 _failure( 403 - f"release advisory cleanup failed during {operation}", 704 + "release advisory cleanup failed during materialized config removal", 404 705 expected="owned release advisory temporary files removed", 405 706 actual=type(exc).__name__, 406 707 repair="python3 scripts/check_rust_release_manifest.py", ··· 421 722 def _validate_acquisition_freshness( 422 723 *, 423 724 advisory_acquired_at: str, 424 - acquired: datetime, 725 + fetch_acquired: datetime, 726 + db_commit_timestamp: str, 727 + db_commit_time: datetime, 425 728 policy_time: datetime, 426 729 ) -> None: 427 730 failures: list[Failure] = [] 428 731 policy_utc = policy_time.astimezone(UTC) 429 - if acquired > policy_utc: 732 + if fetch_acquired > policy_utc: 430 733 failures.append( 431 734 _failure( 432 - "advisory acquisition time is in the future", 433 - expected="acquisition time at or before policy check time", 735 + "advisory fetch time is in the future", 736 + expected="FETCH_HEAD mtime at or before policy check time", 434 737 actual=advisory_acquired_at, 435 738 repair="python3 scripts/check_rust_release_manifest.py", 436 739 ) 437 740 ) 438 - elif policy_utc - acquired > MAXIMUM_DB_STALENESS_DELTA: 741 + elif policy_utc - fetch_acquired > MAXIMUM_DB_FETCH_STALENESS_DELTA: 439 742 failures.append( 440 743 _failure( 441 - "advisory acquisition time is stale", 442 - expected=f"acquisition time within {MAXIMUM_DB_STALENESS}", 744 + "advisory fetch time is stale", 745 + expected=( 746 + "FETCH_HEAD mtime within " 747 + f"{_duration_label(MAXIMUM_DB_FETCH_STALENESS_DELTA)}" 748 + ), 443 749 actual=advisory_acquired_at, 444 750 repair="python3 scripts/check_rust_release_manifest.py", 445 751 ) 446 752 ) 753 + if db_commit_time > policy_utc: 754 + failures.append( 755 + _failure( 756 + "advisory db commit timestamp is in the future", 757 + expected="HEAD commit timestamp at or before policy check time", 758 + actual=db_commit_timestamp, 759 + repair="python3 scripts/check_rust_release_manifest.py", 760 + ) 761 + ) 762 + elif policy_utc - db_commit_time > MAXIMUM_DB_CONTENT_AGE_DELTA: 763 + failures.append( 764 + _failure( 765 + "advisory db content is stale", 766 + expected=( 767 + "HEAD commit timestamp within " 768 + f"{_duration_label(MAXIMUM_DB_CONTENT_AGE_DELTA)}" 769 + ), 770 + actual=db_commit_timestamp, 771 + repair="reacquire RELEASE_ADVISORY_DB_ROOT from a current advisory mirror", 772 + ) 773 + ) 447 774 if failures: 448 775 raise ReleasePolicyError(failures) 449 776 ··· 453 780 *, 454 781 advisory_source_id: str, 455 782 db_urls: Sequence[str], 456 - mode: PolicyMode, 457 - advisory_acquired_at: str | None = None, 458 - db_root: Path | None = None, 783 + db_root: Path, 459 784 cargo_deny: str = "cargo-deny", 460 785 runner: Runner = subprocess.run, 461 786 temp_path_factory: TempPathFactory = _default_temp_path_factory, 462 787 clock: Clock = _utc_now, 463 788 archive_hasher: ArchiveHasher = _default_archive_hasher, 464 789 cleanup_unlink: PathRemover = _unlink_path, 465 - cleanup_rmtree: PathRemover = _remove_tree, 466 790 cleanup_rmdir: PathRemover = _remove_dir, 467 791 ) -> PolicyRun: 468 792 failures = _validate_source(advisory_source_id, db_urls) 469 793 if failures: 470 794 raise ReleasePolicyError(failures) 471 - if mode not in {"refresh-once", "caller-provisioned"}: 795 + if db_root is None: 472 796 raise ReleasePolicyError( 473 797 [ 474 798 _failure( 475 - "advisory policy mode is unsupported", 476 - expected="refresh-once or caller-provisioned", 477 - actual=mode, 478 - repair="python3 scripts/check_rust_release_manifest.py", 479 - ) 480 - ] 481 - ) 482 - if mode == "caller-provisioned" and db_root is None: 483 - raise ReleasePolicyError( 484 - [ 485 - _failure( 486 - "caller-provisioned advisory mode has no db root", 487 - expected="existing advisory db root", 488 - actual="<missing>", 489 - repair="python3 scripts/check_rust_release_manifest.py", 490 - ) 491 - ] 492 - ) 493 - if mode == "caller-provisioned" and advisory_acquired_at is None: 494 - raise ReleasePolicyError( 495 - [ 496 - _failure( 497 - "caller-provisioned advisory mode has no acquisition time", 498 - expected="trusted advisory acquisition RFC3339 timestamp", 799 + "release advisory db root is missing", 800 + expected="RELEASE_ADVISORY_DB_ROOT containing one cargo-deny snapshot", 499 801 actual="<missing>", 500 - repair="python3 scripts/check_rust_release_manifest.py", 802 + repair="provision RELEASE_ADVISORY_DB_ROOT with cargo-deny fetch db", 501 803 ) 502 804 ] 503 805 ) 504 - if mode == "caller-provisioned" and advisory_acquired_at is not None: 505 - _parse_utc(advisory_acquired_at) 506 806 507 807 temp_root = temp_path_factory("advisory-policy") 508 - resolved_db_root = db_root or (temp_root / "advisory-db") 509 808 config_path: Path | None = None 510 809 result: PolicyRun | None = None 511 810 primary_error: ReleasePolicyError | None = None ··· 513 812 config_path = _write_materialized_config( 514 813 root, 515 814 temp_root, 516 - db_root=resolved_db_root, 815 + db_root=db_root, 517 816 db_urls=db_urls, 518 817 ) 519 818 520 - if mode == "refresh-once": 521 - _run( 522 - runner, 523 - [cargo_deny, "--config", str(config_path), "fetch", "db"], 524 - cwd=root, 525 - ) 526 - 527 - clean = _git_stdout( 528 - runner, 529 - resolved_db_root, 530 - [ 531 - "status", 532 - "--porcelain=v1", 533 - "--untracked-files=all", 534 - "--ignored=matching", 535 - ], 536 - ) 537 - if clean: 538 - raise ReleasePolicyError( 539 - [ 540 - _failure( 541 - "advisory db has uncommitted or ignored material", 542 - expected="empty git status including ignored and untracked files", 543 - actual=clean, 544 - repair=( 545 - "git -C <advisory-db-root> status --porcelain=v1 " 546 - "--untracked-files=all --ignored=matching" 547 - ), 548 - ) 549 - ] 550 - ) 551 - db_commit = _git_db_commit(runner, resolved_db_root) 552 - db_archive_sha256 = archive_hasher(resolved_db_root) 819 + snapshot = _locate_advisory_snapshot(db_root) 820 + _assert_snapshot_git_top_level(runner, snapshot) 821 + _assert_snapshot_clean(runner, snapshot) 822 + db_commit = _git_db_commit(runner, snapshot) 823 + db_archive_sha256 = archive_hasher(snapshot) 553 824 failures = validate_snapshot_identity( 554 825 "advisory_snapshot", 555 826 db_commit=db_commit, ··· 557 828 ) 558 829 if failures: 559 830 raise ReleasePolicyError(failures) 560 - if mode == "refresh-once": 561 - acquisition_text = _format_utc(clock()) 562 - else: 563 - assert advisory_acquired_at is not None 564 - acquisition_text = advisory_acquired_at 565 - _parse_utc(acquisition_text) 566 - _run( 831 + db_commit_time = _git_db_commit_timestamp(runner, snapshot) 832 + db_commit_timestamp_text = _format_utc(db_commit_time) 833 + advisory_count = _count_advisories(snapshot) 834 + _validate_advisory_count(advisory_count) 835 + check_result = _run( 567 836 runner, 568 - [ 569 - cargo_deny, 570 - "--config", 571 - str(config_path), 572 - "--locked", 573 - "--offline", 574 - "check", 575 - "advisories", 576 - ], 837 + _advisory_check_argv(cargo_deny, config_path, root), 577 838 cwd=root, 578 839 ) 840 + _assert_scanned_snapshot(check_result.stderr, snapshot) 841 + _assert_snapshot_clean(runner, snapshot) 579 842 580 843 policy_time = clock() 581 - acquired = _parse_utc(acquisition_text) 844 + fetch_acquired = _fetch_head_mtime(snapshot) 845 + acquisition_text = _format_utc(fetch_acquired) 582 846 _validate_acquisition_freshness( 583 847 advisory_acquired_at=acquisition_text, 584 - acquired=acquired, 848 + fetch_acquired=fetch_acquired, 849 + db_commit_timestamp=db_commit_timestamp_text, 850 + db_commit_time=db_commit_time, 585 851 policy_time=policy_time, 586 852 ) 587 853 result = PolicyRun( 588 854 advisory_source_id=advisory_source_id, 855 + db_snapshot_basename=snapshot.name, 589 856 db_commit=db_commit, 590 857 db_archive_sha256=db_archive_sha256, 858 + advisory_count=advisory_count, 591 859 advisory_acquired_at=acquisition_text, 860 + db_commit_timestamp=db_commit_timestamp_text, 592 861 policy_checked_at=_format_utc(policy_time), 593 862 result="pass", 594 863 ) ··· 600 869 _cleanup_temp( 601 870 temp_root, 602 871 config_path, 603 - remove_tree=mode == "refresh-once", 604 872 unlink_path=cleanup_unlink, 605 - remove_tree_path=cleanup_rmtree, 606 873 remove_dir=cleanup_rmdir, 607 874 ) 608 875 except ReleasePolicyError as exc:
+29 -6
scripts/release_candidate_driver.py
··· 452 452 root, 453 453 advisory_source_id=env["RELEASE_ADVISORY_SOURCE_NAME"], 454 454 db_urls=(env["RELEASE_ADVISORY_DB_URL"],), 455 - mode=env["RELEASE_ADVISORY_MODE"], 456 - advisory_acquired_at=env.get("RELEASE_ADVISORY_ACQUIRED_AT"), 457 - db_root=Path(env["RELEASE_ADVISORY_DB_ROOT"]) 458 - if env.get("RELEASE_ADVISORY_DB_ROOT") 459 - else None, 455 + db_root=Path(env["RELEASE_ADVISORY_DB_ROOT"]), 460 456 ) 461 457 462 458 ··· 1912 1908 repair="bash scripts/release.sh --recover", 1913 1909 ) 1914 1910 ) 1915 - for key in ("advisory_acquired_at", "policy_checked_at"): 1911 + snapshot = policy_run.get("db_snapshot_basename") 1912 + if not _safe_retained_basename(snapshot): 1913 + failures.append( 1914 + _failure( 1915 + "retained ledger db snapshot basename is invalid", 1916 + expected="safe snapshot directory basename", 1917 + actual=repr(snapshot), 1918 + repair="bash scripts/release.sh --recover", 1919 + ) 1920 + ) 1921 + advisory_count = policy_run.get("advisory_count") 1922 + if type(advisory_count) is not int or advisory_count <= 0: 1923 + failures.append( 1924 + _failure( 1925 + "retained ledger advisory count is invalid", 1926 + expected="positive integer advisory count", 1927 + actual=repr(advisory_count), 1928 + repair="bash scripts/release.sh --recover", 1929 + ) 1930 + ) 1931 + for key in ( 1932 + "advisory_acquired_at", 1933 + "db_commit_timestamp", 1934 + "policy_checked_at", 1935 + ): 1916 1936 value = policy_run.get(key) 1917 1937 if not isinstance(value, str) or not RFC3339_UTC_RE.fullmatch(value): 1918 1938 failures.append( ··· 2131 2151 failures.extend(_validate_policy_payload(policy_payload)) 2132 2152 if policy_run is not None and policy_payload != { 2133 2153 "advisory_source_id": policy_run.advisory_source_id, 2154 + "db_snapshot_basename": policy_run.db_snapshot_basename, 2134 2155 "db_commit": policy_run.db_commit, 2135 2156 "db_archive_sha256": policy_run.db_archive_sha256, 2157 + "advisory_count": policy_run.advisory_count, 2136 2158 "advisory_acquired_at": policy_run.advisory_acquired_at, 2159 + "db_commit_timestamp": policy_run.db_commit_timestamp, 2137 2160 "policy_checked_at": policy_run.policy_checked_at, 2138 2161 "result": policy_run.result, 2139 2162 }:
+7 -1
scripts/release_ledger.py
··· 55 55 POLICY_RUN_KEYS = frozenset( 56 56 ( 57 57 "advisory_source_id", 58 + "db_snapshot_basename", 58 59 "db_commit", 59 60 "db_archive_sha256", 61 + "advisory_count", 60 62 "advisory_acquired_at", 63 + "db_commit_timestamp", 61 64 "policy_checked_at", 62 65 "result", 63 66 ) ··· 126 129 return failures 127 130 128 131 129 - def _policy_run_payload(policy_run: PolicyRun) -> dict[str, str]: 132 + def _policy_run_payload(policy_run: PolicyRun) -> dict[str, Any]: 130 133 payload = { 131 134 "advisory_source_id": policy_run.advisory_source_id, 135 + "db_snapshot_basename": policy_run.db_snapshot_basename, 132 136 "db_commit": policy_run.db_commit, 133 137 "db_archive_sha256": policy_run.db_archive_sha256, 138 + "advisory_count": policy_run.advisory_count, 134 139 "advisory_acquired_at": policy_run.advisory_acquired_at, 140 + "db_commit_timestamp": policy_run.db_commit_timestamp, 135 141 "policy_checked_at": policy_run.policy_checked_at, 136 142 "result": policy_run.result, 137 143 }
+385 -330
tests/test_release_advisory_policy.py
··· 3 3 4 4 from __future__ import annotations 5 5 6 + import os 6 7 import subprocess 8 + from collections.abc import Sequence 7 9 from datetime import UTC, datetime 8 10 from pathlib import Path 9 11 ··· 11 13 12 14 import scripts.check_rust_release_manifest as checker 13 15 import scripts.release_advisory_policy as policy 16 + 17 + DB_COMMIT = "a" * 40 18 + DB_ARCHIVE = "b" * 64 19 + SNAPSHOT = "advisory-db-1234567890abcdef" 20 + POLICY_TIME = datetime(2026, 7, 20, 12, 0, tzinfo=UTC) 21 + FETCH_TIME = datetime(2026, 7, 20, 11, 30, tzinfo=UTC) 22 + COMMIT_TIME = "2026-07-17T15:52:38Z" 14 23 15 24 MALFORMED_DB_COMMIT_CASES = ( 16 25 ("short-39", "a" * 39), ··· 41 50 class FakeRunner: 42 51 def __init__( 43 52 self, 53 + snapshot: Path, 44 54 *, 45 - status: str = "", 55 + status: str | Sequence[str] = "", 46 56 fail_check: bool = False, 47 - commit_stdout: str = "a" * 40 + "\n", 57 + commit_stdout: str = DB_COMMIT + "\n", 58 + commit_timestamp: str = COMMIT_TIME + "\n", 59 + top_level: Path | None = None, 60 + scanned_path: Path | None = None, 61 + debug_stderr: str | None = None, 48 62 ) -> None: 49 - self.status = status 63 + self.snapshot = snapshot 64 + self.status_outputs = list(status) if not isinstance(status, str) else [status] 50 65 self.fail_check = fail_check 51 66 self.commit_stdout = commit_stdout 67 + self.commit_timestamp = commit_timestamp 68 + self.top_level = top_level 69 + self.scanned_path = scanned_path or snapshot 70 + self.debug_stderr = debug_stderr 52 71 self.events: list[str] = [] 53 72 self.config_bytes: bytes | None = None 54 73 self.cargo_cwds: list[Path | None] = [] 74 + self.cargo_argvs: list[list[str]] = [] 55 75 56 76 def __call__(self, argv, **kwargs) -> subprocess.CompletedProcess[str]: 57 77 command = list(argv) 58 - if command[0] == "cargo-deny" and command[-2:] == ["fetch", "db"]: 59 - self.events.append("fetch") 60 - self.cargo_cwds.append(kwargs.get("cwd")) 61 - self.config_bytes = Path( 62 - command[command.index("--config") + 1] 63 - ).read_bytes() 64 - return subprocess.CompletedProcess(command, 0, "", "") 65 - if command[0] == "cargo-deny" and command[-2:] == ["check", "advisories"]: 66 - self.events.append("check") 67 - self.cargo_cwds.append(kwargs.get("cwd")) 68 - self.config_bytes = Path( 69 - command[command.index("--config") + 1] 70 - ).read_bytes() 71 - assert "--locked" in command 72 - assert "--offline" in command 73 - if self.fail_check: 74 - return subprocess.CompletedProcess(command, 1, "", "denied") 75 - return subprocess.CompletedProcess(command, 0, "", "") 76 - if command[:3] == ["git", "-C", command[2]]: 78 + if command[0] == "cargo-deny": 79 + if command[-2:] == ["fetch", "db"]: 80 + raise AssertionError("cargo-deny fetch subcommand should be gone") 81 + if command[-2:] == ["check", "advisories"]: 82 + self.events.append("check") 83 + self.cargo_cwds.append(kwargs.get("cwd")) 84 + self.cargo_argvs.append(command) 85 + self.config_bytes = Path( 86 + command[command.index("--config") + 1] 87 + ).read_bytes() 88 + if self.fail_check: 89 + return subprocess.CompletedProcess(command, 1, "", "denied") 90 + stderr = self.debug_stderr 91 + if stderr is None: 92 + stderr = ( 93 + "2026-07-21 14:40:36 [DEBUG] " 94 + f"Opening advisory database at '{self.scanned_path}'\n" 95 + ) 96 + return subprocess.CompletedProcess(command, 0, "", stderr) 97 + if command[:2] == ["git", "-C"]: 98 + git_root = Path(command[2]) 77 99 subcommand = command[3:] 100 + if subcommand == ["rev-parse", "--show-toplevel"]: 101 + self.events.append("show-toplevel") 102 + top_level = self.top_level or git_root 103 + return subprocess.CompletedProcess(command, 0, f"{top_level}\n", "") 78 104 if subcommand[:1] == ["status"]: 79 105 self.events.append("status") 80 - return subprocess.CompletedProcess(command, 0, self.status, "") 106 + output = self.status_outputs.pop(0) 107 + if not self.status_outputs: 108 + self.status_outputs.append(output) 109 + return subprocess.CompletedProcess(command, 0, output, "") 81 110 if subcommand[:2] == ["rev-parse", "--verify"]: 82 111 self.events.append("rev-parse") 83 112 return subprocess.CompletedProcess(command, 0, self.commit_stdout, "") 113 + if subcommand == ["show", "-s", "--format=%cI", "HEAD"]: 114 + self.events.append("commit-timestamp") 115 + return subprocess.CompletedProcess( 116 + command, 0, self.commit_timestamp, "" 117 + ) 84 118 raise AssertionError(f"unexpected command: {command}") 85 119 86 120 ··· 94 128 return root 95 129 96 130 97 - class ClockSequence: 98 - def __init__(self, events: list[str], *values: tuple[str, datetime]) -> None: 99 - self.events = events 100 - self.values = list(values) 131 + def _clock(events: list[str] | None = None, value: datetime = POLICY_TIME): 132 + def now() -> datetime: 133 + if events is not None: 134 + events.append("policy-clock") 135 + return value 136 + 137 + return now 101 138 102 - def __call__(self) -> datetime: 103 - label, value = self.values.pop(0) 104 - self.events.append(label) 105 - return value 106 139 140 + def _write_snapshot( 141 + db_root: Path, 142 + *, 143 + name: str = SNAPSHOT, 144 + advisory_count: int = 1, 145 + fetch_time: datetime | None = FETCH_TIME, 146 + ) -> Path: 147 + snapshot = db_root / name 148 + (snapshot / ".git").mkdir(parents=True) 149 + for index in range(advisory_count): 150 + advisory = ( 151 + snapshot / "crates" / f"probe{index}" / f"RUSTSEC-2026-{index:04d}.md" 152 + ) 153 + advisory.parent.mkdir(parents=True, exist_ok=True) 154 + advisory.write_text( 155 + "```toml\n" 156 + "[advisory]\n" 157 + f'id = "RUSTSEC-2026-{index:04d}"\n' 158 + f'package = "probe{index}"\n' 159 + 'date = "2026-01-01"\n' 160 + 'url = "https://example.invalid/RUSTSEC-2026-0001"\n' 161 + 'categories = ["unmaintained"]\n' 162 + "keywords = []\n\n" 163 + "[versions]\n" 164 + "patched = []\n" 165 + "```\n", 166 + encoding="utf-8", 167 + ) 168 + if fetch_time is not None: 169 + fetch_head = snapshot / ".git" / "FETCH_HEAD" 170 + fetch_head.write_text("", encoding="utf-8") 171 + timestamp = fetch_time.timestamp() 172 + os.utime(fetch_head, (timestamp, timestamp)) 173 + return snapshot 107 174 108 - def _clock(events: list[str]): 109 - def now() -> datetime: 110 - events.append("policy-clock") 111 - return datetime(2026, 7, 20, 12, 0, tzinfo=UTC) 112 175 113 - return now 176 + def _db_root( 177 + tmp_path: Path, 178 + *, 179 + advisory_count: int = 1, 180 + fetch_time: datetime | None = FETCH_TIME, 181 + ) -> tuple[Path, Path]: 182 + root = tmp_path / "db-root" 183 + root.mkdir() 184 + (root / "db.lock").write_text("", encoding="utf-8") 185 + return root, _write_snapshot( 186 + root, 187 + advisory_count=advisory_count, 188 + fetch_time=fetch_time, 189 + ) 114 190 115 191 116 - def test_materialized_config_appends_advisories_and_replaces_config( 192 + def _prepare( 117 193 tmp_path: Path, 118 - ) -> None: 119 - runner = FakeRunner() 120 - events = runner.events 194 + *, 195 + runner: FakeRunner | None = None, 196 + db_root: Path | None = None, 197 + snapshot: Path | None = None, 198 + clock=None, 199 + archive: str = DB_ARCHIVE, 200 + cleanup_unlink=policy._unlink_path, 201 + cleanup_rmdir=policy._remove_dir, 202 + ) -> tuple[policy.PolicyRun, FakeRunner, Path]: 121 203 repo = _repo(tmp_path) 122 - 204 + if db_root is None or snapshot is None: 205 + db_root, snapshot = _db_root(tmp_path) 206 + runner = runner or FakeRunner(snapshot) 123 207 result = policy.prepare_policy_run( 124 208 repo, 125 - advisory_source_id="internal-feed", 126 - db_urls=("ssh://example.test/advisory-db.git",), 127 - mode="refresh-once", 209 + advisory_source_id="internal", 210 + db_urls=("ssh://example.test/db.git",), 211 + db_root=db_root, 128 212 runner=runner, 129 213 temp_path_factory=lambda label: tmp_path / label, 130 - clock=ClockSequence( 131 - events, 132 - ("acquisition-clock", datetime(2026, 7, 20, 11, 30, tzinfo=UTC)), 133 - ("policy-clock", datetime(2026, 7, 20, 12, 0, tzinfo=UTC)), 214 + clock=clock or _clock(runner.events), 215 + archive_hasher=lambda observed: ( 216 + runner.events.append(f"archive:{observed.name}") or archive 134 217 ), 135 - archive_hasher=lambda _db: events.append("archive") or "b" * 64, 218 + cleanup_unlink=cleanup_unlink, 219 + cleanup_rmdir=cleanup_rmdir, 136 220 ) 221 + return result, runner, repo 222 + 223 + 224 + def test_materialized_config_and_advisory_check_argv(tmp_path: Path) -> None: 225 + result, runner, repo = _prepare(tmp_path) 137 226 138 227 assert result.result == "pass" 228 + assert result.db_snapshot_basename == SNAPSHOT 229 + assert result.advisory_count == 1 139 230 assert result.advisory_acquired_at == "2026-07-20T11:30:00Z" 231 + assert result.db_commit_timestamp == COMMIT_TIME 140 232 assert runner.config_bytes is not None 141 233 text = runner.config_bytes.decode("utf-8") 142 234 assert text.startswith('[licenses]\nallow = ["MIT"]\n\n[advisories]\n') 143 - assert 'db-urls = ["ssh://example.test/advisory-db.git"]' in text 235 + assert 'db-urls = ["ssh://example.test/db.git"]' in text 144 236 assert "git-fetch-with-cli = true" in text 145 - assert 'maximum-db-staleness = "24 hours"' in text 146 - assert events == [ 147 - "fetch", 237 + assert 'maximum-db-staleness = "PT24H"' in text 238 + assert "24 hours" not in text 239 + 240 + argv = runner.cargo_argvs[0] 241 + assert argv == [ 242 + "cargo-deny", 243 + "--config", 244 + argv[2], 245 + "--manifest-path", 246 + str(repo / "core" / "Cargo.toml"), 247 + "-L", 248 + "debug", 249 + "--locked", 250 + "--offline", 251 + "check", 252 + "advisories", 253 + ] 254 + assert "fetch" not in runner.events 255 + assert runner.events == [ 256 + "show-toplevel", 148 257 "status", 149 258 "rev-parse", 150 - "archive", 151 - "acquisition-clock", 259 + f"archive:{SNAPSHOT}", 260 + "commit-timestamp", 152 261 "check", 262 + "status", 153 263 "policy-clock", 154 264 ] 155 - assert runner.cargo_cwds == [repo, repo] 265 + assert runner.cargo_cwds == [repo] 156 266 157 267 158 268 def test_core_deny_toml_advisories_table_fails_loudly(tmp_path: Path) -> None: 269 + db_root, _snapshot = _db_root(tmp_path) 159 270 with pytest.raises(policy.ReleasePolicyError) as exc: 160 271 policy.prepare_policy_run( 161 272 _repo(tmp_path, '[advisories]\ndb-path = "x"\n'), 162 273 advisory_source_id="internal-feed", 163 274 db_urls=("ssh://example.test/advisory-db.git",), 164 - mode="caller-provisioned", 165 - advisory_acquired_at="2026-07-20T11:00:00Z", 166 - db_root=tmp_path / "db", 167 - runner=FakeRunner(), 275 + db_root=db_root, 276 + runner=FakeRunner(db_root / SNAPSHOT), 168 277 temp_path_factory=lambda label: tmp_path / label, 169 278 ) 170 279 ··· 199 308 _repo(tmp_path), 200 309 advisory_source_id=source_id, 201 310 db_urls=db_urls, 202 - mode="caller-provisioned", 203 - advisory_acquired_at="2026-07-20T11:00:00Z", 204 311 db_root=tmp_path / "db", 205 - runner=FakeRunner(), 312 + runner=FakeRunner(tmp_path / "db" / SNAPSHOT), 206 313 temp_path_factory=lambda label: tmp_path / label, 207 314 ) 208 315 209 316 assert any(failure.error == error for failure in exc.value.failures) 210 317 211 318 212 - def test_caller_provisioned_cache_requires_clean_including_ignored( 213 - tmp_path: Path, 214 - ) -> None: 319 + def test_snapshot_count_must_be_exactly_one(tmp_path: Path) -> None: 320 + db_root = tmp_path / "empty-db" 321 + db_root.mkdir() 322 + 215 323 with pytest.raises(policy.ReleasePolicyError) as exc: 216 324 policy.prepare_policy_run( 217 325 _repo(tmp_path), 218 326 advisory_source_id="internal", 219 327 db_urls=("ssh://example.test/db.git",), 220 - mode="caller-provisioned", 221 - advisory_acquired_at="2026-07-20T11:00:00Z", 222 - db_root=tmp_path / "db", 223 - runner=FakeRunner(status="!! ignored\n?? untracked\n"), 328 + db_root=db_root, 329 + runner=FakeRunner(db_root / SNAPSHOT), 224 330 temp_path_factory=lambda label: tmp_path / label, 225 331 ) 332 + assert exc.value.failures[0].error == "advisory db snapshot count is invalid" 333 + assert exc.value.failures[0].actual == "0" 334 + 335 + db_root = tmp_path / "multi-db" 336 + db_root.mkdir() 337 + first = _write_snapshot(db_root, name="advisory-db-one") 338 + _write_snapshot(db_root, name="advisory-db-two") 339 + with pytest.raises(policy.ReleasePolicyError) as exc: 340 + policy.prepare_policy_run( 341 + _repo(tmp_path), 342 + advisory_source_id="internal", 343 + db_urls=("ssh://example.test/db.git",), 344 + db_root=db_root, 345 + runner=FakeRunner(first), 346 + temp_path_factory=lambda label: tmp_path / f"multi-{label}", 347 + ) 348 + assert exc.value.failures[0].error == "advisory db snapshot count is invalid" 349 + assert exc.value.failures[0].actual == "2" 350 + 351 + 352 + def test_non_top_level_snapshot_fails_walk_up_check(tmp_path: Path) -> None: 353 + db_root, snapshot = _db_root(tmp_path) 354 + runner = FakeRunner(snapshot, top_level=tmp_path) 355 + 356 + with pytest.raises(policy.ReleasePolicyError) as exc: 357 + _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 226 358 227 359 assert ( 228 - exc.value.failures[0].error == "advisory db has uncommitted or ignored material" 360 + exc.value.failures[0].error 361 + == "advisory db snapshot is not an isolated git checkout" 362 + ) 363 + assert "check" not in runner.events 364 + 365 + 366 + def test_scanned_advisory_db_must_match_measured_snapshot(tmp_path: Path) -> None: 367 + db_root, snapshot = _db_root(tmp_path) 368 + runner = FakeRunner(snapshot, scanned_path=tmp_path / "other-db") 369 + 370 + with pytest.raises(policy.ReleasePolicyError) as exc: 371 + _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 372 + 373 + assert ( 374 + exc.value.failures[0].error 375 + == "cargo-deny scanned a different advisory database" 376 + ) 377 + 378 + 379 + def test_missing_debug_line_fails_closed(tmp_path: Path) -> None: 380 + db_root, snapshot = _db_root(tmp_path) 381 + runner = FakeRunner(snapshot, debug_stderr="debug without database path\n") 382 + 383 + with pytest.raises(policy.ReleasePolicyError) as exc: 384 + _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 385 + 386 + assert ( 387 + exc.value.failures[0].error 388 + == "cargo-deny advisory database debug line is missing" 229 389 ) 230 390 231 391 232 - def test_refresh_clock_order_and_policy_before_acquisition_fails( 233 - tmp_path: Path, 234 - ) -> None: 235 - runner = FakeRunner() 392 + def test_absent_fetch_head_fails_closed(tmp_path: Path) -> None: 393 + db_root, snapshot = _db_root(tmp_path, fetch_time=None) 394 + runner = FakeRunner(snapshot) 395 + 396 + with pytest.raises(policy.ReleasePolicyError) as exc: 397 + _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 398 + 399 + assert exc.value.failures[0].error == "advisory db FETCH_HEAD is missing" 400 + 236 401 237 - result = policy.prepare_policy_run( 238 - _repo(tmp_path), 239 - advisory_source_id="internal", 240 - db_urls=("ssh://example.test/db.git",), 241 - mode="refresh-once", 242 - runner=runner, 243 - temp_path_factory=lambda label: tmp_path / label, 244 - clock=ClockSequence( 245 - runner.events, 246 - ("acquisition-clock", datetime(2026, 7, 20, 12, 0, tzinfo=UTC)), 247 - ("policy-clock", datetime(2026, 7, 20, 12, 1, tzinfo=UTC)), 248 - ), 249 - archive_hasher=lambda _db: runner.events.append("archive") or "b" * 64, 402 + def test_stale_fetch_head_fails(tmp_path: Path) -> None: 403 + db_root, snapshot = _db_root( 404 + tmp_path, 405 + fetch_time=datetime(2026, 7, 18, 11, 59, 59, tzinfo=UTC), 250 406 ) 251 407 252 - assert result.advisory_acquired_at == "2026-07-20T12:00:00Z" 253 - assert result.policy_checked_at == "2026-07-20T12:01:00Z" 254 - assert runner.events.index("acquisition-clock") < runner.events.index("check") 255 - assert runner.events.index("check") < runner.events.index("policy-clock") 408 + with pytest.raises(policy.ReleasePolicyError) as exc: 409 + _prepare(tmp_path, db_root=db_root, snapshot=snapshot) 410 + 411 + assert exc.value.failures[0].error == "advisory fetch time is stale" 412 + 413 + 414 + def test_over_age_content_fails(tmp_path: Path) -> None: 415 + db_root, snapshot = _db_root(tmp_path) 416 + runner = FakeRunner(snapshot, commit_timestamp="2026-07-05T11:59:59Z\n") 256 417 257 418 with pytest.raises(policy.ReleasePolicyError) as exc: 258 - policy.prepare_policy_run( 259 - _repo(tmp_path), 260 - advisory_source_id="internal", 261 - db_urls=("ssh://example.test/db.git",), 262 - mode="refresh-once", 263 - runner=FakeRunner(), 264 - temp_path_factory=lambda label: tmp_path / f"early-{label}", 265 - clock=ClockSequence( 266 - [], 267 - ("acquisition-clock", datetime(2026, 7, 20, 12, 0, tzinfo=UTC)), 268 - ("policy-clock", datetime(2026, 7, 20, 11, 59, tzinfo=UTC)), 269 - ), 270 - archive_hasher=lambda _db: "b" * 64, 271 - ) 419 + _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 420 + 421 + assert exc.value.failures[0].error == "advisory db content is stale" 422 + 423 + 424 + def test_zero_advisory_count_fails(tmp_path: Path) -> None: 425 + db_root, snapshot = _db_root(tmp_path, advisory_count=0) 426 + runner = FakeRunner(snapshot) 427 + 428 + with pytest.raises(policy.ReleasePolicyError) as exc: 429 + _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 430 + 431 + assert exc.value.failures[0].error == "advisory db snapshot contains no advisories" 432 + assert "check" not in runner.events 272 433 273 - assert exc.value.failures[0].error == "advisory acquisition time is in the future" 274 434 435 + def test_post_run_dirty_snapshot_fails(tmp_path: Path) -> None: 436 + db_root, snapshot = _db_root(tmp_path) 437 + runner = FakeRunner(snapshot, status=("", "?? late-file\n")) 275 438 276 - def test_stale_and_future_caller_acquisition_times_fail(tmp_path: Path) -> None: 277 - for acquired, expected_error in ( 278 - ("2026-07-18T11:59:59Z", "advisory acquisition time is stale"), 279 - ("2026-07-20T12:00:01Z", "advisory acquisition time is in the future"), 280 - ): 281 - with pytest.raises(policy.ReleasePolicyError) as exc: 282 - policy.prepare_policy_run( 283 - _repo(tmp_path), 284 - advisory_source_id="internal", 285 - db_urls=("ssh://example.test/db.git",), 286 - mode="caller-provisioned", 287 - advisory_acquired_at=acquired, 288 - db_root=tmp_path / "caller-db", 289 - runner=FakeRunner(), 290 - temp_path_factory=lambda label: tmp_path / f"{acquired}-{label}", 291 - clock=_clock([]), 292 - archive_hasher=lambda _db: "b" * 64, 293 - ) 294 - assert exc.value.failures[0].error == expected_error 439 + with pytest.raises(policy.ReleasePolicyError) as exc: 440 + _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 295 441 442 + assert ( 443 + exc.value.failures[0].error 444 + == "advisory db snapshot has uncommitted or ignored material" 445 + ) 446 + assert runner.events.count("status") == 2 296 447 297 - def test_old_commit_with_fresh_acquisition_passes(tmp_path: Path) -> None: 298 - runner = FakeRunner() 299 448 300 - result = policy.prepare_policy_run( 301 - _repo(tmp_path), 302 - advisory_source_id="internal", 303 - db_urls=("ssh://example.test/db.git",), 304 - mode="caller-provisioned", 305 - advisory_acquired_at="2026-07-20T11:30:00Z", 306 - db_root=tmp_path / "caller-db", 449 + def test_fresh_fetch_and_four_day_content_pass(tmp_path: Path) -> None: 450 + db_root, snapshot = _db_root(tmp_path) 451 + runner = FakeRunner(snapshot, commit_timestamp="2026-07-16T12:00:00Z\n") 452 + 453 + result, _runner, _repo_path = _prepare( 454 + tmp_path, 455 + db_root=db_root, 456 + snapshot=snapshot, 307 457 runner=runner, 308 - temp_path_factory=lambda label: tmp_path / label, 309 - clock=_clock(runner.events), 310 - archive_hasher=lambda _db: runner.events.append("archive") or "b" * 64, 311 458 ) 312 459 313 - assert result.db_commit == "a" * 40 314 460 assert result.advisory_acquired_at == "2026-07-20T11:30:00Z" 315 - assert "log" not in runner.events 461 + assert result.db_commit_timestamp == "2026-07-16T12:00:00Z" 316 462 317 463 318 464 def test_prepare_policy_run_accepts_sha256_db_commit(tmp_path: Path) -> None: 319 - result = policy.prepare_policy_run( 320 - _repo(tmp_path), 321 - advisory_source_id="internal", 322 - db_urls=("ssh://example.test/db.git",), 323 - mode="caller-provisioned", 324 - advisory_acquired_at="2026-07-20T11:30:00Z", 325 - db_root=tmp_path / "caller-db", 326 - runner=FakeRunner(commit_stdout="a" * 64 + "\n"), 327 - temp_path_factory=lambda label: tmp_path / label, 328 - clock=_clock([]), 329 - archive_hasher=lambda _db: "b" * 64, 465 + db_root, snapshot = _db_root(tmp_path) 466 + result, _runner, _repo_path = _prepare( 467 + tmp_path, 468 + db_root=db_root, 469 + snapshot=snapshot, 470 + runner=FakeRunner(snapshot, commit_stdout="a" * 64 + "\n"), 330 471 ) 331 472 332 473 assert result.db_commit == "a" * 64 ··· 337 478 tmp_path: Path, 338 479 commit_stdout: str, 339 480 ) -> None: 340 - runner = FakeRunner(commit_stdout=commit_stdout) 481 + db_root, snapshot = _db_root(tmp_path) 482 + runner = FakeRunner(snapshot, commit_stdout=commit_stdout) 341 483 342 484 with pytest.raises(policy.ReleasePolicyError) as exc: 343 - policy.prepare_policy_run( 344 - _repo(tmp_path), 345 - advisory_source_id="internal", 346 - db_urls=("ssh://example.test/db.git",), 347 - mode="caller-provisioned", 348 - advisory_acquired_at="2026-07-20T11:30:00Z", 349 - db_root=tmp_path / "caller-db", 350 - runner=runner, 351 - temp_path_factory=lambda label: tmp_path / label, 352 - clock=_clock(runner.events), 353 - archive_hasher=lambda _db: "b" * 64, 354 - ) 485 + _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 355 486 356 487 assert exc.value.failures[0].error == "advisory_snapshot.db_commit is invalid" 357 488 assert exc.value.failures[0].expected == "exactly 40 or 64 lowercase hex characters" ··· 365 496 tmp_path: Path, 366 497 digest: str, 367 498 ) -> None: 368 - runner = FakeRunner() 499 + db_root, snapshot = _db_root(tmp_path) 500 + runner = FakeRunner(snapshot) 369 501 370 502 with pytest.raises(policy.ReleasePolicyError) as exc: 371 - policy.prepare_policy_run( 372 - _repo(tmp_path), 373 - advisory_source_id="internal", 374 - db_urls=("ssh://example.test/db.git",), 375 - mode="caller-provisioned", 376 - advisory_acquired_at="2026-07-20T11:30:00Z", 377 - db_root=tmp_path / "caller-db", 503 + _prepare( 504 + tmp_path, 505 + db_root=db_root, 506 + snapshot=snapshot, 378 507 runner=runner, 379 - temp_path_factory=lambda label: tmp_path / label, 380 - clock=_clock(runner.events), 381 - archive_hasher=lambda _db: digest, 508 + archive=digest, 382 509 ) 383 510 384 511 assert ( ··· 393 520 def test_policy_run_constructor_accepts_sha256_db_commit() -> None: 394 521 result = policy.PolicyRun( 395 522 advisory_source_id="internal", 523 + db_snapshot_basename=SNAPSHOT, 396 524 db_commit="a" * 64, 397 - db_archive_sha256="b" * 64, 525 + db_archive_sha256=DB_ARCHIVE, 526 + advisory_count=1, 398 527 advisory_acquired_at="2026-07-20T11:30:00Z", 528 + db_commit_timestamp=COMMIT_TIME, 399 529 policy_checked_at="2026-07-20T12:00:00Z", 400 530 result="pass", 401 531 ) ··· 424 554 ) 425 555 for name, value in MALFORMED_ARCHIVE_DIGEST_CASES 426 556 ), 557 + pytest.param( 558 + "db_snapshot_basename", 559 + "../db", 560 + "policy_run.db_snapshot_basename is invalid", 561 + id="snapshot-path", 562 + ), 563 + pytest.param( 564 + "db_snapshot_basename", 565 + "", 566 + "policy_run.db_snapshot_basename is invalid", 567 + id="snapshot-empty", 568 + ), 569 + pytest.param( 570 + "advisory_count", 571 + 0, 572 + "policy_run.advisory_count is invalid", 573 + id="count-zero", 574 + ), 575 + pytest.param( 576 + "advisory_count", 577 + True, 578 + "policy_run.advisory_count is invalid", 579 + id="count-bool", 580 + ), 581 + pytest.param( 582 + "db_commit_timestamp", 583 + "2026-07-19T12:00:00-06:00", 584 + "policy_run.db_commit_timestamp is invalid", 585 + id="commit-time-not-normalized", 586 + ), 427 587 ], 428 588 ) 429 - def test_policy_run_constructor_rejects_malformed_snapshot_identity( 589 + def test_policy_run_constructor_rejects_malformed_receipt_identity( 430 590 field: str, 431 - value: str, 591 + value: object, 432 592 error: str, 433 593 ) -> None: 434 594 kwargs = { 435 595 "advisory_source_id": "internal", 436 - "db_commit": "a" * 40, 437 - "db_archive_sha256": "b" * 64, 596 + "db_snapshot_basename": SNAPSHOT, 597 + "db_commit": DB_COMMIT, 598 + "db_archive_sha256": DB_ARCHIVE, 599 + "advisory_count": 1, 438 600 "advisory_acquired_at": "2026-07-20T11:30:00Z", 601 + "db_commit_timestamp": COMMIT_TIME, 439 602 "policy_checked_at": "2026-07-20T12:00:00Z", 440 603 "result": "pass", 441 604 } ··· 445 608 policy.PolicyRun(**kwargs) 446 609 447 610 assert exc.value.failures[0].error == error 448 - if value and value[0].isupper(): 611 + if isinstance(value, str) and value and value[0].isupper(): 449 612 assert exc.value.failures[0].actual == value 450 613 451 614 ··· 453 616 raise OSError(5, "cleanup failed", "/private/tmp/release-advisory-secret") 454 617 455 618 456 - def _policy_kwargs( 457 - tmp_path: Path, 458 - *, 459 - mode: str, 460 - primary_failure: bool, 461 - ) -> dict: 462 - runner = FakeRunner(status="?? dirty\n" if primary_failure else "") 463 - kwargs = { 464 - "advisory_source_id": "internal", 465 - "db_urls": ("ssh://example.test/db.git",), 466 - "mode": mode, 467 - "runner": runner, 468 - "temp_path_factory": lambda label: tmp_path / f"{mode}-{label}", 469 - "clock": ClockSequence( 470 - runner.events, 471 - ("acquisition-clock", datetime(2026, 7, 20, 11, 30, tzinfo=UTC)), 472 - ("policy-clock", datetime(2026, 7, 20, 12, 0, tzinfo=UTC)), 473 - ) 474 - if mode == "refresh-once" 475 - else _clock(runner.events), 476 - "archive_hasher": lambda _db: "b" * 64, 477 - } 478 - if mode == "caller-provisioned": 479 - kwargs["advisory_acquired_at"] = "2026-07-20T11:30:00Z" 480 - kwargs["db_root"] = tmp_path / "caller-db" 481 - return kwargs 482 - 483 - 619 + @pytest.mark.parametrize("primary_failure", (False, True), ids=("success", "primary")) 484 620 @pytest.mark.parametrize( 485 - ("cleanup_name", "mode", "cleanup_kwargs"), 621 + ("cleanup_name", "cleanup_kwargs"), 486 622 [ 487 - ("unlink", "caller-provisioned", {"cleanup_unlink": _cleanup_failure}), 488 - ("rmdir", "caller-provisioned", {"cleanup_rmdir": _cleanup_failure}), 489 - ("rmtree", "refresh-once", {"cleanup_rmtree": _cleanup_failure}), 623 + ("unlink", {"cleanup_unlink": _cleanup_failure}), 624 + ("rmdir", {"cleanup_rmdir": _cleanup_failure}), 490 625 ], 491 626 ) 492 - @pytest.mark.parametrize("primary_failure", (False, True), ids=("success", "primary")) 493 627 def test_cleanup_failures_surface_without_masking_primary_errors( 494 628 tmp_path: Path, 495 629 cleanup_name: str, 496 - mode: str, 497 630 cleanup_kwargs: dict, 498 631 primary_failure: bool, 499 632 ) -> None: 633 + db_root, snapshot = _db_root(tmp_path) 634 + runner = FakeRunner(snapshot, status="?? dirty\n" if primary_failure else "") 635 + 500 636 with pytest.raises(policy.ReleasePolicyError) as exc: 501 - policy.prepare_policy_run( 502 - _repo(tmp_path), 503 - **_policy_kwargs(tmp_path, mode=mode, primary_failure=primary_failure), 637 + _prepare( 638 + tmp_path, 639 + db_root=db_root, 640 + snapshot=snapshot, 641 + runner=runner, 504 642 **cleanup_kwargs, 505 643 ) 506 644 507 645 errors = [failure.error for failure in exc.value.failures] 508 646 if primary_failure: 509 - assert "advisory db has uncommitted or ignored material" in errors 647 + assert "advisory db snapshot has uncommitted or ignored material" in errors 510 648 else: 511 649 assert errors == [ 512 - f"release advisory cleanup failed during {'refresh temp removal' if cleanup_name == 'rmtree' else 'materialized config removal'}" 650 + "release advisory cleanup failed during materialized config removal" 513 651 ] 514 652 assert any(error.startswith("release advisory cleanup failed") for error in errors) 515 653 assert ( ··· 520 658 ) 521 659 522 660 523 - def _write_caller_db(root: Path) -> list[tuple[str, str, bytes]]: 524 - root.mkdir(parents=True) 525 - (root / "db.txt").write_bytes(b"caller db\n") 526 - (root / "nested").mkdir() 527 - (root / "nested" / "ignored.bin").write_bytes(b"still caller owned\n") 528 - return _caller_db_inventory(root) 529 - 530 - 531 661 def _caller_db_inventory(root: Path) -> list[tuple[str, str, bytes]]: 532 662 items: list[tuple[str, str, bytes]] = [] 533 663 for path in root.rglob("*"): ··· 553 683 primary_failure: bool, 554 684 cleanup_kwargs: dict, 555 685 ) -> None: 556 - caller_db = tmp_path / "caller-db" 557 - before = _write_caller_db(caller_db) 558 - kwargs = _policy_kwargs( 559 - tmp_path, 560 - mode="caller-provisioned", 561 - primary_failure=primary_failure, 562 - ) 686 + db_root, snapshot = _db_root(tmp_path) 687 + before = _caller_db_inventory(db_root) 688 + runner = FakeRunner(snapshot, status="?? dirty\n" if primary_failure else "") 563 689 564 690 if primary_failure or cleanup_kwargs: 565 691 with pytest.raises(policy.ReleasePolicyError): 566 - policy.prepare_policy_run(_repo(tmp_path), **kwargs, **cleanup_kwargs) 692 + _prepare( 693 + tmp_path, 694 + db_root=db_root, 695 + snapshot=snapshot, 696 + runner=runner, 697 + **cleanup_kwargs, 698 + ) 567 699 else: 568 - policy.prepare_policy_run(_repo(tmp_path), **kwargs) 569 - 570 - assert _caller_db_inventory(caller_db) == before 571 - 572 - 573 - def test_caller_timestamp_is_preserved_and_required(tmp_path: Path) -> None: 574 - exact = "2026-07-20T11:30:00+00:00" 575 - result = policy.prepare_policy_run( 576 - _repo(tmp_path), 577 - advisory_source_id="internal", 578 - db_urls=("ssh://example.test/db.git",), 579 - mode="caller-provisioned", 580 - advisory_acquired_at=exact, 581 - db_root=tmp_path / "caller-db", 582 - runner=FakeRunner(), 583 - temp_path_factory=lambda label: tmp_path / label, 584 - clock=_clock([]), 585 - archive_hasher=lambda _db: "b" * 64, 586 - ) 587 - assert result.advisory_acquired_at == exact 700 + _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 588 701 589 - for value, error in ( 590 - (None, "caller-provisioned advisory mode has no acquisition time"), 591 - ("not-a-time", "advisory acquisition time is not RFC3339"), 592 - ): 593 - with pytest.raises(policy.ReleasePolicyError) as exc: 594 - policy.prepare_policy_run( 595 - _repo(tmp_path), 596 - advisory_source_id="internal", 597 - db_urls=("ssh://example.test/db.git",), 598 - mode="caller-provisioned", 599 - advisory_acquired_at=value, 600 - db_root=tmp_path / "caller-db", 601 - runner=FakeRunner(), 602 - temp_path_factory=lambda label: tmp_path / f"{value}-{label}", 603 - ) 604 - assert exc.value.failures[0].error == error 702 + assert _caller_db_inventory(db_root) == before 605 703 606 704 607 705 def test_policy_temps_are_cleaned_without_removing_caller_db(tmp_path: Path) -> None: 608 - refresh_root = tmp_path / "refresh-temp" 609 - policy.prepare_policy_run( 610 - _repo(tmp_path), 611 - advisory_source_id="internal", 612 - db_urls=("ssh://example.test/db.git",), 613 - mode="refresh-once", 614 - runner=FakeRunner(), 615 - temp_path_factory=lambda _label: refresh_root, 616 - clock=ClockSequence( 617 - [], 618 - ("acquisition-clock", datetime(2026, 7, 20, 11, 30, tzinfo=UTC)), 619 - ("policy-clock", datetime(2026, 7, 20, 12, 0, tzinfo=UTC)), 620 - ), 621 - archive_hasher=lambda _db: "b" * 64, 622 - ) 623 - assert not refresh_root.exists() 624 - 625 - failed_refresh_root = tmp_path / "failed-refresh-temp" 626 - with pytest.raises(policy.ReleasePolicyError): 627 - policy.prepare_policy_run( 628 - _repo(tmp_path), 629 - advisory_source_id="internal", 630 - db_urls=("ssh://example.test/db.git",), 631 - mode="refresh-once", 632 - runner=FakeRunner(status="?? dirty\n"), 633 - temp_path_factory=lambda _label: failed_refresh_root, 634 - clock=ClockSequence( 635 - [], 636 - ("acquisition-clock", datetime(2026, 7, 20, 11, 30, tzinfo=UTC)), 637 - ("policy-clock", datetime(2026, 7, 20, 12, 0, tzinfo=UTC)), 638 - ), 639 - archive_hasher=lambda _db: "b" * 64, 640 - ) 641 - assert not failed_refresh_root.exists() 642 - 643 - caller_db = tmp_path / "caller-owned-db" 644 - caller_db.mkdir() 645 - caller_temp = tmp_path / "caller-temp" 646 - policy.prepare_policy_run( 647 - _repo(tmp_path), 648 - advisory_source_id="internal", 649 - db_urls=("ssh://example.test/db.git",), 650 - mode="caller-provisioned", 651 - advisory_acquired_at="2026-07-20T11:30:00Z", 652 - db_root=caller_db, 653 - runner=FakeRunner(), 654 - temp_path_factory=lambda _label: caller_temp, 706 + db_root, snapshot = _db_root(tmp_path) 707 + temp_root = tmp_path / "policy-temp" 708 + _prepare( 709 + tmp_path, 710 + db_root=db_root, 711 + snapshot=snapshot, 712 + runner=FakeRunner(snapshot), 655 713 clock=_clock([]), 656 - archive_hasher=lambda _db: "b" * 64, 657 714 ) 658 - assert caller_db.is_dir() 659 - assert not caller_temp.exists() 715 + assert db_root.is_dir() 716 + assert not (tmp_path / "advisory-policy").exists() 660 717 661 718 with pytest.raises(policy.ReleasePolicyError): 662 719 policy.prepare_policy_run( 663 720 _repo(tmp_path), 664 721 advisory_source_id="internal", 665 722 db_urls=("ssh://example.test/db.git",), 666 - mode="caller-provisioned", 667 - advisory_acquired_at="2026-07-20T11:30:00Z", 668 - db_root=caller_db, 669 - runner=FakeRunner(status="?? dirty\n"), 670 - temp_path_factory=lambda _label: caller_temp, 723 + db_root=db_root, 724 + runner=FakeRunner(snapshot, status="?? dirty\n"), 725 + temp_path_factory=lambda _label: temp_root, 671 726 clock=_clock([]), 672 - archive_hasher=lambda _db: "b" * 64, 727 + archive_hasher=lambda _db: DB_ARCHIVE, 673 728 ) 674 - assert caller_db.is_dir() 675 - assert not caller_temp.exists() 729 + assert db_root.is_dir() 730 + assert not temp_root.exists()
+4 -1
tests/test_release_candidate_driver.py
··· 83 83 "EXPECTED_RELEASE_COMMIT": SOURCE_COMMIT, 84 84 "SOURCE_COMMIT": "b" * 40, 85 85 "RELEASE_MODEL_PACKAGES": "exclude", 86 - "RELEASE_ADVISORY_MODE": "caller-provisioned", 87 86 "RELEASE_ADVISORY_SOURCE_NAME": "fixture", 88 87 "RELEASE_ADVISORY_DB_URL": "ssh://example.test/db.git", 88 + "RELEASE_ADVISORY_DB_ROOT": "/advisory-db", 89 89 } 90 90 ) 91 91 ··· 93 93 def _policy() -> PolicyRun: 94 94 return PolicyRun( 95 95 advisory_source_id="fixture", 96 + db_snapshot_basename="advisory-db-fixture00000000", 96 97 db_commit="b" * 40, 97 98 db_archive_sha256="c" * 64, 99 + advisory_count=1, 98 100 advisory_acquired_at="2026-07-20T11:00:00Z", 101 + db_commit_timestamp="2026-07-19T12:00:00Z", 99 102 policy_checked_at="2026-07-20T12:00:00Z", 100 103 result="pass", 101 104 )
+3
tests/test_release_ledger.py
··· 46 46 def _policy() -> PolicyRun: 47 47 return PolicyRun( 48 48 advisory_source_id="internal", 49 + db_snapshot_basename="advisory-db-fixture00000000", 49 50 db_commit="b" * 40, 50 51 db_archive_sha256="c" * 64, 52 + advisory_count=1, 51 53 advisory_acquired_at="2026-07-20T11:00:00Z", 54 + db_commit_timestamp="2026-07-19T12:00:00Z", 52 55 policy_checked_at="2026-07-20T12:00:00Z", 53 56 result="pass", 54 57 )