From 3c575a497cee79ab22ca3cd45290ae8334a1e8f0 Mon Sep 17 00:00:00 2001 From: Carter Jernigan Date: Tue, 11 Jan 2022 08:11:15 -0500 Subject: [PATCH] Configure build with GitHub Actions Also configure testing with Firebase Test Lab --- .github/actions/setup/action.yml | 87 +++++ .github/workflows/deploy-release.yml | 85 +++++ .github/workflows/deploy-snapshot.yml | 82 +++++ .github/workflows/pull-request.yml | 300 ++++++++++++++++++ .../zcash.dependency-conventions.gradle.kts | 15 + build.gradle.kts | 34 +- PUBLISHING.md => docs/PUBLISHING.md | 20 +- docs/ci.md | 67 ++-- gradle.properties | 10 +- sdk-lib/build.gradle.kts | 13 +- settings.gradle.kts | 2 + 11 files changed, 666 insertions(+), 49 deletions(-) create mode 100644 .github/actions/setup/action.yml create mode 100644 .github/workflows/deploy-release.yml create mode 100644 .github/workflows/deploy-snapshot.yml create mode 100644 .github/workflows/pull-request.yml create mode 100644 build-conventions/src/main/kotlin/zcash.dependency-conventions.gradle.kts rename PUBLISHING.md => docs/PUBLISHING.md (64%) diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 00000000..c1c7eb9b --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,87 @@ +name: 'Setup Java, Rust, and Dependency Cache' +description: "Configures the build environment and caches Gradle, dependencies, and build outputs." +runs: + using: "composite" + steps: + - name: Set Env + shell: bash + run: | + echo "home=${HOME}" >> "$GITHUB_ENV" + - name: Set up Java + uses: actions/setup-java@v2 + with: + distribution: 'zulu' + java-version: 17 + - name: Setup Rust + shell: bash + run: | + rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android + - name: Disable Gradle Daemon + shell: bash + run: | + mkdir ~/.gradle + + echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties + - name: Gradle Wrapper Cache + id: gradle-wrapper-cache + uses: actions/cache@v2.1.7 + with: + path: ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles(format('{0}{1}', github.workspace, '/gradle/wrapper/gradle-wrapper.properties')) }} + - name: Gradle Dependency Cache + id: gradle-dependency-cache + uses: actions/cache@v2.1.7 + with: + path: ~/.gradle/caches/modules-2 + key: ${{ runner.os }}-gradle-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/gradle.properties')) }} + restore-keys: | + ${{ runner.os }}-gradle-deps + # This tries to fall back to the build cache from the main branch, while ensuring that + # main branch builds repopulate the cache each time. + - name: Gradle Build Cache Main + id: gradle-build-cache-main + if: github.event.pull_request.head.sha == '' + uses: actions/cache@v2.1.7 + with: + path: | + ~/.gradle/caches/build-cache-1 + ~/.gradle/caches/transforms-3 + key: ${{ runner.os }}-gradle-build-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-gradle-build + - name: Gradle Build Cache Pull Request + id: gradle-build-cache-pr + if: github.event.pull_request.head.sha != '' + uses: actions/cache@v2.1.7 + with: + path: | + ~/.gradle/caches/build-cache-1 + ~/.gradle/caches/transforms-3 + key: ${{ runner.os }}-gradle-build-${{ github.event.pull_request.base.sha }} + restore-keys: | + ${{ runner.os }}-gradle-build + - name: Rust Cache + id: rust-cache + uses: actions/cache@v2.1.7 + with: + path: | + sdk-lib/target + ~/.cargo + key: ${{ runner.os }}-rust-${{ hashFiles(format('{0}{1}', github.workspace, '/sdk-lib/Cargo.lock'), format('{0}{1}', github.workspace, '/sdk-lib/Cargo.toml'), format('{0}{1}', github.workspace, '/sdk-lib/build.gradle.kts'), format('{0}{1}', github.workspace, '/gradle.properties')) }} + - name: Download Gradle + if: steps.gradle-wrapper-cache.outputs.cache-hit != 'true' + shell: bash + run: | + ./gradlew --version + - name: Download Gradle Dependencies + if: steps.gradle-dependency-cache.outputs.cache-hit != 'true' + shell: bash + run: | + ./gradlew dependencies :sdk-lib:dependencies :demo-app:dependencies + - name: Compile + if: steps.gradle-build-cache-main.outputs.cache-hit != 'true' && steps.gradle-build-cache-pr.outputs.cache-hit != 'true' + shell: bash + env: + ORG_GRADLE_PROJECT_IS_MINIFY_APP_ENABLED: "false" + run: | + ./gradlew assemble assembleAndroidTest diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml new file mode 100644 index 00000000..dc21d3ee --- /dev/null +++ b/.github/workflows/deploy-release.yml @@ -0,0 +1,85 @@ +# Expected secrets +# MAVEN_CENTRAL_USERNAME - Username for Maven Central. +# MAVEN_CENTRAL_PASSWORD - Password for Maven Central. +# MAVEN_SIGNING_KEYRING_FILE_BASE64 - Base64 encoded GPG keyring file. +# MAVEN_SIGNING_KEY_ID - ID for the key in the GPG keyring file. +# MAVEN_SIGNING_PASSWORD - Password for the key in the GPG keyring file. + +name: Deploy Release + +on: + workflow_dispatch: + +concurrency: deploy_release + +jobs: + validate_gradle_wrapper: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + timeout-minutes: 1 + uses: actions/checkout@v2.4.0 + # Gradle Wrapper validation can be flaky + # https://github.com/gradle/wrapper-validation-action/issues/40 + - name: Gradle Wrapper Validation + timeout-minutes: 1 + uses: gradle/wrapper-validation-action@v1.0.4 + + deploy_release: + needs: validate_gradle_wrapper + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + timeout-minutes: 1 + uses: actions/checkout@v2.4.0 + - name: Setup + id: setup + timeout-minutes: 25 + uses: ./.github/actions/setup + - name: Export Maven Signing Key + env: + MAVEN_SIGNING_KEYRING_FILE_BASE64: ${{ secrets.MAVEN_SIGNING_KEYRING_FILE_BASE64 }} + GPG_KEY_PATH: ${{ format('{0}/keyring.gpg', env.home) }} + shell: bash + run: | + echo ${MAVEN_SIGNING_KEYRING_FILE_BASE64} | base64 --decode > ${GPG_KEY_PATH} + # While not strictly necessary, this sanity checks the build before attempting to upload. + # This adds minimal additional build time, since most of the work is cached and re-used + # in the next step. + - name: Deploy to Maven Local + timeout-minutes: 25 + run: | + ./gradlew publishToMavenLocal + - name: Deploy to Maven Central + timeout-minutes: 5 + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + ORG_GRADLE_PROJECT_signing.secretKeyRingFile: ${{ format('{0}/keyring.gpg', env.home) }} + ORG_GRADLE_PROJECT_signing.keyId: ${{ secrets.MAVEN_SIGNING_KEY_ID }} + ORG_GRADLE_PROJECT_signing.password: ${{ secrets.MAVEN_SIGNING_PASSWORD }} + ORG_GRADLE_PROJECT_IS_SNAPSHOT: false + run: | + ./gradlew publish --no-parallel + ./gradlew closeAndReleaseRepository + - name: Collect Artifacts + timeout-minutes: 1 + if: ${{ always() }} + env: + ARTIFACTS_DIR_PATH: ${{ format('{0}/artifacts', env.home) }} + BINARIES_ZIP_PATH: ${{ format('{0}/artifacts/release_binaries.zip', env.home) }} + run: | + mkdir ${ARTIFACTS_DIR_PATH} + + zip -r ${BINARIES_ZIP_PATH} . -i build/outputs/* + - name: Upload Artifacts + if: ${{ always() }} + uses: actions/upload-artifact@v2 + timeout-minutes: 1 + with: + name: Release binaries + path: ~/artifacts diff --git a/.github/workflows/deploy-snapshot.yml b/.github/workflows/deploy-snapshot.yml new file mode 100644 index 00000000..be8ea61d --- /dev/null +++ b/.github/workflows/deploy-snapshot.yml @@ -0,0 +1,82 @@ +# Expected secrets +# MAVEN_CENTRAL_USERNAME - Username for Maven Central +# MAVEN_CENTRAL_PASSWORD - Password for Maven Central + +# Note that snapshot releases do not require GPG signing + +name: Deploy Snapshot + +on: + workflow_dispatch: + push: + branches: + - master + paths-ignore: + - '.github/ISSUE_TEMPLATE/*' + - '.github/PULL_REQUEST_TEMPLATE.md' + - 'LICENSE' + - 'README.md' + - 'docs/**' + +concurrency: deploy_snapshot + +jobs: + validate_gradle_wrapper: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + timeout-minutes: 1 + uses: actions/checkout@v2.4.0 + # Gradle Wrapper validation can be flaky + # https://github.com/gradle/wrapper-validation-action/issues/40 + - name: Gradle Wrapper Validation + timeout-minutes: 1 + uses: gradle/wrapper-validation-action@v1.0.4 + + deploy_snapshot: + needs: validate_gradle_wrapper + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + timeout-minutes: 1 + uses: actions/checkout@v2.4.0 + - name: Setup + id: setup + timeout-minutes: 25 + uses: ./.github/actions/setup + # While not strictly necessary, this sanity checks the build before attempting to upload. + # This adds minimal additional build time, since most of the work is cached and re-used + # in the next step. + - name: Deploy to Maven Local + timeout-minutes: 25 + run: | + ./gradlew publishToMavenLocal + - name: Deploy to Maven Central + timeout-minutes: 5 + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + ORG_GRADLE_PROJECT_IS_SNAPSHOT: true + run: | + ./gradlew publish --no-parallel + - name: Collect Artifacts + timeout-minutes: 1 + if: ${{ always() }} + env: + ARTIFACTS_DIR_PATH: ${{ format('{0}/artifacts', env.home) }} + BINARIES_ZIP_PATH: ${{ format('{0}/artifacts/snapshot_binaries.zip', env.home) }} + run: | + mkdir ${ARTIFACTS_DIR_PATH} + + zip -r ${BINARIES_ZIP_PATH} . -i build/outputs/* + - name: Upload Artifacts + if: ${{ always() }} + uses: actions/upload-artifact@v2 + timeout-minutes: 1 + with: + name: Snapshot binaries + path: ~/artifacts diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 00000000..e70c6766 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,300 @@ +# Expected secrets +# FIREBASE_TEST_LAB_PROJECT - Firebase Test Lab project name +# FIREBASE_TEST_LAB_SERVICE_ACCOUNT - Email address of Firebase Test Lab service account +# FIREBASE_TEST_LAB_WORKLOAD_IDENTITY_PROVIDER - Workload identity provider to generate temporary service account key + +name: Pull Request + +on: + pull_request: + paths-ignore: + - '.github/ISSUE_TEMPLATE/*' + - '.github/PULL_REQUEST_TEMPLATE.md' + - 'LICENSE' + - 'README.md' + - 'docs/**' + +jobs: + validate_gradle_wrapper: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + timeout-minutes: 1 + uses: actions/checkout@v2.4.0 + # Gradle Wrapper validation can be flaky + # https://github.com/gradle/wrapper-validation-action/issues/40 + - name: Gradle Wrapper Validation + timeout-minutes: 1 + uses: gradle/wrapper-validation-action@v1.0.4 + + prime_cache: + runs-on: ubuntu-latest + needs: validate_gradle_wrapper + permissions: + contents: read + steps: + - name: Checkout + timeout-minutes: 1 + uses: actions/checkout@v2.4.0 + - name: Setup + id: setup + timeout-minutes: 25 + uses: ./.github/actions/setup + + check_secrets: + runs-on: ubuntu-latest + outputs: + has-secrets: ${{ steps.check_secrets.outputs.defined }} + steps: + - id: check_secrets + env: + FIREBASE_TEST_LAB_PROJECT: ${{ secrets.FIREBASE_TEST_LAB_PROJECT }} + FIREBASE_TEST_LAB_SERVICE_ACCOUNT: ${{ secrets.FIREBASE_TEST_LAB_SERVICE_ACCOUNT }} + FIREBASE_TEST_LAB_WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.FIREBASE_TEST_LAB_WORKLOAD_IDENTITY_PROVIDER }} + if: "${{ env.FIREBASE_TEST_LAB_PROJECT != '' && env.FIREBASE_TEST_LAB_SERVICE_ACCOUNT != '' && env.FIREBASE_TEST_LAB_WORKLOAD_IDENTITY_PROVIDER != '' }}" + run: echo "::set-output name=defined::true" + + static_analysis_detekt: + needs: prime_cache + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + timeout-minutes: 1 + uses: actions/checkout@v2.4.0 + - name: Setup + id: setup + timeout-minutes: 5 + uses: ./.github/actions/setup + - name: Detekt + timeout-minutes: 4 + run: | + ./gradlew detektAll + - name: Collect Artifacts + timeout-minutes: 1 + if: ${{ always() }} + env: + ARTIFACTS_DIR_PATH: ${{ format('{0}/artifacts', env.home) }} + REPORTS_ZIP_PATH: ${{ format('{0}/artifacts/static_analysis_detekt.zip', env.home) }} + run: | + mkdir ${ARTIFACTS_DIR_PATH} + + zip -r ${REPORTS_ZIP_PATH} . -i build/reports/detekt/* + - name: Upload Artifacts + if: ${{ always() }} + uses: actions/upload-artifact@v2 + timeout-minutes: 1 + with: + name: Detekt static analysis results + path: ~/artifacts + + static_analysis_ktlint: + needs: prime_cache + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + timeout-minutes: 1 + uses: actions/checkout@v2.4.0 + - name: Setup + id: setup + timeout-minutes: 5 + uses: ./.github/actions/setup + - name: Ktlint + timeout-minutes: 4 + run: | + ./gradlew ktlint + - name: Collect Artifacts + timeout-minutes: 1 + if: ${{ always() }} + env: + ARTIFACTS_DIR_PATH: ${{ format('{0}/artifacts', env.home) }} + REPORTS_ZIP_PATH: ${{ format('{0}/artifacts/static_analysis_ktlint.zip', env.home) }} + run: | + mkdir ${ARTIFACTS_DIR_PATH} + + zip -r ${REPORTS_ZIP_PATH} . -i build/reports/ktlint/* + - name: Upload Artifacts + if: ${{ always() }} + uses: actions/upload-artifact@v2 + timeout-minutes: 1 + with: + name: Ktlint static analysis results + path: ~/artifacts + + static_analysis_android_lint: + needs: prime_cache + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + timeout-minutes: 1 + uses: actions/checkout@v2.4.0 + - name: Setup + id: setup + timeout-minutes: 5 + uses: ./.github/actions/setup + - name: Android Lint + timeout-minutes: 25 + env: + # Disable minify, since it makes lint run faster + ORG_GRADLE_PROJECT_IS_MINIFY_APP_ENABLED: false + run: | + ./gradlew :sdk-lib:lintRelease :demo-app:lintZcashmainnetRelease + - name: Collect Artifacts + timeout-minutes: 1 + env: + ARTIFACTS_DIR_PATH: ${{ format('{0}/artifacts', env.home) }} + LINT_ZIP_PATH: ${{ format('{0}/artifacts/android_lint.zip', env.home) }} + run: | + mkdir ${ARTIFACTS_DIR_PATH} + zip -r ${LINT_ZIP_PATH} . -i *build/reports/* + - name: Upload Artifacts + uses: actions/upload-artifact@v2 + timeout-minutes: 1 + with: + name: Android Lint static analysis results + path: ~/artifacts + + test_android_modules: + if: needs.check_secrets.outputs.has-secrets == 'true' + needs: [prime_cache, check_secrets] + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + timeout-minutes: 1 + uses: actions/checkout@v2.4.0 + - name: Setup + id: setup + timeout-minutes: 5 + uses: ./.github/actions/setup + - name: Build + timeout-minutes: 20 + run: | + ./gradlew assembleDebug assembleAndroidTest + - name: Authenticate to Google Cloud for Firebase Test Lab + id: auth_test_lab + uses: google-github-actions/auth@v0.5.0 + with: + create_credentials_file: true + project_id: ${{ secrets.FIREBASE_TEST_LAB_PROJECT }} + service_account: ${{ secrets.FIREBASE_TEST_LAB_SERVICE_ACCOUNT }} + workload_identity_provider: ${{ secrets.FIREBASE_TEST_LAB_WORKLOAD_IDENTITY_PROVIDER }} + access_token_lifetime: '1200s' + - name: Test + timeout-minutes: 20 + env: + # This first environment variable is used by Flank, since the temporary token is missing the project name + GOOGLE_CLOUD_PROJECT: ${{ secrets.FIREBASE_TEST_LAB_PROJECT }} + ORG_GRADLE_PROJECT_ZCASH_FIREBASE_TEST_LAB_API_KEY_PATH: ${{ steps.auth_test_lab.outputs.credentials_file_path }} + run: | + ./gradlew runFlank --parallel + - name: Collect Artifacts + timeout-minutes: 1 + env: + ARTIFACTS_DIR_PATH: ${{ format('{0}/artifacts', env.home) }} + TEST_RESULTS_ZIP_PATH: ${{ format('{0}/artifacts/test_results.zip', env.home) }} + run: | + mkdir ${ARTIFACTS_DIR_PATH} + + zip -r ${TEST_RESULTS_ZIP_PATH} . -i *build/outputs/androidTest-results/* + - name: Upload Artifacts + uses: actions/upload-artifact@v2 + timeout-minutes: 1 + with: + name: Test Android modules results + path: ~/artifacts + + demo_app_release_build: + needs: prime_cache + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + timeout-minutes: 1 + uses: actions/checkout@v2.4.0 + - name: Setup + id: setup + timeout-minutes: 5 + uses: ./.github/actions/setup + # A fake signing key to satisfy creating a "release" build + - name: Export Signing Key + env: + SIGNING_KEY_PATH: ${{ format('{0}/release.jks', env.home) }} + shell: bash + run: | + keytool -genkey -v -keystore $SIGNING_KEY_PATH -keypass android -storepass android -alias androiddebugkey -keyalg RSA -keysize 2048 -validity 100000 -dname "CN=, OU=, O=Test, L=, S=, C=" -noprompt + - name: Build + timeout-minutes: 25 + env: + ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEYSTORE_PATH: ${{ format('{0}/release.jks', env.home) }} + ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEYSTORE_PASSWORD: android + ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEY_ALIAS: androiddebugkey + ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEY_ALIAS_PASSWORD: android + run: | + ./gradlew :demo-app:assembleRelease + - name: Collect Artifacts + timeout-minutes: 1 + env: + ARTIFACTS_DIR_PATH: ${{ format('{0}/artifacts', env.home) }} + BINARIES_ZIP_PATH: ${{ format('{0}/artifacts/binaries.zip', env.home) }} + MAPPINGS_ZIP_PATH: ${{ format('{0}/artifacts/mappings.zip', env.home) }} + run: | + mkdir ${ARTIFACTS_DIR_PATH} + zip -r ${BINARIES_ZIP_PATH} . -i *app/build/outputs/apk/*/release/*.apk *app/build/outputs/bundle/*/release/*.aab + zip -r ${MAPPINGS_ZIP_PATH} . -i *app/build/outputs/mapping/*/mapping.txt + - name: Upload Artifacts + uses: actions/upload-artifact@v2 + timeout-minutes: 1 + with: + name: Demo app release binaries + path: ~/artifacts + + # Performs a button mash test on the release build of the demo app + test_robo_demo_app: + if: needs.check_secrets.outputs.has-secrets == 'true' + needs: [demo_app_release_build, check_secrets] + runs-on: ubuntu-latest + permissions: + packages: read + contents: read + steps: + - name: Checkout + timeout-minutes: 1 + uses: actions/checkout@v2.4.0 + - name: Setup + id: setup + timeout-minutes: 5 + uses: ./.github/actions/setup + - name: Authenticate to Google Cloud for Firebase Test Lab + id: auth_test_lab + uses: google-github-actions/auth@v0.5.0 + with: + create_credentials_file: true + project_id: ${{ secrets.FIREBASE_TEST_LAB_PROJECT }} + service_account: ${{ secrets.FIREBASE_TEST_LAB_SERVICE_ACCOUNT }} + workload_identity_provider: ${{ secrets.FIREBASE_TEST_LAB_WORKLOAD_IDENTITY_PROVIDER }} + access_token_lifetime: '900s' + - name: Download a single artifact + uses: actions/download-artifact@v2 + with: + name: Release binaries + - name: Robo test + timeout-minutes: 15 + env: + # Path depends on `release_build` job, plus path of `Download a single artifact` step + BINARIES_ZIP_PATH: binaries.zip + # This first environment variable is used by Flank, since the temporary token is missing the project name + GOOGLE_CLOUD_PROJECT: ${{ secrets.FIREBASE_TEST_LAB_PROJECT }} + ORG_GRADLE_PROJECT_ZCASH_FIREBASE_TEST_LAB_API_KEY_PATH: ${{ steps.auth_test_lab.outputs.credentials_file_path }} + run: | + unzip ${BINARIES_ZIP_PATH} + ./gradlew :app:runFlankSanityConfig diff --git a/build-conventions/src/main/kotlin/zcash.dependency-conventions.gradle.kts b/build-conventions/src/main/kotlin/zcash.dependency-conventions.gradle.kts new file mode 100644 index 00000000..bbdc79a1 --- /dev/null +++ b/build-conventions/src/main/kotlin/zcash.dependency-conventions.gradle.kts @@ -0,0 +1,15 @@ + +//dependencyLocking { +// lockAllConfigurations() +//} + +tasks { + register("resolveAll") { + doLast { + configurations.filter { + // Add any custom filtering on the configurations to be resolved + it.isCanBeResolved + }.forEach { it.resolve() } + } + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 15ff7750..64dc8699 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,11 +13,12 @@ buildscript { } plugins { + id("com.github.ben-manes.versions") + id("com.osacky.fulladle") + id("io.gitlab.arturbosch.detekt") id("org.jetbrains.dokka") id("org.owasp.dependencycheck") id("zcash.ktlint-conventions") - id("io.gitlab.arturbosch.detekt") - id("com.github.ben-manes.versions") } tasks { @@ -58,3 +59,32 @@ fun isNonStable(version: String): Boolean { return unstableKeywords.any { versionLowerCase.contains(it) } } + +// Firebase Test Lab has min and max values that might differ from our project's +// These are determined by `gcloud firebase test android models list` +@Suppress("MagicNumber", "PropertyName", "VariableNaming") +val FIREBASE_TEST_LAB_MIN_API = 23 +@Suppress("MagicNumber", "PropertyName", "VariableNaming") +val FIREBASE_TEST_LAB_MAX_API = 30 + +val firebaseTestLabKeyPath = project.properties["ZCASH_FIREBASE_TEST_LAB_API_KEY_PATH"].toString() +if (firebaseTestLabKeyPath.isNotBlank()) { + val minSdkVersion = run { + val buildMinSdk = project.properties["ANDROID_MIN_SDK_VERSION"].toString().toInt() + buildMinSdk.coerceAtLeast(FIREBASE_TEST_LAB_MIN_API).toString() + } + val targetSdkVersion = run { + val buildTargetSdk = project.properties["ANDROID_TARGET_SDK_VERSION"].toString().toInt() + buildTargetSdk.coerceAtMost(FIREBASE_TEST_LAB_MAX_API).toString() + } + fladle { + serviceAccountCredentials.set(File(firebaseTestLabKeyPath)) + devices.addAll( + mapOf("model" to "NexusLowRes", "version" to minSdkVersion), + mapOf("model" to "NexusLowRes", "version" to targetSdkVersion) + ) + + @Suppress("MagicNumber") + flakyTestAttempts.set(2) + } +} \ No newline at end of file diff --git a/PUBLISHING.md b/docs/PUBLISHING.md similarity index 64% rename from PUBLISHING.md rename to docs/PUBLISHING.md index 32decce2..4f39bc2e 100644 --- a/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -1,9 +1,19 @@ # Publishing +Two types of artifacts can be published: +1. Snapshot — An unstable release of the SDK for testing +1. Release — A stable release of the SDK + +Control of these modes of release is managed with a Gradle property `IS_SNAPSHOT`, which is + +For both snapshot and release deployments, there are two ways to initiate deployment: +1. Automatically — See [ci.md](ci.md), which describes the continuous integration workflow for deployment. +2. Manually — See the remainder of this document for how to configure manual deployment. + Publishing requires: ### One time only -* Get your dev environment setup to [compile the SDK](https://github.com/zcash/zcash-android-wallet-sdk/#compiling-sources) +* Set up environment to [compile the SDK](https://github.com/zcash/zcash-android-wallet-sdk/#compiling-sources) * Copy the GPG key to a directory with proper permissions (chmod 600). Note: If you'd like to quickly publish locally without subsequently publishing to Maven Central, configure a Gradle property `RELEASE_SIGNING_ENABLED=false` * Create file `~/.gradle/gradle.properties` per the [instructions in this guide](https://proandroiddev.com/publishing-a-maven-artifact-3-3-step-by-step-instructions-to-mavencentral-publishing-bd661081645d) * add your sonotype credentials with these properties @@ -15,18 +25,18 @@ Publishing requires: * `signing.secretKeyRingFile` ### Every time -1. Update the [build number](https://github.com/zcash/zcash-android-wallet-sdk/blob/master/gradle.properties) and the [CHANGELOG](https://github.com/zcash/zcash-android-wallet-sdk/blob/master/CHANGELOG.md) -2. Build locally +1. Update the [build number](https://github.com/zcash/zcash-android-wallet-sdk/blob/master/gradle.properties) and the [CHANGELOG](https://github.com/zcash/zcash-android-wallet-sdk/blob/master/CHANGELOG.md). For release builds, suffix the Gradle invocations below with `-PIS_SNAPSHOT=false`. +3. Build locally * This will install the files in your local maven repo at `~/.m2/repository/cash/z/ecc/android/` ```zsh ./gradlew publishToMavenLocal ``` -3. Publish via the following command: +4. Publish via the following command: ```zsh # This uploads the file to sonotype’s staging area ./gradlew publish --no-daemon --no-parallel ``` -4. Deploy to maven central: +5. Deploy to maven central: ```zsh # This closes the staging repository and releases it to the world ./gradlew closeAndReleaseRepository diff --git a/docs/ci.md b/docs/ci.md index 852de79f..c30dcaf5 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -1,51 +1,46 @@ # Continuous Integration +Continuous integration is set up with GitHub Actions. The workflows are defined in this repo under [/.github/workflows](../.github/workflows). -In order to ensure code changes comply with the target integrations, the following policies should be observed. +Workflows exist for: + * Pull request - On pull request, static analysis and testing is performed. + * Snapshot deployment - On merge to the main branch, a snapshot release is deployed to Maven Central. Concurrency limits are in place, to ensure that only one snapshot deployment can happen at a time. + * Release deployment - Work in progress. Manually invoked workflow to deploy to Maven Central. Concurrency limits are in place, to ensure that only one release deployment can happen at a time. -## Changes to the GitHub project +## Setup +When forking this repository, some secrets need to be defined to set up new continuous integration builds. -The `master` branch is the git repositories default branch. +The secrets passed to GitHub Actions then map to Gradle properties set up within our build scripts. Necessary secrets are documented at the top of each GitHub workflow yml file, as well as reiterated here. -Jobs are managed by PR labels. The following labels are actionable. +### Pull request +* `FIREBASE_TEST_LAB_PROJECT` - Firebase Test Lab project name. +* `FIREBASE_TEST_LAB_SERVICE_ACCOUNT` - Email address of Firebase Test Lab service account. +* `FIREBASE_TEST_LAB_WORKLOAD_IDENTITY_PROVIDER` - Workload identity provider to generate temporary service account key. -|label|targets| -|----|---| -|`safe-to-ci`| `ciBuild`, `ciLintPr` +To obtain the values for these, you'll need to enable the necessary Google Cloud APIs to enable automated access to Firebase Test Lab. +* Configure Firebase Test Lab. Google has [documentation for Jenkins](https://firebase.google.com/docs/test-lab/android/continuous). Although we're using GitHub Actions, the initial requirements are the same. +* Configure [workload identity federation](https://github.com/google-github-actions/auth#setting-up-workload-identity-federation) -### Code changes to the master branch +Once configured, these allow for generation of a temporary key which is then provided to the build through the Gradle property `ZCASH_FIREBASE_TEST_LAB_API_KEY_PATH`. -These changes are identified by modifications to files under the `src` directory or files matching `^.*\.(gradle|toml)$` +Note: Pull requests do not currently run darkside tests. See #361. -- build the project -- save artifacts to GCP bucket with short git hash, and `master` -- update code documentation via `docs` gradle target +### Snapshot deployment +* `MAVEN_CENTRAL_USERNAME` — Username for Maven Central, which maps to the Gradle property `mavenCentralUsername`. +* `MAVEN_CENTRAL_PASSWORD` — Password for Maven Central, which maps to the Gradle property `mavenCentralPassword`. -### New tag is created +GPG keys are not needed for snapshot deployment. -- build the project, save artifacts to GCP bucket with tag -- push artifacts to bintray -- send notification to Slack channel +Note: For documentation on the Gradle properties for Maven deployment, see [Gradle Maven Publish Plugin](https://github.com/vanniktech/gradle-maven-publish-plugin). -## Time based changes +Note: Snapshot builds are configured with a Gradle property `IS_SNAPSHOT`. The workflow automatically sets this property to true for snapshot deployments. This will suffix the version with `-snapshot` and will upload to the snapshot repository. -Periodic continous integration tasks +### Release deployment +* `MAVEN_CENTRAL_USERNAME` — Username for Maven Central, which maps to the Gradle property `mavenCentralUsername`. +* `MAVEN_CENTRAL_PASSWORD` — Password for Maven Central, which maps to the Gradle property `mavenCentralPassword`. +* `MAVEN_SIGNING_KEYRING_FILE_BASE64` — GPG keyring file, base64 encoded. Maps to Gradle property `signing.secretKeyRingFile`. +* `MAVEN_SIGNING_KEY_ID` — Name of key inside GPG keyring file. Maps to Gradle property `signing.keyId`. +* `MAVEN_SIGNING_PASSWORD` — Password for key inside GPG keyring file. Maps to Gradle property `signing.password`. -### Nightly +Note: For documentation on the Gradle properties for Maven deployment, see [Gradle Maven Publish Plugin](https://github.com/vanniktech/gradle-maven-publish-plugin). -- run integration tests -- send notifications for failed builds or failed nightly integrations - -## Targets - -Run gradle targets with the following command. -Note: it is recommended to NOT install `gradle` because the wrapper command (gradlew) takes care of that automatically. -``` -./gradlew -``` -Where `` is one of the following - -- **ciBuild** : build the project and produce the AAR artifact under `build/outputs/aar` -- **ciDeploy** : deploy the AAR artifact to bintray. This will invoke **ciBuild**, if necessary. -- **ciLintPr** : lint the code in response to a PR to verify codestyle and formatting. -- **ciTestPr** : run the basic PR test suite. This will invoke **ciBuild**, if necessary. -- **ciTestNightly** : run the full nightly integration test suite +Note: Snapshot builds are configured with a Gradle property `IS_SNAPSHOT`. The workflow automatically sets this property to false for release deployments. \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 027c3650..0faaa0b5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,8 +18,10 @@ android.builder.sdkDownload=true mavenCentralUsername= mavenCentralPassword= -## Remove -snapshot for release publishing -LIBRARY_VERSION=1.3.0-beta19-snapshot +# Configures whether release is an unstable snapshot. +IS_SNAPSHOT=true + +LIBRARY_VERSION=1.3.0-beta19 POM_NAME=Zcash Android Wallet SDK POM_ARTIFACT_ID=zcash-android-sdk POM_DESCRIPTION=This lightweight SDK connects Android to Zcash. It welds together Rust and Kotlin in a minimal way, allowing third-party Android apps to send and receive shielded transactions easily, securely and privately. @@ -46,6 +48,9 @@ IS_COVERAGE_ENABLED=false # It is disabled by default, because it causes tests to take about 2x longer to run. IS_USE_TEST_ORCHESTRATOR=false +# Optional API key for Firebase Test Lab +ZCASH_FIREBASE_TEST_LAB_API_KEY_PATH= + # Optionally configure minification # For now, don't minify the SDK but do minify the app. Minifying the app helps us test # the proguard-consumer rules in the SDK. @@ -69,6 +74,7 @@ ANDROID_NDK_VERSION=22.1.7171670 ANDROID_GRADLE_PLUGIN_VERSION=7.0.4 DETEKT_VERSION=1.19.0 DOKKA_VERSION=1.6.10 +FULLADLE_VERSION_MATCHER=0.17.3 GRADLE_VERSIONS_PLUGIN_VERSION=0.40.0 KTLINT_VERSION=0.43.0 KSP_VERSION=1.6.10-1.0.2 diff --git a/sdk-lib/build.gradle.kts b/sdk-lib/build.gradle.kts index 1f2c6444..57814e06 100644 --- a/sdk-lib/build.gradle.kts +++ b/sdk-lib/build.gradle.kts @@ -18,14 +18,19 @@ plugins { } // Publishing information +val isSnapshot = project.property("IS_SNAPSHOT").toString().toBoolean() val version = project.property("LIBRARY_VERSION").toString() val ARTIFACT_ID = project.property("POM_ARTIFACT_ID").toString() project.group = "cash.z.ecc.android" -project.version = version +project.version = if (isSnapshot) { + "$version-snapshot" +} else { + version +} publishing { - // Snapshot repo for be manually configured + // Snapshot repo must be manually configured // Release repo is configured automatically by the com.vanniktech.maven.publish plugin - if (version.contains("snapshot")) { + if (isSnapshot) { val mavenCentralUsername = project.property("mavenCentralUsername").toString() val mavenCentralPassword = project.property("mavenCentralPassword").toString() if (mavenCentralUsername.isNotBlank() && mavenCentralPassword.isNotBlank()) { @@ -46,7 +51,7 @@ publishing { } } -if (version.contains("snapshot")) { +if (isSnapshot) { mavenPublish { sonatypeHost = null } diff --git a/settings.gradle.kts b/settings.gradle.kts index 808e3089..c09ce334 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,7 @@ pluginManagement { plugins { val detektVersion = extra["DETEKT_VERSION"].toString() val dokkaVersion = extra["DOKKA_VERSION"].toString() + val fulladleVersion = extra["FULLADLE_VERSION_MATCHER"].toString() val gradleVersionsPluginVersion = extra["GRADLE_VERSIONS_PLUGIN_VERSION"].toString() val kotlinVersion = extra["KOTLIN_VERSION"].toString() val kspVersion = extra["KSP_VERSION"].toString() @@ -19,6 +20,7 @@ pluginManagement { id("com.github.ben-manes.versions") version (gradleVersionsPluginVersion) apply (false) id("com.google.devtools.ksp") version(kspVersion) apply (false) id("com.google.protobuf") version (protobufVersion) apply (false) + id("com.osacky.fulladle") version (fulladleVersion) apply (false) id("io.gitlab.arturbosch.detekt") version (detektVersion) apply (false) id("org.jetbrains.dokka") version (dokkaVersion) apply (false) id("org.jetbrains.kotlin.plugin.allopen") version (kotlinVersion) apply (false)