feat: add support for GitHub-style release notes#1804
feat: add support for GitHub-style release notes#1804DaleSeo wants to merge 4 commits intoknope-dev:mainfrom
Conversation
✅ Deploy Preview for knope ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
6d4f3d7 to
f962bc8
Compare
| @@ -0,0 +1 @@ | |||
| plans/ | |||
There was a problem hiding this comment.
I think ignoring the entire .cursor directory from the top-level .gitignore would be better—just so we don't permanently have a .cursor directory now.
There was a problem hiding this comment.
Oops, I didn't mean to include this file in the PR. I've added it to .git/info/exclude so it won't be tracked for me.
| let indices: Vec<usize> = changes | ||
| .iter() | ||
| .enumerate() | ||
| .filter_map(|(i, change)| change.git.as_ref().map(|_| i)) | ||
| .collect(); | ||
|
|
||
| if indices.is_empty() { | ||
| return Ok(github_state); | ||
| } | ||
|
|
||
| let (token, agent) = initialize_state(github_state)?; | ||
| let authorization = format!("token {token}"); | ||
|
|
||
| for idx in indices { | ||
| let short_hash = match changes.get(idx).and_then(|c| c.git.as_ref()) { | ||
| Some(g) => g.hash.clone(), | ||
| None => continue, | ||
| }; | ||
| match fetch_pr_for_commit(&agent, &authorization, github_config, &short_hash) { | ||
| Ok(Some((pr_number, author_login))) => { | ||
| if let Some(info) = changes.get_mut(idx).and_then(|c| c.git.as_mut()) { | ||
| info.pr_number = Some(pr_number); | ||
| info.author_login = Some(author_login); | ||
| } | ||
| } |
There was a problem hiding this comment.
This is a little simpler / more idiomatic. Also, though, I'd expect it should be Bearer not token—have you tried it yet?
| let indices: Vec<usize> = changes | |
| .iter() | |
| .enumerate() | |
| .filter_map(|(i, change)| change.git.as_ref().map(|_| i)) | |
| .collect(); | |
| if indices.is_empty() { | |
| return Ok(github_state); | |
| } | |
| let (token, agent) = initialize_state(github_state)?; | |
| let authorization = format!("token {token}"); | |
| for idx in indices { | |
| let short_hash = match changes.get(idx).and_then(|c| c.git.as_ref()) { | |
| Some(g) => g.hash.clone(), | |
| None => continue, | |
| }; | |
| match fetch_pr_for_commit(&agent, &authorization, github_config, &short_hash) { | |
| Ok(Some((pr_number, author_login))) => { | |
| if let Some(info) = changes.get_mut(idx).and_then(|c| c.git.as_mut()) { | |
| info.pr_number = Some(pr_number); | |
| info.author_login = Some(author_login); | |
| } | |
| } | |
| let (token, agent) = initialize_state(github_state)?; | |
| let authorization = format!("token {token}"); | |
| for git_info in changes.iter_mut().filter_map(|change| change.git.as_mut()) { | |
| let short_hash = &git_info.hash; | |
| match fetch_pr_for_commit(&agent, &authorization, github_config, short_hash) { | |
| Ok(Some((pr_number, author_login))) => { | |
| git_info.pr_number = Some(pr_number); | |
| git_info.author_login = Some(author_login); | |
| } |
There was a problem hiding this comment.
Both work:
$ TOKEN=$(gh auth token)
$ curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $TOKEN" \
"https://api.github.com/repos/knope-dev/knope/commits/70c183b/pulls"
200%
$ curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $TOKEN" \
"https://api.github.com/repos/knope-dev/knope/commits/70c183b/pulls"
200%Per the GitHub docs, both token and Bearer are accepted.
Switched to Bearer though because it's more commonly used and also supports JWTs.
| state.all_versioned_files, | ||
| &changeset, | ||
| state.ignore_conventional_commits, | ||
| |changes| { |
There was a problem hiding this comment.
Any reason not to move this logic into prepare_release instead of passing a closure?
There was a problem hiding this comment.
No good reason. Moved it. Split prepare_release into gather_changes + apply_release to make it work.
3ca6eac to
d0a2644
Compare
d0a2644 to
b4f0670
Compare
| @@ -0,0 +1 @@ | |||
| plans/ | |||
There was a problem hiding this comment.
Oops, I didn't mean to include this file in the PR. I've added it to .git/info/exclude so it won't be tracked for me.
| state.all_versioned_files, | ||
| &changeset, | ||
| state.ignore_conventional_commits, | ||
| |changes| { |
There was a problem hiding this comment.
No good reason. Moved it. Split prepare_release into gather_changes + apply_release to make it work.
| let indices: Vec<usize> = changes | ||
| .iter() | ||
| .enumerate() | ||
| .filter_map(|(i, change)| change.git.as_ref().map(|_| i)) | ||
| .collect(); | ||
|
|
||
| if indices.is_empty() { | ||
| return Ok(github_state); | ||
| } | ||
|
|
||
| let (token, agent) = initialize_state(github_state)?; | ||
| let authorization = format!("token {token}"); | ||
|
|
||
| for idx in indices { | ||
| let short_hash = match changes.get(idx).and_then(|c| c.git.as_ref()) { | ||
| Some(g) => g.hash.clone(), | ||
| None => continue, | ||
| }; | ||
| match fetch_pr_for_commit(&agent, &authorization, github_config, &short_hash) { | ||
| Ok(Some((pr_number, author_login))) => { | ||
| if let Some(info) = changes.get_mut(idx).and_then(|c| c.git.as_mut()) { | ||
| info.pr_number = Some(pr_number); | ||
| info.author_login = Some(author_login); | ||
| } | ||
| } |
There was a problem hiding this comment.
Both work:
$ TOKEN=$(gh auth token)
$ curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $TOKEN" \
"https://api.github.com/repos/knope-dev/knope/commits/70c183b/pulls"
200%
$ curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $TOKEN" \
"https://api.github.com/repos/knope-dev/knope/commits/70c183b/pulls"
200%Per the GitHub docs, both token and Bearer are accepted.
Switched to Bearer though because it's more commonly used and also supports JWTs.
b4f0670 to
6efc0b2
Compare
a2e9e13 to
02d52ab
Compare
|
Hey @dbanty, how's it going? 👋 Can you take another look at this? 🙏 |
|
Hey @DaleSeo! I haven't forgotten, I just realized a few things about this I want to set up for first:
|
|
Thanks for the heads-up, @dbanty! The async and rate limit work sounds great. I can rebase once that PR lands. Just let me know if there's anything I can help with in the meantime. |
Enable users to generate release notes that match GitHub's auto-generated format — entries like
* Add dark mode by @DaleSeo in #42— by fetching PR metadata from the GitHub API duringPrepareRelease.Summary
change_templatevariables:$pr_numberand$commit_author_login[github]is configuredMotivation
Knope already supports
$commit_author_nameand$commit_hashinchange_templates, but there's no way to include PR numbers or GitHub usernames — the two pieces that make GitHub's default release notes so useful for attributing contributions and linking back to discussions. This bridges that gap while keeping the feature zero-cost for users who don't need it.Design decisions
Lazy / opt-in enrichment — GitHub API calls (authenticated, rate-limited, network-dependent) are only triggered when a user explicitly includes
$pr_numberor$commit_author_loginin theirchange_templatesAND has a[github]config block. Existing users are completely unaffected.Graceful fallback — If the API call fails or a commit has no associated PR, the template is skipped and Knope falls back to the next template in the list. Failures are logged at
warnlevel but never block the release.No impact on
document-change— PR numbers don't exist when a change file is first created. Enrichment happens atPrepareReleasetime, after the change file's commit has been merged via a PR.Changed files
crates/knope-versioning/src/changes/mod.rspr_numberandauthor_loginfields toGitInfocrates/knope-versioning/src/release_notes/mod.rsneeds_forge_data()detection, testscrates/knope/src/integrations/github/pr_info.rscrates/knope/src/integrations/github/mod.rsenrich_git_infocrates/knope/src/integrations/git.rsGitInfofields toNonecrates/knope/src/step/releases/package.rsenrich_git_infoclosure inprepare_releasecrates/knope/src/step/releases/mod.rsneeds_forge_datacheckdocs/.../release-notes.mdx$pr_numberand$commit_author_logindocs/.../customizing-release-notes.mdxExample config
Test plan
cargo clippy --workspacecleanneeds_forge_data()detection (4 cases: local-only, pr_number, author_login, default)GITHUB_TOKENto verify API enrichment end-to-end