Skip to content
Snippets Groups Projects
gitlab-ci-semrel.yml 32.6 KiB
Newer Older
Pierre Smeyers's avatar
Pierre Smeyers committed
# =========================================================================================
Pierre Smeyers's avatar
Pierre Smeyers committed
# Copyright (C) 2021 Orange & contributors
Pierre Smeyers's avatar
Pierre Smeyers committed
#
# 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;
Pierre Smeyers's avatar
Pierre Smeyers committed
# 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
Pierre Smeyers's avatar
Pierre Smeyers committed
# Floor, Boston, MA  02110-1301, USA.
# =========================================================================================
# default workflow rules: Merge Request pipelines
spec:
  inputs:
    image:
      description: The Docker image used to run semantic-release
      default: registry.hub.docker.com/library/node:lts-slim
    version:
      description: The [semantic-release](https://www.npmjs.com/package/semantic-release) version to use
      default: latest
    branches-ref:
      description: Regular expression pattern matching branches from which releases should happen (should match your [semantic-release configuration](https://semantic-release.gitbook.io/semantic-release/usage/configuration#branches))
      default: /^(master|main)$/
    exec-version:
      description: The [@semantic-release/exec](https://www.npmjs.com/package/@semantic-release/exec) version to use
      default: latest
    config-dir:
      description: directory containing your [semantic-release configuration](https://semantic-release.gitbook.io/semantic-release/usage/configuration#configuration-file)
      default: .
    tag-format:
      description: 'For generated `.releaserc` file only. [tagFormat semantic-release option](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/configuration.md#tagformat)e. :warning: don''t forget to double the `$` character so it is not interpreted by GitLab.'
      default: $${version}
    required-plugins-file:
      description: Full path to `semrel-required-plugins.txt` file _(relative to `$CI_PROJECT_DIR`)_
      default: semrel-required-plugins.txt
    release-disabled:
      description: Disable semantic-release
      type: boolean
      default: false
    changelog-enabled:
      description: Add the [@semantic-release/changelog](https://github.com/semantic-release/changelog) plugin which will commit a changelog file in the repository.
      type: boolean
      default: false
    changelog-file:
      description: '[changelogFile @semantic-release/changelog option](https://github.com/semantic-release/changelog#options).'
      default: CHANGELOG.md
    changelog-title:
      description: '[changelogTitle @semantic-release/changelog option](https://github.com/semantic-release/changelog#options). You might want to use markdown format (for example `# MyApp Changelog`).'
      default: ''
    dry-run:
      description: For generated `.releaserc` file only. Activate the [dryRun semantic-release option](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/configuration.md#dryrun) if present.
      type: boolean
      default: false
    auto-release-enabled:
      description: When set the job start automatically. When not set (default), the job is manual.
      type: boolean
      default: false
    hooks-dir:
      description: Hook scripts folder.
      default: .
    commit-message:
      description: '[message @semantic-release/git option](https://github.com/semantic-release/git#message)'
      default: ''
    commit-spec:
      description: "Commit specification `preset` (possible values: `angular`, `codemirror`, `ember`, `eslint`, `express`, `jquery`, `jshint`, `conventionalcommits`). The default is `angular`."
      options:
      - angular
      - codemirror
      - conventionalcommits
      - ember
      - eslint
      - express
      - jquery
      - jshint
      default: 'angular'
    info-on:
      description: Define on which branch(es) the job shall be run
      options:
      - ''
      - prod
      - protected
      - all
      default: ''
---
workflow:
  rules:
    # prevent MR pipeline originating from production or integration branch(es)
    - if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ $PROD_REF || $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ $INTEG_REF'
      when: never
    # on non-prod, non-integration branches: prefer MR pipeline over branch pipeline
    - if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS && $CI_COMMIT_REF_NAME !~ $PROD_REF && $CI_COMMIT_REF_NAME !~ $INTEG_REF'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*tag(,[^],]*)*\]/" && $CI_COMMIT_TAG'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*branch(,[^],]*)*\]/" && $CI_COMMIT_BRANCH'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*mr(,[^],]*)*\]/" && $CI_MERGE_REQUEST_ID'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*default(,[^],]*)*\]/" && $CI_COMMIT_REF_NAME =~ $CI_DEFAULT_BRANCH'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*prod(,[^],]*)*\]/" && $CI_COMMIT_REF_NAME =~ $PROD_REF'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*integ(,[^],]*)*\]/" && $CI_COMMIT_REF_NAME =~ $INTEG_REF'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*dev(,[^],]*)*\]/" && $CI_COMMIT_REF_NAME !~ $PROD_REF && $CI_COMMIT_REF_NAME !~ $INTEG_REF'
      when: never
    - when: always

Pierre Smeyers's avatar
Pierre Smeyers committed
variables:
  # variabilized tracking image
  TBC_TRACKING_IMAGE: registry.gitlab.com/to-be-continuous/tools/tracking:master
Pierre Smeyers's avatar
Pierre Smeyers committed
  # Default Docker image (use a public image - can be overridden)
  SEMREL_IMAGE: $[[ inputs.image ]]
  SEMREL_HOOKS_DIR: $[[ inputs.hooks-dir ]]
  SEMREL_TAG_FORMAT: $[[ inputs.tag-format ]]
  SEMREL_REQUIRED_PLUGINS_FILE: $[[ inputs.required-plugins-file ]]
Pierre Smeyers's avatar
Pierre Smeyers committed
  # undocumented (for internal use only)
  SEMREL_VERIFY_CONDITIONS_CMD: "verify-conditions.sh"
  SEMREL_VERIFY_RELEASE_CMD: "verify-release.sh"
  SEMREL_PREPARE_CMD: "prepare.sh"
  SEMREL_PUBLISH_CMD: "publish.sh"
  SEMREL_SUCCESS_CMD: "success.sh"
  SEMREL_FAIL_CMD: "fail.sh"
  SEMREL_VERSION: $[[ inputs.version ]]
  SEMREL_EXEC_VERSION: $[[ inputs.exec-version ]]
  SEMREL_CONFIG_DIR: $[[ inputs.config-dir ]]
  SEMREL_CHANGELOG_ENABLED: $[[ inputs.changelog-enabled ]]
  SEMREL_CHANGELOG_FILE: $[[ inputs.changelog-file ]]
  SEMREL_CHANGELOG_TITLE: $[[ inputs.changelog-title ]]
  SEMREL_DRY_RUN: $[[ inputs.dry-run ]]
  SEMREL_AUTO_RELEASE_ENABLED: $[[ inputs.auto-release-enabled ]]
  SEMREL_COMMIT_MESSAGE: $[[ inputs.commit-message ]]
  SEMREL_RELEASE_DISABLED: $[[ inputs.release-disabled ]]
  SEMREL_INFO_ON: $[[ inputs.info-on ]]
  SEMREL_COMMIT_SPEC: $[[ inputs.commit-spec ]]

  # default production ref name (pattern)
  PROD_REF: /^(master|main)$/
  SEMREL_BRANCHES_REF: $[[ inputs.branches-ref ]]
Pierre Smeyers's avatar
Pierre Smeyers committed

stages:
  - build
  - test
  - package-build
  - package-test
  - infra
  - deploy
  - acceptance
Pierre Smeyers's avatar
Pierre Smeyers committed
  - publish
  - infra-prod
  - production
Pierre Smeyers's avatar
Pierre Smeyers committed

.semrel-scripts: &semrel-scripts |
  # BEGSCRIPT
  set -e

  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 fail() {
    log_error "$*"
    exit 1
  }

  function assert_defined() {
    if [[ -z "$1" ]]
    then
      log_error "$2"
      exit 1
    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

    # configure for npm
    echo "$certs" > /tmp/custom-ca.pem
    export NODE_EXTRA_CA_CERTS=/tmp/custom-ca.pem

Pierre Smeyers's avatar
Pierre Smeyers committed
    # 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}__"
Cédric OLIVIER's avatar
Cédric OLIVIER committed
        _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;
          if [[ -z "$_not" ]] && [[ "$_cond_val" != "$_cmp_val"* ]]; then continue;
          elif [[ "$_not" ]] && [[ "$_cond_val" == "$_cmp_val"* ]]; then continue;
          if [[ -z "$_not" ]] && [[ "$_cond_val" != *"$_cmp_val" ]]; then continue;
          elif [[ "$_not" ]] && [[ "$_cond_val" == *"$_cmp_val" ]]; then continue;
          if [[ -z "$_not" ]] && [[ "$_cond_val" != *"$_cmp_val"* ]]; then continue;
          elif [[ "$_not" ]] && [[ "$_cond_val" == *"$_cmp_val"* ]]; then continue;
          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"
  }

Pierre Smeyers's avatar
Pierre Smeyers committed
  # evaluate and export a secret
  # - $1: secret variable name
  function eval_secret() {
    name=$1
    value=$(eval echo "\$${name}")
    case "$value" in
    @b64@*)
      decoded=$(mktemp)
      errors=$(mktemp)
      if echo "$value" | cut -c6- | base64 -d > "${decoded}" 2> "${errors}"
      then
        # shellcheck disable=SC2086
        export ${name}="$(cat ${decoded})"
        log_info "Successfully decoded base64 secret \\e[33;1m${name}\\e[0m"
      else
        fail "Failed decoding base64 secret \\e[33;1m${name}\\e[0m:\\n$(sed 's/^/... /g' "${errors}")"
      fi
      ;;
    @hex@*)
      decoded=$(mktemp)
      errors=$(mktemp)
      if echo "$value" | cut -c6- | sed 's/\([0-9A-F]\{2\}\)/\\\\x\1/gI' | xargs printf > "${decoded}" 2> "${errors}"
      then
        # shellcheck disable=SC2086
        export ${name}="$(cat ${decoded})"
        log_info "Successfully decoded hexadecimal secret \\e[33;1m${name}\\e[0m"
      else
        fail "Failed decoding hexadecimal secret \\e[33;1m${name}\\e[0m:\\n$(sed 's/^/... /g' "${errors}")"
      fi
      ;;
    @url@*)
      url=$(echo "$value" | cut -c6-)
      if command -v curl > /dev/null
      then
        decoded=$(mktemp)
        errors=$(mktemp)
        if curl -s -S -f --connect-timeout 5 -o "${decoded}" "$url" 2> "${errors}"
        then
          # shellcheck disable=SC2086
          export ${name}="$(cat ${decoded})"
          log_info "Successfully curl'd secret \\e[33;1m${name}\\e[0m"
        else
          log_warn "Failed getting secret \\e[33;1m${name}\\e[0m:\\n$(sed 's/^/... /g' "${errors}")"
Pierre Smeyers's avatar
Pierre Smeyers committed
        fi
      elif command -v wget > /dev/null
      then
        decoded=$(mktemp)
        errors=$(mktemp)
        if wget -T 5 -O "${decoded}" "$url" 2> "${errors}"
        then
          # shellcheck disable=SC2086
          export ${name}="$(cat ${decoded})"
          log_info "Successfully wget'd secret \\e[33;1m${name}\\e[0m"
        else
          log_warn "Failed getting secret \\e[33;1m${name}\\e[0m:\\n$(sed 's/^/... /g' "${errors}")"
Pierre Smeyers's avatar
Pierre Smeyers committed
        fi
      elif command -v node > /dev/null
      then
        if node -e "const fs=require('fs');function dlFile(url,file,maxRedir=5){return new Promise((resolve,reject)=>{let redirCount=0;const req=require(url.split(':')[0]).get(url,res=>{res.statusCode>=300&&res.statusCode<400&&res.headers.location&&redirCount<maxRedir?(redirCount++,console.log('Follow redirect ('+redirCount+'): '+res.headers.location),dlFile(res.headers.location,file,maxRedir).then(resolve).catch(reject)):200===res.statusCode?(res.pipe(fs.createWriteStream(file)).on('finish',()=>resolve()),res.on('error',reject)):reject(new Error('HTTP error: '+res.statusCode))});req.on('error',reject)})}dlFile('$url','$decoded').then(()=>{console.log('Download complete'),process.exit(0)}).catch(e=>{console.error('Error:',e),process.exit(1)});" 2> "${errors}"
        then
          # shellcheck disable=SC2086
          export ${name}="$(cat ${decoded})"
          log_info "Successfully dl'd secret \\e[33;1m${name}\\e[0m"
        else
          log_warn "Failed getting secret \\e[33;1m${name}\\e[0m:\\n$(sed 's/^/... /g' "${errors}")"
        fi
Pierre Smeyers's avatar
Pierre Smeyers committed
      else
        log_warn "Couldn't get secret \\e[33;1m${name}\\e[0m: no http client found"
Pierre Smeyers's avatar
Pierre Smeyers committed
      fi
      ;;
    esac
  }

  function eval_all_secrets() {
    encoded_vars=$(env | grep -v '^scoped__' | awk -F '=' '/^[a-zA-Z0-9_]*=@(b64|hex|url)@/ {print $1}')
Pierre Smeyers's avatar
Pierre Smeyers committed
    for var in $encoded_vars
    do
      eval_secret "$var"
    done
  }

  function download_file() {
    if command -v wget &> /dev/null
    then
      wget "$1" -O "$2"
    elif command -v curl &> /dev/null
    then
      curl -sfL "$1" -o "$2"
    elif command -v node &> /dev/null
    then
      node -e "const fs=require('fs');function dlFile(url,file,maxRedir=5){return new Promise((resolve,reject)=>{let redirCount=0;const req=require(url.split(':')[0]).get(url,res=>{res.statusCode>=300&&res.statusCode<400&&res.headers.location&&redirCount<maxRedir?(redirCount++,console.log('Follow redirect ('+redirCount+'): '+res.headers.location),dlFile(res.headers.location,file,maxRedir).then(resolve).catch(reject)):200===res.statusCode?(res.pipe(fs.createWriteStream(file)).on('finish',()=>resolve()),res.on('error',reject)):reject(new Error('HTTP error: '+res.statusCode))});req.on('error',reject)})}dlFile('$1','$2').then(()=>{console.log('Download complete'),process.exit(0)}).catch(e=>{console.error('Error:',e),process.exit(1)});"
    else
      fail "wget, curl or node required"
    fi
  }

  function github_get_latest_version() {
    if command -v curl &> /dev/null
    then
      curl -sSf -I "https://github.com/$1/releases/latest" | awk -F '/' -v RS='\r\n' '/location:/ {print $NF}'
    elif command -v node &> /dev/null
    then
      node -e "const https=require('https'); const options={hostname:'github.com', path:'/$1/releases/latest', method:'HEAD'}; https.request(options, (res) => {tokens=res.headers.location.split('/'); console.log(tokens[tokens.length-1]); res.req.destroy()}).end();"
    else
      fail "curl or node required"
    fi
  }

  function maybe_install_packages() {
    if command -v apt-get > /dev/null
    then
      # Debian
      if ! dpkg --status "$@" > /dev/null
      then
        apt-get update
        apt-get install --no-install-recommends --yes --quiet "$@"
      fi
    elif command -v apk > /dev/null
    then
      # Alpine
      if ! apk info --installed "$@" > /dev/null
      then
        apk add --no-cache "$@"
      fi
    else
      log_error "... didn't find any supported package manager to install $*"
      exit 1
    fi
  }

