# GitLab CI can create duplicate pipelines in certain cases when one event that
# triggers a pipeline (e.g. a push) causes another such event (e.g. the
# resulting update to an MR). This block prevents that in common cases.
workflow:
  rules:
    # If pushing to a branch that has an open MR associated with it, don't
    # launch the on-push pipeline.
    - if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS && $CI_PIPELINE_SOURCE == "push"'
      when: never
    - when: always

# Define the potential stages that can be run in a GitLab CI pipeline job
stages:
  - setup
  - lint
  - test
  - test-dependency-matrix
  - docs
  - test-release
  - release
  - handler

variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
  POETRY_VIRTUALENVS_IN_PROJECT: "true"
  # Force nox to produce colorful logs:
  FORCE_COLOR: "true"

.base:
  image: registry.gitlab.com/tumult-labs/ops/ci/ci-full:latest
  before_script:
    - java -version
    - python --version
    - poetry self show
    # TODO(#2140): This could be done more cleanly using Poetry 1.2 dependency
    #              groups with `--only` instead of using --constraint.
    - pip install nox nox-poetry --constraint=<(poetry export --dev -f constraints.txt --without-hashes)
  artifacts:
    when: always
    expire_in: 1 week
  cache:
    # Cache the pip cache. While the cache could be persisted across changes to
    # the Poetry lock file, clearing it when that changes provides a good way to
    # keep the cache from growing too large due to old packages.
    - key:
        files: ["poetry.lock"]
      paths: [".cache/pip"]
  tags: [aws-small]
  interruptible: true

# "Fake" job to prevent dev pipelines from being interrupted on new commits.
# See gitlab-org/gitlab#34221
prevent_dev_interrupt:
  stage: setup
  script:
    - echo "No-op job to prevent dev pipelines from being interrupted"
  variables:
    GIT_STRATEGY: none
  timeout: 1m
  rules:
    - if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"

package:
  extends: .base
  stage: setup
  needs: []
  image: registry.gitlab.com/tumult-labs/ops/ci/ci-full:latest
  script:
    - nox -s build
  artifacts:
    paths: ["dist/"]
  timeout: 10m
  interruptible: true
  tags: [aws-small]

lint:
  extends: .base
  stage: lint
  needs: ["package"]
  script:
    - nox -t lint -- --check
  timeout: 12m
  rules:
    - if: "$CI_COMMIT_TAG"
      when: never
    - if: '$CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "trigger" || $PARENT_PIPELINE_ID'
      when: never
    - when: on_success

# This is a separate job from a regular "lint"
# so that it can inherit artifacts from core
lint_from_core:
  extends: lint
  needs:
    - project: tumult-labs/core
      job: package_linux
      ref: dev
      artifacts: true
    - job: package
      artifacts: true
  rules:
    - if: "$PARENT_PIPELINE_ID"

# This is a separate job from a regular "test"
# so that it can inherit artifacts from core
test_from_core:
  extends: test
  needs:
    - project: tumult-labs/core
      job: package_linux
      ref: dev
      artifacts: true
    - job: package
      artifacts: true
  rules:
    - if: "$PARENT_PIPELINE_ID"

test:
  extends: .base
  stage: test
  needs: ["package"]
  script:
    # Run doctest before test_fast so that it doesn't overwrite the coverage
    # report from test_fast.
    - nox -s test_smoketest test_doctest test_fast
  coverage: '/^TOTAL.+?(\d+\%)$/'
  artifacts:
    paths: ["coverage/"]
    reports:
      junit: junit.xml
    expose_as: "Coverage"
  timeout: 1h
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule" || $CI_COMMIT_TAG || $PARENT_PIPELINE_ID'
      when: never
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
    - when: manual
      allow_failure: true
  tags: [aws-c6a.xlarge]

test_nightly:
  extends: test
  script:
    - nox -s test_slow
  timeout: 23h
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
    - if: '$CI_COMMIT_TAG || $CI_PIPELINE_SOURCE == "trigger" || $PARENT_PIPELINE_ID'
      when: never
    - when: manual
      allow_failure: true

