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
17 changes: 17 additions & 0 deletions .changeset/migrate-meta-chain-line-protocol-label.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@objectstack/cli": patch
---

`os migrate meta` no longer prints the protocol version under the word "runtime", where it read as the installed package version.

The chain line used to end `(runtime 17.0.0)`. That number is `PROTOCOL_VERSION` — the protocol major padded to a semver — and it is not, and never tracks, the version of the installed `@objectstack/cli` or `@objectstack/spec`. On a 17.3.0 install the line appeared beside the real package versions of the same upgrade session (`npm view`, the changelog), so it read as "your runtime is 17.0.0": an apparent downgrade or a stale install, neither of which was true.

The value was never wrong — the label and the semver form were. The line now states the fact in the protocol's own units:

```
Chain: protocol 17 → 17 (this runtime implements protocol 17)
```

The parenthetical is relabelled rather than dropped, because it carries a fact nothing else on screen does: when `--to` stops below this build's major, it is the only place the operator is told where the runtime actually stands (`Chain: protocol 16 → 16 (this runtime implements protocol 17)`).

The `--json` payload is deliberately untouched: its `runtime` key still carries the same padded protocol semver. Renaming a machine-readable key is a contract change owing a reader census and a deprecation window of its own, and it is tracked separately — an e2e pin now asserts the key's current value so that move cannot happen silently.
15 changes: 14 additions & 1 deletion packages/cli/src/commands/migrate/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,11 @@ export default class MigrateMeta extends Command {
await emitJson({
from: result.fromMajor,
to: result.toMajor,
// Deliberately NOT relabelled alongside the human line below:
// this is a machine-readable key on a published payload, so
// moving it is a contract change owing a reader census and a
// deprecation window of its own (#15585, option C). The value is
// the protocol major padded to a semver, not a package version.
runtime: PROTOCOL_VERSION,
applied: result.applied,
todos: result.todos,
Expand All @@ -350,7 +355,15 @@ export default class MigrateMeta extends Command {
}

printInfo(`Config: ${chalk.white(absolutePath)}`);
printInfo(`Chain: protocol ${fromMajor} → ${toMajor} (runtime ${PROTOCOL_VERSION})`);
// State this build's protocol major in the protocol's own units.
// `PROTOCOL_VERSION` is that major padded to a semver ('17.0.0'), never
// the installed package version -- printed as a bare semver under the
// word "runtime" it read as one, so on a 17.3.0 install the operator saw
// an apparent downgrade next to the real package versions of the same
// upgrade session. The fact itself is worth keeping: with `--to` below
// this build's major it is the only line saying where the runtime
// actually stands. So it is relabelled and de-padded, not dropped.
printInfo(`Chain: protocol ${fromMajor} → ${toMajor} (this runtime implements protocol ${PROTOCOL_MAJOR})`);
console.log('');

if (result.applied.length === 0 && result.todos.length === 0) {
Expand Down
69 changes: 69 additions & 0 deletions packages/cli/test/migrate-meta.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { dirname, join, resolve } from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { ObjectStackDefinitionSchema } from '@objectstack/spec';
import { PROTOCOL_MAJOR, PROTOCOL_VERSION } from '@objectstack/spec/kernel';
import { childEnv } from './helpers/serve-process.js';

const execFileP = promisify(execFile);
Expand Down Expand Up @@ -443,3 +444,71 @@ export default defineStack({
expect(refused, '`os validate` must still reject a retired key').toBe(true);
}, 180_000);
});

/**
* The chain line states this build's protocol in the protocol's own units.
*
* `PROTOCOL_VERSION` is the protocol major padded to a semver ('17.0.0') and is
* never the installed package version. Printed here as a bare semver under the
* word "runtime", it read as one: on a 17.3.0 install the operator saw
* "runtime 17.0.0" beside the real package versions of the same upgrade
* session, which reads as an apparent downgrade or a stale install. Nothing
* about the VALUE was wrong; the label and the semver FORM were.
*
* Both halves are asserted, because either one alone is satisfiable the wrong
* way. A line that merely stopped saying "runtime" could still print the padded
* semver in a version position; and a line that dropped the parenthetical
* altogether would lose the one fact it carries -- with `--to` stopping below
* this build's major it is the only place the operator is told where the
* runtime actually stands, which is why the third case drives exactly that.
*
* The `--json` `runtime` key is pinned UNCHANGED here on purpose. It is a
* machine-readable key on a published payload, so moving it is a contract
* change owing a reader census and a deprecation window of its own. This pin is
* what makes that move loud instead of silent.
*/
describe('os migrate meta — the chain line names the protocol, not a package version', () => {
const LABEL_CONFIG = `
export default {
manifest: { id: 'chain_label_e2e', name: 'Chain Label E2E', version: '1.0.0', type: 'app' },
objects: [{ name: 'label_ticket', label: 'Ticket', fields: { title: { type: 'text', label: 'Title' } } }],
};
`;
let labelDir: string;

beforeAll(() => {
labelDir = mkdtempSync(join(tmpdir(), 'os-migrate-meta-label-'));
writeFileSync(join(labelDir, 'objectstack.config.ts'), LABEL_CONFIG);
});

afterAll(() => {
try { rmSync(labelDir, { recursive: true, force: true }); } catch { /* ignore */ }
});

it("names this build's protocol major, in majors", async () => {
const stdout = await runMeta(['--from', String(PROTOCOL_MAJOR)], labelDir);
expect(stdout).toContain(
`Chain: protocol ${PROTOCOL_MAJOR} → ${PROTOCOL_MAJOR} (this runtime implements protocol ${PROTOCOL_MAJOR})`,
);
}, 120_000);

it('prints no padded protocol semver in the human output, under any label', async () => {
const stdout = await runMeta(['--from', String(PROTOCOL_MAJOR)], labelDir);
expect(stdout).not.toContain(PROTOCOL_VERSION);
expect(stdout).not.toMatch(/runtime \d+\.\d+\.\d+/);
}, 120_000);

it('still says where the runtime stands when --to stops below this build\'s major', async () => {
const below = PROTOCOL_MAJOR - 1;
const stdout = await runMeta(['--from', String(below), '--to', String(below)], labelDir);
expect(stdout).toContain(
`Chain: protocol ${below} → ${below} (this runtime implements protocol ${PROTOCOL_MAJOR})`,
);
expect(stdout).not.toContain(PROTOCOL_VERSION);
}, 120_000);

it('leaves the --json `runtime` key exactly as published', async () => {
const parsed = JSON.parse(await runMeta(['--from', String(PROTOCOL_MAJOR), '--json'], labelDir));
expect(parsed.runtime).toBe(PROTOCOL_VERSION);
}, 120_000);
});
Loading