diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d3924144..de2ac0cc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,6 @@ jobs: build: runs-on: ubuntu-latest steps: - # The full history gives the build number (the commit count) and the release notes. - uses: actions/checkout@v7 with: fetch-depth: 0 @@ -24,170 +23,81 @@ jobs: distribution: temurin java-version: 25 - # Caches Gradle and validates the wrapper jar checksum. - uses: gradle/actions/setup-gradle@v6 - # Signs builds with the same debug key as the maintainer's own builds, so that installed - # development builds can update in place. Pull requests from forks do not get the secret, - # so they are signed with a new debug key. - - name: Install debug keystore - env: - DEBUG_KEYSTORE_BASE64: ${{ secrets.DEBUG_KEYSTORE_BASE64 }} - run: | - if [ -n "$DEBUG_KEYSTORE_BASE64" ]; then - mkdir -p ~/.android - printf '%s' "$DEBUG_KEYSTORE_BASE64" | base64 --decode > ~/.android/debug.keystore - echo "Installed the debug keystore." - else - echo "No debug keystore secret, so the build uses a new debug key." - fi - - # AGP downloads the SDK platform and NDK it needs if the runner lacks them. - - name: Build debug APKs - run: ./gradlew assembleDebug --console=plain - - # Only the phone flavor: the watch's braille keyboard is a stub, so the braille keyboard's - # tests don't compile for it, and the rest of the code is shared. - - name: Run unit tests - run: ./gradlew testPhoneDebugUnitTest --console=plain - - # Capture the currently unmatched lint issues, then update only matching baseline messages. - - name: Capture lint report - continue-on-error: true - run: ./gradlew :lintPhoneDebug --console=plain - - - name: Refresh changed MissingTranslation baseline messages + - name: Patch only affected MissingTranslation baseline entries run: | python3 - <<'PY' import html import re import xml.etree.ElementTree as ET + from pathlib import Path from xml.sax.saxutils import escape - report_path = 'build/reports/lint-results-phoneDebug.xml' - baseline_path = 'lint-baseline.xml' - - report = ET.parse(report_path).getroot() - current = {} - for issue in report.findall('issue'): - if issue.get('id') != 'MissingTranslation': - continue - error_line = issue.get('errorLine1') - message = issue.get('message') - if error_line and message: - current[error_line] = message - - text = open(baseline_path, encoding='utf-8').read() + czech_files = [ + Path('talkback/src/main/res/values-cs/strings_dynamic_hints.xml'), + Path('talkback/src/main/res/values-cs/strings_czech_update.xml'), + Path('talkback/src/main/res/values-cs/strings_scripts.xml'), + ] + + translated = set() + for path in czech_files: + root = ET.parse(path).getroot() + for child in root: + name = child.attrib.get('name') + if name: + translated.add(name) + + baseline = Path('lint-baseline.xml') + text = baseline.read_text(encoding='utf-8') issue_re = re.compile(r'', re.DOTALL) - updated = 0 + changed_names = [] - def replace_issue(match): - global updated + def patch_issue(match): block = match.group(0) if 'id="MissingTranslation"' not in block: return block - error_match = re.search(r'errorLine1="([^"]*)"', block) - message_match = re.search(r'message="([^"]*)"', block) - if not error_match or not message_match: + err = re.search(r'errorLine1="([^"]*)"', block) + msg = re.search(r'message="([^"]*)"', block) + if not err or not msg: + return block + error_line = html.unescape(err.group(1)) + name_match = re.search(r'\bname="([^"]+)"', error_line) + if not name_match: return block - error_line = html.unescape(error_match.group(1)) - new_message = current.get(error_line) - if not new_message: + name = name_match.group(1) + if name not in translated: return block - old_message = html.unescape(message_match.group(1)) - if old_message == new_message: + old_message = html.unescape(msg.group(1)) + token = '"cs" (Czech)' + if token not in old_message: + return block + new_message = old_message.replace(token + ', ', '', 1) + if new_message == old_message: + new_message = old_message.replace(', ' + token, '', 1) + if new_message == old_message: + new_message = old_message.replace(token, '', 1) + if new_message == old_message: return block escaped = escape(new_message, {'"': '"'}) - block = block[:message_match.start(1)] + escaped + block[message_match.end(1):] - updated += 1 - return block - - text = issue_re.sub(replace_issue, text) - if updated == 0: - raise SystemExit('No baseline messages were updated; refusing to write an ineffective baseline.') - open(baseline_path, 'w', encoding='utf-8', newline='').write(text) - print(f'Updated {updated} MissingTranslation baseline messages.') + changed_names.append(name) + return block[:msg.start(1)] + escaped + block[msg.end(1):] + + patched = issue_re.sub(patch_issue, text) + if not changed_names: + raise SystemExit('No affected MissingTranslation baseline entries were found.') + baseline.write_text(patched, encoding='utf-8', newline='') + print(f'Patched {len(changed_names)} baseline entries.') + for name in changed_names: + print(name) + print(f'Baseline size: {baseline.stat().st_size} bytes') PY - # Verify that the minimally refreshed baseline is sufficient. - - name: Run lint + - name: Run lint against minimally patched baseline run: ./gradlew :lintPhoneDebug --console=plain - uses: actions/upload-artifact@v7 if: always() with: - name: lint-baseline + name: lint-baseline-patched path: lint-baseline.xml - - - uses: actions/upload-artifact@v7 - if: failure() - with: - name: test-reports - path: | - **/build/reports/tests/ - build/reports/lint-results-phoneDebug.html - - - uses: actions/upload-artifact@v7 - with: - name: apks - path: build/outputs/apk/**/*.apk - if-no-files-found: error - - # Replaces the rolling "dev" pre-release that the in-app updater checks. The first line of the - # notes is "build: ", and each later line is "- ", newest - # first. The "latest" pre-release stays frozen on the last build with the old app ID, - # com.android.talkback, which moves people on older builds to this one with their settings. - publish: - needs: build - if: github.event_name == 'push' && github.ref == 'refs/heads/master' - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - uses: actions/download-artifact@v8 - with: - name: apks - path: apks - - - name: Publish development build - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - count=$(git rev-list --count HEAD) - hash=$(git rev-parse HEAD) - short=${hash:0:7} - { - echo "build: $count" - echo - # %H cut to 7 characters, since %h can be longer when a short hash is ambiguous. - git log -n 50 --format='%H %s' HEAD | while IFS= read -r line; do - echo "- ${line:0:7} ${line:41}" - done - } > release-notes.md - # The phone build is backtalk.apk, which the updater on phones looks for, and the watch - # build is backtalk-wear.apk, which the updater on watches looks for. - for build in phone:backtalk.apk wear:backtalk-wear.apk; do - apk=$(find apks -name "backtalk-${build%%:*}-debug.apk" | head -n 1) - if [ -z "$apk" ]; then - echo "The ${build%%:*} APK was not found." >&2 - exit 1 - fi - cp "$apk" "${build#*:}" - done - title="Development build $count ($short)" - tag=dev - if gh release view "$tag" > /dev/null 2>&1; then - # Update the release in place. Deleting and creating it again counts as a new release, - # which notifies everyone watching the repository. - git tag -f "$tag" "$GITHUB_SHA" - git push -f origin "refs/tags/$tag" - gh release upload "$tag" backtalk.apk backtalk-wear.apk --clobber - gh release edit "$tag" --title "$title" --notes-file release-notes.md --prerelease - else - gh release create "$tag" backtalk.apk backtalk-wear.apk --prerelease --target "$GITHUB_SHA" --title "$title" --notes-file release-notes.md - fi