From a8551b5c2856b45b9e942eb4a7ab3686c3c386f6 Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 21:20:57 +0300 Subject: [PATCH] CI: game and desktop builds check out exactly inputs.tag, verify its release was made by build.yml from that commit, and upload only to it (#40) Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/desktop.yml | 70 ++++++++++++++++++++++++++++++++---- .gitea/workflows/game.yml | 37 +++++++++++++++++-- README.md | 4 ++- 3 files changed, 100 insertions(+), 11 deletions(-) diff --git a/.gitea/workflows/desktop.yml b/.gitea/workflows/desktop.yml index cdfcb0d..9f90e8f 100644 --- a/.gitea/workflows/desktop.yml +++ b/.gitea/workflows/desktop.yml @@ -22,7 +22,39 @@ jobs: if: inputs.os == 'macos' || inputs.os == 'both' runs-on: macos-latest steps: + - name: Проверка релиза + env: + TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }} + API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} + TAG: ${{ inputs.tag }} + run: | + set -e + if ! [[ "$TAG" =~ ^build-[0-9]+$ ]]; then echo "::error::тег '$TAG' не похож на build-N"; exit 1; fi + REL_JSON=$(mktemp) + CODE=$(curl -s -o "$REL_JSON" -w '%{http_code}' -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG") + if [ "$CODE" != "200" ]; then echo "::error::релиза с тегом $TAG нет (HTTP $CODE): сначала дождись сборки build.yml на main"; exit 1; fi + python3 - "$TAG" "$REL_JSON" <<'PY' + import json, os, sys + tag, path = sys.argv[1], sys.argv[2] + r = json.load(open(path)) + assets = [a.get('name') for a in (r.get('assets') or [])] + if r.get('tag_name') != tag or r.get('name') != tag or 'airdef.hmod' not in assets: + print('::error::релиз %s не создан build.yml (нужны имя = тег и вложение airdef.hmod): чужой релиз не трогаем' % tag) + sys.exit(1) + with open(os.environ['GITHUB_ENV'], 'a') as f: + f.write('RELEASE_ID=%s\nRELEASE_SHA=%s\n' % (r['id'], r.get('target_commitish', ''))) + PY - uses: actions/checkout@v4 + with: + ref: refs/tags/${{ inputs.tag }} + - name: Проверка коммита + env: + TAG: ${{ inputs.tag }} + run: | + set -e + HEAD_SHA=$(git rev-parse HEAD) + if [[ "$RELEASE_SHA" =~ ^[0-9a-f]{40}$ ]] && [ "$HEAD_SHA" != "$RELEASE_SHA" ]; then echo "::error::тег $TAG указывает на $HEAD_SHA, а релиз создан из $RELEASE_SHA"; exit 1; fi + echo "собираю $TAG = $HEAD_SHA" - name: Rust run: | command -v cargo || curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal @@ -47,11 +79,10 @@ jobs: env: TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }} API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} - TAG: ${{ inputs.tag }} run: | set -e - ID=$(curl -sf -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])') - curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/airdef-macos-arm64.tar.gz" "$API/releases/$ID/assets?name=airdef-macos-arm64.tar.gz" > /dev/null + # RELEASE_ID взят в шаге «Проверка релиза» (релиз build-N, созданный build.yml) + curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/airdef-macos-arm64.tar.gz" "$API/releases/$RELEASE_ID/assets?name=airdef-macos-arm64.tar.gz" > /dev/null game-windows: if: inputs.os == 'windows' || inputs.os == 'both' @@ -60,7 +91,34 @@ jobs: run: shell: pwsh steps: + - name: Проверка релиза + env: + TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }} + API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} + TAG: ${{ inputs.tag }} + run: | + $ErrorActionPreference = 'Stop' + if ($env:TAG -notmatch '^build-\d+$') { Write-Output "::error::тег '$env:TAG' не похож на build-N"; exit 1 } + $h = @{ Authorization = "token $env:TOKEN" } + try { $r = Invoke-RestMethod -Headers $h "$env:API/releases/tags/$env:TAG" } + catch { Write-Output "::error::релиза с тегом $env:TAG нет: сначала дождись сборки build.yml на main"; exit 1 } + $assets = @($r.assets | ForEach-Object { $_.name }) + if ($r.tag_name -ne $env:TAG -or $r.name -ne $env:TAG -or ($assets -notcontains 'airdef.hmod')) { + Write-Output "::error::релиз $env:TAG не создан build.yml (нужны имя = тег и вложение airdef.hmod): чужой релиз не трогаем"; exit 1 + } + "RELEASE_ID=$($r.id)" | Out-File -Append -Encoding utf8 $env:GITHUB_ENV + "RELEASE_SHA=$($r.target_commitish)" | Out-File -Append -Encoding utf8 $env:GITHUB_ENV - uses: actions/checkout@v4 + with: + ref: refs/tags/${{ inputs.tag }} + - name: Проверка коммита + env: + TAG: ${{ inputs.tag }} + run: | + $ErrorActionPreference = 'Stop' + $head = git rev-parse HEAD + if ($env:RELEASE_SHA -match '^[0-9a-f]{40}$' -and $head -ne $env:RELEASE_SHA) { Write-Output "::error::тег $env:TAG указывает на $head, а релиз создан из $env:RELEASE_SHA"; exit 1 } + Write-Output "собираю $env:TAG = $head" - name: Rust run: | if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { @@ -88,9 +146,7 @@ jobs: env: TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }} API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} - TAG: ${{ inputs.tag }} run: | - $h = @{ Authorization = "token $env:TOKEN" } - $id = (Invoke-RestMethod -Headers $h "$env:API/releases/tags/$env:TAG").id - curl.exe -sf -X POST -H "Authorization: token $env:TOKEN" -F "attachment=@out\airdef-windows-x86_64.zip" "$env:API/releases/$id/assets?name=airdef-windows-x86_64.zip" + # RELEASE_ID взят в шаге «Проверка релиза» (релиз build-N, созданный build.yml) + curl.exe -sf -X POST -H "Authorization: token $env:TOKEN" -F "attachment=@out\airdef-windows-x86_64.zip" "$env:API/releases/$env:RELEASE_ID/assets?name=airdef-windows-x86_64.zip" diff --git a/.gitea/workflows/game.yml b/.gitea/workflows/game.yml index 4306403..6505179 100644 --- a/.gitea/workflows/game.yml +++ b/.gitea/workflows/game.yml @@ -25,7 +25,39 @@ jobs: FREE=$(df --output=avail -BG / | tail -1 | tr -dc 0-9) echo "свободно: ${FREE} ГБ" if [ "$FREE" -lt 15 ]; then echo "::error::на сервере свободно ${FREE} ГБ, нужно хотя бы 15: сборка игры забьёт диск и уронит Gitea"; exit 1; fi + - name: Проверка релиза + env: + TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }} + API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} + TAG: ${{ inputs.tag }} + run: | + set -e + if ! [[ "$TAG" =~ ^build-[0-9]+$ ]]; then echo "::error::тег '$TAG' не похож на build-N"; exit 1; fi + REL_JSON=$(mktemp) + CODE=$(curl -s -o "$REL_JSON" -w '%{http_code}' -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG") + if [ "$CODE" != "200" ]; then echo "::error::релиза с тегом $TAG нет (HTTP $CODE): сначала дождись сборки build.yml на main"; exit 1; fi + python3 - "$TAG" "$REL_JSON" <<'PY' + import json, os, sys + tag, path = sys.argv[1], sys.argv[2] + r = json.load(open(path)) + assets = [a.get('name') for a in (r.get('assets') or [])] + if r.get('tag_name') != tag or r.get('name') != tag or 'airdef.hmod' not in assets: + print('::error::релиз %s не создан build.yml (нужны имя = тег и вложение airdef.hmod): чужой релиз не трогаем' % tag) + sys.exit(1) + with open(os.environ['GITHUB_ENV'], 'a') as f: + f.write('RELEASE_ID=%s\nRELEASE_SHA=%s\n' % (r['id'], r.get('target_commitish', ''))) + PY - uses: actions/checkout@v4 + with: + ref: refs/tags/${{ inputs.tag }} + - name: Проверка коммита + env: + TAG: ${{ inputs.tag }} + run: | + set -e + HEAD_SHA=$(git rev-parse HEAD) + if [[ "$RELEASE_SHA" =~ ^[0-9a-f]{40}$ ]] && [ "$HEAD_SHA" != "$RELEASE_SHA" ]; then echo "::error::тег $TAG указывает на $HEAD_SHA, а релиз создан из $RELEASE_SHA"; exit 1; fi + echo "собираю $TAG = $HEAD_SHA" - name: Системные библиотеки run: | SUDO=$(command -v sudo || true) @@ -45,12 +77,11 @@ jobs: env: TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }} API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} - TAG: ${{ inputs.tag }} run: | set -e D=airdef-linux-x86_64 mkdir -p "$D" out cp horizon/target/fast/horizon-client horizon/target/fast/horizon-ctl airdef-mod/target/horizon-mod/airdef.hmod dist/play.sh docs/INSTALL.md "$D/" tar czf "out/$D.tar.gz" "$D" - ID=$(curl -sf -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])') - curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/$D.tar.gz" "$API/releases/$ID/assets?name=$D.tar.gz" > /dev/null + # RELEASE_ID взят в шаге «Проверка релиза» (релиз build-N, созданный build.yml) + curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/$D.tar.gz" "$API/releases/$RELEASE_ID/assets?name=$D.tar.gz" > /dev/null diff --git a/README.md b/README.md index 5f5a66b..db815cb 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ python3 tools/airdef.py play # запустить (аргументы игр CI: `.gitea/workflows/build.yml` на каждый пуш собирает только мод (`airdef.hmod`), а пуш в `main` публикует его релизом `build-N`. Игру-форк собирают вручную: `.gitea/workflows/game.yml` (Linux) и `.gitea/workflows/desktop.yml` (macOS и Windows, на раннерах с метками `macos-latest` / `windows-latest`); оба запускаются из Actions с тегом -готового релиза и дописывают свои архивы в него. +готового релиза `build-N`. Перед сборкой workflow проверяет, что такой релиз создан `build.yml` (имя релиза = тег, +есть `airdef.hmod`), иначе падает с ошибкой; затем выкачивает ровно коммит этого тега (а не ветку, с которой запущен), +сверяет его с коммитом релиза и дописывает архивы только в этот релиз. ## Как играть