Skip to content
Merged
Show file tree
Hide file tree
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
19 changes: 19 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: 2
updates:
- package-ecosystem: gradle
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
groups:
android-dependencies:
patterns:
- "androidx.*"

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
119 changes: 114 additions & 5 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ name: Проверка Android
on:
push:
branches: [main]
tags: ["v*"]
pull_request:

permissions:
Expand All @@ -16,19 +17,127 @@ jobs:

steps:
- name: Получить исходный код
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: Настроить JDK 17
uses: actions/setup-java@d7793b545071e98d581d3bf084a51c3213318a07 # v4
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: temurin
java-version: "17"

- name: Проверить Gradle Wrapper
uses: gradle/actions/wrapper-validation@ed408507eac070d1f99cc633dbcf757c94c7933a # v4
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0

- name: Настроить Gradle
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0

- name: Проверить пробелы в изменениях
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
if [[ -n "$BASE_SHA" ]]; then
git diff --check "$BASE_SHA"...HEAD
else
git diff --check HEAD^ HEAD
fi

- name: Проверить проект
run: ./gradlew --no-daemon test lint assembleDebug
run: ./gradlew --no-daemon test lint assembleDebug assembleRelease

- name: Сохранить неподписанную релизную сборку
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: CleanRate-unsigned-${{ github.sha }}
path: app/build/outputs/apk/release/app-release-unsigned.apk
if-no-files-found: error
retention-days: 14

- name: Сохранить таблицу деобфускации
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: CleanRate-mapping-${{ github.sha }}
path: app/build/outputs/mapping/release/mapping.txt
if-no-files-found: error
retention-days: 14

provenance:
name: Происхождение сборки тега
if: startsWith(github.ref, 'refs/tags/v')
needs: [test, android-runtime]
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
attestations: write

steps:
- name: Получить неподписанную сборку
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: CleanRate-unsigned-${{ github.sha }}
path: release

- name: Проверить состав артефакта
run: test -f release/app-release-unsigned.apk

- name: Подтвердить происхождение APK
uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4.2.1
with:
subject-path: release/app-release-unsigned.apk

android-runtime:
name: Проверка на Android 15
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read

steps:
- name: Получить исходный код
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Разрешить аппаратное ускорение 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: Настроить JDK 17
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: temurin
java-version: "17"

- name: Настроить Gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0

- name: Запустить инструментальные тесты
id: android-tests
continue-on-error: true
uses: ReactiveCircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
with:
api-level: 35
arch: x86_64
target: default
disable-animations: true
script: >-
./gradlew --no-daemon connectedDebugAndroidTest ||
{ android_test_status=$?; echo "Инструментальные тесты завершились ошибкой. Последние строки logcat:";
adb logcat -d -v threadtime | tail -n 4000; exit "$android_test_status"; }

- name: Сохранить отчёт инструментальных тестов
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Android-15-test-report-${{ github.sha }}
path: |
app/build/outputs/androidTest-results/connected/
app/build/reports/androidTests/connected/
if-no-files-found: warn
retention-days: 7

- name: Завершить проверку при сбое тестов
if: steps.android-tests.outcome == 'failure'
run: exit 1
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

Все заметные изменения проекта перечисляются в этом файле.

## 1.8.1 — 2026-08-02

- данные с устаревшей или неправдоподобно будущей датой больше не считаются актуальными;
- сетевые ответы ограничены по размеру, разрешены только HTTPS-переходы, XML ЦБ разбирается с защитой от внешних сущностей;
- точки графика размещаются по реальным календарным датам, а пропуски данных явно обозначаются;
- кэш истории ограничен, загрузка имеет общий отменяемый тайм-аут, а при ошибке используется сохранённый график;
- резервные адреса текущих и исторических курсов проверяются не только по HTTP-ответу, но и по содержимому данных;
- статус курса учитывает только реально используемые автоматические источники, включая смешанные наборы ЦБ и ручных курсов;
- после перезапуска восстанавливаются последний открытый экран, валютная пара, период и сумма графика;
- смена источника во время обновления не теряет последующий запрос;
- график получил действия для TalkBack;
- проект переведён на Android API 36, Gradle 8.14.5, Android Gradle Plugin 8.11.1 и согласованный Compose BOM;
- CI проверяет релизную сборку и запуск на Android 15, раздельно сохраняет APK и таблицу деобфускации и подтверждает происхождение APK из тегов;
- добавлены Dependabot, инструкция проверяемого выпуска и сведения о происхождении звука.

