SHA256
Move workflows into Gitea's workflow directory and use the supplied ubuntu-latest and macos-arm64 labels. Provision the Android SDK, require KVM without changing host permissions, support SHA-256 checkout and Gitea artifact uploads, and isolate scratch output on persistent runners. Skip untrusted fork PRs and keep release builds unsigned because Gitea ignores GitHub environment approval gates. Local workflow lint, repository invariants, ten preflight regressions, eight evidence-verifier tests, inline shell checks, scratch cleanup and SHA-256 fetch checks pass; remote runner execution remains unverified.
66 lines
4.0 KiB
Python
66 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Static release invariants; these checks do not replace platform/runtime tests."""
|
|
import json
|
|
from pathlib import Path
|
|
import plistlib
|
|
import re
|
|
import subprocess
|
|
import xml.etree.ElementTree as ET
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
required = ["README.md", "LICENSE", "THIRD_PARTY_NOTICES.md", "CONTRIBUTING.md", "SECURITY.md",
|
|
"CHANGELOG.md", "docs/architecture.md", "docs/build-android.md", "docs/build-ios.md",
|
|
"docs/server-compatibility.md", "docs/release-process.md", "docs/validation.md"]
|
|
for name in required:
|
|
assert (ROOT / name).is_file(), f"Missing {name}"
|
|
|
|
paths = subprocess.check_output(["git", "ls-files", "--cached", "--others", "--exclude-standard"], cwd=ROOT, text=True).splitlines()
|
|
for name in paths:
|
|
path = ROOT / name
|
|
if not path.is_file():
|
|
continue
|
|
assert not any(part in {"node_modules", "build", ".gradle", "DerivedData"} for part in path.relative_to(ROOT).parts), name
|
|
assert path.suffix not in {".jks", ".keystore", ".p12", ".mobileprovision", ".apk", ".aab"}, name
|
|
if path.suffix == ".xml":
|
|
ET.parse(path)
|
|
if path.suffix == ".xcprivacy":
|
|
with path.open("rb") as source:
|
|
assert plistlib.load(source)["NSPrivacyTracking"] is False
|
|
if path.suffix == ".json":
|
|
json.loads(path.read_text())
|
|
if path.suffix in {".swift", ".kt", ".kts", ".py", ".md", ".yml", ".xml", ".properties", ".mjs", ".sh"}:
|
|
text = path.read_text()
|
|
assert not re.search(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----", text), f"Private key in {name}"
|
|
if path.suffix == ".md":
|
|
for target in re.findall(r"\]\(([^)]+)\)", text):
|
|
if ":" not in target and not target.startswith("#"):
|
|
assert (path.parent / target.split("#")[0]).exists(), f"Broken link in {name}: {target}"
|
|
|
|
android = "{http://schemas.android.com/apk/res/android}"
|
|
manifest = ET.parse(ROOT / "android/app/src/main/AndroidManifest.xml").getroot()
|
|
app = manifest.find("application")
|
|
assert app.get(android + "usesCleartextTraffic") == "false"
|
|
assert app.get(android + "allowBackup") == "false"
|
|
assert app.get(android + "debuggable") != "true"
|
|
assert len(manifest.findall("application/activity/intent-filter")) == 1, "Unreviewed deep-link entry point"
|
|
network = ET.parse(ROOT / "android/app/src/main/res/xml/network_security_config.xml").getroot()
|
|
assert network.find("base-config").get("cleartextTrafficPermitted") == "false"
|
|
assert [node.get("src") for node in network.findall(".//certificates")] == ["system"]
|
|
assert "distributionSha256Sum=" in (ROOT / "android/gradle/wrapper/gradle-wrapper.properties").read_text()
|
|
workflows = sorted((ROOT / ".gitea/workflows").glob("*.yml"))
|
|
assert {path.name for path in workflows} == {"android.yml", "ios.yml", "release.yml"}, "Missing Gitea workflows"
|
|
for workflow in workflows:
|
|
for action in re.findall(r"uses:\s*([^\s#]+)", workflow.read_text()):
|
|
assert re.fullmatch(r"https://(?:github.com|gitea.com)/[\w.-]+/[\w./-]+@[0-9a-f]{40}", action), f"Unpinned action: {action}"
|
|
expected_runner = "macos-arm64" if workflow.name == "ios.yml" else "ubuntu-latest"
|
|
labels = re.findall(r"runs-on:\s*([^\s#]+)", workflow.read_text())
|
|
assert labels and all(label == expected_runner for label in labels), f"Wrong runner in {workflow.name}"
|
|
checkouts = workflow.read_text().count("https://github.com/actions/checkout@")
|
|
assert checkouts == (2 if workflow.name == "android.yml" else 1), "Each job needs checkout"
|
|
assert workflow.read_text().count("GIT_DEFAULT_HASH: sha256") == checkouts, "SHA-256 checkout must be explicit"
|
|
assert workflow.read_text().count("persist-credentials: false") == checkouts, "Do not retain CI checkout tokens"
|
|
release = (ROOT / ".gitea/workflows/release.yml").read_text()
|
|
assert "secrets." not in release and "environment:" not in release, "Gitea signing needs a separately reviewed approval mechanism"
|
|
assert "NSAllowsArbitraryLoads" not in (ROOT / "ios/project.yml").read_text()
|
|
print(f"Repository invariants: {len(paths)} source/documentation paths checked.")
|