Привіт, я Kernelka! Сьогодні покажу, як перетворити ваші bash скрипти на дорослий проєкт із CI/CD: лінтинг через ShellCheck, тести на Bats, збірка контейнера та автоматичний деплой через Docker і GitLab CI. Це не просто модно — це економія часу та справжня автоматизація задач 🚀

План гри

  • Напишемо мінімальний bash-скрипт
  • Додамо тести на Bats
  • Зберемо Docker-образ
  • Поставимо ShellCheck у CI
  • Автодеплой на сервер по SSH з Docker

Все працюватиме в GitLab CI, а отже підходить для серйозної розробка на Linux та командної роботи.

Підготовка

  • Обліковий запис у GitLab та репозиторій з вашим кодом.
  • Налаштований GitLab Runner з Docker executor (або GitLab SaaS Shared Runners).
  • Сервер із встановленим Docker (це й буде ціль для деплою). Тут саме час згадати, наскільки зручний Docker на Linux 🐳
  • SSH-доступ до сервера (користувач, ключ).

Крок за кроком: How-to

1. Пишемо простий Bash-скрипт

Створіть файл greet.sh і зробіть його виконуваним. Також відразу додамо безпечні опції shell:

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

name="${1:-world}"
echo "Hello, ${name}!"
chmod +x greet.sh

Цей підхід дисциплінує скрипт і спрощує лінтинг.

2. Додаємо тести Bats

Bats — простий фреймворк для тестування bash скрипти. Створіть tests/greet.bats:

#!/usr/bin/env bats

@test "prints default greeting" {
  run ./greet.sh
  [ "$status" -eq 0 ]
  [ "$output" = "Hello, world!" ]
}

@test "greets by name" {
  run ./greet.sh Kernelka
  [ "$status" -eq 0 ]
  [ "$output" = "Hello, Kernelka!" ]
}

3. Dockerfile для запуску

Зберемо міні-образ, що запускає наш скрипт:

# Dockerfile
FROM bash:5.2-alpine
WORKDIR /app
COPY greet.sh .
RUN chmod +x greet.sh
ENTRYPOINT ["./greet.sh"]

4. GitLab CI: лінт, тести, білд

Тепер опишемо конвеєр у .gitlab-ci.yml. Ми розіб’ємо його на етапи: lint (ShellCheck), test (Bats), build (Docker), deploy.

# .gitlab-ci.yml
stages: [lint, test, build, deploy]

variables:
  DOCKER_TLS_CERTDIR: "/certs"
  DOCKER_HOST: tcp://docker:2376
  DOCKER_DRIVER: overlay2
  DOCKER_BUILDKIT: "1"
  IMAGE_NAME: "$CI_REGISTRY_IMAGE/app"
  IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"

lint:shellcheck:
  image: koalaman/shellcheck:stable
  stage: lint
  script:
    - shellcheck -x greet.sh

test:bats:
  image: bats/bats:latest
  stage: test
  script:
    - bats --version
    - chmod +x greet.sh
    - bats -r .

build:image:
  image: docker:24.0
  stage: build
  services:
    - name: docker:24.0-dind
      alias: docker
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
  script:
    - docker build -t "$IMAGE_NAME:$IMAGE_TAG" -t "$IMAGE_NAME:latest" .
    - docker push "$IMAGE_NAME:$IMAGE_TAG"
    - docker push "$IMAGE_NAME:latest"
  rules:
    - if: $CI_COMMIT_BRANCH

Пояснення:

  • koalaman/shellcheck:stable — офіційний образ для лінтингу.
  • bats/bats:latest — готовий до запуску Bats.
  • docker:dind як сервіс дає можливість збирати та пушити образи в реєстр GitLab.

5. Автоматичний деплой через SSH

На сервері має бути встановлений Docker. Налаштуйте секрети в GitLab: SSH_PRIVATE_KEY, SSH_KNOWN_HOSTS (вміст з ssh-keyscan <host>), DEPLOY_USER, DEPLOY_HOST. Потім додайте джоб:

