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
145 changes: 145 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
name: Deploy Release to App Stores

on:
workflow_dispatch:
inputs:
# Versioning information is derived from the main branch's manifest and the corresponding GitHub Release.
deployment_target:
description: 'Select the deployment target'
required: true
type: choice
options:
- google_play_internal_test_track
- production_google_play_and_amazon
default: 'google_play_internal_test_track'
releaseNotes:
description: 'Release notes for the app store listings. Can be same or different from GitHub Release notes.'
required: true
type: string

jobs:
download_and_deploy:
name: 'Download Release APKs and Deploy'
runs-on: ubuntu-latest
steps:
- name: Checkout main branch
uses: actions/checkout@v4
with:
ref: 'main' # Explicitly checkout main to get the latest manifest for version info

- name: Extract versionName and versionCode from Manifest
id: extract_versions
run: |
MANIFEST_PATH="app/src/main/AndroidManifest.xml"
if [ ! -f "$MANIFEST_PATH" ]; then
echo "AndroidManifest.xml not found"
exit 1
fi

VERSION_NAME=$(grep 'android:versionName=' "$MANIFEST_PATH" | sed -n 's/.*android:versionName="\([^"]*\)".*/\1/p')
VERSION_CODE=$(grep 'android:versionCode=' "$MANIFEST_PATH" | sed -n 's/.*android:versionCode="\([0-9]*\)".*/\1/p')

if [ -z "$VERSION_NAME" ] || [ -z "$VERSION_CODE" ]; then
echo "Could not extract versionName or versionCode from $MANIFEST_PATH"
exit 1
fi
echo "Extracted from main's manifest - versionName: $VERSION_NAME, versionCode: $VERSION_CODE"
echo "manifest_version_name=$VERSION_NAME" >> $GITHUB_OUTPUT
echo "manifest_version_code=$VERSION_CODE" >> $GITHUB_OUTPUT

- name: Verify Release Exists for Manifest Version and Set Tag
id: check_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
EXPECTED_TAG="v${{ steps.extract_versions.outputs.manifest_version_name }}"
echo "Verifying if GitHub Release for tag $EXPECTED_TAG exists (based on main branch's AndroidManifest.xml)..."
if ! command -v gh &> /dev/null; then
echo "GitHub CLI (gh) not found. It is required to verify the release."
exit 1
fi
# Check if a release associated with the expected tag exists
gh release view "$EXPECTED_TAG" --repo "$GITHUB_REPOSITORY"
if [ $? -ne 0 ]; then
echo "Error: Release for tag $EXPECTED_TAG not found. Please ensure 'release.yml' has been run successfully for version ${{ steps.extract_versions.outputs.manifest_version_name }} (currently in main's AndroidManifest.xml)."
exit 1
fi
echo "Release for tag $EXPECTED_TAG found."
echo "tag_value=$EXPECTED_TAG" >> $GITHUB_OUTPUT # Sets the output for this step

- name: Set up Ruby for Fastlane
uses: ruby/setup-ruby@v1
with:
ruby-version: '2.7'
bundler-cache: true

- name: Download Android APK from GitHub Release
id: download_android_apk
uses: dwenegar/download-release-assets@v1
with:
repo: ${{ github.repository }}
tag: ${{ steps.check_release.outputs.tag_value }} # Use tag_value from check_release
name: app-android-release-v${{ steps.extract_versions.outputs.manifest_version_name }}.apk
path: downloaded_apks
fail_on_unmatched_assets: true

- name: Download Amazon APK from GitHub Release
id: download_amazon_apk
uses: dwenegar/download-release-assets@v1
with:
repo: ${{ github.repository }}
tag: ${{ steps.check_release.outputs.tag_value }} # Use tag_value from check_release
name: app-amazon-release-v${{ steps.extract_versions.outputs.manifest_version_name }}.apk
path: downloaded_apks
fail_on_unmatched_assets: true

- name: Verify Downloaded APKs
id: verify_apks
run: |
ANDROID_APK_PATH="downloaded_apks/app-android-release-v${{ steps.extract_versions.outputs.manifest_version_name }}.apk"
AMAZON_APK_PATH="downloaded_apks/app-amazon-release-v${{ steps.extract_versions.outputs.manifest_version_name }}.apk"

if [ ! -f "$ANDROID_APK_PATH" ]; then
echo "Failed to download $ANDROID_APK_PATH"
exit 1
elif [ ! -f "$AMAZON_APK_PATH" ]; then
echo "Failed to download $AMAZON_APK_PATH"
exit 1
else
echo "Both APKs downloaded successfully."
fi

