Skip to content
Snippets Groups Projects
gitlab-ci-{{cookiecutter.project_slug}}.yml 31.4 KiB
Newer Older
Pierre Smeyers's avatar
Pierre Smeyers committed
# =========================================================================================
# 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
    - 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

{%- if cookiecutter.template_type == 'build' %}
# 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
{%- elif cookiecutter.template_type == 'acceptance' %}
# acceptance job prototype: implement adaptive pipeline rules
.acceptance-policy:
  rules:
    # exclude tags
    - if: $CI_COMMIT_TAG
      when: never
    # on production or integration branch(es): auto & failing
    - if: '$CI_COMMIT_REF_NAME =~ $PROD_REF || $CI_COMMIT_REF_NAME =~ $INTEG_REF'
    # disable if no review environment
    - if: '$REVIEW_ENABLED != "true"'
      when: never
    # on ADAPTIVE_PIPELINE_DISABLED: auto & failing
    - if: '$ADAPTIVE_PIPELINE_DISABLED == "true"'
    # 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
{%- endif %}

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

  # Default Docker image (use a public image - can be overridden)
  {{ cookiecutter.template_PREFIX }}_IMAGE: "registry.hub.docker.com/{{ cookiecutter.project_slug }}:latest"

{%- if cookiecutter.template_type == 'build' %}
  # Default arguments for 'build' command
  {{ cookiecutter.template_PREFIX }}_BUILD_ARGS: "build --with-default-args"

  # Default arguments for 'publish' command
  {{ cookiecutter.template_PREFIX }}_PUBLISH_ARGS: "publish --with-default-args"

  {{ cookiecutter.template_PREFIX }}_LINT_IMAGE: "registry.hub.docker.com/{{ cookiecutter.project_slug }}-lint:latest"
  {{ cookiecutter.template_PREFIX }}_LINT_ARGS: "--serevity=medium"

  {{ cookiecutter.template_PREFIX }}_DEPCHECK_IMAGE: "registry.hub.docker.com/{{ cookiecutter.project_slug }}-depcheck:latest"
{%- elif cookiecutter.template_type == 'deploy' %}
  {{ cookiecutter.template_PREFIX }}_BASE_APP_NAME: "$CI_PROJECT_NAME"
  {{ cookiecutter.template_PREFIX }}_REVIEW_AUTOSTOP_DURATION: "4 hours"
Pierre Smeyers's avatar
Pierre Smeyers committed

  # default: one-click deploy
  {{ cookiecutter.template_PREFIX }}_PROD_DEPLOY_STRATEGY: manual
{%- elif cookiecutter.template_type == 'acceptance' %}
  # Directory where {{ cookiecutter.template_PREFIX }} tests are implemented
  {{ cookiecutter.template_PREFIX }}_ROOT_DIR: "."
{%- endif %}

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

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

.{{ cookiecutter.template_prefix }}-scripts: &{{ cookiecutter.template_prefix }}-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

    # List of typical bundles
    bundles="/etc/ssl/certs/ca-certificates.crt"                            # Debian/Ubuntu/Gentoo etc.
    bundles="${bundles} /etc/ssl/cert.pem"                                  # Alpine Linux
    bundles="${bundles} /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"  # CentOS/RHEL 7
    bundles="${bundles} /etc/pki/tls/certs/ca-bundle.crt"                   # Fedora/RHEL 6
    bundles="${bundles} /etc/ssl/ca-bundle.pem"                             # OpenSUSE
    bundles="${bundles} /etc/pki/tls/cacert.pem"                            # OpenELEC

    # Try to find the right bundle to update it with custom CA certificates
    for bundle in ${bundles}
    do
      # import if bundle exists
      if [[ -f "${bundle}" ]]
      then
        # Import certificates in bundle
        echo "${certs}" | tr -d '\r' >> "${bundle}"

        log_info "Custom CA certificates imported in \\e[33;1m${bundle}\\e[0m"
        ca_imported=1
        break
      fi
    done

    if [[ -z "$ca_imported" ]]
    then
      log_warn "Could not import custom CA certificates !"
    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"
  }

  # 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}")"
        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}")"
        fi
      else
        fail "Couldn't get secret \\e[33;1m${name}\\e[0m: no http client found"
      fi
      ;;
    esac
  }

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

  function exec_hook() {
    if [[ ! -x "$1" ]] && ! chmod +x "$1"
    then
      log_warn "... could not make \\e[33;1m${1}\\e[0m executable: please do it (chmod +x)"
      # fallback technique
      sh "$1"
    else
      "$1"
    fi
  }

