Skip to content

Commit ca4751a

Browse files
authored
feat(infra): electron app can hot update renderer layer (#1209)
* ci Signed-off-by: Innei <i@innei.in> * feat: hotupdate renderer impl * fix: skip dev Signed-off-by: Innei <i@innei.in> * update logci Signed-off-by: Innei <i@innei.in> * update logci Signed-off-by: Innei <i@innei.in> * feat: tracker Signed-off-by: Innei <tukon479@gmail.com> * log Signed-off-by: Innei <tukon479@gmail.com> * fix: hash Signed-off-by: Innei <tukon479@gmail.com> * fix: build Signed-off-by: Innei <tukon479@gmail.com> * fix: deps Signed-off-by: Innei <tukon479@gmail.com> * Update nightly.yml * update Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * update minimunm Signed-off-by: Innei <tukon479@gmail.com> * calc main hash Signed-off-by: Innei <tukon479@gmail.com> * add main Hash logic Signed-off-by: Innei <tukon479@gmail.com> * remove Signed-off-by: Innei <tukon479@gmail.com> * fix Signed-off-by: Innei <tukon479@gmail.com> * log Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * test Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * fix: update Signed-off-by: Innei <tukon479@gmail.com> * debug Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * fix Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * fix: only one Signed-off-by: Innei <tukon479@gmail.com> * try fix cookie Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * fix cookie Signed-off-by: Innei <tukon479@gmail.com> * fix: domain Signed-off-by: Innei <tukon479@gmail.com> * fix Signed-off-by: Innei <tukon479@gmail.com> * fix: remove test code Signed-off-by: Innei <tukon479@gmail.com> * fix Signed-off-by: Innei <tukon479@gmail.com> --------- Signed-off-by: Innei <i@innei.in> Signed-off-by: Innei <tukon479@gmail.com>
1 parent 59709f0 commit ca4751a

37 files changed

+1025
-95
lines changed

.github/workflows/build-render.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: Build Electron Render
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
env:
9+
VITE_WEB_URL: ${{ vars.VITE_WEB_URL }}
10+
VITE_API_URL: ${{ vars.VITE_API_URL }}
11+
VITE_IMGPROXY_URL: ${{ vars.VITE_IMGPROXY_URL }}
12+
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
13+
VITE_OPENPANEL_CLIENT_ID: ${{ vars.VITE_OPENPANEL_CLIENT_ID }}
14+
VITE_OPENPANEL_API_URL: ${{ vars.VITE_OPENPANEL_API_URL }}
15+
VITE_FIREBASE_CONFIG: ${{ vars.VITE_FIREBASE_CONFIG }}
16+
NODE_OPTIONS: --max-old-space-size=8192
17+
18+
jobs:
19+
build-render:
20+
runs-on: ${{ matrix.os }}
21+
22+
strategy:
23+
fail-fast: false
24+
matrix:
25+
os: [ubuntu-latest]
26+
27+
permissions:
28+
id-token: write
29+
contents: write
30+
attestations: write
31+
32+
steps:
33+
- name: Check out Git repository
34+
uses: actions/checkout@v4
35+
with:
36+
lfs: true
37+
38+
- name: Cache pnpm modules
39+
uses: actions/cache@v4
40+
with:
41+
path: ~/.pnpm-store
42+
key: ${{ matrix.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
43+
restore-keys: |
44+
${{ matrix.os }}-
45+
46+
- name: Setup pnpm
47+
uses: pnpm/action-setup@v4
48+
49+
- name: Use Node.js
50+
uses: actions/setup-node@v4
51+
with:
52+
node-version: 22
53+
cache: "pnpm"
54+
55+
- name: Install dependencies
56+
run: pnpm i
57+
- name: Build
58+
run: pnpm build:render
59+
env:
60+
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
61+
62+
- name: Setup Version
63+
id: version
64+
uses: ./.github/actions/setup-version
65+
66+
- name: Create Release Draft
67+
uses: softprops/action-gh-release@v2
68+
with:
69+
name: v${{ steps.version.outputs.APP_VERSION }}
70+
draft: false
71+
prerelease: true
72+
tag_name: v${{ steps.version.outputs.APP_VERSION }}
73+
files: |
74+
dist/manifest.yml
75+
dist/*.tar.gz

.github/workflows/build.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ jobs:
103103
104104
- name: Install dependencies
105105
run: pnpm i
106-
106+
- name: Update main hash
107+
run: pnpm update:main-hash
107108
- name: Build
108109
if: matrix.os != 'macos-latest'
109110
run: npm exec turbo run //#build

.github/workflows/nightly.yml

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,25 +72,20 @@ jobs:
7272
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
7373
PP_PATH=$RUNNER_TEMP/build_pp.provisionprofile
7474
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
75-
7675
# import certificate and provisioning profile from secrets
7776
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
7877
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
79-
8078
# create temporary keychain
8179
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
8280
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
8381
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
84-
8582
# import certificate to keychain
8683
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
8784
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
8885
security list-keychain -d user -s $KEYCHAIN_PATH
89-
9086
# apply provisioning profile
9187
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
9288
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
93-
9489
- name: Install dependencies
9590
run: pnpm i
9691

@@ -115,6 +110,9 @@ jobs:
115110
fi
116111
echo "Updated version to $NIGHTLY_VERSION"
117112
113+
- name: Update main hash
114+
run: pnpm update:main-hash
115+
118116
- name: Build
119117
if: matrix.os != 'macos-latest'
120118
run: pnpm build
@@ -130,6 +128,10 @@ jobs:
130128
KEYCHAIN_PATH: ${{ runner.temp }}/app-signing.keychain-db
131129
run: pnpm build:macos
132130

131+
- name: Build (Render)
132+
run: pnpm build:render
133+
env:
134+
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
133135
- name: Upload artifacts
134136
uses: actions/upload-artifact@v4
135137
with:
@@ -140,6 +142,9 @@ jobs:
140142
out/make/**/*.AppImage
141143
out/make/**/*.yml
142144
out/make/**/*.dmg
145+
dist/manifest.yml
146+
dist/*.tar.gz
147+
143148
retention-days: 7
144149

145150
- name: Generate artifact attestation
@@ -152,23 +157,28 @@ jobs:
152157
out/make/**/*.exe
153158
out/make/**/*.AppImage
154159
out/make/**/*.yml
160+
dist/manifest.yml
161+
dist/*.tar.gz
155162
156163
- name: Create Nightly Release
157164
uses: softprops/action-gh-release@v2
158165
with:
159166
name: Nightly ${{ env.NIGHTLY_VERSION }}
160167
draft: false
161168
prerelease: true
162-
tag_name: nightly-${{ env.NIGHTLY_VERSION }}
169+
tag_name: ${{ env.NIGHTLY_VERSION }}
163170
files: |
164171
out/make/**/*.dmg
165172
out/make/**/*.zip
166173
out/make/**/*.exe
167174
out/make/**/*.AppImage
168175
out/make/**/*.yml
176+
dist/manifest.yml
177+
dist/*.tar.gz
178+
169179
body: |
170180
This is an automated nightly release for testing purposes.
171-
Version: 0.0.0-nightly.${{ env.NIGHTLY_VERSION }}
181+
Version: ${{ env.NIGHTLY_VERSION }}
172182
173183
**Warning:** This build may be unstable and is not recommended for production use.
174184
env:

apps/main/global.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
import "../../types/vite"
22
import "../../types/authjs"
3+
4+
declare global {
5+
const GIT_COMMIT_HASH: string
6+
}
7+
export {}

apps/main/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,29 @@
2828
"@eneris/push-receiver": "4.3.0",
2929
"@follow/shared": "workspace:*",
3030
"@mozilla/readability": "^0.5.0",
31+
"@openpanel/web": "1.0.1",
3132
"@sentry/electron": "5.7.0",
3233
"builder-util-runtime": "9.2.10",
3334
"electron-context-menu": "4.0.4",
3435
"electron-log": "5.2.2",
3536
"electron-squirrel-startup": "1.0.1",
3637
"electron-updater": "^6.3.9",
38+
"es-toolkit": "1.26.1",
3739
"fast-folder-size": "2.3.0",
3840
"font-list": "1.5.1",
3941
"i18next": "^24.0.0",
42+
"js-yaml": "4.1.0",
4043
"linkedom": "^0.18.5",
4144
"lowdb": "7.0.1",
4245
"msedge-tts": "1.3.4",
46+
"node-machine-id": "1.1.12",
4347
"ofetch": "1.4.1",
4448
"semver": "7.6.3",
49+
"tar": "7.4.3",
4550
"vscode-languagedetection": "npm:@vscode/vscode-languagedetection@^1.0.22"
4651
},
4752
"devDependencies": {
53+
"@types/js-yaml": "4.0.9",
4854
"@types/node": "^22.9.3",
4955
"electron": "33.2.0",
5056
"electron-devtools-installer": "3.2.0",

apps/main/src/constants/app.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
// 5min
1+
import path from "node:path"
2+
3+
import { app } from "electron"
4+
25
export const UNREAD_BACKGROUND_POLLING_INTERVAL = 1000 * 60 * 5
36

7+
export const HOTUPDATE_RENDER_ENTRY_DIR = path.resolve(app.getPath("userData"), "render")
8+
9+
export const GITHUB_OWNER = process.env.GITHUB_OWNER || "RSSNext"
10+
export const GITHUB_REPO = process.env.GITHUB_REPO || "follow"
11+
412
// https://github.com/electron/electron/issues/25081
513
export const START_IN_TRAY_ARGS = "--start-in-tray"

apps/main/src/constants/system.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { machineIdSync } from "node-machine-id"
2+
3+
export const DEVICE_ID = machineIdSync()

apps/main/src/index.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { APP_PROTOCOL } from "@follow/shared/constants"
44
import { env } from "@follow/shared/env"
55
import { imageRefererMatches, selfRefererMatches } from "@follow/shared/image"
66
import { app, BrowserWindow, session } from "electron"
7+
import type { Cookie } from "electron/main"
78
import squirrelStartup from "electron-squirrel-startup"
89

10+
import { DEVICE_ID } from "./constants/system"
911
import { isDev, isMacOS } from "./env"
1012
import { initializeAppStage0, initializeAppStage1 } from "./init"
1113
import { updateProxy } from "./lib/proxy"
@@ -14,6 +16,7 @@ import { store } from "./lib/store"
1416
import { registerAppTray } from "./lib/tray"
1517
import { setAuthSessionToken, updateNotificationsToken } from "./lib/user"
1618
import { registerUpdater } from "./updater"
19+
import { cleanupOldRender } from "./updater/hot-updater"
1720
import {
1821
createMainWindow,
1922
getMainWindow,
@@ -23,6 +26,9 @@ import {
2326

2427
if (isDev) console.info("[main] env loaded:", env)
2528

29+
const apiURL = process.env["VITE_API_URL"] || import.meta.env.VITE_API_URL
30+
31+
console.info("[main] device id:", DEVICE_ID)
2632
if (squirrelStartup) {
2733
app.quit()
2834
}
@@ -56,7 +62,7 @@ function bootstrap() {
5662
// This method will be called when Electron has finished
5763
// initialization and is ready to create browser windows.
5864
// Some APIs can only be used after this event occurs.
59-
app.whenReady().then(() => {
65+
app.whenReady().then(async () => {
6066
// Default open or close DevTools by F12 in development
6167
// and ignore CommandOrControl + R in production.
6268
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
@@ -69,6 +75,28 @@ function bootstrap() {
6975

7076
mainWindow = createMainWindow()
7177

78+
// restore cookies
79+
const cookies = store.get("cookies") as Cookie[]
80+
if (cookies) {
81+
Promise.all(
82+
cookies.map((cookie) => {
83+
const setCookieDetails: Electron.CookiesSetDetails = {
84+
url: apiURL,
85+
name: cookie.name,
86+
value: cookie.value,
87+
domain: cookie.domain,
88+
path: cookie.path,
89+
secure: cookie.secure,
90+
httpOnly: cookie.httpOnly,
91+
expirationDate: cookie.expirationDate,
92+
sameSite: cookie.sameSite as "unspecified" | "no_restriction" | "lax" | "strict",
93+
}
94+
95+
return mainWindow.webContents.session.cookies.set(setCookieDetails)
96+
}),
97+
)
98+
}
99+
72100
updateProxy()
73101
registerUpdater()
74102
registerAppTray()
@@ -135,7 +163,7 @@ function bootstrap() {
135163
}
136164
})
137165

138-
app.on("before-quit", () => {
166+
app.on("before-quit", async () => {
139167
// store window pos when before app quit
140168
const window = getMainWindow()
141169
if (!window || window.isDestroyed()) return
@@ -147,6 +175,12 @@ function bootstrap() {
147175
x: bounds.x,
148176
y: bounds.y,
149177
})
178+
await session.defaultSession.cookies.flushStore()
179+
180+
const cookies = await session.defaultSession.cookies.get({})
181+
store.set("cookies", cookies)
182+
183+
await cleanupOldRender()
150184
})
151185

152186
const handleOpen = async (url: string) => {
@@ -158,7 +192,6 @@ function bootstrap() {
158192
const token = urlObj.searchParams.get("token")
159193
const userId = urlObj.searchParams.get("userId")
160194

161-
const apiURL = process.env["VITE_API_URL"] || import.meta.env.VITE_API_URL
162195
if (token && apiURL) {
163196
setAuthSessionToken(token)
164197
mainWindow.webContents.session.cookies.set({

apps/main/src/init.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ export const initializeAppStage1 = () => {
5858
// code. You can also put them in separate files and require them here.
5959

6060
registerMenuAndContextMenu()
61-
6261
registerPushNotifications()
6362
clearCacheCronJob()
6463
checkAndCleanCodeCache()

apps/main/src/lib/op.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { env } from "@follow/shared/env"
2+
import { OpenPanel } from "@openpanel/web"
3+
import { app } from "electron"
4+
5+
import { DEVICE_ID } from "~/constants/system"
6+
7+
export const op = new OpenPanel({
8+
clientId: env.VITE_OPENPANEL_CLIENT_ID ?? "",
9+
trackScreenViews: false,
10+
trackOutgoingLinks: false,
11+
trackAttributes: false,
12+
trackHashChanges: false,
13+
apiUrl: env.VITE_OPENPANEL_API_URL,
14+
})
15+
16+
op.setGlobalProperties({
17+
device_id: DEVICE_ID,
18+
app_version: app.getVersion(),
19+
build: "electron",
20+
})

0 commit comments

Comments
 (0)