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
17 changes: 15 additions & 2 deletions .github/workflows/android-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,21 @@ jobs:
distribution: temurin
java-version: "17"

- name: Set up Android SDK
uses: android-actions/setup-android@v3
- name: Configure Android SDK
run: |
ANDROID_HOME="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}}"
echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV"
echo "ANDROID_SDK_ROOT=$ANDROID_HOME" >> "$GITHUB_ENV"
if [ ! -x "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" ]; then
mkdir -p "$ANDROID_HOME/cmdline-tools"
curl -fsSL https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -o "$RUNNER_TEMP/cmdline-tools.zip"
unzip -q "$RUNNER_TEMP/cmdline-tools.zip" -d "$RUNNER_TEMP/cmdline-tools"
rm -rf "$ANDROID_HOME/cmdline-tools/latest"
mv "$RUNNER_TEMP/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
fi
echo "$ANDROID_HOME/cmdline-tools/latest/bin" >> "$GITHUB_PATH"
echo "$ANDROID_HOME/platform-tools" >> "$GITHUB_PATH"
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses >/dev/null

- name: Install Android SDK packages
run: sdkmanager "platforms;android-36" "build-tools;36.0.0" "ndk;30.0.14904198"
Expand Down
188 changes: 116 additions & 72 deletions .github/workflows/android-release.yml
Original file line number Diff line number Diff line change
@@ -1,36 +1,73 @@
name: Android Release
name: Publish Tagged Android Release

on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag_name:
description: "Release tag. Defaults to v<versionName>."
required: false
tag:
description: Existing immutable release tag to publish
required: true
type: string
workflow_call:
inputs:
tag:
required: true
type: string
prerelease:
description: "Mark GitHub release as a prerelease."
required: false
default: false
type: boolean

permissions:
contents: write

jobs:
signed-release:
name: Build signed release artifacts
publish:
runs-on: ubuntu-latest

env:
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}

steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Resolve requested tag
id: requested
env:
INPUT_TAG: ${{ inputs.tag }}
REF_TAG: ${{ github.ref_name }}
run: |
if [ -n "$INPUT_TAG" ]; then TAG="$INPUT_TAG"; else TAG="$REF_TAG"; fi
case "$TAG" in v[0-9]*.[0-9]*.[0-9]*) ;; *) echo "::error::Invalid release tag: $TAG"; exit 2 ;; esac
echo "tag=$TAG" >> "$GITHUB_OUTPUT"

- uses: actions/checkout@v4
with:
ref: ${{ steps.requested.outputs.tag }}
fetch-depth: 0

- name: Verify tag and release identity
env:
TAG: ${{ steps.requested.outputs.tag }}
run: |
git fetch origin --tags
test "$(git rev-parse HEAD)" = "$(git rev-list -n 1 "$TAG")"
test "$(git cat-file -t "$TAG")" = "tag"
test "$(python3 scripts/release_tool.py metadata | python3 -c 'import json,sys; print(json.load(sys.stdin)["tag"])')" = "$TAG"
python3 scripts/release_tool.py validate --skip-branch --allow-existing-tag

- name: Detect an existing complete GitHub Release
id: existing
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.requested.outputs.tag }}
run: |
mkdir -p release-artifacts
if gh release view "$TAG" >/dev/null 2>&1; then
gh release download "$TAG" --dir release-artifacts
(cd release-artifacts && sha256sum --check SHA256SUMS)
echo "complete=true" >> "$GITHUB_OUTPUT"
else
echo "complete=false" >> "$GITHUB_OUTPUT"
fi

- name: Validate signing secrets
if: steps.existing.outputs.complete != 'true'
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
Expand All @@ -46,23 +83,41 @@ jobs:
fi

- name: Set up JDK
if: steps.existing.outputs.complete != 'true'
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"

- name: Set up Android SDK
uses: android-actions/setup-android@v3
- name: Configure Android SDK
if: steps.existing.outputs.complete != 'true'
run: |
ANDROID_HOME="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}}"
echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV"
echo "ANDROID_SDK_ROOT=$ANDROID_HOME" >> "$GITHUB_ENV"
if [ ! -x "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" ]; then
mkdir -p "$ANDROID_HOME/cmdline-tools"
curl -fsSL https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -o "$RUNNER_TEMP/cmdline-tools.zip"
unzip -q "$RUNNER_TEMP/cmdline-tools.zip" -d "$RUNNER_TEMP/cmdline-tools"
rm -rf "$ANDROID_HOME/cmdline-tools/latest"
mv "$RUNNER_TEMP/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
fi
echo "$ANDROID_HOME/cmdline-tools/latest/bin" >> "$GITHUB_PATH"
echo "$ANDROID_HOME/platform-tools" >> "$GITHUB_PATH"
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses >/dev/null