{%- if cookiecutter.template_type == 'build' %}
  function output_coverage() {
    echo "[TODO]: compute and output global coverage result"
    echo "11% covered"
  }
{%- elif cookiecutter.template_type == 'deploy' %}
  # Converts a string to SCREAMING_SNAKE_CASE
  function to_ssc() {
    echo "$1" | tr '[:lower:]' '[:upper:]' | tr '[:punct:]' '_'
  }

  function awkenvsubst() {
    awk '!/# *nosubst/{while(match($0,"[$%]{[^}]*}")) {var=substr($0,RSTART+2,RLENGTH-3);val=ENVIRON[var]; gsub(/["\\]/,"\\\\&",val); gsub("\n","\\n",val);gsub("\r","\\r",val); gsub("[$%]{"var"}",val)}}1'
  }

  # login to the hosting platform
  function {{ cookiecutter.template_prefix }}_login() {
    api_url=${ENV_API_URL:-${{ cookiecutter.template_PREFIX }}_API_URL}
    api_token=${ENV_API_TOKEN:-${{ cookiecutter.template_PREFIX }}_API_TOKEN}
    project=$ENV_PROJECT
    
    assert_defined "$api_url" 'Missing required API url'
    assert_defined "$api_token" 'Missing required API token'
    assert_defined "$project" 'Missing required project'
    
    {{ cookiecutter.cli_tool }} login --url="$api_url" --token="$api_token" --project="$project"
  }

Pierre Smeyers's avatar
Pierre Smeyers committed
  # application deployment function
  function {{ cookiecutter.template_prefix }}_deploy() {
    export environment_type=$ENV_TYPE
    export environment_name=${ENV_APP_NAME:-{{ '${' }}{{ cookiecutter.template_PREFIX }}_BASE_APP_NAME}${ENV_APP_SUFFIX}}
    environment_url=${ENV_URL:-${{ cookiecutter.template_PREFIX }}_ENVIRONMENT_URL}
Pierre Smeyers's avatar
Pierre Smeyers committed
    # also export environment_name in SCREAMING_SNAKE_CASE format (may be useful with Kubernetes env variables)
    environment_name_ssc=$(to_ssc "$environment_name")
Pierre Smeyers's avatar
Pierre Smeyers committed
    export environment_name_ssc

    # variables expansion in $environment_url
    environment_url=$(echo "$environment_url" | awkenvsubst)
    export environment_url
    # extract hostname from $environment_url
    hostname=$(echo "$environment_url" | awk -F[/:] '{print $4}')
    export hostname

    log_info "--- \\e[32mdeploy\\e[0m"
    log_info "--- \$environment_type: \\e[33;1m${environment_type}\\e[0m"
    log_info "--- \$environment_name: \\e[33;1m${environment_name}\\e[0m"
    log_info "--- \$environment_name_ssc: \\e[33;1m${environment_name_ssc}\\e[0m"
    log_info "--- \$hostname: \\e[33;1m${hostname}\\e[0m"

    # unset any upstream deployment env & artifacts
    rm -f {{ cookiecutter.project_slug }}.env
    rm -f environment_url.txt

    # TODO: implement the deployment here

    # persist environment url
    if [[ -f environment_url.txt ]]
    then
      environment_url=$(cat environment_url.txt)
      export environment_url
      log_info "--- dynamic environment url found: (\\e[33;1m$environment_url\\e[0m)"
    else
      echo "$environment_url" > environment_url.txt
    fi
    echo -e "environment_type=$environment_type\\nenvironment_name=$environment_name\\nenvironment_url=$environment_url" >> {{ cookiecutter.project_slug }}.env
