#!groovy
// to test groovy snippets online:
//    https://www.jdoodle.com/execute-groovy-online

pipeline {

  agent none

  options {
    disableConcurrentBuilds()
    lock resource: 'pyimporters_plugins'
  }

  // variables declared in environment block can not be changed in any steps/stages
  environment {
    PATH_HOME = '/home/jenkins'
    TEST_REPORT_DIR = '/root/test-reports'
    PYTHONPYCACHEPREFIX = '/tmp/.pytest_cache'
    PYTHONDONTWRITEBYTECODE = '1'
    JENKINS_UIDGID = '1004:1004'

    MAJOR_VERSION = "${MAJOR_VERSION_PY312}"
    MINOR_VERSION = "${MINOR_VERSION_PY312}"
  }

  stages {
    stage('Catch build termination') {
      agent {
        node {
          label 'pre-build'
          customWorkspace "${PATH_HOME}/${JOB_NAME}"
        }
      }
      stages {
        stage('Analyse build cause') {
          steps {
            script {
              // Les submodules viennent du checkout SCM du job ('Advanced sub-modules
            // behaviours'), fait sur l'hote : rien a faire ici, et failOnCheckoutDrift
            // est deja appele au stage 'Analyse build cause'.
              // email-ext, developpe par emailext et non par Groovy -- d'ou les apostrophes.
              if (env._SEND_MAIL) {
                env._CUSTOM_RECIPIENTS = '${ADMIN_RECIPIENTS}' + ';' + env._CUSTOM_RECIPIENTS
              }
            }
          }
        }
      }
    }

    stage('Generate new version') {

      agent {
        node {
          label 'built-in'
          customWorkspace "${PATH_HOME}/${JOB_NAME}"
        }
      }

      stages {
        stage('Add credentials') {
          steps {
            script {
              // Add password file for uv publishing
              sh "cp ${PATH_HOME}/.passwd-pypi .env"
            }
          }
        }

        stage('Commit new version') {
          steps {
            script {
              println("attempt to publish ${JOB_NAME} with version: ${MAJOR_VERSION}.${MINOR_VERSION}.${BUILD_ID}")

              // push updates of file __init__.py
              withCredentials([gitUsernamePassword(credentialsId: 'bitbucket-user', gitToolName: 'git-tool')]) {
                sh 'git pull'
                sh "echo '\"\"\"Sherpa knowledge import plugins\"\"\"' > pyimporters_plugins/__init__.py"
                sh "echo '' >> pyimporters_plugins/__init__.py"
                sh "echo '__version__ = \"${MAJOR_VERSION}.${MINOR_VERSION}.${BUILD_ID}\"' >> pyimporters_plugins/__init__.py"
                // '(no-build)' est le marqueur que abortOnNoBuildCommit() de la lib
                // cherche : sans lui, ce commit declenche un build, qui commite une
                // nouvelle version, qui declenche un build -- une boucle. C'est ce que
                // l'ancien analyseBuildCause local couvrait avec son propre test sur
                // '[Jenkins CI]', que la lib ne fait pas.
                // '[Jenkins CI]' est CONSERVE : analyseBuildCause s'en sert encore pour
                // ignorer un build declenche en amont par un commit de CI.
                sh 'git commit pyimporters_plugins/__init__.py -m "[Jenkins CI] Commit on version files (no-build)" || echo "No changes to commit"'
                sh 'git push'
              }
            }
          }
        }
      }
    }

    stage('Build, test and publish') {

      agent {
        // dockerfile agent
        // Mounted volume for Junit reports
        //   - docker: /root/test-reports
        //   - host  : /tmp/_${JOB_NAME}/test-reports
        dockerfile {
          label 'docker-build'
          customWorkspace "${PATH_HOME}/${JOB_NAME}"
          filename 'Dockerfile'
          args "-u root --privileged -v /tmp/_${JOB_NAME}/test-reports:${TEST_REPORT_DIR}"
        }
      }

      stages {
        stage('Install uv & ruff') {
          steps {
            // Le conteneur tourne en root alors que le workspace appartient a
            // jenkins (1004) : sans cette exception, git refuse d'y operer
            // ('dubious ownership'). '*' plutot que le seul WORKSPACE, pour
            // couvrir aussi le depot du submodule. Conteneur jetable en root :
            // la config globale meurt avec lui.
            sh "git config --global --add safe.directory '*'"
            // Fonctions reutilisables : kairntech/jenkins-tools-lib, sous le nom global
            // 'tools'. Chargee en DYNAMIQUE, et non via l'annotation @Library('tools') _ :
            // ce Jenkins a SCRIPT_SPLITTING_TRANSFORMATION actif, et le '_' de
            // l'annotation est une declaration de variable locale, que cette
            // transformation interdit (JENKINS-37984). gradleDockerPipeline fait pareil.
            //
            // Dans un script{} : la lib etant chargee a l'execution, ses vars ne sont pas
            // des steps connus a la lecture du pipeline. script{} est du Groovy scripte,
            // donc sans la validation de noms de steps que Declarative applique a steps{}.
            script {
              library 'tools'
              // Les submodules viennent du checkout SCM du job ('Advanced sub-modules
              // behaviours'), fait sur l'hote : rien a faire ici. failOnCheckoutDrift
              // rend bruyante la derive que l'option 'tip of branch' peut introduire.
              failOnCheckoutDrift()
            }
            installTask()
            // LOCK_VERSIONED=false : py:sync résout à neuf, ce que faisait le
            // 'rm -f uv.lock' d'avant. uv est installé par la task elle-meme.
            sh 'task py:sync'
          }
        }

        stage('Lint and format python code') {
          steps {
            sh 'task py:lint'
          }
        }

        stage('Test with pytest') {
          steps {
            // purge python cache from any previous run
            sh "rm -rf ${PYTHONPYCACHEPREFIX}"
            // remove any previous results.xml file
            sh "rm -f ${TEST_REPORT_DIR}/results.xml"
            sh "task py:test PYTEST_ARGS='--junit-xml=${TEST_REPORT_DIR}/results.xml'"
          }
        }

        stage('Publish on PyPI') {
          environment {
            UV_PUBLISH_USERNAME = getUserName '.env'
            UV_PUBLISH_PASSWORD = getUserPass '.env'
          }
          steps {
            // remove any previous folder dist
            sh 'rm -rf dist'
            // create (as root) folder dist
            sh 'mkdir dist'
            // pull recent updates of file __init__.py
            withCredentials([gitUsernamePassword(credentialsId: 'bitbucket-user', gitToolName: 'git-tool')]) {
              sh 'git config --global pull.rebase false'
              sh "git config --global --add safe.directory ${WORKSPACE}"
              sh 'git pull'
            }
            // put back owner of .git folder
            sh "chown -R ${JENKINS_UIDGID} ${WORKSPACE}/.git"
            // put back owner of pulled file
            sh "chown ${JENKINS_UIDGID} pyimporters_plugins/__init__.py"
            // get git status
            sh 'git status'
            // build and publish on PyPI
            sh '''
               export COMMIT_VERSION=$( cat pyimporters_plugins/__init__.py|grep version|cut -d '"' -f2|tr -s '[:blank:]' )
               export BUILD_VERSION="${MAJOR_VERSION}"."${MINOR_VERSION}"."${BUILD_ID}"
               if [ "${COMMIT_VERSION}" = "${BUILD_VERSION}" ] ; then task py:build && task py:publish ; fi
               '''
            // remove current folder dist
            sh 'rm -rf dist'
            // remove current folder .hypothesis
            sh 'rm -rf .hypothesis'
            // remove current folder .tox
            sh 'rm -rf .tox'
          }
        }
      }
    }
  }

  post {
    // only triggered when blue or green sign
    success {
      // node is specified here to get an agent
      node('built-in') {
        // keep using customWorkspace to store Junit report
        ws("${PATH_HOME}/${JOB_NAME}") {
          script {
            // Rechargee ici : si le build echoue AVANT le stage 1, la lib n'a jamais ete
            // chargee, et la notification d'echec echouerait elle-meme.
            library 'tools'
            publishTestReport()
            onJobSuccess()
          }
        }
      }
    }
    // triggered when red sign
    failure {
      node('built-in') {
        ws("${PATH_HOME}/${JOB_NAME}") {
          script {
            library 'tools'
            publishTestReport()
            onJobFailure()
          }
        }
      }
    }
    // triggered when black sign
    aborted {
      println 'post-declarative message: abort job'
    }
  }
}