- name: Create Google Play Key File
run: echo "${{ secrets.GOOGLE_PLAY_JSON_KEY_DATA }}" > /tmp/google_play_key.json
env:
GOOGLE_PLAY_JSON_KEY_DATA: ${{ secrets.GOOGLE_PLAY_JSON_KEY_DATA }}

- name: Deploy to Google Play (Internal Test Track)
if: github.event.inputs.deployment_target == 'google_play_internal_test_track'
run: |
bundle exec fastlane deploy_playstore_test \
apk_path:"downloaded_apks/app-android-release-v${{ steps.extract_versions.outputs.manifest_version_name }}.apk" \
version_name:"${{ steps.extract_versions.outputs.manifest_version_name }}" \
version_code:"${{ steps.extract_versions.outputs.manifest_version_code }}" \
release_notes:"${{ github.event.inputs.releaseNotes }}"

- name: Deploy to Google Play (Production)
if: github.event.inputs.deployment_target == 'production_google_play_and_amazon'
run: |
bundle exec fastlane deploy_playstore_production \
apk_path:"downloaded_apks/app-android-release-v${{ steps.extract_versions.outputs.manifest_version_name }}.apk" \
version_name:"${{ steps.extract_versions.outputs.manifest_version_name }}" \
version_code:"${{ steps.extract_versions.outputs.manifest_version_code }}" \
release_notes:"${{ github.event.inputs.releaseNotes }}"

- name: Deploy to Amazon Appstore
if: github.event.inputs.deployment_target == 'production_google_play_and_amazon'
env:
AMAZON_CLIENT_ID: ${{ secrets.AMAZON_CLIENT_ID }}
AMAZON_CLIENT_SECRET: ${{ secrets.AMAZON_CLIENT_SECRET }}
run: |
bundle exec fastlane deploy_amazon_appstore \
apk_path:"downloaded_apks/app-amazon-release-v${{ steps.extract_versions.outputs.manifest_version_name }}.apk" \
version_name:"${{ steps.extract_versions.outputs.manifest_version_name }}" \
version_code:"${{ steps.extract_versions.outputs.manifest_version_code }}" \
release_notes:"${{ github.event.inputs.releaseNotes }}"
110 changes: 110 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: Build and Create GitHub Release

on:
workflow_dispatch: {} # No inputs from user at trigger time

jobs:
build_and_create_release:
name: 'Build APKs and Create GitHub Release'
runs-on: ubuntu-latest
permissions: # Needed by softprops/action-gh-release to create releases
contents: write
outputs: # Output extracted versions for potential use in other workflows if chained via API
tag_created: v${{ steps.extract_versions.outputs.manifest_version_name }}
version_name_extracted: ${{ steps.extract_versions.outputs.manifest_version_name }}
version_code_extracted: ${{ steps.extract_versions.outputs.manifest_version_code }}
steps:
- name: Checkout code (full history for release notes)
uses: actions/checkout@v4
with:
fetch-depth: 0 # Needed for changelog generation between tags

- name: Extract versionName and versionCode from Manifest
id: extract_versions
run: |
MANIFEST_PATH="app/src/main/AndroidManifest.xml"
if [ ! -f "$MANIFEST_PATH" ]; then
echo "AndroidManifest.xml not found"
exit 1
fi

VERSION_NAME=$(grep 'android:versionName=' "$MANIFEST_PATH" | sed -n 's/.*android:versionName="\([^"]*\)".*/\1/p')
VERSION_CODE=$(grep 'android:versionCode=' "$MANIFEST_PATH" | sed -n 's/.*android:versionCode="\([0-9]*\)".*/\1/p')

if [ -z "$VERSION_NAME" ] || [ -z "$VERSION_CODE" ]; then
echo "Could not extract versionName or versionCode from $MANIFEST_PATH"
exit 1
fi

echo "Extracted versionName: $VERSION_NAME"
echo "Extracted versionCode: $VERSION_CODE"
echo "manifest_version_name=$VERSION_NAME" >> $GITHUB_OUTPUT
echo "manifest_version_code=$VERSION_CODE" >> $GITHUB_OUTPUT

