Use pinned shared Gitee contracts as the sole runtime source
This commit is contained in:
@@ -45,6 +45,7 @@ python3 - "$OUT/manifest.json" "$VERSION" "$source_ref" "$source_dirty" "$go_ver
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
manifest_path, version, source_ref, source_dirty, go_version, out, project_root = sys.argv[1:]
|
||||
@@ -67,9 +68,13 @@ manifest = {
|
||||
"go.mod": sha256("go.mod"),
|
||||
"go.sum": sha256("go.sum"),
|
||||
},
|
||||
"contract_source": {
|
||||
"repository": "git@gitee.com:zzmbac/sip-contracts.git",
|
||||
"commit": subprocess.check_output(["git", "-C", str(project / "contracts/schema"), "rev-parse", "HEAD"], text=True).strip(),
|
||||
},
|
||||
"contract_attestation": {
|
||||
"local_contract_manifest_sha256": project_sha256("contracts/local/manifest.json"),
|
||||
"local_mq_topology_sha256": project_sha256("contracts/local/mq-topology.json"),
|
||||
"contract_verification_manifest_sha256": project_sha256("contracts/manifest.json"),
|
||||
"shared_mq_topology_sha256": project_sha256("contracts/schema/mq-topology.json"),
|
||||
"historical_upstream_manifest_sha256": project_sha256("docs/archive/upstream/manifest.txt"),
|
||||
"proto_manifest_sha256": project_sha256("proto/manifest.json"),
|
||||
},
|
||||
|
||||
@@ -1,40 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check provenance and reproducible digests of the sole current local bundle."""
|
||||
import hashlib
|
||||
import json
|
||||
"""Verify the pinned shared contract; never fetch or use a local fallback."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
URL = 'git@gitee.com:zzmbac/sip-contracts.git'
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BUNDLE = ROOT / "contracts" / "local"
|
||||
MANIFEST = BUNDLE / "manifest.json"
|
||||
def git(root, *args):
|
||||
return subprocess.check_output(['git', '-C', str(root), *args], text=True, stderr=subprocess.STDOUT).strip()
|
||||
|
||||
|
||||
def digest(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
def check_checkout(root):
|
||||
if (root / 'contracts/local').exists():
|
||||
raise ValueError('contracts/local is a forbidden parallel contract source')
|
||||
entry = git(root, 'ls-files', '--stage', '--', 'contracts/schema').split()
|
||||
if len(entry) != 4 or entry[0] != '160000' or entry[2] != '0':
|
||||
raise ValueError('contracts/schema must be a tracked Git submodule')
|
||||
sub = root / 'contracts/schema'
|
||||
if not (sub / '.git').exists():
|
||||
raise ValueError('contract submodule not initialized; run git submodule update --init --recursive')
|
||||
configured = git(root, 'config', '-f', '.gitmodules', '--get', 'submodule.contracts/schema.url')
|
||||
if configured != URL or git(sub, 'remote', 'get-url', 'origin') != URL:
|
||||
raise ValueError('contract submodule origin must be ' + URL)
|
||||
commit = git(sub, 'rev-parse', 'HEAD')
|
||||
if commit != entry[1]:
|
||||
raise ValueError('contract HEAD does not match the project pin; stage the verified contracts/schema pointer')
|
||||
if git(sub, 'status', '--porcelain', '--untracked-files=all'):
|
||||
raise ValueError('contract submodule has uncommitted changes; verify and commit them in the shared repository')
|
||||
return commit
|
||||
|
||||
|
||||
def main() -> None:
|
||||
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
||||
for relative, expected in manifest["sources"].items():
|
||||
path = ROOT / relative
|
||||
actual = digest(path)
|
||||
if actual != expected:
|
||||
raise SystemExit(f"source hash mismatch: {relative}: {actual} != {expected}")
|
||||
|
||||
paths = sorted(
|
||||
(p for p in BUNDLE.rglob("*.json")
|
||||
if p != MANIFEST and (p.parent == BUNDLE or p.relative_to(BUNDLE).parts[0] == "examples")),
|
||||
key=lambda p: p.relative_to(BUNDLE).as_posix(),
|
||||
)
|
||||
if len(paths) < 10:
|
||||
raise SystemExit("missing current contract examples or schemas")
|
||||
listing = "".join(f"{p.relative_to(BUNDLE).as_posix()} {digest(p)}\n" for p in paths)
|
||||
actual = hashlib.sha256(listing.encode("utf-8")).hexdigest()
|
||||
if actual != manifest["bundle_sha256"]:
|
||||
raise SystemExit(f"contract bundle hash mismatch: {actual} != {manifest['bundle_sha256']}")
|
||||
print(f"current local contract: {len(paths)} JSON files, source and bundle hashes valid")
|
||||
def main():
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
commit = check_checkout(root)
|
||||
checker_path = root / 'contracts/verify.py'
|
||||
spec = importlib.util.spec_from_file_location('shared_contract_verify', checker_path)
|
||||
checker = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(checker)
|
||||
count = checker.verify_bundle(checker_path.parent)
|
||||
print(f'shared contract {commit}: {count} JSON files; source/bundle hashes and offline references valid')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except (OSError, ValueError, subprocess.CalledProcessError) as exc:
|
||||
raise SystemExit(str(exc)) from exc
|
||||
|
||||
@@ -2,4 +2,6 @@
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
python3 scripts/check-current-contracts.py
|
||||
python3 -m unittest discover -s scripts -p 'test_contract_checkout.py' -v
|
||||
python3 -m unittest discover -s contracts -p 'test_*.py' -v
|
||||
go test ./contracts -run 'TestCurrentContract' -count=1
|
||||
|
||||
@@ -52,6 +52,8 @@ import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import subprocess
|
||||
|
||||
root, release = map(pathlib.Path, sys.argv[1:])
|
||||
manifest = json.loads((release / "manifest.json").read_text())
|
||||
assert manifest["manifest_version"] == 1
|
||||
@@ -60,20 +62,25 @@ assert manifest["security"] == {"credentials_embedded": False, "production_appro
|
||||
assert manifest["binary"]["sha256"] == hashlib.sha256((release / "sip-go-agent").read_bytes()).hexdigest()
|
||||
assert manifest["go_version"].startswith("go version go1.27.1 ")
|
||||
|
||||
topology = json.loads((root / "contracts/local/mq-topology.json").read_text())
|
||||
assert manifest["contract_source"] == {
|
||||
"repository": "git@gitee.com:zzmbac/sip-contracts.git",
|
||||
"commit": subprocess.check_output(["git", "-C", str(root / "contracts/schema"), "rev-parse", "HEAD"], text=True).strip(),
|
||||
}
|
||||
topology = json.loads((root / "contracts/schema/mq-topology.json").read_text())
|
||||
assert topology["ownership"] == "saas"
|
||||
assert topology["dispatcher"]["control"]["queue"].endswith(".v1")
|
||||
assert topology["dispatcher"]["task"]["queue"].endswith(".v1")
|
||||
assert topology["saas"]["result"]["queue"].endswith(".v1")
|
||||
expected_attestation = {
|
||||
"local_contract_manifest_sha256": hashlib.sha256((root / "contracts/local/manifest.json").read_bytes()).hexdigest(),
|
||||
"local_mq_topology_sha256": hashlib.sha256((root / "contracts/local/mq-topology.json").read_bytes()).hexdigest(),
|
||||
"contract_verification_manifest_sha256": hashlib.sha256((root / "contracts/manifest.json").read_bytes()).hexdigest(),
|
||||
"shared_mq_topology_sha256": hashlib.sha256((root / "contracts/schema/mq-topology.json").read_bytes()).hexdigest(),
|
||||
"historical_upstream_manifest_sha256": hashlib.sha256((root / "docs/archive/upstream/manifest.txt").read_bytes()).hexdigest(),
|
||||
"proto_manifest_sha256": hashlib.sha256((root / "proto/manifest.json").read_bytes()).hexdigest(),
|
||||
}
|
||||
assert manifest["contract_attestation"] == expected_attestation
|
||||
print("local artifact SHA-256:", manifest["binary"]["sha256"])
|
||||
print("current local contract SHA-256:", expected_attestation["local_contract_manifest_sha256"])
|
||||
print("pinned shared contract:", manifest["contract_source"]["commit"])
|
||||
print("contract verification manifest SHA-256:", expected_attestation["contract_verification_manifest_sha256"])
|
||||
print("source dirty:", manifest["source_dirty"])
|
||||
print("production approved:", manifest["security"]["production_approval"])
|
||||
PY
|
||||
@@ -121,7 +128,7 @@ assert manifest["version"] == version
|
||||
assert manifest["scope"] == "local-development"
|
||||
assert manifest["security"]["production_approval"] is False
|
||||
assert set(manifest["contract_attestation"]) == {
|
||||
"local_contract_manifest_sha256", "local_mq_topology_sha256",
|
||||
"contract_verification_manifest_sha256", "shared_mq_topology_sha256",
|
||||
"historical_upstream_manifest_sha256", "proto_manifest_sha256",
|
||||
}
|
||||
assert all(len(digest) == 64 for digest in manifest["contract_attestation"].values())
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
spec = importlib.util.spec_from_file_location('contract_checker', Path(__file__).with_name('check-current-contracts.py'))
|
||||
checker = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(checker)
|
||||
URL = 'git@gitee.com:zzmbac/sip-contracts.git'
|
||||
|
||||
|
||||
class ContractCheckoutTest(unittest.TestCase):
|
||||
def git(self, cwd, *args):
|
||||
return subprocess.check_output(['git', '-c', 'user.name=Contract Test', '-c', 'user.email=contracts@example.invalid', '-C', str(cwd), *args], stderr=subprocess.STDOUT, text=True).strip()
|
||||
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
base = Path(self.temp.name)
|
||||
source = base / 'source'
|
||||
source.mkdir()
|
||||
self.git(source, 'init', '-q')
|
||||
(source / 'test.schema.json').write_text('{}\n')
|
||||
self.git(source, 'add', '.')
|
||||
self.git(source, 'commit', '-qm', 'initial')
|
||||
self.root = base / 'consumer'
|
||||
self.root.mkdir()
|
||||
self.git(self.root, 'init', '-q')
|
||||
self.git(self.root, '-c', 'protocol.file.allow=always', 'submodule', 'add', '-q', str(source), 'contracts/schema')
|
||||
self.sub = self.root / 'contracts/schema'
|
||||
self.git(self.sub, 'remote', 'set-url', 'origin', URL)
|
||||
self.git(self.root, 'config', '-f', '.gitmodules', 'submodule.contracts/schema.url', URL)
|
||||
self.git(self.root, 'add', '.')
|
||||
self.git(self.root, 'commit', '-qm', 'pin contract')
|
||||
|
||||
def test_clean_pinned_checkout(self):
|
||||
self.assertEqual(checker.check_checkout(self.root), self.git(self.sub, 'rev-parse', 'HEAD'))
|
||||
|
||||
def test_uninitialized_submodule_is_rejected(self):
|
||||
self.git(self.root, 'submodule', 'deinit', '-f', '--', 'contracts/schema')
|
||||
with self.assertRaisesRegex(ValueError, 'not initialized'):
|
||||
checker.check_checkout(self.root)
|
||||
|
||||
def test_unpinned_commit_is_rejected(self):
|
||||
(self.sub / 'test.schema.json').write_text('{"changed": true}\n')
|
||||
self.git(self.sub, 'add', '.')
|
||||
self.git(self.sub, 'commit', '-qm', 'changed')
|
||||
with self.assertRaisesRegex(ValueError, 'does not match'):
|
||||
checker.check_checkout(self.root)
|
||||
|
||||
def test_dirty_submodule_is_rejected(self):
|
||||
(self.sub / 'untracked.txt').write_text('unexpected\n')
|
||||
with self.assertRaisesRegex(ValueError, 'uncommitted'):
|
||||
checker.check_checkout(self.root)
|
||||
|
||||
def test_wrong_origin_is_rejected(self):
|
||||
self.git(self.sub, 'remote', 'set-url', 'origin', 'https://example.invalid/other.git')
|
||||
with self.assertRaisesRegex(ValueError, 'origin'):
|
||||
checker.check_checkout(self.root)
|
||||
|
||||
def test_parallel_local_contract_is_rejected(self):
|
||||
(self.root / 'contracts/local').mkdir()
|
||||
with self.assertRaisesRegex(ValueError, 'parallel'):
|
||||
checker.check_checkout(self.root)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# Explicit maintenance operation: fetch latest shared main before contract edits.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
sub=contracts/schema
|
||||
if [[ ! -e "$sub/.git" ]]; then
|
||||
git submodule update --init -- "$sub"
|
||||
fi
|
||||
python3 scripts/check-current-contracts.py
|
||||
git -C "$sub" fetch origin main
|
||||
git -C "$sub" merge --ff-only FETCH_HEAD
|
||||
if [[ "$(git -C "$sub" rev-parse HEAD)" != "$(git -C "$sub" rev-parse FETCH_HEAD)" ]]; then
|
||||
echo 'Contract checkout is ahead of shared main; publish/reconcile it explicitly before updating.' >&2
|
||||
exit 1
|
||||
fi
|
||||
python3 contracts/verify.py
|
||||
python3 -m unittest discover -s contracts -p 'test_*.py' -v
|
||||
# This stages only the contract pointer. Implementations, tests and the pointer
|
||||
# still need a reviewed consumer-project commit; unrelated files are untouched.
|
||||
git add -- "$sub"
|
||||
python3 scripts/check-current-contracts.py
|
||||
printf 'Shared contract updated to %s; pointer staged, consumer changes still require verification and commit.\n' "$(git -C "$sub" rev-parse HEAD)"
|
||||
Reference in New Issue
Block a user