image: php

variables:
  TEST_IMAGE: ${CI_REGISTRY_IMAGE}/engelsystem:${CI_COMMIT_REF_SLUG}
  RELEASE_IMAGE: ${CI_REGISTRY_IMAGE}/engelsystem:latest
  MYSQL_DATABASE: engelsystem
  MYSQL_USER: engel
  MYSQL_PASSWORD: engelsystem
  MYSQL_HOST: mariadb
  MYSQL_RANDOM_ROOT_PASSWORD: "yes"
  MYSQL_INITDB_SKIP_TZINFO: "yes"
  DOCROOT: /var/www/

stages:
  - prepare
  - validate
  - build
  - test
  - release
  - deploy
  - deploy-production
  - stop

.use_cache: &use_cache
  cache:
    key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
    paths:
      - .yarn-cache/
      - vendor/

# for jobs that depend on composer
.use_composer: &use_composer
  <<: *use_cache
  needs:
    - composer install
  before_script:
    - composer install --no-ansi --no-progress

# for jobs that depend on yarn
.use_yarn: &use_yarn
  <<: *use_cache
  needs:
    - yarn install
  before_script:
    - yarn install --check-frontend --cache-folder .yarn-cache

#
# Preparation
#

composer validate:
  image: composer:latest
  stage: prepare
  script:
    - composer --no-ansi validate --strict

composer install:
  <<: *use_cache
  image: composer:latest
  stage: prepare
  needs:
    - composer audit
    - composer validate
  script:
    - composer install --no-ansi --no-progress

composer audit:
  image: php:latest
  stage: prepare
  needs:
    - composer validate
  before_script:
    - curl -Ls https://github.com/symfony/cli/releases/latest/download/symfony_linux_amd64.gz | gzip -d > /bin/symfony
    - chmod +x /bin/symfony
  script:
    - symfony check:security --no-ansi

yarn-validate:
  image: node:alpine
  stage: prepare
  before_script:
    - yarn global add package-json-validator
    - export PATH=$PATH:~/.yarn/bin
  script:
    - pjv

yarn install:
  <<: *use_cache
  image: node:alpine
  stage: prepare
  needs:
    - yarn-validate
    - yarn audit
  script:
    - yarn install --check-frontend --cache-folder .yarn-cache

yarn audit:
  image: node:alpine
  stage: prepare
  needs:
    - yarn-validate
  script:
    - yarn audit

generate-version:
  image: alpine
  stage: prepare
  artifacts:
    name: "${CI_JOB_NAME}_${CI_JOB_ID}_version"
    expire_in: 1 day
    paths:
      - ./storage/app/VERSION
  before_script:
    - apk add -q git
  script:
    - VERSION="$(git describe --abbrev=0 --tags)-${CI_COMMIT_REF_NAME}+${CI_PIPELINE_ID}.${CI_COMMIT_SHORT_SHA}"
    - echo "${VERSION}"
    - echo -n "${VERSION}" > storage/app/VERSION

#
# Validation
#

phpcs:
  <<: *use_composer
  image: composer:latest
  stage: validate
  script:
    # tell phpcs the PHP version to check against
    # we are using the min suppported version here
    - ./vendor/bin/phpcs --config-set php_version 80100
    - ./vendor/bin/phpcs -p --no-colors --basepath="$PWD"

phpstan:
  <<: *use_composer
  image: composer:latest
  stage: validate
  script:
    - ./vendor/bin/phpstan --no-progress

yarn check:
  <<: *use_yarn
  image: node:alpine
  stage: validate
  script:
    - yarn check

yarn lint:
  <<: *use_yarn
  image: node:alpine
  stage: validate
  script:
    # Install git, so that tools can use .gitignore.
    # Not done in before_script because of <<: *use_yarn.
    - apk add --no-cache git
    - yarn lint

translations lint:
  image: alpine
  stage: prepare
  before_script:
    - apk add gettext
  script:
    - find resources/lang -type f -name '*.po' -exec sh -c 'msgfmt "${1%.*}.po" -o"${1%.*}.mo"' shell {} \;
    - '[[ $(find resources/lang -type f -name "*.po" | wc -l) == $(find resources/lang -type f -name "*.mo" | wc -l) ]]'

