Перейти к основному содержимому

Каким должен быть task runner для сборки проектов

· 2 мин. чтения
Дмитрий Чудный
Автор сайта

Соображения по поводу того, каким хотелось бы видеть инструмент для запуска многошагового CI/CD процесса проектов

  • Процесс сборки разделён на атомарные логические единицы - шаги сборки
  • Чтобы описать весь процесс прописываем какие шаги сборки нам нужны в нём, корневой шаг сборки
  • Шаги сборки могут иметь другие вложенные шаги сборки, получается дерево шагов сборки
  • Шаги сборки могут зависеть от результатов работы предыдущих шагов сборки, исходящие результаты одних шагов передаются в параметры других шагов
  • Инструмент сам разрешает порядок выполнения шагов по их зависимости друг от друга
  • Шаги сборки могут выполняться как последовательно так и параллельно, это решает инструмент
  • Интерфейс шага сборки имеет строгое декларативное описание:
    • Структура и типы входящих параметров
    • Структура и типы исходящих результатов
  • Инструмент выполняет валидацию параметров и результатов шагов сборки, не пропуская некорректные данные
  • Исходящие данные одного шага сборки можно передать во входящие параметры другого шага
  • Исходящие данные шага сборки можно получить в JSON или другом формате
  • Описание сборки выполняется на языке программирования, что позволяет
    • Генерировать шаги сборки с разными параметрами (матрица, комбинации параметров)
    • Читать файлы
    • Задавать сложные условия и выражения для вычисления параметров
    • В разных шагах сборки можно использовать объекты описанные где-то в одном месте, не надо ничего копипастить
    • Из библиотек доступно множество функций для удобной реализации логики сборки
  • Есть удобный встроенный шаблонизатор для генерации файлов скриптов и конфигов
  • Перед запуском сборки можно посмотреть сгенерированную конфигурацию
  • Возможность описать шаги и процесс сборки по-умолчанию и применять его во многих проектах
  • Предусмотрено версионирование шагов и процесса сборки, в проектах можно переходить на новую версию процесса сборки по мере необходимости и возможности
  • В проекте можно переопределить логику выполнения любого шага сборки, при этом можно вызвать логику шага по-умолчанию
  • В описании шага сборки можно указать docker контейнер, в котором его необходимо запустить (для CI/DI окружения)
  • При запуске сборки настройкой можно отключить использование docker контейнеров и все шаги запускать как есть на машине (для локальной разработки)

Bash скрипты

· 5 мин. чтения
Дмитрий Чудный
Автор сайта

Используем плагины и инструменты

🔥 Обязательные требования

Указываем командную оболочку для выполнения скриптов: Bash

#!/usr/bin/env bash

Разные оболочки по-разному выполняют команды.

Режим прекращения выполнения в случае ошибки любой команды

set -eo pipefail

Используем $SELF_DIR для относительных путей

SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
RELATIVE_PATH=${RELATIVE_PATH:-"$SELF_DIR/relative/path"}

Задаём переменным значения по-умолчанию

CI_BUILD_NUMBER=${CI_BUILD_NUMBER:-0}

Даём возможность задавать значения параметров снаружи

# ❌ Плохо: нельзя передать другое значение снаружи
XCODE_APP_NAME="application"
# ✅ Хорошо: можно задать любое значение переменной снаружи
XCODE_APP_NAME=${XCODE_APP_NAME:-"application"}

Делим скрипт на логические части при помощи функций

task_foo () {
# Script of task foo
}

task_boo () {
# Script of task boo
}

task_foo
# task_boo - временно отключено

🟢 Хорошие практики и шаблоны

Ветвление по имени команды в первом аргументе

task_foo () {
echo "exec: task_foo"
}

task_boo () {
echo "exec: task_boo first arg: $1"
echo "exec: task_boo all task args: $\*"
}

"$@"
./script.sh task_boo --one --two
exec: task_boo first arg: --one
exec: task_boo all task args: --one --two

Скрипт с подкомандами

#!/usr/bin/env bash
set -eo pipefail
SELF_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"

clean() {
echo "[INFO] clean"
}

build() {
echo "[INFO] build"
}

deploy() {
echo "[INFO] deploy"
}

default() {
clean
build
deploy
}

if [[ -z "$1" ]]; then
default
elif [[ $(type -t "$1") == function ]]; then
"$@"
else
echo "Unknown script command: $1" && exit 1
fi

