From c385dd42fe6da5b40086f0ec3d1e7d855e93c234 Mon Sep 17 00:00:00 2001 From: PathGao Date: Fri, 31 Jul 2026 10:56:07 +0800 Subject: [PATCH 1/3] fix: restore collapsed toolbar settings --- scripts/reloadOpenToolbar.test.ts | 6 ++-- src/lib/components/Settings.svelte | 56 ++++++++++++++++++------------ 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/scripts/reloadOpenToolbar.test.ts b/scripts/reloadOpenToolbar.test.ts index b7830656..194660df 100644 --- a/scripts/reloadOpenToolbar.test.ts +++ b/scripts/reloadOpenToolbar.test.ts @@ -24,10 +24,10 @@ test('preview-level open shortcut handles Ctrl/Cmd+O outside Monaco without doub assert.match(markdownViewer, /cmdOrCtrl[\s\S]*key === 'o'[\s\S]*selectFile\(\)/); }); -test('editor toolbar delegates to Monaco actions and exposes expected Markdown commands', () => { +test('editor toolbar forwards Monaco actions and optional payloads', () => { assert.match(markdownViewer, /import EditorToolbar from '\.\/components\/EditorToolbar\.svelte'/); - assert.match(markdownViewer, / editorPane\?\.runEditorAction\(actionId\)\}/); - assert.match(editor, /export function runEditorAction\(actionId: string\)/); + assert.match(markdownViewer, / editorPane\?\.runEditorAction\(actionId, payload\)\}/); + assert.match(editor, /export function runEditorAction\(actionId: string, payload\?: any\)/); for (const actionId of [ 'fmt-bold', diff --git a/src/lib/components/Settings.svelte b/src/lib/components/Settings.svelte index f1b8d7d6..15d8673e 100644 --- a/src/lib/components/Settings.svelte +++ b/src/lib/components/Settings.svelte @@ -1172,9 +1172,13 @@

{t('settings.toolbarsSettings', settings.language)}

-
-
-

{t('settings.applicationToolbar', settings.language)}

+
+ + + {t('settings.applicationToolbar', settings.language)} + +
+
{/each} +
-
- -
-
-

{t('settings.editorToolbar', settings.language)}

+ + +
+ + + {t('settings.editorToolbar', settings.language)} + +
+
{/each} +
-
+ {:else if activeCategory === 'files'}
@@ -1712,10 +1722,21 @@ display: none; } - .toolbar-section { - display: flex; - flex-direction: column; - gap: 8px; + .toolbar-settings-chevron { + width: 7px; + height: 7px; + border-right: 1.5px solid var(--color-fg-muted); + border-bottom: 1.5px solid var(--color-fg-muted); + transform: rotate(-45deg); + transition: transform 0.12s ease; + } + + .toolbar-settings[open] .toolbar-settings-chevron { + transform: rotate(45deg); + } + + .toolbar-settings-body { + padding-bottom: 12px; } .toolbar-section-header { @@ -1725,15 +1746,6 @@ padding-bottom: 2px; } - .toolbar-section-header h3 { - margin: 0; - font-size: 12px; - font-weight: 600; - color: var(--color-fg-muted); - text-transform: uppercase; - letter-spacing: 0.5px; - } - .toolbar-settings-list { display: flex; flex-direction: column; From e3bdb7fe3fce3bc05ff84e4d0d32866aa60088d3 Mon Sep 17 00:00:00 2001 From: PathGao Date: Fri, 31 Jul 2026 13:17:29 +0800 Subject: [PATCH 2/3] fix: preserve tabs after document read errors --- scripts/documentLoadFailure.test.ts | 10 ++++++++++ src/lib/sessions/documentSession.svelte.ts | 9 ++++----- 2 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 scripts/documentLoadFailure.test.ts diff --git a/scripts/documentLoadFailure.test.ts b/scripts/documentLoadFailure.test.ts new file mode 100644 index 00000000..8fc579d0 --- /dev/null +++ b/scripts/documentLoadFailure.test.ts @@ -0,0 +1,10 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const session = readFileSync('src/lib/sessions/documentSession.svelte.ts', 'utf8'); + +test('a transient missing-file read error preserves the open tab', () => { + assert.doesNotMatch(session, /tabManager\.closeTab/); + assert.match(session, /options\.onError\('Error loading file', error\);/); +}); diff --git a/src/lib/sessions/documentSession.svelte.ts b/src/lib/sessions/documentSession.svelte.ts index 4817f096..591d2c81 100644 --- a/src/lib/sessions/documentSession.svelte.ts +++ b/src/lib/sessions/documentSession.svelte.ts @@ -142,11 +142,10 @@ export function createDocumentSession(options: DocumentSessionOptions) { if (filePath) options.saveRecentFile(filePath); } catch (error) { console.error('Error loading file:', error); - const errorText = String(error); - if (errorText.includes('The system cannot find the file specified') || errorText.includes('No such file or directory')) { - options.deleteRecentFile(filePath); - if (tabManager.activeTab?.path === filePath) tabManager.closeTab(tabManager.activeTab.id); - } else options.onError('Error loading file', error); + // A watcher can observe a transient gap while another process replaces + // the file. Keep the already-open buffer and recent-file entry instead + // of closing the tab and discarding the user's recovery path. + options.onError('Error loading file', error); } } From 264c493a9fecc08a7622a997c31a19e9fab2fb50 Mon Sep 17 00:00:00 2001 From: PathGao Date: Fri, 31 Jul 2026 13:19:06 +0800 Subject: [PATCH 3/3] ci: sign and notarize macOS releases --- .github/workflows/build.yml | 28 ++++++++++++++++++++++++++++ RELEASING.md | 13 ++++++++++++- scripts/macosReleaseSigning.test.ts | 20 ++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 scripts/macosReleaseSigning.test.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 604a1218..ab876e7d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -228,6 +228,30 @@ jobs: fi # --- MacOS Build --- + - name: Import Apple Developer ID certificate + if: matrix.platform == 'macos-latest' + shell: bash + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + set -euo pipefail + echo -n "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12 + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security set-keychain-settings -lut 21600 build.keychain + security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain + identity=$(security find-identity -v -p codesigning build.keychain | sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p' | head -1) + if [ -z "$identity" ]; then + echo 'No Developer ID Application signing identity was imported.' >&2 + exit 1 + fi + echo "APPLE_SIGNING_IDENTITY=$identity" >> "$GITHUB_ENV" + rm certificate.p12 + - name: Build MacOS (Universal) if: matrix.platform == 'macos-latest' env: @@ -235,6 +259,10 @@ jobs: NODE_OPTIONS: "--max-old-space-size=8192" TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }} run: npm run tauri build -- --target universal-apple-darwin - name: Upload MacOS Artifacts diff --git a/RELEASING.md b/RELEASING.md index ac07ede7..f4f50849 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -25,9 +25,21 @@ In the GitHub repo settings → Secrets and variables → Actions → New reposi |---------------------------------------|----------------------------------------------------| | `TAURI_SIGNING_PRIVATE_KEY` | full content of `~/.tauri/markpad-updater.key` | | `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | the password you set in step 1 | +| `APPLE_CERTIFICATE` | base64-encoded Developer ID Application `.p12` | +| `APPLE_CERTIFICATE_PASSWORD` | password used when exporting that `.p12` | +| `KEYCHAIN_PASSWORD` | temporary GitHub Actions keychain password | +| `APPLE_ID` | Apple ID used for notarization | +| `APPLE_PASSWORD` | app-specific password for that Apple ID | +| `APPLE_TEAM_ID` | Apple Developer team ID | The build workflow reads both at signing time on macOS, Windows, and Linux runners. +### 3. Configure macOS code signing and notarization + +The release workflow imports a `Developer ID Application` certificate on the macOS runner, signs the universal bundle, then asks Apple to notarize and staple it. Create the certificate in the Apple Developer portal, export it from Keychain Access as a password-protected `.p12`, encode it with `base64 -i certificate.p12`, and store the resulting single-line value in `APPLE_CERTIFICATE`. + +Use an app-specific Apple ID password for `APPLE_PASSWORD`. The certificate, Apple ID, and team ID must belong to the same Developer Program team. A release intentionally fails at the macOS build step if the certificate cannot be imported or no `Developer ID Application` identity is found; publishing an ad-hoc-signed macOS bundle would break persistent TCC folder grants. + ### 3. Send the public key content Send the **single-line content** of `~/.tauri/markpad-updater.key.pub` (no comments, no header lines) to the developer who'll commit it to `src-tauri/tauri.conf.json` under `plugins.updater.pubkey`. Until that placeholder is replaced, auto-update is inert — the app surfaces a clean error state instead of contacting the update server. @@ -92,6 +104,5 @@ Mention this clearly in the release notes for the first auto-update-capable vers ## Out of scope (not handled by this workflow) -- **Apple Developer ID code-signing & notarization** — `.app` bundles are unsigned. macOS may show a Gatekeeper warning on first launch. Minisign verification by the updater is independent of Apple code-signing. - **Windows Authenticode signing** — neither the portable `.exe` nor the `*-setup.exe` NSIS installer is signed with a code-signing certificate. Users may see a SmartScreen warning. Minisign verification by the updater is independent. - **Retroactive signing** of older releases. diff --git a/scripts/macosReleaseSigning.test.ts b/scripts/macosReleaseSigning.test.ts new file mode 100644 index 00000000..29c4ff2e --- /dev/null +++ b/scripts/macosReleaseSigning.test.ts @@ -0,0 +1,20 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const workflow = readFileSync('.github/workflows/build.yml', 'utf8'); + +test('the release workflow imports a Developer ID certificate before building macOS', () => { + assert.match(workflow, /name: Import Apple Developer ID certificate/); + assert.match(workflow, /APPLE_CERTIFICATE: \$\{\{ secrets\.APPLE_CERTIFICATE \}\}/); + assert.match(workflow, /security import certificate\.p12/); + assert.match(workflow, /APPLE_SIGNING_IDENTITY/); +}); + +test('the macOS release build receives notarization credentials', () => { + const macosBuild = workflow.slice(workflow.indexOf('name: Build MacOS (Universal)')); + + assert.match(macosBuild, /APPLE_ID: \$\{\{ secrets\.APPLE_ID \}\}/); + assert.match(macosBuild, /APPLE_PASSWORD: \$\{\{ secrets\.APPLE_PASSWORD \}\}/); + assert.match(macosBuild, /APPLE_TEAM_ID: \$\{\{ secrets\.APPLE_TEAM_ID \}\}/); +});