Repository navigation
fix(release): gate release identity, model/updater admission, and platform trust #1126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
seonghobae
wants to merge
312
commits into
develop
Choose a base branch
from
fix/trusted-release-version-identity-960
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 11 commits
Commits
Show all changes
312 commits
Select commit
Hold shift + click to select a range
340b476
fix(release): bind updater manifest to receipt-authorized bytes
seonghobae 4d6f7cb
fix(release): publish receipt-bound updater manifest
seonghobae 64fbd14
fix(release): preserve exact updater signature text
seonghobae 889554d
test(release): cover manifest publication boundary cleanly
seonghobae f0d6dd5
docs(release): trace receipt-bound updater manifest publication
seonghobae db4ad06
test(release): reproduce hosted asset re-verification gap
seonghobae e022794
fix(release): re-admit downloaded hosted release assets
seonghobae 3c9221b
fix(release): re-verify draft and published release bytes
seonghobae 3412a19
test(release): format hosted re-verification coverage
seonghobae 4c1dfe9
docs(release): trace hosted release byte re-verification
seonghobae 0f13ef0
test(release): require immutable release attestation gate
seonghobae b65f00c
fix(release): verify immutable release attestation
seonghobae d6cc359
docs(release): trace immutable release attestation gate
seonghobae 57ced89
test(release): require updater manifest security metadata
seonghobae 21ee4ff
fix(release): bind updater security metadata to receipts
seonghobae 209f58e
docs(release): trace updater security metadata boundary
seonghobae 857e1e9
test(release): cover updater policy metadata admission
seonghobae 4467a9e
test(distribution): require Rust updater lifecycle core
seonghobae 1339cfd
feat(distribution): add updater lifecycle Rust core crate
seonghobae 0fbb2e8
build(distribution): lock updater lifecycle core
seonghobae 42fdeed
fix(distribution): enforce updater anti-replay and rollback decisions
seonghobae cd25fab
docs(distribution): trace anti-replay rollback decision core
seonghobae d80a477
chore(distribution): deny warnings and missing rustdoc
seonghobae a0a9eec
docs(architecture): register Distribution update decision core
seonghobae 9df9e61
docs(product): establish commercial technical gap baseline
seonghobae fa5690b
test(distribution): require durable highest-seen state suite
seonghobae f07f35e
feat(distribution): establish highest-seen state crate
seonghobae 30159e0
build(distribution): lock highest-seen state graph
seonghobae 95889b6
feat(distribution): persist replay authority as bounded append log
seonghobae 05a8dc9
docs(distribution): trace durable updater freshness state
seonghobae 9a481cc
docs(product): record durable updater-state progress
seonghobae fb1ac16
fix(distribution): disambiguate bounded state read
seonghobae 6826c92
docs(architecture): register durable Distribution state owner
seonghobae e525aa1
test(distribution): require authenticated runtime admission suite
seonghobae 5c95912
feat(distribution): add runtime admission crate manifest
seonghobae b9f72be
build(distribution): lock runtime path dependency graph
seonghobae 85db601
feat(distribution): bound raw updater metadata as provisional input
seonghobae def74ef
refactor(distribution): keep provisional runtime admission stateless
seonghobae 418c68d
build(distribution): minimize provisional runtime lock graph
seonghobae 1c52911
docs(distribution): correct raw updater metadata trust boundary
seonghobae d0bdea0
docs(product): expose updater metadata-authentication gap
seonghobae 8533403
docs(architecture): separate provisional updater metadata from authority
seonghobae 59bc8c8
fix(updater): pin provisional release download namespace
seonghobae daad6e5
fix(updater): keep URL admission Rust-stable
seonghobae e25c3f1
docs(updater): trace pinned release URL admission
seonghobae 1e1f1c2
test(distribution): require bounded updater download contract
seonghobae f183c0c
feat(distribution): add bounded updater artifact streaming
seonghobae 19c5983
docs(distribution): trace bounded updater streaming boundary
seonghobae 21f3bee
docs(product): make bounded updater download gap code-current
seonghobae 539fbe2
docs(architecture): add bounded updater download owner
seonghobae dcc04b7
test(distribution): require crash-safe updater staging sink
seonghobae ed079fd
feat(distribution): add exclusive updater staging sink
seonghobae 7620248
test(distribution): cover updater staging failure cleanup
seonghobae 00825be
docs(distribution): trace exclusive updater staging lifecycle
seonghobae e485b3a
docs(product): record updater staging boundary
seonghobae a956bcf
test(distribution): remove unverified sealed artifacts on drop
seonghobae e76abdd
fix(distribution): clean sealed artifacts until trust promotion
seonghobae 2909133
docs(distribution): trace sealed-artifact cleanup before trust promotion
seonghobae a065ceb
docs(product): keep sealed updater bytes provisional
seonghobae 56aa746
test(distribution): require sealed read-only descriptor stream
seonghobae 13ca9b7
test(distribution): stop depending on writable sealed handle
seonghobae 6144302
fix(distribution): withhold writable sealed artifact handle
seonghobae fdd9705
docs(traceability): bind sealed updater reads to descriptor
seonghobae 5568e22
docs(product): keep sealed updater descriptor read-only
seonghobae f6723cf
docs(architecture): withhold sealed updater write capability
seonghobae 1c8d83d
docs(architecture): restore sources and narrow sealed reader capability
seonghobae 5713689
docs(architecture): restore exact verification wording
seonghobae e1274be
test(distribution): RED bound sealed reads to admitted bytes
seonghobae c451096
fix(distribution): cap sealed verifier reads at admitted bytes
seonghobae e294147
test(distribution): reject sealed descriptor truncation
seonghobae f6b376a
docs(distribution): trace sealed reader byte-bound repair
seonghobae 56d215e
docs(product): keep sealed verifier resource bound code-current
seonghobae 9f7bddd
test(distribution): require admitted artifact transport binding
seonghobae 663affc
fix(distribution): bind admitted artifact transport fields
seonghobae ed7426f
docs(distribution): trace admitted transport binding
seonghobae 9c95c71
docs(product): align updater transport gap baseline
seonghobae 46c4cba
docs(architecture): bind updater transport metadata owner
seonghobae 9d1dc2f
test(release): reject runtime-incompatible release versions
seonghobae e268c9b
fix(release): align release version grammar with updater core
seonghobae 97f9c2a
docs(traceability): bind stable release version grammar
seonghobae 6cee7a6
docs(product): record stable release grammar invariant
seonghobae 1cf9656
test(release): reject version components beyond u64
seonghobae 1c44f25
fix(release): align stable version range with runtime
seonghobae c2fd0cd
docs(traceability): close stable version range drift
seonghobae ba426b4
docs(product): record stable version range invariant
seonghobae 6e6ebe2
feat(distribution): add updater transport crate
seonghobae 5f29863
build(distribution): lock transport owner graph
seonghobae 2819481
feat(distribution): add transport response boundary
seonghobae 8f39dfc
test(distribution): require admitted GitHub release redirect
seonghobae 1b4f7a0
test(distribution): gate transport contract in root suite
seonghobae 4964c3c
fix(distribution): admit one-hop GitHub release asset redirects
seonghobae d424dd8
docs(distribution): trace updater transport policy
seonghobae adc52ef
docs(product): align distribution transport gap baseline
seonghobae 2e77173
docs(architecture): register updater transport boundary
seonghobae 8313e9f
test(distribution): reject malformed Tauri signature envelopes
seonghobae 6e5e42f
fix(distribution): validate Tauri signature envelope before network
seonghobae 03c1314
test(release): reject non-base64 updater signatures
seonghobae 2c7c772
fix(release): enforce Tauri signature envelope before publication
seonghobae fd906e2
docs(distribution): trace signature-envelope admission
seonghobae 0e318ef
docs(product): align updater signature-envelope gap
seonghobae 2f5af3e
docs(architecture): align updater signature-envelope ownership
seonghobae e376325
test(distribution): reject encoded updater response bodies
seonghobae 606095e
fix(distribution): reject transformed updater response bodies
seonghobae f616421
docs(distribution): trace exact response-byte framing admission
seonghobae 80fdc39
docs(product): record exact-byte updater response framing
seonghobae c3dbebe
test(distribution): redact redirect queries in diagnostics
seonghobae be91505
fix(distribution): redact redirect query data from diagnostics
seonghobae 6b8cabb
docs(distribution): trace updater diagnostic redaction
seonghobae 935266a
test(distribution): bound updater signature debug output
seonghobae 1a4e8f5
fix(distribution): bound signature diagnostics
seonghobae b290bf0
docs(distribution): trace bounded signature diagnostics
seonghobae 027ba14
test(distribution): reject provisional signature debug exposure
seonghobae 90855cc
fix(distribution): redact provisional signature diagnostics
seonghobae bd17041
docs(distribution): record provisional diagnostic redaction
seonghobae 93526ae
docs(gap): keep updater diagnostics buyer truth current
seonghobae 5b0ddb5
test(distribution): require restart recovery for stale staging artifact
seonghobae b7a1839
fix(distribution): reclaim stale unverified staging artifact on restart
seonghobae e1b4a81
docs(distribution): trace stale staging restart recovery
seonghobae 84d4a7b
docs(product): record updater staging restart recovery boundary
seonghobae dcd7a80
docs(distribution): align bounded staging with restart recovery
seonghobae 74565b8
repair(distribution): return product baseline to canonical owner
seonghobae 82843b4
test(distribution): reject reclaiming active staging attempts
seonghobae 40cd754
fix(distribution): lease staging namespace before stale recovery
seonghobae ebf9428
test(distribution): account for persistent staging lease sentinel
seonghobae d2d1872
test(distribution): cover sealed lease and lease symlink boundary
seonghobae ebefa23
docs(distribution): trace active staging lease recovery
seonghobae 9046e90
docs(distribution): make bounded staging lease code-current
seonghobae 752b534
test(distribution): avoid platform-specific reads under staging lease
seonghobae c30167d
docs(distribution): record cross-platform staging lease fixture
seonghobae 41afd2a
test(distribution): require staging lease contracts on shipped OS fam…
seonghobae cfb3ec1
ci(distribution): gate staging lease tests on Windows macOS and Linux
seonghobae 137d162
docs(distribution): require cross-platform staging lease evidence
seonghobae 44265a0
test(distribution): prove staging lease across real processes
seonghobae a999376
docs(distribution): trace real-process staging lease coverage
seonghobae af89337
test(release): red for descriptor-bound identity inputs
seonghobae 42d34b5
fix(release): bind identity reads to stable descriptors
seonghobae 0ef8912
docs(release): trace descriptor-bound identity admission
seonghobae 68f1bb5
test(release): reject nonstandard JSON constants in identity inputs
seonghobae 494aa0f
fix(release): require strict standard JSON identity metadata
seonghobae 6808f29
docs(traceability): record strict JSON release identity admission
seonghobae 8d56ab1
test(distribution): reject malformed nonselected signature envelope
seonghobae 4e28d0c
fix(distribution): validate all updater signature envelopes
seonghobae 9b691d7
refactor(distribution): keep signature syntax in metadata owner
seonghobae 5b85e03
test(distribution): assert signature envelope owner boundary
seonghobae 89f97a2
docs(distribution): make metadata owner authoritative for signature e…
seonghobae eb29922
test(distribution): cover signature envelope padding bounds
seonghobae a62b9ca
docs(architecture): align updater signature-envelope ownership
seonghobae 26ff403
test(distribution): use admitted signature envelopes
seonghobae 11a5a47
test(distribution): bind redirect decisions to policy identity
seonghobae 35b851e
fix(distribution): bind redirect state to policy identity
seonghobae 3f7daa8
docs(distribution): trace redirect policy binding repair
seonghobae a418640
test(release): reject same-size identity mutation during read
seonghobae 438dca0
fix(release): verify stable identity bytes across descriptor reads
seonghobae 24f8135
docs(release): trace stable descriptor snapshot admission
seonghobae f0b38c0
test(release): reject path replacement during identity read
seonghobae 906b4f4
fix(release): revalidate identity path after stable read
seonghobae 9671144
docs(traceability): bind release reads back to repository paths
seonghobae e25fd44
test(distribution): reject concurrent freshness-state writer
seonghobae dd2ae7f
fix(distribution): serialize highest-seen state access
seonghobae 027963a
docs(distribution): trace highest-seen state lease
seonghobae e578644
test(distribution-state): reject impossible torn version prefixes
seonghobae e793d00
fix(distribution-state): reject impossible torn version prefixes
seonghobae e8a064c
docs(distribution-state): trace impossible torn-tail admission
seonghobae b633482
test(distribution): reject hard-linked freshness state
seonghobae 4f8c933
fix(distribution): reject hard-linked freshness authority
seonghobae bd694de
docs(distribution): trace hard-link state admission
seonghobae 8832e62
test(ci): require hosted coverage for Distribution owners
seonghobae c5a09d5
fix(ci): execute standalone Distribution owner tests
seonghobae 835bfb5
docs(ci): bind Distribution owners to hosted platform tests
seonghobae 4aca07e
test(distribution): reject replacement-path cleanup
seonghobae bc72745
fix(distribution): preserve replaced staging paths
seonghobae ecec997
docs(distribution): trace staging cleanup identity
seonghobae e07a50d
fix(distribution): publish Windows freshness state without alias muta…
seonghobae 5303792
test(distribution): scope link-count rejection to Unix
seonghobae 2f231d6
test(distribution): prove Windows state alias preservation
seonghobae c2b61b0
docs(distribution): correct Windows freshness-state authority
seonghobae e3313b9
docs(architecture): align updater state publication semantics
seonghobae 7904f10
test(distribution): reject vulnerable HTTP TLS admission
seonghobae 40496d5
fix(distribution): gate HTTP TLS dependency admission
seonghobae 117962a
docs(distribution): trace HTTP TLS dependency admission
seonghobae 3def1e1
fix(ci): run Distribution HTTP gate in quickcheck
seonghobae 5f81143
test(distribution): reject vendored native TLS no-ALPN feature
seonghobae b26fc7d
fix(distribution): reject every native TLS reqwest feature
seonghobae fc22b46
docs(distribution): trace complete reqwest TLS feature admission
seonghobae 621bac6
test(distribution): reject implicit reqwest decoding features
seonghobae 43d7863
fix(distribution): allow-list reqwest transport features
seonghobae eb10409
test(distribution): cover reqwest feature allow-list
seonghobae 372dd9d
docs(distribution): make reqwest feature admission explicit
seonghobae d36c4d6
test(distribution): reject target-specific reqwest bypass
seonghobae da209f1
fix(distribution): inspect target-scoped reqwest dependencies
seonghobae 2b10fe9
docs(distribution): cover target-scoped dependency admission
seonghobae c58ed76
style(distribution): keep dependency admission formatter-safe
seonghobae 7330af6
test(distribution): reject renamed reqwest admission bypass
seonghobae 5356dcf
fix(distribution): admit renamed reqwest by package identity
seonghobae 49b84eb
docs(distribution): trace Cargo reqwest alias admission
seonghobae cf6655d
test(distribution): reject workspace-inherited reqwest bypass
seonghobae 8284ae6
fix(distribution): reject workspace-inherited reqwest ownership
seonghobae 6958feb
docs(distribution): trace workspace reqwest inheritance
seonghobae a8d91b3
test(distribution): reject noncanonical reqwest sources
seonghobae 914609c
fix(distribution): pin reqwest to canonical registry source
seonghobae e6a0663
test(distribution): reject noncanonical rustls source
seonghobae 4fdc294
fix(distribution): require canonical rustls lock source
seonghobae 2ff17b7
docs(distribution): trace HTTP dependency source provenance
seonghobae 09852dd
test(distribution): reject unbounded reqwest requirements
seonghobae 106d1f6
fix(distribution): bound reqwest version requirements
seonghobae bd359a3
docs(distribution): trace bounded reqwest requirements
seonghobae 308f4a6
test(distribution): expose Windows staging replacement cleanup
seonghobae 1d60331
fix(distribution): defer unprovable Windows staging unlink
seonghobae 1a6ee09
test(distribution): prove deferred Windows staging reclaim
seonghobae 0bae56f
docs(distribution): trace Windows staging cleanup authority
seonghobae c69e91d
docs(architecture): align Windows staging cleanup semantics
seonghobae 7996cc2
test(distribution): align sealed cleanup with Windows contract
seonghobae 2acb2f3
test(distribution): align transport staging cleanup by platform
seonghobae 4c94b46
test(distribution): align staging lifecycle with Windows cleanup
seonghobae 2e4c355
test(distribution): clean persistent lease fixtures explicitly
seonghobae 6a85d35
test(distribution): account for persistent staging lease
seonghobae 57385c1
docs(distribution): align bounded staging cleanup claims
seonghobae 64fe0bc
docs(distribution): make restart recovery OS-cleanup current
seonghobae 106e03a
test(distribution): reject unreviewed reqwest release lines
seonghobae 13be264
fix(distribution): bind reqwest admission to reviewed release line
seonghobae 7662556
docs(distribution): trace reviewed reqwest release line
seonghobae 8564741
fix(distribution): expose in-flight staging path
seonghobae fd29aa1
test(distribution): bind redirects to full provisional identity
seonghobae 6b0fe81
fix(distribution): bind redirect token to full candidate identity
seonghobae b79a250
docs(distribution): trace full redirect identity binding
seonghobae 9901b2d
test(distribution): preserve candidate identity through sealing
seonghobae e42f6e6
fix(distribution): bind sealed artifacts to release identity
seonghobae 3ef753f
test(distribution): read sealed artifact through owned descriptor
seonghobae 50f072e
docs(distribution): trace sealed candidate identity binding
seonghobae dea8f7b
test(distribution): redact sealed transport diagnostics
seonghobae 1136d34
docs(distribution): trace sealed transport diagnostics
seonghobae dbe7988
test(distribution): reject prerelease TLS boundary drift
seonghobae 98a53d6
fix(distribution): compare TLS advisory prereleases correctly
seonghobae a3737a3
docs(distribution): trace prerelease advisory boundary repair
seonghobae 269bd4a
test(distribution): align prerelease rejection receipt
seonghobae c85402f
docs(distribution): record prerelease regression verification
seonghobae 340e7eb
test(distribution): reject transitive HTTP source substitution
seonghobae 92162da
fix(distribution): bind full HTTP lock graph provenance
seonghobae 6cded37
docs(distribution): trace full HTTP lock provenance
seonghobae 5324118
test(distribution): cover lock provenance admission edges
seonghobae 4920cf2
docs(distribution): record lock provenance edge evidence
seonghobae ed5be00
test(distribution): reject reqwest feature forwarding
seonghobae bc56faa
fix(distribution): block reqwest feature forwarding
seonghobae 0f74972
test(distribution): cover weak reqwest feature forwarding
seonghobae ae98559
docs(distribution): trace reqwest feature forwarding gate
seonghobae da5c690
test(distribution): document integration test crates
seonghobae 2cd906c
fix(release): revalidate publication asset list
seonghobae 528ba04
docs(distribution): explain consumed staging cleanup
seonghobae 070a463
test(release): specify explicit UMX-HQ acquisition contracts
seonghobae 07daac9
feat(release): acquire UMX-HQ weights through explicit pinned tooling
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| #!/usr/bin/env python3 | ||
| """Fail closed when BandScope release-version projections disagree.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import os | ||
| import sys | ||
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
| _REPOSITORY_ROOT = Path(__file__).resolve().parents[2] | ||
|
|
||
|
|
||
| def _read_json_object(metadata_path: Path) -> dict[str, Any]: | ||
| """Read one release metadata document and require a JSON object root.""" | ||
| try: | ||
| metadata_document = json.loads(metadata_path.read_text(encoding="utf-8")) | ||
| except (OSError, UnicodeError, json.JSONDecodeError) as metadata_error: | ||
| raise ValueError( | ||
| f"could not read release metadata: {metadata_path.name}" | ||
| ) from metadata_error | ||
| if not isinstance(metadata_document, dict): | ||
| raise ValueError(f"release metadata must be an object: {metadata_path.name}") | ||
| return metadata_document | ||
|
|
||
|
|
||
| def _required_string( | ||
| metadata_document: dict[str, Any], field_name: str, source_name: str | ||
| ) -> str: | ||
| """Return a non-empty string field without coercing malformed metadata.""" | ||
| field_value = metadata_document.get(field_name) | ||
| if ( | ||
| not isinstance(field_value, str) | ||
| or not field_value.strip() | ||
| or field_value != field_value.strip() | ||
| ): | ||
| raise ValueError( | ||
| f"{source_name} {field_name} must be a non-empty trimmed string" | ||
| ) | ||
| return field_value | ||
|
|
||
|
|
||
| def verify_release_identity( | ||
| repository_root: Path, release_tag: str | None = None | ||
| ) -> str: | ||
| """Verify package, Tauri, and optional tag versions against ``VERSION``.""" | ||
| try: | ||
| version_text = (repository_root / "VERSION").read_text(encoding="utf-8") | ||
| except (OSError, UnicodeError) as identity_error: | ||
| raise ValueError("could not read authoritative VERSION") from identity_error | ||
|
|
||
| version_lines = version_text.splitlines() | ||
| if ( | ||
| len(version_lines) != 1 | ||
| or not version_lines[0] | ||
| or version_lines[0] != version_lines[0].strip() | ||
| or version_text != f"{version_lines[0]}\n" | ||
| ): | ||
| raise ValueError("VERSION must contain exactly one non-empty version line") | ||
| release_version = version_lines[0] | ||
|
|
||
| package_document = _read_json_object(repository_root / "package.json") | ||
| tauri_document = _read_json_object( | ||
| repository_root / "apps" / "desktop" / "src-tauri" / "tauri.conf.json" | ||
| ) | ||
|
|
||
| package_version = _required_string( | ||
| package_document, "version", "package.json" | ||
| ) | ||
| tauri_version = _required_string( | ||
| tauri_document, "version", "tauri.conf.json" | ||
| ) | ||
| if package_version != release_version: | ||
| raise ValueError("package.json version does not match VERSION") | ||
| if tauri_version != release_version: | ||
| raise ValueError("tauri.conf.json version does not match VERSION") | ||
|
|
||
| if release_tag is not None and release_tag != f"v{release_version}": | ||
| raise ValueError("release tag does not match VERSION") | ||
|
|
||
| return release_version | ||
|
|
||
|
|
||
| def main() -> int: | ||
| """Run the release identity gate for repository and tag-triggered workflows.""" | ||
| release_tag = ( | ||
| os.environ.get("GITHUB_REF_NAME") | ||
| if os.environ.get("GITHUB_REF_TYPE") == "tag" | ||
| else None | ||
| ) | ||
| try: | ||
| release_version = verify_release_identity( | ||
| _REPOSITORY_ROOT, release_tag=release_tag | ||
| ) | ||
| except ValueError as identity_error: | ||
| print(f"release identity check failed: {identity_error}", file=sys.stderr) | ||
| return 1 | ||
| print(f"BandScope release identity verified: v{release_version}") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
174 changes: 174 additions & 0 deletions
174
services/analysis-engine/tests/test_release_version_identity.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| """Release identity contracts for the packaged BandScope desktop application.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import importlib.util | ||
| import json | ||
| from pathlib import Path | ||
| from types import ModuleType | ||
|
|
||
| import pytest | ||
|
|
||
| _REPOSITORY_ROOT = Path(__file__).resolve().parents[3] | ||
| _GUARD_PATH = _REPOSITORY_ROOT / "scripts" / "checks" / "verify_release_identity.py" | ||
| _BUILD_BASELINE_PATH = _REPOSITORY_ROOT / ".github" / "workflows" / "build-baseline.yml" | ||
|
|
||
|
|
||
| def _load_guard() -> ModuleType: | ||
| """Load the repository-owned release identity guard from its executable path.""" | ||
| assert _GUARD_PATH.is_file(), "release preflight must own a version identity guard" | ||
| guard_module_spec = importlib.util.spec_from_file_location( | ||
| "verify_release_identity", _GUARD_PATH | ||
| ) | ||
| assert guard_module_spec is not None and guard_module_spec.loader is not None | ||
| guard_module = importlib.util.module_from_spec(guard_module_spec) | ||
| guard_module_spec.loader.exec_module(guard_module) | ||
| return guard_module | ||
|
|
||
|
|
||
| def _write_release_metadata(repository_root: Path, release_version: str) -> None: | ||
| """Write the minimum release metadata consumed by the identity guard.""" | ||
| (repository_root / "apps" / "desktop" / "src-tauri").mkdir(parents=True) | ||
| (repository_root / "VERSION").write_text( | ||
| f"{release_version}\n", encoding="utf-8" | ||
| ) | ||
| (repository_root / "package.json").write_text( | ||
| json.dumps({"name": "bandscope", "version": release_version}), | ||
| encoding="utf-8", | ||
| ) | ||
| ( | ||
| repository_root / "apps" / "desktop" / "src-tauri" / "tauri.conf.json" | ||
| ).write_text( | ||
| json.dumps( | ||
| { | ||
| "productName": "BandScope", | ||
| "version": release_version, | ||
| "identifier": "com.bandscope.desktop", | ||
| } | ||
| ), | ||
| encoding="utf-8", | ||
| ) | ||
|
|
||
|
|
||
| def _workflow_job_block(workflow_text: str, job_name: str) -> str: | ||
| """Return one top-level GitHub Actions job without requiring a YAML runtime dependency.""" | ||
| job_marker = f" {job_name}:" | ||
| workflow_lines = workflow_text.splitlines() | ||
| try: | ||
| job_start_index = workflow_lines.index(job_marker) | ||
| except ValueError as lookup_error: | ||
| raise AssertionError(f"workflow job is missing: {job_name}") from lookup_error | ||
|
|
||
| job_end_index = len(workflow_lines) | ||
| for line_index in range(job_start_index + 1, len(workflow_lines)): | ||
| workflow_line = workflow_lines[line_index] | ||
| if ( | ||
| workflow_line.startswith(" ") | ||
| and not workflow_line.startswith(" ") | ||
| and workflow_line.endswith(":") | ||
| ): | ||
| job_end_index = line_index | ||
| break | ||
| return "\n".join(workflow_lines[job_start_index:job_end_index]) | ||
|
|
||
|
|
||
| def test_release_preflight_executes_version_identity_guard() -> None: | ||
| """Keep release preflight fail-closed when version projections drift.""" | ||
| quickcheck_text = ( | ||
| _REPOSITORY_ROOT / "scripts" / "harness" / "quickcheck.sh" | ||
| ).read_text(encoding="utf-8") | ||
| release_workflow_text = ( | ||
| _REPOSITORY_ROOT / ".github" / "workflows" / "release.yml" | ||
| ).read_text(encoding="utf-8") | ||
|
|
||
| assert "python3 scripts/checks/verify_release_identity.py" in quickcheck_text | ||
| assert "./scripts/harness/quickcheck.sh" in release_workflow_text | ||
|
|
||
|
|
||
| def test_tag_build_and_publication_depend_on_release_identity_gate() -> None: | ||
| """Block package construction and publication when release identity is invalid.""" | ||
| build_workflow_text = _BUILD_BASELINE_PATH.read_text(encoding="utf-8") | ||
|
|
||
| identity_job = _workflow_job_block(build_workflow_text, "release-identity") | ||
| assert "run: python3 scripts/checks/verify_release_identity.py" in identity_job | ||
|
|
||
| for build_job_name in ( | ||
| "build-windows-native", | ||
| "build-windows-arm64", | ||
| "build-macos-native", | ||
| "build-macos-arm64", | ||
| ): | ||
| build_job = _workflow_job_block(build_workflow_text, build_job_name) | ||
| assert "needs: release-identity" in build_job | ||
|
|
||
| publication_job = _workflow_job_block( | ||
| build_workflow_text, "publish-immutable-release" | ||
| ) | ||
| for required_job_name in ("release-identity", "gate-windows", "gate-macos"): | ||
| assert f" - {required_job_name}" in publication_job | ||
|
|
||
|
|
||
| def test_repository_release_version_matches_authoritative_version_file() -> None: | ||
| """Verify checked-in projections without creating another version authority.""" | ||
| release_guard = _load_guard() | ||
| version_text = (_REPOSITORY_ROOT / "VERSION").read_text(encoding="utf-8") | ||
| assert version_text.endswith("\n") | ||
| expected_version = version_text.removesuffix("\n") | ||
| assert "\n" not in expected_version | ||
| assert ( | ||
| release_guard.verify_release_identity(_REPOSITORY_ROOT) == expected_version | ||
| ) | ||
|
|
||
|
|
||
| def test_release_identity_guard_rejects_metadata_drift(tmp_path: Path) -> None: | ||
| """Reject a package projection that diverges from the authoritative version.""" | ||
| release_guard = _load_guard() | ||
| _write_release_metadata(tmp_path, "1.2.3") | ||
| package_document = json.loads( | ||
| (tmp_path / "package.json").read_text(encoding="utf-8") | ||
| ) | ||
| package_document["version"] = "1.2.4" | ||
| (tmp_path / "package.json").write_text( | ||
| json.dumps(package_document), encoding="utf-8" | ||
| ) | ||
|
|
||
| with pytest.raises(ValueError, match="package.json version does not match VERSION"): | ||
| release_guard.verify_release_identity(tmp_path) | ||
|
|
||
|
|
||
| def test_release_identity_guard_rejects_wrong_tag(tmp_path: Path) -> None: | ||
| """Reject a version tag that does not identify the exact VERSION release.""" | ||
| release_guard = _load_guard() | ||
| _write_release_metadata(tmp_path, "1.2.3") | ||
|
|
||
| with pytest.raises(ValueError, match="release tag does not match VERSION"): | ||
| release_guard.verify_release_identity(tmp_path, release_tag="v1.2.2") | ||
|
|
||
|
|
||
| def test_release_identity_guard_rejects_multiline_version_authority(tmp_path: Path) -> None: | ||
| """Reject an ambiguous VERSION file even if projections repeat the same text.""" | ||
| release_guard = _load_guard() | ||
| _write_release_metadata(tmp_path, "1.2.3") | ||
| ambiguous_version = "1.2.3\n2.0.0" | ||
| (tmp_path / "VERSION").write_text( | ||
| f"{ambiguous_version}\n", encoding="utf-8" | ||
| ) | ||
| (tmp_path / "package.json").write_text( | ||
| json.dumps({"name": "bandscope", "version": ambiguous_version}), | ||
| encoding="utf-8", | ||
| ) | ||
| (tmp_path / "apps" / "desktop" / "src-tauri" / "tauri.conf.json").write_text( | ||
| json.dumps( | ||
| { | ||
| "productName": "BandScope", | ||
| "version": ambiguous_version, | ||
| "identifier": "com.bandscope.desktop", | ||
| } | ||
| ), | ||
| encoding="utf-8", | ||
| ) | ||
|
|
||
| with pytest.raises( | ||
| ValueError, match="VERSION must contain exactly one non-empty version line" | ||
| ): | ||
| release_guard.verify_release_identity(tmp_path) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.