Merge pull request #105 from okhsunrog/ci/speed-up-gradle-jobs

ci: speed up gradle jobs + add ruff for python scripts
This commit is contained in:
Danila Gornushko 2026-04-27 00:01:36 +03:00 committed by GitHub
commit 7fd63f5e22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 120 additions and 50 deletions

View file

@ -38,6 +38,39 @@ jobs:
- name: Mark workspace safe
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
# Python lint + format (ruff). Fast (~100 ms on 1800 LoC) so it
# runs first — fails before the slow Rust/Gradle steps.
- name: ruff format
uses: astral-sh/ruff-action@v4.0.0
with:
args: format --check
- name: ruff check
uses: astral-sh/ruff-action@v4.0.0
with:
args: check
# Cache cargo deps + target dirs for clippy/test. Same key shape as
# zygisk + lsposed jobs — when those run on the same Cargo.lock the
# restore-keys fallback shares warm artifacts across jobs.
- name: Cache cargo
uses: actions/cache@v5
with:
path: |
/usr/local/cargo/registry
/usr/local/cargo/git
zygisk/target
lsposed/native/target
key: cargo-${{ runner.os }}-lint-${{ hashFiles('zygisk/Cargo.lock', 'lsposed/native/Cargo.lock') }}
restore-keys: cargo-${{ runner.os }}-lint-
# Gradle cache (deps + configuration cache + wrapper). cache-read-only
# on PRs so only main pushes write — keeps the cache from churning on
# every PR's branch-scoped key.
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: ${{ github.event_name == 'pull_request' }}
# Codegen
- name: Verify generated iface lists are up to date
run: |
@ -74,17 +107,12 @@ jobs:
# Kotlin
- name: ktlint
run: ktlint "lsposed/**/*.kt"
# Gobley's cargo plugin reads ANDROID_NDK_ROOT (not _HOME) to find the
# NDK at gradle configure time. The CI image only sets _HOME; export
# _ROOT here until the next image rebuild bakes it in.
- name: Android lint
run: |
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
cd lsposed && ./gradlew --no-daemon :app:lint
- name: Kotlin unit tests
run: |
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
cd lsposed && ./gradlew --no-daemon :app:testDebugUnitTest
# Single Gradle invocation: lint + tests share one configuration
# phase + warm daemon. Configures Gobley's cargo plugin once instead
# of twice. ANDROID_NDK_ROOT is baked into the CI image
# (Dockerfile ENV), no manual export needed.
- name: Android lint + Kotlin unit tests
run: cd lsposed && ./gradlew :app:lint :app:testDebugUnitTest
kmod:
runs-on: ubuntu-latest
@ -191,6 +219,13 @@ jobs:
key: cargo-${{ runner.os }}-lsposed-${{ hashFiles('lsposed/native/Cargo.lock') }}
restore-keys: cargo-${{ runner.os }}-lsposed-
# Gradle cache (deps + configuration cache + wrapper). cache-read-only
# on PRs so only main pushes write the cache.
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: ${{ github.event_name == 'pull_request' }}
- name: Set up keystore
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
@ -218,14 +253,23 @@ jobs:
storeFile=$KEYSTORE_PATH
EOF
# Gobley's cargo plugin reads ANDROID_NDK_ROOT (not _HOME) to find the
# NDK at gradle configure time. The CI image only sets _HOME; export
# _ROOT here until the next image rebuild bakes it in.
# Release tags get the full assembleRelease (R8/ProGuard, signed APK
# ready for the GitHub release). PRs and main pushes get assembleDebug
# — same code paths exercised, no R8 step (~1.52 min faster).
# `case` instead of `[[`: container jobs default to /bin/sh (POSIX).
- name: Build APK
run: |
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
cd "$GITHUB_WORKSPACE/lsposed" && ./gradlew --no-daemon assembleRelease
cp app/build/outputs/apk/release/app-release.apk "$GITHUB_WORKSPACE/vpnhide.apk"
cd "$GITHUB_WORKSPACE/lsposed"
case "$GITHUB_REF" in
refs/tags/v*)
./gradlew assembleRelease
cp app/build/outputs/apk/release/app-release.apk "$GITHUB_WORKSPACE/vpnhide.apk"
;;
*)
./gradlew assembleDebug
cp app/build/outputs/apk/debug/app-debug.apk "$GITHUB_WORKSPACE/vpnhide.apk"
;;
esac
- name: Upload artifact
uses: actions/upload-artifact@v7