Pierre Smeyers's avatar
Pierre Smeyers committed
  }

  # environment cleanup function
  function {{ cookiecutter.template_prefix }}_delete() {
    export environment_type=$ENV_TYPE
    export environment_name=${ENV_APP_NAME:-{{ '${' }}{{ cookiecutter.template_PREFIX }}_BASE_APP_NAME}${ENV_APP_SUFFIX}}
Pierre Smeyers's avatar
Pierre Smeyers committed
    # also export environment_name in SCREAMING_SNAKE_CASE format (may be useful with Kubernetes env variables)
    environment_name_ssc=$(to_ssc "$environment_name")
Pierre Smeyers's avatar
Pierre Smeyers committed
    export environment_name_ssc

    log_info "--- \\e[32mcleanup\\e[0m"
    log_info "--- \$environment_type: \\e[33;1m${environment_type}\\e[0m"
    log_info "--- \$environment_name: \\e[33;1m${environment_name}\\e[0m"
    log_info "--- \$environment_name_ssc: \\e[33;1m${environment_name_ssc}\\e[0m"

    # TODO: implement the cleanup here
  }
{%- elif cookiecutter.template_type == 'acceptance' %}
  # retrieve server url to test from upstream artifacts ($environment_url variable or 'environment_url.txt' file)
  function eval_env_url() {
    # shellcheck disable=SC2154
    if [[ -n "$environment_url" ]]
    then
      {{ cookiecutter.template_PREFIX }}_BASE_URL="$environment_url"
      export {{ cookiecutter.template_PREFIX }}_BASE_URL
      log_info "Upstream \$environment_url variable set: use base url \\e[33;1m${{ cookiecutter.template_PREFIX }}_BASE_URL\\e[0m"
    elif [[ -f environment_url.txt ]]
    then
      {{ cookiecutter.template_PREFIX }}_BASE_URL=$(cat environment_url.txt)
      export {{ cookiecutter.template_PREFIX }}_BASE_URL
      log_info "Upstream environment_url.txt file found: use base url \\e[33;1m${{ cookiecutter.template_PREFIX }}_BASE_URL\\e[0m"
    else
      log_info "No upstream environment url found: leave default"
    fi
  }
{%- endif %}

  unscope_variables
  eval_all_secrets

  # ENDSCRIPT

{%- if cookiecutter.template_type == 'build' %}
# job prototype
# defines default Docker image, tracking probe, cache policy and tags
.{{ cookiecutter.template_prefix }}-base:
  image: ${{ cookiecutter.template_PREFIX }}_IMAGE
  services:
    - name: "$TBC_TRACKING_IMAGE"
      command: ["--service", "{{ cookiecutter.project_slug }}", "1.0.0"]
  before_script:
    - *{{ cookiecutter.template_prefix }}-scripts
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
  # Cache downloaded dependencies and plugins between builds.
  # To keep cache across branches add 'key: "$CI_JOB_NAME"'
  # TODO (if necessary): define cache policy here
  cache:
    # cache shall be per branch per template
    key: "$CI_COMMIT_REF_SLUG-{{ cookiecutter.project_slug }}"
    paths:
      - .cache/

# (example) build & test job
{{ cookiecutter.template_prefix }}-build:
  extends: .{{ cookiecutter.template_prefix }}-base
  stage: build
  script:
    - mkdir -p -m 777 reports
    # TODO (if possible): $TRACE set enables debug logs on the tool
    # TODO (if possible): force test tool to produce JUnit report(s)
    # TODO (if possible): force test tool to compute code coverage with report
    - {{ cookiecutter.cli_tool }} ${TRACE+--verbose} --coverage --junit --output=reports/{{ cookiecutter.template_prefix }}-test.xunit.xml ${{ cookiecutter.template_PREFIX }}_BUILD_ARGS
    - output_coverage
  # TODO: code coverage support and GitLab integration (see: https://docs.gitlab.com/ee/ci/yaml/#coverage)
  coverage: '/^(\d+.\d+\%) covered$/'
  # keep build artifacts and test reports (see: https://docs.gitlab.com/ee/ci/yaml/#artifactsreportsjunit)
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 day
    reports:
      # TODO: Unit tests use JUnit format and GitLab integration (see: https://docs.gitlab.com/ee/ci/yaml/#artifactsreports)
      junit:
        - reports/{{ cookiecutter.template_prefix }}-test.xunit.xml
    paths:
        - build/
        - reports/