- name: Check if Git Tag Exists
id: check_tag
run: |
TAG_NAME="v${{ steps.extract_versions.outputs.manifest_version_name }}"
echo "Checking for existing tag: $TAG_NAME"
# Fetch all tags from remote to ensure local git knows about them
git fetch --tags --force # --force to overwrite local tags with remote, ensuring up-to-date check
# Check if tag exists locally (which implies it would have been fetched if remote)
if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
echo "Error: Tag $TAG_NAME already exists. Halting workflow to prevent duplicate release."
exit 1
fi
# As an additional safeguard, explicitly check remote.
# git ls-remote --tags origin refs/tags/$TAG_NAME will output the tag if it exists on remote.
# If it outputs something, the tag exists.
if git ls-remote --tags origin | grep -q "refs/tags/$TAG_NAME$"; then
echo "Error: Tag $TAG_NAME confirmed to exist on remote 'origin'."
exit 1
fi
echo "Tag $TAG_NAME does not appear to exist locally or on remote 'origin'. Proceeding."

- name: Set up Java (for Gradle)
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'

- name: Decode and Place Keystore
run: |
echo "${{ secrets.SIGNING_KEYSTORE_DATA }}" | base64 --decode > ${{ github.workspace }}/app/release.keystore
env:
SIGNING_KEYSTORE_DATA: ${{ secrets.SIGNING_KEYSTORE_DATA }}

- name: Build Release APKs (Android and Amazon)
env:
SIGNING_KEYSTORE_PATH: ${{ github.workspace }}/app/release.keystore
SIGNING_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
run: |
chmod +x ./gradlew
# Gradle uses versionName and versionCode from the manifest directly
./gradlew :app:assembleAndroidRelease :app:assembleAmazonRelease

- name: Verify Versioned APKs Exist
run: |
MVN="${{ steps.extract_versions.outputs.manifest_version_name }}" # Manifest Version Name
echo "Checking for app/android/release/app-android-release-v${MVN}.apk"
test -f "app/android/release/app-android-release-v${MVN}.apk"
echo "Checking for app/amazon/release/app-amazon-release-v${MVN}.apk"
test -f "app/amazon/release/app-amazon-release-v${MVN}.apk"
echo "Versioned APKs verified."

- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ steps.extract_versions.outputs.manifest_version_name }}
name: Release v${{ steps.extract_versions.outputs.manifest_version_name }}
# body parameter is removed/empty to allow auto-generation
files: |
app/android/release/app-android-release-v${{ steps.extract_versions.outputs.manifest_version_name }}.apk
app/amazon/release/app-amazon-release-v${{ steps.extract_versions.outputs.manifest_version_name }}.apk
fail_on_unmatched_files: true # Ensures workflow fails if APKs are not found
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
115 changes: 115 additions & 0 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Deployment Process

This document outlines the process for building, releasing, and deploying the Coin Collection Android application. This is managed by two distinct GitHub Actions workflows:

1. **Build and Create GitHub Release** (`.github/workflows/release.yml`): Builds and signs the APK, then creates a formal GitHub Release with the APK as an attachment. GitHub Releases are picked up automatically by the F-Droid App Repository.
2. **Deploy Release to App Stores** (`.github/workflows/deploy.yml`): Takes an existing GitHub Release (and its attached APKs), downloads the APKs, and deploys them to the specified app store tracks (Google Play Internal, Google Play Production, Amazon Appstore).

## CI/CD Pipeline Overview

- **`.github/workflows/release.yml` (Build and Create GitHub Release)**:
- **Purpose**: To create an official, signed build of the application and a corresponding GitHub Release. This release includes the android and amazon APKs.
- **Trigger**: Manual (`workflow_dispatch`).

- **`.github/workflows/deploy.yml` (Deploy Release to App Stores)**:
- **Purpose**: To deploy pre-built APKs (from GitHub Release) to various app store tracks.
- **Trigger**: Manual (`workflow_dispatch`).
- **Targets**: Select between the following deployment targets:
- `google_play_internal_test_track` Select this first to test the release version of the app via the Google Play test track.
- `production_google_play_and_amazon` Select this to release to all Google Play / Amazon App Store users.

## Recommended Deployment Flow

This flow ensures a consistent artifact is built, released, tested, and deployed.

**Step 1: Build and Create GitHub Release**

1. Navigate to `Actions > Build and Create GitHub Release`.
2. Click **"Run workflow"**.
- **Outcome**:
- The `release.yml` workflow runs.
- Signed APKs are built using the provided version information in AndroidManifest.xml.
- A GitHub Release and release tag are created.

**Step 2: Deploy to Google Play Internal Test Track for Verification**

1. Navigate to `Actions > Deploy Release to App Stores`.
2. Click **"Run workflow"**.
- **Deployment target**: Select `google_play_internal_test_track`.
- **Outcome**:
- The `deploy.yml` workflow runs.
- It downloads APKs from the GitHub Release created in Step 1 (e.g., from tag `v1.0.5`).
- The downloaded Android APK is deployed to the Google Play Store's Internal test track using the `deploy_playstore_test` Fastlane lane.

