// A completed prod promotion tags the project <RELEASE>, and xivocc-build.sh prod
// refuses to run again while that tag exists. It is therefore the authoritative record
// of "already promoted", and lets the pipeline be rerun after a partial failure. The
// docker registry cannot answer this: a prod build pushes the floating
// <MAJOR>.<MINOR>.latest tag alongside the exact release, so the floating tag reports
// every project as done as soon as the first one is promoted.
//
// The tag is read over SSH rather than the GitLab REST API: the build agents reach
// gitlab.com through the git SSH transport, and HTTPS calls to the API time out.
// ls-remote exits 0 with an empty output when the tag does not exist, and non-zero
// when the repository cannot be reached at all. Only the first case means "not promoted
// yet": an unreachable repository is reported so a network problem is not mistaken for
// a list of projects left to promote.
def prodTagExists(project, release) {
  def tagRef = "refs/tags/${release}"

  def result = sh(
    script: "git ls-remote --tags git@gitlab.com:xivo.solutions/${project}.git '${tagRef}' 2>/dev/null; echo \"exit=\$?\"",
    returnStdout: true
  ).trim()

  if (!result.endsWith('exit=0')) {
    echo "Could not read the tags of ${project} (${result.split('\n').last()}), will attempt the promotion."
    return false
  }

  return result.contains(tagRef)
}

pipeline {
  agent none
  parameters {
    string(name: 'RELEASE', description: 'Release numeric name (e.g. 2026.10.01)', trim: true)
    choice(name: 'RELEASE_TYPE', choices: ['IV', 'BUGFIX'], description: 'Select the type of release. IV promotes every docker image, BUGFIX only the ones of DOCKER_LIST.')
    string(name: 'DOCKER_LIST', defaultValue: '', description: """
List of projects with docker images to promote for this release separated with , (e.g. xucserver,xivo-agid,edge-coturn).

Only read when RELEASE_TYPE is BUGFIX: an IV promotes every image of resources/docker-images.json.

Left empty, it is read from the release-note job's TAGGED_PROJECTS file.
""", trim: true)
    string(name: 'PACKAGER_LIST', defaultValue: '', description: """
List of projects with debian package(s) (packagers) to release for this release separated with , (e.g. xivo,xivo-confgend,xivo-sysconfd).

Left empty, it is read from the release-note job's TAGGED_PROJECTS file.
""", trim: true)
    string(name: 'LTS_CODENAME', description: 'The production release name (eg. kuma)', trim: true)
    booleanParam(name: 'DO_TECHNICAL_RELEASE', defaultValue: true, description: 'Publish xivo-<X.Y>-latest on the archive repo')
    booleanParam(name: 'DO_PROD_RELEASE', defaultValue: true, description: 'Execute the production release on debian repo')
    booleanParam(name: 'SKIP_DOCKER', defaultValue: false, description: 'Skip the docker promotion stage (it already completed in a previous run).')
    booleanParam(name: 'SKIP_DEBIAN', defaultValue: false, description: 'Skip the debian release stage (it already completed in a previous run).')
  }
  stages {
    stage('check-parameters') {
      agent any
      steps {
        script {
          // The debian stage publishes without asking for a confirmation, so the values
          // its repository names are built from are checked before anything is promoted.
          if (!(params.RELEASE ==~ /[0-9]{4}\.[0-9]{2}\.[0-9]{2}/)) {
            error "RELEASE must be a numeric release like 2026.10.01, got '${params.RELEASE}'."
          }
          if (!params.SKIP_DEBIAN && params.DO_PROD_RELEASE && !params.LTS_CODENAME) {
            error 'LTS_CODENAME is required by the prod-release stage: it names the repository to publish to.'
          }
          echo "PROD build for XiVO ${params.RELEASE} (${params.LTS_CODENAME}), release type ${params.RELEASE_TYPE}"
        }
      }
    }
    stage('resolve-projects') {
      agent any
      steps {
        script {
          // The playbook that launches this job has already parsed the release note and
          // passes the lists, so release-note is not run again here. The file is only
          // the fallback for a run started by hand from Jenkins. Each list falls back on
          // its own, so a rerun can narrow down a single list and let the other be
          // resolved as usual.
          //
          // An IV promotes every image, so its docker list is never read.
          def needsDockerList = params.RELEASE_TYPE == 'BUGFIX' && !params.SKIP_DOCKER && !params.DOCKER_LIST
          def fromFile = [:]
          if (needsDockerList || !params.PACKAGER_LIST) {
            fromFile = readProperties file: '/var/lib/jenkins/workspace/release-note/TAGGED_PROJECTS'
          }

          env.DOCKER_PROJECTS = params.DOCKER_LIST ?: "${fromFile['DOCKER_PROJECTS'] ?: ''}"
          env.DEBIAN_PROJECTS = params.PACKAGER_LIST ?: "${fromFile['DEBIAN_PROJECTS'] ?: ''}"

          echo """Project lists for this release. Copy them as parameters to rerun this build:
- DOCKER_LIST=${env.DOCKER_PROJECTS}
- PACKAGER_LIST=${env.DEBIAN_PROJECTS}"""
        }
      }
    }
    stage('promote-docker-images') {
      when {
        expression { !params.SKIP_DOCKER }
      }
      steps {
        script {
          switch (params.RELEASE_TYPE) {
            case 'BUGFIX':
              promoteListedImages()
              break
            case 'IV':
              // The all-images job carries the list and its own already-promoted check,
              // so an IV needs no list resolved here.
              build(
                job: 'build-all-docker-images',
                parameters: [
                  string(name: 'BRANCH_OR_TAG', value: "${params.RELEASE}-rc"),
                  string(name: 'BUILD_MODE', value: 'prod'),
                  string(name: 'RELEASE', value: "${params.RELEASE}")
                ]
              )
              break
            default:
              error "Invalid RELEASE_TYPE: ${params.RELEASE_TYPE}"
          }
        }
      }
    }
    stage('release-debian-packages') {
      when {
        expression { !params.SKIP_DEBIAN }
      }
      steps {
        script {
          // asterisk is released by asterisk-publish-new-version, which keeps its own
          // confirmation, so it is never part of a release packager list.
          def packagers = env.DEBIAN_PROJECTS.split(',').collect { it.trim() }.findAll { it && it != 'asterisk' }

          if (!packagers) {
            packagers = ['xivo']
            echo "No debian project to release: adding 'xivo' anyway to have the debian part released."
          }

          build(
            job: 'debian-packages-prod-build-process',
            parameters: [
              string(name: 'RELEASE', value: "${params.RELEASE}"),
              string(name: 'PACKAGER_LIST', value: "${packagers.join(',')}"),
              string(name: 'LTS_CODENAME', value: "${params.LTS_CODENAME}"),
              booleanParam(name: 'DO_TECHNICAL_RELEASE', value: params.DO_TECHNICAL_RELEASE),
              booleanParam(name: 'DO_PROD_RELEASE', value: params.DO_PROD_RELEASE)
            ]
          )
        }
      }
    }
  }
}

