diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c3609be..a5a163e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -7,7 +7,27 @@ on:
workflow_dispatch:
jobs:
- release:
+ create-release:
+ permissions:
+ contents: write
+ runs-on: ubuntu-latest
+ outputs:
+ release-id: ${{ steps.create.outputs.id }}
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Create draft release
+ id: create
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: ${{ github.ref_name }}
+ name: 'Writer ${{ github.ref_name }}'
+ body: 'See the assets to download and install this version.'
+ draft: true
+ prerelease: false
+
+ build:
+ needs: create-release
permissions:
contents: write
strategy:
@@ -58,9 +78,5 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
- tagName: v__VERSION__
- releaseName: 'Writer v__VERSION__'
- releaseBody: 'See the assets to download and install this version.'
- releaseDraft: true
- prerelease: false
+ releaseId: ${{ needs.create-release.outputs.release-id }}
args: ${{ matrix.args }}
diff --git a/docs/integration/gh.md b/docs/integration/gh.md
new file mode 100644
index 0000000..0125566
--- /dev/null
+++ b/docs/integration/gh.md
@@ -0,0 +1,252 @@
+---
+title: GitHub Gist Integration Spec
+updated: 2026-03-19
+---
+
+## Goals
+
+- Browse and import public gists from any GitHub user.
+- Authenticate via GitHub OAuth device flow to access private gists.
+- Publish documents as new gists (public or secret) and update existing ones.
+- Follow the same architectural patterns as the AT Protocol / Tangled integration.
+
+## GitHub Gist API
+
+All endpoints use `https://api.github.com`. Gists are lightweight multi-file snippets. We treat each gist as a single document, using the first file when a gist contains multiple files.
+
+### Relevant Endpoints
+
+| Operation | Endpoint | Method | Auth |
+| ------------- | ------------------------- | ------ | ---------- |
+| List public | `/users/{username}/gists` | GET | No |
+| List personal | `/gists` | GET | Required |
+| Get | `/gists/{gist_id}` | GET | Optional\* |
+| Create | `/gists` | POST | Required |
+| Update | `/gists/{gist_id}` | PATCH | Required |
+| Delete | `/gists/{gist_id}` | DELETE | Required |
+| List starred | `/gists/starred` | GET | Required |
+
+\* Auth optional for public gists; required for secret gists owned by the user.
+
+Pagination: `per_page` (max 100), `page`, plus `Link` header with `rel="next"`.
+
+Rate limits: 60 req/hr unauthenticated, 5 000 req/hr with token.
+
+### Gist Record Shape
+
+```json
+{
+ "id": "aa5a315d61ae9438b18d",
+ "description": "Example gist",
+ "public": true,
+ "html_url": "https://gist.github.com/...",
+ "files": {
+ "hello.md": {
+ "filename": "hello.md",
+ "type": "text/markdown",
+ "language": "Markdown",
+ "size": 1234,
+ "content": "..."
+ }
+ },
+ "owner": { "login": "octocat", "avatar_url": "..." },
+ "created_at": "2026-01-01T00:00:00Z",
+ "updated_at": "2026-01-15T12:00:00Z"
+}
+```
+
+Note: `GET /users/{username}/gists` returns truncated gist objects (no `content` in files). A follow-up `GET /gists/{gist_id}` is needed to fetch full file contents.
+
+## Authentication
+
+### GitHub OAuth Device Flow
+
+The device flow is ideal for desktop/CLI apps — no loopback server or redirect URI needed.
+
+1. `POST https://github.com/login/device/code` with `client_id` and `scope=gist`.
+2. Response includes `device_code`, `user_code`, and `verification_uri`.
+3. Display `user_code` to the user and open `verification_uri` in the system browser.
+4. Poll `POST https://github.com/login/oauth/access_token` with `device_code` + `client_id` until the user authorizes (respect `interval` from step 2).
+5. Receive `access_token` (no refresh token for device flow; token does not expire unless revoked).
+
+### Token Lifecycle
+
+- **Access token:** Does not expire. Valid until the user revokes it in GitHub settings.
+- **Scope:** `gist` — read/write access to gists only.
+- **Storage:** Persist token in app data directory (`github-token.json`). Encrypt at rest via Tauri's platform keychain integration if available, otherwise store as plaintext JSON alongside other session files.
+
+### Client Registration
+
+Register a GitHub OAuth App at `https://github.com/settings/developers`:
+
+- Enable "Device flow" in the app settings.
+- No callback URL required for device flow.
+- `client_id` is public (no client secret needed for device flow).
+
+## Data Flow
+
+```text
+┌──────────────┐ Tauri commands ┌───────────────────┐
+│ Frontend │ ───────────────────── │ src-tauri/ │
+│ (React/TS) │ │ commands.rs │
+│ │ ◄── CommandResponse │ + github.rs │
+└──────────────┘ └────────┬──────────┘
+ │
+ reqwest + token auth
+ │
+ ┌────────▼──────────┐
+ │ api.github.com │
+ │ (REST API v3) │
+ └───────────────────┘
+```
+
+## Backend Module Structure
+
+```sh
+src-tauri/src/
+├── github/
+│ ├── mod.rs # re-exports GithubState, GithubSession, GistRecord
+│ ├── auth.rs # device flow, token persistence, logout
+│ └── gists.rs # list, get, create, update, delete helpers
+```
+
+### Types
+
+```rust
+pub struct GithubState {
+ client: reqwest::Client,
+ token: RwLock