Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .agents/skills/generate-release-note/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
name: generate-release-note
description: Generate and insert a user-facing release note entry for the current GitHub pull request in docs/about/release-notes.md. Use when the user asks to create a release note, changelog entry, user-facing PR summary, or to update release notes for a PR.
---

# Generate Release Note

## Overview

Generate one concise, user-facing release note entry for the current PR and insert it into the latest release section of `docs/about/release-notes.md`.

## Workflow

1. Inspect the current release note style:

```bash
sed -n '1,120p' docs/about/release-notes.md
```

2. Gather current PR context:

```bash
gh pr view --json number,title,body,url,closingIssuesReferences,files,commits
```

3. Draft one release note bullet in the style of the latest section. Use English by default.

4. Choose the target category:
- `Fixed` for bug fixes and corrected user-visible behavior.
- `Added` for new user-facing capabilities.
- `Changed` for changed behavior, defaults, or compatibility.
- `Maintenance` for dependencies, packaging, CI, release automation, or maintainer-facing work.
- Prefer an existing category in the latest release section when it fits.

5. Insert the bullet:

```bash
python3 .agents/skills/generate-release-note/scripts/insert_release_note.py \
--category Fixed \
--note '* Fix anchor link validation so strict builds fail when missing anchors are reported as warnings. #30'
```

6. Re-read the top of `docs/about/release-notes.md` and make sure the entry landed in the correct latest release section.

## Placement Rules

Always update `docs/about/release-notes.md`. Insert into the first `## Version ... (...)` section in the file, which is the current next-release section.

If the top section is an unreleased version such as `## Version 1.7.1 (Unreleased)`, use that section. If the next version number is already known, use that version section. Do not insert into older published versions.

When a target category already exists, append the entry at the end of that category. When it does not exist, create the category inside the latest release section before the next version heading.

## Entry Style

Match the existing release note source style:

- Use `* ` bullets.
- Write concise, user-facing English.
- Describe behavior or value users can observe.
- Avoid implementation details such as file names, commit hashes, tests, or internal refactors unless directly relevant to users.
- Use backticks for commands, options, configuration keys, package names, and code-like terms.

Examples:

```markdown
* Fix `mkdocs serve --livereload` so the option is recognized correctly again. #4
* Remove the unmaintained `mergedeep` dependency by replacing it with an internal deep-merge helper for inherited YAML configuration. #29
```

## Link Rules

If `closingIssuesReferences` has user issues, end the entry with the issue number references such as `#30`. Keep issue references short in the source file; `docs/hooks.py` rewrites them to the correct GitHub issue links during docs rendering.

If there is no user issue, end the entry with the bare PR number reference such as `#123`.

If multiple user issues are relevant, include all issue references at the end of the same bullet.

## Verification

After insertion, check:

- The entry is under the latest `## Version ... (...)` section.
- The category heading is appropriate and matches nearby release note style.
- Issue-backed entries use bare issue references.
- PR-backed entries use bare PR references.
- The entry is not duplicated.
4 changes: 4 additions & 0 deletions .agents/skills/generate-release-note/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "Generate release note"
short_description: "Generate user-facing release notes"
default_prompt: "Use $generate-release-note to generate and insert a user-facing release note for this PR."
110 changes: 110 additions & 0 deletions .agents/skills/generate-release-note/scripts/insert_release_note.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import re
import sys
from pathlib import Path

DEFAULT_RELEASE_NOTES = Path("docs/about/release-notes.md")
VERSION_HEADING_RE = re.compile(r"^## Version .+ \([^)]+\)\s*$", re.MULTILINE)
CATEGORY_HEADING_RE = re.compile(r"^### (?P<category>.+?)\s*$", re.MULTILINE)


def normalize_note(note: str) -> str:
note = note.strip()
if not note:
raise SystemExit("Release note cannot be empty.")
if not note.startswith("* "):
note = f"* {note}"
return note


def find_latest_release_section(text: str) -> tuple[int, int]:
matches = list(VERSION_HEADING_RE.finditer(text))
if not matches:
raise SystemExit("No '## Version ... (...)' release section found.")

start = matches[0].start()
end = matches[1].start() if len(matches) > 1 else len(text)
return start, end


def find_category(section: str, category: str) -> tuple[int, int] | None:
matches = list(CATEGORY_HEADING_RE.finditer(section))
for index, match in enumerate(matches):
if match.group("category").strip().casefold() == category.casefold():
start = match.end()
end = (
matches[index + 1].start() if index + 1 < len(matches) else len(section)
)
return start, end
return None


def append_to_category(section: str, category: str, note: str) -> str:
existing = find_category(section, category)
if existing is None:
section = section.rstrip()
return f"{section}\n\n### {category}\n\n{note}\n\n"

_category_start, category_end = existing
before = section[:category_end].rstrip()
after = section[category_end:].lstrip("\n")

if note in before:
return section

inserted = f"{before}\n{note}\n"
if after:
return f"{inserted}\n{after}"
return f"{inserted}\n"


def insert_release_note(text: str, category: str, note: str) -> str:
section_start, section_end = find_latest_release_section(text)
section = text[section_start:section_end]
new_section = append_to_category(section, category, note)
return f"{text[:section_start]}{new_section}{text[section_end:]}"


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Insert a release note bullet into the latest release section."
)
parser.add_argument("--note", required=True, help="Release note bullet to insert.")
parser.add_argument(
"--category",
required=True,
help="Target category heading, for example Fixed, Added, Changed, or Maintenance.",
)
parser.add_argument(
"--file",
type=Path,
default=DEFAULT_RELEASE_NOTES,
help=f"Release notes file to update. Defaults to {DEFAULT_RELEASE_NOTES}.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print the updated file content without writing it.",
)
return parser.parse_args()


def main() -> None:
args = parse_args()
note = normalize_note(args.note)
text = args.file.read_text(encoding="utf-8")
updated = insert_release_note(text, args.category.strip(), note)

if args.dry_run:
sys.stdout.write(updated)
return

if updated != text:
args.file.write_text(updated, encoding="utf-8")


if __name__ == "__main__":
main()
19 changes: 19 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Development Rules

## Quality Gate

* Before every final commit, run `uvx prek run -a`.
* Pre-commit must pass before committing. If it fails, fix the reported issues and rerun `uvx prek run -a` until it passes.
* Do not use `git commit --no-verify` to bypass required checks.

## Git Safety

* Run `git status` before staging or committing.
* Stage only files changed for the current task, using explicit paths.
* Do not use `git add .` or `git add -A`, because they can include unrelated work.
* Do not run destructive commands such as `git reset --hard`, `git checkout .`, or `git clean -fd` unless the user explicitly asks for them.

## Project Notes

* When updating release notes, edit `docs/about/release-notes.md` and add entries to the latest version section near the top of the file.
* Keep release note entries user-facing and consistent with the surrounding section style.
Loading
Loading