**Step 3: Test Thoroughly on Google Play Internal Track**

1. Access your Google Play Console.
2. Distribute the internal test build to your testers.
3. Conduct comprehensive testing of the application.

**Step 4: Deploy to Production Stores (Google Play Production & Amazon Appstore)**

1. Once the build from Step 2 has been successfully tested and approved:
2. Navigate to `Actions > Deploy Release to App Stores` again.
3. Click **"Run workflow"**.
- **Deployment target**: Select `production_google_play_and_amazon`.
- **Outcome**:
- The `deploy.yml` workflow runs again.
- It downloads the android and amazon APKs from the GitHub Release (e.g., from tag `v1.0.5`).
- The Android APK is deployed to the Google Play Store's Production track via the `deploy_playstore_production` Fastlane lane.
- The Amazon APK is deployed to the Amazon Appstore via the `deploy_amazon_appstore` Fastlane lane.

This process ensures that the exact artifact built and attached to the GitHub Release is the one tested and subsequently deployed to production environments.

## Fastlane Lanes

The Fastlane lanes defined in `fastlane/Fastfile` are now designed to deploy pre-built APKs. They expect an `apk_path` parameter pointing to the APK to be deployed.

- `deploy_playstore_test`:
- Deploys the APK (specified by `options[:apk_path]`) to the Google Play Store's **internal** test track.
- Uses `options[:release_notes]` for the changelog on this track.
- `deploy_playstore_production`:
- Deploys the APK (specified by `options[:apk_path]`) to the Google Play Store's **production** track.
- Uses `options[:release_notes]` for the changelog on this track.
- `deploy_amazon_appstore`:
- Deploys the APK (specified by `options[:apk_path]`) to the Amazon Appstore.
- If `options[:release_notes]` and `options[:version_code]` are provided, writes release notes to `fastlane/metadata/android/en-US/changelogs/#{options[:version_code]}.txt` for the plugin to pick up.

## Prerequisites: GitHub Secrets Setup

The following GitHub Secrets must be configured in the repository settings (`Settings > Secrets and variables > Actions`):

1. **`GOOGLE_PLAY_JSON_KEY_DATA`**:
- **Description**: JSON key for Google Play Console service account. Allows Fastlane to authenticate with Google Play Developer API.
- **Used by**: `deploy.yml` (for Fastlane `supply` action).
- **How to obtain**: Google Play Console > Setup > API access.

2. **`SIGNING_KEYSTORE_DATA`**:
- **Description**: Base64 encoded Android signing keystore file (`.jks` or `.keystore`).
- **Used by**: `release.yml` (for signing the APK during the Gradle build).
- **How to obtain**: Convert your keystore file to base64.
- macOS/Linux: `base64 -i your_keystore_file.jks -o keystore_base64.txt`
- Windows (PowerShell): `[Convert]::ToBase64String([IO.File]::ReadAllBytes("your_keystore_file.jks")) | Out-File keystore_base64.txt -Encoding ASCII`
- Copy the content of `keystore_base64.txt` into the secret.

3. **`SIGNING_KEYSTORE_PASSWORD`**:
- **Description**: Password for the Android signing keystore.
- **Used by**: `release.yml`.

4. **`SIGNING_KEY_ALIAS`**:
- **Description**: Alias for the signing key within the keystore.
- **Used by**: `release.yml`.

5. **`SIGNING_KEY_PASSWORD`**:
- **Description**: Password for the specific key alias.
- **Used by**: `release.yml`.

6. **`AMAZON_CLIENT_ID`**:
- **Description**: Client ID for Amazon App Submission API.
- **Used by**: `deploy.yml` (for Fastlane `amazon_appstore` plugin).
- **How to obtain**: Amazon Developer Console documentation.

7. **`AMAZON_CLIENT_SECRET`**:
- **Description**: Client Secret for Amazon App Submission API.
- **Used by**: `deploy.yml`.
- **How to obtain**: Amazon Developer Console documentation.
3 changes: 2 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
source "https://rubygems.org"

gem "fastlane"
gem "fastlane", "~> 2.212.0"
gem 'fastlane-plugin-amazon_appstore', '~> 1.1.0'
2 changes: 1 addition & 1 deletion fastlane/Appfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
json_key_file(ENV['GOOGLE_PLAY_KEY_PATH'] || "/tmp/google_play_key.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
package_name("com.spencerpages")
Loading
Loading