Skip to content

Build Non-App Store Release #35

Build Non-App Store Release

Build Non-App Store Release #35

Workflow file for this run

name: Draft Cleepp Release
on:
create:
workflow_dispatch:
branches:
- forkmain
inputs:
releaseVersion:
description: "Version to title release with (like: 1.0rc3)"
type: string
required: false
isPrerelease:
description: "Prerelease"
type: boolean
required: true
default: true
env:
projectfile: Maccy.xcodeproj
productname: Cleepp
buildscheme: Cleepp
bundlename: Cleepp.app
jobs:
build:
name: Build Cleepp Non-AppStore Variant and Draft Release
runs-on: macos-14 # required to use Xcode 15, "macos-latest" is actually older and uses Xcode 14
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Patch Xcode 15.3
uses: jpmhouston/patch-package-resolved@v1
# this fixes a mysterious build failure
# xcodebuild: error: Could not resolve package dependencies:
# Package.resolved file is corrupted or malformed; fix or delete the file
# to continue: unknown 'PinsStorage' version '3'
# should probably remove this when upgrading the "runs-on" platform
- name: Install pandoc
# pandoc is used by sparkle step and by one of the xcode project's build rules
run: |
:
brew update
brew install pandoc
- name: Validate
id: version
run: |
:
echo "- Extract version from project"
version=$(xcodebuild -scheme "${{ env.buildscheme }}" -configuration Release \
-project "${{ env.projectfile }}" -showBuildSettings \
| sed -nr 's/^.*MARKETING_VERSION = (.*)$/\1/p') 2>/dev/null
if [[ -z $version ]] ; then
echo "::error::Unable to determine a version number for the current state of the xcode project"
exit 1
elif [[ -z "${{ inputs.releaseVersion }}" || $version == "${{ inputs.releaseVersion }}" ]] ; then
echo "- Build version is $version"
else
echo "- Build version is $version but overriding with ${{ inputs.releaseVersion }} for release & file names"
version="${{ inputs.releaseVersion }}"
fi
releaseName="${{ env.productname }} $version"
echo "- Parsing trigger"
if [[ "${{ github.event_name }}" == workflow_dispatch ]] ; then
if git show-ref --tags v$version --quiet ; then
tag="v$version"
echo "- Will build and draft release \"$releaseName\" with matching tag \"$tag\""
else
echo "- Will build and draft untagged release \"$releaseName\" because triggered manually and no tag \"v$version\" was found"
echo "git show-ref --tags"
git show-ref --tags
echo "::notice::Will skip notarizing and updating sparkle appcast"
fi
elif [[ "${{ github.ref }}" == refs/tags/* ]] ; then
ref="${{ github.ref }}"
tag="${ref:10}" # magic number 10 being the length of "refs/tags/", stripping that to leave just the tag name
echo "- Will build and draft release \"$releaseName\" because triggered by pushed tag \"$tag\""
else
echo "::error::Not triggered manually or by a tag (github.event_name == ${{ github.event_name }}, gihub.ref == ${{ github.ref }})"
exit 1
fi
echo "version=$version" >> $GITHUB_OUTPUT
echo "releaseName=$releaseName" >> $GITHUB_OUTPUT
if [[ -n $tag ]] ; then
echo "tag=$tag" >> $GITHUB_OUTPUT
fi
- name: Release Notes
id: notes
run: |
:
echo "- Collecting release notes"
allNotesFilename=releasenotes.md
currentNotesFilename="${{ steps.version.outputs.releaseName }}.md"
if [[ ! -f $allNotesFilename ]] ; then
echo "::error::Release notes file is missing"
exit 1
fi
echo -n "" > "$currentNotesFilename"
thisversion=''
prevversion=''
while read line || [[ -n $line ]] ; do
if [[ -z $thisversion ]]; then
thisversion=$(echo $line | sed -n -e '/^# / s/^# //p')
if [[ -n $thisversion && $thisversion != "${{ steps.version.outputs.version }}" ]] ; then
echo "::warning::Version $thisversion at the top of the release notes doesn't match build version ${{ steps.version.outputs.version }}"
break
fi
continue
fi
prevversion=$(echo $line | sed -n -e '/^# / s/^# //p')
if [[ -n $prevversion ]] ; then
break
fi
echo $line >> "$currentNotesFilename"
done < "$allNotesFilename"
# sed command removes initial and trailing blank lines, don't ask me how it works
# from https://unix.stackexchange.com/a/552195
cat "$currentNotesFilename" | sed -e '/./,$!d' -e :a -e '/^\n*$/{$d;N;ba' -e '}' \
| tee "$currentNotesFilename" >/dev/null
numlines=$(wc -l "$currentNotesFilename" | cut -w -f2)
if [[ $numlines -gt 0 ]] ; then
echo "- Saving $numlines lines of release notes to \"$currentNotesFilename\""
else
echo "- Saving placeholder release notes to \"$currentNotesFilename\""
echo "Release notes unavailable at this time" > "$currentNotesFilename"
fi
echo "notesMarkdown=$currentNotesFilename" >> $GITHUB_OUTPUT
- name: Build
run: |
:
xcodebuild -version
# requires that env.projectfile is the name of the .xcodeproj, env.buildscheme is
# a valid build scheme, and and env.bundlename is name of the produced .app
set -o pipefail && xcodebuild clean build analyze \
-scheme "${{ env.buildscheme }}" -configuration Release \
-project "${{ env.projectfile }}" -derivedDataPath . | \
xcbeautify --renderer github-actions --preserve-unbeautified
- name: "Codesign app bundle"
run: |
:
test -d "Build/Products/Release/${{ env.bundlename }}" || exit 1
ls -ald "Build/Products/Release/"*.app
exit 0 # !!! until the above env vars are correctly defined
if [[ -z "${{ secrets.PROD_MACOS_CERTIFICATE }}" ]] ; then
echo "::error::Secret PROD_MACOS_CERTIFICATE not defined"
exit 1
fi
if [[ -z "${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}" ]] ; then
echo "::error::Secret PROD_MACOS_CI_KEYCHAIN_PWD not defined"
exit 1
fi
if [[ -z "${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}" ]] ; then
echo "::error::Secret PROD_MACOS_CERTIFICATE_PWD not defined"
exit 1
fi
if [[ -z "${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}" ]] ; then
echo "::error::Secret PROD_MACOS_CERTIFICATE_NAME not defined"
exit 1
fi
# Turn our base64-encoded certificate back to a regular .p12 file
echo "- Base64-encode certificate to make \"certificate.p12\""
echo "${{ secrets.PROD_MACOS_CERTIFICATE }}" | base64 --decode > certificate.p12
# We need to create a new keychain, otherwise using the certificate will prompt
# with a UI dialog asking for the certificate password, which we can't
# use in a headless CI environment
echo "- Create unlocked keychain \"build.keychain\""
security create-keychain -p "${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}" build.keychain
echo "- Import \"certificate.p12\" into \"build.keychain\""
security import certificate.p12 -k build.keychain \
-P "${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}" build.keychain
# We finally codesign our app bundle, specifying the Hardened runtime option
echo "- Sign"
xcrun codesign --force -s "${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}" \
--options runtime "Build/Products/Release/${{ env.app }}" -v
- name: "Notarize app bundle"
if: success() && steps.version.outputs.tag
run: |
:
test -d "Build/Products/Release/${{ env.bundlename }}" || exit 1
exit 0 # !!! until the above env vars are correctly defined
if [[ -z "${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}" ]] ; then
echo "::error::Secret PROD_MACOS_NOTARIZATION_APPLE_ID not defined"
exit 1
fi
if [[ -z "${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}" ]] ; then
echo "::error::Secret PROD_MACOS_NOTARIZATION_TEAM_ID not defined"
exit 1
fi
if [[ -z "${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}" ]] ; then
echo "::error::Secret PROD_MACOS_NOTARIZATION_PWD not defined"
exit 1
fi
# Store the notarization credentials so that we can prevent a UI password dialog
# from blocking the CI
echo "- Create keychain profile"
xcrun notarytool store-credentials "notarytool-profile" \
--apple-id "${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}" \
--team-id "${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}" \
--password "${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}"
# We can't notarize an app bundle directly, but we need to compress it as an archive.
# Therefore, we create a zip file containing our app bundle, so that we can send it to the
# notarization service
echo "- Create temp notarization archive"
ditto -c -k --keepParent "Build/Products/Release/${{ env.bundlename }}" "notarization.zip"
# Here we send the notarization request to the Apple's Notarization service, waiting for the result.
# This typically takes a few seconds inside a CI environment, but it might take more depending on the App
# characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
# you're curious
echo "- Notarize"
xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait
# Finally, we need to "attach the staple" to our executable, which will allow our app to be
# validated by macOS even when an internet connection is not available.
echo "- Attach staple"
xcrun stapler staple "Build/Products/Release/${{ env.bundlename }}"
- name: Build Zip File
id: zip
run: |
:
test -d "Build/Products/Release/${{ env.bundlename }}" || exit 1
readmeFileName="/${{ env.productname }} version ${{ steps.version.outputs.version }} read me.rtf"
archiveFileName="${{ steps.version.outputs.releaseName }}.zip"
archiveDir="Build/Products/Release/${{ steps.version.outputs.releaseName }}"
archiveFile="Build/Products/Release/$archiveFileName"
echo "- Assemble built app and readme file \"$readmeFileName\" in ${archiveDir}"
mkdir "$archiveDir"
cp "Designs/Cleepp/Cleepp download read me.rtf" "$archiveDir/$readmeFileName"
mv "Build/Products/Release/${{ env.bundlename }}" "$archiveDir"
echo "- Create \"${archiveFileName}\" from contents of ${archiveDir}"
ditto -c -k --sequesterRsrc --keepParent "$archiveDir" "${archiveFile}"
echo "file=${archiveFile}" >> $GITHUB_OUTPUT
- name: Setup Sparkle
if: success() && steps.version.outputs.tag
uses: jozefizso/setup-sparkle@v1
with:
version: 2.6.0
- name: Generate Sparkle appcast.xml
if: success() && steps.version.outputs.tag
run: |
:
if [[ -z "${{ secrets.SPARKLE_PRIVATE_KEY }}" ]] ; then
echo "::error::Secret SPARKLE_PRIVATE_KEY not defined"
exit 1
fi
if ! command -v pandoc >/dev/null 2>&1 || ! command -v generate_appcast >/dev/null 2>&1 ; then
echo "::error::Required executables not all found: pandoc, generate_appcast"
exit 1
fi
currentNotesFilename="${{ steps.notes.outputs.notesMarkdown }}"
htmlNotesFilename="${{ steps.version.outputs.releaseName }}.html" # name must be this for generate-appcast
htmlTemplateFilename=htmlnotestemplate.html
echo "- Converting release notes to html"
echo '$body$' > "$htmlTemplateFilename"
pandoc --standalone --template "$htmlTemplateFilename" --metadata title="Release Notes" \
"$currentNotesFilename" > "$htmlNotesFilename"
echo "- Updating appcast"
echo "::add-mask::$PRIVATE_KEY_SECRET"
echo "{{ secrets.SPARKLE_PRIVATE_KEY }}" | generate_appcast --ed-key-file - \
--link https://github.com/${{ github.repository }}/releases \
--download-url-prefix https://github.com/${{ github.repository }}/releases/download/v${{ steps.version.outputs.version }}/ \
--embed-release-notes -o appcast.xml \
Build/Products/Release/
- name: Commit appcast.xml
if: success() && steps.version.outputs.tag
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: Automated Change to appcast.xml
file_pattern: 'appcast.xml'
status_options: '--untracked-files=no'
- name: Draft Untagged Release
if: success() && steps.version.outputs.tag == ''
uses: softprops/action-gh-release@v2
with:
name: "${{ steps.version.outputs.releaseName }}"
draft: true
prerelease: ${{ inputs.isPrerelease }}
body_path: "${{ steps.notes.outputs.notesMarkdown }}"
files: "${{ steps.zip.outputs.file }}"
fail_on_unmatched_files: true
- name: Draft Tagged Release
if: success() && steps.version.outputs.tag
uses: softprops/action-gh-release@v2
with:
name: "${{ steps.version.outputs.releaseName }}"
tag_name: "${{ steps.version.outputs.tag }}"
draft: true
prerelease: ${{ inputs.isPrerelease }}
body_path: "${{ steps.notes.outputs.notesMarkdown }}"
files: "${{ steps.zip.outputs.file }}"
fail_on_unmatched_files: true
- name: Fin
run: |
:
echo "::notice::Release \"${{ steps.version.outputs.releaseName }}\" draft created continaing \"${{ steps.zip.outputs.file }}\""