Skip to content

Commit d253b89

Browse files
feat: add issue body truncation to prevent GitHub API errors
Add truncation logic to handle GitHub's 65,000 character limit for issue bodies. When command_output or parsed_output fields are very large, they are now proportionally truncated before template rendering. - Add truncation.py utility with proportional budget distribution - Add constants for configurable max body length (default: 60,000) - Integrate truncation in tac_sync_issues_cli and render_issue_bodies - Add 24 unit tests for all truncation functions - Support GITHUB_MAX_ISSUE_BODY_LENGTH env var for configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3acbbc1 commit d253b89

5 files changed

Lines changed: 686 additions & 2 deletions

File tree

github_ops_manager/configuration/cli.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
from github_ops_manager.processing.yaml_processor import YAMLProcessor
1818
from github_ops_manager.schemas.default_issue import IssueModel, IssuesYAMLModel, PullRequestModel
1919
from github_ops_manager.synchronize.driver import run_process_issues_workflow
20+
from github_ops_manager.utils.constants import DEFAULT_MAX_ISSUE_BODY_LENGTH
2021
from github_ops_manager.utils.tac import find_issue_with_title
2122
from github_ops_manager.utils.templates import construct_jinja2_template_from_file, render_template_with_model
23+
from github_ops_manager.utils.truncation import truncate_test_case_outputs
2224
from github_ops_manager.utils.yaml import dump_yaml_to_file, load_test_case_definitions_from_directory, load_yaml_file
2325

2426
load_dotenv()
@@ -77,6 +79,10 @@ def tac_sync_issues_cli(
7779
# can be constructed.
7880
template = construct_jinja2_template_from_file(Path(__file__).parent.parent / "templates" / "tac_issues_body.j2")
7981

82+
# Get max body length from environment variable or use default
83+
max_body_length = int(os.getenv("GITHUB_MAX_ISSUE_BODY_LENGTH", str(DEFAULT_MAX_ISSUE_BODY_LENGTH)))
84+
typer.echo(f"Using max issue body length: {max_body_length}")
85+
8086
# Iterate through the test case definitions and ensure matching issues
8187
# exist in the YAML file.
8288
for test_case_definition in testing_as_code_test_case_definitions_model.test_cases:
@@ -100,10 +106,13 @@ def tac_sync_issues_cli(
100106
# script will have a control label called "script-already-created".
101107
# Additionally, a field named "generated_script_path" will be
102108
# populated with the path to the created test automation script.
109+
#
110+
# Truncate outputs if they would exceed the max body length
111+
truncated_test_case = truncate_test_case_outputs(test_case_definition, max_body_length)
103112
new_issue = IssueModel(
104113
title=test_case_definition.title,
105114
body=render_template_with_model(
106-
model=test_case_definition,
115+
model=truncated_test_case,
107116
template=template,
108117
),
109118
labels=test_case_definition.labels,
@@ -124,8 +133,10 @@ def tac_sync_issues_cli(
124133
desired_issues_yaml_model.issues.append(new_issue)
125134
else:
126135
# Update the existing issue based upon the test case definition.
136+
# Truncate outputs if they would exceed the max body length
137+
truncated_test_case = truncate_test_case_outputs(test_case_definition, max_body_length)
127138
existing_issue.body = render_template_with_model(
128-
model=test_case_definition,
139+
model=truncated_test_case,
129140
template=template,
130141
)
131142
existing_issue.labels = test_case_definition.labels

github_ops_manager/synchronize/issues.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Contains synchronization logic for GitHub issues."""
22

3+
import os
34
import time
45

56
import jinja2
@@ -11,7 +12,9 @@
1112
from github_ops_manager.synchronize.models import SyncDecision
1213
from github_ops_manager.synchronize.results import AllIssueSynchronizationResults, IssueSynchronizationResult
1314
from github_ops_manager.synchronize.utils import compare_github_field, compare_label_sets
15+
from github_ops_manager.utils.constants import DEFAULT_MAX_ISSUE_BODY_LENGTH
1416
from github_ops_manager.utils.templates import construct_jinja2_template_from_file
17+
from github_ops_manager.utils.truncation import truncate_data_dict_outputs
1518

1619
logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__)
1720

@@ -140,8 +143,15 @@ async def render_issue_bodies(issues_yaml_model: IssuesYAMLModel) -> IssuesYAMLM
140143
logger.error("Encountered a syntax error with the provided issue template", issue_template=issues_yaml_model.issue_template, error=str(exc))
141144
raise
142145

146+
# Get max body length from environment variable or use default
147+
max_body_length = int(os.getenv("GITHUB_MAX_ISSUE_BODY_LENGTH", str(DEFAULT_MAX_ISSUE_BODY_LENGTH)))
148+
143149
for issue in issues_yaml_model.issues:
144150
if issue.data is not None:
151+
# Truncate command outputs if present to fit within GitHub's body limit
152+
if "commands" in issue.data:
153+
issue.data = truncate_data_dict_outputs(issue.data, max_body_length)
154+
145155
# Render with all issue fields available
146156
render_context = issue.model_dump()
147157
try:

github_ops_manager/utils/constants.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,24 @@
2323

2424
DEFAULT_RELEASE_NOTES_HEADER = "# Release Notes\n\nThis document tracks the new features, enhancements, and bug fixes for each release."
2525
"""Default header expected in release notes file."""
26+
27+
# Issue Body Truncation Constants
28+
# -------------------------------
29+
30+
DEFAULT_MAX_ISSUE_BODY_LENGTH = 60000
31+
"""Default maximum length for issue bodies (leaves margin for GitHub's 65,536 limit)."""
32+
33+
GITHUB_MAX_ISSUE_BODY_LENGTH = 65536
34+
"""GitHub's actual maximum issue body length."""
35+
36+
TRUNCATION_SUFFIX = "\n... [truncated - {remaining} characters removed]"
37+
"""Suffix template appended to truncated content. Use .format(remaining=N) to fill in count."""
38+
39+
TEMPLATE_OVERHEAD_PER_COMMAND = 500
40+
"""Estimated character overhead per command in the TAC issue template (markdown, code fences)."""
41+
42+
BASE_TEMPLATE_OVERHEAD = 2000
43+
"""Estimated base overhead for non-command template content (headers, pass criteria, etc)."""
44+
45+
MIN_OUTPUT_LENGTH = 500
46+
"""Minimum characters to preserve in truncated output fields for readability."""

0 commit comments

Comments
 (0)