✅ Более короткий вариант проверки вызова только функции из скрипта

[[ ! $(type -t "$1") == function ]] && echo "Not specified or invalid script command: $1" && exit 1

Определение путей относительно исполняемого скрипта

BASH_SOURCE[0] содержит путь, к скрипту, который исполняется.

Он может быть как абсолютным, так и относительным.

В тексте скрипта надёжнее использовать абсолютные пути, которые не зависят от текущей папки.

Чтобы получить абсолютные пути относительно расположения исполняемого скрипта, используем команды перечисленные далее.

SELF_DIR

# Папка расположения исполняемого скрипта
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"

SELF_PATH

# Полный путь расположения исполняемого скрипта
SELF_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"

Логирование в начала и конца выполнения скрипта

SELF_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
echo "($SELF_PATH) begin ..."
# код скрипта
echo "($SELF_PATH) done"

Выполнение поиска бинарника для выполнения вместо явного указания пути

Допустим не известно точно, где расположен бинарник deno, но зато оболочка знает как его найти, тогда используем:

#!/usr/bin/env -S deno run

Параметр -S указываем обязательно, чтобы строка команды с аргументами была разделена через пробел.

Без -S она передаётся как один аргумент и как правило происходит ошибка.

См. 23.2.2 -S/--split-string usage in scripts

Чтение значения параметра из .env фала

Значение может быть без кавычек или в двойных кавычках

CI_BUILD_VERSION_PREFIX=$(sed -nE 's/^CI_BUILD_VERSION_PREFIX\="?([^"]*)"?$/\1/p' "$SELF_DIR/common.env")
CI_BUILD_VERSION_PREFIX=1.0.0
# или
CI_BUILD_VERSION_PREFIX="1.0.0"

Скрипт вызван на выполнение или подключен через source

#!/bin/bash
# Определить какое-то значение
get_some_value() {
echo "Это значение, которое надо было определить"
}

if [ "$0" = "${BASH_SOURCE[0]}" ]; then
# ✅ Тут функция будет вызвана, только если скрипт запушен на выполнение
# ⭕️ Не будет вызвана, если скрипт подключен через \`source\`
get_some_value
fi
# ✅ Получение и отображение в логе значения, вызовом скрипта
./get_some_value.sh

# ✅ Получение значение, вызовом функции из скрипта
source ./get_some_value.sh

if [[ "\<условие>" ]]; then
some_value
fi

Короткий if/else для определения значения (аналог тернарного if ? then : else)

local exclude_platform
[[ $PLATFORM = "ios" ]] && exclude_platform="android" || exclude_platform="ios"

Прочитать значение поля из JSON файла

local version
version=$(node -p "require('./package.json').version")local version
version=$(deno eval -p "(await import('./package.json', {assert: {type: 'json'}})).default.version")

Проверить есть ли скрипт в package.json scripts

local mycustomscript
mycustomscript=$(node -p "require('./package.json').scripts.mycustomscript || ''")
if [[ -n "$mycustomscript" ]]; then
npm run mycustomscript
fi

Задать значение переменной окружения по-умолчанию в зависимости от значения другой переменной окружения

export RUBY_SETUP=${RUBY_SETUP:-$([ "$PLATFORM" = "ios" ] && echo "true")}
export GEMS_INSTALL=${GEMS_INSTALL:-$([ "$PLATFORM" = "ios" ] && echo "true")}

Получить имя файла из пути к файлу

file_name="$(basename "$file_path")"

Получить расширение файла или имя файла без расширения

# file_name: development.android.env

extension_min="${file_name##\*.}"
# extension_min: env

extension_max="${file_name#\*.}"
# extension_max: android.env

filename_min="${file_name%%.\*}"
# filename_min: development

filename_max="${file_name%.\*}"
# filename_max: development.android

Найти исполняемые или не исполняемые файлы

# Найти исполняемые файлы
find .git/hooks -type f -name 'pre-\*' -maxdepth 1 -not -perm -a=x -print

# Найти НЕ исполняемые файлы
find .git/hooks -type f -name 'pre-\*' -maxdepth 1 -perm -a=x -print

Добавить числовое значение к номеру сборки в переменной окружения

