mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-05-14 02:56:38 +02:00
* feat(#2975): adopt changeset-fragment workflow to eliminate CHANGELOG conflicts Two PRs that both edit `### Fixed` in CHANGELOG.md always conflict on merge. Recently bit on #2960/#2972 in the same session — fix-the-conflict-and-rebase tax. Replace the shared-file model with per-PR fragment files that never share lines. Implementation built TDD per #2975, vertical slices with structured-IR assertions throughout: scripts/changeset/parse.cjs - fragment text → typed record + frozen FRAGMENT_ERROR enum (8 tests) scripts/changeset/render.cjs - fragments → structured IR with Keep-a-Changelog section ordering (2 tests) scripts/changeset/serialize.cjs - IR ↔ markdown round-trip pair (parse(serialize(ir)) === ir, 3 tests) scripts/changeset/cli.cjs - file-I/O wrapper with --json mode; reads .changeset/, folds into CHANGELOG.md, deletes consumed fragments. Idempotent. (1 test) scripts/changeset/lint.cjs - pure verdict (changedFiles, labels) → { ok, reason } via LINT_REASON enum. Honors `no-changelog` label. (5 tests) scripts/changeset/new.cjs - fragment scaffolder with random adjective-noun-noun filename. Tests assert via parseFragment round-trip. (3 tests) Total: 22 tests, all assertions on typed structured fields. No regex on text, no String#includes on file content. Lint clean across 356 test files. Supporting: .changeset/README.md - format spec + workflow docs .changeset/eager-hawks-rally.md - dogfood fragment for THIS PR (will be the first thing the new release tool consumes) .github/workflows/changeset-required.yml - CI: every PR runs lint.cjs package.json - npm run changeset, changelog:render, lint:changeset CONTRIBUTING.md - new "CHANGELOG Entries — Drop a Fragment" section between PR Guidelines and Testing Standards Closes #2975 * fix(#2975): address CodeRabbit findings on changeset workflow 7 valid findings (4 Major, 3 Minor); all addressed: scripts/changeset/parse.cjs - Preserve fragment body verbatim. Previously body.trim() ate intentional leading whitespace (code blocks, etc.); now trim() is used only for the emptiness check, and a single trailing newline is stripped (the editor-added one) so well-formed fragments round-trip byte-for-byte. Added a regression test asserting a code-block-leading body is preserved. scripts/changeset/cli.cjs - Validate flag values during argument parsing. parseArgs now returns { ok, opts | error }; rejects `--repo` etc. with no following value or with another flag as the value. main() surfaces the error message before exiting 2. - Handle post-write fragment-deletion failures. After CHANGELOG.md is written, any unlink failure is captured into a structured deleteFailures list with reason 'fail_fragment_delete'; cmdRender returns exitCode=1 with the partial-failure detail instead of leaving the changelog updated and fragments behind (which would cause double-consumption on rerun). scripts/changeset/lint.cjs - Treat CHANGELOG.md as a linted user-facing path. Direct edits to CHANGELOG.md (the bypass route around the new workflow) now fail the lint with FAIL_MISSING_FRAGMENT. Added a regression test for that case. - Use cp.execFileSync instead of cp.execSync for the git diff call. Eliminates the shell-interpolation surface on GITHUB_BASE_REF; git's own arg parser remains the validator. scripts/changeset/new.cjs - Atomic fragment creation. existsSync() + writeFileSync was racy under concurrent invocations. Now writeFileSync uses { flag: 'wx' } which fails EEXIST on collision; the random-name retry loop catches EEXIST and re-rolls. Throws explicitly after 16 attempts rather than silently overwriting. .changeset/README.md - Add language tag `md` to the format example fence (markdownlint MD040). All 25 changeset tests pass; lint clean (356 test files, 0 violations). * fix(#2975): sanitize --type and validate flag values in new.cjs (CR fixes) Two CR findings on scripts/changeset/new.cjs: 1. (Minor) `type` was embedded in frontmatter without sanitization. A newline in the value (e.g. `--type 'Fixed\ntype: Added'`) would corrupt the fragment. scaffoldFragment now validates `type` against the Keep-a-Changelog ALLOWED_TYPES set BEFORE writing — same set parse.cjs uses on consume. Throws with a typed error referencing the allowed values; tests cover the newline case + 4 other non-allowed values. 2. (Minor) `--repo` (and other value-taking flags) without a value silently set opts.repo to undefined, which produced a cryptic ERR_INVALID_ARG_TYPE deep inside path.join. parseArgs now mirrors the cli.cjs convention: returns { ok, opts | error }, validates that the next token exists and is not itself another flag, and surfaces a precise "missing value for --repo" message before exit. Added 3 tests: missing-trailing-value, flag-as-value, well-formed. 29 tests pass across the changeset suite (4 new regression tests).
73 lines
3.0 KiB
JavaScript
73 lines
3.0 KiB
JavaScript
'use strict';
|
|
process.env.GSD_TEST_MODE = '1';
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('node:path');
|
|
|
|
const { serializeChangelog, parseChangelog } = require(path.join(__dirname, '..', 'scripts', 'changeset', 'serialize.cjs'));
|
|
|
|
// Round-trip property: serialize(IR) → parse(text) → IR equals original.
|
|
// Tests assert on the parsed IR shape, not the serialized text contents.
|
|
|
|
describe('changeset serialize: IR → markdown round-trip (#2975)', () => {
|
|
test('a single-section IR round-trips through serialize → parse', () => {
|
|
const ir = {
|
|
releaseHeader: { version: '1.0.0', date: '2026-01-01' },
|
|
sections: [
|
|
{ type: 'Fixed', bullets: [{ pr: 1, body: 'fix something.' }] },
|
|
],
|
|
priorChangelog: null,
|
|
};
|
|
const text = serializeChangelog(ir);
|
|
const back = parseChangelog(text);
|
|
|
|
assert.equal(back.releases[0].version, '1.0.0');
|
|
assert.equal(back.releases[0].date, '2026-01-01');
|
|
assert.equal(back.releases[0].sections.length, 1);
|
|
assert.equal(back.releases[0].sections[0].type, 'Fixed');
|
|
assert.equal(back.releases[0].sections[0].bullets.length, 1);
|
|
assert.equal(back.releases[0].sections[0].bullets[0].pr, 1);
|
|
});
|
|
});
|
|
|
|
describe('changeset serialize: multi-section + prior content (#2975)', () => {
|
|
const { serializeChangelog, parseChangelog } = require(require('node:path').join(__dirname, '..', 'scripts', 'changeset', 'serialize.cjs'));
|
|
|
|
test('round-trips an IR with three section types and multiple bullets per section', () => {
|
|
const ir = {
|
|
releaseHeader: { version: '1.42.0', date: '2026-05-01' },
|
|
sections: [
|
|
{ type: 'Added', bullets: [{ pr: 1, body: 'add A' }, { pr: 2, body: 'add B' }] },
|
|
{ type: 'Changed', bullets: [{ pr: 3, body: 'change C' }] },
|
|
{ type: 'Fixed', bullets: [{ pr: 4, body: 'fix D' }, { pr: 5, body: 'fix E' }] },
|
|
],
|
|
priorChangelog: null,
|
|
};
|
|
const back = parseChangelog(serializeChangelog(ir));
|
|
assert.equal(back.releases.length, 1);
|
|
assert.deepEqual(
|
|
back.releases[0].sections.map((s) => ({ type: s.type, prs: s.bullets.map((b) => b.pr) })),
|
|
[
|
|
{ type: 'Added', prs: [1, 2] },
|
|
{ type: 'Changed', prs: [3] },
|
|
{ type: 'Fixed', prs: [4, 5] },
|
|
],
|
|
);
|
|
});
|
|
|
|
test('prior CHANGELOG content survives serialize → parse as a separate release block', () => {
|
|
const priorText = '## [0.9.0] - 2025-12-01\n\n### Fixed\n\n- old fix (#100)\n';
|
|
const ir = {
|
|
releaseHeader: { version: '1.0.0', date: '2026-01-01' },
|
|
sections: [{ type: 'Added', bullets: [{ pr: 200, body: 'new feature' }] }],
|
|
priorChangelog: priorText,
|
|
};
|
|
const back = parseChangelog(serializeChangelog(ir));
|
|
assert.equal(back.releases.length, 2);
|
|
assert.equal(back.releases[0].version, '1.0.0');
|
|
assert.equal(back.releases[1].version, '0.9.0');
|
|
assert.equal(back.releases[1].sections[0].bullets[0].pr, 100);
|
|
});
|
|
});
|