diff --git a/AGENTS.md b/AGENTS.md index 446a043..f5e652f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,6 +72,8 @@ branch names, credentials, signing material, or other secrets. - Release builds and signing are separate from PR CI. Release artifacts are created only through the dedicated release workflow/Fastlane lane. +- Automated release PRs use a merge commit preserving the release branch history; verify both + parents and the tested tree before tagging the actual merge commit. - Tagged releases publish signed APKs and `SHA256SUMS` to GitHub Releases. Google Play integration is removed. Document installation from release assets; PR APKs are for testing only. - Tagged releases scan all signed APKs with VirusTotal before publication using the release-only diff --git a/docs/en/fastlane.md b/docs/en/fastlane.md index d6b8b1a..d1dec3e 100644 --- a/docs/en/fastlane.md +++ b/docs/en/fastlane.md @@ -51,7 +51,7 @@ That command lists the lanes available in the checked-out version of the project | `bundle exec fastlane android test` | Runs `native_tests` and `android_checks`; this is the normal pre-commit command; excludes Python, Docker integration and emulator tests. | | `bundle exec fastlane android debug_artifact` | Builds `app/build/outputs/apk/debug/app-debug.apk`. | | `bundle exec fastlane android release_prepare version:0.1.2` | Generates EN/RU notes, increments the version and creates a release PR (requires API/token setup). | -| `bundle exec fastlane android release_finish version:0.1.2 pr:123 head:FULL_SHA` | Requires full CI, squash merges the specified PR head and tags the merged commit. | +| `bundle exec fastlane android release_finish version:0.1.2 pr:123 head:FULL_SHA` | Requires full CI, merges the specified PR head with a merge commit and tags the merged commit. | | `bundle exec fastlane android virus_total` | Scan signed APKs in `dist/release` (or `artifacts:PATH`); requires `VIRUSTOTAL_API_KEY`, blocks on detections/API failures. | | `bundle exec fastlane android release_artifacts` | Builds and verifies the signed release APKs and `SHA256SUMS` in `dist/release`. | diff --git a/docs/en/release-automation.md b/docs/en/release-automation.md index 9117875..ff648e8 100644 --- a/docs/en/release-automation.md +++ b/docs/en/release-automation.md @@ -2,7 +2,7 @@ Run **Actions → Prepare and merge release → Run workflow**, select `main`, and enter an explicit new version such as `0.1.2` (without `v`). Starting this workflow authorizes -creation of a release PR, automatic squash merge after full CI, and a release tag. +creation of a release PR, automatic merge with a merge commit after full CI, and a release tag. The existing **Release Android artifacts** workflow then builds/signs/publishes APKs. This does not replace the [device release review](release-testing.md). @@ -38,14 +38,14 @@ Keep the existing Android signing secrets for the tag workflow: `ANDROID_SIGNING_KEY_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD`. The preparation and PR checks do not receive them. -Enable squash merges. Protect `main`, require `Change scope`, `Python tests and style`, +Enable merge commits. Protect `main`, require `Change scope`, `Python tests and style`, `Native Go tests`, `Android tests and checks`, `Android emulator API 26 / Device tests`, and `Android emulator API 35 / Device tests`, and disable bypass for the bot. If policy requires human review, approve the generated PR yourself; automation does not approve its own PR or bypass that policy. A denied merge stops finalization; rerun the failed finalization job after approval. Merge queues are not supported by -this direct squash-merge workflow. Tag rules must permit the bot to create `v*` tags. +this direct merge workflow. Tag rules must permit the bot to create `v*` tags. Use a separate bot account if you want its permissions distinct from your own. This initial implementation uses a PAT; GitHub App token minting/renewal is not @@ -85,9 +85,10 @@ allows PR CI and the tag-triggered release build to run normally. are insufficient. The recorded comparison base must match the release parent. The PR number is also recorded as a successful CI step because GitHub may remove run-to-PR links after merge; this allows a verified retry of tag creation. -7. Recheck PR identity and unchanged `main`, then request squash merge with the +7. Recheck PR identity and unchanged `main`, then request a merge commit with the expected head SHA. GitHub protections still apply. Verify that the resulting - commit belongs to `main` and has exactly the tested head's tree. + commit belongs to `main`, has the verified base and head as its two parents, + and has exactly the tested head's tree. 8. Create lightweight tag `vX.Y.Z` on **the actual merged commit**, never on the release branch head or whatever `main` happens to point to later. The PAT causes the existing tag build to run. Its new GitHub Release uses the same committed diff --git a/docs/ru/fastlane.md b/docs/ru/fastlane.md index ca70ed3..036be43 100644 --- a/docs/ru/fastlane.md +++ b/docs/ru/fastlane.md @@ -52,7 +52,7 @@ bundle exec fastlane lanes | `bundle exec fastlane android test` | Выполняет `native_tests` и `android_checks`; основная команда перед коммитом; без Python, Docker-интеграции и тестов на эмуляторах. | | `bundle exec fastlane android debug_artifact` | Собирает `app/build/outputs/apk/debug/app-debug.apk`. | | `bundle exec fastlane android release_prepare version:0.1.2` | Генерирует EN/RU changelog, повышает версию и создаёт release PR (нужна настройка API/токенов). | -| `bundle exec fastlane android release_finish version:0.1.2 pr:123 head:FULL_SHA` | Ждёт полного CI, выполняет squash merge указанного head PR и тегирует итоговый commit. | +| `bundle exec fastlane android release_finish version:0.1.2 pr:123 head:FULL_SHA` | Ждёт полного CI, выполняет слияние указанного head PR с отдельным merge-коммитом и тегирует итоговый commit. | | `bundle exec fastlane android virus_total` | Проверяет подписанные APK в `dist/release` (или `artifacts:PATH`); нужен `VIRUSTOTAL_API_KEY`, обнаружения/ошибки API блокируют публикацию. | | `bundle exec fastlane android release_artifacts` | Собирает и проверяет подписанные APK и `SHA256SUMS` в `dist/release`. | diff --git a/docs/ru/release-automation.md b/docs/ru/release-automation.md index d37ff50..be7c847 100644 --- a/docs/ru/release-automation.md +++ b/docs/ru/release-automation.md @@ -2,7 +2,7 @@ Откройте **Actions → Prepare and merge release → Run workflow**, выберите `main` и задайте новую версию, например `0.1.2` (без `v`). Запуск разрешает создание -релизного PR, автоматический squash merge после полного CI и создание тега. +релизного PR, автоматическое слияние с merge-коммитом после полного CI и создание тега. Существующий **Release Android artifacts** затем соберёт, подпишет и опубликует APK. Это не заменяет [проверку релиза на устройствах](release-testing.md). @@ -38,13 +38,13 @@ Actions. PR CI не получает API-ключ и не загружает APK `ANDROID_SIGNING_KEY_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD`. Подготовка и проверки PR их не получают. -Разрешите squash merge. Защитите `main`, сделайте обязательными `Change scope`, +Разрешите merge-коммиты. Защитите `main`, сделайте обязательными `Change scope`, `Python tests and style`, `Native Go tests`, `Android tests and checks`, `Android emulator API 26 / Device tests`, `Android emulator API 35 / Device tests` и отключите обход правил для бота. Если правила требуют человеческое ревью, одобрите созданный PR самостоятельно: workflow не одобряет себя и не обходит запрет. После отказа merge можно одобрить PR и перезапустить упавший finalize job. Merge queue этим -workflow с прямым squash merge не поддерживается. Правила тегов должны разрешать +workflow с прямым merge не поддерживается. Правила тегов должны разрешать боту создавать `v*`. Отдельный аккаунт бота позволяет отделить его права от ваших. В первой реализации @@ -87,9 +87,10 @@ workflow с прямым squash merge не поддерживается. Пра релизного commit. Номер PR также сохраняется успешным шагом CI: GitHub может убрать связь run с PR после merge, а этот шаг позволяет проверить повторное создание тега. -7. Повторная проверка PR и неизменности `main`, squash merge с ожидаемым head SHA. +7. Повторная проверка PR и неизменности `main`, merge с отдельным merge-коммитом и ожидаемым head SHA. Правила GitHub продолжают действовать. Проверка принадлежности итогового - commit к `main` и точного совпадения его дерева с проверенным head. + commit к `main`, двух родителей (проверенные base и head) и точного + совпадения его дерева с проверенным head. 8. Lightweight-тег `vX.Y.Z` ставится **на фактический commit после merge**, не на head ветки и не на более поздний случайный `main`. PAT запускает обычную сборку по тегу. Новый GitHub Release использует те же EN/RU тексты из Git. diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 3e6a0a8..3a20bde 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -158,7 +158,7 @@ platform :android do end end - desc "Wait for full release PR CI, squash merge, and tag its verified merged commit" + desc "Wait for full release PR CI, create a merge commit, and tag its verified merged commit" lane :release_finish do |options| version = options[:version] || ENV.fetch("RELEASE_VERSION") pr = options[:pr] || ENV.fetch("RELEASE_PR") diff --git a/scripts/release_automation.py b/scripts/release_automation.py index b7fa67c..05c00d5 100644 --- a/scripts/release_automation.py +++ b/scripts/release_automation.py @@ -409,7 +409,7 @@ def finish(version, number, head): ) result = api( f"pulls/{number}/merge", - {"sha": head, "merge_method": "squash"}, + {"sha": head, "merge_method": "merge"}, method="PUT", ) require( @@ -420,6 +420,10 @@ def finish(version, number, head): require(pr["merged"], "PR is not merged; refusing tag") merged = pr["merge_commit_sha"] command("git", "fetch", "origin", "main") + require( + command("git", "show", "-s", "--format=%P", merged).split() == [parent, head], + "Release must have a merge commit with the verified base and head; refusing tag", + ) require( command("git", "show", "-s", "--format=%T", merged) == command("git", "show", "-s", "--format=%T", head), diff --git a/scripts/tests/test_release_automation.py b/scripts/tests/test_release_automation.py index 40decc7..50f822f 100644 --- a/scripts/tests/test_release_automation.py +++ b/scripts/tests/test_release_automation.py @@ -306,12 +306,11 @@ def fake_api(path, payload=None, method=None): return copy.deepcopy(pr) if path == "pulls/7/merge": self.assertEqual( - {"sha": pr["head"]["sha"], "merge_method": "squash"}, payload + {"sha": pr["head"]["sha"], "merge_method": "merge"}, payload ) self.assertEqual("PUT", method) git("switch", "-q", "main") - git("merge", "--squash", pr["head"]["sha"]) - git("commit", "-qm", "Release v0.1.2") + git("merge", "--no-ff", "-qm", "Release v0.1.2", pr["head"]["sha"]) git("push", "-q", "origin", "main") pr.update( merged=True, @@ -427,6 +426,19 @@ def advanced_main(path, payload=None, method=None): git("rev-parse", "refs/tags/v0.1.2", cwd=remote), ) self.assertNotEqual(head, pr["merge_commit_sha"]) + self.assertEqual( + [base, head], + git( + "show", "-s", "--format=%P", pr["merge_commit_sha"] + ).split(), + ) + merged_commit = pr["merge_commit_sha"] + pr["merge_commit_sha"] = head + with self.assertRaisesRegex( + RuntimeError, "must have a merge commit" + ): + m.finish("0.1.2", 7, head) + pr["merge_commit_sha"] = merged_commit m.finish("0.1.2", 7, head) self.assertEqual(1, calls.count("pulls/7/merge")) self.assertEqual(1, calls.count("git/refs"))