Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release Best Practices Guide #149

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions docs/guides/documentation/releases/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Releases

<pre align="center">Streamline software releases through consistent templates, automated aggregation and notifications.</pre>
riverma marked this conversation as resolved.
Show resolved Hide resolved

## Introduction

**Background**: Making releases can be a chore. From ensuring the release notes have helpful information to aggregating many updates and notifying your users - doing the process right can be time consuming and fraught with misunderstandings. In this guide we recommend a standardized, automated way to notify your users of new releases. For example, we recommend using a `.github/release.yml` template to allow for more readable release notes, showcasing the importance of each update. We also provide automation for aggregating releases from many repositories into a super-release that provides a holistic view of project progress. Finally, we recommend automated notifications through GitHub integrations to help keep everyone informed.
riverma marked this conversation as resolved.
Show resolved Hide resolved

**Use Cases**:
riverma marked this conversation as resolved.
Show resolved Hide resolved
- Automating release notes generation with a readable template
- Aggregating multiple repository releases for projects with many repositories
- Streamlining release notifications to keep teams and users informed automatically

---

## Prerequisites
* A GitHub account and repository
riverma marked this conversation as resolved.
Show resolved Hide resolved
* Basic understanding of GitHub Actions and workflows
* Access to IM channels like Slack for integration

---

## Quick Start
**[⬇️ Recommended GitHub release.yml Template](release.yml)**

_A customizable GitHub-specific release template. Customize it to fit your project's labeling scheme and presentation preferences for release notes and place it in your GitHub repository's `.github/release.yml` path._

**[⬇️ Python Script to Aggregate GitHub Releases](gh_aggregate_release_notes.py)**

_A Python script that aggregates release notes from many repositories and generates a "super" release text._

NOTE: You'll need a configuration file to use the above Python script. See step 2.2 below for an example file that you can customize for your project.


---

## Step-by-Step Guide

1. **Customize Release Notes**:
- Create a `.github/release.yml` file in your repository.
- Download our [GitHub release notes template](release.yml) and place the contents into your `.github/release.yml` file.
- Customize our recommended template as a baseline and to configure how release notes are generated based on your project's issue labels.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@riverma Adding a picture(s) of how to this step generates release notes would be helpful for new users wanting to adopt SLIM.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion @pogi7 - will do here 👍


2. **Aggregate Releases**:
We recommend using a script to automatically aggregate release notes from multiple repositories. Details below.

2.1 Download our script
- Copy/download our script to your local machine
```
curl --output gh_aggregate_release_notes.py https://raw.githubusercontent.com/NASA-AMMOS/slim/issue-97/docs/guides/documentation/releases/gh_aggregate_release_notes.py
```

