Skip to content

Commit 4474cd2

Browse files
Merge pull request #20 from appdevforall/fix/m15-build-binary-sync-fallback
fix(build): cache-first + token-fallback for native artifact sync (M15)
2 parents 3bf4f1b + 2eb882d commit 4474cd2

3 files changed

Lines changed: 45 additions & 14 deletions

File tree

‎.github/workflows/android-sanity-check.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ jobs:
2020
name: Lint & Compile Check
2121
runs-on: ubuntu-latest
2222

23+
# M15: expose a token to the gradle steps so the native-artifact sync can fall
24+
# back to an authenticated GitHub API call when the unauthenticated one is
25+
# rate-limited (HTTP 403). secrets.GITHUB_TOKEN is provided automatically.
26+
env:
27+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28+
2329
defaults:
2430
run:
2531
working-directory: ./controller

‎controller/app/build.gradle‎

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -224,19 +224,10 @@ task syncNativeArtifacts {
224224
def targetTag = versionFile.text.trim()
225225
println " -> Target tag pinned to: ${targetTag}"
226226

227-
// 2. Fetch the exact Release directly from GitHub API
228-
def apiUrl = new URL("https://api.github.com/repos/${repoName}/releases/tags/${targetTag}")
229-
def connection = (HttpURLConnection) apiUrl.openConnection()
230-
connection.setRequestProperty("Accept", "application/vnd.github.v3+json")
231-
232-
if (connection.responseCode != 200) {
233-
throw new GradleException(">> [IIAB Hook] GitHub API error: Tag ${targetTag} not found (HTTP ${connection.responseCode})")
234-
}
235-
227+
// 2. Smart caching FIRST -- never touch the network if the pinned
228+
// binaries are already present and verified locally (M15: do not
229+
// couple every build to the GitHub API).
236230
def slurper = new JsonSlurper()
237-
def targetRelease = slurper.parse(connection.inputStream)
238-
239-
// 3. Smart Caching: Skip if we already have this exact release and all files exist!
240231
def currentTag = tagTrackerFile.exists() ? tagTrackerFile.text.trim() : ""
241232

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

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

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

258+
// 3. Fetch release metadata. Try UNAUTHENTICATED first so a fork
259+
// without a token still builds; only if that fails (e.g. HTTP 403
260+
// rate-limit) retry WITH a token when one is available in the
261+
// environment. Independent builds never need to define a token.
262+
def openReleaseConn = { String authToken ->
263+
def relUrl = new URL("https://api.github.com/repos/${repoName}/releases/tags/${targetTag}")
264+
def c = (HttpURLConnection) relUrl.openConnection()
265+
c.setRequestProperty("Accept", "application/vnd.github.v3+json")
266+
c.setRequestProperty("User-Agent", "iiab-android-build")
267+
if (authToken?.trim()) c.setRequestProperty("Authorization", "Bearer ${authToken.trim()}")
268+
return c
269+
}
270+
271+
def connection = openReleaseConn(null) // attempt 1: unauthenticated
272+
int code = connection.responseCode
273+
if (code != 200) {
274+
def envToken = System.getenv("GITHUB_TOKEN") ?: System.getenv("GH_TOKEN")
275+
if (envToken?.trim()) {
276+
println " -> Unauthenticated API call returned HTTP ${code}; retrying with token (first attempt likely rate-limited)..."
277+
connection = openReleaseConn(envToken) // attempt 2: authenticated fallback
278+
code = connection.responseCode
279+
}
280+
}
281+
if (code != 200) {
282+
throw new GradleException(">> [IIAB Hook] GitHub API error for tag ${targetTag}: HTTP ${code}. " +
283+
"This is usually GitHub API rate-limiting; set GITHUB_TOKEN (CI provides one automatically) to raise the limit.")
284+
}
285+
286+
def targetRelease = slurper.parse(connection.inputStream)
287+
267288
// 4. Find the zip asset URL in the targeted release
268289
def zipAsset = targetRelease.assets.find { it.name == 'termux-binaries-latest.zip' }
269290
if (zipAsset == null) {

‎controller/docs/TECH_DEBT_PLAN.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ _Last updated: 2026-06-17. Tracks remediation work against the findings below. I
5757
- 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.
5858
- 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.
5959

60+
**M15 — Build coupled to network for native artifacts: FIXED** (PR `fix/m15-build-binary-sync-fallback`)
61+
- `: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.
62+
- 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*.
63+
6064
**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**.
6165
## 1. Executive summary
6266

0 commit comments

Comments
 (0)