Skip to content

Commit 8aaad38

Browse files
hotlongclaude
andcommitted
build(releases): add objectui-range.mjs — frontend delta for a version range
The aggregation helper docs/releases-maintenance.md describes: given two framework revs (or explicit objectui SHAs), resolve the pinned .objectui-sha at each and print objectui's feat/fix commits for that range, grouped by type with the largest touched areas — ready to paste into a release page's Console section. Supports --json/--from/--to/--all and degrades to printing just the SHA range when no ../objectui checkout is present. Verified end-to-end against scratch repos; the maintenance doc now points at the script. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SXFPRKA2ixrZmRFwK7Cxh6
1 parent 87aa2ce commit 8aaad38

2 files changed

Lines changed: 166 additions & 8 deletions

File tree

docs/releases-maintenance.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,20 @@ platform version maps to.
7070

7171
Backend content comes from the spec/package changesets. Frontend content comes from
7272
objectui's history for the SHA range bundled in that release. Because `.objectui-sha`
73-
is version-controlled, the range for any two release points is:
73+
is version-controlled, `scripts/objectui-range.mjs` computes that range from any two
74+
framework revisions and prints the feat/fix commits grouped by type + the largest
75+
touched areas — ready to paste into a Console section:
7476

7577
```bash
76-
# objectui commit range bundled between two framework revisions
77-
OLD=$(git show <old-rev>:.objectui-sha)
78-
NEW=$(git show <new-rev>:.objectui-sha)
79-
git -C ../objectui log --no-merges --format='- %s' "$OLD..$NEW" | grep -E '^- (feat|fix)'
78+
# frontend delta bundled between two framework revisions (needs ../objectui)
79+
node scripts/objectui-range.mjs <old-rev> <new-rev> # e.g. the two release commits
80+
node scripts/objectui-range.mjs --from <sha> --to <sha> --json # explicit SHAs / tooling
8081
```
8182

