Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ jobs:
- name: Generate release notes
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
ARGS="generate --use-git-fallback --output /tmp/release-notes.md"
Expand Down
66 changes: 0 additions & 66 deletions .github/workflows/releaseforge.yml

This file was deleted.

4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ inputs:
template-name:
description: "Built-in template: semver-release-notes, conventional-changelog, version-analysis"
required: false
github-token:
description: "GitHub token for resolving contributor usernames from emails"
required: false
use-git-fallback:
description: "Fall back to git commit log analysis if LLM fails"
required: false
Expand Down Expand Up @@ -87,6 +90,7 @@ runs:
GEMINI_API_KEY: ${{ inputs.api-key }}
OPENAI_API_KEY: ${{ inputs.api-key }}
ANTHROPIC_API_KEY: ${{ inputs.api-key }}
GITHUB_TOKEN: ${{ inputs.github-token }}
run: |
CMD="${{ inputs.command }}"

Expand Down
8 changes: 7 additions & 1 deletion actions/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,16 @@ func (a *GenerateAction) Execute(c *cli.Context) error {
return err
}

// Setup GitHub service for contributor resolution
ghSvc := services.NewGitHubService(c.String("github-token"))
if ghSvc.HasToken() {
helpers.Log.Info().Msg("GitHub token provided — will resolve contributor usernames")
}

// Get detailed commits
var detailedCommits []domain.DetailedCommit
if len(commits) > 0 {
detailedCommits, err = a.gitSvc.GetCommitDetails(commits)
detailedCommits, err = a.gitSvc.GetCommitDetails(commits, ghSvc)
if err != nil {
helpers.Log.Warn().Msgf("Could not get commit details: %v", err)
}
Expand Down
6 changes: 6 additions & 0 deletions flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ var forceGitModeFlag = cli.BoolFlag{
Usage: "Force using git commit log analysis instead of LLM (no API key needed)",
}

var githubTokenFlag = cli.StringFlag{
Name: "github-token",
Usage: "GitHub token for resolving contributor usernames from emails",
EnvVar: "GITHUB_TOKEN",
}

var verboseFlag = cli.BoolFlag{
Name: "verbose, v",
Usage: "Enable verbose/debug logging",
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func main() {
outputFlag,
useGitFallbackFlag,
forceGitModeFlag,
githubTokenFlag,
verboseFlag,
}

Expand Down
6 changes: 4 additions & 2 deletions services/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func (g *GitService) parseAndFilterCommits(output string, ignoreList []string) (
return commits, nil
}

func (g *GitService) GetCommitDetails(commits []domain.CommitInfo) ([]domain.DetailedCommit, error) {
func (g *GitService) GetCommitDetails(commits []domain.CommitInfo, ghSvc *GitHubService) ([]domain.DetailedCommit, error) {
helpers.Log.Info().Msg("Gathering detailed commit information...")
var detailed []domain.DetailedCommit

Expand All @@ -137,7 +137,9 @@ func (g *GitService) GetCommitDetails(commits []domain.CommitInfo) ([]domain.Det
emailOut, err := g.runGit("show", c.Hash, "--format=%ae", "--no-patch")
if err == nil {
dc.AuthorEmail = strings.TrimSpace(emailOut)
if ghUser := extractGitHubUser(dc.AuthorEmail); ghUser != "" {
if ghSvc != nil {
dc.Author = ghSvc.ResolveAuthor(dc.Author, dc.AuthorEmail)
} else if ghUser := extractGitHubUser(dc.AuthorEmail); ghUser != "" {
dc.Author = "@" + ghUser
}
}
Expand Down
166 changes: 166 additions & 0 deletions services/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package services

import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync"

"github.com/AxeForging/releaseforge/helpers"
)

type GitHubService struct {
token string
cache map[string]string
mu sync.Mutex
}

func NewGitHubService(token string) *GitHubService {
return &GitHubService{
token: token,
cache: make(map[string]string),
}
}

func (gh *GitHubService) HasToken() bool {
return gh.token != ""
}

func (gh *GitHubService) ResolveUsername(email string) string {
if email == "" {
return ""
}

// Check noreply format first (no API needed)
if user := extractGitHubUser(email); user != "" {
return user
}

if !gh.HasToken() {
return ""
}

gh.mu.Lock()
if cached, ok := gh.cache[email]; ok {
gh.mu.Unlock()
return cached
}
gh.mu.Unlock()

username := gh.searchUserByEmail(email)

gh.mu.Lock()
gh.cache[email] = username
gh.mu.Unlock()

return username
}

func (gh *GitHubService) searchUserByEmail(email string) string {
url := fmt.Sprintf("https://api.github.com/search/users?q=%s+in:email", email)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return ""
}

req.Header.Set("Authorization", "Bearer "+gh.token)
req.Header.Set("Accept", "application/vnd.github+json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
helpers.Log.Debug().Msgf("GitHub API request failed for %s: %v", email, err)
return ""
}
defer resp.Body.Close()

if resp.StatusCode != 200 {
helpers.Log.Debug().Msgf("GitHub API returned %d for email %s", resp.StatusCode, email)
return ""
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return ""
}

var result struct {
TotalCount int `json:"total_count"`
Items []struct {
Login string `json:"login"`
} `json:"items"`
}

if err := json.Unmarshal(body, &result); err != nil {
return ""
}

if result.TotalCount > 0 && len(result.Items) > 0 {
username := result.Items[0].Login
helpers.Log.Info().Msgf("Resolved email %s → @%s", email, username)
return username
}

// Try commit search as fallback (works even when email isn't public)
return gh.searchCommitAuthor(email)
}

func (gh *GitHubService) searchCommitAuthor(email string) string {
url := fmt.Sprintf("https://api.github.com/search/commits?q=author-email:%s", email)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return ""
}

req.Header.Set("Authorization", "Bearer "+gh.token)
req.Header.Set("Accept", "application/vnd.github+json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
return ""
}
defer resp.Body.Close()

if resp.StatusCode != 200 {
return ""
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return ""
}

var result struct {
TotalCount int `json:"total_count"`
Items []struct {
Author struct {
Login string `json:"login"`
} `json:"author"`
} `json:"items"`
}

if err := json.Unmarshal(body, &result); err != nil {
return ""
}

if result.TotalCount > 0 && len(result.Items) > 0 {
login := result.Items[0].Author.Login
if login != "" {
helpers.Log.Info().Msgf("Resolved email %s → @%s (via commit search)", email, login)
return login
}
}

return ""
}

// ResolveAuthor takes an author name and email and returns the best display name
func (gh *GitHubService) ResolveAuthor(name, email string) string {
if username := gh.ResolveUsername(email); username != "" {
return "@" + strings.ToLower(username)
}
return name
}
2 changes: 1 addition & 1 deletion services/semver.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ func (s *SemverService) GetCommitsBetween(fromTag, toRef string, maxCommits int)
return nil, err
}

detailed, err := s.git.GetCommitDetails(commits)
detailed, err := s.git.GetCommitDetails(commits, nil)
if err != nil {
return nil, err
}
Expand Down