From 62bc5feee66aec91ccbeb9db55c380418f5a111d Mon Sep 17 00:00:00 2001 From: HarishV14 Date: Fri, 13 Mar 2026 15:06:45 +0530 Subject: [PATCH 1/8] feat: Add batch mode for release updates with per-subdomain artifacts --- .github/workflows/release_update.yml | 68 +++++++++------ fastlane/Fastfile | 120 +++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 25 deletions(-) diff --git a/.github/workflows/release_update.yml b/.github/workflows/release_update.yml index d490e3b29..d4ff678f2 100644 --- a/.github/workflows/release_update.yml +++ b/.github/workflows/release_update.yml @@ -3,10 +3,17 @@ name: Release update on: workflow_dispatch: inputs: - subdomain: - description: "Enter the institute's subdomain or all to release update(all -> deploy update for all institute)" - required: true + mode: + description: "Release mode: 'all' releases updates for all institutes. 'batch' releases updates for the provided comma-separated list." default: "all" + 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." @@ -25,31 +32,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 +86,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: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_to subdomain:${{ github.event.inputs.subdomain }} release_option:${{ github.event.inputs.release_option }} + bundle exec fastlane release_update_batch subdomains:"${{ github.event.inputs.subdomains }}" release_option:${{ github.event.inputs.release_option }} - name: Store app artifacts + if: ${{ github.event.inputs.mode == 'all' }} uses: actions/upload-artifact@v4 with: name: app @@ -107,3 +109,19 @@ 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: ${{ github.event.inputs.mode == 'batch' }} + uses: actions/upload-artifact@v4 + with: + name: batch-app-artifacts + path: | + batch_artifacts + + - name: Store batch summary + if: ${{ github.event.inputs.mode }} == 'batch' + uses: actions/upload-artifact@v4 + with: + name: batch-summary + path: | + release_update_batch_summary.json diff --git a/fastlane/Fastfile b/fastlane/Fastfile index ec1c9e193..afba8aa53 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,9 +1,129 @@ +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 = [] + + 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 + + app_config = nil + begin + app_config = update_app_version(subdomain: subdomain) + rescue => e + failed << { "subdomain" => subdomain, "step" => "update_app_version", "error" => e.message } + next + end + + begin + generate_customized_app_files(app_config: app_config) + rescue => e + failed << { "subdomain" => subdomain, "step" => "generate_customized_app_files", "error" => e.message } + next + end + + begin + artifact_root = File.join("batch_artifacts", subdomain) + FileUtils.mkdir_p(artifact_root) + + 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" } + ] + + missing = [] + expected_artifacts.each do |artifact| + if File.exist?(artifact["src"]) + FileUtils.cp(artifact["src"], File.join(artifact_root, artifact["dst"])) + else + missing << artifact["src"] + end + end + + if missing.any? + raise "Missing build artifacts: #{missing.join(', ')}" + end + rescue => e + failed << { "subdomain" => subdomain, "step" => "collect_artifacts", "error" => e.message } + next + end + + begin + deploy_app( + play_console_key_file: app_config["play_console_key_file"], + package_name: app_config["package_name"], + release_option: release_option + ) + rescue => e + failed << { "subdomain" => subdomain, "step" => "deploy_app", "error" => e.message } + next + end + + succeeded << subdomain + 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" From 62f38218d7828c6a8626211f29d55e3db1e48acb Mon Sep 17 00:00:00 2001 From: HarishV14 Date: Fri, 13 Mar 2026 15:16:41 +0530 Subject: [PATCH 2/8] Added get_app_config verification subdomain verfication from api --- fastlane/Fastfile | 10 ++++++++++ fastlane/actions/fetch_subdomains.rb | 3 +++ fastlane/actions/get_app_config.rb | 3 +++ fastlane/actions/update_app_version.rb | 3 +++ 4 files changed, 19 insertions(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index afba8aa53..f1312dc40 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -40,6 +40,16 @@ lane :release_update_batch do |params| next end + 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 + rescue => e + failed << { "subdomain" => subdomain, "step" => "validate_subdomain", "error" => e.message } + next + end + app_config = nil begin app_config = update_app_version(subdomain: subdomain) 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 From 2a612871a4a87ef8d56b92408cbeb4b1a4fd3874 Mon Sep 17 00:00:00 2001 From: HarishV14 Date: Fri, 13 Mar 2026 15:28:51 +0530 Subject: [PATCH 3/8] gemini suggest changes --- fastlane/Fastfile | 57 ++++++++++++++++------------------------------- 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index f1312dc40..d59a61832 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -31,6 +31,12 @@ lane :release_update_batch do |params| 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 << { @@ -40,70 +46,45 @@ lane :release_update_batch do |params| 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 - rescue => e - failed << { "subdomain" => subdomain, "step" => "validate_subdomain", "error" => e.message } - next - end - app_config = nil - begin + current_step = "update_app_version" app_config = update_app_version(subdomain: subdomain) - rescue => e - failed << { "subdomain" => subdomain, "step" => "update_app_version", "error" => e.message } - next - end - begin + current_step = "generate_customized_app_files" generate_customized_app_files(app_config: app_config) - rescue => e - failed << { "subdomain" => subdomain, "step" => "generate_customized_app_files", "error" => e.message } - next - end - begin + current_step = "collect_artifacts" artifact_root = File.join("batch_artifacts", subdomain) FileUtils.mkdir_p(artifact_root) - 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" } - ] - missing = [] expected_artifacts.each do |artifact| - if File.exist?(artifact["src"]) - FileUtils.cp(artifact["src"], File.join(artifact_root, artifact["dst"])) + src = artifact["src"] + if File.exist?(src) + FileUtils.cp(src, File.join(artifact_root, artifact["dst"])) else - missing << artifact["src"] + missing << src end end + raise "Missing build artifacts: #{missing.join(', ')}" if missing.any? - if missing.any? - raise "Missing build artifacts: #{missing.join(', ')}" - end - rescue => e - failed << { "subdomain" => subdomain, "step" => "collect_artifacts", "error" => e.message } - next - end - - begin + 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" => "deploy_app", "error" => e.message } - next + failed << { "subdomain" => subdomain, "step" => current_step, "error" => e.message } end - - succeeded << subdomain end ended_at = Time.now From 3e3c965e40072f45804b72f77203ea8295b764b1 Mon Sep 17 00:00:00 2001 From: HarishV14 Date: Fri, 13 Mar 2026 15:42:45 +0530 Subject: [PATCH 4/8] refractor --- .github/workflows/release_update.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_update.yml b/.github/workflows/release_update.yml index d4ff678f2..321e55c20 100644 --- a/.github/workflows/release_update.yml +++ b/.github/workflows/release_update.yml @@ -5,7 +5,7 @@ on: inputs: mode: description: "Release mode: 'all' releases updates for all institutes. 'batch' releases updates for the provided comma-separated list." - default: "all" + default: "batch" type: choice options: - "all" From 22aee2f74014f6c37b8486102a4a3ca72f781b74 Mon Sep 17 00:00:00 2001 From: HarishV14 Date: Fri, 13 Mar 2026 15:53:25 +0530 Subject: [PATCH 5/8] testing --- .github/workflows/release_update.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release_update.yml b/.github/workflows/release_update.yml index 321e55c20..ff2004b63 100644 --- a/.github/workflows/release_update.yml +++ b/.github/workflows/release_update.yml @@ -1,6 +1,9 @@ name: Release update on: + push: + branches: + - "**" workflow_dispatch: inputs: mode: @@ -13,7 +16,7 @@ on: subdomains: description: "Batch mode only: comma-separated subdomains (example: inst1,inst2,inst3). Can contain a single subdomain." required: false - default: "" + default: "lmsdemo,lms" release_option: description: "Choose the type of release. 'Completed' for a full release or 'Draft' for a pre-release version." From 301d9f0ba9665395af7a587d9fefdd1e0f0b1eb1 Mon Sep 17 00:00:00 2001 From: HarishV14 Date: Fri, 13 Mar 2026 16:05:12 +0530 Subject: [PATCH 6/8] removed --- .github/workflows/release_update.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/release_update.yml b/.github/workflows/release_update.yml index ff2004b63..321e55c20 100644 --- a/.github/workflows/release_update.yml +++ b/.github/workflows/release_update.yml @@ -1,9 +1,6 @@ name: Release update on: - push: - branches: - - "**" workflow_dispatch: inputs: mode: @@ -16,7 +13,7 @@ on: subdomains: description: "Batch mode only: comma-separated subdomains (example: inst1,inst2,inst3). Can contain a single subdomain." required: false - default: "lmsdemo,lms" + default: "" release_option: description: "Choose the type of release. 'Completed' for a full release or 'Draft' for a pre-release version." From c66aeb75177b2f20b9585c25676c0f946458fdb6 Mon Sep 17 00:00:00 2001 From: HarishV14 Date: Fri, 13 Mar 2026 16:27:34 +0530 Subject: [PATCH 7/8] testing --- .github/workflows/release_update.yml | 92 +++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release_update.yml b/.github/workflows/release_update.yml index 321e55c20..e7f123180 100644 --- a/.github/workflows/release_update.yml +++ b/.github/workflows/release_update.yml @@ -1,6 +1,12 @@ name: Release update on: + push: + branches: + - "**" + paths: + - "fastlane/**" + - ".github/workflows/release_update.yml" workflow_dispatch: inputs: mode: @@ -25,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 }} @@ -101,7 +108,7 @@ jobs: bundle exec fastlane release_update_batch subdomains:"${{ github.event.inputs.subdomains }}" release_option:${{ github.event.inputs.release_option }} - name: Store app artifacts - if: ${{ github.event.inputs.mode == 'all' }} + if: ${{ always() && github.event.inputs.mode == 'all' }} uses: actions/upload-artifact@v4 with: name: app @@ -111,7 +118,7 @@ jobs: app/build/outputs/apk/debug/app-debug.apk - name: Store batch app artifacts - if: ${{ github.event.inputs.mode == 'batch' }} + if: ${{ always() && github.event.inputs.mode == 'batch' }} uses: actions/upload-artifact@v4 with: name: batch-app-artifacts @@ -119,9 +126,88 @@ jobs: batch_artifacts - name: Store batch summary - if: ${{ github.event.inputs.mode }} == 'batch' + 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: + 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 + + - 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 From d0f89710340b665c89fea942669edf45359ef95e Mon Sep 17 00:00:00 2001 From: HarishV14 Date: Fri, 13 Mar 2026 16:34:50 +0530 Subject: [PATCH 8/8] testing2 --- .github/workflows/release_update.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/release_update.yml b/.github/workflows/release_update.yml index e7f123180..2e9714f5f 100644 --- a/.github/workflows/release_update.yml +++ b/.github/workflows/release_update.yml @@ -137,6 +137,8 @@ jobs: 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: @@ -149,6 +151,14 @@ jobs: 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