diff --git a/.github/actions/android-build-firebase/action.yml b/.github/actions/android-build-firebase/action.yml new file mode 100644 index 0000000..0e4e2c0 --- /dev/null +++ b/.github/actions/android-build-firebase/action.yml @@ -0,0 +1,90 @@ +name: Enterprise build +description: Builds and uploads app to Firebase App Distribution. + +inputs: + ## Required Inputs + test_gradle_task: + description: "A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest`" + required: true + package_gradle_task: + description: "A Gradle task for packaging universal APK, eg. 'packageEnterpriseUniversalApk'" + required: true + upload_gradle_task: + description: "A Gradle task for uploading APK, for example `appDistributionUploadEnterprise`" + required: true + app_distribution_groups: + description: "Comma-separated list of Firebase App Distribution group IDs" + required: true + app_distribution_service_account: + required: true + description: "JSON key of service account with permissions to upload build to Firebase App Distribution" + + ## Optional Inputs + version_name: + description: "Version name. Example: '1.X.X-snapshot'" + required: false + build_number_offset: + description: "Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers." + required: false + default: 0 + release_notes: + description: "Release notes for this build" + required: false + default: ${{ github.event.head_commit.message }} + kmp_flavor: + description: "KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects" + required: false + default: 'test' + secret_properties_file: + description: "A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig." + required: false + default: 'secrets.properties' + secret_properties: + required: false + description: "Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input." + +runs: + using: "composite" + steps: + - name: Prepare Environment + shell: bash + run: | + { + echo "ANDROID_BUILD_NUMBER=$((GITHUB_RUN_NUMBER + ${{ inputs.build_number_offset}} ))"; + echo "KMP_FLAVOR=${{ inputs.kmp_flavor }}" + } >> "$GITHUB_ENV" + + # Set ANDROID_VERSION_NAME only if VERSION_NAME is provided + if [[ -n "${{ inputs.version_name }}" ]]; then + echo "ANDROID_VERSION_NAME=${{ inputs.version_name }}" >> "$GITHUB_ENV" + fi + + echo '${{ inputs.secret_properties }}' > ${{ inputs.secret_properties_file }} + - name: Run Unit tests + shell: bash + run: ./gradlew --continue ${{ inputs.test_gradle_task }} + - name: Build universal APK + id: build_apk + shell: bash + run: | + ./gradlew ${{ inputs.package_gradle_task }} -P buildkonfig.flavor="$KMP_FLAVOR" + APK_FILE=$(find . -name '*.apk' | grep -v -e '.*intermediate' -e '.*baseline') + echo "universal_apk_file=$APK_FILE" >> "$GITHUB_OUTPUT" + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: universal_apk + path: ${{ steps.build_apk.outputs.universal_apk_file }} + - name: Upload APK to Firebase App Distribution + shell: bash + run: | + echo '${{ inputs.app_distribution_service_account }}' > "firebase_credentials.json" + + ./gradlew ${{ inputs.upload_gradle_task }} \ + --serviceCredentialsFile="firebase_credentials.json" \ + --groups="${{ inputs.app_distribution_groups }}" \ + --artifactType="APK" \ + --artifactPath="${{ steps.build_apk.outputs.universal_apk_file }}" \ + --releaseNotes='${{ inputs.release_notes }}' + + rm firebase_credentials.json diff --git a/.github/actions/android-build-googlePlay/action.yml b/.github/actions/android-build-googlePlay/action.yml new file mode 100644 index 0000000..dfed8da --- /dev/null +++ b/.github/actions/android-build-googlePlay/action.yml @@ -0,0 +1,99 @@ +name: Release build +description: Builds and uploads app to Google Play. + +inputs: + ## Required Inputs + bundle_gradle_task: + description: "A Gradle task for assembling app bundle, for example `bundleRelease`" + required: true + version_name: + description: "Version name. Example: '1.0.0'" + required: true + signing_keystore_password: + description: "Password to provided keystore" + required: true + signing_key_alias: + description: "Alias of the signing key in the provided keystore" + required: true + signing_key_password: + description: "Password to the key in the provided keystore" + required: true + google_play_application_id: + description: "Google Play applicationId" + required: true + google_play_whatsnew_dir: + description: "Path to directory with changelog files according to documentation in https://github.com/r0adkll/upload-google-play" + required: true + google_play_publish_service_account: + required: true + description: "JSON key of service account with permissions to upload build to Google Play" + + ## Optional Inputs + changes_not_sent_for_review: + description: 'A changesNotSentForReview Google Play flag. Enable when last google review failed, disable when last review was successful.' + required: false + default: false + build_number_offset: + description: "Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers." + required: false + default: 0 + kmp_flavor: + description: "KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects" + required: false + default: 'prod' + secret_properties_file: + description: "A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig." + required: false + default: 'secrets.properties' + secret_properties: + required: false + description: "Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input." + +runs: + using: "composite" + steps: + - name: Prepare Environment + shell: bash + run: | + { + echo "ANDROID_BUILD_NUMBER=$((GITHUB_RUN_NUMBER + ${{ inputs.build_number_offset}} ))"; + echo "ANDROID_VERSION_NAME=${{ inputs.version_name }}"; + echo "KMP_FLAVOR=${{ inputs.kmp_flavor }}" + } >> "$GITHUB_ENV" + + echo '${{ inputs.secret_properties }}' > ${{ inputs.secret_properties_file }} + - name: Generate Artifacts (AAB) + id: build_aab + shell: bash + env: + ANDROID_KEYSTORE_PASSWORD: ${{ inputs.signing_keystore_password }} + ANDROID_KEY_ALIAS: ${{ inputs.signing_key_alias }} + ANDROID_KEY_PASSWORD: ${{ inputs.signing_key_password }} + run: | + ./gradlew ${{ inputs.bundle_gradle_task }} -P buildkonfig.flavor="$KMP_FLAVOR" + BUNDLE_FILE=$(find . -name '*.aab' | grep -v -e '.*intermediate' -e '.*baseline') + MAPPING_FILE=$(find . -name mapping.txt) + + echo "bundle_file=$BUNDLE_FILE" >> "$GITHUB_OUTPUT" + echo "mapping_file=$MAPPING_FILE" >> "$GITHUB_OUTPUT" + - name: Upload AAB artifact + uses: actions/upload-artifact@v4 + with: + name: release_bundle + path: ${{ steps.build_aab.outputs.bundle_file }} + - name: Upload Mapping file artifact + uses: actions/upload-artifact@v4 + with: + name: mapping_file + path: ${{ steps.build_aab.outputs.mapping_file }} + - name: Upload to Google Play + uses: r0adkll/upload-google-play@v1.1.1 + with: + serviceAccountJsonPlainText: ${{ inputs.google_play_publish_service_account }} + packageName: ${{ inputs.google_play_application_id }} + releaseFiles: ${{ steps.build_aab.outputs.bundle_file }} + track: internal + status: draft + whatsNewDirectory: ${{ inputs.google_play_whatsnew_dir }} + mappingFile: ${{ steps.build_aab.outputs.mapping_file }} + changesNotSentForReview: ${{ toJSON(inputs.changes_not_sent_for_review) }} diff --git a/.github/actions/android-check/action.yml b/.github/actions/android-check/action.yml new file mode 100644 index 0000000..1cc5c02 --- /dev/null +++ b/.github/actions/android-check/action.yml @@ -0,0 +1,30 @@ +name: Android Check +description: Runs lint checks and unit tests. + +inputs: + # Required inputs + lint_gradle_task: + description: "A Gradle task(s) for executing lint check, for example `lintCheck lintRelease`" + required: true + test_gradle_task: + description: "A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest`" + required: true + +runs: + using: "composite" + steps: + - name: Run Lint Check + shell: bash + run: ./gradlew --continue ${{ inputs.lint_gradle_task }} + - name: Run Unit Tests + shell: bash + run: ./gradlew --continue ${{ inputs.test_gradle_task }} + - name: Danger action + uses: MeilCli/danger-action@v2 + continue-on-error: true + with: + plugins_file: 'Gemfile' + danger_file: 'Dangerfile' + danger_id: 'danger-pr' + env: + DANGER_GITHUB_API_TOKEN: ${{ github.token }} diff --git a/.github/actions/android-generate-baseline-profiles/action.yml b/.github/actions/android-generate-baseline-profiles/action.yml new file mode 100644 index 0000000..aa916db --- /dev/null +++ b/.github/actions/android-generate-baseline-profiles/action.yml @@ -0,0 +1,56 @@ +name: Generate Baseline Profiles +description: Generates baseline profiles and creates a PR with the changes + +inputs: + # Required inputs + generate_gradle_task: + description: "A Gradle task for generating baseline profiles, for example `generateBaselineProfile`" + required: true + signing_keystore_password: + description: "Password to provided keystore" + required: true + signing_key_alias: + description: "Alias of the signing key in the provided keystore" + required: true + signing_key_password: + description: "Password to the key in the provided keystore" + required: true + secret_properties_file: + description: "A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig." + required: false + default: 'secrets.properties' + secret_properties: + required: false + description: "Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input." + +runs: + using: "composite" + steps: + - name: Enable KVM group perms + shell: bash + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + ls /dev/kvm + - name: Prepare Environment + shell: bash + run: | + echo '${{ inputs.secret_properties }}' > ${{ inputs.secret_properties_file }} + - name: Generate profiles + env: + ANDROID_KEYSTORE_PASSWORD: ${{ inputs.signing_keystore_password }} + ANDROID_KEY_ALIAS: ${{ inputs.signing_key_alias }} + ANDROID_KEY_PASSWORD: ${{ inputs.signing_key_password }} + shell: bash + run: ./gradlew ${{ inputs.generate_gradle_task }} + -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true + -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + commit-message: 'Generate baseline profiles' + branch: 'feature/generate-baseline-profiles' + title: 'Generate baseline profiles' + body: '' + diff --git a/.github/actions/android-setup-environment/action.yml b/.github/actions/android-setup-environment/action.yml new file mode 100644 index 0000000..ae04a82 --- /dev/null +++ b/.github/actions/android-setup-environment/action.yml @@ -0,0 +1,54 @@ +name: Set up Android environment +description: Sets up Java, Gradle and Ruby and other preconditions for CI runs at Futured Android workflows. + +inputs: + # Java + java: + description: "Whether to set up Java" + required: false + default: 'true' + java_version: + description: "Java version to use, eg. '17'." + required: false + default: '17' + java_distribution: + description: "Java distribution to use, eg 'zulu'." + required: false + default: 'zulu' + # Ruby + ruby: + description: "Whether to set up Ruby" + required: false + default: 'true' + ruby_version: + description: "Ruby version." + required: false + default: '3.4' + # Gradle + gradle: + description: "Whether to set up Gradle" + required: false + default: 'true' + gradle_cache_encryption_key: + description: "Configuration cache encryption key. Leave empty if you don't need cache." + required: false + +runs: + using: "composite" + steps: + - name: Setup Ruby + if: ${{ inputs.ruby == 'true' }} + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ inputs.ruby_version }} + - name: Setup Java + if: ${{ inputs.java == 'true' }} + uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.java_version }} + distribution: ${{ inputs.java_distribution }} + - name: Setup Gradle + if: ${{ inputs.gradle == 'true' }} + uses: gradle/actions/setup-gradle@v4 + with: + cache-encryption-key: ${{ inputs.gradle_cache_encryption_key }} diff --git a/.github/actions/ios-kmp-build/action.yml b/.github/actions/ios-kmp-build/action.yml new file mode 100644 index 0000000..d556454 --- /dev/null +++ b/.github/actions/ios-kmp-build/action.yml @@ -0,0 +1,86 @@ +name: iOS KMP Build +description: Builds iOS app (optionally with KMP framework) and uploads to TestFlight + +inputs: + # Required + match_password: + description: "Password for decrypting of certificates and provisioning profiles." + required: true + app_store_connect_api_key_key: + description: "Private App Store Connect API key for submitting build to App Store." + required: true + app_store_connect_api_key_key_id: + required: true + description: "Private App Store Connect API key for submitting build to App Store." + app_store_connect_api_key_issuer_id: + required: true + description: "Private App Store Connect API issuer key for submitting build to App Store." + # Optional + secret_xcconfig_path: + description: "Path to the .xcconfig file. Selected secret properties will be appended to the end of this file." + required: false + secret_properties: + required: false + description: "Secrets in the format KEY = VALUE (one per line)." + secret_required_keys: + description: "Comma-separated list of required secret keys." + required: false + kmp_swift_package_integration: + description: "Whether KMP is integrated in Xcode project as a Swift Package" + required: false + kmp_swift_package_path: + description: "If `kmp_swift_package_integration` is 'true', then specifies a location of local Swift Package with Makefile. Example: 'iosApp/shared/KMP`" + required: false + kmp_swift_package_flavor: + description: "If `kmp_swift_package_integration`, specifies build flavor of KMP Package" + required: false + custom_values: + description: "Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: \"CUSTOM-1: 1; CUSTOM-2: 2\"" + required: false + testflight_changelog: + required: false + description: "Will be used as TestFlight changelog" + +runs: + using: "composite" + steps: + - name: Export secrets to .xcconfig file + if: ${{ inputs.secret_xcconfig_path != '' }} + uses: futuredapp/.github/.github/actions/ios-export-secrets@main + with: + XCCONFIG_PATH: ${{ inputs.secret_xcconfig_path }} + SECRET_PROPERTIES: ${{ inputs.secret_properties }} + REQUIRED_KEYS: ${{ inputs.secret_required_keys }} + - name: Build KMP Package + if: ${{ inputs.kmp_swift_package_integration }} + env: + KMP_BUILD_FLAVOR: ${{ inputs.kmp_swift_package_flavor }} + KMP_FRAMEWORK_BUILD_TYPE: release + shell: bash + run: | + cd ${{ inputs.kmp_swift_package_path }} + make build + - name: Fastlane Beta + working-directory: iosApp + shell: bash + run: | + gem install bundler + bundle install --jobs 4 --retry 3 + bundle exec fastlane beta + env: + MATCH_PASSWORD: ${{ inputs.match_password }} + PR_TITLE: ${{ inputs.testflight_changelog }} + APP_STORE_CONNECT_API_KEY_KEY: ${{ inputs.app_store_connect_api_key_key }} + APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ inputs.app_store_connect_api_key_key_id }} + APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ inputs.app_store_connect_api_key_issuer_id }} + CUSTOM_VALUES: ${{ inputs.custom_values }} + - name: Upload IPA + uses: actions/upload-artifact@v4 + with: + name: Build.ipa + path: build_output/*.ipa + - name: Upload dSYM + uses: actions/upload-artifact@v4 + with: + name: Build.app.dSYM.zip + path: build_output/*.app.dSYM.zip diff --git a/.github/actions/kmp-detect-changes/action.yml b/.github/actions/kmp-detect-changes/action.yml new file mode 100644 index 0000000..2f796b0 --- /dev/null +++ b/.github/actions/kmp-detect-changes/action.yml @@ -0,0 +1,35 @@ +name: KMP Detect Changes +description: Detects changes in KMP project to determine which platform-specific workflows should run + +inputs: + USE_GIT_LFS: + description: "Whether to download Git-LFS files" + type: boolean + required: false + default: false + +outputs: + iosFiles: + description: "Whether files affecting iOS build changed (all files except those in androidApp/)" + value: ${{ steps.file-changes.outputs.iosFiles }} + androidFiles: + description: "Whether files affecting Android build changed (all files except those in iosApp/)" + value: ${{ steps.file-changes.outputs.androidFiles }} + +runs: + using: "composite" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: ${{ inputs.USE_GIT_LFS }} + + - name: Detect Changes + uses: dorny/paths-filter@v3 + id: file-changes + with: + filters: | + iosFiles: + - "!(androidApp/**)" + androidFiles: + - "!(iosApp/**)" diff --git a/.github/actions/universal-detect-changes-and-generate-changelog/action.yml b/.github/actions/universal-detect-changes-and-generate-changelog/action.yml index 4b58016..1b5876d 100644 --- a/.github/actions/universal-detect-changes-and-generate-changelog/action.yml +++ b/.github/actions/universal-detect-changes-and-generate-changelog/action.yml @@ -27,6 +27,9 @@ outputs: merged_branches: description: "List of merged branch names" value: ${{ steps.generate_changelog.outputs.merged_branches }} + cache_key: + description: "Cache key to store latest built commit for this branch" + value: ${{ steps.cache_keys.outputs.cache_key_prefix }}-${{ github.sha }} runs: using: "composite" @@ -40,7 +43,26 @@ runs: id: cache_keys shell: bash run: | - echo "prev_run=$(expr ${{ github.run_number }} - 1)" >> $GITHUB_OUTPUT + BRANCH_NAME="${{ github.head_ref }}" + if [ -z "$BRANCH_NAME" ]; then + BRANCH_NAME="${{ github.ref_name }}" + fi + BRANCH_SAFE=$(echo "$BRANCH_NAME" | sed 's#[^A-Za-z0-9._-]#_#g') + + # Include workflow name in cache key to avoid conflicts between workflows + WORKFLOW_NAME="${{ github.workflow }}" + WORKFLOW_SAFE=$(echo "$WORKFLOW_NAME" | sed 's#[^A-Za-z0-9._-]#_#g') + CACHE_KEY_PREFIX="latest_builded_commit-${WORKFLOW_SAFE}-${BRANCH_SAFE}" + + if [ "${{ inputs.debug }}" == "true" ]; then + echo "[DEBUG] BRANCH_SAFE='$BRANCH_SAFE'" + echo "[DEBUG] WORKFLOW_NAME='$WORKFLOW_NAME'" + echo "[DEBUG] WORKFLOW_SAFE='$WORKFLOW_SAFE'" + echo "[DEBUG] CACHE_KEY_PREFIX='$CACHE_KEY_PREFIX'" + echo "[DEBUG] CALCULATED_CACHE_KEY='$CACHE_KEY_PREFIX-${{ github.sha }}'" + fi + echo "branch=$BRANCH_SAFE" >> $GITHUB_OUTPUT + echo "cache_key_prefix=$CACHE_KEY_PREFIX" >> $GITHUB_OUTPUT - name: Restore cache for previous build commit SHA id: restore_last_build_cache @@ -48,9 +70,9 @@ runs: continue-on-error: true with: path: latest_builded_commit.txt - key: latest_builded_commit-${{ steps.cache_keys.outputs.prev_run }} + key: ${{ steps.cache_keys.outputs.cache_key_prefix }}-${{ github.sha }} restore-keys: | - latest_builded_commit- + ${{ steps.cache_keys.outputs.cache_key_prefix }}- - name: Determine commit range for changelog and skip build id: determine_range diff --git a/.github/workflows/android-cloud-check.yml b/.github/workflows/android-cloud-check.yml index 519bf97..cefc60c 100644 --- a/.github/workflows/android-cloud-check.yml +++ b/.github/workflows/android-cloud-check.yml @@ -57,32 +57,14 @@ jobs: uses: actions/checkout@v4 with: lfs: ${{ inputs.USE_GIT_LFS }} - - name: Setup Ruby - uses: ruby/setup-ruby@v1 + - name: Set up environment + uses: futuredapp/.github/.github/actions/android-setup-environment@main with: - ruby-version: '3.4' - - name: Setup Java - uses: actions/setup-java@v4 + java_version: ${{ inputs.JAVA_VERSION }} + java_distribution: ${{ inputs.JAVA_DISTRIBUTION }} + gradle_cache_encryption_key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} + - name: Run checks + uses: futuredapp/.github/.github/actions/android-check@main with: - java-version: ${{ inputs.JAVA_VERSION }} - distribution: ${{ inputs.JAVA_DISTRIBUTION }} - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - with: - cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - - name: Run Lint Check - shell: bash - run: ./gradlew --continue ${{ inputs.LINT_GRADLE_TASKS }} - - name: Run Unit Tests - shell: bash - run: ./gradlew --continue ${{ inputs.TEST_GRADLE_TASKS }} - - name: Danger action - uses: MeilCli/danger-action@v2 - continue-on-error: true - with: - plugins_file: 'Gemfile' - danger_file: 'Dangerfile' - danger_id: 'danger-pr' - env: - # The secrets.GITHUB_TOKEN is implicitly provided by trigger workflow - DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + lint_gradle_task: ${{ inputs.LINT_GRADLE_TASKS }} + test_gradle_task: ${{ inputs.TEST_GRADLE_TASKS }} diff --git a/.github/workflows/android-generate-baseline-profiles.yml b/.github/workflows/android-cloud-generate-baseline-profiles.yml similarity index 60% rename from .github/workflows/android-generate-baseline-profiles.yml rename to .github/workflows/android-cloud-generate-baseline-profiles.yml index 1428004..90a19b4 100644 --- a/.github/workflows/android-generate-baseline-profiles.yml +++ b/.github/workflows/android-cloud-generate-baseline-profiles.yml @@ -74,40 +74,18 @@ jobs: uses: actions/checkout@v4 with: lfs: ${{ inputs.USE_GIT_LFS }} - - name: Enable KVM group perms - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - ls /dev/kvm - - name: Setup Ruby - uses: ruby/setup-ruby@v1 + - name: Set up environment + uses: futuredapp/.github/.github/actions/android-setup-environment@main with: - ruby-version: '3.0' - - name: Setup Java - uses: actions/setup-java@v4 + java_version: ${{ inputs.JAVA_VERSION }} + java_distribution: ${{ inputs.JAVA_DISTRIBUTION }} + gradle_cache_encryption_key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} + - name: Generate baseline profiles + uses: futuredapp/.github/.github/actions/android-generate-baseline-profiles@main with: - java-version: ${{ inputs.JAVA_VERSION }} - distribution: ${{ inputs.JAVA_DISTRIBUTION }} - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - with: - cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - - name: Prepare Environment - run: | - echo '${{ secrets.SECRET_PROPERTIES }}' > ${{ inputs.SECRET_PROPERTIES_FILE }} - - name: Setup GMD - run: ./gradlew ${{ inputs.TASK_NAME }} - -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true - -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" - env: - ANDROID_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} - ANDROID_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} - ANDROID_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} - - name: Create Pull Request - uses: peter-evans/create-pull-request@v7 - with: - commit-message: 'Generate baseline profiles' - branch: 'feature/generate-baseline-profiles' - title: 'Generate baseline profiles' - body: '' + generate_gradle_task: ${{ inputs.TASK_NAME }} + signing_keystore_password: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} + signing_key_alias: ${{ secrets.SIGNING_KEY_ALIAS }} + signing_key_password: ${{ secrets.SIGNING_KEY_PASSWORD }} + secret_properties_file: ${{ inputs.SECRET_PROPERTIES_FILE }} + secret_properties: ${{ secrets.SECRET_PROPERTIES }} diff --git a/.github/workflows/android-cloud-nightly-build.yml b/.github/workflows/android-cloud-nightly-build.yml new file mode 100644 index 0000000..edcb878 --- /dev/null +++ b/.github/workflows/android-cloud-nightly-build.yml @@ -0,0 +1,158 @@ +name: Android nightly build + +on: + workflow_call: + inputs: + ## Required Inputs + TEST_GRADLE_TASKS: + description: "A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest`" + required: true + type: string + PACKAGE_GRADLE_TASK: + description: "A Gradle task for packaging universal APK, eg. 'packageEnterpriseUniversalApk'" + required: true + type: string + UPLOAD_GRADLE_TASK: + description: "A Gradle task for uploading APK, for example `appDistributionUploadEnterprise`" + required: true + type: string + APP_DISTRIBUTION_GROUPS: + description: "Comma-separated list of app distribution group IDs" + required: true + type: string + + ## Optional Inputs + USE_GIT_LFS: + description: "Whether to download Git-LFS files" + type: boolean + required: false + default: false + VERSION_NAME: + description: "Version name. Example: '1.X.X-snapshot'" + required: false + type: string + BUILD_NUMBER_OFFSET: + description: "Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers." + required: false + type: number + default: 0 + KMP_FLAVOR: + description: "KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects" + required: false + type: string + default: 'test' + SECRET_PROPERTIES_FILE: + description: "A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig." + required: false + type: string + default: secrets.properties + JAVA_VERSION: + description: "Java version to use" + required: false + type: string + default: '17' + JAVA_DISTRIBUTION: + description: "Java distribution to use" + required: false + type: string + default: 'zulu' + GRADLE_OPTS: + description: "Gradle options" + required: false + type: string + default: "" + CHANGELOG_DEBUG: + description: "Enable debug mode for changelog generation. Default is false." + type: boolean + required: false + default: false + CHANGELOG_CHECKOUT_DEPTH: + description: "The depth of the git history to fetch for changelog generation. Default is 100." + type: number + required: false + default: 100 + CHANGELOG_FALLBACK_LOOKBACK: + description: "The amount of time to look back for merge commits when no previous build commit is found. Default is 24 hours." + type: string + required: false + default: "24 hours" + TIMEOUT_MINUTES: + description: "Job timeout in minutes" + required: false + type: number + default: 30 + + secrets: + APP_DISTRIBUTION_SERVICE_ACCOUNT: + required: true + description: "JSON key of service account with permissions to upload build to Firebase App Distribution" + GRADLE_CACHE_ENCRYPTION_KEY: + required: false + description: "Configuration cache encryption key" + SECRET_PROPERTIES: + required: false + description: "Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input." + +jobs: + changelog: + outputs: + skip_build: ${{ steps.detect_changes.outputs.skip_build }} + changelog: ${{ steps.detect_changes.outputs.changelog }} + cache_key: ${{ steps.detect_changes.outputs.cache_key }} + name: Detect changes and generate changelog + runs-on: ubuntu-latest + steps: + - name: Detect changes and generate changelog + id: detect_changes + uses: futuredapp/.github/.github/actions/universal-detect-changes-and-generate-changelog@main + with: + checkout_depth: ${{ inputs.CHANGELOG_CHECKOUT_DEPTH }} + debug: ${{ inputs.CHANGELOG_DEBUG }} + fallback_lookback: ${{ inputs.CHANGELOG_FALLBACK_LOOKBACK }} + build: + name: Enterprise build + runs-on: ubuntu-latest + needs: changelog + if: ${{ needs.changelog.outputs.skip_build != 'true' }} + timeout-minutes: ${{ inputs.TIMEOUT_MINUTES }} + env: + GRADLE_OPTS: ${{ inputs.GRADLE_OPTS }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: ${{ inputs.USE_GIT_LFS }} + - name: Set up environment + uses: futuredapp/.github/.github/actions/android-setup-environment@main + with: + java_version: ${{ inputs.JAVA_VERSION }} + java_distribution: ${{ inputs.JAVA_DISTRIBUTION }} + gradle_cache_encryption_key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} + - name: Build and upload to App Distribution + uses: futuredapp/.github/.github/actions/android-build-firebase@main + with: + test_gradle_task: ${{ inputs.TEST_GRADLE_TASKS }} + package_gradle_task: ${{ inputs.PACKAGE_GRADLE_TASK }} + upload_gradle_task: ${{ inputs.UPLOAD_GRADLE_TASK }} + app_distribution_groups: ${{ inputs.APP_DISTRIBUTION_GROUPS }} + app_distribution_service_account: ${{ secrets.APP_DISTRIBUTION_SERVICE_ACCOUNT }} + version_name: ${{ inputs.VERSION_NAME }} + build_number_offset: ${{ inputs.BUILD_NUMBER_OFFSET }} + release_notes: ${{ needs.changelog.outputs.changelog }} + kmp_flavor: ${{ inputs.KMP_FLAVOR }} + secret_properties_file: ${{ inputs.SECRET_PROPERTIES_FILE }} + secret_properties: ${{ secrets.SECRET_PROPERTIES }} + - name: Save latest build commit SHA to file + if: success() && needs.changelog.outputs.skip_build != 'true' + shell: bash + run: | + echo "${{ github.sha }}" > latest_builded_commit.txt + if [ "${{ inputs.CHANGELOG_DEBUG }}" == 'true' ]; then + echo "[DEBUG] Saved commit SHA ${{ github.sha }} to latest_builded_commit.txt" + fi + - name: Store latest build commit SHA in cache + if: success() && needs.changelog.outputs.skip_build != 'true' + uses: actions/cache/save@v4 + with: + path: latest_builded_commit.txt + key: ${{ needs.changelog.outputs.cache_key }} diff --git a/.github/workflows/android-cloud-release-firebaseAppDistribution.yml b/.github/workflows/android-cloud-release-firebaseAppDistribution.yml index 9f96423..9724d3e 100644 --- a/.github/workflows/android-cloud-release-firebaseAppDistribution.yml +++ b/.github/workflows/android-cloud-release-firebaseAppDistribution.yml @@ -8,18 +8,14 @@ on: description: "A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest`" required: true type: string - BUNDLE_GRADLE_TASK: - description: "A Gradle task for assembling app bundle, for example `bundleEnterprise`" + PACKAGE_GRADLE_TASK: + description: "A Gradle task for packaging universal APK, eg. 'packageEnterpriseUniversalApk'" required: true type: string UPLOAD_GRADLE_TASK: description: "A Gradle task for uploading APK, for example `appDistributionUploadEnterprise`" required: true type: string - SIGNING_KEYSTORE_PATH: - description: "Path to keystore for signing of universal APK. Example: `keystore/debug.jks' or 'androidApp/signing/debug.keystore'." - required: true - type: string APP_DISTRIBUTION_GROUPS: description: "Comma-separated list of app distribution group IDs" required: true @@ -55,11 +51,6 @@ on: required: false type: string default: secrets.properties - TIMEOUT_MINUTES: - description: "Job timeout in minutes" - required: false - type: number - default: 30 JAVA_VERSION: description: "Java version to use" required: false @@ -75,17 +66,13 @@ on: required: false type: string default: "" + TIMEOUT_MINUTES: + description: "Job timeout in minutes" + required: false + type: number + default: 30 secrets: - SIGNING_KEYSTORE_PASSWORD: - description: "Password to provided keystore" - required: true - SIGNING_KEY_ALIAS: - description: "Alias of the signing key in the provided keystore" - required: true - SIGNING_KEY_PASSWORD: - description: "Password to the key in the provided keystore" - required: true APP_DISTRIBUTION_SERVICE_ACCOUNT: required: true description: "JSON key of service account with permissions to upload build to Firebase App Distribution" @@ -99,74 +86,32 @@ on: jobs: build: name: Enterprise Build - runs-on: [ ubuntu-latest ] + runs-on: ubuntu-latest timeout-minutes: ${{ inputs.TIMEOUT_MINUTES }} env: GRADLE_OPTS: ${{ inputs.GRADLE_OPTS }} - FIREBASE_CREDENTIALS_FILE: firebase_credentials.json - BUNDLETOOL_URL: https://github.com/google/bundletool/releases/download/1.17.2/bundletool-all-1.17.2.jar - EXCLUDE_AAB_FILTER: .*intermediate - ANDROID_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} - ANDROID_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} - ANDROID_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} steps: - name: Checkout uses: actions/checkout@v4 with: lfs: ${{ inputs.USE_GIT_LFS }} - - name: Setup Java - uses: actions/setup-java@v4 + - name: Set up environment + uses: futuredapp/.github/.github/actions/android-setup-environment@main with: - java-version: ${{ inputs.JAVA_VERSION }} - distribution: ${{ inputs.JAVA_DISTRIBUTION }} - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + java_version: ${{ inputs.JAVA_VERSION }} + java_distribution: ${{ inputs.JAVA_DISTRIBUTION }} + gradle_cache_encryption_key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} + - name: Build and upload to App Distribution + uses: futuredapp/.github/.github/actions/android-build-firebase@main with: - cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - - name: Prepare Environment - run: | - { - echo "ANDROID_BUILD_NUMBER=$((GITHUB_RUN_NUMBER + ${{ inputs.BUILD_NUMBER_OFFSET}} ))"; - echo "KMP_FLAVOR=${{ inputs.KMP_FLAVOR }}" - } >> "$GITHUB_ENV" - - # Set ANDROID_VERSION_NAME only if VERSION_NAME is provided - if [[ -n "${{ inputs.VERSION_NAME }}" ]]; then - echo "ANDROID_VERSION_NAME=${{ inputs.VERSION_NAME }}" >> "$GITHUB_ENV" - fi - - echo '${{ secrets.SECRET_PROPERTIES }}' > ${{ inputs.SECRET_PROPERTIES_FILE }} - echo '${{ secrets.APP_DISTRIBUTION_SERVICE_ACCOUNT }}' > "$FIREBASE_CREDENTIALS_FILE" - - name: Run Unit tests - shell: bash - run: ./gradlew --continue ${{ inputs.TEST_GRADLE_TASKS }} - - name: Generate Artifacts (AAB and APK) - id: artifacts - shell: bash - run: | - ./gradlew ${{ inputs.BUNDLE_GRADLE_TASK }} -P buildkonfig.flavor="$KMP_FLAVOR" - BUNDLE_FILE=$(find . -name '*.aab' | grep -v '.*intermediate') - - wget -O bundletool.jar ${{ env.BUNDLETOOL_URL }} - java -jar bundletool.jar build-apks \ - --bundle "$BUNDLE_FILE" \ - --output universal.apks \ - --mode universal \ - --ks ${{ inputs.SIGNING_KEYSTORE_PATH }} \ - --ks-pass pass:${{ secrets.SIGNING_KEYSTORE_PASSWORD }} \ - --ks-key-alias ${{ secrets.SIGNING_KEY_ALIAS }} \ - --key-pass pass:${{ secrets.SIGNING_KEY_PASSWORD }} - unzip universal.apks -d universal_apk - UNIVERSAL_APK_FILE=$(find universal_apk/ -name '*.apk') - - echo "bundle_file=$BUNDLE_FILE" >> "$GITHUB_OUTPUT" - echo "universal_apk_file=$UNIVERSAL_APK_FILE" >> "$GITHUB_OUTPUT" - - name: Upload to Firebase App Distribution - shell: bash - run: | - ./gradlew ${{ inputs.UPLOAD_GRADLE_TASK }} \ - --serviceCredentialsFile="$FIREBASE_CREDENTIALS_FILE" \ - --groups="${{ inputs.APP_DISTRIBUTION_GROUPS }}" \ - --artifactType="APK" \ - --artifactPath="${{ steps.artifacts.outputs.universal_apk_file }}" \ - --releaseNotes='${{ inputs.RELEASE_NOTES }}' + test_gradle_task: ${{ inputs.TEST_GRADLE_TASKS }} + package_gradle_task: ${{ inputs.PACKAGE_GRADLE_TASK }} + upload_gradle_task: ${{ inputs.UPLOAD_GRADLE_TASK }} + app_distribution_groups: ${{ inputs.APP_DISTRIBUTION_GROUPS }} + app_distribution_service_account: ${{ secrets.APP_DISTRIBUTION_SERVICE_ACCOUNT }} + version_name: ${{ inputs.VERSION_NAME }} + build_number_offset: ${{ inputs.BUILD_NUMBER_OFFSET }} + release_notes: ${{ inputs.RELEASE_NOTES }} + kmp_flavor: ${{ inputs.KMP_FLAVOR }} + secret_properties_file: ${{ inputs.SECRET_PROPERTIES_FILE }} + secret_properties: ${{ secrets.SECRET_PROPERTIES }} diff --git a/.github/workflows/android-cloud-release-googlePlay.yml b/.github/workflows/android-cloud-release-googlePlay.yml index eb30bf4..25c1325 100644 --- a/.github/workflows/android-cloud-release-googlePlay.yml +++ b/.github/workflows/android-cloud-release-googlePlay.yml @@ -12,10 +12,6 @@ on: description: "A Gradle task for assembling app bundle, for example `bundleRelease`" required: true type: string - SIGNING_KEYSTORE_PATH: - description: "Path to keystore for signing of universal APK. Example: `keystore/debug.jks' or 'androidApp/signing/debug.keystore'" - required: true - type: string GOOGLE_PLAY_APPLICATION_ID: description: "Google Play applicationId" required: true @@ -96,52 +92,29 @@ jobs: timeout-minutes: ${{ inputs.TIMEOUT_MINUTES }} env: GRADLE_OPTS: ${{ inputs.GRADLE_OPTS }} - EXCLUDE_AAB_FILTER: .*intermediate steps: - name: Checkout uses: actions/checkout@v4 with: lfs: ${{ inputs.USE_GIT_LFS }} - - name: Setup Java - uses: actions/setup-java@v4 - with: - java-version: ${{ inputs.JAVA_VERSION }} - distribution: ${{ inputs.JAVA_DISTRIBUTION }} - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + - name: Set up environment + uses: futuredapp/.github/.github/actions/android-setup-environment@main with: - cache-disabled: true - - name: Prepare Environment - run: | - { - echo "ANDROID_BUILD_NUMBER=$((GITHUB_RUN_NUMBER + ${{ inputs.BUILD_NUMBER_OFFSET}} ))"; - echo "ANDROID_VERSION_NAME=${{ inputs.VERSION_NAME }}"; - echo "KMP_FLAVOR=${{ inputs.KMP_FLAVOR }}" - } >> "$GITHUB_ENV" - - echo '${{ secrets.SECRET_PROPERTIES }}' > ${{ inputs.SECRET_PROPERTIES_FILE }} - - name: Generate Artifacts (AAB) - id: artifacts - env: - ANDROID_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} - ANDROID_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} - ANDROID_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} - shell: bash - run: | - ./gradlew ${{ inputs.BUNDLE_GRADLE_TASK }} -P buildkonfig.flavor="$KMP_FLAVOR" - BUNDLE_FILE=$(find . -name '*.aab' | grep -v '.*intermediate') - MAPPING_FILE=$(find . -name mapping.txt) - - echo "bundle_file=$BUNDLE_FILE" >> "$GITHUB_OUTPUT" - echo "mapping_file=$MAPPING_FILE" >> "$GITHUB_OUTPUT" - - name: Upload to Google Play - uses: r0adkll/upload-google-play@v1.1.1 + java_version: ${{ inputs.JAVA_VERSION }} + java_distribution: ${{ inputs.JAVA_DISTRIBUTION }} + - name: Build and release + uses: futuredapp/.github/.github/actions/android-build-googlePlay@main with: - serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_PUBLISH_SERVICE_ACCOUNT }} - packageName: ${{ inputs.GOOGLE_PLAY_APPLICATION_ID }} - releaseFiles: ${{ steps.artifacts.outputs.bundle_file }} - track: internal - status: draft - whatsNewDirectory: ${{ inputs.GOOGLE_PLAY_WHATSNEW_DIRECTORY }} - mappingFile: ${{ steps.artifacts.outputs.mapping_file }} - changesNotSentForReview: ${{ toJSON(inputs.CHANGES_NOT_SENT_FOR_REVIEW) }} + bundle_gradle_task: ${{ inputs.BUNDLE_GRADLE_TASK }} + version_name: ${{ inputs.VERSION_NAME }} + signing_keystore_password: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} + signing_key_alias: ${{ secrets.SIGNING_KEY_ALIAS }} + signing_key_password: ${{ secrets.SIGNING_KEY_PASSWORD }} + google_play_application_id: ${{ inputs.GOOGLE_PLAY_APPLICATION_ID }} + google_play_whatsnew_dir: ${{ inputs.GOOGLE_PLAY_WHATSNEW_DIRECTORY }} + google_play_publish_service_account: ${{ secrets.GOOGLE_PLAY_PUBLISH_SERVICE_ACCOUNT }} + changes_not_sent_for_review: ${{ inputs.CHANGES_NOT_SENT_FOR_REVIEW }} + build_number_offset: ${{ inputs.BUILD_NUMBER_OFFSET }} + kmp_flavor: ${{ inputs.KMP_FLAVOR }} + secret_properties_file: ${{ inputs.SECRET_PROPERTIES_FILE }} + secret_properties: ${{ secrets.SECRET_PROPERTIES }} diff --git a/.github/workflows/ios-kmp-selfhosted-build.yml b/.github/workflows/ios-kmp-selfhosted-build.yml index d501e28..4c8caf8 100644 --- a/.github/workflows/ios-kmp-selfhosted-build.yml +++ b/.github/workflows/ios-kmp-selfhosted-build.yml @@ -52,6 +52,11 @@ on: type: string required: false default: "" + changelog: + description: "Will be used as TestFlight changelog" + type: string + required: false + default: ${{ github.event.pull_request.title }} secrets: MATCH_PASSWORD: @@ -87,50 +92,25 @@ jobs: uses: actions/checkout@v4 with: lfs: ${{ inputs.use_git_lfs }} - - name: Export secrets to .xcconfig file - if: ${{ inputs.xcconfig_path != '' }} - uses: futuredapp/.github/.github/actions/ios-export-secrets@main - with: - XCCONFIG_PATH: ${{ inputs.xcconfig_path }} - SECRET_PROPERTIES: ${{ secrets.SECRET_PROPERTIES }} - REQUIRED_KEYS: ${{ inputs.required_keys }} - - name: Setup Java - uses: actions/setup-java@v4 - with: - java-version: ${{ inputs.java_version }} - distribution: ${{ inputs.java_distribution }} - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - with: - cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - - name: Build KMP Package - if: ${{ inputs.kmp_swift_package_integration }} - env: - KMP_BUILD_FLAVOR: ${{ inputs.kmp_swift_package_flavor }} - KMP_FRAMEWORK_BUILD_TYPE: release - run: | - cd ${{ inputs.kmp_swift_package_path }} - make build - - name: Fastlane Beta - working-directory: iosApp - run: | - gem install bundler - bundle install --jobs 4 --retry 3 - bundle exec fastlane beta - env: - MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} - PR_TITLE: ${{ github.event.pull_request.title }} - APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} - APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} - APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} - CUSTOM_VALUES: ${{ inputs.custom_values }} - - name: Upload IPA - uses: actions/upload-artifact@v4 + - name: Set up environment + uses: futuredapp/.github/.github/actions/android-setup-environment@main with: - name: Build.ipa - path: build_output/*.ipa - - name: Upload dSYM - uses: actions/upload-artifact@v4 + java_version: ${{ inputs.java_version }} + java_distribution: ${{ inputs.java_distribution }} + gradle_cache_encryption_key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} + ruby: 'false' + - name: Build and upload to TestFlight + uses: futuredapp/.github/.github/actions/ios-kmp-build@main with: - name: Build.app.dSYM.zip - path: build_output/*.app.dSYM.zip + match_password: ${{ secrets.MATCH_PASSWORD }} + app_store_connect_api_key_key: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} + app_store_connect_api_key_key_id: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} + app_store_connect_api_key_issuer_id: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} + testflight_changelog: ${{ inputs.changelog }} + secret_xcconfig_path: ${{ inputs.xcconfig_path }} + secret_properties: ${{ secrets.SECRET_PROPERTIES }} + secret_required_keys: ${{ inputs.required_keys }} + kmp_swift_package_integration: ${{ inputs.kmp_swift_package_integration }} + kmp_swift_package_path: ${{ inputs.kmp_swift_package_path }} + kmp_swift_package_flavor: ${{ inputs.kmp_swift_package_flavor }} + custom_values: ${{ inputs.custom_values }} diff --git a/.github/workflows/ios-selfhosted-build.yml b/.github/workflows/ios-selfhosted-build.yml index 796d0cc..2bdf7fa 100644 --- a/.github/workflows/ios-selfhosted-build.yml +++ b/.github/workflows/ios-selfhosted-build.yml @@ -155,4 +155,4 @@ jobs: uses: actions/cache/save@v4 with: path: latest_builded_commit.txt - key: latest_builded_commit-${{ github.run_number }} + key: ${{ steps.detect_changes.outputs.cache_key }} diff --git a/.github/workflows/kmp-cloud-detect-changes.yml b/.github/workflows/kmp-cloud-detect-changes.yml index 152afce..50caa63 100644 --- a/.github/workflows/kmp-cloud-detect-changes.yml +++ b/.github/workflows/kmp-cloud-detect-changes.yml @@ -3,6 +3,9 @@ name: Detect Changes # This workflow is a central decision point in the KMP project CI/CD pipeline that determines # which platform-specific workflows should run based on changed files. # +# This workflow serves as a wrapper around the reusable kmp-detect-changes action +# (.github/actions/kmp-detect-changes) to provide a callable workflow interface. +# # The following project structure is assumed: Android app in `androidApp/` path and iOS app in `iosApp/` path # relative to the repository root. # @@ -17,11 +20,15 @@ name: Detect Changes # Example: # jobs: # changes: -# uses: ./.github/workflows/util_detect_changes.yml +# uses: ./.github/workflows/kmp-cloud-detect-changes.yml # ios-build: # needs: changes # if: ${{ needs.changes.outputs.iosFiles == 'true' }} # uses: ./.github/workflows/build_ios.yml +# +# Note: For direct action usage in other workflows, you can also use: +# - name: Detect Changes +# uses: futuredapp/.github/.github/actions/kmp-detect-changes@main on: workflow_call: @@ -45,20 +52,12 @@ jobs: runs-on: ubuntu-latest outputs: # Matches all files which can affect iOS build if changed - iosFiles: ${{ steps.file-changes.outputs.iosFiles }} + iosFiles: ${{ steps.detect.outputs.iosFiles }} # Matches all files which can affect Android build if changed - androidFiles: ${{ steps.file-changes.outputs.androidFiles }} + androidFiles: ${{ steps.detect.outputs.androidFiles }} steps: - - name: Checkout - uses: actions/checkout@v4 - with: - lfs: ${{ inputs.USE_GIT_LFS }} - name: Detect Changes - uses: dorny/paths-filter@v3 - id: file-changes + id: detect + uses: futuredapp/.github/.github/actions/kmp-detect-changes@main with: - filters: | - iosFiles: - - "!(androidApp/**)" - androidFiles: - - "!(iosApp/**)" + USE_GIT_LFS: ${{ inputs.USE_GIT_LFS }} diff --git a/.github/workflows/kmp-combined-nightly-build.yml b/.github/workflows/kmp-combined-nightly-build.yml new file mode 100644 index 0000000..c28716a --- /dev/null +++ b/.github/workflows/kmp-combined-nightly-build.yml @@ -0,0 +1,237 @@ +name: KMP nightly build + +on: + workflow_call: + inputs: + ## Required Inputs + ANDROID_TEST_GRADLE_TASK: + description: "A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest`" + required: true + type: string + ANDROID_PACKAGE_GRADLE_TASK: + description: "A Gradle task for packaging universal APK, eg. 'packageEnterpriseUniversalApk'" + required: true + type: string + ANDROID_UPLOAD_GRADLE_TASK: + description: "A Gradle task for uploading APK, for example `appDistributionUploadEnterprise`" + required: true + type: string + KMP_FLAVOR: + description: "KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects" + required: true + type: string + FIREBASE_APP_DISTRIBUTION_GROUPS: + description: "Comma-separated list of app distribution group IDs" + required: true + type: string + + ## Optional Inputs + TIMEOUT_MINUTES: + description: "Job timeout in minutes" + required: false + type: number + default: 30 + USE_GIT_LFS: + description: "Whether to download Git-LFS files" + type: boolean + required: false + default: false + GRADLE_OPTS: + description: "Gradle options" + required: false + type: string + default: "" + JAVA_VERSION: + description: "Java version to use" + required: false + type: string + default: '17' + JAVA_DISTRIBUTION: + description: "Java distribution to use" + required: false + type: string + default: 'zulu' + ANDROID_VERSION_NAME: + description: "Version name. Example: '1.X.X-snapshot'" + required: false + type: string + ANDROID_BUILD_NUMBER_OFFSET: + description: "Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers." + required: false + type: number + default: 0 + KMP_SWIFT_PACKAGE_INTEGRATION: + description: "Whether KMP is integrated in Xcode project as a Swift Package" + required: false + type: boolean + default: false + KMP_SWIFT_PACKAGE_PATH: + description: "If `KMP_SWIFT_PACKAGE_INTEGRATION` is 'true', then specifies a location of local Swift Package with Makefile. Example: 'iosApp/shared/KMP`" + required: false + type: string + ANDROID_SECRET_PROPERTIES_FILE: + description: "A path to file that will be populated with contents of 'android_secret_properties' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig." + required: false + type: string + default: secrets.properties + IOS_SECRET_XCCONFIG_PATH: + description: "Path to the .xcconfig file. Selected secret properties will be appended to the end of this file." + type: string + required: false + IOS_SECRET_REQUIRED_KEYS: + description: "Comma-separated list of required secret keys." + type: string + required: false + IOS_CUSTOM_VALUES: + description: "Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: \"CUSTOM-1: 1; CUSTOM-2: 2\"" + required: false + type: string + CHANGELOG_DEBUG: + description: "Enable debug mode for changelog generation. Default is false." + type: boolean + required: false + default: false + CHANGELOG_CHECKOUT_DEPTH: + description: "The depth of the git history to fetch for changelog generation. Default is 100." + type: number + required: false + default: 100 + CHANGELOG_FALLBACK_LOOKBACK: + description: "The amount of time to look back for merge commits when no previous build commit is found. Default is 24 hours." + type: string + required: false + default: "24 hours" + + secrets: + FIREBASE_APP_DISTRIBUTION_SERVICE_ACCOUNT: + required: true + description: "JSON key of service account with permissions to upload build to Firebase App Distribution" + GRADLE_CACHE_ENCRYPTION_KEY: + required: false + description: "Configuration cache encryption key" + ANDROID_SECRET_PROPERTIES: + required: false + description: "Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'ANDROID_SECRET_PROPERTIES_FILE' input." + IOS_SECRET_PROPERTIES: + required: false + description: "Secrets in the format KEY = VALUE (one per line)." + IOS_MATCH_PASSWORD: + description: "Password for decrypting of certificates and provisioning profiles." + required: true + IOS_APP_STORE_CONNECT_API_KEY_KEY: + description: "Private App Store Connect API key for submitting build to App Store." + required: true + IOS_APP_STORE_CONNECT_API_KEY_KEY_ID: + required: true + description: "Private App Store Connect API key for submitting build to App Store." + IOS_APP_STORE_CONNECT_API_KEY_ISSUER_ID: + required: true + description: "Private App Store Connect API issuer key for submitting build to App Store." + +jobs: + changelog: + outputs: + skip_build: ${{ steps.detect_changes.outputs.skip_build }} + changelog: ${{ steps.detect_changes.outputs.changelog }} + cache_key: ${{ steps.detect_changes.outputs.cache_key }} + name: Detect changes and generate changelog + runs-on: ubuntu-latest + steps: + - name: Detect changes and generate changelog + id: detect_changes + uses: futuredapp/.github/.github/actions/universal-detect-changes-and-generate-changelog@main + with: + checkout_depth: ${{ inputs.CHANGELOG_CHECKOUT_DEPTH }} + debug: ${{ inputs.CHANGELOG_DEBUG }} + fallback_lookback: ${{ inputs.CHANGELOG_FALLBACK_LOOKBACK }} + ios_build: + name: iOS Build + runs-on: self-hosted + needs: changelog + if: ${{ needs.changelog.outputs.skip_build != 'true' }} + timeout-minutes: ${{ inputs.TIMEOUT_MINUTES }} + env: + GRADLE_OPTS: ${{ inputs.GRADLE_OPTS }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: ${{ inputs.USE_GIT_LFS }} + - name: Set up environment + uses: futuredapp/.github/.github/actions/android-setup-environment@main + with: + java_version: ${{ inputs.JAVA_VERSION }} + java_distribution: ${{ inputs.JAVA_DISTRIBUTION }} + gradle_cache_encryption_key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} + ruby: 'false' + - name: Build and upload to TestFlight + uses: futuredapp/.github/.github/actions/ios-kmp-build@main + with: + match_password: ${{ secrets.IOS_MATCH_PASSWORD }} + app_store_connect_api_key_key: ${{ secrets.IOS_APP_STORE_CONNECT_API_KEY_KEY }} + app_store_connect_api_key_key_id: ${{ secrets.IOS_APP_STORE_CONNECT_API_KEY_KEY_ID }} + app_store_connect_api_key_issuer_id: ${{ secrets.IOS_APP_STORE_CONNECT_API_KEY_ISSUER_ID }} + secret_xcconfig_path: ${{ inputs.IOS_SECRET_XCCONFIG_PATH }} + secret_properties: ${{ secrets.IOS_SECRET_PROPERTIES }} + secret_required_keys: ${{ inputs.IOS_SECRET_REQUIRED_KEYS }} + kmp_swift_package_integration: ${{ inputs.KMP_SWIFT_PACKAGE_INTEGRATION }} + kmp_swift_package_path: ${{ inputs.KMP_SWIFT_PACKAGE_PATH }} + kmp_swift_package_flavor: ${{ inputs.KMP_FLAVOR }} + custom_values: ${{ inputs.IOS_CUSTOM_VALUES }} + testflight_changelog: ${{ needs.changelog.outputs.changelog }} + android_build: + name: Android Build + runs-on: ubuntu-latest + needs: changelog + if: ${{ needs.changelog.outputs.skip_build != 'true' }} + timeout-minutes: ${{ inputs.TIMEOUT_MINUTES }} + env: + GRADLE_OPTS: ${{ inputs.GRADLE_OPTS }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: ${{ inputs.USE_GIT_LFS }} + - name: Set up environment + uses: futuredapp/.github/.github/actions/android-setup-environment@main + with: + java_version: ${{ inputs.JAVA_VERSION }} + java_distribution: ${{ inputs.JAVA_DISTRIBUTION }} + gradle_cache_encryption_key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} + - name: Build and upload to App Distribution + uses: futuredapp/.github/.github/actions/android-build-firebase@main + with: + test_gradle_task: ${{ inputs.ANDROID_TEST_GRADLE_TASK }} + package_gradle_task: ${{ inputs.ANDROID_PACKAGE_GRADLE_TASK }} + upload_gradle_task: ${{ inputs.ANDROID_UPLOAD_GRADLE_TASK }} + app_distribution_groups: ${{ inputs.FIREBASE_APP_DISTRIBUTION_GROUPS }} + app_distribution_service_account: ${{ secrets.FIREBASE_APP_DISTRIBUTION_SERVICE_ACCOUNT }} + version_name: ${{ inputs.ANDROID_VERSION_NAME }} + build_number_offset: ${{ inputs.ANDROID_BUILD_NUMBER_OFFSET }} + release_notes: ${{ needs.changelog.outputs.changelog }} + kmp_flavor: ${{ inputs.KMP_FLAVOR }} + secret_properties_file: ${{ inputs.ANDROID_SECRET_PROPERTIES_FILE }} + secret_properties: ${{ secrets.ANDROID_SECRET_PROPERTIES }} + save_cache: + name: Cache last build commit SHA + runs-on: ubuntu-latest + needs: + - ios_build + - android_build + - changelog + steps: + - name: Save latest build commit SHA to file + if: success() && needs.changelog.outputs.skip_build != 'true' + shell: bash + run: | + echo "${{ github.sha }}" > latest_builded_commit.txt + if [ "${{ inputs.CHANGELOG_DEBUG }}" == 'true' ]; then + echo "[DEBUG] Saved commit SHA ${{ github.sha }} to latest_builded_commit.txt" + fi + - name: Store latest build commit SHA in cache + if: success() && needs.changelog.outputs.skip_build != 'true' + uses: actions/cache/save@v4 + with: + path: latest_builded_commit.txt + key: ${{ needs.changelog.outputs.cache_key }} + diff --git a/README.md b/README.md index 3d96d06..0b03544 100644 --- a/README.md +++ b/README.md @@ -23,20 +23,24 @@ All the available reusable workflows are listed in the following table. ### Available workflows -| Platform | Runner | Action | File | Description | -|:---------------|:------------|:--------------------------|:-----------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------| -| Universal | Cloud | Backup | [`universal-cloud-backup`](.github/workflows/universal-cloud-backup.yml) | Backups currently checked out ref to a remote repository. | -| Universal | Self-hosted | Backup | [`universal-selfhosted-backup`](.github/workflows/universal-selfhosted-backup.yml) | Backups currently checked out ref to a remote repository. | -| iOS | Self-hosted | Test | [`ios-selfhosted-test`](.github/workflows/ios-selfhosted-test.yml) | Lints and tests the PR. | -| iOS | Self-hosted | Build | [`ios-selfhosted-build`](.github/workflows/ios-selfhosted-build.yml) | Creates enterprise release build and submits the build to Futured App Store Connect. | -| iOS | Self-hosted | Release | [`ios-selfhosted-release`](.github/workflows/ios-selfhosted-release.yml) | Creates release build and submits it to App Store Connect. | -| iOS (KMP) | Self-hosted | Test | [`ios-kmp-selfhosted-test`](.github/workflows/ios-kmp-selfhosted-test.yml) | Lints and tests the PR. | -| iOS (KMP) | Self-hosted | Build | [`ios-kmp-selfhosted-build`](.github/workflows/ios-kmp-selfhosted-build.yml) | Creates enterprise release build and submits the build to Futured App Store Connect. | -| iOS (KMP) | Self-hosted | Release | [`ios-kmp-selfhosted-release`](.github/workflows/ios-kmp-selfhosted-release.yml) | Creates release build and submits it to App Store Connect. | -| Android (+KMP) | Cloud | Tests & Lint checks | [`android-cloud-check`](.github/workflows/android-cloud-check.yml) | Runs unit tests and lint checks on pull request. | -| Android (+KMP) | Cloud | Firebase Snapshot Release | [`android-cloud-release-firebaseAppDistribution`](.github/workflows/android-cloud-release-firebaseAppDistribution.yml) | Publishes QA Snapshot build to Firebase App Distribution. | -| Android (+KMP) | Cloud | Google Play Release | [`android-cloud-release-googlePlay`](.github/workflows/android-cloud-release-googlePlay.yml) | Publishes release build to Google Play. | -| KMP | Cloud | Detect Changes | [`kmp-cloud-detect-changes`](.github/workflows/kmp-cloud-detect-changes.yml) | Detects changed sources in KMP projects for conditional job execution. | +| Platform | Runner | Action | File | Description | +|:---------------|:------------|:----------------------------|:-----------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------| +| Universal | Any | Workflows Lint | [`workflows-lint`](.github/workflows/workflows-lint.yml) | Lints GitHub workflow files for syntax and best practices. | +| Universal | Cloud | Backup | [`universal-cloud-backup`](.github/workflows/universal-cloud-backup.yml) | Backups currently checked out ref to a remote repository. | +| Universal | Self-hosted | Backup | [`universal-selfhosted-backup`](.github/workflows/universal-selfhosted-backup.yml) | Backups currently checked out ref to a remote repository. | +| iOS | Self-hosted | Test | [`ios-selfhosted-test`](.github/workflows/ios-selfhosted-test.yml) | Lints and tests the PR. | +| iOS | Self-hosted | Build | [`ios-selfhosted-build`](.github/workflows/ios-selfhosted-build.yml) | Creates enterprise release build and submits the build to Futured App Store Connect. | +| iOS | Self-hosted | Release | [`ios-selfhosted-release`](.github/workflows/ios-selfhosted-release.yml) | Creates release build and submits it to App Store Connect. | +| iOS (KMP) | Self-hosted | Test | [`ios-kmp-selfhosted-test`](.github/workflows/ios-kmp-selfhosted-test.yml) | Lints and tests the PR. | +| iOS (KMP) | Self-hosted | Build | [`ios-kmp-selfhosted-build`](.github/workflows/ios-kmp-selfhosted-build.yml) | Creates enterprise release build and submits the build to Futured App Store Connect. | +| iOS (KMP) | Self-hosted | Release | [`ios-kmp-selfhosted-release`](.github/workflows/ios-kmp-selfhosted-release.yml) | Creates release build and submits it to App Store Connect. | +| Android (+KMP) | Cloud | Tests & Lint checks | [`android-cloud-check`](.github/workflows/android-cloud-check.yml) | Runs unit tests and lint checks on pull request. | +| Android (+KMP) | Cloud | Firebase Snapshot Release | [`android-cloud-release-firebaseAppDistribution`](.github/workflows/android-cloud-release-firebaseAppDistribution.yml) | Publishes QA Snapshot build to Firebase App Distribution. | +| Android (+KMP) | Cloud | Google Play Release | [`android-cloud-release-googlePlay`](.github/workflows/android-cloud-release-googlePlay.yml) | Publishes release build to Google Play. | +| Android | Cloud | Nightly Build | [`android-cloud-nightly-build`](.github/workflows/android-cloud-nightly-build.yml) | Automated nightly builds with Firebase App Distribution deployment. | +| Android (+KMP) | Cloud | Generate Baseline Profiles | [`android-cloud-generate-baseline-profiles`](.github/workflows/android-cloud-generate-baseline-profiles.yml) | Generates baseline profiles and creates PR with changes. | +| KMP | Cloud | Detect Changes | [`kmp-cloud-detect-changes`](.github/workflows/kmp-cloud-detect-changes.yml) | Detects changed sources in KMP projects for conditional job execution. | +| KMP | Cloud | Combined Nightly Build | [`kmp-combined-nightly-build`](.github/workflows/kmp-combined-nightly-build.yml) | Automated nightly builds for both iOS and Android platforms in KMP projects. | ## Contributors