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
8 changes: 4 additions & 4 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ jobs:
node-version: "24"
- run: npm ci
working-directory: mcp-server
- name: Sync manifest.json version from release tag
- name: Sync and verify all release metadata from tag
working-directory: mcp-server
env:
TAG_NAME: ${{ github.event.release.tag_name }}
run: jq --arg v "${TAG_NAME#v}" '.version = $v' manifest.json > manifest.tmp && mv manifest.tmp manifest.json
run: node ../scripts/sync-release-version.mjs "$TAG_NAME"
- run: npx mcpb pack
working-directory: mcp-server
- uses: actions/upload-artifact@v7
Expand Down Expand Up @@ -59,9 +59,9 @@ jobs:
registry-url: "https://registry.npmjs.org"
- run: npm ci
working-directory: mcp-server
- name: Sync version from release tag
- name: Sync and verify all release metadata from tag
working-directory: mcp-server
run: npm version "${TAG_NAME#v}" --no-git-tag-version --allow-same-version
run: node ../scripts/sync-release-version.mjs "$TAG_NAME"
env:
TAG_NAME: ${{ github.event.release.tag_name }}
- name: Publish to npm
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: "22"
- run: node --test scripts/sync-release-version.test.mjs
- run: npm ci
- run: npm ci
working-directory: mcp-server
Expand Down
6 changes: 6 additions & 0 deletions docs/0-requirements.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -667,3 +667,9 @@ embedding が失敗した record は incomplete と分かる形で残し、次
新 API は `memory_history`、`record_source_use`、`record_outcome`。設定値、上限、ID/version、batch/idempotency、ledger、error、0008先行移行、既存行 backfill と artifact の契約は [2-feedback-memory.ja.md](2-feedback-memory.ja.md) に定義する。会話/本文/credential の複製は行わず、品質向上の主張は別評価を必要とする。

#259 の版契約: live inline doc/wiki は返した本文の SHA-256 を version とし、index snapshot と provenance を分ける。同じ索引 timestamp でも live 本文更新は別 source_id にする。本文を memory に複製しない。

## リリース metadata の一致(Issue #261)