2.2 Create a configuration file with your project's release information. Save the file with extension `.yml` A sample is provided below:
```
github_token: <INSERT YOUR GH PERSONAL TOKEN>
Copy link

@nutjob4life nutjob4life Mar 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use a brief hint here on how to get a token—maybe down on the FAQ?

In retrospect, I think this is awfully risky. Your token could afford a lot of permissions unless it's carefully scoped—and putting it into a file makes it too easy to check into version control (yay detect-secrets!) Maybe it should be an environment variable?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My comment is to link to the GH Actions documentation directly -- I wouldn't put "" here, but instead I would make it explicit with ${{ secrets.GITHUB_TOKEN }}. Then in 2.2, something like, "Create a release notes template (.yml) that's authorized to publish your project."

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's OK because "secrets.GITHUB_TOKEN" is mnemonically explicit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nutjob4life - I was debating between an environment variable and a file. I felt that a file could be kept secure on a system with permission control, whereas the environment variable approach keeps a credential on the command-line history. Though I see the risk of committing this file and your point! Are these our only two choices, I'm curious about best practices here.

@jpl-jengelke - agreed to linking to GH directly and letting their docs handle the UI specifics.

urls:
- https://github.com/your-org/your-repo-1/releases/tag/v1.1.0
- https://github.com/your-org/your-repo-2/releases/tag/v2.5.3
```
2.3 Run the script to generate aggregated release notes
```
$ python gh_aggregate_release_notes.py your_configuration_file.yml
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I like the concept of building release notes based off the GH API and the script looks light enough. When I first read about this I though of using GH's built-in facilities to just copy it. Could it all be done in an action file without a separate Python component? What if the actual release notes stored in the 'releases' section of the repository are used or the macros that create them there are reused? I think if GH products are reused then GH handles maintenance, the notes are uniform and the maintenance footprint is reduced. (I can see Python versions and module releases being pesky as time goes forward, plus shouldn't there be a dependencies file (requirements.txt)?


2.3 Review your aggregated release notes
- Your aggregated release notes should be printed to standard output (`stdout`)

3. **Set Up Notifications**:
- Integrate GitHub with your IM channels, such as Slack, for example installing and configuring extensions like: [GitHub's Slack integration](https://slack.github.com).
- Conduct individual outreach to your customers/users to encourage them to watch your repository for release updates to stay informed about new releases through GitHub's notification system.

---

## Frequently Asked Questions (FAQ)

- Q: Can I customize which pull requests or issues appear in the release notes?
- A: Yes, by using labels and the `exclude-labels` field in the `.github/release.yml` file, you can control the inclusion of pull requests and issues in your release notes.

---

## Credits

**Authorship**:
- [@riverma](https://www.github.com/riverma)

**Acknowledgements**:
* [@hookhua](https://github.com/hookhua) for inspiration to make this guide.
* [HySDS project](https://github.com/hysds) for inspiration and feedback.

---

## Feedback and Contributions

We welcome feedback and contributions to help improve and grow this page. Please see our [contribution guidelines](https://nasa-ammos.github.io/slim/docs/contribute/contributing/).
99 changes: 99 additions & 0 deletions docs/guides/documentation/releases/gh_aggregate_release_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import requests

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script should probably include some comments at the top about dependencies. For example, it does import requests so needs a Python installation (or virtual environment) with Requests installed. Bonus points for mentioning a version number of requests too 👍 (same for tqdm, yaml).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Echoes my comment above... Also, Python 3+.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also may want to add a little try-except error catching to explain to a casual user what's wrong. What if the template itself is munged up? That can be hard to catch.

Some minimal logging (even just print statements) may be helpful, too.

import yaml
import argparse
from urllib.parse import urlparse
from time import sleep
from tqdm import tqdm
import random

def read_config(file_path):
"""
Reads the YAML configuration file and returns its contents.

Args:
file_path (str): The path to the YAML configuration file.

Returns:
dict: The contents of the YAML file if successful, None otherwise.
"""
with open(file_path, 'r') as stream:
try:
return yaml.safe_load(stream)
except yaml.YAMLError as exc:
print(exc)
return None

def get_release_notes(url, github_token):
"""
Extracts the owner, repo, and tag from the given GitHub URL and retrieves the release notes using the GitHub API.
Implements retries with exponential backoff and jitter for handling API rate limits and network issues.

Args:
url (str): The URL of the GitHub release tag page.
github_token (str): The GitHub token used for authentication.

Returns:
str: The release notes text if successful, empty string otherwise.
"""
parsed_url = urlparse(url)
hostname = parsed_url.hostname
path_parts = parsed_url.path.split('/')
owner, repo, tag = path_parts[1], path_parts[2], path_parts[-1]

# Be agnostic to GitHub.com or GitHub Enterprise repo URLs
api_url_prefix = f"https://api.github.com/repos/{owner}/{repo}" if hostname == "github.com" else f"https://{hostname}/api/v3/repos/{owner}/{repo}"
api_url = f"{api_url_prefix}/releases/tags/{tag}"
headers = {
"Authorization": f"token {github_token}",
"Accept": "application/vnd.github.v3+json"
}

max_retries = 5
retry_num = 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use Tenacity here to automate retries over the function. It would also handle the manifest other potential exceptions that could occur.

backoff_factor = 2
while retry_num < max_retries:
response = requests.get(api_url, headers=headers)
if response.status_code == 200:
return response.json().get("body", "")
else:
print(f"Error fetching release notes for {url}: {response.status_code}. Retrying...")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move the retry_num += 1 up and add an if retry_num < max_retries to condition the sleep? The "edge" case is that GitHub gives non-200 every single time, but we end up doing a final sleep for 2 × 2⁴ + [0.0..1.0] seconds—up to 33 seconds—to then just abort without a final attempt.

In other words, change from attempt…sleep…attempt…sleep…attempt…sleep…attempt…sleep…abortattempt…sleep…attempt…sleep…attempt…sleep…attempt…abort 🤷

Not a huge deal but I've been known to be rather pedantic 😂

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me look into this thanks @nutjob4life

sleep_time = backoff_factor * (2 ** retry_num) + random.uniform(0, 1)
sleep(sleep_time)
retry_num += 1

print(f"Failed to fetch release notes after {max_retries} attempts.")
return ""

def main(config_file):
"""
Main function to aggregate GitHub release notes.
It reads a YAML configuration file for GitHub repository URLs and a GitHub token,
then retrieves and prints the release notes for each repository URL listed.

Args:
config_file (str): The path to the YAML configuration file containing the GitHub token and URLs.
"""
config = read_config(config_file)
if not config:
print("Failed to read configuration.")
return

github_token = config.get("github_token")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above about putting the token into the .yml (or .yaml where I come from 😁) file.

urls = config.get("urls", [])

release_notes = ""

for url in tqdm(urls, desc="Downloading release notes"):
notes = get_release_notes(url, github_token)
if notes:
repo_name = url.split("/")[4] # Assumes a specific URL structure.
release_notes += f"# {repo_name}\n\n{notes}\n\n"

print(release_notes)

if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Aggregates GitHub release notes from a configuration file.")
parser.add_argument('config_file', type=str, help="Path to the YAML configuration file")
args = parser.parse_args()

main(args.config_file)
29 changes: 29 additions & 0 deletions docs/guides/documentation/releases/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# place in .github/release.yml

changelog:
exclude: # exclude any PRs labels we don't want in the changelog
labels:
- ignore-for-release
- skip-changelog
categories: # group PRs by your most important labels. Add / customize as needed.
- title: "🚀 Features"
labels:
- enhancement
- title: "🐛 Bug Fixes"
labels:
- bug
- title: "📚 Documentation"
labels:
- documentation
- title: 📦 Dependencies
labels:
- dependencies
- title: 💻 Other Changes
labels:
- "*"
exclude: # place the list of all labels you've categorized above to avoid duplication!
labels:
- enhancement
- bug
- documentation
- dependencies
Loading