Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Tooling] Introduce publish_release lane #3382

Open
wants to merge 4 commits into
base: release/7.79
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 119 additions & 79 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ SUPPORTED_LOCALES = [
Dotenv.load(USER_ENV_FILE_PATH)
DEFAULT_BRANCH = 'main'
ENV['SUPPLY_UPLOAD_MAX_RETRIES'] = '5'
GH_REPOSITORY = 'automattic/pocket-casts-android'
GITHUB_REPO = 'automattic/pocket-casts-android'
APPS_APP = 'app'
APPS_AUTOMOTIVE = 'automotive'
APPS_WEAR = 'wear'
Expand Down Expand Up @@ -144,12 +144,12 @@ platform :android do
# We cannot use the `copy_branch_protection` action here as it would create a branch protection rule with the same
# restrictions we have in `main`, and the release managers wouldn't be able to push due to permissions.
# This should be changed only when we have PC Android releases done on CI, when the CI bot is the one running `git push`.
set_branch_protection(repository: GH_REPOSITORY, branch: "release/#{new_version}")
set_branch_protection(repository: GITHUB_REPO, branch: "release/#{new_version}")

begin
# Move PRs to next milestone
moved_prs = update_assigned_milestone(
repository: GH_REPOSITORY,
repository: GITHUB_REPO,
from_milestone: new_version,
to_milestone: release_version_next,
comment: "Version `#{new_version}` has now entered code-freeze, so the milestone of this PR has been updated to `#{release_version_next}`."
Expand All @@ -158,7 +158,7 @@ platform :android do

# Add ❄️ marker to milestone title to indicate we entered code-freeze
set_milestone_frozen_marker(
repository: GH_REPOSITORY,
repository: GITHUB_REPO,
milestone: new_version
)
rescue StandardError => e
Expand Down Expand Up @@ -290,75 +290,6 @@ platform :android do
create_backmerge_pr
end

# @param branch_to_build [String] The branch to build. Defaults to the current git branch.
#
lane :trigger_release_build do |branch_to_build: git_branch|
buildkite_trigger_build(
buildkite_organization: 'automattic',
buildkite_pipeline: 'pocket-casts-android',
branch: branch_to_build,
pipeline_file: 'release-builds.yml'
)
end

# Builds and uploads a new build to Google Play (without releasing it)
#
# - Uses the current version to decide if this is a beta or production build
# - Builds the apps for external distribution
# - Uploads the builds to 'beta' or 'production' Play Store channel (but does not release it)
# - Creates draft Github release
#
# @param skip_confirm [Boolean] If true, avoids any interactive prompt
# @param skip_prechecks [Boolean] If true, skips android_build_preflight
# @param create_gh_release [Boolean] If true, creates a draft GitHub release
#
lane :build_and_upload_to_play_store do |skip_prechecks: false, skip_confirm: false, create_gh_release: false|
version = version_name_current
build_code = build_code_current
is_beta = beta_version?(version)
unless skip_prechecks
# Match branch names that begin with `release/`
ensure_git_branch(branch: '^release/') unless is_ci

UI.important("Building version #{version_name_current} (#{build_code_current}) for upload to Google Play Console")
UI.user_error!("Terminating as requested. Don't forget to run the remainder of this automation manually.") unless skip_confirm || UI.confirm('Do you want to continue?')

# Check local repo status
ensure_git_status_clean unless is_ci

android_build_preflight
end

release_assets = []

APPS.each do |app|
build_bundle(app: app, version: version, build_code: build_code)

aab_artifact_path = aab_artifact_path(app, version)
UI.error("Unable to find a build artifact at #{aab_artifact_path}") unless File.exist? aab_artifact_path

track = play_store_track(app: app, is_beta: is_beta)
upload_to_play_store(
**UPLOAD_TO_PLAY_STORE_COMMON_OPTIONS,
aab: aab_artifact_path,
track: track,
release_status: 'draft'
)
release_assets << aab_artifact_path

signed_apk_artifact_path = signed_apk_artifact_path(app, version)
download_universal_apk_from_google_play(
package_name: APP_PACKAGE_NAME,
version_code: version_code_for_app(app: app, version_code: build_code),
destination: signed_apk_artifact_path,
json_key: UPLOAD_TO_PLAY_STORE_JSON_KEY
)
release_assets << signed_apk_artifact_path
end

create_gh_release(version: version, prerelease: is_beta, release_assets: release_assets.compact) if create_gh_release
end

# Update the rollout of all 3 variants (app, automotive, wear) of the current version to the given value
#
# @param percent [Float] The rollout percentage, between 0 and 1
Expand Down Expand Up @@ -424,16 +355,125 @@ platform :android do
version = release_version_current

# Wrap up
remove_branch_protection(repository: GH_REPOSITORY, branch: "release/#{version}")
set_milestone_frozen_marker(repository: GH_REPOSITORY, milestone: version, freeze: false)
create_new_milestone(repository: GH_REPOSITORY, need_appstore_submission: true)
close_milestone(repository: GH_REPOSITORY, milestone: version)
remove_branch_protection(repository: GITHUB_REPO, branch: "release/#{version}")
set_milestone_frozen_marker(repository: GITHUB_REPO, milestone: version, freeze: false)
create_new_milestone(repository: GITHUB_REPO, need_appstore_submission: true)
close_milestone(repository: GITHUB_REPO, milestone: version)

push_to_git_remote(tags: false)
trigger_release_build(branch_to_build: "release/#{version}")
create_backmerge_pr
end

# This lane publishes a release on GitHub and creates a PR to backmerge the current release branch into the next release/ branch
#
# @param [Boolean] skip_confirm (default: false) If set, will skip the confirmation prompt before running the rest of the lane
#
lane :publish_release do |skip_confirm: false|
ensure_git_status_clean
ensure_git_branch(branch: '^release/')

version_number = release_version_current

current_branch = "release/#{version_number}"
next_release_branch = "release/#{release_version_next}"

UI.important <<~PROMPT
Publish the #{version_number} release. This will:
- Publish the existing draft `#{version_number}` release on GitHub
- Which will also have GitHub create the associated git tag, pointing to the tip of the branch
- If the release branch for the next version `#{next_release_branch}` already exists, backmerge `#{current_branch}` into it
- If needed, backmerge `#{current_branch}` back into `#{DEFAULT_BRANCH}`
- Delete the `#{current_branch}` branch
PROMPT
UI.user_error!("Terminating as requested. Don't forget to run the remainder of this automation manually.") unless skip_confirm || UI.confirm('Do you want to continue?')

UI.important "Publishing release #{version_number} on GitHub"

publish_github_release(
repository: GITHUB_REPO,
name: version_number
)

create_backmerge_pr
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See @iangmaia 's comment on the original PR about this.

  • In most cases there won't be anything to merge from release/N->main at that stage so creation of such a PR would be expected to be skipped by the create_release_backmerge_pull_request action… but given PCAndroid uses "Squash Merge", the action will detect that some commits from release/N are not present in main and thus still create such a release/N->main backmerge PR anyway.
  • Given that we don't expect any last-minute commits having landed in release/N branch after the finalize_release happened earlier in the scenario, in practice it should be OK 99% of the time to skip the creation of this release/N->main backmerge PR. There's still a risk that a late commit that didn't warrant a resubmission (e.g. last-minute tooling fix landed after finalize_release but not requiring a new run of finalize_release after it landed because it only changed tooling, not the binary being submitted) would be lost though, but that might be an acceptable balance too?
  • It's important to note though that, regardless of all the above, we still want to ensure that there's a release/N->release/N+1 backmerge PR being created to make sure we include all the latest fixes of version N in version N+1 for those late fixes that might have landed after the code freeze of version N+1 (and the creation of release/N+1 branch) already happened.

As a result, we might want to adjust this call to only backmerge to the next release/N+1 branch (if it already exists at that time, which should be the case given the scenario timing)… but not to main?


Ideally we should probably find a way to improve our create_release_backmerge_pull_request action in release-toolkit so it can support the case of repos that are using squash-merges, so that its implementation logic to detect if such a PR is necessary would not be based on "if there are commits in the release/* branch not present in the merge-base of release/* and main", but instead based on the diff being empty or not (worth pondering if we should then do such a comparison on the two-dots vs three-dots diff and consider any potential edge cases though)

In the meantime, since the current Release Scenario never mentioned creating a backmerge PR amongst the tasks of the "Release" milestone, maybe it'd be just as simple to remove that call from publish_release, to keep the current status quo? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we should probably find a way to improve our create_release_backmerge_pull_request action in release-toolkit so it can support the case of repos that are using squash-merges

I agree. I vaguely remember one of my first implementations was using the actual diff, but iinm using only the diff had its own problems. Perhaps we could combine the current implementation with that approach as well 🤔

Also to ponder, GitHub itself doesn't seem to use such approach and just creates the PR 🤷

In the meantime, since the current Release Scenario never mentioned creating a backmerge PR amongst the tasks of the "Release" milestone, maybe it'd be just as simple to remove that call from publish_release, to keep the current status quo? 🤔

Yep, I've decided together with the team to follow this approach for WPAndroid / WPiOS for now. Additionally, they also argued that creating a PR from this step can be somewhat unexpected for the release manager, but I guess that could be solved with either a separate step or better communication.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, they also argued that creating a PR from this step can be somewhat unexpected for the release manager, but I guess that could be solved with either a separate step or better communication.

I can see how it can be confuising to have a backmerge PR release/N->trunk be created at that stage if no new commit has landed in release/N since the submission, but yet the action creates a PR nonetheless because of the repo using squash-merge and the action thus detecting commits from release/N missing from trunk.

But if we solve this to support the use case of repos using squash-merge (maybe just checking for the diff being non-empty in those cases indeed?), then at that point I think we should still keep the publish_release lane being the one doing the backmerge PRs, including because it's also the lane that ensures that the release/N branch is deleted once the tag has been created by the GitHub Release.

We can't delete the release/* branch until we've made sure we created a git tag (by way of publishing the GitHub release) first, and we can't delete the release/* branch until we ensured that everything that was in that release/* branch has been propagated to other branches (trunk and release/N+1) first (either by a merge-commit or by a squash-merge), otherwise for the very rare cases where we might have added a commit post-finalize_release (e.g. tooling fix or similar) we risk losing that fix forever instead of it being propagated to next versions. So making this backmerge PR be done in a separate Task in the release scenario might not be practically possible as the release/* branch would have already been deleted by the previous task calling publish_release at that point.

But as long as we manage to make the create_release_backmerge_pull_request action properly detect when a PR is not necessary on repos using squash-merge like it does for repos using merge-commits, in 95% of cases there won't be a PR created at publish_release stage anymore so that won't create some unexpected surprise for the release manager, all while ensuring that in the rare cases where it would be necessary to create the PR to ensure not to lose post-submission (tooling?) fixes, it is still done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as long as we manage to make the create_release_backmerge_pull_request action properly detect when a PR is not necessary on repos using squash-merge like it does for repos using merge-commits

Yeah, I think that's the main point: making the action work properly for repos with a squash merge policy.

The PR(s) creation during release publishing (or eventual breakdown into different Rv2 tasks, or a better task description to the release manager, etc) can be a separate discussion.


# At this point, an intermediate branch has been created by creating a backmerge PR to a hotfix or the next version release branch.
# This allows us to safely delete the `release/*` remote branch.
remove_branch_protection(repository: GITHUB_REPO, branch: current_branch)
Fastlane::Helper::GitHelper.delete_remote_branch_if_exists!(current_branch)
Fastlane::Helper::GitHelper.checkout_and_pull(DEFAULT_BRANCH)
Fastlane::Helper::GitHelper.delete_local_branch_if_exists!(current_branch)
end

# @param branch_to_build [String] The branch to build. Defaults to the current git branch.
#
lane :trigger_release_build do |branch_to_build: git_branch|
buildkite_trigger_build(
buildkite_organization: 'automattic',
buildkite_pipeline: 'pocket-casts-android',
branch: branch_to_build,
pipeline_file: 'release-builds.yml'
)
end

# Builds and uploads a new build to Google Play (without releasing it)
#
# - Uses the current version to decide if this is a beta or production build
# - Builds the apps for external distribution
# - Uploads the builds to 'beta' or 'production' Play Store channel (but does not release it)
# - Creates draft Github release
#
# @param skip_confirm [Boolean] If true, avoids any interactive prompt
# @param skip_prechecks [Boolean] If true, skips android_build_preflight
# @param create_gh_release [Boolean] If true, creates a draft GitHub release
#
lane :build_and_upload_to_play_store do |skip_prechecks: false, skip_confirm: false, create_gh_release: false|
version = version_name_current
build_code = build_code_current
is_beta = beta_version?(version)
unless skip_prechecks
# Match branch names that begin with `release/`
ensure_git_branch(branch: '^release/') unless is_ci

UI.important("Building version #{version_name_current} (#{build_code_current}) for upload to Google Play Console")
UI.user_error!("Terminating as requested. Don't forget to run the remainder of this automation manually.") unless skip_confirm || UI.confirm('Do you want to continue?')

# Check local repo status
ensure_git_status_clean unless is_ci

android_build_preflight
end

release_assets = []

APPS.each do |app|
build_bundle(app: app, version: version, build_code: build_code)

aab_artifact_path = aab_artifact_path(app, version)
UI.error("Unable to find a build artifact at #{aab_artifact_path}") unless File.exist? aab_artifact_path

track = play_store_track(app: app, is_beta: is_beta)
upload_to_play_store(
**UPLOAD_TO_PLAY_STORE_COMMON_OPTIONS,
aab: aab_artifact_path,
track: track,
release_status: 'draft'
)
release_assets << aab_artifact_path

signed_apk_artifact_path = signed_apk_artifact_path(app, version)
download_universal_apk_from_google_play(
package_name: APP_PACKAGE_NAME,
version_code: version_code_for_app(app: app, version_code: build_code),
destination: signed_apk_artifact_path,
json_key: UPLOAD_TO_PLAY_STORE_JSON_KEY
)
release_assets << signed_apk_artifact_path
end

create_gh_release(version: version, prerelease: is_beta, release_assets: release_assets.compact) if create_gh_release
end

# Builds and bundles the given app
#
# @param version [String] The version to create
Expand Down Expand Up @@ -572,7 +612,7 @@ platform :android do
#
private_lane :create_gh_release do |version:, prerelease: false, release_assets: []|
create_github_release(
repository: GH_REPOSITORY,
repository: GITHUB_REPO,
version: version,
release_notes_file_path: EXTRACTED_RELEASE_NOTES_PATH,
prerelease: prerelease,
Expand All @@ -583,7 +623,7 @@ platform :android do

lane :create_backmerge_pr do |version: release_version_current|
create_release_backmerge_pull_request(
repository: GH_REPOSITORY,
repository: GITHUB_REPO,
source_branch: "release/#{version}",
default_branch: DEFAULT_BRANCH,
labels: ['Releases'],
Expand Down
Loading