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
50 changes: 46 additions & 4 deletions actions/generate_changelog/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,55 @@ inputs:
github-token:
description: 'GitHub token with permissions to read PRs and create comments'
required: true
llm-provider:
description: 'LLM provider to use. One of: anthropic, openai, bedrock'
required: false
default: 'anthropic'
# Anthropic inputs
anthropic-api-key:
description: 'API key for Anthropic'
required: true
description: 'API key for Anthropic (required when llm-provider is anthropic)'
required: false
anthropic-model:
description: 'Anthropic model to use for generating the changelog'
required: false
default: 'claude-haiku-4-5'
# OpenAI inputs (also supports any OpenAI-compatible endpoint)
openai-api-key:
description: 'API key for OpenAI or an OpenAI-compatible provider (required when llm-provider is openai)'
required: false
openai-base-url:
description: 'Base URL for an OpenAI-compatible API endpoint. Defaults to the official OpenAI API.'
required: false
openai-model:
description: 'Model to use when llm-provider is openai'
required: false
default: 'gpt-4o-mini'
# Amazon Bedrock inputs — authenticate with either a Bedrock API key or an IAM access key pair
aws-bearer-token-bedrock:
description: 'Amazon Bedrock API key (bearer token). Takes precedence over IAM credentials when llm-provider is bedrock.'
required: false
aws-access-key-id:
description: 'AWS IAM access key ID. Used when llm-provider is bedrock and no bearer token is provided.'
required: false
aws-secret-access-key:
description: 'AWS IAM secret access key. Used when llm-provider is bedrock and no bearer token is provided.'
required: false
aws-region:
description: 'AWS region for Amazon Bedrock. Defaults to AWS_REGION env var if set, otherwise us-east-1.'
required: false
bedrock-model:
description: 'Amazon Bedrock model ID to use for generating the changelog'
required: false
default: 'us.amazon.nova-lite-v1:0'
# Shared inputs
prompt-template:
description: 'Path to a custom prompt template file for the LLM'
required: false
default: '.github/changelog-prompt.txt'
comment-header:
description: 'Header text for the comment that will contain the changelog'
required: false
default: '## 📝 Draft Changelog Entry'
default: '## Draft Changelog Entry'
max_tokens:
description: 'Maximum number of tokens to generate in the response'
required: false
Expand Down Expand Up @@ -67,16 +101,24 @@ runs:
shell: bash
run: |
python -m pip install --upgrade pip
pip install anthropic requests PyGithub
pip install anthropic openai boto3 requests PyGithub

- name: Generate changelog
if: ${{ steps.should-run.outputs.should_run == 'true' }}
shell: bash
run: python ${{ github.action_path }}/generate_changelog.py
env:
GITHUB_TOKEN: ${{ inputs.github-token }}
LLM_PROVIDER: ${{ inputs.llm-provider }}
ANTHROPIC_API_KEY: ${{ inputs.anthropic-api-key }}
ANTHROPIC_MODEL: ${{ inputs.anthropic-model }}
OPENAI_API_KEY: ${{ inputs.openai-api-key }}
OPENAI_MODEL: ${{ inputs.openai-model }}
AWS_BEARER_TOKEN_BEDROCK: ${{ inputs.aws-bearer-token-bedrock }}
AWS_ACCESS_KEY_ID: ${{ inputs.aws-access-key-id }}
AWS_SECRET_ACCESS_KEY: ${{ inputs.aws-secret-access-key }}
AWS_REGION: ${{ inputs.aws-region || env.AWS_REGION || 'us-east-1' }}
BEDROCK_MODEL: ${{ inputs.bedrock-model }}
PROMPT_TEMPLATE_PATH: ${{ inputs.prompt-template }}
COMMENT_HEADER: ${{ inputs.comment-header }}
PR_NUMBER: ${{ steps.should-run.outputs.pr_number }}
Expand Down
201 changes: 185 additions & 16 deletions actions/generate_changelog/generate_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,40 @@
import os
import sys
import json
from github import Github, Auth
import boto3
from anthropic import Anthropic
from botocore.exceptions import ClientError, NoCredentialsError
from github import Github, Auth
from openai import OpenAI, AuthenticationError, RateLimitError


