diff --git a/.github/workflows/release_update.yml b/.github/workflows/release_update.yml index d490e3b29..2e9714f5f 100644 --- a/.github/workflows/release_update.yml +++ b/.github/workflows/release_update.yml @@ -1,12 +1,25 @@ name: Release update on: + push: + branches: + - "**" + paths: + - "fastlane/**" + - ".github/workflows/release_update.yml" workflow_dispatch: inputs: - subdomain: - description: "Enter the institute's subdomain or all to release update(all -> deploy update for all institute)" - required: true - default: "all" + mode: + description: "Release mode: 'all' releases updates for all institutes. 'batch' releases updates for the provided comma-separated list." + default: "batch" + type: choice + options: + - "all" + - "batch" + subdomains: + description: "Batch mode only: comma-separated subdomains (example: inst1,inst2,inst3). Can contain a single subdomain." + required: false + default: "" release_option: description: "Choose the type of release. 'Completed' for a full release or 'Draft' for a pre-release version." @@ -18,6 +31,7 @@ on: jobs: release_update: + if: ${{ github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest env: GITHUB_ACCESS_KEY: ${{ secrets.GH_ACCESS_KEY }} @@ -25,31 +39,17 @@ jobs: API_ACCESS_KEY: ${{ secrets.API_ACCESS_KEY }} steps: - - name: Validate branch based on subdomain + - name: Validate inputs run: | - echo "Current branch: $GITHUB_REF" - SUBDOMAIN="${{ github.event.inputs.subdomain }}" - BRANCH_NAME="${GITHUB_REF#refs/heads/}" - # Define the special subdomains - SPECIAL_SUBDOMAINS="brilliantpalalms brilliantpalaelearn race" - if echo "$SPECIAL_SUBDOMAINS" | grep -w "$SUBDOMAIN" > /dev/null; then - if [ "$BRANCH_NAME" != "$SUBDOMAIN" ]; then - RED='\033[0;31m' - NC='\033[0m' # No Color - echo -e "${RED}ERROR: For subdomain '$SUBDOMAIN', workflow must run on branch '$SUBDOMAIN'. Current branch is '$BRANCH_NAME'.${NC}" - echo -e "${RED}This is because the '$SUBDOMAIN' institute has a customized UI maintained in a separate branch named '$SUBDOMAIN'.${NC}" - echo -e "${RED}Please merge the latest changes from 'master' into the '$SUBDOMAIN' branch and run this release update from that branch.${NC}" + MODE="${{ github.event.inputs.mode }}" + SUBDOMAINS="${{ github.event.inputs.subdomains }}" + if [ "$MODE" = "batch" ]; then + if [ -z "$(echo "$SUBDOMAINS" | tr -d '[:space:]')" ]; then + echo "ERROR: mode=batch requires non-empty 'subdomains' input (comma-separated)." exit 1 fi - else - echo "Subdomain '$SUBDOMAIN' can run on any branch. Current branch is '$BRANCH_NAME'." fi - - name: Validate subdomain - if: ${{ github.event.inputs.subdomain }} != 'all' - run: | - curl -f https://${{ github.event.inputs.subdomain }}.testpress.in/api/v2.5/admin/android/app-config/ -H "API-access-key: $API_ACCESS_KEY" - - name: Setup JDK 17 uses: actions/setup-java@v3 with: @@ -93,13 +93,22 @@ jobs: wget https://media.testpress.in/static/android/zoom_sdk.zip unzip -o ./zoom_sdk.zip - - name: Build and deploy app + - name: Build and deploy app (all) + if: ${{ github.event.inputs.mode == 'all' }} run: | export LC_ALL=en_US.UTF-8 export LANG=en_US.UTF-8 - bundle exec fastlane release_update_to subdomain:${{ github.event.inputs.subdomain }} release_option:${{ github.event.inputs.release_option }} + bundle exec fastlane release_update_to subdomain:all release_option:${{ github.event.inputs.release_option }} + + - name: Build and deploy app (batch) + if: ${{ github.event.inputs.mode == 'batch' }} + run: | + export LC_ALL=en_US.UTF-8 + export LANG=en_US.UTF-8 + bundle exec fastlane release_update_batch subdomains:"${{ github.event.inputs.subdomains }}" release_option:${{ github.event.inputs.release_option }} - name: Store app artifacts + if: ${{ always() && github.event.inputs.mode == 'all' }} uses: actions/upload-artifact@v4 with: name: app @@ -107,3 +116,108 @@ jobs: app/build/outputs/apk/release/app-release.apk app/build/outputs/bundle/release/app-release.aab app/build/outputs/apk/debug/app-debug.apk + + - name: Store batch app artifacts + if: ${{ always() && github.event.inputs.mode == 'batch' }} + uses: actions/upload-artifact@v4 + with: + name: batch-app-artifacts + path: | + batch_artifacts + + - name: Store batch summary + if: ${{ always() && github.event.inputs.mode == 'batch' }} + uses: actions/upload-artifact@v4 + with: + name: batch-summary + path: | + release_update_batch_summary.json + + push_batch_build: + if: ${{ github.event_name == 'push' }} + runs-on: ubuntu-latest + env: + GITHUB_ACCESS_KEY: ${{ secrets.GH_ACCESS_KEY }} + GITHUB_USERNAME: ${{ secrets.GH_USERNAME }} + API_ACCESS_KEY: ${{ secrets.API_ACCESS_KEY }} + + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + + - name: Validate secrets + run: | + if [ -z "$(echo "$API_ACCESS_KEY" | tr -d '[:space:]')" ]; then + echo "ERROR: API_ACCESS_KEY is not available." + exit 1 + fi + if [ -z "$(echo "$GITHUB_USERNAME" | tr -d '[:space:]')" ]; then + echo "ERROR: GITHUB_USERNAME is not available (required for GitHub Packages Maven auth)." + exit 1 + fi + if [ -z "$(echo "$GITHUB_ACCESS_KEY" | tr -d '[:space:]')" ]; then + echo "ERROR: GITHUB_ACCESS_KEY is not available (required for GitHub Packages Maven auth)." + exit 1 + fi + + - name: Setup JDK 17 + uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: 17 + + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + + - name: Caching Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Setup ruby and fastlane + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + bundler-cache: true + + - name: Cache Zoom SDK files + id: zoom-cache + uses: actions/cache@v3 + with: + path: | + ./mobilertc + ./commonlib + key: zoom_sdk_files + + - name: Setup Zoom SDK + if: steps.zoom-cache.outputs.cache-hit != 'true' + run: | + wget https://media.testpress.in/static/android/zoom_sdk.zip + unzip -o ./zoom_sdk.zip + + - name: Batch release (draft) + run: | + export LC_ALL=en_US.UTF-8 + export LANG=en_US.UTF-8 + bundle exec fastlane release_update_batch subdomains:"lms,lmsdemo" release_option:draft + + - name: Store push batch app artifacts + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: push-batch-app-artifacts + path: | + batch_artifacts + + - name: Store push batch release summary + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: push-batch-release-summary + path: | + release_update_batch_summary.json diff --git a/fastlane/Fastfile b/fastlane/Fastfile index ec1c9e193..d59a61832 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,9 +1,120 @@ +require 'json' +require 'fileutils' +require 'time' + desc "Build customized app for an institute" lane :build_app_files do |params| app_config = get_app_config(subdomain: params[:subdomain]) generate_customized_app_files(app_config: app_config, split_apk: params[:split_apk]) end +desc "Release updates for a comma-separated batch of subdomains (always continues on error)" +lane :release_update_batch do |params| + raw_subdomains = params[:subdomains].to_s + release_option = params[:release_option] + + subdomains = raw_subdomains + .split(/[,\s]+/) + .map { |s| s.strip.downcase } + .reject(&:empty?) + .uniq + + if subdomains.empty? + UI.user_error!("No subdomains provided. Pass subdomains:\"inst1,inst2,inst3\"") + end + + current_branch = sh("git rev-parse --abbrev-ref HEAD", log: false).strip + special_branch_subdomains = %w[brilliantpalalms brilliantpalaelearn race] + + started_at = Time.now + succeeded = [] + failed = [] + skipped = [] + + expected_artifacts = [ + { "src" => File.join("app", "build", "outputs", "bundle", "release", "app-release.aab"), "dst" => "app-release.aab" }, + { "src" => File.join("app", "build", "outputs", "apk", "release", "app-release.apk"), "dst" => "app-release.apk" }, + { "src" => File.join("app", "build", "outputs", "apk", "debug", "app-debug.apk"), "dst" => "app-debug.apk" } + ].freeze + + subdomains.each do |subdomain| + if special_branch_subdomains.include?(subdomain) && current_branch != subdomain + skipped << { + "subdomain" => subdomain, + "reason" => "Requires running on branch '#{subdomain}' (current: '#{current_branch}')" + } + next + end + + current_step = "validate_subdomain" + begin + config = get_app_config(subdomain: subdomain) + unless config.is_a?(Hash) && config["package_name"].to_s.strip != "" + raise "Invalid app-config response (missing package_name)" + end + + current_step = "update_app_version" + app_config = update_app_version(subdomain: subdomain) + + current_step = "generate_customized_app_files" + generate_customized_app_files(app_config: app_config) + + current_step = "collect_artifacts" + artifact_root = File.join("batch_artifacts", subdomain) + FileUtils.mkdir_p(artifact_root) + + missing = [] + expected_artifacts.each do |artifact| + src = artifact["src"] + if File.exist?(src) + FileUtils.cp(src, File.join(artifact_root, artifact["dst"])) + else + missing << src + end + end + raise "Missing build artifacts: #{missing.join(', ')}" if missing.any? + + current_step = "deploy_app" + deploy_app( + play_console_key_file: app_config["play_console_key_file"], + package_name: app_config["package_name"], + release_option: release_option + ) + + succeeded << subdomain + rescue => e + failed << { "subdomain" => subdomain, "step" => current_step, "error" => e.message } + end + end + + ended_at = Time.now + summary = { + "mode" => "batch", + "release_option" => release_option, + "branch" => current_branch, + "started_at" => started_at.iso8601, + "ended_at" => ended_at.iso8601, + "duration_seconds" => (ended_at - started_at).to_i, + "requested_subdomains" => subdomains, + "succeeded" => succeeded, + "failed" => failed, + "skipped" => skipped, + "counts" => { + "requested" => subdomains.length, + "succeeded" => succeeded.length, + "failed" => failed.length, + "skipped" => skipped.length + } + } + + File.write("release_update_batch_summary.json", JSON.pretty_generate(summary)) + + UI.message("Batch release summary: requested=#{subdomains.length}, succeeded=#{succeeded.length}, failed=#{failed.length}, skipped=#{skipped.length}") + if failed.any? + UI.user_error!("Batch release completed with failures. See release_update_batch_summary.json") + end +end + lane :release_update_to do |params| release_option = params[:release_option] if params[:subdomain] == "all" diff --git a/fastlane/actions/fetch_subdomains.rb b/fastlane/actions/fetch_subdomains.rb index 55f4bf71d..3a8bdfd9d 100644 --- a/fastlane/actions/fetch_subdomains.rb +++ b/fastlane/actions/fetch_subdomains.rb @@ -12,6 +12,9 @@ def self.run(params) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true response = http.request(request) + unless response.is_a?(Net::HTTPSuccess) + UI.user_error!("Fetch subdomains failed: HTTP #{response.code} - #{response.body}") + end return JSON.parse(response.body) end end diff --git a/fastlane/actions/get_app_config.rb b/fastlane/actions/get_app_config.rb index d5027c00d..5339aefe2 100644 --- a/fastlane/actions/get_app_config.rb +++ b/fastlane/actions/get_app_config.rb @@ -12,6 +12,9 @@ def self.run(params) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true response = http.request(request) + unless response.is_a?(Net::HTTPSuccess) + UI.user_error!("App config validation failed for '#{params[:subdomain]}': HTTP #{response.code} - #{response.body}") + end return JSON.parse(response.body) end diff --git a/fastlane/actions/update_app_version.rb b/fastlane/actions/update_app_version.rb index 14ba3dfa5..098bbf768 100644 --- a/fastlane/actions/update_app_version.rb +++ b/fastlane/actions/update_app_version.rb @@ -13,6 +13,9 @@ def self.run(params) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true response = http.request(request) + unless response.is_a?(Net::HTTPSuccess) + UI.user_error!("Android update failed for '#{params[:subdomain]}': HTTP #{response.code} - #{response.body}") + end return JSON.parse(response.body) end