test_examples:
  extends: test
  script:
    - nox -s test_examples
  coverage: null
  artifacts: null
  timeout: 15m
  tags: [aws-small]

# Requires the following CI variables to be set:
# * NIGHTLY_HANDLER_TOKEN, a GitLab project access token with the read_api permission
# * NIGHTLY_SLACK_WEBHOOK_URL, a Slack incoming webhook for posting to the appropriate
#   channel
nightly_handler:
  stage: handler
  image: registry.gitlab.com/tumult-labs/ops/ci/ci-full:latest
  script:
    - source .pipeline_handlers
    - nightly_handler
  variables:
    GIT_DEPTH: 1
  timeout: 2m
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: always
  interruptible: false

docs:
  extends: .base
  stage: docs
  needs: ["package"]
  script:
    - nox -t docs
  artifacts:
    paths:
      - public/
    expose_as: "Documentation"
  timeout: 25m
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "trigger" || $PARENT_PIPELINE_ID'
      when: never
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_TAG'
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
    - when: manual
      allow_failure: true

test_release_linux:
  extends: .base
  stage: test-release
  needs: ["package"]
  parallel:
    matrix:
      - PYTHON_VERSION: ["3.7", "3.8", "3.9", "3.10", "3.11"]
  image: registry.gitlab.com/tumult-labs/ops/ci/linux:python${PYTHON_VERSION}
  script:
    - nox -s release_smoketest release_test
  coverage: '/^TOTAL.+?(\d+\%)$/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
      junit: junit.xml
  timeout: 1h
  rules:
    - if: '$CI_COMMIT_TAG'
    - if: '$CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "trigger" || $PARENT_PIPELINE_ID'
      when: never
    - when: manual
      allow_failure: true
  tags: [aws-c6a.xlarge]

test_dep_matrix:
  extends: .base
  needs: ["package"]
  stage: test-dependency-matrix
  parallel:
    matrix:
      - PYTHON_VERSION: ["3.7", "3.8", "3.9", "3.10", "3.11"]
        TEST_TYPE: ["oldest", "newest"]
      - PYTHON_VERSION: ["3.7"]
        TEST_TYPE: ["pyspark3.1", "pyspark3.2"]
  image: registry.gitlab.com/tumult-labs/ops/ci/linux:python${PYTHON_VERSION}
  rules:
    - if: '$CI_COMMIT_TAG || $CI_PIPELINE_SOURCE == "trigger" || $PARENT_PIPELINE_ID'
      when: never
    - when: manual
      allow_failure: true
  script:
    - pip install -U pip setuptools
    - nox -s "test_multi_deps(${PYTHON_VERSION}-${TEST_TYPE})"
  cache:
    - key:
        prefix: "$PYTHON_VERSION-$TEST_TYPE"
        files: ["poetry.lock"]
      paths: [".cache/pip"]
  timeout: 3h
  tags: [aws-c6a.xlarge]
  interruptible: true

publish:
  stage: release
  dependencies: [package]
  image: registry.gitlab.com/tumult-labs/ops/ci/ci-full:latest
  script:
    - poetry publish -u __token__ -p "${RELEASE_PYPI_TOKEN}"
  timeout: 3m
  rules:
    - if: '$CI_COMMIT_TAG'
  interruptible: false

