mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
docs: add maintainer release runbook
This commit is contained in:
parent
da5650ceb4
commit
5edc9f7d85
@ -71,8 +71,10 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||||
The app requests Bluetooth, location (required for BLE scanning), and notification permissions at runtime.
|
||||
|
||||
Release APKs and the Android App Bundle can be rebuilt byte-for-byte in the
|
||||
pinned Linux container. See [Reproducible builds](docs/reproducible-builds.md)
|
||||
for the build, GitHub release, and Google Play verification procedures.
|
||||
pinned Linux container. Maintainers should follow the
|
||||
[Android release guide](docs/maintainer-release-guide.md). See
|
||||
[Reproducible builds](docs/reproducible-builds.md) for the build trust model
|
||||
and public GitHub/Google Play verification procedures.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
582
docs/maintainer-release-guide.md
Normal file
582
docs/maintainer-release-guide.md
Normal file
@ -0,0 +1,582 @@
|
||||
# Android maintainer release guide
|
||||
|
||||
This is the operational runbook for publishing a Bitchat Android release to
|
||||
GitHub and Google Play. Follow it from top to bottom for every release.
|
||||
|
||||
The central rule is:
|
||||
|
||||
> GitHub Actions builds and attests the unsigned release. A maintainer signs
|
||||
> those exact files locally. Nothing is rebuilt after the tag.
|
||||
|
||||
No keystore or password is stored in GitHub, GitHub Actions, the repository,
|
||||
release notes, or workflow artifacts.
|
||||
|
||||
For the technical trust model and third-party verification instructions, see
|
||||
[Reproducible builds](reproducible-builds.md).
|
||||
|
||||
## Release flow
|
||||
|
||||
| Stage | Where it happens | Result |
|
||||
|---|---|---|
|
||||
| Approve source | Pull requests and the release gate | One reviewed commit on `main` |
|
||||
| Create tag | Maintainer machine | Signed `vX.Y.Z` tag |
|
||||
| Build twice | GitHub Actions | Two identical unsigned APK/AAB builds |
|
||||
| Promote build | GitHub Actions | Attested `verified-unsigned-release` artifact |
|
||||
| Sign | Maintainer machine | Three installable APKs and one Play upload AAB |
|
||||
| Test Play build | Play Console internal track | Play-generated APKs tested before public rollout |
|
||||
| Prepare release | Maintainer machine and GitHub draft | Checksummed public release assets |
|
||||
| Publish | GitHub Releases and Play Console | Public GitHub release and promoted Play rollout |
|
||||
|
||||
## What the signing identities mean
|
||||
|
||||
These are four separate signing identities:
|
||||
|
||||
1. **Git tag signature**: identifies the maintainer who approved the source
|
||||
commit. It is created locally by `git tag -s`.
|
||||
2. **GitHub APK signature**: lets Android install and update the APKs published
|
||||
on GitHub. Use the existing GitHub release key. Its certificate SHA-256 must
|
||||
match `BITCHAT_GITHUB_RELEASE_CERT_SHA256` in `gradle.properties`.
|
||||
3. **Play upload signature**: the maintainer signs the AAB with the Play upload
|
||||
key so Play will accept it.
|
||||
4. **Play app signature**: Google generates device APKs and signs them with the
|
||||
separate Play app-signing key.
|
||||
|
||||
APK and AAB signatures are embedded in those files. Do not create or publish
|
||||
detached `.sig` files. GitHub build-provenance attestations are stored by GitHub
|
||||
and verified with `gh attestation verify`; they are not release asset files.
|
||||
|
||||
Never request, export, or use Google's Play app-signing private key during this
|
||||
process.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Access
|
||||
|
||||
The maintainer needs:
|
||||
|
||||
- permission to push a release tag and create GitHub Releases;
|
||||
- a GitHub CLI login authorized for
|
||||
`permissionlesstech/bitchat-android`;
|
||||
- Play Console permission to create and promote releases for
|
||||
`com.bitchat.droid`; and
|
||||
- access to the project's release approval record.
|
||||
|
||||
Check the GitHub login:
|
||||
|
||||
```bash
|
||||
gh auth status
|
||||
```
|
||||
|
||||
### Local tools
|
||||
|
||||
Install:
|
||||
|
||||
- Git;
|
||||
- [GitHub CLI](https://cli.github.com/);
|
||||
- the exact JDK in `tools/reproducible-builds/TOOLCHAIN.env`;
|
||||
- Android SDK Build Tools 37.0.0, including `apksigner` and `zipalign`; and
|
||||
- Docker with Linux/amd64 support if doing the optional independent rebuild.
|
||||
|
||||
Android Studio's SDK Manager can install the required Build Tools version.
|
||||
Select **SDK Tools**, enable **Show Package Details**, and install 37.0.0.
|
||||
|
||||
Load the toolchain pins and check the local tools:
|
||||
|
||||
```bash
|
||||
export JAVA_HOME=/secure/path/to/jdk-21
|
||||
export ANDROID_SDK_ROOT=/secure/path/to/android-sdk
|
||||
source tools/reproducible-builds/TOOLCHAIN.env
|
||||
gh --version
|
||||
"$JAVA_HOME/bin/java" -version
|
||||
"$ANDROID_SDK_ROOT/build-tools/$ANDROID_BUILD_TOOLS_VERSION/apksigner" version
|
||||
"$ANDROID_SDK_ROOT/build-tools/$ANDROID_BUILD_TOOLS_VERSION/zipalign" -h
|
||||
```
|
||||
|
||||
The JDK output must match `JAVA_VERSION`; the Android path must use
|
||||
`ANDROID_BUILD_TOOLS_VERSION`.
|
||||
|
||||
### Local key material
|
||||
|
||||
Keep these in a password manager or encrypted offline storage:
|
||||
|
||||
- the existing GitHub APK release keystore, alias, store password, and key
|
||||
password;
|
||||
- the Play upload keystore, alias, store password, and key password; and
|
||||
- the maintainer's configured Git tag signing key.
|
||||
|
||||
The GitHub APK key and Play upload key may be different. Treat them as different
|
||||
credentials even if the project's historical setup placed them in one
|
||||
keystore.
|
||||
|
||||
Do not put a keystore inside the repository checkout. Do not place passwords on
|
||||
a command line, in a shell profile, or in a release-notes file.
|
||||
|
||||
## 1. Prepare and approve the release commit
|
||||
|
||||
1. Update `versionCode` and `versionName` in `app/build.gradle.kts`.
|
||||
`versionCode` must be greater than every build previously uploaded to Play.
|
||||
`versionName` must match the release tag without the leading `v`.
|
||||
2. Merge all intended release changes into `main`.
|
||||
3. Confirm required CI checks are green.
|
||||
4. Complete the
|
||||
[physical-device and cross-client release gate](release-gate-runbook.md).
|
||||
Keep its privacy-reviewed evidence in the release approval record; do not
|
||||
attach raw device identifiers or logs to the public release.
|
||||
5. Confirm the release notes and user-visible Play changelog are ready.
|
||||
|
||||
Start from a clean, current checkout:
|
||||
|
||||
```bash
|
||||
git switch main
|
||||
git pull --ff-only
|
||||
git status --short
|
||||
git log -1 --oneline
|
||||
```
|
||||
|
||||
`git status --short` must print nothing.
|
||||
|
||||
Set shell variables for the rest of the release:
|
||||
|
||||
```bash
|
||||
export REPOSITORY=permissionlesstech/bitchat-android
|
||||
export TAG=vX.Y.Z
|
||||
export VERSION_CODE=NN
|
||||
export RELEASE_DIR="release-${TAG#v}"
|
||||
```
|
||||
|
||||
Confirm the source version:
|
||||
|
||||
```bash
|
||||
grep -nE 'versionCode|versionName' app/build.gradle.kts
|
||||
test "$(sed -n 's/.*versionName = "\([^"]*\)".*/\1/p' app/build.gradle.kts)" = "${TAG#v}"
|
||||
test "$(sed -n 's/.*versionCode = \([0-9][0-9]*\).*/\1/p' app/build.gradle.kts)" = "$VERSION_CODE"
|
||||
```
|
||||
|
||||
Stop if either `test` command fails.
|
||||
|
||||
## 2. Create and push the release tag
|
||||
|
||||
Create a signed annotated tag on the approved commit:
|
||||
|
||||
```bash
|
||||
git tag -s "$TAG" -m "Bitchat Android $TAG"
|
||||
git tag -v "$TAG"
|
||||
git push origin "$TAG"
|
||||
```
|
||||
|
||||
Do not create the GitHub Release yet. Pushing the tag starts the `Release`
|
||||
workflow, which checks out that exact tag.
|
||||
|
||||
Never move or replace a release tag after pushing it. If source must change,
|
||||
increment `versionCode`, create a new version, and use a new tag.
|
||||
|
||||
## 3. Wait for the reproducible GitHub build
|
||||
|
||||
Find the `Release` workflow run:
|
||||
|
||||
```bash
|
||||
gh run list \
|
||||
--repo "$REPOSITORY" \
|
||||
--workflow release.yml \
|
||||
--limit 20
|
||||
```
|
||||
|
||||
Copy the run ID for `$TAG`, then:
|
||||
|
||||
```bash
|
||||
export RUN_ID=123456789
|
||||
gh run watch "$RUN_ID" --repo "$REPOSITORY" --exit-status
|
||||
gh run view "$RUN_ID" --repo "$REPOSITORY"
|
||||
```
|
||||
|
||||
The run is successful only when:
|
||||
|
||||
- both independent unsigned build jobs pass;
|
||||
- the byte-for-byte comparison passes;
|
||||
- provenance attestation succeeds; and
|
||||
- `verified-unsigned-release` is uploaded.
|
||||
|
||||
If the tag already existed and the workflow must be dispatched manually, run
|
||||
it against the tag ref:
|
||||
|
||||
```bash
|
||||
gh workflow run release.yml \
|
||||
--repo "$REPOSITORY" \
|
||||
--ref "$TAG" \
|
||||
-f tag="$TAG"
|
||||
```
|
||||
|
||||
The workflow rejects a dispatch from a different ref because that would produce
|
||||
incorrect provenance.
|
||||
|
||||
### Accessing the artifacts
|
||||
|
||||
With the CLI:
|
||||
|
||||
```bash
|
||||
test ! -e "$RELEASE_DIR"
|
||||
gh run download "$RUN_ID" \
|
||||
--repo "$REPOSITORY" \
|
||||
--name verified-unsigned-release \
|
||||
--dir "$RELEASE_DIR"
|
||||
```
|
||||
|
||||
In the GitHub web interface:
|
||||
|
||||
1. Open **Actions** in the repository.
|
||||
2. Open the **Release** workflow run for `$TAG`.
|
||||
3. Scroll to **Artifacts**.
|
||||
4. Download **verified-unsigned-release**.
|
||||
|
||||
Do not sign `unsigned-release-a` or `unsigned-release-b`. Those are the two
|
||||
replicas retained for diagnostics. Only `verified-unsigned-release` passed the
|
||||
comparison gate. Workflow artifacts expire after 30 days, so finish the release
|
||||
before then.
|
||||
|
||||
## 4. Verify the downloaded unsigned release
|
||||
|
||||
The directory must initially contain exactly these canonical files:
|
||||
|
||||
- `BUILDINFO.json`
|
||||
- `SHA256SUMS.unsigned`
|
||||
- `bitchat-android-arm64-unsigned.apk`
|
||||
- `bitchat-android-armv7-unsigned.apk`
|
||||
- `bitchat-android-universal-unsigned.apk`
|
||||
- `bitchat-android-x86-unsigned.apk`
|
||||
- `bitchat-android-x86_64-unsigned.apk`
|
||||
- `bitchat-android-release-unsigned.aab`
|
||||
|
||||
Verify the checksum manifest:
|
||||
|
||||
```bash
|
||||
(
|
||||
cd "$RELEASE_DIR"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum -c SHA256SUMS.unsigned
|
||||
else
|
||||
shasum -a 256 -c SHA256SUMS.unsigned
|
||||
fi
|
||||
)
|
||||
```
|
||||
|
||||
Verify that `BUILDINFO.json` identifies the tag commit:
|
||||
|
||||
```bash
|
||||
TAG_COMMIT="$(git rev-list -n 1 "$TAG")"
|
||||
ARTIFACT_COMMIT="$(
|
||||
sed -n 's/.*"sourceCommit": *"\([^"]*\)".*/\1/p' \
|
||||
"$RELEASE_DIR/BUILDINFO.json"
|
||||
)"
|
||||
test "$TAG_COMMIT" = "$ARTIFACT_COMMIT"
|
||||
```
|
||||
|
||||
Verify GitHub's provenance for every unsigned APK and AAB:
|
||||
|
||||
```bash
|
||||
for artifact in \
|
||||
"$RELEASE_DIR"/*-unsigned.apk \
|
||||
"$RELEASE_DIR"/*-unsigned.aab
|
||||
do
|
||||
gh attestation verify "$artifact" --repo "$REPOSITORY"
|
||||
done
|
||||
```
|
||||
|
||||
Stop immediately if a checksum, commit, or attestation check fails.
|
||||
|
||||
## 5. Sign the GitHub APKs locally
|
||||
|
||||
Set the SDK and GitHub release-key locations. Load passwords from the password
|
||||
manager without placing their values in shell history:
|
||||
|
||||
```bash
|
||||
export ANDROID_SDK_ROOT=/secure/path/to/android-sdk
|
||||
export BITCHAT_GITHUB_KEYSTORE=/secure/path/to/github-release.jks
|
||||
export BITCHAT_GITHUB_KEY_ALIAS=release-key-alias
|
||||
|
||||
printf 'GitHub keystore password: '
|
||||
IFS= read -r -s BITCHAT_GITHUB_KEYSTORE_PASSWORD
|
||||
printf '\nGitHub key password: '
|
||||
IFS= read -r -s BITCHAT_GITHUB_KEY_PASSWORD
|
||||
printf '\n'
|
||||
export BITCHAT_GITHUB_KEYSTORE_PASSWORD
|
||||
export BITCHAT_GITHUB_KEY_PASSWORD
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
tools/reproducible-builds/sign-release.sh "$RELEASE_DIR"
|
||||
```
|
||||
|
||||
The helper:
|
||||
|
||||
- re-verifies `SHA256SUMS.unsigned`;
|
||||
- uses the pinned Android Build Tools;
|
||||
- signs each selected APK twice and requires identical results;
|
||||
- verifies the embedded APK signatures;
|
||||
- rejects a key whose certificate fingerprint does not match the pinned
|
||||
GitHub release certificate; and
|
||||
- writes `SHA256SUMS`.
|
||||
|
||||
It creates:
|
||||
|
||||
- `bitchat-android-arm64.apk`
|
||||
- `bitchat-android-universal.apk`
|
||||
- `bitchat-android-x86_64.apk`
|
||||
|
||||
The unsigned armv7 and x86 APKs remain available for reproducibility, but are
|
||||
not published as signed install targets under the current release policy.
|
||||
|
||||
If the helper stops after creating any signed file, do not continue or overwrite
|
||||
files manually. Start again in a new directory downloaded from the same
|
||||
successful workflow run.
|
||||
|
||||
## 6. Sign the Google Play AAB locally
|
||||
|
||||
Set the pinned JDK and Play upload-key locations, then load the passwords:
|
||||
|
||||
```bash
|
||||
export JAVA_HOME=/secure/path/to/jdk-21
|
||||
export BITCHAT_PLAY_UPLOAD_KEYSTORE=/secure/path/to/play-upload.jks
|
||||
export BITCHAT_PLAY_UPLOAD_KEY_ALIAS=upload-key-alias
|
||||
|
||||
printf 'Play upload keystore password: '
|
||||
IFS= read -r -s BITCHAT_PLAY_KEYSTORE_PASSWORD
|
||||
printf '\nPlay upload key password: '
|
||||
IFS= read -r -s BITCHAT_PLAY_KEY_PASSWORD
|
||||
printf '\n'
|
||||
export BITCHAT_PLAY_KEYSTORE_PASSWORD
|
||||
export BITCHAT_PLAY_KEY_PASSWORD
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
tools/reproducible-builds/sign-play-bundle.sh "$RELEASE_DIR"
|
||||
```
|
||||
|
||||
The helper creates `bitchat-android-play-upload.aab`, verifies its JAR
|
||||
signature, proves that every non-signature payload entry matches the canonical
|
||||
unsigned AAB, and updates `SHA256SUMS`.
|
||||
|
||||
This is the only file to upload to Play Console:
|
||||
|
||||
```text
|
||||
release-X.Y.Z/bitchat-android-play-upload.aab
|
||||
```
|
||||
|
||||
Do not upload an APK or `bitchat-android-release-unsigned.aab` to Play. Do not
|
||||
open Android Studio and rebuild the bundle.
|
||||
|
||||
Remove passwords from the environment after both signing steps:
|
||||
|
||||
```bash
|
||||
unset BITCHAT_GITHUB_KEYSTORE_PASSWORD
|
||||
unset BITCHAT_GITHUB_KEY_PASSWORD
|
||||
unset BITCHAT_PLAY_KEYSTORE_PASSWORD
|
||||
unset BITCHAT_PLAY_KEY_PASSWORD
|
||||
```
|
||||
|
||||
## 7. Prepare all GitHub Release assets
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
tools/reproducible-builds/prepare-github-release.sh "$RELEASE_DIR"
|
||||
```
|
||||
|
||||
The helper verifies all checksums and renames the public build information and
|
||||
checksum manifests. It refuses missing or pre-existing release files.
|
||||
|
||||
The final GitHub Release must contain all 13 files below:
|
||||
|
||||
| Asset | Signed? | Why it is published |
|
||||
|---|---:|---|
|
||||
| `bitchat-android-arm64.apk` | APK release key | Primary direct-install APK |
|
||||
| `bitchat-android-universal.apk` | APK release key | Fallback direct-install APK |
|
||||
| `bitchat-android-x86_64.apk` | APK release key | x86_64 install APK |
|
||||
| Five `bitchat-android-*-unsigned.apk` files | No | Reproducibility inputs for every ABI target |
|
||||
| `bitchat-android-release-unsigned.aab` | No | Canonical reproducible Play input |
|
||||
| `bitchat-android-play-upload.aab` | Play upload key | Exact bundle uploaded to Play |
|
||||
| `BITCHAT_BUILDINFO.json` | GitHub attestation | Source commit and pinned toolchain |
|
||||
| `BITCHAT_SHA256SUMS.unsigned` | GitHub attestation | Original canonical CI manifest |
|
||||
| `BITCHAT_SHA256SUMS` | No detached signature | SHA-256 for every published asset |
|
||||
|
||||
Run the public checksum verification once more:
|
||||
|
||||
```bash
|
||||
(
|
||||
cd "$RELEASE_DIR"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum -c BITCHAT_SHA256SUMS
|
||||
else
|
||||
shasum -a 256 -c BITCHAT_SHA256SUMS
|
||||
fi
|
||||
)
|
||||
```
|
||||
|
||||
Do not add keystores, certificate exports, passwords, raw release-gate logs,
|
||||
local paths, or device/user identifiers to this directory.
|
||||
|
||||
## 8. Create release notes and a draft GitHub Release
|
||||
|
||||
Create a local release-notes file. At minimum it must contain:
|
||||
|
||||
```markdown
|
||||
## Bitchat Android vX.Y.Z
|
||||
|
||||
- Version code: NN
|
||||
- Source tag: vX.Y.Z
|
||||
- GitHub APK signing certificate SHA-256: FINGERPRINT
|
||||
- Play upload certificate SHA-256: FINGERPRINT
|
||||
- Play app-signing certificate SHA-256: FINGERPRINT
|
||||
|
||||
Checksums, canonical unsigned APK/AAB inputs, the exact Play upload AAB, and
|
||||
build information are attached. See `docs/reproducible-builds.md` for public
|
||||
verification instructions.
|
||||
|
||||
## Changes
|
||||
|
||||
- User-visible change
|
||||
```
|
||||
|
||||
The GitHub APK fingerprint comes from `gradle.properties`. Obtain both Play
|
||||
certificate fingerprints from **Play Console > Setup > App integrity**. Publish
|
||||
only the SHA-256 fingerprints, not certificate subject details or private-key
|
||||
material.
|
||||
|
||||
Create a draft release and upload every prepared asset:
|
||||
|
||||
```bash
|
||||
gh release create "$TAG" "$RELEASE_DIR"/* \
|
||||
--repo "$REPOSITORY" \
|
||||
--verify-tag \
|
||||
--draft \
|
||||
--title "Bitchat Android $TAG" \
|
||||
--notes-file "release-notes-$TAG.md"
|
||||
```
|
||||
|
||||
`--verify-tag` prevents `gh` from silently creating a tag at the wrong commit.
|
||||
GitHub automatically exposes source archives for the tag; do not upload separate
|
||||
source ZIP or tar files.
|
||||
|
||||
Inspect the draft:
|
||||
|
||||
```bash
|
||||
gh release view "$TAG" --repo "$REPOSITORY" --web
|
||||
```
|
||||
|
||||
Keep it as a draft until the Play internal-track checks below pass.
|
||||
|
||||
## 9. Upload and test the AAB in Google Play
|
||||
|
||||
1. Open Play Console and select `com.bitchat.droid`.
|
||||
2. Open **Test and release > Testing > Internal testing**.
|
||||
3. Create a new release.
|
||||
4. Upload exactly:
|
||||
`release-X.Y.Z/bitchat-android-play-upload.aab`.
|
||||
5. Confirm Play accepts the upload signature and reports the expected package,
|
||||
`versionCode`, and `versionName`.
|
||||
6. Add the user-visible Play release notes.
|
||||
7. Resolve blocking Play checks, save the release, and start the internal
|
||||
rollout.
|
||||
8. Install the build through the internal-test opt-in link on a representative
|
||||
physical device. Confirm startup, upgrade from the previous public release,
|
||||
networking, and the release-critical scenarios.
|
||||
9. In **App bundle explorer**, select the uploaded version. If the account
|
||||
permits downloading the original AAB, download it and verify that its digest
|
||||
matches `bitchat-android-play-upload.aab` in `BITCHAT_SHA256SUMS`.
|
||||
10. In **Setup > App integrity**, confirm the Play app-signing certificate
|
||||
SHA-256 is the value recorded in the GitHub release notes.
|
||||
|
||||
Promote this same tested Play release from internal testing to production.
|
||||
Do not rebuild or upload a second AAB for production. Use a staged production
|
||||
rollout when appropriate.
|
||||
|
||||
Google signs the device APKs with the Play app-signing key, so Play-delivered
|
||||
APKs will not be byte-identical to the GitHub APKs. That is expected.
|
||||
|
||||
## 10. Publish GitHub and promote Play
|
||||
|
||||
After the internal Play build passes and the GitHub draft has all 13 assets:
|
||||
|
||||
```bash
|
||||
gh release edit "$TAG" \
|
||||
--repo "$REPOSITORY" \
|
||||
--draft=false \
|
||||
--latest
|
||||
```
|
||||
|
||||
Then complete or schedule the production promotion in Play Console. If managed
|
||||
publishing is enabled, send the approved changes for review and publish them at
|
||||
the coordinated release time.
|
||||
|
||||
Do not replace assets after the GitHub Release is public. If any published
|
||||
binary is wrong, create a new version and release.
|
||||
|
||||
## 11. Verify the public release
|
||||
|
||||
From a clean checkout of the tag:
|
||||
|
||||
```bash
|
||||
git checkout "$TAG"
|
||||
tools/reproducible-builds/verify-github-release.sh "$TAG" --no-rebuild
|
||||
```
|
||||
|
||||
For the strongest check, omit `--no-rebuild` and let the pinned container
|
||||
rebuild and compare all unsigned artifacts:
|
||||
|
||||
```bash
|
||||
tools/reproducible-builds/verify-github-release.sh "$TAG"
|
||||
```
|
||||
|
||||
Also verify:
|
||||
|
||||
- the GitHub release is marked **Latest**;
|
||||
- the three signed APKs install and show the expected version;
|
||||
- the Play listing shows the intended production version and rollout state;
|
||||
- a Play-installed build is signed by the app-signing certificate recorded in
|
||||
the release notes; and
|
||||
- the final GitHub URL and Play status are added to the internal release
|
||||
approval record.
|
||||
|
||||
## Failure and retry rules
|
||||
|
||||
- **A GitHub build or comparison fails:** do not sign anything. Fix the source,
|
||||
increment the version, and create a new tag. Rerun the same tag only for an
|
||||
infrastructure-only failure that did not change source.
|
||||
- **A checksum, provenance, or commit check fails:** stop. Do not publish.
|
||||
- **A local signing helper fails:** use a fresh download directory. Do not
|
||||
overwrite partially created signed artifacts.
|
||||
- **Play rejects the upload key:** stop and resolve the registered upload key in
|
||||
Play Console. Never substitute the app-signing key.
|
||||
- **Play has accepted the version code but the binary must change:** increment
|
||||
`versionCode` and create a new release.
|
||||
- **A GitHub draft is wrong:** keep it private while correcting it from a fresh,
|
||||
verified artifact directory.
|
||||
- **A public release is wrong:** do not move the tag or replace assets. Publish
|
||||
a new version.
|
||||
|
||||
## Final checklist
|
||||
|
||||
- [ ] Version name and version code updated and approved
|
||||
- [ ] Required CI and physical-device release gate passed
|
||||
- [ ] Signed tag pushed from the approved commit
|
||||
- [ ] `Release` workflow and two-build comparison passed
|
||||
- [ ] `verified-unsigned-release` downloaded by run ID
|
||||
- [ ] Checksums, source commit, and attestations verified
|
||||
- [ ] GitHub APKs signed locally with the pinned certificate
|
||||
- [ ] Play AAB signed locally with the registered upload key
|
||||
- [ ] `BITCHAT_SHA256SUMS` verifies all 13 release assets
|
||||
- [ ] GitHub draft created with certificate fingerprints and all assets
|
||||
- [ ] Exact signed AAB uploaded to and tested on Play internal track
|
||||
- [ ] GitHub Release published
|
||||
- [ ] Same tested Play release promoted to production
|
||||
- [ ] Public GitHub and Play verification completed
|
||||
|
||||
## Official references
|
||||
|
||||
- [GitHub CLI: download workflow artifacts](https://cli.github.com/manual/gh_run_download)
|
||||
- [GitHub CLI: create a release](https://cli.github.com/manual/gh_release_create)
|
||||
- [Google Play App Signing](https://support.google.com/googleplay/android-developer/answer/9842756)
|
||||
- [Play Console App bundle explorer](https://support.google.com/googleplay/android-developer/answer/9859152)
|
||||
@ -10,6 +10,11 @@ to GitHub Actions. Anyone can reproduce the unsigned artifacts; maintainers
|
||||
download the verified CI output, sign the selected GitHub APKs and Play upload
|
||||
AAB locally, then manually publish those exact files.
|
||||
|
||||
Maintainers should use the complete
|
||||
[Android maintainer release guide](maintainer-release-guide.md) for the
|
||||
step-by-step release procedure, artifact inventory, local signing commands, and
|
||||
GitHub/Google Play publication checklist.
|
||||
|
||||
## What is pinned
|
||||
|
||||
- Gradle wrapper version and distribution SHA-256
|
||||
@ -202,105 +207,13 @@ uploads it manually to Google Play.
|
||||
|
||||
## Maintainer release process
|
||||
|
||||
Pushing a `vX.Y.Z` tag runs `.github/workflows/release.yml`:
|
||||
Follow the
|
||||
[Android maintainer release guide](maintainer-release-guide.md). It is the
|
||||
authoritative operational runbook from version preparation through the signed
|
||||
tag, GitHub Actions artifact download, local APK/AAB signing, GitHub draft,
|
||||
Play internal test, public rollout, and post-release verification.
|
||||
|
||||
1. two jobs independently build the canonical unsigned release;
|
||||
2. a comparison job requires exact byte equality;
|
||||
3. GitHub generates provenance attestations for the canonical unsigned
|
||||
artifacts; and
|
||||
4. Actions uploads `verified-unsigned-release` for the maintainer to download.
|
||||
|
||||
The workflow has read-only repository access and no signing secrets. It does not
|
||||
create a GitHub Release or upload to Google Play.
|
||||
|
||||
To rerun an existing tag manually, dispatch the workflow against the tag ref
|
||||
(not the default branch):
|
||||
|
||||
```bash
|
||||
gh workflow run release.yml \
|
||||
--repo permissionlesstech/bitchat-android \
|
||||
--ref vX.Y.Z \
|
||||
-f tag=vX.Y.Z
|
||||
```
|
||||
|
||||
The workflow rejects a dispatch whose selected ref and requested tag resolve to
|
||||
different commits, keeping the GitHub provenance tied to the source actually
|
||||
built.
|
||||
|
||||
### 1. Download the verified build
|
||||
|
||||
Find the completed tag workflow's run ID, then download the promoted replica:
|
||||
|
||||
```bash
|
||||
gh run download RUN_ID \
|
||||
--repo permissionlesstech/bitchat-android \
|
||||
--name verified-unsigned-release \
|
||||
--dir release
|
||||
```
|
||||
|
||||
Verify `release/SHA256SUMS.unsigned` before signing.
|
||||
|
||||
### 2. Sign GitHub APKs locally
|
||||
|
||||
Install Android Build Tools 37.0.0 and set these local environment variables:
|
||||
|
||||
- `ANDROID_SDK_ROOT`
|
||||
- `BITCHAT_GITHUB_KEYSTORE`
|
||||
- `BITCHAT_GITHUB_KEY_ALIAS`
|
||||
- `BITCHAT_GITHUB_KEYSTORE_PASSWORD`
|
||||
- `BITCHAT_GITHUB_KEY_PASSWORD`
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
tools/reproducible-builds/sign-release.sh release
|
||||
```
|
||||
|
||||
The helper signs the arm64, x86_64, and universal APKs twice, requires the two
|
||||
signed results to match, and verifies their certificate against
|
||||
`BITCHAT_GITHUB_RELEASE_CERT_SHA256` in `gradle.properties`.
|
||||
|
||||
### 3. Sign the Play AAB locally
|
||||
|
||||
Use JDK 21 and the Play upload key—not the Play app-signing key managed by
|
||||
Google. Set:
|
||||
|
||||
- `JAVA_HOME`
|
||||
- `BITCHAT_PLAY_UPLOAD_KEYSTORE`
|
||||
- `BITCHAT_PLAY_UPLOAD_KEY_ALIAS`
|
||||
- `BITCHAT_PLAY_KEYSTORE_PASSWORD`
|
||||
- `BITCHAT_PLAY_KEY_PASSWORD`
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
tools/reproducible-builds/sign-play-bundle.sh release
|
||||
```
|
||||
|
||||
This creates `release/bitchat-android-play-upload.aab`, verifies its JAR
|
||||
signature, proves that its non-signature payload matches the canonical unsigned
|
||||
AAB, and updates `SHA256SUMS`. Upload that exact signed AAB to Play Console; do
|
||||
not rebuild it in Android Studio.
|
||||
|
||||
### 4. Prepare and publish the GitHub Release
|
||||
|
||||
After both local signing steps:
|
||||
|
||||
```bash
|
||||
tools/reproducible-builds/prepare-github-release.sh release
|
||||
gh release create vX.Y.Z release/* \
|
||||
--repo permissionlesstech/bitchat-android \
|
||||
--verify-tag \
|
||||
--draft \
|
||||
--title "Release vX.Y.Z" \
|
||||
--notes "See docs/reproducible-builds.md for verification instructions."
|
||||
```
|
||||
|
||||
Review the draft assets and Play internal-track result before publishing the
|
||||
GitHub Release and promoting the Play rollout.
|
||||
|
||||
Never store a keystore or password in the repository, GitHub Actions secrets,
|
||||
workflow artifacts, logs, release notes, or build information.
|
||||
The workflow has no signing secrets and never publishes a release by itself.
|
||||
|
||||
## Updating dependencies or toolchains
|
||||
|
||||
|
||||
@ -23,9 +23,15 @@ required=(
|
||||
BUILDINFO.json
|
||||
SHA256SUMS
|
||||
SHA256SUMS.unsigned
|
||||
bitchat-android-arm64-unsigned.apk
|
||||
bitchat-android-arm64.apk
|
||||
bitchat-android-armv7-unsigned.apk
|
||||
bitchat-android-play-upload.aab
|
||||
bitchat-android-release-unsigned.aab
|
||||
bitchat-android-universal-unsigned.apk
|
||||
bitchat-android-universal.apk
|
||||
bitchat-android-x86-unsigned.apk
|
||||
bitchat-android-x86_64-unsigned.apk
|
||||
bitchat-android-x86_64.apk
|
||||
)
|
||||
for artifact in "${required[@]}"; do
|
||||
@ -35,6 +41,28 @@ for artifact in "${required[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
for artifact_path in "$RELEASE_DIR"/*; do
|
||||
if [ ! -f "$artifact_path" ]; then
|
||||
echo "error: unexpected non-file in release directory: $(basename "$artifact_path")" >&2
|
||||
exit 1
|
||||
fi
|
||||
artifact="$(basename "$artifact_path")"
|
||||
case "$artifact" in
|
||||
BUILDINFO.json|SHA256SUMS|SHA256SUMS.unsigned|\
|
||||
bitchat-android-arm64-unsigned.apk|bitchat-android-arm64.apk|\
|
||||
bitchat-android-armv7-unsigned.apk|\
|
||||
bitchat-android-play-upload.aab|bitchat-android-release-unsigned.aab|\
|
||||
bitchat-android-universal-unsigned.apk|bitchat-android-universal.apk|\
|
||||
bitchat-android-x86-unsigned.apk|\
|
||||
bitchat-android-x86_64-unsigned.apk|bitchat-android-x86_64.apk)
|
||||
;;
|
||||
*)
|
||||
echo "error: unexpected release artifact: $artifact" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
for destination in BITCHAT_BUILDINFO.json BITCHAT_SHA256SUMS BITCHAT_SHA256SUMS.unsigned; do
|
||||
if [ -e "$RELEASE_DIR/$destination" ]; then
|
||||
echo "error: public release manifest already exists: $destination" >&2
|
||||
|
||||
@ -65,20 +65,27 @@ trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||
export BITCHAT_GITHUB_KEYSTORE_PASSWORD
|
||||
export BITCHAT_GITHUB_KEY_PASSWORD
|
||||
|
||||
declare -A signed_names=(
|
||||
["bitchat-android-arm64-unsigned.apk"]="bitchat-android-arm64.apk"
|
||||
["bitchat-android-universal-unsigned.apk"]="bitchat-android-universal.apk"
|
||||
["bitchat-android-x86_64-unsigned.apk"]="bitchat-android-x86_64.apk"
|
||||
unsigned_names=(
|
||||
"bitchat-android-arm64-unsigned.apk"
|
||||
"bitchat-android-universal-unsigned.apk"
|
||||
"bitchat-android-x86_64-unsigned.apk"
|
||||
)
|
||||
signed_names=(
|
||||
"bitchat-android-arm64.apk"
|
||||
"bitchat-android-universal.apk"
|
||||
"bitchat-android-x86_64.apk"
|
||||
)
|
||||
|
||||
for unsigned_name in "${!signed_names[@]}"; do
|
||||
for ((index = 0; index < ${#unsigned_names[@]}; index++)); do
|
||||
unsigned_name="${unsigned_names[$index]}"
|
||||
signed_name="${signed_names[$index]}"
|
||||
unsigned_apk="$RELEASE_DIR/$unsigned_name"
|
||||
signed_apk="$RELEASE_DIR/${signed_names[$unsigned_name]}"
|
||||
signed_apk="$RELEASE_DIR/$signed_name"
|
||||
first_signed="$TEMP_DIR/first.apk"
|
||||
second_signed="$TEMP_DIR/second.apk"
|
||||
|
||||
if [ -e "$signed_apk" ]; then
|
||||
echo "error: signed APK already exists: ${signed_names[$unsigned_name]}" >&2
|
||||
echo "error: signed APK already exists: $signed_name" >&2
|
||||
exit 1
|
||||
fi
|
||||
"$ZIPALIGN" -c -P 16 4 "$unsigned_apk"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user