- name: Install Android SDK packages
if: steps.existing.outputs.complete != 'true'
run: sdkmanager "platforms;android-36" "build-tools;36.0.0" "ndk;30.0.14904198"

- name: Set up Rust
if: steps.existing.outputs.complete != 'true'
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android

- name: Cache Rust artifacts
if: steps.existing.outputs.complete != 'true'
uses: actions/cache@v4
with:
path: |
Expand All @@ -74,77 +129,66 @@ jobs:
rust-${{ runner.os }}-

- name: Install cargo-ndk
if: steps.existing.outputs.complete != 'true'
run: cargo install cargo-ndk --locked

- name: Set up Gradle
if: steps.existing.outputs.complete != 'true'
uses: gradle/actions/setup-gradle@v4

- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
bundler-cache: true

- name: Decode release keystore
if: steps.existing.outputs.complete != 'true'
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
keystore="$RUNNER_TEMP/voice-inbox-release.jks"
printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$keystore"
echo "ANDROID_KEYSTORE_FILE=$keystore" >> "$GITHUB_ENV"

- name: Build signed release APK
run: ./gradlew :app:assembleRelease

- name: Build signed release AAB
run: ./gradlew :app:bundleRelease

- name: Stage release files
id: release_files
- name: Build signed APK and AAB once
if: steps.existing.outputs.complete != 'true'
run: |
version=$(grep 'versionName =' app/build.gradle.kts | head -n1 | cut -d '"' -f2)
if [ -z "$version" ]; then
echo "::error::Unable to read versionName from app/build.gradle.kts"
exit 1
fi
tag="${{ inputs.tag_name }}"
if [ -z "$tag" ]; then
tag="v$version"
fi

mkdir -p release-dist
cp app/build/outputs/apk/release/app-release.apk "release-dist/voice-inbox-$version.apk"
cp app/build/outputs/bundle/release/app-release.aab "release-dist/voice-inbox-$version.aab"

echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "apk=release-dist/voice-inbox-$version.apk" >> "$GITHUB_OUTPUT"
echo "aab=release-dist/voice-inbox-$version.aab" >> "$GITHUB_OUTPUT"

- name: Upload signed release APK
uses: actions/upload-artifact@v4
with:
name: voice-inbox-release-apk
path: ${{ steps.release_files.outputs.apk }}
if-no-files-found: error

- name: Upload signed release AAB
uses: actions/upload-artifact@v4
with:
name: voice-inbox-release-aab
path: ${{ steps.release_files.outputs.aab }}
if-no-files-found: error
./gradlew :app:assembleRelease :app:bundleRelease --no-daemon
version=$(python3 scripts/release_tool.py metadata | python3 -c 'import json,sys; print(json.load(sys.stdin)["versionName"])')
cp app/build/outputs/apk/release/app-release.apk "release-artifacts/voice-inbox-$version.apk"
cp app/build/outputs/bundle/release/app-release.aab "release-artifacts/voice-inbox-$version.aab"
(cd release-artifacts && sha256sum "voice-inbox-$version.apk" "voice-inbox-$version.aab" > SHA256SUMS)
(cd release-artifacts && sha256sum --check SHA256SUMS)

