Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,10 @@ are system-wide while the test runs: use a disposable Windows runner with admin
rights, not a workstation with an active VPN. A failed baseline loopback check is
an error, not a skip. The configuration suite covers all 24 combinations of route
mode, profile/system permissions and the blocking option, including DNS policy.

Release assets after 0.6.0 contain the installer, the complete application ZIP,
the source ZIP and verification metadata. Both native cores are already in the
application ZIP and installer; no separate core ZIPs are published. The signed
manifest retains `sing-box` and `sing-box-h3` component inventories for older
reboot-update workers, referencing the same application ZIP. Release retries and
changelog baseline selection still recognize the old asset layout through 0.6.0.
32 changes: 19 additions & 13 deletions scripts/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,23 @@ def validate_native_payload(payload):
raise ValueError("Legacy sing-box binaries must not be packaged.")


def payload_components(payload, staging, app_version, runtime_files, legacy_runtime=False):
app_name = f"WindowsMegaProxy-{app_version}-win-x64-app.zip"
app_files = archive(payload, staging / app_name, exclude=("dependencies",) if legacy_runtime else ())
components = {"app": {"version": app_version, "file": app_name, "files": app_files + runtime_files}}
for name, core_version in (("sing-box", CORE_VERSION), ("sing-box-h3", H3_CORE_VERSION)):
prefix = "dependencies/" + name + "/"
if legacy_runtime:
filename = f"WindowsMegaProxy-{app_version}-win-x64-{name}.zip"
files = archive(payload / "dependencies" / name, staging / filename, prefix=prefix)
else:
# Older reboot workers still require both core file inventories.
filename = app_name
files = [entry for entry in app_files if entry["path"].startswith(prefix)]
components[name] = {"version": core_version, "file": filename, "files": files}
return components


def main():
parser = argparse.ArgumentParser()
parser.add_argument("--preview", action="store_true", help="Use a temporary signing key; never publish as a production update.")
Expand Down Expand Up @@ -224,18 +241,7 @@ def main():
with zipfile.ZipFile(wintun) as upstream:
license_file = next(name for name in upstream.namelist() if name.endswith("/LICENSE.txt"))
(payload / "licenses/Wintun-LICENSE.txt").write_bytes(upstream.read(license_file))
components = {}
for component, directory, prefix, exclude, component_version in (
("app", payload, "", ("dependencies",) if legacy_runtime else (), app_version),
("sing-box", payload / "dependencies/sing-box", "dependencies/sing-box/", (), CORE_VERSION),
("sing-box-h3", payload / "dependencies/sing-box-h3", "dependencies/sing-box-h3/", (), H3_CORE_VERSION),
):
filename = f"WindowsMegaProxy-{app_version}-win-x64-{component}.zip"
files = archive(directory, staging / filename, prefix=prefix, exclude=exclude)
if component == "app":
# Installed file inventory includes downloaded runtime files for old boot-update workers.
files.extend(runtime_files)
components[component] = {"version": component_version, "file": filename, "files": files}
components = payload_components(payload, staging, app_version, runtime_files, legacy_runtime)
definitions = {
"AppVersion": app_version, "VersionDirectory": f"{app_version}-{commit[:12]}",
"RepositoryRoot": str(ROOT), "PayloadDirectory": str(payload), "ReleaseDirectory": str(staging),
Expand Down Expand Up @@ -319,7 +325,7 @@ def main():
"authenticodeCertificateSha256": signer.fingerprint,
"runtime": {"version": runtime["version"], "url": runtime_file["url"], "sha512": runtime_file["hash"]} if legacy_runtime else runtime}
signed_manifest(manifest, staging, key)
names = [item["file"] for item in components.values()] + ["update-manifest.json", "update-manifest.sig", "update-public-key.txt"]
names = sorted({item["file"] for item in components.values()}) + ["update-manifest.json", "update-manifest.sig", "update-public-key.txt"]
checksums(staging, names)
for name in (*names, "SHA256SUMS"):
shutil.copy2(staging / name, output / name)
Expand Down
12 changes: 9 additions & 3 deletions scripts/publish_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,16 @@ def release_state(repo, tag):
return state


