Skip to content
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
20 changes: 18 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ on:
workflow_dispatch:
inputs:
branch:
description: 'Target branch for release'
description: 'Target branch to release from'
required: true
default: 'master'
type: string
version:
description: 'Release version'
required: true
type: string

previous_version:
description: 'Previous version, starting point for release notes generator'
required: true
type: string
jobs:
release:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -40,10 +45,21 @@ jobs:
with:
working-directory: sssd

- name: Install release notes dependencies
shell: bash
run: dnf install -y pandoc python3-pypandoc

- name: Execute release script
working-directory: sssd
shell: bash
env:
GH_TOKEN: ${{ secrets.BOT_TOKEN }}
run: |
./scripts/release.sh "${{ inputs.branch }}" "${{ inputs.version }}"
./scripts/release.sh "${{ inputs.branch }}" "${{ inputs.version }}" "${{ inputs.previous_version }}"

- name: Execute release notes script
working-directory: sssd
shell: bash
run: |
# Release notes file is generated from the release script
./scripts/generate-full-release-notes.sh "${{ inputs.version }}" "/tmp/sssd-${{ inputs.version }}.rst" sssd-bot "${{ secrets.BOT_TOKEN }}"
111 changes: 111 additions & 0 deletions scripts/fixed-issues.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env bash

set -euo pipefail

# Parse arguments
FROM=""
TO="HEAD"
FORMAT="plain"

# Pattern to find issues
pattern="Resolves: https://github.com/SSSD/sssd/issues/[0-9]+"

while [[ $# -gt 0 ]]; do
case $1 in
--from=*)
FROM="${1#*=}"
shift
;;
--from)
FROM="$2"
shift 2
;;
--to=*)
TO="${1#*=}"
shift
;;
--to)
TO="$2"
shift 2
;;
--format=*)
FORMAT="${1#*=}"
shift
;;
--format)
FORMAT="$2"
shift 2
;;
*)
echo "Unknown option: $1" >&2
echo "Usage: $0 --from <ref> [--to <ref>] [--format plain|rst|md]" >&2
exit 1
;;
esac
done

# Validate required arguments
if [[ -z "$FROM" ]]; then
echo "Error: --from is required" >&2
echo "Usage: $0 --from <ref> [--to <ref>] [--format plain|rst|md]" >&2
exit 1
fi

# Validate format
if [[ "$FORMAT" != "plain" && "$FORMAT" != "rst" && "$FORMAT" != "md" ]]; then
echo "Error: --format must be 'plain', 'rst' or 'md'" >&2
exit 1
fi

# Extract issue URLs from git log
issue_urls=$(
git log "$FROM..$TO" \
| grep -oE "$pattern" \
| sed 's/^Resolves: //' \
| sort -u \
| grep -v '^$' \
|| true
)

if [[ -z "$issue_urls" ]]; then
echo "No issues found in commits from $FROM to $TO" >&2
exit 0
fi

# Process each issue
for url in $issue_urls; do
# Extract issue number from URL
issue_number=$(echo "$url" | grep -oE '[0-9]+$')

# Get issue details using gh
issue_json=$(
gh issue view "$issue_number" --json number,title,state 2>/dev/null || echo ""
)

if [[ -z "$issue_json" ]]; then
echo "Warning: Could not fetch issue #$issue_number" >&2
continue
fi

# Parse JSON with jq
state=$(echo "$issue_json" | jq -r '.state')
title=$(echo "$issue_json" | jq -r '.title')

# Only include closed issues
if [[ "$state" != "CLOSED" ]]; then
continue
fi

# Output based on format
case "$FORMAT" in
plain)
echo "* #$issue_number $url - $title"
;;
md)
echo "* [#$issue_number]($url) - $title"
;;
rst)
echo "* \`#$issue_number <$url>\`__ - $title"
;;
esac
done
66 changes: 66 additions & 0 deletions scripts/generate-full-release-notes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
#
# Generate release notes for sssd.io

set -euo pipefail

FROM=""
TO="HEAD"
VERSION=""
FORMAT="rst"
scriptdir=`realpath \`dirname "$0"\``