## 1.8.0 — 2026-08-02

- мировой курс переведён на открытый источник `exchange-api` с лицензией CC0 1.0;
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

1. Создайте ответвление репозитория и отдельную ветку.
2. Не добавляйте ключи подписи, пароли, APK, AAB, `local.properties` и локальные пути.
3. Выполните `./gradlew test lint assembleDebug`.
3. Выполните `./gradlew test lint assembleDebug assembleRelease`.
4. Заголовок и описание запроса на слияние, а также комментарии об изменениях пишите на русском языке.
5. Опишите изменение и результат проверки в запросе на слияние.

Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Конвертер валют для Android
# Конвертер валют для Android

Персональный Android-конвертер валют без рекламы, регистрации и аналитики.

Expand All @@ -23,7 +23,7 @@
## Установка APK

1. Откройте раздел **Releases** справа на странице репозитория.
2. Выберите последний выпуск и скачайте файл `CleanRate-1.8.0.apk`.
2. Выберите последний выпуск и скачайте файл `CleanRate-1.8.1.apk`.
3. Если Android спросит разрешение, временно разрешите установку из браузера или файлового менеджера.
4. Откройте APK и подтвердите установку.
5. После установки верните запрет на установку неизвестных приложений, если он больше не нужен.
Expand All @@ -41,6 +41,9 @@
- введённые суммы и собственные курсы банка остаются на устройстве;
- ключ подписи не хранится в репозитории.

Официальный сертификат подписи имеет SHA-256-отпечаток
`1798311602fa64b2f43832bcd16252d6ae338654653f9b149f4b33261b83ded0`.

Полный текст: [политика конфиденциальности](PRIVACY.md). Инструкции по сообщениям об уязвимостях: [SECURITY.md](SECURITY.md).

## Источники курсов
Expand All @@ -52,15 +55,15 @@

## Сборка

Нужны JDK 17 и Android SDK 35.
Нужны JDK 17 и Android SDK 36.

```shell
./gradlew test lint assembleDebug
./gradlew test lint assembleDebug assembleRelease
```

Отладочный APK появится в `app/build/outputs/apk/debug/app-debug.apk`.

Релизные APK подписываются владельцем проекта локально. Ключ подписи и его пароль нельзя добавлять в Git, задачи CI или публичные архивы.
Релизные APK подписываются владельцем проекта локально. Ключ подписи и его пароль нельзя добавлять в Git, задачи CI или публичные архивы. Полная проверяемая процедура описана в [инструкции по выпуску](RELEASING.md).

## Участие в разработке

Expand Down
19 changes: 19 additions & 0 deletions RELEASE_NOTES_1.8.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Чистый курс 1.8.1

Технический выпуск, который делает обновление курсов и публикацию APK проверяемыми.

## Что изменилось

- приложение отличает свежую дату курса от времени последней проверки и предупреждает об устаревших данных;
- ответы сервисов ограничены по размеру, перенаправления остаются только на HTTPS, XML Банка России защищён от внешних сущностей;
- график сохраняет календарное расстояние между датами и сообщает о пропущенных точках;
- выбор дат графика доступен через действия TalkBack;
- кэш истории ограничен 24 наборами, а одна загрузка — 60 секундами; при тайм-ауте доступен ранее сохранённый график;
- резервные источники используются и тогда, когда основной адрес ответил некорректными или устаревшими данными;
- предупреждение учитывает только источники, которые действительно используются выбранными валютами ЦБ или ручными курсами;
- последний открытый экран, валютная пара, период и сумма графика восстанавливаются после перезапуска;
- смена источника во время загрузки безопасно запускает необходимое повторное обновление;
- целевая версия Android обновлена до API 36;
- CI собирает debug и release, запускает инструментальные тесты на Android 15, раздельно сохраняет APK и таблицу деобфускации, а для тега создаёт аттестацию происхождения неподписанного APK.

