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
59 changes: 56 additions & 3 deletions .githooks/sync-versions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {execFileSync} from 'node:child_process'
import {mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync} from 'node:fs'
import {tmpdir} from 'node:os'
import {dirname, join} from 'node:path'
import {afterEach, describe, expect, it} from 'vitest'
import {afterEach, describe, expect, it} from 'bun:test'
import {runSyncVersions} from './sync-versions'

function writeJson(filePath: string, value: Record<string, unknown>): void {
Expand Down Expand Up @@ -56,6 +56,22 @@ function createFixtureRepo(): string {
version: initialVersion,
private: true
})
writeJson(join(rootDir, 'obsidian-plugin', 'package.json'), {
name: 'tnmso',
version: initialVersion,
private: true
})
const manifest = {
id: 'tnmso',
name: 'TNMSO',
version: initialVersion,
minAppVersion: '1.0.0',
isDesktopOnly: false
}
writeJson(join(rootDir, 'obsidian-plugin', 'manifest.json'), manifest)
writeJson(join(rootDir, 'manifest.json'), manifest)
writeJson(join(rootDir, 'obsidian-plugin', 'versions.json'), {[initialVersion]: '1.0.0'})
writeJson(join(rootDir, 'versions.json'), {[initialVersion]: '1.0.0'})
writeJson(join(rootDir, 'cli', 'npm', 'darwin-arm64', 'package.json'), {
name: '@truenine/memory-sync-cli-darwin-arm64',
version: initialVersion
Expand Down Expand Up @@ -149,6 +165,14 @@ function expectSharedVersionSurfaces(rootDir: string, nextVersion: string): void
})
expect(JSON.parse(readFileSync(join(rootDir, 'gui', 'package.json'), 'utf-8')) as {version: string}).toMatchObject({version: nextVersion})
expect(JSON.parse(readFileSync(join(rootDir, 'doc', 'package.json'), 'utf-8')) as {version: string}).toMatchObject({version: nextVersion})
expect(JSON.parse(readFileSync(join(rootDir, 'obsidian-plugin', 'package.json'), 'utf-8')) as {version: string}).toMatchObject({version: nextVersion})
const pluginManifest = JSON.parse(readFileSync(join(rootDir, 'obsidian-plugin', 'manifest.json'), 'utf-8')) as {version: string, minAppVersion: string}
expect(pluginManifest).toMatchObject({version: nextVersion, minAppVersion: '1.0.0'})
expect(JSON.parse(readFileSync(join(rootDir, 'manifest.json'), 'utf-8'))).toEqual(pluginManifest)
expect(JSON.parse(readFileSync(join(rootDir, 'obsidian-plugin', 'versions.json'), 'utf-8'))).toMatchObject({[nextVersion]: '1.0.0'})
expect(JSON.parse(readFileSync(join(rootDir, 'versions.json'), 'utf-8'))).toEqual(
JSON.parse(readFileSync(join(rootDir, 'obsidian-plugin', 'versions.json'), 'utf-8'))
)
expect(JSON.parse(readFileSync(join(rootDir, 'cli', 'npm', 'darwin-arm64', 'package.json'), 'utf-8')) as {version: string}).toMatchObject({version: nextVersion})
expect(JSON.parse(readFileSync(join(rootDir, 'cli', 'npm', 'linux-x64-gnu', 'package.json'), 'utf-8')) as {version: string}).toMatchObject({version: nextVersion})
expect(readFileSync(join(rootDir, 'Cargo.toml'), 'utf-8')).toContain(`version = "${nextVersion}"`)
Expand Down Expand Up @@ -197,7 +221,12 @@ describe('sync-versions hook', () => {
'gui/src-tauri/Cargo.toml',
'gui/src-tauri/tauri.conf.json',
'mcp/package.json',
'package.json'
'manifest.json',
'obsidian-plugin/manifest.json',
'obsidian-plugin/package.json',
'obsidian-plugin/versions.json',
'package.json',
'versions.json'
]))
})

Expand Down Expand Up @@ -229,7 +258,12 @@ describe('sync-versions hook', () => {
'gui/src-tauri/Cargo.toml',
'gui/src-tauri/tauri.conf.json',
'mcp/package.json',
'package.json'
'manifest.json',
'obsidian-plugin/manifest.json',
'obsidian-plugin/package.json',
'obsidian-plugin/versions.json',
'package.json',
'versions.json'
]))
})

Expand All @@ -252,6 +286,25 @@ describe('sync-versions hook', () => {
expectSharedVersionSurfaces(rootDir, nextVersion)
})