/**
 * Rapatrie le rapport JUnit depuis le volume monte, hors workspace, puis le publie.
 *
 * publishJunitTestResults() de la lib ne convient pas ici : elle publie les fichiers
 * TEST-*.xml, la convention Gradle/Surefire, alors que pytest ecrit results.xml -- et
 * elle ne prend aucun parametre. Les erreurs sont avalees : un rapport absent ne doit
 * pas faire echouer le post.
 */
def publishTestReport() {
  try {
    sh 'rm -f results.xml'
    sh "cp /tmp/_${JOB_NAME}/test-reports/results.xml results.xml"
    junit 'results.xml'
  } catch (Exception e) {
    println 'Exception occurred: ' + e.toString()
  }
}

/**
 * Installe go-task si l'agent ne l'a pas deja. C'est le seul prerequis manuel du
 * Taskfile : uv et l'interpreteur sont amorces par les tasks elles-memes.
 */
def installTask() {
  // Pourquoi pas maybeInstallTaskfile() de la lib 'tools' : elle installe dans
  // ~/.local/bin, ce qui convient a un agent hote mais pas ici -- task tourne dans
  // le conteneur, ou HOME=/root et /root/.local/bin n'est pas dans le PATH de
  // l'image. Ici on est root, donc /usr/local/bin est accessible et deja dans le
  // PATH : c'est plus simple que de propager un PATH.
  //
  // -b /usr/local/bin : deja dans le PATH de l'image, donc pas besoin de toucher
  // au PATH du pipeline. Surcharger PATH dans le bloc environment d'un pipeline
  // 'agent none' le casse : env.HOME et env.PATH y sont nuls (aucun node alloue),
  // la valeur devient "null/.local/bin:null" et le premier sh reste bloque.
  // uv, lui, est amorce par les tasks elles-memes, qui l'appellent en absolu.
  sh '''
     if ! command -v task >/dev/null 2>&1; then
       sh -c "$(curl -sSL https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
     fi
     task --version
     '''
}

// return UV_PUBLISH_USERNAME from given file (backwards compatible with FLIT_USERNAME)
def getUserName(path) {
  def USERNAME = sh(
                 script: "grep -E '(UV_PUBLISH_USERNAME|FLIT_USERNAME)' ${path}|cut -d '=' -f2|head -1",
                 returnStdout: true
               ).trim()
  return USERNAME
}

// return UV_PUBLISH_PASSWORD from given file (backwards compatible with FLIT_PASSWORD)
def getUserPass(path) {
  def USERPASS = sh(
                 script: "grep -E '(UV_PUBLISH_PASSWORD|FLIT_PASSWORD)' ${path}|cut -d '=' -f2|head -1",
                 returnStdout: true
               ).trim()
  return USERPASS
}
