# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Patrick Lehmann                                                                                                    #
#                                                                                                                      #
# ==================================================================================================================== #
# Copyright 2020-2025 The pyTooling Authors                                                                            #
#                                                                                                                      #
# Licensed under the Apache License, Version 2.0 (the "License");                                                      #
# you may not use this file except in compliance with the License.                                                     #
# You may obtain a copy of the License at                                                                              #
#                                                                                                                      #
#   http://www.apache.org/licenses/LICENSE-2.0                                                                         #
#                                                                                                                      #
# Unless required by applicable law or agreed to in writing, software                                                  #
# distributed under the License is distributed on an "AS IS" BASIS,                                                    #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.                                             #
# See the License for the specific language governing permissions and                                                  #
# limitations under the License.                                                                                       #
#                                                                                                                      #
# SPDX-License-Identifier: Apache-2.0                                                                                  #
# ==================================================================================================================== #
name: Nightly

on:
  workflow_call:
    inputs:
      ubuntu_image:
        description: 'Name of the Ubuntu image.'
        required: false
        default: 'ubuntu-24.04'
        type: string
      nightly_name:
        description: 'Name of the nightly release.'
        required: false
        default: 'nightly'
        type: string
      nightly_title:
        description: 'Title of the nightly release.'
        required: false
        default: ''
        type: string
      nightly_description:
        description: 'Description of the nightly release.'
        required: false
        default: 'Release of artifacts from latest CI pipeline.'
        type: string
      draft:
        description: 'Specify if this is a draft.'
        required: false
        default: false
        type: boolean
      prerelease:
        description: 'Specify if this is a pre-release.'
        required: false
        default: false
        type: boolean
      latest:
        description: 'Specify if this is the latest release.'
        required: false
        default: false
        type: boolean
      replacements:
        description: 'Multi-line string containing search=replace patterns.'
        required: false
        default: ''
        type: string
      assets:
        description: 'Multi-line string containing artifact:file:title asset descriptions.'
        required: true
        type: string
      inventory-json:
        type: string
        required: false
        default: ''
      inventory-version:
        type: string
        required: false
        default: ''
      inventory-categories:
        type: string
        required: false
        default: ''
      tarball-name:
        type: string
        required: false
        default: '__pyTooling_upload_artifact__.tar'
      can-fail:
        type: boolean
        required: false
        default: false

jobs:
  Release:
    name: 📝 Update 'Nightly Page' on GitHub
    runs-on: ${{ inputs.ubuntu_image }}
    continue-on-error: ${{ inputs.can-fail }}
    permissions:
      contents: write
      actions:  write
