diff --git a/.github/workflows/mcp-release-please.yml b/.github/workflows/mcp-release-please.yml index 2b3b0c17c3..3145169bab 100644 --- a/.github/workflows/mcp-release-please.yml +++ b/.github/workflows/mcp-release-please.yml @@ -52,6 +52,39 @@ jobs: manifest-file: .release-please-manifest.json token: ${{ secrets.GITHUB_TOKEN }} + # release-please's extra-files JSON-path updater doesn't reliably reach package-lock.json's + # per-workspace version fields (keys containing "/", nested under a manifest-mode component + # block) -- confirmed empirically (both the mcp-v0.7.0 dry run's precursor and the first two + # engine-v0.2.0 dry runs left it stale, breaking npm ci with "Missing: @ from + # lock file"). Patches it directly on whichever release branch(es) release-please just + # created/updated, using the same branch naming convention its own commits already rely on. + - name: Sync package-lock.json on any release branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git remote set-url origin "https://github.com/${GITHUB_REPOSITORY}.git" + gh auth setup-git + for component in mcp engine; do + branch="release-please--branches--main--components--${component}" + if ! git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then + echo "No release branch for $component, skipping." + continue + fi + git fetch origin "$branch" + git checkout -B "sync-check-${component}" "origin/$branch" + node scripts/sync-release-lockfile-versions.mjs packages/gittensory-mcp packages/gittensory-engine + if git diff --quiet package-lock.json; then + echo "package-lock.json already in sync on $branch." + else + git add package-lock.json + git commit -m "chore(release): sync package-lock.json" + git push origin "HEAD:$branch" + fi + done + # release-please creates the component tag + GitHub Release with GITHUB_TOKEN, which (by # GitHub's recursion-prevention rule) does NOT fire push/tag-based workflows. workflow_dispatch # IS exempt from that rule, so dispatch the OIDC publish workflow explicitly, telling it diff --git a/package-lock.json b/package-lock.json index 98c7ab69dc..7ac44d3355 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15899,7 +15899,7 @@ "version": "0.7.0", "license": "AGPL-3.0-only", "dependencies": { - "@jsonbored/gittensory-engine": "^0.1.0", + "@jsonbored/gittensory-engine": ">=0.1.0 <1.0.0", "@modelcontextprotocol/sdk": "1.29.0", "zod": "^4.4.3" }, @@ -15915,7 +15915,7 @@ "version": "0.1.0", "license": "AGPL-3.0-only", "dependencies": { - "@jsonbored/gittensory-engine": "0.1.0" + "@jsonbored/gittensory-engine": ">=0.1.0 <1.0.0" }, "bin": { "gittensory-miner": "bin/gittensory-miner.js" diff --git a/packages/gittensory-engine/src/version.ts b/packages/gittensory-engine/src/version.ts index be6f0b62b7..2a4c8fbbab 100644 --- a/packages/gittensory-engine/src/version.ts +++ b/packages/gittensory-engine/src/version.ts @@ -1,5 +1,4 @@ -/** - * Published semver of `@jsonbored/gittensory-engine`. Keep in lockstep with `package.json` `version` - * (enforced by `test/unit/engine-version.test.ts`). - */ -export const ENGINE_VERSION = "0.1.0"; +import ownPackageJson from "../package.json" with { type: "json" }; + +/** Published semver of `@jsonbored/gittensory-engine`, derived from this package's own package.json. */ +export const ENGINE_VERSION: string = ownPackageJson.version; diff --git a/packages/gittensory-mcp/package.json b/packages/gittensory-mcp/package.json index 20ffe8a77f..f623d7bca7 100644 --- a/packages/gittensory-mcp/package.json +++ b/packages/gittensory-mcp/package.json @@ -38,7 +38,7 @@ "build": "node --check bin/gittensory-mcp.js && node --check lib/local-branch.js && node --check scripts/gittensor-score-preview.mjs" }, "dependencies": { - "@jsonbored/gittensory-engine": "^0.1.0", + "@jsonbored/gittensory-engine": ">=0.1.0 <1.0.0", "@modelcontextprotocol/sdk": "1.29.0", "zod": "^4.4.3" }, diff --git a/packages/gittensory-miner/package.json b/packages/gittensory-miner/package.json index fa11aa7ad3..f05833fcc8 100644 --- a/packages/gittensory-miner/package.json +++ b/packages/gittensory-miner/package.json @@ -34,7 +34,7 @@ "build": "node --check bin/gittensory-miner.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/event-ledger-cli.js && node --check lib/claim-ledger.js && node --check lib/claim-ledger-expiry.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-discovery.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/plan-store-cli.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/governor-ledger-cli.js && node --check lib/manage-status.js && node --check lib/manage-poll.js && node --check lib/status.js && node --check lib/laptop-init.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-task-generation.js && node --check lib/calibration-types.js && node --check lib/calibration.js" }, "dependencies": { - "@jsonbored/gittensory-engine": "0.1.0" + "@jsonbored/gittensory-engine": ">=0.1.0 <1.0.0" }, "engines": { "node": ">=22.13.0" diff --git a/release-please-config.json b/release-please-config.json index de61be1350..0583fc66ba 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -4,26 +4,12 @@ "packages/gittensory-mcp": { "release-type": "node", "component": "mcp", - "package-name": "@jsonbored/gittensory-mcp", - "extra-files": [ - { - "type": "json", - "path": "package-lock.json", - "jsonpath": "$.packages['packages/gittensory-mcp'].version" - } - ] + "package-name": "@jsonbored/gittensory-mcp" }, "packages/gittensory-engine": { "release-type": "node", "component": "engine", - "package-name": "@jsonbored/gittensory-engine", - "extra-files": [ - { - "type": "json", - "path": "package-lock.json", - "jsonpath": "$.packages['packages/gittensory-engine'].version" - } - ] + "package-name": "@jsonbored/gittensory-engine" } }, "include-component-in-tag": true, diff --git a/scripts/sync-release-lockfile-versions.mjs b/scripts/sync-release-lockfile-versions.mjs new file mode 100644 index 0000000000..cb7c0712c0 --- /dev/null +++ b/scripts/sync-release-lockfile-versions.mjs @@ -0,0 +1,43 @@ +#!/usr/bin/env node +// release-please's `extra-files` JSON-path updater doesn't reliably reach package-lock.json's +// per-workspace version fields, whose keys contain slashes (e.g. "packages/gittensory-engine") +// nested under a manifest-mode component's own release-please-config.json block -- confirmed +// empirically (mcp-v0.7.0/engine-v0.2.0 dry runs both left package-lock.json un-synced, breaking +// `npm ci` with "Missing: @jsonbored/gittensory-engine@0.1.0 from lock file"). This does the same +// single-line replacement a human would make by hand: find the workspace's own manifest-mirror +// entry, replace just its "version" value. No JSON.parse/stringify round-trip on the whole +// multi-thousand-line lockfile, which would risk reordering/reformatting far beyond the one line +// that actually changed. +import { readFileSync, writeFileSync } from "node:fs"; + +const targets = process.argv.slice(2); +if (targets.length === 0) { + console.error("Usage: node sync-release-lockfile-versions.mjs [ ...]"); + process.exit(1); +} + +const lockPath = "package-lock.json"; +let content = readFileSync(lockPath, "utf8"); +let changed = false; + +for (const workspacePath of targets) { + const version = JSON.parse(readFileSync(`${workspacePath}/package.json`, "utf8")).version; + const escapedKey = workspacePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + // Anchors on the workspace's own block header + its "name" line (both stable, unique) so this + // can't accidentally match a different package's "version" line elsewhere in the file. + const pattern = new RegExp(`("${escapedKey}":\\s*\\{\\s*\\n\\s*"name":[^\\n]*\\n\\s*"version":\\s*")[^"]*(")`); + if (!pattern.test(content)) { + console.error(`${workspacePath}: pattern not found in ${lockPath} -- nothing changed.`); + continue; + } + const updated = content.replace(pattern, `$1${version}$2`); + if (updated === content) { + console.log(`${workspacePath}: already at ${version}.`); + } else { + content = updated; + changed = true; + console.log(`${workspacePath}: synced to ${version}.`); + } +} + +if (changed) writeFileSync(lockPath, content); diff --git a/test/unit/gittensory-engine-scaffold.test.ts b/test/unit/gittensory-engine-scaffold.test.ts index 5307c6ca31..b7da0b3fbb 100644 --- a/test/unit/gittensory-engine-scaffold.test.ts +++ b/test/unit/gittensory-engine-scaffold.test.ts @@ -7,7 +7,7 @@ import enginePkg from "../../packages/gittensory-engine/package.json"; describe("gittensory-engine package scaffold", () => { it("declares the published package identity", () => { expect(enginePkg.name).toBe("@jsonbored/gittensory-engine"); - expect(enginePkg.version).toBe("0.1.0"); + expect(enginePkg.version).toMatch(/^\d+\.\d+\.\d+$/); expect(enginePkg.type).toBe("module"); expect(enginePkg.license).toBe("AGPL-3.0-only"); expect(enginePkg.publishConfig?.access).toBe("public");