name: Continuous integration

on:
  push:
  workflow_dispatch:
    inputs:
      simulatorVersion:
        description: New version of the underlying simulation tool (e.g., '1.5.6')
        required: true
  schedule:
    - cron: "0 0 * * *"

jobs:
  continuousIntegration:
    name: Update simulator version, lint, test, compile documentation, and release
    runs-on: ubuntu-latest
    outputs:
      mainBranch: ${{ steps.get-main-branch.outputs.mainBranch }}
      mainBranchRef: ${{ steps.get-main-branch.outputs.mainBranchRef }}
      mainBranchHeadRevision: ${{ steps.get-main-branch.outputs.mainBranchHeadRevision }}
      version: ${{ steps.get-version-number.outputs.version }}
      simulatorId: ${{ steps.get-docker-image-tag.outputs.simulatorId }}
      simulatorVersion: ${{ steps.get-docker-image-tag.outputs.simulatorVersion }}
      simulatorName: ${{ steps.get-docker-image-tag.outputs.simulatorName }}
      dockerImageBaseUrl: ${{ steps.get-docker-image-tag.outputs.dockerImageBaseUrl }}
      dockerRegistry: ${{ steps.get-docker-image-tag.outputs.dockerRegistry }}
      release: ${{ steps.determine-if-release-needed.outputs.release }}
      docsChanged: ${{ steps.commit-docs.outputs.docsChanged }}
    steps:
      - name: Clone repository
        run: |
          git clone https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} .

      - id: get-main-branch
        name: Determine main branch
        run: |
          mainBranch=$(git symbolic-ref refs/remotes/origin/HEAD | cut -d '/' -f 4)
          mainBranchHeadRevision=$(git rev-parse refs/remotes/origin/${mainBranch})
          echo "::set-output name=mainBranch::$mainBranch"
          echo "::set-output name=mainBranchRef::refs/heads/$mainBranch"
          echo "::set-output name=mainBranchHeadRevision::$mainBranchHeadRevision"

      - name: Checkout ref
        run: |
          if [[ "${{ github.ref }}" =~ ^refs/heads/ ]]; then
            branch=$(echo "${{ github.ref }}" | cut -d'/' -f 3-)
            git checkout ${branch}
          else
            git checkout ${{ github.ref }}
          fi

      #############################################
      ## Install package and its dependencies
      #############################################
      - name: Install Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.9'

      - name: Setup pip cache
        uses: actions/cache@v2
        with:
          path: /opt/hostedtoolcache/Python
          key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements.optional.txt') }}
          restore-keys: |
            ${{ runner.os }}-pip-

      - name: Install pip and setuptools
        run: |
          python -m pip install --upgrade pip
          python -m pip install --upgrade setuptools

      # install package
      - name: Install the package
        run: python -m pip install .[all]

      #############################################
      ## Update the version of the simulator
      #############################################
      - id: update-simulator-version
        name: Update the version of the simulator
        if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
        run: |
          if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
            simulatorVersion="${{ github.event.inputs.simulatorVersion }}"
          else
            python -m pip install -U python-copasi
            simulatorVersion=$(python -m pip show python-copasi | grep "Version: " | cut -d " " -f 2)
          fi

          simulatorSpecVersion=$(jq -r '.version' biosimulators.json)
          set +e
          dpkg --compare-versions "$simulatorVersion" ge "$simulatorSpecVersion"
          if [[ $? = 0 ]]; then
            simulatorVersionLatest="true"
          else
            simulatorVersionLatest="false"
          fi
          dpkg --compare-versions "$simulatorVersion" gt "$simulatorSpecVersion"
          if [[ $? = 0 ]]; then
            simulatorVersionNewer="true"
          else
            simulatorVersionNewer="false"
          fi
          set -e

          sudo apt-get update -y
          sudo apt-get install -y --no-install-recommends jq moreutils

          IMAGE_BASE_URL=$(jq -r '.image.url' biosimulators.json | cut -d : -f 1)
          jq ".version = \"${simulatorVersion}\"" biosimulators.json | sponge biosimulators.json
          jq ".image.url = \"${IMAGE_BASE_URL}:${simulatorVersion}\"" biosimulators.json | sponge biosimulators.json

          sed -i -E \
            "s/SIMULATOR_VERSION=([^ \n]+|\".*?\")/SIMULATOR_VERSION=\"${simulatorVersion}\"/" \
            Dockerfile

          echo "::set-output name=simulatorVersion::$simulatorVersion"
          echo "::set-output name=simulatorVersionLatest::$simulatorVersionLatest"
          echo "::set-output name=simulatorVersionNewer::$simulatorVersionNewer"

      - name: Cancel build if the specifications already have the latest version of COPASI
        uses: andymckay/cancel-action@0.2
        if: github.event_name == 'schedule' && steps.update-simulator-version.outputs.simulatorVersionNewer != 'true'

      #############################################
      ## Lint
      #############################################
      - name: Install flake8
        run: python -m pip install flake8

      - name: Lint the package
        run: python -m flake8

      #############################################
      ## Build Docker image
      #############################################
      - id: get-version-number
        name: Get version number
        env:
          TAG: ${{ github.ref }}
        run: |
          if [[ "${TAG}" =~ ^refs/tags/ ]]; then
            version="${TAG/refs\/tags\//}"
          else
            version=$(python -c "import glob; import importlib.util; version_filename = glob.glob('**/_version.py', recursive=True)[0]; spec = importlib.util.spec_from_file_location('module.name', version_filename); module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module); print(module.__version__)")
          fi

          echo "::set-output name=version::$version"

      - id: get-docker-image-tag
        name: Determine Docker image tag
        run: |
          sudo apt-get update -y
          sudo apt-get install -y --no-install-recommends jq

          SIMULATOR_ID=$(jq -r '.id' biosimulators.json)
          SIMULATOR_VERSION=$(jq -r '.version' biosimulators.json)
          SIMULATOR_NAME=$(jq -r '.name' biosimulators.json)
          IMAGE_BASE_URL=$(jq -r '.image.url' biosimulators.json | cut -d : -f 1)
          DOCKER_REGISTRY=$(echo $IMAGE_BASE_URL | cut -d / -f 1)

          echo "::set-output name=simulatorId::${SIMULATOR_ID}"
          echo "::set-output name=simulatorVersion::${SIMULATOR_VERSION}"
          echo "::set-output name=simulatorName::${SIMULATOR_NAME}"
          echo "::set-output name=dockerImageBaseUrl::${IMAGE_BASE_URL}"
          echo "::set-output name=dockerRegistry::${DOCKER_REGISTRY}"

      - name: Build Docker image
        run: |
          REVISION=$(git rev-parse HEAD)
          CREATED=$(date --rfc-3339=seconds | sed 's/ /T/')
          docker build \
            --label org.opencontainers.image.source=https://github.com/${{ github.repository }} \
            --label org.opencontainers.image.revision=${REVISION} \
            --label org.opencontainers.image.created=${CREATED} \
            --build-arg VERSION=${{ steps.get-version-number.outputs.version }} \
            --build-arg SIMULATOR_VERSION=${{ steps.get-docker-image-tag.outputs.simulatorVersion }} \
            --tag ${{ steps.get-docker-image-tag.outputs.dockerImageBaseUrl }}:${{ steps.get-docker-image-tag.outputs.simulatorVersion }} \
            --tag ${{ steps.get-docker-image-tag.outputs.dockerImageBaseUrl }}:latest \
            .

      #############################################
      ## Test and upload coverage report to Codecov
      #############################################
      - name: Install pytest
        run: python -m pip install pytest pytest-cov

      - name: Install the requirements for the tests
        run: python -m pip install .[tests]

      - name: Run the tests
        run: python -m pytest tests/ --cov=./biosimulators_copasi/ --cov-report=xml

      - name: Upload the coverage report to Codecov
        uses: codecov/codecov-action@v2
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          flags: unittests
          file: ./coverage.xml

      #############################################
      ## Compile documentation
      #############################################
      - name: Install the requirements for compiling the documentation
        run: python -m pip install -r docs-src/requirements.txt

      - name: Compile the documentation
        run: |
          sphinx-apidoc . setup.py --output-dir docs-src/source --force --module-first --no-toc
          mkdir -p docs-src/_static
          sphinx-build docs-src docs

      #############################################
      ## Commit and push new version of simulator
      #############################################
      # If new version of simulator, commit and push the new version
      - name: Commit the revised version of the simulator
        if: steps.update-simulator-version.outputs.simulatorVersion && steps.update-simulator-version.outputs.simulatorVersionLatest == 'true'
        run: |
          git config --local user.email "biosimulators.daemon@gmail.com"
          git config --local user.name "biosimulatorsdaemon"
          git config pull.rebase false
          git stash -- biosimulators.json Dockerfile
          git clean -f -d
          git checkout .
          git pull
          git stash pop
          git add biosimulators.json Dockerfile
          git commit -m "Updating version of simulator"

      - name: Push the revised version of the simulator
        if: steps.update-simulator-version.outputs.simulatorVersion && steps.update-simulator-version.outputs.simulatorVersionLatest == 'true'
        uses: ad-m/github-push-action@master
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          branch: ${{ github.ref }}

      #############################################
      ## Release
      #############################################

      - id: determine-if-release-needed
        name: Determine if a release should be made
        run: |
          release="0"

          if [ ! -z "${{ steps.update-simulator-version.outputs.simulatorVersion }}" ]; then
            if [ "${{ github.ref }}" == "${{ steps.get-main-branch.outputs.mainBranchRef }}" ]; then
              release="1"
            fi
          fi

          if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
            tag_hash=$(git rev-parse "${{ github.ref }}")
            if [ "$tag_hash" == "${{ steps.get-main-branch.outputs.mainBranchHeadRevision }}" ]; then
              release="1"
            fi
          fi

          echo "::set-output name=release::$release"

      # If new tag, commit and push documentation
      - id: commit-docs
        name: Commit the compiled documentation
        if: startsWith(github.ref, 'refs/tags/') && steps.determine-if-release-needed.outputs.release == '1'
        run: |
          git config --local user.email "biosimulators.daemon@gmail.com"
          git config --local user.name "biosimulatorsdaemon"
          git config pull.rebase false
          git stash
          git checkout ${{ steps.get-main-branch.outputs.mainBranch }}
          git pull
          set +e
          git stash pop
          git add docs
          git commit -m "Updating compiled documentation"
          git checkout .
          git clean -f -d
          if [[ $? = 0 ]]; then
            docsChanged=1
          else
            docsChanged=0
          fi
          echo "::set-output name=docsChanged::$docsChanged"

      - name: Push the compiled documentation
        if: startsWith(github.ref, 'refs/tags/') && steps.determine-if-release-needed.outputs.release == '1' && steps.commit-docs.outputs.docsChanged == '1'
        uses: ad-m/github-push-action@master
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          branch: ${{ steps.get-main-branch.outputs.mainBranch }}

      # Create GitHub release
      - name: Create GitHub release
        if: startsWith(github.ref, 'refs/tags/') && steps.determine-if-release-needed.outputs.release == '1'
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ steps.get-version-number.outputs.version }}
          release_name: Release ${{ steps.get-version-number.outputs.version }}

      # Create PyPI release
      - name: Create PyPI release
        if: startsWith(github.ref, 'refs/tags/') && steps.determine-if-release-needed.outputs.release == '1'
        env:
          TWINE_USERNAME: __token__
          TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
        run: |
          # Install pandoc
          sudo apt-get update -y
          sudo apt-get install -y --no-install-recommends wget

          wget https://github.com/jgm/pandoc/releases -O /tmp/pandocVersions.html
          urlPart=`grep "\.deb" /tmp/pandocVersions.html | head -n 1 | cut -d'/' -f2-7 | cut -d'"' -f1`
          wget "https://github.com/$urlPart" -O /tmp/pandoc.deb
          sudo dpkg -i /tmp/pandoc.deb
          rm /tmp/pandocVersions.html
          rm /tmp/pandoc.deb

          # Convert README to .rst format
          pandoc --from=gfm --output=README.rst --to=rst README.md

          # Install twine
          python -m pip install wheel twine

          # Create packages to upload to PyPI
          python setup.py sdist
          python setup.py bdist_wheel

          # Upload packages to PyPI
          twine upload dist/*

      # build Docker image and push to GitHub Container Registry
      - name: Push Docker image
        if: steps.determine-if-release-needed.outputs.release == '1'
        run: |
          docker login ${{ steps.get-docker-image-tag.outputs.dockerRegistry }} \
            --username ${{ secrets.DOCKER_REGISTRY_USERNAME }} \
            --password ${{ secrets.DOCKER_REGISTRY_TOKEN }}
          docker push ${{ steps.get-docker-image-tag.outputs.dockerImageBaseUrl }}:${{ steps.get-docker-image-tag.outputs.simulatorVersion }}

          if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] || [ "${{ steps.update-simulator-version.outputs.simulatorVersionLatest }}" == "true" ]; then
            docker push ${{ steps.get-docker-image-tag.outputs.dockerImageBaseUrl }}:latest
          fi

      # Submit to BioSimulators registry
      - name: Submit to BioSimulators registry
        if: steps.determine-if-release-needed.outputs.release == '1'
        run: |
          REVISION=$(git rev-parse HEAD)
          IMAGE_DIGEST=$(docker image inspect ${{ steps.get-docker-image-tag.outputs.dockerImageBaseUrl }}:${{ steps.get-docker-image-tag.outputs.simulatorVersion }} | jq -r '.[0].RepoDigests[0]' | cut -d "@" -f 2-)
          curl \
            -X POST \
            -u ${{ secrets.GH_ISSUE_USERNAME }}:${{ secrets.GH_ISSUE_TOKEN }} \
            -H "Accept: application/vnd.github.v3+json" \
            https://api.github.com/repos/biosimulators/Biosimulators/issues \
            -d "{\"labels\": [\"Validate/submit simulator\"], \"title\": \"Submit ${{ steps.get-docker-image-tag.outputs.simulatorName }} ${{ steps.get-docker-image-tag.outputs.simulatorVersion }}\", \"body\": \"---\nid: ${{ steps.get-docker-image-tag.outputs.simulatorId }}\nversion: ${{ steps.get-docker-image-tag.outputs.simulatorVersion }}\nspecificationsUrl: https://raw.githubusercontent.com/${{ github.repository }}/${REVISION}/biosimulators.json\nspecificationsPatch:\n  version: ${{ steps.get-docker-image-tag.outputs.simulatorVersion }}\n  image:\n    url: ${{ steps.get-docker-image-tag.outputs.dockerImageBaseUrl }}:${{ steps.get-docker-image-tag.outputs.simulatorVersion }}\n    digest: \\\"${IMAGE_DIGEST}\\\"\nvalidateImage: true\ncommitSimulator: true\n\n---\"}"
