
Changeset Pr
- 49 installs
- 2.6k repo stars
- Updated August 4, 2026
- module-federation/core
Helps with ai & agent building tasks.
About
changeset-pr is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- changeset-pr
- AI & Agent Building
- AI-coding skill
Changeset Pr by the numbers
- 49 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #7,329 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/module-federation/core --skill changeset-prAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 49 |
|---|---|
| repo stars | ★ 2.6k |
| Last updated | August 4, 2026 |
| Repository | module-federation/core ↗ |
What it does
Helps with ai & agent building tasks.
Files
Changeset PR
Overview
Create a repo-correct changeset for the current branch, or update an existing one without widening scope unnecessarily. Verify both syntax and package scope before handoff.
Ground the changeset in the live branch diff, not stale branch intent. Always inspect the current branch state against its base before choosing package scope or release type.
Workflow
1. Confirm whether a changeset is needed. 2. Identify the publishable package scope from the live branch diff. 3. Create or edit one .changeset/*.md file. 4. Validate the file against repo config and branch scope. 5. Report the exact commands run and any ambiguity that remains.
Decide Whether A Changeset Is Needed
- Add a changeset when a publishable package behavior changes.
- Do not add one for docs-only or non-behavioral repo changes unless the user explicitly wants release metadata anyway.
- If unsure whether the change is user-visible enough to merit a release note, inspect existing changesets in
.changeset/and bias toward a short patch changeset rather than skipping silently.
Read references/repo-conventions.md when you need the repo-specific fixed-group, ignore-list, or release-flow details.
Determine Scope
First inspect the live branch state:
git diff --name-status origin/main...HEAD
git diff --stat origin/main...HEAD
git log --oneline --decorate --no-merges origin/main..HEADUse that to separate real publishable-package behavior changes from repo-local docs, skills, tooling, or cleanup.
Start with the helper script:
python3 .codex/skills/changeset-pr/scripts/inspect_changeset_scope.py --base origin/mainUse its output to separate:
- touched publishable packages
- ignored packages
- fixed groups that will affect release planning
If the branch touches multiple publishable packages, include only the packages whose behavior actually changed. Do not add app/example packages from the ignore list.
Create Or Update The Changeset
Prefer editing an existing branch changeset when one already covers the same change. Otherwise create a new file in .changeset/ with the standard format:
---
"@module-federation/pkg-name": patch
---
Brief user-facing summary of the change.Rules:
- Keep the summary brief and release-note oriented.
- Avoid implementation-detail dumps and nested bullets.
- Use
patch,minor, ormajorunless there is a specific reason to usenone. - Quote package names in frontmatter.
- Keep package scope tight even if the fixed group later broadens the computed plan.
The repo has a custom helper:
pnpm run changegenUse it only if the user explicitly wants generated changeset text or the touched package is already covered by its configured package paths. Otherwise write the file directly.
Validate
There is no dedicated changeset validate command in the official CLI. Use these checks instead:
1. Validate branch scope against the file:
python3 .codex/skills/changeset-pr/scripts/inspect_changeset_scope.py --base origin/main --file .changeset/<file>.md2. Validate that Changesets can parse and plan the release:
python3 .codex/skills/changeset-pr/scripts/run_changeset_status.py --verbose3. When machine-readable output is useful:
python3 .codex/skills/changeset-pr/scripts/run_changeset_status.py --output /tmp/changeset-status.jsonInterpretation:
statusverifies parseability and computed release planning.statusdoes not prove the changeset is branch-local or minimal in this repo because other pending changesets may already exist.- Fixed-group packages can cause broader or higher bumps than the frontmatter alone suggests.
- Prefer the helper script over direct
pnpm exec changeset statusin Codex runs because shell wrappers in non-TTY sessions can add/dev/ttynoise or otherwise make the raw CLI output unreliable.
Update Existing Changesets
When asked to update a changeset for a branch or PR:
- search
.changeset/*.mdfor the affected package name first - prefer editing the existing file if it clearly belongs to the same branch work
- avoid creating duplicate files for the same single change unless the branch intentionally has multiple release notes
After editing, rerun both validation steps.
Report Back
Always report:
- whether the branch needed a changeset
- which packages were included
- which commands were run
- whether
changeset statussucceeded - any fixed-group or ignored-package caveats
interface:
display_name: 'Changeset PR'
short_description: 'Create and verify PR changesets'
default_prompt: 'Use $changeset-pr to create or update the correct changeset for this branch and verify its scope.'
Repo Changesets Notes
Current Config
.changeset/config.jsonsetsbaseBranchtomain.commitisfalse, so creating a changeset does not auto-commit anything.updateInternalDependenciesispatch.- A large
fixedgroup covers many publishable@module-federation/*packages. ignoreexcludes apps/examples and@changesets/assemble-release-plan.
Practical Implications
- A changeset that names one package can still produce a broader or higher release in
changeset statusbecause of the fixed group. pnpm exec changeset statusis useful for syntax and release-plan verification, but not enough by itself to prove branch-local scope in this repo because the repo can already contain other pending changesets.- Use the helper script in
scripts/inspect_changeset_scope.pyto compare the branch diff to the changeset file.
Relevant Repo Commands
pnpm run changeset
pnpm run changeset:status
pnpm exec changeset status --verbose
pnpm exec changeset status --output /tmp/changeset-status.json
python3 .codex/skills/changeset-pr/scripts/inspect_changeset_scope.py --base origin/main
python3 .codex/skills/changeset-pr/scripts/inspect_changeset_scope.py --base origin/main --file .changeset/<file>.mdRelease Flow Notes
- The release PR workflow builds
@changesets/assemble-release-planbefore the release action runs. - The repo uses a workspace-local fork of
@changesets/assemble-release-plan. - Do not run publish commands unless the user explicitly asks for release execution.
#!/usr/bin/env python3
import argparse
import json
import subprocess
import sys
from pathlib import Path
VALID_RELEASE_TYPES = {"patch", "minor", "major", "none"}
def run(cmd: list[str], cwd: Path) -> str:
result = subprocess.run(
cmd,
cwd=str(cwd),
check=True,
capture_output=True,
text=True,
)
return result.stdout.strip()
def find_repo_root(start: Path) -> Path:
return Path(run(["git", "rev-parse", "--show-toplevel"], start))
def load_changeset_config(repo_root: Path) -> dict:
return json.loads((repo_root / ".changeset" / "config.json").read_text())
def discover_packages(repo_root: Path) -> dict[str, dict]:
packages: dict[str, dict] = {}
for pkg_json in repo_root.rglob("package.json"):
if "node_modules" in pkg_json.parts:
continue
try:
data = json.loads(pkg_json.read_text())
except Exception:
continue
name = data.get("name")
if not name:
continue
rel_dir = pkg_json.parent.relative_to(repo_root)
private = bool(data.get("private", False))
packages[name] = {
"dir": str(rel_dir),
"private": private,
}
return packages
def changed_files(repo_root: Path, base: str) -> list[str]:
merge_base = run(["git", "merge-base", "HEAD", base], repo_root)
out = run(["git", "diff", "--name-only", f"{merge_base}...HEAD"], repo_root)
if not out:
return []
return [line for line in out.splitlines() if line]
def package_for_file(path_str: str, packages: dict[str, dict]) -> str | None:
best_name = None
best_len = -1
for name, meta in packages.items():
pkg_dir = meta["dir"].rstrip("/")
if not pkg_dir:
continue
if path_str == pkg_dir or path_str.startswith(f"{pkg_dir}/"):
if len(pkg_dir) > best_len:
best_len = len(pkg_dir)
best_name = name
return best_name
def parse_changeset_file(path: Path) -> dict:
text = path.read_text().strip()
lines = text.splitlines()
if len(lines) < 3 or lines[0].strip() != "---":
raise ValueError("Changeset file must start with frontmatter delimited by ---")
end_index = None
for i in range(1, len(lines)):
if lines[i].strip() == "---":
end_index = i
break
if end_index is None:
raise ValueError("Changeset file frontmatter is missing the closing ---")
releases: dict[str, str] = {}
for raw in lines[1:end_index]:
line = raw.strip()
if not line:
continue
if ":" not in line:
raise ValueError(f"Invalid frontmatter line: {raw}")
pkg, release_type = line.split(":", 1)
pkg = pkg.strip().strip("\"'")
release_type = release_type.strip().strip("\"'")
if release_type not in VALID_RELEASE_TYPES:
raise ValueError(f"Invalid release type for {pkg}: {release_type}")
releases[pkg] = release_type
summary = "\n".join(lines[end_index + 1 :]).strip()
return {"releases": releases, "summary": summary}
def build_report(repo_root: Path, base: str, file_path: Path | None) -> dict:
config = load_changeset_config(repo_root)
packages = discover_packages(repo_root)
ignored = set(config.get("ignore", []))
fixed_groups = [set(group) for group in config.get("fixed", [])]
files = changed_files(repo_root, base)
touched_packages = sorted(
{
pkg
for file_path_str in files
for pkg in [package_for_file(file_path_str, packages)]
if pkg
}
)
touched_publishable = [pkg for pkg in touched_packages if pkg not in ignored]
report = {
"base": base,
"changed_files_count": len(files),
"touched_packages": touched_packages,
"touched_publishable_packages": touched_publishable,
"ignored_touched_packages": [pkg for pkg in touched_packages if pkg in ignored],
"fixed_groups_hit": [
sorted(group)
for group in fixed_groups
if any(pkg in group for pkg in touched_publishable)
],
}
if file_path is not None:
parsed = parse_changeset_file(file_path)
listed = sorted(parsed["releases"].keys())
unknown = [pkg for pkg in listed if pkg not in packages]
ignored_listed = [pkg for pkg in listed if pkg in ignored]
missing_for_touched = [pkg for pkg in touched_publishable if pkg not in listed]
extra_without_touched_files = [pkg for pkg in listed if pkg not in touched_publishable]
report["changeset"] = {
"path": str(file_path.relative_to(repo_root)),
"releases": parsed["releases"],
"summary_present": bool(parsed["summary"]),
"unknown_packages": unknown,
"ignored_packages": ignored_listed,
"missing_touched_publishable_packages": missing_for_touched,
"packages_without_touched_files": extra_without_touched_files,
}
return report
def print_text(report: dict) -> None:
print(f"Base: {report['base']}")
print(f"Changed files: {report['changed_files_count']}")
def print_list(label: str, values: list[str]) -> None:
print(f"{label}:")
if not values:
print("- none")
return
for value in values:
print(f"- {value}")
print_list("Touched packages", report["touched_packages"])
print_list("Touched publishable packages", report["touched_publishable_packages"])
print_list("Ignored touched packages", report["ignored_touched_packages"])
fixed_groups = report["fixed_groups_hit"]
print("Fixed groups hit:")
if not fixed_groups:
print("- none")
else:
for group in fixed_groups:
print(f"- {', '.join(group)}")
changeset = report.get("changeset")
if not changeset:
return
print(f"Changeset file: {changeset['path']}")
print("Listed releases:")
if not changeset["releases"]:
print("- none")
else:
for pkg, release_type in changeset["releases"].items():
print(f"- {pkg}: {release_type}")
print(f"Summary present: {'yes' if changeset['summary_present'] else 'no'}")
print_list("Unknown packages", changeset["unknown_packages"])
print_list("Ignored packages in changeset", changeset["ignored_packages"])
print_list(
"Touched publishable packages missing from changeset",
changeset["missing_touched_publishable_packages"],
)
print_list(
"Changeset packages without touched files",
changeset["packages_without_touched_files"],
)
def main() -> int:
parser = argparse.ArgumentParser(
description="Inspect changed packages on the branch and validate a changeset file against repo config."
)
parser.add_argument("--base", default="origin/main", help="Branch or ref to diff against")
parser.add_argument("--file", help="Optional .changeset/*.md file to validate")
parser.add_argument("--json", action="store_true", help="Emit JSON")
args = parser.parse_args()
cwd = Path.cwd()
repo_root = find_repo_root(cwd)
file_path = Path(args.file).resolve() if args.file else None
try:
report = build_report(repo_root, args.base, file_path)
except subprocess.CalledProcessError as exc:
sys.stderr.write(exc.stderr)
return exc.returncode
except Exception as exc:
sys.stderr.write(f"{exc}\n")
return 1
if args.json:
print(json.dumps(report, indent=2, sort_keys=True))
else:
print_text(report)
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from pathlib import Path
def run(cmd: list[str], cwd: Path) -> str:
result = subprocess.run(
cmd,
cwd=str(cwd),
check=True,
capture_output=True,
text=True,
)
return result.stdout.strip()
def find_repo_root(start: Path) -> Path:
return Path(run(["git", "rev-parse", "--show-toplevel"], start))
def find_changeset_cli(repo_root: Path) -> Path:
candidates = [
repo_root / "node_modules" / "@changesets" / "cli" / "bin.js",
repo_root / "node_modules" / ".bin" / "changeset",
]
for candidate in candidates:
if candidate.exists():
return candidate
raise FileNotFoundError("Unable to locate Changesets CLI under node_modules")
def main() -> int:
parser = argparse.ArgumentParser(
description="Run Changesets status without shell wrappers so output is stable in non-TTY environments."
)
parser.add_argument(
"--repo-root",
default=".",
help="Repo root. Defaults to current working directory.",
)
parser.add_argument(
"--output",
help="Optional path to write Changesets JSON output.",
)
parser.add_argument(
"--verbose",
action="store_true",
help="Pass --verbose to Changesets status.",
)
parser.add_argument(
"--json",
action="store_true",
help="Emit a small JSON wrapper with command, exit code, stdout, and stderr.",
)
args = parser.parse_args()
repo_root = find_repo_root(Path(args.repo_root).resolve())
cli = find_changeset_cli(repo_root)
cmd = ["node", str(cli), "status"]
if args.verbose:
cmd.append("--verbose")
if args.output:
cmd.extend(["--output", str(Path(args.output).resolve())])
proc = subprocess.run(
cmd,
cwd=str(repo_root),
capture_output=True,
text=True,
check=False,
)
if args.json:
payload = {
"command": cmd,
"exit_code": proc.returncode,
"stdout": proc.stdout,
"stderr": proc.stderr,
}
sys.stdout.write(json.dumps(payload, indent=2) + "\n")
else:
if proc.stdout:
sys.stdout.write(proc.stdout)
if proc.stderr:
sys.stderr.write(proc.stderr)
return proc.returncode
if __name__ == "__main__":
raise SystemExit(main())