Code and comments ("EXPLANATION:") on how my nightly builds work (it's close to the release builds). With the discussion you're already having maybe it's enough to gain some insight from this instead of blindly replicating my code. It's been in use for a long time now and can surely be made better. Helper files in subsequent emails. I don't intend for anyone to copy this out, I will supply the VM where all of this is included already - this is to add to the discussion you're already having.
This script is run every X minutes from cron. Triggering a similar script on commit as a CI build would be way better.
#!/bin/bash# pulls latest Hatari sources, builds and copies to known external URL
if [ ! -z "$(/opt/local/bin/pidof automated_build.sh)" ]; then
  exit 0
fi
export SDKROOT="/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk"export PATH="$PATH":/opt/local/bin/
cd /Users/troed/dev/hatari
# here to give the network some time to wake up
sleep 5
# if nothing has changed, do nothing
git remote update
git log ..@{u} > ../change.tmp
if [ -s ../change.tmp ]; then
  xcodebuild -sdk "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk" -UseModernBuildSystem=NO clean -project Hatari.xcodeproj > ../hatari_nightlies/build.log
  # has the project regenerated and lost our build script?
  # EXPLANATION: When the Xcode project is re-generated the manually inserted post buildscript is lost. This code re-inserts it. This is a dirty fix that hopefully can be avoided by someone who does this better than me :) This is the script that runs otool and fixes files and paths that are packaged together for a redistributable binary.
  found=`cat "Hatari.xcodeproj/project.pbxproj" | grep script2 | wc -l`
  if [ ${found} -eq 0 ]; then
    a="/* Begin PBXShellScriptBuildPhase section */"
    b=$(cat script2)
    c='/* Build configuration list for PBXNativeTarget "hatari" */;'
    d="                                3B81ADC925A4C8110093582A /* ShellScript */,"
    rm temp
    while IFS= read -r line; do
      input="$line"
      if [[ $line = *"$c" ]]; then
        echo "$input" >> temp
        IFS= read -r line
        echo "$line" >> temp
        IFS= read -r line
        echo "$line" >> temp
        IFS= read -r line
        echo "$line" >> temp
        IFS= read -r line
        echo "$line" >> temp
        IFS= read -r line
        echo "$line" >> temp
        echo "$d" >> temp
      else
        output=${input/$a/"$b"}
        echo "$output" >> temp
      fi
    done < "Hatari.xcodeproj/project.pbxproj"
    mv temp Hatari.xcodeproj/project.pbxproj
  fi
  today=`date '+%Y%m%d_%H%M%S'`
  # EXPLANATION: I have local changes made to a few files that I need to keep.
  git stash
  git checkout master
  git pull
  git stash pop
  ret=$?
  cp ../change.tmp ../change.tm2
# add newline to end of change.log if there is none
# EXPLANATION: Nightly builds include a change.log with commit comments
  [ -n "$(tail -c1 ../hatari_nightlies/change.log)" ] && echo >> ../hatari_nightlies/change.log
  cat ../hatari_nightlies/change.log >> ../change.tm2
  cp ../change.tm2 ../hatari_nightlies/change.log
  if [ ${ret} -eq 0 ]; then
    bsuccess=false
# make sure __DATE__ and __TIME__ are up2date
# EXPLANATION: Doesn't build otherwise
    touch src/main.c
    xcodebuild -sdk "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk" -UseModernBuildSystem=NO -scheme ALL_BUILD -project Hatari.xcodeproj >> ../hatari_nightlies/build.log
    if [ $? -eq 0 ]; then
      # final sanity check that our build script indeed has run
      # EXPLANATION: Dirty hack, counting whether otool has run and now points to included files instead of dynamically linked from the build system
      patched=`otool -L src/Release/Hatari.app/Contents/MacOS/hatari | grep "@executable_path" | wc -l`
      if [ ${patched} -eq 5 ]; then
        bsuccess=true
      fi
    fi
    if [ "$bsuccess" = true ]; then
      # this needs to be hatari in lowercase, CMake generates 'Hatari'
      defaults write /Users/troed/dev/hatari/src/Release/Hatari.app/Contents/Info.plist CFBundleExecutable -string "hatari"
      # EXPLANATION: Well, otherwise we can't codesign ... 
      security unlock-keychain -p <password> login.keychain
      export CODESIGN_ALLOCATE="/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/codesign_allocate"
      codesign --force --options=runtime --sign "Developer ID Application: Troed Sangberg (467URBKK99)" src/Release/libFloppy.a >> ../hatari_nightlies/build.log
      codesign --force --deep --options=runtime --sign "Developer ID Application: Troed Sangberg (467URBKK99)" --entitlements /Users/troed/dev/hatari/hatari.app.xcent src/Release/hatari.app >> ../hatari_nightlies/build.log
      dest=Hatari.$today.dmg
      cd /tmp/
      /Users/troed/npm-global/lib/node_modules/appdmg/bin/appdmg.js /Users/troed/dev/hataridev.json $dest >> /Users/troed/dev/hatari_nightlies/build.log
# notarize our dmg
      uuid=$(/usr/bin/xcrun altool --notarize-app --primary-bundle-id "org.tuxfamily.Hatari" --username "troed@xxxxxxxx" --password <password> --file $dest --output-format "xml" | xmllint --xpath "string(//plist/dict/dict/string)" -) 
# need to loop here waiting for notarizing to succeed
      count=0
      inprogress=true
      success=false
      while [ "${count}" -lt 20 ] && [ "${inprogress}" == true ]; do
        sleep 60
        result=$(/usr/bin/xcrun altool --notarization-info "$uuid" --username "troed@xxxxxxxx" --password <password> | grep "Status:")         echo ${result} >> /Users/troed/dev/hatari_nightlies/build.log
        words=($result)
        if [[ "${words[1]}" == "success" ]]; then
          inprogress=false
          success=true
        elif [[ "${words[1]}" == "invalid" ]]; then
          inprogress=false
        else
          ((count++))
        fi
      done
      if [ "$success" = true ]; then
        /usr/bin/xcrun stapler staple $dest
        zip -r $dest.zip $dest
        mv $dest.zip '/Users/troed/dev/hatari_macos/'
        rm -rf /tmp/$dest*
        /Users/troed/dev/notify.sh "Hatari macOS successfully built"
      else
        # signing failed, zip up the log
        dest=Hatari.${today}.build_failed.log
        zip -j ${dest}.zip /Users/troed/dev/hatari_nightlies/build.log
        mv ${dest}.zip '/Users/troed/dev/hatari_macos/'
        /Users/troed/dev/notify.sh "Hatari macOS build failure"
      fi
    else
      # build failed, zip up the log
      dest=Hatari.${today}.build_failed.log
      zip -j ${dest}.zip /Users/troed/dev/hatari_nightlies/build.log
      mv ${dest}.zip '/Users/troed/dev/hatari_macos/'
      /Users/troed/dev/notify.sh "Hatari macOS build failure"
    fi
  else
    # pull failed with unresolved changes
    touch '/Users/troed/dev/hatari_macos/Hatari.'${today}'.build_failed'
    /Users/troed/dev/notify.sh "Hatari macOS merge failure"
  fi
fi
# delete all but the 10 latest .zip files
cd "/Users/troed/dev/hatari_macos/"
ls -tp *.zip | grep -v '/$' | tail -n +11 | tr '\n' '\0' | xargs -0 rm --