Skip to content

feat(chatlist): add mark-as-read to the multi-select toolbar #376

feat(chatlist): add mark-as-read to the multi-select toolbar

feat(chatlist): add mark-as-read to the multi-select toolbar #376

Workflow file for this run

name: Required CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
changes:
runs-on: ubuntu-latest
outputs:
runtime: ${{ steps.classify.outputs.runtime }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: { fetch-depth: 0 }
- name: Classify changed paths
id: classify
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: |
set -euo pipefail
runtime=true
if [[ -n "$BASE_SHA" && ! "$BASE_SHA" =~ ^0+$ ]] && git cat-file -e "$BASE_SHA^{commit}"; then
git diff --check "$BASE_SHA" "$GITHUB_SHA"
runtime=false
while IFS= read -r path; do
case "$path" in
*.md|LICENSE|docs/*|site/*|screenshots/*|fastlane/metadata/*) ;;
*) runtime=true; break ;;
esac
done < <(git diff --name-only "$BASE_SHA" "$GITHUB_SHA")
fi
echo "runtime=$runtime" >> "$GITHUB_OUTPUT"
# Pure-JVM protocol module: fast, no Android SDK needed. Runs in parallel
# with the app job so a slow/hanging app build never blocks irc feedback.
irc:
needs: changes
if: needs.changes.outputs.runtime == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
env:
MOTD_FUZZ_PROFILE: pr
MOTD_FUZZ_SEED: ${{ github.event.pull_request.head.sha || github.sha }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: { distribution: temurin, java-version: 21 }
- uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
- name: irc unit tests
run: ./gradlew :irc:test --stacktrace
- name: Upload IRC generated-test failure
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: irc-fuzz-failure-${{ github.run_id }}
path: |
irc/build/fuzz-failures/**
irc/build/reports/tests/test/**
if-no-files-found: ignore
# Android app: debug + release unit tests, lint, and APK assembly. The release APK is unsigned in
# required CI; tag builds inject the production signing secrets in release.yml.
app:
needs: changes
if: needs.changes.outputs.runtime == 'true'
runs-on: ubuntu-latest
timeout-minutes: 35
env:
MOTD_FUZZ_PROFILE: pr
MOTD_FUZZ_SEED: ${{ github.event.pull_request.head.sha || github.sha }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: { distribution: temurin, java-version: 21 }
- uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
- name: Kotlin formatting
run: ./gradlew ktlintCheck --stacktrace
- name: App unit tests (debug and release)
run: >-
./gradlew :app:testDebugUnitTest :app:testReleaseUnitTest
--stacktrace
- name: Upload app generated-test failure
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: app-fuzz-failure-${{ github.run_id }}
path: |
app/build/fuzz-failures/**
app/build/reports/tests/testDebugUnitTest/**
app/build/reports/tests/testReleaseUnitTest/**
if-no-files-found: ignore
- name: Verify checked-in Room schemas
run: |
git diff --exit-code -- app/schemas
test -z "$(git ls-files --others --exclude-standard -- app/schemas)"
- name: Verify FOSS dependency boundary
run: |
if ./gradlew :app:dependencies --configuration releaseRuntimeClasspath \
| rg -i 'firebase|play-services'; then
echo "Google-only dependencies reached the FOSS runtime classpath" >&2
exit 1
fi
# Lint occasionally hits a rare Gradle-worker classloader race that
# surfaces as NoClassDefFoundError in ModifierDeclarationDetector. The old
# --no-daemon --max-workers=1 guard never fully prevented it (hence this
# retry) while costing ~30x the wall-clock, so lint now runs with the warm
# daemon and the repo's bounded worker cap; the bounded retry (2 attempts,
# each now sharing the daemon) is the actual safety net for the rare race.
- name: Android lint (warnings as errors)
run: |
for attempt in 1 2; do
echo "::group::lint attempt $attempt"
./gradlew :app:lintDebug :app:lintRelease --stacktrace \
&& { echo "::endgroup::"; exit 0; }
echo "::endgroup::"
echo "lint attempt $attempt failed; retrying" >&2
done
exit 1
# Native packaging is irrelevant to formatting and unit-test failures. Defer the large
# toolchain setup so deterministic red runs report sooner and avoid the download entirely.
- uses: ./.github/actions/setup-native-toolchain
- name: Assemble debug and release APKs
run: >-
./gradlew :app:assembleDebug :app:assembleRelease
--stacktrace
- name: Validate E2E shell and Compose configuration
run: ./test/e2e/validate.sh
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: motd-debug
path: app/build/outputs/apk/debug/app-debug.apk
headless:
needs: changes
if: needs.changes.outputs.runtime == 'true'
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: { distribution: temurin, java-version: 21 }
- uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
- 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
# A cached AVD + snapshot is only valid for the emulator that produced it,
# so the key has to carry the emulator revision: when the runner image
# upgrades its bundled emulator, a prefix-identical key would otherwise
# restore a stale snapshot and degrade into odd boots and flaky runs.
- name: Resolve emulator revision for the AVD cache key
id: emulator-revision
run: |
set -uo pipefail
sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-$HOME/Android/Sdk}}"
revision=""
# Preferred source: the emulator binary the run will actually use.
if [ -x "$sdk/emulator/emulator" ]; then
revision="$(timeout 60 "$sdk/emulator/emulator" -version 2>/dev/null \
| sed -n 's/.*emulator version \([0-9][0-9A-Za-z.]*\).*/\1/p' \
| head -n 1)" || revision=""
fi
# Fallback: the installed SDK package revision, no binary launch.
if [ -z "$revision" ] && [ -x "$sdk/cmdline-tools/latest/bin/sdkmanager" ]; then
revision="$(timeout 300 "$sdk/cmdline-tools/latest/bin/sdkmanager" \
--list_installed 2>/dev/null \
| awk -F'|' '$1 ~ /^[[:space:]]*emulator[[:space:]]*$/ {
gsub(/[[:space:]]/, "", $2); print $2; exit }')" || revision=""
fi
# Last resorts keep the key non-empty. The runner image version moves
# whenever the bundled emulator does, and a fully unresolvable
# revision falls back to a per-run key so the job cold-creates the AVD
# instead of trusting a cache entry it cannot validate.
if [ -z "$revision" ] && [ -n "${ImageVersion:-}" ]; then
revision="image-${ImageVersion}"
fi
if [ -z "$revision" ]; then
revision="unresolved-${GITHUB_RUN_ID:-$(date +%s)}"
fi
revision="$(printf '%s' "$revision" | tr -c 'A-Za-z0-9._-' '-')"
echo "Emulator revision for AVD cache key: $revision"
echo "revision=$revision" >> "$GITHUB_OUTPUT"
# Cache the AVD + a warm boot snapshot so runs load a snapshot instead of
# cold-booting. Key covers every dimension that changes the image/AVD.
# No restore-keys on purpose: a prefix match would reintroduce exactly the
# stale-AVD reuse the emulator revision is here to prevent.
- name: AVD cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-api34-default-x86_64-pixel6-emu-${{ steps.emulator-revision.outputs.revision }}
- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
with:
api-level: 34
target: default
arch: x86_64
profile: pixel_6
force-avd-creation: false
disable-animations: false
emulator-options: -no-window -noaudio -no-boot-anim -gpu swiftshader_indirect
script: echo "Generated AVD snapshot for caching."
- name: Start hermetic bouncer stack
run: ./test/e2e/hermetic-stack.sh up
- name: Run required isolated journeys
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
with:
api-level: 34
target: default
arch: x86_64
profile: pixel_6
force-avd-creation: false
disable-animations: true
# Load the cached snapshot; -no-snapshot-save keeps the cache immutable
# across runs (replaces the old cold-boot -no-snapshot).
emulator-options: -no-window -no-snapshot-save -noaudio -no-boot-anim -gpu swiftshader_indirect
script: ./test/e2e/fast-suite.sh connected
- name: Upload privacy-safe required diagnostics
# always(), not failure(): the journeys also record a passing timeline
# snapshot, and a red run is only diffable against a green baseline.
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: headless-diagnostics
# ONLY the launcher's bundle. AGP keeps its own copy of the same per-test output under
# app/build/outputs, and uploading that directly would be the shorter path — but nothing
# audits it. e2e_audit_required_artifacts runs over this directory alone, so a second path
# would publish whatever the instrumentation happened to leave in an additional-output
# directory without the allowlist or the forbidden-field grep ever seeing it. The scrape
# into this bundle (e2e_collect_gradle_required_e2e_artifacts) is the audited route, and a
# scrape that misses is no longer silent: a red run with no per-test capture is now
# reported as ARTIFACTS_MISSING rather than under the real failure's name.
path: test/e2e/artifacts/fast-suite/**
if-no-files-found: ignore
- name: Tear down hermetic stack
if: always()
run: ./test/e2e/hermetic-stack.sh down || true
component-ui:
needs: changes
if: needs.changes.outputs.runtime == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: { distribution: temurin, java-version: 21 }
- uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
- 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
- name: Run hermetic component journeys
run: ./test/e2e/component-suite.sh
- name: Upload component test report
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: component-ui-report
path: |
app/build/reports/androidTests/managedDevice/**
app/build/outputs/androidTest-results/managedDevice/**
if-no-files-found: ignore
gate:
if: always()
needs: [changes, irc, app, headless, component-ui]
runs-on: ubuntu-latest
steps:
- name: Require every applicable CI tier
env:
CHANGES_RESULT: ${{ needs.changes.result }}
RUNTIME_CHANGED: ${{ needs.changes.outputs.runtime }}
IRC_RESULT: ${{ needs.irc.result }}
APP_RESULT: ${{ needs.app.result }}
HEADLESS_RESULT: ${{ needs.headless.result }}
COMPONENT_RESULT: ${{ needs.component-ui.result }}
run: |
test "$CHANGES_RESULT" = success
test "$RUNTIME_CHANGED" = true || {
test "$IRC_RESULT" = skipped
test "$APP_RESULT" = skipped
test "$HEADLESS_RESULT" = skipped
test "$COMPONENT_RESULT" = skipped
exit
}
test "$IRC_RESULT" = success
test "$APP_RESULT" = success
test "$HEADLESS_RESULT" = success
test "$COMPONENT_RESULT" = success