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
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
name: CI
run-name: CI · ${{ github.event.pull_request.title || github.ref_name }}

on:
workflow_dispatch:
inputs:
release_dry_run_version:
description: "Optional release version X.Y.Z for a read-only check with live changelog generation"
required: false
type: string
pull_request:
push:
branches: [main]
Expand All @@ -14,7 +20,40 @@ concurrency:
cancel-in-progress: true

jobs:
release-dry-run:
name: Release dry run
if: github.event_name == 'workflow_dispatch' && inputs.release_dry_run_version != ''
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- name: Check release configuration and generate changelogs
env:
GH_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_RELEASE_MODEL: ${{ vars.OPENAI_RELEASE_MODEL || secrets.OPENAI_RELEASE_MODEL }}
RELEASE_VERSION: ${{ inputs.release_dry_run_version }}
run: |
test -n "$GH_TOKEN" && test -n "$OPENAI_API_KEY" && test -n "$OPENAI_RELEASE_MODEL"
gh auth setup-git
python3 scripts/release_automation.py prepare --version "$RELEASE_VERSION" --dry-run

release-identity:
name: Release CI identity
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: 'Comparison base: ${{ github.event.pull_request.base.sha }}'
run: 'true'
- name: 'Pull request: ${{ github.event.pull_request.number }}'
run: 'true'

android:
name: Android tests and checks
if: github.event_name != 'workflow_dispatch' || inputs.release_dry_run_version == ''
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
Expand Down Expand Up @@ -64,6 +103,7 @@ jobs:
run: echo "[Download debug APK and unsigned release APK]($ARTIFACT_URL)" >> "$GITHUB_STEP_SUMMARY"

python:
if: github.event_name != 'workflow_dispatch' || inputs.release_dry_run_version == ''
name: Python tests and style
runs-on: ubuntu-latest
timeout-minutes: 10
Expand All @@ -84,6 +124,8 @@ jobs:
- run: bundle exec fastlane android python_checks

dev-server:
name: Development server tests
if: github.event_name != 'workflow_dispatch' || inputs.release_dry_run_version == ''
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
Expand Down
74 changes: 74 additions & 0 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Prepare and merge release
run-name: Release v${{ inputs.version }}

on:
workflow_dispatch:
inputs:
version:
description: "New version X.Y.Z; merges after full CI, then publishes via the tag workflow"
required: true
type: string

permissions:
contents: read

concurrency:
group: prepare-release
cancel-in-progress: false

