Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,41 @@ 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:
# Fix OOM issue
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
Expand Down
13 changes: 12 additions & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
10 changes: 10 additions & 0 deletions scripts/documentLoadFailure.test.ts
Original file line number Diff line number Diff line change
@@ -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\);/);
});
20 changes: 20 additions & 0 deletions scripts/macosReleaseSigning.test.ts
Original file line number Diff line number Diff line change
@@ -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 \}\}/);
});
6 changes: 3 additions & 3 deletions scripts/reloadOpenToolbar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, /<EditorToolbar[\s\S]*onaction=\{\(actionId\) => editorPane\?\.runEditorAction\(actionId\)\}/);
assert.match(editor, /export function runEditorAction\(actionId: string\)/);
assert.match(markdownViewer, /<EditorToolbar[\s\S]*onaction=\{\(actionId, payload\) => editorPane\?\.runEditorAction\(actionId, payload\)\}/);
assert.match(editor, /export function runEditorAction\(actionId: string, payload\?: any\)/);

for (const actionId of [
'fmt-bold',
Expand Down
56 changes: 34 additions & 22 deletions src/lib/components/Settings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -1172,9 +1172,13 @@
<h2>{t('settings.toolbarsSettings', settings.language)}</h2>
</div>

<div class="toolbar-section">
<div class="toolbar-section-header">
<h3>{t('settings.applicationToolbar', settings.language)}</h3>
<details class="toolbar-settings toolbar-settings-accordion">
<summary class="toolbar-settings-summary">
<span class="toolbar-settings-chevron" aria-hidden="true"></span>
<span>{t('settings.applicationToolbar', settings.language)}</span>
</summary>
<div class="toolbar-settings-body">
<div class="toolbar-section-header">
<button
type="button"
class="reset-text-btn"
Expand Down Expand Up @@ -1248,12 +1252,17 @@
</div>
</div>
{/each}
</div>
</div>
</div>

<div class="toolbar-section" style="margin-top: 24px;">
<div class="toolbar-section-header">
<h3>{t('settings.editorToolbar', settings.language)}</h3>
</details>

<details class="toolbar-settings toolbar-settings-accordion">
<summary class="toolbar-settings-summary">
<span class="toolbar-settings-chevron" aria-hidden="true"></span>
<span>{t('settings.editorToolbar', settings.language)}</span>
</summary>
<div class="toolbar-settings-body">
<div class="toolbar-section-header">
<button
type="button"
class="reset-text-btn"
Expand Down Expand Up @@ -1311,8 +1320,9 @@
</div>
</div>
{/each}
</div>
</div>
</div>
</details>
</div>
{:else if activeCategory === 'files'}
<div class="settings-group">
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down
9 changes: 4 additions & 5 deletions src/lib/sessions/documentSession.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
Loading