name: Generate Sparkle Appcast description: Creates Sparkle Appcast for a new release and generates delta patch files inputs: sparklePrivateKey: description: Private key used for Sparkle signing required: true baseImage: description: Disk image to base the Sparkle Appcast on required: true channel: description: Sparkle Appcast channel to use required: false default: stable count: description: Number of old versions to generate deltas for required: false default: '1' urlPrefix: description: URL prefix to use for Sparkle downloads required: true customTitle: description: Custom title to use for Appcast required: false customLink: description: Custom link to use for Appcast required: false runs: using: composite steps: - name: Check Runner Operating System 🏃‍♂️ if: runner.os != 'macOS' shell: bash run: | : Check Runner Operating System 🏃‍♂️ echo '::notice::sparkle-appcast action requires a macOS-based runner.' exit 2 - name: Install Dependencies shell: zsh --no-rcs --errexit --pipefail {0} run: | : Install Dependencies if (( ${+RUNNER_DEBUG} )) setopt XTRACE print ::group::Install Dependencies brew update brew install --quiet coreutils pandoc print ::endgroup:: - name: Set Up Sparkle ✨ shell: zsh --no-rcs --errexit --pipefail {0} run: | : Set Up Sparkle ✨ if (( ${+RUNNER_DEBUG} )) setopt XTRACE local version local base_url local hash IFS=';' read -r version base_url hash <<< \ "$(jq -r ' .configurePresets[] | select(.name=="dependencies") | .vendor["obsproject.com/obs-studio"].tools.sparkle | {version, baseUrl, hash} | join(";") ' CMakePresets.json)" mkdir -p Sparkle && pushd Sparkle curl -s -L -O "${base_url}/${version}/Sparkle-${version}.tar.xz" local checksum="$(sha256sum Sparkle-${version}.tar.xz | cut -d " " -f 1)" if [[ ${hash} != ${checksum} ]] { print "::error::Sparkle-${version}.tar.xz checksum mismatch: ${checksum} (expected: ${hash})" exit 2 } tar -xJf "Sparkle-${version}.tar.xz" popd mkdir builds mkdir -p output/appcasts/stable - name: Download Builds 📥 id: builds shell: zsh --no-rcs --errexit --pipefail {0} run: | : Download Builds 📥 if (( ${+RUNNER_DEBUG} )) setopt XTRACE pushd builds local image_location=(${{ inputs.baseImage }}) hdiutil attach -readonly -noverify -noautoopen -plist ${image_location} > result.plist local -i num_entities=$(( $(plutil -extract system-entities raw -- result.plist) - 1 )) local keys local mount_point for i ({0..${num_entities}}) { keys=($(plutil -extract system-entities.${i} raw -- result.plist)) if [[ ${keys} == *mount-point* ]] { mount_point=$(plutil -extract system-entities.${i}.mount-point raw -- result.plist) break } } local feed_url local info_plist=(${mount_point}/*.app/Contents/Info.plist) if [[ -f ${info_plist} ]] { feed_url=$(plutil -extract SUFeedURL raw -- ${info_plist}) } else { print '::error:: No Info.plist file found in specified disk image.' hdiutil detach ${mount_point} exit 2 } print "feedUrl=${feed_url}" >> $GITHUB_OUTPUT hdiutil detach ${mount_point} curl -s -L -O ${feed_url} # The Xpath Xplained: # # //rss/channel/item - Select every node, under a # node, under a node, # which: # [*...] - Has a child node, which # [local-name()='channel'] - Has the local name "channel" # (required to match the # namespaced sparkle:channel node), # which in turn has # [text()=''] - A text node that contains the # content of inputs.channel # /enclosure/@url - Then select the "url" attribute of # every node under # these matching nodes local -a artifacts=($(\ xmllint \ -xpath "//rss/channel/item[*[local-name()='channel'][text()='${{ inputs.channel }}']]/enclosure/@url" \ ${feed_url:t} \ | sed -n 's/.*url="\(.*\)"/\1/p') ) local url local file_name for i ({1..${{ inputs.count }}}) { url="${artifacts[${i}]}" file_name="${artifacts[${i}]:t}" curl -s -L -O ${url} } mv ${{ inputs.baseImage }} ${PWD} rm -rf - result.plist popd - name: Prepare Release Notes 📝 shell: zsh --no-rcs --errexit --pipefail {0} run: | : Prepare Release Notes 📝 if (( ${+RUNNER_DEBUG} )) setopt XTRACE git tag -l --format='%(contents:subject)' ${GITHUB_REF_NAME} > notes.rst echo '###################################################' >> notes.rst echo '' >> notes.rst git tag -l --format='%(contents:body)' ${GITHUB_REF_NAME} >> notes.rst pandoc -f rst -t html notes.rst -o output/appcasts/notes_${{ inputs.channel }}.html - name: Generate Appcast 🎙️ shell: zsh --no-rcs --errexit --pipefail {0} run: | : Generate Appcast 🎙️ if (( ${+RUNNER_DEBUG} )) setopt XTRACE print -n '${{ inputs.sparklePrivateKey }}' >> eddsa_private.key local feed_url='${{ steps.builds.outputs.feedUrl }}' local arch=${${${(s:_:)feed_url:t}[2]}//x86/x86_64} local sparkle_options=( --verbose --ed-key-file eddsa_private.key --download-url-prefix '${{ inputs.urlPrefix }}/' --full-release-notes-url "${feed_url//updates_*/notes_${{ inputs.channel }}.html}" --maximum-versions 0 --maximum-deltas '${{ inputs.count }}' --channel '${{ inputs.channel }}' ) if [[ '${{ inputs.channel }}' == 'stable' ]] { sparkle_options+=(--phased-rollout-interval 86400) } Sparkle/bin/generate_appcast \ ${sparkle_options} \ builds local -a deltas=(builds/*.delta(N)) if (( #deltas )) { mkdir -p output/sparkle_deltas/${arch} mv ${deltas} output/sparkle_deltas/${arch} } mv builds/*.xml output/appcasts - name: Adjust Appcast 🎙️ shell: zsh --no-rcs --errexit --pipefail {0} run: | : Adjust Appcast 🎙️ if (( ${+RUNNER_DEBUG} )) setopt XTRACE local feed_url='${{ steps.builds.outputs.feedUrl }}' local arch=${${${(s:_:)feed_url:t}[2]}//x86/x86_64} local -a appcasts=(output/appcasts/*_v2.xml) local adjusted for appcast (${appcasts}) { adjusted="${appcast//.xml/-adjusted.xml}" xsltproc \ --stringparam pDeltaUrl "${{ inputs.urlPrefix }}/sparkle_deltas/${arch}/" \ --stringparam pSparkleUrl '${{ inputs.urlPrefix }}/' \ --stringparam pCustomTitle '${{ inputs.customTitle }}' \ --stringparam pCustomLink '${{ inputs.customLink }}' \ -o ${adjusted} ${GITHUB_ACTION_PATH}/appcast_adjust.xslt ${appcast} xmllint --format ${adjusted} >! ${appcast} rm ${adjusted} } - name: Create Legacy Appcast 📟 shell: zsh --no-rcs --errexit --pipefail {0} run: | : Create Legacy Appcast 📟 if (( ${+RUNNER_DEBUG} )) setopt XTRACE local -a appcasts=(output/appcasts/*_v2.xml) local legacy local feed_url='${{ steps.builds.outputs.feedUrl }}' local arch=${${${(s:_:)feed_url:t}[2]}//x86/x86_64} for appcast (${appcasts}) { legacy="${appcast//.xml/-legacy.xml}" xsltproc \ -o ${legacy} ${GITHUB_ACTION_PATH}/appcast_legacy.xslt ${appcast} xmllint --format ${legacy} >! output/appcasts/stable/${${appcast:t}//_v2.xml/.xml} if [[ ${arch} == x86_64 ]] xmllint --format ${legacy} >! output/appcasts/stable/${${appcast:t}//_x86_64_v2.xml/.xml} rm ${legacy} }