Данные, настройки и последний экран сохраняются. Обновление устанавливается поверх 1.8.0 без удаления пользовательских данных.
66 changes: 66 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Проверяемый выпуск Android-приложения

Эта процедура связывает исходный коммит, проверку CI, неподписанную сборку, локальную подпись и опубликованный APK. Секретный ключ остаётся только у владельца.

## Один раз для репозитория

1. Включить **Settings → General → Releases → Release immutability**. Правило действует только на будущие выпуски.
2. Включить **Settings → Security → Advanced Security → Private vulnerability reporting**, Dependabot alerts и Dependabot security updates.
3. Защитить `main`: изменения через pull request и обязательная успешная проверка **Тесты, анализ и сборка**.
4. Настроить подпись тегов Git и проверить, что GitHub показывает метку **Verified**.

## Проверка исходного кода

1. Убедиться, что версия в `app/build.gradle.kts`, `CHANGELOG.md` и заметках выпуска совпадает.
2. Выполнить на чистом рабочем дереве:

```shell
./gradlew --no-daemon test lint assembleDebug assembleRelease
git diff --check
```

3. Слить прошедший ревью pull request в `main`.
4. Создать подписанный тег на точном коммите выпуска и отправить его:

```shell
git tag -s v1.8.1 -m "Чистый курс 1.8.1"
git push origin v1.8.1
```

## Получение и подпись APK

1. Дождаться успешной задачи GitHub Actions для тега.
2. Скачать артефакт `CleanRate-unsigned-COMMIT_SHA` из этой задачи.
3. Проверить его происхождение:

```shell
gh attestation verify app-release-unsigned.apk --repo Etogerman/clean-rate-android
```

4. Подписать APK локально штатным `apksigner`, не копируя ключ в репозиторий или CI:

```shell
apksigner sign --ks /БЕЗОПАСНЫЙ/ПУТЬ/release.jks --out CleanRate-1.8.1.apk app-release-unsigned.apk
apksigner verify --verbose --print-certs CleanRate-1.8.1.apk
```

Ожидаемый SHA-256-отпечаток сертификата:

```text
1798311602fa64b2f43832bcd16252d6ae338654653f9b149f4b33261b83ded0
```

5. Создать контрольную сумму:

```shell
shasum -a 256 CleanRate-1.8.1.apk > CleanRate-1.8.1-SHA256SUMS.txt
```

## Публикация

1. Создать черновик GitHub Release на уже проверенном теге `v1.8.1`.
2. Приложить `CleanRate-1.8.1.apk`, файл контрольной суммы и текст `RELEASE_NOTES_1.8.1.md`.
3. Ещё раз скачать файлы из черновика, сверить SHA-256, имя пакета, `versionCode`, `versionName` и сертификат.
4. Опубликовать выпуск. После публикации не заменять файлы; исправления выпускать новой версией.

Подписанный APK закономерно отличается по SHA-256 от неподписанного артефакта CI: блок подписи добавляется после сборки. Проверка сертификата подтверждает владельца, аттестация — происхождение исходной неподписанной сборки, а неизменяемый Release защищает опубликованные файлы от последующей замены.
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

Не публикуйте ключи, пароли, персональные данные или подробности реально эксплуатируемой уязвимости в открытом Issue.

После публикации репозитория используйте **Security → Advisories → New draft security advisory**. Если функция недоступна, создайте краткий Issue без технических подробностей и попросите владельца проекта открыть приватный канал.
Используйте **Security → Advisories → Report a vulnerability**. Эта форма передаёт отчёт владельцу приватно. Не создавайте публичный Issue с техническими подробностями уязвимости.

## Проверка APK

Скачивайте APK только из раздела Releases этого репозитория и сверяйте SHA-256 с файлом `SHA256SUMS.txt`, приложенным к тому же выпуску.
Скачивайте APK только из раздела Releases этого репозитория и сверяйте SHA-256 с файлом `SHA256SUMS.txt`, приложенным к тому же выпуску. Дополнительно проверьте сертификат командой `apksigner verify --print-certs CleanRate-VERSION.apk`: SHA-256 должен совпасть с отпечатком из README.
Loading