# =========================================================================================
# Copyright (C) 2021 Orange & contributors
#
# This program is free software; you can redistribute it and/or modify it under the terms 
# of the GNU Lesser General Public License as published by the Free Software Foundation; 
# either version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along with this 
# program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth 
# Floor, Boston, MA  02110-1301, USA.
# =========================================================================================
# default workflow rules: Merge Request pipelines
workflow:
  rules:
    # prevent branch pipeline when an MR is open (prefer MR pipeline)
    - if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS'
      when: never
    - when: always

# test job prototype: implement adaptive pipeline rules
.test-policy:
  rules:
    # on tag: auto & failing
    - if: $CI_COMMIT_TAG
    # on ADAPTIVE_PIPELINE_DISABLED: auto & failing
    - if: '$ADAPTIVE_PIPELINE_DISABLED == "true"'
    # on production or integration branch(es): auto & failing
    - if: '$CI_COMMIT_REF_NAME =~ $PROD_REF || $CI_COMMIT_REF_NAME =~ $INTEG_REF'
    # early stage (dev branch, no MR): manual & non-failing
    - if: '$CI_MERGE_REQUEST_ID == null && $CI_OPEN_MERGE_REQUESTS == null'
      when: manual
      allow_failure: true
    # Draft MR: auto & non-failing
    - if: '$CI_MERGE_REQUEST_TITLE =~ /^Draft:.*/'
      allow_failure: true
    # else (Ready MR): auto & failing
    - when: on_success

variables:
  # variabilized tracking image
  TBC_TRACKING_IMAGE: "$CI_REGISTRY/to-be-continuous/tools/tracking:master"

  # Default ng workspace
  NG_WORKSPACE_DIR: .

  # Default Docker image for ANGULAR CLI (can be overridden)
  NG_CLI_IMAGE: trion/ng-cli-karma:latest

  # Angular lint
  NG_LINT_ARGS: "lint"

  # Angular test
  NG_TEST_ARGS: >-
    test
    --code-coverage
    --reporters progress,junit

  NG_E2E_ARGS: >-
    e2e

  # Angular Build
  NG_BUILD_ARGS: "build --prod"

  # default production ref name (pattern)
  PROD_REF: '/^(master|main)$/'
  # default integration ref name (pattern)
  INTEG_REF: '/^develop$/'

  # ==================================================
  # Variables for publication
  # ==================================================

  # NG_PUBLISH_ENABLED

  # List of projects to publish, use space (" ") for separation
  # ex: NG_PUBLISH_PROJECTS: "Project1 Project2 myLib"
  # By default, NG_PUBLISH_PROJECTS is the value of angular.json, "defaultProject" property

  # Set some args of `npm publish` command
  # ex: NG_PUBLISH_ARGS: "--dry-run"
  NG_PUBLISH_ARGS: '--verbose'

# ==================================================
# Stages definition
# ==================================================
stages:
  - build
  - test
  - publish

