Skip to content

Commit d124d93

Browse files
committed
feat: config github actions
1 parent 8e4c1b3 commit d124d93

2 files changed

Lines changed: 252 additions & 0 deletions

File tree

.github/workflows/package.yml

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
name: Cross-platform Release
2+
3+
# Display the Tag Name (e.g., "v1.0.0") as the workflow run title
4+
run-name: Release ${{ github.ref_name }}
5+
6+
permissions:
7+
contents: write
8+
9+
on:
10+
push:
11+
tags:
12+
- 'v*'
13+
workflow_dispatch:
14+
15+
jobs:
16+
# ==================================================
17+
# JOB 1: Build Binaries
18+
# ==================================================
19+
build:
20+
name: Build ${{ matrix.tag }}
21+
runs-on: ${{ matrix.runner }}
22+
strategy:
23+
fail-fast: false
24+
matrix:
25+
include:
26+
# Windows (Standard x64)
27+
- runner: windows-latest
28+
os: windows
29+
python: "3.10"
30+
arch: x64
31+
tag: windows-x64
32+
33+
# macOS (Apple Silicon M1/M2/M3) - Mainstream
34+
- runner: macos-latest
35+
os: macos
36+
python: "3.10"
37+
arch: arm64
38+
tag: macos-arm64
39+
40+
# Linux (Standard x64 Server/Desktop)
41+
- runner: ubuntu-latest
42+
os: ubuntu
43+
python: "3.10"
44+
arch: x64
45+
tag: linux-x64
46+
47+
# Linux (ARM64 for Raspberry Pi / Oracle Cloud)
48+
# Note: Uses GitHub's new ARM runner
49+
- runner: ubuntu-24.04-arm
50+
os: ubuntu
51+
python: "3.10"
52+
arch: arm64
53+
tag: linux-arm64
54+
55+
steps:
56+
- name: Checkout repository
57+
uses: actions/checkout@v4
58+
59+
# Resolve Version ID
60+
- name: Resolve Version
61+
id: version
62+
shell: bash
63+
run: |
64+
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
65+
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
66+
else
67+
echo "VERSION=nightly-${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
68+
fi
69+
70+
- name: Set up Python
71+
uses: actions/setup-python@v5
72+
with:
73+
python-version: ${{ matrix.python }}
74+
architecture: ${{ matrix.arch }}
75+
76+
- name: Install dependencies
77+
run: |
78+
python -m pip install --upgrade pip
79+
pip install -r requirements.txt pyinstaller
80+
81+
# Build Command
82+
- name: Build with PyInstaller
83+
shell: bash
84+
run: |
85+
pyinstaller --clean --onefile \
86+
--collect-all rich \
87+
--name "mailbot-${{ matrix.tag }}" main.py
88+
89+
# Bundle binary and create default config
90+
- name: Bundle binary and config
91+
shell: bash
92+
run: |
93+
set -euo pipefail
94+
VERSION="${{ steps.version.outputs.VERSION }}"
95+
BUNDLE="mailbot-${VERSION}-${{ matrix.tag }}"
96+
97+
# Locate the binary (handles .exe on Windows automatically)
98+
BIN_SRC=$(ls dist/mailbot-* | head -n 1)
99+
100+
mkdir -p bundle/$BUNDLE
101+
cp "$BIN_SRC" "bundle/$BUNDLE/"
102+
103+
# Create default config.json
104+
cat > "bundle/$BUNDLE/config.json" <<'EOF'
105+
{
106+
"poll_interval": 60,
107+
"max_retries": 3,
108+
"log_level": "INFO",
109+
"accounts": [],
110+
"notifiers": []
111+
}
112+
EOF
113+
114+
# Zip the bundle using Python (cross-platform way)
115+
python - <<'PY'
116+
import shutil
117+
from pathlib import Path
118+
119+
bundle_root = Path("bundle")
120+
for pkg_dir in bundle_root.iterdir():
121+
if pkg_dir.is_dir():
122+
shutil.make_archive(str(pkg_dir), "zip", root_dir=pkg_dir)
123+
PY
124+
125+
- name: Upload Artifact
126+
uses: actions/upload-artifact@v4
127+
with:
128+
name: mailbot-${{ steps.version.outputs.VERSION }}-${{ matrix.tag }}
129+
path: bundle/*.zip
130+
if-no-files-found: error
131+
132+
# ==================================================
133+
# JOB 2: Publish Release
134+
# ==================================================
135+
release:
136+
name: Publish Release
137+
needs: build
138+
runs-on: ubuntu-latest
139+
if: startsWith(github.ref, 'refs/tags/')
140+
141+
steps:
142+
- name: Download All Artifacts
143+
uses: actions/download-artifact@v4
144+
with:
145+
pattern: mailbot-*
146+
path: release_assets
147+
merge-multiple: true
148+
149+
- name: Create Release
150+
uses: softprops/action-gh-release@v2
151+
with:
152+
name: MailBot ${{ github.ref_name }} # Title of the Release page
153+
files: release_assets/*
154+
draft: true
155+
generate_release_notes: true
156+
env:
157+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

scripts/package.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""Package MailBot with PyInstaller and prepare release archives."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import json
7+
import platform
8+
import shutil
9+
import subprocess
10+
import sys
11+
from pathlib import Path
12+
13+
14+
def main() -> int:
15+
parser = argparse.ArgumentParser(description="Build a release bundle for MailBot")
16+
parser.add_argument("--entry", default="main.py", help="Entry-point script")
17+
parser.add_argument("--name", default="MailBot", help="Executable base name")
18+
parser.add_argument("--variant", default="linux-x64", help="Platform tag, e.g. macos-arm64")
19+
parser.add_argument("--tag", default="", help="Release tag, e.g. v1.9.1 (optional)")
20+
parser.add_argument(
21+
"--clean",
22+
action="store_true",
23+
help="Remove build artifacts before packaging",
24+
)
25+
args = parser.parse_args()
26+
27+
system = platform.system()
28+
machine = platform.machine() or "unknown"
29+
30+
variant = args.variant or f"{system}-{machine}"
31+
tag = args.tag.strip()
32+
33+
dist_root = Path("dist")
34+
dist_target = dist_root / variant
35+
build_root = Path("build") / variant
36+
37+
if args.clean:
38+
shutil.rmtree(dist_root, ignore_errors=True)
39+
shutil.rmtree(build_root, ignore_errors=True)
40+
41+
pyinstaller_cmd = [
42+
sys.executable,
43+
"-m",
44+
"PyInstaller",
45+
"--name",
46+
args.name,
47+
"--onefile",
48+
"--distpath",
49+
str(dist_target),
50+
"--workpath",
51+
str(build_root),
52+
"--specpath",
53+
str(build_root),
54+
args.entry,
55+
]
56+
57+
print("Running PyInstaller:", "\"" + " ".join(pyinstaller_cmd) + "\"")
58+
result = subprocess.run(pyinstaller_cmd, check=False)
59+
if result.returncode != 0:
60+
print("PyInstaller failed.")
61+
return result.returncode
62+
63+
if not dist_target.exists():
64+
print(f"Expected dist directory does not exist: {dist_target}")
65+
return 1
66+
67+
# Ensure a default config.json is shipped so the binary can start with sane defaults
68+
_ensure_default_config(dist_target)
69+
70+
archive_name = f"{tag}-{variant}" if tag else f"{args.name}-{variant}"
71+
archive_base = dist_root / archive_name
72+
shutil.make_archive(str(archive_base), "zip", root_dir=dist_target)
73+
74+
print("Release bundle created:", f"{archive_base}.zip")
75+
return 0
76+
77+
78+
if __name__ == "__main__":
79+
raise SystemExit(main())
80+
81+
82+
def _ensure_default_config(dist_target: Path) -> None:
83+
"""Write a minimal config.json into the dist folder if absent."""
84+
dist_target.mkdir(parents=True, exist_ok=True)
85+
cfg_path = dist_target / "config.json"
86+
if cfg_path.exists():
87+
return
88+
default_cfg = {
89+
"poll_interval": 60,
90+
"max_retries": 3,
91+
"log_level": "INFO",
92+
"accounts": [],
93+
"notifiers": [],
94+
}
95+
cfg_path.write_text(json.dumps(default_cfg, indent=2), encoding="utf-8")

0 commit comments

Comments
 (0)