# (example) linter job
{{ cookiecutter.template_prefix }}-lint:
  extends: .{{ cookiecutter.template_prefix }}-base
  stage: build
  image: ${{ cookiecutter.template_PREFIX }}_LINT_IMAGE
  # force no dependency
  dependencies: []
  script:
    - mkdir -p -m 777 reports
    - {{ cookiecutter.cli_tool }}_lint ${TRACE+--verbose} --output=reports/{{ cookiecutter.template_prefix }}-lint.native.json ${{ cookiecutter.template_PREFIX }}_LINT_ARGS
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 day
    when: always
    paths:
      - reports/{{ cookiecutter.template_prefix }}-lint.*
  rules:
    # exclude if ${{ cookiecutter.template_PREFIX }}_LINT_DISABLED
    - if: '${{ cookiecutter.template_PREFIX }}_LINT_DISABLED == "true"'
      when: never
    # .test-policy rules
    - !reference [.test-policy, rules]

# (example) dependency check job
{{ cookiecutter.template_prefix }}-depcheck:
  extends: .{{ cookiecutter.template_prefix }}-base
  stage: test
  image: ${{ cookiecutter.template_PREFIX }}_DEPCHECK_IMAGE
  # force no dependency
  dependencies: []
  script:
    - {{ cookiecutter.cli_tool }}_depcheck ${TRACE+--verbose} ${{ cookiecutter.template_PREFIX }}_DEPCHECK_ARGS
  rules:
    # on schedule: auto
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      allow_failure: true
      when: always
    # all other cases: manual & non-blocking
    - when: manual
      allow_failure: true

# (example) publish job activated on env (${{ cookiecutter.template_PREFIX }}_PUBLISH_ENABLED), with required ${{ cookiecutter.template_PREFIX }}_PUBLISH_LOGIN and ${{ cookiecutter.template_PREFIX }}_PUBLISH_PASSWORD env verification
{{ cookiecutter.template_prefix }}-publish:
  extends: .{{ cookiecutter.template_prefix }}-base
  stage: publish
  before_script:
    - *{{ cookiecutter.template_prefix }}-scripts
    # verify ${{ cookiecutter.template_PREFIX }}_PUBLISH_LOGIN and ${{ cookiecutter.template_PREFIX }}_PUBLISH_PASSWORD are set
    - assert_defined "${{ cookiecutter.template_PREFIX }}_PUBLISH_LOGIN" 'Missing required env ${{ cookiecutter.template_PREFIX }}_PUBLISH_LOGIN'
    - assert_defined "${{ cookiecutter.template_PREFIX }}_PUBLISH_PASSWORD" 'Missing required env ${{ cookiecutter.template_PREFIX }}_PUBLISH_PASSWORD'
    - {{ cookiecutter.cli_tool }} login --login=${{ cookiecutter.template_PREFIX }}_PUBLISH_LOGIN --password=${{ cookiecutter.template_PREFIX }}_PUBLISH_PASSWORD
  script:
    - {{ cookiecutter.cli_tool }} ${{ cookiecutter.template_PREFIX }}_PUBLISH_ARGS
  rules:
    # exclude if ${{ cookiecutter.template_PREFIX }}_PUBLISH_ENABLED unset
    - if: '${{ cookiecutter.template_PREFIX }}_PUBLISH_ENABLED != "true"'
      when: never
    # on integration or production branch(es): manual & non-blocking
    - if: '$CI_COMMIT_REF_NAME =~ $INTEG_REF || $CI_COMMIT_REF_NAME =~ $PROD_REF'
      when: manual
      allow_failure: true
{%- elif cookiecutter.template_type == 'deploy' %}
# job prototype
# defines default Docker image, tracking probe, cache policy and tags
# Required vars for login:
# @var ENV_API_URL   : env-specific {{ cookiecutter.template_name }} API url
# @var ENV_API_TOKEN : env-specific {{ cookiecutter.template_name }} API token
# @var ENV_PROJECT   : env-specific project name
Pierre Smeyers's avatar
Pierre Smeyers committed
.{{ cookiecutter.template_prefix }}-base:
  image: ${{ cookiecutter.template_PREFIX }}_IMAGE
  services:
    - name: "$TBC_TRACKING_IMAGE"
      command: ["--service", "{{ cookiecutter.project_slug }}", "1.0.0"]
  before_script:
    - *{{ cookiecutter.template_prefix }}-scripts
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - {{ cookiecutter.template_prefix }}_login
Pierre Smeyers's avatar
Pierre Smeyers committed

