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
6 changes: 6 additions & 0 deletions .github/workflows/android-sanity-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ jobs:
name: Lint & Compile Check
runs-on: ubuntu-latest

# M15: expose a token to the gradle steps so the native-artifact sync can fall
# back to an authenticated GitHub API call when the unauthenticated one is
# rate-limited (HTTP 403). secrets.GITHUB_TOKEN is provided automatically.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

defaults:
run:
working-directory: ./controller
Expand Down
49 changes: 35 additions & 14 deletions controller/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -224,19 +224,10 @@ task syncNativeArtifacts {
def targetTag = versionFile.text.trim()
println " -> Target tag pinned to: ${targetTag}"

// 2. Fetch the exact Release directly from GitHub API
def apiUrl = new URL("https://api.github.com/repos/${repoName}/releases/tags/${targetTag}")
def connection = (HttpURLConnection) apiUrl.openConnection()
connection.setRequestProperty("Accept", "application/vnd.github.v3+json")

if (connection.responseCode != 200) {
throw new GradleException(">> [IIAB Hook] GitHub API error: Tag ${targetTag} not found (HTTP ${connection.responseCode})")
}

// 2. Smart caching FIRST -- never touch the network if the pinned
// binaries are already present and verified locally (M15: do not
// couple every build to the GitHub API).
def slurper = new JsonSlurper()
def targetRelease = slurper.parse(connection.inputStream)

// 3. Smart Caching: Skip if we already have this exact release and all files exist!
def currentTag = tagTrackerFile.exists() ? tagTrackerFile.text.trim() : ""

// List all critical binaries that must exist
Expand All @@ -257,13 +248,43 @@ task syncNativeArtifacts {

// We also verify that the manifest exists in order to be able to audit.
def manifestFile = new File(assetsDir, "ninja_manifest.json")
def isCached = (currentTag == targetRelease.tag_name && allBinariesExist && manifestFile.exists())
def isCached = (currentTag == targetTag && allBinariesExist && manifestFile.exists())

if (isCached) {
println " -> [CACHED] Artifacts are up to date (${currentTag}) and physically present. Skipping download."
println " -> [CACHED] Artifacts are up to date (${currentTag}) and physically present. Skipping API + download."
} else {
println " -> Cache miss, missing physical files, or missing manifest. Proceeding with download..."

// 3. Fetch release metadata. Try UNAUTHENTICATED first so a fork
// without a token still builds; only if that fails (e.g. HTTP 403
// rate-limit) retry WITH a token when one is available in the
// environment. Independent builds never need to define a token.
def openReleaseConn = { String authToken ->
def relUrl = new URL("https://api.github.com/repos/${repoName}/releases/tags/${targetTag}")
def c = (HttpURLConnection) relUrl.openConnection()
c.setRequestProperty("Accept", "application/vnd.github.v3+json")
c.setRequestProperty("User-Agent", "iiab-android-build")
if (authToken?.trim()) c.setRequestProperty("Authorization", "Bearer ${authToken.trim()}")
return c
}

def connection = openReleaseConn(null) // attempt 1: unauthenticated
int code = connection.responseCode
if (code != 200) {
def envToken = System.getenv("GITHUB_TOKEN") ?: System.getenv("GH_TOKEN")
if (envToken?.trim()) {
println " -> Unauthenticated API call returned HTTP ${code}; retrying with token (first attempt likely rate-limited)..."
connection = openReleaseConn(envToken) // attempt 2: authenticated fallback
code = connection.responseCode
}
}
if (code != 200) {
throw new GradleException(">> [IIAB Hook] GitHub API error for tag ${targetTag}: HTTP ${code}. " +
"This is usually GitHub API rate-limiting; set GITHUB_TOKEN (CI provides one automatically) to raise the limit.")
}

def targetRelease = slurper.parse(connection.inputStream)

// 4. Find the zip asset URL in the targeted release
def zipAsset = targetRelease.assets.find { it.name == 'termux-binaries-latest.zip' }
if (zipAsset == null) {
Expand Down
4 changes: 4 additions & 0 deletions controller/docs/TECH_DEBT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ _Last updated: 2026-06-17. Tracks remediation work against the findings below. I
- New shared `util/ProcessRunner.run(cmd)`: `redirectErrorStream(true)` + full drain + returns `{exitCode, output}`, so a single read cannot deadlock and callers can log/handle failures.
- Migrated the raw `exec().waitFor()` sites (backup, `chmod -R`, the three `rm -rf` wipes); empty catches now log. Left the extraction path (already drains stderr) and the `getprop` read (reads stdout) as-is. No new unit test (process glue, not pure logic — fragile to run a shell in unit tests on a Windows dev box); verified by inspection + CI compile.

**M15 — Build coupled to network for native artifacts: FIXED** (PR `fix/m15-build-binary-sync-fallback`)
- `:app:syncNativeArtifacts` (`preBuild` dependency) called the GitHub API **unauthenticated on every build**; since `jniLibs/*.so` are gitignored, CI always downloads and hit GitHub's 60/hr unauthenticated limit -> intermittent **HTTP 403** ("tag not found"), failing `assembleDebug` on unrelated PRs.
- Fix: (1) **cache-first** — check the local tracker/binaries/manifest before any network call, so builds with artifacts present skip the API entirely; (2) **fallback auth** — fetch release metadata UNAUTHENTICATED first (so forks without a token still build), and only on failure retry with `GITHUB_TOKEN`/`GH_TOKEN` from the env; (3) clearer rate-limit error. CI passes `secrets.GITHUB_TOKEN` to the gradle steps (job-level env). No token is ever *required*.

**Phase 1 — Security hardening: IN PROGRESS.** Done so far: **S1** (PR #9), **M4**, **S3** (PR #10), **D6** (PR #12), **D2** (PR #13), **D12**. Remaining: **D11**, **S4**, **F15**.
## 1. Executive summary

Expand Down
Loading