2a32b998f6
- test: юнит-тесты и сборка на каждый push в main, на PR и по кнопке - publish: по кнопке тесты и публикация в реестр Tansu; ci/publish.sh сам считает патч-версию по реестру (major.minor — из package.json) и не публикует, если хэш кода совпал с последней опубликованной версией - .npmrc и publishConfig — реестр Tansu, repository — Tansu/tnsu-ui-kit - README: установка без токена, публикация кнопкой Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
5.6 KiB
Bash
Executable File
109 lines
5.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Публикация @tnsu/ui-kit в реестр организации Tansu с автоматической версией.
|
||
#
|
||
# bash ci/publish.sh
|
||
#
|
||
# Версия: в package.json задаются только major.minor (патч там не важен). Патч считается здесь:
|
||
# следующий после самого большого опубликованного с тем же major.minor, или 0. В git ничего не коммитится.
|
||
#
|
||
# Хэш кода: sha256 от путей и git-хэшей файлов пакета (тесты и истории Storybook не входят). Кладётся
|
||
# в публикуемый package.json (поле sourceHash). Перед публикацией скачивается последняя опубликованная
|
||
# версия и хэш сравнивается: код тот же — публикации нет. Хэш берётся из самого архива, а не из
|
||
# метаданных реестра: архив реестр хранит как есть.
|
||
#
|
||
# Переменные: PACKAGES_TOKEN (scope write:package), REGISTRY_BASE (по умолчанию
|
||
# http://git.tnsu.kz/api/packages/Tansu), DRY_RUN=1 — всё посчитать и собрать, но не публиковать.
|
||
#
|
||
# Коды выхода: 0 — опубликовано; 1 — ошибка; 3 — публиковать нечего (код не менялся).
|
||
set -euo pipefail
|
||
cd "$(dirname "$0")/.."
|
||
|
||
REGISTRY_BASE="${REGISTRY_BASE:-http://git.tnsu.kz/api/packages/Tansu}"
|
||
DRY_RUN="${DRY_RUN:-}"
|
||
PACKAGES_TOKEN="${PACKAGES_TOKEN:-}"
|
||
|
||
WORK="$(mktemp -d)"
|
||
trap 'rm -rf "$WORK"' EXIT
|
||
|
||
source_hash() {
|
||
git ls-files -s -- "$@" | grep -vE '\.(test\.ts|stories\.tsx)$' | awk '{print $2" "$4}' | sha256sum | cut -c1-16
|
||
}
|
||
|
||
max_version() {
|
||
node -e '
|
||
const v = require("fs").readFileSync(0, "utf8").split(/\s+/).filter((s) => /^\d+\.\d+\.\d+$/.test(s));
|
||
const key = (s) => s.split(".").map(Number);
|
||
v.sort((a, b) => { const x = key(a), y = key(b); return x[0] - y[0] || x[1] - y[1] || x[2] - y[2]; });
|
||
process.stdout.write(v.length ? v[v.length - 1] : "");
|
||
'
|
||
}
|
||
|
||
next_version() {
|
||
local repo_version="$1" published="$2"
|
||
node -e '
|
||
const [repo, published] = [process.argv[1], process.argv[2]];
|
||
const [major, minor] = repo.split(".").map(Number);
|
||
const versions = published.split(/\s+/).filter((s) => /^\d+\.\d+\.\d+$/.test(s)).map((s) => s.split(".").map(Number));
|
||
const newer = versions.find(([a, b]) => a > major || (a === major && b > minor));
|
||
if (newer) { console.error(`В реестре уже есть ${newer.join(".")}, а в репозитории ${major}.${minor}: поднимите версию в репозитории.`); process.exit(1); }
|
||
const patches = versions.filter(([a, b]) => a === major && b === minor).map(([, , c]) => c);
|
||
process.stdout.write(`${major}.${minor}.${patches.length ? Math.max(...patches) + 1 : 0}`);
|
||
' "$repo_version" "$published"
|
||
}
|
||
|
||
# Реестр публичный, читать можно без токена.
|
||
fetch() {
|
||
local url="$1" output="$2" status
|
||
status=$(curl -sS -o "$output" -w '%{http_code}' "$url") || status=000
|
||
echo "$status"
|
||
}
|
||
|
||
hash=$(source_hash src package.json vite.config.ts tsconfig.json global.d.ts README.md)
|
||
repo_version=$(node -p "require('./package.json').version")
|
||
latest="" published="" latest_hash=""
|
||
|
||
status=$(fetch "$REGISTRY_BASE/npm/@tnsu%2Fui-kit" "$WORK/packument.json")
|
||
case "$status" in
|
||
200)
|
||
published=$(node -e 'process.stdout.write(Object.keys(require(process.argv[1]).versions || {}).join(" "))' "$WORK/packument.json")
|
||
latest=$(echo "$published" | tr ' ' '\n' | max_version)
|
||
;;
|
||
404) ;;
|
||
*) echo "Реестр npm ответил $status" >&2; cat "$WORK/packument.json" >&2 || true; exit 1 ;;
|
||
esac
|
||
|
||
if [[ -n $latest ]]; then
|
||
tarball=$(node -e 'process.stdout.write(require(process.argv[1]).versions[process.argv[2]].dist.tarball)' "$WORK/packument.json" "$latest")
|
||
status=$(fetch "$tarball" "$WORK/latest.tgz")
|
||
[[ $status == 200 ]] || { echo "Не скачался @tnsu/ui-kit@$latest: $status" >&2; exit 1; }
|
||
latest_hash=$(tar -xzOf "$WORK/latest.tgz" package/package.json | node -e 'process.stdout.write(JSON.parse(require("fs").readFileSync(0, "utf8")).sourceHash || "")')
|
||
fi
|
||
|
||
echo "хэш кода $hash; последняя опубликованная ${latest:-нет} (хэш ${latest_hash:-нет})"
|
||
if [[ -n $latest_hash && $latest_hash == "$hash" ]]; then
|
||
echo "Публиковать нечего: код не менялся с $latest." >&2
|
||
exit 3
|
||
fi
|
||
|
||
version=$(next_version "$repo_version" "$published")
|
||
echo "публикую $version"
|
||
|
||
# Версия и хэш — только в рабочей копии на время публикации.
|
||
cp package.json "$WORK/package.json.orig"
|
||
trap 'cp "$WORK/package.json.orig" package.json 2> /dev/null; rm -rf "$WORK"' EXIT
|
||
node -e '
|
||
const fs = require("fs"); const p = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
||
p.version = process.argv[1]; p.sourceHash = process.argv[2];
|
||
fs.writeFileSync("package.json", JSON.stringify(p, null, 2) + "\n");
|
||
' "$version" "$hash"
|
||
|
||
npm run build > /dev/null
|
||
if [[ -n $DRY_RUN ]]; then
|
||
npm pack --dry-run 2>&1 | grep -E 'name:|version:|total files'
|
||
echo "DRY_RUN: не публикую"
|
||
else
|
||
printf '//%s:_authToken=%s\n' "${REGISTRY_BASE#*://}/npm/" "$PACKAGES_TOKEN" > "$WORK/npmrc"
|
||
NPM_CONFIG_USERCONFIG="$WORK/npmrc" npm publish
|
||
fi
|
||
echo "Готово: @tnsu/ui-kit@$version"
|