Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
3f7a13d
fix(recorder): keep recordings out of the cache
criticalAY Oct 12, 2026
2ff269e
fix(recorder): clean up old recordings at startup
criticalAY Oct 12, 2026
675ee6d
refactor(card-browser): deduplicate card state icons
david-allison Oct 15, 2026
93078f3
test: support disabling backend loading
david-allison Oct 14, 2026
4b0be9c
test: Windows support for FileProvider host paths
david-allison Oct 12, 2026
e363704
refactor(note-editor): extract `launchEditor`
david-allison Oct 13, 2026
90ca322
fix(note-editor): stop flicker on cancelling 'Gallery'
david-allison Oct 13, 2026
77052f0
test(reviewer): wait for autofocus before changing fields
david-allison Oct 13, 2026
4f0c574
feat(study-screen): support HTML `nosuggest`
david-allison Oct 2, 2026
99b6f9b
fix(editor): use backend filenames when pasting media
david-allison Oct 15, 2026
84f4ad1
perf(deck-picker): release the background image when stopped
criticalAY Oct 14, 2026
2d10b79
refactor: use REQUEST_ for Fragment Result API request keys
david-allison Oct 14, 2026
3241819
lint: enforce REQUEST_ for Fragment Result API request keys
david-allison Oct 14, 2026
4338790
build(ci): store screenshot baselines for dependency-updates
criticalAY Oct 14, 2026
e1adc09
build(release): add the release to the publish run summary
criticalAY Oct 15, 2026
e59c39e
test(profiles): fix flaky SwitchProfilesFragmentTest
criticalAY Oct 15, 2026
57eb96f
fix(mediacheck): handle onRenderProcessGone with SafeWebViewLayout
Ratnesh5 Oct 15, 2026
4448506
fix(card-viewer): allow HTTP/HTTPS card links
david-allison Oct 13, 2026
78fa213
fix(card-viewer): allow deep links if a browser isn't installed
david-allison Oct 13, 2026
7548b99
fix(card-viewer): suppress failed card link launches
david-allison Oct 13, 2026
b399981
fix(card-viewer): avoid catching `Error` during link launches
david-allison Oct 13, 2026
8590180
fix(reviewer): handle rejected external link launches
david-allison Oct 15, 2026
0a117f7
build(buildSrc): add Compose convention plugin
david-allison Oct 15, 2026
2e764b0
build: add `:note-editor` module
david-allison Oct 14, 2026
8c1e4a0
refactor(note-editor): extract toolbar icons to modules
david-allison Oct 15, 2026
233134f
refactor(note-editor): extract ToolbarPreferences
david-allison Oct 15, 2026
f151fb9
feat(compose): support previews for all app themes
david-allison Oct 15, 2026
1b25591
refactor: use `RESULT_` for fragment result key names
david-allison Oct 15, 2026
e07cf6f
lint: enforce `RESULT_` for fragment result key names
david-allison Oct 15, 2026
d64360c
refactor(test): convert java to kotlin
david-allison Oct 15, 2026
507e758
refactor(sync): move fullUploadOrDownload
david-allison Oct 15, 2026
a60a9c9
refactor(sync): document `FullSync:MediaSync`
david-allison Oct 15, 2026
7b2baa8
fix(import): do not close import dialog on startup [tablet]
david-allison Oct 15, 2026
25cf98f
fix(card-template-editor): count the cards a new card type creates
criticalAY Oct 13, 2026
8860d8a
test(reminders): stabilize code coverage
david-allison Oct 15, 2026
205f71c
test(reviewer): stabilize code coverage
david-allison Oct 15, 2026
3784723
fix(one-way-sync): apply metered connection restrictions for media
david-allison Oct 15, 2026
5d18a0b
fix(ci): mark a skipped weekly publish as 'cancelled'
david-allison Oct 12, 2026
5c078fa
fix(release): push the version bump before uploading to Play
criticalAY Oct 15, 2026
7c1f39b
Bumped version to 2.26.0alpha5
mikehardy Oct 15, 2026
b3d74d3
refactor: prefix intent extra keys with `EXTRA_`
criticalAY Oct 15, 2026
7e29d24
lint: enforce `EXTRA_` for intent extra keys
criticalAY Oct 15, 2026
6f03d60
fix(settings): reword web links note
uditpandey727 Oct 16, 2026
ea1a3db
fix(release): stop if the release tag is missing
criticalAY Oct 16, 2026
b742597
ci(emulator): pin the build time for the whole job
criticalAY Oct 15, 2026
7b51627
ci(emulator): run the release smoke test in its own steps
criticalAY Oct 15, 2026
cd90578
ci(emulator): add a tablet to scheduled runs
criticalAY Oct 15, 2026
e331d34
test(study-options): fix flaky buried count test
criticalAY Oct 16, 2026
311111b
fix(ci): retry GitHub API calls in the label workflow
criticalAY Oct 16, 2026
ec6c2ec
fix(deck-picker): close FAB when background clicked
snowtimeglass Oct 16, 2026
5c8c0d7
test(deck-picker): coverage for closing FAB
snowtimeglass Oct 16, 2026
9c86a91
fix(tts): prefer voices from the default engine
david-allison Oct 17, 2026
ba8fa41
Bumped version to 2.26.0alpha6
mikehardy Oct 18, 2026
d77eb29
docs(ci): explain screenshot dependency
david-allison Oct 17, 2026
3fd4723
test: move `ScreenshotTestCategory` to `:common` fixtures
david-allison Oct 17, 2026
3e6e583
build: move Roborazzi to convention plugin
david-allison Oct 17, 2026
d290688
build: share screenshot commands across modules
david-allison Oct 17, 2026
3369559
test: clear obsolete differences
david-allison Oct 17, 2026
530d145
feat(ci): add screenshots tests for all modules
david-allison Oct 17, 2026
87dc3a7
fix(navigation-drawer): don't crash on a config change before setup
criticalAY Oct 18, 2026
099055f
test(profiles): fix flaky profile ID uniqueness test
criticalAY Oct 18, 2026
b7dcd5c
test: add fallback filename test for ImportUtils.validateFileName
Kushagra2024-code Oct 19, 2026
2dd553e
test: add modifier and cloze tests for previewer TypeAnswer
Kushagra2024-code Oct 19, 2026
e8a5af2
feat(manage-note-types): show progress from the ViewModel
criticalAY Oct 16, 2026
ad8ccfd
fix(progress): block back while an operation runs
criticalAY Oct 18, 2026
9ea9607
fix(deck-picker): stop 'Add' FAB overshooting
david-allison Oct 17, 2026
7b026a0
fix(tests): fix flake by waiting for deck list
david-allison Oct 13, 2026
a7e70ba
fix(previewer): stop audio when the card side changes
criticalAY Oct 17, 2026
628a198
build(ci): add emulator test results to workflow summary (#22011)
Ratnesh5 Oct 13, 2026
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
32 changes: 21 additions & 11 deletions .github/check_alpha_release.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@
const fs = require("node:fs");

module.exports = async function checkAlphaRelease({ github, context, core }) {
async function skipRelease(reason) {
core.info(`Skipping scheduled release: ${reason}`);
await github.rest.actions.cancelWorkflowRun({
...context.repo,
run_id: context.runId,
});
// Cancellation is asynchronous. Keep this step alive so cancelWorkflowRun can kill it.
await keepProcessAlive();
}

const metadata = JSON.parse(
fs.readFileSync("AnkiDroid/build/outputs/release-metadata.json", "utf8"),
);
if (metadata.releaseType !== "alpha") {
core.info("Skipping scheduled release: main is not on an alpha version.");
return false;
return await skipRelease("main is not on an alpha version.");
}

const releases = await github.paginate(github.rest.repos.listReleases, {
Expand All @@ -22,33 +31,34 @@ module.exports = async function checkAlphaRelease({ github, context, core }) {
release => !release.draft && today - Math.floor(Date.parse(release.published_at) / day) < 7,
);
if (recentRelease) {
core.info(
`Skipping scheduled release: ${recentRelease.tag_name} was published fewer than seven UTC calendar days ago.`,
return await skipRelease(
`${recentRelease.tag_name} was published fewer than seven UTC calendar days ago.`,
);
return false;
}

const latestAlpha = releases
.filter(release => !release.draft && /^v\d+\.\d+\.\d+alpha\d+$/.test(release.tag_name))
.sort((a, b) => Date.parse(b.published_at) - Date.parse(a.published_at))[0];
if (!latestAlpha) {
core.info("Skipping scheduled release: no published alpha to compare against.");
return false;
return await skipRelease("no published alpha to compare against.");
}

const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({
...context.repo,
basehead: `${latestAlpha.tag_name}...${context.sha}`,
});
if (comparison.status !== "ahead" || comparison.ahead_by === 0) {
core.info(
`Skipping scheduled release: no new commits descending from ${latestAlpha.tag_name}.`,
);
return false;
return await skipRelease(`no new commits descending from ${latestAlpha.tag_name}.`);
}

core.info(
`Publishing an alpha: ${comparison.ahead_by} new commits since ${latestAlpha.tag_name}.`,
);
return true;
};

async function keepProcessAlive() {
// Fail after five minutes if cancellation never arrives.
await new Promise(resolve => setTimeout(resolve, 5 * 60 * 1000));
throw new Error("Timed out waiting for workflow cancellation.");
}
97 changes: 89 additions & 8 deletions .github/report_test_failures.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later

"""Report failures and skipped tests from JUnit XML using the standard library."""
"""Report failures, skipped tests, and overview summary from JUnit XML using the standard library."""

import argparse
import html
Expand All @@ -12,6 +12,19 @@
import xml.etree.ElementTree as ET


TABLE_HEADER = (
"## Test Results\n\n"
"| Suite | Result | Duration | Tests | Passed | Failed | Skipped |\n"
"| ----- | ------ | -------- | ----- | ------ | ------ | ------- |\n"
)


def format_duration(seconds):
minutes = int(seconds // 60)
rem_seconds = seconds % 60
return f"{minutes:02d}m {rem_seconds:06.3f}s"


def read_ignored_tests(log_file):
ignored = set()
failed = set()
Expand All @@ -26,22 +39,61 @@ def read_ignored_tests(log_file):
return ignored - failed


def main(results_dir, log_file=None, pattern="TEST-*.xml", title="Test failures"):
def get_report_duration(root):
suites = root.findall("testsuite")
if suites:
total = 0.0
for s in suites:
try:
total += float(s.get("time", 0) or 0)
except (ValueError, TypeError):
pass
if total > 0.0:
return total
try:
root_time = float(root.get("time", 0) or 0)
if root_time > 0.0:
return root_time
except (ValueError, TypeError):
pass
total = 0.0
for tc in root.iter("testcase"):
try:
total += float(tc.get("time", 0) or 0)
except (ValueError, TypeError):
pass
return total


def main(results_dir, log_file=None, pattern="TEST-*.xml", title="Test failures", suite=None):
failures = []
skipped = []
ignored = read_ignored_tests(log_file)
total_duration = 0.0
passed_count = 0
failed_count = 0
skipped_count = 0

for report in sorted(results_dir.rglob(pattern)):
try:
root = ET.parse(report).getroot()
except (ET.ParseError, OSError) as error:
failures.append(f"Could not read {report}: {error}")
if os.environ.get("GITHUB_ACTIONS") == "true":
print(f"::warning::Could not read {report}: {error}")
else:
print(f"Warning: Could not read {report}: {error}", file=sys.stderr)
continue

total_duration += get_report_duration(root)

for test in root.iter("testcase"):
name = f"{test.get('classname', '')}.{test.get('name', '')}"
has_failure = False
has_skipped = False

for result in test:
if result.tag not in ("failure", "error", "skipped"):
continue
name = f"{test.get('classname', '')}.{test.get('name', '')}"
details = "".join(result.itertext()).strip()
message = result.get("message", "")
# Android can report assumption violations as failures or errors.
Expand All @@ -56,10 +108,30 @@ def main(results_dir, log_file=None, pattern="TEST-*.xml", title="Test failures"
"org.junit.AssumptionViolatedException",
"org.junit.internal.AssumptionViolatedException",
):
has_skipped = True
skipped.append(f"{name}\n{details or result.get('type', 'Test skipped')}")
else:
has_failure = True
failures.append(f"{name}\n{details or result.get('type', 'Test failed')}")

if has_failure:
failed_count += 1
elif has_skipped:
skipped_count += 1
else:
passed_count += 1

total_tests = passed_count + failed_count + skipped_count
is_successful = failed_count == 0 and total_tests > 0
result_text = "SUCCESS" if is_successful else "FAILURE"
elapsed = format_duration(total_duration)

if suite:
print(
f"{suite}: {result_text} ({elapsed}) - "
f"{total_tests} tests: {passed_count} passed, {failed_count} failed, {skipped_count} skipped"
)

for failure in failures:
if os.environ.get("GITHUB_ACTIONS") == "true":
# GitHub renders the escaped message as a multiline error in the log.
Expand All @@ -79,19 +151,27 @@ def main(results_dir, log_file=None, pattern="TEST-*.xml", title="Test failures"
if os.environ.get("GITHUB_ACTIONS") == "true":
print("::endgroup::")

if (failures or skipped) and os.environ.get("GITHUB_STEP_SUMMARY"):
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
if os.environ.get("GITHUB_STEP_SUMMARY"):
with open(os.environ["GITHUB_STEP_SUMMARY"], "a+", encoding="utf-8") as summary:
summary.seek(0)
existing_content = summary.read()
if suite:
if "## Test Results" not in existing_content:
summary.write(TABLE_HEADER)
summary.write(f"| {suite} | **{result_text}** | {elapsed} | {total_tests} | {passed_count} | {failed_count} | {skipped_count} |\n\n")

if failures:
summary.write(f"### {title}\n\n")
for failure in failures:
summary.write(f"<pre>{html.escape(failure)}</pre>\n\n")
if skipped:
skipped_title = f"Skipped tests ({len(skipped)})"
summary.write(f"<details><summary>{skipped_title}</summary>\n\n")
for skip in skipped:
summary.write(f"<pre>{html.escape(skip)}</pre>\n\n")
summary.write("</details>\n\n")

return 1 if failures else 0
return 1 if (failures or (suite and not is_successful)) else 0


if __name__ == "__main__":
Expand All @@ -100,5 +180,6 @@ def main(results_dir, log_file=None, pattern="TEST-*.xml", title="Test failures"
parser.add_argument("log_file", type=Path, nargs="?")
parser.add_argument("--pattern", default="TEST-*.xml", help="Recursive report glob relative to results_dir")
parser.add_argument("--title", default="Test failures", help="Failure heading in the job summary")
parser.add_argument("--suite", help="Suite name for markdown summary table")
args = parser.parse_args()
sys.exit(main(args.results_dir, args.log_file, args.pattern, args.title))
sys.exit(main(args.results_dir, args.log_file, args.pattern, args.title, args.suite))
6 changes: 6 additions & 0 deletions .github/workflows/label.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
retries: 3
script: |
let removeLabelsList = [
"Pending Merge",
Expand Down Expand Up @@ -42,6 +43,7 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
request: { retries: 0 },
body: `Maintainers: Please [Sync Translations](https://github.com/ankidroid/Anki-Android/actions/workflows/sync_translations.yml) to produce a commit with only the automated changes from this PR.

Read more about updating strings on the wiki,
Expand Down Expand Up @@ -80,6 +82,7 @@ jobs:
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
retries: 3
script: |
const I18N_FILES = [
"01-core",
Expand Down Expand Up @@ -133,6 +136,7 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
request: { retries: 0 },
body: `> [!IMPORTANT]
> **Maintainers**: This PR contains https://github.com/ankidroid/Anki-Android/labels/Strings changes

Expand Down Expand Up @@ -207,6 +211,7 @@ jobs:
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
retries: 3
script: |
const creator = context.payload.sender.login
const opts = github.rest.issues.listForRepo.endpoint.merge({
Expand Down Expand Up @@ -240,6 +245,7 @@ jobs:
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
retries: 3
script: |
await github.rest.issues.addLabels({
issue_number: context.issue.number,
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ concurrency:
jobs:
check_release:
name: 'Check whether a release is due'
permissions:
contents: read
actions: write # Cancel this run when a scheduled publish is skipped.
if: github.event_name == 'schedule' && github.repository == 'ankidroid/Anki-Android' && github.ref == 'refs/heads/main'
timeout-minutes: 20
runs-on: ubuntu-latest
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/screenshot_comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
name: Check if there are valid files
shell: bash
run: |
# Find Roborazzi diff PNGs in the downloaded artifact (in <TestClass>/).
# Find Roborazzi diff PNGs in the downloaded artifact (in <module>/<TestClass>/).
mapfile -t files_to_add < <(find . -name "*_compare.png" -type f)
if [ ${#files_to_add[@]} -gt 0 ]; then
exist_valid_files="true"
Expand All @@ -82,17 +82,18 @@ jobs:
BASE_REF: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.pull_requests[0].base.ref || github.event.repository.default_branch }}
shell: bash
run: |
# Find Roborazzi diff PNGs to mention in the comment (in <TestClass>/).
# Find Roborazzi diff PNGs to mention in the comment (in <module>/<TestClass>/).
mapfile -t files < <(find . -name "*_compare.png" -type f | sort)
total=${#files[@]}
# Cap the full list so we remain within GitHub's comment limit
max_details=100

# Group by test class - the file's immediate parent directory.
# Group by module and test class, after the artifact's per-run directory.
declare -A class_counts class_files
for file in "${files[@]}"; do
class_dir="${file%/*}"
class="${class_dir##*/}"
class="${class_dir#./screenshot-diff/}"
class="${class#*/}"
class_counts[$class]=$(( ${class_counts[$class]:-0} + 1 ))
class_files[$class]+="$(basename "$file")"$'\n'
done
Expand Down
29 changes: 14 additions & 15 deletions .github/workflows/screenshot_compare.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,14 @@ jobs:
gradle-version: wrapper

- name: Warm Gradle Cache
# This makes sure we fetch gradle network resources with a retry
# Keep compilation here: robolectricSdkDownload only downloads SDK jars.
# Compiling resolves test dependencies within the retry (Issue 21682).
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
with:
timeout_minutes: 10
retry_wait_seconds: 60
max_attempts: 3
command: ./gradlew robolectricSdkDownload :AnkiDroid:compilePlayDebugUnitTestKotlin -Pscreenshot --daemon
command: ./gradlew robolectricSdkDownload compileScreenshotTests -Pscreenshot --daemon

- name: Download base branch screenshots
uses: dawidd6/action-download-artifact@eab87c9830c39eff17e5a6eadb20bfb4bc880477 # v27
Expand All @@ -72,30 +73,28 @@ jobs:
name: screenshot
workflow: screenshot_store.yml
branch: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.event.repository.default_branch }}
event: push

- name: Compare screenshots
id: compare-screenshot-test
run: |
./gradlew compareRoborazziPlayDebug -Pscreenshot --stacktrace
./gradlew compareScreenshots -Pscreenshot --stacktrace

- name: Stage screenshot diffs
if: ${{ always() }}
env:
# Folder name inside the artifact (e.g. "screenshot-diff-pr-12345" on PRs, "screenshot-diff-main" on push)
SUBDIR: ${{ github.event_name == 'pull_request' && format('screenshot-diff-pr-{0}', github.event.number) || format('screenshot-diff-{0}', github.ref_name) }}
run: |
# Trim 'AnkiDroid/build/outputs/roborazzi' from the artifact
# Only keep the _compare images of failing cases: <class>/diffs/*_compare.png moves to <class>/
# Preserve module paths so identically named tests cannot overwrite one another.
mkdir -p "screenshot-diff/$SUBDIR"
shopt -s nullglob
for diffs_dir in AnkiDroid/build/outputs/roborazzi/*/diffs; do
class_name="$(basename "$(dirname "$diffs_dir")")"
compares=("$diffs_dir"/*_compare.png)
if (( ${#compares[@]} )); then
mkdir -p "screenshot-diff/$SUBDIR/$class_name"
mv "${compares[@]}" "screenshot-diff/$SUBDIR/$class_name/"
fi
done
while IFS= read -r -d '' compare; do
module="${compare%%/build/reports/screenshot-diffs/*}"
relative="${compare#*/build/reports/screenshot-diffs/}"
destination="screenshot-diff/$SUBDIR/${module#./}/$relative"
mkdir -p "$(dirname "$destination")"
cp "$compare" "$destination"
done < <(find . -type f -path '*/build/reports/screenshot-diffs/*_compare.png' -print0)

- name: Upload screenshot diffs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down Expand Up @@ -132,4 +131,4 @@ jobs:
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pr # downloaded by screenshot_comment.yml
path: pr/
path: pr/
Loading
Loading