export CI_BUILD_NUMBER=${CI_BUILD_NUMBER:-0}
export BUILD_NUMBER_SHIFT=${BUILD_NUMBER_SHIFT:-0}
export BUILD_NUMBER=$((CI_BUILD_NUMBER + BUILD_NUMBER_SHIFT))

Получить текущие дату, время и зону

# 2023-09-01 14:49:29 MSK
date +'%Y-%m-%d %H:%M:%S %Z'

Сравнить файлы по содержимому

if cmp -s "path/to/file1" "path/to/file2"; then
echo "Files equals"
fi

Удалить содержимое директории без удаления самой директории

shopt -s dotglob
rm -fr ./path/to/dir/*

Подгрузить переменные окружения из .env файла, сделав им export

set -o allexport;
source .env
set +o allexport;

Прочитать версию bundler из Gemfile.lock

BUNDLED WITH
2.1.4

Команда для получения чистого значения версии

grep -A 1 "BUNDLED WITH" Gemfile.lock | tail -n 1 | xargs

Ссылки

Блог на Docusaurus и TinaCMS

· 4 мин. чтения
Дмитрий Чудный
Автор сайта

Процесс создания блога на базе статического генератора Docusaurus и Tina CMS.

Задача

Сделать статический сайт, ядром которого будет блог. Сайт не будет требовать ни веб-сервера ни базы данных. По содержимому будет создаваться набор статических файлов. Они будут заливаться в S3 хранилище, которое будет предоставлено как веб-сайт через сервис Хостинг статических сайтов от Yandex Cloud.

Код сайта

  • Клонировал репозиторий: tinacms/tinasaurus
  • Внёс правки, чтобы
    • Обновить зависимости
    • Удалить всё что, мне не нужно на сайте
    • Добавить всё что мне нужно на сайте
    • Настроить сборку по своим требованиям

Хостинг

  • Выбрал домен второго уровня: www.chudnyi.com, верхнего уровня не подходит так как на Хостинг статических сайтов от Yandex Cloud можно настроить только DNS запись типа CNAME, который не поддерживает домены верхнего уровня.
  • Создал облако и каталог в Яндекс.Облако
  • Создал сервисный аккаунт для деплоя файлов сайта в S3 бакет
    • Создал сервисному аккаунту новый статический ключ доступа, получил значения AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY
    • Сохранил все параметры в локальный .env файл
AWS_REGION=ru-central1-b
AWS_ENDPOINT=https://storage.yandexcloud.net
AWS_BUCKET=www.chudnyi.com
AWS_ACCESS_KEY_ID=<your key>
AWS_SECRET_ACCESS_KEY=<your secret>
  • Создал сертификат в менеджере сертификатов от Let's Encrypt
    • Указал домены: chudnyi.com и www.chudnyi.com
    • Задал в DNS записях домена необходимые TXT записи для проверки прав на домен
    • Сертификат в менеджере сертификатов Яндекс выпускается не быстро, может несколько часов
  • Создал бакет в Object Storage с точно таким же именем (это важно) как домен сайта: www.chudnyi.com
    • Максимальный размер ограничил в 1 Гб, можно оставить без ограничения
    • Доступ на чтение объектов: Публичный
    • Класс хранилища: Стандартное
    • В разделе "Веб-сайт"
      • Включил "Хостинг"
      • Главная страница: index.html
      • Страница ошибки: 404.html
    • В разделе HTTPS
      • Источник: Certificate Manager
      • Выбрал сертификат ранее созданный в менеджере сертификатов
    • В разделе Права доступа
      • Нажал кнопку "Назначить роли" сверху справа в углу
      • Выбрал сервисный аккаунт для деплоя сайта
      • Назначил роли: storage.uploader, storage.viewer, storage.editor
  • Добавил в бакет тестовую страницу index.html
  • Проверил, что тестовая страница открывается по ссылке http://www.chudnyi.com.website.yandexcloud.net/
  • В DNS записи своего домена добавил CNAME с параметрами
    • Subdomain: www
    • Canonical name: www.chudnyi.com.website.yandexcloud.net.
    • На обновление DNS записей потребуется время, лучше сделать это заранее
    • Через некоторое время, когда обновится DNS запись CNAME, можно проверить открывается ли тестовая страница по ссылке https://www.chudnyi.com/

Сборка

В package.json заменил команду сборки сайта. Таким образом буду получать статический сайт, который не требует бэкенда Tina.

"scripts": {
"build": "tinacms dev --datalayer-port 9001 -c 'docusaurus build'",
}

Указал порт отличный от 9000 в параметре --datalayer-port 9001 для того, чтобы сборка не падала при запущенном локально dev сервере.

После успешной сборки файлы сайта будут находиться в локальной папке проекта: build.

Добавил скрипт scripts/clean-tina.ts для удаления папки build/admin, так как админка Tina на боевом сайте мне не нужна.

Тестирование

После сборки можно запускаю локальный веб-сервер, чтобы потестировать сайт вручную

"scripts": {
"serve": "docusaurus serve"
}

Деплой

Чтобы выполнить загрузку сайта на боевой хостинг необходимо синхронизировать локальную папку build c S3 бакетом так, чтобы они стали полностью идентичны. При этом каждому файлу обязательно нужно правильно задать HTTP заголовок Content-Type, который сервер будет отдавать в ответе на запрос файла.

Варианты решения описаны тут: Устранение проблем с некорректным MIME-типов объектов при их загрузке в Object Storage | Yandex Cloud - Документация

Попробовав отдельные инструменты aws s3 sync и s3cmd, решил написать свой простой скрипт синхронизации на базе npm библиотеки s3-sync-client. Таким образом мне было проще реализовать определение и задание MIME типа каждому файлу.

"scripts": {
"deploy_s3": "tsx scripts/s3-sync.ts",
"deploy_s3:dev": "dotenv -- npm run deploy_s3"
}

Где

✅ После загрузки всех файлов в S3 хранилище, можно проверять работу сайта по целевой ссылке: https://www.chudnyi.com/

Сылки

Реализовать на сайте

· 2 мин. чтения
Дмитрий Чудный
Автор сайта

🔲 Актуально

  • Отображение описания и картинки, когда делишься ссылкой в мессенджерах и соцсетях
  • Настроить SEO данные Search engine optimization (SEO) | Docusaurus
  • Не работает подсветка синтаксиса блоков кода
  • Разобраться в embeded блоке кода с указанием имени файла, оно не отображается
  • Добавить возможность комментировать статьи на сайте: Варианты: Добавляем комментарии в Docusaurus | Блог _AMD_
  • Восстановить раздел документов, когда они появятся, включить ссылку на редактирование
  • Оптимизация загрузки картинок 📦 plugin-ideal-image | Docusaurus
  • Можно ли развернуть редактор текста статьи Tina на весь экран или сделать его больше
  • Редактор тегов Tina не предоставляет выбор из уже имеющихся тегов. Ввод тега всякий раз как первый. Это не удобно и возможны дубликаты тегов с немного отличным написанием.
  • Сделать, чтобы сайт открывался в странах, где заблокированы IP адреса Яндекса

🧐 Возможно

✅ Сделано

  • Задание автора поста блога по-умолчанию на себя
  • Добавить ссылку на список постов блога по годам "архив"
  • Добавить ссылку на список тегов постов блога
  • Отобразить дату редактирования поста и сортировать посты по дате редактирования
  • Решена проблема редиректа с https://chudnyi.com/ на https://www.chudnyi.com/ при помощи https://redirect.pizza/
  • Добавлено свойство disabled в элемент навигации, чтобы можно было скрывать их, не удаляя из кода
  • Добавлен режим черновиков для постов блога. Посты с тегом draft не попадают в боевую сборку сайта, но отображаются в dev режиме.
  • Добавлен static/robots.txt
  • Добавлен локальный поиск через easyops-cn/docusaurus-search-local: Offline/local search for Docusaurus v2/v3
  • Добавлена яндекс метрика через sgromkov/docusaurus-plugin-yandex-metrica: Yandex.Metrica plugin for Docusaurus v2
  • Ссылки на статьи блога сделаны вложенными в путь /blog при помощи изменения permalink в processBlogPosts

⭕️ Не актуально

  • Выбор автора поста блога из списка blog/authors.yml - не нужно, пока автор я один
  • Создание постов блога через tina не файлами, а папками, чтобы в них оказывались картинки поста - не нужно, картинки добавляются через Tina Media Manager и лежат все в /static/img
  • Не работает вставка картинок из буфера обмена в редакторе tina
  • Добавление в начало имени папки поста блога даты, чтобы сортировка файлов была по времени