jobs:
prepare:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
pr: ${{ steps.prepare.outputs.pr }}
head: ${{ steps.prepare.outputs.head }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4.10"
bundler-cache: true
- name: Prepare release PR
id: prepare
env:
GH_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_RELEASE_MODEL: ${{ vars.OPENAI_RELEASE_MODEL || secrets.OPENAI_RELEASE_MODEL }}
RELEASE_VERSION: ${{ inputs.version }}
run: |
test -n "$GH_TOKEN" || { echo 'Configure RELEASE_BOT_TOKEN'; exit 1; }
test -n "$OPENAI_API_KEY" || { echo 'Configure OPENAI_API_KEY'; exit 1; }
test -n "$OPENAI_RELEASE_MODEL" || { echo 'Configure OPENAI_RELEASE_MODEL in Actions Variables or Secrets'; exit 1; }
gh auth setup-git
bundle exec fastlane android release_prepare

finalize:
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 70
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4.10"
bundler-cache: true
- name: Require full CI, merge, and tag
env:
GH_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }}
RELEASE_VERSION: ${{ inputs.version }}
RELEASE_PR: ${{ needs.prepare.outputs.pr }}
RELEASE_HEAD: ${{ needs.prepare.outputs.head }}
run: |
test -n "$GH_TOKEN" || { echo 'Configure RELEASE_BOT_TOKEN'; exit 1; }
gh auth setup-git
bundle exec fastlane android release_finish
11 changes: 10 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ jobs:
version_name="$(sed -nE 's/^[[:space:]]*versionName = "([^"]+)"/\1/p' app/build.gradle.kts)"
test "$GITHUB_REF_NAME" = "v$version_name"
git merge-base --is-ancestor HEAD origin/main
version_code="$(sed -nE 's/^[[:space:]]*versionCode = ([0-9]+)/\1/p' app/build.gradle.kts)"
test -s "fastlane/metadata/android/en-US/changelogs/$version_code.txt"
test -s "fastlane/metadata/android/ru-RU/changelogs/$version_code.txt"
- uses: actions/setup-java@v5
with:
distribution: temurin
Expand Down Expand Up @@ -102,8 +105,14 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
version_code="$(sed -nE 's/^[[:space:]]*versionCode = ([0-9]+)/\1/p' app/build.gradle.kts)"
notes="$RUNNER_TEMP/release-notes.md"
en="fastlane/metadata/android/en-US/changelogs/$version_code.txt"
ru="fastlane/metadata/android/ru-RU/changelogs/$version_code.txt"
test -s "$en" && test -s "$ru"
{ printf '## English\n\n'; cat "$en"; printf '\n## Русский\n\n'; cat "$ru"; } > "$notes"
cd dist/release
sha256sum -c SHA256SUMS
gh release create "$GITHUB_REF_NAME" ./*.apk mapping.txt SHA256SUMS \
--repo "$GITHUB_REPOSITORY" --verify-tag --generate-notes \
--repo "$GITHUB_REPOSITORY" --verify-tag --notes-file "$notes" \
--title "Message487 $GITHUB_REF_NAME"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ google-services.json
# Android Profiling
*.hprof
.bundle/
vendor/bundle/
.kotlin/
.DS_Store
fastlane/report.xml
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ A custom webhook is also supported. Telegram forwarding is one possible workflow
app does not depend on Telegram.

**Status:** development preview with notification/SMS capture, a persistent encrypted outbox,
background delivery, automatic retries and a delivery journal. Webhook requests require a Bearer token, stored encrypted on the device. Signed APK release automation is configured; see [Releases](docs/en/releases.md).
background delivery, automatic retries and a delivery journal. Webhook requests require a Bearer token, stored encrypted on the device. Signed APK release automation is configured; see [Releases](docs/en/releases.md) and [release automation](docs/en/release-automation.md).

## Getting started

Expand Down Expand Up @@ -127,7 +127,7 @@ Fastlane's `debug_artifact` lane builds only the debug APK. `checks` runs JVM/Ro
debug/release lint, and builds debug and unsigned release APKs under `app/build/outputs/apk/`.

PR CI has no release signing credentials and does not require an emulator.
For signed APK releases, see [Releases](docs/en/releases.md).
For signed APK releases, see [Releases](docs/en/releases.md) and [release automation](docs/en/release-automation.md).
Store graphics and their provenance are documented in [Branding](assets/branding/README.md).

See the [project context](docs/en/project-context.md) for remaining product decisions.
Expand Down
14 changes: 14 additions & 0 deletions docs/en/apk-installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ certificate before opening Android's installer. Allow installation from Message4
return to the update screen and press **Install update** again. Different signing keys cannot
update one another; keep the existing app data and use the matching distribution.

If installation is blocked on Samsung Galaxy, see **Samsung Galaxy: Auto Blocker** below.

## Download and install

Expand All @@ -34,10 +35,23 @@ The app requires Android 8.0 or newer. Use the APK attached to the project relea
not a repackaged copy. Release assets include `SHA256SUMS` for checking file integrity.
Menu names vary by Android version and manufacturer.

## Samsung Galaxy: Auto Blocker

If **Auto Blocker** blocks installation, open **Settings → Security and privacy →
Auto Blocker**, temporarily turn it off and retry installing the APK from the official
release. You still need to allow installation from the browser or file manager;
for an update downloaded inside the app, allow installation from **Message487**.

Turn Auto Blocker back on after installation. When enabled, it must be turned off again
before the next APK update, including one downloaded inside Message487. Menu names depend
on the model and One UI version.
[Samsung instructions](https://www.samsung.com/us/support/answer/ANS10003636/).

## Identify the blocking screen

| What you see | Next step |
| --- | --- |
| Samsung reports an Auto Blocker restriction | Follow the Samsung Galaxy section above |
| Installation from this source is not allowed | Grant the browser/file manager permission as above |
| Play Protect suggests scanning an unknown app | Run the offered scan and follow its result |
| Play Protect blocks installation because the app requests sensitive data | Read the Play Protect section below |
Expand Down
103 changes: 103 additions & 0 deletions docs/en/release-automation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Release automation

[English](release-automation.md) | [Русский](../ru/release-automation.md)

Once the implementation is merged into `main`, open **Actions → Prepare and merge release →
Run workflow**, choose `main` and enter a new version without `v`. This authorizes a release PR,
squash merge after full CI, and a tag on the verified merged commit. **Release Android artifacts**
then signs and publishes the APK. Merging the implementation does not itself release a version.

## One-time manual setup

In [Settings → Secrets and variables → Actions](https://github.com/andre487/AndroidMessage487/settings/secrets/actions), add:

| Type | Name | Value |
| --- | --- | --- |
| Secret | `OPENAI_API_KEY` | API-project key for changelog generation; requests are billed to that project. |
| Variable or Secret | `OPENAI_RELEASE_MODEL` | A model available to that project supporting Responses API Structured Outputs, for example `gpt-4o-mini`. Variables take precedence; no model substitution. |
| Secret | `RELEASE_BOT_TOKEN` | Expiring fine-grained PAT scoped only to this repository: Contents read/write, Pull requests read/write, Actions read. Renew before expiration. |

A separate token lets PR CI and the tag workflow run automatically; see
[GitHub token behavior](https://docs.github.com/en/actions/concepts/security/github_token).
For this personally owned repository, create the fine-grained PAT as its owner, `andre487`.
A separate bot cannot use a fine-grained PAT to write to another user's public repository.
For a separate bot, invite it as a collaborator and use its classic PAT with `public_repo`;
that token is not restricted to one repository. A bot fine-grained PAT requires an organization-owned
repository and organization membership. See [PAT limitations](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens).
Never put tokens in PRs or logs.

Keep the existing `ANDROID_SIGNING_KEY_BASE64`, `ANDROID_KEYSTORE_PASSWORD`,
`ANDROID_KEY_ALIAS` and `ANDROID_KEY_PASSWORD` secrets. Preparation and PR CI never receive them.
The tag build keeps the same signing key so existing installations can upgrade.

Enable squash merging in **Settings → General → Pull Requests**. Protect `main` with required
**Release CI identity**, **Android tests and checks**, **Python tests and style** and
**Development server tests**; do not grant the bot a bypass. Require human review if you want to
review notes before merge. The workflow cannot approve itself. After a rejected merge, approve
and rerun the failed finalize job. Required merge queues are unsupported. Tag rules must allow
the bot to create `v*` tags.

## Each release

1. Merge intended changes and wait for CI. On a device, check SMS/notification delivery, queue and
settings preservation during upgrade, and update checks. CI does not replace device testing.
2. Run [Prepare and merge release](https://github.com/andre487/AndroidMessage487/actions/workflows/prepare-release.yml)
from `main` with a version higher than the app and latest stable tag; for example `0.0.6` after
`0.0.5`. Running it authorizes automatic merge and publication after checks pass.
3. Read the EN/RU notes in the generated PR linked from the Actions summary. Approve if required.
If merge was rejected, approve and choose **Re-run failed jobs**. Do not start preparation again
for the same version: existing branches are deliberately not overwritten.
4. Wait for **Release Android artifacts**. Check both language sections, `message487.apk`, the
versioned APK, `mapping.txt` and `SHA256SUMS`. Install the published APK over the previous version
and confirm data preservation.
5. Check F-Droid publication; update the external `fdroid/fdroiddata` recipe if needed with the
release commit SHA, versionName/versionCode and APK source. This workflow does not update the
recipe or control F-Droid publication timing. See [signed releases](releases.md) for reproducibility.

## Dry run before publication

In **Actions → CI → Run workflow**, select the implementation branch and enter
`release_dry_run_version`, for example `0.0.6`. The check reads the repository and
Actions through `RELEASE_BOT_TOKEN`, checks squash merge availability and generates
real EN/RU notes through OpenAI. This is a billed API request. Results appear in the
Actions summary. It does not write changelogs, create branches/PRs, merge, tag, sign
or publish. It does not prove write permissions or satisfaction of branch protection.
Leaving the field empty runs normal CI. Local equivalent from a clean checkout:
`bundle exec fastlane android release_prepare version:0.0.6 dry_run:true`.

## Behavior and recovery

Generation sends commit messages and diff statistics since the highest stable `vX.Y.Z` tag
reachable from the selected `main` commit to OpenAI, not source code or signing secrets.
History over 100,000 characters, API errors, refusals and invalid responses stop before file writes.
[Structured Outputs](https://developers.openai.com/api/docs/guides/structured-outputs) validates
format, not factual accuracy; notes remain visible in the PR.

The `release/vX.Y.Z` commit changes only `versionName`, `versionCode` incremented by one, and two
new `fastlane/metadata/android/{en-US,ru-RU}/changelogs/<versionCode>.txt` files, each 1–500 characters.
Historical notes remain unchanged. GitHub Release uses the same texts. You choose the version;
prereleases are unsupported.

Finalize waits up to 60 minutes for successful full CI for that PR, head SHA and comparison base.
Skipped/neutral jobs are not success. Changed PRs or an advanced `main` stop merge. The merged
tree must equal the checked head tree before a tag is created on the actual merged commit.
Finalize runs trusted workflow code, never code from the release PR. Retries never force-push,
move tags or overwrite published Releases.

For failed CI, fix the cause and rerun checks, then rerun failed preparation jobs if head/base
are unchanged. Generation is not repeated. If merged but untagged, retry finalize. For a failed
tag build, retry that build without moving the tag.

If head/base changed, deliberately update the release branch so its single release commit is
based on current `main`, then wait for full CI. From a trusted `main` checkout with authorized
`gh` and Fastlane, finish with:

```sh
export GITHUB_REPOSITORY=andre487/AndroidMessage487
bundle exec fastlane android release_finish version:0.0.6 pr:123 head:FULL_40_CHARACTER_SHA
```

This merges and tags; it is not a dry run. If branch push succeeded but PR creation failed,
manually create and inspect the PR before using this command. Local preparation from a clean
current `main` checkout with the same API/token/model configuration:
`bundle exec fastlane android release_prepare version:0.0.6`.
6 changes: 5 additions & 1 deletion docs/en/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

Installing on a phone? See [APK installation and Android restrictions](apk-installation.md).

Use [release automation](release-automation.md) to prepare the version and EN/RU changelog.
The guide covers manual secret setup and release dispatch.

Run `bundle exec fastlane android release_artifacts` with JDK 21 and Android SDK 36.
The lane runs Android JVM/Compose tests and debug/release lint, then builds a signed release APK. It checks the APK certificate, package/version and non-debuggable
flag.
Expand Down Expand Up @@ -50,7 +53,8 @@ GitHub Release. PR workflows do not consume signing secrets.
- After the workflow is merged into the default branch, manual dispatch also builds artifacts only.
- For publication, increment `versionCode`, set the intended `versionName` in `app/build.gradle.kts`,
and merge the reviewed change after all required PR checks pass. Push the matching `v<versionName>`
tag. The workflow requires the tag commit to be contained in `main` and rejects a version mismatch.
tag. The workflow requires the tag commit to be contained in `main`, the version to match the tag,
and EN/RU changelogs for the current `versionCode`. GitHub Release uses these texts.
It publishes the verified APKs, mapping and checksums to GitHub Releases. An existing Release is
not overwritten by a rerun.

Expand Down
Loading
Loading