diff --git a/.buildkite/commands/release-build.sh b/.buildkite/commands/release-build.sh new file mode 100755 index 00000000..081c0440 --- /dev/null +++ b/.buildkite/commands/release-build.sh @@ -0,0 +1,10 @@ +#!/bin/bash -eu + +echo "--- :rubygems: Setting up Gems" +install_gems + +echo "--- :closed_lock_with_key: Installing Secrets" +bundle exec fastlane run configure_apply + +echo "--- :hammer_and_wrench: Building and Uploading to Play Store" +bundle exec fastlane build_and_upload_to_play_store track:'internal' release_status:'completed' diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 6d8445f7..c2b43e1b 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -50,7 +50,7 @@ steps: ./gradlew :app:lintDebug upload_sarif_to_github 'app/build/reports/lint-results-debug.sarif' - plugins: [ $CI_TOOLKIT ] + plugins: [$CI_TOOLKIT] artifact_paths: - "**/build/reports/lint-results*.*" notify: @@ -62,7 +62,7 @@ steps: ./gradlew detekt upload_sarif_to_github 'app/build/reports/detekt/detekt.sarif' - plugins: [ $CI_TOOLKIT ] + plugins: [$CI_TOOLKIT] artifact_paths: - "**/build/reports/detekt/*.*" notify: diff --git a/.buildkite/release-builds.yml b/.buildkite/release-builds.yml new file mode 100644 index 00000000..07a5781d --- /dev/null +++ b/.buildkite/release-builds.yml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json +--- + +# This pipeline is meant to be run via the action `buildkite_add_trigger_step`, which calls the buildkite-agent to add it as a trigger step + +agents: + queue: "android" + +steps: + - label: Gradle Wrapper Validation + command: validate_gradle_wrapper + priority: 1 + agents: + queue: linter + + # Wait for Gradle Wrapper to be validated + - wait + + - label: 🕵️‍♂️ Lint + command: ./gradlew lintRelease + key: lint + plugins: [$CI_TOOLKIT] + artifact_paths: + - "**/build/reports/lint-results*.*" + + - label: ":hammer_and_wrench: :android: Build Release and Upload to Play Store" + command: .buildkite/commands/release-build.sh + priority: 1 + depends_on: lint + plugins: [$CI_TOOLKIT] + artifact_paths: + - "app/build/outputs/bundle/release/*.aab" diff --git a/.buildkite/release-pipelines/new-alpha-release.yml b/.buildkite/release-pipelines/new-alpha-release.yml new file mode 100644 index 00000000..9d0fadfb --- /dev/null +++ b/.buildkite/release-pipelines/new-alpha-release.yml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json +--- + +steps: + - label: ":rocket: New Alpha Release" + key: new-alpha-release + plugins: [$CI_TOOLKIT] + command: | + echo "--- :robot_face: Use bot for Git operations" + source use-bot-for-git + + echo "--- :ruby: Setup Ruby Tools" + install_gems + + echo "--- :rocket: Create New Alpha" + bundle exec fastlane new_alpha_release skip_confirm:true + agents: + queue: tumblr-metal diff --git a/Gemfile b/Gemfile index 926ccb20..89cdc436 100644 --- a/Gemfile +++ b/Gemfile @@ -4,4 +4,4 @@ source 'https://rubygems.org' gem 'danger-dangermattic', '~> 1.2' gem 'fastlane', '~> 2.228' -gem 'fastlane-plugin-wpmreleasetoolkit', '~> 13.3' +gem 'fastlane-plugin-wpmreleasetoolkit', '~> 13.4' diff --git a/Gemfile.lock b/Gemfile.lock index a90fd1c9..3a38700b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -167,7 +167,7 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.4.1) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) - fastlane-plugin-wpmreleasetoolkit (13.3.1) + fastlane-plugin-wpmreleasetoolkit (13.4.0) activesupport (>= 6.1.7.1) buildkit (~> 1.5) chroma (= 0.2.0) @@ -354,7 +354,7 @@ PLATFORMS DEPENDENCIES danger-dangermattic (~> 1.2) fastlane (~> 2.228) - fastlane-plugin-wpmreleasetoolkit (~> 13.3) + fastlane-plugin-wpmreleasetoolkit (~> 13.4) BUNDLED WITH 2.6.3 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 73507392..0a064a6d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -7,19 +7,79 @@ UI.user_error!('Please run fastlane via `bundle exec`') unless FastlaneCore::Hel ######################################################################## # Constants ######################################################################## +DEFAULT_BRANCH = 'trunk' + +BUILDKITE_ORG_NAME = 'automattic' +BUILDKITE_PIPELINE = 'gravatar-android' + +PROJECT_ROOT_FOLDER = File.dirname(File.expand_path(__dir__)) + +VERSION_PROPERTIES_PATH = File.join(PROJECT_ROOT_FOLDER, 'version.properties') +VERSION_FILE = Fastlane::Wpmreleasetoolkit::Versioning::AndroidVersionFile.new(version_properties_path: VERSION_PROPERTIES_PATH) + +VERSION_CALCULATOR = Fastlane::Wpmreleasetoolkit::Versioning::SemanticVersionCalculator.new +VERSION_FORMATTER = Fastlane::Wpmreleasetoolkit::Versioning::RCNotationVersionFormatter.new +# Using DerivedBuildCodeFormatter to derive build codes from version name (format: 1XXYYYZNN) +# This eliminates the need for manual build code incrementing, provides deterministic versioning, +# and also avoids the need to do a new beta for version N+1 every time we do a hotfix for version N +BUILD_CODE_FORMATTER = Fastlane::Wpmreleasetoolkit::Versioning::DerivedBuildCodeFormatter.new(prefix: '1', major_digits: 2, minor_digits: 3, patch_digits: 1, build_digits: 2) PROTOTYPE_BUILD_DOMAIN = 'https://cdn.a8c-ci.services' -PROTOTYPE_BUILD_TYPE = 'release' +RELEASE_BUILD_TYPE = 'Release' platform :android do - # Builds the Gravatar Demo app prototype APK, uploads it to S3, and posts a download link as a comment on the associated pull request. + # Builds a release app bundle (.aab) for the Gravatar Android app + # + # This lane cleans the project and builds a release bundle (.aab) that can be uploaded to the Google Play Store. + # + lane :build_bundle do + gradle(task: 'clean') + + gradle( + task: 'bundle', + build_type: RELEASE_BUILD_TYPE + ) + end + + # Uploads the app bundle to the Google Play Store + # + # This lane uploads a previously built app bundle to the specified track on Google Play Store + # with the given release status and rollout percentage. + # + # @param track [String] The track to upload to. One of: 'internal', 'alpha', 'beta', 'production' + # @param release_status [String] The release status. One of: 'completed', 'draft', 'halted', 'inProgress' + # @param rollout [String] The percentage of users who should get the update, as a string between '0' and '1' + # @env [String] PLAY_STORE_SERVICE_ACCOUNT_UPLOAD_KEY The Google Play Store service account JSON key data + # + # @example Upload to internal track as draft + # upload_to_store(track: 'internal', release_status: 'draft', rollout: nil) + # @example Upload to production with 50% rollout + # upload_to_store(track: 'production', release_status: 'completed', rollout: '0.5') + # + lane :upload_to_store do |track:, release_status:, rollout:| + require_env_vars!('PLAY_STORE_SERVICE_ACCOUNT_UPLOAD_KEY') + + upload_to_play_store( + package_name: 'com.gravatar.app', + aab: File.join(PROJECT_ROOT_FOLDER, 'app', 'build', 'outputs', 'bundle', 'release', 'app-release.aab'), + track: track, + release_status: release_status, + rollout: rollout, + skip_upload_metadata: (track != 'production'), + skip_upload_changelogs: (track != 'production'), + skip_upload_images: true, + skip_upload_screenshots: true, + json_key_data: ENV.fetch('PLAY_STORE_SERVICE_ACCOUNT_UPLOAD_KEY', nil) + ) + end + + # Builds the Gravatar Demo app prototype APK, uploads it to S3, and posts a download link as a comment on the associated pull request # - # Intended for use in CI to provide reviewers with an installable build for testing. - # @env [String] BUILDKITE_ARTIFACTS_S3_BUCKET The S3 bucket to upload artifacts to. Must be set. - # @env [String] BUILDKITE_PULL_REQUEST The pull request number. + # This lane is intended for use in CI to provide reviewers with an installable build for testing. + # It builds a release APK, uploads it to S3, and posts the download link as a PR comment. # lane :build_and_upload_prototype_build do - UI.user_error!("'BUILDKITE_ARTIFACTS_S3_BUCKET' must be defined as an environment variable.") unless ENV['BUILDKITE_ARTIFACTS_S3_BUCKET'] + require_env_vars!('BUILDKITE_ARTIFACTS_S3_BUCKET') comment_params = { project: 'Automattic/Gravatar-Android', @@ -34,7 +94,7 @@ platform :android do gradle( task: 'assemble', - build_type: PROTOTYPE_BUILD_TYPE + build_type: RELEASE_BUILD_TYPE ) upload_path = upload_to_s3( @@ -50,7 +110,7 @@ platform :android do app_display_name: 'Gravatar Android', download_url: install_url, metadata: { - 'Build Type': PROTOTYPE_BUILD_TYPE + 'Build Type': RELEASE_BUILD_TYPE }, fold: true ) @@ -61,19 +121,192 @@ platform :android do ) end - def generate_prototype_build_number - if ENV['BUILDKITE'] - commit = ENV.fetch('BUILDKITE_COMMIT', nil)[0, 7] - branch = ENV['BUILDKITE_BRANCH'].parameterize - pr_num = ENV.fetch('BUILDKITE_PULL_REQUEST', nil) + # Builds an app bundle and uploads it to the Google Play Store + # + # This is a convenience lane that combines building the app bundle and uploading it to the Play Store. + # It uses internal track with draft releases by default while the release process is being refined. + # + # @param track [String] The Play Store track to upload to (default: 'internal') + # @param release_status [String] The Play Store release status (default: 'draft') + # @see upload_to_store + # + lane :build_and_upload_to_play_store do |track: 'internal', release_status: 'draft'| + # TODO: using `internal` track with `draft` releases by default while the release itself is in draft mode + build_bundle + upload_to_store(track: track, release_status: release_status, rollout: release_status == 'draft' ? nil : '1') + end - pr_num == 'false' ? "#{branch}-#{commit}" : "pr#{pr_num}-#{commit}" - else - repo = Git.open(PROJECT_ROOT_FOLDER) - commit = repo.current_branch.parameterize - branch = repo.revparse('HEAD')[0, 7] + ##################################################################################### + # Release lanes + ##################################################################################### + + # Initiates a new alpha release by bumping the build code and triggering a release build + # + # This lane will checkout and pull the trunk branch (if running on CI), bump the build code, + # push the changes to the remote repository, and trigger a new alpha build in Buildkite. + # + # @param skip_confirm [Boolean] Skip the confirmation prompt (default: false) + # + lane :new_alpha_release do |skip_confirm: false| + require_env_vars!('BUILDKITE_TOKEN') + ensure_git_status_clean + + UI.important <<~PROMPT + Initiating a new alpha release. + This will: + - Checkout and pull the #{DEFAULT_BRANCH} branch (if running on CI) + - Bump the build code from #{build_code_current} to #{build_code_next} + - Trigger a new alpha build in Buildkite + PROMPT + next unless skip_confirm || UI.confirm('Continue?') + + Fastlane::Helper::GitHelper.checkout_and_pull(DEFAULT_BRANCH) if is_ci? + + bump_build_code + + push_to_git_remote - "#{branch}-#{commit}" + trigger_release_build + end + + # Triggers a new alpha build on CI using Buildkite + # + # This lane triggers a release build in Buildkite for the current git branch. + # The build will use the current release version and build code. + # + lane :trigger_release_build do + require_env_vars!('BUILDKITE_TOKEN') + + trigger_buildkite_build( + branch: git_branch, + pipeline: 'release-builds.yml', + message: "Release Build #{release_version_current} (#{build_code_current})" + ) + end + + def trigger_buildkite_build(branch:, pipeline:, message: nil) + environment = { + 'RELEASE_VERSION' => release_version_current + } + + common_args = { + pipeline_file: pipeline, + branch: branch, + message: message || "Build #{pipeline.sub('.yml', '')}", + environment: environment + } + + if is_ci? + buildkite_add_trigger_step(**common_args) + else + # For local development, call Buildkite API to start a new build + buildkite_trigger_build( + buildkite_organization: BUILDKITE_ORG_NAME, + buildkite_pipeline: BUILDKITE_PIPELINE, + **common_args + ) end end end + +##################################################################################### +# Versioning utils +##################################################################################### + +# Updates the build code in version.properties by incrementing the build number and deriving the new build code +# +def bump_build_code + VERSION_FILE.write_version( + version_name: release_version_current, + version_code: build_code_next + ) + commit_version_update(message: '[skip ci] Bump build code') +end + +# Updates the version name in version.properties to the next release version and commits the change +# +def bump_version + VERSION_FILE.write_version( + version_name: release_version_next + ) + commit_version_update(message: '[skip ci] Bump app version') +end + +# Commits the version update to the version.properties file +# +def commit_version_update(message:) + Fastlane::Helper::GitHelper.commit( + message: message, + files: VERSION_PROPERTIES_PATH + ) +end + +# Returns the current version name from `version.properties` without needing formatting or calculations +# +def version_name_current + VERSION_FILE.read_version_name +end + +# Returns the release version of the app in the format `1.2` or `1.2.3` if it is a hotfix +# +def release_version_current + current_version = VERSION_FORMATTER.parse(version_name_current) + VERSION_FORMATTER.release_version(current_version) +end + +# Returns the next release version of the app in the format `1.2` or `1.2.3` if it is a hotfix +# +def release_version_next + current_version = VERSION_FORMATTER.parse(version_name_current) + release_version_next = VERSION_CALCULATOR.next_release_version(version: current_version) + VERSION_FORMATTER.release_version(release_version_next) +end + +# Returns the current build code of the app derived from the current version +# +def build_code_current + current_version = VERSION_FORMATTER.parse(version_name_current) + BUILD_CODE_FORMATTER.build_code(version: current_version) +end + +# Returns the next build code of the app derived from the current version with incremented build number +# +def build_code_next + current_version = VERSION_FORMATTER.parse(version_name_current) + # Create a new version with incremented build number + next_version = Fastlane::Models::AppVersion.new( + current_version.major, + current_version.minor, + current_version.patch, + current_version.build_number + 1 + ) + BUILD_CODE_FORMATTER.build_code(version: next_version) +end + +# Generates a build number for the prototype build +# +def generate_prototype_build_number + if ENV['BUILDKITE'] + commit = ENV.fetch('BUILDKITE_COMMIT', nil)[0, 7] + branch = ENV['BUILDKITE_BRANCH'].parameterize + pr_num = ENV.fetch('BUILDKITE_PULL_REQUEST', nil) + + pr_num == 'false' ? "#{branch}-#{commit}" : "pr#{pr_num}-#{commit}" + else + repo = Git.open(PROJECT_ROOT_FOLDER) + commit = repo.current_branch.parameterize + branch = repo.revparse('HEAD')[0, 7] + + "#{branch}-#{commit}" + end +end + +##################################################################################### +# Other utils +##################################################################################### + +def require_env_vars!(*keys) + keys.each do |key| + UI.user_error!("Environment variable '#{key}' is not set.") unless ENV.key?(key) + end +end diff --git a/version.properties b/version.properties index 773d02d7..d3c0e496 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ -versionName=0.1 -versionCode=4 \ No newline at end of file +versionName=0.21 +versionCode=100021001 \ No newline at end of file