1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[bre-1089] mac desktop publish automation using fastlane (#16091)

* Use Fastlane to publish to Apple App Store

* Publish MacOS build number as artifact

* Download and source build number from artifact

* Refactor Fastlane file to use already existing builds in TestFlight

* fastfile changes, release workflow changes, gitignore addition

* reorder steps to after dist dir is created

* resolve pathing issue

* upload step path fix

* make comments more clear

* enable phased rollout, add auto-submit checkbox

* move logic from release to publish workflow

* configure dry run properly for MAS

* edit file for testing

* workflow testing

* verbose logging for debugging

* update to look at releases

* remove verbose flag for next test

* add verbose logging back

* disable precheck

* hardcode app v for test

* hardcode app v for testing

* additional test

* log build numbers

* remove testing values, prep for draft PR

* flip metadata bool for testing

* comment out branch check

* hardcode locales

* add metadata and locales change

* lane change

* more logging for finding build

* address logs feedback

* edit_live false

* testing

* extra logging from apple api

* testing

* workaround for attaching build attempt

* workaround patch update

* simplify and retest skip metadata true

* turn precheck true

* remove autosubmit checkbox, add live edit true for testing release notes formatting

* re-org dispatch, rename dir to release_notes, flip live edit to false

* another formatting attempt

* additional formatting changes

* account for double space, add dash to beginning

* different formatting approach

* format test

* simplified notes formatting test, double line after each period

* proper formatting

* rename file for rust linter

* remove testing comments

* remove default string from notes, logic to check for empty release notes in mas_publish, formatting

* add validation logic after publishing

---------

Co-authored-by: Micaiah Martin <github@sourcecodemt.com>
This commit is contained in:
aj-bw
2025-08-28 10:53:17 -04:00
committed by GitHub
parent 7bc04e2218
commit 3202b56614
5 changed files with 304 additions and 4 deletions

View File

@@ -1299,6 +1299,7 @@ jobs:
$package = Get-Content -Raw -Path electron-builder.json | ConvertFrom-Json
$package | Add-Member -MemberType NoteProperty -Name buildVersion -Value "$env:BUILD_NUMBER"
$package | ConvertTo-Json -Depth 32 | Set-Content -Path electron-builder.json
Write-Output "### MacOS App Store build number: $env:BUILD_NUMBER"
- name: Install Node dependencies
@@ -1374,6 +1375,23 @@ jobs:
CSC_FOR_PULL_REQUEST: true
run: npm run pack:mac:mas
- name: Create MacOS App Store build number artifact
shell: pwsh
env:
BUILD_NUMBER: ${{ needs.setup.outputs.build_number }}
run: |
$buildInfo = @{
buildNumber = $env:BUILD_NUMBER
}
$buildInfo | ConvertTo-Json | Set-Content -Path dist/macos-build-number.json
- name: Upload MacOS App Store build number artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: macos-build-number.json
path: apps/desktop/dist/macos-build-number.json
if-no-files-found: error
- name: Upload .pkg artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:

View File

@@ -18,10 +18,15 @@ on:
type: string
default: latest
electron_rollout_percentage:
description: 'Staged Rollout Percentage for Electron'
required: true
description: 'Staged Rollout Percentage for Electron (ignored if Electron publish disabled)'
required: false
default: '10'
type: string
electron_publish:
description: 'Publish to Electron (auto-updater)'
required: true
default: true
type: boolean
snap_publish:
description: 'Publish to Snap store'
required: true
@@ -32,6 +37,15 @@ on:
required: true
default: true
type: boolean
mas_publish:
description: 'Publish to Mac App Store'
required: true
default: true
type: boolean
release_notes:
description: 'Release Notes'
required: false
type: string
jobs:
setup:
@@ -71,7 +85,7 @@ jobs:
echo "Release Version: ${{ inputs.version }}"
echo "version=${{ inputs.version }}"
$TAG_NAME="desktop-v${{ inputs.version }}"
TAG_NAME="desktop-v${{ inputs.version }}"
echo "Tag name: $TAG_NAME"
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
@@ -109,6 +123,7 @@ jobs:
name: Electron blob publish
runs-on: ubuntu-22.04
needs: setup
if: inputs.electron_publish
permissions:
contents: read
packages: read
@@ -292,6 +307,92 @@ jobs:
run: choco push --source=https://push.chocolatey.org/
working-directory: apps/desktop/dist
mas:
name: Deploy Mac App Store
runs-on: macos-15
needs: setup
permissions:
contents: read
id-token: write
if: inputs.mas_publish
env:
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
_RELEASE_TAG: ${{ needs.setup.outputs.tag_name }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate release notes for MAS
if: inputs.mas_publish && (inputs.release_notes == '' || inputs.release_notes == null)
run: |
echo "❌ Release notes are required when publishing to Mac App Store"
echo "Please provide release notes using the 'Release Notes' input field"
exit 1
- name: Download MacOS App Store build number
working-directory: apps/desktop
run: wget https://github.com/bitwarden/clients/releases/download/${{ env._RELEASE_TAG }}/macos-build-number.json
- name: Setup Ruby and Install Fastlane
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
with:
ruby-version: '3.0'
bundler-cache: false
working-directory: apps/desktop
- name: Install Fastlane
working-directory: apps/desktop
run: gem install fastlane
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-clients
secrets: "APP-STORE-CONNECT-AUTH-KEY,APP-STORE-CONNECT-TEAM-ISSUER"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Publish to App Store
env:
APP_STORE_CONNECT_TEAM_ISSUER: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-TEAM-ISSUER }}
APP_STORE_CONNECT_AUTH_KEY: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }}
working-directory: apps/desktop
run: |
BUILD_NUMBER=$(jq -r '.buildNumber' macos-build-number.json)
CHANGELOG="${{ inputs.release_notes }}"
IS_DRY_RUN="${{ inputs.publish_type == 'Dry Run' }}"
if [ "$IS_DRY_RUN" = "true" ]; then
echo "🧪 DRY RUN MODE - Testing without actual App Store submission"
echo "📦 Would publish build $BUILD_NUMBER to Mac App Store"
else
echo "🚀 PRODUCTION MODE - Publishing to Mac App Store"
echo "📦 Publishing build $BUILD_NUMBER to Mac App Store"
fi
echo "📝 Release notes (${#CHANGELOG} chars): ${CHANGELOG:0:100}..."
# Validate changelog length (App Store limit is 4000 chars)
if [ ${#CHANGELOG} -gt 4000 ]; then
echo "❌ Release notes too long: ${#CHANGELOG} characters (max 4000)"
exit 1
fi
fastlane publish --verbose \
app_version:"${{ env._PKG_VERSION }}" \
build_number:$BUILD_NUMBER \
changelog:"$CHANGELOG" \
dry_run:$IS_DRY_RUN
update-deployment:
name: Update Deployment Status
runs-on: ubuntu-22.04
@@ -300,6 +401,7 @@ jobs:
- electron-blob
- snap
- choco
- mas
permissions:
contents: read
deployments: write

View File

@@ -124,7 +124,8 @@ jobs:
apps/desktop/artifacts/Bitwarden-${{ env.PKG_VERSION }}-universal.pkg.archive,
apps/desktop/artifacts/${{ env.RELEASE_CHANNEL }}.yml,
apps/desktop/artifacts/${{ env.RELEASE_CHANNEL }}-linux.yml,
apps/desktop/artifacts/${{ env.RELEASE_CHANNEL }}-mac.yml"
apps/desktop/artifacts/${{ env.RELEASE_CHANNEL }}-mac.yml,
apps/desktop/artifacts/macos-build-number.json"
commit: ${{ github.sha }}
tag: desktop-v${{ env.PKG_VERSION }}
name: Desktop v${{ env.PKG_VERSION }}

View File

@@ -3,3 +3,8 @@ dist-safari/
*.env
PlugIns/safari.appex/
xcuserdata/
# Fastlane
fastlane/report.xml
fastlane/README.md
fastlane/release_notes/

View File

@@ -0,0 +1,174 @@
default_platform(:mac)
# Static configuration for the Mac desktop app
require 'json'
require 'base64'
APP_CONFIG = {
app_identifier: "com.bitwarden.desktop",
release_notes_path: "fastlane/release_notes",
locales: ["ca", "zh-Hans", "zh-Hant", "da", "nl-NL", "en-US", "fi", "fr-FR", "de-DE", "id", "it", "ja", "ko", "no", "pt-PT", "pt-BR", "ru", "es-ES", "es-MX", "sv", "tr", "vi", "en-GB", "th"]
}
platform :mac do
desc "Prepare release notes from changelog"
lane :prepare_release_notes do |options|
changelog = options[:changelog] || "Bug fixes and improvements"
# Split on periods and format with bullet points
# Try different formatting approaches for App Store Connect
formatted_changelog = changelog
.split('.')
.map(&:strip)
.reject(&:empty?)
.map { |item| "• #{item}" }
.join("\n")
UI.message("Original changelog: #{changelog[0,100]}#{changelog.length > 100 ? '...' : ''}")
UI.message("Formatted changelog: #{formatted_changelog[0,100]}#{formatted_changelog.length > 100 ? '...' : ''}")
# Create release notes directories and files for all locales
APP_CONFIG[:locales].each do |locale|
dir = "release_notes/#{locale}"
FileUtils.mkdir_p(dir)
File.write("#{dir}/release_notes.txt", formatted_changelog)
UI.message("Creating release notes for #{locale}")
end
# Create release notes hash for deliver
notes = APP_CONFIG[:locales].each_with_object({}) do |locale, hash|
file_path = "release_notes/#{locale}/release_notes.txt"
if File.exist?(file_path)
hash[locale] = File.read(file_path)
else
UI.important("No release notes found for #{locale} at #{file_path}, skipping.")
end
end
UI.success("✅ Prepared release notes for #{APP_CONFIG[:locales].count} locales")
notes
end
desc "Display configuration information"
lane :show_config do |options|
build_number = (options[:build_number] || ENV["BUILD_NUMBER"]).to_s.strip
app_version = (options[:app_version] || ENV["APP_VERSION"]).to_s.strip
UI.message("📦 App ID: #{APP_CONFIG[:app_identifier]}")
UI.message("🏷️ Version: #{app_version.empty? ? '(not set)' : app_version}")
UI.message("🔢 Build Number: #{build_number}")
UI.message("🌍 Locales: #{APP_CONFIG[:locales].count}")
end
desc "Publish desktop to the Mac App Store"
lane :publish do |options|
build_number = (options[:build_number] || ENV["BUILD_NUMBER"]).to_s.strip
app_version = (options[:app_version] || ENV["APP_VERSION"]).to_s.strip
changelog = options[:changelog] || "Bug fixes and improvements"
is_dry_run = options[:dry_run] == "true" || options[:dry_run] == true
if is_dry_run
UI.header("🧪 DRY RUN: Testing Bitwarden Desktop App Store submission")
else
UI.header("🚀 Publishing Bitwarden Desktop to Mac App Store")
end
# Show configuration info
show_config(build_number: build_number, app_version: app_version)
# Validate app_version
UI.user_error!("❌ APP_VERSION is required") if app_version.nil? || app_version.empty?
# Validate build_number
UI.user_error!("❌ BUILD_NUMBER is required") if build_number.nil? || build_number.empty?
# Prepare release notes for all locales
notes = prepare_release_notes(changelog: changelog)
if is_dry_run
UI.important("🧪 DRY RUN MODE - Skipping actual App Store Connect submission")
UI.message("✅ Validation passed")
UI.message("✅ Release notes prepared for #{APP_CONFIG[:locales].count} locales")
UI.message("✅ Release notes: #{changelog[0,100]}#{changelog.length > 100 ? '...' : ''}")
UI.success("🎯 DRY RUN COMPLETE - Everything looks ready for production!")
next # Use 'next' instead of 'return' in fastlane lanes
end
# Set up App Store Connect API
app_store_connect_api_key(
key_id: "6TV9MKN3GP",
issuer_id: ENV["APP_STORE_CONNECT_TEAM_ISSUER"],
key_content: Base64.encode64(ENV["APP_STORE_CONNECT_AUTH_KEY"]),
is_key_content_base64: true
)
UI.message("📝 Using release notes for #{notes.keys.count} locales")
UI.message("🎯 Publishing version #{app_version} with build #{build_number}")
# Upload to App Store Connect
deliver(
platform: "osx",
app_identifier: APP_CONFIG[:app_identifier],
app_version: app_version,
build_number: build_number,
metadata_path: "metadata",
skip_binary_upload: true,
skip_screenshots: true,
skip_metadata: false, # Enable metadata upload to include release notes
release_notes: notes,
edit_live: false,
submit_for_review: true, # if this is false, the build number does not attach to the draft
phased_release: true, # Enable 7-day phased rollout
precheck_include_in_app_purchases: false,
run_precheck_before_submit: false,
automatic_release: true,
force: true
)
# Verify submission in App Store Connect (skip in dry run mode)
unless is_dry_run
UI.message("⏳ Waiting 60 seconds for App Store Connect to process submission...")
sleep(60)
UI.message("🔍 Verifying submission in App Store Connect...")
# Find the app
app = Spaceship::ConnectAPI::App.find(APP_CONFIG[:app_identifier])
UI.user_error!("❌ App not found in App Store Connect") if app.nil?
# Find the version we just submitted
versions = app.get_app_store_versions
target_version = nil
versions.each do |v|
if v.version_string == app_version
target_version = v
break
end
end
UI.user_error!("❌ Version #{app_version} not found in App Store Connect after submission") if target_version.nil?
UI.success("✅ Version #{app_version} found in App Store Connect")
UI.message("📊 Current status: #{target_version.app_store_state}")
# Validate build attachment
if target_version.build.nil?
UI.user_error!("❌ No build attached to version #{app_version}")
elsif target_version.build.version != build_number
UI.user_error!("❌ Wrong build attached: found #{target_version.build.version}, expected #{build_number}")
else
UI.success("✅ Build #{build_number} correctly attached to version #{app_version}")
end
# Check submission status
valid_states = ["WAITING_FOR_REVIEW", "IN_REVIEW"]
unless valid_states.include?(target_version.app_store_state)
UI.user_error!("❌ Unexpected submission state: #{target_version.app_store_state}. Expected one of: #{valid_states.join(', ')}")
end
UI.success("🎉 Verification complete: Version #{app_version} with build #{build_number} successfully submitted!")
else
UI.success("🧪 DRY RUN: Skipping App Store Connect verification")
end
end
end