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: īŋŊ 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"