###############################################################################################
#                                Script definition                                            #
###############################################################################################
.ng-cli-scripts: &ng-cli-scripts |
  # BEGSCRIPT
  set -e

  function log_debug() {
    if [[ -n "$TRACE" ]]
    then
      echo -e "[\\e[1;36mTRACE\\e[0m] $*"
    fi
  }

  function log_info() {
      echo -e "[\\e[1;94mINFO\\e[0m] $*"
  }

  function log_warn() {
      echo -e "[\\e[1;93mWARN\\e[0m] $*"
  }

  function log_error() {
      echo -e "[\\e[1;91mERROR\\e[0m] $*"
  }

  function assert_defined() {
    if [[ -z "$1" ]]
    then
      log_error "$2"
      exit 1
    fi
  }

  function sonar_lint_report() {
    if [[ "$SONAR_HOST_URL" ]] || [[ "$SONAR_URL" ]]
    then
      mkdir -p -m 777 reports
      # generate ts lint report in json for SONARqube
      # shellcheck disable=SC2086
      ng $NG_LINT_ARGS --format=json --force > reports/ng-lint.tslint.json
    fi
  }

  function install_ca_certs() {
    certs=$1
    if [[ -z "$certs" ]]
    then
      return
    fi

    # import in system
    if echo "$certs" >> /etc/ssl/certs/ca-certificates.crt
    then
      log_info "CA certificates imported in \\e[33;1m/etc/ssl/certs/ca-certificates.crt\\e[0m"
    fi
    if echo "$certs" >> /etc/ssl/cert.pem
    then
      log_info "CA certificates imported in \\e[33;1m/etc/ssl/cert.pem\\e[0m"
    fi

    # import in Java keystore (if keytool command found)
    if command -v keytool > /dev/null
    then
      # shellcheck disable=SC2046
      javahome=${JAVA_HOME:-$(dirname $(readlink -f $(command -v java)))/..}
      # shellcheck disable=SC2086
      keystore=${JAVA_KEYSTORE_PATH:-$(ls -1 $javahome/jre/lib/security/cacerts 2>/dev/null || ls -1 $javahome/lib/security/cacerts 2>/dev/null || echo "")}
      if [[ -f "$keystore" ]]
      then
        storepass=${JAVA_KEYSTORE_PASSWORD:-changeit}
        nb_certs=$(echo "$certs" | grep -c 'END CERTIFICATE')
        log_info "importing $nb_certs certificates in Java keystore \\e[33;1m$keystore\\e[0m..."
        for idx in $(seq 0 $((nb_certs - 1)))
        do
          # TODO: use keytool option -trustcacerts ?
          if echo "$certs" | awk "n==$idx { print }; /END CERTIFICATE/ { n++ }" | keytool -noprompt -import -alias "imported CA Cert $idx" -keystore "$keystore" -storepass "$storepass"
          then
            log_info "... CA certificate [$idx] successfully imported"
          else
            log_warn "... Failed importing CA certificate [$idx]: abort"
            return
          fi
        done
      else
        log_warn "Java keystore \\e[33;1m$keystore\\e[0m not found: could not import CA certificates"
      fi
    fi
  }

  function unscope_variables() {
    _scoped_vars=$(env | awk -F '=' "/^scoped__[a-zA-Z0-9_]+=/ {print \$1}" | sort)
    if [[ -z "$_scoped_vars" ]]; then return; fi
    log_info "Processing scoped variables..."
    for _scoped_var in $_scoped_vars
    do
      _fields=${_scoped_var//__/:}
      _condition=$(echo "$_fields" | cut -d: -f3)
      case "$_condition" in
      if) _not="";;
      ifnot) _not=1;;
      *)
        log_warn "... unrecognized condition \\e[1;91m$_condition\\e[0m in \\e[33;1m${_scoped_var}\\e[0m"
        continue
      ;;
      esac
      _target_var=$(echo "$_fields" | cut -d: -f2)
      _cond_var=$(echo "$_fields" | cut -d: -f4)
      _cond_val=$(eval echo "\$${_cond_var}")
      _test_op=$(echo "$_fields" | cut -d: -f5)
      case "$_test_op" in
      defined)
        if [[ -z "$_not" ]] && [[ -z "$_cond_val" ]]; then continue; 
        elif [[ "$_not" ]] && [[ "$_cond_val" ]]; then continue; 
        fi
        ;;
      equals|startswith|endswith|contains|in|equals_ic|startswith_ic|endswith_ic|contains_ic|in_ic)
        # comparison operator
        # sluggify actual value
        _cond_val=$(echo "$_cond_val" | tr '[:punct:]' '_')
        # retrieve comparison value
        _cmp_val_prefix="scoped__${_target_var}__${_condition}__${_cond_var}__${_test_op}__"
        _cmp_val=${_scoped_var#"$_cmp_val_prefix"}
        # manage 'ignore case'
        if [[ "$_test_op" == *_ic ]]
        then
          # lowercase everything
          _cond_val=$(echo "$_cond_val" | tr '[:upper:]' '[:lower:]')
          _cmp_val=$(echo "$_cmp_val" | tr '[:upper:]' '[:lower:]')
        fi
        case "$_test_op" in
        equals*)
          if [[ -z "$_not" ]] && [[ "$_cond_val" != "$_cmp_val" ]]; then continue; 
          elif [[ "$_not" ]] && [[ "$_cond_val" == "$_cmp_val" ]]; then continue; 
          fi
          ;;
        startswith*)
          if [[ -z "$_not" ]] && [[ "$_cond_val" != "$_cmp_val"* ]]; then continue; 
          elif [[ "$_not" ]] && [[ "$_cond_val" == "$_cmp_val"* ]]; then continue; 
          fi
          ;;
        endswith*)
          if [[ -z "$_not" ]] && [[ "$_cond_val" != *"$_cmp_val" ]]; then continue; 
          elif [[ "$_not" ]] && [[ "$_cond_val" == *"$_cmp_val" ]]; then continue; 
          fi
          ;;
        contains*)
          if [[ -z "$_not" ]] && [[ "$_cond_val" != *"$_cmp_val"* ]]; then continue; 
          elif [[ "$_not" ]] && [[ "$_cond_val" == *"$_cmp_val"* ]]; then continue; 
          fi
          ;;
        in*)
          if [[ -z "$_not" ]] && [[ "__${_cmp_val}__" != *"__${_cond_val}__"* ]]; then continue; 
          elif [[ "$_not" ]] && [[ "__${_cmp_val}__" == *"__${_cond_val}__"* ]]; then continue; 
          fi
          ;;
        esac
        ;;
      *)
        log_warn "... unrecognized test operator \\e[1;91m${_test_op}\\e[0m in \\e[33;1m${_scoped_var}\\e[0m"
        continue
        ;;
      esac
      # matches
      _val=$(eval echo "\$${_target_var}")
      log_info "... apply \\e[32m${_target_var}\\e[0m from \\e[32m\$${_scoped_var}\\e[0m${_val:+ (\\e[33;1moverwrite\\e[0m)}"
      _val=$(eval echo "\$${_scoped_var}")
      export "${_target_var}"="${_val}"
    done
    log_info "... done"
  }

  function read_default_project() {
    # current directory should be ${NG_WORKSPACE_DIR}
    angular_json=./angular.json
    default_project=$(node -pe "require('${angular_json}').defaultProject")
    default_project_type=$(node -pe "require('${angular_json}').projects['${default_project}'].projectType")
  }

  function run_ng_build() {
    read_default_project
    case ${default_project_type} in
        library)
          # shellcheck disable=SC2086
          ng $NG_BUILD_ARGS
          ;;
        *)
          # shellcheck disable=SC2086
          ng $NG_BUILD_ARGS --no-progress
          ;;
    esac
  }

  function run_ng_build_libs() {
    # current directory should be ${NG_WORKSPACE_DIR}
    angular_json=./angular.json
    libs=$(node -pe "Object.entries(require('${angular_json}').projects).filter(entry => entry[1].projectType === 'library').map(entry => entry[0]).join(' ')")
    log_debug "parsed libs: ${libs}"
    if [ -n "${libs}" ]; then
      log_info "libs detected: ${libs}"
      for lib in ${libs}; do
        log_debug "building lib ${lib}"
        ng build "${lib}"
      done
    fi
  }

  function configure_gitlab_instance_level_npm_registry_auth() {
    npm config set "//${CI_SERVER_HOST}/api/v4/packages/npm/:_authToken" "${CI_JOB_TOKEN}"
  }

  function compute_gitlab_registry_url() {
    gitlab_registry_url="https://${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm"
  }

  function configure_npm_publish_registry_auth() {
    publish_registry=${NPM_PUBLISH_REGISTRY}
    if [[ "${publish_registry}" ]]; then
      log_info "configured publish registry: ${publish_registry}"
      if [[ "$NPM_PUBLISH_TOKEN" ]]; then
        shopt -s extglob
        npm_publish_registry_host_and_path=${publish_registry/#http?(s):/}
        shopt -u extglob
        npm config set "${npm_publish_registry_host_and_path}:_authToken" "${NPM_PUBLISH_TOKEN}"
      fi
    else
      compute_gitlab_registry_url
      log_info "No defined publication registry, falling back to GitLab packages ${gitlab_registry_url}"
      publish_registry=${gitlab_registry_url}
      log_info "be sure to have your projects name starting with @${CI_PROJECT_ROOT_NAMESPACE}/"
      npm config set "@${CI_PROJECT_ROOT_NAMESPACE}:registry" "https://${CI_SERVER_HOST}/api/v4/packages/npm/"
      npm config set "//${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken" "${CI_JOB_TOKEN}"
    fi

  }

  function npm_publish() {
    projects_to_publish=${NG_PUBLISH_PROJECTS}
    if [ -z "${projects_to_publish}" ]; then
      read_default_project
      log_info "No projects specified, falling back to default project: \\e[33;1m${default_project}\\e[0m."
      projects_to_publish=${default_project}
    fi

    log_info "Publishing the following projects: ${projects_to_publish}..."
    # current directory should be ${NG_WORKSPACE_DIR}
    angular_json=./angular.json
    package_json=./package.json

    # first we check all specified projects exist
    for project in ${projects_to_publish}; do
      if ! projectRoot=$(node -pe "require('${angular_json}').projects['${project}'].root"); then
        log_error "unknown project: ${project}";
        exit 1;
      fi
    done

    for project in ${projects_to_publish}; do

      projectTypeSelector="projects['${project}'].projectType"
      projectType=$(node -pe "require('${angular_json}').${projectTypeSelector}")

      case ${projectType} in
      library)
        # output dir for library is defined in ng-package.json
        projectRoot=$(node -pe "require('${angular_json}').projects['${project}'].root")
        projectRelativeOutputDir=$(node -pe "require('./${projectRoot}/ng-package.json').dest")
        projectOutputDir=${projectRoot}/${projectRelativeOutputDir}
        ;;
      *)
        # other output dir is defined in angular.json
        projectOutputDir=$(node -pe "require('${angular_json}').projects['${project}'].architect.build.options.outputPath")

        log_warn "It is not recommended to publish a project with '${projectType}' type as npm package."
        log_warn "Please consider using another packaging solution as this support might be limited."

        # WORKAROUND:
        # as application type projects don't generate a package.json which is required for publishing
        # we generate one with the global package.json version
        if [ ! -f "${projectOutputDir}/package.json" ]; then
          global_version=$(node -pe "require('${package_json}').version")
          cat > "${projectOutputDir}"/package.json <<- EOF
  {
    "name": "${project}",
    "version": "${global_version}"
  }
  EOF
        fi
        ;;
      esac

      projectOutputDir=$(realpath "${projectOutputDir}")
      if [ ! -d "${projectOutputDir}" ]; then
        log_error "'${project}' project output folder '${projectOutputDir}' not found"
        log_error "please check that module '${project}' has been built"
        exit 1
      fi

      log_info "Publishing project '${project}' from ${projectOutputDir}"
      npm publish "${projectOutputDir}" "${NG_PUBLISH_ARGS}"
    done

  }

  unscope_variables

  # ENDSCRIPT

