Skip to content

Make Notarized DMG Release #1277

Make Notarized DMG Release

Make Notarized DMG Release #1277

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