fix(lint): mark new outbound_api strings as translatable=false to res… #31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Static Analysis & Race Detection | |
| on: | |
| push: | |
| branches: [glucodroid] | |
| paths-ignore: | |
| - '**.md' | |
| - 'docs/**' | |
| pull_request: | |
| branches: [glucodroid] | |
| paths-ignore: | |
| - '**.md' | |
| - 'docs/**' | |
| workflow_dispatch: | |
| schedule: | |
| # Run nightly at 3 AM UTC (offset from UX QA at 2 AM) | |
| - cron: '0 3 * * *' | |
| env: | |
| GRADLE_OPTS: -Xmx4g -XX:+HeapDumpOnOutOfMemoryError | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| jobs: | |
| # ── 1. Android Lint ────────────────────────────────────────────────────────── | |
| lint: | |
| name: Android Lint (nogoogle release) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| - uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.gradle/caches | |
| ~/.gradle/wrapper | |
| key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} | |
| restore-keys: ${{ runner.os }}-gradle- | |
| - name: Run lint | |
| run: | | |
| ./gradlew :Common:lintMobileLibre3SiDexNogoogleRelease \ | |
| --no-daemon --warning-mode=all | |
| # abortOnError is false in build.gradle so this always exits 0; | |
| # we parse severity ourselves below. | |
| - name: Summarise lint results | |
| if: always() | |
| run: | | |
| XML=$(find Common/build/reports -name "lint-results-mobileLibre3SiDexNogoogleRelease.xml" | head -1) | |
| if [[ -z "$XML" ]]; then | |
| echo "No lint XML found." >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| fi | |
| python3 - "$XML" << 'PYEOF' | |
| import xml.etree.ElementTree as ET, sys, os, collections | |
| tree = ET.parse(sys.argv[1]) | |
| issues = tree.getroot().findall("issue") | |
| by_sev = collections.defaultdict(list) | |
| for iss in issues: | |
| by_sev[iss.get("severity","?")].append(iss) | |
| summary = os.environ.get("GITHUB_STEP_SUMMARY","") | |
| lines = ["## Android Lint Results\n"] | |
| lines.append(f"| Severity | Count |") | |
| lines.append(f"|---|---|") | |
| for sev in ("Error","Warning","Information","Hint"): | |
| if sev in by_sev: | |
| lines.append(f"| {sev} | {len(by_sev[sev])} |") | |
| lines.append("") | |
| # Thread-safety focused categories | |
| RACE_CATS = {"VisibleForTests","WrongThread","NotThreadSafe", | |
| "SynchronizedOrAtomicFieldUpdater","ThreadSafe"} | |
| race_hits = [i for i in issues if i.get("id","") in RACE_CATS] | |
| if race_hits: | |
| lines.append("### Thread-Safety Findings\n") | |
| for iss in race_hits[:30]: | |
| loc = iss.find("location") | |
| f = os.path.basename(loc.get("file","?")) if loc is not None else "?" | |
| l = loc.get("line","?") if loc is not None else "?" | |
| msg = iss.get("message","")[:160] | |
| sev = iss.get("severity","?") | |
| lines.append(f"- **[{sev}]** `{f}:{l}` — {msg}") | |
| if len(race_hits) > 30: | |
| lines.append(f"- … and {len(race_hits)-30} more") | |
| lines.append("") | |
| # Top-10 error categories | |
| errors = by_sev.get("Error",[]) | |
| if errors: | |
| lines.append("### Errors (top categories)\n") | |
| cat_counts = collections.Counter(i.get("category","?") for i in errors) | |
| for cat, cnt in cat_counts.most_common(10): | |
| lines.append(f"- **{cat}**: {cnt}") | |
| lines.append("") | |
| if summary: | |
| with open(summary, "a") as fh: | |
| fh.write("\n".join(lines) + "\n") | |
| else: | |
| print("\n".join(lines)) | |
| # Exit 1 if any Error-severity issues exist | |
| if by_sev.get("Error"): | |
| print(f"::error::{len(by_sev['Error'])} lint error(s) found — see report artifact") | |
| sys.exit(1) | |
| PYEOF | |
| - name: Upload lint reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: lint-reports | |
| path: | | |
| Common/build/reports/lint-results-mobileLibre3SiDexNogoogleRelease.xml | |
| Common/build/reports/lint-results-mobileLibre3SiDexNogoogleRelease.html | |
| retention-days: 30 | |
| # ── 2. Race / Thread-Safety Detection ──────────────────────────────────────── | |
| race-detection: | |
| name: Race & Thread-Safety Detection (API ${{ matrix.api-level }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| strategy: | |
| matrix: | |
| api-level: [31, 34] | |
| fail-fast: false | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| - uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.gradle/caches | |
| ~/.gradle/wrapper | |
| key: ${{ runner.os }}-gradle-race-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} | |
| restore-keys: ${{ runner.os }}-gradle- | |
| - name: Enable KVM | |
| run: | | |
| echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ | |
| | sudo tee /etc/udev/rules.d/99-kvm4all.rules | |
| sudo udevadm control --reload-rules | |
| sudo udevadm trigger --name-match=kvm | |
| ls -l /dev/kvm | |
| - name: Build debug + test APKs (nogoogle) | |
| run: | | |
| ./gradlew \ | |
| :Common:assembleMobileLibre3SiDexNogoogleDebug \ | |
| :Common:assembleMobileLibre3SiDexNogoogleDebugAndroidTest \ | |
| --no-daemon --warning-mode=all \ | |
| -Pno_x86 -Pno_x86_64 | |
| - name: Run on emulator — capture thread violations | |
| uses: reactivecircus/android-emulator-runner@v2 | |
| with: | |
| api-level: ${{ matrix.api-level }} | |
| arch: x86_64 | |
| profile: Nexus 6P | |
| force-avd-creation: false | |
| emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none | |
| disable-animations: true | |
| disk-size: 6000M | |
| heap-size: 512M | |
| script: bash .github/scripts/race-detection.sh | |
| - name: Analyse logcat for race / thread violations | |
| if: always() | |
| run: | | |
| python3 - << 'PYEOF' | |
| import re, sys, os | |
| logcat = open("logcat-full.log").read() if os.path.exists("logcat-full.log") else "" | |
| instr = open("instrumentation.log").read() if os.path.exists("instrumentation.log") else "" | |
| PATTERNS = { | |
| "StrictMode violation": r"StrictMode policy violation", | |
| "CalledFromWrongThread": r"CalledFromWrongThreadException", | |
| "ConcurrentModification": r"ConcurrentModificationException", | |
| "DeadObjectException": r"DeadObjectException", | |
| "Main-thread IO": r"StrictMode.*disk (read|write)", | |
| "ANR": r"Input dispatching timed out|ANR in", | |
| "Looper not prepared": r"Can.t create handler inside thread.*Looper.prepare", | |
| "IllegalStateException (thread)": r"IllegalStateException.*thread|thread.*IllegalStateException", | |
| "Test failure": r"FAILURES!!|FAILED \(", | |
| } | |
| findings = {} | |
| for label, pattern in PATTERNS.items(): | |
| hits = re.findall(f"(?m)^.*{pattern}.*$", logcat + "\n" + instr) | |
| if hits: | |
| findings[label] = hits | |
| summary_path = os.environ.get("GITHUB_STEP_SUMMARY","") | |
| lines = [f"## Race & Thread-Safety Scan — API ${{ matrix.api-level }}\n"] | |
| if findings: | |
| lines.append(f"**{len(findings)} category(ies) with findings:**\n") | |
| for label, hits in findings.items(): | |
| lines.append(f"### {label} ({len(hits)} occurrence(s))") | |
| for h in hits[:5]: | |
| lines.append(f"```\n{h.strip()[:200]}\n```") | |
| if len(hits) > 5: | |
| lines.append(f"*… and {len(hits)-5} more — see logcat artifact*") | |
| lines.append("") | |
| else: | |
| lines.append("No thread-safety violations or race indicators detected.\n") | |
| # Note about StrictMode being commented out in Applic.java | |
| lines.append("> **Note:** `StrictMode` is commented out in `Applic.java:792`.") | |
| lines.append("> Uncommenting `startstrictmode()` would surface additional violations here.\n") | |
| output = "\n".join(lines) | |
| if summary_path: | |
| with open(summary_path, "a") as fh: | |
| fh.write(output + "\n") | |
| print(output) | |
| if findings: | |
| critical = {k for k in findings if k not in ("Test failure",)} | |
| if critical: | |
| print(f"::warning::Thread-safety findings in {len(critical)} category(ies) — see step summary") | |
| PYEOF | |
| - name: Upload race-detection artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: race-detection-api${{ matrix.api-level }} | |
| path: | | |
| logcat-full.log | |
| instrumentation.log | |
| batterystats.txt | |
| retention-days: 14 | |
| # ── 3. Redroid Kernel Probe (ARM64 runner) ─────────────────────────────────── | |
| # Checks whether GitHub's ubuntu-24.04-arm runners expose the binder and | |
| # ashmem kernel modules required to run Redroid natively (no QEMU layer). | |
| # This job never fails CI — it only reports findings so we know whether | |
| # a Redroid-on-Actions path is viable. | |
| redroid-kernel-probe: | |
| name: Redroid Kernel Probe (ARM64) | |
| runs-on: ubuntu-24.04-arm | |
| timeout-minutes: 5 | |
| continue-on-error: true | |
| steps: | |
| - name: Check kernel modules and Docker capabilities | |
| run: | | |
| echo "## Redroid Kernel Probe — \$(uname -r) [\$(uname -m)]" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| check() { | |
| local label="$1" result="$2" detail="$3" | |
| if [[ "$result" == "ok" ]]; then | |
| echo "| ✅ | $label | \`$detail\` |" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "| ❌ | $label | $detail |" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| } | |
| echo "| | Check | Detail |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|---|---|---|" >> "$GITHUB_STEP_SUMMARY" | |
| # binder | |
| binder=$(grep binder /proc/filesystems 2>/dev/null | awk '{print $2}') | |
| [[ -n "$binder" ]] && check "binder in /proc/filesystems" ok "$binder" \ | |
| || check "binder in /proc/filesystems" fail "not found" | |
| # ashmem | |
| ashmem=$(grep ashmem /proc/misc 2>/dev/null | awk '{print $2}') | |
| [[ -n "$ashmem" ]] && check "ashmem in /proc/misc" ok "$ashmem" \ | |
| || check "ashmem in /proc/misc" fail "not found" | |
| # /dev/binder device node | |
| [[ -e /dev/binder ]] \ | |
| && check "/dev/binder device node" ok "$(ls -l /dev/binder)" \ | |
| || check "/dev/binder device node" fail "not present" | |
| # /dev/ashmem device node | |
| [[ -e /dev/ashmem ]] \ | |
| && check "/dev/ashmem device node" ok "$(ls -l /dev/ashmem)" \ | |
| || check "/dev/ashmem device node" fail "not present" | |
| # modprobe availability (can we load modules?) | |
| modprobe_out=$(modprobe binder_linux 2>&1 || true) | |
| [[ -z "$modprobe_out" || "$modprobe_out" == *"already loaded"* ]] \ | |
| && check "modprobe binder_linux" ok "loaded or already present" \ | |
| || check "modprobe binder_linux" fail "$modprobe_out" | |
| # Docker privileged mode (required by Redroid) | |
| docker_ok=$(docker run --rm --privileged alpine uname -m 2>/dev/null || echo "failed") | |
| [[ "$docker_ok" == "aarch64" ]] \ | |
| && check "Docker privileged containers" ok "aarch64 confirmed" \ | |
| || check "Docker privileged containers" fail "$docker_ok" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "**Kernel:** \$(uname -r) " >> "$GITHUB_STEP_SUMMARY" | |
| echo "**Arch:** \$(uname -m)" >> "$GITHUB_STEP_SUMMARY" | |
| # Print raw details to the log for debugging | |
| echo "=== /proc/filesystems (android-related) ===" | |
| grep -E "binder|ashmem" /proc/filesystems /proc/misc 2>/dev/null || echo "(none)" | |
| echo "=== /dev entries ===" | |
| ls -l /dev/binder /dev/ashmem 2>/dev/null || echo "(none)" | |
| echo "=== kernel modules ===" | |
| lsmod 2>/dev/null | grep -E "binder|ashmem" || echo "(none loaded)" | |
| # ── 4. Gate ─────────────────────────────────────────────────────────────────── | |
| analysis-gate: | |
| name: Analysis Gate | |
| runs-on: ubuntu-latest | |
| needs: [lint, race-detection, redroid-kernel-probe] | |
| if: always() | |
| steps: | |
| - name: Check results | |
| run: | | |
| lint="${{ needs.lint.result }}" | |
| race="${{ needs.race-detection.result }}" | |
| probe="${{ needs.redroid-kernel-probe.result }}" | |
| echo "Lint: $lint" | |
| echo "Race detection: $race" | |
| echo "Redroid kernel probe: $probe (informational only)" | |
| # Race detection uses ::warning:: not exit 1, so failures here are | |
| # genuine infrastructure problems, not finding warnings. | |
| # Probe is continue-on-error so it never blocks the gate. | |
| for r in "$lint" "$race"; do | |
| if [[ "$r" == "failure" ]]; then | |
| echo "One or more analysis jobs failed — check artifacts and step summaries." | |
| exit 1 | |
| fi | |
| done | |
| echo "All analysis jobs completed." |