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:
18
.github/workflows/build-desktop.yml
vendored
18
.github/workflows/build-desktop.yml
vendored
@@ -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:
|
||||
|
||||
108
.github/workflows/publish-desktop.yml
vendored
108
.github/workflows/publish-desktop.yml
vendored
@@ -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
|
||||
|
||||
3
.github/workflows/release-desktop.yml
vendored
3
.github/workflows/release-desktop.yml
vendored
@@ -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 }}
|
||||
|
||||
5
apps/desktop/.gitignore
vendored
5
apps/desktop/.gitignore
vendored
@@ -3,3 +3,8 @@ dist-safari/
|
||||
*.env
|
||||
PlugIns/safari.appex/
|
||||
xcuserdata/
|
||||
|
||||
# Fastlane
|
||||
fastlane/report.xml
|
||||
fastlane/README.md
|
||||
fastlane/release_notes/
|
||||
|
||||
174
apps/desktop/fastlane/fastfile
Normal file
174
apps/desktop/fastlane/fastfile
Normal 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
|
||||
Reference in New Issue
Block a user