Skip to content

Commit 66d8f4c

Browse files
committed
feat: Auto-updater, NSIS installer, and CI/CD pipeline
1 parent cd2ee87 commit 66d8f4c

15 files changed

Lines changed: 660 additions & 69 deletions

File tree

.github/workflows/release.yml

Lines changed: 218 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ on:
44
push:
55
tags:
66
- 'v*'
7-
release:
8-
types: [created]
97

108
permissions:
119
contents: write
@@ -15,112 +13,276 @@ jobs:
1513
runs-on: ${{ matrix.os }}
1614
strategy:
1715
matrix:
18-
os: [ubuntu-latest, windows-latest]
19-
python-version: ['3.13']
16+
include:
17+
- os: windows-latest
18+
target: x86_64-pc-windows-msvc
19+
sidecar-ext: .exe
20+
- os: ubuntu-22.04
21+
target: x86_64-unknown-linux-gnu
22+
sidecar-ext: ''
2023

2124
steps:
2225
- uses: actions/checkout@v4
2326

2427
- name: Set up Python
25-
uses: actions/setup-python@v4
28+
uses: actions/setup-python@v5
2629
with:
27-
python-version: ${{ matrix.python-version }}
30+
python-version: '3.13'
2831

2932
- name: Install Node.js
3033
uses: actions/setup-node@v4
3134
with:
32-
node-version: '20'
35+
node-version: '22'
36+
37+
- name: Install Rust
38+
uses: dtolnay/rust-toolchain@stable
3339

34-
- name: Install dependencies (Linux)
35-
if: matrix.os == 'ubuntu-latest'
40+
- name: Rust cache
41+
uses: Swatinem/rust-cache@v2
42+
with:
43+
workspaces: frontend/src-tauri
44+
45+
- name: Install system dependencies (Linux)
46+
if: runner.os == 'Linux'
3647
run: |
3748
sudo apt-get update
38-
sudo apt-get install -y portaudio19-dev python3-dev
49+
sudo apt-get install -y \
50+
libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev \
51+
patchelf portaudio19-dev
3952
4053
- name: Install Python dependencies
4154
run: |
4255
python -m pip install --upgrade pip
4356
pip install pyinstaller
44-
# Install project dependencies directly from pyproject.toml without building the package
4557
python -c "
4658
import tomllib
4759
with open('pyproject.toml', 'rb') as f:
4860
data = tomllib.load(f)
4961
deps = data['project']['dependencies']
50-
print('Installing dependencies:', deps)
5162
import subprocess
5263
subprocess.run(['pip', 'install'] + deps, check=True)
5364
"
54-
# Install certifi for SSL certificate support
5565
pip install certifi
5666
57-
- name: Install frontend dependencies
58-
run: |
59-
cd frontend
60-
npm install
61-
cd ..
67+
- name: Build sidecar binary
68+
run: python scripts/build_sidecar.py
6269

63-
- name: Build frontend
64-
run: |
65-
cd frontend
66-
npm run build
67-
cd ..
70+
- name: Install frontend dependencies
71+
working-directory: frontend
72+
run: npm install
6873

69-
- name: Build executable
70-
run: pyinstaller echo.spec --clean --noconfirm
74+
- name: Build Tauri app
75+
working-directory: frontend
76+
env:
77+
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
78+
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
79+
run: npx tauri build
7180