# This job requires the DOCS_TOKEN_USER and DOCS_TOKEN variables to be set. They
# should be the name and token of a project access token from the
# tumult-labs/ops/docs.tmlt.dev project with the read_repository and
# write_repository permissions. The bot user for this token must have permission
# to push the protected `prod` branch.
publish_docs:
  stage: release
  image: registry.gitlab.com/tumult-labs/ops/ci/ci-full:latest
  dependencies: [docs]
  variables:
    GIT_STRATEGY: none
  before_script:
    - |
      if [[ -z "$DOCS_TOKEN_USER" || -z "$DOCS_TOKEN" ]]; then
        echo "Credentials for publishing to docs repo are not set"
        exit 1
      fi
    - |
      if [[ -z "$CI_COMMIT_TAG" ]]; then
        version="$CI_COMMIT_BRANCH"
        docs_version="$version"
      else
        version="$CI_COMMIT_TAG"
        # Check if this tag is a pre-release.
        if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-(alpha|beta|rc)\.[0-9]+$ ]]; then
          echo "Version $version is a pre-release, skipping docs publication."
          exit 0
        fi
        # Convert X.Y.Z semantic version to vX.Y for docs.
        docs_version="v$(echo $version | sed -E 's/^([[:digit:]]+\.[[:digit:]]+).*/\1/')"
      fi
    - echo "Publishing version $version as $docs_version..."
  script:
    - git clone https://$DOCS_TOKEN_USER:$DOCS_TOKEN@gitlab.com/tumult-labs/ops/docs.tmlt.dev.git
    # Remove old docs published as this version, if any
    - rm -rf docs.tmlt.dev/public/analytics/$docs_version
    - mv public/ docs.tmlt.dev/public/analytics/$docs_version
    - cd docs.tmlt.dev
    - git config user.name "gitlab"
    - git config user.email "gitlab@tmlt.io"
    - git add public
    - git diff --staged --stat
    - git commit -m "[auto] Publish docs for $version ($CI_COMMIT_SHORT_SHA)" --author "$CI_COMMIT_AUTHOR"
    - git push
  timeout: 3m
  interruptible: false
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
    - if: '$CI_COMMIT_TAG'

# Requires the following CI variables to be set:
# * VERSION, the semantic version number to be given to the release. This would
#   typically be specified when launching the manual job.
# * RELEASER_TOKEN_USERNAME and RELEASER_TOKEN, a GitLab project access token
#   (and an associated username) with the api permission and the ability to push
#   tags.
trigger_release:
  extends: .base
  stage: handler
  script:
    - |
      if [[ -z "$RELEASER_TOKEN_USERNAME" || -z "$RELEASER_TOKEN" ]]; then
        echo "RELEASER_TOKEN and/or RELEASER_TOKEN_USERNAME is unset, exiting..."
        exit 1
      fi
    - nox -s prepare_release
    - git config user.name "gitlab"
    - git config user.email "gitlab@tmlt.io"
    - git checkout -B "release/$VERSION"
    - git add -u
    - git diff --staged --stat
    # This commit may be empty for pre-releases, because they don't update the changelog.
    - git commit -m "[auto] Release $VERSION" --author "$CI_COMMIT_AUTHOR" --allow-empty
    - git tag "$VERSION"
    - nox -s post_release
    - git add -u
    - git diff --staged --stat
    - git commit -m "[auto] Post-release $VERSION" --author "$CI_COMMIT_AUTHOR" --allow-empty
    - |
      git remote add origin-https \
          "https://$RELEASER_TOKEN_USERNAME:$RELEASER_TOKEN@gitlab.com/$CI_PROJECT_PATH.git"
    - git push origin-https "release/$VERSION" --push-option ci.skip
    - git push origin-https "$VERSION"
    - |
      cat > body.json <<EOF
      {
        "source_branch": "release/$VERSION",
        "target_branch": "$CI_COMMIT_BRANCH",
        "title": "Merge release $VERSION",
        "assignee_id": $GITLAB_USER_ID,
        "remove_source_branch": true,
        "squash": false,
        "approvals_before_merge": 1
      }
      EOF
    - cat body.json
    - |
      http_status=$(
        curl -XPOST -s -o response.json -w "%{http_code}" \
            "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests" \
            --header "PRIVATE-TOKEN: $RELEASER_TOKEN" \
            -H "Content-Type: application/json" -d "@body.json"
      )
      echo "Response body:"
      cat response.json | jq
      if [[ $http_status -ne 201 ]]; then
        echo "Got unexpected HTTP status $http_status, exiting..."
        exit 1
      fi
  timeout: 5m
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule" && ($CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_BRANCH =~ /^stable\/.*$/)'
      when: manual
  interruptible: false

# Requires the following CI variables to be set:
# * RELEASE_SLACK_WEBHOOK_URL, a Slack incoming webhook for posting to the appropriate
#   channel
release_handler:
  stage: handler
  script:
    - source .pipeline_handlers
    - release_handler
  timeout: 2m
  rules:
    - if: '$CI_COMMIT_TAG'
  interruptible: false
