Files
kompas3d-mcp/.gitea/workflows/release.yml
T
mikhail 55045184e2 ci: разрешить равную версию в release.yml только для ретрая того же коммита
Гейт «версия не должна уменьшаться» отвергал и равную версию безусловно, что
делало невозможным повторный прогон на том же теге (Task 10 Step 4 плана).
Теперь сравнение version+sourceSha читается из plugin/server.lock.json ветки
dist: равная версия проходит дальше только если sourceSha из dist совпадает с
текущим github.sha (ретрай уже опубликованного релиза — ниже по пайплайну он
и так идемпотентен: upsert релиза/ассета, повторная сверка sha256, force-push
dist тем же содержимым). Другой sourceSha при той же версии — попытка
подменить уже опубликованный релиз другим кодом — отвергается с явным
сообщением, какой коммит уже занимает эту версию. Меньшая версия отвергается
как и раньше.
2026-07-31 02:55:39 +03:00

179 lines
11 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: release
on:
push:
tags: ['v*']
# Релизы сериализуются: параллельные теги не должны гонять ветку dist одновременно
# (обе гоняющиеся сборки делают `git push --force dist`, и без сериализации может
# выиграть та, что стартовала позже, но закончила раньше — с более старой версией).
concurrency:
group: release
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
env:
OWNER: mikhail
REPO: kompas3d-mcp
GITEA: https://git.shahovalov.ru
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Пушим в dist собственным GITEA_TOKEN явным URL (см. последний шаг) —
# заголовок авторизации, который checkout оставляет в git-конфиге для
# выданного джобу токена, тут не нужен и может конфликтовать с ним
# (два способа аутентификации на один и тот же хост в одном запросе).
persist-credentials: false
- name: инструменты
run: apt-get update -qq && apt-get install -y -qq jq zip curl git
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: проверить формат тега
run: |
echo "${{ github.ref_name }}" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|| { echo "тег должен быть вида vX.Y.Z, получено: ${{ github.ref_name }}"; exit 1; }
- name: версия не должна уменьшаться
run: |
NEW="${{ github.ref_name }}"; NEW="${NEW#v}"
CURRENT_SHA="${{ github.sha }}"
# Сверяем по plugin/server.lock.json (не plugin.json): в нём вместе с версией
# лежит и sourceSha, а он нужен ниже, чтобы отличить ретрай того же релиза от
# попытки переопубликовать ту же версию другим коммитом.
#
# `git show origin/dist:...` может не найти ни ветку, ни файл (первый релиз —
# dist ещё не существует). Не полагаемся на `cmd || echo fallback` вокруг пайпа
# с jq: jq на пустом stdin ничего не печатает, но выходит с кодом 0, поэтому
# такой fallback никогда бы не сработал — здесь fallback навешан на сам git show,
# jq вызывается только когда файл реально прочитан. Без dist сравнивать sourceSha
# не с чем — OLD_SOURCE_SHA остаётся пустым, и равенство версий ниже недостижимо
# (OLD_VERSION="0.0.0" всегда меньше валидного тега vX.Y.Z, кроме вырожденного
# v0.0.0, который проверка формата тега не отсекает, но это не наш случай).
git fetch origin dist 2>/dev/null || true
OLD_VERSION="0.0.0"
OLD_SOURCE_SHA=""
if git show origin/dist:plugin/server.lock.json > /tmp/dist-lock.json 2>/dev/null; then
OLD_VERSION=$(jq -r '.version // "0.0.0"' /tmp/dist-lock.json)
OLD_SOURCE_SHA=$(jq -r '.sourceSha // ""' /tmp/dist-lock.json)
fi
if [ "$OLD_VERSION" = "$NEW" ]; then
# Равная версия допускается ровно в одном случае: тег указывает на тот же
# коммит, из которого эта версия уже была опубликована в dist — это ретрай
# (сбойнул шаг после публикации ассета, тег пересоздали на том же SHA и т.п.),
# и весь пайплайн ниже идемпотентен сам по себе (upsert релиза/ассета, повторная
# сверка sha256, force-push dist тем же содержимым). Если sourceSha другой — под
# тем же номером версии протаскивают другой код, это не ретрай, а подмена уже
# опубликованного релиза; такое отвергаем, а не публикуем молча поверх старого.
if [ -n "$OLD_SOURCE_SHA" ] && [ "$OLD_SOURCE_SHA" = "$CURRENT_SHA" ]; then
echo "ok: повтор релиза $NEW с того же коммита $CURRENT_SHA"
else
echo "версия $NEW уже опубликована из коммита ${OLD_SOURCE_SHA:-<неизвестно>} — поднимите версию"
exit 1
fi
else
SMALLEST=$(printf '%s\n%s\n' "$OLD_VERSION" "$NEW" | sort -V | head -1)
if [ "$SMALLEST" != "$OLD_VERSION" ]; then
echo "версия $NEW не больше опубликованной $OLD_VERSION"
exit 1
fi
echo "ok: $NEW > $OLD_VERSION"
fi
- name: publish
run: |
VERSION="${{ github.ref_name }}"; VERSION="${VERSION#v}"
dotnet publish src/Kompas.Mcp.Host/Kompas.Mcp.Host.csproj \
-c Release -r win-x64 --self-contained true \
-p:EnableWindowsTargeting=true -p:Version="$VERSION" \
-o out/publish
test -f out/publish/kompas-mcp.exe || { echo "нет kompas-mcp.exe в out/publish"; exit 1; }
# Плоская структура: exe и зависимости прямо в корне архива, без каталога out/publish/.
(cd out/publish && zip -qr "../kompas-mcp-$VERSION-win-x64.zip" .)
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "ASSET=out/kompas-mcp-$VERSION-win-x64.zip" >> "$GITHUB_ENV"
echo "SHA256=$(sha256sum "out/kompas-mcp-$VERSION-win-x64.zip" | cut -d' ' -f1)" >> "$GITHUB_ENV"
- name: upsert релиза и ассета
run: |
API="$GITEA/api/v1/repos/$OWNER/$REPO"
AUTH="Authorization: token ${{ secrets.GITEA_TOKEN }}"
TAG="${{ github.ref_name }}"
NAME="kompas-mcp-$VERSION-win-x64.zip"
# Найти существующий релиз по тегу; создать, если его ещё нет. Различаем "нет
# релиза" (404) от прочих ошибок API явно по HTTP-коду — не глотаем и то, и
# другое одним `|| echo ""`, как в черновике: сетевая/авторизационная ошибка
# не должна молча трактоваться как "релиза нет, создаём новый".
HTTP=$(curl -s -o /tmp/release.json -w '%{http_code}' -H "$AUTH" "$API/releases/tags/$TAG")
if [ "$HTTP" = "200" ]; then
ID=$(jq -r '.id' /tmp/release.json)
elif [ "$HTTP" = "404" ]; then
HTTP=$(curl -s -o /tmp/release.json -w '%{http_code}' -X POST -H "$AUTH" \
-H 'Content-Type: application/json' \
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\"}" "$API/releases")
if [ "$HTTP" != "201" ]; then
echo "не удалось создать релиз $TAG (HTTP $HTTP)"; cat /tmp/release.json; exit 1
fi
ID=$(jq -r '.id' /tmp/release.json)
else
echo "не удалось получить релиз по тегу $TAG (HTTP $HTTP)"; cat /tmp/release.json; exit 1
fi
# Повторный прогон на том же теге не должен падать на "ассет уже существует":
# старый ассет с тем же именем удаляется перед загрузкой нового.
HTTP=$(curl -s -o /tmp/assets.json -w '%{http_code}' -H "$AUTH" "$API/releases/$ID/assets")
if [ "$HTTP" != "200" ]; then
echo "не удалось получить список ассетов релиза $ID (HTTP $HTTP)"; cat /tmp/assets.json; exit 1
fi
EXISTING=$(jq -r --arg name "$NAME" '.[] | select(.name==$name) | .id' /tmp/assets.json)
if [ -n "$EXISTING" ]; then
curl -sf -X DELETE -H "$AUTH" "$API/releases/$ID/assets/$EXISTING"
fi
HTTP=$(curl -s -o /tmp/asset.json -w '%{http_code}' -X POST -H "$AUTH" \
-F "attachment=@$ASSET" "$API/releases/$ID/assets?name=$NAME")
if [ "$HTTP" != "201" ]; then
echo "не удалось загрузить ассет $NAME (HTTP $HTTP)"; cat /tmp/asset.json; exit 1
fi
echo "URL=$GITEA/$OWNER/$REPO/releases/download/$TAG/$NAME" >> "$GITHUB_ENV"
- name: сверить опубликованный ассет
run: |
curl -sfL "$URL" -o /tmp/published.zip
ACTUAL=$(sha256sum /tmp/published.zip | cut -d' ' -f1)
[ "$ACTUAL" = "$SHA256" ] || { echo "SHA256 опубликованного ассета не совпал: ожидался $SHA256, получен $ACTUAL"; exit 1; }
- name: обновить ветку dist
run: |
git config user.name "gitea-actions"
git config user.email "actions@git.shahovalov.ru"
git remote set-url origin "$GITEA/$OWNER/$REPO.git"
# dist — не история, а единственный коммит поверх коммита с тегом (см. архитектуру
# в docs/superpowers/plans/2026-07-31-kompas-plugin-distribution.md): каждый новый
# релиз пересобирает dist заново поверх своего тега, поэтому push принудительный.
# concurrency-группа выше гарантирует, что это не гонка между двумя релизами разом.
git checkout -B dist "${{ github.sha }}"
jq --arg v "$VERSION" --arg s "$SHA256" --arg u "$URL" --arg src "${{ github.sha }}" \
'.version=$v | .sha256=$s | .url=$u | .sourceSha=$src' \
plugin/server.lock.json > /tmp/lock.json && mv /tmp/lock.json plugin/server.lock.json
jq --arg v "$VERSION" '.version=$v' \
plugin/.claude-plugin/plugin.json > /tmp/manifest.json && mv /tmp/manifest.json plugin/.claude-plugin/plugin.json
git add plugin/server.lock.json plugin/.claude-plugin/plugin.json
git commit -m "release: kompas $VERSION"
git push --force "https://x-access-token:${{ secrets.GITEA_TOKEN }}@git.shahovalov.ru/$OWNER/$REPO.git" dist