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
9 changes: 7 additions & 2 deletions .github/workflows/require-release-note.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ jobs:
name: Require release note
runs-on: ubuntu-latest
steps:
- name: Require release-notes.md update
- name: Require release note
uses: coreos/actions-lib/require-file-change@main
with:
path: docs/release-notes.md
path-regex: '^docs/release-notes\.d/[a-z0-9]+(-[a-z0-9]+)*\.(breaking|feature|change|bugfix)\.md$'
status: added
override-label: skip-notes
- name: Check out repository
uses: actions/checkout@v6
- name: Validate release-note fragments
run: go run ./internal/util/tools/release-notes validate
15 changes: 10 additions & 5 deletions .opencode/skills/release_note_update/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
---
name: release_note_update
description: Automate release notes update and offer tagging.
description: Finalize a release from release-note fragments without committing.
---

<!-- Maintained in https://github.com/coreos/repo-templates -->
<!-- Do not edit downstream. -->

# Release Note Update

This skill prepares release notes by running the codified script.
This skill finalizes release notes from validated fragments by running the
repository's release-note tool.

## Usage

Expand All @@ -20,6 +24,7 @@ Then execute:
.opencode/skills/release_note_update/run.sh <version> [<date>]
```

The script handles everything: parsing the unreleased section, converting items to imperative mood, creating the versioned section, updating the unreleased header for the next version, and creating the commit.

See `README.md` in this directory for detailed documentation.
The script validates and renders `docs/release-notes.d` fragments, creates the
versioned section, updates the Upcoming heading to the next minor version, and
deletes consumed fragments. It does not create a commit. Review and commit the
result separately.
195 changes: 7 additions & 188 deletions .opencode/skills/release_note_update/run.sh
Original file line number Diff line number Diff line change
@@ -1,202 +1,21 @@
#!/usr/bin/env bash
# Maintained in https://github.com/coreos/repo-templates
# Do not edit downstream.

set -euo pipefail

usage() {
echo "Usage: $0 VERSION [DATE]"
echo ""
echo "Move unreleased items in docs/release-notes.md to a new version section."
echo ""
echo "Arguments:"
echo " VERSION Release version (e.g. 2.26.0)"
echo " DATE Release date in YYYY-MM-DD format (default: today)"
echo "Usage: $0 VERSION [DATE]" >&2
exit 1
}

if [[ $# -lt 1 ]]; then
if [[ $# -lt 1 || $# -gt 2 ]]; then
usage
fi

VERSION="$1"
DATE="${2:-$(date +%Y-%m-%d)}"

if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: VERSION must be in X.Y.Z format, got: $VERSION"
exit 1
fi

if ! [[ "$DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "Error: DATE must be in YYYY-MM-DD format, got: $DATE"
exit 1
fi

REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
cd "$REPO_ROOT"

RELNOTES="docs/release-notes.md"

if [[ ! -f "$RELNOTES" ]]; then
echo "Error: $RELNOTES not found"
exit 1
fi

# Find the unreleased section
UNRELEASED_LINE=$(grep -n '(unreleased)' "$RELNOTES" | head -1 | cut -d: -f1)
if [[ -z "$UNRELEASED_LINE" ]]; then
echo "Error: Could not find an '(unreleased)' section in $RELNOTES"
exit 1
fi

# Find the next release section (next "## " header after unreleased)
NEXT_RELEASE_LINE=$(awk "NR>$UNRELEASED_LINE && /^## /{print NR; exit}" "$RELNOTES")
if [[ -z "$NEXT_RELEASE_LINE" ]]; then
NEXT_RELEASE_LINE=$(wc -l < "$RELNOTES")
fi

# Extract the unreleased content between the two headers
UNRELEASED_CONTENT=$(sed -n "$((UNRELEASED_LINE + 1)),$((NEXT_RELEASE_LINE - 1))p" "$RELNOTES")

# Parse sections and their items
declare -A SECTION_ITEMS
CURRENT_SECTION=""
SECTIONS_ORDER=()

while IFS= read -r line; do
if [[ "$line" =~ ^###[[:space:]]+(.*) ]]; then
CURRENT_SECTION="${BASH_REMATCH[1]}"
SECTIONS_ORDER+=("$CURRENT_SECTION")
SECTION_ITEMS["$CURRENT_SECTION"]=""
elif [[ -n "$CURRENT_SECTION" && "$line" =~ ^-[[:space:]] ]]; then
if [[ -n "${SECTION_ITEMS[$CURRENT_SECTION]}" ]]; then
SECTION_ITEMS["$CURRENT_SECTION"]+=$'\n'
fi
SECTION_ITEMS["$CURRENT_SECTION"]+="$line"
fi
done <<< "$UNRELEASED_CONTENT"

# Convert items to imperative mood
to_imperative() {
local line="$1"
# Strip leading "- " to get the first word
local rest="${line#- }"
local first_word="${rest%% *}"
local remainder="${rest#* }"
local lower_word=${first_word,,}

local imperative=""
case "$lower_word" in
added|adding|adds) imperative="Add" ;;
fixed|fixing|fixes) imperative="Fix" ;;
updated|updating|updates) imperative="Update" ;;
removed|removing|removes) imperative="Remove" ;;
changed|changing|changes) imperative="Change" ;;
created|creating|creates) imperative="Create" ;;
supported|supporting|supports) imperative="Support" ;;
included|including|includes) imperative="Include" ;;
dropped|dropping|drops) imperative="Drop" ;;
marked|marking|marks) imperative="Mark" ;;
bumped|bumping|bumps) imperative="Bump" ;;
moved|moving|moves) imperative="Move" ;;
renamed|renaming|renames) imperative="Rename" ;;
stabilized|stabilizing|stabilizes) imperative="Stabilize" ;;
implemented|implementing|implements) imperative="Implement" ;;
enabled|enabling|enables) imperative="Enable" ;;
disabled|disabling|disables) imperative="Disable" ;;
allowed|allowing|allows) imperative="Allow" ;;
prevented|preventing|prevents) imperative="Prevent" ;;
deprecated|deprecating|deprecates) imperative="Deprecate" ;;
replaced|replacing|replaces) imperative="Replace" ;;
migrated|migrating|migrates) imperative="Migrate" ;;
resolved|resolving|resolves) imperative="Resolve" ;;
improved|improving|improves) imperative="Improve" ;;
refactored|refactoring|refactors) imperative="Refactor" ;;
*)
# Already imperative or unknown — leave as-is
echo "$line"
return
;;
esac

if [[ "$rest" == "$first_word" ]]; then
echo "- $imperative"
else
echo "- $imperative $remainder"
fi
}

# Calculate next unreleased version (bump minor)
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
NEXT_MINOR=$((MINOR + 1))
NEXT_VERSION="${MAJOR}.${NEXT_MINOR}.0"

# Build the new file content
{
# Everything before the unreleased header
head -n "$((UNRELEASED_LINE - 1))" "$RELNOTES"

# New unreleased section (empty)
echo "## Upcoming Ignition $NEXT_VERSION (unreleased)"
echo ""
echo "### Breaking changes"
echo ""
echo "### Features"
echo ""
echo "### Changes"
echo ""
echo "### Bug fixes"
echo ""
echo ""

# New release section
echo "## Ignition $VERSION ($DATE)"

# Only include sections that have items
for section in "${SECTIONS_ORDER[@]}"; do
items="${SECTION_ITEMS[$section]}"
if [[ -n "$items" ]]; then
echo ""
echo "### $section"
echo ""
while IFS= read -r item; do
to_imperative "$item"
done <<< "$items"
fi
done
echo ""

# Everything from the next release onward
tail -n +"$NEXT_RELEASE_LINE" "$RELNOTES"
} > "$RELNOTES.tmp"
mv "$RELNOTES.tmp" "$RELNOTES"

# Count items
TOTAL_ITEMS=0
echo "Release notes updated for $VERSION ($DATE)"
echo ""
for section in "${SECTIONS_ORDER[@]}"; do
items="${SECTION_ITEMS[$section]}"
if [[ -n "$items" ]]; then
count=$(echo "$items" | wc -l)
TOTAL_ITEMS=$((TOTAL_ITEMS + count))
echo " $count ${section,,}"
fi
done
echo ""

if [[ $TOTAL_ITEMS -eq 0 ]]; then
echo " (no items in unreleased section)"
fi

echo "New unreleased section created for $NEXT_VERSION"
echo ""

# Commit
git add "$RELNOTES"
git commit -m "docs/release-notes: update for $VERSION"

echo "Committed: docs/release-notes: update for $VERSION"
echo ""
echo "Next steps:"
echo " 1. Review changes: git show"
echo " 2. Push to remote when ready"
echo " 3. Create a GitHub release/tag if needed"
cd "$REPO_ROOT"
exec go run ./internal/util/tools/release-notes finalize "$VERSION" "$DATE"
48 changes: 48 additions & 0 deletions docs/release-notes.d/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!-- Maintained in https://github.com/coreos/repo-templates -->
<!-- Do not edit downstream. -->

# Release-note fragments

Every pull request with a user-visible change should add a release-note
fragment to this directory. Name it `<lowercase-kebab-slug>.<category>.md`,
using a short descriptive slug made of lowercase letters, numbers, and single
hyphens. `<category>` is one of:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

- `breaking` for breaking changes
- `feature` for new features
- `change` for other behavior or project changes
- `bugfix` for bug fixes

If one pull request needs multiple notes in the same category, give each
fragment a different descriptive slug.

Write the note as the body of a Markdown list item, without the leading `- `.
Multiline Markdown, including fenced code blocks, is supported. The
release-note tool adds the list marker and renders the pull request link on a
separate continuation line after the fragment body.
After the pull request is merged, finalization finds the fragment's adding
commit in Git first-parent history and derives the pull request link from the
merge or squash commit subject. It also verifies that the exact fragment is
present in `HEAD`, so uncommitted fragments or reused filenames cannot inherit
an older pull request. No PR number is needed in a normal fragment.

For exceptional migrations or backports whose adding commit cannot identify
the original pull request, the first line may specify an override such as
`<!-- release-note-pr: 2343 -->`. Do not use this override in the normal
contributor workflow.

Run these commands from the repository root:

```console
go run ./internal/util/tools/release-notes validate
go run ./internal/util/tools/release-notes preview
go run ./internal/util/tools/release-notes finalize 2.29.0 2026-11-12
```

`validate` and `preview` work before merge and do not modify fragments. Preview
omits links that still need Git history resolution and shows links from
explicit overrides. `finalize` requires at least one fragment, resolvable pull
request links, and the version in the current Upcoming heading. It replaces
that section with the rendered release, creates the next minor Upcoming
scaffold, and deletes the consumed fragments. It does not create a Git commit
or use network APIs.
Loading
Loading