mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 16:53:35 +08:00
- 将各平台驱动拆分为独立 ZIP,并为 DuckDB 同包携带运行库 - 仅生成 ZIP 下载候选,按 Gatewaysentry 与 GitHub 短测速结果调整优先级 - 扩展发布索引的归档摘要与条目元数据,强化跨平台资产校验 - 禁止裸驱动与 CI 总包进入 VPS 镜像,并校验载荷摘要和索引一致性 - 显式控制驱动镜像发布,自动清理失败暂存、过期目录和旧版本
1083 lines
40 KiB
Python
1083 lines
40 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import contextlib
|
|
import hashlib
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import zipfile
|
|
|
|
|
|
MODULE_PATH = pathlib.Path(__file__).with_name("validate-driver-release-assets.py")
|
|
SPEC = importlib.util.spec_from_file_location("validate_driver_release_assets", MODULE_PATH)
|
|
MODULE = importlib.util.module_from_spec(SPEC)
|
|
assert SPEC.loader is not None
|
|
SPEC.loader.exec_module(MODULE)
|
|
|
|
|
|
class ValidateDriverReleaseAssetsTests(unittest.TestCase):
|
|
def test_infer_asset_path(self):
|
|
self.assertEqual(
|
|
MODULE.infer_asset_path("clickhouse-driver-agent-darwin-arm64"),
|
|
"MacOS/clickhouse-driver-agent-darwin-arm64",
|
|
)
|
|
self.assertEqual(
|
|
MODULE.infer_asset_path("clickhouse-driver-agent-windows-arm64.exe"),
|
|
"Windows/clickhouse-driver-agent-windows-arm64.exe",
|
|
)
|
|
self.assertEqual(
|
|
MODULE.infer_asset_path("mongodb-driver-agent-v1-windows-amd64.exe"),
|
|
"Windows/mongodb-driver-agent-v1-windows-amd64.exe",
|
|
)
|
|
self.assertEqual(
|
|
MODULE.infer_asset_path("mongodb-driver-agent-v2-darwin-arm64"),
|
|
"MacOS/mongodb-driver-agent-v2-darwin-arm64",
|
|
)
|
|
self.assertEqual(
|
|
MODULE.infer_asset_path("clickhouse-driver-agent-linux-arm64"),
|
|
"Linux/clickhouse-driver-agent-linux-arm64",
|
|
)
|
|
self.assertEqual(MODULE.infer_asset_path("duckdb.dll"), "Windows/duckdb.dll")
|
|
self.assertIsNone(MODULE.infer_asset_path("duckdb-driver.zip"))
|
|
|
|
def test_infer_driver_zip_asset_name(self):
|
|
self.assertEqual(
|
|
MODULE.infer_driver_zip_asset_name("clickhouse-driver-agent-darwin-arm64"),
|
|
"clickhouse-driver-agent-darwin-arm64.zip",
|
|
)
|
|
self.assertEqual(
|
|
MODULE.infer_driver_zip_asset_name("mariadb-driver-agent-windows-amd64.exe"),
|
|
"mariadb-driver-agent-windows-amd64.zip",
|
|
)
|
|
self.assertEqual(
|
|
MODULE.infer_driver_zip_asset_name("mongodb-driver-agent-v1-windows-arm64.exe"),
|
|
"mongodb-driver-agent-v1-windows-arm64.zip",
|
|
)
|
|
self.assertEqual(
|
|
MODULE.infer_driver_zip_asset_name("duckdb-driver-agent-windows-amd64.exe"),
|
|
"duckdb-driver-agent-windows-amd64.zip",
|
|
)
|
|
self.assertIsNone(MODULE.infer_driver_zip_asset_name("duckdb.dll"))
|
|
|
|
def test_validate_release_assets_extracts_independent_zip(self):
|
|
name = "clickhouse-driver-agent-darwin-arm64"
|
|
zip_name = f"{name}.zip"
|
|
payload = b"zipped-driver-agent"
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": zip_name,
|
|
"browser_download_url": f"https://example.test/{zip_name}",
|
|
}
|
|
]
|
|
}
|
|
manifest = {
|
|
"assets": {
|
|
name: {
|
|
"platform": "darwin/arm64",
|
|
"revision": "src-expected",
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
}
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory(prefix="gonavi-validate-release-assets-") as tmp:
|
|
archive_path = pathlib.Path(tmp) / zip_name
|
|
with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
|
archive.writestr(f"MacOS/{name}", payload)
|
|
archive_bytes = archive_path.read_bytes()
|
|
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
|
release["assets"][0]["size"] = len(archive_bytes)
|
|
release["assets"][0]["digest"] = f"sha256:{archive_sha}"
|
|
manifest["assets"][name]["size"] = len(payload)
|
|
release_index = {
|
|
"assets": {zip_name: len(archive_bytes)},
|
|
"assetSha256": {zip_name: archive_sha},
|
|
"entries": {
|
|
name: {
|
|
"archive": zip_name,
|
|
"path": f"MacOS/{name}",
|
|
"size": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
},
|
|
}
|
|
|
|
downloaded_urls = []
|
|
probed_payloads = []
|
|
|
|
def fake_download(url, destination):
|
|
downloaded_urls.append(url)
|
|
destination.write_bytes(archive_path.read_bytes())
|
|
|
|
def fake_probe(path):
|
|
probed_payloads.append(path.read_bytes())
|
|
return "src-expected"
|
|
|
|
original_download = MODULE.download_url
|
|
original_probe = MODULE.probe_metadata_revision
|
|
try:
|
|
MODULE.download_url = fake_download
|
|
MODULE.probe_metadata_revision = fake_probe
|
|
mismatches, skipped = MODULE.validate_release_assets(
|
|
release,
|
|
manifest,
|
|
runtime_platform="darwin/arm64",
|
|
release_index=release_index,
|
|
)
|
|
finally:
|
|
MODULE.download_url = original_download
|
|
MODULE.probe_metadata_revision = original_probe
|
|
|
|
self.assertEqual(mismatches, [])
|
|
self.assertEqual(skipped, [])
|
|
self.assertEqual(downloaded_urls, [f"https://example.test/{zip_name}"])
|
|
self.assertEqual(probed_payloads, [payload])
|
|
|
|
def test_indexed_duckdb_archive_verifies_runtime_library_entry(self):
|
|
name = "duckdb-driver-agent-windows-amd64.exe"
|
|
zip_name = "duckdb-driver-agent-windows-amd64.zip"
|
|
agent_payload = b"duckdb-agent"
|
|
library_payload = b"wrong-duckdb-library"
|
|
expected_library_sha = hashlib.sha256(b"expected-duckdb-library").hexdigest()
|
|
manifest = {
|
|
"assets": {
|
|
name: {
|
|
"platform": "windows/amd64",
|
|
"revision": "src-expected",
|
|
"size": len(agent_payload),
|
|
"sha256": hashlib.sha256(agent_payload).hexdigest(),
|
|
}
|
|
}
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory(prefix="gonavi-validate-release-assets-") as tmp:
|
|
archive_path = pathlib.Path(tmp) / zip_name
|
|
with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
|
archive.writestr(f"Windows/{name}", agent_payload)
|
|
archive.writestr("Windows/duckdb.dll", library_payload)
|
|
archive_bytes = archive_path.read_bytes()
|
|
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": zip_name,
|
|
"size": len(archive_bytes),
|
|
"digest": f"sha256:{archive_sha}",
|
|
"browser_download_url": f"https://example.test/{zip_name}",
|
|
}
|
|
]
|
|
}
|
|
release_index = {
|
|
"assets": {zip_name: len(archive_bytes)},
|
|
"assetSha256": {zip_name: archive_sha},
|
|
"entries": {
|
|
name: {
|
|
"archive": zip_name,
|
|
"path": f"Windows/{name}",
|
|
"size": len(agent_payload),
|
|
"sha256": hashlib.sha256(agent_payload).hexdigest(),
|
|
},
|
|
"duckdb.dll": {
|
|
"archive": zip_name,
|
|
"path": "Windows/duckdb.dll",
|
|
"size": len(library_payload),
|
|
"sha256": expected_library_sha,
|
|
},
|
|
},
|
|
}
|
|
|
|
def fake_download(_url, destination):
|
|
destination.write_bytes(archive_bytes)
|
|
|
|
original_download = MODULE.download_url
|
|
original_probe = MODULE.probe_metadata_revision
|
|
try:
|
|
MODULE.download_url = fake_download
|
|
MODULE.probe_metadata_revision = lambda _path: "src-expected"
|
|
mismatches, skipped = MODULE.validate_release_assets(
|
|
release,
|
|
manifest,
|
|
runtime_platform="windows/amd64",
|
|
release_index=release_index,
|
|
)
|
|
finally:
|
|
MODULE.download_url = original_download
|
|
MODULE.probe_metadata_revision = original_probe
|
|
|
|
self.assertEqual(skipped, [])
|
|
self.assertEqual(
|
|
mismatches,
|
|
[
|
|
(
|
|
"duckdb.dll",
|
|
"entry_sha256",
|
|
hashlib.sha256(library_payload).hexdigest(),
|
|
expected_library_sha,
|
|
)
|
|
],
|
|
)
|
|
|
|
def test_indexed_validation_rejects_ci_bundle_archive_maps(self):
|
|
name = "clickhouse-driver-agent-linux-amd64"
|
|
zip_name = f"{name}.zip"
|
|
archive_bytes = b"independent-archive"
|
|
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
|
bundle_bytes = b"ci-completion-bundle"
|
|
bundle_sha = hashlib.sha256(bundle_bytes).hexdigest()
|
|
payload = b"driver"
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": zip_name,
|
|
"size": len(archive_bytes),
|
|
"digest": f"sha256:{archive_sha}",
|
|
"browser_download_url": f"https://example.test/{zip_name}",
|
|
},
|
|
{
|
|
"name": MODULE.CI_BUNDLE_ASSET_NAME,
|
|
"size": len(bundle_bytes),
|
|
"digest": f"sha256:{bundle_sha}",
|
|
"browser_download_url": f"https://example.test/{MODULE.CI_BUNDLE_ASSET_NAME}",
|
|
},
|
|
]
|
|
}
|
|
manifest = {
|
|
"assets": {
|
|
name: {
|
|
"platform": "linux/amd64",
|
|
"revision": "src-expected",
|
|
"size": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
}
|
|
}
|
|
release_index = {
|
|
"assets": {
|
|
zip_name: len(archive_bytes),
|
|
MODULE.CI_BUNDLE_ASSET_NAME: len(bundle_bytes),
|
|
},
|
|
"assetSha256": {
|
|
zip_name: archive_sha,
|
|
MODULE.CI_BUNDLE_ASSET_NAME: bundle_sha,
|
|
},
|
|
"entries": {
|
|
name: {
|
|
"archive": zip_name,
|
|
"path": f"Linux/{name}",
|
|
"size": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
},
|
|
}
|
|
|
|
def fail_download(_url, _destination):
|
|
raise AssertionError("cross-platform indexed asset should not be downloaded")
|
|
|
|
original_download = MODULE.download_url
|
|
try:
|
|
MODULE.download_url = fail_download
|
|
mismatches, skipped = MODULE.validate_release_assets(
|
|
release,
|
|
manifest,
|
|
runtime_platform="darwin/arm64",
|
|
release_index=release_index,
|
|
)
|
|
finally:
|
|
MODULE.download_url = original_download
|
|
|
|
self.assertEqual(skipped, [])
|
|
self.assertIn(
|
|
(MODULE.CI_BUNDLE_ASSET_NAME, "index_ci_bundle", "present in assets", "absent"),
|
|
mismatches,
|
|
)
|
|
self.assertIn(
|
|
(
|
|
MODULE.CI_BUNDLE_ASSET_NAME,
|
|
"index_ci_bundle_sha256",
|
|
"present in assetSha256",
|
|
"absent",
|
|
),
|
|
mismatches,
|
|
)
|
|
|
|
def test_indexed_validation_rejects_entry_backed_by_ci_bundle(self):
|
|
name = "clickhouse-driver-agent-linux-amd64"
|
|
zip_name = f"{name}.zip"
|
|
archive_bytes = b"independent-archive"
|
|
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
|
payload = b"driver"
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": zip_name,
|
|
"size": len(archive_bytes),
|
|
"digest": f"sha256:{archive_sha}",
|
|
"browser_download_url": f"https://example.test/{zip_name}",
|
|
}
|
|
]
|
|
}
|
|
manifest = {
|
|
"assets": {
|
|
name: {
|
|
"platform": "linux/amd64",
|
|
"revision": "src-expected",
|
|
"size": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
}
|
|
}
|
|
release_index = {
|
|
"assets": {zip_name: len(archive_bytes)},
|
|
"assetSha256": {zip_name: archive_sha},
|
|
"entries": {
|
|
name: {
|
|
"archive": zip_name,
|
|
"path": f"Linux/{name}",
|
|
"size": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
},
|
|
"rogue-driver-agent": {
|
|
"archive": MODULE.CI_BUNDLE_ASSET_NAME,
|
|
"path": "Linux/rogue-driver-agent",
|
|
"size": 1,
|
|
"sha256": "a" * 64,
|
|
},
|
|
},
|
|
}
|
|
|
|
def fail_download(_url, _destination):
|
|
raise AssertionError("cross-platform indexed asset should not be downloaded")
|
|
|
|
original_download = MODULE.download_url
|
|
try:
|
|
MODULE.download_url = fail_download
|
|
mismatches, skipped = MODULE.validate_release_assets(
|
|
release,
|
|
manifest,
|
|
runtime_platform="darwin/arm64",
|
|
release_index=release_index,
|
|
)
|
|
finally:
|
|
MODULE.download_url = original_download
|
|
|
|
self.assertEqual(skipped, [])
|
|
self.assertIn(
|
|
(
|
|
"rogue-driver-agent",
|
|
"index_ci_bundle_entry",
|
|
MODULE.CI_BUNDLE_ASSET_NAME,
|
|
"individual driver archive",
|
|
),
|
|
mismatches,
|
|
)
|
|
|
|
def test_indexed_validation_rejects_malformed_cross_platform_duckdb_library(self):
|
|
name = MODULE.DUCKDB_WINDOWS_AGENT_NAME
|
|
zip_name = MODULE.infer_driver_zip_asset_name(name)
|
|
archive_bytes = b"duckdb-archive"
|
|
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
|
payload = b"duckdb-agent"
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": zip_name,
|
|
"size": len(archive_bytes),
|
|
"digest": f"sha256:{archive_sha}",
|
|
"browser_download_url": f"https://example.test/{zip_name}",
|
|
}
|
|
]
|
|
}
|
|
manifest = {
|
|
"assets": {
|
|
name: {
|
|
"platform": "windows/amd64",
|
|
"revision": "src-expected",
|
|
"size": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
}
|
|
}
|
|
release_index = {
|
|
"assets": {zip_name: len(archive_bytes)},
|
|
"assetSha256": {zip_name: archive_sha},
|
|
"entries": {
|
|
name: {
|
|
"archive": zip_name,
|
|
"path": f"Windows/{name}",
|
|
"size": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
},
|
|
MODULE.DUCKDB_WINDOWS_LIBRARY_NAME: {
|
|
"archive": "missing.zip",
|
|
"path": "../Windows/duckdb.dll",
|
|
"size": -1,
|
|
"sha256": "invalid",
|
|
},
|
|
},
|
|
}
|
|
|
|
def fail_download(_url, _destination):
|
|
raise AssertionError("cross-platform indexed asset should not be downloaded")
|
|
|
|
original_download = MODULE.download_url
|
|
try:
|
|
MODULE.download_url = fail_download
|
|
mismatches, skipped = MODULE.validate_release_assets(
|
|
release,
|
|
manifest,
|
|
runtime_platform="darwin/arm64",
|
|
release_index=release_index,
|
|
)
|
|
finally:
|
|
MODULE.download_url = original_download
|
|
|
|
self.assertEqual(skipped, [])
|
|
self.assertIn(
|
|
(MODULE.DUCKDB_WINDOWS_LIBRARY_NAME, "index_asset", "missing.zip", "present in assets"),
|
|
mismatches,
|
|
)
|
|
self.assertIn(
|
|
(
|
|
MODULE.DUCKDB_WINDOWS_LIBRARY_NAME,
|
|
"index_entry_path",
|
|
"../Windows/duckdb.dll",
|
|
"safe relative archive path",
|
|
),
|
|
mismatches,
|
|
)
|
|
self.assertIn(
|
|
(MODULE.DUCKDB_WINDOWS_LIBRARY_NAME, "index_entry_size", -1, "non-negative integer"),
|
|
mismatches,
|
|
)
|
|
self.assertIn(
|
|
(MODULE.DUCKDB_WINDOWS_LIBRARY_NAME, "index_entry_sha256", "invalid", "sha256"),
|
|
mismatches,
|
|
)
|
|
self.assertIn(
|
|
(MODULE.DUCKDB_WINDOWS_LIBRARY_NAME, "index_archive", "missing.zip", zip_name),
|
|
mismatches,
|
|
)
|
|
|
|
def test_indexed_validation_requires_duckdb_library_mapping(self):
|
|
name = MODULE.DUCKDB_WINDOWS_AGENT_NAME
|
|
zip_name = MODULE.infer_driver_zip_asset_name(name)
|
|
archive_bytes = b"duckdb-archive"
|
|
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
|
payload = b"duckdb-agent"
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": zip_name,
|
|
"size": len(archive_bytes),
|
|
"digest": f"sha256:{archive_sha}",
|
|
"browser_download_url": f"https://example.test/{zip_name}",
|
|
}
|
|
]
|
|
}
|
|
manifest = {
|
|
"assets": {
|
|
name: {
|
|
"platform": "windows/amd64",
|
|
"revision": "src-expected",
|
|
"size": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
}
|
|
}
|
|
release_index = {
|
|
"assets": {zip_name: len(archive_bytes)},
|
|
"assetSha256": {zip_name: archive_sha},
|
|
"entries": {
|
|
name: {
|
|
"archive": zip_name,
|
|
"path": f"Windows/{name}",
|
|
"size": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
},
|
|
}
|
|
|
|
def fail_download(_url, _destination):
|
|
raise AssertionError("cross-platform indexed asset should not be downloaded")
|
|
|
|
original_download = MODULE.download_url
|
|
try:
|
|
MODULE.download_url = fail_download
|
|
mismatches, skipped = MODULE.validate_release_assets(
|
|
release,
|
|
manifest,
|
|
runtime_platform="darwin/arm64",
|
|
release_index=release_index,
|
|
)
|
|
finally:
|
|
MODULE.download_url = original_download
|
|
|
|
self.assertEqual(skipped, [])
|
|
self.assertIn(
|
|
(
|
|
MODULE.DUCKDB_WINDOWS_LIBRARY_NAME,
|
|
"index_entry",
|
|
"",
|
|
f"present with {MODULE.DUCKDB_WINDOWS_AGENT_NAME}",
|
|
),
|
|
mismatches,
|
|
)
|
|
|
|
def test_validate_release_assets_reports_independent_zip_raw_sha_mismatch(self):
|
|
name = "mariadb-driver-agent-windows-amd64.exe"
|
|
zip_name = "mariadb-driver-agent-windows-amd64.zip"
|
|
payload = b"unexpected-driver-agent"
|
|
expected_manifest_sha = "d" * 64
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": zip_name,
|
|
"browser_download_url": f"https://example.test/{zip_name}",
|
|
"digest": f"sha256:{hashlib.sha256(b'archive-bytes').hexdigest()}",
|
|
}
|
|
]
|
|
}
|
|
manifest = {
|
|
"assets": {
|
|
name: {
|
|
"platform": "windows/amd64",
|
|
"revision": "src-expected",
|
|
"sha256": expected_manifest_sha,
|
|
}
|
|
}
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory(prefix="gonavi-validate-release-assets-") as tmp:
|
|
archive_path = pathlib.Path(tmp) / zip_name
|
|
with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
|
archive.writestr(f"Windows/{name}", payload)
|
|
archive_bytes = archive_path.read_bytes()
|
|
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
|
release["assets"][0]["size"] = len(archive_bytes)
|
|
release["assets"][0]["digest"] = f"sha256:{archive_sha}"
|
|
manifest["assets"][name]["size"] = len(payload)
|
|
release_index = {
|
|
"assets": {zip_name: len(archive_bytes)},
|
|
"assetSha256": {zip_name: archive_sha},
|
|
"entries": {
|
|
name: {
|
|
"archive": zip_name,
|
|
"path": f"Windows/{name}",
|
|
"size": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
},
|
|
}
|
|
|
|
def fail_download(_url, _destination):
|
|
raise AssertionError("cross-platform indexed asset should not be downloaded")
|
|
|
|
original_download = MODULE.download_url
|
|
original_probe = MODULE.probe_metadata_revision
|
|
try:
|
|
MODULE.download_url = fail_download
|
|
MODULE.probe_metadata_revision = lambda _path: "src-expected"
|
|
mismatches, skipped = MODULE.validate_release_assets(
|
|
release,
|
|
manifest,
|
|
runtime_platform="linux/amd64",
|
|
release_index=release_index,
|
|
)
|
|
finally:
|
|
MODULE.download_url = original_download
|
|
MODULE.probe_metadata_revision = original_probe
|
|
|
|
self.assertEqual(skipped, [])
|
|
self.assertEqual(
|
|
mismatches,
|
|
[(name, "index_sha256", hashlib.sha256(payload).hexdigest(), expected_manifest_sha)],
|
|
)
|
|
|
|
def test_validate_release_assets_skips_cross_platform_zip_revision_probe(self):
|
|
name = "mongodb-driver-agent-v1-linux-arm64"
|
|
zip_name = f"{name}.zip"
|
|
payload = b"cross-platform-driver-agent"
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": zip_name,
|
|
"browser_download_url": f"https://example.test/{zip_name}",
|
|
}
|
|
]
|
|
}
|
|
manifest = {
|
|
"assets": {
|
|
name: {
|
|
"platform": "linux/arm64",
|
|
"revision": "src-expected",
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
}
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory(prefix="gonavi-validate-release-assets-") as tmp:
|
|
archive_path = pathlib.Path(tmp) / zip_name
|
|
with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
|
archive.writestr(f"Linux/{name}", payload)
|
|
archive_bytes = archive_path.read_bytes()
|
|
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
|
release["assets"][0]["size"] = len(archive_bytes)
|
|
release["assets"][0]["digest"] = f"sha256:{archive_sha}"
|
|
manifest["assets"][name]["size"] = len(payload)
|
|
release_index = {
|
|
"assets": {zip_name: len(archive_bytes)},
|
|
"assetSha256": {zip_name: archive_sha},
|
|
"entries": {
|
|
name: {
|
|
"archive": zip_name,
|
|
"path": f"Linux/{name}",
|
|
"size": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
},
|
|
}
|
|
|
|
def fail_download(_url, _destination):
|
|
raise AssertionError("cross-platform indexed asset should not be downloaded")
|
|
|
|
def fail_probe(_path):
|
|
raise AssertionError("cross-platform zip entry should not be executed")
|
|
|
|
original_download = MODULE.download_url
|
|
original_probe = MODULE.probe_metadata_revision
|
|
try:
|
|
MODULE.download_url = fail_download
|
|
MODULE.probe_metadata_revision = fail_probe
|
|
mismatches, skipped = MODULE.validate_release_assets(
|
|
release,
|
|
manifest,
|
|
runtime_platform="darwin/arm64",
|
|
release_index=release_index,
|
|
)
|
|
finally:
|
|
MODULE.download_url = original_download
|
|
MODULE.probe_metadata_revision = original_probe
|
|
|
|
self.assertEqual(mismatches, [])
|
|
self.assertEqual(skipped, [])
|
|
|
|
def test_validate_release_assets_reports_missing_independent_zip_entry(self):
|
|
name = "clickhouse-driver-agent-linux-amd64"
|
|
zip_name = f"{name}.zip"
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": zip_name,
|
|
"browser_download_url": f"https://example.test/{zip_name}",
|
|
}
|
|
]
|
|
}
|
|
manifest = {
|
|
"assets": {
|
|
name: {
|
|
"platform": "linux/amd64",
|
|
"revision": "src-expected",
|
|
"sha256": hashlib.sha256(b"driver").hexdigest(),
|
|
}
|
|
}
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory(prefix="gonavi-validate-release-assets-") as tmp:
|
|
archive_path = pathlib.Path(tmp) / zip_name
|
|
with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
|
archive.writestr(name, b"driver")
|
|
archive_bytes = archive_path.read_bytes()
|
|
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
|
release["assets"][0]["size"] = len(archive_bytes)
|
|
release["assets"][0]["digest"] = f"sha256:{archive_sha}"
|
|
manifest["assets"][name]["size"] = len(b"driver")
|
|
release_index = {
|
|
"assets": {zip_name: len(archive_bytes)},
|
|
"assetSha256": {zip_name: archive_sha},
|
|
"entries": {
|
|
name: {
|
|
"archive": zip_name,
|
|
"path": f"Linux/{name}",
|
|
"size": len(b"driver"),
|
|
"sha256": hashlib.sha256(b"driver").hexdigest(),
|
|
}
|
|
},
|
|
}
|
|
|
|
def fake_download(_url, destination):
|
|
destination.write_bytes(archive_path.read_bytes())
|
|
|
|
original_download = MODULE.download_url
|
|
try:
|
|
MODULE.download_url = fake_download
|
|
mismatches, skipped = MODULE.validate_release_assets(
|
|
release,
|
|
manifest,
|
|
runtime_platform="linux/amd64",
|
|
release_index=release_index,
|
|
)
|
|
finally:
|
|
MODULE.download_url = original_download
|
|
|
|
self.assertEqual(skipped, [])
|
|
self.assertEqual(
|
|
mismatches,
|
|
[(name, "zip_entry", "", f"Linux/{name}")],
|
|
)
|
|
|
|
def test_indexed_validation_checks_release_archive_metadata_without_download(self):
|
|
name = "clickhouse-driver-agent-linux-amd64"
|
|
zip_name = f"{name}.zip"
|
|
payload = b"driver"
|
|
expected_archive = b"indexed-archive"
|
|
expected_archive_sha = hashlib.sha256(expected_archive).hexdigest()
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": zip_name,
|
|
"size": len(expected_archive) + 1,
|
|
"digest": f"sha256:{hashlib.sha256(b'wrong-archive').hexdigest()}",
|
|
"browser_download_url": f"https://example.test/{zip_name}",
|
|
}
|
|
]
|
|
}
|
|
manifest = {
|
|
"assets": {
|
|
name: {
|
|
"platform": "linux/amd64",
|
|
"revision": "src-expected",
|
|
"size": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
}
|
|
}
|
|
release_index = {
|
|
"assets": {zip_name: len(expected_archive)},
|
|
"assetSha256": {zip_name: expected_archive_sha},
|
|
"entries": {
|
|
name: {
|
|
"archive": zip_name,
|
|
"path": f"Linux/{name}",
|
|
"size": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
},
|
|
}
|
|
|
|
def fail_download(_url, _destination):
|
|
raise AssertionError("cross-platform indexed asset should not be downloaded")
|
|
|
|
original_download = MODULE.download_url
|
|
try:
|
|
MODULE.download_url = fail_download
|
|
mismatches, skipped = MODULE.validate_release_assets(
|
|
release,
|
|
manifest,
|
|
runtime_platform="darwin/arm64",
|
|
release_index=release_index,
|
|
)
|
|
finally:
|
|
MODULE.download_url = original_download
|
|
|
|
self.assertEqual(skipped, [])
|
|
self.assertEqual(
|
|
mismatches,
|
|
[
|
|
(zip_name, "archive_size", len(expected_archive) + 1, len(expected_archive)),
|
|
(
|
|
zip_name,
|
|
"archive_sha256",
|
|
hashlib.sha256(b"wrong-archive").hexdigest(),
|
|
expected_archive_sha,
|
|
),
|
|
],
|
|
)
|
|
|
|
def test_malformed_new_index_does_not_fall_back_to_zip_downloads(self):
|
|
name = "clickhouse-driver-agent-linux-amd64"
|
|
zip_name = f"{name}.zip"
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": zip_name,
|
|
"browser_download_url": f"https://example.test/{zip_name}",
|
|
}
|
|
]
|
|
}
|
|
manifest = {
|
|
"assets": {
|
|
name: {
|
|
"platform": "linux/amd64",
|
|
"revision": "src-expected",
|
|
"size": 6,
|
|
"sha256": hashlib.sha256(b"driver").hexdigest(),
|
|
}
|
|
}
|
|
}
|
|
release_index = {"assets": {zip_name: 123}, "entries": {}}
|
|
|
|
def fail_download(_url, _destination):
|
|
raise AssertionError("malformed new index must fail before downloading archives")
|
|
|
|
original_download = MODULE.download_url
|
|
try:
|
|
MODULE.download_url = fail_download
|
|
with self.assertRaisesRegex(RuntimeError, "assetSha256"):
|
|
MODULE.validate_release_assets(
|
|
release,
|
|
manifest,
|
|
runtime_platform="linux/amd64",
|
|
release_index=release_index,
|
|
)
|
|
finally:
|
|
MODULE.download_url = original_download
|
|
|
|
def test_main_downloads_index_and_avoids_cross_platform_archive(self):
|
|
name = "mongodb-driver-agent-v1-linux-arm64"
|
|
zip_name = f"{name}.zip"
|
|
payload = b"driver"
|
|
archive_size = 123
|
|
archive_sha = hashlib.sha256(b"archive").hexdigest()
|
|
manifest = {
|
|
"assets": {
|
|
name: {
|
|
"platform": "linux/arm64",
|
|
"revision": "src-expected",
|
|
"size": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
}
|
|
}
|
|
release_index = {
|
|
"assets": {zip_name: archive_size},
|
|
"assetSha256": {zip_name: archive_sha},
|
|
"entries": {
|
|
name: {
|
|
"archive": zip_name,
|
|
"path": f"Linux/{name}",
|
|
"size": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
},
|
|
}
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": MODULE.MANIFEST_ASSET_NAME,
|
|
"browser_download_url": "https://example.test/manifest",
|
|
},
|
|
{
|
|
"name": MODULE.INDEX_ASSET_NAME,
|
|
"browser_download_url": "https://example.test/index",
|
|
},
|
|
{
|
|
"name": zip_name,
|
|
"size": archive_size,
|
|
"digest": f"sha256:{archive_sha}",
|
|
"browser_download_url": f"https://example.test/{zip_name}",
|
|
},
|
|
]
|
|
}
|
|
downloaded_urls = []
|
|
|
|
def fake_download(url, destination):
|
|
downloaded_urls.append(url)
|
|
if url.endswith("/manifest"):
|
|
destination.write_text(json.dumps(manifest), encoding="utf-8")
|
|
elif url.endswith("/index"):
|
|
destination.write_text(json.dumps(release_index), encoding="utf-8")
|
|
else:
|
|
raise AssertionError("main should not download a cross-platform archive")
|
|
|
|
original_load_release = MODULE.load_release
|
|
original_download = MODULE.download_url
|
|
original_runtime = MODULE.current_runtime_platform
|
|
original_argv = sys.argv
|
|
try:
|
|
MODULE.load_release = lambda _repo, _tag: release
|
|
MODULE.download_url = fake_download
|
|
MODULE.current_runtime_platform = lambda: "darwin/arm64"
|
|
sys.argv = [str(MODULE_PATH), "--tag", "dev-latest"]
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertEqual(MODULE.main(), 0)
|
|
finally:
|
|
MODULE.load_release = original_load_release
|
|
MODULE.download_url = original_download
|
|
MODULE.current_runtime_platform = original_runtime
|
|
sys.argv = original_argv
|
|
|
|
self.assertEqual(
|
|
downloaded_urls,
|
|
["https://example.test/manifest", "https://example.test/index"],
|
|
)
|
|
|
|
def test_validate_release_assets_reports_sha_mismatch(self):
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": "clickhouse-driver-agent-darwin-arm64",
|
|
"browser_download_url": "https://example.test/clickhouse-driver-agent-darwin-arm64",
|
|
}
|
|
]
|
|
}
|
|
manifest = {
|
|
"assets": {
|
|
"clickhouse-driver-agent-darwin-arm64": {
|
|
"revision": "src-expected",
|
|
"sha256": "deadbeef",
|
|
}
|
|
}
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory(prefix="gonavi-validate-release-assets-") as tmp:
|
|
payload_path = pathlib.Path(tmp) / "clickhouse-driver-agent-darwin-arm64"
|
|
payload_path.write_bytes(b"test-binary")
|
|
|
|
def fake_download(url, destination):
|
|
destination.write_bytes(payload_path.read_bytes())
|
|
|
|
original_download = MODULE.download_url
|
|
original_probe = MODULE.probe_metadata_revision
|
|
try:
|
|
MODULE.download_url = fake_download
|
|
MODULE.probe_metadata_revision = lambda _path: "src-expected"
|
|
mismatches, skipped = MODULE.validate_release_assets(release, manifest)
|
|
finally:
|
|
MODULE.download_url = original_download
|
|
MODULE.probe_metadata_revision = original_probe
|
|
|
|
self.assertEqual(skipped, [])
|
|
self.assertEqual(len(mismatches), 1)
|
|
name, field, actual, expected = mismatches[0]
|
|
self.assertEqual(name, "clickhouse-driver-agent-darwin-arm64")
|
|
self.assertEqual(field, "sha256")
|
|
self.assertEqual(actual, hashlib.sha256(b"test-binary").hexdigest())
|
|
self.assertEqual(expected, "deadbeef")
|
|
|
|
def test_validate_release_assets_reports_revision_mismatch(self):
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": "clickhouse-driver-agent-darwin-arm64",
|
|
"browser_download_url": "https://example.test/clickhouse-driver-agent-darwin-arm64",
|
|
}
|
|
]
|
|
}
|
|
payload = b"test-binary"
|
|
manifest = {
|
|
"assets": {
|
|
"clickhouse-driver-agent-darwin-arm64": {
|
|
"platform": "darwin/arm64",
|
|
"revision": "src-expected",
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
}
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory(prefix="gonavi-validate-release-assets-") as tmp:
|
|
payload_path = pathlib.Path(tmp) / "clickhouse-driver-agent-darwin-arm64"
|
|
payload_path.write_bytes(payload)
|
|
|
|
def fake_download(url, destination):
|
|
destination.write_bytes(payload_path.read_bytes())
|
|
|
|
original_download = MODULE.download_url
|
|
original_probe = MODULE.probe_metadata_revision
|
|
try:
|
|
MODULE.download_url = fake_download
|
|
MODULE.probe_metadata_revision = lambda _path: "src-actual"
|
|
mismatches, skipped = MODULE.validate_release_assets(release, manifest, runtime_platform="darwin/arm64")
|
|
finally:
|
|
MODULE.download_url = original_download
|
|
MODULE.probe_metadata_revision = original_probe
|
|
|
|
self.assertEqual(skipped, [])
|
|
self.assertEqual(mismatches, [("clickhouse-driver-agent-darwin-arm64", "revision", "src-actual", "src-expected")])
|
|
|
|
def test_validate_release_assets_skips_cross_platform_revision_probe(self):
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": "clickhouse-driver-agent-darwin-amd64",
|
|
"browser_download_url": "https://example.test/clickhouse-driver-agent-darwin-amd64",
|
|
}
|
|
]
|
|
}
|
|
payload = b"darwin-binary"
|
|
manifest = {
|
|
"assets": {
|
|
"clickhouse-driver-agent-darwin-amd64": {
|
|
"platform": "darwin/amd64",
|
|
"revision": "src-expected",
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
}
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory(prefix="gonavi-validate-release-assets-") as tmp:
|
|
payload_path = pathlib.Path(tmp) / "clickhouse-driver-agent-darwin-amd64"
|
|
payload_path.write_bytes(payload)
|
|
|
|
def fake_download(url, destination):
|
|
destination.write_bytes(payload_path.read_bytes())
|
|
|
|
def fail_probe(_path):
|
|
raise AssertionError("cross-platform asset should not be executed")
|
|
|
|
original_download = MODULE.download_url
|
|
original_probe = MODULE.probe_metadata_revision
|
|
try:
|
|
MODULE.download_url = fake_download
|
|
MODULE.probe_metadata_revision = fail_probe
|
|
mismatches, skipped = MODULE.validate_release_assets(release, manifest, runtime_platform="linux/amd64")
|
|
finally:
|
|
MODULE.download_url = original_download
|
|
MODULE.probe_metadata_revision = original_probe
|
|
|
|
self.assertEqual(mismatches, [])
|
|
self.assertEqual(skipped, [])
|
|
|
|
def test_validate_release_assets_uses_release_digest_without_download(self):
|
|
payload = b"darwin-binary"
|
|
digest = hashlib.sha256(payload).hexdigest()
|
|
release = {
|
|
"assets": [
|
|
{
|
|
"name": "clickhouse-driver-agent-darwin-amd64",
|
|
"digest": f"sha256:{digest}",
|
|
"browser_download_url": "https://example.test/clickhouse-driver-agent-darwin-amd64",
|
|
}
|
|
]
|
|
}
|
|
manifest = {
|
|
"assets": {
|
|
"clickhouse-driver-agent-darwin-amd64": {
|
|
"platform": "darwin/amd64",
|
|
"revision": "src-expected",
|
|
"sha256": digest,
|
|
}
|
|
}
|
|
}
|
|
|
|
def fail_download(_url, _destination):
|
|
raise AssertionError("release digest should avoid downloading cross-platform assets")
|
|
|
|
original_download = MODULE.download_url
|
|
try:
|
|
MODULE.download_url = fail_download
|
|
mismatches, skipped = MODULE.validate_release_assets(release, manifest, runtime_platform="linux/amd64")
|
|
finally:
|
|
MODULE.download_url = original_download
|
|
|
|
self.assertEqual(mismatches, [])
|
|
self.assertEqual(skipped, [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|