PROVIDER_ANTHROPIC = "anthropic"
PROVIDER_OPENAI = "openai"
PROVIDER_BEDROCK = "bedrock"
SUPPORTED_PROVIDERS = (PROVIDER_ANTHROPIC, PROVIDER_OPENAI, PROVIDER_BEDROCK)


def get_env_vars():
"""Get all required environment variables in one function."""
provider = os.environ.get("LLM_PROVIDER", PROVIDER_ANTHROPIC).lower()

env_vars = {
"github_token": os.environ.get("GITHUB_TOKEN"),
"llm_provider": provider,
# Anthropic
"anthropic_api_key": os.environ.get("ANTHROPIC_API_KEY"),
"anthropic_model": os.environ.get("ANTHROPIC_MODEL", "claude-haiku-4-5"),
# OpenAI (also supports any OpenAI-compatible endpoint via openai_base_url)
"openai_api_key": os.environ.get("OPENAI_API_KEY"),
"openai_base_url": os.environ.get("OPENAI_BASE_URL") or None,
"openai_model": os.environ.get("OPENAI_MODEL", "gpt-4o-mini"),
# Amazon Bedrock — boto3 resolves credentials from its chain at runtime
# (AWS_BEARER_TOKEN_BEDROCK, AWS_ACCESS_KEY_ID/SECRET, instance profile, etc.)
"aws_region": os.environ.get("AWS_REGION", "us-east-1"),
"bedrock_model": os.environ.get("BEDROCK_MODEL", "us.amazon.nova-lite-v1:0"),
# Shared
"prompt_template_path": os.environ.get("PROMPT_TEMPLATE_PATH"),
"comment_header": os.environ.get("COMMENT_HEADER", "## 📝 Draft Changelog Entry"),
"comment_header": os.environ.get("COMMENT_HEADER", "## Draft Changelog Entry"),
"repo_full_name": os.environ.get("REPO_FULL_NAME"),
"pr_number": os.environ.get("PR_NUMBER"),
"max_tokens": int(os.environ.get("MAX_TOKENS", 1500)),
Expand All @@ -24,8 +46,19 @@ def get_env_vars():
missing_vars = []
if not env_vars["github_token"]:
missing_vars.append("GITHUB_TOKEN")
if not env_vars["anthropic_api_key"]:

if provider not in SUPPORTED_PROVIDERS:
print(
f"Error: Unsupported LLM_PROVIDER '{provider}'. Must be one of: {', '.join(SUPPORTED_PROVIDERS)}"
)
sys.exit(1)

if provider == PROVIDER_ANTHROPIC and not env_vars["anthropic_api_key"]:
missing_vars.append("ANTHROPIC_API_KEY")
elif provider == PROVIDER_OPENAI and not env_vars["openai_api_key"]:
missing_vars.append("OPENAI_API_KEY")
# Bedrock credentials are resolved by boto3's credential chain at runtime
# (bearer token, IAM key pair, instance profile, ECS/Lambda role, OIDC, etc.)

if missing_vars:
print(f"Error: Missing required environment variables: {', '.join(missing_vars)}")
Expand Down Expand Up @@ -69,7 +102,6 @@ def get_pr_data(env_vars):

def get_custom_prompt_template(env_vars):
"""Load custom prompt template if provided, otherwise use the default template file."""
# Try to load custom prompt from the path provided in env vars
try:
if env_vars["prompt_template_path"] and os.path.exists(
env_vars["prompt_template_path"]
Expand Down Expand Up @@ -141,11 +173,9 @@ def generate_changelog_with_anthropic(pr_data, env_vars):

return response.content[0].text
except Exception as e:
# Per Anthropic docs: https://docs.anthropic.com/en/api/errors
error_type = None
error_msg = str(e).lower()

# Try to extract error type from the exception
if hasattr(e, "type"):
error_type = e.type
elif "invalid_request_error" in error_msg:
Expand All @@ -161,46 +191,185 @@ def generate_changelog_with_anthropic(pr_data, env_vars):
elif "api_error" in error_msg:
error_type = "api_error"

# Handle specific error types
if error_type == "invalid_request_error" and "credit balance is too low" in error_msg:
# Insufficient credits case
print("ERROR: Anthropic API account has insufficient credits.")
comment = (
"## ⚠️ Changelog Generation Error\n\n"
"## Changelog Generation Error\n\n"
"The changelog could not be generated because the Anthropic API account has insufficient credits.\n\n"
"Please check your Anthropic account billing status and ensure you have available credits."
)
post_error_comment(comment, pr_data, env_vars)
elif error_type == "rate_limit_error":
# Rate limit exceeded
print("ERROR: Anthropic API rate limit exceeded.")
comment = (
"## ⚠️ Changelog Generation Error\n\n"
"## Changelog Generation Error\n\n"
"The changelog could not be generated because the Anthropic API rate limit was exceeded.\n\n"
"Please try again later. See https://docs.anthropic.com/en/api/rate-limits for more information."
)
post_error_comment(comment, pr_data, env_vars)
elif error_type == "authentication_error":
# Invalid API key
print("ERROR: Invalid Anthropic API key.")
comment = (
"## ⚠️ Changelog Generation Error\n\n"
"## Changelog Generation Error\n\n"
"The changelog could not be generated because the Anthropic API key is invalid or has expired.\n\n"
"Please check your API key configuration."
)
post_error_comment(comment, pr_data, env_vars)
else:
# Generic error case
print(f"Error generating changelog with Anthropic: {e}")
comment = (
"## ⚠️ Changelog Generation Error\n\n"
"## Changelog Generation Error\n\n"
"The changelog could not be generated due to an error with the Anthropic API.\n\n"
f"Error details: {str(e)}"
)
post_error_comment(comment, pr_data, env_vars)
return None


def generate_changelog_with_openai(pr_data, env_vars):
"""Generate a changelog entry using OpenAI's API or any OpenAI-compatible endpoint."""
client = OpenAI(
api_key=env_vars["openai_api_key"],
base_url=env_vars["openai_base_url"],
)

prompt_template = get_custom_prompt_template(env_vars)
formatted_pr_data = format_pr_data_for_prompt(pr_data)
prompt = prompt_template.format(pr_data=formatted_pr_data)

try:
response = client.chat.completions.create(
model=env_vars["openai_model"],
max_tokens=env_vars["max_tokens"],
temperature=env_vars["temperature"],
messages=[{"role": "user", "content": prompt}],
)

return response.choices[0].message.content
except AuthenticationError as e:
print("ERROR: Invalid OpenAI API key.")
comment = (
"## Changelog Generation Error\n\n"
"The changelog could not be generated because the OpenAI API key is invalid or has expired.\n\n"
"Please check your API key configuration."
)
post_error_comment(comment, pr_data, env_vars)
except RateLimitError as e:
error_msg = str(e).lower()
if "insufficient_quota" in error_msg or "quota" in error_msg:
print("ERROR: OpenAI API account has insufficient quota.")
comment = (
"## Changelog Generation Error\n\n"
"The changelog could not be generated because the OpenAI API account has insufficient quota.\n\n"
"Please check your OpenAI account billing status and ensure you have available credits."
)
else:
print("ERROR: OpenAI API rate limit exceeded.")
comment = (
"## Changelog Generation Error\n\n"
"The changelog could not be generated because the OpenAI API rate limit was exceeded.\n\n"
"Please try again later. See https://platform.openai.com/docs/guides/rate-limits for more information."
)
post_error_comment(comment, pr_data, env_vars)
except Exception as e:
print(f"Error generating changelog with OpenAI: {e}")
comment = (
"## Changelog Generation Error\n\n"
"The changelog could not be generated due to an error with the OpenAI API.\n\n"
f"Error details: {str(e)}"
)
post_error_comment(comment, pr_data, env_vars)
return None


def generate_changelog_with_bedrock(pr_data, env_vars):
"""Generate a changelog entry using Amazon Bedrock's Converse API."""
prompt_template = get_custom_prompt_template(env_vars)
formatted_pr_data = format_pr_data_for_prompt(pr_data)
prompt = prompt_template.format(pr_data=formatted_pr_data)

try:
# boto3 resolves credentials from its full chain: AWS_BEARER_TOKEN_BEDROCK,
# AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY env vars, instance profiles,
# ECS/Lambda roles, OIDC assume-role, ~/.aws/credentials, etc.
client = boto3.client(
service_name="bedrock-runtime",
region_name=env_vars["aws_region"],
)

response = client.converse(
modelId=env_vars["bedrock_model"],
messages=[{"role": "user", "content": [{"text": prompt}]}],
inferenceConfig={
"maxTokens": env_vars["max_tokens"],
"temperature": env_vars["temperature"],
},
)

return response["output"]["message"]["content"][0]["text"]
except NoCredentialsError:
print("ERROR: AWS credentials are missing or invalid.")
comment = (
"## Changelog Generation Error\n\n"
"The changelog could not be generated because the AWS credentials are missing or invalid.\n\n"
"Please check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY configuration."
)
post_error_comment(comment, pr_data, env_vars)
except ClientError as e:
error_code = e.response["Error"]["Code"]
if error_code in ("AccessDeniedException", "UnauthorizedException"):
print(f"ERROR: AWS access denied for Bedrock model '{env_vars['bedrock_model']}'.")
comment = (
"## Changelog Generation Error\n\n"
"The changelog could not be generated because access to the Amazon Bedrock model was denied.\n\n"
"Please ensure your AWS credentials have the `bedrock:InvokeModel` permission and that "
f"model access is enabled for `{env_vars['bedrock_model']}` in your AWS account."
)
elif error_code == "ThrottlingException":
print("ERROR: Amazon Bedrock request was throttled.")
comment = (
"## Changelog Generation Error\n\n"
"The changelog could not be generated because the Amazon Bedrock request was throttled.\n\n"
"Please try again later."
)
elif error_code == "ValidationException":
print(f"ERROR: Invalid request to Amazon Bedrock: {e}")
comment = (
"## Changelog Generation Error\n\n"
"The changelog could not be generated due to an invalid request to Amazon Bedrock.\n\n"
f"Please verify the model ID `{env_vars['bedrock_model']}` is correct and supported in region "
f"`{env_vars['aws_region']}`.\n\nError details: {str(e)}"
)
else:
print(f"Error generating changelog with Amazon Bedrock: {e}")
comment = (
"## Changelog Generation Error\n\n"
"The changelog could not be generated due to an error with Amazon Bedrock.\n\n"
f"Error details: {str(e)}"
)
post_error_comment(comment, pr_data, env_vars)
except Exception as e:
print(f"Error generating changelog with Amazon Bedrock: {e}")
comment = (
"## Changelog Generation Error\n\n"
"The changelog could not be generated due to an unexpected error with Amazon Bedrock.\n\n"
f"Error details: {str(e)}"
)
post_error_comment(comment, pr_data, env_vars)
return None


def generate_changelog(pr_data, env_vars):
"""Route changelog generation to the configured LLM provider."""
provider = env_vars["llm_provider"]
if provider == PROVIDER_ANTHROPIC:
return generate_changelog_with_anthropic(pr_data, env_vars)
elif provider == PROVIDER_OPENAI:
return generate_changelog_with_openai(pr_data, env_vars)
elif provider == PROVIDER_BEDROCK:
return generate_changelog_with_bedrock(pr_data, env_vars)


def post_error_comment(error_message, pr_data, env_vars):
"""Post an error comment explaining why changelog generation failed."""
try:
Expand Down Expand Up @@ -236,7 +405,7 @@ def main():
print("Draft changelog comment already exists. Taking no action.")
return

changelog_text = generate_changelog_with_anthropic(pr_data, env_vars)
changelog_text = generate_changelog(pr_data, env_vars)
if not changelog_text:
print("Failed to generate changelog text")
sys.exit(1)
Expand Down
Loading
Loading