View file

@ -103,6 +103,10 @@ CI runs the same checks. See [.github/workflows/ci.yml](../.github/workflows/ci.
python3 scripts/codegen-interfaces.py
git diff --quiet # must be clean
# Python (ruff, config in pyproject.toml). uvx runs without installing anything global.
uvx ruff format --check
uvx ruff check
# Rust
cd zygisk && cargo fmt --check && cargo ndk -t arm64-v8a clippy -- -D warnings
cd ../lsposed/native && cargo fmt --check && cargo ndk -t arm64-v8a clippy -- -D warnings
@ -116,8 +120,7 @@ gcc -O2 -Wall -Werror -o /tmp/test_iface_lists kmod/test_iface_lists.c && /tmp/t
# Kotlin
ktlint "lsposed/**/*.kt"
cd lsposed && ./gradlew --no-daemon :app:lint
cd lsposed && ./gradlew --no-daemon :app:testDebugUnitTest
cd lsposed && ./gradlew :app:lint :app:testDebugUnitTest
```
## Build versions

View file

@ -45,8 +45,11 @@ import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
from build_lib import get_build_version, make_zip, version_sort_key # type: ignore[import-not-found]
from build_lib import ( # type: ignore[import-not-found]
get_build_version,
make_zip,
version_sort_key,
)
# Module file name on disk after `make`.
KMOD_KO = "vpnhide_kmod.ko"
@ -128,13 +131,9 @@ def native_build_one(
module_prop = staging / "module.prop"
content = module_prop.read_text(encoding="utf-8")
content = re.sub(
r"^version=.*", f"version=v{build_version}", content, flags=re.MULTILINE
)
content = re.sub(r"^version=.*", f"version=v{build_version}", content, flags=re.MULTILINE)
if re.search(r"^gkiVariant=", content, flags=re.MULTILINE):
content = re.sub(
r"^gkiVariant=.*", f"gkiVariant={kmi}", content, flags=re.MULTILINE
)
content = re.sub(r"^gkiVariant=.*", f"gkiVariant={kmi}", content, flags=re.MULTILINE)
else:
content = content.rstrip() + f"\ngkiVariant={kmi}\n"
update_json_url = (
@ -219,9 +218,7 @@ def find_runtime() -> tuple[str, bool]:
sys.exit(1)
def container_build_one(
runtime: str, is_podman: bool, repo_root: Path, kmi: str
) -> None:
def container_build_one(runtime: str, is_podman: bool, repo_root: Path, kmi: str) -> None:
image = f"ghcr.io/ylarod/ddk-min:{kmi}-{DDK_IMAGE_TAG}"
mount_spec = f"{repo_root}:/work"
cmd = [runtime, "run", "--rm"]
@ -314,10 +311,7 @@ def main() -> int:
parser.add_argument(
"--kdir",
type=str,
help=(
"Kernel source directory (overrides KDIR/KERNEL_SRC). Implies "
"native mode."
),
help=("Kernel source directory (overrides KDIR/KERNEL_SRC). Implies native mode."),
)
parser.add_argument(
"--clang-dir",

21
pyproject.toml Normal file
View file

@ -0,0 +1,21 @@
# Ruff config — applies to every .py in the repo.
# All our scripts are stdlib-only by policy (see scripts/build_lib.py
# header), so ruff is a dev/CI tool here, never a runtime dependency.
[tool.ruff]
target-version = "py312" # CI image (Ubuntu 24.04) ships Python 3.12
line-length = 100
extend-exclude = [
# third-party / vendored — not our code, don't lint
"zygisk/third_party",
# cargo build outputs that may contain stray .py files
"**/target",
# local-only worktrees / agent state, untracked
".claude",
]
[tool.ruff.lint]
# Conservative rule set: pycodestyle (E/W) + pyflakes (F) + isort (I)
# + bugbear foot-guns (B) + pyupgrade (UP) + simplifications (SIM).
# No pylint-style noise, no mypy.
select = ["E", "F", "W", "I", "B", "UP", "SIM"]

View file

@ -117,8 +117,8 @@ def parse_fragment(path: Path) -> dict:
if date_match.start() > en_match.start():
raise ValueError(f"{path.name}: date line must appear before the language sections")
en_body = text[en_match.end():ru_match.start()].strip()
ru_body = text[ru_match.end():].strip()
en_body = text[en_match.end() : ru_match.start()].strip()
ru_body = text[ru_match.end() :].strip()
if not en_body:
raise ValueError(f"{path.name}: empty English section")
if not ru_body:

View file

@ -464,7 +464,7 @@ def emit_rust(rules: list[Rule], tests: list[TestVector]) -> str:
expected = "true" if t.is_vpn else "false"
lines.append(
f" assert_eq!(matches_vpn({rust_byte_lit(t.name)}), {expected}, "
f"\"matches_vpn({t.name!r})\");"
f'"matches_vpn({t.name!r})");'
)
lines.append(" }")
lines.append("}")

View file

@ -114,8 +114,7 @@ def main() -> int:
for past in data.get("history", []):
if past.get("version") == version:
console.print(
f"[red]error:[/red] v{version} already exists in history[]. "
"Pick a new version.",
f"[red]error:[/red] v{version} already exists in history[]. Pick a new version.",
)
return 1
@ -169,11 +168,14 @@ def main() -> int:
console.print()
console.print("[bold]Next steps:[/bold]")
console.print(f" git commit -am \"chore: release v{version}\"")
console.print(f' git commit -am "chore: release v{version}"')
console.print(f" git tag v{version} && git push && git push origin v{version}")
console.print(" # CI builds artifacts and creates a DRAFT release — review on the Releases page, click Publish")
console.print(
" # CI builds artifacts and creates a DRAFT release — "
"review on the Releases page, click Publish"
)
console.print(" ./scripts/update-json.sh")
console.print(f" git commit -am \"chore: update-json for v{version}\"")
console.print(f' git commit -am "chore: update-json for v{version}"')
console.print(" git push")
return 0

View file

@ -22,9 +22,7 @@ def github_token() -> str | None:
if token := os.environ.get("GITHUB_TOKEN"):
return token
try:
out = subprocess.run(
["gh", "auth", "token"], capture_output=True, text=True, check=True
)
out = subprocess.run(["gh", "auth", "token"], capture_output=True, text=True, check=True)
return out.stdout.strip() or None
except (FileNotFoundError, subprocess.CalledProcessError):
return None
@ -34,9 +32,7 @@ headers = {"Accept": "application/vnd.github+json"}
if token := github_token():
headers["Authorization"] = f"Bearer {token}"
resp = httpx.get(
"https://api.github.com/repos/okhsunrog/vpnhide/releases", headers=headers
)
resp = httpx.get("https://api.github.com/repos/okhsunrog/vpnhide/releases", headers=headers)
resp.raise_for_status()
releases = resp.json()
@ -53,7 +49,13 @@ for release in releases:
total = sum(a["download_count"] for a in assets)
grand_total += total
table = Table(title=f"{release['tag_name']} ({total} downloads)", title_style="bold", show_header=False, box=None, padding=(0, 1))
table = Table(
title=f"{release['tag_name']} ({total} downloads)",
title_style="bold",
show_header=False,
box=None,
padding=(0, 1),
)
table.add_column("Asset", style="cyan")
table.add_column("Count", justify="right", style="yellow")
table.add_column("Bar", style="green", no_wrap=True)

View file

@ -18,7 +18,11 @@ import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
from build_lib import get_build_version, make_zip, version_sort_key # type: ignore[import-not-found]
from build_lib import ( # type: ignore[import-not-found]
get_build_version,
make_zip,
version_sort_key,
)
def main() -> int: