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