- name: Publish GitHub Release and immutable assets
if: steps.existing.outputs.complete != 'true'
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.requested.outputs.tag }}
run: |
CODE=$(python3 scripts/release_tool.py metadata | python3 -c 'import json,sys; print(json.load(sys.stdin)["versionCode"])')
NOTES="fastlane/metadata/android/en-US/changelogs/$CODE.txt"
gh release create "$TAG" release-artifacts/* --verify-tag --title "Voice Inbox $TAG" --notes-file "$NOTES"

- name: Create GitHub release
- name: Decode Google Play service account
env:
GITHUB_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ steps.release_files.outputs.tag }}
RELEASE_VERSION: ${{ steps.release_files.outputs.version }}
PRERELEASE: ${{ inputs.prerelease }}
SERVICE_ACCOUNT_BASE64: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
run: |
prerelease_args=""
if [ "$PRERELEASE" = "true" ]; then
prerelease_args="--prerelease"
if [ -z "$SERVICE_ACCOUNT_BASE64" ]; then
echo "::error::GOOGLE_PLAY_SERVICE_ACCOUNT is required for Google Play upload"
exit 1
fi
printf '%s' "$SERVICE_ACCOUNT_BASE64" | base64 --decode > service-account.json

gh release create "$RELEASE_TAG" \
"${{ steps.release_files.outputs.apk }}" \
"${{ steps.release_files.outputs.aab }}" \
--target "$GITHUB_SHA" \
--title "Voice Inbox $RELEASE_VERSION" \
--notes "Signed Android release artifacts for Voice Inbox $RELEASE_VERSION." \
$prerelease_args
- name: Upload the published AAB to Play Internal testing
run: |
TAG="${{ steps.requested.outputs.tag }}"
CODE=$(python3 scripts/release_tool.py metadata | python3 -c 'import json,sys; print(json.load(sys.stdin)["versionCode"])')
TRACK=$(python3 scripts/release_tool.py metadata | python3 -c 'import json,sys; print(json.load(sys.stdin)["playTrack"])')
AAB=$(find release-artifacts -maxdepth 1 -name 'voice-inbox-*.aab' | head -n1)
bundle exec fastlane android upload_release \
aab:"$AAB" \
version_code:"$CODE" \
metadata_path:"${{ github.workspace }}/fastlane/metadata/android" \
json_key:"${{ github.workspace }}/service-account.json" \
track:"$TRACK"
112 changes: 112 additions & 0 deletions .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: Create Android Release

on:
pull_request_target:
branches: [master]
types: [closed]
workflow_dispatch:
inputs:
head_branch:
description: Merged release/* or hotfix/* branch for recovery
required: true
type: string
commit_sha:
description: Exact merge commit contained in master
required: true
type: string

permissions:
contents: write
checks: read
pull-requests: read

concurrency:
group: android-release-${{ github.event.pull_request.merge_commit_sha || inputs.commit_sha }}
cancel-in-progress: false

jobs:
create-tag:
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true &&
(startsWith(github.event.pull_request.head.ref, 'release/') ||
startsWith(github.event.pull_request.head.ref, 'hotfix/')))
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.identity.outputs.tag }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.merge_commit_sha || inputs.commit_sha }}
fetch-depth: 0

- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
bundler-cache: true

- name: Fetch stable history and tags
run: git fetch origin master --tags

- name: Require successful protected pull-request checks
if: github.event_name == 'pull_request_target'
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: gh pr checks "$PR_NUMBER" --required

- name: Resolve merged release identity
id: identity
env:
HEAD_BRANCH: ${{ github.event.pull_request.head.ref || inputs.head_branch }}
BASE_BRANCH: ${{ github.event.pull_request.base.ref || 'master' }}
MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha || inputs.commit_sha }}
MERGED: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged }}
run: |
python3 scripts/release_tool.py candidate \
--head-branch "$HEAD_BRANCH" \
--base-branch "$BASE_BRANCH" \
--merge-commit "$MERGE_COMMIT" \
--merged "$MERGED" > release-candidate.json
tag=$(python3 -c 'import json; print(json.load(open("release-candidate.json"))["tag"])')
echo "tag=$tag" >> "$GITHUB_OUTPUT"

- name: Decode Google Play service account
env:
SERVICE_ACCOUNT_BASE64: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
run: |
if [ -z "$SERVICE_ACCOUNT_BASE64" ]; then
echo "::error::GOOGLE_PLAY_SERVICE_ACCOUNT is required for release publishing"
exit 1
fi
printf '%s' "$SERVICE_ACCOUNT_BASE64" | base64 --decode > service-account.json

- name: Repeat repository and Google Play validation
run: >-
python3 scripts/release_tool.py validate --skip-branch --check-play
--service-account service-account.json

- name: Create immutable annotated tag
env:
TAG: ${{ steps.identity.outputs.tag }}
MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha || inputs.commit_sha }}
run: |
test -z "$(git tag --list "$TAG")"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" "$MERGE_COMMIT" -m "Release $TAG"
git push origin "refs/tags/$TAG"

publish:
needs: create-tag
uses: ./.github/workflows/android-release.yml
with:
tag: ${{ needs.create-tag.outputs.tag }}
secrets: inherit

sync-development:
needs: [create-tag, publish]
uses: ./.github/workflows/sync-master-to-develop.yml
with:
tag: ${{ needs.create-tag.outputs.tag }}
secrets: inherit
Loading
Loading