while [[ $# -gt 0 ]]; do
case $1 in
--from=*)
FROM="${1#*=}"
shift
;;
--from)
FROM="$2"
shift 2
;;
--to=*)
TO="${1#*=}"
shift
;;
--to)
TO="$2"
shift 2
;;
--version=*)
VERSION="${1#*=}"
shift
;;
--version)
VERSION="$2"
shift 2
;;
*)
echo "Unknown option: $1" >&2
echo "Usage: $0 --from <ref> --to <ref> --version <version>" >&2
exit 1
;;
esac
done

notes=`$scriptdir/generate-release-notes.py --from $FROM --to $TO --version $VERSION --format $FORMAT`
fixed_issues=`$scriptdir/fixed-issues.sh --from $FROM --to $TO --format $FORMAT`
gitlog=`git shortlog --pretty=format:"%h %s" -w0,4 $FROM..$TO`

echo "$notes"
echo ""
echo "Tickets Fixed"
echo "-------------"
echo ""
echo "$fixed_issues"
echo ""
echo "Detailed Changelog"
echo "------------------"
echo ""
echo ".. code-block:: release-notes-shortlog"
echo ""
echo " $ git shortlog --pretty=format:\"%h %s\" -w0,4 $FROM..$TO"
echo ""
echo "$gitlog" | sed 's/^/ /'
echo ""
136 changes: 136 additions & 0 deletions scripts/generate-release-notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env python3

import argparse
import re
import subprocess
import sys
import pypandoc


class ReleaseNote:
"""Represents a category of release notes."""

def __init__(self, tag, title):
self.tag = tag
self.title = title

def findall(self, git_log):
"""Extract all notes for this tag from the git log."""
# Pattern matches :tag: followed by content until empty line or next tag
pattern = rf"^ *:{self.tag}:((?:(?!(?:^ *:\w+:| *$)).*\n)+)"
matches = re.findall(pattern, git_log, re.MULTILINE)

if not matches:
return []

notes = []
for match in matches:
# Join multiline notes, preserving markdown formatting
note = " ".join([line.strip() for line in match.split("\n")])
notes.append(f"* {note}")

return notes

def generate(self, git_log):
notes = self.findall(git_log)
if not notes:
return ""

output = f"### {self.title}\n\n"
output += "\n".join(notes)
return output


class ReleaseNotesGenerator:
"""Generate release notes from git commit messages."""

def __init__(self, from_ref, to_ref, version):
self.from_ref = from_ref
self.to_ref = to_ref
self.version = version

self.project_name = "SSSD"
self.categories = [
ReleaseNote("relnote", "General information"),
ReleaseNote("feature", "New features"),
ReleaseNote("fixes", "Important fixes"),
ReleaseNote("packaging", "Packaging changes"),
ReleaseNote("config", "Configuration changes"),
]

def get_git_log(self, from_ref, to_ref):
"""Get git log between two references."""
result = subprocess.run(
["git", "log", f"{from_ref}..{to_ref}"],
capture_output=True,
text=True,
check=True,
)

return result.stdout

def generate(self):
"""Generate release notes in markdown."""
git_log = self.get_git_log(self.from_ref, self.to_ref)
output = f"# {self.project_name} {self.version} Release Notes\n"
output += "\n"
output += "## Highlights\n"

# Generate sections for each category
for category in self.categories:
notes = category.generate(git_log)
if notes:
output += "\n"
output += notes
output += "\n"

return output.strip()


def main():
parser = argparse.ArgumentParser(
description="Generate release notes from git commit messages"
)
parser.add_argument(
"--from", type=str, required=True, dest="from_ref", help="Start point reference"
)
parser.add_argument(
"--to",
type=str,
default="HEAD",
dest="to_ref",
help="End point reference (default: HEAD)",
)
parser.add_argument(
"--version", type=str, required=True, help="New release version"
)
parser.add_argument(
"--format",
type=str,
choices=["md", "rst"],
default="md",
help="Output format (default: md)",
)

args = parser.parse_args()

try:
generator = ReleaseNotesGenerator(args.from_ref, args.to_ref, args.version)
output = generator.generate()

# Convert markdown to requested format with 80 char line wrapping
extra_args = ["--wrap=auto", "--columns=80"]
output = pypandoc.convert_text(
output, args.format, format="md", extra_args=extra_args
)

print(output)
except subprocess.CalledProcessError as e:
print(f"Error: git command failed: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)

if __name__ == "__main__":
main()
Loading
Loading