it('accepts the TNMSO package version as the staged system version source', () => {
const rootDir = createFixtureRepo()
tempDirs.push(rootDir)

const nextVersion = '2026.10324.10319'
writeJson(join(rootDir, 'obsidian-plugin', 'package.json'), {
name: 'tnmso',
version: nextVersion,
private: true
})
runGit(rootDir, ['add', 'obsidian-plugin/package.json'])

const result = runSyncVersions({rootDir})

expect(result.targetVersion).toBe(nextVersion)
expect(result.versionSource).toBe('obsidian-plugin/package.json')
expectSharedVersionSurfaces(rootDir, nextVersion)
})

it('fails when staged package.json files propose conflicting versions', () => {
const rootDir = createFixtureRepo()
tempDirs.push(rootDir)
Expand Down
55 changes: 54 additions & 1 deletion .githooks/sync-versions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env tsx
#!/usr/bin/env bun
/**
* Version Sync Script
* Auto-sync all publishable package versions before commit.
Expand Down Expand Up @@ -45,6 +45,57 @@ function writeJsonFile(filePath: string, value: VersionedJson): void {
writeFileSync(filePath, JSON.stringify(value, null, 2) + '\n', 'utf-8')
}

function writeJsonFileIfChanged(
filePath: string,
value: VersionedJson,
changedPaths: Set<string>,
): void {
let current: VersionedJson | undefined
try {
current = readJsonFile(filePath)
} catch {
current = undefined
}

if (current != null && JSON.stringify(current) === JSON.stringify(value)) {
return
}

writeJsonFile(filePath, value)
changedPaths.add(filePath)
}

function syncObsidianReleaseMetadata(
rootDir: string,
targetVersion: string,
changedPaths: Set<string>,
): void {
const pluginManifestPath = resolve(rootDir, 'obsidian-plugin', 'manifest.json')
const rootManifestPath = resolve(rootDir, 'manifest.json')
const pluginVersionsPath = resolve(rootDir, 'obsidian-plugin', 'versions.json')
const rootVersionsPath = resolve(rootDir, 'versions.json')
const manifest = readJsonFile(pluginManifestPath)
const minAppVersion = manifest.minAppVersion

if (manifest.id !== 'tnmso' || typeof minAppVersion !== 'string' || minAppVersion.trim() === '') {
throw new Error('TNMSO manifest must define id=tnmso and a non-empty minAppVersion')
}

const updatedManifest = {...manifest, version: targetVersion}
writeJsonFileIfChanged(pluginManifestPath, updatedManifest, changedPaths)
writeJsonFileIfChanged(rootManifestPath, updatedManifest, changedPaths)

let versions: VersionedJson
try {
versions = readJsonFile(pluginVersionsPath)
} catch {
versions = {}
}
const updatedVersions = {...versions, [targetVersion]: minAppVersion}
writeJsonFileIfChanged(pluginVersionsPath, updatedVersions, changedPaths)
writeJsonFileIfChanged(rootVersionsPath, updatedVersions, changedPaths)
}

function discoverFilesByName(baseDir: string, fileName: string): string[] {
const found: string[] = []
const entries = readdirSync(baseDir, {withFileTypes: true})
Expand Down Expand Up @@ -469,6 +520,8 @@ export function runSyncVersions(options: SyncVersionsOptions = {}): SyncVersions
syncJsonVersion(filePath, target.version, changedPaths)
}

syncObsidianReleaseMetadata(rootDir, target.version, changedPaths)

stageFiles(rootDir, [...changedPaths].sort())

return {
Expand Down
31 changes: 31 additions & 0 deletions .github/actions/setup-bun/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Setup Bun
description: Setup the pinned Bun runtime and optionally install TNMSO dependencies

inputs:
bun-version:
description: Bun version
required: false
default: "1.3.14"
install:
description: Whether to install dependencies
required: false
default: "true"
working-directory:
description: Directory containing bun.lock
required: false
default: obsidian-plugin

runs:
using: composite
steps:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ inputs.bun-version }}

- name: Install Bun dependencies
if: inputs.install == 'true'
shell: bash
run: bun install --frozen-lockfile --cwd "$BUN_WORKING_DIRECTORY"
env:
BUN_WORKING_DIRECTORY: ${{ inputs.working-directory }}
2 changes: 1 addition & 1 deletion .github/actions/setup-rust/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ runs:
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ inputs.rust-version }}
components: rustfmt
components: rustfmt,clippy
targets: ${{ inputs.targets }}

- name: Cache cargo
Expand Down
127 changes: 91 additions & 36 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-24.04
outputs:
docs: ${{ steps.filter.outputs.docs }}
gui: ${{ steps.filter.outputs.gui }}
obsidian: ${{ steps.filter.outputs.obsidian }}
steps:
- uses: actions/checkout@v6

Expand All @@ -40,16 +40,37 @@ jobs:
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'xtask/**'
gui:
- 'gui/**'
- '.github/actions/build-gui-platform/action.yml'
- '.github/actions/setup-tauri/action.yml'
obsidian:
- 'obsidian-plugin/**'
- 'manifest.json'
- 'versions.json'
- '.githooks/sync-versions.ts'
- '.githooks/sync-versions.test.ts'
- 'scripts/shared/check-version-surfaces.ts'
- '.github/actions/setup-bun/action.yml'
- '.github/workflows/ci.yml'
- '.github/workflows/release.yml'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'xtask/**'

version-surfaces:
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v6

- uses: ./.github/actions/setup-bun
with:
install: "false"

- name: Test version synchronization
run: bun test ./.githooks/sync-versions.test.ts

- name: Validate current version surfaces
shell: bash
run: |
version="$(bun -e 'console.log(require("./package.json").version)')"
bun scripts/shared/check-version-surfaces.ts "$version"

validate-monorepo:
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
Expand Down Expand Up @@ -93,38 +114,41 @@ jobs:
run: cargo build --release -p tnmsc -p tnmsm

- name: CLI packaging smoke
run: cargo test -p tnmsc-local-tests packaging_smoke_covers_release_binary_and_global_install -- --exact --nocapture

- name: MCP packaging smoke
run: cargo test -p tnmsm-local-tests packaging_smoke_covers_release_binary_and_global_install -- --exact --nocapture

gui-smoke:
needs: changes
if: |
(github.event_name != 'pull_request' || github.event.pull_request.draft == false) &&
needs.changes.outputs.gui == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v6

- uses: ./.github/actions/setup-node-pnpm

- name: Read GUI version
id: gui-version
shell: bash
run: |
version="$(node -p 'require("./gui/package.json").version')"
echo "version=$version" >> "$GITHUB_OUTPUT"
set -euo pipefail
target/release/tnmsc assemble-npm --profile release

- uses: ./.github/actions/setup-tauri
with:
version: ${{ steps.gui-version.outputs.version }}
pack_dir="$(mktemp -d)"
prefix_dir="$(mktemp -d)"
trap 'rm -rf "$pack_dir" "$prefix_dir"' EXIT
pnpm -C cli/npm/linux-x64-gnu pack --pack-destination "$pack_dir"
pnpm -C cli pack --pack-destination "$pack_dir"

- name: Build GUI
run: cargo run -p xtask -- gui-build
platform_tarballs=("$pack_dir"/truenine-memory-sync-cli-linux-x64-gnu-*.tgz)
main_tarballs=("$pack_dir"/truenine-memory-sync-cli-[0-9]*.tgz)
npm install --global --prefix "$prefix_dir" "${platform_tarballs[0]}" "${main_tarballs[0]}" --ignore-scripts
"$prefix_dir/bin/tnmsc" help >"$pack_dir/tnmsc-help.txt"
grep -q 'install' "$pack_dir/tnmsc-help.txt"

- name: Test GUI
run: pnpm -C gui test
- name: MCP packaging smoke
shell: bash
run: |
set -euo pipefail
target/release/tnmsm assemble-npm --profile release

pack_dir="$(mktemp -d)"
prefix_dir="$(mktemp -d)"
trap 'rm -rf "$pack_dir" "$prefix_dir"' EXIT
pnpm -C mcp/npm/linux-x64-gnu pack --pack-destination "$pack_dir"
pnpm -C mcp pack --pack-destination "$pack_dir"

platform_tarballs=("$pack_dir"/truenine-memory-sync-mcp-linux-x64-gnu-*.tgz)
main_tarballs=("$pack_dir"/truenine-memory-sync-mcp-[0-9]*.tgz)
npm install --global --prefix "$prefix_dir" "${platform_tarballs[0]}" "${main_tarballs[0]}" --ignore-scripts
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
| "$prefix_dir/bin/tnmsm" >"$pack_dir/tnmsm-initialize.json"
grep -q '"jsonrpc":"2.0"' "$pack_dir/tnmsm-initialize.json"

docs-check:
needs: changes
Expand All @@ -151,3 +175,34 @@ jobs:

- name: Build docs
run: pnpm -C doc run build

obsidian-plugin-check:
needs: changes
if: |
(github.event_name != 'pull_request' || github.event.pull_request.draft == false) &&
needs.changes.outputs.obsidian == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@v6

- uses: ./.github/actions/setup-bun

- name: Test TNMSO
run: bun test
working-directory: obsidian-plugin

- name: Typecheck TNMSO
run: bun run check:type
working-directory: obsidian-plugin

- name: Lint TNMSO
run: bun run lint
working-directory: obsidian-plugin

- name: Build and package TNMSO
run: |
bun run build
bun run package:release
bun run verify:dist
working-directory: obsidian-plugin
2 changes: 1 addition & 1 deletion .github/workflows/debug-gui-rebuild.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Debug GUI Rebuild
name: GUI Build (Manual)

on:
workflow_dispatch:
Expand Down
Loading