両 CD packaging job は bridge package、lock root、MCP Bundle manifest、MCP registry の npm version を同じ検証済み release tag から同期し、pack / publish 前に一致を assert する。依存版、schema、runtime 条件、Worker の挙動を保持する。対象 field と検証 command は [版数と公開成果物](installation.ja.md#versioning-and-published-artifacts) に定義する。commit 済み版は placeholder のままとし、trusted publishing は別変更とする。

source の `docs/_Sidebar.md` は既存 Feedback Memory EN/JA ページを参照し、release 後の Wiki navigation に含める。
6 changes: 6 additions & 0 deletions docs/0-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -672,3 +672,9 @@ Implement retrieval history, selected/validated/used records, retrieved/usage ac
The APIs are `memory_history`, `record_source_use`, and `record_outcome`. [2-feedback-memory.md](2-feedback-memory.md) owns constants, bounds, source version identity, batch/idempotency, ledger, errors, migration 0008-before-deployment, historical backfill and bridge artifacts. Do not duplicate conversations, bodies or credentials. Claims of retrieval quality improvement require separate evaluation.

Issue #259 version contract: live inline doc/wiki uses the returned-text SHA-256 version and provenance separate from the index snapshot. Changed live text gets a different source ID even at the same index timestamp. Do not duplicate the body in memory.

## Release metadata consistency (Issue #261)

Both CD packaging jobs must derive the bridge package, lock root, MCP Bundle manifest and MCP registry npm versions from the same validated release tag and assert agreement before packing or publishing. Preserve dependency versions, schemas, runtime constraints and Worker behavior. [Versioning and published artifacts](installation.md#versioning-and-published-artifacts) owns the affected fields and validation command. Committed versions remain placeholders; trusted publishing is a separate change.

The source `docs/_Sidebar.md` must link the existing Feedback Memory EN/JA pages so release wiki navigation includes them.
1 change: 1 addition & 0 deletions docs/_Sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
- Installation: [[EN|installation]] / [[JA|installation.ja]]
- Requirements: [[EN|0-requirements]] / [[JA|0-requirements.ja]]
- Memory Philosophy: [[EN|1-memory-philosophy]] / [[JA|1-memory-philosophy.ja]]
- Feedback Memory: [[EN|2-feedback-memory]] / [[JA|2-feedback-memory.ja]]
27 changes: 15 additions & 12 deletions docs/installation.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,25 +462,28 @@ POST /admin/backfill-issue-index?repo=owner/repo

## Versioning and published artifacts

公開される成果物の版数は **GitHub Release の tag** から来る。リポジトリにコミットされている `version` フィールドはどれもソースではない。
公開 bridge の版は **GitHub Release tag** から生成する。tag は `v` に canonical SemVer を続けた形(例 `v0.12.0`、`v1.2.3-rc.1`)。commit 済みの版は placeholder とする。

`.github/workflows/cd.yml` は `release: published` を契機に走り、pack / publish の前に tag から版数を振り直す。
`.github/workflows/cd.yml` の独立した両 packaging job は、pack / publish 前に `mcp-server/` から同じ command を呼ぶ。

- npm — `mcp-server/` で `npm version "${TAG_NAME#v}" --no-git-tag-version --allow-same-version`
- `.mcpb` bundle — `mcp-server/` で `jq --arg v "${TAG_NAME#v}" '.version = $v' manifest.json`
```sh
node ../scripts/sync-release-version.mjs "$TAG_NAME"
```

したがって作業ツリー上の `version` 値は公開成果物に一切届かない。
script は tag 全体と metadata 4 ファイルを検証してから書き込み、次の field だけを同期する。書き込み後に読み直し、全項目が tag の `v` を除いた版に一致することを assert する。

| 場所 | 役割 |
| 場所 | 更新する field |
|---|---|
| `package.json`(root) | worker の build 用のみ。`private: true` で公開されない |
| `mcp-server/package.json` | placeholder。公開時に tag から上書きされる |
| `mcp-server/manifest.json` | placeholder。公開時に tag から上書きされる |
| `mcp-server/server.json` | npm tarball に同梱される MCP registry metadata。release workflow は読みも書き換えもしない |
| `mcp-server/package.json` | `version`。実行時 MCP serverInfo / remote clientInfo もこの版を使う |
| `mcp-server/package-lock.json` | `version` と `packages[""].version` |
| `mcp-server/manifest.json` | `version` |
| `mcp-server/server.json` | `version` と全 `registryType: "npm"` package entry の `version` |

依存版、schema 識別子、Node 条件、非 npm registry entry は保持する。private な root package の Worker 版は対象外。不正 tag、読取不可・不正 JSON、version field 欠落、書込後の不一致は pack / publish step より前に job を止める。既存の npm 認証と release asset upload は維持する。

これらの値が公開版数より遅れているのは設計どおりの状態であって、不整合ではない。手で合わせる対象でもない — 次のリリースがどのみち自身の tag から上書きするので、手編集は「このファイルが権威である」という誤った印象を残すだけになる。
`--check` を付けると生成済み metadata の一致だけを検証し、書き込まない。`node --test scripts/sync-release-version.test.mjs` と CI で、複数版、書込前の拒否、対象外 field 保持、retry、CD と同じ相対 command を検証する。

実際に公開されている版数を見るには、release tag(`gh release list`)か registry(`npm view github-rag-mcp version`)を参照する。
作業ツリーの placeholder が公開版より遅れていてもよいが、**各公開 artifact 内の metadata は tag と一致する必要がある**。一つのファイルだけの同期では足りない。公開版の確認には release tag または npm registry を使う。この同期処理自体は publish / tag 作成 / Worker deploy を行わない。

## Troubleshooting

Expand Down
27 changes: 15 additions & 12 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,25 +462,28 @@ Operational notes:

## Versioning and published artifacts

The version of every published artifact comes from the **GitHub Release tag**. No `version` field committed in this repository is the source.
Published bridge versions come from a **GitHub Release tag**, using `v` followed by canonical SemVer (for example `v0.12.0` or `v1.2.3-rc.1`). Committed versions are placeholders.

`.github/workflows/cd.yml` runs on `release: published` and rewrites the version from the tag before it packs or publishes:
Both isolated packaging jobs in `.github/workflows/cd.yml` run the same command from `mcp-server/` before packing or publishing:

- npm — `npm version "${TAG_NAME#v}" --no-git-tag-version --allow-same-version` in `mcp-server/`
- `.mcpb` bundle — `jq --arg v "${TAG_NAME#v}" '.version = $v' manifest.json` in `mcp-server/`
```sh
node ../scripts/sync-release-version.mjs "$TAG_NAME"
```

So the `version` values sitting in the working tree never reach a published artifact:
The script validates the entire tag and all four metadata files before writing. It synchronizes only these fields, then re-reads the files and asserts that every field equals the tag without its `v` prefix:

| Location | Role |
| Location | Fields rewritten |
|---|---|
| `package.json` (root) | worker build only, `private: true`, never published |
| `mcp-server/package.json` | placeholder, overwritten from the tag at publish time |
| `mcp-server/manifest.json` | placeholder, overwritten from the tag at publish time |
| `mcp-server/server.json` | MCP registry metadata carried in the npm tarball; the release workflow neither reads nor rewrites it |
| `mcp-server/package.json` | `version`, also used by runtime MCP serverInfo and remote clientInfo |
| `mcp-server/package-lock.json` | `version` and `packages[""].version` |
| `mcp-server/manifest.json` | `version` |
| `mcp-server/server.json` | `version` and every `registryType: "npm"` package entry's `version` |

Dependency versions, schema identifiers, Node requirements and non-npm registry entries are preserved. The private root package's Worker version is outside this contract. Invalid tags, unreadable/malformed metadata, missing version fields or mismatched on-disk output fail the job before its pack/publish step. Existing npm authentication and release asset upload remain unchanged.

These values are expected to lag behind the published version. That divergence is the designed state, not a defect, and it is not something to repair by hand: the next release overwrites them from its own tag regardless, so a manual edit only leaves the impression that the file is authoritative.
Add `--check` to verify already-generated metadata without writing. Local fixture regression coverage runs with `node --test scripts/sync-release-version.test.mjs` and in CI; it checks multiple versions, rejection before writes, field preservation, replay and the actual CD relative command.

To read the version that is actually published, look at the release tag (`gh release list`) or the registry (`npm view github-rag-mcp version`).
Working-tree placeholders may lag behind a release. Metadata **inside each published artifact** must agree with the tag; updating one file alone is insufficient. To inspect the published version, use the release tag or npm registry. This synchronization does not publish, create tags or deploy the Worker.

## Troubleshooting

Expand Down
119 changes: 119 additions & 0 deletions scripts/sync-release-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env node
import { readFile, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { isDeepStrictEqual } from 'node:util';

const FILES = ['package.json', 'package-lock.json', 'manifest.json', 'server.json'];
const CORE = '(0|[1-9][0-9]*)';
const TAG_PATTERN = new RegExp(`^v${CORE}\\.${CORE}\\.${CORE}(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$`);
const DEFAULT_PACKAGE_DIR = fileURLToPath(new URL('../mcp-server/', import.meta.url));

export function parseReleaseTag(tag) {
const match = typeof tag === 'string' ? TAG_PATTERN.exec(tag) : null;
if (!match || match[0] !== tag || (match[4] && match[4].split('.').some(part => /^0[0-9]+$/.test(part)))) {
throw new Error('Release tag must be v-prefixed canonical SemVer');
}
return tag.slice(1);
}

function object(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}

function versionPaths(name, data) {
if (!object(data) || typeof data.version !== 'string') throw new Error(`Invalid release metadata: ${name}`);
const paths = [['version']];
if (name === 'package-lock.json') {
if (!object(data.packages) || !object(data.packages['']) || typeof data.packages[''].version !== 'string') {
throw new Error('Invalid package-lock root version');
}
paths.push(['packages', '', 'version']);
}
if (name === 'server.json') {
if (!Array.isArray(data.packages)) throw new Error('Invalid registry packages');
const npm = data.packages.flatMap((entry, index) => object(entry) && entry.registryType === 'npm' ? [index] : []);
if (!npm.length || npm.some(index => typeof data.packages[index].version !== 'string')) {
throw new Error('Registry metadata must contain versioned npm entries');
}
for (const index of npm) paths.push(['packages', index, 'version']);
}
return paths;
}

function get(data, path) { return path.reduce((value, key) => value[key], data); }
function set(data, path, value) {
const parent = path.slice(0, -1).reduce((current, key) => current[key], data);
parent[path.at(-1)] = value;
}

async function load(packageDir) {
const records = [];
for (const name of FILES) {
let text, data;
try {
text = await readFile(resolve(packageDir, name), 'utf8');
data = JSON.parse(text);
} catch { throw new Error(`Cannot read release metadata: ${name}`); }
records.push({ name, text, data, paths: versionPaths(name, data) });
}
return records;
}

function assertVersion(records, version) {
for (const record of records) {
for (const path of record.paths) {
if (get(record.data, path) !== version) throw new Error(`Release version mismatch: ${record.name}:${JSON.stringify(path)}`);
}
}
}

export async function syncReleaseVersion({ tag, packageDir = DEFAULT_PACKAGE_DIR, checkOnly = false }) {
const version = parseReleaseTag(tag);
const records = await load(packageDir);
if (checkOnly) {
assertVersion(records, version);
return { version, checkedFields: records.reduce((count, record) => count + record.paths.length, 0), changedFiles: 0 };
}

// Validate every input and prepare every change before writing any file.
const planned = records.map(record => {
const next = structuredClone(record.data);
for (const path of record.paths) set(next, path, version);
const restored = structuredClone(next);
for (const path of record.paths) set(restored, path, get(record.data, path));
if (!isDeepStrictEqual(restored, record.data)) throw new Error('Unexpected change outside release version fields');
return { ...record, next, output: JSON.stringify(next, null, 2) + '\n' };
});
let changedFiles = 0;
for (const record of planned) {
if (record.output === record.text) continue;
await writeFile(resolve(packageDir, record.name), record.output, 'utf8');
changedFiles++;
}

// Re-read on-disk metadata; a packaging job must stop on any disagreement.
const written = await load(packageDir);
assertVersion(written, version);
for (let index = 0; index < planned.length; index++) {
if (!isDeepStrictEqual(written[index].data, planned[index].next)) throw new Error('Release metadata write verification failed');
}
return { version, checkedFields: written.reduce((count, record) => count + record.paths.length, 0), changedFiles };
}

async function main(args) {
let tag, packageDir = DEFAULT_PACKAGE_DIR, checkOnly = false;
for (let index = 0; index < args.length; index++) {
const arg = args[index];
if (arg === '--check' && !checkOnly) checkOnly = true;
else if (arg === '--package-dir' && index + 1 < args.length) packageDir = resolve(args[++index]);
else if (!tag && !arg.startsWith('--')) tag = arg;
else throw new Error('Usage: sync-release-version.mjs <v-semver-tag> [--check] [--package-dir directory]');
}
const result = await syncReleaseVersion({ tag, packageDir, checkOnly });
console.log(`Release metadata ${checkOnly ? 'verified' : 'synchronized'}: ${result.version}; ${result.checkedFields} fields; ${result.changedFiles} changed files`);
}

if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main(process.argv.slice(2)).catch(error => { console.error(error.message); process.exitCode = 1; });
}
Loading
Loading