# Deploy job prototype
# Can be extended to define a concrete environment
#
# @var ENV_TYPE      : environment type
# @var ENV_APP_NAME  : env-specific application name
# @var ENV_APP_SUFFIX: env-specific application suffix
# @var ENV_URL       : env-specific application url
Pierre Smeyers's avatar
Pierre Smeyers committed
.{{ cookiecutter.template_prefix }}-deploy:
  extends: .{{ cookiecutter.template_prefix }}-base
  stage: deploy
  variables:
    ENV_APP_SUFFIX: "-$CI_ENVIRONMENT_SLUG"
  script:
    - {{ cookiecutter.template_prefix }}_deploy
Pierre Smeyers's avatar
Pierre Smeyers committed
  artifacts:
    name: "$ENV_TYPE env url for $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    # TODO: propagate deployed env url in a environment_url.txt file
    paths:
      - environment_url.txt
    reports:
      # TODO: propagate deployed env info in a dotenv artifact
      dotenv: {{ cookiecutter.project_slug }}.env
  environment:
    url: "$environment_url" # can be either static or dynamic

# Cleanup job prototype
# Can be extended for each deletable environment
#
# @var ENV_TYPE      : environment type
# @var ENV_APP_NAME  : env-specific application name
# @var ENV_APP_SUFFIX: env-specific application suffix
Pierre Smeyers's avatar
Pierre Smeyers committed
.{{ cookiecutter.template_prefix }}-cleanup:
  extends: .{{ cookiecutter.template_prefix }}-base
  stage: deploy
  # force no dependencies
  dependencies: []
  variables:
    ENV_APP_SUFFIX: "-$CI_ENVIRONMENT_SLUG"
  script:
    - {{ cookiecutter.template_prefix }}_delete
Pierre Smeyers's avatar
Pierre Smeyers committed
  environment:
    action: stop

# deploy to review env (only on feature branches)
# disabled by default, enable this job by setting ${{ cookiecutter.template_PREFIX }}_REVIEW_PROJECT.
{{ cookiecutter.template_prefix }}-review:
  extends: .{{ cookiecutter.template_prefix }}-deploy
  variables:
    ENV_TYPE: review
    ENV_APP_NAME: "${{ cookiecutter.template_PREFIX }}_REVIEW_APP_NAME"
    ENV_URL: "${{ cookiecutter.template_PREFIX }}_REVIEW_ENVIRONMENT_URL"
    ENV_API_URL: "${{ cookiecutter.template_PREFIX }}_REVIEW_API_URL"
    ENV_API_TOKEN: "${{ cookiecutter.template_PREFIX }}_REVIEW_API_TOKEN"
    ENV_PROJECT: "${{ cookiecutter.template_PREFIX }}_REVIEW_PROJECT"
  environment:
    name: review/$CI_COMMIT_REF_NAME
    on_stop: {{ cookiecutter.template_prefix }}-cleanup-review
    auto_stop_in: "${{ cookiecutter.template_PREFIX }}_REVIEW_AUTOSTOP_DURATION"
