diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d122bd20..523eec5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,13 +120,6 @@ jobs: - name: Check formatting run: cargo +1.85.0 fmt --all -- --check - - name: Check Workshop dependency - env: - RUSTUP_TOOLCHAIN: 1.85.0 - run: | - python3 -m unittest discover -s scripts -p 'test_check_workshop_dependency.py' - python3 scripts/check-workshop-dependency.py - # Keep the surviving owner-independent Wright crates on the declared # MSRV. The provider-backed consumer crates are covered by the stable # full-workspace gate and the dedicated integration jobs below. @@ -520,15 +513,6 @@ jobs: shell: bash run: cargo build --locked -p wright-cli -p wright-lsp - - name: Record build identity - run: >- - python3 scripts/write-build-identity.py - --output target/debug/wright-dist.build.json - --revision "${{ github.sha }}" - --runner-os "${{ runner.os }}" - --toolchain stable - --profile dev - - name: Upload distribution binaries uses: actions/upload-artifact@v7 with: @@ -538,7 +522,6 @@ jobs: target/debug/wright.exe target/debug/wright-lsp target/debug/wright-lsp.exe - target/debug/wright-dist.build.json if-no-files-found: error dist-validation: @@ -600,15 +583,9 @@ jobs: name: wright-dist-cli-${{ matrix.os }} path: target/debug - - name: Verify build identity - run: >- - python scripts/verify-build-identity.py - --identity target/debug/wright-dist.build.json - --revision "${{ github.sha }}" - --runner-os "${{ runner.os }}" - --wright target/debug/wright - --wright-lsp target/debug/wright-lsp - + - name: Prepare distribution executables + if: runner.os != 'Windows' + run: chmod +x target/debug/wright target/debug/wright-lsp - name: Validate package metadata and installers if: matrix.channel == 'install.sh' || matrix.channel == 'install.ps1' diff --git a/scripts/check-workshop-dependency.py b/scripts/check-workshop-dependency.py deleted file mode 100644 index 0101e335..00000000 --- a/scripts/check-workshop-dependency.py +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env python3 -"""Validate Wright's single released or candidate workshop-rs dependency contract.""" - -from __future__ import annotations - -import json -import re -import subprocess -from pathlib import Path - - -ROOT = Path(__file__).resolve().parent.parent -CANDIDATE_SOURCE = re.compile( - r"^git\+https://github\.com/wrightkit/workshop-rs\.git\?rev=([0-9a-f]{40})#([0-9a-f]{40})$" -) - - -def is_pinned_git_candidate(source: str | None) -> bool: - if source is None: - return False - match = CANDIDATE_SOURCE.fullmatch(source) - return match is not None and match.group(1) == match.group(2) - - -def metadata() -> dict: - result = subprocess.run( - ["cargo", "metadata", "--locked", "--format-version", "1"], - cwd=ROOT, - capture_output=True, - text=True, - ) - if result.returncode != 0: - raise SystemExit( - "workshop dependency validation failed: cargo metadata failed\n" - + result.stderr - ) - return json.loads(result.stdout) - - -def main() -> int: - data = metadata() - packages_by_id = {package["id"]: package for package in data["packages"]} - workspace_packages = [ - packages_by_id[package_id] for package_id in data["workspace_members"] - ] - workshop_packages = [ - package for package in data["packages"] if package["name"] == "workshop-rs" - ] - - if len(workshop_packages) != 1: - versions = ", ".join( - f"{package['version']} ({package['source'] or 'unpublished'})" - for package in workshop_packages - ) - raise SystemExit( - "workshop dependency validation failed: expected exactly one resolved " - f"workshop-rs package, found {len(workshop_packages)}: {versions or 'none'}" - ) - - workshop = workshop_packages[0] - source = workshop.get("source") - is_registry = bool(source and source.startswith("registry+")) - is_pinned_git = is_pinned_git_candidate(source) - - if not (is_registry or is_pinned_git): - raise SystemExit( - "workshop dependency validation failed: workshop-rs must come from a " - f"released registry or pinned git candidate, got {source or 'unpublished'}" - ) - - direct = [] - for package in workspace_packages: - for dependency in package["dependencies"]: - if dependency["name"] == "workshop-rs": - direct.append((package["name"], dependency)) - - if not direct: - raise SystemExit( - "workshop dependency validation failed: no workspace package directly " - "consumes workshop-rs" - ) - - aliases = [ - f"{package}: {dependency['rename']}" - for package, dependency in direct - if dependency.get("rename") - ] - if aliases: - raise SystemExit( - "workshop dependency validation failed: renamed workshop-rs " - f"dependencies are not allowed ({', '.join(aliases)})" - ) - - requirements = {dependency["req"] for _, dependency in direct} - if is_registry: - if len(requirements) != 1 or not re.fullmatch( - r"\^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?", next(iter(requirements), "") - ): - consumers = ", ".join( - f"{package} ({dependency['req']})" for package, dependency in direct - ) - raise SystemExit( - "workshop dependency validation failed: direct consumers must use " - f"one ordinary compatible SemVer requirement, found {consumers}" - ) - else: - if requirements != {"*"}: - consumers = ", ".join( - f"{package} ({dependency['req']})" for package, dependency in direct - ) - raise SystemExit( - "workshop dependency validation failed: git candidate direct consumers " - f"must use '*', found {consumers}" - ) - - consumers = ", ".join(sorted(package for package, _ in direct)) - print( - f"workshop-rs contract: {workshop['version']} from {source} " - f"({next(iter(requirements))}; {len(direct)} direct consumers: {consumers})" - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/test_check_workshop_dependency.py b/scripts/test_check_workshop_dependency.py deleted file mode 100644 index 22a35830..00000000 --- a/scripts/test_check_workshop_dependency.py +++ /dev/null @@ -1,43 +0,0 @@ -import unittest -from importlib.util import module_from_spec, spec_from_file_location -from pathlib import Path - -spec = spec_from_file_location( - "check_workshop_dependency", Path(__file__).with_name("check-workshop-dependency.py") -) -assert spec is not None and spec.loader is not None -checker = module_from_spec(spec) -spec.loader.exec_module(checker) -is_pinned_git_candidate = checker.is_pinned_git_candidate - - -class PinnedGitCandidateTests(unittest.TestCase): - def test_accepts_exact_revision_pin(self): - revision = "ac5a6a4cf15bfccc5597cfd6ccb7b5028dfd5053" - source = ( - "git+https://github.com/wrightkit/workshop-rs.git?rev=" - f"{revision}#{revision}" - ) - self.assertTrue(is_pinned_git_candidate(source)) - - def test_rejects_git_sources_without_an_explicit_full_revision(self): - resolved = "ac5a6a4cf15bfccc5597cfd6ccb7b5028dfd5053" - sources = ( - f"git+https://github.com/wrightkit/workshop-rs.git#{resolved}", - f"git+https://github.com/wrightkit/workshop-rs.git?branch=main#{resolved}", - "git+https://github.com/wrightkit/workshop-rs.git?rev=v1.0.0#" - f"{resolved}", - "git+https://github.com/wrightkit/workshop-rs.git?rev=" - f"{resolved}#0000000000000000000000000000000000000000", - f"git+https://github.com/other/workshop-rs.git?rev={resolved}#{resolved}", - ) - for source in sources: - with self.subTest(source=source): - self.assertFalse(is_pinned_git_candidate(source)) - - def test_rejects_missing_source(self): - self.assertFalse(is_pinned_git_candidate(None)) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/verify-build-identity.py b/scripts/verify-build-identity.py deleted file mode 100755 index 52c1fd89..00000000 --- a/scripts/verify-build-identity.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -"""Verify a downloaded CI build identity and prepare native executables.""" - -from __future__ import annotations - -import argparse -import json -import os -from pathlib import Path - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--identity", type=Path, required=True) - parser.add_argument("--revision", required=True) - parser.add_argument("--runner-os", required=True) - parser.add_argument("--wright", type=Path, required=True) - parser.add_argument("--wright-lsp", type=Path, required=True) - args = parser.parse_args() - - identity = json.loads(args.identity.read_text()) - expected = { - "revision": args.revision, - "runner_os": args.runner_os, - "toolchain": "stable", - "profile": "dev", - "packages": ["wright-cli", "wright-lsp"], - } - for key, value in expected.items(): - if identity.get(key) != value: - raise SystemExit(f"build identity mismatch for {key}: {identity.get(key)!r}") - - if args.runner_os != "Windows": - os.chmod(args.wright, 0o755) - os.chmod(args.wright_lsp, 0o755) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/write-build-identity.py b/scripts/write-build-identity.py deleted file mode 100755 index 7bbdcfcf..00000000 --- a/scripts/write-build-identity.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -"""Write the machine-readable identity for a CI build artifact.""" - -from __future__ import annotations - -import argparse -import json -import subprocess -from pathlib import Path - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--output", type=Path, required=True) - parser.add_argument("--revision", required=True) - parser.add_argument("--runner-os", required=True) - parser.add_argument("--target") - parser.add_argument("--toolchain", required=True) - parser.add_argument("--profile", required=True) - args = parser.parse_args() - - identity = { - "revision": args.revision, - "runner_os": args.runner_os, - "target": args.target - or subprocess.check_output(["rustc", "-vV"], text=True).split("host: ", 1)[1].splitlines()[0], - "toolchain": args.toolchain, - "profile": args.profile, - "packages": ["wright-cli", "wright-lsp"], - "features": [], - } - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(json.dumps(identity, separators=(",", ":")) + "\n") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main())