diff --git a/includes/cache.yml b/includes/cache.yml new file mode 100644 index 0000000000000000000000000000000000000000..434f372cbde4c77caddf41b83bb03342793dada3 --- /dev/null +++ b/includes/cache.yml @@ -0,0 +1,8 @@ +.cache-node-modules: + cache: + key: "$CI_COMMIT_REF_SLUG" # we have a fallback configured using CACHE_FALLBACK_KEY + policy: pull-push + paths: + - "$APP_DIR/node_modules/" + - "$APP_DIR/.next/cache/" + - "$CYPRESS_CACHE_FOLDER" diff --git a/includes/docker-build.yml b/includes/docker-build.yml new file mode 100644 index 0000000000000000000000000000000000000000..89e74cbbb1b1447f37433af48f3e720843b3de17 --- /dev/null +++ b/includes/docker-build.yml @@ -0,0 +1,93 @@ +.ensureStorybookDockerfile: &ensureStorybookDockerfile | + function ensureStorybookDockerfile() { + # this is for meteor + if [ ! -f Dockerfile-storybook ]; then + echo "Creating Dockerfile-storybook" + cat > Dockerfile-storybook <<EOF + FROM nginx + EXPOSE 80 + COPY .storybook-out/ /usr/share/nginx/html + EOF + + fi + + } + +.docker-build-base: + image: docker:18-dind + retry: 2 + services: + - docker:18-dind + variables: + DOCKER_DRIVER: overlay2 + +.docker-build-app-base: + extends: .docker-build-base + script: + - docker login --username gitlab-ci-token --password $CI_JOB_TOKEN $CI_REGISTRY + + - docker pull $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG || true + - docker build --cache-from $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG --tag $CI_REGISTRY_IMAGE:$IMAGE_TAG $DOCKER_DIR + - docker push $CI_REGISTRY_IMAGE:$IMAGE_TAG + - docker tag $CI_REGISTRY_IMAGE:$IMAGE_TAG $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG + - docker push $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG + +.docker-build-storybook-base: + extends: .docker-build-base + script: + - *ensureStorybookDockerfile + - ensureStorybookDockerfile + - docker login --username gitlab-ci-token --password $CI_JOB_TOKEN $CI_REGISTRY + - docker pull $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG-storybook || true + - docker build --cache-from $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG-storybook --tag $CI_REGISTRY_IMAGE:$IMAGE_TAG-storybook $DOCKER_DIR -f Dockerfile-storybook + - docker push $CI_REGISTRY_IMAGE:$IMAGE_TAG-storybook + - docker tag $CI_REGISTRY_IMAGE:$IMAGE_TAG-storybook $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG-storybook + - docker push $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG-storybook + +.docker-build: + stage: docker-build + extends: + - .rules-always + - .docker-build-app-base + needs: ["app-build"] + +.docker-build-storybook: + stage: docker-build + only: + refs: + - master + - main + - production + - tags + - merge_requests + variables: + - $STORYBOOK == "true" + extends: .docker-build-storybook-base + needs: ["storybook-build"] + +.storybook-build: + extends: + - .cache-node-modules + cache: + policy: pull + stage: build + only: + refs: + - master + - main + - production + - tags + - merge_requests + variables: + - $STORYBOOK == "true" + + needs: ["setup-build-info"] + # don't do it after the semantic release commit, but do it after tag + script: + - cd $APP_PATH + - if [ -f ./.nvmrc ]; then source /root/.nvm/nvm.sh && nvm install <<< .nvmrc; fi + - yarn install --frozen-lockfile + - yarn build-storybook -c .storybook -s public -o .storybook-out --quiet + artifacts: + paths: + - $CI_PROJECT_DIR/.storybook-out diff --git a/includes/env.yml b/includes/env.yml new file mode 100644 index 0000000000000000000000000000000000000000..a01aa0d5753845e6af6d6c7bc23e5e8c61fdad67 --- /dev/null +++ b/includes/env.yml @@ -0,0 +1,50 @@ +.env-base: + variables: + # deprecated, use KUBE_NAMESPACE + NAMESPACE: "${CUSTOMER_NAME}-${APP_NAME}-${ENV_SHORT}" + + HOST_CANONICAL: $APP_NAME-$CI_ENVIRONMENT_SLUG.$CUSTOMER_NAME.panter.cloud + IMAGE_PULL_SECRET: gitlab-registry-$COMPONENT_NAME + environment: + url: https://$APP_NAME-$CI_ENVIRONMENT_SLUG.$CUSTOMER_NAME.panter.cloud + name: ${ENV_SHORT}-${COMPONENT_NAME} + kubernetes: + namespace: "${CUSTOMER_NAME}-${APP_NAME}-${ENV_SHORT}" + +.env-review: + extends: .env-base + variables: + ENV_SHORT: review + REPLICAS: 1 + MONGODB_REPLICAS: 1 + MONGODB_STORAGE_CLASS: "standard" + MONGODB_BACKUP_ENABLED: "false" + DEPLOY_SSL_ISSUER: letsencrypt-staging + KUBE_APP_NAME: ${COMPONENT_NAME}-${CI_COMMIT_REF_SLUG} + + environment: + name: ${ENV_SHORT}-${COMPONENT_NAME}/$CI_COMMIT_REF_NAME + on_stop: review-stop + auto_stop_in: 2 weeks + +.env-dev: + extends: .env-base + variables: + REPLICAS: 1 + MONGODB_REPLICAS: 1 + MONGODB_STORAGE_CLASS: "standard" + DEPLOY_SSL_ISSUER: letsencrypt-staging + ENV_SHORT: dev + +.env-stage: + extends: .env-base + variables: + REPLICAS: 2 + DEPLOY_SSL_ISSUER: letsencrypt-staging + ENV_SHORT: stage + +.env-prod: + extends: .env-base + variables: + STORYBOOK: "false" # don't deploy storybook to prod, never + ENV_SHORT: prod diff --git a/includes/kubernetes.yml b/includes/kubernetes.yml new file mode 100644 index 0000000000000000000000000000000000000000..8e66fb41a16904d180c099a6d2a50394b347e690 --- /dev/null +++ b/includes/kubernetes.yml @@ -0,0 +1,128 @@ +.auto_devops: &auto_devops | + + function kubernetesEnsureNamespace() { + echo "Ensure Namespace $NAMESPACE" + kubectl describe namespace "$NAMESPACE" || kubectl create namespace "$NAMESPACE" + } + + function kubernetesCreateSecret() { + kubectl create secret -n "$NAMESPACE" \ + docker-registry $IMAGE_PULL_SECRET \ + --docker-server="$CI_REGISTRY" \ + --docker-username="${CI_DEPLOY_USER:-$CI_REGISTRY_USER}" \ + --docker-password="${CI_DEPLOY_PASSWORD:-$CI_REGISTRY_PASSWORD}" \ + --docker-email="$GITLAB_USER_EMAIL" \ + -o yaml --dry-run | kubectl replace -n "$NAMESPACE" --force -f - + } + + function kubernetesDeploy() { + + echo "Deploy to kubernetes" + RELEASE_NAME=${CUSTOMER_NAME}-${APP_NAME}-${CI_ENVIRONMENT_SLUG} + echo "Release: $RELEASE_NAME" + echo "URL (canonical): $HOST_CANONICAL" + helm init --client-only --stable-repo-url=https://charts.helm.sh/stable + helm repo add panter-charts http://panter.git.panter.biz/helm-charts + helm repo update + echo "panter chart repo added" + + # use helmArgs defined in variables + helmArgsEvaluated=$(echo $helmArgs | envsubst ) + + # this bash-brainfuck simply splits $helmArgs by newline to an array... + readarray -t helmArgsArray <<<"$helmArgsEvaluated" + + #Log the time until selfdestruct in case of review environment + if [[ ${ENV_SHORT} == "review" ]]; then echo "NOTE - This deployment will stop itself after 2 weeks"; fi + + # find all values files and generate an array out of it + files=(values.yml values-$ENV_SHORT.yml values-$CI_ENVIRONMENT_SLUG.yml) + values=() + for i in "${files[@]}"; do if [ -f $VALUES_PATH/$i ]; then values+=(--values $VALUES_PATH/$i); fi; done + echo "reading values files from $VALUES_PATH. found ${values[@]}" + + echo "doing helm upgrade" + helm upgrade --install "$RELEASE_NAME" $CHART_NAME \ + --wait \ + --timeout 600 \ + --cleanup-on-fail \ + --set global.componentName="$KUBE_APP_NAME" \ + --set global.ENV_SHORT="$ENV_SHORT" \ + --set image.repository="$CI_REGISTRY_IMAGE" \ + --set image.pullSecret="$IMAGE_PULL_SECRET" \ + --set image.tag="$IMAGE_TAG" \ + --set image.pullPolicy=IfNotPresent \ + --set storybook.enabled=$STORYBOOK \ + --set application.replicas="$REPLICAS" \ + --set application.hostCanonical="$HOST_CANONICAL" \ + --set application.worker.enabled=$WORKER_ENABLED \ + --set mongodb.enabled=$MONGODB_ENABLED \ + --set mongodb.backup.enabled=$MONGODB_BACKUP_ENABLED \ + --set mongodb-replicaset.replicas=$MONGODB_REPLICAS \ + --set mongodb-replicaset.fullnameOverride=$KUBE_APP_NAME-mongodb-replicaset \ + --set mongodb-replicaset.persistentVolume.storageClass="$MONGODB_STORAGE_CLASS" \ + --set prisma.enabled=$PRISMA_ENABLED \ + --set gitlab.visualReviewEnabled="$GITLAB_VISUAL_REVIEW_ENABLED" \ + --set gitlab.app="$CI_PROJECT_PATH_SLUG" \ + --set gitlab.projectPath="$CI_PROJECT_PATH" \ + --set gitlab.mergeRequestId="$CI_MERGE_REQUEST_IID" \ + --set gitlab.projectId="$CI_PROJECT_ID" \ + --set gitlab.env="$CI_ENVIRONMENT_SLUG" \ + --set namespace="$NAMESPACE" \ + --namespace="$NAMESPACE" \ + ${helmArgsArray[@]} \ + ${values[@]} + echo "Deployment successful 😻" + } + + function kubernetesDelete() { + RELEASE_NAME=${CUSTOMER_NAME}-${APP_NAME}-${CI_ENVIRONMENT_SLUG} + echo "Delete $RELEASE_NAME" + name="$RELEASE_NAME" + + if [[ -n "$(helm ls -q "^$name$")" ]]; then + helm delete --purge "$name" + + fi + } + +.deploy-to-kubernetes: + extends: .env-base + retry: 2 + script: + - *auto_devops + - kubernetesEnsureNamespace + - kubernetesCreateSecret + - kubernetesDeploy + +.deploy-to-kubernetes-review: + extends: + - .deploy-to-kubernetes + - .env-review + +.kubernetes-review-stop: + variables: + KUBE_APP_NAME: ${COMPONENT_NAME}-${CI_COMMIT_REF_SLUG} + ENV_SHORT: review + GIT_STRATEGY: none + environment: + name: ${ENV_SHORT}-${COMPONENT_NAME}/$CI_COMMIT_REF_NAME + action: stop + script: + - *auto_devops + - kubernetesDelete + +.deploy-to-kubernetes-dev: + extends: + - .deploy-to-kubernetes + - .env-dev + +.deploy-to-kubernetes-stage: + extends: + - .deploy-to-kubernetes + - .env-stage + +.deploy-to-kubernetes-prod: + extends: + - .deploy-to-kubernetes + - .env-prod diff --git a/includes/open-mr.yml b/includes/open-mr.yml new file mode 100644 index 0000000000000000000000000000000000000000..495a1c7abc0b8894b2e18a035ad9b722423aa93c --- /dev/null +++ b/includes/open-mr.yml @@ -0,0 +1,14 @@ +.openMr: + image: tmaier/gitlab-auto-merge-request:1 + before_script: [] # We do not need any setup work, let's remove the global one (if any) + variables: + GIT_STRATEGY: none # We do not need a clone of the GIT repository to create a Merge Request + stage: openMr + script: + - export GITLAB_PRIVATE_TOKEN=$GL_TOKEN + - merge-request.sh # The name of the script + only: + refs: + - branches + variables: + - $AUTOMATIC_MR == "true" diff --git a/includes/rules.yml b/includes/rules.yml new file mode 100644 index 0000000000000000000000000000000000000000..80f36c2a2be0154841f876d4b94ca3bd754d9a47 --- /dev/null +++ b/includes/rules.yml @@ -0,0 +1,45 @@ +.rules-always: + rules: + - if: $CI_COMMIT_TAG + - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\).*/ # not after semantic release commit + when: never + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + - if: $CI_COMMIT_BRANCH =~ /^[0-9]+\.([0-9]+|x)\.x$/ # hotfix branches + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + +.rules-always-but-not-on-tags: + rules: + - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\).*/ # not after semantic release commit + when: never + - if: $CI_COMMIT_TAG + when: never + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + - if: $CI_COMMIT_BRANCH =~ /^[0-9]+\.([0-9]+|x)\.x$/ # hotfix branches + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + +.rules-dev-deploy: + rules: + - if: $CI_COMMIT_MESSAGE =~ /^chore.*/ # not after semaitc release commit + when: never + - if: $CI_COMMIT_MESSAGE =~ /^chore\(maintenance\).*/ # not aftter hotfix release + when: never + - if: $CI_COMMIT_TAG # after releases + when: on_success + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + when: on_success + +.rules-stage-deploy: + rules: + - if: $CI_COMMIT_TAG && $CI_COMMIT_MESSAGE =~ /^chore\(maintenance\).*/ # d + when: manual # do not do automatic release after a hotfix / maintenance release + - if: $CI_COMMIT_TAG && $STAGING_ENABLED == "true" + when: on_success + +.rules-prod-deploy: + rules: + - if: $CI_COMMIT_TAG && $CI_COMMIT_MESSAGE =~ /^chore\(maintenance\).*/ # d + when: manual # do not do automatic release after a hotfix / maintenance release + - if: $CI_COMMIT_TAG && $STAGING_ENABLED != "true" # do only automatic release when staging is not enabled + when: on_success + - if: $CI_COMMIT_TAG + when: manual diff --git a/includes/semantic-release.yml b/includes/semantic-release.yml new file mode 100644 index 0000000000000000000000000000000000000000..014b3026727c5c810709f11945596a75e6afa19f --- /dev/null +++ b/includes/semantic-release.yml @@ -0,0 +1,90 @@ +.semanticRelease-script: &semanticRelease-script | + + function semanticRelease() { + # 1. create "@semantic-release/changelog" + # 2. commit it to git (and push) + # 3. optional: update package.json + + # better branches settings: https://github.com/semantic-release/semantic-release/issues/1581 + + echo "doing semantic release" + + yarn global add --silent semantic-release \ + @semantic-release/commit-analyzer \ + @semantic-release/release-notes-generator \ + https://github.com/panter/git.git#b4037626cc583d3e9f7995ea0604b23730d0a22e \ + @semantic-release/changelog \ + @semantic-release/gitlab \ + @semantic-release/exec; + + if [ ! -e .releaserc ]; then + cat > .releaserc <<EOF + { + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + preset: "angular", + releaseRules: [ + { breaking: true, release: "major" }, + { revert: true, release: "patch" }, + + { type: "feat", release: "minor" }, + { type: "docs", release: "patch" }, + { type: "fix", release: "patch" }, + { type: "perf", release: "patch" }, + # always fall back to a patch release + { + message: "*", + release: "patch", + }, + ], + }, + ], + "@semantic-release/release-notes-generator", + "@semantic-release/changelog", + ["@semantic-release/git", { + "message": "chore(\${branch.type}): \${nextRelease.version}\n\n\${nextRelease.notes}" + }], + ["@semantic-release/gitlab", { + "gitlabUrl": "https://git.panter.ch" + }] + ], + "branches": [ + "+([0-9])?(.{+([0-9]),x}).x", + "master", + "main", + "next", + "next-major", + { + "name": "beta", + "prerelease": true + }, + { + "name": "alpha", + "prerelease": true + } + ], + } + EOF + fi + semantic-release + } + +.semantic-release-base: + script: + - *semanticRelease-script + - semanticRelease + +.create-release: + stage: create-release + extends: .semantic-release-base + rules: + - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\).*/ # not after semantic release commit + when: never + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $AUTO_RELEASE == "true" + when: on_success + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + when: manual + - if: $CI_COMMIT_BRANCH =~ /^[0-9]+\.([0-9]+|x)\.x$/ # hotfix branches + when: manual diff --git a/includes/setup.yml b/includes/setup.yml new file mode 100644 index 0000000000000000000000000000000000000000..01d793a8b72efecd8d83959beffc3cd5ba9c63f9 --- /dev/null +++ b/includes/setup.yml @@ -0,0 +1,21 @@ +.setup-build-info: + stage: setup + extends: + - .rules-always + retry: 2 + script: + - cd $APP_PATH + - BUILD_TAG="$(git describe --tags || git rev-parse HEAD)" + - BUILD_ID="$COMPONENT_NAME-$BUILD_TAG" + - BUILD_COMMIT="$(git rev-parse HEAD)" + - BUILD_TIME="$(date)" + - echo "BUILD_TAG=$BUILD_TAG" >> build.env + - echo "BUILD_ID=$BUILD_ID" >> build.env + - echo "BUILD_COMMIT=$BUILD_COMMIT" >> build.env + - echo "BUILD_TIME=$BUILD_TIME" >> build.env + - echo '{"id":"'$BUILD_ID'","commit":"'$BUILD_COMMIT'","tag":"'$BUILD_TAG'","time":"'$BUILD_TIME'"}' > __build_info.json + artifacts: + paths: + - $CI_PROJECT_DIR/__build_info.json + reports: + dotenv: build.env diff --git a/meteor-kubernetes.yml b/meteor-kubernetes.yml new file mode 100644 index 0000000000000000000000000000000000000000..f3798fb7cc4545d04ac7e06ceaf55f3145341f35 --- /dev/null +++ b/meteor-kubernetes.yml @@ -0,0 +1,54 @@ +include: + - node-kubernetes.yml + +app-build: + extends: .app-build + script: + - cd $APP_PATH + - echo "add healthcheck package" + - meteor add panter:healthroute --allow-superuser + - meteor add qualia:prod-shell --allow-superuser + - echo "current path is $(pwd)" + - yarn + - echo "building into $CI_PROJECT_DIR" + - TOOL_NODE_FLAGS="--max_old_space_size=3584 --min_semi_space_size=8 --max_semi_space_size=256 --optimize_for_size" meteor build $CI_PROJECT_DIR --architecture os.linux.x86_64 --allow-superuser --server-only --directory + - cp $CI_PROJECT_DIR/__build_info.json $CI_PROJECT_DIR/bundle/programs/server + artifacts: + paths: + - $CI_PROJECT_DIR/__build_info.json + - $CI_PROJECT_DIR/bundle + +.ensureDocker: &ensureDocker | + function ensureDockerfile() { + if [ ! -f .dockerignore ]; then + cat > .dockerignore <<EOF + node_modules + .git + + EOF + fi + + if [ ! -f Dockerfile ]; then + echo "Creating Dockerfile" + NODE_VERSION_FULL=$(cat bundle/.node_version.txt) + NODE_VERSION=${NODE_VERSION_FULL/v} # remove v + + cat > Dockerfile <<EOF + FROM node:$NODE_VERSION + ADD . /src + RUN cd /src/bundle/programs/server && npm install + + WORKDIR /src/bundle + EXPOSE 8888 + CMD ["node", "main.js"] + EOF + + fi + + } + +docker-build: + extends: .docker-build + before_script: + - *ensureDocker + - ensureDockerfile diff --git a/node-kubernetes.yml b/node-kubernetes.yml index d23d9c8d3d3e82259c34b13cccba3e4839ecfb30..7573cf19c0877c6a813cdbc2c5c763d083723cce 100644 --- a/node-kubernetes.yml +++ b/node-kubernetes.yml @@ -44,3 +44,52 @@ docker-build: before_script: - *ensureDocker - ensureDockerfile + +.before-script-yarn: + extends: .cache-node-modules + before_script: + - cd $APP_PATH + - if [ -f ./.nvmrc ]; then source /root/.nvm/nvm.sh && nvm install <<< .nvmrc; fi + - yarn install --frozen-lockfile + +.test-node: + stage: test + + extends: + - .before-script-yarn + - .test-base + + script: + - yarn test + +test: + extends: .test-node + +.lint-node: + stage: test + extends: + - .before-script-yarn + - .lint-base + + script: + - yarn lint + needs: [] + +lint: + extends: .lint-node + +.app-build-node: + extends: + - .before-script-yarn + - .app-build-base + script: + - yarn build + + artifacts: + paths: + - $CI_PROJECT_DIR/__build_info.json + - $CI_PROJECT_DIR/dist + - $CI_PROJECT_DIR/.next + +app-build: + extends: .app-build-node diff --git a/panter-kubernetes-base.yml b/panter-kubernetes-base.yml index 2ec95255e861f5f81fd0ab8ef7ff40ca891354e4..c6f041c48f065cf0ef785069995335be41eb3906 100644 --- a/panter-kubernetes-base.yml +++ b/panter-kubernetes-base.yml @@ -1,3 +1,13 @@ +include: + - /includes/env.yml + - /includes/cache.yml + - /includes/rules.yml + - /includes/setup.yml + - /includes/docker-build.yml + - /includes/semantic-release.yml + - /includes/kubernetes.yml + - /includes/open-mr.yml + variables: CUSTOMER_NAME: panter APP_NAME: demo @@ -8,16 +18,18 @@ variables: WORKER_ENABLED: "false" # whether to create a custom worker CHART_NAME: panter-charts/the-panter-chart REPLICAS: "3" # number of pods (default for prod) + MONGODB_ENABLED: "false" MONGODB_BACKUP_ENABLED: "true" MONGODB_REPLICAS: "3" # nubmer of mongodb replicas (default for prod) MONGODB_STORAGE_CLASS: "fast" + DOCKER_DIR: "." GITLAB_VISUAL_REVIEW_ENABLED: "false" STORYBOOK: "false" AUTOMATIC_MR: "false" - # PRISMA + # PRISMA 1 (deprecated) PRISMA_ENABLED: "false" # INTERNAL VARIABLES, don't override @@ -27,6 +39,10 @@ variables: # APP NAME LOCAL TO THE APP NAMESPACEGIT S KUBE_APP_NAME: ${COMPONENT_NAME} + # see https://on.cypress.io/caching + CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/.cache/Cypress" + CACHE_FALLBACK_KEY: $CI_DEFAULT_BRANCH + # Docker DOCKER_HOST: tcp://0.0.0.0:2375 DOCKER_TLS_CERTDIR: "" @@ -45,464 +61,42 @@ image: panterch/docker-ci-kubernetes-deploy # SCRIPTS #---------------------- -.auto_devops: &auto_devops | - - function semanticRelease() { - # 1. create "@semantic-release/changelog" - # 2. commit it to git (and push) - # 3. optional: update package.json - - # better branches settings: https://github.com/semantic-release/semantic-release/issues/1581 - - echo "doing semantic release" - - yarn global add --silent semantic-release \ - @semantic-release/commit-analyzer \ - @semantic-release/release-notes-generator \ - https://github.com/panter/git.git#b4037626cc583d3e9f7995ea0604b23730d0a22e \ - @semantic-release/changelog \ - @semantic-release/gitlab \ - @semantic-release/exec; - - if [ ! -e .releaserc ]; then - cat > .releaserc <<EOF - { - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - preset: "angular", - releaseRules: [ - { breaking: true, release: "major" }, - { revert: true, release: "patch" }, - - { type: "feat", release: "minor" }, - { type: "docs", release: "patch" }, - { type: "fix", release: "patch" }, - { type: "perf", release: "patch" }, - # always fall back to a patch release - { - message: "*", - release: "patch", - }, - ], - }, - ], - "@semantic-release/release-notes-generator", - "@semantic-release/changelog", - ["@semantic-release/git", { - "message": "chore(\${branch.type}): \${nextRelease.version}\n\n\${nextRelease.notes}" - }], - ["@semantic-release/gitlab", { - "gitlabUrl": "https://git.panter.ch" - }] - ], - "branches": [ - "+([0-9])?(.{+([0-9]),x}).x", - "master", - "main", - "next", - "next-major", - { - "name": "beta", - "prerelease": true - }, - { - "name": "alpha", - "prerelease": true - } - ], - } - EOF - fi - semantic-release - } - - - function kubernetesEnsureNamespace() { - echo "Ensure Namespace $NAMESPACE" - kubectl describe namespace "$NAMESPACE" || kubectl create namespace "$NAMESPACE" - } - - function kubernetesCreateSecret() { - kubectl create secret -n "$NAMESPACE" \ - docker-registry $IMAGE_PULL_SECRET \ - --docker-server="$CI_REGISTRY" \ - --docker-username="${CI_DEPLOY_USER:-$CI_REGISTRY_USER}" \ - --docker-password="${CI_DEPLOY_PASSWORD:-$CI_REGISTRY_PASSWORD}" \ - --docker-email="$GITLAB_USER_EMAIL" \ - -o yaml --dry-run | kubectl replace -n "$NAMESPACE" --force -f - - } - - function kubernetesDeploy() { - - echo "Deploy to kubernetes" - RELEASE_NAME=${CUSTOMER_NAME}-${APP_NAME}-${CI_ENVIRONMENT_SLUG} - echo "Release: $RELEASE_NAME" - echo "URL (canonical): $HOST_CANONICAL" - helm init --client-only --stable-repo-url=https://charts.helm.sh/stable - helm repo add panter-charts http://panter.git.panter.biz/helm-charts - helm repo update - echo "panter chart repo added" - - # use helmArgs defined in variables - helmArgsEvaluated=$(echo $helmArgs | envsubst ) - - # this bash-brainfuck simply splits $helmArgs by newline to an array... - readarray -t helmArgsArray <<<"$helmArgsEvaluated" - - #Log the time until selfdestruct in case of review environment - if [[ ${ENV_SHORT} == "review" ]]; then echo "NOTE - This deployment will stop itself after 2 weeks"; fi - - # find all values files and generate an array out of it - files=(values.yml values-$ENV_SHORT.yml values-$CI_ENVIRONMENT_SLUG.yml) - values=() - for i in "${files[@]}"; do if [ -f $VALUES_PATH/$i ]; then values+=(--values $VALUES_PATH/$i); fi; done - echo "reading values files from $VALUES_PATH. found ${values[@]}" - - echo "doing helm upgrade" - helm upgrade --install "$RELEASE_NAME" $CHART_NAME \ - --wait \ - --timeout 600 \ - --cleanup-on-fail \ - --set global.componentName="$KUBE_APP_NAME" \ - --set global.ENV_SHORT="$ENV_SHORT" \ - --set image.repository="$CI_REGISTRY_IMAGE" \ - --set image.pullSecret="$IMAGE_PULL_SECRET" \ - --set image.tag="$IMAGE_TAG" \ - --set image.pullPolicy=IfNotPresent \ - --set storybook.enabled=$STORYBOOK \ - --set application.replicas="$REPLICAS" \ - --set application.hostCanonical="$HOST_CANONICAL" \ - --set application.worker.enabled=$WORKER_ENABLED \ - --set mongodb.enabled=$MONGODB_ENABLED \ - --set mongodb.backup.enabled=$MONGODB_BACKUP_ENABLED \ - --set mongodb-replicaset.replicas=$MONGODB_REPLICAS \ - --set mongodb-replicaset.fullnameOverride=$KUBE_APP_NAME-mongodb-replicaset \ - --set mongodb-replicaset.persistentVolume.storageClass="$MONGODB_STORAGE_CLASS" \ - --set prisma.enabled=$PRISMA_ENABLED \ - --set gitlab.visualReviewEnabled="$GITLAB_VISUAL_REVIEW_ENABLED" \ - --set gitlab.app="$CI_PROJECT_PATH_SLUG" \ - --set gitlab.projectPath="$CI_PROJECT_PATH" \ - --set gitlab.mergeRequestId="$CI_MERGE_REQUEST_IID" \ - --set gitlab.projectId="$CI_PROJECT_ID" \ - --set gitlab.env="$CI_ENVIRONMENT_SLUG" \ - --set namespace="$NAMESPACE" \ - --namespace="$NAMESPACE" \ - ${helmArgsArray[@]} \ - ${values[@]} - echo "Deployment successful 😻" - } - - function kubernetesDelete() { - RELEASE_NAME=${CUSTOMER_NAME}-${APP_NAME}-${CI_ENVIRONMENT_SLUG} - echo "Delete $RELEASE_NAME" - name="$RELEASE_NAME" - - if [[ -n "$(helm ls -q "^$name$")" ]]; then - helm delete --purge "$name" - - fi - } - -.ensureStorybookDockerfile: &ensureStorybookDockerfile | - function ensureStorybookDockerfile() { - # this is for meteor - if [ ! -f Dockerfile-storybook ]; then - echo "Creating Dockerfile-storybook" - cat > Dockerfile-storybook <<EOF - FROM nginx - EXPOSE 80 - COPY .storybook-out/ /usr/share/nginx/html - EOF - - fi - - } - -### jobs - -.openMr: - image: tmaier/gitlab-auto-merge-request:1 - before_script: [] # We do not need any setup work, let's remove the global one (if any) - variables: - GIT_STRATEGY: none # We do not need a clone of the GIT repository to create a Merge Request - stage: openMr - script: - - export GITLAB_PRIVATE_TOKEN=$GL_TOKEN - - merge-request.sh # The name of the script - only: - refs: - - branches - variables: - - $AUTOMATIC_MR == "true" - -.cache-node-modules: &cache-node-modules - key: "node-modules" - policy: pull-push - paths: - - "$APP_DIR/node_modules/" - - "$APP_DIR/.next/cache/" - -.docker-build-base: - image: docker:18-dind - retry: 2 - services: - - docker:18-dind - variables: - DOCKER_DRIVER: overlay2 - -.docker-build-app-base: - extends: .docker-build-base - script: - - docker login --username gitlab-ci-token --password $CI_JOB_TOKEN $CI_REGISTRY - - - docker pull $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG || true - - docker build --cache-from $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG --tag $CI_REGISTRY_IMAGE:$IMAGE_TAG $DOCKER_DIR - - docker push $CI_REGISTRY_IMAGE:$IMAGE_TAG - - docker tag $CI_REGISTRY_IMAGE:$IMAGE_TAG $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG - - docker push $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG -.docker-build-storybook-base: - extends: .docker-build-base - script: - - *ensureStorybookDockerfile - - ensureStorybookDockerfile - - docker login --username gitlab-ci-token --password $CI_JOB_TOKEN $CI_REGISTRY - - docker pull $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG-storybook || true - - docker build --cache-from $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG-storybook --tag $CI_REGISTRY_IMAGE:$IMAGE_TAG-storybook $DOCKER_DIR -f Dockerfile-storybook - - docker push $CI_REGISTRY_IMAGE:$IMAGE_TAG-storybook - - docker tag $CI_REGISTRY_IMAGE:$IMAGE_TAG-storybook $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG-storybook - - docker push $CI_REGISTRY_IMAGE:$CACHE_IMAGE_TAG-storybook - -.defaultEnv: - variables: - NAMESPACE: "${CUSTOMER_NAME}-${APP_NAME}-${ENV_SHORT}" - HOST_CANONICAL: $APP_NAME-$CI_ENVIRONMENT_SLUG.$CUSTOMER_NAME.panter.cloud - IMAGE_PULL_SECRET: gitlab-registry-$COMPONENT_NAME - environment: - url: https://$APP_NAME-$CI_ENVIRONMENT_SLUG.$CUSTOMER_NAME.panter.cloud - name: ${ENV_SHORT}-${COMPONENT_NAME} - -.semantic-release-base: - cache: - <<: *cache-node-modules - script: - - *auto_devops - - semanticRelease - -.deploy-to-kubernetes: - extends: .defaultEnv - retry: 2 - script: - - *auto_devops - - kubernetesEnsureNamespace - - kubernetesCreateSecret - - kubernetesDeploy - -.deploy-to-kubernetes-review: - extends: .deploy-to-kubernetes - variables: - ENV_SHORT: review - REPLICAS: 1 - MONGODB_REPLICAS: 1 - MONGODB_STORAGE_CLASS: "standard" - MONGODB_BACKUP_ENABLED: "false" - DEPLOY_SSL_ISSUER: letsencrypt-staging - KUBE_APP_NAME: ${COMPONENT_NAME}-${CI_COMMIT_REF_SLUG} - - environment: - name: ${ENV_SHORT}-${COMPONENT_NAME}/$CI_COMMIT_REF_NAME - on_stop: review-stop - auto_stop_in: 2 weeks - -.kubernetes-review-stop: - when: manual - variables: - KUBE_APP_NAME: ${COMPONENT_NAME}-${CI_COMMIT_REF_SLUG} - ENV_SHORT: review - GIT_STRATEGY: none - environment: - name: ${ENV_SHORT}-${COMPONENT_NAME}/$CI_COMMIT_REF_NAME - action: stop - script: - - *auto_devops - - kubernetesDelete - cache: {} - -.deploy-to-kubernetes-dev: - extends: .deploy-to-kubernetes - variables: - REPLICAS: 1 - MONGODB_REPLICAS: 1 - MONGODB_STORAGE_CLASS: "standard" - DEPLOY_SSL_ISSUER: letsencrypt-staging - ENV_SHORT: dev - -.deploy-to-kubernetes-stage: - extends: .deploy-to-kubernetes - - variables: - REPLICAS: 2 - DEPLOY_SSL_ISSUER: letsencrypt-staging - ENV_SHORT: stage - -.deploy-to-kubernetes-prod: - extends: .deploy-to-kubernetes - variables: - STORYBOOK: "false" # don't deploy storybook to prod, never - ENV_SHORT: prod - -# ---------------------- -# RESTRICTION INCLUDES -# ---------------------- -.only-default: &only-default - only: - refs: - - master - - main - - production - - tags - - merge_requests - - /^[0-9]+\.([0-9]+|x)\.x$/ # hotfix branches -.only-storybook: &only-storybook - only: - refs: - - master - - main - - production - - tags - - merge_requests - variables: - - $STORYBOOK == "true" - -.only-on-merge-request: &only-on-merge-request - only: - refs: - - merge_requests - - /^[0-9]+\.([0-9]+|x)\.x$/ # hotfix branches - -.not-after-semantic-release: ¬-after-semantic-release - except: - refs: - - tags - variables: - - $CI_COMMIT_MESSAGE =~ /^chore.*/ - -.only-on-master: &only-on-master - only: - refs: - - master - - main - -.not-on-master: ¬-on-master - except: - refs: - - master - - main - #---------------------- # DEFAULT STAGE TEMPLATES #---------------------- -.setup-build-info: - stage: setup - <<: *only-default - retry: 2 - script: - - cd $APP_PATH - - BUILD_TAG="$(git describe --tags || git rev-parse HEAD)" - - BUILD_ID="$COMPONENT_NAME-$BUILD_TAG" - - BUILD_COMMIT="$(git rev-parse HEAD)" - - BUILD_TIME="$(date)" - - echo "BUILD_TAG=$BUILD_TAG" >> build.env - - echo "BUILD_ID=$BUILD_ID" >> build.env - - echo "BUILD_COMMIT=$BUILD_COMMIT" >> build.env - - echo "BUILD_TIME=$BUILD_TIME" >> build.env - - echo '{"id":"'$BUILD_ID'","commit":"'$BUILD_COMMIT'","tag":"'$BUILD_TAG'","time":"'$BUILD_TIME'"}' > __build_info.json - artifacts: - paths: - - $CI_PROJECT_DIR/__build_info.json - reports: - dotenv: build.env - -.setup: - stage: setup - # this is now a noop and deprecated +.test-base: + stage: test + needs: [] + extends: + - .rules-always-but-not-on-tags .test: - stage: test - # do it also before semantic release, but not as a result of semantic release - <<: *only-default - <<: *not-after-semantic-release - cache: - <<: *cache-node-modules + extends: .test-base script: - - cd $APP_PATH - - if [ -f ./.nvmrc ]; then source /root/.nvm/nvm.sh && nvm install <<< .nvmrc; fi - - yarn install --frozen-lockfile - - yarn test - needs: [] -.lint: + - echo "not implemented" + +.lint-base: stage: test - # do it also before semantic release, but not as a result of semantic release - <<: *only-default - <<: *not-after-semantic-release - cache: - <<: *cache-node-modules + extends: + - .rules-always-but-not-on-tags + +.lint: + extends: .lint-base script: - - cd $APP_PATH - - if [ -f ./.nvmrc ]; then source /root/.nvm/nvm.sh && nvm install <<< .nvmrc; fi - - yarn install --frozen-lockfile - - yarn lint - needs: [] + - echo "not implemented" -.storybook-build: +.app-build-base: stage: build - <<: *only-storybook - cache: - <<: *cache-node-modules + retry: 2 + extends: + - .rules-always needs: ["setup-build-info"] - # don't do it after the semantic release commit, but do it after tag - script: - - cd $APP_PATH - - if [ -f ./.nvmrc ]; then source /root/.nvm/nvm.sh && nvm install <<< .nvmrc; fi - - yarn install --frozen-lockfile - - yarn build-storybook -c .storybook -s public -o .storybook-out --quiet - artifacts: - paths: - - $CI_PROJECT_DIR/.storybook-out .app-build: - stage: build - retry: 2 - <<: *only-default - cache: - <<: *cache-node-modules - # don't do it after the semantic release commit, but do it after tag + extends: .app-build-base script: - - echo $BUILD_ID - - cd $APP_PATH - - if [ -f ./.nvmrc ]; then source /root/.nvm/nvm.sh && nvm install <<< .nvmrc; fi - - yarn install --frozen-lockfile - - yarn build - needs: ["setup-build-info"] - artifacts: - paths: - - $CI_PROJECT_DIR/__build_info.json - - $CI_PROJECT_DIR/dist - - $CI_PROJECT_DIR/.next - -.docker-build: - stage: docker-build - <<: *only-default - extends: .docker-build-app-base - needs: ["app-build"] - -.docker-build-storybook: - stage: docker-build - <<: *only-storybook - extends: .docker-build-storybook-base - needs: ["storybook-build"] + - echo "not implemented" # ---------------------- # REVIEW APP @@ -510,107 +104,82 @@ image: panterch/docker-ci-kubernetes-deploy .review-deploy: stage: deploy - <<: *only-on-merge-request + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' retry: 2 extends: .deploy-to-kubernetes-review .review-stop: - tags: - stage: review-stop - when: manual - extends: .kubernetes-review-stop - <<: *only-on-merge-request - -# on hotfix branches, do automatic review-stop -.review-stop-automatic-on-hotfix: tags: stage: review-stop extends: .kubernetes-review-stop - only: - refs: - - /^[0-9]+\.([0-9]+|x)\.x$/ # hotfix branches - -.create-release-automatic: - stage: create-release - extends: .semantic-release-base - only: - refs: - - master - - main - variables: - - $AUTO_RELEASE == "true" - <<: *not-after-semantic-release - -.create-release: - stage: create-release - extends: .semantic-release-base - only: - refs: - - master - - main - - /^[0-9]+\.([0-9]+|x)\.x$/ # hotfix branches - <<: *not-after-semantic-release - when: manual + rules: + - if: $CI_COMMIT_BRANCH =~ /^[0-9]+\.([0-9]+|x)\.x$/ # automatic on hotfix branches + when: on_success + allow_failure: true + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + when: manual + allow_failure: true .dev-deploy: stage: deploy - <<: *only-on-master - <<: *not-after-semantic-release - retry: 2 - extends: .deploy-to-kubernetes-dev - -.dev-deploy-after-release: - stage: deploy - only: ["tags"] - # do not do the automatic release on hotfix branches - except: - variables: - - $CI_COMMIT_MESSAGE =~ /^chore\(maintenance\).*/ # do not do automatic release after a hotfix / maintenance release - extends: .deploy-to-kubernetes-dev - -.stage-deploy-automatic: - stage: deploy - only: - refs: - - tags - variables: - - $STAGING_ENABLED == "true" - # do not do the automatic release on hotfix branches - except: - variables: - - $CI_COMMIT_MESSAGE =~ /^chore\(maintenance\).*/ # do not do automatic release after a hotfix / maintenance release - - extends: .deploy-to-kubernetes-stage + extends: + - .rules-dev-deploy + - .deploy-to-kubernetes-dev .stage-deploy: stage: deploy - only: ["tags"] - when: manual - extends: .deploy-to-kubernetes-stage - -.prod-deploy-automatic: - stage: deploy - only: ["tags"] - except: - variables: - - $STAGING_ENABLED == "true" - - $CI_COMMIT_MESSAGE =~ /^chore\(maintenance\).*/ # do not do automatic release after a hotfix / maintenance release - - extends: .deploy-to-kubernetes-prod + extends: + - .rules-stage-deploy + - .deploy-to-kubernetes-stage .prod-deploy: stage: deploy - only: ["tags"] - when: manual - extends: .deploy-to-kubernetes-prod + extends: + - .rules-prod-deploy + - .deploy-to-kubernetes-prod .verify-base: - extends: .defaultEnv stage: verify - <<: *only-default - cache: - <<: *cache-node-modules - policy: pull + +.verify-review: + stage: verify + extends: + - .verify-base + - .env-review + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + needs: ["review-deploy"] + +.verify-dev: + stage: verify + extends: + - .verify-base + - .env-dev + needs: ["dev-deploy"] + rules: + - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\).*/ # not after semantic release commit + when: never + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + +.verify-stage: + stage: verify + extends: + - .verify-base + - .env-stage + needs: ["stage-deploy"] + rules: + - if: $STAGING_ENABLED == "true" && $CI_COMMIT_TAG + +.verify-prod: + stage: verify + extends: + - .verify-base + - .env-prod + needs: ["prod-deploy"] + rules: + - if: $CI_COMMIT_TAG + #---------------------- # DEFAULT STAGE DEFINITIONS #---------------------- @@ -660,29 +229,14 @@ review-deploy: review-stop: extends: .review-stop -review-stop-automatic-on-hotfix: - extends: .review-stop-automatic-on-hotfix - dev-deploy: extends: .dev-deploy -dev-deploy-after-release: - extends: .dev-deploy-after-release - create-release: extends: .create-release -create-release-automatic: - extends: .create-release-automatic - stage-deploy: extends: .stage-deploy -stage-deploy-automatic: - extends: .stage-deploy-automatic - prod-deploy: extends: .prod-deploy - -prod-deploy-automatic: - extends: .prod-deploy-automatic