name: release # master -> релиз v<версия>, если такого ещё нет # rc -> пре-релиз v<версия>-rcN, N — следующий свободный номер # тег vX.Y.Z -> релиз, тег vX.Y.Z-rcN -> пре-релиз # ручной запуск -> только сборки, файлы остаются артефактами прогона on: push: #branches: [master, rc] tags: ["v*"] workflow_dispatch: concurrency: group: release-${{ github.ref }} env: API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} jobs: create-release: runs-on: linux outputs: version: ${{ steps.meta.outputs.version }} tag: ${{ steps.meta.outputs.tag }} skip: ${{ steps.meta.outputs.skip }} release_id: ${{ steps.release.outputs.id }} steps: - uses: actions/checkout@v4 - id: meta name: Версия из CMakeLists.txt, тег и тип релиза env: TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | version=$(sed -nE 's/^project\(BiLauncher VERSION ([0-9]+\.[0-9]+\.[0-9]+).*/\1/p' CMakeLists.txt) [ -n "$version" ] || { echo "Не нашёл project(BiLauncher VERSION ...)"; exit 1; } # Теги релизов, включая черновики (токену с правом записи API отдаёт и их) release_tags=$(curl -fsS -H "Authorization: token $TOKEN" "$API/releases?limit=50" \ | python3 -c 'import json,sys; [print(r["tag_name"]) for r in json.load(sys.stdin)]') has_release() { printf '%s\n' "$release_tags" | grep -xF "$1" > /dev/null; } # У черновика тега в git ещё нет, поэтому смотрим и в git, и в релизы known() { git ls-remote --tags origin | sed -E 's#.*refs/tags/##; s/\^\{\}$//' printf '%s\n' "$release_tags" } tag=""; skip=false case "${GITHUB_REF_TYPE}:${GITHUB_REF_NAME}" in tag:*) tag="${GITHUB_REF_NAME}" base="${tag#v}"; base="${base%%-*}" if [ "$base" != "$version" ]; then echo "Тег $tag не совпадает с версией $version в CMakeLists.txt"; exit 1 fi # Тег мог создать сам релиз из прогона по ветке — тогда второй раз не собираем if has_release "$tag"; then echo "Релиз $tag уже есть"; skip=true fi ;; branch:master) tag="v$version" if known | grep -xF "$tag" > /dev/null; then echo "Релиз $tag уже есть, поднимите версию в CMakeLists.txt"; skip=true fi ;; branch:rc) n=$(known | sed -nE "s/^v${version//./\\.}-rc([0-9]+)\$/\1/p" | sort -n | tail -1) tag="v$version-rc$(( ${n:-0} + 1 ))" ;; esac [ -n "$tag" ] && version="${tag#v}" echo "version=$version" >> "$GITHUB_OUTPUT" echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "skip=$skip" >> "$GITHUB_OUTPUT" echo "Версия: $version, тег: ${tag:-нет}, пропуск: $skip" - id: release name: Черновик релиза if: steps.meta.outputs.tag != '' && steps.meta.outputs.skip != 'true' env: TOKEN: ${{ secrets.RELEASE_TOKEN }} TAG: ${{ steps.meta.outputs.tag }} run: | full="${TAG#v}"; base="${full%%-*}" section() { awk -v v="$1" ' /^## \[/ { if (on) exit; if (index($0, "[" v "]")) { on=1; next } } on' CHANGELOG.md } # Сначала раздел ровно под тег ([1.0.1-rc1]), иначе общий под версию ([1.0.1]) notes=$(section "$full") [ -n "$notes" ] || notes=$(section "$base") [ -n "$notes" ] || echo "::warning::В CHANGELOG.md нет раздела [$full] или [$base]" prerelease=false; case "$TAG" in *-rc*) prerelease=true;; esac # target_commitish: у новой ветки тег появится на этом коммите при публикации body=$(python3 -c 'import json,sys; print(json.dumps({"tag_name":sys.argv[1],"target_commitish":sys.argv[4],"name":"BiLauncher "+sys.argv[1],"body":sys.argv[2],"draft":True,"prerelease":sys.argv[3]=="true"}))' \ "$TAG" "$notes" "$prerelease" "$GITHUB_SHA") id=$(curl -fsS -X POST "$API/releases" \ -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ -d "$body" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])') echo "id=$id" >> "$GITHUB_OUTPUT" echo "Создан черновик $([ "$prerelease" = true ] && echo пре-)релиза $TAG (id $id)" build-linux: needs: create-release if: needs.create-release.outputs.skip != 'true' runs-on: linux env: VERSION: ${{ needs.create-release.outputs.version }} # Только для релизов/пре-релизов; при ручном запуске канал определит CMake по ветке BUILD_VERSION: ${{ needs.create-release.outputs.tag != '' && needs.create-release.outputs.version || '' }} RELEASE_ID: ${{ needs.create-release.outputs.release_id }} outputs: sums: ${{ steps.pack.outputs.sums }} steps: - uses: actions/checkout@v4 - name: Сборка run: | export PATH="$QT_TOOLS/CMake/bin:$QT_TOOLS/Ninja:$PATH" rm -rf build AppDir dist cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH="$QT_ROOT" -DLAUNCHER_BUILD_VERSION="$BUILD_VERSION" \ -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=lib cmake --build build -j 8 cmake --install build --prefix "$PWD/AppDir/usr" - id: pack name: AppImage и tar.gz run: | mkdir -p dist cp AppDir/usr/share/applications/bilauncher.desktop AppDir/ cp AppDir/usr/share/icons/hicolor/256x256/apps/bilauncher.png AppDir/ ln -sf usr/bin/BiLauncher AppDir/AppRun ARCH=x86_64 appimagetool --no-appstream AppDir "dist/BiLauncher-$VERSION-x86_64.AppImage" cp -a AppDir/usr "BiLauncher-$VERSION" tar -czf "dist/BiLauncher-$VERSION-linux-x86_64.tar.gz" "BiLauncher-$VERSION" rm -rf "BiLauncher-$VERSION" ls -lh dist echo "sums=$(cd dist && sha256sum * | base64 -w0)" >> "$GITHUB_OUTPUT" - name: Загрузка в релиз if: env.RELEASE_ID != '' env: TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | for f in dist/*; do curl -fsS -X POST -H "Authorization: token $TOKEN" \ -F "attachment=@$f" "$API/releases/$RELEASE_ID/assets?name=$(basename "$f")" > /dev/null echo "Загружен $(basename "$f")" done - name: Артефакты прогона if: env.RELEASE_ID == '' uses: actions/upload-artifact@v4 with: name: linux path: dist/ build-windows: needs: create-release if: needs.create-release.outputs.skip != 'true' runs-on: windows env: VERSION: ${{ needs.create-release.outputs.version }} # Только для релизов/пре-релизов; при ручном запуске канал определит CMake по ветке BUILD_VERSION: ${{ needs.create-release.outputs.tag != '' && needs.create-release.outputs.version || '' }} RELEASE_ID: ${{ needs.create-release.outputs.release_id }} outputs: sums: ${{ steps.build.outputs.sums }} steps: - uses: actions/checkout@v4 - id: build name: Сборка, установщик и portable-архив shell: powershell run: | $ErrorActionPreference = "Stop" $build = "C:\r\bl-build"; $stage = "C:\r\bl-stage"; $dist = "$PWD\dist" Remove-Item -Recurse -Force $build, $stage, $dist -ErrorAction SilentlyContinue $vs = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * ` -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath Import-Module "$vs\Common7\Tools\Microsoft.VisualStudio.DevShell.dll" Enter-VsDevShell -VsInstallPath $vs -SkipAutomaticLocation -DevCmdArguments "-arch=x64 -host_arch=x64" $env:PATH = "$env:QT_TOOLS\CMake_64\bin;$env:QT_TOOLS\Ninja;$env:PATH" cmake -S . -B $build -G Ninja -DCMAKE_BUILD_TYPE=Release "-DCMAKE_PREFIX_PATH=$env:QT_ROOT" "-DLAUNCHER_BUILD_VERSION=$env:BUILD_VERSION" if ($LASTEXITCODE) { throw "cmake configure" } cmake --build $build if ($LASTEXITCODE) { throw "cmake build" } cmake --install $build --prefix $stage if ($LASTEXITCODE) { throw "cmake install" } New-Item -ItemType Directory -Force $dist | Out-Null & "$env:ISCC_DIR\ISCC.exe" /Q "/DAppVersion=$env:VERSION" "/DSourceDir=$stage" "/DOutputDir=$dist" packaging\windows\BiLauncher.iss if ($LASTEXITCODE) { throw "iscc" } Remove-Item "$stage\bin\vc_redist.x64.exe" -ErrorAction SilentlyContinue $portable = "C:\r\BiLauncher-$env:VERSION" Remove-Item -Recurse -Force $portable -ErrorAction SilentlyContinue Move-Item $stage $portable & "$env:SystemRoot\System32\tar.exe" -a -c -f "$dist\BiLauncher-$env:VERSION-win64-portable.zip" -C C:\r "BiLauncher-$env:VERSION" if ($LASTEXITCODE) { throw "zip" } Remove-Item -Recurse -Force $portable Get-ChildItem $dist | Format-Table Name, Length $sums = Get-ChildItem $dist | ForEach-Object { "{0} {1}" -f (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLower(), $_.Name } $b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($sums -join "`n") + "`n")) [IO.File]::AppendAllText($env:GITHUB_OUTPUT, "sums=$b64`n") - name: Загрузка в релиз if: env.RELEASE_ID != '' shell: powershell env: TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | foreach ($f in Get-ChildItem dist) { curl.exe -fsS -X POST -H "Authorization: token $env:TOKEN" ` -F "attachment=@$($f.FullName)" "$env:API/releases/$env:RELEASE_ID/assets?name=$($f.Name)" | Out-Null if ($LASTEXITCODE) { throw "Не удалось загрузить $($f.Name)" } "Загружен $($f.Name)" } - name: Артефакты прогона if: env.RELEASE_ID == '' uses: actions/upload-artifact@v4 with: name: windows path: dist/ build-macos: needs: create-release if: needs.create-release.outputs.skip != 'true' runs-on: macos env: VERSION: ${{ needs.create-release.outputs.version }} # Только для релизов/пре-релизов; при ручном запуске канал определит CMake по ветке BUILD_VERSION: ${{ needs.create-release.outputs.tag != '' && needs.create-release.outputs.version || '' }} RELEASE_ID: ${{ needs.create-release.outputs.release_id }} outputs: sums: ${{ steps.pack.outputs.sums }} steps: - uses: actions/checkout@v4 - name: Сборка (universal, arm64 + x86_64) run: | export PATH="$QT_TOOLS/CMake/CMake.app/Contents/bin:$QT_TOOLS/Ninja:$PATH" rm -rf build stage dist dmgroot cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH="$QT_ROOT" -DLAUNCHER_BUILD_VERSION="$BUILD_VERSION" \ -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" cmake --build build cmake --install build --prefix "$PWD/stage" - id: pack name: Ad-hoc подпись и DMG run: | codesign --force --deep --sign - stage/BiLauncher.app codesign --verify --deep --strict stage/BiLauncher.app mkdir -p dmgroot dist cp -R stage/BiLauncher.app dmgroot/ ln -s /Applications dmgroot/Applications hdiutil create -volname "BiLauncher" -srcfolder dmgroot -ov -format UDZO \ "dist/BiLauncher-$VERSION-macOS-universal.dmg" rm -rf dmgroot ls -lh dist echo "sums=$(cd dist && shasum -a 256 * | base64 | tr -d '\n')" >> "$GITHUB_OUTPUT" - name: Загрузка в релиз if: env.RELEASE_ID != '' env: TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | for f in dist/*; do curl -fsS -X POST -H "Authorization: token $TOKEN" \ -F "attachment=@$f" "$API/releases/$RELEASE_ID/assets?name=$(basename "$f")" > /dev/null echo "Загружен $(basename "$f")" done - name: Артефакты прогона if: env.RELEASE_ID == '' uses: actions/upload-artifact@v4 with: name: macos path: dist/ publish: needs: [create-release, build-linux, build-windows, build-macos] if: needs.create-release.outputs.release_id != '' runs-on: linux env: RELEASE_ID: ${{ needs.create-release.outputs.release_id }} TOKEN: ${{ secrets.RELEASE_TOKEN }} steps: - name: SHA256SUMS.txt и публикация run: | { echo "${{ needs.build-windows.outputs.sums }}" | base64 -d echo "${{ needs.build-linux.outputs.sums }}" | base64 -d echo "${{ needs.build-macos.outputs.sums }}" | base64 -d } | tr -d '\r' | sed '/^$/d' > SHA256SUMS.txt cat SHA256SUMS.txt curl -fsS -X POST -H "Authorization: token $TOKEN" \ -F "attachment=@SHA256SUMS.txt" "$API/releases/$RELEASE_ID/assets?name=SHA256SUMS.txt" > /dev/null curl -fsS -X PATCH -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ -d '{"draft": false}' "$API/releases/$RELEASE_ID" > /dev/null echo "Релиз опубликован"