// Promotes the images named by DOCKER_LIST, one per-project job each, the way an IV goes
// through build-all-docker-images. Attempts every project instead of stopping at the
// first failure, then names exactly the ones to put back in DOCKER_LIST.
void promoteListedImages() {
  def projects = env.DOCKER_PROJECTS.split(',').collect { it.trim() }.findAll { it }

  if (!projects) {
    echo 'No docker image to promote. Skipping stage.'
    return
  }

  echo "Now build job will be ran for each docker image: ${projects.join(',')}."
  echo 'And they will be launched with these parameters:'
  echo "- BRANCH_OR_TAG=${params.RELEASE}-rc"
  echo '- BUILD_MODE=prod'

  def alreadyPromoted = []
  def promoted = []
  def failed = []

  projects.each { project ->
    if (prodTagExists(project, params.RELEASE)) {
      echo "${project} is already tagged ${params.RELEASE}, skipping."
      alreadyPromoted << project
      return
    }

    def jobStatus = build job: "${project}-docker-auto-v2",
      propagate: false,
      parameters: [
        string(name: 'BRANCH_OR_TAG', value: "${params.RELEASE}-rc"),
        string(name: 'BUILD_MODE', value: 'prod')
      ]

    if (jobStatus.result == 'SUCCESS' || jobStatus.result == 'UNSTABLE') {
      echo "Job for ${project} was successful."
      promoted << project
    } else {
      echo "Build job for ${project} has failed."
      failed << project
    }
  }

  echo """Docker promotion summary for ${params.RELEASE}:
- promoted now: ${promoted.join(', ') ?: 'none'}
- already promoted: ${alreadyPromoted.join(', ') ?: 'none'}
- failed: ${failed.join(', ') ?: 'none'}"""

  if (failed) {
    error "Docker promotion incomplete. Rerun this job with DOCKER_LIST=${failed.join(',')}"
  }

  echo 'All docker images are promoted.'
}
