Skip to content
Closed
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
69 changes: 67 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
name: Build

# One workflow for every event. The build job runs for pushes, pull requests and
# version tags and always produces the unsigned release APK. On a vMAJOR.MINOR.PATCH
# tag the release job takes that exact artifact, signs it with the keystore held in
# repository secrets, and attaches it to a GitHub Release. The key is only ever
# decoded inside the release job.
on:
push:
branches: [main]
tags: ["v[0-9]+.[0-9]+.[0-9]+"]
pull_request:
workflow_dispatch:

Expand All @@ -11,7 +17,7 @@ permissions:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }}

jobs:
build:
Expand All @@ -37,13 +43,22 @@ jobs:
- name: Lint
run: ./gradlew lintDebug

# A version tag stamps versionName/versionCode into the build (see
# app/build.gradle.kts); every other event builds with the in-repo defaults.
- name: Build release APK
run: ./gradlew assembleRelease
run: |
set -euo pipefail
args=()
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
args+=("-PreleaseVersion=${GITHUB_REF_NAME#v}")
fi
./gradlew assembleRelease "${args[@]}"

- uses: actions/upload-artifact@v7
with:
name: RemoteSync
path: app/build/outputs/apk/release/*.apk
if-no-files-found: error

- uses: actions/upload-artifact@v7
if: always()
Expand All @@ -52,3 +67,53 @@ jobs:
path: |
**/build/reports/lint-results-*.html
**/build/reports/tests/

release:
if: startsWith(github.ref, 'refs/tags/v')
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: write
steps:
- uses: actions/download-artifact@v7
with:
name: RemoteSync
path: apk

- uses: android-actions/setup-android@v3
with:
packages: build-tools;36.0.0

# apksigner is the SDK's own signing tool
# (https://developer.android.com/tools/apksigner); it re-signs the artifact the
# build job produced, so what ships is byte-for-byte what was tested.
- name: Sign the build artifact
env:
SIGNING_KEYSTORE_BASE64: ${{ secrets.SIGNING_KEYSTORE_BASE64 }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
run: |
set -euo pipefail
umask 077
printf '%s' "$SIGNING_KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.jks"
unsigned=$(ls apk/*.apk)
"$ANDROID_HOME/build-tools/36.0.0/apksigner" sign \
--ks "$RUNNER_TEMP/release.jks" \
--ks-key-alias "$SIGNING_KEY_ALIAS" \
--ks-pass env:SIGNING_STORE_PASSWORD \
--key-pass env:SIGNING_KEY_PASSWORD \
--out "RemoteSync-${GITHUB_REF_NAME}.apk" \
"$unsigned"
"$ANDROID_HOME/build-tools/36.0.0/apksigner" verify --print-certs "RemoteSync-${GITHUB_REF_NAME}.apk"

- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: gh release create "$GITHUB_REF_NAME" "RemoteSync-${GITHUB_REF_NAME}.apk" --title "$GITHUB_REF_NAME" --generate-notes

- name: Remove signing key
if: always()
run: rm -f "$RUNNER_TEMP/release.jks"
76 changes: 0 additions & 76 deletions .github/workflows/release.yml

This file was deleted.

7 changes: 4 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ plugins {
alias(libs.plugins.hilt)
}

// Release signing, per https://developer.android.com/studio/publish/app-signing:
// Local release signing, per https://developer.android.com/studio/publish/app-signing:
// keystore.properties (storeFile, storePassword, keyAlias, keyPassword) is loaded
// from the project root, or from ~/.android/release/ on a developer machine. Neither
// file is committed; without one the release build is produced unsigned.
// file is committed; without one the release build is produced unsigned, which is what
// CI does (the workflow signs the artifact afterwards with apksigner).
val keystorePropertiesFile = listOf(
rootProject.file("keystore.properties"),
File(System.getProperty("user.home"), ".android/release/keystore.properties"),
Expand All @@ -20,7 +21,7 @@ val keystoreProperties = Properties().apply {
keystorePropertiesFile?.inputStream()?.use(::load)
}

// Release builds take their version from the git tag: release.yml passes
// Release builds take their version from the git tag: the workflow passes
// -PreleaseVersion=1.2.3 for tag v1.2.3. versionCode is derived so it stays monotonic.
val releaseVersion = providers.gradleProperty("releaseVersion").orNull
fun versionCodeOf(version: String): Int {
Expand Down
Loading