Pierre Smeyers's avatar
Pierre Smeyers committed
  function extract_release_config_from_package_json() {
    package_json="./package.json"
    if [[ -f "${package_json}" ]]; then
      release_config=$(node -pe "JSON.stringify(require('${package_json}').release, null, 2)")
      case "$release_config" in
        "undefined"|"null") release_config="" ;;
      esac
      echo "$release_config"
Pierre Smeyers's avatar
Pierre Smeyers committed
    fi
  }

  function prepare_semantic_release() {
    git config --global --add safe.directory "$(pwd)"
Pierre Smeyers's avatar
Pierre Smeyers committed
    if [[ -f ".releaserc" ]]; then
      log_info "\\e[33;1m.releaserc\\e[0m file found"
      semrelConfigFile=".releaserc"
    elif [[ -f ".releaserc.yml" ]]; then
      log_info "\\e[33;1m.releaserc.yml\\e[0m file found"
      semrelConfigFile=".releaserc.yml"
    elif [[ -f ".releaserc.yaml" ]]; then
      log_info "\\e[33;1m.releaserc.yaml\\e[0m file found"
      semrelConfigFile=".releaserc.yaml"
    elif [[ -f ".releaserc.json" ]]; then
      log_info "\\e[33;1m.releaserc.json\\e[0m file found"
      semrelConfigFile=".releaserc.json"
    elif [[ -f ".releaserc.js" ]]; then
      log_info "\\e[33;1m.releaserc.js\\e[0m file found"
      semrelConfigFile=".releaserc.js"
    else
      releaseConfig="$(extract_release_config_from_package_json)"
      if [[ -n "${releaseConfig}" ]]; then
        log_info "release configuration found in \\e[33;1mpackage.json\\e[0m file"
        # exporting release configuration in dedicated file for required plugins installation
        semrelConfigFile=".release_config_from_package_json"
        echo "${releaseConfig}" > "${semrelConfigFile}"
      else
        log_info "semantic release configuration file not found, generating default \\e[33;1m.releaserc\\e[0m"
        semrelConfigFile=".releaserc"
        if [[ -n "$TRACE" ]]; then
          debug="true"
        else
          debug="false"
        fi
        commitPresetConfig=$(generate_commit_preset_conf)
Pierre Smeyers's avatar
Pierre Smeyers committed
        changelogPluginConfig=$(generate_changelog_plugin_conf)
        execPluginConfig=$(generate_exec_plugin_conf)
        gitPluginConfig=$(generate_git_plugin_conf)
        {
          echo "debug: ${debug}"
          echo ""
          echo "tagFormat: '${SEMREL_TAG_FORMAT}'"
Pierre Smeyers's avatar
Pierre Smeyers committed
          echo ""
          echo "  - - '@semantic-release/commit-analyzer'"
          echo "${commitPresetConfig}"
          echo "  - - '@semantic-release/release-notes-generator'"
          echo "${commitPresetConfig}"
Pierre Smeyers's avatar
Pierre Smeyers committed
          echo "${changelogPluginConfig}"
          echo "${execPluginConfig}"
          echo "${gitPluginConfig}"
          echo ""
          echo "branches:"
Pierre Smeyers's avatar
Pierre Smeyers committed
        } > "${semrelConfigFile}"
        cat "${semrelConfigFile}"
      fi
    fi
  function install_semantic_release_plugins() {
Pierre Smeyers's avatar
Pierre Smeyers committed
    log_info "installing required plugins"
    # shellcheck disable=SC2046
    if [[ -f "${SEMREL_REQUIRED_PLUGINS_FILE}" ]]; then
      while IFS= read -r line || [[ -n "$line" ]]
      do
        required_plugins="${required_plugins} $line"
      done <<< $(cat "${SEMREL_REQUIRED_PLUGINS_FILE}")
    fi

Pierre Smeyers's avatar
Pierre Smeyers committed
    # shellcheck disable=SC2046
    while IFS= read -r line || [[ -n "$line" ]]
    do
      plugin=$(echo "$line" | cut -d\" -f2)
      required_plugins="${required_plugins} $plugin"
    done <<< $(yq eval ".plugins[]" "${semrelConfigFile}" -o=json --indent 0)

    # shellcheck disable=SC2086
    npm install --global "semantic-release@${SEMREL_VERSION}" ${required_plugins}
    
    if [[ ! -f "${SEMREL_REQUIRED_PLUGINS_FILE}" && -n "${SEMREL_COMMIT_SPEC}" ]]; then
      case "$SEMREL_COMMIT_SPEC" in
        cc)
          SEMREL_COMMIT_SPEC=conventionalcommits
          ;;
      esac
      npm install --global "conventional-changelog-$SEMREL_COMMIT_SPEC" 
    fi

    if [[ -n "$TRACE" ]]; then
        if [[ -f "./package.json" ]]; then
          log_info "Installed devDependencies..."
          npm pkg get devDependencies
        fi
        log_info "Globally installed packages..."
        npm list --global
      fi
  # this script console output is inserted in generated file: DO NOT ADD LOGS
  function generate_commit_preset_conf() {
    if [[ -n "${SEMREL_COMMIT_SPEC}" ]]; then
      if [[ "${SEMREL_COMMIT_SPEC}" == "cc" ]]; then
        conventionalCommits="conventionalcommits"  
      fi   
      echo "    - preset: '${conventionalCommits:-$SEMREL_COMMIT_SPEC}'"
    fi
  }
  
Pierre Smeyers's avatar
Pierre Smeyers committed
  # this script console output is inserted in generated file: DO NOT ADD LOGS
  function generate_changelog_plugin_conf() {
    if [[ "${SEMREL_CHANGELOG_ENABLED}" = "true" ]]; then
Pierre Smeyers's avatar
Pierre Smeyers committed
      if [[ -n "${SEMREL_CHANGELOG_FILE}" ]] || [[ -n "${SEMREL_CHANGELOG_TITLE}" ]]; then
        if [[ -n "${SEMREL_CHANGELOG_FILE}" ]]; then
          changeLogConfig="changelogFile: '${SEMREL_CHANGELOG_FILE}'"
Pierre Smeyers's avatar
Pierre Smeyers committed
        fi
        if [[ -n "${SEMREL_CHANGELOG_TITLE}" ]]; then
          changeLogConfig=$(echo -e "${changeLogConfig:+${changeLogConfig}\n      }changelogTitle: '${SEMREL_CHANGELOG_TITLE}'")
Pierre Smeyers's avatar
Pierre Smeyers committed
        fi
        echo "  - - '@semantic-release/changelog'"
        echo "    - ${changeLogConfig}"
Pierre Smeyers's avatar
Pierre Smeyers committed
      else
        echo "  - '@semantic-release/changelog'"
Pierre Smeyers's avatar
Pierre Smeyers committed
      fi
    else
      echo ""
    fi
  }

  # this script console output is inserted in generated file: DO NOT ADD LOGS
  function generate_git_plugin_conf() {
    # git plugin has default changelog file as asset by default so
Pierre Smeyers's avatar
Pierre Smeyers committed
    # we need to add it explicitly if the user configured a custom changelogFile
    echo "  - - '@semantic-release/git'"
    if [[ "${SEMREL_CHANGELOG_ENABLED}" = "true" ]] && [[ -n "${SEMREL_CHANGELOG_FILE}" ]]; then
      echo "    - assets:"
      echo "      - '${SEMREL_CHANGELOG_FILE}'"
      echo "      - 'package.json'"
      echo "      - 'package-lock.json'"
      echo "      - 'npm-shrinkwrap.json'"
      if [[ -n "${SEMREL_COMMIT_MESSAGE}" ]]; then
        echo "      message: \"${SEMREL_COMMIT_MESSAGE}\""
      fi
Pierre Smeyers's avatar
Pierre Smeyers committed
    else
      if [[ -n "${SEMREL_COMMIT_MESSAGE}" ]]; then
        echo "    - message: \"${SEMREL_COMMIT_MESSAGE}\""
      fi
Pierre Smeyers's avatar
Pierre Smeyers committed
    fi
  }

  # this script console output is inserted in generated file: DO NOT ADD LOGS
  function generate_exec_plugin_conf() {
    scriptsConfig=""
Pierre Smeyers's avatar
Pierre Smeyers committed
    scriptPath=${SEMREL_HOOKS_DIR}/${SEMREL_VERIFY_CONDITIONS_CMD}
    if [[ -f "${scriptPath}" ]]; then
      chmod +x "${scriptPath}"
      scriptsConfig="${tabs}verifyConditionsCmd: '${scriptPath}'"
      tabs="      "
Pierre Smeyers's avatar
Pierre Smeyers committed
    fi
    scriptPath=${SEMREL_HOOKS_DIR}/${SEMREL_VERIFY_RELEASE_CMD}
    if [[ -f "${scriptPath}" ]]; then
      chmod +x "${scriptPath}"
      scriptsConfig=$(echo -e "${scriptsConfig}\n${tabs}verifyReleaseCmd: '\"${scriptPath}\" \"\${lastRelease.version}\" \"\${nextRelease.version}\" \"\${nextRelease.type}\"'")
      tabs="      "
Pierre Smeyers's avatar
Pierre Smeyers committed
    fi
    scriptPath=${SEMREL_HOOKS_DIR}/${SEMREL_PREPARE_CMD}
    if [[ -f "${scriptPath}" ]]; then
      chmod +x "${scriptPath}"
      scriptsConfig=$(echo -e "${scriptsConfig}\n${tabs}prepareCmd: '\"${scriptPath}\" \"\${lastRelease.version}\" \"\${nextRelease.version}\" \"\${nextRelease.type}\"'")
      tabs="      "
Pierre Smeyers's avatar
Pierre Smeyers committed
    fi
    scriptPath=${SEMREL_HOOKS_DIR}/${SEMREL_PUBLISH_CMD}
    if [[ -f "${scriptPath}" ]]; then
      chmod +x "${scriptPath}"
      scriptsConfig=$(echo -e "${scriptsConfig}\n${tabs}publishCmd: '\"${scriptPath}\" \"\${nextRelease.version}\" \"\${options.branch}\" \"\${commits.length}\" \"\${Date.now()}\"'")
      tabs="      "
Pierre Smeyers's avatar
Pierre Smeyers committed
    fi
    scriptPath=${SEMREL_HOOKS_DIR}/${SEMREL_SUCCESS_CMD}
    if [[ -f "${scriptPath}" ]]; then
      chmod +x "${scriptPath}"
      scriptsConfig=$(echo -e "${scriptsConfig}\n${tabs}successCmd: '\"${scriptPath}\" \"\${lastRelease.version}\" \"\${nextRelease.version}\"'")
      tabs="      "
Pierre Smeyers's avatar
Pierre Smeyers committed
    fi
    scriptPath=${SEMREL_HOOKS_DIR}/${SEMREL_FAIL_CMD}
    if [[ -f "${scriptPath}" ]]; then
      chmod +x "${scriptPath}"
      scriptsConfig=$(echo -e "${scriptsConfig}\n${tabs}failCmd: '\"${scriptPath}\" \"\${lastRelease.version}\" \"\${nextRelease.version}\"'")
      tabs="      "
Pierre Smeyers's avatar
Pierre Smeyers committed
    fi
    if [[ -n "${scriptsConfig}" ]]; then
Pierre Smeyers's avatar
Pierre Smeyers committed
      echo "${scriptsConfig}"
    else
      echo ""
    fi
  }

    if ! command -v yq > /dev/null
    then
      yq_version=$(github_get_latest_version mikefarah/yq)
      yq_binary=yq_linux_amd64
      yq_url="https://github.com/mikefarah/yq/releases/download/${yq_version}/${yq_binary}.tar.gz"
Pytgaen's avatar
Pytgaen committed
      yq_cache="$XDG_CACHE_HOME/yq-$(echo "$yq_url" | md5sum | cut -d" " -f1)"

      if [[ -f "$yq_cache" ]]
      then
        log_info "yq found in cache: reuse"
      else
        log_info "yq not found in cache: download"
        log_info "Download latest yq version: \\e[32m$yq_url\\e[0m"
        download_file "${yq_url}" "${yq_binary}.tar.gz"
        tar xvf "${yq_binary}.tar.gz"
        mkdir -p "$XDG_CACHE_HOME"
        mv "${yq_binary}" "$yq_cache"
      fi 
      ln -s "$yq_cache" /usr/bin/yq
Pierre Smeyers's avatar
Pierre Smeyers committed
  }

  function dotenv_semrel_info() {
    # removing user conf as we need to override it temporarily (git reset will put things back to normal)
    # see https://www.npmjs.com/package/cosmiconfig for configuration files resolution order (we will use .releaserc)
    releaserc_file="${semrelConfigFile}"
Pierre Smeyers's avatar
Pierre Smeyers committed
    rm -f "package.json"
    yq eval -oyaml -P 'with_entries(select((.key | . != "plugins") and (.key | . != "verifyConditions")))' "${releaserc_file}" > "${releaserc_file}.new"
Pierre Smeyers's avatar
Pierre Smeyers committed

    # Generating the hook scripts that will generate the dotenv file
    # The dotenv file is generated in $TMPDIR so it will survive the git reset
    dotenv_tmp="$(mktemp -t semrel-info-XXXXXXXXXX.dotenv)"
Pierre Smeyers's avatar
Pierre Smeyers committed

    export_last_version_hook_script="./export-last-version.sh"
    {
      echo "#!/bin/bash"
      echo "{"
      echo "echo \"SEMREL_INFO_LAST_VERSION=\$1\""
      echo "} > \"${dotenv_tmp}\""
    } > "${export_last_version_hook_script}"
    chmod +x ${export_last_version_hook_script}
Pierre Smeyers's avatar
Pierre Smeyers committed
    export_next_version_hook_script="./export-next-version.sh"
    {
      echo "#!/bin/bash"
      echo "{"
      echo "echo \"SEMREL_INFO_NEXT_VERSION=\$1\""
      echo "echo \"SEMREL_INFO_NEXT_VERSION_TYPE=\$2\""
      echo "} >> \"${dotenv_tmp}\""
    } > "${export_next_version_hook_script}"
    chmod +x ${export_next_version_hook_script}

    if [[ -n "$TRACE" ]]; then
Pierre Smeyers's avatar
Pierre Smeyers committed
      echo "generated analyzeCommits hook script:"
      cat "${export_last_version_hook_script}"
      echo "generated verifyRelease hook script:"
      cat "${export_next_version_hook_script}"
    fi

    # Generating temporary semantic-release config
    {
      echo ""
      echo "# injected (replace your plugins) plugins by the template to generate dotenv"
      echo ""
Pierre Smeyers's avatar
Pierre Smeyers committed
      echo "plugins: ["
      echo "  \"@semantic-release/commit-analyzer\","
      echo "  ["
      echo "    \"@semantic-release/exec\","
      echo "    {"
      echo "      \"analyzeCommitsCmd\": \"${export_last_version_hook_script} \\\"\${lastRelease.version}\\\"\"",
      echo "      \"verifyReleaseCmd\": \"${export_next_version_hook_script} \\\"\${nextRelease.version}\\\" \\\"\${nextRelease.type}\\\"\""
      echo "    }"
      echo "  ],"
      echo "]"
    } >> "${releaserc_file}.new"

    mv -f "${releaserc_file}.new" ".releaserc"
    if [[ -n "$TRACE" ]]; then
      log_info "--- generated .releaserc:"
      cat ".releaserc"
    npm install --global "semantic-release@${SEMREL_VERSION}" "@semantic-release/exec@${SEMREL_EXEC_VERSION}"
    semantic-release --dry-run
Pierre Smeyers's avatar
Pierre Smeyers committed

    # Rollback temporary semantic-release configuration
    git reset --hard

    mv "${dotenv_tmp}" ./semrel.out.env
    log_info "--- semrel dotenv artifact:"
    cat ./semrel.out.env
  function configure_commit_signing() {
    if [[ -z "${SEMREL_GPG_SIGNKEY}" ]]; then
      log_info "No GPG key provided."
      return
    fi

    log_info "Setting commit signing up."

    if [[ ! -f "${HOME}/.gnupg" ]]; then
      log_info "creating GPG base configuration"
      gpg -k
    fi

    if [[ ! -f "${SEMREL_GPG_SIGNKEY}" ]]; then
      fail "SEMREL_GPG_SIGNKEY is not a file."
    fi
    if ! gpg --batch --dry-run --yes --import "${SEMREL_GPG_SIGNKEY}"; then
      fail "Could not import GPG key."
    fi
    # import the key and extract its ID from the command output
    _GPG_KEY_ID=$(gpg --batch --yes --import "${SEMREL_GPG_SIGNKEY}" 2>&1 | grep "key [A-F0-9]" | head -n 1 | sed -e 's/^.*key \([A-F0-9]*\): .*$/\1/g')
    if [[ -z "${_GPG_KEY_ID}" ]]; then
        fail "Could not extract key ID from gpg --import command."
    fi
    git config --global commit.gpgsign true
    git config --global user.signingkey "${_GPG_KEY_ID}"

    log_info "Commit signing setup complete."
  }

  unscope_variables
Pierre Smeyers's avatar
Pierre Smeyers committed
  eval_all_secrets

  # ENDSCRIPT

.semrel-base:
  image: $SEMREL_IMAGE
  services:
    - name: "$TBC_TRACKING_IMAGE"
      command: ["--service", "semrel", "3.11.3"]
Pierre Smeyers's avatar
Pierre Smeyers committed
  before_script:
    - !reference [.semrel-scripts]
Pierre Smeyers's avatar
Pierre Smeyers committed
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - maybe_install_packages ca-certificates git openssh-client gpg gpg-agent
    - cd "${SEMREL_CONFIG_DIR}"
    - prepare_semantic_release
    - install_semantic_release_plugins
Pytgaen's avatar
Pytgaen committed
  variables:
    # download cache
    XDG_CACHE_HOME: "$CI_PROJECT_DIR/.cache"
    # NPM cache
    npm_config_cache: "$CI_PROJECT_DIR/.npm"
  # Cache downloaded dependencies and plugins between builds.
  # To keep cache across branches add 'key: "$CI_JOB_NAME"'
Pierre Smeyers's avatar
Pierre Smeyers committed
  cache:
    # cache shall be per branch per template
    key: "$CI_COMMIT_REF_SLUG-SEMREL"
Pytgaen's avatar
Pytgaen committed
    when: always
Pierre Smeyers's avatar
Pierre Smeyers committed
    paths:
Pytgaen's avatar
Pytgaen committed
      - "$CI_PROJECT_DIR/.npm"
      - "$XDG_CACHE_HOME"

Pierre Smeyers's avatar
Pierre Smeyers committed

semantic-release-info:
  extends: .semrel-base
  stage: .pre
  script:
    - dotenv_semrel_info
  artifacts:
    reports:
      dotenv: "${SEMREL_CONFIG_DIR}/semrel.out.env"
Pierre Smeyers's avatar
Pierre Smeyers committed
  rules:
Cédric OLIVIER's avatar
Cédric OLIVIER committed
    - if: $CI_COMMIT_TAG
      when: never
Pierre Smeyers's avatar
Pierre Smeyers committed
    - if: '$SEMREL_INFO_ON == "prod" && $CI_COMMIT_REF_NAME =~ $PROD_REF'
    - if: '$SEMREL_INFO_ON == "branches-ref" && $CI_COMMIT_REF_NAME =~ $SEMREL_BRANCHES_REF'
Pierre Smeyers's avatar
Pierre Smeyers committed
    - if: '$SEMREL_INFO_ON == "protected" && $CI_COMMIT_REF_PROTECTED == "true"'
    - if: '$SEMREL_INFO_ON == "all"'

semantic-release:
  extends: .semrel-base
  stage: publish
  script:
    - configure_commit_signing
    - if [[ "$SEMREL_DRY_RUN" == "true" ]]; then dry_run_opt="--dry-run"; fi
Pierre Smeyers's avatar
Pierre Smeyers committed
    - semantic-release ${TRACE:+--debug} --ci $dry_run_opt
Pierre Smeyers's avatar
Pierre Smeyers committed
  rules:
    - if: '$SEMREL_RELEASE_DISABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
Cédric OLIVIER's avatar
Cédric OLIVIER committed
    - if: $CI_COMMIT_TAG
    # exclude if branch doesn't match $SEMREL_BRANCHES_REF
    - if: '$CI_COMMIT_REF_NAME !~ $SEMREL_BRANCHES_REF'
      when: never
    # if $SEMREL_AUTO_RELEASE_ENABLED: auto
    - if: '$SEMREL_AUTO_RELEASE_ENABLED == "true"'
    # else manual
    - when: manual