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 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 <item> node, under a
        #                                   <channel> node, under a <rss> 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()='<channel>']            - A text node that contains the
        #                                   content of inputs.channel
        # /enclosure/@url                 - Then select the "url" attribute of
        #                                   every <enclosure> node under
        #                                   these matching <item> 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}
        }
