Skip to content

[Tooling] Migrate Prototype Builds from App Center to Firebase App Distribution #24199

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

Merged
merged 5 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
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
4 changes: 2 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ gem 'dotenv'
# See failures like https://buildkite.com/automattic/wordpress-ios/builds/24053#019234f2-80a5-40f6-b55e-2f420e6483a8/3840-3915
# and https://github.com/fastlane/fastlane/pull/22256
gem 'fastlane', '~> 2.227'
gem 'fastlane-plugin-appcenter', '~> 2.1'
gem 'fastlane-plugin-firebase_app_distribution', '~> 0.10'
gem 'fastlane-plugin-sentry'
# This comment avoids typing to switch to a development version for testing.
#
# gem 'fastlane-plugin-wpmreleasetoolkit', git: 'https://github.com/wordpress-mobile/release-toolkit', ref: ''
gem 'fastlane-plugin-wpmreleasetoolkit', '~> 12.5'
gem 'fastlane-plugin-wpmreleasetoolkit', '~> 13.0'
gem 'rake'
gem 'rubocop', '~> 1.74'
gem 'rubocop-rake', '~> 0.7'
Expand Down
18 changes: 11 additions & 7 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ GEM
base64
nkf
rexml
activesupport (8.0.1)
activesupport (8.0.2)
base64
benchmark (>= 0.3)
bigdecimal
Expand Down Expand Up @@ -60,7 +60,6 @@ GEM
connection_pool (2.5.0)
cork (0.3.0)
colored2 (~> 3.1)
csv (3.3.2)
danger (9.5.1)
base64 (~> 0.2)
claide (~> 1.0)
Expand Down Expand Up @@ -167,11 +166,12 @@ GEM
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-appcenter (2.1.3)
csv
fastlane-plugin-firebase_app_distribution (0.10.0)
google-apis-firebaseappdistribution_v1 (~> 0.3.0)
google-apis-firebaseappdistribution_v1alpha (~> 0.2.0)
fastlane-plugin-sentry (1.28.0)
os (~> 1.1, >= 1.1.4)
fastlane-plugin-wpmreleasetoolkit (12.5.0)
fastlane-plugin-wpmreleasetoolkit (13.0.0)
activesupport (>= 6.1.7.1)
buildkit (~> 1.5)
chroma (= 0.2.0)
Expand Down Expand Up @@ -204,6 +204,10 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-firebaseappdistribution_v1 (0.3.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-firebaseappdistribution_v1alpha (0.2.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
Expand Down Expand Up @@ -365,9 +369,9 @@ DEPENDENCIES
danger-dangermattic (~> 1.2)
dotenv
fastlane (~> 2.227)
fastlane-plugin-appcenter (~> 2.1)
fastlane-plugin-firebase_app_distribution (~> 0.10)
fastlane-plugin-sentry
fastlane-plugin-wpmreleasetoolkit (~> 12.5)
fastlane-plugin-wpmreleasetoolkit (~> 13.0)
rake
rmagick (~> 6.1.1)
rubocop (~> 1.74)
Expand Down
1 change: 0 additions & 1 deletion WordPress/Credentials/Secrets-example.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ class ApiCredentials: NSObject {
// Other Services
static let tenorApiKey = ""
static let sentryDSN = ""
static let appCenterAppId = ""
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Once this PR lands, we might want to remove that constant from the actual Secrets-*.swift files in ~/.mobile-secrets (see .configure setup), as from a quick grep in the source code, this constant doesn't seem to be used anywhere in the codebase anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

static let encryptedLogKey = ""
static let debuggingKey = ""
static let docsBotId = ""
Expand Down
2 changes: 1 addition & 1 deletion docs/feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ case localDeveloper // debug
case a8cBranchTest // alpha

/// Beta released internally for Automattic employees
case a8cPrereleaseTesting. // internal - AppCenter / TestFlight
case a8cPrereleaseTesting. // internal - Firebase App Distribution / TestFlight

/// Production build released in the app store
case appStore // release
Expand Down
6 changes: 6 additions & 0 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,9 @@ end
def editorial_branch_name(version: release_version_current)
"release_notes/#{version}"
end

def pull_request_number
# Buildkite sets this env var to the PR number if on a PR, but to 'false' (and not nil) if not on a PR
pr_num = ENV.fetch('BUILDKITE_PULL_REQUEST', 'false')
pr_num == 'false' ? nil : Integer(pr_num)
end
2 changes: 0 additions & 2 deletions fastlane/env/project.env-example
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
INT_EXPORT_TEAM_ID=<Team id for internal distribution>
EXT_EXPORT_TEAM_ID=<Team id for public distribution>

APPCENTER_PUBLIC_ID=<AppCenter Public Id>

Comment on lines -4 to -5
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Similarly, once this PR lands, we might want to remove that env var value from the actual project.env file in ~/.mobile-secrets (see .configure setup), as it doesn't seem used anymore either.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

FASTLANE_ITC_TEAM_ID=<Team ID for AppStore Connect>

SENTRY_ORG_SLUG=<Org Slug for Sentry>
Expand Down
1 change: 0 additions & 1 deletion fastlane/env/user.env-example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ FASTLANE_USER=<Your Apple ID for fastlane>
DELIVER_USER=<Your Apple ID for fastlane>

GHHELPER_ACCESS=<GitHub access token>
APPCENTER_API_TOKEN=<AppCenter Api Token>
SENTRY_AUTH_TOKEN=<Sentry Auth Token>

BUILDKITE_TOKEN=<Buildkite Personal Access Token>
162 changes: 61 additions & 101 deletions fastlane/lanes/build.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
# frozen_string_literal: true

# Sentry
SENTRY_ORG_SLUG = 'a8c'
SENTRY_PROJECT_SLUG_WORDPRESS = 'wordpress-ios'
SENTRY_PROJECT_SLUG_JETPACK = 'jetpack-ios'
APPCENTER_OWNER_NAME = 'automattic'
APPCENTER_OWNER_TYPE = 'organization'

# Prototype Builds in Firebase App Distribution
PROTOTYPE_BUILD_XCODE_CONFIGURATION = 'Release-Alpha'
FIREBASE_APP_CONFIG_WORDPRESS = {
app_name: 'WordPress',
app_icon: ':wordpress:', # Use Buildkite emoji
app_id: '1:124902176124:ios:ff9714d0b53aac821620f9',
testers_group: 'wordpress-ios---prototype-builds'
}.freeze
FIREBASE_APP_CONFIG_JETPACK = {
app_name: 'Jetpack',
app_icon: ':jetpack:', # Use Buildkite emoji
app_id: '1:124902176124:ios:121c494b82f283ec1620f9',
testers_group: 'jetpack-ios---prototype-builds'
}.freeze

CONCURRENT_SIMULATORS = 2

# Shared options to use when invoking `build_app` (`gym`).
Expand Down Expand Up @@ -251,7 +266,7 @@
)
end

# Builds the WordPress app for a Prototype Build ("WordPress Alpha" scheme), and uploads it to App Center
# Builds the WordPress app for a Prototype Build ("WordPress Alpha" scheme), and uploads it to Firebase App Distribution
#
# @called_by CI
#
Expand All @@ -264,14 +279,13 @@
build_and_upload_prototype_build(
scheme: 'WordPress',
output_app_name: 'WordPress Alpha',
appcenter_app_name: 'WPiOS-One-Offs',
app_icon: ':wordpress:', # Use Buildkite emoji
firebase_app_config: FIREBASE_APP_CONFIG_WORDPRESS,
sentry_project_slug: SENTRY_PROJECT_SLUG_WORDPRESS,
app_identifier: 'org.wordpress.alpha'
)
end

# Builds the Jetpack app for a Prototype Build ("Jetpack" scheme), and uploads it to App Center
# Builds the Jetpack app for a Prototype Build ("Jetpack" scheme), and uploads it to Firebase App Distribution
#
# @called_by CI
#
Expand All @@ -284,8 +298,7 @@
build_and_upload_prototype_build(
scheme: 'Jetpack',
output_app_name: 'Jetpack Alpha',
appcenter_app_name: 'jetpack-installable-builds',
app_icon: ':jetpack:', # Use Buildkite emoji
firebase_app_config: FIREBASE_APP_CONFIG_JETPACK,
sentry_project_slug: SENTRY_PROJECT_SLUG_JETPACK,
app_identifier: 'com.jetpack.alpha'
)
Expand All @@ -306,49 +319,19 @@
# Helper Functions
#################################################


# Generates a build number for Prototype Builds, based on the PR number and short commit SHA1
# Builds a Prototype Build for WordPress or Jetpack, then uploads it to Firebase App Distribution and comment with a link to it on the PR.
#
# @note This function uses Buildkite-specific ENV vars
#
def generate_prototype_build_number
if ENV['BUILDKITE']
commit = ENV.fetch('BUILDKITE_COMMIT', nil)[0, 7]
branch = ENV.fetch('BUILDKITE_BRANCH', nil)
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
branch = repo.revparse('HEAD')[0, 7]

"#{branch}-#{commit}"
end
end

# Builds a Prototype Build for WordPress or Jetpack, then uploads it to App Center and comment with a link to it on the PR.
#
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/ParameterLists
def build_and_upload_prototype_build(scheme:, output_app_name:, appcenter_app_name:, app_icon:, sentry_project_slug:, app_identifier:)
configuration = 'Release-Alpha'

# Get the current build version, and update it if needed
version_config_path = File.join(PROJECT_ROOT_FOLDER, 'config', 'Version.public.xcconfig')
versions = Xcodeproj::Config.new(File.new(version_config_path)).to_hash
build_number = generate_prototype_build_number
UI.message("Updating build version to #{build_number}")
versions['VERSION_LONG'] = build_number
new_config = Xcodeproj::Config.new(versions)
new_config.save_as(Pathname.new(version_config_path))
def build_and_upload_prototype_build(scheme:, output_app_name:, firebase_app_config:, sentry_project_slug:, app_identifier:)
build_number = ENV.fetch('BUILDKITE_BUILD_NUMBER', '0')
pr_or_branch = pull_request_number&.then { |num| "PR ##{num}" } || ENV.fetch('BUILDKITE_BRANCH', nil)

# Build
build_app(
scheme: scheme,
workspace: WORKSPACE_PATH,
configuration: configuration,
configuration: PROTOTYPE_BUILD_XCODE_CONFIGURATION,
clean: true,
xcargs: { VERSION_LONG: build_number, VERSION_SHORT: pr_or_branch }.compact,
output_directory: BUILD_PRODUCTS_PATH,
output_name: output_app_name,
derived_data_path: DERIVED_DATA_PATH,
Expand All @@ -357,21 +340,8 @@ def build_and_upload_prototype_build(scheme:, output_app_name:, appcenter_app_na
export_options: { **COMMON_EXPORT_OPTIONS, method: 'enterprise' }
)

# Upload to App Center
commit = ENV.fetch('BUILDKITE_COMMIT', 'Unknown')
pr = ENV.fetch('BUILDKITE_PULL_REQUEST', nil)
release_notes = <<~NOTES
- Branch: `#{ENV.fetch('BUILDKITE_BRANCH', 'Unknown')}`\n
- Commit: [#{commit[0...7]}](https://github.com/#{GITHUB_REPO}/commit/#{commit})\n
- Pull Request: [##{pr}](https://github.com/#{GITHUB_REPO}/pull/#{pr})\n
NOTES

upload_build_to_app_center(
name: appcenter_app_name,
file: lane_context[SharedValues::IPA_OUTPUT_PATH],
dsym: lane_context[SharedValues::DSYM_OUTPUT_PATH],
release_notes: release_notes,
distribute_to_everyone: false
upload_build_to_firebase_app_distribution(
firebase_app_config: firebase_app_config
)

# Upload dSYMs to Sentry
Expand All @@ -388,33 +358,7 @@ def build_and_upload_prototype_build(scheme:, output_app_name:, appcenter_app_na
build_version: build_number,
app_identifier: app_identifier
)

# Post PR Comment
comment_body = prototype_build_details_comment(
app_display_name: output_app_name,
app_icon: app_icon,
app_center_org_name: APPCENTER_OWNER_NAME,
metadata: { Configuration: configuration },
fold: true
)

comment_on_pr(
project: GITHUB_REPO,
pr_number: Integer(ENV.fetch('BUILDKITE_PULL_REQUEST', nil)),
reuse_identifier: "prototype-build-link-#{appcenter_app_name}",
body: comment_body
)

# Attach version information as Buildkite metadata and annotation
appcenter_id = lane_context.dig(SharedValues::APPCENTER_BUILD_INFORMATION, 'id')
metadata = versions.merge(build_type: 'Prototype', 'appcenter:id': appcenter_id)
buildkite_metadata(set: metadata)
appcenter_install_url = "https://install.appcenter.ms/orgs/#{APPCENTER_OWNER_NAME}/apps/#{appcenter_app_name}/releases/#{appcenter_id}"
list = metadata.map { |k, v| " - **#{k}**: #{v}" }.join("\n")
buildkite_annotate(context: "appcenter-info-#{output_app_name}", style: 'info', message: "#{output_app_name} [App Center Build](#{appcenter_install_url}) Info:\n\n#{list}")
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/ParameterLists

def inject_buildkite_analytics_environment(xctestrun_path:)
require 'plist'
Expand Down Expand Up @@ -467,23 +411,39 @@ def send_slack_message(message:, channel: '#build-and-ship')
)
end

def upload_build_to_app_center(
name:,
file:,
dsym:,
release_notes:,
distribute_to_everyone:
)
appcenter_upload(
api_token: get_required_env('APPCENTER_API_TOKEN'),
owner_name: APPCENTER_OWNER_NAME,
owner_type: APPCENTER_OWNER_TYPE,
app_name: name,
file: file,
dsym: dsym,
# Uploads a build to Firebase App Distribution and post the corresponding PR comment
#
# @param [Hash<Symbol, String>] firebase_app_config A hash with the app name as the key and the Firebase app ID and testers group as the value
# Typically one of FIREBASE_APP_CONFIG_WORDPRESS or FIREBASE_APP_CONFIG_JETPACK
#
def upload_build_to_firebase_app_distribution(firebase_app_config:)
release_notes = <<~NOTES
Pull Request: ##{pull_request_number || 'N/A'}
Branch: `#{ENV.fetch('BUILDKITE_BRANCH', 'N/A')}`
Commit: #{ENV.fetch('BUILDKITE_COMMIT', 'N/A')[0...7]}
NOTES

firebase_app_distribution(
app: firebase_app_config[:app_id],
service_credentials_json_data: get_required_env('FIREBASE_APP_DISTRIBUTION_ACCOUNT_KEY'),
release_notes: release_notes,
destinations: distribute_to_everyone ? '*' : 'Collaborators',
notify_testers: false
groups: firebase_app_config[:testers_group]
)

return if pull_request_number.nil?

# PR Comment
comment_body = prototype_build_details_comment(
app_display_name: firebase_app_config[:app_name],
app_icon: firebase_app_config[:app_icon],
metadata: { Configuration: PROTOTYPE_BUILD_XCODE_CONFIGURATION },
fold: true
)
comment_on_pr(
project: GITHUB_REPO,
pr_number: pull_request_number,
reuse_identifier: "prototype-build-link-#{firebase_app_config[:app_id]}",
body: comment_body
)
end

Expand Down