Pierre Smeyers's avatar
Pierre Smeyers committed
  resource_group: review/$CI_COMMIT_REF_NAME
  rules:
    # exclude tags
    - if: $CI_COMMIT_TAG
      when: never
    # exclude if $CLEANUP_ALL_REVIEW set to 'force'
    - if: '$CLEANUP_ALL_REVIEW == "force"'
      when: never
    # only on non-production, non-integration branches, with ${{ cookiecutter.template_PREFIX }}_REVIEW_PROJECT set
    - if: '${{ cookiecutter.template_PREFIX }}_REVIEW_PROJECT && $CI_COMMIT_REF_NAME !~ $PROD_REF && $CI_COMMIT_REF_NAME !~ $INTEG_REF'

# cleanup review env (automatically triggered once branches are deleted)
{{ cookiecutter.template_prefix }}-cleanup-review:
  extends: .{{ cookiecutter.template_prefix }}-cleanup
  variables:
    ENV_TYPE: review
    ENV_APP_NAME: "${{ cookiecutter.template_PREFIX }}_REVIEW_APP_NAME"
    ENV_API_URL: "${{ cookiecutter.template_PREFIX }}_REVIEW_API_URL"
    ENV_API_TOKEN: "${{ cookiecutter.template_PREFIX }}_REVIEW_API_TOKEN"
    ENV_PROJECT: "${{ cookiecutter.template_PREFIX }}_REVIEW_PROJECT"
  environment:
    name: review/$CI_COMMIT_REF_NAME
    action: stop
  # TODO: use resource group
  resource_group: review/$CI_COMMIT_REF_NAME
  rules:
    # exclude tags
    - if: $CI_COMMIT_TAG
      when: never
    # only on non-production, non-integration branches, with ${{ cookiecutter.template_PREFIX }}_REVIEW_PROJECT set
    - if: '${{ cookiecutter.template_PREFIX }}_REVIEW_PROJECT && $CI_COMMIT_REF_NAME !~ $PROD_REF && $CI_COMMIT_REF_NAME !~ $INTEG_REF'
      when: manual
      allow_failure: true

# deploy to integration env (only on develop branch)
{{ cookiecutter.template_prefix }}-integration:
  extends: .{{ cookiecutter.template_prefix }}-deploy
  variables:
    ENV_TYPE: integration
    ENV_APP_NAME: "${{ cookiecutter.template_PREFIX }}_INTEG_APP_NAME"
    ENV_URL: "${{ cookiecutter.template_PREFIX }}_INTEG_ENVIRONMENT_URL"
    ENV_API_URL: "${{ cookiecutter.template_PREFIX }}_INTEG_API_URL"
    ENV_API_TOKEN: "${{ cookiecutter.template_PREFIX }}_INTEG_API_TOKEN"
    ENV_PROJECT: "${{ cookiecutter.template_PREFIX }}_INTEG_PROJECT"
  environment:
    name: integration
  # TODO: use resource group
  resource_group: integration
  rules:
    # only on integration branch(es), with ${{ cookiecutter.template_PREFIX }}_INTEG_PROJECT set
    - if: '${{ cookiecutter.template_PREFIX }}_INTEG_PROJECT && $CI_COMMIT_REF_NAME =~ $INTEG_REF'

# deploy to staging env (only on master branch)
{{ cookiecutter.template_prefix }}-staging:
  extends: .{{ cookiecutter.template_prefix }}-deploy
  variables:
    ENV_TYPE: staging
    ENV_APP_NAME: "${{ cookiecutter.template_PREFIX }}_STAGING_APP_NAME"
    ENV_URL: "${{ cookiecutter.template_PREFIX }}_STAGING_ENVIRONMENT_URL"
    ENV_API_URL: "${{ cookiecutter.template_PREFIX }}_STAGING_API_URL"
    ENV_API_TOKEN: "${{ cookiecutter.template_PREFIX }}_STAGING_API_TOKEN"
    ENV_PROJECT: "${{ cookiecutter.template_PREFIX }}_STAGING_PROJECT"
  environment:
    name: staging
  # TODO: use resource group
  resource_group: staging
  rules:
    # only on production branch(es), with ${{ cookiecutter.template_PREFIX }}_STAGING_PROJECT set
    - if: '${{ cookiecutter.template_PREFIX }}_STAGING_PROJECT && $CI_COMMIT_REF_NAME =~ $PROD_REF'