#
# Build
#

.container_template: &container_definition
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [ '' ]
  before_script:
    - mkdir -p /kaniko/.docker
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}"
      > /kaniko/.docker/config.json

build-image:
  <<: *container_definition
  stage: build
  needs:
    - phpcs
    - phpstan
    - composer validate
    - yarn check
    - yarn lint
    - translations lint
    - generate-version
  dependencies:
    - generate-version
  script:
    - /kaniko/executor --context ${CI_PROJECT_DIR}
      --dockerfile ${CI_PROJECT_DIR}/docker/Dockerfile
      --destination "${TEST_IMAGE}"
      --cache=true

#
# Test
#

test:
  image:
    name: ${TEST_IMAGE}
    entrypoint: [ '' ]
  stage: test
  needs: [ build-image ]
  services:
    - mariadb:10.2
  artifacts:
    name: "${CI_JOB_NAME}_${CI_JOB_ID}"
    expire_in: 1 week
    when: always
    paths:
      - ./coverage/
      - ./unittests.xml
    reports:
      junit: ./unittests.xml
  coverage: '/^\s*Lines:\s*(\d+(?:\.\d+)?%)/'
  before_script:
    - apk add -q ${PHPIZE_DEPS} && pecl install pcov > /dev/null && docker-php-ext-enable pcov
    - curl -sS https://getcomposer.org/installer | php -- --no-ansi --install-dir /usr/local/bin/ --filename composer
    - cp -R tests/ phpunit.xml "${DOCROOT}"
    - HOMEDIR=$PWD
    - cd "${DOCROOT}"
    - composer --no-ansi install
    - ./bin/migrate
  script:
    - >-
      php -d memory_limit=1024M -d pcov.enabled=1 -d pcov.directory=. vendor/bin/phpunit -vvv --colors=never
      --coverage-text --coverage-html "${HOMEDIR}/coverage/"
      --log-junit "${HOMEDIR}/unittests.xml"
  after_script:
    - '"${DOCROOT}/bin/migrate" down'

dump-database:
  image:
    name: ${TEST_IMAGE}
    entrypoint: [ '' ]
  stage: test
  needs: [ build-image ]
  services:
    - mariadb:10.2
  artifacts:
    expire_in: 1 week
    paths:
      - initial-install.sql
  before_script:
    - apk add -q mariadb-client
    - HOMEDIR=$PWD
    - cd "${DOCROOT}"
    - ./bin/migrate
  script:
    - >-
      mysqldump -h "${MYSQL_HOST}" -u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" "${MYSQL_DATABASE}"
      > "${HOMEDIR}/initial-install.sql"

generate-assets:
  image:
    name: $TEST_IMAGE
    entrypoint: [ '' ]
  stage: test
  needs: [ build-image ]
  artifacts:
    name: "${CI_JOB_NAME}_${CI_JOB_ID}_assets"
    expire_in: 1 day
    paths:
      - ./public/assets
  script:
    - mv /var/www/public/assets/ public/

#
# Release
#

release-image:
  <<: *container_definition
  stage: release
  needs:
    - test
  dependencies: [ ]
  script:
    - echo -e "FROM ${TEST_IMAGE}"
      | /kaniko/executor
      --dockerfile /dev/stdin
      --destination "${RELEASE_IMAGE}"
      --cache=true
  only:
    - main

.deploy_template: &deploy_definition
  stage: release
  image:
    name: ${TEST_IMAGE}
    entrypoint: [ '' ]
  before_script:
    - apk add -q bash rsync openssh-client

build-release-file:
  <<: *deploy_definition
  stage: release
  needs:
    - build-image
    - yarn audit
    - composer audit
    - test
    - dump-database
    - generate-assets
  dependencies:
    - build-image
    - dump-database
    - generate-assets
  artifacts:
    name: release_${CI_COMMIT_REF_SLUG}_${CI_JOB_ID}_${CI_COMMIT_SHA}
    expire_in: 1 week
    paths:
      - ./release/
  script:
    - rsync -vAax "${DOCROOT}" "${DOCROOT}/.babelrc" "${DOCROOT}/.browserslistrc" "initial-install.sql" release/
    - rsync -vAax public/assets release/public/

pages:
  image: node:alpine
  stage: release
  needs: [ test ]
  dependencies: [ test ]
  script:
    - rm -rf public
    - mv coverage public
    - cp unittests.xml public/
  artifacts:
    expire_in: 1 week
    paths:
      - public
  only:
    - main
  variables:
    GIT_STRATEGY: none

#
# Deploy staging
#

.deploy_template_script:
  # Configure SSH
  - &deploy_template_script |-
    eval $(ssh-agent -s) && echo "${SSH_PRIVATE_KEY}" | ssh-add -
    rsync -vAax public/assets ${DOCROOT}/public/
    cd "${DOCROOT}"

deploy:
  <<: *deploy_definition
  stage: deploy
  needs: &deploy_needs
    - release-image
    - generate-assets
  dependencies: *deploy_needs
  environment:
    name: rsync-staging
    deployment_tier: development
  only:
    - main
  script:
    # Check if deployment variables where set
    - |-
      if [ -z "${SSH_PRIVATE_KEY}" ] || [ -z "${STAGING_REMOTE}" ] || [ -z "${STAGING_REMOTE_PATH}" ]; then
        echo "Skipping deployment"
        exit
      fi
    - *deploy_template_script
    # Deploy to server
    - ./bin/deploy.sh -r "${STAGING_REMOTE}" -p "${STAGING_REMOTE_PATH}" -i "${CI_JOB_ID}-${CI_COMMIT_SHA}"

.kubectl_deployment: &kubectl_deployment
  stage: deploy
  image:
    name: bitnami/kubectl:latest
    entrypoint: [ '' ]
  needs:
    - test
    - build-image
  before_script:
    - &kubectl_deployment_script |-
      if [[ -z "${KUBE_INGRESS_BASE_DOMAIN}" ]]; then echo "Skipping deployment"; exit; fi
      if [[ -n "${KUBE_CONTEXT}" ]]; then kubectl config use-context "${KUBE_CONTEXT}"; fi
      if [[ -z "${KUBE_NAMESPACE}" ]]; then export KUBE_NAMESPACE=${CI_PROJECT_PATH_SLUG}-${CI_ENVIRONMENT_SLUG}; fi

.deploy_k8s: &deploy_k8s
  <<: *kubectl_deployment
  dependencies: [ ]
  artifacts:
    name: deployment.yaml
    expire_in: 1 day
    when: always
    paths:
      - deployment.yaml
  script:
    # CI_ENVIRONMENT_URL is the URL configured in the GitLab environment
    - export CI_ENVIRONMENT_URL="${CI_ENVIRONMENT_URL:-https://${CI_PROJECT_PATH_SLUG}.${KUBE_INGRESS_BASE_DOMAIN}/}"
    - export CI_IMAGE=$RELEASE_IMAGE
    - export CI_INGRESS_CLASS=${CI_INGRESS_CLASS:-traefik}
    - export CI_INGRESS_MATCH=${CI_INGRESS_MATCH:-$( if [[ "$CI_INGRESS_CLASS" == "nginx" ]]; then echo '/?(.*)'; fi )}
    - export CI_INGRESS_TRAEFIK_ENTRYPOINT=${CI_INGRESS_TRAEFIK_ENTRYPOINT:-websecure}
    - export CI_INGRESS_DOMAIN=$(echo "$CI_ENVIRONMENT_URL" | grep -oP '(?:https?://)?\K([^/]+)' | head -n1)
    - export CI_INGRESS_PATH=$(echo "$CI_ENVIRONMENT_URL" | grep -oP '(?:https?://)?(?:[^/])+\K(.*)')
    - '[[ "${CI_INGRESS_PATH}" == /* ]] || export CI_INGRESS_PATH="/${CI_INGRESS_PATH}"'
    - export CI_KUBE_NAMESPACE=$KUBE_NAMESPACE
    # Any available storage class like default, local-path (if you know what you are doing ;), longhorn etc.
    - export CI_PVC_SC=${CI_PVC_SC:-"${CI_PVC_SC_LOCAL:-local-path}"}
    - export CI_REPLICAS=${CI_REPLICAS_REVIEW:-${CI_REPLICAS:-2}}
    - export CI_APP_NAME=${CI_APP_NAME:-Engelsystem}
    - export CI_CLUSTER_ISSUER=${CI_CLUSTER_ISSUER:-letsencrypt}
    - export CI_SETUP_ADMIN_PASSWORD=${CI_SETUP_ADMIN_PASSWORD}

    - echo "Generating config"
    - cp deployment.tpl.yaml deployment.yaml
    - >-
      for env in ${!CI_*}; do
        sed -i "s#<${env}>#$( echo "${!env}" | head -n1 | sed -e 's~\\~\\\\~' -e 's~#~\\#~' )#g" deployment.yaml;
      done

    - echo "Checking namespace ${CI_KUBE_NAMESPACE}"
    - kubectl get namespace "${CI_KUBE_NAMESPACE}" > /dev/null 2>&1 || kubectl create namespace "${CI_KUBE_NAMESPACE}"

    - echo "Deploying to ${CI_ENVIRONMENT_URL}"
    - kubectl -n "${CI_KUBE_NAMESPACE}" diff -f deployment.yaml || true
    - kubectl -n "${CI_KUBE_NAMESPACE}" apply -f deployment.yaml
    - >-
      kubectl -n "${CI_KUBE_NAMESPACE}" wait --for=condition=Ready pods --timeout=${CI_WAIT_TIMEOUT:-5}m
      -l app=$CI_PROJECT_PATH_SLUG -l tier=database
    - >-
      kubectl -n "${CI_KUBE_NAMESPACE}" wait --for=condition=Ready pods --timeout=${CI_WAIT_TIMEOUT:-5}m
      -l app=$CI_PROJECT_PATH_SLUG -l tier=application -l commit=$CI_COMMIT_SHORT_SHA

