Skip to content
Merged
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
194 changes: 52 additions & 142 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'<issue\b.*?</issue>', 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, {'"': '&quot;'})
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: <commit count>", and each later line is "- <short hash> <subject>", 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
Loading