mirror of
https://github.com/1andrevich/re-sputnik.git
synced 2026-08-23 23:56:35 +00:00
219 lines
9 KiB
YAML
219 lines
9 KiB
YAML
name: Release
|
|
|
|
# Tagged releases for the (public, GPLv3) re-sputnik repo.
|
|
#
|
|
# Flow: push a tag `vX.Y.Z` →
|
|
# 1. test — unit tests + compile check, and verify the tag matches __version__
|
|
# 2. build — PyInstaller build matrix (Windows x64, macOS arm64, Linux x86_64 +
|
|
# aarch64); each job uploads its packaged asset as a CI artifact
|
|
# 3. release— gather every asset, pull the matching CHANGELOG section as notes,
|
|
# and publish ONE GitHub Release in THIS repo (built-in GITHUB_TOKEN,
|
|
# no PAT / no separate showcase repo).
|
|
#
|
|
# build.yml stays for on-demand (workflow_dispatch) CI artifact builds; this file
|
|
# owns tag releases, so there is no double-publish.
|
|
#
|
|
# Builds are UNSIGNED: Windows shows SmartScreen; macOS Gatekeeper quarantines the
|
|
# .app (right-click → Open, or `xattr -dr com.apple.quarantine`). Add signing in the
|
|
# marked TODO blocks + repo secrets when ready.
|
|
|
|
on:
|
|
push:
|
|
tags: ["v*"]
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: "Release tag to build AND publish (e.g. v0.1.0). Leave empty = build only."
|
|
required: false
|
|
type: string
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
# Fast gate: tests must pass and (on a tag) the tag must equal __version__ before
|
|
# the expensive 4-platform build runs.
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Verify release tag matches __version__
|
|
if: startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && inputs.tag != '')
|
|
shell: bash
|
|
env:
|
|
REL_TAG: ${{ inputs.tag || github.ref_name }}
|
|
run: |
|
|
file_ver=$(grep -oE '__version__[[:space:]]*=[[:space:]]*"[^"]+"' src/re_sputnik/__init__.py | grep -oE '[0-9][^"]*')
|
|
tag_ver="${REL_TAG#v}"
|
|
echo "file=$file_ver tag=$tag_ver"
|
|
if [ "$file_ver" != "$tag_ver" ]; then
|
|
echo "::error::Tag '${REL_TAG}' != __version__ '${file_ver}'. Bump src/re_sputnik/__init__.py or fix the tag."
|
|
exit 1
|
|
fi
|
|
|
|
- uses: actions/setup-python@v6
|
|
with:
|
|
python-version: "3.13"
|
|
- name: Install (with dev deps)
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
pip install ".[dev]"
|
|
- name: Compile check
|
|
run: python -m compileall -q src
|
|
- name: Run tests
|
|
run: pytest -q
|
|
|
|
build:
|
|
name: ${{ matrix.label }}
|
|
needs: test
|
|
runs-on: ${{ matrix.os }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- os: windows-latest
|
|
label: Windows x64
|
|
asset: Re-Sputnik-windows-x64.exe
|
|
- os: macos-14 # Apple Silicon (arm64)
|
|
label: macOS arm64
|
|
asset: Re-Sputnik-macos-arm64.zip
|
|
- os: ubuntu-22.04 # old glibc on purpose → AppImage runs on older distros
|
|
label: Linux x86_64
|
|
asset: Re-Sputnik-linux-x86_64.AppImage
|
|
apparch: x86_64
|
|
- os: ubuntu-24.04-arm # GitHub-hosted arm64 runner
|
|
label: Linux aarch64
|
|
asset: Re-Sputnik-linux-aarch64.AppImage
|
|
apparch: aarch64
|
|
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: "3.13"
|
|
|
|
- name: Install Linux build deps (Tk runtime + FUSE for AppImage)
|
|
if: runner.os == 'Linux'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y python3-tk libfuse2
|
|
python -c "import tkinter; print('tkinter OK', tkinter.TkVersion)"
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
pip install ".[build]"
|
|
|
|
- name: Bake icons (SVG → PNG)
|
|
continue-on-error: true # committed PNGs are the fallback if this can't run
|
|
run: |
|
|
python scripts/render_icons.py
|
|
python scripts/gen_icons.py
|
|
|
|
- name: Generate .icns (macOS app icon)
|
|
if: runner.os == 'macOS'
|
|
continue-on-error: true
|
|
run: |
|
|
set -e
|
|
ICONDIR=src/re_sputnik/resources/branding
|
|
SRC_PNG="$ICONDIR/icon_256.png"
|
|
if [ -f "$SRC_PNG" ]; then
|
|
mkdir -p icon.iconset
|
|
for s in 16 32 64 128 256 512; do
|
|
sips -z $s $s "$SRC_PNG" --out "icon.iconset/icon_${s}x${s}.png" >/dev/null 2>&1 || true
|
|
d=$((s*2)); sips -z $d $d "$SRC_PNG" --out "icon.iconset/icon_${s}x${s}@2x.png" >/dev/null 2>&1 || true
|
|
done
|
|
iconutil -c icns icon.iconset -o "$ICONDIR/icon.icns" || true
|
|
fi
|
|
|
|
- name: Build with PyInstaller
|
|
run: pyinstaller --clean --noconfirm re_sputnik.spec
|
|
|
|
# ── Windows ──────────────────────────────────────────────────────────────
|
|
# TODO sign: signtool sign /fd sha256 /tr <timestamp-url> /td sha256 \
|
|
# /f cert.pfx /p ${{ secrets.WINDOWS_CERT_PW }} dist\Re-Sputnik.exe
|
|
- name: Package (Windows → .exe)
|
|
if: runner.os == 'Windows'
|
|
shell: bash
|
|
run: cp "dist/Re-Sputnik.exe" "${{ matrix.asset }}"
|
|
|
|
# ── macOS ────────────────────────────────────────────────────────────────
|
|
# Ship the .app zipped with ditto (preserves the bundle's structure/symlinks/
|
|
# permissions — plain `zip` can corrupt it). A .dmg with an Applications
|
|
# symlink + background is a nicer "later" once the app is signed/notarized
|
|
# (codesign --options runtime + notarytool + stapler; needs APPLE_* secrets).
|
|
- name: Package (macOS → zipped .app)
|
|
if: runner.os == 'macOS'
|
|
run: |
|
|
ditto -c -k --keepParent "dist/Re-Sputnik.app" "${{ matrix.asset }}"
|
|
|
|
# ── Linux ────────────────────────────────────────────────────────────────
|
|
- name: Package (Linux → AppImage)
|
|
if: runner.os == 'Linux'
|
|
run: |
|
|
set -e
|
|
APP=Re-Sputnik
|
|
mkdir -p AppDir/usr/bin
|
|
cp "dist/$APP" "AppDir/usr/bin/$APP"
|
|
chmod +x "AppDir/usr/bin/$APP"
|
|
ICON=src/re_sputnik/resources/branding/icon_256.png
|
|
if [ -f "$ICON" ]; then cp "$ICON" "AppDir/$APP.png"; else : > "AppDir/$APP.png"; fi
|
|
printf '[Desktop Entry]\nType=Application\nName=Re:Sputnik\nExec=%s\nIcon=%s\nCategories=Network;Utility;\nTerminal=false\n' "$APP" "$APP" > "AppDir/$APP.desktop"
|
|
printf '#!/bin/sh\nHERE="$(dirname "$(readlink -f "$0")")"\nexec "$HERE/usr/bin/%s" "$@"\n' "$APP" > AppDir/AppRun
|
|
chmod +x AppDir/AppRun
|
|
wget -q "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${{ matrix.apparch }}.AppImage" -O appimagetool
|
|
chmod +x appimagetool
|
|
ARCH=${{ matrix.apparch }} ./appimagetool --appimage-extract-and-run AppDir "${{ matrix.asset }}"
|
|
|
|
- name: Upload asset as CI artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: ${{ matrix.asset }}
|
|
path: ${{ matrix.asset }}
|
|
if-no-files-found: error
|
|
|
|
# One Release in THIS repo, with every platform's asset attached. Runs on a tag
|
|
# push, or on a manual dispatch that provides a `tag` input.
|
|
release:
|
|
needs: build
|
|
if: startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && inputs.tag != '')
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write # GITHUB_TOKEN creates the Release in this repo
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Download all build assets
|
|
uses: actions/download-artifact@v7
|
|
with:
|
|
path: _assets # each artifact lands in _assets/<name>/<file>
|
|
|
|
- name: Extract release notes from CHANGELOG
|
|
shell: bash
|
|
env:
|
|
REL_TAG: ${{ inputs.tag || github.ref_name }}
|
|
run: |
|
|
ver="${REL_TAG#v}"
|
|
# index()-based, no regex — avoids awk treating "[ver]" as a char class.
|
|
awk -v marker="## [$ver]" '
|
|
index($0, marker)==1 {f=1; next}
|
|
f && index($0, "## [")==1 {exit}
|
|
f {print}
|
|
' CHANGELOG.md > RELEASE_NOTES.md
|
|
if [ ! -s RELEASE_NOTES.md ]; then
|
|
printf 'Re:Sputnik %s\n' "$REL_TAG" > RELEASE_NOTES.md
|
|
fi
|
|
echo "----- notes -----"; cat RELEASE_NOTES.md
|
|
|
|
- name: Publish GitHub Release
|
|
uses: softprops/action-gh-release@v3
|
|
with:
|
|
tag_name: ${{ inputs.tag || github.ref_name }}
|
|
name: Re:Sputnik ${{ inputs.tag || github.ref_name }}
|
|
body_path: RELEASE_NOTES.md
|
|
prerelease: ${{ contains(inputs.tag || github.ref_name, '-') }} # v1.2.3-beta → prerelease
|
|
files: _assets/**/*
|
|
fail_on_unmatched_files: true
|