72-
- name: Create portable package (Linux)
73-
if: matrix.os == 'ubuntu-latest'
81+
- name: Collect artifacts (Windows)
82+
if: runner.os == 'Windows'
83+
shell: bash
7484
run: |
75-
mkdir -p dist/echo-portable
76-
cp dist/echo dist/echo-portable/
77-
chmod +x dist/echo dist/echo-portable/echo
78-
cp README.md dist/echo-portable/ 2>/dev/null || true
85+
mkdir -p artifacts
86+
cp frontend/src-tauri/target/release/bundle/nsis/*.exe artifacts/ || true
87+
cp frontend/src-tauri/target/release/bundle/nsis/*.nsis.zip artifacts/ || true
88+
cp frontend/src-tauri/target/release/bundle/nsis/*.nsis.zip.sig artifacts/ || true
7989
80-
- name: Create portable package (Windows)
81-
if: matrix.os == 'windows-latest'
90+
- name: Collect artifacts (Linux)
91+
if: runner.os == 'Linux'
8292
run: |
83-
mkdir -p dist/echo-portable
84-
copy dist\echo.exe dist\echo-portable\
85-
copy README.md dist\echo-portable\ 2>nul || echo README.md not found
93+
mkdir -p artifacts
94+
cp frontend/src-tauri/target/release/bundle/appimage/*.AppImage artifacts/ || true
95+
cp frontend/src-tauri/target/release/bundle/appimage/*.AppImage.tar.gz artifacts/ || true
96+
cp frontend/src-tauri/target/release/bundle/appimage/*.AppImage.tar.gz.sig artifacts/ || true
97+
cp frontend/src-tauri/target/release/bundle/deb/*.deb artifacts/ || true
8698
8799
- name: Upload artifacts
88100
uses: actions/upload-artifact@v4
89101
with:
90-
name: build-${{ matrix.os }}
91-
path: |
92-
dist/echo*
93-
dist/echo-portable/**
94-
retention-days: 1
102+
name: build-${{ matrix.target }}
103+
path: artifacts/*
104+
retention-days: 3
95105

96106
release:
97107
needs: build
98108
runs-on: ubuntu-latest
99-
if: always()
100109

101110
steps:
111+
- uses: actions/checkout@v4
112+
102113
- name: Download all artifacts
103114
uses: actions/download-artifact@v4
115+
with:
116+
path: artifacts
117+
merge-multiple: true
118+
119+
- name: List artifacts
120+
run: ls -la artifacts/
104121

105-
- name: Clean up existing release (only if triggered by tag)
106-
if: github.event_name == 'push'
122+
- name: Get version from tag
123+
id: version
124+
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
125+
126+
- name: Generate latest.json
107127
run: |
108-
gh release delete ${{ github.ref_name }} --yes || echo "No existing release to delete"
109-
env:
110-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
128+
VERSION="${{ steps.version.outputs.version }}"
129+
TAG="${GITHUB_REF_NAME}"
130+
131+
# Read signatures
132+
WIN_SIG=""
133+
LINUX_SIG=""
134+
if [ -f "artifacts/Echo_${VERSION}_x64-setup.nsis.zip.sig" ]; then
135+
WIN_SIG=$(cat "artifacts/Echo_${VERSION}_x64-setup.nsis.zip.sig")
136+
fi
137+
if ls artifacts/*_amd64.AppImage.tar.gz.sig 1>/dev/null 2>&1; then
138+
LINUX_SIG=$(cat artifacts/*_amd64.AppImage.tar.gz.sig)
139+
fi
140+
141+
# Build platforms JSON
142+
PLATFORMS="{}"
143+
if [ -n "$WIN_SIG" ]; then
144+
PLATFORMS=$(echo "$PLATFORMS" | jq \
145+
--arg sig "$WIN_SIG" \
146+
--arg url "https://gitee.com/KaUpane/echo/releases/download/${TAG}/Echo_${VERSION}_x64-setup.nsis.zip" \
147+
'. + {"windows-x86_64": {"signature": $sig, "url": $url}}')
148+
fi
149+
if [ -n "$LINUX_SIG" ]; then
150+
APPIMAGE_NAME=$(basename artifacts/*_amd64.AppImage.tar.gz .sig 2>/dev/null | head -1)
151+
PLATFORMS=$(echo "$PLATFORMS" | jq \
152+
--arg sig "$LINUX_SIG" \
153+
--arg url "https://gitee.com/KaUpane/echo/releases/download/${TAG}/${APPIMAGE_NAME}" \
154+
'. + {"linux-x86_64": {"signature": $sig, "url": $url}}')
155+
fi
156+
157+
# Write latest.json
158+
jq -n \
159+
--arg version "$VERSION" \
160+
--arg pub_date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
161+
--argjson platforms "$PLATFORMS" \
162+
'{version: $version, pub_date: $pub_date, platforms: $platforms}' \
163+
> artifacts/latest.json
164+
165+
echo "Generated latest.json:"
166+
cat artifacts/latest.json
111167
112-
- name: Create/Update Release
113-
uses: softprops/action-gh-release@v1
168+
- name: Create GitHub Release
169+
uses: softprops/action-gh-release@v2
114170
with:
115-
files: |
116-
build-ubuntu-latest/*
117-
build-windows-latest/*
171+
files: artifacts/*
118172
draft: false
119173
prerelease: false
120-
generate_release_notes: ${{ github.event_name == 'push' }} # Only generate notes if triggered by tag push
174+
generate_release_notes: true
121175
tag_name: ${{ github.ref_name }}
122176
name: Echo ${{ github.ref_name }}
123-
# If release was created via gh release, preserve its notes and update with binaries
124-
append_body: ${{ github.event_name == 'release' }}
125177
env:
126-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
178+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
179+
180+
- name: Publish to Gitee Release
181+
env:
182+
GITEE_TOKEN: ${{ secrets.GITEE_PERSONAL_ACCESS_TOKEN }}
183+
TAG: ${{ github.ref_name }}
184+
VERSION: ${{ steps.version.outputs.version }}
185+
run: |
186+
OWNER="KaUpane"
187+
REPO="echo"
188+
189+
# Delete existing release for this tag (if re-running)
190+
EXISTING=$(curl -s "https://gitee.com/api/v5/repos/${OWNER}/${REPO}/releases/tags/${TAG}?access_token=${GITEE_TOKEN}")
191+
EXISTING_ID=$(echo "$EXISTING" | jq -r '.id // empty')
192+
if [ -n "$EXISTING_ID" ]; then
193+
echo "Deleting existing Gitee release ${EXISTING_ID}..."
194+
curl -s -X DELETE "https://gitee.com/api/v5/repos/${OWNER}/${REPO}/releases/${EXISTING_ID}?access_token=${GITEE_TOKEN}"
195+
fi
196+
197+
# Create release
198+
echo "Creating Gitee release for ${TAG}..."
199+
RELEASE_RESPONSE=$(curl -s -X POST "https://gitee.com/api/v5/repos/${OWNER}/${REPO}/releases" \
200+
-H "Content-Type: application/json" \
201+
-d "{
202+
\"access_token\": \"${GITEE_TOKEN}\",
203+
\"tag_name\": \"${TAG}\",
204+
\"name\": \"Echo ${TAG}\",
205+
\"body\": \"Automated release for ${TAG}. Download the installer for your platform below.\",
206+
\"prerelease\": false
207+
}")
208+
209+
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
210+
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
211+
echo "Failed to create Gitee release!"
212+
echo "$RELEASE_RESPONSE"
213+
exit 1
214+
fi
215+
echo "Created Gitee release ID: ${RELEASE_ID}"
216+
217+
# Upload each artifact
218+
for FILE in artifacts/*; do
219+
FILENAME=$(basename "$FILE")
220+
echo "Uploading ${FILENAME} to Gitee..."
221+
curl -s -X POST "https://gitee.com/api/v5/repos/${OWNER}/${REPO}/releases/${RELEASE_ID}/attach_files" \
222+
-H "Content-Type: multipart/form-data" \
223+
-F "access_token=${GITEE_TOKEN}" \
224+
-F "file=@${FILE}" || echo "Warning: failed to upload ${FILENAME}"
225+
done
226+
227+
echo "Gitee release published successfully!"
228+
229+
- name: Update Gitee 'latest' release
230+
env:
231+
GITEE_TOKEN: ${{ secrets.GITEE_PERSONAL_ACCESS_TOKEN }}
232+
TAG: ${{ github.ref_name }}
233+
VERSION: ${{ steps.version.outputs.version }}
234+
run: |
235+
OWNER="KaUpane"
236+
REPO="echo"
237+
238+
# Delete existing 'latest' release
239+
EXISTING=$(curl -s "https://gitee.com/api/v5/repos/${OWNER}/${REPO}/releases/tags/latest?access_token=${GITEE_TOKEN}")
240+
EXISTING_ID=$(echo "$EXISTING" | jq -r '.id // empty')
241+
if [ -n "$EXISTING_ID" ]; then
242+
echo "Deleting existing Gitee 'latest' release..."
243+
curl -s -X DELETE "https://gitee.com/api/v5/repos/${OWNER}/${REPO}/releases/${EXISTING_ID}?access_token=${GITEE_TOKEN}"
244+
fi
245+
246+
# Delete the 'latest' tag if it exists (via API)
247+
curl -s -X DELETE "https://gitee.com/api/v5/repos/${OWNER}/${REPO}/git/refs/tags/latest?access_token=${GITEE_TOKEN}" || true
248+
249+
# Create 'latest' tag pointing to the same commit
250+
COMMIT_SHA=$(git rev-parse HEAD)
251+
curl -s -X POST "https://gitee.com/api/v5/repos/${OWNER}/${REPO}/git/refs" \
252+
-H "Content-Type: application/json" \
253+
-d "{
254+
\"access_token\": \"${GITEE_TOKEN}\",
255+
\"ref\": \"refs/tags/latest\",
256+
\"sha\": \"${COMMIT_SHA}\"
257+
}" || true
258+
259+
# Create 'latest' release with update artifacts
260+
RELEASE_RESPONSE=$(curl -s -X POST "https://gitee.com/api/v5/repos/${OWNER}/${REPO}/releases" \
261+
-H "Content-Type: application/json" \
262+
-d "{
263+
\"access_token\": \"${GITEE_TOKEN}\",
264+
\"tag_name\": \"latest\",
265+
\"name\": \"Latest (${TAG})\",
266+
\"body\": \"This release always points to the latest version. Current: ${TAG}\",
267+
\"prerelease\": false
268+
}")
269+
270+
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
271+
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
272+
echo "Warning: Failed to create Gitee 'latest' release"
273+
echo "$RELEASE_RESPONSE"
274+
exit 0
275+
fi
276+
277+
# Upload latest.json and update bundles to 'latest' release
278+
for FILE in artifacts/*.nsis.zip artifacts/*.AppImage.tar.gz artifacts/latest.json; do
279+
[ -f "$FILE" ] || continue
280+
FILENAME=$(basename "$FILE")
281+
echo "Uploading ${FILENAME} to Gitee 'latest' release..."
282+
curl -s -X POST "https://gitee.com/api/v5/repos/${OWNER}/${REPO}/releases/${RELEASE_ID}/attach_files" \
283+
-H "Content-Type: multipart/form-data" \
284+
-F "access_token=${GITEE_TOKEN}" \
285+
-F "file=@${FILE}" || echo "Warning: failed to upload ${FILENAME}"
286+
done
287+
288+
echo "Gitee 'latest' release updated successfully!"

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ config.yaml
7070
!config.yaml.example
7171
completed_exams.json
7272

73+
# Signing keys
74+
keys/
75+
7376
# Tauri
7477
frontend/src-tauri/target/
7578
frontend/src-tauri/binaries/

frontend/package-lock.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
},
1111
"dependencies": {
1212
"@tauri-apps/api": "^2.10.1",
13+
"@tauri-apps/plugin-process": "^2.3.1",
14+
"@tauri-apps/plugin-updater": "^2.10.0",
1315
"lamejsfix": "^1.0.1",
1416
"pinia": "^2.1.0",
1517
"vue": "^3.4.0"

0 commit comments

Comments
 (0)