###############################################################################################
#                                Generic job definition                                       #
###############################################################################################
.ng-cli-base:
  image: $NG_CLI_IMAGE
  services:
    - name: "$TBC_TRACKING_IMAGE"
      command: ["--service", "angular", "3.2.1"]
  # cache configuration
  cache:
    key: "$CI_COMMIT_REF_SLUG-angular"
    paths:
      - ${NG_WORKSPACE_DIR}/.npm/
  before_script:
    - *ng-cli-scripts
    - cd ${NG_WORKSPACE_DIR}
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    # NPM_CONFIG_REGISTRY is not supported by old npm versions: force with cli
    - if [[ "$NPM_CONFIG_REGISTRY" ]]; then npm config set registry $NPM_CONFIG_REGISTRY; fi
    - |
      if [[ "$NPM_CONFIG_SCOPED_REGISTRIES" ]];
      then
        for scoped_registry in $NPM_CONFIG_SCOPED_REGISTRIES
        do
          npm config set "${scoped_registry%%:*}":registry "${scoped_registry#*:}";
        done
      fi
    - configure_gitlab_instance_level_npm_registry_auth
    - npm ci --cache .npm --prefer-offline

###############################################################################################
#                                      check stage:                                           #
#                                        - ng-lint                                            #
###############################################################################################
ng-lint:
  extends: .ng-cli-base
  stage: build
  script:
    # generate lint report for sonar
    - sonar_lint_report
    # display ng lint result for job console
    - ng $NG_LINT_ARGS
  artifacts:
    when: always # save artifact even if test failed
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    paths:
      - $NG_WORKSPACE_DIR/reports
    expire_in: 1 day
  rules:
    - if: '$NG_LINT_DISABLED == "true"'
      when: never
    - !reference [.test-policy, rules]

