c2ebb1ba160e421bd5f6d33007db6f462323d87a
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6f2520786d |
feat(#498): single Package Identity seam for /gsd:update + fix runtime undefined-name bug (#499)
* feat(#498): generated package-identity seam derived from package.json Introduce a single source for GSD's published-package coordinates: scripts/generate-package-identity.cjs (pure deriveIdentity + formatManualInstall + render) emits the generated get-shit-done/bin/lib/package-identity.cjs with values baked from package.json at build time. Baking is required because the installed tree carries only a synthetic {"type":"commonjs"} package.json, so a runtime require('package.json').name resolves to undefined (#378). Reconciles Wired into npm run build; a parity test fails CI if the committed file drifts from package.json. Refs #498 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#498): repoint update worker + check-latest-version at the seam - check-latest-version.cjs sources PACKAGE_NAME from the package-identity seam instead of a re-typed literal (single source; #2992's constant guarantee is preserved since the seam bakes from package.json). - gsd-check-update-worker.js no longer does require('../package.json').name (resolved to undefined in the installed tree → background update check silently broken, #378). It now delegates the latest-version lookup to checkLatestVersion(), collapsing the duplicated npm-view call onto the single deterministic adapter and inheriting its typed {ok,version,reason} surface. - Move the PR #3102 Windows shell-gate contract test onto execNpm (where the spawn now lives) and assert the worker no longer spawns npm directly. - Rewrite the #378 contract: worker must NOT use require(package.json).name and must delegate; check-latest-version PACKAGE_NAME is single-sourced from the seam. Fixes #378-class runtime breakage. Refs #498 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#498): changeset for package-identity seam + update-check fix Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#498): drift-guard lint — value-check GSD coordinate literals against the seam scripts/lint-package-identity-drift.cjs scans the runtime/code surface (bin/, hooks/, scripts/, get-shit-done/) and asserts every GSD package name and GitHub repo slug literal equals the Package Identity seam's current value. Passes today; fails the moment a repoint isn't propagated (rename package.json, regenerate the seam, and stale literals are reported until updated). This is the second adapter that makes the seam real and a repoint mechanically safe. Enforced via tests/issue-498-identity-drift-lint.test.cjs (scanRepo === []) under npm test; also exposed as `npm run check:identity-drift`. Refs #498 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#498): update-context projection — port update.md resolution to a tested seam Add get-shit-done/bin/lib/update-context.cjs: a pure, injected-fs port of update.md's ~280-line get_installed_version bash. resolveUpdateContext() reproduces the full precedence cascade (preferred fast-path -> local probe -> global probe via env overrides then $HOME -> LOCAL-if-distinct -> scope cascade -> UNKNOWN) and returns the 4-field contract { installedVersion, scope, runtime, gsdDir }. The fs is injected so every branch is finally testable without a live multi-runtime install. Expose it as `gsd-tools update-context [--config-dir <d>] [--runtime <r>] --json`. Purely additive — update.md is unchanged in this commit; the workflow swap follows separately. Refs #498 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#498): swap update.md resolution to the update-context projection Replace ~280 lines of inline runtime/scope/config-dir bash in update.md's get_installed_version step with a call to `gsd-tools update-context --json` (60 lines: derive PREFERRED_* from execution_context, resolve gsd-tools.cjs, parse the 4-field JSON). Behavior is unchanged — the projection reproduces the same cascade — but the logic is now tested in update-context.cjs instead of untestable bash-in-markdown. Relocate the #3608 antigravity-first-class contract onto the projection (RUNTIME_DIRS order, inferPreferredRuntime, envRuntimeDirs) plus a behavioral test; keep the execution_context path-classification assertion on update.md. Re-point install.test's custom-config-dir assertion (kilo.jsonc/KILO_CONFIG) to update-context.cjs where that detection now lives. Full root suite: 2022 pass / 0 fail. Refs #498 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#498): record Update Context Module in CONTEXT.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#498): CI — avoid bare gsd-tools in update.md; register new CLI modules - update.md update-context invocation: resolve the PATH gsd-tools shim into a variable and call "$GSD_TOOLS" (never a bare `gsd-tools` command) — satisfies the #2851 workflow-bare-gsd-tools guard. - Register package-identity.cjs and update-context.cjs in docs/INVENTORY.md (CLI Modules 76 -> 78 + rows) and regenerate docs/INVENTORY-MANIFEST.json, fixing inventory-counts and inventory-manifest-sync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#498): make update-context + parity tests OS-agnostic (Windows CI) Two Windows-only test failures, both test-portability (production code is fine — the real-fs CLI integration test passed on Windows): - update-context resolver tests + bug-3608 behavioral test used POSIX path-string keys in their fake fs, but the resolver builds lookups via path.join/resolve (backslash + drive letter on Windows) → keys never matched → everything resolved to UNKNOWN/claude. Normalize fake-fs keys and gsdDir comparisons through path.resolve so they match on both platforms. - package-identity parity test compared render() (LF) to the committed file, which Windows git checks out as CRLF (no .gitattributes eol rule). Normalize line endings before comparing, matching the repo convention (autonomous-decomposition, bug-3707). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#498): update.md backup must use GSD_DIR (adversarial-review finding) The get_installed_version rewrite emits GSD_DIR but dropped the probe-loop variables LOCAL_DIR/GLOBAL_DIR. The backup_custom_files step still read those, so RUNTIME_DIR went empty for every LOCAL/GLOBAL install and detect-custom-files was skipped — and since the update then runs a clean install that wipes managed dirs (commands/gsd, get-shit-done), user-added files could be deleted without the intended backup. Set RUNTIME_DIR="$GSD_DIR" directly (the resolved config dir; empty for UNKNOWN scope, which still skips the backup). Add a structural regression (tests/issue-498-update-backup-runtime-dir.test.cjs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#503): re-point Antigravity .agent detection at the #498 projection #499 moves the runtime/scope detection cascade out of update.md inline bash into get-shit-done/bin/lib/update-context.cjs. The #503 regression test asserted on the inline RUNTIME_DIRS array, which no longer exists, so it would fail against the projected update.md even though the .agent guarantee is preserved. Rewrite it to verify the surviving surfaces: - behavioral: resolveUpdateContext resolves a LOCAL ./.agent install to the antigravity runtime (the original root cause, now covered by adding ['antigravity', '.agent'] to the projection RUNTIME_DIRS table) - update.md prose classifier still maps /.agent/ -> antigravity - the post-update cache-clear for-dir loop still includes .agent Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#498): finish de-hardcoding consumers + close adversarial-review parity gaps Restore the consumer de-hardcoding that is the point of the seam, and close the parity gaps an adversarial review (codex) found in the update-context projection. De-hardcode the repo slug + install command in the changeset tooling — #516 only single-sourced the package NAME, leaving 'open-gsd/get-shit-done-redux' hardcoded in scripts/changeset/cli.cjs and github-release-notes.cjs. Route both through the seam's repoSlug/packageName so a rename is a regenerate, not a hand edit. The drift-lint real scan now reports zero divergent coordinate literals. Projection parity vs the old inline bash, as ONE consistent rule (trustedVersionAt) applied on every path: - expand a leading ~/ in preferredConfigDir before the fast path (the bash ran expand_home first; a custom --config-dir ~/foo otherwise fell to UNKNOWN) - trust a version only when BOTH VERSION and the update.md marker exist — fast path AND LOCAL/GLOBAL cascade; a partial dir falls to 0.0.0 keeping scope - apply the same same-path dedup to the 0.0.0 fallback so a partial install probed from cwd===home is not misdetected as LOCAL Adds regression tests for tilde expansion, VERSION-only (cascade + fast path), and the cwd===home partial-install dedup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b54026e106 |
chore(#516): single-source the package name from package.json (#517)
Adds get-shit-done/bin/lib/package-identity.cjs as the single source of truth for PACKAGE_NAME, derived from package.json `name` via require. Refactors all runtime code-line occurrences in bin/install.js, get-shit-done/bin/check-latest-version.cjs, get-shit-done/bin/lib/shell-command-projection.cjs, get-shit-done/bin/lib/verify.cjs, scripts/changeset/cli.cjs, scripts/changeset/github-release-notes.cjs, and scripts/release-tarball-smoke.cjs to import PACKAGE_NAME from the identity module instead of hardcoding the literal. The package name is unchanged (@opengsd/get-shit-done-redux). Behaviour is byte-identical: all --help, hint, and release-notes strings render exactly as before. Golden-literal tests (bug-2992, bug-378) keep their hardcoded expected values and remain GREEN. Adds tests/package-name-single-source.test.cjs lint guard: fails CI if @opengsd/get-shit-done-redux appears as a code-line literal in runtime .cjs/.js outside the identity module, enforcing a one-file rename path. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d3eaf6aec1 |
docs(#23): document changeset extract CLI contract (#479)
Add scripts/changeset/README.md specifying the cli.cjs extract subcommand: invocation, flags, version validation, exit-code table (0/1/2), and output shapes for text and --json modes. Corrects two inaccuracies from the triage table against the source: v-prefixed versions ARE accepted (stripped), and it is pre-release/ build suffixes that are rejected — not the v prefix. Also documents the full exit-1 surface (missing flags, invalid semver, missing changelog) and the exit-2 overlap (empty range vs malformed argv) so external callers do not conflate "no releases in range" with failure. Closes #23 Co-authored-by: CI Rebase Check <ci@gsd-redux> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6913dbcdb1 | fix(10): centralize semver comparison policy across hooks and changeset | ||
|
|
334a64168e |
chore(npm): rebrand packages to @opengsd scope (#127)
* chore(npm): rebrand packages to @opengsd scope Rename: - get-shit-done-redux → @opengsd/get-shit-done-redux - @gsd-redux/sdk → @opengsd/gsd-sdk Add publishConfig.access=public for first-time scoped publish. CLI binary names (get-shit-done-redux, gsd-sdk, gsd-tools) unchanged. Sweeps install commands, npx invocations, CI publish/version-check workflows, tests, docs, READMEs (all translations), and the PACKAGE_NAME constant in check-latest-version. Bumps qs 6.15.1 → 6.15.2 to clear a moderate advisory surfaced by the audit-clean test (GHSA-q8mj-m7cp-5q26). Closes #126 * chore: pin 2.0.0 release + remove canary workflow - Bump both packages 1.50.0-canary.0 → 2.0.0 for first @opengsd publish - Remove .github/workflows/canary.yml and canary dist-tag handling in release.yml / release-sdk.yml - Drop canary section from VERSIONING.md Refs #126 * chore: address review findings + harden tarball-smoke timeout - .changeset/opengsd-org-rename.md: match project's custom parse.cjs frontmatter (type: Changed / pr: 127); the scoped @changesets/cli keys were silently rejected. - CONTEXT.md: drop two canary-stream policy lines and a dangling DEFECT.CANARY-VERSION-LEAK.cross-ref now that canary.yml is gone. - tests/release-tarball-smoke.install.test.cjs: pass timeout: 600_000 for npm pack + global install; the 3-minute runNpm default was timing out on slower Docker hosts (cartographer). Refs #126 * fix(sdk): add missing type/runtime devDependencies for build prepublishOnly invokes tsc which couldn't resolve @types/node, @types/ws, or synckit. They had been hoisted from root but were not declared in sdk/'s own package.json — first publish from a clean SDK tree failed. Refs #126 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ci): use npm pack stdout instead of glob to find tarball `npm pack --silent` for a scoped package (@opengsd/get-shit-done-redux) produces `opengsd-get-shit-done-redux-*.tgz`, not `get-shit-done-redux-*.tgz`. Capture the filename from stdout instead of a hardcoded glob so the step works regardless of package name format. Fixes smoke (ubuntu-latest, 22, false) CI failure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci: treat workflow-file changes as test-skip eligible `.github/workflows/install-smoke.yml` (and other workflow files) were in neither `test.yml` paths nor `test-skip.yml` paths-ignore, so neither workflow ran on a workflow-only commit — leaving the required test-skip check perpetually missing. Refs #126 * chore: reset version to 1.0.0 for first @opengsd publish Nothing has been published yet under the @opengsd scope, so the inaugural release uses 1.0.0 rather than 2.0.0. The "major bump" in the changeset reflects the breaking install-command change for users migrating from the prior unscoped `get-shit-done-redux`, not a numeric continuation from a 1.x line under the new identity. Refs #126 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
2a915c1b82 |
chore: migrate references from gsd-build to open-gsd/get-shit-done-redux (#120) (#121)
Security-motivated migration of all stale repository and npm-scope references. Three categories of changes (58 files, 174 substitutions): 1. gsd-build → open-gsd (security-critical): - .github/workflows/release-sdk.yml — npm token comment, tarball filename pattern - .github/workflows/hotfix.yml — same - .changeset/fix-3406-detect-stale-sdk-shadow.md — @gsd-build/sdk → @open-gsd/sdk - .changeset/sharp-quails-leap.md — same - get-shit-done/workflows/update.md — CHANGELOG raw GitHub URL 2. GSD-redux org slug → open-gsd (canonical rename): - package.json + sdk/package.json — repository/homepage/bugs metadata - All README.*.md — live badge and link sections - CONTRIBUTING.md, CONTEXT.md, QUICK-WINS-CONFIRMED-BUGS.md - .coderabbit.yaml, .release-monitor.sh, scripts/sync-rulesets.sh - docs/** — all live agent/ADR/user-facing documentation - tests/** — repo slug assertions and test fixtures - scripts/changeset/cli.cjs + github-release-notes.cjs - .github/ISSUE_TEMPLATE/*, .github/pull_request_template.md - bin/install.js, get-shit-done/bin/lib/model-catalog.cjs - sdk/HANDOVER-*.md, sdk/src/*.test.ts 3. CLAUDE.md (gitignored local file — not in this commit): Updated separately outside git: --repo gsd-build/get-shit-done → --repo open-gsd/get-shit-done-redux with security warning. Intentionally unchanged: CHANGELOG.md, docs/RELEASE-*.md, .changeset/README.md, .changeset/build-hooks-atomic-write.md, README.md migration table (historical fork record), tests/changeset-serialize.test.cjs line 78 (serialization fixture). The gsd-build/get-shit-done repo is compromised (rug-pull documented in README.md). Do not push to or interact with that repo. Closes #120 |
||
|
|
ea67479bfb |
fix(3496): include all version patterns in changelog extraction (#90)
* fix(3496): include all version patterns in changelog extraction parseChangelog now handles multi-line bullets (continuation lines starting with two or more spaces) where the (#NNNN) PR trailer appears on a continuation line, not the opening dash line. The previous single-line regex silently dropped every such bullet, causing Feature/Enhancement sections to return 0 entries. Also adds an `extract` subcommand to scripts/changeset/cli.cjs: changeset/cli.cjs extract --from VERSION --to VERSION [--changelog FILE] [--json] Extracts releases strictly after --from (exclusive) and up to and including --to (inclusive). Accepts v-prefixed versions. Exits 2 when no releases fall in range, giving /gsd:update a deterministic range-aware helper instead of vague/manual extraction. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(3496): use production parseChangelog in markdown-mode assertion Replace raw stdout.includes() in the emits-markdown test with a parseChangelog call on the output so the assertion targets version strings via the production parser rather than a raw substring match. Eliminates the output-grep anti-pattern flagged by test-rigor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(changeset): add fragment for fix #3796 (issue #3496) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(3496): reject malformed --from/--to semver in extract with structured error `parseSemver` coerced non-numeric components to 0 (e.g. `1.41.x` → `1.41.0`), making range selection silently wrong under typos or version-shape drift. Add a strict N.N.N validation gate before comparison; exit 1 with a JSON error report when either bound fails. Add two regression tests covering alphabetic and dotted-letter inputs. Codex adversarial review finding: high severity (scripts/changeset/cli.cjs:226-244) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(3496): preserve bullets without PR trailer in parseChangelog (pr: null) Previously flushBullet() silently discarded any bullet that lacked a trailing (# NNNN) token. On the real CHANGELOG.md this dropped 7 entries from v1.41.0 alone, so cmdExtract returned incomplete release notes to the /gsd:update confirmation step. Store PR-less bullets as { body, pr: null } instead. Update cmdExtract's textOutput renderer to emit `- body` (no trailer) for null-pr bullets. Add regression tests: - serialize: preserves bullets without trailer as pr:null (not dropped) - cli extract: preserves PR-less and PR bullets together in extracted JSON - cli extract: rejects malformed --from/--to (1.41.x, foo) with exit 1 Codex adversarial review finding: high severity (scripts/changeset/serialize.cjs:64-73) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(3496): wire extract into update.md + reject pre-release in range, fix CHANGELOG parser edge cases BLOCKER fixes: - F1: workflows/update.md show_changes_and_confirm step now invokes `scripts/changeset/cli.cjs extract --from $INSTALLED_VERSION --to $LATEST_VERSION --changelog $CHANGELOG_TMP --json` with explicit exit-2 handling ("no releases in range") and fallback text. The prior prose ("extract entries between versions") was never wired to the binary and silently skipped intermediate versions (#3496). - F2: releases.filter in cmdExtract now rejects any rel.version that does not pass SEMVER_RE before numeric-tuple comparison. parseSemver('1.0.0-rc.1') previously returned [1,0,0] (same as '1.0.0'), causing pre-release entries to corrupt range queries. Architectural choice: skip pre-release + 4-part versions with a stderr warning; full semver §11 pre-release ordering deferred to a consolidation issue (see F8 note below). MAJOR fixes: - F3 (serialize.cjs): releaseMatch regex updated to /^##\s+\[([^\]]+)\](?:\([^)]*\))?\s*(?:-\s*(\S+))?/ so linked-header format `## [1.42.1](url) - 2026-05-15` captures the date correctly. - F4 (serialize.cjs): continuation-line test now checks `!/^\s+-\s/` so ` - nested item` terminates the current bullet instead of folding in. - F5 (cli.cjs): 4-part versions (e.g. 1.0.0.1) fail SEMVER_RE and are skipped by the same guard added for F2. No separate code path needed. - F6 (serialize.cjs): v-prefix stripped from in-file version capture; `## [v1.0.0]` now parses as version "1.0.0". - F7 (serialize.cjs): continuation-line indentation relaxed from /^[ \t]{2}/ to /^\s+/ so 1-space-indented continuations fold correctly (F4's bullet-terminator guard prevents nested bullets from being folded). MINOR fixes: - F9 (cli.cjs): unknown-command path now exits 1 instead of 2 (exit 2 is reserved for "no releases in range" semantic). - F10 (cli.cjs): text-mode exit-2 path now writes "no releases found in range (from=X, to=Y)" to stderr. - F11 (tests): exit-2 test now asserts r.json is present and r.json.releases.length === 0. NOTE — F8 (semver consolidation): this codebase has 5+ distinct semver comparators with divergent pre-release policies; this PR adds a 6th (the SEMVER_RE guard in cmdExtract). A follow-up consolidation issue should be filed to unify all call sites. Out of scope for this PR. Regression tests added for F1–F6: pre-release exclusion, linked-header date parsing, nested-bullet termination, 4-part/v-prefix edge cases, and workflow wiring. All 15 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(lint): add allow-test-rule annotation to F1 workflow wiring test The F1 test reads get-shit-done/workflows/update.md (a product markdown file, not CJS source) to assert the extract subcommand invocation was wired. The lint-no-source-grep detector flags any readFileSync-bound variable used with .includes() regardless of file extension; annotate with // allow-test-rule to exempt this legitimate product-content assertion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: apply deterministic barrier to locking-bugs #1927 config-set test The 'both concurrent config-set calls persist their values' test used Promise.all([execAsync(A), execAsync(B)]) without a barrier, which is non-deterministic under Docker load: one subprocess can complete before the other starts (no real contention) or both can race O_EXCL and observe stale fs state (lost write / assertion failure). Mirrors the locking-bugs:180 and :235 redesigns: erect a barrier file, spawn both subprocesses, wait for both to signal readiness via ready files, then drop the barrier simultaneously so both config-set calls genuinely contend on withPlanningLock. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
dff176bfd2 |
chore: rebrand to GSD-redux/get-shit-done-redux
Mirror of code, issues, and PRs from the upstream gsd-build/get-shit-done, which appears compromised or abandoned (maintainer unreachable since 2026-04-01; $GSD token linked to rug-pull). - Adds rebrand notice block at top of English README - Removes $GSD token badge and @gsd_foundation X badge (keeps Discord) - Renames npm packages: get-shit-done-cc -> get-shit-done-redux, @gsd-build/sdk -> @gsd-redux/sdk - Updates all repo URLs across docs, workflows, package.json, bin/ - Updates ci@gsd-build -> ci@gsd-redux in workflow git identities - Leaves CHANGELOG and .changeset/* alone (historical, time-stamped) |
||
|
|
7f8b5701bf |
Enforce documentation updates via lint:docs + PR templates (#3651)
* Enforce documentation updates via lint:docs + PR templates (#3213) New scripts/lint-docs-required.cjs + Docs Required CI workflow fail any PR whose changeset fragment is typed Added / Changed / Deprecated / Removed without modifying at least one file under docs/. Mirrors scripts/changeset/lint.cjs: pure evaluateLint({ changedFiles, fragments, labels }) returning { ok, reason, triggering } over a frozen LINT_REASON enum; CLI wrapper reads the PR diff and parses each touched changeset fragment via the existing parseFragment helper. Escape hatches: - no-docs PR label (global) - per-fragment <!-- docs-exempt: <reason> --> marker, all triggering fragments must carry it for the PR to pass Fixed and Security fragments do not trigger the lint — bug fixes restore documented behavior, they do not introduce new behavior to document. PR templates (enhancement.md, feature.md) gain a Documentation checklist section pointing at the which-doc-to-update matrix. CONTRIBUTING.md adds a Documentation Updates section codifying that matrix, the English-canonical language policy for docs/ and the root README, and the two opt-out routes. Closes #3213 * Address Codex review: fail-closed on malformed fragments and strip docs-exempt marker from rendered release notes (#3213) Two P2 issues caught by `codex review --base main`: 1) Malformed fragments could silently bypass docs enforcement. parseFragment would return ok:false on a triggering Added fragment with bad frontmatter and readFragmentsFromDisk dropped it, so evaluateLint saw no triggering fragments and passed. The changeset-required lint only checks fragment _presence_ not _validity_, so the assumed fallback did not catch it. Fix: readFragmentsFromDisk now returns { fragments, malformed }; evaluateLint accepts a malformed param and emits a new FAIL_MALFORMED_FRAGMENT verdict that outranks every OK path (including the no-docs label) — a parse failure must be fixed before docs lint can decide anything else. 2) The per-fragment <!-- docs-exempt: reason --> marker lived in the fragment body, so the existing changelog (serializeChangelog) and GitHub release-notes (formatBullet) serializers published it verbatim. Worse, both serializers append `(#NNNN)` to the body's last line — with the marker as the trailing line, the PR suffix attached to the hidden comment instead of the visible bullet. Fix: parseFragment now extracts the marker into a typed `docsExempt` field and strips it from `body`, so all downstream renderers produce clean output without remembering to strip. The regex is anchored to its own line (^...$ with m flag) so inline mentions of the marker syntax in documentation (e.g. inside backticks) cannot accidentally exempt a fragment. Bounded character class [^\n>] keeps the regex linear-time. Test additions: - tests/lint-docs-required.test.cjs: FAIL_MALFORMED_FRAGMENT coverage, end-to-end "Added fragment with bad pr → malformed → fail-closed" regression test, updated readFragmentsFromDisk return-shape assertions, isExemptFragment now checks the typed docsExempt field rather than body content. - tests/changeset-parse.test.cjs: extractDocsExempt extraction cases (with/ without reason, case-insensitive, EMPTY_BODY when body is only a marker), inline-mention false-positive guard, real-marker-wins-when-also-inline test. - tests/changeset-new.test.cjs: fragment shape now includes docsExempt: null. CONTRIBUTING.md updated to clarify the "on its own line" requirement and the parse-time stripping behavior. The bootstrap fragment cleaned up so its body no longer contains a literal marker example that would have triggered the false-positive case. Full suite: 9696/9696 pass. * CRLF-safe docs-exempt marker stripping (Codex review pass 2, #3213) Second `codex review --commit` pass caught a CRLF regression in the docs-exempt extraction added in the previous commit. Repro: a Windows-authored fragment ---\r\ntype: Added\r\npr: 1\r\n---\r\nFeature.\r\n\r\n<!-- docs-exempt: x -->\r\n would parse to body `Feature.\r\n\r\n\r` because: - The previous trailing-newline slice trimmed only `\n`, leaving `\r`. - DOCS_EXEMPT_RE was anchored with `$` only — in multiline mode `$` matches before `\n` but does not consume `\r`, so the marker line's trailing `\r` was left behind after replace. - The cleanup regex stripped trailing `\n` but not `\r`. Net effect: serializeChangelog emitted - Feature.\r \r \r (#1) — the `(#1)` PR suffix landed on a blank line instead of attached to the visible bullet. Same bug surfaces in github-release-notes formatBullet. Fix: - DOCS_EXEMPT_RE: add `\r?` before `$` so the regex consumes the CR of a CRLF terminator. Switch reason character class from `[^\n>]` to `[^\r\n>]` so CRLF-authored reasons don't carry a trailing `\r`. - extractDocsExempt cleanup: `[ \t\r]+$/gm` strips trailing `\r` on each line; `(?:\r?\n){3,}` collapses CRLF triple-blank-lines; `[\r\n]+$` strips every trailing line terminator (LF or CR). - parseFragment trailing-newline slice: CRLF-aware — strips `\r\n` (2 chars) before falling through to single `\n`. Tests: two CRLF regression cases in tests/changeset-parse.test.cjs — Codex's exact repro (end-to-end through serializeChangelog) plus the no-marker CRLF passthrough case. Full suite: 9698/9698 pass. * CRLF regression test asserts on parseChangelog IR not rendered text (Codex review pass 3, #3213) Third `codex review` pass caught that the CRLF regression test added in the previous commit asserted on serializeChangelog's rendered Markdown via `out.split('\n')` + `assert.match`. That violates CONTRIBUTING.md's "Prohibited: Raw Text Matching on Test Outputs" rule and the documented serializer contract in `serialize.cjs`: > tests assert via round-trip (parse(serialize(ir))) > rather than by inspecting serialized text Replace the regex check with the established `parseChangelog(out)` round-trip and assert on the structured `{ body: 'Feature.', pr: 1 }` bullet. This is also a stronger regression check than the substring match: Codex's own probe in the review session confirmed the pre-fix buggy body shape (`Feature.\r\n\r\n\r`) breaks parseChangelog's bullet regex entirely (returns `bullets: []`), so the round-trip catches the exact failure mode end-to-end. Full suite: 9698/9698 pass. * Address CodeRabbit findings: anchor link + require non-empty docs-exempt reason (#3213) CodeRabbit's review on the PR caught two actionable issues, both quick wins. Anchor link in PR templates pointed to a heading that does not exist. The CONTRIBUTING.md heading "Documentation Updates — Update the Relevant Docs" contains an em-dash, which GitHub strips entirely when generating anchor slugs (it does NOT collapse to a hyphen). The actual anchor is #documentation-updates-update-the-relevant-docs (single hyphen between every word), not #documentation-updates--update-the-relevant-docs (double hyphen where the em-dash was). Both feature.md and enhancement.md fixed. The docs-exempt marker matched a bare `<!-- docs-exempt -->` with no reason, which defeats the entire purpose of the escape hatch — the marker exists to leave an audit trail explaining WHY a PR is exempt. Without a reason it is a silent bypass. Fix: DOCS_EXEMPT_RE now requires both the colon AND a non-whitespace first reason character. Bare `<!-- docs-exempt -->`, empty `<!-- docs-exempt: -->`, and whitespace-only `<!-- docs-exempt: -->` are all rejected as if the marker were not present (`docsExempt: null`). The lint then falls through to its normal docs-required / no-docs-label checks. `isExemptFragment` in the lint module tightened too — defense-in-depth: even if a caller constructs a fragment with `docsExempt: ''` directly, it does not count as exempt. The predicate now requires `typeof === 'string'` and non-empty after trim. Tests: - changeset-parse.test.cjs: three new explicit-rejection cases (bare marker, empty reason, whitespace-only reason). Existing DOCS_EXEMPT_RE shape test extended with negative assertions for the same three forms. - lint-docs-required.test.cjs: prior "empty reason still exempt" test inverted — empty/whitespace docsExempt now produces FAIL_DOCS_MISSING. isExemptFragment helper test extended with the same negative cases. - CONTRIBUTING.md: clarified that the reason is required and non-empty. Skipped CodeRabbit's third finding ("use `npm run lint:docs` in CI workflow instead of `node scripts/lint-docs-required.cjs`") — the existing changeset-required.yml uses the direct-node form for the equivalent changeset lint, so the new docs-required.yml is convention-consistent. Switching one without the other would create drift, and switching both is out of scope for #3213. Bootstrap fragment continues to extract cleanly under the stricter regex (verified — `docsExempt` field still contains the full bootstrap reason). Full suite: 9701/9701 pass. |
||
|
|
a51fc86a18 |
feat: generate release notes from changeset slugs (#3383)
* feat: generate release notes from changeset slugs * fix: harden release note generator inputs * fix: address release note review nits |
||
|
|
9d5db87249 |
feat(#2975): adopt changeset-fragment workflow to eliminate CHANGELOG conflicts (#2978)
* 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). |