ci: target Gitea Ubuntu and native Apple Silicon runners
iOS validation / simulator (push) Failing after 3s
Android validation / emulator (35) (push) Failing after 9s
Android validation / emulator (37.0) (push) Failing after 9s
Android validation / build (push) Successful in 3m40s

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.
This commit is contained in:
Yuvomi Mobile Contributors committed 2026-10-21 07:40:19 +02:00
1 parent 2d12491516
commit 11bf4ca620
16 files changed
+442 -192

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Validate prerequisites without installing packages or modifying the runner host.
set -euo pipefail
fail() { printf 'Runner prerequisite: %s\n' "$*" >&2; exit 1; }
for tool in git curl unzip python3; do
command -v "$tool" >/dev/null || fail "install $tool in the job environment"
done
case "${1:-}" in
android|emulator)
[[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] || fail 'ubuntu-latest must provide Linux x86_64'
if [[ "$1" == emulator ]]; then
[[ -c /dev/kvm && -r /dev/kvm && -w /dev/kvm ]] || fail 'expose /dev/kvm to the job and grant the runner user read/write access (nested virtualization required on VMs)'
fi
;;
ios)
[[ "$(uname -s)" == Darwin && "$(uname -m)" == arm64 ]] || fail 'macos-arm64 must execute natively on the Apple Silicon host'
command -v xcodebuild >/dev/null || fail 'install full Xcode and select it with xcode-select'
xcodebuild -version
xcrun --find swift >/dev/null
xcrun simctl list devices available >/dev/null
;;
*) fail 'usage: bash scripts/check-ci-runner.sh android|emulator|ios' ;;
esac
+13 -2
View File
@@ -47,8 +47,19 @@ network = ET.parse(ROOT / "android/app/src/main/res/xml/network_security_config.
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()
for workflow in (ROOT / ".github/workflows").glob("*.yml"):
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"[\w.-]+/[\w./-]+@[0-9a-f]{40}", action), f"Unpinned action: {action}"
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.")
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""Lint Gitea workflows using actionlint's GitHub-compatible grammar.
Only the forge-specific absolute action URL syntax is normalized for actionlint;
the real URLs/pins and runner assignments are checked by check-repository.py.
"""
from pathlib import Path
import re
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parents[1]
actionlint = sys.argv[1] if len(sys.argv) > 1 else "actionlint"
base = Path("/tmp/opencode") if Path("/tmp/opencode").exists() else None
with tempfile.TemporaryDirectory(prefix="yuvomi-workflow-lint-", dir=base) as temporary:
config = Path(temporary) / "actionlint.yaml"
config.write_text("self-hosted-runner:\n labels: [macos-arm64]\n")
workflows = sorted((ROOT / ".gitea/workflows").glob("*.yml"))
if not workflows:
raise SystemExit("No Gitea workflows found")
for path in workflows:
normalized = re.sub(r"(uses:\s*)https://(?:github.com|gitea.com)/", r"\1", path.read_text())
subprocess.run([actionlint, "-config-file", str(config), "-stdin-filename", str(path), "-"],
input=normalized, text=True, check=True, cwd=ROOT)
print(f"actionlint: {len(workflows)} Gitea workflows passed (action URL syntax normalized).")
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Test runner prerequisite decisions with fake tools, not actual Apple SDKs."""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
SCRIPT = Path(__file__).with_name("check-ci-runner.sh")
class RunnerPrerequisiteTests(unittest.TestCase):
def run_check(self, mode, system="Linux", architecture="x86_64", missing=None, broken_xcode=False):
with tempfile.TemporaryDirectory(prefix="yuvomi-runner-test-") as temporary:
directory = Path(temporary)
for name in ["git", "curl", "unzip", "python3", "xcodebuild", "xcrun"]:
if name == missing:
continue
path = directory / name
status = 1 if name == "xcodebuild" and broken_xcode else 0
path.write_text(f"#!/bin/sh\nexit {status}\n")
path.chmod(0o700)
uname = directory / "uname"
uname.write_text('#!/bin/sh\ncase "$1" in -s) echo "$TEST_SYSTEM";; -m) echo "$TEST_ARCH";; esac\n')
uname.chmod(0o700)
return subprocess.run(["/bin/bash", str(SCRIPT), mode], capture_output=True, text=True,
env={**os.environ, "PATH": temporary, "TEST_SYSTEM": system,
"TEST_ARCH": architecture})
def test_android_accepts_linux_x64(self):
self.assertEqual(self.run_check("android").returncode, 0)
def test_android_rejects_arm(self):
self.assertNotEqual(self.run_check("android", architecture="aarch64").returncode, 0)
def test_android_rejects_macos(self):
self.assertNotEqual(self.run_check("android", system="Darwin").returncode, 0)
def test_ios_accepts_native_arm_with_tools(self):
self.assertEqual(self.run_check("ios", system="Darwin", architecture="arm64").returncode, 0)
def test_ios_rejects_rosetta_or_intel(self):
self.assertNotEqual(self.run_check("ios", system="Darwin").returncode, 0)
def test_ios_rejects_linux(self):
self.assertNotEqual(self.run_check("ios", architecture="arm64").returncode, 0)
def test_missing_python_is_actionable(self):
result = self.run_check("android", missing="python3")
self.assertNotEqual(result.returncode, 0)
self.assertIn("install python3", result.stderr)
def test_missing_full_xcode_fails(self):
result = self.run_check("ios", system="Darwin", architecture="arm64", missing="xcodebuild")
self.assertNotEqual(result.returncode, 0)
self.assertIn("full Xcode", result.stderr)
def test_broken_xcode_fails(self):
self.assertNotEqual(self.run_check("ios", system="Darwin", architecture="arm64",
broken_xcode=True).returncode, 0)
def test_unknown_mode_fails(self):
self.assertNotEqual(self.run_check("other").returncode, 0)
if __name__ == "__main__":
unittest.main()