Skip to content

fix(naga): 新版NagaAgent回调端口与逻辑 (#51) #71

fix(naga): 新版NagaAgent回调端口与逻辑 (#51)

fix(naga): 新版NagaAgent回调端口与逻辑 (#51) #71

Workflow file for this run

name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
id-token: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
env:
PYTHON_VERSION: "3.12"
NODE_VERSION: "22"
APP_DIR: apps/undefined-console
jobs:
verify-python:
name: Verify Python package
runs-on: ubuntu-latest
environment: release
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: uv.lock
- name: Install Python dependencies
run: uv sync --group ci -p ${{ env.PYTHON_VERSION }}
- name: Validate release tag matches package version
env:
TAG_NAME: ${{ github.ref_name }}
run: |
uv run python - <<'PY'
import os
import pathlib
import re
import sys
import tomllib
tag_name = os.environ["TAG_NAME"]
expected_version = tag_name.removeprefix("v")
pyproject = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))
project_version = str(pyproject["project"]["version"]).strip()
init_text = pathlib.Path("src/Undefined/__init__.py").read_text(encoding="utf-8")
match = re.search(r'__version__\s*=\s*"([^"]+)"', init_text)
if match is None:
raise SystemExit("Could not find __version__ in src/Undefined/__init__.py")
init_version = match.group(1).strip()
errors: list[str] = []
if project_version != init_version:
errors.append(
f"Version mismatch: pyproject.toml={project_version}, src/Undefined/__init__.py={init_version}"
)
if project_version != expected_version:
errors.append(
f"Tag/version mismatch: tag={tag_name}, expected package version={expected_version}, actual={project_version}"
)
if errors:
raise SystemExit("\n".join(errors))
print(
f"Validated release version {project_version} from tag {tag_name}, pyproject.toml, and src/Undefined/__init__.py"
)
PY
- name: Cache Ruff
uses: actions/cache@v4
with:
path: .ruff_cache
key: ${{ runner.os }}-ruff-${{ hashFiles('**/uv.lock') }}
restore-keys: |
${{ runner.os }}-ruff-
- name: Run Ruff
run: |
uv run ruff check .
uv run ruff format --check .
- name: Cache Mypy
uses: actions/cache@v4
with:
path: .mypy_cache
key: ${{ runner.os }}-mypy-${{ hashFiles('**/uv.lock') }}
restore-keys: |
${{ runner.os }}-mypy-
- name: Run Mypy
run: uv run mypy .
- name: Run tests
run: uv run pytest tests/
- name: Build Python distributions
run: uv build
- name: Verify wheel contains packaged resources
run: |
uv run python -c "import glob, zipfile; wheels=glob.glob('dist/*.whl'); assert wheels, 'no wheel built'; z=zipfile.ZipFile(wheels[0]); names=set(z.namelist()); required={'config.toml.example','res/prompts/undefined.xml','img/xlwy.jpg'}; missing=sorted([p for p in required if p not in names]); assert not missing, f'missing in wheel: {missing}'"
- name: Upload Python distributions
uses: actions/upload-artifact@v4
with:
name: python-dist
path: dist/*
if-no-files-found: error
verify-console:
name: Verify console app
runs-on: ubuntu-latest
environment: release
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache npm and node_modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-undefined-console-npm-${{ hashFiles('apps/undefined-console/package-lock.json', 'apps/undefined-console/package.json', 'apps/undefined-console/biome.json') }}
restore-keys: |
${{ runner.os }}-undefined-console-npm-
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo registry and target
uses: Swatinem/rust-cache@v2
with:
workspaces: |
apps/undefined-console/src-tauri -> target
- name: Install Linux system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf
- name: Install app dependencies
working-directory: ${{ env.APP_DIR }}
run: npm ci
- name: Run console checks
working-directory: ${{ env.APP_DIR }}
run: npm run check
build-tauri-desktop:
name: Build Tauri desktop (${{ matrix.label }})
runs-on: ${{ matrix.os }}
environment: release
needs:
- verify-python
- verify-console
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
label: linux-x64
bundles: appimage,deb
- os: windows-latest
label: windows-x64
bundles: nsis,msi
- os: macos-15
label: macos-x64
bundles: dmg
rust-target: x86_64-apple-darwin
- os: macos-15
label: macos-arm64
bundles: dmg
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache npm and node_modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-undefined-console-npm-${{ hashFiles('apps/undefined-console/package-lock.json', 'apps/undefined-console/package.json', 'apps/undefined-console/biome.json') }}
restore-keys: |
${{ runner.os }}-undefined-console-npm-
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.rust-target }}
- name: Cache cargo registry and target
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.label }}
workspaces: |
apps/undefined-console/src-tauri -> target
- name: Install Linux system dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf
- name: Install app dependencies
working-directory: ${{ env.APP_DIR }}
run: npm ci
- name: Build Tauri bundles
working-directory: ${{ env.APP_DIR }}
shell: bash
run: |
TAURI_ARGS="--ci --bundles ${{ matrix.bundles }}"
if [ -n "${{ matrix.rust-target }}" ]; then
TAURI_ARGS="$TAURI_ARGS --target ${{ matrix.rust-target }}"
fi
if [ "${{ runner.os }}" = "Linux" ]; then
NO_STRIP=true npm run tauri:build -- $TAURI_ARGS
else
npm run tauri:build -- $TAURI_ARGS
fi
- name: Collect release artifacts
shell: bash
env:
TAG: ${{ github.ref_name }}
LABEL: ${{ matrix.label }}
RUST_TARGET: ${{ matrix.rust-target }}
run: |
copy_single_match() {
local search_root="$1"
local pattern="$2"
local destination="$3"
local matches=()
while IFS= read -r line; do
matches+=("$line")
done < <(find "$search_root" -type f -name "$pattern" | sort)
if [ "${#matches[@]}" -ne 1 ]; then
echo "Expected exactly one match for $pattern under $search_root, found ${#matches[@]}" >&2
printf 'Matches:\n%s\n' "${matches[*]}" >&2
exit 1
fi
cp "${matches[0]}" "$destination"
}
if [ -n "$RUST_TARGET" ]; then
BUNDLE_ROOT="$APP_DIR/src-tauri/target/$RUST_TARGET/release/bundle"
else
BUNDLE_ROOT="$APP_DIR/src-tauri/target/release/bundle"
fi
mkdir -p release-artifacts
case "$LABEL" in
linux-x64)
copy_single_match "$BUNDLE_ROOT" '*.AppImage' "release-artifacts/Undefined-Console-${TAG}-${LABEL}.AppImage"
copy_single_match "$BUNDLE_ROOT" '*.deb' "release-artifacts/Undefined-Console-${TAG}-${LABEL}.deb"
;;
windows-x64)
copy_single_match "$BUNDLE_ROOT" '*.exe' "release-artifacts/Undefined-Console-${TAG}-${LABEL}-setup.exe"
copy_single_match "$BUNDLE_ROOT" '*.msi' "release-artifacts/Undefined-Console-${TAG}-${LABEL}.msi"
;;
macos-x64|macos-arm64)
copy_single_match "$BUNDLE_ROOT" '*.dmg' "release-artifacts/Undefined-Console-${TAG}-${LABEL}.dmg"
;;
esac
- name: Upload desktop artifacts
uses: actions/upload-artifact@v4
with:
name: tauri-${{ matrix.label }}
path: release-artifacts/*
if-no-files-found: error
build-tauri-android:
name: Build Tauri Android
runs-on: ubuntu-latest
environment: release
needs:
- verify-python
- verify-console
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache npm and node_modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-undefined-console-npm-${{ hashFiles('apps/undefined-console/package-lock.json', 'apps/undefined-console/package.json', 'apps/undefined-console/biome.json') }}
restore-keys: |
${{ runner.os }}-undefined-console-npm-
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android
- name: Cache cargo registry and target
uses: Swatinem/rust-cache@v2
with:
key: android
workspaces: |
apps/undefined-console/src-tauri -> target
- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Ensure Android platform packages
run: sdkmanager --install "platform-tools" "platforms;android-34" "build-tools;34.0.0"
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Install app dependencies
working-directory: ${{ env.APP_DIR }}
run: npm ci
- name: Initialize Android project for CI
working-directory: ${{ env.APP_DIR }}
run: npm run tauri:android:init
- name: Build Android APK
# If signing secrets are wired into the generated Android project, replace the
# debug fallback with a release build. The scaffold keeps the expectation explicit
# while still publishing an installable APK on every non-iOS release.
working-directory: ${{ env.APP_DIR }}
run: npm run tauri:android:debug -- --ci --apk
- name: Collect Android artifact
shell: bash
env:
TAG: ${{ github.ref_name }}
run: |
copy_single_match() {
local search_root="$1"
local pattern="$2"
local destination="$3"
local matches=()
while IFS= read -r line; do
matches+=("$line")
done < <(find "$search_root" -type f -name "$pattern" | sort)
if [ "${#matches[@]}" -ne 1 ]; then
echo "Expected exactly one match for $pattern under $search_root, found ${#matches[@]}" >&2
printf 'Matches:\n%s\n' "${matches[*]}" >&2
exit 1
fi
cp "${matches[0]}" "$destination"
}
mkdir -p release-artifacts
copy_single_match "$APP_DIR/src-tauri" '*.apk' "release-artifacts/Undefined-Console-${TAG}-android-universal.apk"
- name: Upload Android artifact
uses: actions/upload-artifact@v4
with:
name: tauri-android
path: release-artifacts/*
if-no-files-found: error
publish-release:
name: Publish release assets
runs-on: ubuntu-latest
needs:
- verify-python
- build-tauri-desktop
- build-tauri-android
environment: release
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch tags
run: git fetch --tags --force
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
path: release-downloads
merge-multiple: true
- name: Build release notes
shell: bash
run: |
TAG_NAME="${{ github.ref_name }}"
TAG_TYPE=$(git cat-file -t "$TAG_NAME")
TAG_CONTENT=""
if [ "$TAG_TYPE" = "tag" ]; then
TAG_CONTENT=$(git tag -l --format='%(contents)' "$TAG_NAME")
TAG_CONTENT=$(echo "$TAG_CONTENT" | sed '/-----BEGIN PGP SIGNATURE-----/,/-----END PGP SIGNATURE-----/d')
fi
PREV_TAG=$(git describe --tags --abbrev=0 "${TAG_NAME}^" 2>/dev/null || git rev-list --max-parents=0 HEAD)
if [ -n "$TAG_CONTENT" ]; then
echo "$TAG_CONTENT" > tag_message.txt
printf '\n---\n\n' >> tag_message.txt
else
: > tag_message.txt
fi
cat >> tag_message.txt <<'NOTES'
## 📦 Bundles
- Python: wheel + sdist
- Desktop: Windows (`.exe`, `.msi`), Linux (`.AppImage`, `.deb`), macOS Intel/Apple Silicon (`.dmg`)
- Android: universal `.apk`
## 🔐 Signing expectations
- Windows/Linux artifacts are built directly from CI.
- macOS signing/notarization can be added later via Apple secrets.
- Android currently ships an installable APK on every release; wire signing secrets into the generated Android project to promote it to a signed release build.
## 📝 Detailed Changes
NOTES
FEAT_COMMITS=$(git log ${PREV_TAG}..${TAG_NAME} --grep='^feat' --pretty=format:'* %s (%h)' 2>/dev/null || true)
if [ -n "$FEAT_COMMITS" ]; then
echo -e "\n### 🚀 Features" >> tag_message.txt
echo "$FEAT_COMMITS" >> tag_message.txt
fi
FIX_COMMITS=$(git log ${PREV_TAG}..${TAG_NAME} --grep='^fix' --pretty=format:'* %s (%h)' 2>/dev/null || true)
if [ -n "$FIX_COMMITS" ]; then
echo -e "\n### 🐛 Bug Fixes" >> tag_message.txt
echo "$FIX_COMMITS" >> tag_message.txt
fi
OTHER_COMMITS=$(git log ${PREV_TAG}..${TAG_NAME} --grep='^feat\|^fix' --invert-grep --pretty=format:'* %s (%h)' 2>/dev/null || true)
if [ -n "$OTHER_COMMITS" ]; then
echo -e "\n### 🛠 Maintenance & Others" >> tag_message.txt
echo "$OTHER_COMMITS" >> tag_message.txt
fi
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create ${{ github.ref_name }} release-downloads/* \
--title "Undefined ${{ github.ref_name }}" \
--notes-file tag_message.txt
publish-pypi:
name: Publish Python package to PyPI
runs-on: ubuntu-latest
needs:
- verify-python
- publish-release
environment: release
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: uv.lock
- name: Download Python dist
uses: actions/download-artifact@v4
with:
name: python-dist
path: dist
- name: Verify downloaded distributions
env:
TAG_NAME: ${{ github.ref_name }}
run: |
uv run python - <<'PY'
import os
import pathlib
expected_version = os.environ["TAG_NAME"].removeprefix("v")
dist_dir = pathlib.Path("dist")
files = sorted(path.name for path in dist_dir.glob("*"))
if not files:
raise SystemExit("No Python distributions were downloaded")
bad = [name for name in files if f"-{expected_version}" not in name]
if bad:
raise SystemExit(
"Downloaded distributions do not match release tag version "
f"{expected_version}: {bad}"
)
print("Distributions ready for publish:")
for name in files:
print(f" - {name}")
PY
- name: Publish to PyPI
run: uv publish --check-url https://pypi.org/simple/ dist/*