82-
The framework changesets also embed companion frontend notes inline ("Companion
83-
objectui PR ships…", renderer notes), which are enough to write an accurate Console
84-
section without the objectui checkout.
83+
Without an objectui checkout it still prints the SHA range to inspect. The framework
84+
changesets also embed companion frontend notes inline ("Companion objectui PR
85+
ships…", renderer notes), which are enough to write an accurate Console section on
86+
their own.
8587

8688
## Drift guard
8789

scripts/objectui-range.mjs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env node
2+
// objectui-range — summarize the frontend (objectui) delta bundled between two
3+
// framework revisions, ready to paste into a release page's Console section.
4+
//
5+
// The platform is one version-locked train and the Console UI is frozen into
6+
// @objectstack/console at the objectui commit pinned in .objectui-sha. To learn
7+
// "what did the frontend change between platform version A and B", you diff the
8+
// pinned SHA at two framework revisions and read objectui's log for that range.
9+
// This script does exactly that — the aggregation layer described in
10+
// docs/releases-maintenance.md.
11+
//
12+
// Usage:
13+
// node scripts/objectui-range.mjs <old-rev> [new-rev]
14+
// old-rev / new-rev are FRAMEWORK git refs (tags, branches, SHAs).
15+
// new-rev defaults to the working-tree .objectui-sha (the current pin).
16+
// node scripts/objectui-range.mjs --from <objectui-sha> --to <objectui-sha>
17+
// skip the framework lookup; use explicit objectui SHAs.
18+
// … --json emit structured JSON instead of markdown
19+
// … --all include every conventional-commit type (default: feat + fix)
20+
//
21+
// Env:
22+
// OBJECTUI_ROOT=/path/to/objectui (default: ../objectui, like bump-objectui.sh)
23+
import { execFileSync } from 'node:child_process';
24+
import { fileURLToPath } from 'node:url';
25+
import { dirname, join } from 'node:path';
26+
import { existsSync, readFileSync } from 'node:fs';
27+
28+
const FRAMEWORK_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
29+
const OBJECTUI_ROOT =
30+
process.env.OBJECTUI_ROOT || join(FRAMEWORK_ROOT, '..', 'objectui');
31+
32+
const argv = process.argv.slice(2);
33+
const has = (f) => argv.includes(f);
34+
const val = (f) => {
35+
const i = argv.indexOf(f);
36+
return i >= 0 ? argv[i + 1] : undefined;
37+
};
38+
const positional = argv.filter(
39+
(a, i) => !a.startsWith('--') && argv[i - 1] !== '--from' && argv[i - 1] !== '--to',
40+
);
41+
42+
const JSON_OUT = has('--json');
43+
const ALL_TYPES = has('--all');
44+
45+
if (has('-h') || has('--help')) {
46+
console.log(readFileSync(fileURLToPath(import.meta.url), 'utf8').split('\n')
47+
.filter((l) => l.startsWith('//')).map((l) => l.slice(3)).join('\n'));
48+
process.exit(0);
49+
}
50+
51+
function die(msg) {
52+
console.error(`✗ objectui-range: ${msg}`);
53+
process.exit(1);
54+
}
55+
56+
function git(cwd, args) {
57+
return execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8' }).trim();
58+
}
59+
60+
// Resolve the objectui SHA pinned at a given framework rev (or the working tree).
61+
function pinAt(rev) {
62+
if (rev === undefined) {
63+
const p = join(FRAMEWORK_ROOT, '.objectui-sha');
64+
if (!existsSync(p)) die('.objectui-sha not found in the working tree');
65+
return readFileSync(p, 'utf8').trim();
66+
}
67+
try {
68+
return git(FRAMEWORK_ROOT, ['show', `${rev}:.objectui-sha`]).trim();
69+
} catch {
70+
return die(`cannot read .objectui-sha at framework rev '${rev}'`);
71+
}
72+
}
73+
74+
let fromSha = val('--from');
75+
let toSha = val('--to');
76+
if (!fromSha || !toSha) {
77+
if (positional.length < 1) {
78+
die('need <old-rev> [new-rev], or --from <sha> --to <sha>. See --help/header.');
79+
}
80+
fromSha = pinAt(positional[0]);
81+
toSha = pinAt(positional[1]); // undefined → working-tree pin
82+
}
83+
84+
if (!existsSync(join(OBJECTUI_ROOT, '.git'))) {
85+
die(
86+
`no objectui checkout at ${OBJECTUI_ROOT}.\n` +
87+
` Clone it as a sibling, or set OBJECTUI_ROOT=/path/to/objectui.\n` +
88+
` Range to inspect once available: ${fromSha.slice(0, 12)}..${toSha.slice(0, 12)}`,
89+
);
90+
}
91+
92+
if (fromSha === toSha) {
93+
console.log(`objectui unchanged (${fromSha.slice(0, 12)}) — no frontend delta in this range.`);
94+
process.exit(0);
95+
}
96+
97+
let rawLog;
98+
try {
99+
rawLog = git(OBJECTUI_ROOT, [
100+
'log', '--no-merges', '--format=%H%x09%s', `${fromSha}..${toSha}`,
101+
]);
102+
} catch {
103+
die(
104+
`cannot walk ${fromSha.slice(0, 12)}..${toSha.slice(0, 12)} in ${OBJECTUI_ROOT}.\n` +
105+
` Fetch it: git -C ${OBJECTUI_ROOT} fetch --all`,
106+
);
107+
}
108+
109+
const CC = /^(feat|fix|refactor|perf|docs|test|chore|build|ci|style|revert)(?:\(([^)]+)\))?(!)?:\s*(.+)$/;
110+
const KEEP = ALL_TYPES ? null : new Set(['feat', 'fix']);
111+
112+
const commits = [];
113+
for (const line of rawLog.split('\n').filter(Boolean)) {
114+
const tab = line.indexOf('\t');
115+
const sha = line.slice(0, tab);
116+
const subject = line.slice(tab + 1);
117+
const m = subject.match(CC);
118+
const type = m ? m[1] : 'other';
119+
const scope = m ? m[2] || '' : '';
120+
const desc = m ? m[4] : subject;
121+
if (KEEP && !KEEP.has(type)) continue;
122+
commits.push({ sha, type, scope, desc, subject });
123+
}
124+
125+
// Top touched areas (by scope) across the kept commits — the "big picture" line.
126+
const areaCounts = {};
127+
for (const c of commits) if (c.scope) areaCounts[c.scope] = (areaCounts[c.scope] || 0) + 1;
128+
const topAreas = Object.entries(areaCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
129+
130+
const byType = { feat: [], fix: [] };
131+
for (const c of commits) (byType[c.type] || (byType[c.type] = [])).push(c);
132+
133+
if (JSON_OUT) {
134+
console.log(JSON.stringify(
135+
{ from: fromSha, to: toSha, count: commits.length, topAreas, commits }, null, 2,
136+
));
137+
process.exit(0);
138+
}
139+
140+
const line = (c) => `- ${c.scope ? `**${c.scope}** — ` : ''}${c.desc}`;
141+
const out = [];
142+
out.push(`<!-- objectui ${fromSha.slice(0, 12)}..${toSha.slice(0, 12)}${commits.length} commit(s) -->`);
143+
if (topAreas.length) {
144+
out.push('', `_Largest areas: ${topAreas.map(([a, n]) => `${a} (${n})`).join(', ')}_`);
145+
}
146+
if (byType.feat?.length) {
147+
out.push('', '### Features', ...byType.feat.map(line));
148+
}
149+
if (byType.fix?.length) {
150+
out.push('', '### Fixes', ...byType.fix.map(line));
151+
}
152+
const extra = Object.keys(byType).filter((t) => t !== 'feat' && t !== 'fix' && byType[t].length);
153+
for (const t of extra) out.push('', `### ${t}`, ...byType[t].map(line));
154+
if (commits.length === 0) out.push('', '_No feat/fix commits in range (try --all)._');
155+
156+
console.log(out.join('\n'));

0 commit comments

Comments
 (0)