releasing-code
Imported from alexei-led/cc-thingz/dist/claude/dev-flow/skills/releasing-code.
Install
npx skills add https://github.com/alexei-led/cc-thingz/tree/master/dist/claude/dev-flow/skills/releasing-code
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install alexei-led-cc-thingz@llmmart
git clone https://github.com/alexei-led/cc-thingz.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole alexei-led/cc-thingz collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Releasing Code
Prepare a release, route its publication, write its notes, or repair notes on a published release. Read notes.md when writing or checking release content. Follow the project's own release instructions where they are more specific.
Safety rules
- Publishing and repair change external state. Before any external mutation, get explicit user authorization for the exact repository, tag, and action. Skill routing, a tool approval, a passing check, or workflow ownership is not authorization.
- When a workflow owns the GitHub release, use that workflow's supported trigger. Do not publish around it with the CLI.
- Never overwrite package versions, move an existing local or published tag, force-push, or add
--clobber. - If the publisher, repository policy, a multi-step publication path, or the prior publication state is unclear, stop and ask.
Prepare
Done when the notes pass the checker, the full staged release diff is reviewed, and the project's release validation passes. Commit, tag, or publish only after the notes are reviewed.
- Identify the project, the intended version and tag, the last published release, and the publisher. Read the working tree, tags, recent commits, diffs, and release configuration; do not infer policy from a workflow filename.
- Take changes since the verified previous release from commit bodies and diffs. Keep only user-visible changes that shipped in this release.
- Use the committed changelog or other established release source as the single notes source. Do not write a separate body for the hosting service.
- For each declared breaking change, write an
Upgradesection with the required action, when to take it (before or after upgrading), and the consequence of skipping it.
Determine who publishes
Read the workflow steps and scripts for the operation that actually creates or updates the GitHub release. A tag workflow that only builds or uploads package artifacts does not own the GitHub release.
Publish
Verify that the exact tag points to the intended release commit. Before resuming or skipping work, check whether a hosting release already exists and confirm its tag, exact title, and attached artifact identity. Do not overwrite an existing release to force progress.
If a workflow owns the release, trigger it and inspect its result.
If no workflow owns the release, use the CLI with an explicit repository, the verified tag, the exact tag as title, and the same reviewed notes file:
gh release create "$TAG" --repo owner/name --verify-tag \ --title "$TAG" --notes-file "$NOTES_FILE"Audit the artifacts attached to the tagged release separately. An installed-package audit is not evidence that the release assets are correct.
Repair notes only
- Require an already-published release; refuse to repair a draft or prerelease. Verify that the tag and release identify the intended version and package or artifact identity. The current title may be wrong when title correction is part of the repair.
- Select the notes source explicitly as a full commit SHA, not the immutable tag. Read only the changelog from that commit; version, package metadata, and assets still come from the tag. Regenerate the section and validate it. Change no versions, assets, or tags.
- Before mutating, back up the current title, body, and publication state (draft, prerelease, and latest flags) somewhere durable.
- Apply the repair through the owning workflow's repair path. With no workflow owner, use
gh release edit "$TAG" --repo owner/name --title "$TAG" --notes-file "$NOTES_FILE". - Read the release back and verify its tag, exact title, body, and assets. A successful command is not proof of the published result.
Optional release guard
The packaged pre-tool release-guard hook runs only when HOOK_RELEASE_GUARD=1. It checks direct gh release create and gh release edit forms against the notes checker and permits read-only queries and --dry-run. It is not authorization and not a shell parser: wrappers, aliases, nested shells, scripts, and other publishers are outside its scope.
Files (cc-thingz)
-
references
-
notes.md 1.8 KB
# Release Notes The committed release section, usually the version section in `CHANGELOG.md`, is the only authored notes source. ## Content - Explain each user-visible change and why it matters. Name affected users, behavior, or configuration when known. - Check claims against the commits and diffs since the verified previous release. Do not claim a fix for a bug that no published version had. - Aim for about 150 words for a patch and 250 for a minor release. These are review budgets, not truncation limits: keep migration steps, compatibility details, security notices, and known issues. - A meaningful one-line patch note is valid. Headings, a version number, `Release VERSION`, `TODO`-only content, or a compare link alone are not release notes. - Keep generated package or distribution metadata out of the authored summary. - Append a full compare link only when the previous release tag is verified. - Use the exact release tag as the release title, for example `v1.4.0`. - Leave out empty sections. ## Structure ```markdown ## Summary <one or two sentences about the main user-visible change> ## Changes - <what changed and who benefits> ## Upgrade 1. <required migration action before or after upgrading> 2. <restart or setup action, when required> ``` In `Upgrade`, state the action first and the risk second: > Run `tool migrate` before you start 2.0. Without it, 2.0 rejects the old configuration. ## Check the notes Run the packaged checker before committing or publishing: ```bash python3 <skill-dir>/scripts/release_notes.py check release-notes.md --budget patch python3 <skill-dir>/scripts/release_notes.py check-changelog \ --changelog CHANGELOG.md --version X.Y.Z --budget minor ``` It rejects placeholder-only content and reports over-budget notes as advisory. It never truncates text.
-
-
scripts
-
release_notes.py 15.2 KB
#!/usr/bin/env python3 """Validate and render curated release-note content without external dependencies.""" from __future__ import annotations import argparse import json import re import subprocess import sys import tomllib from collections.abc import Sequence from dataclasses import dataclass from pathlib import Path TAG_RE = re.compile(r"^v(?P<version>\d+\.\d+\.\d+)$") VERSION_HEADER_RE = re.compile( r"^## \[(?P<version>\d+\.\d+\.\d+)\](?:\s+-\s+.*)?\s*$", re.MULTILINE ) LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)") WORD_RE = re.compile(r"\b[\w]+(?:[-’'][\w]+)*\b", re.UNICODE) PLACEHOLDER_RE = re.compile( r"^(?:" r"release(?:\s+(?:version|v?\d+\.\d+\.\d+|version\s*\d+\.\d+\.\d+|x\.y\.z))?" r"|version(?:\s+(?:v?\d+\.\d+\.\d+|x\.y\.z))?" r"|v?\d+\.\d+\.\d+" r"|todo(?:\s*[:—-].*)?" r"|tbd" r"|coming\s+soon" r"|full\s+changelog" r"|compare(?:\s+changes)?" r"|add\s+(?:release\s+)?notes?(?:\s+here)?" r"|to\s+be\s+determined" r")$", re.IGNORECASE, ) COMPARE_URL_RE = re.compile(r"(?:/compare/|compare\.)", re.IGNORECASE) class ReleaseNotesError(ValueError): """Raised when release notes are missing, placeholders, or invalid.""" @dataclass(frozen=True, slots=True) class NotesReport: word_count: int budget: int | None @property def over_budget(self) -> bool: return self.budget is not None and self.word_count > self.budget def version_from_tag(tag: str) -> str: match = TAG_RE.fullmatch(tag.strip()) if match is None: raise ReleaseNotesError(f"tag must match vX.Y.Z: {tag}") return match.group("version") def _version_tuple(version: str) -> tuple[int, int, int]: major, minor, patch = version_from_tag(version).split(".") return int(major), int(minor), int(patch) def verify_previous_tag( previous_tag: str, current_tag: str, repository_root: Path ) -> None: previous_version = _version_tuple(previous_tag) current_version = _version_tuple(current_tag) if previous_version >= current_version: raise ReleaseNotesError( f"previous tag {previous_tag} is not older than {current_tag}" ) result = subprocess.run( ["git", "rev-parse", "--verify", "--quiet", f"refs/tags/{previous_tag}"], cwd=repository_root, capture_output=True, text=True, check=False, ) if result.returncode != 0: raise ReleaseNotesError(f"previous tag {previous_tag} is not present locally") def collect_release_versions(root: Path) -> dict[str, str]: versions: dict[str, str] = {} def add(label: str, value: object) -> None: if not isinstance(value, str) or not re.fullmatch(r"\d+\.\d+\.\d+", value): raise ReleaseNotesError(f"invalid release version in {label}: {value!r}") versions[label] = value try: project = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8")) project_name = project["project"]["name"] add("pyproject.toml", project["project"]["version"]) lock = tomllib.loads((root / "uv.lock").read_text(encoding="utf-8")) locked_projects = [ package for package in lock["package"] if package.get("name") == project_name ] if len(locked_projects) != 1: raise ReleaseNotesError( f"uv.lock must contain exactly one {project_name} project package" ) add("uv.lock", locked_projects[0]["version"]) bundle = json.loads((root / "agentbundle.json").read_text(encoding="utf-8")) add("agentbundle.json distribution", bundle["distribution"]["version"]) for index, composition in enumerate(bundle.get("composition", [])): aggregate = composition.get("aggregate") if aggregate is not None: add( f"agentbundle.json composition {index}", aggregate["metadata"]["version"], ) manifests = sorted((root / "src/.agentbundler/packages").glob("*.json")) if not manifests: raise ReleaseNotesError( "no src/.agentbundler/packages/*.json manifests found" ) for path in manifests: data = json.loads(path.read_text(encoding="utf-8")) add(str(path.relative_to(root)), data["metadata"]["version"]) for relative in ( "package.json", "src/skills/browser-automation/scripts/package.json", ): data = json.loads((root / relative).read_text(encoding="utf-8")) add(relative, data["version"]) except ( AttributeError, KeyError, OSError, TypeError, json.JSONDecodeError, tomllib.TOMLDecodeError, ) as exc: raise ReleaseNotesError( f"cannot read release version manifests: {exc}" ) from exc return versions def validate_release_version_contract( versions: dict[str, str], tag: str | None = None ) -> str: if not versions: raise ReleaseNotesError("no release version manifests were checked") distinct = set(versions.values()) if len(distinct) != 1: details = ", ".join(f"{name}={value}" for name, value in versions.items()) raise ReleaseNotesError(f"release version manifests disagree: {details}") version = next(iter(distinct)) if tag is not None: expected = version_from_tag(tag) if version != expected: raise ReleaseNotesError( f"release tag {tag} does not match " f"package/distribution version {version}" ) return version def extract_changelog_section(changelog: str, version: str) -> str: matches = list(VERSION_HEADER_RE.finditer(changelog)) for index, match in enumerate(matches): if match.group("version") != version: continue start = match.end() end = matches[index + 1].start() if index + 1 < len(matches) else len(changelog) section = changelog[start:end].strip() validate_notes(section) return section raise ReleaseNotesError(f"CHANGELOG.md is missing section for {version}") def _content_lines(notes: str) -> list[str]: content: list[str] = [] visible_notes = re.sub(r"<!--.*?-->", "", notes, flags=re.DOTALL) for line in visible_notes.splitlines(): stripped = line.strip() if ( not stripped or stripped.startswith("#") or re.fullmatch(r"[-*_]{3,}", stripped) ): continue stripped = re.sub(r"^\s*(?:[-*+]\s+|\d+[.)]\s+|>\s*)", "", stripped) stripped = LINK_RE.sub( lambda match: ( "" if COMPARE_URL_RE.search(match.group(2)) and re.search(r"(?:full\s+)?changelog|compare", match.group(1), re.I) else match.group(1) ), stripped, ) stripped = re.sub(r"https?://\S+", "", stripped) stripped = re.sub(r"`([^`]*)`", r"\1", stripped) stripped = re.sub(r"[*_~]", "", stripped).strip(" \t-:,.;") if not stripped or PLACEHOLDER_RE.fullmatch(stripped): continue content.append(stripped) return content def validate_notes(notes: str, budget: int | None = None) -> NotesReport: if budget is not None and budget < 1: raise ValueError("word budget must be positive") content = _content_lines(notes) if not content: raise ReleaseNotesError("release notes need meaningful user-visible content") word_count = len(WORD_RE.findall(" ".join(content))) return NotesReport(word_count=word_count, budget=budget) def budget_for_kind(kind: str) -> int: if kind == "patch": return 150 if kind == "minor": return 250 raise ValueError(f"unsupported release kind: {kind}") def validate_repository(repository: str) -> str: if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repository): raise ReleaseNotesError(f"repository must be owner/name: {repository}") return repository def validate_release_identity( identity: dict[str, object], tag: str, expected_assets: Sequence[str], expected_title: str | None = None, ) -> None: version_from_tag(tag) if identity.get("tagName") != tag: raise ReleaseNotesError(f"release identity tag does not match {tag}") if identity.get("isDraft") is not False: raise ReleaseNotesError("refusing repair of a draft or unknown-state release") if identity.get("isPrerelease") is not False: raise ReleaseNotesError( "refusing repair of a prerelease or unknown-state release" ) assets = identity.get("assets") if not isinstance(assets, list) or any( not isinstance(asset, dict) or not isinstance(asset.get("name"), str) for asset in assets ): raise ReleaseNotesError("release identity has invalid assets") actual_assets = [asset["name"] for asset in assets] if sorted(actual_assets) != sorted(expected_assets): raise ReleaseNotesError("release assets do not match the expected package set") if expected_title is not None and identity.get("name") != expected_title: raise ReleaseNotesError(f"release title was not updated to {expected_title}") def render_release_notes( tag: str, changelog_section: str, plugins: Sequence[tuple[str, str]], repository: str, previous_tag: str | None = None, ) -> str: version_from_tag(tag) validate_repository(repository) validate_notes(changelog_section) lines = ["## Changes", "", changelog_section.strip()] if plugins: lines.extend( [ "", "## Plugins", "", "| Plugin | Description |", "| ------ | ----------- |", ] ) lines.extend( f"| **{_markdown_cell(name)}** | {_markdown_cell(description)} |" for name, description in plugins ) lines.extend( [ "", "## Distribution", "", "Artifacts are rendered by Agent Bundler from the repository root:", "", "```bash", "agbun build --root .", "```", ] ) if previous_tag is not None: previous_version = _version_tuple(previous_tag) if previous_version >= _version_tuple(tag): raise ReleaseNotesError( f"previous tag {previous_tag} is not older than {tag}" ) lines.extend( [ "", "## Full Changelog", "", f"https://github.com/{repository}/compare/{previous_tag}...{tag}", ] ) return "\n".join(lines) + "\n" def _markdown_cell(value: str) -> str: return " ".join(value.split()).replace("|", "\\|") def _report(report: NotesReport, kind: str) -> None: print(f"release notes: {report.word_count} words") if report.over_budget: print( f"release notes: advisory budget exceeded for {kind} release " f"({report.word_count}/{report.budget} words); review for clarity, " "but do not remove critical migration or known-issue information", file=sys.stderr, ) def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) subparsers = parser.add_subparsers(dest="command", required=True) check = subparsers.add_parser("check", help="validate a Markdown notes file") check.add_argument("file", type=Path) check.add_argument("--budget", choices=("patch", "minor")) check_changelog = subparsers.add_parser( "check-changelog", help="validate a release section in CHANGELOG.md" ) check_changelog.add_argument("--changelog", type=Path, default=Path("CHANGELOG.md")) check_changelog.add_argument( "--version", required=True, help="release version X.Y.Z" ) check_changelog.add_argument("--budget", choices=("patch", "minor")) check_versions = subparsers.add_parser( "check-versions", help="check that release version manifests are synchronized" ) check_versions.add_argument("--root", type=Path, default=Path(".")) check_versions.add_argument("--tag", help="also require manifests to match vX.Y.Z") check_release = subparsers.add_parser( "check-release", help="validate the release tag, manifests, and changelog section", ) check_release.add_argument("--root", type=Path, default=Path(".")) check_release.add_argument("--tag", required=True, help="release tag vX.Y.Z") check_release.add_argument("--changelog", type=Path, default=Path("CHANGELOG.md")) check_release.add_argument("--budget", choices=("patch", "minor")) check_identity = subparsers.add_parser( "check-release-identity", help="reject drafts and verify a published release tag and package assets", ) check_identity.add_argument("--identity", type=Path, required=True) check_identity.add_argument("--tag", required=True, help="release tag vX.Y.Z") check_identity.add_argument( "--expected-asset", action="append", required=True, dest="expected_assets" ) check_identity.add_argument("--title", help="also require the exact release title") return parser.parse_args(argv) def main(argv: Sequence[str] | None = None) -> int: args = parse_args(argv) try: budget_kind = getattr(args, "budget", None) budget = budget_for_kind(budget_kind) if budget_kind else None if args.command == "check": notes = args.file.read_text(encoding="utf-8") report = validate_notes(notes, budget) elif args.command == "check-changelog": section = extract_changelog_section( args.changelog.read_text(encoding="utf-8"), args.version ) report = validate_notes(section, budget) elif args.command == "check-versions": versions = collect_release_versions(args.root) version = validate_release_version_contract(versions, args.tag) print(f"release version manifests agree at {version}") return 0 elif args.command == "check-release-identity": identity = json.loads(args.identity.read_text(encoding="utf-8")) if not isinstance(identity, dict): raise ReleaseNotesError("release identity JSON must be an object") validate_release_identity( identity, args.tag, args.expected_assets, args.title ) print(f"release identity verified for {args.tag}") return 0 else: versions = collect_release_versions(args.root) validate_release_version_contract(versions, args.tag) section = extract_changelog_section( args.changelog.read_text(encoding="utf-8"), args.tag[1:] ) budget = budget_for_kind(args.budget) if args.budget else None report = validate_notes(section, budget) _report(report, args.budget or "release") except (OSError, ReleaseNotesError) as exc: print(f"release notes: {exc}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())
-
-
SKILL.md 4.4 KB
--- {"description":"Prepare, publish, or repair a software release and write its release notes. Use for cutting a versioned release or editing release notes. NOT for updating an installed package, publishing articles, or discussing a release; for general documentation use documenting-code.","name":"releasing-code"} --- # Releasing Code Prepare a release, route its publication, write its notes, or repair notes on a published release. Read [notes.md](references/notes.md) when writing or checking release content. Follow the project's own release instructions where they are more specific. ## Safety rules - Publishing and repair change external state. Before any external mutation, get explicit user authorization for the exact repository, tag, and action. Skill routing, a tool approval, a passing check, or workflow ownership is not authorization. - When a workflow owns the GitHub release, use that workflow's supported trigger. Do not publish around it with the CLI. - Never overwrite package versions, move an existing local or published tag, force-push, or add `--clobber`. - If the publisher, repository policy, a multi-step publication path, or the prior publication state is unclear, stop and ask. ## Prepare Done when the notes pass the checker, the full staged release diff is reviewed, and the project's release validation passes. Commit, tag, or publish only after the notes are reviewed. - Identify the project, the intended version and tag, the last published release, and the publisher. Read the working tree, tags, recent commits, diffs, and release configuration; do not infer policy from a workflow filename. - Take changes since the verified previous release from commit bodies and diffs. Keep only user-visible changes that shipped in this release. - Use the committed changelog or other established release source as the single notes source. Do not write a separate body for the hosting service. - For each declared breaking change, write an `Upgrade` section with the required action, when to take it (before or after upgrading), and the consequence of skipping it. ## Determine who publishes Read the workflow steps and scripts for the operation that actually creates or updates the GitHub release. A tag workflow that only builds or uploads package artifacts does not own the GitHub release. ## Publish 1. Verify that the exact tag points to the intended release commit. Before resuming or skipping work, check whether a hosting release already exists and confirm its tag, exact title, and attached artifact identity. Do not overwrite an existing release to force progress. 2. If a workflow owns the release, trigger it and inspect its result. 3. If no workflow owns the release, use the CLI with an explicit repository, the verified tag, the exact tag as title, and the same reviewed notes file: ```bash gh release create "$TAG" --repo owner/name --verify-tag \ --title "$TAG" --notes-file "$NOTES_FILE" ``` 4. Audit the artifacts attached to the tagged release separately. An installed-package audit is not evidence that the release assets are correct. ## Repair notes only 1. Require an already-published release; refuse to repair a draft or prerelease. Verify that the tag and release identify the intended version and package or artifact identity. The current title may be wrong when title correction is part of the repair. 2. Select the notes source explicitly as a full commit SHA, not the immutable tag. Read only the changelog from that commit; version, package metadata, and assets still come from the tag. Regenerate the section and validate it. Change no versions, assets, or tags. 3. Before mutating, back up the current title, body, and publication state (draft, prerelease, and latest flags) somewhere durable. 4. Apply the repair through the owning workflow's repair path. With no workflow owner, use `gh release edit "$TAG" --repo owner/name --title "$TAG" --notes-file "$NOTES_FILE"`. 5. Read the release back and verify its tag, exact title, body, and assets. A successful command is not proof of the published result. ## Optional release guard The packaged pre-tool `release-guard` hook runs only when `HOOK_RELEASE_GUARD=1`. It checks direct `gh release create` and `gh release edit` forms against the notes checker and permits read-only queries and `--dry-run`. It is not authorization and not a shell parser: wrappers, aliases, nested shells, scripts, and other publishers are outside its scope.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.