ืืจืึธืฅ ืืขืจ ืคืึทืงื ืึทื ืึทืืขืืขื ืืืืืกื ืืืฉืืืืืขืก ืึทื ืืขืกืืื ื ืืืื ืืืืืืืืืจื ืืื ืืืืืืืง ืืื ื ืืืืืง, ืืื ืคืืืข ืืึธืื ืฉืืื ืืื ืขืก ืืืืืึธืืึทืืืฉ ืคึฟืึทืจ ืึท ืืึทื ื ืฆืืึทื, ืืื ืื ืืืึทืกืื ืึทืก ืคืื Habr ืขืก ืืื ื ืืฉื ืึท ืืืื ืจืขืฆืขืคึผื ืคึฟืึทืจ ืืึทืฉืืขืืืงื ืึท ืงืึธืืืื ืึทืฆืืข ืคืื โโโโืึทืืึท ืคืึธืืงืก ืคึผืจืึธืืืงืื ืืื ืืขื ื ืืฉืข ืืื (ืืื ืืืขืจ ืืึทืืืืกืืข) GitLab ืืื JUnit. ืืึธืืืจ ืึธื ืคืืื ืืขื ืจืืก!
ืื ืืจืึทืืึทืงืืขืจื
ืขืจืฉืืขืจ, ืืึธืื ืืืจ ืืขืื ืขืืืขืืข ืงืึธื ืืขืงืกื:
- ืืื ื ืึทืืข ืืื ืืืขืจ ืึทืคึผืืึทืงืืืฉืึทื ื ืืืืคื ืืืืฃ Kubernetes, ืืืจ ืืืขืื ืืึทืืจืึทืืื ืคืืืกื ืืืง ืืขืกืฅ ืืืืฃ ืื ืฆืื ืขืืขื ืื ืคืจืึทืกืืจืึทืงืืฉืขืจ.
- ืคึฟืึทืจ ืคึฟืึทืจืืึทืืืื ื ืืื ืืืคึผืืืืืึทื ื ืืืจ ื ืืฆื
werf (ืืื ืืขืจืืื ืขื ืคืื ืื ืคืจืึทืกืืจืึทืงืืฉืขืจ ืงืึทืืคึผืึธืื ืึทื ืฅ, ืืึธืก ืืืื ืืืืืึธืืึทืืืฉ ืืืื ืึทื ืืขืื ืืื ืื ืืืึทืืืื). - ืืื ืืืขื ื ืืฉื ืืืื ืืื ืื ืืขืืึทืืืก ืคืื ืื ืคืึทืงืืืฉ ืฉืึทืคืื ื ืคืื ืืขืกืฅ: ืืื ืืื ืืืขืจ ืคืึทื, ืืขืจ ืงืืืขื ื ืฉืจืืืื ืื ืืขืกืฅ ืืื, ืืื ืืืจ ื ืึธืจ ืขื ืฉืืจ ืืืืขืจ ืงืึทืืขืจ (ืืื ืื ืืืึทืืืึทื ืคืื ืึท ืงืึธืจืึทืกืคึผืึทื ืืื ื ืืึทืจืืื ืืื ืื ืฆืื ืืืคืืืกื ืืขืื).
ืืื ืืืขื ืืขืจ ืืขื ืขืจืึทื ืกืืงืืืึทื ืก ืคืื ืึทืงืฉืึทื ื ืงืืงื ืืื?
- ืืืืขื ืื ืึทืคึผืืึทืงืืืฉืึทื - ืืืจ ืืืขืื ืคืึทืจืืึธืื ืื ืืึทืฉืจืืึทืืื ื ืคืื ืืขื ืืื ืข.
- ืฆืขืืืืงืืขื ืื ืึทืคึผืืึทืงืืืฉืึทื ืฆื ืึท ืืึทืืื ืืขืจ ื ืึธืืขื ืคืื ืื Kubernetes ืงื ืืื ืืื ืึธื ืืืืื ืืขืกืืื ื.
- ืืืื ืคึฟืึทืจ ืึทืจืืึทืคืึทืงืฅ ืืื ืคึผืึทืจืกืื ื JUnit ืจืืคึผืึธืจืฅ ืืื GitLab.
- ืืืืืืื ื ืึท ืคืจืืขืจ ืืืฉืืคื ื ืึทืืขืกืคึผืึทืกืข.
ืืืฆื - ืฆื ืืืคึผืืึทืืขื ืืืืฉืึทื!
Customize
ืืืืืึทื ืกื
ืืึธืืืจ ืึธื ืืืืื ืืื ืึท ืคืจืึทืืืขื ื .gitlab-ci.yaml
, ืืืึธืก ืืืฉืจืืืื ืืืคึผืืืืื ื ืื ืึทืคึผืืึทืงืืืฉืึทื ืืื ืคืืืกื ืืืง ืืขืกืฅ. ืื ืืืกืืื ื ืืื ืืขืืืขื ืืึทื ืฅ ืืืึทืืืืึทื ืึทืก, ืึทืืื ืขืก ืืื ืืขืืืขื ืืึธืจ ืกืึทืคึผืืึทืืขื ืืึทื ืืื ืืึทืืขืจืงืื ืืขื:
variables:
# ะพะฑััะฒะปัะตะผ ะฒะตััะธั werf, ะบะพัะพััั ัะพะฑะธัะฐะตะผัั ะธัะฟะพะปัะทะพะฒะฐัั
WERF_VERSION: "1.0 beta"
.base_deploy: &base_deploy
script:
# ัะพะทะดะฐะตะผ namespace ะฒ K8s, ะตัะปะธ ะตะณะพ ะฝะตั
- kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# ะทะฐะณััะถะฐะตะผ werf ะธ ะดะตะฟะปะพะธะผ โ ะฟะพะดัะพะฑะฝะตะต ะพะฑ ััะพะผ ัะผ. ะฒ ะดะพะบัะผะตะฝัะฐัะธะธ
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#deploy-stage)
- type multiwerf && source <(multiwerf use ${WERF_VERSION})
- werf version
- type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
- werf deploy --stages-storage :local
--namespace ${CI_ENVIRONMENT_SLUG}
--set "global.commit_ref_slug=${CI_COMMIT_REF_SLUG:-''}"
# ะฟะตัะตะดะฐะตะผ ะฟะตัะตะผะตะฝะฝัั `run_tests`
# ะพะฝะฐ ะฑัะดะตั ะธัะฟะพะปัะทะพะฒะฐัััั ะฒ ัะตะฝะดะตัะต Helm-ัะตะปะธะทะฐ
--set "global.run_tests=${RUN_TESTS:-no}"
--set "global.env=${CI_ENVIRONMENT_SLUG}"
# ะธะทะผะตะฝัะตะผ timeout (ะฑัะฒะฐัั ะดะพะปะณะธะต ัะตััั) ะธ ะฟะตัะตะดะฐะตะผ ะตะณะพ ะฒ ัะตะปะธะท
--set "global.ci_timeout=${CI_TIMEOUT:-900}"
--timeout ${CI_TIMEOUT:-900}
dependencies:
- Build
.test-base: &test-base
extends: .base_deploy
before_script:
# ัะพะทะดะฐะตะผ ะดะธัะตะบัะพัะธั ะดะปั ะฑัะดััะตะณะพ ะพััะตัะฐ, ะธัั
ะพะดั ะธะท $CI_COMMIT_REF_SLUG
- mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# ะฒัะฝัะถะดะตะฝะฝัะน ะบะพัััะปั, ั.ะบ. GitLab ั
ะพัะตั ะฟะพะปััะธัั ะฐััะตัะฐะบัั ะฒ ัะฒะพะตะผ build-dirโะต
- mkdir ./tests || true
- ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
after_script:
# ะฟะพัะปะต ะพะบะพะฝัะฐะฝะธั ัะตััะพะฒ ัะดะฐะปัะตะผ ัะตะปะธะท ะฒะผะตััะต ั Jobโะพะผ
# (ะธ, ะฒะพะทะผะพะถะฝะพ, ะตะณะพ ะธะฝััะฐััััะบัััะพะน)
- type multiwerf && source <(multiwerf use ${WERF_VERSION})
- werf version
- type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
- werf dismiss --namespace ${CI_ENVIRONMENT_SLUG} --with-namespace
# ะผั ัะฐะทัะตัะฐะตะผ ะฟะฐะดะตะฝะธั, ะฝะพ ะฒั ะผะพะถะตัะต ัะดะตะปะฐัั ะธะฝะฐัะต
allow_failure: true
variables:
RUN_TESTS: 'yes'
# ะทะฐะดะฐะตะผ ะบะพะฝัะตะบัั ะฒ werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
WERF_KUBE_CONTEXT: 'admin@stage-cluster'
tags:
# ะธัะฟะพะปัะทัะตะผ ัะฐะฝะฝะตั ั ัะตะณะพะผ `werf-runner`
- werf-runner
artifacts:
# ััะตะฑัะตััั ัะพะฑัะฐัั ะฐััะตัะฐะบั ะดะปั ัะพะณะพ, ััะพะฑั ะตะณะพ ะผะพะถะฝะพ ะฑัะปะพ ัะฒะธะดะตัั
# ะฒ ะฟะฐะนะฟะปะฐะนะฝะต ะธ ัะบะฐัะฐัั โ ะฝะฐะฟัะธะผะตั, ะดะปั ะฑะพะปะตะต ะฒะดัะผัะธะฒะพะณะพ ะธะทััะตะฝะธั
paths:
- ./tests/${CI_COMMIT_REF_SLUG}/*
# ะฐััะตัะฐะบัั ััะฐััะต ะฝะตะดะตะปะธ ะฑัะดัั ัะดะฐะปะตะฝั
expire_in: 7 day
# ะฒะฐะถะฝะพ: ััะธ ัััะพะบะธ ะพัะฒะตัะฐัั ะทะฐ ะฟะฐััะธะฝะณ ะพััะตัะฐ GitLabโะพะผ
reports:
junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml
# ะดะปั ัะฟัะพัะตะฝะธั ะทะดะตัั ะฟะพะบะฐะทะฐะฝั ะฒัะตะณะพ ะดะฒะต ััะฐะดะธะธ
# ะฒ ัะตะฐะปัะฝะพััะธ ะถะต ั ะฒะฐั ะธั
ะฑัะดะตั ะฑะพะปััะต โ ะบะฐะบ ะผะธะฝะธะผัะผ ะธะท-ะทะฐ ะดะตะฟะปะพั
stages:
- build
- tests
build:
stage: build
script:
# ัะฑะพัะบะฐ โ ัะฝะพะฒะฐ ะฟะพ ะดะพะบัะผะตะฝัะฐัะธะธ ะฟะพ werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#build-stage)
- type multiwerf && source <(multiwerf use ${WERF_VERSION})
- werf version
- type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
- werf build-and-publish --stages-storage :local
tags:
- werf-runner
except:
- schedules
run tests:
<<: *test-base
environment:
# "ัะฐะผะฐ ัะพะปั" ะธะผะตะฝะพะฒะฐะฝะธั namespaceโะฐ
# (https://docs.gitlab.com/ce/ci/variables/predefined_variables.html)
name: tests-${CI_COMMIT_REF_SLUG}
stage: tests
except:
- schedules
Kubernetes
ืืืฆื ืืื ืืขืจ ืืืขืืืืืึทืืขืจ .helm/templates
ืืึธืืืจ ืืึทืื YAML ืืื ืืืฉืึธื - tests-job.yaml
- ืฆื ืืืืคื ืืขืกืฅ ืืื ืื Kubernetes ืจืขืกืืจืกื ืขืก ืืึทืจืฃ. ืืขื ืืขืจืงืืขืจืื ืืขื ื ืึธื ืืืกืืื ื:
{{- if eq .Values.global.run_tests "yes" }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: tests-script
data:
tests.sh: |
echo "======================"
echo "${APP_NAME} TESTS"
echo "======================"
cd /app
npm run test:ci
cp report.xml /app/test_results/${CI_COMMIT_REF_SLUG}/
echo ""
echo ""
echo ""
chown -R 999:999 /app/test_results/${CI_COMMIT_REF_SLUG}
---
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Chart.Name }}-test
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "2"
"werf/watch-logs": "true"
spec:
activeDeadlineSeconds: {{ .Values.global.ci_timeout }}
backoffLimit: 1
template:
metadata:
name: {{ .Chart.Name }}-test
spec:
containers:
- name: test
command: ['bash', '-c', '/app/tests.sh']
{{ tuple "application" . | include "werf_container_image" | indent 8 }}
env:
- name: env
value: {{ .Values.global.env }}
- name: CI_COMMIT_REF_SLUG
value: {{ .Values.global.commit_ref_slug }}
- name: APP_NAME
value: {{ .Chart.Name }}
{{ tuple "application" . | include "werf_container_env" | indent 8 }}
volumeMounts:
- mountPath: /app/test_results/
name: data
- mountPath: /app/tests.sh
name: tests-script
subPath: tests.sh
tolerations:
- key: dedicated
operator: Exists
- key: node-role.kubernetes.io/master
operator: Exists
restartPolicy: OnFailure
volumes:
- name: data
persistentVolumeClaim:
claimName: {{ .Chart.Name }}-pvc
- name: tests-script
configMap:
name: tests-script
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .Chart.Name }}-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Mi
storageClassName: {{ .Chart.Name }}-{{ .Values.global.commit_ref_slug }}
volumeName: {{ .Values.global.commit_ref_slug }}
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: {{ .Values.global.commit_ref_slug }}
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 10Mi
local:
path: /mnt/tests/
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- kube-master
persistentVolumeReclaimPolicy: Delete
storageClassName: {{ .Chart.Name }}-{{ .Values.global.commit_ref_slug }}
{{- end }}
ืืืึธืก ืืื ืคืื ืจืขืกืืจืกื ืืืกืงืจืืืื ืืื ืืขื ืงืึทื ืคืืืืขืจืืืฉืึทื? ืืืขื ืืืคึผืืืืื ื, ืืืจ ืืึทืื ืึท ืืื ืฆืืง ื ืึทืืขืกืคึผืึทืกืข ืคึฟืึทืจ ืื ืคึผืจืืืขืงื (ืืึธืก ืืื ืื ืืขืืืืื ืืื .gitlab-ci.yaml
- tests-${CI_COMMIT_REF_SLUG}
) ืืื ืจืึธืืื ืขืก ืืืืก:
- ืงืึธื ืคืืืืึทืคึผ ืืื ืคึผืจืืืืจื ืฉืจืืคื;
- ืึทืจืืขื ืืื ืึท ืืึทืฉืจืืึทืืื ื ืคืื ืื ืคึผืึธื ืืื ืื ืกืคึผืขืกืืคืืขื ืืืจืขืงืืืื
command
, ืืืึธืก ื ืึธืจ ืืืืคื ืื ืืขืกืฅ; - ืคึผืืืง ืืื ืคึผืืืง, ืืืึธืก ืืึธืื ืืืจ ืฆื ืงืจืึธื ืคึผืจืืืืจื ืืึทืื.
ืืึทืฆืึธืื ืืคืืขืจืงืืึทืืงืืึทื ืฆื ืื ืื ืืจืึทืืึทืงืืขืจื ืฆืืฉืืึทื ื ืืื if
ืืื ืื ืึธื ืืืื ืคืื ืื ืืึทืฉืืึทืืคึผืขืจืืขื - ืึทืงืึธืจืืื ืืื, ืื ืืขืจืข YAML ืืขืงืขืก ืคืื ืื ืืขืื ืืฉืึทืจื ืืื ืื ืึทืคึผืืึทืงืืืฉืึทื ืืืื ืืืื ืืื ืืขืืืืงืื ืืื ืคืึทืจืงืขืจื ืคึผืืึทื ืึทืืื ืึทื ืืื ืืึธื ื ืื ืืึทืงืืืขื ืืืคึผืืืื ืืขืฉืึทืก ืืขืกืืื ื. ืืืก ืืื:
{{- if ne .Values.global.run_tests "yes" }}
---
ั ะดััะณะพะน ัะผะปะธะบ
{{- end }}
ืึธืืขืจ, ืืืื ืื ืืขืกืฅ ืืึทืจืคื ืขืืืขืืข ืื ืคืจืึทืกืืจืึทืงืืฉืขืจ (ืืืฉื, Redis, RabbitMQ, Mongo, PostgreSQL ...) - ืืืืขืจ ืืึทืืืก ืงืขื ืขื ืืืื ืงืืื ืืขืฉ ืืืืก. ืฆืขืืืืงืืขื ืืื ืืืื ืืื ืึท ืคึผืจืึธืืข ืกืืืืืืข ... ืึทืืืฉืึทืกืืื ื ืืื ืืื ืืืจ ืืขื ืคึผืึทืกืืง, ืคืื ืืืืฃ.
ืืขืฆื ืคืึทืจืืื ืื
ืืืืึทื ืคึฟืึทืจืืึทืืืื ื ืืื ืืืคึผืืืืืึทื ื ื ืืฆื werf ืืื ื ืึธื ืืจืืขืื ืืืืื ืืืืฃ ืื ืืืืขื ืกืขืจืืืขืจ (ืืื ืืืืืึทื-ืจืึทื ืขืจ), ืืื ืื ืคึผืึธื ืืื ืืขืกืฅ ืืื ืืึธื ืืฉื ืืืืฃ ืื ืืขื, ืืืจ ืืึทืจืคึฟื ืฆื ืฉืึทืคึฟื ืึท ืืืขืืืืืึทืืขืจ /mnt/tests
ืืืืฃ ืืขื ืืขื ืืื ืืขืื ืขืก ืฆื ืื ืืืืคืขืจ, ืคึฟืึทืจ ืืืึทืฉืคึผืื, ืืืจื NFS. ื ืืืืืืื ืืืึทืฉืคึผืื ืืื ืืขืจืงืืขืจืื ืืขื ืงืขื ืขื ืืืื ืืขืคึฟืื ืขื ืืื
ืืขืจ ืจืขืืืืืึทื ืืืขื ืืืื:
user@kube-master:~$ cat /etc/exports | grep tests
/mnt/tests IP_gitlab-builder/32(rw,nohide,insecure,no_subtree_check,sync,all_squash,anonuid=999,anongid=998)
user@gitlab-runner:~$ cat /etc/fstab | grep tests
IP_kube-master:/mnt/tests /mnt/tests nfs4 _netdev,auto 0 0
ืงืืื ืขืจ ืคืึทืจืืืขืจื ืืึทืื ืึท NFS ืืึทื ืืืืื ืืืืึทื ืืืืฃ ืืืืืึทื-ืจืึทื ืขืจ, ืืื ืืึทื ืืึทืื ืืื ื ืขืก ืืื ืคึผืึธืืก.
ืืึธื
ืืืจ ืงืขื ืืืื ืึทืกืงืื ื ืืืึธืก ืงืึธืืคึผืืืฆืืจื ืึทืืฅ ืืืจื ืฉืึทืคึฟื ืึท ืึทืจืืขื ืืืื ืืืจ ืงืขื ืขื ืคืฉืื ืืืืคื ืึท ืฉืจืืคื ืืื ืืขืกืฅ ืืืืึทื ืืืืฃ ืื ืฉืึธื ืจืึทื ืขืจ? ืืขืจ ืขื ืืคืขืจ ืืื ืืึทื ืฅ ื ืืฉืืืง ...
ืขืืืขืืข ืืขืกืฅ ืืึทืจืคื ืึทืงืกืขืก ืฆื ืื ืื ืคืจืึทืกืืจืึทืงืืฉืขืจ (MongoDB, RabbitMQ, PostgreSQL, ืืื"ื ื) ืฆื ืืึทืฉืืขืืืงื ืึทื ืืื ืึทืจืืขื ืจืืืืืง. ืืืจ ืืึทืื ืืขืกืืื ื ืืื ืึทืคืืื - ืืื ืืขื ืฆืืืึทื ื, ืขืก ืืื ืืจืื ื ืฆื ืึทืจืืึทื ื ืขืืขื ืึทืืึท ื ืึธื ืขื ืืืืื. ืืื ืืขืจืฆื ืฆื ืืขื, ืืืจ ืืึทืงืืืขื ื ืึธืจืืึทื ืืืคึผืืืืืึทื ื ืฆืืืึทื ื (ืืคืืื ืืืื ื ืืฆื NFS, ื ืึธื ืืึทืื ืืื ื ืคืื ืืืจืขืงืืขืจืื).
ืืึทืืืขืก
ืืืึธืก ืืืขื ืืืจ ืืขื ืืืขื ืืืจ ืฆืืืืืื ืื ืฆืืืขืืจืืื ืงืึทื ืคืืืืขืจืืืฉืึทื?
ืื ืฆืื ืืืคืืืกื ืืขืื ืืืขื ืืืืึทืื ืงืืฆืขืจ ืกืืึทืืืกืืืง ืคึฟืึทืจ ืืขืกืฅ ืืืืคื ืืื ืืืื ืืขืฆืืข ืจืขืจื - ืืื ืืข:
ืืขืืขืจ ืืขืืช ืงืขื ืขื ืืืื ืงืืืงื ืืึธ ืคึฟืึทืจ ืคืจืืื:
NB: ะะฝะธะผะฐัะตะปัะฝัะน ัะธัะฐัะตะปั ะทะฐะผะตัะธั, ััะพ ะผั ัะตััะธััะตะผ NodeJS-ะฟัะธะปะพะถะตะฝะธะต, ะฐ ะฝะฐ ัะบัะธะฝัะพัะฐั
โ .NETโฆ ะะต ัะดะธะฒะปัะนัะตัั: ะฟัะพััะพ ะฒ ัะฐะผะบะฐั
ะฟะพะดะณะพัะพะฒะบะธ ััะฐััะธ ะฝะต ะฝะฐัะปะพัั ะพัะธะฑะพะบ ะฒ ัะตััะธัะพะฒะฐะฝะธะธ ะฟะตัะฒะพะณะพ ะฟัะธะปะพะถะตะฝะธั, ะทะฐัะพ ะฝะฐัะปะธ ะธั
ะฒ ะดััะณะพะผ.
ืกืึธืฃ
ืืื ืืืจ ืงืขื ืขื ืืขื, ืืึธืจื ืืฉื ืงืึธืืคึผืืืฆืืจื!
ืืื ืคึผืจืื ืฆืืคึผ, ืืืื ืืืจ ืฉืืื ืืึธืื ืึท ืฉืึธื ืงืึทืืขืงืืขืจ ืืื ืขืก ืึทืจืืขื, ืึธืืขืจ ืืืจ ืืึธื ื ืื ืืึทืจืคึฟื ืงืืืขืจื ืขืืขืก, ืึทืืึทืืฉืื ื ืืขืกืืื ื ืฆื ืขืก ืืืขื ืืืื ืึทื ืืคืืื ืกืืืคึผืืขืจ ืึทืจืืขื ืืื ืืืกืงืจืืืื ืืึธ. ืืื ืืื
ืคึผืก
ืืืืขื ืขื ืืืื ืืืืฃ ืืื ืืืขืจ ืืืึธื:
- ยซ
ืืขืกืืขืจ ืกื / ืกื ืคึผืจืึทืงืืืกืื ืืื Kubernetes ืืื GitLab (ืจืขืฆืขื ืืืข ืืื ืืืืืขื ืืึทืจืืื) "; - ยซ
ืขืฆืืช ืคึฟืึทืจ ืงืจืืืืืื ื ืื ืื ืืืึธืจืงืคืืึธืื ืืื GitLab CI "; - ยซ
GitLab CI ืคึฟืึทืจ ืงืขืกืืืืขืจืืืง ืื ืึทืืจืืืฉืึทื ืืื ืขืงืกืคึผืจืขืก ืืื ืคึผืจืึธืืืงืฆืืข '.
ืืงืืจ: www.habr.com