Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ea9ac33
Reorganize
132ikl Aug 15, 2025
32499c4
Simplify display-notices implementation
132ikl Aug 15, 2025
b08f27d
Handle N/A and empty summary differently
132ikl Aug 16, 2025
498adc6
Handle third level headings
132ikl Aug 16, 2025
35d9891
Add additional empty keywords
132ikl Aug 17, 2025
4f1a32d
Add section detection
132ikl Aug 17, 2025
3e4ee41
Add section generation
132ikl Aug 18, 2025
1993ec1
Add PR links for single line summaries
132ikl Aug 18, 2025
9db5c52
Generate release notes from template
132ikl Aug 18, 2025
497cd36
Generate Hall of Fame section
132ikl Aug 18, 2025
16f9d32
Generate full change log section
132ikl Aug 18, 2025
af7ff4d
Filter out bot PRs
132ikl Aug 18, 2025
1c5c8a3
Merge main into release-notes-extract
132ikl Aug 18, 2025
344b1ee
Move is_bot check
132ikl Aug 18, 2025
b04fc45
Remove old files
132ikl Aug 18, 2025
4a9b7a3
`path self` resolves relative paths
132ikl Aug 18, 2025
1aee18f
Allow PRs with no release notes summary but notes:ready label
132ikl Aug 19, 2025
6fcfbd1
Convert to module and add Bahex's completions
132ikl Aug 20, 2025
bfb4848
Modularize
132ikl Aug 20, 2025
ecf88b2
Fix version completion
132ikl Aug 25, 2025
a50e2e9
Change executables into module commands
132ikl Aug 25, 2025
e3e0cf6
Rename pr-notes to release-notes
132ikl Aug 25, 2025
6f7ab42
Add check-prs command
132ikl Aug 25, 2025
b107c7f
Fix summary parsing
132ikl Aug 25, 2025
ed6457e
Fix summary parsing again
132ikl Aug 25, 2025
8ad560e
Integrate create-pr with release notes generator
132ikl Aug 25, 2025
cfc374b
Fix completions
132ikl Aug 25, 2025
e2c3f6b
Fix how version appears in release notes
132ikl Aug 25, 2025
f01bc44
Add count to display-notices
132ikl Aug 27, 2025
1bd4074
Add --as-table flag to check-prs
132ikl Aug 29, 2025
7104a48
Fix check-prs input/output types
132ikl Aug 29, 2025
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
27 changes: 27 additions & 0 deletions make_release/notes/completions.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export const example_version = $"v0.((version).minor + 1).0"
export const current_build_date = ((version).build_time | parse '{date} {_}').0.date

export def last-release-date []: nothing -> datetime {
if $env.cached-var?.relase-date? == null {
$env.cached-var.relase-date = (
^gh release list
--repo "nushell/nushell"
--exclude-drafts --exclude-pre-releases
--limit 1
--json "createdAt"
)
| from json
| $in.0.createdAt
| into datetime
| $in
}
$env.cached-var.relase-date
}

export def "nu-complete version" [] { [$example_version] }
export def "nu-complete date" [add?: duration = 0wk] {
let date = last-release-date | $in + $add
[{value: ($date | format date '%F') description: ($date | to text -n)}]
}
export def "nu-complete date current" [] { nu-complete date 0wk }
export def "nu-complete date next" [] { nu-complete date 6wk }
69 changes: 35 additions & 34 deletions make_release/release-note/create-pr.nu → make_release/notes/create-pr.nu
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env nu

use std log

use completions.nu *
use tools.nu release-notes

