name: Update_Apps

# Controls when the workflow will run
on:
  # run at 00:00 UTC every Wednesday
  schedule:
    - cron: "0 0 * * 3"
  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
# this workflow contains 3 jobs that run sequentially: get-latest-app-versions, test-updated-apps, and create-pr
# get-latest-app-versions gets the latests app versions and modifies the working directory of the repository with those changes. then it uploads that working directory of the git repository as an artifact for use in later jobs
# test-updated-apps runs a matrix of testcases where all updated apps from the first job are tested for successful installation/uninstallation in the working directory of the git repoistory downloaded from the artifact
# create-pr then uses the output from get-latest-app-versions and test-updated-apps to finalize a PR where only apps that passed all testcases are updated and any that fail have a descriptive error written in the PR comment
jobs:
  get-latest-app-versions:
    name: Get Latest App Versions
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    outputs:
      UPDATED_APPS: ${{ steps.updated.outputs.UPDATED_APPS }}
      FAILED_APPS: ${{ steps.failed.outputs.FAILED_APPS }}

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - uses: actions/checkout@v3

      # Runs a set of commands using the runners shell
      - name: Run app update scripts
        env:
          GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
        run: |
          # print user info
          echo $USER $USERNAME $(id) $(whoami)
          
          #source pi-apps functions

          #export all functions and variables
          set -a
          #make DIRECTORY equal to GITHUB_WORKSPACE, for subscripts and api functions
          DIRECTORY=$GITHUB_WORKSPACE
          source $GITHUB_WORKSPACE/api

          #add special functions
          get_release() {
            curl -s --header "Authorization: token $GH_PERSONAL_ACCESS_TOKEN" "https://api.github.com/repos/$1/releases/latest" | jq -r '.tag_name' | sed s/^v//g
          }

          get_release_raw() {
            curl -s --header "Authorization: token $GH_PERSONAL_ACCESS_TOKEN" "https://api.github.com/repos/$1/releases/latest" | jq -r '.tag_name'
          }

          get_prerelease() {
            curl -s --header "Authorization: token $GH_PERSONAL_ACCESS_TOKEN" "https://api.github.com/repos/$1/releases" | jq -r 'map(select(.prerelease)) | first | .tag_name' | sed s/^v//g
          }

          get_prerelease_raw() {
            curl -s --header "Authorization: token $GH_PERSONAL_ACCESS_TOKEN" "https://api.github.com/repos/$1/releases" | jq -r 'map(select(.prerelease)) | first | .tag_name'
          }

          function validate_url(){
            if command wget --timeout=5 -q --spider "$1"; then
              return 0
            else
              return 1
            fi
          }

          #stop exporting functions
          set +a

          #make sure all update scripts are executable
          chmod +x $GITHUB_WORKSPACE/.github/workflows/updates/*.sh
          cd $GITHUB_WORKSPACE
          apps=( .github/workflows/updates/*.sh )
          for app_directory in "${apps[@]}"; do
            echo
            #make sure we are still in the main workspace (incase an update script left off elsewhere)
            cd $GITHUB_WORKSPACE
            export app_name="$(echo ${app_directory%.*} | sed 's:.*/::')"
            echo "$app_name"
            status "Checking $app_name for updates"
            # move to app folder
            cd "$GITHUB_WORKSPACE/apps/$app_name"
            # run app update script
            "$GITHUB_WORKSPACE/$app_directory"
          done
          cd

      - name: Output updated apps
        id: updated
        run: |
          if test -f /tmp/updated_apps; then
            sort -u /tmp/updated_apps > /tmp/updated_apps_sorted
            echo "UPDATED_APPS<<EOF" >> $GITHUB_OUTPUT
            cat /tmp/updated_apps_sorted >> $GITHUB_OUTPUT
            echo "EOF" >> $GITHUB_OUTPUT
          fi
      - name: Output failed apps
        id: failed
        run: |
          if test -f /tmp/failed_apps; then
            echo "FAILED_APPS<<EOF" >> $GITHUB_OUTPUT
            cat /tmp/failed_apps | sed '0~1 a\\' >> $GITHUB_OUTPUT
            echo "EOF" >> $GITHUB_OUTPUT
          fi
      - name: Tar GITHUB_WORKSPACE
        run: |
          cd ${{ runner.temp }}
          tar -C $GITHUB_WORKSPACE -cvf GITHUB_WORKSPACE.tar .

      - name: Upload GITHUB_WORKSPACE Artifact
        uses: actions/upload-artifact@v3
        with:
          name: GITHUB_WORKSPACE
          path: ${{ runner.temp }}/GITHUB_WORKSPACE.tar

  test-updated-apps:
    runs-on: ubuntu-latest
    needs: get-latest-app-versions
    strategy:
      matrix:
        include:
          - os: buster
            arch: armhf
            image: https://downloads.raspberrypi.org/raspios_oldstable_armhf/images/raspios_oldstable_armhf-2023-05-03/2023-05-03-raspios-buster-armhf.img.xz
            cache: yes
          - os: buster
            arch: arm64
            image: https://downloads.raspberrypi.org/raspios_arm64/images/raspios_arm64-2021-05-28/2021-05-07-raspios-buster-arm64.zip
            cache: yes
          - os: bullseye
            arch: armhf
            image: https://downloads.raspberrypi.org/raspios_armhf/images/raspios_armhf-2023-05-03/2023-05-03-raspios-bullseye-armhf.img.xz
            cache: yes
          - os: bullseye
            arch: arm64
            image: https://downloads.raspberrypi.org/raspios_arm64/images/raspios_arm64-2023-05-03/2023-05-03-raspios-bullseye-arm64.img.xz
            cache: yes
          - os: bookworm
            arch: arm64
            image: RPIOS_BOOKWORM_URL
            cache: no
    name: Testing on ${{ matrix.os }} - ${{ matrix.arch }}
    outputs:
      FAILED_UPDATE_APPS_buster_armhf: ${{ steps.failed.outputs.FAILED_UPDATE_APPS_buster_armhf || '' }}
      FAILED_UPDATE_APPS_buster_arm64: ${{ steps.failed.outputs.FAILED_UPDATE_APPS_buster_arm64 || '' }}
      FAILED_UPDATE_APPS_bullseye_armhf: ${{ steps.failed.outputs.FAILED_UPDATE_APPS_bullseye_armhf || '' }}
      FAILED_UPDATE_APPS_bullseye_arm64: ${{ steps.failed.outputs.FAILED_UPDATE_APPS_bullseye_arm64 || '' }}
      FAILED_UPDATE_APPS_bookworm_arm64: ${{ steps.failed.outputs.FAILED_UPDATE_APPS_bookworm_arm64 || '' }}
      UPDATED_APPS_buster_armhf: ${{ steps.updated.outputs.UPDATED_APPS_buster_armhf || '' }}
      UPDATED_APPS_buster_arm64: ${{ steps.updated.outputs.UPDATED_APPS_buster_arm64 || '' }}
      UPDATED_APPS_bullseye_armhf: ${{ steps.updated.outputs.UPDATED_APPS_bullseye_armhf || '' }}
      UPDATED_APPS_bullseye_arm64: ${{ steps.updated.outputs.UPDATED_APPS_bullseye_arm64 || '' }}
      UPDATED_APPS_bookworm_arm64: ${{ steps.updated.outputs.UPDATED_APPS_bookworm_arm64 || '' }}
    steps:
      # restore GITHUB_WORKSPACE
      - uses: actions/download-artifact@v3
        with:
          name: GITHUB_WORKSPACE
          path: ${{ runner.temp }}
      - name: Extract GITHUB_WORKSPACE
        run: |
          cd ${{ runner.temp }}
          tar xf GITHUB_WORKSPACE.tar -C $GITHUB_WORKSPACE
          cd $GITHUB_WORKSPACE

      - name: Replace secret reference with contents
        if: ${{ matrix.image == 'RPIOS_BOOKWORM_URL' }}
        run: |
          image_url=${{ secrets[matrix.image] }}
          echo "::add-mask::$image_url"
          echo "image_url=$image_url" >> "$GITHUB_ENV"

      - name: Replace non-secret reference with contents
        if: ${{ matrix.image != 'RPIOS_BOOKWORM_URL' }}
        run: |
          image_url=${{ matrix.image }}
          echo "image_url=$image_url" >> "$GITHUB_ENV"

      - name: Write UPDATED_APPS and FAILED_APPS from OUTPUT of get-latest-app-versions to ENV
        run: |
          echo "UPDATED_APPS<<EOF" >> $GITHUB_ENV
          echo "${{needs.get-latest-app-versions.outputs.UPDATED_APPS}}" >> $GITHUB_ENV
          echo "EOF" >> $GITHUB_ENV

          echo "FAILED_APPS<<EOF" >> $GITHUB_ENV
          echo "${{needs.get-latest-app-versions.outputs.FAILED_APPS}}" >> $GITHUB_ENV
          echo "EOF" >> $GITHUB_ENV

      - name: Test installing updated apps on ${{ matrix.os }} - ${{ matrix.arch }}
        uses: theofficialgman/arm-runner-action@v12
        with:
          base_image: ${{ env.image_url }}
          # bind mount the directory so any changes propogate to outside the chroot
          bind_mount_repository: yes

          # give the image more space
          image_additional_mb: 5000

          # disable image caching to not leak the URL on some systems
          enable_image_caching: ${{ matrix.cache }}

          # set CPUs to use
          cpu: cortex-a7:cortex-a72

          # use custom /proc/cpuinfo
          cpu_info: cpuinfo/raspberrypi_4b

          # user runner name as default path
          copy_repository_path: /home/runner/pi-apps

          # export github env back to outside the chroot
          export_github_env: yes

          import_github_env: true

          # set shell to bash
          shell: /bin/bash
          commands: |
            sudo chown runner:docker /home/runner
            # print user info
            echo $USER $USERNAME $(id) $(whoami)
            sudo bash -c 'echo $USER $USERNAME $(id) $(whoami)'

            # create standard directories
            mkdir -p  $HOME/.local/share/applications $HOME/.local/bin
            sudo mkdir -p /usr/local/bin /usr/local/share/applications

            # install pi-apps dependencies
            sudo apt update
            sudo apt install -y yad curl wget aria2 lsb-release software-properties-common apt-utils imagemagick bc librsvg2-bin locales shellcheck git wmctrl xdotool x11-utils rsync

            # runonce-entries is run in the build tester, runonce requires that all api functions be available to subprocess (like is done in the gui script)
            #for the will_reinstall() and list_intersect() functions
            set -a #make all functions in the api available to subprocesses
            #make DIRECTORY equal to GITHUB_WORKSPACE, for subscripts and api functions
            DIRECTORY=$(pwd)
            source "${DIRECTORY}/api" || error "failed to source ${DIRECTORY}/api"
            #Run runonce entries
            "${DIRECTORY}/etc/runonce-entries"
            set +a #stop exporting functions
            cd ${DIRECTORY}

            # upgrade cmake to 3.20+ from theofficialgman ppa to fix QEMU only issue https://gitlab.kitware.com/cmake/cmake/-/issues/20568
            package_is_new_enough cmake 3.20 || debian_ppa_installer "theofficialgman/cmake-bionic" "bionic" "0ACACB5D1E74E484" || exit 1

            # store apps changed from last commit to working directory in variable
            mapfile -t changed_apps < <(git diff --name-only | grep ^apps/ | awk -F '/' '{print $2}' | sort -u)

            # clean out any app status files
            rm -rf ./data/status

            # attempt to install updated apps using manage script loop
            # if any app fails, add it to the FAILED_INSTALL_APPS or FAILED_UNINSTALL_APPS variable
            if [[ ${{ matrix.arch }} == armhf ]]; then
              # skip checking DDNet (due to Rust) if on arm32 under QEMU: https://github.com/rust-lang/cargo/issues/8719
              # skip checking Microsoft PowerShell due to QEMU instability with .NET: https://github.com/Botspot/pi-apps/pull/2252#issuecomment-1403043945
              # skip WPS Office on 32bit as it requires  "64bit" kernel to be detected. QEMU reports an armv7 kernel. WPS Office is still tested on arm64 (which will verify if the deb works and installs correctly)
              skiplist="DDNet
            Microsoft PowerShell
            WPS Office"
            else
              # skip checking Microsoft PowerShell due to QEMU instability with .NET: https://github.com/Botspot/pi-apps/pull/2252#issuecomment-1403043945
              skiplist="Microsoft PowerShell"
            fi            
            for app in "${changed_apps[@]}"; do
              # only attempt install/uninstall if updated app is supported on this architecture
              scriptname="$(script_name_cpu "$app")" #will be install, install-32, install-64, or package
              if [ -z "$scriptname" ];then
                continue
              fi
              # skip app if is in the skiplist
              if echo "$skiplist" | grep -Fxq "$app"; then
                continue
              fi
              ./manage install "$app" || { FAILED_INSTALL_APPS+="$app"$'\n'; UPDATED_APPS=$(echo "$UPDATED_APPS" | sed "/$app/d"); }
              ./manage uninstall "$app" || { FAILED_UNINSTALL_APPS+="$app"$'\n'; UPDATED_APPS=$(echo "$UPDATED_APPS" | sed "/$app/d"); }
            done

            if [ ! -z "$FAILED_INSTALL_APPS" ] || [ ! -z "$FAILED_UNINSTALL_APPS" ]; then
              echo "FAILED_UPDATE_APPS<<EOF" >> $GITHUB_ENV
              echo "$FAILED_INSTALL_APPS$FAILED_UNINSTALL_APPS" >> $GITHUB_ENV
              echo "EOF" >> $GITHUB_ENV
              echo "UPDATED_APPS<<EOF" >> $GITHUB_ENV
              echo "$UPDATED_APPS" >> $GITHUB_ENV
              echo "EOF" >> $GITHUB_ENV
            fi
      - name: Write FAILED_APPS to OUTPUT
        id: failed
        run: |
          echo "FAILED_UPDATE_APPS_${{ matrix.os }}_${{ matrix.arch }}<<EOF" >> $GITHUB_OUTPUT
          echo "${{env.FAILED_UPDATE_APPS}}" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT
      - name: Write UPDATED_APPS to OUTPUT
        id: updated
        run: |
          echo "UPDATED_APPS_${{ matrix.os }}_${{ matrix.arch }}<<EOF" >> $GITHUB_OUTPUT
          echo "${{env.UPDATED_APPS}}" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

  create-pr:
    needs: [get-latest-app-versions,test-updated-apps]
    runs-on: ubuntu-latest
    steps:
      # restore GITHUB_WORKSPACE
      - uses: actions/download-artifact@v3
        with:
          name: GITHUB_WORKSPACE
          path: ${{ runner.temp }}
      - name: Extract GITHUB_WORKSPACE
        run: |
          cd ${{ runner.temp }}
          tar xf GITHUB_WORKSPACE.tar -C $GITHUB_WORKSPACE
          cd $GITHUB_WORKSPACE
      - name: Delete artifact
        uses: geekyeggo/delete-artifact@v2
        with:
          name: GITHUB_WORKSPACE
          failOnError: false

      - name: Revert failed apps and generate list
        run: |
          ALL_FAILED_APPS="${{needs.test-updated-apps.outputs.FAILED_UPDATE_APPS_buster_armhf}}"$'\n'"${{needs.test-updated-apps.outputs.FAILED_UPDATE_APPS_buster_arm64}}"$'\n'"${{needs.test-updated-apps.outputs.FAILED_UPDATE_APPS_bullseye_armhf}}"$'\n'"${{needs.test-updated-apps.outputs.FAILED_UPDATE_APPS_bullseye_arm64}}"$'\n'"${{needs.test-updated-apps.outputs.FAILED_UPDATE_APPS_bookworm_arm64}}"
          ALL_FAILED_APPS="$(echo "$ALL_FAILED_APPS" | sort -u | awk NF)"
          echo "ALL_FAILED_APPS<<EOF" >> $GITHUB_ENV
          echo "$ALL_FAILED_APPS" >> $GITHUB_ENV
          echo "EOF" >> $GITHUB_ENV
          error_string=""
          IFS=$'\n'
          for app in $ALL_FAILED_APPS; do
            git checkout -- "apps/$app"
            error_string+='![badge-error][badge-error]'" Failed to install $app on "

            if echo "${{needs.test-updated-apps.outputs.FAILED_UPDATE_APPS_buster_armhf}}" | grep -Fxq "$app"; then
              error_string+="buster armhf, "
            fi
            if echo "${{needs.test-updated-apps.outputs.FAILED_UPDATE_APPS_buster_arm64}}" | grep -Fxq "$app"; then
              error_string+="buster arm64, "
            fi
            if echo "${{needs.test-updated-apps.outputs.FAILED_UPDATE_APPS_bullseye_armhf}}" | grep -Fxq "$app"; then
              error_string+="bullseye armhf, "
            fi
            if echo "${{needs.test-updated-apps.outputs.FAILED_UPDATE_APPS_bullseye_arm64}}" | grep -Fxq "$app"; then
              error_string+="bullseye arm64, "
            fi
            if echo "${{needs.test-updated-apps.outputs.FAILED_UPDATE_APPS_bookworm_arm64}}" | grep -Fxq "$app"; then
              error_string+="bookworm arm64, "
            fi

            error_string+="reverting to previous version."$'\n'$'\n'
          done
          true
          echo "ALL_FAILED_APPS_ERROR_STRING<<EOF" >> $GITHUB_ENV
          echo "$error_string" >> $GITHUB_ENV
          echo "EOF" >> $GITHUB_ENV

      - name: Generate updated apps list
        run: |
          list_subtract() { #Outputs a list of apps from stdin, minus the ones that appear in $1 using the UPDATED_APP format
            # for example, the following two inputs will be a match
            # - BlockBench-arm64: 4.7.4 -> 4.8.0
            # BlockBench
            #                       change \n to -\n     | change '^' to '- '
            grep -ve "$(echo "$1" | sed -z 's/\n/\-\n/g' | sed 's/^/- /g')"
            true
          }
          ALL_UPDATED_APPS="${{needs.test-updated-apps.outputs.UPDATED_APPS_buster_armhf}}"$'\n'"${{needs.test-updated-apps.outputs.UPDATED_APPS_buster_arm64}}"$'\n'"${{needs.test-updated-apps.outputs.UPDATED_APPS_bullseye_armhf}}"$'\n'"${{needs.test-updated-apps.outputs.UPDATED_APPS_bullseye_arm64}}"$'\n'"${{needs.test-updated-apps.outputs.UPDATED_APPS_bookworm_arm64}}"
          ALL_UPDATED_APPS="$(echo "$ALL_UPDATED_APPS" | sort -u | awk NF)"
          [ ! -z "$ALL_UPDATED_APPS" ] && ALL_UPDATED_APPS="$(echo "$ALL_UPDATED_APPS" | list_subtract "$ALL_FAILED_APPS")"
          echo "ALL_UPDATED_APPS<<EOF" >> $GITHUB_ENV
          echo "$ALL_UPDATED_APPS" >> $GITHUB_ENV
          echo "EOF" >> $GITHUB_ENV

      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v4
        with:
          author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
          commit-message: |
            Update App Versions, run by GitHub Actions - Apps updated:

            ${{ env.ALL_UPDATED_APPS }}
          branch: "auto-app-updates-NOT-FOR-USERS"
          title: "[AUTO] Update App Versions"
          body: |
            [badge-error]: https://github.com/Pi-Apps-Coders/GitHub-Markdown/blob/main/blockquotes/badge/dark-theme/error.svg?raw=true 'Error'
            [badge-warning]: https://github.com/Pi-Apps-Coders/GitHub-Markdown/blob/main/blockquotes/badge/dark-theme/warning.svg?raw=true 'Warning'
            [badge-issue]: https://github.com/Pi-Apps-Coders/GitHub-Markdown/blob/main/blockquotes/badge/dark-theme/issue.svg?raw=true 'Issue'
            [badge-check]: https://github.com/Pi-Apps-Coders/GitHub-Markdown/blob/main/blockquotes/badge/dark-theme/check.svg?raw=true 'Check'
            [badge-info]: https://github.com/Pi-Apps-Coders/GitHub-Markdown/blob/main/blockquotes/badge/dark-theme/info.svg?raw=true 'Info'
            Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action.
            Workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
            Apps updated:
            ```
            ${{ env.ALL_UPDATED_APPS }}
            ```
            ${{needs.get-latest-app-versions.outputs.FAILED_APPS}}
            ${{ env.ALL_FAILED_APPS_ERROR_STRING }}