deploy:prod:
  image: alpine:3.19
  stage: deploy
  needs: ["build:image"]
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
    - chmod 600 ~/.ssh/known_hosts
  script:
    - ssh "$DEPLOY_USER@$DEPLOY_HOST" "docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD && \
      docker pull $IMAGE_NAME:latest && \
      docker stop app || true && docker rm app || true && \
      docker run -d --name app -p 8080:8080 $IMAGE_NAME:latest"
  environment:
    name: production
    url: http://$DEPLOY_HOST:8080
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

Тут ми заходимо на сервер по SSH та оновлюємо контейнер до свіжого образу. Мінімум магії — максимум надійності.

Альтернативні способи

  • Замість прямого docker run використовуйте docker compose на сервері: зручно для кількох сервісів.
  • Додайте shfmt для автоформатування шела (через окремий джоб у lint).
  • Зробіть pre-commit хуки (локально) з shellcheck і shfmt, щоб ловити помилки до пушу.
  • Для великих команд — артефакти з junit-репортами Bats (через bats --tap + конвертори) і моніторинг статусу в GitLab.

GUI-спосіб у GitLab

  1. У репозиторії відкрийте CI/CD → Editor, оберіть Template: Docker + додайте свої джоби lint/test.
  2. У Settings → CI/CD → Variables додайте секрети: SSH_PRIVATE_KEY, SSH_KNOWN_HOSTS, DEPLOY_USER, DEPLOY_HOST. Позначте як Protected/Masked.
  3. В CI/CD → Pipelines слідкуйте за етапами: lint, test, build, deploy. Клацніть на джоб, щоб подивитися логи.

FAQ

ShellCheck ламається на моєму синтаксисі Bash

Додайте # shellcheck shell=bash зверху файлу або запустіть із ключем -s bash. Якщо це відомий виняток — точково ігноруйте правило через # shellcheck disable=SCXXXX, але з поясненням у коментарі.

Bats не знаходить файл скрипта

Переконайтеся, що тести виконуються з кореня репозиторію, а файл має права на виконання: chmod +x greet.sh. У тестах запускайте ./greet.sh, а не просто greet.sh.

Docker build падає на GitLab Runner

Перевірте, що сервіс docker:dind доданий, а змінні DOCKER_HOST та DOCKER_TLS_CERTDIR встановлені. Якщо нестача диска — приберіть кеш, очистіть неактуальні образи на Runner.

Помилка SSH: host key verification failed

Додайте відбиток хоста в змінну SSH_KNOWN_HOSTS через ssh-keyscan. Або один раз вручну залогіньтеся з локальної машини, перевіривши ключ сервера, і перенесіть його в CI змінну.

Деплой успішний, але застосунок недоступний

Перевірте, що ви публікуєте правильний порт (-p 8080:8080), що застосунок реально слухає цей порт у контейнері, і що firewall на сервері пропускає трафік.

Як проганяти локально, без CI?

У термінал Linux виконайте: shellcheck greet.sh, bats -r . та docker build .. Це корисно для швидкого циклу розробки.

Порада від Kernelka

Додавайте set -euo pipefail у всі продакшн-скрипти та тримайте їх короткими й ідемпотентними. Якщо потрібно багато логіки — розбейте на функції та покрийте критичні гілки тестами Bats. Для чутливих секретів у деплої використовуйте лише захищені змінні GitLab. Маленькі кроки + стабільний CI = надійність щодня ❤️

Підсумок

  • Ви маєте робочий конвеєр GitLab CI з етапами lint, test, build, deploy.
  • ShellCheck ловить типові помилки шела ще до збирання.
  • Bats гарантує, що поведінка скриптів стабільна.
  • Docker-образи публікуються до реєстру, а сервер автоматично тягне останню версію.
  • Це надійна база для подальшої масштабної автоматизації задач і професійної розробка на Linux.