Make Notarized DMG Release #1277
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Make Notarized DMG Release | |
on: | |
workflow_dispatch: | |
inputs: | |
release-type: | |
description: "Build type (product review or public release)" | |
required: true | |
default: review | |
type: choice | |
options: | |
- review | |
- release | |
create-dmg: | |
description: "Create DMG image" | |
required: true | |
default: false | |
type: boolean | |
asana-task-url: | |
description: "Asana release task URL" | |
required: false | |
type: string | |
workflow_call: | |
inputs: | |
release-type: | |
description: "Build type (product review or public release)" | |
required: true | |
default: release | |
type: string | |
create-dmg: | |
description: "Create DMG image" | |
required: true | |
default: true | |
type: boolean | |
asana-task-url: | |
description: "Asana release task URL" | |
required: false | |
type: string | |
branch: | |
description: "Branch name" | |
required: false | |
type: string | |
skip-notify: | |
description: "Skip Mattermost notification" | |
required: false | |
default: false | |
type: boolean | |
secrets: | |
APPLE_API_KEY_BASE64: | |
required: true | |
APPLE_API_KEY_ID: | |
required: true | |
APPLE_API_KEY_ISSUER: | |
required: true | |
ASANA_ACCESS_TOKEN: | |
required: true | |
AWS_ACCESS_KEY_ID: | |
required: true | |
AWS_ACCESS_KEY_ID_RELEASE_S3: | |
required: true | |
AWS_SECRET_ACCESS_KEY: | |
required: true | |
AWS_SECRET_ACCESS_KEY_RELEASE_S3: | |
required: true | |
MATCH_PASSWORD: | |
required: true | |
MM_HANDLES_BASE64: | |
required: true | |
MM_WEBHOOK_URL: | |
required: true | |
SSH_PRIVATE_KEY_FASTLANE_MATCH: | |
required: true | |
jobs: | |
export-notarized-app: | |
name: Export Notarized App | |
runs-on: macos-15-xlarge | |
outputs: | |
app-version: ${{ steps.set-outputs.outputs.app-version }} | |
app-name: ${{ steps.set-outputs.outputs.app-name }} | |
upload-to: ${{ steps.is-official-release.outputs.upload-to }} | |
test-build-s3-path: ${{ steps.upload-dsyms-to-s3-test-build.outputs.test-build-s3-path }} | |
dsym-s3-path: ${{ steps.upload-dsyms-to-s3-test-build.outputs.dsym-s3-path }} | |
dsym-url: ${{ steps.upload-dsyms-to-s3-test-build.outputs.dsym-url }} | |
env: | |
release-type: ${{ github.event.inputs.release-type || inputs.release-type }} | |
asana-task-url: ${{ github.event.inputs.asana-task-url || inputs.asana-task-url }} | |
branch: ${{ inputs.branch || github.ref_name }} | |
steps: | |
- name: Register SSH key for certificates repository access | |
uses: webfactory/[email protected] | |
with: | |
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY_FASTLANE_MATCH }} | |
- name: Check out the code | |
uses: actions/checkout@v4 | |
with: | |
submodules: recursive | |
ref: ${{ env.branch }} | |
- name: Set up fastlane | |
run: bundle install | |
- name: Sync code signing assets | |
env: | |
APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} | |
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
APPLE_API_KEY_ISSUER: ${{ secrets.APPLE_API_KEY_ISSUER }} | |
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} | |
SSH_PRIVATE_KEY_FASTLANE_MATCH: ${{ secrets.SSH_PRIVATE_KEY_FASTLANE_MATCH }} | |
run: | | |
if [[ "${{ env.release-type }}" == "release" ]]; then | |
bundle exec fastlane sync_signing_dmg_release | |
else | |
bundle exec fastlane sync_signing_dmg_review | |
fi | |
- name: Check if this is an official release build | |
id: is-official-release | |
env: | |
is-official-release: ${{ env.release-type == 'release' && (startsWith(env.branch, 'release') || startsWith(env.branch, 'hotfix')) }} | |
run: | | |
if [[ "${{ env.is-official-release }}" == "true" ]]; then | |
echo "upload-to=s3" >> $GITHUB_OUTPUT | |
echo "upload-to=s3" >> $GITHUB_ENV | |
else | |
echo "upload-to=s3testbuilds" >> $GITHUB_OUTPUT | |
echo "upload-to=s3testbuilds" >> $GITHUB_ENV | |
fi | |
- name: Select Xcode | |
run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer | |
- name: Archive and notarize the app | |
id: archive | |
env: | |
APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} | |
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
APPLE_API_KEY_ISSUER: ${{ secrets.APPLE_API_KEY_ISSUER }} | |
ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} | |
run: | | |
# import API Key from secrets | |
export APPLE_API_KEY_PATH="$RUNNER_TEMP/apple_api_key.pem" | |
echo -n "$APPLE_API_KEY_BASE64" | base64 --decode -o $APPLE_API_KEY_PATH | |
if [[ "${{ runner.debug }}" == "1" ]]; then | |
./scripts/archive.sh ${{ env.release-type }} -r | |
else | |
./scripts/archive.sh ${{ env.release-type }} | |
fi | |
- name: Set app name and version | |
id: set-outputs | |
run: | | |
echo "app-version=${{ env.app-version }}" >> $GITHUB_OUTPUT | |
echo "app-name=${{ env.app-name }}" >> $GITHUB_OUTPUT | |
echo "dsym-name=DuckDuckGo-${{ env.app-version }}-dSYM.zip" >> $GITHUB_OUTPUT | |
- name: Upload app artifact | |
uses: actions/upload-artifact@v4 | |
with: | |
name: DuckDuckGo-${{ env.release-type }}-${{ env.app-version }}.app | |
path: ${{ github.workspace }}/release/DuckDuckGo-${{ env.app-version }}.zip | |
- name: Upload dSYMs artifact | |
uses: actions/upload-artifact@v4 | |
with: | |
name: DuckDuckGo-${{ env.release-type }}-dSYM-${{ env.app-version }} | |
path: ${{ github.workspace }}/release/${{ steps.set-outputs.outputs.dsym-name }} | |
- name: Upload dSYMs to S3 | |
if: ${{ env.upload-to == 's3' }} | |
id: upload-dsyms-to-s3 | |
env: | |
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} | |
DSYM_BUCKET_NAME: ${{ vars.DSYM_BUCKET_NAME }} | |
DSYM_BUCKET_PREFIX: ${{ vars.DSYM_BUCKET_PREFIX }} | |
DSYM_NAME: ${{ steps.set-outputs.outputs.dsym-name }} | |
DSYM_LOCAL_PATH: "${{ github.workspace }}/release/${{ steps.set-outputs.outputs.dsym-name }}" | |
run: | | |
dsym_s3_path="s3://${DSYM_BUCKET_NAME}/${DSYM_BUCKET_PREFIX}/${DSYM_NAME}" | |
aws s3 cp $DSYM_LOCAL_PATH $dsym_s3_path | |
echo "dsym-s3-path=${dsym_s3_path}" >> $GITHUB_OUTPUT | |
- name: Upload dSYMs to S3 (test build) | |
if: ${{ env.upload-to == 's3testbuilds' }} | |
id: upload-dsyms-to-s3-test-build | |
env: | |
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_RELEASE_S3 }} | |
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_RELEASE_S3 }} | |
AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} | |
DSYM_NAME: ${{ steps.set-outputs.outputs.dsym-name }} | |
DSYM_LOCAL_PATH: "${{ github.workspace }}/release/${{ steps.set-outputs.outputs.dsym-name }}" | |
DSYM_URL_ROOT: ${{ vars.DMG_URL_ROOT }} | |
RELEASE_BUCKET_NAME: ${{ vars.RELEASE_BUCKET_NAME }} | |
RELEASE_BUCKET_PREFIX: ${{ vars.RELEASE_BUCKET_PREFIX }} | |
REVIEW_BUILDS_BUCKET_PREFIX: ${{ vars.REVIEW_BUILDS_BUCKET_PREFIX }} | |
run: | | |
ref_sha="$(git rev-parse --short HEAD)" | |
test_build_s3_path="s3://${RELEASE_BUCKET_NAME}/${RELEASE_BUCKET_PREFIX}/${REVIEW_BUILDS_BUCKET_PREFIX}/${ref_sha}/" | |
dsym_s3_path="${test_build_s3_path}${DSYM_NAME}" | |
echo "test-build-s3-path=${test_build_s3_path}" >> $GITHUB_OUTPUT | |
# Calculate the URL for the dSYM to report it later | |
s3_bucket_url="s3://${RELEASE_BUCKET_NAME}/${RELEASE_BUCKET_PREFIX}/" | |
dsym_url="${dsym_s3_path/#${s3_bucket_url}/${DSYM_URL_ROOT}}" # replace S3 bucket url with CDN URL | |
echo "dsym-url=${dsym_url}" >> $GITHUB_OUTPUT | |
aws s3 cp $DSYM_LOCAL_PATH $dsym_s3_path --acl public-read | |
echo "dsym-s3-path=${dsym_s3_path}" >> $GITHUB_OUTPUT | |
- name: Report success | |
if: ${{ env.upload-to == 's3' }} | |
env: | |
DSYM_S3_PATH: ${{ steps.upload-dsyms-to-s3.outputs.dsym-s3-path }} | |
TAG: ${{ env.app-version }} | |
WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} | |
run: | | |
bundle exec fastlane run asana_log_message \ | |
task_url:"${{ env.asana-task-url }}" \ | |
template_name:"debug-symbols-uploaded" \ | |
github_handle:"${{ github.actor }}" \ | |
is_scheduled_release:"${{ github.event_name == 'schedule' }}" | |
create-dmg: | |
name: Create DMG | |
needs: export-notarized-app | |
if: ${{ github.event.inputs.create-dmg == true || inputs.create-dmg == true }} | |
runs-on: macos-15 | |
env: | |
app-version: ${{ needs.export-notarized-app.outputs.app-version }} | |
app-name: ${{ needs.export-notarized-app.outputs.app-name }} | |
asana-task-url: ${{ github.event.inputs.asana-task-url || inputs.asana-task-url }} | |
upload-to: ${{ needs.export-notarized-app.outputs.upload-to }} | |
release-type: ${{ github.event.inputs.release-type || inputs.release-type }} | |
steps: | |
- name: Check out the code | |
uses: actions/checkout@v4 | |
with: | |
submodules: recursive | |
ref: ${{ inputs.branch || github.ref_name }} | |
sparse-checkout: | | |
.github | |
Gemfile | |
Gemfile.lock | |
fastlane | |
- name: Set up fastlane | |
run: bundle install | |
- name: Fetch app bundle | |
uses: actions/download-artifact@v4 | |
with: | |
name: DuckDuckGo-${{ env.release-type }}-${{ env.app-version }}.app | |
path: ${{ github.workspace }}/dmg | |
- name: Extract app bundle | |
run: | | |
ditto -xk DuckDuckGo-${{ env.app-version }}.zip . | |
rm -f DuckDuckGo-${{ env.app-version }}.zip | |
working-directory: ${{ github.workspace }}/dmg | |
- name: Install create-dmg | |
run: brew install create-dmg | |
- name: Create DMG | |
id: create-dmg | |
env: | |
GH_TOKEN: ${{ github.token }} | |
run: | | |
dmg="duckduckgo-${{ env.app-version }}.dmg" | |
curl -fLSs $(gh api https://api.github.com/repos/${{ github.repository }}/contents/scripts/assets/dmg-background.png?ref=${{ github.ref }} --jq .download_url) \ | |
--output dmg-background.png | |
# Using APFS filesystem as per https://github.com/actions/runner-images/issues/7522#issuecomment-2299918092 | |
create-dmg --volname "${{ env.app-name }}" \ | |
--filesystem APFS \ | |
--icon "${{ env.app-name }}.app" 140 160 \ | |
--background "dmg-background.png" \ | |
--window-size 600 400 \ | |
--icon-size 120 \ | |
--app-drop-link 430 160 "${dmg}" \ | |
"dmg" | |
echo "dmg=${dmg}" >> $GITHUB_OUTPUT | |
- name: Upload DMG artifact | |
uses: actions/upload-artifact@v4 | |
with: | |
name: DuckDuckGo-${{ env.release-type }}-${{ env.app-version }}.dmg | |
path: ${{ github.workspace }}/${{ steps.create-dmg.outputs.dmg }} | |
- name: Upload DMG to S3 | |
id: upload-dmg-to-s3 | |
env: | |
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_RELEASE_S3 }} | |
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_RELEASE_S3 }} | |
AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} | |
TEST_BUILD_S3_PATH: ${{ needs.export-notarized-app.outputs.test-build-s3-path }} | |
DMG_LOCAL_PATH: "${{ github.workspace }}/${{ steps.create-dmg.outputs.dmg }}" | |
DMG_URL_ROOT: ${{ vars.DMG_URL_ROOT }} | |
RELEASE_BUCKET_NAME: ${{ vars.RELEASE_BUCKET_NAME }} | |
RELEASE_BUCKET_PREFIX: ${{ vars.RELEASE_BUCKET_PREFIX }} | |
run: | | |
if [[ "${{ env.upload-to }}" == 's3' ]]; then | |
dmg_s3_path="s3://${RELEASE_BUCKET_NAME}/${RELEASE_BUCKET_PREFIX}/${{ steps.create-dmg.outputs.dmg }}" | |
else | |
dmg_s3_path="${TEST_BUILD_S3_PATH}${{ steps.create-dmg.outputs.dmg }}" | |
# Calculate the URL for the dSYM to report it later | |
s3_bucket_url="s3://${RELEASE_BUCKET_NAME}/${RELEASE_BUCKET_PREFIX}/" | |
dmg_url="${dmg_s3_path/#${s3_bucket_url}/${DMG_URL_ROOT}}" # replace S3 bucket url with CDN URL | |
echo "dmg-url=${dmg_url}" >> $GITHUB_OUTPUT | |
fi | |
aws s3 cp $DMG_LOCAL_PATH $dmg_s3_path --acl public-read | |
echo "dmg-s3-path=${dmg_s3_path}" >> $GITHUB_OUTPUT | |
- name: Report success | |
if: ${{ env.upload-to == 's3' }} | |
env: | |
DMG_URL: ${{ vars.DMG_URL_ROOT }}${{ steps.create-dmg.outputs.dmg }} | |
TAG: ${{ env.app-version }} | |
WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} | |
run: | | |
bundle exec fastlane run asana_log_message \ | |
task_url:"${{ env.asana-task-url }}" \ | |
template_name:"dmg-uploaded" \ | |
github_handle:"${{ github.actor }}" \ | |
is_scheduled_release:"${{ github.event_name == 'schedule' }}" | |
- name: Report success (test build) | |
if: ${{ env.upload-to == 's3testbuilds' }} | |
env: | |
DMG_URL: ${{ steps.upload-dmg-to-s3.outputs.dmg-url }} | |
DMG_S3_PATH: ${{ steps.upload-dmg-to-s3.outputs.dmg-s3-path }} | |
DSYM_URL: ${{ needs.export-notarized-app.outputs.dsym-url }} | |
DSYM_S3_PATH: ${{ needs.export-notarized-app.outputs.dsym-s3-path }} | |
WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} | |
run: | | |
echo "## Build Artifacts" >> $GITHUB_STEP_SUMMARY | |
echo "---" >> $GITHUB_STEP_SUMMARY | |
echo "🔗 [${DMG_S3_PATH}](${DMG_URL})" >> $GITHUB_STEP_SUMMARY | |
echo "🔗 [${DSYM_S3_PATH}](${DSYM_URL})" >> $GITHUB_STEP_SUMMARY | |
if [[ -n "${{ env.asana-task-url }}" ]]; then | |
bundle exec fastlane run asana_add_comment \ | |
task_url:"${{ env.asana-task-url }}" \ | |
comment:"New build is available at ${DMG_URL}." | |
fi | |
mattermost: | |
name: Send Mattermost message | |
needs: [export-notarized-app, create-dmg] | |
if: ${{ always() && inputs.skip-notify == false }} | |
runs-on: ubuntu-latest | |
env: | |
success: ${{ (needs.export-notarized-app.result == 'success') && (needs.create-dmg.result == 'success' || needs.create-dmg.result == 'skipped') }} | |
failure: ${{ (needs.export-notarized-app.result == 'failure') || (needs.create-dmg.result == 'failure') }} | |
steps: | |
- name: Send Mattermost message | |
if: ${{ env.success || env.failure }} # Don't execute when cancelled | |
env: | |
ASANA_TASK_URL: ${{ github.event.inputs.asana-task-url || inputs.asana-task-url }} | |
GH_TOKEN: ${{ github.token }} | |
RELEASE_TYPE: ${{ github.event.inputs.release-type || inputs.release-type }} | |
WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
run: | | |
curl -fLSs $(gh api https://api.github.com/repos/${{ github.repository }}/contents/scripts/assets/release-mm-template.json?ref=${{ github.ref }} --jq .download_url) \ | |
--output message-template.json | |
export MM_USER_HANDLE=$(base64 -d <<< ${{ secrets.MM_HANDLES_BASE64 }} | jq ".${{ github.actor }}" | tr -d '"') | |
if [[ -z "${MM_USER_HANDLE}" ]]; then | |
echo "Mattermost user handle not known for ${{ github.actor }}, skipping sending message" | |
else | |
if [[ -n "${ASANA_TASK_URL}" ]]; then | |
export ASANA_LINK=" | [:asana: Asana task](${ASANA_TASK_URL})" | |
fi | |
if [[ "${{ env.success }}" == "true" ]]; then | |
status="success" | |
else | |
status="failure" | |
fi | |
curl -s -H 'Content-type: application/json' \ | |
-d "$(envsubst < message-template.json | jq ".${status}")" \ | |
${{ secrets.MM_WEBHOOK_URL }} | |
fi |