def open-pr [
repo: path
remote: string
Expand Down Expand Up @@ -31,13 +32,10 @@ def clean [repo: path] {
}

# open the release note PR interactively
#
# # Example
# [this PR](https://github.com/nushell/nushell.github.io/pull/916) has been created with the script
# > ./make_release/release-note/create-pr 0.81 2023-06-06
def main [
version: string # the version of the release, e.g. `0.80`
date: datetime # the date of the upcoming release, e.g. `2023-05-16`
@example "Create a PR for the next release" $"create-pr ($example_version) \(($current_build_date) + 6wk\)"
export def main [
version: string@"nu-complete version" # the version of the release
date: datetime@"nu-complete date next" # the date of the upcoming release
] {
let repo = ($nu.temp-path | path join (random uuid))
let branch = $"release-notes-($version)"
Expand All @@ -49,22 +47,19 @@ def main [
let title = $"Release notes for `($version)`"
let body = $"Please add your new features and breaking changes to the release notes
by opening PRs against the `release-notes-($version)` branch.

## TODO
- [ ] PRs that need to land before the release, e.g. [deprecations]\(https://github.com/nushell/nushell/labels/deprecation\) or [removals]\(https://github.com/nushell/nushell/pulls?q=is%3Apr+is%3Aopen+label%3Aremoval-after-deprecation\)
- [ ] PRs that need to land before the release, e.g. [deprecations] or [removals]
- [ ] add the full changelog
- [ ] categorize each PR
- [ ] write all the sections and complete all the `TODO`s"
- [ ] write all the sections and complete all the `TODO`s
[deprecations]: https://github.com/nushell/nushell/labels/deprecation
[removals]: https://github.com/nushell/nushell/pulls?q=is%3Apr+is%3Aopen+label%3Aremoval-after-deprecation"

log info "creating release note from template"
let release_note = $env.CURRENT_FILE
| path dirname
| path join "template.md"
| open
| str replace --all "{{VERSION}}" $version
log info "generating release notes"
let release_note = release-notes $version

log info $"branch: ($branch)"
log info $"blog: ($blog_path | str replace $repo "" | path split | skip 1 | path join)"
log info $"blog: ($blog_path | path relative-to $repo | path basename)"
log info $"title: ($title)"

match (["yes" "no"] | input list --fuzzy "Inspect the release note document? ") {
Expand All @@ -76,48 +71,54 @@ by opening PRs against the `release-notes-($version)` branch.
}

let temp_file = $nu.temp-path | path join $"(random uuid).md"
$release_note | save --force $temp_file
[
"<!-- WARNING: Changes made to this file are NOT included in the PR -->"
""
$release_note
] | to text | save --force $temp_file
^$env.EDITOR $temp_file
rm --recursive --force $temp_file
},
"no" | "" | _ => {},
"no" | "" | _ => { }
}

match (["no" "yes"] | input list --fuzzy "Open release note PR? ") {
"yes" => {},
"yes" => { },
"no" | "" | _ => {
log warning "aborting."
return
},
}
}

log info "setting up nushell.github.io repo"
git clone https://github.com/nushell/nushell.github.io $repo --origin nushell --branch main --single-branch
git -C $repo remote set-url nushell --push [email protected]:nushell/nushell.github.io.git
^git clone https://github.com/nushell/nushell.github.io $repo --origin nushell --branch main --single-branch
^git -C $repo remote set-url nushell --push [email protected]:nushell/nushell.github.io.git

log info "creating release branch"
git -C $repo checkout -b $branch
^git -C $repo checkout -b $branch

log info "writing release note"
$release_note | save --force $blog_path

log info "committing release note"
git -C $repo add $blog_path
git -C $repo commit -m $"($title)\n\n($body)"
^git -C $repo add $blog_path
^git -C $repo commit -m $"($title)\n\n($body)"

log info "pushing release note to nushell"
git -C $repo push nushell $branch
^git -C $repo push nushell $branch

let out = (do -i { gh auth status } | complete)
let out = (do -i { ^gh auth status } | complete)
if $out.exit_code != 0 {
clean $repo

let pr_url = $"https://github.com/nushell/nushell.github.io/compare/($branch)?expand=1"
error make --unspanned {
msg: ([
$out.stderr
$"please open the PR manually from a browser (ansi blue_underline)($pr_url)(ansi reset)"
] | str join "\n")
msg: (
[
$out.stderr
$"please open the PR manually from a browser (ansi blue_underline)($pr_url)(ansi reset)"
] | str join "\n"
)
}
}

Expand Down
178 changes: 178 additions & 0 deletions make_release/notes/generate.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# The sections to be included in the release notes
const SECTIONS = [
[label, h2, h3];
["notes:breaking-changes", "Breaking changes", "Other breaking changes"]
["notes:additions", "Additions", "Other additions"]
["notes:deprecations", "Deprecations", "Other deprecations"]
["notes:removals", "Removals", "Other removals"]
["notes:other", "Other changes", "Additional changes"]
["notes:fixes", "Bug fixes", "Other fixes"]
["notes:mention", null, null]
]

use notice.nu *
use util.nu *

# Attempt to extract the "Release notes summary" section from a PR.
#
# Multiple checks are done to ensure that each PR has a valid release notes summary.
# If any issues are detected, a "notices" column with additional information is added.
export def get-release-notes []: record -> record {
mut pr = $in

let has_ready_label = "notes:ready" in $pr.labels.name
let sections = $SECTIONS | where label in $pr.labels.name
let hall_of_fame = $SECTIONS | where label == "notes:mention" | only

# Extract the notes section
mut notes = if "## Release notes summary" in $pr.body {
$pr.body | extract-notes
} else if $has_ready_label {
# If no release notes summary exists but ready label is set, treat as empty
$pr = $pr | add-notice warning "no release notes section but notes:ready label"
""
} else {
return ($pr | add-notice error "no release notes section")
}

# Check for empty notes section
if ($notes | is-empty-keyword) {
if ($sections | where label != "notes:mention" | is-not-empty) {
return ($pr | add-notice error "empty summary has a category other than Hall of Fame")
}

if ($notes | is-empty) and not $has_ready_label {
$pr = $pr | add-notice warning "empty release notes section and no explicit label"
}

$pr = $pr | insert section $hall_of_fame
$pr = $pr | insert notes ($pr.title | clean-title)
return $pr
}

# If the notes section isn't empty, make sure we have the ready label
if not $has_ready_label {
return ($pr | add-notice error $"no notes:ready label")
}

# Check that exactly one category is selected
let section = if ($sections | is-empty) {
$pr = $pr | add-notice info "no explicit release notes category selected (defaults to Hall of Fame)"
$hall_of_fame
} else if ($sections | length) > 1 {
return ($pr | add-notice error "multiple release notes categories selected")
} else {
$sections | only
}

# Add section to PR
$pr = $pr | insert section $section

let lines = $notes | lines | length
if $section.label == "notes:mention" and ($lines > 1) {
return ($pr | add-notice error "multi-line summaries in Hall of Fame section")
}

# Add PR title as default heading for multi-line summaries
if $lines > 1 and not ($notes starts-with "###") {
$pr = $pr | add-notice info "multi-line summaries with no explicit title (using PR title as heading title)"
$notes = "### " + ($pr.title | clean-title) ++ (char nl) ++ $notes
}

# Check for suspiciously short release notes section
if ($notes | split words | length) < 10 {
$pr = $pr | add-notice warning "release notes section that is less than 10 words"
}

$pr | insert notes $notes
}

# Extracts the "Release notes summary" section of the PR description
export def extract-notes []: string -> string {
lines
# skip until release notes heading
| skip until { $in starts-with "## Release notes summary" }
# this should already have been checked
| if ($in | is-empty) { assert false } else {}
| skip 1 # remove header
# extract until next heading
| take until {
$in starts-with "# " or $in starts-with "## " or $in starts-with "---"
}
| str join (char nl)
# remove HTML comments
| str replace -amr '<!--\O*?-->' ''
| str trim
}

# Generate the release notes from the list of PRs.
export def generate-notes [version: string]: table -> string {
let prs = $in

const template_path = path self "template.md"
let template = open $template_path
let arguments = {
# chop off the `v` in the version
version: ($version | str substring 1..),
changes: ($prs | generate-changes-section),
hall_of_fame: ($prs | generate-hall-of-fame)
changelog: (generate-full-changelog $version)
}

$arguments | format pattern $template
}

# Generate the "Changes" section of the release notes.
export def generate-changes-section []: table -> string {
group-by --to-table section.label
| rename section prs
# sort sections in order of appearance in table
| sort-by {|i| $SECTIONS | enumerate | where item.label == $i.section | only }
# Hall of Fame is handled separately
| where section != "notes:mention"
| each { generate-section }
| str join (char nl)
}

# Generate a subsection of the "Changes" section of the release notes.
export def generate-section []: record<section: string, prs: table> -> string {
let prs = $in.prs
let section = $prs.0.section

mut body = []
let multiline = $prs | where ($it.notes | lines | length) > 1
let bullet = $prs | where ($it.notes | lines | length) == 1

# Add header
$body ++= [$"## ($section.h2)"]

# Add multi-line summaries
$body ++= $multiline.notes

# Add single-line summaries
if ($multiline | is-not-empty) {
$body ++= [$"### ($section.h3)"]
}
$body ++= $bullet | each {|pr| "* " ++ $pr.notes ++ $" \(($pr | pr-link)\)" }

$body | str join (char nl)
}

# Generate the "Hall of Fame" section of the release notes.
export def generate-hall-of-fame []: table -> string {
where section.label == "notes:mention"
# If the PR has no notes, use the title
| update notes {|pr| default -e $pr.title }
| update author { md-link $'@($in.login)' $'https://github.com/($in.login)' }
| insert link { pr-link }
| select author notes link
| rename -c {notes: change}
| to md
| escape-tag
}

# Generate the "Full changelog" section of the release notes.
export def generate-full-changelog [version: string]: nothing -> string {
list-prs --milestone=$version
| pr-table
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
#!/usr/bin/env nu


# Prepare the GitHub release text
def main [
export def main [
versionname: string # The version we release now
bloglink: string # The link to the blogpost
date?: datetime # the date of the last release (default to 6 weeks ago, excluded)
Expand Down
3 changes: 3 additions & 0 deletions make_release/notes/mod.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export use tools.nu *
export use gh-release-excerpt.nu
export use create-pr.nu
30 changes: 30 additions & 0 deletions make_release/notes/notice.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Add an entry to the "notices" field of a PR
export def add-notice [type: string, message: string]: record -> record {
upsert notices {
append {type: $type, message: $message}
}
}

# Print all of the notices associated with a PR
export def display-notices []: table -> nothing {
let prs = $in
let types = [
[type, color, rank];
[info, (ansi default), 0]
[warning, (ansi yellow), 1]
[error, (ansi red), 2]
]

$prs
| flatten -a notices
| group-by --to-table type? message?
| sort-by {|i| $types | where type == $i.type | only rank } message
| each {|e|
let color = $types | where type == $e.type | only color
let number = $e.items | length
print $"($color)($number) PR\(s\) with ($e.message):"
$e.items | each { format-pr | print $"- ($in)" }
print ""
}
print -n (ansi reset)
}
Loading