# Deploy to production if on branch master and variable {{ cookiecutter.template_PREFIX }}_PROD_PROJECT defined and AUTODEPLOY_TO_PROD is set
{{ cookiecutter.template_prefix }}-production:
  extends: .{{ cookiecutter.template_prefix }}-deploy
  stage: production
  variables:
    ENV_TYPE: production
    ENV_APP_SUFFIX: "" # no suffix for prod
    ENV_APP_NAME: "${{ cookiecutter.template_PREFIX }}_PROD_APP_NAME"
    ENV_URL: "${{ cookiecutter.template_PREFIX }}_PROD_ENVIRONMENT_URL"
    ENV_API_URL: "${{ cookiecutter.template_PREFIX }}_PROD_API_URL"
    ENV_API_TOKEN: "${{ cookiecutter.template_PREFIX }}_PROD_API_TOKEN"
    ENV_PROJECT: "${{ cookiecutter.template_PREFIX }}_PROD_PROJECT"
  environment:
    name: production
  # TODO: use resource group
  resource_group: production
  rules:
    # exclude non-production branches
    - if: '$CI_COMMIT_REF_NAME !~ $PROD_REF'
      when: never
    # exclude if ${{ cookiecutter.template_PREFIX }}_PROD_PROJECT not set
    - if: '${{ cookiecutter.template_PREFIX }}_PROD_PROJECT == null || ${{ cookiecutter.template_PREFIX }}_PROD_PROJECT == ""'
      when: never
    - if: '${{ cookiecutter.template_PREFIX }}_PROD_DEPLOY_STRATEGY == "manual"'
      when: manual
    - if: '${{ cookiecutter.template_PREFIX }}_PROD_DEPLOY_STRATEGY == "auto"'
{%- elif cookiecutter.template_type == 'acceptance' %}
# The main job that starts acceptance tests tool
# on master branch: automatically started after staging deployment
# on non-master branch: manually started after review env deployment (requires $REVIEW_ENABLED to be set)
{{ cookiecutter.project_slug }}:
  image: ${{ cookiecutter.template_PREFIX }}_IMAGE
  services:
    - name: "$TBC_TRACKING_IMAGE"
      command: ["--service", "{{ cookiecutter.project_slug }}", "1.0.0"]
  stage: acceptance
  # TODO (if necessary): define cache policy here
  cache:
    # cache shall be per branch per template
    key: "${CI_COMMIT_REF_SLUG}-{{ cookiecutter.project_slug }}"
    paths:
      - .cache/
  before_script:
    - *{{ cookiecutter.template_prefix }}-scripts
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - eval_env_url
    - cd "${{ cookiecutter.template_PREFIX }}_ROOT_DIR"
    # TODO (if necessary): do setup stuff here
  script:
    # TODO: run {{ cookiecutter.cli_tool }} tests
    # TODO (if possible): $TRACE set enables debug logs on the tool
    - mkdir -p -m 777 reports
    - {{ cookiecutter.cli_tool }} run ${TRACE+--verbose} --env BASE_URL=${{ cookiecutter.template_PREFIX }}_BASE_URL --junit --output=reports/{{ cookiecutter.project_slug }}.xunit.xml ${{ cookiecutter.template_PREFIX }}_EXTRA_ARGS
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    when: always
    paths:
      - ${{ cookiecutter.template_PREFIX }}_ROOT_DIR/reports/{{ cookiecutter.project_slug }}.*
    reports:
      # TODO (if possible): Acceptance tests use JUnit format and GitLab integration (see: https://docs.gitlab.com/ee/ci/yaml/#artifactsreports)
      junit:
        - ${{ cookiecutter.template_PREFIX }}_ROOT_DIR/reports/{{ cookiecutter.project_slug }}.xunit.xml
    expire_in: 1 day
  rules:
    - !reference [.acceptance-policy, rules]
{%- endif %}