From ea9ac33294f86a129b9179257fd6c491f3894af3 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Fri, 15 Aug 2025 18:56:47 -0400 Subject: [PATCH 01/29] Reorganize --- make_release/release-note/notes.nu | 35 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/make_release/release-note/notes.nu b/make_release/release-note/notes.nu index abefc71d..f94d7d96 100755 --- a/make_release/release-note/notes.nu +++ b/make_release/release-note/notes.nu @@ -10,7 +10,7 @@ export def list-prs [ --since: datetime # list PRs on or after this date (defaults to 4 weeks ago if `--milestone` is not provided) --milestone: string # only list PRs in a certain milestone --label: string # the PR label to filter by, e.g. 'good-first-issue' -] { +]: nothing -> table { query-prs $repo --since=$since --milestone=$milestone --label=$label | select author title number mergedAt url | sort-by mergedAt --reverse @@ -23,7 +23,7 @@ def query-prs [ --since: datetime # list PRs on or after this date (defaults to 4 weeks ago if `--milestone` is not provided) --milestone: string # only list PRs in a certain milestone --label: string # the PR label to filter by, e.g. 'good-first-issue' -] { +]: nothing -> table { mut query_parts = [] if $since != null or $milestone == null { @@ -54,9 +54,9 @@ export def pr-notes [ --since: datetime # list PRs on or after this date (defaults to 4 weeks ago if `--milestone` is not provided) --milestone: string # only list PRs in a certain milestone --label: string # the PR label to filter by, e.g. 'good-first-issue' -] { +]: nothing -> table { let processed = ( - list-prs $repo --since=$since --milestone=$milestone --label=$label + query-prs $repo --since=$since --milestone=$milestone --label=$label | sort-by mergedAt | each { get-release-notes } ) @@ -68,13 +68,6 @@ export def pr-notes [ | select author title number mergedAt url notes } -def format-pr []: record -> string { - let pr = $in - let text = $"#($pr.number): ($pr.title)" - $pr.url - | ansi link -t $text - | "- " ++ $in -} # Attempt to extract the "Release notes summary" section from a PR. # @@ -133,11 +126,12 @@ def extract-notes []: string -> string { | str trim } +# Check if the release notes section was left empty def notes-are-empty []: string -> bool { $in in ["", "N/A"] } -# Adds an entry to the "notices" field of a PR +# Add an entry to the "notices" field of a PR def add-notice [type: string, message: string]: record -> record { upsert notices { append {type: $type, message: $message} @@ -145,10 +139,7 @@ def add-notice [type: string, message: string]: record -> record { } # Print all of the notices associated with a PR -def display-notices [] { - let prs = $in - - $prs +def display-notices []: table -> nothing { # Create row with PR info for each notice | each {|pr| get notices | each {|e| @@ -163,7 +154,7 @@ def display-notices [] { } # Print notices of a certain type -def display-notice-type [type: string] { +def display-notice-type [type: string]: table -> nothing { let prs = $in let colors = {error: (ansi red), warning: (ansi yellow)} let color = $colors | get $type @@ -173,9 +164,17 @@ def display-notice-type [type: string] { | sort-by message | each {|e| print $"($color)PRs with ($e.message):" - $e.items | each { format-pr | print } + $e.items | each { format-pr | print $"- ($in)" } print "" } + | ignore +} + +# Format a PR nicely, including a link +def format-pr []: record -> string { + let pr = $in + let text = $"#($pr.number): ($pr.title)" + $pr.url | ansi link -t $text } # Format the output of `list-prs` as a markdown table From 32499c454979a8a5d0736a64fb1d470a9b3b48ef Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Fri, 15 Aug 2025 19:30:33 -0400 Subject: [PATCH 02/29] Simplify display-notices implementation --- make_release/release-note/notes.nu | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/make_release/release-note/notes.nu b/make_release/release-note/notes.nu index f94d7d96..14a62cc5 100755 --- a/make_release/release-note/notes.nu +++ b/make_release/release-note/notes.nu @@ -140,29 +140,15 @@ def add-notice [type: string, message: string]: record -> record { # Print all of the notices associated with a PR def display-notices []: table -> nothing { - # Create row with PR info for each notice - | each {|pr| - get notices | each {|e| - $pr | insert type $e.type | insert message $e.message - } - } - | flatten - | group-by --to-table type - | sort-by -r type - | each {|e| $e.items | display-notice-type $e.type } - | ignore -} - -# Print notices of a certain type -def display-notice-type [type: string]: table -> nothing { let prs = $in let colors = {error: (ansi red), warning: (ansi yellow)} - let color = $colors | get $type $prs - | group-by message --to-table - | sort-by message + | flatten -a notices + | group-by --to-table type message + | sort-by -r type | each {|e| + let color = $colors | get $e.type print $"($color)PRs with ($e.message):" $e.items | each { format-pr | print $"- ($in)" } print "" From b08f27d80d327dc1ce4e2453611e0f04b4852642 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Fri, 15 Aug 2025 23:23:44 -0400 Subject: [PATCH 03/29] Handle N/A and empty summary differently --- make_release/release-note/notes.nu | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/make_release/release-note/notes.nu b/make_release/release-note/notes.nu index 14a62cc5..953e3a9a 100755 --- a/make_release/release-note/notes.nu +++ b/make_release/release-note/notes.nu @@ -64,8 +64,8 @@ export def pr-notes [ $processed | display-notices $processed - | where notes? != null - | select author title number mergedAt url notes + | where {|pr| "error" not-in ($pr.notices?.type? | default []) } + | select author title number mergedAt url notes? } @@ -84,14 +84,19 @@ def get-release-notes []: record -> record { let notes = $pr.body | extract-notes let has_ready_label = $READY in $pr.labels.name - # If the notes are empty, it doesn't need any labels - if ($notes | notes-are-empty) { + # Check for empty notes section + if ($notes | is-empty) { if not $has_ready_label { $pr = $pr | add-notice warning "empty release notes section and no explicit label" } return $pr } + # Check for N/A notes section + if $notes == "N/A" { + return $pr + } + # If the notes section isn't empty, make sure we have the ready label if $READY not-in $pr.labels.name { return ($pr | add-notice error $"no ($READY) label") @@ -145,7 +150,7 @@ def display-notices []: table -> nothing { $prs | flatten -a notices - | group-by --to-table type message + | group-by --to-table type? message? | sort-by -r type | each {|e| let color = $colors | get $e.type From 498adc6e5678802c44f47211d1c6a0faf02dd353 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Fri, 15 Aug 2025 23:27:15 -0400 Subject: [PATCH 04/29] Handle third level headings --- make_release/release-note/notes.nu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make_release/release-note/notes.nu b/make_release/release-note/notes.nu index 953e3a9a..abe0c0b4 100755 --- a/make_release/release-note/notes.nu +++ b/make_release/release-note/notes.nu @@ -124,7 +124,7 @@ def extract-notes []: string -> string { | if ($in | is-empty) { assert false } else {} | skip 1 # remove header # extract until next heading - | take until { $in starts-with "##" } + | take until { $in starts-with "## " or $in starts-with "---" } | str join (char nl) # remove HTML comments | str replace -amr '' '' From 35d9891554449409b800e3e1b6b55ae7456a880e Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Sun, 17 Aug 2025 18:35:34 -0400 Subject: [PATCH 05/29] Add additional empty keywords --- make_release/release-note/notes.nu | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/make_release/release-note/notes.nu b/make_release/release-note/notes.nu index abe0c0b4..98ecf0c0 100755 --- a/make_release/release-note/notes.nu +++ b/make_release/release-note/notes.nu @@ -93,7 +93,7 @@ def get-release-notes []: record -> record { } # Check for N/A notes section - if $notes == "N/A" { + if ($notes | is-empty-keyword) { return $pr } @@ -131,9 +131,9 @@ def extract-notes []: string -> string { | str trim } -# Check if the release notes section was left empty -def notes-are-empty []: string -> bool { - $in in ["", "N/A"] +# Check if the release notes section was explicitly left empty +def is-empty-keyword []: string -> bool { + str downcase | $in in ["n/a", "nothing", "none", "nan"] } # Add an entry to the "notices" field of a PR From 4f1a32d9d83867d279d536c61588939f2bdf40fc Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Sun, 17 Aug 2025 19:08:37 -0400 Subject: [PATCH 06/29] Add section detection --- make_release/release-note/notes.nu | 113 ++++++++++++++++++++--------- 1 file changed, 80 insertions(+), 33 deletions(-) diff --git a/make_release/release-note/notes.nu b/make_release/release-note/notes.nu index 98ecf0c0..93564f21 100755 --- a/make_release/release-note/notes.nu +++ b/make_release/release-note/notes.nu @@ -1,8 +1,16 @@ use std/assert - -def md-link [text: string, link: string] { - $"[($text)]\(($link)\)" -} +use std-rfc/iter only + +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] +] # List all merged PRs since the last release export def list-prs [ @@ -55,19 +63,18 @@ export def pr-notes [ --milestone: string # only list PRs in a certain milestone --label: string # the PR label to filter by, e.g. 'good-first-issue' ]: nothing -> table { - let processed = ( - query-prs $repo --since=$since --milestone=$milestone --label=$label - | sort-by mergedAt - | each { get-release-notes } - ) - - $processed | display-notices - - $processed + query-prs $repo --since=$since --milestone=$milestone --label=$label + | sort-by mergedAt + | each { get-release-notes } + | collect + | tee { display-notices } | where {|pr| "error" not-in ($pr.notices?.type? | default []) } - | select author title number mergedAt url notes? + | select author title number section mergedAt url notes? } +def pr-notes-sections [] { + +} # Attempt to extract the "Release notes summary" section from a PR. # @@ -75,36 +82,59 @@ export def pr-notes [ # If any issues are detected, a "notices" column with additional information is added. def get-release-notes []: record -> record { mut pr = $in - const READY = "pr:release-notes-mention" if "## Release notes summary" not-in $pr.body { return ($pr | add-notice error "no release notes section") } - let notes = $pr.body | extract-notes - let has_ready_label = $READY in $pr.labels.name + mut notes = $pr.body | extract-notes + + 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 # Check for empty notes section - if ($notes | is-empty) { - if not $has_ready_label { + 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" } - return $pr - } - # Check for N/A notes section - if ($notes | is-empty-keyword) { + $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 $READY not-in $pr.labels.name { - return ($pr | add-notice error $"no ($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 } - # Check that a category is selected - if ($pr.labels.name | where $it starts-with "pr:" | reject $READY | is-empty) { - $pr = $pr | add-notice warning "no explicit release notes category selected (defaults to mention)" + # 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 @@ -131,9 +161,17 @@ def extract-notes []: string -> string { | str trim } -# Check if the release notes section was explicitly left empty +# Clean up a PR title +def clean-title []: string -> string { + # remove any prefixes and capitalize + str replace -r '^[^\s]+: ' "" + | str trim + | str capitalize +} + +# Check if the release notes section was left empty def is-empty-keyword []: string -> bool { - str downcase | $in in ["n/a", "nothing", "none", "nan"] + str downcase | $in in ["", "n/a", "nothing", "none", "nan"] } # Add an entry to the "notices" field of a PR @@ -146,14 +184,19 @@ def add-notice [type: string, message: string]: record -> record { # Print all of the notices associated with a PR def display-notices []: table -> nothing { let prs = $in - let colors = {error: (ansi red), warning: (ansi yellow)} + 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 -r type + | sort-by {|i| $types | where type == $i.type | only rank } message | each {|e| - let color = $colors | get $e.type + let color = $types | where type == $e.type | only color print $"($color)PRs with ($e.message):" $e.items | each { format-pr | print $"- ($in)" } print "" @@ -168,6 +211,10 @@ def format-pr []: record -> string { $pr.url | ansi link -t $text } +def md-link [text: string, link: string] { + $"[($text)]\(($link)\)" +} + # Format the output of `list-prs` as a markdown table export def pr-table [] { sort-by author number From 1993ec1407260479a369f5ca94cb1d3616d4ca5d Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Sun, 17 Aug 2025 20:20:18 -0400 Subject: [PATCH 07/29] Add PR links for single line summaries --- make_release/release-note/notes.nu | 8 +++++++- make_release/release-note/out.html | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/make_release/release-note/notes.nu b/make_release/release-note/notes.nu index be3ebc8e..18fb05b3 100755 --- a/make_release/release-note/notes.nu +++ b/make_release/release-note/notes.nu @@ -100,7 +100,7 @@ def generate-section []: record -> string { if ($multiline | is-not-empty) { $body ++= [$"### ($section.h3)"] } - $body ++= $bullet | each { "* " ++ $in.notes } + $body ++= $bullet | each {|pr| "* " ++ $pr.notes ++ $" \(($pr | pr-link)\)" } $body | str join (char nl) } @@ -240,10 +240,16 @@ def format-pr []: record -> string { $pr.url | ansi link -t $text } +# Create a markdown link def md-link [text: string, link: string] { $"[($text)]\(($link)\)" } +# Get a link to a PR +def pr-link []: record -> string { + md-link $"#($in.number)" $in.url +} + # Format the output of `list-prs` as a markdown table export def pr-table [] { sort-by author number diff --git a/make_release/release-note/out.html b/make_release/release-note/out.html index a8b8a3b0..bbafac52 100644 --- a/make_release/release-note/out.html +++ b/make_release/release-note/out.html @@ -30,5 +30,6 @@

Additional changes

  • Previously, a record key = would not be quoted in to nuon, producing invalid nuon output. It wasn’t quoted in other string values either, but it didn’t cause problems there. Now any -string containing = gets quoted.
  • +string containing = gets quoted. (#16440) From 9db5c52473773cc203bfef0cfaac44300bc8da11 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Sun, 17 Aug 2025 21:33:19 -0400 Subject: [PATCH 08/29] Generate release notes from template --- make_release/release-note/notes.nu | 39 ++++++++++++++++++++------- make_release/release-note/template.md | 26 ++++++------------ 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/make_release/release-note/notes.nu b/make_release/release-note/notes.nu index 18fb05b3..a5945da6 100755 --- a/make_release/release-note/notes.nu +++ b/make_release/release-note/notes.nu @@ -56,32 +56,48 @@ def query-prs [ | from json } -# Get the release notes for all merged PRs on a repo. +# Generate the release notes for the specified version. export def pr-notes [ version: string # the version to generate release notes for -]: nothing -> table { +]: nothing -> string { query-prs --milestone=$version | sort-by mergedAt | each { get-release-notes } | collect | tee { display-notices } | where {|pr| "error" not-in ($pr.notices?.type? | default []) } - | generate-notes + | generate-notes $version +} + +# Generate the release notes from the list of PRs. +def generate-notes [version: string]: table -> string { + let prs = $in + + const template_path = path self | path dirname | path join "template.md" + let template = open $template_path + let arguments = { + version: $version, + changes: ($prs | generate-changes-section), + hall_of_fame: ($prs | generate-hall-of-fame) + } + + $arguments | format pattern $template } -# Generate the release notes. -def generate-notes [] { + +# Generate the "Changes" section of the release notes. +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 } - # TODO: handle hall of fame properly + # Hall of Fame is handled separately | where section != "notes:mention" | each { generate-section } | str join (char nl) } -# Generate a section of the release notes. +# Generate a subsection of the "Changes" section of the release notes. def generate-section []: record -> string { let prs = $in.prs let section = $prs.0.section @@ -105,9 +121,14 @@ def generate-section []: record -> string { $body | str join (char nl) } +# Generate the "Hall of Fame" section of the release notes. +def generate-hall-of-fame []: table -> string { + "TODO" +} + # Attempt to extract the "Release notes summary" section from a PR. # -# If a valid release notes section is found, a "notes" column is added. +# 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. def get-release-notes []: record -> record { mut pr = $in @@ -230,7 +251,7 @@ def display-notices []: table -> nothing { $e.items | each { format-pr | print $"- ($in)" } print "" } - | ignore + print -n (ansi reset) } # Format a PR nicely, including a link diff --git a/make_release/release-note/template.md b/make_release/release-note/template.md index a56b762a..8a9be81e 100644 --- a/make_release/release-note/template.md +++ b/make_release/release-note/template.md @@ -1,24 +1,24 @@ --- -title: Nushell {{VERSION}} +title: Nushell {version} author: The Nu Authors author_site: https://www.nushell.sh/blog author_image: https://www.nushell.sh/blog/images/nu_logo.png -excerpt: Today, we're releasing version {{VERSION}} of Nu. This release adds... +excerpt: Today, we're releasing version {version} of Nu. This release adds... --- -# Nushell {{VERSION}} +# Nushell {version} -Today, we're releasing version {{VERSION}} of Nu. This release adds... +Today, we're releasing version {version} of Nu. This release adds... # Where to get it -Nu {{VERSION}} is available as [pre-built binaries](https://github.com/nushell/nushell/releases/tag/{{VERSION}}) or from [crates.io](https://crates.io/crates/nu). If you have Rust installed you can install it using `cargo install nu`. +Nu {version} is available as [pre-built binaries](https://github.com/nushell/nushell/releases/tag/{version}) or from [crates.io](https://crates.io/crates/nu). If you have Rust installed you can install it using `cargo install nu`. As part of this release, we also publish a set of optional [plugins](https://www.nushell.sh/book/plugins.html) you can install and use with Nushell. @@ -44,15 +44,7 @@ As part of this release, we also publish a set of optional [plugins](https://www # Changes -## Additions - -## Breaking changes - -## Deprecations - -## Removals - -## Bug fixes and other changes +{changes} # Notes for plugin developers @@ -60,15 +52,13 @@ As part of this release, we also publish a set of optional [plugins](https://www Thanks to all the contributors below for helping us solve issues, improve documentation, refactor code, and more! :pray: -| author | title | link | -| ------------------------------------ | ----- | ------------------------------------------------------- | -| [@author](https://github.com/author) | ... | [#12345](https://github.com/nushell/nushell/pull/12345) | +{hall_of_fame} # Full changelog +{changelog} From af7ff4d39493f8356c5cd6303f2859a13e77244e Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Sun, 17 Aug 2025 22:08:13 -0400 Subject: [PATCH 11/29] Filter out bot PRs --- make_release/release-note/notes.nu | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/make_release/release-note/notes.nu b/make_release/release-note/notes.nu index fef69725..4cf2bce9 100755 --- a/make_release/release-note/notes.nu +++ b/make_release/release-note/notes.nu @@ -88,7 +88,8 @@ def generate-notes [version: string]: table -> string { # Generate the "Changes" section of the release notes. def generate-changes-section []: table -> string { - group-by --to-table section.label + where not author.is_bot + | 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 } @@ -124,7 +125,8 @@ def generate-section []: record -> string { # Generate the "Hall of Fame" section of the release notes. def generate-hall-of-fame []: table -> string { - where section.label == "notes:mention" + where not author.is_bot + | 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)' } From 344b1eeb26ca0275bd2ed7f099d63cdd3dba2611 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Sun, 17 Aug 2025 22:29:22 -0400 Subject: [PATCH 12/29] Move is_bot check --- make_release/release-note/notes.nu | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/make_release/release-note/notes.nu b/make_release/release-note/notes.nu index 4cf2bce9..17ba7658 100755 --- a/make_release/release-note/notes.nu +++ b/make_release/release-note/notes.nu @@ -61,9 +61,9 @@ export def pr-notes [ version: string # the version to generate release notes for ]: nothing -> string { query-prs --milestone=$version + | where not author.is_bot | sort-by mergedAt | each { get-release-notes } - | collect | tee { display-notices } | where {|pr| "error" not-in ($pr.notices?.type? | default []) } | generate-notes $version @@ -88,8 +88,7 @@ def generate-notes [version: string]: table -> string { # Generate the "Changes" section of the release notes. def generate-changes-section []: table -> string { - where not author.is_bot - | group-by --to-table section.label + 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 } @@ -125,8 +124,7 @@ def generate-section []: record -> string { # Generate the "Hall of Fame" section of the release notes. def generate-hall-of-fame []: table -> string { - where not author.is_bot - | where section.label == "notes:mention" + 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)' } From b04fc45fd900337c254e30508cdb1247b2692bd5 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Sun, 17 Aug 2025 22:31:47 -0400 Subject: [PATCH 13/29] Remove old files --- make_release/release-note/out.html | 35 ------ make_release/release-note/prs.nuon | 185 ----------------------------- 2 files changed, 220 deletions(-) delete mode 100644 make_release/release-note/out.html delete mode 100644 make_release/release-note/prs.nuon diff --git a/make_release/release-note/out.html b/make_release/release-note/out.html deleted file mode 100644 index bbafac52..00000000 --- a/make_release/release-note/out.html +++ /dev/null @@ -1,35 +0,0 @@ -

    Other changes

    -

    Improved error -messages for misspelled flags

    -

    Previously, the help text for a missing flag would list all of them, -which could get verbose on a single line:

    -
    ~> ls --full-path
    -Error: nu::parser::unknown_flag
    -
    -  × The `ls` command doesn't have flag `full-path`.
    -   ╭─[entry #8:1:4]
    - 1 │ ls --full-path
    -   ·    ─────┬─────
    -   ·         ╰── unknown flag
    -   ╰────
    -  help: Available flags: --help(-h), --all(-a), --long(-l), --short-names(-s), --full-paths(-f), --du(-d), --directory(-D), --mime-type(-m), --threads(-t). Use
    -        `--help` for more information.
    -

    The new error message only suggests the closest flag:

    -
    > ls --full-path
    -Error: nu::parser::unknown_flag
    -
    -  × The `ls` command doesn't have flag `full-path`.
    -   ╭─[entry #23:1:4]
    - 1 │ ls --full-path
    -   ·    ─────┬─────
    -   ·         ╰── unknown flag
    -   ╰────
    -  help: Did you mean: `--full-paths`?
    -

    Additional changes

    -
      -
    • Previously, a record key = would not be quoted in -to nuon, producing invalid nuon output. It wasn’t quoted in -other string values either, but it didn’t cause problems there. Now any -string containing = gets quoted. (#16440)
    • -
    diff --git a/make_release/release-note/prs.nuon b/make_release/release-note/prs.nuon deleted file mode 100644 index 30a5015b..00000000 --- a/make_release/release-note/prs.nuon +++ /dev/null @@ -1,185 +0,0 @@ -[{author: {id: U_kgDOBdQsGQ, is_bot: false, login: kaathewisegit, name: "Andrej Kolčin"}, body: "# Description - -Currently, when Nushell encounters an unknown flag, it prints all options in the help string. This is pretty verbose and uses the `formatted_flags` signature method, which isn't used anywhere else. This commit refactors the parser to use `did_you_mean` instead, which only suggest one closest option or sends the user to `help` if nothing close is found. - - -## Release notes summary - What our users need to know - -### Improved error messages for misspelled flags - -Previously, the help text for a missing flag would list all of them, which could get verbose on a single line: - -```nushell -~> ls --full-path -Error: nu::parser::unknown_flag - - × The `ls` command doesn't have flag `full-path`. - ╭─[entry #8:1:4] - 1 │ ls --full-path - · ─────┬───── - · ╰── unknown flag - ╰──── - help: Available flags: --help(-h), --all(-a), --long(-l), --short-names(-s), --full-paths(-f), --du(-d), --directory(-D), --mime-type(-m), --threads(-t). Use - `--help` for more information. -``` - -The new error message only suggests the closest flag: - -```nushell -> ls --full-path -Error: nu::parser::unknown_flag - - × The `ls` command doesn't have flag `full-path`. - ╭─[entry #23:1:4] - 1 │ ls --full-path - · ─────┬───── - · ╰── unknown flag - ╰──── - help: Did you mean: `--full-paths`? -``` - - ---- - -Closes #16418 -", labels: [[id, name, description, color]; ["MDU6TGFiZWwxNTIyNDA0MTI3", parser, "Issues related to parsing", "f4825d"], ["LA_kwDOCxaBas8AAAACH_kfUw", "notes:ready", "The \"Release notes summary\" section of this PR is ready to be included in our release notes.", "0e8a16"], ["LA_kwDOCxaBas8AAAACH_mOuw", "notes:other", "", "1D76DB"]], mergedAt: "2025-08-13T11:25:18Z", number: 16427, title: "Improve wrong flag help", url: "https://github.com/nushell/nushell/pull/16427", section: {label: "notes:other", "h2": "Other changes", "h3": "Additional changes"}, notes: "### Improved error messages for misspelled flags - -Previously, the help text for a missing flag would list all of them, which could get verbose on a single line: - -```nushell -~> ls --full-path -Error: nu::parser::unknown_flag - - × The `ls` command doesn't have flag `full-path`. - ╭─[entry #8:1:4] - 1 │ ls --full-path - · ─────┬───── - · ╰── unknown flag - ╰──── - help: Available flags: --help(-h), --all(-a), --long(-l), --short-names(-s), --full-paths(-f), --du(-d), --directory(-D), --mime-type(-m), --threads(-t). Use - `--help` for more information. -``` - -The new error message only suggests the closest flag: - -```nushell -> ls --full-path -Error: nu::parser::unknown_flag - - × The `ls` command doesn't have flag `full-path`. - ╭─[entry #23:1:4] - 1 │ ls --full-path - · ─────┬───── - · ╰── unknown flag - ╰──── - help: Did you mean: `--full-paths`? -```"}, {author: {id: "MDQ6VXNlcjg3NTE2MTM=", is_bot: false, login: "132ikl", name: rose}, body: " - -We discussed in a recent meeting that we want to better automate our release notes, and that we could do this by keeping track of the release note entries within the PRs themselves. I've taken this opportunity to try to simplify our PR template (with much input from @sholderbach) and also carve out a dedicated space for release notes. - -Most of the text in the old PR template has been moved to a new section in CONTRIBUTING.md titled \"Tips for submitting PRs\". This also gives us some room to expand on some things that we previously kept brief due to space constraints. The new PR template is brief, and contains a minimal amount of comments. There are three main sections: -1. Free-form space at the top for motivation and technical details (roughly equivalent to \"Description\") -2. Release notes summary (roughly equivalent to \"User-Facing Changes\", but more strict/specific) -3. Tasks after submitting (roughly equivalent to \"After Submitting\") - -The purpose of each section is briefly spelled out in the PR template, and it directs the author to CONTRIBUTING.md for more details. Once this is in place, we can start to pull out the release notes summary section with a script and build our release notes from that. - -I've used the new template for this PR description to help folks get a sense of what it will look like. - -## Release notes summary - What our users need to know - -N/A - -## Tasks after submitting - -- [ ] Update release note scripts to pull from the \"Release notes summary\" section -- [ ] Rework `pr:` labels", labels: [], mergedAt: "2025-08-13T20:10:47Z", number: 16412, title: "Rework PR template", url: "https://github.com/nushell/nushell/pull/16412", section: {label: "notes:mention", "h2": null, "h3": null}, notes: "Rework PR template"}, {author: {id: "MDQ6VXNlcjM0Mzg0MA==", is_bot: false, login: fdncred, name: "Darren Schroeder"}, body: "The PR upgrades nushell to rust version 1.87.0. - -## Dev overview from clippy -- I added `result_large_err` to clippy in the root Cargo.toml to avoid the warnings (and a few places in plugins). At some point a more proper fix, perhaps boxing these, will need to be performed. This PR is to just get us over the hump. -- I boxed a couple areas in some commands -- I changed `rdr.bytes()` to `BufReader::new(rdr).bytes()` in nu-json - -## Release notes summary - What our users need to know -Users can use rust version 1.87.0 to compile nushell now - -## Tasks after submitting -N/A", labels: [[id, name, description, color]; ["LA_kwDOCxaBas8AAAABAIPHWA", rust, "Pull requests that update Rust code", "000000"], ["LA_kwDOCxaBas8AAAACH_kfUw", "notes:ready", "The \"Release notes summary\" section of this PR is ready to be included in our release notes.", "0e8a16"]], mergedAt: "2025-08-14T16:27:34Z", number: 16437, title: "update to rust version 1.87.0", url: "https://github.com/nushell/nushell/pull/16437", notices: [[type, message]; [info, "no explicit release notes category selected (defaults to Hall of Fame)"]], section: {label: "notes:mention", "h2": null, "h3": null}, notes: "Users can use rust version 1.87.0 to compile nushell now"}, {author: {id: "MDQ6VXNlcjM1OTA4Mjk=", is_bot: false, login: cptpiepmatz, name: Piepmatz}, body: " -In this PR I added two string types to `nu-utils`, the `UniqueString` and `SharedString`. Both are owned string types but come with a lot of optimizations that should be very useful for us. - -Both types represent immutable strings which is what we use a lot. They are optimized for static strings to be dereferenced instead of copied, they both store small strings on the stack instead of the heap and both are compact due to the fact that we don't need mutability so the capacity can go. - -I added two dependencies that implement these two strings `byteyarn` and `lean_string`. The `byteyarn` string provides a `Box`-like string while the `lean_string` provides an `Arc`-like string. - -I added this abstraction to allows us later to use other implemenation if we choose to do so. - -With these strings we should be able to heavily push down our memory footprint and struct sizes. Especially the `ShellError` should benefit from it. - -## Release notes summary - What our users need to know - -N/A - -## Tasks after submitting - -- [ ] Use the `UniqueString` and `SharedString` where applicable -", labels: [], mergedAt: "2025-08-16T16:01:39Z", number: 16446, title: "Add well-optimized string types", url: "https://github.com/nushell/nushell/pull/16446", section: {label: "notes:mention", "h2": null, "h3": null}, notes: "Add well-optimized string types"}, {author: {id: "MDQ6VXNlcjIyMjU2MTU0", is_bot: false, login: WindSoilder, name: Wind}, body: "I noticed some clippy errors while running clippy under 1.88. -``` -error: variables can be used directly in the `format!` string - --> src/config_files.rs:204:25 - | -204 | warn!(\"AutoLoading: {:?}\", path); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args - = note: `-D clippy::uninlined-format-args` implied by `-D warnings` - = help: to override `-D warnings` add `#[allow(clippy::uninlined_format_args)]` -``` -And this pr is going to fix this. - -## Release notes summary - What our users need to know -NaN - -## Tasks after submitting -NaN -", labels: [[id, name, description, color]; ["MDU6TGFiZWwxNTIyNDA0MTI3", parser, "Issues related to parsing", "f4825d"], ["LA_kwDOCxaBas8AAAABAAvhuQ", "pr:plugins", "This PR is related to plugins", "d4c5f9"], ["LA_kwDOCxaBas8AAAACH_kfUw", "notes:ready", "The \"Release notes summary\" section of this PR is ready to be included in our release notes.", "0e8a16"]], mergedAt: "2025-08-17T10:41:36Z", number: 16452, title: "fix uninlined_format_args clippy warnings", url: "https://github.com/nushell/nushell/pull/16452", section: {label: "notes:mention", "h2": null, "h3": null}, notes: "Fix uninlined_format_args clippy warnings"}, {author: {id: "MDQ6VXNlcjg3NTE2MTM=", is_bot: false, login: "132ikl", name: rose}, body: " -## Motivation and technical details - -This PR adds a \"Motivation and technical details\" section to the PR template. It seems like not having an explicit section for this has tripped some people up, so this PR adds an explicit heading for this to go under. - -## Release notes summary - What our users need to know - -N/A", labels: [], mergedAt: "2025-08-17T20:29:21Z", number: 16458, title: "Add \"Motivation and technical details", url: "https://github.com/nushell/nushell/pull/16458", section: {label: "notes:mention", "h2": null, "h3": null}, notes: "Add \"Motivation and technical details"}, {author: {id: "MDQ6VXNlcjU3NDAz", is_bot: false, login: weirdan, name: "Bruce Weirdan"}, body: "Fixes nushell/nushell#16438 - -## Release notes summary - What our users need to know - -Previously, a record key `=` would not be quoted in `to nuon`, producing invalid nuon output. It wasn't quoted in other string values either, but it didn't cause problems there. Now any string containing `=` gets quoted.", labels: [[id, name, description, color]; ["LA_kwDOCxaBas8AAAABFrA60w", nuon-format, "I/O and spec of the nuon data format", "4ABBDE"], ["LA_kwDOCxaBas8AAAABU5xICA", "pr:release-note-mention", "Addition/Improvement to be mentioned in the release notes", "19CE1C"], ["LA_kwDOCxaBas8AAAACH_kfUw", "notes:ready", "The \"Release notes summary\" section of this PR is ready to be included in our release notes.", "0e8a16"], ["LA_kwDOCxaBas8AAAACH_mOuw", "notes:other", "", "1D76DB"]], mergedAt: "2025-08-17T20:33:08Z", number: 16440, title: "Quote strings containing `=`", url: "https://github.com/nushell/nushell/pull/16440", section: {label: "notes:other", "h2": "Other changes", "h3": "Additional changes"}, notes: "Previously, a record key `=` would not be quoted in `to nuon`, producing invalid nuon output. It wasn't quoted in other string values either, but it didn't cause problems there. Now any string containing `=` gets quoted."}, {author: {id: "MDQ6VXNlcjg3NTE2MTM=", is_bot: false, login: "132ikl", name: rose}, body: "This PR removes the \"Question\" issue template, and adds a section to the issue creation menu labeled \"Question\" which redirects to a new Q&A discussion post. - -I have this set up on my fork if you want to see what it looks like: https://github.com/132ikl/nushell/issues/new/choose - -## Release notes summary - What our users need to know -N/A - -## Tasks after submitting -- [ ] Convert all issues currently tagged with the \"question\" label into Discussions", labels: [], mergedAt: "2025-08-17T20:33:30Z", number: 16443, title: "Redirect \"Questions\" issue option to Discussions", url: "https://github.com/nushell/nushell/pull/16443", section: {label: "notes:mention", "h2": null, "h3": null}, notes: "Redirect \"Questions\" issue option to Discussions"}] \ No newline at end of file From 4a9b7a3c0cdc9186aac678002f2a2a663e06d449 Mon Sep 17 00:00:00 2001 From: rose <132@ikl.sh> Date: Mon, 18 Aug 2025 11:37:21 -0400 Subject: [PATCH 14/29] `path self` resolves relative paths Co-authored-by: Bahex <17417311+Bahex@users.noreply.github.com> --- make_release/release-note/notes.nu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make_release/release-note/notes.nu b/make_release/release-note/notes.nu index 17ba7658..b1b261b7 100755 --- a/make_release/release-note/notes.nu +++ b/make_release/release-note/notes.nu @@ -73,7 +73,7 @@ export def pr-notes [ def generate-notes [version: string]: table -> string { let prs = $in - const template_path = path self | path dirname | path join "template.md" + const template_path = path self "template.md" let template = open $template_path let arguments = { version: $version, From 1aee18fd077df6b72a6e44589079b80c85e18a1b Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Tue, 19 Aug 2025 12:43:26 -0400 Subject: [PATCH 15/29] Allow PRs with no release notes summary but notes:ready label --- make_release/release-note/notes.nu | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/make_release/release-note/notes.nu b/make_release/release-note/notes.nu index b1b261b7..9695dbbc 100755 --- a/make_release/release-note/notes.nu +++ b/make_release/release-note/notes.nu @@ -148,16 +148,21 @@ def generate-full-changelog [version: string]: nothing -> string { def get-release-notes []: record -> record { mut pr = $in - if "## Release notes summary" not-in $pr.body { - return ($pr | add-notice error "no release notes section") - } - - mut notes = $pr.body | extract-notes - 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) { From 6fcfbd1a0c2bbe5ddf08ceaaa2e28388f90aeb89 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Wed, 20 Aug 2025 19:31:26 -0400 Subject: [PATCH 16/29] Convert to module and add Bahex's completions Co-authored-by: Bahex <17417311+Bahex@users.noreply.github.com> --- make_release/notes/completions.nu | 26 +++++++++++++++++++ .../{release-note => notes}/create-pr.nu | 2 +- .../gh-release-excerpt.nu | 0 .../{release-note/notes.nu => notes/mod.nu} | 13 +++++++--- .../{release-note => notes}/template.md | 0 5 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 make_release/notes/completions.nu rename make_release/{release-note => notes}/create-pr.nu (99%) rename make_release/{release-note => notes}/gh-release-excerpt.nu (100%) rename make_release/{release-note/notes.nu => notes/mod.nu} (95%) rename make_release/{release-note => notes}/template.md (100%) diff --git a/make_release/notes/completions.nu b/make_release/notes/completions.nu new file mode 100644 index 00000000..cfbee9a4 --- /dev/null +++ b/make_release/notes/completions.nu @@ -0,0 +1,26 @@ +export const example_version = $"0.((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 + } + $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 } diff --git a/make_release/release-note/create-pr.nu b/make_release/notes/create-pr.nu similarity index 99% rename from make_release/release-note/create-pr.nu rename to make_release/notes/create-pr.nu index 6d258f56..3cf0fe96 100755 --- a/make_release/release-note/create-pr.nu +++ b/make_release/notes/create-pr.nu @@ -35,7 +35,7 @@ def clean [repo: path] { # # 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 [ +export 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` ] { diff --git a/make_release/release-note/gh-release-excerpt.nu b/make_release/notes/gh-release-excerpt.nu similarity index 100% rename from make_release/release-note/gh-release-excerpt.nu rename to make_release/notes/gh-release-excerpt.nu diff --git a/make_release/release-note/notes.nu b/make_release/notes/mod.nu similarity index 95% rename from make_release/release-note/notes.nu rename to make_release/notes/mod.nu index 9695dbbc..9e0820c8 100755 --- a/make_release/release-note/notes.nu +++ b/make_release/notes/mod.nu @@ -1,6 +1,10 @@ use std/assert use std-rfc/iter only +use completions.nu * + +export use create-pr.nu + const SECTIONS = [ [label, h2, h3]; ["notes:breaking-changes", "Breaking changes", "Other breaking changes"] @@ -13,10 +17,11 @@ const SECTIONS = [ ] # List all merged PRs since the last release +@example $"List all merged for ($example_version)" $"list-prs --milestone ($example_version)" export def list-prs [ repo: string = 'nushell/nushell' # the name of the repo, e.g. 'nushell/nushell' - --since: datetime # list PRs on or after this date (defaults to 4 weeks ago if `--milestone` is not provided) - --milestone: string # only list PRs in a certain milestone + --since: datetime@"nu-complete date current" # list PRs on or after this date (defaults to 4 weeks ago if `--milestone` is not provided) + --milestone: string@"nu-complete version" # only list PRs in a certain milestone --label: string # the PR label to filter by, e.g. 'good-first-issue' ]: nothing -> table { query-prs $repo --since=$since --milestone=$milestone --label=$label @@ -28,8 +33,8 @@ export def list-prs [ # Construct a GitHub query for merged PRs on a repo. def query-prs [ repo: string = 'nushell/nushell' # the name of the repo, e.g. 'nushell/nushell' - --since: datetime # list PRs on or after this date (defaults to 4 weeks ago if `--milestone` is not provided) - --milestone: string # only list PRs in a certain milestone + --since: datetime@"nu-complete date current" # list PRs on or after this date (defaults to 4 weeks ago if `--milestone` is not provided) + --milestone: string@"nu-complete version" # only list PRs in a certain milestone --label: string # the PR label to filter by, e.g. 'good-first-issue' ]: nothing -> table { mut query_parts = [] diff --git a/make_release/release-note/template.md b/make_release/notes/template.md similarity index 100% rename from make_release/release-note/template.md rename to make_release/notes/template.md From bfb48483778aaacf5b5f0270417b3a7c89f5783b Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Wed, 20 Aug 2025 19:36:14 -0400 Subject: [PATCH 17/29] Modularize --- make_release/notes/generate.nu | 175 +++++++++++++++++++++++ make_release/notes/mod.nu | 244 +-------------------------------- make_release/notes/notice.nu | 29 ++++ make_release/notes/util.nu | 35 +++++ 4 files changed, 243 insertions(+), 240 deletions(-) create mode 100644 make_release/notes/generate.nu create mode 100644 make_release/notes/notice.nu create mode 100644 make_release/notes/util.nu diff --git a/make_release/notes/generate.nu b/make_release/notes/generate.nu new file mode 100644 index 00000000..0aa1f6e2 --- /dev/null +++ b/make_release/notes/generate.nu @@ -0,0 +1,175 @@ +# 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 "---" } + | str join (char nl) + # remove HTML comments + | str replace -amr '' '' + | 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 = { + version: $version, + 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 -> 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 +} diff --git a/make_release/notes/mod.nu b/make_release/notes/mod.nu index 9e0820c8..1657d58f 100755 --- a/make_release/notes/mod.nu +++ b/make_release/notes/mod.nu @@ -1,21 +1,13 @@ use std/assert use std-rfc/iter only +use util.nu * use completions.nu * +use notice.nu * +use generate.nu * export use create-pr.nu -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] -] - # List all merged PRs since the last release @example $"List all merged for ($example_version)" $"list-prs --milestone ($example_version)" export def list-prs [ @@ -63,7 +55,7 @@ def query-prs [ # Generate the release notes for the specified version. export def pr-notes [ - version: string # the version to generate release notes for + version: string@"nu-complete version" # the version to generate release notes for ]: nothing -> string { query-prs --milestone=$version | where not author.is_bot @@ -74,234 +66,6 @@ export def pr-notes [ | generate-notes $version } -# Generate the release notes from the list of PRs. -def generate-notes [version: string]: table -> string { - let prs = $in - - const template_path = path self "template.md" - let template = open $template_path - let arguments = { - version: $version, - 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. -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. -def generate-section []: record -> 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. -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. -def generate-full-changelog [version: string]: nothing -> string { - list-prs --milestone=$version - | pr-table -} - -# 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. -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 -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 "---" } - | str join (char nl) - # remove HTML comments - | str replace -amr '' '' - | str trim -} - -# Clean up a PR title -def clean-title []: string -> string { - # remove any prefixes and capitalize - str replace -r '^[^\s]+: ' "" - | str trim - | str capitalize -} - -# Check if the release notes section was left empty -def is-empty-keyword []: string -> bool { - str downcase | $in in ["", "n/a", "nothing", "none", "nan"] -} - -# Add an entry to the "notices" field of a PR -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 -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 - print $"($color)PRs with ($e.message):" - $e.items | each { format-pr | print $"- ($in)" } - print "" - } - print -n (ansi reset) -} - -# Format a PR nicely, including a link -def format-pr []: record -> string { - let pr = $in - let text = $"#($pr.number): ($pr.title)" - $pr.url | ansi link -t $text -} - -# Escape > and < -def escape-tag [] { - str replace -a ">" ">" - | str replace -a "<" "<" -} - -# Create a markdown link -def md-link [text: string, link: string] { - $"[($text)]\(($link)\)" -} - -# Get a link to a PR -def pr-link []: record -> string { - md-link $"#($in.number)" $in.url -} - # Format the output of `list-prs` as a markdown table export def pr-table [] { sort-by author number diff --git a/make_release/notes/notice.nu b/make_release/notes/notice.nu new file mode 100644 index 00000000..059b5216 --- /dev/null +++ b/make_release/notes/notice.nu @@ -0,0 +1,29 @@ +# 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 + print $"($color)PRs with ($e.message):" + $e.items | each { format-pr | print $"- ($in)" } + print "" + } + print -n (ansi reset) +} diff --git a/make_release/notes/util.nu b/make_release/notes/util.nu new file mode 100644 index 00000000..4116aefb --- /dev/null +++ b/make_release/notes/util.nu @@ -0,0 +1,35 @@ +# Clean up a PR title +export def clean-title []: string -> string { + # remove any prefixes and capitalize + str replace -r '^[^\s]+: ' "" + | str trim + | str capitalize +} + +# Check if the release notes section was left empty +export def is-empty-keyword []: string -> bool { + str downcase | $in in ["", "n/a", "nothing", "none", "nan"] +} + +# Format a PR nicely, including a link +export def format-pr []: record -> string { + let pr = $in + let text = $"#($pr.number): ($pr.title)" + $pr.url | ansi link -t $text +} + +# Escape > and < +export def escape-tag [] { + str replace -a ">" ">" + | str replace -a "<" "<" +} + +# Create a markdown link +export def md-link [text: string, link: string] { + $"[($text)]\(($link)\)" +} + +# Get a link to a PR +export def pr-link []: record -> string { + md-link $"#($in.number)" $in.url +} From ecf88b2377a71ce25b075019743de570a7be3301 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Mon, 25 Aug 2025 10:42:21 -0400 Subject: [PATCH 18/29] Fix version completion --- make_release/notes/completions.nu | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/make_release/notes/completions.nu b/make_release/notes/completions.nu index cfbee9a4..f9e16b8d 100644 --- a/make_release/notes/completions.nu +++ b/make_release/notes/completions.nu @@ -1,4 +1,4 @@ -export const example_version = $"0.((version).minor + 1).0" +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 { @@ -13,8 +13,9 @@ export def last-release-date []: nothing -> datetime { | from json | $in.0.createdAt | into datetime + | "v" ++ $in } - $env.cached-var.relase-date + "v" ++ $env.cached-var.relase-date } export def "nu-complete version" [] { [$example_version] } From a50e2e99aab83248116ba019a80cb4093e61ac74 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Mon, 25 Aug 2025 10:43:35 -0400 Subject: [PATCH 19/29] Change executables into module commands --- make_release/notes/create-pr.nu | 2 -- make_release/notes/gh-release-excerpt.nu | 5 +---- make_release/notes/mod.nu | 1 + 3 files changed, 2 insertions(+), 6 deletions(-) mode change 100755 => 100644 make_release/notes/create-pr.nu mode change 100755 => 100644 make_release/notes/gh-release-excerpt.nu mode change 100755 => 100644 make_release/notes/mod.nu diff --git a/make_release/notes/create-pr.nu b/make_release/notes/create-pr.nu old mode 100755 new mode 100644 index 3cf0fe96..10398fa8 --- a/make_release/notes/create-pr.nu +++ b/make_release/notes/create-pr.nu @@ -1,5 +1,3 @@ -#!/usr/bin/env nu - use std log def open-pr [ diff --git a/make_release/notes/gh-release-excerpt.nu b/make_release/notes/gh-release-excerpt.nu old mode 100755 new mode 100644 index 01713b2f..4aaaeb58 --- a/make_release/notes/gh-release-excerpt.nu +++ b/make_release/notes/gh-release-excerpt.nu @@ -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) diff --git a/make_release/notes/mod.nu b/make_release/notes/mod.nu old mode 100755 new mode 100644 index 1657d58f..3a1b94fe --- a/make_release/notes/mod.nu +++ b/make_release/notes/mod.nu @@ -6,6 +6,7 @@ use completions.nu * use notice.nu * use generate.nu * +export use gh-release-excerpt.nu export use create-pr.nu # List all merged PRs since the last release From e3e0cf601051596bef50bfbb2428b243dc45c4e6 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Mon, 25 Aug 2025 10:46:29 -0400 Subject: [PATCH 20/29] Rename pr-notes to release-notes --- make_release/notes/mod.nu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make_release/notes/mod.nu b/make_release/notes/mod.nu index 3a1b94fe..38bbe47e 100644 --- a/make_release/notes/mod.nu +++ b/make_release/notes/mod.nu @@ -55,7 +55,7 @@ def query-prs [ } # Generate the release notes for the specified version. -export def pr-notes [ +export def release-notes [ version: string@"nu-complete version" # the version to generate release notes for ]: nothing -> string { query-prs --milestone=$version From 6f7ab42cbb57eb5b2873a0769675da8bd37721b7 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Mon, 25 Aug 2025 10:56:40 -0400 Subject: [PATCH 21/29] Add check-prs command --- make_release/notes/mod.nu | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/make_release/notes/mod.nu b/make_release/notes/mod.nu index 38bbe47e..27345f8d 100644 --- a/make_release/notes/mod.nu +++ b/make_release/notes/mod.nu @@ -67,6 +67,17 @@ export def release-notes [ | generate-notes $version } +# Check the release note summaries for the specified version. +export def check-prs [ + version: string@"nu-complete version" # the version to generate release notes for +]: nothing -> nothing { + query-prs --milestone=$version + | where not author.is_bot + | sort-by mergedAt + | each { get-release-notes } + | display-notices +} + # Format the output of `list-prs` as a markdown table export def pr-table [] { sort-by author number From b107c7fae33445e240167c4b57429a882f35ca53 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Mon, 25 Aug 2025 10:58:32 -0400 Subject: [PATCH 22/29] Fix summary parsing --- make_release/notes/generate.nu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make_release/notes/generate.nu b/make_release/notes/generate.nu index 0aa1f6e2..e0f6b988 100644 --- a/make_release/notes/generate.nu +++ b/make_release/notes/generate.nu @@ -96,7 +96,7 @@ export def extract-notes []: string -> string { | if ($in | is-empty) { assert false } else {} | skip 1 # remove header # extract until next heading - | take until { $in starts-with "## " or $in starts-with "---" } + | take until { $in starts-with "#" or $in starts-with "---" } | str join (char nl) # remove HTML comments | str replace -amr '' '' From ed6457e3ada71a7d1a35196e0219041dac06e97d Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Mon, 25 Aug 2025 11:02:15 -0400 Subject: [PATCH 23/29] Fix summary parsing again --- make_release/notes/generate.nu | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/make_release/notes/generate.nu b/make_release/notes/generate.nu index e0f6b988..bff4a276 100644 --- a/make_release/notes/generate.nu +++ b/make_release/notes/generate.nu @@ -96,7 +96,9 @@ export def extract-notes []: string -> string { | if ($in | is-empty) { assert false } else {} | skip 1 # remove header # extract until next heading - | take until { $in starts-with "#" or $in starts-with "---" } + | take until { + $in starts-with "# " or $in starts-with "## " or $in starts-with "---" + } | str join (char nl) # remove HTML comments | str replace -amr '' '' From 8ad560e31fbe41c340abedb59da2a0ae9798f55f Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Mon, 25 Aug 2025 11:11:52 -0400 Subject: [PATCH 24/29] Integrate create-pr with release notes generator --- make_release/notes/create-pr.nu | 65 ++++++------ make_release/notes/mod.nu | 179 +------------------------------- make_release/notes/tools.nu | 178 +++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+), 209 deletions(-) create mode 100644 make_release/notes/tools.nu diff --git a/make_release/notes/create-pr.nu b/make_release/notes/create-pr.nu index 10398fa8..4b7993cb 100644 --- a/make_release/notes/create-pr.nu +++ b/make_release/notes/create-pr.nu @@ -1,5 +1,8 @@ use std log +use completions.nu * +use tools.nu release-notes + def open-pr [ repo: path remote: string @@ -29,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 +@example "Create a PR for the next release" $"create-pr ($example_version) \(($current_build_date) + 6wk\)" export 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` + 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)" @@ -47,22 +47,19 @@ export 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? ") { @@ -74,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 + [ + "" + "" + $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 git@github.com: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 git@github.com: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" + ) } } diff --git a/make_release/notes/mod.nu b/make_release/notes/mod.nu index 27345f8d..8ddf38ac 100644 --- a/make_release/notes/mod.nu +++ b/make_release/notes/mod.nu @@ -1,180 +1,3 @@ -use std/assert -use std-rfc/iter only - -use util.nu * -use completions.nu * -use notice.nu * -use generate.nu * - +export use tools.nu * export use gh-release-excerpt.nu export use create-pr.nu - -# List all merged PRs since the last release -@example $"List all merged for ($example_version)" $"list-prs --milestone ($example_version)" -export def list-prs [ - repo: string = 'nushell/nushell' # the name of the repo, e.g. 'nushell/nushell' - --since: datetime@"nu-complete date current" # list PRs on or after this date (defaults to 4 weeks ago if `--milestone` is not provided) - --milestone: string@"nu-complete version" # only list PRs in a certain milestone - --label: string # the PR label to filter by, e.g. 'good-first-issue' -]: nothing -> table { - query-prs $repo --since=$since --milestone=$milestone --label=$label - | select author title number mergedAt url - | sort-by mergedAt --reverse - | update author { get login } -} - -# Construct a GitHub query for merged PRs on a repo. -def query-prs [ - repo: string = 'nushell/nushell' # the name of the repo, e.g. 'nushell/nushell' - --since: datetime@"nu-complete date current" # list PRs on or after this date (defaults to 4 weeks ago if `--milestone` is not provided) - --milestone: string@"nu-complete version" # only list PRs in a certain milestone - --label: string # the PR label to filter by, e.g. 'good-first-issue' -]: nothing -> table { - mut query_parts = [] - - if $since != null or $milestone == null { - let date = $since | default ((date now) - 4wk) | format date '%Y-%m-%d' - $query_parts ++= [ $'merged:>($date)' ] - } - - if $milestone != null { - $query_parts ++= [ $'milestone:"($milestone)"' ] - } - - if $label != null { - $query_parts ++= [ $'label:($label)' ] - } - - let query = $query_parts | str join ' ' - - (gh --repo $repo pr list --state merged - --limit (inf | into int) - --json author,title,number,mergedAt,url,body,labels - --search $query) - | from json -} - -# Generate the release notes for the specified version. -export def release-notes [ - version: string@"nu-complete version" # the version to generate release notes for -]: nothing -> string { - query-prs --milestone=$version - | where not author.is_bot - | sort-by mergedAt - | each { get-release-notes } - | tee { display-notices } - | where {|pr| "error" not-in ($pr.notices?.type? | default []) } - | generate-notes $version -} - -# Check the release note summaries for the specified version. -export def check-prs [ - version: string@"nu-complete version" # the version to generate release notes for -]: nothing -> nothing { - query-prs --milestone=$version - | where not author.is_bot - | sort-by mergedAt - | each { get-release-notes } - | display-notices -} - -# Format the output of `list-prs` as a markdown table -export def pr-table [] { - sort-by author number - | update author { md-link $'@($in)' $'https://github.com/($in)' } - | insert link { pr-link } - | select author title link - | to md - | escape-tag -} - -const toc = '[[toc](#table-of-contents)]' - -# Generate and write the table of contents to a release notes file -export def write-toc [file: path] { - let known_h1s = [ - "# Highlights and themes of this release", - "# Changes", - "# Notes for plugin developers", - "# Hall of fame", - "# Full changelog", - ] - - let lines = open $file | lines | each { str trim -r } - - let content_start = 2 + ( - $lines - | enumerate - | where item == '# Table of contents' - | first - | get index - ) - - let data = ( - $lines - | slice $content_start.. - | wrap line - | insert level { - get line | split chars | take while { $in == '#' } | length - } - | insert nocomment { - # We assume that comments only have one `#` - if ($in.level != 1) { - return true - } - let line = $in.line - - # Try to use the whitelist first - if ($known_h1s | any {|| $line =~ $in}) { - return true - } - - # We don't know so let's ask - let user = ([Ignore Accept] | - input list $"Is this a code comment or a markdown h1 heading:(char nl)(ansi blue)($line)(ansi reset)(char nl)Choose if we include it in the TOC!") - match $user { - "Accept" => {true} - "Ignore" => {false} - } - - } - ) - - let table_of_contents = ( - $data - | where level in 1..=3 and nocomment == true - | each {|header| - let indent = '- ' | fill -w ($header.level * 2) -a right - - let text = $header.line | str trim -l -c '#' | str trim -l - let text = if $text ends-with $toc { - $text | str substring ..<(-1 * ($toc | str length)) | str trim -r - } else { - $text - } - - let link = ( - $text - | str downcase - | str kebab-case - ) - - $"($indent)[_($text)_]\(#($link)-toc\)" - } - ) - - let content = $data | each { - if $in.level in 1..=3 and not ($in.line ends-with $toc) and $in.nocomment { - $'($in.line) ($toc)' - } else { - $in.line - } - } - - [ - ...($lines | slice ..<$content_start) - ...$table_of_contents - ...$content - ] - | save -r -f $file -} diff --git a/make_release/notes/tools.nu b/make_release/notes/tools.nu new file mode 100644 index 00000000..c39ecf38 --- /dev/null +++ b/make_release/notes/tools.nu @@ -0,0 +1,178 @@ +# Tools for creating the release notes. +use std/assert +use std-rfc/iter only + +use util.nu * +use completions.nu * +use notice.nu * +use generate.nu * + +# List all merged PRs since the last release +@example $"List all merged for ($example_version)" $"list-prs --milestone ($example_version)" +export def list-prs [ + repo: string = 'nushell/nushell' # the name of the repo, e.g. 'nushell/nushell' + --since: datetime@"nu-complete date current" # list PRs on or after this date (defaults to 4 weeks ago if `--milestone` is not provided) + --milestone: string@"nu-complete version" # only list PRs in a certain milestone + --label: string # the PR label to filter by, e.g. 'good-first-issue' +]: nothing -> table { + query-prs $repo --since=$since --milestone=$milestone --label=$label + | select author title number mergedAt url + | sort-by mergedAt --reverse + | update author { get login } +} + +# Construct a GitHub query for merged PRs on a repo. +def query-prs [ + repo: string = 'nushell/nushell' # the name of the repo, e.g. 'nushell/nushell' + --since: datetime@"nu-complete date current" # list PRs on or after this date (defaults to 4 weeks ago if `--milestone` is not provided) + --milestone: string@"nu-complete version" # only list PRs in a certain milestone + --label: string # the PR label to filter by, e.g. 'good-first-issue' +]: nothing -> table { + mut query_parts = [] + + if $since != null or $milestone == null { + let date = $since | default ((date now) - 4wk) | format date '%Y-%m-%d' + $query_parts ++= [ $'merged:>($date)' ] + } + + if $milestone != null { + $query_parts ++= [ $'milestone:"($milestone)"' ] + } + + if $label != null { + $query_parts ++= [ $'label:($label)' ] + } + + let query = $query_parts | str join ' ' + + (gh --repo $repo pr list --state merged + --limit (inf | into int) + --json author,title,number,mergedAt,url,body,labels + --search $query) + | from json +} + +# Generate the release notes for the specified version. +export def release-notes [ + version: string@"nu-complete version" # the version to generate release notes for +]: nothing -> string { + query-prs --milestone=$version + | where not author.is_bot + | sort-by mergedAt + | each { get-release-notes } + | tee { display-notices } + | where {|pr| "error" not-in ($pr.notices?.type? | default []) } + | generate-notes $version +} + +# Check the release note summaries for the specified version. +export def check-prs [ + version: string@"nu-complete version" # the version to generate release notes for +]: nothing -> nothing { + query-prs --milestone=$version + | where not author.is_bot + | sort-by mergedAt + | each { get-release-notes } + | display-notices +} + +# Format the output of `list-prs` as a markdown table +export def pr-table [] { + sort-by author number + | update author { md-link $'@($in)' $'https://github.com/($in)' } + | insert link { pr-link } + | select author title link + | to md + | escape-tag +} + +const toc = '[[toc](#table-of-contents)]' + +# Generate and write the table of contents to a release notes file +export def write-toc [file: path] { + let known_h1s = [ + "# Highlights and themes of this release", + "# Changes", + "# Notes for plugin developers", + "# Hall of fame", + "# Full changelog", + ] + + let lines = open $file | lines | each { str trim -r } + + let content_start = 2 + ( + $lines + | enumerate + | where item == '# Table of contents' + | first + | get index + ) + + let data = ( + $lines + | slice $content_start.. + | wrap line + | insert level { + get line | split chars | take while { $in == '#' } | length + } + | insert nocomment { + # We assume that comments only have one `#` + if ($in.level != 1) { + return true + } + let line = $in.line + + # Try to use the whitelist first + if ($known_h1s | any {|| $line =~ $in}) { + return true + } + + # We don't know so let's ask + let user = ([Ignore Accept] | + input list $"Is this a code comment or a markdown h1 heading:(char nl)(ansi blue)($line)(ansi reset)(char nl)Choose if we include it in the TOC!") + match $user { + "Accept" => {true} + "Ignore" => {false} + } + + } + ) + + let table_of_contents = ( + $data + | where level in 1..=3 and nocomment == true + | each {|header| + let indent = '- ' | fill -w ($header.level * 2) -a right + + let text = $header.line | str trim -l -c '#' | str trim -l + let text = if $text ends-with $toc { + $text | str substring ..<(-1 * ($toc | str length)) | str trim -r + } else { + $text + } + + let link = ( + $text + | str downcase + | str kebab-case + ) + + $"($indent)[_($text)_]\(#($link)-toc\)" + } + ) + + let content = $data | each { + if $in.level in 1..=3 and not ($in.line ends-with $toc) and $in.nocomment { + $'($in.line) ($toc)' + } else { + $in.line + } + } + + [ + ...($lines | slice ..<$content_start) + ...$table_of_contents + ...$content + ] + | save -r -f $file +} From cfc374b9c47c01bd7fa4cae439a3135acfb4dfa7 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Mon, 25 Aug 2025 11:13:03 -0400 Subject: [PATCH 25/29] Fix completions --- make_release/notes/completions.nu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/make_release/notes/completions.nu b/make_release/notes/completions.nu index f9e16b8d..0f86ed35 100644 --- a/make_release/notes/completions.nu +++ b/make_release/notes/completions.nu @@ -13,9 +13,9 @@ export def last-release-date []: nothing -> datetime { | from json | $in.0.createdAt | into datetime - | "v" ++ $in + | $in } - "v" ++ $env.cached-var.relase-date + $env.cached-var.relase-date } export def "nu-complete version" [] { [$example_version] } From e2c3f6bbd1399b5e663d4203eadca7b4a37218a7 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Mon, 25 Aug 2025 11:19:30 -0400 Subject: [PATCH 26/29] Fix how version appears in release notes --- make_release/notes/generate.nu | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/make_release/notes/generate.nu b/make_release/notes/generate.nu index bff4a276..09daa275 100644 --- a/make_release/notes/generate.nu +++ b/make_release/notes/generate.nu @@ -112,7 +112,8 @@ export def generate-notes [version: string]: table -> string { const template_path = path self "template.md" let template = open $template_path let arguments = { - version: $version, + # 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) From f01bc44813adb75a02f3b691b3aad2cd3275417f Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Wed, 27 Aug 2025 14:37:53 -0400 Subject: [PATCH 27/29] Add count to display-notices --- make_release/notes/notice.nu | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/make_release/notes/notice.nu b/make_release/notes/notice.nu index 059b5216..dbf3bbbe 100644 --- a/make_release/notes/notice.nu +++ b/make_release/notes/notice.nu @@ -21,7 +21,8 @@ export def display-notices []: table -> nothing { | sort-by {|i| $types | where type == $i.type | only rank } message | each {|e| let color = $types | where type == $e.type | only color - print $"($color)PRs with ($e.message):" + let number = $e.items | length + print $"($color)($number) PR\(s\) with ($e.message):" $e.items | each { format-pr | print $"- ($in)" } print "" } From 1bd4074cdb11d7183f81b97731e3c21c206dad33 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Fri, 29 Aug 2025 11:33:42 -0400 Subject: [PATCH 28/29] Add --as-table flag to check-prs --- make_release/notes/notice.nu | 25 +++++++++++++++---------- make_release/notes/tools.nu | 3 ++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/make_release/notes/notice.nu b/make_release/notes/notice.nu index dbf3bbbe..ba7bacb1 100644 --- a/make_release/notes/notice.nu +++ b/make_release/notes/notice.nu @@ -1,3 +1,10 @@ +const TYPES = [ + [type, color, rank]; + [info, (ansi default), 0] + [warning, (ansi yellow), 1] + [error, (ansi red), 2] +] + # Add an entry to the "notices" field of a PR export def add-notice [type: string, message: string]: record -> record { upsert notices { @@ -5,22 +12,20 @@ export def add-notice [type: string, message: string]: record -> record { } } -# Print all of the notices associated with a PR -export def display-notices []: table -> nothing { +export def group-notices []: table -> table { 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 + | sort-by {|i| $TYPES | where type == $i.type | only rank } message +} + +# Print all of the notices associated with a PR +export def display-notices []: table -> nothing { + group-notices | each {|e| - let color = $types | where type == $e.type | only color + 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)" } diff --git a/make_release/notes/tools.nu b/make_release/notes/tools.nu index c39ecf38..d825c510 100644 --- a/make_release/notes/tools.nu +++ b/make_release/notes/tools.nu @@ -68,12 +68,13 @@ export def release-notes [ # Check the release note summaries for the specified version. export def check-prs [ version: string@"nu-complete version" # the version to generate release notes for + --as-table (-t) # output PR checks as a table ]: nothing -> nothing { query-prs --milestone=$version | where not author.is_bot | sort-by mergedAt | each { get-release-notes } - | display-notices + | if $as_table { group-notices } else { display-notices } } # Format the output of `list-prs` as a markdown table From 7104a480cfb3d6c31102cdadd04e852d484e760e Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Fri, 29 Aug 2025 11:35:28 -0400 Subject: [PATCH 29/29] Fix check-prs input/output types --- make_release/notes/tools.nu | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/make_release/notes/tools.nu b/make_release/notes/tools.nu index d825c510..65848aa8 100644 --- a/make_release/notes/tools.nu +++ b/make_release/notes/tools.nu @@ -69,7 +69,10 @@ export def release-notes [ export def check-prs [ version: string@"nu-complete version" # the version to generate release notes for --as-table (-t) # output PR checks as a table -]: nothing -> nothing { +]: [ + nothing -> nothing, + nothing -> table +] { query-prs --milestone=$version | where not author.is_bot | sort-by mergedAt