From 2a32b998f65520abd50ba64485020b862382c7b7 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Wed, 28 Oct 2026 17:19:14 +0500 Subject: [PATCH 1/3] =?UTF-8?q?=D0=9F=D0=B0=D0=B9=D0=BF=D0=BB=D0=B0=D0=B9?= =?UTF-8?q?=D0=BD=D1=8B=20test=20=D0=B8=20publish,=20=D1=80=D0=B5=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D1=80=20=E2=80=94=20=D0=BE=D1=80=D0=B3=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20Tansu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .gitattributes | 3 + .gitea/workflows/publish.yml | 34 +++++++++++ .gitea/workflows/test.yml | 29 ++++++++++ .npmrc | 2 +- README.md | 59 +++++++------------ ci/publish.sh | 108 +++++++++++++++++++++++++++++++++++ package.json | 5 +- 7 files changed, 201 insertions(+), 39 deletions(-) create mode 100644 .gitattributes create mode 100644 .gitea/workflows/publish.yml create mode 100644 .gitea/workflows/test.yml create mode 100755 ci/publish.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4abe4da --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Скрипты и пайплайны запускаются на Linux-раннере: только LF. +*.sh text eol=lf +.gitea/**/*.yml text eol=lf diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..5f4ad36 --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,34 @@ +name: publish + +# Публикация @tnsu/ui-kit в реестр организации Tansu — только по кнопке (Actions -> publish -> Run workflow). +# Сначала юнит-тесты и сборка, потом ci/publish.sh: версию он считает сам (major.minor — из package.json, +# патч — следующий после опубликованного), а если код не менялся с последней опубликованной версии, +# падает с «Публиковать нечего». +# +# Секреты: PACKAGES_USER — логин в Gitea, PACKAGES_TOKEN — его токен со scope write:package +# (пользователь должен состоять в организации Tansu с правом записи). +on: + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install + run: npm ci --no-audit --no-fund + + - name: Unit tests + run: npm test + + - name: Publish + env: + PACKAGES_TOKEN: ${{ secrets.PACKAGES_TOKEN }} + run: bash ci/publish.sh diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml new file mode 100644 index 0000000..4f261c2 --- /dev/null +++ b/.gitea/workflows/test.yml @@ -0,0 +1,29 @@ +name: test + +# Юнит-тесты и сборка пакета: на каждый push в main, на pull request и по кнопке. +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install + run: npm ci --no-audit --no-fund + + - name: Unit tests + run: npm test + + - name: Build + run: npm run build diff --git a/.npmrc b/.npmrc index 8e18031..c0751e2 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -@tnsu:registry=http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/ +@tnsu:registry=http://git.tnsu.kz/api/packages/Tansu/npm/ diff --git a/README.md b/README.md index f57bbdc..bee6203 100644 --- a/README.md +++ b/README.md @@ -4,40 +4,15 @@ > Внутренняя память проекта (решения, план, прогресс) — в [PROJECT.md](./PROJECT.md). -## Первичная настройка (один раз на каждого разработчика) - -Пакет лежит в приватном registry (Gitea), поэтому перед первой установкой нужно один -раз настроить доступ. Дальше про это можно забыть. - -**Шаг 1. Создать токен в Gitea:** -1. Открой http://git.tnsu.kz → кликни на свой аватар справа вверху → **Настройки**. -2. Слева: **Приложения**. -3. В блоке «Управление токенами» → **Создать новый токен**: - - Имя: например `npm-read`. - - Права (scopes): найди строку **`package`** и поставь **«Прочитанные»** (Read). - Остальное — «Нет доступа». -4. Нажми **Генерировать токен** и **сразу скопируй** его (показывается один раз). - -**Шаг 2. Прописать токен у себя на компьютере.** -Создай (или дополни) файл `.npmrc` в своей домашней папке -(`C:\Users\<твоё-имя>\.npmrc`) одной строкой: - -``` -//git.tnsu.kz/api/packages/timur.gorodetskiy/npm/:_authToken=ВСТАВЬ_СВОЙ_ТОКЕН -``` - -> ⚠️ Токен — это как пароль. Он лежит в домашней папке, **вне проекта**, и в git не попадает. -> Никогда не коммить его и не вставляй в код. - -Всё. Теперь `npm i` в любом проекте, где подключён `@tnsu/ui-kit`, будет работать. - ## Установка в проект -Убедись, что в корне проекта есть файл `.npmrc` со строкой (для request-portal он уже -закоммичен): +Пакет лежит в реестре организации **Tansu** на git.tnsu.kz. Реестр публичный, а сам git.tnsu.kz +доступен только из внутренней сети, поэтому **токен для установки не нужен** (из сети или по VPN). + +В корне проекта — файл `.npmrc` со строкой: ``` -@tnsu:registry=http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/ +@tnsu:registry=http://git.tnsu.kz/api/packages/Tansu/npm/ ``` Затем: @@ -86,11 +61,21 @@ npm run storybook # витрина компонентов (localhost:6006) ### Публикация новой версии -```powershell -# 1. поднять version в package.json (напр. 0.0.1 → 0.0.2) -npm run build -npm publish -``` +Публикует пайплайн **publish** в Gitea — по кнопке: Actions → publish → Run workflow. Сначала юнит-тесты +и сборка, потом публикация в реестр Tansu. -> npm не даёт перезаписать уже опубликованную версию — перед публикацией всегда -> бампай `version` в package.json. +- **Версию поднимает пайплайн.** В `package.json` задаются только major.minor (патч там не важен): + новая функциональность — поднять minor руками (`0.14.x` → `0.15.0`). Патч — следующий после самого + большого опубликованного с тем же major.minor. В git ничего не коммитится. +- **Тот же код не публикуется.** Пайплайн считает хэш исходников (`src` без тестов и историй Storybook, + `package.json`, конфиги сборки, README), кладёт его в публикуемый `package.json` (поле `sourceHash`) + и сравнивает с последней опубликованной версией. Совпало — пайплайн падает с «Публиковать нечего». +- Секреты: `PACKAGES_USER` (логин в Gitea) и `PACKAGES_TOKEN` (его токен со scope `write:package`, + участник организации Tansu с правом записи). + +Пробный прогон без публикации: `DRY_RUN=1 bash ci/publish.sh`. + +Версии до 0.14.0 скопированы из прежнего реестра `timur.gorodetskiy` без хэша, поэтому первая публикация +пайплайном выпустит `0.14.1` даже без изменений кода; дальше проверка работает как обычно. + +На каждый push и pull request пайплайн **test** гоняет юнит-тесты и сборку. diff --git a/ci/publish.sh b/ci/publish.sh new file mode 100755 index 0000000..49cc5e6 --- /dev/null +++ b/ci/publish.sh @@ -0,0 +1,108 @@ +#!/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" diff --git a/package.json b/package.json index 89701a7..bdf5903 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,10 @@ "license": "ISC", "repository": { "type": "git", - "url": "http://git.tnsu.kz/timur.gorodetskiy/tnsu-ui-kit.git" + "url": "http://git.tnsu.kz/Tansu/tnsu-ui-kit.git" + }, + "publishConfig": { + "registry": "http://git.tnsu.kz/api/packages/Tansu/npm/" }, "type": "module", "main": "./dist/index.cjs", -- 2.34.1 From 6fd3672edcde3e89f38853b67369c36741344aaf Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Wed, 28 Oct 2026 17:23:56 +0500 Subject: [PATCH 2/3] =?UTF-8?q?=D0=A1=D1=81=D1=8B=D0=BB=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=80=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=B9=20=E2=80=94=20=D0=BF=D1=80=D0=B5=D0=B6=D0=BD?= =?UTF-8?q?=D1=8F=D1=8F=20=D1=80=D0=B5=D0=BF=D0=B0=20timur.gorodetskiy/tns?= =?UTF-8?q?u-ui-kit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Код остаётся в этой репе, публикация по-прежнему в реестр организации Tansu. Co-Authored-By: Claude Opus 5.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bdf5903..127ff40 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "license": "ISC", "repository": { "type": "git", - "url": "http://git.tnsu.kz/Tansu/tnsu-ui-kit.git" + "url": "http://git.tnsu.kz/timur.gorodetskiy/tnsu-ui-kit.git" }, "publishConfig": { "registry": "http://git.tnsu.kz/api/packages/Tansu/npm/" -- 2.34.1 From 65fc6ced30c3fd10379439ae683d993d3e3cd8ad Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Wed, 28 Oct 2026 17:31:00 +0500 Subject: [PATCH 3/3] =?UTF-8?q?=D0=9F=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20ui-kit:=20=D0=BD=D1=83=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D1=81=D0=B5=D0=BA?= =?UTF-8?q?=D1=80=D0=B5=D1=82=20PACKAGES=5FTOKEN,=20=D0=BB=D0=BE=D0=B3?= =?UTF-8?q?=D0=B8=D0=BD=20=D0=BD=D0=B5=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=B7=D1=83=D0=B5=D1=82=D1=81=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/publish.yml | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml index 5f4ad36..97c03c0 100644 --- a/.gitea/workflows/publish.yml +++ b/.gitea/workflows/publish.yml @@ -5,8 +5,8 @@ name: publish # патч — следующий после опубликованного), а если код не менялся с последней опубликованной версии, # падает с «Публиковать нечего». # -# Секреты: PACKAGES_USER — логин в Gitea, PACKAGES_TOKEN — его токен со scope write:package -# (пользователь должен состоять в организации Tansu с правом записи). +# Секрет: PACKAGES_TOKEN — токен Gitea со scope write:package (его владелец должен состоять в организации +# Tansu с правом записи). Логин не нужен: npm авторизуется одним токеном. on: workflow_dispatch: diff --git a/README.md b/README.md index bee6203..c179e47 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,8 @@ npm run storybook # витрина компонентов (localhost:6006) - **Тот же код не публикуется.** Пайплайн считает хэш исходников (`src` без тестов и историй Storybook, `package.json`, конфиги сборки, README), кладёт его в публикуемый `package.json` (поле `sourceHash`) и сравнивает с последней опубликованной версией. Совпало — пайплайн падает с «Публиковать нечего». -- Секреты: `PACKAGES_USER` (логин в Gitea) и `PACKAGES_TOKEN` (его токен со scope `write:package`, - участник организации Tansu с правом записи). +- Секрет: `PACKAGES_TOKEN` — токен Gitea со scope `write:package` (владелец — участник организации Tansu + с правом записи). Логин не нужен. Пробный прогон без публикации: `DRY_RUN=1 bash ci/publish.sh`. -- 2.34.1