diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdbb9104..76eb9530 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - name: Test evidence and runner regressions run: npm run test:unified:lib - name: Repository root allowlist and public-content guard - run: node --test tests/toolchain/repo-root-contract.mjs tests/toolchain/public-content-contract.mjs + run: node --test tests/toolchain/repo-root-contract.mjs tests/toolchain/public-content-contract.mjs tests/toolchain/version-contract.mjs - name: Schematic routing visual gate run: | npm run report:schematic -- --out test-artifacts/ci-schematic-routing diff --git a/CLAUDE.md b/CLAUDE.md index 140b09be..924b4fa4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ admin merge authority is not a non-author review approval. ## Project Overview -GraphDone is a graph-native management system that reimagines work coordination through dependencies and democratic prioritization rather than hierarchical assignments. Currently in active development (v0.3.1-alpha) with TLS/SSL production readiness. +GraphDone is a graph-native management system that reimagines work coordination through dependencies and democratic prioritization rather than hierarchical assignments. Currently in active alpha development (version: root `package.json`) with TLS/SSL production readiness. ## Core Philosophy diff --git a/README.md b/README.md index b82e309c..64468e67 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ![GraphDone's living graph — priority glow, breathing nodes, energy flowing along dependencies](./docs/graphdone_ui.png) ![License](https://img.shields.io/badge/license-MIT-blue.svg) -![Version](https://img.shields.io/badge/version-0.3.1--alpha-orange.svg) +![Version](https://img.shields.io/badge/version-0.4.0--alpha-orange.svg) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg) > 🔒 **SECURITY NOTE**: GraphDone supports both HTTP (development) and HTTPS/TLS (production) modes. For production deployment, enable TLS encryption and use production-grade authentication. See [TLS/SSL Setup Guide](./docs/tls-ssl-setup.md) for configuration details. diff --git a/deployment/docker-compose.http.yml b/deployment/docker-compose.http.yml index 0a4060c7..3fde37a6 100644 --- a/deployment/docker-compose.http.yml +++ b/deployment/docker-compose.http.yml @@ -36,7 +36,7 @@ services: graphdone-api: container_name: graphdone-api-prod - image: gd-core-api:0.3.1-alpha + image: gd-core-api:0.4.0-alpha build: context: .. dockerfile: packages/server/Dockerfile @@ -79,7 +79,7 @@ services: graphdone-web: container_name: graphdone-web-prod - image: gd-core-web:0.3.1-alpha + image: gd-core-web:0.4.0-alpha build: context: .. dockerfile: packages/web/Dockerfile diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index d82883e1..786322d3 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -40,7 +40,7 @@ services: graphdone-api: container_name: graphdone-api - image: gd-core-api:0.3.1-alpha + image: gd-core-api:0.4.0-alpha build: context: .. dockerfile: packages/server/Dockerfile @@ -80,7 +80,7 @@ services: graphdone-web: container_name: graphdone-web - image: gd-core-web:0.3.1-alpha + image: gd-core-web:0.4.0-alpha build: context: .. dockerfile: packages/web/Dockerfile diff --git a/package-lock.json b/package-lock.json index c4c36ad9..e24ef495 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "graphdone", - "version": "0.3.1-alpha", + "version": "0.4.0-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "graphdone", - "version": "0.3.1-alpha", + "version": "0.4.0-alpha", "license": "MIT", "workspaces": [ "packages/*", @@ -13517,7 +13517,7 @@ }, "packages/core": { "name": "@graphdone/core", - "version": "0.3.1-alpha", + "version": "0.4.0-alpha", "license": "MIT", "dependencies": { "neo4j-driver": "^5.15.0", @@ -14001,7 +14001,7 @@ }, "packages/mcp-server": { "name": "@graphdone/mcp-server", - "version": "0.3.1-alpha", + "version": "0.4.0-alpha", "license": "MIT", "dependencies": { "@graphdone/core": "*", @@ -14490,7 +14490,7 @@ }, "packages/server": { "name": "@graphdone/server", - "version": "0.3.1-alpha", + "version": "0.4.0-alpha", "license": "MIT", "dependencies": { "@apollo/server": "^5.5.1", @@ -15017,7 +15017,7 @@ }, "packages/web": { "name": "@graphdone/web", - "version": "0.3.1-alpha", + "version": "0.4.0-alpha", "license": "MIT", "dependencies": { "@apollo/client": "^3.8.0", diff --git a/package.json b/package.json index 75dd863e..a0ad32c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphdone", - "version": "0.3.1-alpha", + "version": "0.4.0-alpha", "description": "Management for teams who think differently", "private": true, "workspaces": [ diff --git a/packages/core/package.json b/packages/core/package.json index 8754a867..cbd7fef7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@graphdone/core", - "version": "0.3.1-alpha", + "version": "0.4.0-alpha", "description": "Core graph engine for GraphDone", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 2170e8e9..34ca2136 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@graphdone/mcp-server", - "version": "0.3.1-alpha", + "version": "0.4.0-alpha", "description": "MCP server for GraphDone graph operations", "main": "dist/index.js", "type": "module", diff --git a/packages/mcp-server/src/health-server.ts b/packages/mcp-server/src/health-server.ts index e3a14d51..3131c755 100644 --- a/packages/mcp-server/src/health-server.ts +++ b/packages/mcp-server/src/health-server.ts @@ -25,7 +25,7 @@ export function startHealthServer(port = 3128) { status: 'healthy', timestamp: new Date().toISOString(), server: 'graphdone-mcp', - version: '0.3.1-alpha', + version: '0.4.0-alpha', uptime: process.uptime(), pid: process.pid, capabilities: [ diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 01c1d714..cde2219a 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -31,7 +31,7 @@ import { const server = new Server( { name: 'graphdone-mcp-server', - version: '0.3.1-alpha', + version: '0.4.0-alpha', }, { capabilities: { diff --git a/packages/mcp-server/tests/health-server.test.ts b/packages/mcp-server/tests/health-server.test.ts index 7df81861..dd161eb3 100644 --- a/packages/mcp-server/tests/health-server.test.ts +++ b/packages/mcp-server/tests/health-server.test.ts @@ -27,7 +27,7 @@ describe('MCP Health Server', () => { expect(health).toBeDefined(); expect(health.status).toBe('healthy'); expect(health.server).toBe('graphdone-mcp'); - expect(health.version).toBe('0.3.1-alpha'); + expect(health.version).toBe('0.4.0-alpha'); expect(health.capabilities).toBeDefined(); expect(Array.isArray(health.capabilities)).toBe(true); expect(health.capabilities.length).toBeGreaterThan(0); diff --git a/packages/mcp-server/tests/neo4j-contract.test.ts b/packages/mcp-server/tests/neo4j-contract.test.ts index f81794c0..7af87776 100644 --- a/packages/mcp-server/tests/neo4j-contract.test.ts +++ b/packages/mcp-server/tests/neo4j-contract.test.ts @@ -317,7 +317,7 @@ describe.skipIf(!configured)('MCP GraphService — real Neo4j contract', () => { try { await client.connect(transport, { timeout: 5000, maxTotalTimeout: 5000 }); pid = transport.pid; - expect(client.getServerVersion()).toEqual({ name: 'graphdone-mcp-server', version: '0.3.1-alpha' }); + expect(client.getServerVersion()).toEqual({ name: 'graphdone-mcp-server', version: '0.4.0-alpha' }); expect(client.getServerCapabilities()).toEqual({ tools: {} }); const listed = await client.listTools(undefined, options); expect(listed.tools.find(tool => tool.name === 'create_node')?.inputSchema.required).toEqual(['title', 'type']); diff --git a/packages/server/package.json b/packages/server/package.json index 357bc6b8..90e76d41 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@graphdone/server", - "version": "0.3.1-alpha", + "version": "0.4.0-alpha", "description": "GraphQL API server for GraphDone", "type": "module", "main": "dist/index.js", diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index c551fe7b..6dc1dfbf 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -107,7 +107,7 @@ const PORT = Number(process.env.PORT) || 4127; async function startServer() { // Use the start time from ./start command if available, otherwise use current time const startTime = process.env.GRAPHDONE_START_TIME ? parseInt(process.env.GRAPHDONE_START_TIME) : Date.now(); - console.log('🚀 GraphDone Server v0.3.1-alpha starting...'); // eslint-disable-line no-console + console.log('🚀 GraphDone Server v0.4.0-alpha starting...'); // eslint-disable-line no-console if (process.env.GRAPHDONE_START_TIME) { console.log(`🕰️ Using ./start command timing: ${process.env.GRAPHDONE_START_TIME}`); // eslint-disable-line no-console } diff --git a/packages/web/.env.example b/packages/web/.env.example index 26732756..bc1255ce 100644 --- a/packages/web/.env.example +++ b/packages/web/.env.example @@ -4,7 +4,7 @@ VITE_GRAPHQL_WS_URL=ws://localhost:3127/graphql # App configuration VITE_APP_NAME=GraphDone -VITE_APP_VERSION=0.3.1-alpha +VITE_APP_VERSION=0.4.0-alpha # Feature flags VITE_ENABLE_SUBSCRIPTIONS=true diff --git a/packages/web/package.json b/packages/web/package.json index b4e76f46..12a7ee43 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@graphdone/web", - "version": "0.3.1-alpha", + "version": "0.4.0-alpha", "description": "React web application for GraphDone", "type": "module", "scripts": { diff --git a/packages/web/src/utils/version.ts b/packages/web/src/utils/version.ts index fc57fd0f..1c08c98b 100644 --- a/packages/web/src/utils/version.ts +++ b/packages/web/src/utils/version.ts @@ -1 +1 @@ -export const APP_VERSION = '0.3.1-alpha'; \ No newline at end of file +export const APP_VERSION = '0.4.0-alpha'; \ No newline at end of file diff --git a/scripts/generate-clean-report.sh b/scripts/generate-clean-report.sh index fb853a7e..b2fe650f 100755 --- a/scripts/generate-clean-report.sh +++ b/scripts/generate-clean-report.sh @@ -666,7 +666,7 @@ cat > "$HTML_REPORT" << 'HTMLEOF' diff --git a/scripts/install.sh b/scripts/install.sh index f5601d14..b45acdf5 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -2929,7 +2929,7 @@ install_graphdone() { printf "\n\n" # Fetch latest version from GitHub releases - GRAPHDONE_VERSION="v0.3.1-alpha" # Fallback version + GRAPHDONE_VERSION="v0.4.0-alpha" # Fallback version if command -v curl >/dev/null 2>&1; then LATEST_VERSION=$(curl -sf --max-time 3 https://api.github.com/repos/GraphDone/GraphDone-Core/releases/latest 2>/dev/null | grep -o '"tag_name": *"[^"]*"' | sed 's/"tag_name": *"\(.*\)"/\1/' 2>/dev/null) if [ -n "$LATEST_VERSION" ]; then diff --git a/scripts/update-version-minimal.sh b/scripts/update-version-minimal.sh index 55351708..deef505b 100755 --- a/scripts/update-version-minimal.sh +++ b/scripts/update-version-minimal.sh @@ -1,62 +1,63 @@ #!/bin/bash - -# Minimal version update script -# Only updates what absolutely cannot read from root package.json - -set -e - -NEW_VERSION="$1" - -if [ -z "$NEW_VERSION" ]; then - echo "Usage: $0 " - echo "Example: $0 0.3.2-alpha" +# Set the release version everywhere it is recorded. +# +# scripts/update-version-minimal.sh 0.4.0-alpha +# +# Manifests and their package-lock.json entries are edited in place (no +# re-resolution); the remaining copies (runtime-reported versions, local image +# tags, installer fallback, README badge) are listed in +# tests/toolchain/version-contract.mjs, which fails CI if any of them is missed. +set -euo pipefail + +NEW_VERSION="${1:-}" +if ! [[ "$NEW_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$ ]]; then + echo "Usage: $0 e.g. $0 0.4.0-alpha" >&2 exit 1 fi - -echo "Updating version to $NEW_VERSION..." - -# 1. Update root package.json (THE single source of truth) -echo "✓ Updating root package.json..." -sed -i.bak "s/\"version\": \"[0-9]*\.[0-9]*\.[0-9]*-alpha\"/\"version\": \"$NEW_VERSION\"/" package.json -rm package.json.bak - -# 2. Update Docker image tags (cannot read package.json at build time) -echo "✓ Updating Docker image tags..." -for compose in deployment/docker-compose*.yml; do - if [ -f "$compose" ]; then - sed -i.bak "s/gd-core-[^:]*:[0-9]*\.[0-9]*\.[0-9]*-alpha/&/g; s/:[0-9]*\.[0-9]*\.[0-9]*-alpha/:$NEW_VERSION/g" "$compose" - rm "$compose.bak" - fi +cd "$(dirname "$0")/.." + +OLD_VERSION="$(node -p "require('./package.json').version")" +echo "Updating version $OLD_VERSION -> $NEW_VERSION" + +# 1. Root and workspace manifests, plus their entries in package-lock.json. +# Only the version fields change: re-resolving the lockfile (npm version, +# npm install) would also rewrite unrelated dev/peer flags. +NEW_VERSION="$NEW_VERSION" node - <<'NODE' +const fs = require('fs'); +const version = process.env.NEW_VERSION; +const edit = (file, change) => { + const json = JSON.parse(fs.readFileSync(file, 'utf8')); + change(json); + fs.writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`); +}; +const workspaces = fs.readdirSync('packages').filter(dir => fs.existsSync(`packages/${dir}/package.json`)); +edit('package.json', json => { json.version = version; }); +for (const dir of workspaces) edit(`packages/${dir}/package.json`, json => { json.version = version; }); +edit('package-lock.json', lock => { + lock.version = version; + lock.packages[''].version = version; + for (const dir of workspaces) if (lock.packages[`packages/${dir}`]) lock.packages[`packages/${dir}`].version = version; +}); +NODE + +# 2. Every other recorded copy of the old version (exact-string replacement). +replace() { # file old new + OLD="$2" NEW="$3" perl -0pi -e 's/\Q$ENV{OLD}\E/$ENV{NEW}/g' "$1" +} +for file in \ + packages/web/src/utils/version.ts \ + packages/web/.env.example \ + packages/mcp-server/src/index.ts \ + packages/mcp-server/src/health-server.ts \ + packages/mcp-server/tests/health-server.test.ts \ + packages/mcp-server/tests/neo4j-contract.test.ts \ + packages/server/src/index.ts \ + scripts/install.sh \ + deployment/docker-compose.yml \ + deployment/docker-compose.http.yml; do + replace "$file" "$OLD_VERSION" "$NEW_VERSION" done +replace README.md "badge/version-${OLD_VERSION//-/--}-" "badge/version-${NEW_VERSION//-/--}-" -# 3. Update web version utility -echo "✓ Updating web version utility..." -sed -i.bak "s/APP_VERSION = '[0-9]*\.[0-9]*\.[0-9]*-alpha'/APP_VERSION = '$NEW_VERSION'/g" packages/web/src/utils/version.ts -rm packages/web/src/utils/version.ts.bak - -# 4. Update README badge -echo "✓ Updating README version badge..." -sed -i.bak "s/version-[0-9]*\.[0-9]*\.[0-9]*--alpha/version-${NEW_VERSION//./-}/g" README.md -rm README.md.bak - -# 5. Update MCP server hardcoded versions -echo "✓ Updating MCP server versions..." -sed -i.bak "s/version: '[0-9]*\.[0-9]*\.[0-9]*-alpha'/version: '$NEW_VERSION'/g" packages/mcp-server/src/index.ts -sed -i.bak "s/version: '[0-9]*\.[0-9]*\.[0-9]*-alpha'/version: '$NEW_VERSION'/g" packages/mcp-server/src/health-server.ts -sed -i.bak "s/toBe('[0-9]*\.[0-9]*\.[0-9]*-alpha')/toBe('$NEW_VERSION')/g" packages/mcp-server/tests/health-server.test.ts -rm packages/mcp-server/src/index.ts.bak packages/mcp-server/src/health-server.ts.bak packages/mcp-server/tests/health-server.test.ts.bak - - -echo -echo "🎉 Version updated to $NEW_VERSION" -echo -echo "📝 What was updated:" -echo " • Root package.json (source of truth)" -echo " • Docker image tags (can't import from package.json)" -echo " • README.md version badge" -echo " • MCP server hardcoded versions" -echo " • Web version utility constant" -echo -echo "📦 Everything else imports automatically from root package.json!" -echo -echo "🚀 Next: npm install && git add . && git commit -m \"Update version to v$NEW_VERSION\"" \ No newline at end of file +node --test tests/toolchain/version-contract.mjs >/dev/null +echo "Version $NEW_VERSION recorded everywhere (tests/toolchain/version-contract.mjs passes)." diff --git a/tests/toolchain/version-contract.mjs b/tests/toolchain/version-contract.mjs new file mode 100644 index 00000000..4e4a8e47 --- /dev/null +++ b/tests/toolchain/version-contract.mjs @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import { readFileSync, readdirSync, existsSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import test from 'node:test'; + +// Every place that carries the release version, with the exact text that must +// hold it. The release workflow refuses a tag whose manifests differ; this +// contract also keeps the runtime-reported versions (web, MCP, server log, +// installer fallback, local image tags, README badge) from going stale. +// scripts/update-version-minimal.sh updates all of them. +const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); +const read = path => readFileSync(resolve(root, path), 'utf8'); +const version = JSON.parse(read('package.json')).version; + +export const LOCATIONS = [ + ['packages/web/src/utils/version.ts', v => `APP_VERSION = '${v}'`], + ['packages/web/.env.example', v => `VITE_APP_VERSION=${v}`], + ['packages/mcp-server/src/index.ts', v => `version: '${v}'`], + ['packages/mcp-server/src/health-server.ts', v => `version: '${v}'`], + ['packages/mcp-server/tests/health-server.test.ts', v => `toBe('${v}')`], + ['packages/mcp-server/tests/neo4j-contract.test.ts', v => `version: '${v}'`], + ['packages/server/src/index.ts', v => `GraphDone Server v${v} starting`], + ['scripts/install.sh', v => `GRAPHDONE_VERSION="v${v}"`], + ['deployment/docker-compose.yml', v => `gd-core-api:${v}`], + ['deployment/docker-compose.yml', v => `gd-core-web:${v}`], + ['deployment/docker-compose.http.yml', v => `gd-core-api:${v}`], + ['deployment/docker-compose.http.yml', v => `gd-core-web:${v}`], + ['README.md', v => `badge/version-${v.replaceAll('-', '--')}-`], +]; + +test('every workspace manifest and the lockfile carry the root version', () => { + for (const dir of readdirSync(resolve(root, 'packages'))) { + const file = `packages/${dir}/package.json`; + if (existsSync(resolve(root, file))) assert.equal(JSON.parse(read(file)).version, version, file); + } + const lock = JSON.parse(read('package-lock.json')); + assert.equal(lock.version, version, 'package-lock.json'); + assert.equal(lock.packages[''].version, version, 'package-lock.json packages[""]'); + for (const [path, entry] of Object.entries(lock.packages)) { + if (/^packages\/[^/]+$/.test(path)) assert.equal(entry.version, version, `package-lock.json ${path}`); + } +}); + +test('runtime-reported versions, image tags and the badge carry the root version', () => { + const stale = LOCATIONS.filter(([path, expected]) => !read(path).includes(expected(version))) + .map(([path, expected]) => `${path}: expected "${expected(version)}"`); + assert.deepEqual(stale, []); +});