---
# This action is centrally managed in https://github.com/<organization>/.github/
# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in
# the above-mentioned repo.

# This workflow is intended to work with all our organization Docker projects. A readme named `DOCKER_README.md`
# will be used to update the description on Docker hub.

# custom comments in dockerfiles:

# `# platforms: `
#    Comma separated list of platforms, i.e. `# platforms: linux/386,linux/amd64`. Docker platforms can alternatively
#    be listed in a file named `.docker_platforms`.
# `# platforms_pr: `
#    Comma separated list of platforms to run for PR events, i.e. `# platforms_pr: linux/amd64`. This will take
#    precedence over the `# platforms: ` directive.
# `# artifacts: `
#    `true` to build in two steps, stopping at `artifacts` build stage and extracting the image from there to the
#    GitHub runner.

name: CI Docker

on:
  pull_request:
    branches: [master, nightly]
    types: [opened, synchronize, reopened]
  push:
    branches: [master, nightly]
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  check_dockerfiles:
    name: Check Dockerfiles
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Find dockerfiles
        id: find
        run: |
          dockerfiles=$(find . -type f -iname "Dockerfile" -o -iname "*.dockerfile")

          echo "found dockerfiles: ${dockerfiles}"

          # do not quote to keep this as a single line
          echo dockerfiles=${dockerfiles} >> $GITHUB_OUTPUT

          MATRIX_COMBINATIONS=""
          for FILE in ${dockerfiles}; do
            # extract tag from file name
            tag=$(echo $FILE | sed -r -z -e 's/(\.\/)*.*\/(Dockerfile)/None/gm')
            if [[ $tag == "None" ]]; then
              MATRIX_COMBINATIONS="$MATRIX_COMBINATIONS {\"dockerfile\": \"$FILE\"},"
            else
              tag=$(echo $FILE | sed -r -z -e 's/(\.\/)*.*\/(.+)(\.dockerfile)/-\2/gm')
              MATRIX_COMBINATIONS="$MATRIX_COMBINATIONS {\"dockerfile\": \"$FILE\", \"tag\": \"$tag\"},"
            fi
          done

          # removes the last character (i.e. comma)
          MATRIX_COMBINATIONS=${MATRIX_COMBINATIONS::-1}

          # setup matrix for later jobs
          matrix=$((
            echo "{ \"include\": [$MATRIX_COMBINATIONS] }"
          ) | jq -c .)

          echo $matrix
          echo $matrix | jq .
          echo "matrix=$matrix" >> $GITHUB_OUTPUT

    outputs:
      dockerfiles: ${{ steps.find.outputs.dockerfiles }}
      matrix: ${{ steps.find.outputs.matrix }}

  check_changelog:
    name: Check Changelog
    needs: [check_dockerfiles]
    if: ${{ needs.check_dockerfiles.outputs.dockerfiles }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        if: ${{ github.ref == 'refs/heads/master' || github.base_ref == 'master' }}
        uses: actions/checkout@v3

      - name: Verify Changelog
        id: verify_changelog
        if: ${{ github.ref == 'refs/heads/master' || github.base_ref == 'master' }}
        # base_ref for pull request check, ref for push
        uses: LizardByte/.github/actions/verify_changelog@master
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
    outputs:
      next_version: ${{ steps.verify_changelog.outputs.changelog_parser_version }}
      next_version_bare: ${{ steps.verify_changelog.outputs.changelog_parser_version_bare }}
      last_version: ${{ steps.verify_changelog.outputs.latest_release_tag_name }}
      release_body: ${{ steps.verify_changelog.outputs.changelog_parser_description }}

  setup_release:
    name: Setup Release
    needs: check_changelog
    runs-on: ubuntu-latest
    steps:
      - name: Set release details
        id: release_details
        env:
          RELEASE_BODY: ${{ needs.check_changelog.outputs.release_body }}
        run: |
          # determine to create a release or not
          if [[ $GITHUB_EVENT_NAME == "push" ]]; then
            RELEASE=true
          else
            RELEASE=false
          fi

          # set the release tag
          COMMIT=${{ github.sha }}
          if [[ $GITHUB_REF == refs/heads/master ]]; then
            TAG="${{ needs.check_changelog.outputs.next_version }}"
            RELEASE_NAME="${{ needs.check_changelog.outputs.next_version }}"
            RELEASE_BODY="$RELEASE_BODY"
            PRE_RELEASE="false"
          elif [[ $GITHUB_REF == refs/heads/nightly ]]; then
            TAG="nightly-dev"
            RELEASE_NAME="nightly"
            RELEASE_BODY="automated nightly release - $(date -u +'%Y-%m-%dT%H:%M:%SZ') - ${COMMIT}"
            PRE_RELEASE="true"
          fi

          echo "create_release=${RELEASE}" >> $GITHUB_OUTPUT
          echo "release_tag=${TAG}" >> $GITHUB_OUTPUT
          echo "release_commit=${COMMIT}" >> $GITHUB_OUTPUT
          echo "release_name=${RELEASE_NAME}" >> $GITHUB_OUTPUT
          echo "pre_release=${PRE_RELEASE}" >> $GITHUB_OUTPUT

          # this is stupid but works for multiline strings
          echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV
          echo "$RELEASE_BODY" >> $GITHUB_ENV
          echo "EOF" >> $GITHUB_ENV

    outputs:
      create_release: ${{ steps.release_details.outputs.create_release }}
      release_tag: ${{ steps.release_details.outputs.release_tag }}
      release_commit: ${{ steps.release_details.outputs.release_commit }}
      release_name: ${{ steps.release_details.outputs.release_name }}
      release_body: ${{ env.RELEASE_BODY }}
      pre_release: ${{ steps.release_details.outputs.pre_release }}

  lint_dockerfile:
    needs: [check_dockerfiles]
    if: ${{ needs.check_dockerfiles.outputs.dockerfiles }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix: ${{ fromJson(needs.check_dockerfiles.outputs.matrix) }}
    name: Lint Dockerfile${{ matrix.tag }}

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Hadolint
        id: hadolint
        uses: hadolint/hadolint-action@v3.1.0
        with:
          dockerfile: ${{ matrix.dockerfile }}
          ignore: DL3008,DL3013,DL3016,DL3018,DL3028,DL3059
          output-file: ./hadolint.log
          verbose: true

      - name: Log
        if: failure()
        run: |
          echo "Hadolint outcome: ${{ steps.hadolint.outcome }}" >> $GITHUB_STEP_SUMMARY
          cat "./hadolint.log" >> $GITHUB_STEP_SUMMARY

  docker:
    needs: [check_dockerfiles, check_changelog, setup_release]
    if: ${{ needs.check_dockerfiles.outputs.dockerfiles }}
    runs-on: ubuntu-latest
    permissions:
      packages: write
      contents: write
    strategy:
      fail-fast: false
      matrix: ${{ fromJson(needs.check_dockerfiles.outputs.matrix) }}
    name: Docker${{ matrix.tag }}

    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          submodules: recursive

      - name: Prepare
        id: prepare
        env:
          NV: ${{ needs.check_changelog.outputs.next_version }}
        run: |
          # get branch name
          BRANCH=${GITHUB_HEAD_REF}

          RELEASE=false

          if [ -z "$BRANCH" ]; then
            echo "This is a PUSH event"
            BRANCH=${{ github.ref_name }}
            COMMIT=${{ github.sha }}
            CLONE_URL=${{ github.event.repository.clone_url }}
            if [[ $BRANCH == "master" ]]; then
              RELEASE=true
            fi
          else
            echo "This is a PULL REQUEST event"
            COMMIT=${{ github.event.pull_request.head.sha }}
            CLONE_URL=${{ github.event.pull_request.head.repo.clone_url }}
          fi

          # determine to push image to dockerhub and ghcr or not
          if [[ $GITHUB_EVENT_NAME == "push" ]]; then
            PUSH=true
          else
            PUSH=false
          fi

          # setup the tags
          REPOSITORY=${{ github.repository }}
          BASE_TAG=$(echo $REPOSITORY | tr '[:upper:]' '[:lower:]')

          TAGS="${BASE_TAG}:${COMMIT:0:7}${{ matrix.tag }},ghcr.io/${BASE_TAG}:${COMMIT:0:7}${{ matrix.tag }}"

          if [[ $GITHUB_REF == refs/heads/master ]]; then
            TAGS="${TAGS},${BASE_TAG}:latest${{ matrix.tag }},ghcr.io/${BASE_TAG}:latest${{ matrix.tag }}"
            TAGS="${TAGS},${BASE_TAG}:master${{ matrix.tag }},ghcr.io/${BASE_TAG}:master${{ matrix.tag }}"
          elif [[ $GITHUB_REF == refs/heads/nightly ]]; then
            TAGS="${TAGS},${BASE_TAG}:nightly${{ matrix.tag }},ghcr.io/${BASE_TAG}:nightly${{ matrix.tag }}"
          else
            TAGS="${TAGS},${BASE_TAG}:test${{ matrix.tag }},ghcr.io/${BASE_TAG}:test${{ matrix.tag }}"
          fi

          if [[ ${NV} != "" ]]; then
            TAGS="${TAGS},${BASE_TAG}:${NV}${{ matrix.tag }},ghcr.io/${BASE_TAG}:${NV}${{ matrix.tag }}"
          fi

          # parse custom directives out of dockerfile
          # try to get the platforms from the dockerfile custom directive, i.e. `# platforms: xxx,yyy`
          # directives for PR event, i.e. not push event
          if [[ ${PUSH} == "false" ]]; then
            while read -r line; do
              if [[ $line == "# platforms_pr: "* && $PLATFORMS == "" ]]; then
                # echo the line and use `sed` to remove the custom directive
                PLATFORMS=$(echo -e "$line" | sed 's/# platforms_pr: //')
              elif [[ $PLATFORMS != "" ]]; then
                # break while loop once all custom "PR" event directives are found
                break
              fi
            done <"${{ matrix.dockerfile }}"
          fi
          # directives for all events... above directives will not be parsed if they were already found
          while read -r line; do
            if [[ $line == "# platforms: "* && $PLATFORMS == "" ]]; then
              # echo the line and use `sed` to remove the custom directive
              PLATFORMS=$(echo -e "$line" | sed 's/# platforms: //')
            elif [[ $line == "# artifacts: "* && $ARTIFACTS == "" ]]; then
              # echo the line and use `sed` to remove the custom directive
              ARTIFACTS=$(echo -e "$line" | sed 's/# artifacts: //')
            elif [[ $line == "# no-cache-filters: "* && $NO_CACHE_FILTERS == "" ]]; then
              # echo the line and use `sed` to remove the custom directive
              NO_CACHE_FILTERS=$(echo -e "$line" | sed 's/# no-cache-filters: //')
            elif [[ $PLATFORMS != "" && $ARTIFACTS != "" && $NO_CACHE_FILTERS != "" ]]; then
              # break while loop once all custom directives are found
              break
            fi
          done <"${{ matrix.dockerfile }}"
          # if PLATFORMS is blank, fall back to the legacy method of reading from the `.docker_platforms` file
          if [[ $PLATFORMS == "" ]]; then
            # read the platforms from `.docker_platforms`
            PLATFORMS=$(<.docker_platforms)
          fi
          # if PLATFORMS is still blank, fall back to `linux/amd64`
          if [[ $PLATFORMS == "" ]]; then
            PLATFORMS="linux/amd64"
          fi

          echo "branch=${BRANCH}" >> $GITHUB_OUTPUT
          echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
          echo "commit=${COMMIT}" >> $GITHUB_OUTPUT
          echo "clone_url=${CLONE_URL}" >> $GITHUB_OUTPUT
          echo "release=${RELEASE}" >> $GITHUB_OUTPUT
          echo "artifacts=${ARTIFACTS}" >> $GITHUB_OUTPUT
          echo "no_cache_filters=${NO_CACHE_FILTERS}" >> $GITHUB_OUTPUT
          echo "platforms=${PLATFORMS}" >> $GITHUB_OUTPUT
          echo "push=${PUSH}" >> $GITHUB_OUTPUT
          echo "tags=${TAGS}" >> $GITHUB_OUTPUT

      - name: Set Up QEMU
        uses: docker/setup-qemu-action@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
        id: buildx

      - name: Cache Docker Layers
        uses: actions/cache@v3
        with:
          path: /tmp/.buildx-cache
          key: Docker-buildx${{ matrix.tag }}-${{ github.sha }}
          restore-keys: |
            Docker-buildx${{ matrix.tag }}-

      - name: Log in to Docker Hub
        if: ${{ steps.prepare.outputs.push == 'true' }}  # PRs do not have access to secrets
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

      - name: Log in to the Container registry
        if: ${{ steps.prepare.outputs.push == 'true' }}  # PRs do not have access to secrets
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ secrets.GH_BOT_NAME }}
          password: ${{ secrets.GH_BOT_TOKEN }}

      - name: Build artifacts
        if: ${{ steps.prepare.outputs.artifacts == 'true' }}
        id: build_artifacts
        uses: docker/build-push-action@v4
        with:
          context: ./
          file: ${{ matrix.dockerfile }}
          target: artifacts
          outputs: type=local,dest=artifacts
          push: false
          platforms: ${{ steps.prepare.outputs.platforms }}
          build-args: |
            BRANCH=${{ steps.prepare.outputs.branch }}
            BUILD_DATE=${{ steps.prepare.outputs.build_date }}
            BUILD_VERSION=${{ needs.check_changelog.outputs.next_version }}
            COMMIT=${{ steps.prepare.outputs.commit }}
            CLONE_URL=${{ steps.prepare.outputs.clone_url }}
            RELEASE=${{ steps.prepare.outputs.release }}
          tags: ${{ steps.prepare.outputs.tags }}
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache
          no-cache-filters: ${{ steps.prepare.outputs.no_cache_filters }}

      - name: Build and push
        id: build
        uses: docker/build-push-action@v4
        with:
          context: ./
          file: ${{ matrix.dockerfile }}
          push: ${{ steps.prepare.outputs.push }}
          platforms: ${{ steps.prepare.outputs.platforms }}
          build-args: |
            BRANCH=${{ steps.prepare.outputs.branch }}
            BUILD_DATE=${{ steps.prepare.outputs.build_date }}
            BUILD_VERSION=${{ needs.check_changelog.outputs.next_version }}
            COMMIT=${{ steps.prepare.outputs.commit }}
            CLONE_URL=${{ steps.prepare.outputs.clone_url }}
            RELEASE=${{ steps.prepare.outputs.release }}
          tags: ${{ steps.prepare.outputs.tags }}
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache
          no-cache-filters: ${{ steps.prepare.outputs.no_cache_filters }}

      - name: Arrange Artifacts
        if: ${{ steps.prepare.outputs.artifacts == 'true' }}
        working-directory: artifacts
        run: |
          # artifacts will be in sub directories named after the docker target platform, e.g. `linux_amd64`
          # so move files to the artifacts directory
          # https://unix.stackexchange.com/a/52816
          find ./ -type f -exec mv -t ./ -n '{}' +

          # remove provenance file
          rm -f ./provenance.json

      - name: Upload Artifacts
        if: ${{ steps.prepare.outputs.artifacts == 'true' }}
        uses: actions/upload-artifact@v3
        with:
          name: Docker${{ matrix.tag }}
          path: artifacts/

      - name: Create/Update GitHub Release
        if: ${{ needs.setup_release.outputs.create_release == 'true' && steps.prepare.outputs.artifacts == 'true' }}
        uses: ncipollo/release-action@v1
        with:
          name: ${{ needs.setup_release.outputs.release_name }}
          tag: ${{ needs.setup_release.outputs.release_tag }}
          commit: ${{ needs.setup_release.outputs.release_commit }}
          artifacts: "*artifacts/*"
          token: ${{ secrets.GH_BOT_TOKEN }}
          allowUpdates: true
          body: ${{ needs.setup_release.outputs.release_body }}
          discussionCategory: announcements
          prerelease: ${{ needs.setup_release.outputs.pre_release }}

      - name: Update Docker Hub Description
        if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
        uses: peter-evans/dockerhub-description@v3
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_PASSWORD }}  # token is not currently supported
          repository: ${{ env.BASE_TAG }}
          short-description: ${{ github.event.repository.description }}
          readme-filepath: ./DOCKER_README.md