def package_names(tag):
def package_names(tag, *, bundled=None):
version = tag[1:]
prefix = f"WindowsMegaProxy-{version}"
# Published releases through 0.6.0 have separate core assets and remain immutable.
if bundled is None:
bundled = tuple(map(int, version.split("."))) > (0, 6, 0)
app = prefix + "-win-x64-app.zip"
return {"app": prefix + "-win-x64-app.zip", "installer": prefix + "-win-x64-setup.exe",
"sing-box": prefix + "-win-x64-sing-box.zip", "sing-box-h3": prefix + "-win-x64-sing-box-h3.zip",
"sing-box": app if bundled else prefix + "-win-x64-sing-box.zip",
"sing-box-h3": app if bundled else prefix + "-win-x64-sing-box-h3.zip",
"source": prefix + "-source.zip"}


Expand All @@ -81,7 +86,8 @@ def verify(directory, tag, commit):
if manifest.get("signingKeyId") != hashlib.sha256(public).hexdigest():
raise RuntimeError("Release signing key ID differs from the public key")
Ed25519PublicKey.from_public_bytes(public).verify((directory / "update-manifest.sig").read_bytes(), manifest_bytes)
names = package_names(tag)
components = manifest["components"]
names = package_names(tag, bundled=components["sing-box"]["file"] == components["app"]["file"])
if set(manifest["components"]) != set(names):
raise RuntimeError("Unexpected release components")
for component, name in names.items():
Expand Down
17 changes: 17 additions & 0 deletions scripts/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,23 @@ def test_app_and_core_archives_have_independent_root_relative_paths(self):
with zipfile.ZipFile(root / "core.zip") as output:
self.assertEqual(output.namelist(), ["dependencies/sing-box/libcronet.dll"])

def test_bundled_cores_keep_inventories_without_duplicate_archives(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
payload = root / "payload"
for name in ("sing-box", "sing-box-h3"):
directory = payload / "dependencies" / name
directory.mkdir(parents=True)
(directory / "core.dll").write_bytes(name.encode())
(payload / "app.exe").write_bytes(b"app")
components = package.payload_components(payload, root, "0.6.1", [])
self.assertEqual(len(list(root.glob("*.zip"))), 1)
for name in ("sing-box", "sing-box-h3"):
self.assertEqual(components[name]["file"], components["app"]["file"])
self.assertEqual(components[name]["files"], [entry for entry in components["app"]["files"]
if entry["path"].startswith("dependencies/" + name + "/")])
self.assertEqual(len(components[name]["files"]), 1)

def test_signature_authenticates_exact_manifest_bytes(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
Expand Down
16 changes: 16 additions & 0 deletions scripts/test_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ def row(path, malicious=0):

@patch.dict("os.environ", {"GITHUB_STEP_SUMMARY": ""})
class VirusTotalTests(unittest.TestCase):
def test_compact_manifest_and_legacy_assets(self):
self.assertEqual(len(set(release.package_names("v0.6.0").values())), 5)
self.assertEqual(len(set(release.package_names("v0.6.1").values())), 3)
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
fixture(root)
manifest = json.loads((root / "update-manifest.json").read_bytes())
for name in ("sing-box", "sing-box-h3"):
(root / manifest["components"][name]["file"]).unlink()
manifest["components"][name] = dict(manifest["components"]["app"], version="core-version")
signed_manifest(manifest, root, Ed25519PrivateKey.generate())
names = set(release.package_names(TAG, bundled=True).values()) | release.METADATA
checksums(root, sorted(names - {"SHA256SUMS"}))
self.assertEqual(release.verify(root, TAG, COMMIT), names)


def test_release_limit_counts_malicious_and_suspicious_per_file(self):
for malicious, suspicious in ((0, 0), (10, 0), (9, 1), (0, 10), (10, 1), (11, 0), (0, 11)):
with self.subTest(malicious=malicious, suspicious=suspicious), tempfile.TemporaryDirectory() as temporary:
Expand Down
6 changes: 6 additions & 0 deletions scripts/test_release_notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ def published(tag, date="2026-11-01T12:00:00Z"):


class ReleaseNotesTests(unittest.TestCase):
def test_compact_release_is_a_valid_baseline(self):
release = published("v0.6.1")
self.assertFalse(any("sing-box" in asset["name"] for asset in release["assets"]))
with patch.object(notes, "gh", return_value=json.dumps([[release]])), patch.object(notes.sp, "run", return_value=Mock(returncode=0)):
self.assertEqual(notes.previous_release("0.7.0"), "v0.6.1")

def test_no_releases_is_the_first_release(self):
with patch.object(notes, "gh", return_value="[[]]"), patch.object(notes.sp, "run") as run:
self.assertIsNone(notes.previous_release("0.0.1"))
Expand Down
Loading