###############################################################################################
#                                      build stage:                                           #
#                                        - ng-build                                           #
###############################################################################################
ng-build:
  extends: .ng-cli-base
  stage: build
  script:
    # build libs so they are available for other projects tests
    - run_ng_build_libs
    # launch unit test and code coverage
    - ng $NG_TEST_ARGS --watch=false --no-progress
    # build production artifact
    - run_ng_build
  coverage: '/^Statements\s*:\s*([^%]+)/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: "$NG_WORKSPACE_DIR/reports/ng-coverage.cobertura.xml"
      junit:
        - "$NG_WORKSPACE_DIR/reports/ng-test.xunit.xml"
    when: always # save artifact even if test failed
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    paths:
      - $NG_WORKSPACE_DIR/dist
      - $NG_WORKSPACE_DIR/reports/ng-test-*"
      - $NG_WORKSPACE_DIR/reports/ng-coverage-*"
    expire_in: 1 day

###############################################################################################
#                                      test stage:                                           #
#                                        - ng-e2e                                           #
###############################################################################################
ng-e2e:
  extends: .ng-cli-base
  stage: test
  script:
    - ng $NG_E2E_ARGS
  artifacts:
    reports:
      junit: $NG_WORKSPACE_DIR/reports/ng-e2e.xunit.xml
    when: always # save artifact even if test failed
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    paths:
      - $NG_WORKSPACE_DIR/reports/ng-e2e.*
    expire_in: 1 day
  rules:
    # only run if feature is enabled
    - if: '$NG_E2E_ENABLED != "true"'
      when: never
    - !reference [.test-policy, rules]

###############################################################################################
#                                      publish stage:                                         #
#                                        - npm-publish                                        #
###############################################################################################
ng-publish:
  extends: .ng-cli-base
  stage: publish
  before_script:
    - *ng-cli-scripts
    - cd ${NG_WORKSPACE_DIR}
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - configure_gitlab_instance_level_npm_registry_auth
    - configure_npm_publish_registry_auth
    - npm ci --cache .npm --prefer-offline
  script:
    - npm_publish
  rules:
    # on production branche: manual
    - if: '$NG_PUBLISH_ENABLED == "true" && $CI_COMMIT_REF_NAME =~ $PROD_REF'
      when: manual
