Publish DMG Release #140
Workflow file for this run
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: Publish DMG Release | |
on: | |
workflow_dispatch: | |
inputs: | |
asana-task-url: | |
description: "Asana release task URL" | |
required: true | |
type: string | |
tag: | |
description: "Tag to publish" | |
required: true | |
type: string | |
release-type: | |
description: "Release type" | |
required: true | |
type: choice | |
options: | |
- internal | |
- public | |
- hotfix | |
workflow_call: | |
inputs: | |
asana-task-url: | |
description: "Asana release task URL" | |
required: true | |
type: string | |
branch: | |
description: "Branch name" | |
required: false | |
type: string | |
secrets: | |
ASANA_ACCESS_TOKEN: | |
required: true | |
AWS_ACCESS_KEY_ID_RELEASE_S3: | |
required: true | |
AWS_SECRET_ACCESS_KEY_RELEASE_S3: | |
required: true | |
GHA_ELEVATED_PERMISSIONS_TOKEN: | |
required: true | |
SPARKLE_PRIVATE_KEY: | |
required: true | |
jobs: | |
# This is only run for public and hotfix releases, so only when it's triggered manually. | |
# Internal release has been tagged as part of code_freeze or bump_interal_release workflows | |
tag-public-release: | |
name: Tag public release | |
# Run if release-type is provided (not empty) and is not internal | |
if: github.event.inputs.release-type != null && github.event.inputs.release-type != 'internal' | |
uses: ./.github/workflows/tag_release.yml | |
with: | |
asana-task-url: ${{ inputs.asana-task-url || github.event.inputs.asana-task-url }} | |
branch: ${{ github.ref_name }} | |
prerelease: false | |
secrets: | |
ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} | |
GHA_ELEVATED_PERMISSIONS_TOKEN: ${{ secrets.GHA_ELEVATED_PERMISSIONS_TOKEN }} | |
publish-to-sparkle: | |
name: Publish a release to Sparkle | |
env: | |
RELEASE_TYPE: ${{ github.event.inputs.release-type || 'internal' }} | |
SPARKLE_DIR: ${{ github.workspace }}/sparkle-updates | |
asana-task-url: ${{ inputs.asana-task-url || github.event.inputs.asana-task-url }} | |
needs: [tag-public-release] | |
# Allow to run even if the tag-public-release job was skipped (e.g. for internal releases) | |
# or failed (for public releases or hotfixes), because tagging doesn't block publishing the release | |
if: always() | |
runs-on: macos-14-xlarge | |
timeout-minutes: 10 | |
steps: | |
- name: Download tag artifact | |
id: download-tag | |
# Only look for the tag artifact when the tag input is empty | |
if: github.event.inputs.tag == null || github.event.inputs.tag == '' | |
continue-on-error: true | |
uses: actions/download-artifact@v4 | |
with: | |
name: tag | |
path: .github | |
- name: Set tag variable | |
run: | | |
if [[ "${{ steps.download-tag.outcome }}" == 'success' ]]; then | |
echo "TAG=$(<.github/tag)" >> $GITHUB_ENV | |
else | |
echo "TAG=${{ github.event.inputs.tag }}" >> $GITHUB_ENV | |
fi | |
- name: Verify the tag | |
id: verify-tag | |
run: | | |
tag_regex='^[0-9]+\.[0-9]+\.[0-9]+-[0-9]+$' | |
if [[ ! "$TAG" =~ $tag_regex ]]; then | |
echo "::error::The provided tag ($TAG) has incorrect format (attempted to match ${tag_regex})." | |
exit 1 | |
fi | |
echo "release-version=${TAG//-/.}" >> $GITHUB_OUTPUT | |
# Always check out main first, because the release branch might have been deleted (for public releases) | |
- name: Check out the code | |
uses: actions/checkout@v4 | |
with: | |
fetch-depth: 0 # Fetch all history and tags in order to extract Asana task URLs from git log | |
submodules: recursive | |
ref: main | |
- name: Check out the branch if it exists | |
env: | |
branch: ${{ inputs.branch || github.ref_name }} | |
run: | | |
if [[ -z "${branch}" ]] || git ls-remote --exit-code --heads origin "${branch}"; then | |
echo "::notice::Checking out ${branch} branch." | |
git checkout "${branch}" | |
else | |
echo "::notice::Branch ${branch} doesn't exist on the remote repository, staying on main." | |
fi | |
- name: Select Xcode | |
run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer | |
- name: Extract Asana Task ID | |
id: task-id | |
uses: ./.github/actions/asana-extract-task-id | |
with: | |
task-url: ${{ env.asana-task-url }} | |
- name: Fetch and validate release notes | |
env: | |
TASK_ID: ${{ steps.task-id.outputs.task-id }} | |
ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} | |
run: | | |
curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}?opt_fields=notes" \ | |
-H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ | |
| jq -r .data.notes > release_task_content.txt | |
raw_release_notes="$(./scripts/extract_release_notes.sh -r < release_task_content.txt)" | |
if [[ ${#raw_release_notes} == 0 || "$raw_release_notes" == *"<-- Add release notes here -->"* ]]; then | |
echo "::error::Release notes are empty or contain a placeholder. Please add release notes to the Asana task and restart the workflow." | |
exit 1 | |
fi | |
./scripts/extract_release_notes.sh < release_task_content.txt > release_notes.html | |
echo "RELEASE_NOTES_FILE=release_notes.html" >> $GITHUB_ENV | |
- name: Set up Sparkle tools | |
env: | |
SPARKLE_URL: https://github.com/sparkle-project/Sparkle/releases/download/${{ vars.SPARKLE_VERSION }}/Sparkle-${{ vars.SPARKLE_VERSION }}.tar.xz | |
run: | | |
curl -fLSs $SPARKLE_URL | tar xJ bin | |
echo "${{ github.workspace }}/bin" >> $GITHUB_PATH | |
- name: Fetch DMG | |
id: fetch-dmg | |
env: | |
DMG_NAME: duckduckgo-${{ steps.verify-tag.outputs.release-version }}.dmg | |
run: | | |
# Public release doesn't need fetching a DMG (it's already uploaded to S3) | |
if [[ "${RELEASE_TYPE}" != 'public' ]]; then | |
DMG_URL="${{ vars.DMG_URL_ROOT }}${DMG_NAME}" | |
curl -fLSs -o "$DMG_NAME" "$DMG_URL" | |
fi | |
echo "dmg-name=$DMG_NAME" >> $GITHUB_OUTPUT | |
echo "dmg-path=$DMG_NAME" >> $GITHUB_OUTPUT | |
- name: Generate appcast | |
id: appcast | |
env: | |
DMG_PATH: ${{ steps.fetch-dmg.outputs.dmg-path }} | |
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} | |
VERSION: ${{ steps.verify-tag.outputs.release-version }} | |
run: | | |
echo -n "$SPARKLE_PRIVATE_KEY" > sparkle_private_key | |
chmod 600 sparkle_private_key | |
case "$RELEASE_TYPE" in | |
"internal") | |
./scripts/appcast_manager/appcastManager.swift \ | |
--release-to-internal-channel \ | |
--dmg ${DMG_PATH} \ | |
--release-notes-html release_notes.html \ | |
--key sparkle_private_key | |
;; | |
"public") | |
./scripts/appcast_manager/appcastManager.swift \ | |
--release-to-public-channel \ | |
--version ${VERSION} \ | |
--release-notes-html release_notes.html \ | |
--key sparkle_private_key | |
;; | |
"hotfix") | |
./scripts/appcast_manager/appcastManager.swift \ | |
--release-hotfix-to-public-channel \ | |
--dmg ${DMG_PATH} \ | |
--release-notes-html release_notes.html \ | |
--key sparkle_private_key | |
;; | |
*) | |
;; | |
esac | |
appcast_patch_name="appcast2-${VERSION}.patch" | |
mv -f ${{ env.SPARKLE_DIR }}/appcast_diff.txt ${{ env.SPARKLE_DIR }}/${appcast_patch_name} | |
echo "appcast-patch-name=${appcast_patch_name}" >> $GITHUB_OUTPUT | |
- name: Upload appcast diff artifact | |
uses: actions/upload-artifact@v4 | |
with: | |
name: ${{ steps.appcast.outputs.appcast-patch-name }} | |
path: ${{ env.SPARKLE_DIR }}/${{ steps.appcast.outputs.appcast-patch-name }} | |
- name: Upload to S3 | |
id: upload | |
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 }} | |
VERSION: ${{ steps.verify-tag.outputs.release-version }} | |
run: | | |
# Back up existing appcast2.xml | |
OLD_APPCAST_NAME=appcast2_old.xml | |
echo "OLD_APPCAST_NAME=${OLD_APPCAST_NAME}" >> $GITHUB_ENV | |
curl -fLSs "${{ vars.DMG_URL_ROOT }}appcast2.xml" --output "${OLD_APPCAST_NAME}" | |
# Upload files to S3 | |
if [[ "${RELEASE_TYPE}" == "internal" ]]; then | |
./scripts/upload_to_s3/upload_to_s3.sh --run --force | |
else | |
./scripts/upload_to_s3/upload_to_s3.sh --run --force --overwrite-duckduckgo-dmg "${VERSION}" | |
fi | |
if [[ -f "${{ env.SPARKLE_DIR }}/uploaded_files_list.txt" ]]; then | |
echo "FILES_UPLOADED=$(awk '{ print "<li><code>"$1"</code></li>"; }' < ${{ env.SPARKLE_DIR }}/uploaded_files_list.txt | tr '\n' ' ')" >> $GITHUB_ENV | |
else | |
echo "FILES_UPLOADED='No files uploaded.'" >> $GITHUB_ENV | |
fi | |
- name: Update Asana for the release | |
id: update-asana | |
if: ${{ env.RELEASE_TYPE != 'internal' }} | |
continue-on-error: true | |
env: | |
ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} | |
BRANCH: ${{ github.ref_name }} | |
run: | | |
version="$(cut -d '/' -f 2 <<< "$BRANCH")" | |
./scripts/update_asana_for_release.sh public \ | |
${{ steps.task-id.outputs.task-id }} \ | |
${{ vars.MACOS_APP_BOARD_DONE_SECTION_ID }} \ | |
"${version}" \ | |
announcement-task-contents.txt | |
echo "announcement-task-contents=$(sed 's/"/\\"/g' < announcement-task-contents.txt)" >> $GITHUB_OUTPUT | |
- name: Get tasks since last internal release | |
id: get-tasks-since-last-internal-release | |
if: contains(github.event.inputs.release-type, '') || github.event.inputs.release-type == 'internal' | |
env: | |
GH_TOKEN: ${{ github.token }} | |
run: | | |
tasks="$(./scripts/update_asana_for_release.sh list-tasks-in-last-internal-release)" | |
echo "tasks=$tasks" >> $GITHUB_OUTPUT | |
- name: Set common environment variables | |
if: always() | |
env: | |
DMG_NAME: ${{ steps.fetch-dmg.outputs.dmg-name }} | |
run: | | |
echo "APPCAST_PATCH_NAME=${{ steps.appcast.outputs.appcast-patch-name }}" >> $GITHUB_ENV | |
echo "DMG_NAME=${DMG_NAME}" >> $GITHUB_ENV | |
echo "DMG_URL=${{ vars.DMG_URL_ROOT }}${DMG_NAME}" >> $GITHUB_ENV | |
echo "RELEASE_BUCKET_NAME=${{ vars.RELEASE_BUCKET_NAME }}" >> $GITHUB_ENV | |
echo "RELEASE_BUCKET_PREFIX=${{ vars.RELEASE_BUCKET_PREFIX }}" >> $GITHUB_ENV | |
echo "RELEASE_TASK_ID=${{ steps.task-id.outputs.task-id }}" >> $GITHUB_ENV | |
echo "TASKS_SINCE_LAST_INTERNAL_RELEASE=${{ steps.get-tasks-since-last-internal-release.outputs.tasks }}" >> $GITHUB_ENV | |
echo "VERSION=${{ steps.verify-tag.outputs.release-version }}" >> $GITHUB_ENV | |
echo "WORKFLOW_URL=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_ENV | |
- name: Set up Asana templates | |
if: always() | |
id: asana-templates | |
run: | | |
if [[ ${{ steps.upload.outcome }} == "success" ]]; then | |
if [[ "${RELEASE_TYPE}" == "internal" ]]; then | |
echo "task-template=validate-check-for-updates-internal" >> $GITHUB_OUTPUT | |
echo "comment-template=validate-check-for-updates-internal" >> $GITHUB_OUTPUT | |
if [[ -n "${TASKS_SINCE_LAST_INTERNAL_RELEASE}" ]]; then | |
echo "release-task-comment-template=internal-release-complete-with-tasks" >> $GITHUB_OUTPUT | |
else | |
echo "release-task-comment-template=internal-release-complete" >> $GITHUB_OUTPUT | |
fi | |
else | |
echo "task-template=validate-check-for-updates-public" >> $GITHUB_OUTPUT | |
echo "comment-template=validate-check-for-updates-public" >> $GITHUB_OUTPUT | |
echo "release-task-comment-template=public-release-complete" >> $GITHUB_OUTPUT | |
fi | |
else | |
echo "task-template=appcast-failed-${RELEASE_TYPE}" >> $GITHUB_OUTPUT | |
echo "comment-template=appcast-failed-${RELEASE_TYPE}" >> $GITHUB_OUTPUT | |
fi | |
- name: Create Asana task | |
id: create-task | |
if: always() | |
uses: ./.github/actions/asana-create-action-item | |
with: | |
access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} | |
release-task-url: ${{ env.asana-task-url }} | |
template-name: ${{ steps.asana-templates.outputs.task-template }} | |
- name: Create Asana task to handle Asana paperwork | |
id: create-asana-paperwork-task | |
if: ${{ steps.update-asana.outcome == 'failure' }} | |
uses: ./.github/actions/asana-create-action-item | |
env: | |
APP_BOARD_ASANA_PROJECT_ID: ${{ vars.MACOS_APP_BOARD_ASANA_PROJECT_ID }} | |
with: | |
access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} | |
release-task-url: ${{ env.asana-task-url }} | |
template-name: update-asana-for-public-release | |
- name: Create Asana task to announce the release | |
id: create-announcement-task | |
if: ${{ env.RELEASE_TYPE != 'internal' }} | |
uses: ./.github/actions/asana-create-action-item | |
env: | |
html-notes: ${{ steps.update-asana.outputs.announcement-task-contents }} | |
with: | |
access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} | |
html-notes: ${{ env.html-notes }} | |
release-task-url: ${{ env.asana-task-url }} | |
task-name: Announce the release to the company | |
- name: Upload patch to the Asana task | |
id: upload-patch | |
if: success() | |
uses: ./.github/actions/asana-upload | |
with: | |
access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} | |
file-name: ${{ env.SPARKLE_DIR }}/${{ steps.appcast.outputs.appcast-patch-name }} | |
task-id: ${{ steps.create-task.outputs.new-task-id }} | |
- name: Upload old appcast file to the Asana task | |
id: upload-old-appcast | |
if: success() | |
uses: ./.github/actions/asana-upload | |
with: | |
access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} | |
file-name: ${{ env.OLD_APPCAST_NAME }} | |
task-id: ${{ steps.create-task.outputs.new-task-id }} | |
- name: Upload release notes to the Asana task | |
id: upload-release-notes | |
if: success() | |
uses: ./.github/actions/asana-upload | |
with: | |
access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} | |
file-name: ${{ env.RELEASE_NOTES_FILE }} | |
task-id: ${{ steps.create-task.outputs.new-task-id }} | |
- name: Report status | |
if: always() | |
uses: ./.github/actions/asana-log-message | |
env: | |
ANNOUNCEMENT_TASK_ID: ${{ steps.create-announcement-task.outputs.new-task-id }} | |
ASSIGNEE_ID: ${{ steps.create-task.outputs.assignee-id }} | |
TASK_ID: ${{ steps.create-task.outputs.new-task-id }} | |
with: | |
access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} | |
task-url: ${{ env.asana-task-url }} | |
template-name: ${{ steps.asana-templates.outputs.comment-template }} | |
- name: Add a comment to the release task | |
if: success() | |
uses: ./.github/actions/asana-add-comment | |
with: | |
access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} | |
task-url: ${{ env.asana-task-url }} | |
template-name: ${{ steps.asana-templates.outputs.release-task-comment-template }} | |
# This is only run for public and hotfix releases | |
create-variants: | |
name: Create DMG Variants | |
needs: [publish-to-sparkle] | |
# Run if release-type is provided (not empty) an is not internal | |
if: github.event.inputs.release-type != null && github.event.inputs.release-type != 'internal' | |
uses: duckduckgo/macos-browser/.github/workflows/create_variants.yml@main | |
secrets: | |
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} | |
P12_PASSWORD: ${{ secrets.P12_PASSWORD }} | |
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} | |
APPSTORE_CI_PROVISION_PROFILE_BASE64: ${{ secrets.APPSTORE_CI_PROVISION_PROFILE_BASE64 }} | |
CI_PROVISION_PROFILE_BASE64: ${{ secrets.CI_PROVISION_PROFILE_BASE64 }} | |
DBP_AGENT_APPSTORE_CI_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_APPSTORE_CI_PROVISION_PROFILE_BASE64 }} | |
DBP_AGENT_CI_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_CI_PROVISION_PROFILE_BASE64 }} | |
DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64 }} | |
DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64 }} | |
INTEGRATION_TESTS_APPSTORE_CI_PROVISION_PROFILE_BASE64: ${{ secrets.INTEGRATION_TESTS_APPSTORE_CI_PROVISION_PROFILE_BASE64 }} | |
INTEGRATION_TESTS_CI_PROVISION_PROFILE_BASE64: ${{ secrets.INTEGRATION_TESTS_CI_PROVISION_PROFILE_BASE64 }} | |
NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64 }} | |
NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64 }} | |
NETP_NOTIFICATIONS_CI_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_NOTIFICATIONS_CI_PROVISION_PROFILE_BASE64 }} | |
NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64 }} | |
NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64 }} | |
NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64 }} | |
NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64 }} | |
RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.RELEASE_PROVISION_PROFILE_BASE64 }} | |
REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.REVIEW_PROVISION_PROFILE_BASE64 }} | |
UNIT_TESTS_APPSTORE_CI_PROVISION_PROFILE_BASE64: ${{ secrets.UNIT_TESTS_APPSTORE_CI_PROVISION_PROFILE_BASE64 }} | |
UNIT_TESTS_CI_PROVISION_PROFILE_BASE64: ${{ secrets.UNIT_TESTS_CI_PROVISION_PROFILE_BASE64 }} | |
VPN_APPEX_APPSTORE_CI_PROVISION_PROFILE_BASE64: ${{ secrets.VPN_APPEX_APPSTORE_CI_PROVISION_PROFILE_BASE64 }} | |
VPN_APP_APPSTORE_CI_PROVISION_PROFILE_BASE64: ${{ secrets.VPN_APP_APPSTORE_CI_PROVISION_PROFILE_BASE64 }} | |
VPN_APP_CI_PROVISION_PROFILE_BASE64: ${{ secrets.VPN_APP_CI_PROVISION_PROFILE_BASE64 }} | |
VPN_PROXY_EXTENSION_CI_PROVISION_PROFILE_BASE64: ${{ secrets.VPN_PROXY_EXTENSION_CI_PROVISION_PROFILE_BASE64 }} | |
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 }} | |
MM_HANDLES_BASE64: ${{ secrets.MM_HANDLES_BASE64 }} | |
MM_WEBHOOK_URL: ${{ secrets.MM_WEBHOOK_URL }} | |
AWS_ACCESS_KEY_ID_RELEASE_S3: ${{ secrets.AWS_ACCESS_KEY_ID_RELEASE_S3 }} | |
AWS_SECRET_ACCESS_KEY_RELEASE_S3: ${{ secrets.AWS_SECRET_ACCESS_KEY_RELEASE_S3 }} |