#      attestations: write

    steps:
      - name: ⚠️ Deprecation Warning
        run: printf "::warning title=%s::%s\n" "NightlyRelease" "'NightlyRelease.yml' template is deprecated. Please switch to 'PublishReleaseNotes.yml'. See https://pytooling.github.io/Actions/JobTemplate/Release/PublishReleaseNotes.html"

      - name: ⏬ Checkout repository
        uses: actions/checkout@v6
        with:
          # The command 'git describe' (used for version) needs the history.
          fetch-depth: 0

      - name: 🔧 Install zstd
        run: sudo apt-get install -y --no-install-recommends zstd

      - name: 📑 Delete (old) Release Page
        id: deleteReleasePage
        run: |
          set +e

          ANSI_LIGHT_RED=$'\x1b[91m'
          ANSI_LIGHT_GREEN=$'\x1b[92m'
          ANSI_LIGHT_YELLOW=$'\x1b[93m'
          ANSI_NOCOLOR=$'\x1b[0m'

          export GH_TOKEN=${{ github.token }}

          printf "%s" "Deleting release '${{ inputs.nightly_name }}' ... "
          message="$(gh release delete ${{ inputs.nightly_name }} --yes 2>&1)"
          if [[ $? -eq 0 ]]; then
            printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}"
          elif [[ "${message}" == "release not found" ]]; then
            printf "%s\n" "${ANSI_LIGHT_YELLOW}[NOT FOUND]${ANSI_NOCOLOR}"
          else
            printf "%s\n" "${ANSI_LIGHT_RED}[FAILED]${ANSI_NOCOLOR}"
            printf "  %s\n" "${ANSI_LIGHT_RED}Couldn't delete release '${{ inputs.nightly_name }}' -> Error: '${message}'.${ANSI_NOCOLOR}"
            printf "::error title=%s::%s\n" "InternalError" "Couldn't delete release '${{ inputs.nightly_name }}' -> Error: '${message}'."
            exit 1
          fi

      - name: 📑 (Re)create (new) Release Page
        id: createReleasePage
        run: |
          set +e

          ANSI_LIGHT_RED=$'\x1b[91m'
          ANSI_LIGHT_GREEN=$'\x1b[92m'
          ANSI_NOCOLOR=$'\x1b[0m'

          export GH_TOKEN=${{ github.token }}

          addDraft="--draft"

          if [[ "${{ inputs.prerelease }}" == "true" ]]; then
            addPreRelease="--prerelease"
          fi

          if [[ "${{ inputs.latest }}" == "false" ]]; then
            addLatest="--latest=false"
          fi

          if [[ "${{ inputs.nightly_title }}" != "" ]]; then
            addTitle=("--title" "${{ inputs.nightly_title }}")
          fi

          cat <<'EOF' > __NoTeS__.md
          ${{ inputs.nightly_description }}
          EOF
          if [[ -s __NoTeS__.md ]]; then
            addNotes=("--notes-file" "__NoTeS__.md")
          fi

          # Apply replacements
          while IFS=$'\r\n' read -r patternLine; do
            # skip empty lines
            [[ "$patternLine" == "" ]] && continue

            pattern="${patternLine%%=*}"
            replacement="${patternLine#*=}"
            sed -i -e "s/%$pattern%/$replacement/g" "__NoTeS__.md"
          done <<<'${{ inputs.replacements }}'

          # Add footer line
          cat <<EOF >> __NoTeS__.md

          --------
          Published from [${{ github.workflow }}](https://github.com/Paebbels/ghdl/actions/runs/${{ github.run_id }}) workflow triggered by @${{ github.actor }} on $(date '+%Y-%m-%d %H:%M:%S %Z').
          EOF

          printf "%s\n" "Creating release '${{ inputs.nightly_name }}' ... "
          message="$(gh release create "${{ inputs.nightly_name }}" --verify-tag $addDraft $addPreRelease $addLatest "${addTitle[@]}" "${addNotes[@]}" 2>&1)"
          if [[ $? -eq 0 ]]; then
            printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}"
          else
            printf "%s\n" "${ANSI_LIGHT_RED}[FAILED]${ANSI_NOCOLOR}"
            printf "  %s\n" "${ANSI_LIGHT_RED}Couldn't create release '${{ inputs.nightly_name }}' -> Error: '${message}'.${ANSI_NOCOLOR}"
            printf "::error title=%s::%s\n" "InternalError" "Couldn't create release '${{ inputs.nightly_name }}' -> Error: '${message}'."
            exit 1
          fi

      - name: 📥 Download artifacts and upload as assets
        id: uploadAssets
        run: |
          set +e

          ANSI_LIGHT_RED=$'\x1b[91m'
          ANSI_LIGHT_GREEN=$'\x1b[92m'
          ANSI_LIGHT_YELLOW=$'\x1b[93m'
          ANSI_LIGHT_BLUE=$'\x1b[94m'
          ANSI_NOCOLOR=$'\x1b[0m'

          export GH_TOKEN=${{ github.token }}

          Replace() {
            line="$1"
            while IFS=$'\r\n' read -r patternLine; do
              # skip empty lines
              [[ "$patternLine" == "" ]] && continue

              pattern="${patternLine%%=*}"
              replacement="${patternLine#*=}"
              line="${line//"%$pattern%"/"$replacement"}"
            done <<<'${{ inputs.replacements }}'
            printf "%s\n" "$line"
          }

          # Create JSON inventory
          if [[ "${{ inputs.inventory-json }}" != "" ]]; then
            VERSION="1.0"

            # Split categories by ',' into a Bash array.
            # See https://stackoverflow.com/a/45201229/3719459
            if [[ "${{ inputs.inventory-categories }}" != "" ]]; then
              readarray -td, inventoryCategories <<<"${{ inputs.inventory-categories }},"
              unset 'inventoryCategories[-1]'
              declare -p inventoryCategories
            else
              inventoryCategories=""
            fi

            jsonInventory=$(jq -c -n \
              --arg version "${VERSION}" \
              --arg date "$(date +"%Y-%m-%dT%H-%M-%S%:z")" \
              --argjson jsonMeta "$(jq -c -n \
                --arg tag "${{ inputs.nightly_name }}" \
                --arg version "${{ inputs.inventory-version }}" \
                --arg hash "${{ github.sha }}" \
                --arg repo "${{ github.server_url }}/${{ github.repository }}" \
                --arg release "${{ github.server_url }}/${{ github.repository }}/releases/download/${{ inputs.nightly_name }}" \
                --argjson categories "$(jq -c -n \
                  '$ARGS.positional' \
                  --args "${inventoryCategories[@]}" \
                )" \
                '{"tag": $tag, "version": $version, "git-hash": $hash, "repository-url": $repo, "release-url": $release, "categories": $categories}' \
              )" \
              '{"version": 1.0, "timestamp": $date, "meta": $jsonMeta, "files": {}}'
            )
          fi

          ERRORS=0
          # A dictionary of 0/1 to avoid duplicate downloads
          declare -A downloadedArtifacts
          # A dictionary to check for duplicate asset files in release
          declare -A assetFilenames
          while IFS=$'\r\n' read -r assetLine; do
            if [[ "${assetLine}" == "" || "${assetLine:0:1}" == "#" ]]; then
              continue
            fi

            # split assetLine colon separated triple: artifact:asset:title
            artifact="${assetLine%%:*}"
            assetLine="${assetLine#*:}"
            asset="${assetLine%%:*}"
            assetLine="${assetLine#*:}"
            if [[ "${{ inputs.inventory-json }}" == "" ]]; then
              categories=""
              title="${assetLine##*:}"
            else
              categories="${assetLine%%:*}"
              title="${assetLine##*:}"
            fi

            # remove leading whitespace
            asset="${asset#"${asset%%[![:space:]]*}"}"
            categories="${categories#"${categories%%[![:space:]]*}"}"
            title="${title#"${title%%[![:space:]]*}"}"

            # apply replacements
            asset="$(Replace "${asset}")"
            title="$(Replace "${title}")"

            printf "%s\n" "Publish asset '${asset}' from artifact '${artifact}' with title '${title}'"
            printf "  %s" "Checked asset for duplicates ... "
            if [[ -n "${assetFilenames[$asset]}" ]]; then
              printf "%s\n" "${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}"
              printf "::error title=%s::%s\n" "DuplicateAsset" "Asset '${asset}' from artifact '${artifact}' was already uploaded to release '${{ inputs.nightly_name }}'."
              ERRORS=$((ERRORS + 1))
              continue
            else
              printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}"
              assetFilenames[$asset]=1
            fi

            # Download artifact by artifact name
            if [[ -n "${downloadedArtifacts[$artifact]}" ]]; then
              printf "  %s\n" "downloading '${artifact}' ... ${ANSI_LIGHT_YELLOW}[SKIPPED]${ANSI_NOCOLOR}"
            else
              printf "  downloading '${artifact}' ...\n"
              printf "    %s" "gh run download $GITHUB_RUN_ID --dir \"${artifact}\" --name \"${artifact}\" "
              gh run download $GITHUB_RUN_ID --dir "${artifact}" --name "${artifact}"
              if [[ $? -eq 0 ]]; then
                printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}"
              else
                printf "%s\n" "${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}"
                printf "      %s\n" "${ANSI_LIGHT_RED}Couldn't download artifact '${artifact}'.${ANSI_NOCOLOR}"
                printf "::error title=%s::%s\n" "ArtifactNotFound" "Couldn't download artifact '${artifact}'."
                ERRORS=$((ERRORS + 1))
                continue
              fi
              downloadedArtifacts[$artifact]=1

              printf "  %s" "Checking for embedded tarball ... "
              if [[ -f "${artifact}/${{ inputs.tarball-name }}" ]]; then
                printf "%s\n" "${ANSI_LIGHT_GREEN}[FOUND]${ANSI_NOCOLOR}"

                pushd "${artifact}" > /dev/null

                printf "    %s" "Extracting embedded tarball ... "
                tar -xf "${{ inputs.tarball-name }}"
                if [[ $? -ne 0 ]]; then
                  printf "%s\n" "${ANSI_LIGHT_RED}[FAILED]${ANSI_NOCOLOR}"
                else
                  printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}"
                fi

                printf "    %s" "Removing temporary tarball ... "
                rm -f "${{ inputs.tarball-name }}"
                if [[ $? -ne 0 ]]; then
                  printf "%s\n" "${ANSI_LIGHT_RED}[FAILED]${ANSI_NOCOLOR}"
                else
                  printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}"
                fi

                popd > /dev/null
              else
                printf "%s\n" "${ANSI_LIGHT_YELLOW}[SKIPPED]${ANSI_NOCOLOR}"
              fi
            fi

            # Check if artifact should be compressed (zip, tgz) or if asset was part of the downloaded artifact.
            printf "  %s" "checking asset '${artifact}/${asset}' ... "
            if [[ "${asset}" == !*.zip ]]; then
              printf "%s\n" "${ANSI_LIGHT_GREEN}[ZIP]${ANSI_NOCOLOR}"
              asset="${asset##*!}"
              printf "::group::    %s\n" "Compressing artifact '${artifact}' to '${asset}' ..."
              (
                cd "${artifact}" && \
                zip -r "../${asset}" *
              )
              retCode=$?
              printf "::endgroup::\n"
              if [[ $retCode -eq 0 ]]; then
                printf "    %s\n" "Compression ${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}"
                uploadFile="${asset}"
              else
                printf "    %s\n" "Compression ${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}"
                printf "      %s\n" "${ANSI_LIGHT_RED}Couldn't compress '${artifact}' to zip file '${asset}'.${ANSI_NOCOLOR}"
                printf "::error title=%s::%s\n" "CompressionError" "Couldn't compress '${artifact}' to zip file '${asset}'."
                ERRORS=$((ERRORS + 1))
                continue
              fi
            elif [[ "${asset}" == !*.tgz || "${asset}" == !*.tar.gz || "${asset}" == \$*.tgz || "${asset}" == \$*.tar.gz ]]; then
              printf "%s\n" "${ANSI_LIGHT_GREEN}[TAR/GZ]${ANSI_NOCOLOR}"

              if [[ "${asset:0:1}" == "\$" ]]; then
                asset="${asset##*$}"
                dirName="${asset%.*}"
                printf "    %s\n" "Compressing artifact '${artifact}' to '${asset}' ..."
                tar -c --gzip --owner=0 --group=0 --file="${asset}" --directory="${artifact}" --transform "s|^\.|${dirName%.tar}|" .
                retCode=$?
              else
                asset="${asset##*!}"
                printf "    %s\n" "Compressing artifact '${artifact}' to '${asset}' ..."
                (
                  cd "${artifact}" && \
                  tar -c --gzip --owner=0 --group=0 --file="../${asset}" *
                )
                retCode=$?
              fi

              if [[ $retCode -eq 0 ]]; then
                printf "    %s\n" "Compression ${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}"
                uploadFile="${asset}"
              else
                printf "    %s\n" "Compression ${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}"
                printf "      %s\n" "${ANSI_LIGHT_RED}Couldn't compress '${artifact}' to tgz file '${asset}'.${ANSI_NOCOLOR}"
                printf "::error title=%s::%s\n" "CompressionError" "Couldn't compress '${artifact}' to tgz file '${asset}'."
                ERRORS=$((ERRORS + 1))
                continue
              fi
            elif [[ "${asset}" == !*.tzst || "${asset}" == !*.tar.zst || "${asset}" == \$*.tzst || "${asset}" == \$*.tar.zst ]]; then
              printf "%s\n" "${ANSI_LIGHT_GREEN}[ZST]${ANSI_NOCOLOR}"

              if [[ "${asset:0:1}" == "\$" ]]; then
                asset="${asset##*$}"
                dirName="${asset%.*}"
                printf "    %s\n" "Compressing artifact '${artifact}' to '${asset}' ..."
                tar -c --zstd --owner=0 --group=0 --file="${asset}" --directory="${artifact}" --transform "s|^\.|${dirName%.tar}|" .
                retCode=$?
              else
                asset="${asset##*!}"
                printf "    %s\n" "Compressing artifact '${artifact}' to '${asset}' ..."
                (
                  cd "${artifact}" && \
                  tar -c --zstd --owner=0 --group=0 --file="../${asset}" *
                )
                retCode=$?
              fi

              if [[ $retCode -eq 0 ]]; then
                printf "    %s\n" "Compression ${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}"
                uploadFile="${asset}"
              else
                printf "    %s\n" "Compression ${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}"
                printf "      %s\n" "${ANSI_LIGHT_RED}Couldn't compress '${artifact}' to zst file '${asset}'.${ANSI_NOCOLOR}"
                printf "::error title=%s::%s\n" "CompressionError" "Couldn't compress '${artifact}' to zst file '${asset}'."
                ERRORS=$((ERRORS + 1))
                continue
              fi
            elif [[ -e "${artifact}/${asset}" ]]; then
              printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}"
              uploadFile="${artifact}/${asset}"
            else
              printf "%s\n" "${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}"
              printf "    %s\n" "${ANSI_LIGHT_RED}Couldn't find asset '${asset}' in artifact '${artifact}'.${ANSI_NOCOLOR}"
              printf "::error title=%s::%s\n" "FileNotFound" "Couldn't find asset '${asset}' in artifact '${artifact}'."
              ERRORS=$((ERRORS + 1))
              continue
            fi

            # Add asset to JSON inventory
            if [[ "${{ inputs.inventory-json }}" != "" ]]; then
              if [[ "${categories}" != "${title}" ]]; then
                printf "  %s\n" "adding file '${uploadFile#*/}' with '${categories//;/ → }' to JSON inventory ..."
                category=""
                jsonEntry=$(jq -c -n \
                  --arg title "${title}" \
                  --arg file "${uploadFile#*/}" \
                  '{"file": $file, "title": $title}' \
                )

                while [[ "${categories}" != "${category}" ]]; do
                  category="${categories##*,}"
                  categories="${categories%,*}"
                  jsonEntry=$(jq -c -n --arg cat "${category}" --argjson value "${jsonEntry}" '{$cat: $value}')
                done

                jsonInventory=$(jq -c -n \
                  --argjson inventory "${jsonInventory}" \
                  --argjson file "${jsonEntry}" \
                  '$inventory * {"files": $file}' \
                )
              else
                printf "  %s\n" "adding file '${uploadFile#*/}' to JSON inventory ... ${ANSI_LIGHT_YELLOW}[SKIPPED]${ANSI_NOCOLOR}"
              fi
            fi

            # Upload asset to existing release page
            printf "  %s" "uploading asset '${asset}' from '${uploadFile}' with title '${title}' ... "
            gh release upload ${{ inputs.nightly_name }} "${uploadFile}#${title}" --clobber
            if [[ $? -eq 0 ]]; then
              printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}"
            else
              printf "%s\n" "${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}"
              printf "    %s\n" "${ANSI_LIGHT_RED}Couldn't upload asset '${asset}' from '${uploadFile}' to release '${{ inputs.nightly_name }}'.${ANSI_NOCOLOR}"
              printf "::error title=%s::%s\n" "UploadError" "Couldn't upload asset '${asset}' from '${uploadFile}' to release '${{ inputs.nightly_name }}'."
              ERRORS=$((ERRORS + 1))
              continue
            fi
          done <<<'${{ inputs.assets }}'

          if [[ "${{ inputs.inventory-json }}" != "" ]]; then
            inventoryTitle="Release Inventory (JSON)"

            printf "%s\n" "Publish asset '${{ inputs.inventory-json }}' with title '${inventoryTitle}'"
            printf "::group::${ANSI_LIGHT_BLUE}%s${ANSI_NOCOLOR}\n" "Writing JSON inventory to '${{ inputs.inventory-json }}' ...."
            printf "%s\n" "$(jq -n --argjson inventory "${jsonInventory}" '$inventory')" > "${{ inputs.inventory-json }}"
            cat "${{ inputs.inventory-json }}"
            printf "::endgroup::\n"

            # Upload inventory asset to existing release page
            printf "  %s" "uploading asset '${{ inputs.inventory-json }}' title '${inventoryTitle}' ... "
            gh release upload ${{ inputs.nightly_name }} "${{ inputs.inventory-json }}#${inventoryTitle}" --clobber
            if [[ $? -eq 0 ]]; then
              printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}"
            else
              printf "%s\n" "${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}"
              printf "    %s\n" "${ANSI_LIGHT_RED}Couldn't upload asset '${{ inputs.inventory-json }}' to release '${{ inputs.nightly_name }}'.${ANSI_NOCOLOR}"
              printf "::error title=%s::%s\n" "UploadError" "Couldn't upload asset '${{ inputs.inventory-json }}' to release '${{ inputs.nightly_name }}'."
              ERRORS=$((ERRORS + 1))
              continue
            fi
          fi

          printf "::group::${ANSI_LIGHT_BLUE}%s${ANSI_NOCOLOR}\n" "Inspecting downloaded artifacts ..."
          tree -pash -L 3 .
          printf "::endgroup::\n"

          if [[ $ERRORS -ne 0 ]]; then
            printf "%s\n" "${ANSI_LIGHT_RED}${ERRORS} errors detected in previous steps.${ANSI_NOCOLOR}"
            exit 1
          fi

      - name: 📑 Remove draft state from Release Page
        if: ${{ ! inputs.draft }}
        run: |
          set +e

          ANSI_LIGHT_RED=$'\x1b[91m'
          ANSI_LIGHT_GREEN=$'\x1b[92m'
          ANSI_NOCOLOR=$'\x1b[0m'

          export GH_TOKEN=${{ github.token }}

          # Remove draft-state from release page
          printf "%s" "Remove draft-state from release '${title}' ... "
          gh release edit --draft=false "${{ inputs.nightly_name }}"
          if [[ $? -eq 0 ]]; then
            printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}"
          else
            printf "%s\n" "${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}"
            printf "  %s\n" "${ANSI_LIGHT_RED}Couldn't remove draft-state from release '${{ inputs.nightly_name }}'.${ANSI_NOCOLOR}"
            printf "::error title=%s::%s\n" "ReleasePage" "Couldn't remove draft-state from release '${{ inputs.nightly_name }}'."
          fi
