Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.git
**/node_modules
.turbo
**/.turbo
dist
apps/desktop/out
apps/web/dist
apps/share-viewer/dist
apps/server/web/dist
apps/server/bin
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Contract fixtures are hashed byte for byte by the TypeScript, Go server, and
# TUI consumers, so they must check out with LF on every platform, including
# Windows runners whose Git defaults to CRLF conversion.
packages/bridge-contract/fixtures/** text eol=lf
apps/server/internal/vault/testdata/** text eol=lf
apps/server/internal/httpserver/testdata/** text eol=lf
70 changes: 70 additions & 0 deletions .github/workflows/boundary-artifact-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Prepare boundary artifact release
on:
workflow_dispatch:
inputs:
artifact:
type: choice
options: [core, web, viewer]
required: true
source_commit:
description: Reviewed full source commit SHA
type: string
required: true
permissions:
contents: read
concurrency:
group: boundary-release-${{ inputs.artifact }}
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.source_commit }}
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- uses: actions/setup-go@v6
with:
go-version: stable
- run: npm ci
- run: npm run typecheck
- run: npm run test:run
- run: npm run test:web-artifact
- if: inputs.artifact == 'core'
run: npm run test:app-core-package && npm run test:app-core-browser
- if: inputs.artifact == 'core'
env:
ZEN_CORE_VITE_VERSION: 8.2.2
run: npm run test:app-core-package && npm run test:app-core-browser
- id: prepare
env:
ARTIFACT: ${{ inputs.artifact }}
APPROVED_SOURCE: ${{ inputs.source_commit }}
run: node tooling/scripts/prepare-boundary-release.mjs "$ARTIFACT"
- uses: actions/upload-artifact@v4
with:
name: boundary-release
path: ${{ steps.prepare.outputs.directory }}
if-no-files-found: error
draft:
needs: build
runs-on: ubuntu-latest
environment: boundary-artifacts
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
name: boundary-release
path: release
- env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.build.outputs.tag }}
SOURCE_COMMIT: ${{ inputs.source_commit }}
run: gh release create "$TAG" release/* --target "$SOURCE_COMMIT" --title "$TAG" --draft --prerelease --notes "Immutable boundary artifacts. Validate consumer pins before publishing this draft."
61 changes: 56 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ concurrency:
cancel-in-progress: true

jobs:
server:
name: Go server without frontend dependencies
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/server
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: stable
cache-dependency-path: apps/server/go.sum
- run: go vet ./...
- run: go test ./...

production-dependency-audit:
name: Production dependency audit
runs-on: ubuntu-latest
Expand Down Expand Up @@ -55,17 +70,53 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
# Pin a modern Go instead of go.mod's `go 1.22`: the 1.22 macOS
# linker omits the LC_UUID load command, which the updated
# macos-latest dyld now rejects ("missing LC_UUID", abort trap) when
# launching `go test` binaries. go.mod stays at 1.22 (its real
# minimum), so Nix/release builds are unaffected.
# Keep macOS on the current linker; older Go toolchains produced
# binaries without the LC_UUID load command required by dyld.
go-version: stable
cache-dependency-path: apps/server/go.sum

- name: Install dependencies
run: npm ci

- name: Verify standalone shared packages
run: npm run test:shared-packages

- name: Verify isolated editor package and assets
if: matrix.os == 'ubuntu-latest'
run: npm run test:app-core-package

- name: Exercise the installed editor in Chrome
if: matrix.os == 'ubuntu-latest'
run: npm run test:app-core-browser

- name: Verify the editor with the mobile Vite version
if: matrix.os == 'ubuntu-latest'
env:
ZEN_CORE_VITE_VERSION: 8.2.2
run: npm run test:app-core-package

- name: Exercise the Vite 8 editor in Chrome
if: matrix.os == 'ubuntu-latest'
run: npm run test:app-core-browser

- name: Collect browser evidence
if: always() && matrix.os == 'ubuntu-latest'
run: node tooling/scripts/collect-app-core-evidence.mjs "$RUNNER_TEMP/app-core-browser-evidence"

- name: Retain browser evidence
if: always() && matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
with:
name: app-core-browser-evidence
path: ${{ runner.temp }}/app-core-browser-evidence
if-no-files-found: ignore

- name: Verify contract fixture copies
run: npm run check:contract-fixtures

- name: Verify browser asset build lock
run: npm run test:web-dist-lock

- name: Typecheck and build app
env:
GOCACHE: ${{ runner.temp }}/go-build-cache
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/share-viewer-artifact.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Public share viewer artifact

on:
workflow_dispatch:
pull_request:
paths:
- 'apps/share-viewer/**'
- 'packages/**'
- 'tooling/scripts/**'
- 'package*.json'
- 'tsconfig.base.json'
- 'LICENSE'
- '.github/workflows/share-viewer-artifact.yml'

permissions:
contents: read

jobs:
candidate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- run: npm ci
- run: node --test tooling/scripts/pack-web-artifact.test.mjs
- run: npm run pack:share-viewer
- uses: actions/upload-artifact@v4
with:
name: share-viewer-candidate
path: dist/viewer-artifacts/*
if-no-files-found: error
67 changes: 67 additions & 0 deletions .github/workflows/web-artifact.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Self-hosted web artifact boundary

on:
workflow_dispatch:
pull_request:
paths:
- 'apps/web/**'
- 'apps/server/**'
- 'packages/**'
- 'tooling/scripts/**'
- 'package*.json'
- 'tsconfig.base.json'
- 'LICENSE'
- '.github/workflows/web-artifact.yml'

permissions:
contents: read

jobs:
browser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run test:web-artifact
- run: npm run check:contract-fixtures
- run: npm run artifact:web
- uses: actions/upload-artifact@v4
with:
name: self-hosted-web-candidate
path: dist/web-artifacts/*
if-no-files-found: error

server:
needs: browser
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
with:
sparse-checkout: apps/server
- uses: actions/setup-go@v6
with:
go-version: stable
cache-dependency-path: apps/server/go.sum
- uses: actions/download-artifact@v4
with:
name: self-hosted-web-candidate
path: ${{ runner.temp }}/web-artifact
- name: Test and build using the pinned archive with Go alone
shell: bash
working-directory: apps/server
run: |
go vet ./...
go test ./...
manifests=("$RUNNER_TEMP"/web-artifact/*.tgz.json)
test "${#manifests[@]}" -eq 1
go run ./cmd/prepare-web -manifest "${manifests[0]}" -output web/dist
go test -tags=embed_web ./web
go build -tags=embed_web -trimpath -o bin/zennotes-server ./cmd/zennotes-server
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ COPY packages/shared-domain/package.json packages/shared-domain/package.json
COPY packages/shared-ui/package.json packages/shared-ui/package.json
COPY apps/desktop/package.json apps/desktop/package.json
COPY apps/server/package.json apps/server/package.json
COPY apps/share-viewer/package.json apps/share-viewer/package.json

RUN npm ci --no-audit --no-fund --loglevel=error

Expand Down Expand Up @@ -55,7 +56,7 @@ ENV CGO_ENABLED=0 \
GOFLAGS=-trimpath

WORKDIR /app/apps/server
RUN go build -ldflags="-s -w" -o /out/zennotes-server ./cmd/zennotes-server
RUN go build -tags=embed_web -ldflags="-s -w" -o /out/zennotes-server ./cmd/zennotes-server

FROM scratch
LABEL org.opencontainers.image.title="ZenNotes" \
Expand Down
22 changes: 22 additions & 0 deletions apps/desktop/src/main/databases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
writeDatabaseRows
} from './databases'

import { getVaultSettings, setVaultSettings, invalidateVaultSettingsCache, writeNoteComments, readNoteComments } from './vault'

const tmpDirs: string[] = []
async function makeVault(): Promise<string> {
const dir = await mkdtemp(path.join(os.tmpdir(), 'zennotes-db-'))
Expand Down Expand Up @@ -107,6 +109,26 @@ describe('renameDatabase', () => {
})
})


describe('database rename comments', () => {
it.each(['inbox', 'root'] as const)('moves record comments with custom folder settings in %s mode', async (location) => {
const root = await makeVault()
await setVaultSettings(root, { ...await getVaultSettings(root), primaryNotesLocation: location, systemFolderPaths: { inbox: 'My Notes' }, folderIcons: { 'inbox:Work/People.base': 'book' }, folderColors: { 'inbox:Work/People.base': 'blue' } })
const doc = await createDatabase(root, 'inbox', 'Work', 'People')
const page = await createRecordPage(root, doc.path, 'Record', 'Record body.')
await writeNoteComments(root, page, [{ notePath: page, anchorStart: 0, anchorEnd: 6, anchorText: 'Record', body: 'Keep comment' }])
const renamed = await renameDatabase(root, doc.path, 'Customers')
const nextPage = renamed.replace('data.csv', 'Record.md')
expect(await readNoteComments(root, nextPage)).toMatchObject([{ notePath: nextPage, body: 'Keep comment' }])
expect(await readNoteComments(root, page)).toEqual([])
invalidateVaultSettingsCache(root)
const settings = await getVaultSettings(root)
expect(settings.folderIcons['inbox:Work/Customers.base']).toBe('book')
expect(settings.folderColors['inbox:Work/Customers.base']).toBe('blue')
expect(settings.folderIcons['inbox:Work/People.base']).toBeUndefined()
})
})

describe('adopting a plain CSV (no sidecar)', () => {
it('infers schema, materializes the sidecar + stable ids, and is stable on re-read', async () => {
const root = await makeVault()
Expand Down
18 changes: 16 additions & 2 deletions apps/desktop/src/main/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ import {
databaseDataPath,
databaseSidecarPath,
folderRoot,
folderForRelativePath,
getVaultSettings,
renameFolder,
renameFolderTrees,
sanitizeNoteTitle,
uniqueTitle,
writeFileAtomic
Expand Down Expand Up @@ -330,7 +334,7 @@ export async function deleteDatabase(root: string, csvRel: string): Promise<void
/**
* Rename a database in place: rename its `<oldName>.base` folder to
* `<newName>.base` (non-colliding). Returns the new `data.csv` path. Because the
* data, schema, and pages all live inside, nothing else needs rewriting.
* data, schema, and pages move together; the parallel comment tree follows too.
*/
export async function renameDatabase(
root: string,
Expand All @@ -354,7 +358,17 @@ export async function renameDatabase(
break
}
}
await fs.rename(databaseDataPath(root, formDir), databaseDataPath(root, targetRel))
const settings = await getVaultSettings(root)
const folder = folderForRelativePath(formDir, settings)
const top = folder ? await folderRoot(root, folder) : null
const oldSub = top ? toPosix(path.relative(top, databaseDataPath(root, formDir))) : null
if (folder && top && oldSub && oldSub !== '..' && !oldSub.startsWith('../')) {
const newSub = toPosix(path.relative(top, databaseDataPath(root, targetRel)))
await renameFolder(root, folder, oldSub, newSub)
} else {
// Existing root-level databases remain accessible even in inbox mode.
await renameFolderTrees(root, formDir, targetRel)
}
return csvPathForFormDir(targetRel)
}

Expand Down
Loading
Loading