.deploy_k8s_stop: &deploy_k8s_stop
  <<: *kubectl_deployment
  stage: stop
  dependencies: [ ]
  variables:
    GIT_STRATEGY: none
  when: manual
  script:
    - kubectl delete all,ingress,pvc -l app=$CI_PROJECT_PATH_SLUG -l environment=$CI_ENVIRONMENT_SLUG

deploy-k8s-review:
  <<: *deploy_k8s
  environment:
    name: review/${CI_COMMIT_REF_NAME}
    on_stop: stop-k8s-review
    auto_stop_in: 1 week
    url: https://${CI_PROJECT_PATH_SLUG}-review.${KUBE_INGRESS_BASE_DOMAIN}/${CI_COMMIT_REF_SLUG}
    deployment_tier: development
  variables:
    CI_REPLICAS_REVIEW: 1
    CI_APP_NAME: review/${CI_COMMIT_REF_NAME}
  before_script:
    - *kubectl_deployment_script
    - RELEASE_IMAGE=$TEST_IMAGE

stop-k8s-review:
  <<: *deploy_k8s_stop
  needs: [ deploy-k8s-review ]
  environment:
    name: review/${CI_COMMIT_REF_NAME}
    action: stop
    deployment_tier: development

#
# Deploy production
#

deploy-production:
  <<: *deploy_definition
  stage: deploy-production
  needs:
    - test
    - yarn audit
    - composer audit
    - build-image
    - generate-assets
  dependencies:
    - build-image
    - generate-assets
  environment:
    name: rsync-production
    deployment_tier: production
  when: manual
  only:
    - main
  script:
    # Check if deployment variables where set
    - |-
      if [ -z "${SSH_PRIVATE_KEY}" ] || [ -z "${PRODUCTION_REMOTE}" ] || [ -z "${PRODUCTION_REMOTE_PATH}" ]; then
        echo "Skipping deployment"
        exit
      fi
    - *deploy_template_script
    # Deploy to server
    - ./bin/deploy.sh -r "${PRODUCTION_REMOTE}" -p "${PRODUCTION_REMOTE_PATH}" -i "${CI_JOB_ID}-${CI_COMMIT_SHA}"

deploy-k8s-production:
  <<: *deploy_k8s
  stage: deploy-production
  needs:
    - release-image
    - yarn audit
    - composer audit
  environment:
    name: production
    on_stop: stop-k8s-production
  when: manual
  only:
    - main

stop-k8s-production:
  <<: *deploy_k8s_stop
  needs: [ deploy-k8s-production ]
  only:
    - main
  environment:
    name: production
    action: stop
