Files
Krow-workspace/.github/workflows/product-release.yml
Achintha Isuru 8b9a58adb1 feat: Add mobile CI/CD secrets setup for APK signing
- Updated Makefile to include new command for setting up mobile CI secrets.
- Enhanced tools.mk with setup-mobile-ci-secrets target.
- Created setup-mobile-github-secrets.sh script for configuring GitHub Secrets for APK signing.
- Added APK signing implementation summary documentation.
- Created detailed APK signing setup guide.
- Added GitHub secrets checklist for easy reference.
2026-03-05 13:55:38 -05:00

414 lines
16 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: 📦 Product Release
on:
workflow_dispatch:
inputs:
app:
description: '📦 Product'
required: true
type: choice
options:
- worker-mobile-app
- client-mobile-app
environment:
description: '🌍 Environment'
required: true
type: choice
options:
- dev
- stage
- prod
create_github_release:
description: '📦 Create GitHub Release'
required: true
type: boolean
default: true
prerelease:
description: '🔖 Mark as Pre-release'
required: false
type: boolean
default: false
jobs:
validate-and-create-release:
name: 🚀 Create Product Release
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
version: ${{ steps.version.outputs.version }}
tag_name: ${{ steps.tag.outputs.tag_name }}
steps:
- name: 📥 Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: <EFBFBD> Make scripts executable
run: |
chmod +x .github/scripts/*.sh
echo "✅ Scripts are now executable"
- name: 📖 Extract version from version file
id: version
run: |
VERSION=$(.github/scripts/extract-version.sh "${{ github.event.inputs.app }}")
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "📌 Extracted version: ${VERSION}"
- name: 🏷️ Generate tag name
id: tag
run: |
TAG_NAME=$(.github/scripts/generate-tag-name.sh \
"${{ github.event.inputs.app }}" \
"${{ github.event.inputs.environment }}" \
"${{ steps.version.outputs.version }}")
echo "tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT
echo "🎯 Tag to create: ${TAG_NAME}"
- name: 🔍 Check if tag already exists
run: |
TAG_NAME="${{ steps.tag.outputs.tag_name }}"
if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
echo "❌ Error: Tag $TAG_NAME already exists"
echo "💡 Tip: Update the version in the version file before creating a new release"
exit 1
fi
echo "✅ Tag does not exist, proceeding..."
- name: 📋 Extract release notes from CHANGELOG
id: release_notes
run: |
.github/scripts/extract-release-notes.sh \
"${{ github.event.inputs.app }}" \
"${{ steps.version.outputs.version }}" \
"${{ github.event.inputs.environment }}" \
"${{ steps.tag.outputs.tag_name }}" \
"/tmp/release_notes.md"
echo "notes_file=/tmp/release_notes.md" >> $GITHUB_OUTPUT
- name: 🏷️ Create Git Tag
run: |
TAG_NAME="${{ steps.tag.outputs.tag_name }}"
APP="${{ github.event.inputs.app }}"
ENV="${{ github.event.inputs.environment }}"
VERSION="${{ steps.version.outputs.version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG_NAME" -m "🚀 Release ${APP} product ${VERSION} to ${ENV}"
git push origin "$TAG_NAME"
echo "✅ Tag created and pushed: $TAG_NAME"
- name: 📦 Create GitHub Release
if: ${{ github.event.inputs.create_github_release == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG_NAME="${{ steps.tag.outputs.tag_name }}"
APP="${{ github.event.inputs.app }}"
ENV="${{ github.event.inputs.environment }}"
VERSION="${{ steps.version.outputs.version }}"
# Generate release title
if [ "$APP" = "worker-mobile-app" ]; then
APP_DISPLAY="Worker Mobile Application"
else
APP_DISPLAY="Client Mobile Application"
fi
ENV_UPPER=$(echo "$ENV" | tr '[:lower:]' '[:upper:]')
RELEASE_NAME="Krow With Us - ${APP_DISPLAY} - ${ENV_UPPER} - v${VERSION}"
echo "📦 Creating GitHub Release: $RELEASE_NAME"
# Create release
if [ "${{ github.event.inputs.prerelease }}" = "true" ]; then
gh release create "$TAG_NAME" \
--title "$RELEASE_NAME" \
--notes-file "${{ steps.release_notes.outputs.notes_file }}" \
--prerelease
echo "🔖 Pre-release created successfully"
else
gh release create "$TAG_NAME" \
--title "$RELEASE_NAME" \
--notes-file "${{ steps.release_notes.outputs.notes_file }}"
echo "✅ Release created successfully"
fi
- name: 📊 Generate Release Summary
run: |
.github/scripts/create-release-summary.sh \
"${{ github.event.inputs.app }}" \
"${{ github.event.inputs.environment }}" \
"${{ steps.version.outputs.version }}" \
"${{ steps.tag.outputs.tag_name }}"
build-mobile-artifacts:
name: 📱 Build Mobile APK
runs-on: ubuntu-latest
needs: validate-and-create-release
if: ${{ github.event.inputs.app == 'worker-mobile-app' || github.event.inputs.app == 'client-mobile-app' }}
permissions:
contents: write
steps:
- name: 📥 Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🟢 Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: 'backend/*/package-lock.json'
- name: 🔥 Install Firebase CLI
run: |
npm install -g firebase-tools
firebase --version
echo " Note: Firebase CLI installed for Data Connect SDK generation"
echo " If SDK generation fails, ensure Data Connect SDK files are committed to repo"
- name: ☕ Setup Java
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: 🐦 Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.5'
channel: 'stable'
cache: true
- name: 🔧 Install Melos
run: |
dart pub global activate melos
echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH
- name: 📦 Install Dependencies
run: |
make mobile-install
- name: 🔐 Setup APK Signing
env:
# Worker Mobile (Staff App) Secrets
WORKER_KEYSTORE_DEV_BASE64: ${{ secrets.WORKER_KEYSTORE_DEV_BASE64 }}
WORKER_KEYSTORE_STAGING_BASE64: ${{ secrets.WORKER_KEYSTORE_STAGING_BASE64 }}
WORKER_KEYSTORE_PROD_BASE64: ${{ secrets.WORKER_KEYSTORE_PROD_BASE64 }}
WORKER_KEYSTORE_PASSWORD_DEV: ${{ secrets.WORKER_KEYSTORE_PASSWORD_DEV }}
WORKER_KEYSTORE_PASSWORD_STAGING: ${{ secrets.WORKER_KEYSTORE_PASSWORD_STAGING }}
WORKER_KEYSTORE_PASSWORD_PROD: ${{ secrets.WORKER_KEYSTORE_PASSWORD_PROD }}
WORKER_KEY_ALIAS_DEV: ${{ secrets.WORKER_KEY_ALIAS_DEV }}
WORKER_KEY_ALIAS_STAGING: ${{ secrets.WORKER_KEY_ALIAS_STAGING }}
WORKER_KEY_ALIAS_PROD: ${{ secrets.WORKER_KEY_ALIAS_PROD }}
WORKER_KEY_PASSWORD_DEV: ${{ secrets.WORKER_KEY_PASSWORD_DEV }}
WORKER_KEY_PASSWORD_STAGING: ${{ secrets.WORKER_KEY_PASSWORD_STAGING }}
WORKER_KEY_PASSWORD_PROD: ${{ secrets.WORKER_KEY_PASSWORD_PROD }}
# Client Mobile Secrets
CLIENT_KEYSTORE_DEV_BASE64: ${{ secrets.CLIENT_KEYSTORE_DEV_BASE64 }}
CLIENT_KEYSTORE_STAGING_BASE64: ${{ secrets.CLIENT_KEYSTORE_STAGING_BASE64 }}
CLIENT_KEYSTORE_PROD_BASE64: ${{ secrets.CLIENT_KEYSTORE_PROD_BASE64 }}
CLIENT_KEYSTORE_PASSWORD_DEV: ${{ secrets.CLIENT_KEYSTORE_PASSWORD_DEV }}
CLIENT_KEYSTORE_PASSWORD_STAGING: ${{ secrets.CLIENT_KEYSTORE_PASSWORD_STAGING }}
CLIENT_KEYSTORE_PASSWORD_PROD: ${{ secrets.CLIENT_KEYSTORE_PASSWORD_PROD }}
CLIENT_KEY_ALIAS_DEV: ${{ secrets.CLIENT_KEY_ALIAS_DEV }}
CLIENT_KEY_ALIAS_STAGING: ${{ secrets.CLIENT_KEY_ALIAS_STAGING }}
CLIENT_KEY_ALIAS_PROD: ${{ secrets.CLIENT_KEY_ALIAS_PROD }}
CLIENT_KEY_PASSWORD_DEV: ${{ secrets.CLIENT_KEY_PASSWORD_DEV }}
CLIENT_KEY_PASSWORD_STAGING: ${{ secrets.CLIENT_KEY_PASSWORD_STAGING }}
CLIENT_KEY_PASSWORD_PROD: ${{ secrets.CLIENT_KEY_PASSWORD_PROD }}
run: |
APP="${{ github.event.inputs.app }}"
ENV="${{ github.event.inputs.environment }}"
echo "🔐 Setting up Android signing for $APP in $ENV environment..."
# Determine which keystore to use
if [ "$APP" = "worker-mobile-app" ]; then
APP_TYPE="WORKER"
APP_NAME="STAFF" # CodeMagic uses STAFF in env var names
else
APP_TYPE="CLIENT"
APP_NAME="CLIENT"
fi
# Convert environment to uppercase for env var names
ENV_UPPER=$(echo "$ENV" | tr '[:lower:]' '[:upper:]')
if [ "$ENV_UPPER" = "STAGE" ]; then
ENV_UPPER="STAGING" # CodeMagic uses STAGING instead of STAGE
fi
# Get the keystore secret name dynamically
KEYSTORE_BASE64_VAR="${APP_TYPE}_KEYSTORE_${ENV_UPPER}_BASE64"
KEYSTORE_PASSWORD_VAR="${APP_TYPE}_KEYSTORE_PASSWORD_${ENV_UPPER}"
KEY_ALIAS_VAR="${APP_TYPE}_KEY_ALIAS_${ENV_UPPER}"
KEY_PASSWORD_VAR="${APP_TYPE}_KEY_PASSWORD_${ENV_UPPER}"
# Get values using indirect expansion
KEYSTORE_BASE64="${!KEYSTORE_BASE64_VAR}"
KEYSTORE_PASSWORD="${!KEYSTORE_PASSWORD_VAR}"
KEY_ALIAS="${!KEY_ALIAS_VAR}"
KEY_PASSWORD="${!KEY_PASSWORD_VAR}"
# Check if secrets are configured
if [ -z "$KEYSTORE_BASE64" ]; then
echo "⚠️ WARNING: Keystore secret $KEYSTORE_BASE64_VAR is not configured!"
echo "⚠️ APK will be built UNSIGNED for $ENV environment."
echo "⚠️ Please configure GitHub Secrets as documented in docs/RELEASE/APK_SIGNING_SETUP.md"
exit 0
fi
# Create temporary directory for keystore
KEYSTORE_DIR="${{ runner.temp }}/keystores"
mkdir -p "$KEYSTORE_DIR"
KEYSTORE_PATH="$KEYSTORE_DIR/release.jks"
# Decode keystore from base64
echo "$KEYSTORE_BASE64" | base64 -d > "$KEYSTORE_PATH"
if [ ! -f "$KEYSTORE_PATH" ]; then
echo "❌ Failed to decode keystore!"
exit 1
fi
echo "✅ Keystore decoded successfully"
echo "📦 Keystore size: $(ls -lh "$KEYSTORE_PATH" | awk '{print $5}')"
# Export environment variables for build.gradle.kts
# Using CodeMagic-compatible variable names
echo "CI=true" >> $GITHUB_ENV
echo "CM_KEYSTORE_PATH_${APP_NAME}=$KEYSTORE_PATH" >> $GITHUB_ENV
echo "CM_KEYSTORE_PASSWORD_${APP_NAME}=$KEYSTORE_PASSWORD" >> $GITHUB_ENV
echo "CM_KEY_ALIAS_${APP_NAME}=$KEY_ALIAS" >> $GITHUB_ENV
echo "CM_KEY_PASSWORD_${APP_NAME}=$KEY_PASSWORD" >> $GITHUB_ENV
echo "✅ Signing environment configured for $APP_NAME ($ENV environment)"
echo "🔑 Using key alias: $KEY_ALIAS"
- name: 🏗️ Build APK
id: build_apk
run: |
APP="${{ github.event.inputs.app }}"
if [ "$APP" = "worker-mobile-app" ]; then
echo "📱 Building Staff (Worker) APK..."
make mobile-staff-build PLATFORM=apk MODE=release
APP_NAME="staff"
else
echo "📱 Building Client APK..."
make mobile-client-build PLATFORM=apk MODE=release
APP_NAME="client"
fi
# Find the generated APK (Flutter places it in build/app/outputs/flutter-apk/)
APK_PATH=$(find apps/mobile/apps/${APP_NAME}/build/app/outputs/flutter-apk -name "app-release.apk" 2>/dev/null | head -n 1)
# Fallback to searching entire apps directory if not found
if [ -z "$APK_PATH" ]; then
APK_PATH=$(find apps/mobile/apps/${APP_NAME} -name "app-release.apk" | head -n 1)
fi
if [ -z "$APK_PATH" ]; then
echo "❌ Error: APK not found!"
echo "Searched in apps/mobile/apps/${APP_NAME}/"
find apps/mobile/apps/${APP_NAME} -name "*.apk" || echo "No APK files found"
exit 1
fi
echo "✅ APK built successfully: $APK_PATH"
echo "app_name=${APP_NAME}" >> $GITHUB_OUTPUT
echo "apk_path=${APK_PATH}" >> $GITHUB_OUTPUT
- name: ✅ Verify APK Signature
run: |
APK_PATH="${{ steps.build_apk.outputs.apk_path }}"
if [ ! -f "$APK_PATH" ]; then
echo "❌ APK not found at: $APK_PATH"
exit 1
fi
echo "🔍 Verifying APK signature..."
# Check if APK is signed
if jarsigner -verify -verbose "$APK_PATH" 2>&1 | grep -q "jar verified"; then
echo "✅ APK is properly signed!"
# Extract certificate details
echo ""
echo "📜 Certificate Details:"
jarsigner -verify -verbose -certs "$APK_PATH" 2>&1 | grep -A 3 "X.509" || true
# Get signer info
echo ""
echo "🔑 Signer Information:"
keytool -printcert -jarfile "$APK_PATH" | head -n 15
else
echo "⚠️ WARNING: APK signature verification failed or APK is unsigned!"
echo ""
echo "This may happen if:"
echo " 1. GitHub Secrets are not configured for this environment"
echo " 2. Keystore credentials are incorrect"
echo " 3. Build configuration didn't apply signing"
echo ""
echo "See: docs/RELEASE/APK_SIGNING_SETUP.md for setup instructions"
# Don't fail the build, just warn
# exit 1
fi
- name: 📤 Upload APK as Artifact
uses: actions/upload-artifact@v4
with:
name: ${{ github.event.inputs.app }}-${{ needs.validate-and-create-release.outputs.version }}-${{ github.event.inputs.environment }}
path: apps/mobile/apps/${{ steps.build_apk.outputs.app_name }}/build/app/outputs/flutter-apk/app-release.apk
if-no-files-found: error
retention-days: 30
- name: 📦 Attach APK to GitHub Release
if: ${{ github.event.inputs.create_github_release == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG_NAME="${{ needs.validate-and-create-release.outputs.tag_name }}"
APP="${{ github.event.inputs.app }}"
APP_NAME="${{ steps.build_apk.outputs.app_name }}"
VERSION="${{ needs.validate-and-create-release.outputs.version }}"
ENV="${{ github.event.inputs.environment }}"
# Find APK in build output
APK_PATH="apps/mobile/apps/${APP_NAME}/build/app/outputs/flutter-apk/app-release.apk"
if [ ! -f "$APK_PATH" ]; then
echo "❌ Error: APK not found at $APK_PATH"
echo "Searching for APK files..."
find apps/mobile/apps/${APP_NAME} -name "*.apk"
exit 1
fi
# Create proper APK name based on app type
if [ "$APP" = "worker-mobile-app" ]; then
APK_NAME="krow-withus-worker-mobile-${ENV}-v${VERSION}.apk"
else
APK_NAME="krow-withus-client-mobile-${ENV}-v${VERSION}.apk"
fi
# Copy APK with proper name
cp "$APK_PATH" "/tmp/$APK_NAME"
# Upload to GitHub Release
echo "📤 Uploading $APK_NAME to release $TAG_NAME..."
gh release upload "$TAG_NAME" "/tmp/$APK_NAME" --clobber
echo "✅ APK attached to release: $APK_NAME"