From 3a17257ea893e7adaad16bdeb9575f4e5720d01a Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Wed, 20 May 2026 13:18:43 -0700 Subject: [PATCH 1/2] Skip preview NDK; detect all SDK versions from manifest PR #72 fixed the preview platform but the build still failed: the NDK 30.0.14904198 is itself a preview package (uses android-sdk-preview-license), so sdkmanager still demanded the preview license. The check only catches *platform* previews; NDK previews are marked by their license instead. Rather than chase one preview axis at a time, detect all three versions (platform, build-tools, NDK) from the repository manifest under one unified preview filter: skip any package that is obsolete, carries a element, or uses android-sdk-preview-license. This also removes the cmdline-tools download / `sdkmanager --list` path, which was the source of the preview leakage. - Dockerfile: NDK_VERSION 30.0.14904198 -> 29.0.14206865 (latest stable) - check-sdk-updates: single manifest-based detection for all three versions Verified against the live manifest: platform=36.1, build-tools=37.0.0, ndk=29.0.14206865; all preview packages excluded. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/check-sdk-updates.yml | 88 +++++++++++-------------- Dockerfile | 2 +- 2 files changed, 40 insertions(+), 50 deletions(-) diff --git a/.github/workflows/check-sdk-updates.yml b/.github/workflows/check-sdk-updates.yml index 7aa47f1..033b391 100644 --- a/.github/workflows/check-sdk-updates.yml +++ b/.github/workflows/check-sdk-updates.yml @@ -53,67 +53,57 @@ jobs: - name: Check for Android SDK updates id: sdk run: | - # Use SDK_TOOLS version from Dockerfile to download cmdline-tools - SDK_TOOLS=$(grep '^ARG SDK_TOOLS=' Dockerfile | cut -d= -f2) - if ! wget -q "https://dl.google.com/android/repository/commandlinetools-linux-${SDK_TOOLS}.zip" -O /tmp/cmdline-tools.zip; then - echo "Failed to download cmdline-tools" - echo "updated=false" >> $GITHUB_OUTPUT - exit 0 - fi - - unzip -q /tmp/cmdline-tools.zip -d /tmp/android-cmdline - SDK_ROOT=/tmp/android-sdk - mkdir -p "$SDK_ROOT" - - # List available packages - PACKAGES=$(/tmp/android-cmdline/cmdline-tools/bin/sdkmanager --list --sdk_root="$SDK_ROOT" 2>/dev/null || echo "") - if [ -z "$PACKAGES" ]; then - echo "Failed to list SDK packages" - echo "updated=false" >> $GITHUB_OUTPUT - exit 0 - fi - - # Get latest available versions. - # - # Platforms must come from the repository manifest, not `sdkmanager --list`: - # preview platforms such as android-37.0 are tagged channel-0 (stable) but - # carry a element, so they appear in --list as if stable yet - # require the separate Android SDK Preview license at install time, which - # breaks the unattended Docker build. The presence of a element - # is the reliable preview marker, so we parse the manifest with a real XML - # parser and skip any platform that has one. Versions are MAJOR.MINOR - # (e.g. 36.1, 37.0). + # Determine the latest *stable* SDK versions from Google's repository + # manifest. We deliberately do NOT use `sdkmanager --list`: it surfaces + # preview packages as if they were stable (the preview platform + # android-37.0 is tagged channel-0, and preview NDKs such as 30.0.14904198 + # are listed too), which then break the unattended Docker build when + # sdkmanager demands the separate Android SDK Preview license. The manifest + # carries reliable preview markers, so we parse it with a real XML parser + # and skip any package that is obsolete, carries a element, or + # uses the android-sdk-preview-license. if ! wget -q "https://dl.google.com/android/repository/repository2-3.xml" -O /tmp/android-repo.xml; then echo "Failed to download SDK repository manifest" echo "updated=false" >> $GITHUB_OUTPUT exit 0 fi - LATEST_PLATFORM=$(python3 - /tmp/android-repo.xml <<'PY' + eval "$(python3 - /tmp/android-repo.xml <<'PY' import re, sys, xml.etree.ElementTree as ET def local(tag): # strip XML namespace, if any return tag.rsplit('}', 1)[-1] root = ET.parse(sys.argv[1]).getroot() - best, best_key = '', None - for pkg in root.iter(): - if local(pkg.tag) != 'remotePackage' or pkg.get('obsolete') == 'true': - continue - m = re.fullmatch(r'platforms;android-(\d+(?:\.\d+)?)', pkg.get('path', '')) - if not m: - continue - # Skip previews: any descendant element marks a preview. - if any(local(e.tag) == 'codename' for e in pkg.iter()): - continue - ver = m.group(1) - key = tuple(int(x) for x in ver.split('.')) - if best_key is None or key > best_key: - best, best_key = ver, key - print(best) + + def is_preview(pkg): + if pkg.get('obsolete') == 'true': + return True + for e in pkg.iter(): + if local(e.tag) == 'codename': + return True + if local(e.tag) == 'uses-license' and e.get('ref') == 'android-sdk-preview-license': + return True + return False + + def latest(version_re): + best, best_key = '', None + for pkg in root.iter(): + if local(pkg.tag) != 'remotePackage' or is_preview(pkg): + continue + m = re.fullmatch(version_re, pkg.get('path', '')) + if not m: + continue + ver = m.group(1) + key = tuple(int(x) for x in ver.split('.')) + if best_key is None or key > best_key: + best, best_key = ver, key + return best + + print('LATEST_PLATFORM=%s' % latest(r'platforms;android-(\d+(?:\.\d+)?)')) + print('LATEST_BUILD_TOOLS=%s' % latest(r'build-tools;(\d+\.\d+\.\d+)')) + print('LATEST_NDK=%s' % latest(r'ndk;(\d+\.\d+\.\d+)')) PY - ) - LATEST_BUILD_TOOLS=$(echo "$PACKAGES" | grep 'build-tools;' | grep -oP 'build-tools;\K[0-9.]+' | sort -V | tail -1) - LATEST_NDK=$(echo "$PACKAGES" | grep -P '^\s+ndk;' | grep -oP 'ndk;\K[0-9.]+' | sort -V | tail -1) + )" # Get current versions from Dockerfile CURRENT_PLATFORM=$(grep '^ARG COMPILE_SDK=' Dockerfile | cut -d= -f2) diff --git a/Dockerfile b/Dockerfile index 7fbf9ca..8d5a073 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ ARG VERSION=2.334.0-ubuntu-noble ARG JAVA_VERSION=21 ARG COMPILE_SDK=36.1 ARG BUILD_TOOLS=37.0.0 -ARG NDK_VERSION=30.0.14904198 +ARG NDK_VERSION=29.0.14206865 ARG SDK_TOOLS=8512546_latest ARG ANDROID_ROOT=/usr/local/lib/android From 1d5879852dbaaa7127981e116575a1d9c60fb2be Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Wed, 20 May 2026 13:46:04 -0700 Subject: [PATCH 2/2] Address review: fail loudly if manifest parsing fails Previously the manifest parse ran as `eval "$(python3 ...)"`, so a truncated/corrupt download or parse error would be swallowed and the step would continue with empty LATEST_* values. Capture the python output, check its exit status, and validate all three versions are non-empty (python also exits non-zero if any is missing). On failure, report it and set updated=false instead of silently continuing. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/check-sdk-updates.yml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check-sdk-updates.yml b/.github/workflows/check-sdk-updates.yml index 033b391..78f5694 100644 --- a/.github/workflows/check-sdk-updates.yml +++ b/.github/workflows/check-sdk-updates.yml @@ -67,7 +67,7 @@ jobs: echo "updated=false" >> $GITHUB_OUTPUT exit 0 fi - eval "$(python3 - /tmp/android-repo.xml <<'PY' + if ! SDK_VERSIONS=$(python3 - /tmp/android-repo.xml <<'PY' import re, sys, xml.etree.ElementTree as ET def local(tag): # strip XML namespace, if any @@ -99,11 +99,23 @@ jobs: best, best_key = ver, key return best - print('LATEST_PLATFORM=%s' % latest(r'platforms;android-(\d+(?:\.\d+)?)')) - print('LATEST_BUILD_TOOLS=%s' % latest(r'build-tools;(\d+\.\d+\.\d+)')) - print('LATEST_NDK=%s' % latest(r'ndk;(\d+\.\d+\.\d+)')) + platform = latest(r'platforms;android-(\d+(?:\.\d+)?)') + build_tools = latest(r'build-tools;(\d+\.\d+\.\d+)') + ndk = latest(r'ndk;(\d+\.\d+\.\d+)') + if not (platform and build_tools and ndk): + sys.stderr.write('Failed to detect stable versions: ' + 'platform=%r build_tools=%r ndk=%r\n' % (platform, build_tools, ndk)) + sys.exit(1) + print('LATEST_PLATFORM=%s' % platform) + print('LATEST_BUILD_TOOLS=%s' % build_tools) + print('LATEST_NDK=%s' % ndk) PY - )" + ); then + echo "Failed to parse SDK repository manifest" + echo "updated=false" >> $GITHUB_OUTPUT + exit 0 + fi + eval "$SDK_VERSIONS" # Get current versions from Dockerfile CURRENT_PLATFORM=$(grep '^ARG COMPILE_SDK=' Dockerfile | cut -d= -f2)