Build Non-App Store Release #35
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: 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 }}\"" |