release notes: add ai generator for improvements and bug fixes - #22850
release notes: add ai generator for improvements and bug fixes#22850qiancai wants to merge 38 commits into
Conversation
|
Skipping CI for Draft Pull Request. |
There was a problem hiding this comment.
Code Review
This pull request introduces a new set of scripts to automate the generation of TiDB release notes using AI. The changes include modules for GitHub data fetching, Excel workbook processing, and AI prompt generation. Feedback provided includes suggestions for memory-efficient text processing and adding logging for truncated file summaries.
|
|
||
|
|
||
| def tail_output(text: str, max_lines: int = 40, max_chars: int = 4000) -> str: | ||
| tail = "\n".join(text.strip().splitlines()[-max_lines:]) |
| lines: list[str] = [] | ||
| page = 1 | ||
| total_chars = 0 | ||
| while len(lines) < max_files: |
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
ee6c21e to
2750b34
Compare
…and removed the --start-row/--end-row feature:
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughAdds a two-phase release-note generator. It processes Excel workbooks, filters release scope, retrieves GitHub data, generates and validates AI results, persists checkpoints, and exports grouped Markdown release notes. ChangesRelease Notes Generator
Estimated code review effort: 5 (Critical) | ~120 minutes Mergeability Score: 🟡 Moderate · up to This PR adds an AI release-note generator, but the current implementation can misclassify pull requests, write workbook data under the wrong columns, fail or misreport AI generation, accept vulnerable dependency versions, and use the wrong default output filename. These bounded correctness and security risks should be fixed or explicitly accepted before merging. Sequence Diagram(s)sequenceDiagram
participant Operator
participant CLI
participant ExcelWorkbook
participant GitHubClient
participant AIClient
participant MarkdownWriter
Operator->>CLI: run generate
CLI->>ExcelWorkbook: load and preprocess workbook
ExcelWorkbook->>GitHubClient: fetch issues and pull requests
GitHubClient-->>ExcelWorkbook: return cached metadata
ExcelWorkbook->>AIClient: generate validated row result
AIClient-->>ExcelWorkbook: return release note and documentation impact
ExcelWorkbook-->>CLI: save processed workbook
Operator->>CLI: run export-markdown
CLI->>ExcelWorkbook: collect Markdown entries
ExcelWorkbook-->>CLI: return entries
CLI->>MarkdownWriter: write release file
MarkdownWriter-->>Operator: write Markdown output
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 15
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 0a49cb05-195e-4962-b6fe-265fa8d63f52
📒 Files selected for processing (16)
scripts/release-notes-ai-generator/__main__.pyscripts/release-notes-ai-generator/ai_client.pyscripts/release-notes-ai-generator/cli.pyscripts/release-notes-ai-generator/constants.pyscripts/release-notes-ai-generator/excel_workbook.pyscripts/release-notes-ai-generator/github_client.pyscripts/release-notes-ai-generator/markdown_writer.pyscripts/release-notes-ai-generator/models.pyscripts/release-notes-ai-generator/prompts/doc-impact.mdscripts/release-notes-ai-generator/prompts/generation.mdscripts/release-notes-ai-generator/prompts/release-note.mdscripts/release-notes-ai-generator/release-notes-generator-readme.mdscripts/release-notes-ai-generator/requirements.txtscripts/release-notes-ai-generator/scope_filter.pyscripts/release-notes-ai-generator/tests/test_doc_impact.pyscripts/release-notes-ai-generator/utils.py
| DEFAULT_MODEL = "gpt-5.4" | ||
| MAX_OUTPUT_TOKENS = 16384 | ||
| TEMPERATURE = 0.1 | ||
| REASONING_MODEL_PREFIXES = ("o1", "o3", "o4", "gpt-5") | ||
|
|
||
| def __init__(self, model: str | None, timeout: int): | ||
| from openai import OpenAI | ||
|
|
||
| key = os.environ.get("AZURE_OPENAI_KEY", "") | ||
| base_url = ( | ||
| os.environ.get("AZURE_OPENAI_BASE_URL") | ||
| or os.environ.get("OPENAI_BASE_URL", "") | ||
| ) | ||
| if not key: | ||
| raise ValueError( | ||
| "AZURE_OPENAI_KEY environment variable is required " | ||
| "when using --ai-provider azure" | ||
| ) | ||
| if not base_url: | ||
| raise ValueError( | ||
| "AZURE_OPENAI_BASE_URL or OPENAI_BASE_URL environment variable " | ||
| "is required when using --ai-provider azure" | ||
| ) | ||
| self.client = OpenAI(api_key=key, base_url=base_url, timeout=timeout) | ||
| if not hasattr(self.client, "responses"): | ||
| raise ValueError( | ||
| "The installed OpenAI Python SDK does not support the Responses API. " | ||
| "Install a newer 'openai' package version before using --ai-provider azure." | ||
| ) | ||
| self.model = model or self.DEFAULT_MODEL |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Azure OpenAI v1 API model field deployment name openai/v1 base_url requirement
💡 Result:
When using the Azure OpenAI v1 API, the configuration requirements for the base_url and the model field are as follows: Base URL Requirement You must configure your client to use the specific /openai/v1/ route [1][2]. The base_url should be structured as: https://{YOUR-RESOURCE-NAME}.openai.azure.com/openai/v1/ [1][3] This path enables the v1 API, which uses implicit versioning, meaning an explicit api-version parameter is no longer required in your API calls [1][2][4]. Model Field Requirement In the Azure OpenAI environment, the model parameter is always required and must be set to your specific deployment name, not the underlying model name (e.g., 'gpt-4o') [5][6][7]. While the deployment name can be identical to the model name, it is a separate identifier created during the deployment process, and the API will return an error if you provide the model name instead of the deployment name [6][8][7]. Summary of Key Changes - Use the standard OpenAI client (OpenAI) rather than the Azure-specific client (AzureOpenAI) when utilizing the v1 API [1][7]. - The api-version parameter is deprecated for these v1 calls [1][7]. - The model argument must always map to your defined deployment name [5][4].
Citations:
- 1: https://learn.microsoft.com/en-us/azure/foundry/openai/api-version-lifecycle
- 2: https://learn.microsoft.com/en-us/azure/foundry/foundry-models/concepts/endpoints
- 3: https://learn.microsoft.com/en-us/azure/foundry/openai/latest
- 4: https://learn.microsoft.com/en-us/azure/foundry/how-to/model-inference-to-openai-migration
- 5: https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/switching-endpoints
- 6: https://learn.microsoft.com/en-us/azure/foundry-classic/openai/how-to/create-resource
- 7: https://learn.microsoft.com/en-us/azure/developer/ai/how-to/azure-openai-to-responses
- 8: https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/migration
🏁 Script executed:
#!/bin/bash
set -eu
file="scripts/release-notes-ai-generator/ai_client.py"
printf '%s\n' '--- target implementation ---'
sed -n '145,220p' "$file"
printf '%s\n' '--- Azure configuration references ---'
rg -n -i --glob '!node_modules' \
'AZURE_OPENAI_(KEY|BASE_URL|DEPLOYMENT)|OPENAI_BASE_URL|ai-provider azure|ai-model|DEFAULT_MODEL' .
printf '%s\n' '--- repository tests and documentation candidates ---'
git ls-files | rg -i '(^|/)(test|tests|docs|readme)|release-notes-ai-generator'Repository: pingcap/docs
Length of output: 6484
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- CLI options ---'
sed -n '80,120p' scripts/release-notes-ai-generator/cli.py
printf '%s\n' '--- Azure documentation ---'
sed -n '55,75p' scripts/release-notes-ai-generator/release-notes-generator-readme.md
sed -n '120,135p' scripts/release-notes-ai-generator/release-notes-generator-readme.md
printf '%s\n' '--- entry point examples ---'
sed -n '1,35p' scripts/release-notes-ai-generator/__main__.py
printf '%s\n' '--- declared SDK dependency ---'
cat scripts/release-notes-ai-generator/requirements.txt
printf '%s\n' '--- read-only configuration behavior probe ---'
python3 - <<'PY'
from pathlib import Path
import re
client = Path("scripts/release-notes-ai-generator/ai_client.py").read_text()
cli = Path("scripts/release-notes-ai-generator/cli.py").read_text()
readme = Path(
"scripts/release-notes-ai-generator/release-notes-generator-readme.md"
).read_text()
default = re.search(r'DEFAULT_MODEL\s*=\s*"([^"]+)"', client).group(1)
cli_default = re.search(
r'"--ai-model".*?default=AzureOpenAIClient\.DEFAULT_MODEL',
cli,
re.S,
)
azure_vars = sorted(set(re.findall(r'AZURE_OPENAI_[A-Z_]+', readme)))
has_deployment_var = "AZURE_OPENAI_DEPLOYMENT" in readme or "AZURE_OPENAI_DEPLOYMENT" in client
documented_base_url = re.findall(
r'https?://[^\s`<>()]+(?:/openai/v1/)?',
readme,
)
print({"azure_default_model": default})
print({"cli_uses_azure_default": bool(cli_default)})
print({"documented_azure_variables": azure_vars})
print({"deployment_variable_documented_or_read": has_deployment_var})
print({"base_url_examples": documented_base_url})
PYRepository: pingcap/docs
Length of output: 6189
Use an Azure deployment name and require an Azure v1 base URL.
When --ai-provider azure is selected, model must be the Azure deployment name. The current default is the public model name gpt-5.4, and no deployment setting is documented. Require an explicit deployment name or --ai-model.
Require /openai/v1/ in the configured base URL, or append it before constructing OpenAI. The current check accepts any non-empty URL and defers invalid endpoint errors to the API.
🧰 Tools
🪛 Ruff (0.16.1)
[warning] 169-169: Missing return type annotation for special method __init__
Add return type annotation: None
(ANN204)
[warning] 178-181: Avoid specifying long messages outside the exception class
(TRY003)
[warning] 183-186: Avoid specifying long messages outside the exception class
(TRY003)
[warning] 189-192: Avoid specifying long messages outside the exception class
(TRY003)
| def _run(self, prompt: str, _output_schema: dict[str, Any]) -> str: | ||
| kwargs: dict[str, Any] = { | ||
| "model": self.model, | ||
| "input": [{"role": "user", "content": prompt}], | ||
| "max_output_tokens": self.MAX_OUTPUT_TOKENS, | ||
| } | ||
| logger.debug("Azure OpenAI prompt:\n%s", prompt) | ||
| if not self._is_reasoning_model(): | ||
| kwargs["temperature"] = self.TEMPERATURE | ||
| response = self.client.responses.create(**kwargs) | ||
| return response.output_text.strip() |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
openai python SDK responses.create text format json_schema structured outputs parameter
💡 Result:
To use Structured Outputs with the OpenAI Python SDK, you can employ the response_format parameter in the client.chat.completions.create() method, or use the higher-level client.chat.completions.parse() helper [1][2][3]. 1. Using response_format with create(): You can specify the json_schema type within the response_format parameter. This requires providing a schema object with a name, a strict flag, and the JSON schema itself [1][4]. Example: client.chat.completions.create( model="gpt-4o-2024-08-06", messages=[{"role": "user", "content": "..."}], response_format={ "type": "json_schema", "json_schema": { "name": "my_schema", "strict": True, "schema": { "type": "object", "properties": {"key": {"type": "string"}}, "required": ["key"], "additionalProperties": False } } }) 2. Using client.chat.completions.parse(): The SDK provides a parse() helper that simplifies this process by allowing you to pass a Pydantic model directly [2][3]. The SDK automatically converts the Pydantic model into the required JSON schema, handles the API call, and parses the response into your Pydantic object [2]. Example: from pydantic import BaseModel class MyResponse(BaseModel): key: str completion = client.chat.completions.parse( model="gpt-4o-2024-08-06", messages=[{"role": "user", "content": "..."}], response_format=MyResponse,) print(completion.choices.message.parsed) Key Details: - Compatibility: Structured Outputs is supported in models such as gpt-4o-2024-08-06, gpt-4o-mini-2024-07-18, and later [5][6][4]. - Strictness: When using json_schema, setting "strict": True ensures the model output adheres exactly to your schema [1][4]. - Benefits: Unlike basic JSON mode, Structured Outputs guarantees schema adherence, ensuring the generated JSON matches your defined structure reliably [5][7].
Citations:
- 1: https://developers.openai.com/cookbook/examples/structured_outputs_intro
- 2: https://github.com/openai/openai-python/blob/main/helpers.md
- 3: https://github.com/openai/openai-python/blob/f16fbbd2/src/openai/resources/chat/completions/completions.py
- 4: https://openai.com/index/introducing-structured-outputs-in-the-api/
- 5: https://developers.openai.com/api/docs/guides/structured-outputs?api-mode=chat
- 6: https://developers.openai.com/api/docs/guides/structured-outputs
- 7: https://developers.openai.com/api/docs/guides/structured-outputs.md
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- relevant implementation ---'
sed -n '1,230p' scripts/release-notes-ai-generator/ai_client.py
printf '%s\n' '--- schema construction and validation ---'
sed -n '430,560p' scripts/release-notes-ai-generator/ai_client.py
printf '%s\n' '--- OpenAI dependency declarations ---'
rg -n -i 'openai|responses\.create|json_schema|output_schema' \
--glob '!*.lock' --glob '!node_modules/**' .Repository: pingcap/docs
Length of output: 42041
🌐 Web query:
OpenAI Responses API Python SDK responses.create text format json_schema strict incomplete status output_text official documentation
💡 Result:
When using the OpenAI Python SDK to interact with the Responses API, you can enforce structured, schema-compliant outputs by configuring the text format in your request. Setting strict: true within a json_schema definition guarantees that the model output adheres strictly to the provided JSON schema [1][2]. Key Technical Details: - Configuration: Structured outputs are enabled by passing a text parameter with a format object in your request, specifying type: "json_schema", strict: true, and your desired schema [1][3]. - Status Handling: The API returns a status field, which can be completed, failed, in_progress, cancelled, queued, or incomplete [4]. - Incomplete Responses: When status is incomplete, the incomplete_details object provides a reason (such as max_output_tokens or content_filter), which explains why the output is unfinished [4][1]. - SDK Parsing Considerations: - If you use the SDK's auto-parsing features (e.g., passing a Pydantic model to text_format), it may attempt to parse the response before the terminal status is confirmed [5]. - In cases of status: "incomplete", attempting to parse the output as JSON can lead to validation errors because the output is truncated [5][6]. - It is recommended to verify that response.status == "completed" before performing manual parsing, or to implement defensive handling for incomplete or null outputs in your stream processing logic [5][7]. Known Behaviors and Best Practices: - Always check the response.status before assuming the output is valid JSON, especially when using streaming [5]. - When working with strict: true and structured outputs, failure to satisfy the schema typically results in an incomplete status rather than malformed JSON, unless specific edge cases (such as combining certain tools like file_search with strict schemas) are encountered [8]. - If you encounter TypeError in streaming, ensure your handling logic defensively treats a null response.output as an empty list [7].
Citations:
- 1: https://developers.openai.com/api/docs/guides/structured-outputs
- 2: https://developers.openai.com/cookbook/examples/structured_outputs_intro
- 3: https://developers.openai.com/api/docs/guides/structured-outputs.md
- 4: https://github.com/openai/openai-python/blob/5e8f09c2/src/openai/types/responses/response.py
- 5: Responses streaming structured output parses incomplete JSON before terminal incomplete status openai/openai-python#3263
- 6: Incomplete responses are not handled by
parse_responsewhen using Structured Output openai/openai-python#2486 - 7: responses.stream: parse_response crashes with TypeError when response.output is None openai/openai-python#3459
- 8: https://community.openai.com/t/responses-api-strict-json-schema-returns-malformed-json-when-combined-with-file-search-include-file-search-call-results/1380608
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- complete output schema ---'
sed -n '218,330p' scripts/release-notes-ai-generator/ai_client.py
printf '%s\n' '--- OpenAI SDK response_text implementation from the pinned source reference ---'
curl -fsSL \
https://raw.githubusercontent.com/openai/openai-python/5e8f09c2/src/openai/resources/responses/responses.py \
| rg -n -A25 -B8 'def output_text|output_text'Repository: pingcap/docs
Length of output: 18379
🌐 Web query:
site:developers.openai.com/api/docs/guides/structured-outputs Responses API text format json_schema additionalProperties required
💡 Result:
In the context of JSON Schema as used in modern structured API responses (such as OpenAI's Structured Outputs), additionalProperties and required are two distinct keywords that define the structure and validation of object types [1]. 1. required: This keyword is an array of strings that explicitly lists the keys that must be present in a JSON object [1]. If a specified key is missing from the input or output, the schema validation will fail. 2. additionalProperties: This keyword defines whether the object is allowed to contain keys that are not explicitly defined in the properties object [1]. - When set to false, it prohibits any properties other than those listed in the properties keyword. - For specific API implementations, such as OpenAI's Structured Outputs, setting additionalProperties: false is a mandatory requirement to ensure the model only generates the fields you have strictly defined [1]. In practice, when defining a schema for structured API responses, you should define your object structure using properties, list the essential fields in required, and set additionalProperties to false to enforce strict schema adherence [1].
Citations:
🌐 Web query:
site:github.com/openai/openai-python "class Response" "output_text" incomplete_details
💡 Result:
In the context of the OpenAI Python SDK, specifically within the Responses API (openai.types.responses), the terms Response, output_text, and incomplete_details refer to key components of the API's structured response model [1][2][3]. Response Class The Response class is a Pydantic BaseModel that serves as the top-level container for API responses [1][4]. It includes several properties that define the outcome of a model generation request, such as id, created_at, model, output, and incomplete_details [1][3]. incomplete_details The incomplete_details property is an optional field of type IncompleteDetails that provides information on why a response may be incomplete [1][3]. It typically contains a reason field, which can be one of several values, such as max_output_tokens or content_filter [3]. output_text Property The Response class often includes a convenience property named output_text [5]. This property is designed to aggregate all output_text items found within the output list of the response [5]. Technical Note and Issues - Usage: When accessing generated content, it is often recommended to use the output_text convenience property rather than manually indexing the output array, as the latter may depend on the model's specific response structure [1][5]. - Known Issues: Historically, issues have been reported where the output_text property could raise a TypeError if the API returned null values for the text field within an output_text content item [6]. Developers have implemented fixes to ensure that the output_text property safely ignores or handles null content text, ensuring the returned value remains a string [6]. If no text content is available, the property typically returns an empty string [5].
Citations:
- 1: https://github.com/openai/openai-python/blob/main/src/openai/types/responses/response.py
- 2: https://github.com/openai/openai-python/blob/main/src/openai/types/responses/response_output_text_param.py
- 3: https://github.com/openai/openai-python/blob/6ba31bcb/src/openai/types/responses/response.py
- 4: https://github.com/openai/openai-python/blob/f16fbbd2/src/openai/types/responses/response.py
- 5: https://github.com/openai/openai-python/blob/5e8f09c2/src/openai/types/responses/response.py
- 6: Response.output_text fails to handle null text values in content items openai/openai-python#3011
🏁 Script executed:
#!/bin/bash
set -eu
base=https://raw.githubusercontent.com/openai/openai-python/v1.66.0/src/openai
printf '%s\n' '--- Responses API request parameters ---'
curl -fsSL "$base/types/responses/response_create_params.py" \
| rg -n -A35 -B10 'text:|TextConfig|ResponseTextConfig|format'
printf '%s\n' '--- Response output_text implementation ---'
curl -fsSL "$base/types/responses/response.py" \
| rg -n -A25 -B10 'def output_text|incomplete_details|class Response'Repository: pingcap/docs
Length of output: 9445
🌐 Web query:
site:learn.microsoft.com Azure OpenAI Responses API structured outputs text format json_schema incomplete
💡 Result:
When using structured outputs in Azure OpenAI, the json_schema must be fully defined and compliant with the requirements of the API to avoid errors. If you encounter issues where the output or schema appears incomplete, consider the following technical constraints and troubleshooting steps grounded in official Azure OpenAI documentation: 1. API Surface Discrepancies: Ensure you are using the correct property for your API. The Chat Completions API requires the schema to be defined within response_format, whereas the Responses API requires the schema to be defined under text.format [1][2]. Mixing these or using incorrect structures will cause the API to reject the request [3]. 2. Model and API Version Support: Structured outputs are not supported on all model versions or deployment configurations [3]. Ensure your deployment is using a supported model (e.g., gpt-4o, gpt-4o-mini) and an API version that supports structured outputs (e.g., 2024-08-01-preview or newer GA versions) [3]. 3. Incomplete JSON Output ("finish_reason": "length"): If the JSON output is truncated or "incomplete," this is frequently caused by the max_tokens limit [4][5]. If the finish_reason in the response is length, the model stopped because it reached the token limit [5]. You must increase max_tokens or reduce the prompt length to allow the model to complete the full JSON structure [5][6]. 4. Schema Strictness: When using strict: true, the model enforces strict adherence to your provided JSON Schema [1]. If your schema definition is overly complex, missing required fields, or uses unsupported JSON Schema features, the API may return a 400 error or fail to produce the expected output. Always validate your schema against the official JSON Schema specification [1]. 5. Avoid "JSON Mode" Confusion: Ensure you are not confusing "JSON mode" with "Structured Outputs" [5]. JSON mode (type: "json_object") only guarantees valid JSON but does not enforce a specific schema [1][5]. If you require schema guarantees, you must use type: "json_schema" [1][7]. If you are receiving a specific 400 error code, check the error details in the response body, as they often pinpoint which part of the schema or request object is considered invalid or incomplete [3].
Citations:
- 1: https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/structured-outputs
- 2: https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/responses
- 3: https://learn.microsoft.com/en-us/answers/questions/5899259/json-schema-not-supported-in-gpt-chat-latest-2026
- 4: https://learn.microsoft.com/en-us/dotnet/api/azure.ai.inference.chatcompletionsoptions.responseformat?view=azure-dotnet-preview
- 5: https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/json-mode
- 6: https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/json-mode
- 7: https://learn.microsoft.com/rest/api/aifoundry/project/responses
Pass output_schema to the Responses API and reject incomplete responses.
AzureOpenAIClient._run ignores output_schema. For deployments that support Structured Outputs, pass it through text.format with type: "json_schema" and strict: True. Check response.status before reading output_text, because an incomplete response can contain partial or no text. Include response.incomplete_details.reason in the error. Rename _output_schema to output_schema.
Source: Linters/SAST tools
| "--ai-timeout", | ||
| type=int, | ||
| default=600, | ||
| help="Timeout in seconds for each AI command invocation.", |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
According to the official Python documentation, what happens when subprocess.run() receives zero or a negative timeout value?
💡 Result:
The official Python documentation for the subprocess module defines the timeout parameter as a duration in seconds [1][2]. While the documentation notes that users should be aware of behavior related to extremely small timeout values [1][3], it does not explicitly document the specific behavior of subprocess.run when provided with a zero or negative timeout value. In practice, the behavior is inconsistent across different environments and versions. Internally, subprocess.run passes the timeout to Popen.communicate [1][4]. Historical technical discussions and issue trackers indicate that: 1. On POSIX-compliant systems, a negative timeout value has historically been treated as zero, resulting in an immediate check for process completion followed by a TimeoutExpired exception if the process is still running [5]. 2. In other environments, such as on Windows, negative values have been treated as unsigned integers, leading to unintended, excessively long waits [5]. Because the official documentation does not explicitly define or guarantee behavior for non-positive timeouts, supplying zero or negative values is not recommended and may result in platform-dependent behavior or future errors [1][6].
Citations:
- 1: https://docs.python.org/3/library/subprocess.html
- 2: https://docs.python.org/3.11/library/subprocess.html
- 3: https://github.com/python/cpython/blob/master/Doc/library/subprocess.rst
- 4: https://github.com/python/cpython/blob/refs/heads/main/Lib/subprocess.py
- 5: https://bugs.python.org/msg413496
- 6: gh-133089: Use original timeout value for
TimeoutExpiredwhen the funcsubprocess.runis called with a timeout python/cpython#133103
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- target file ---'
sed -n '95,145p' scripts/release-notes-ai-generator/cli.py
sed -n '225,265p' scripts/release-notes-ai-generator/cli.py
printf '%s\n' '--- timeout and client call sites ---'
rg -n -C 4 'ai_timeout|CodexAIClient|AzureOpenAIClient|subprocess\.(run|Popen)|communicate\(' scripts/release-notes-ai-generatorRepository: pingcap/docs
Length of output: 8388
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- client implementations ---'
sed -n '103,205p' scripts/release-notes-ai-generator/ai_client.py
printf '%s\n' '--- validation helper and callers ---'
rg -n -C 5 'def validate_positive_int|validate_positive_int\(' scripts/release-notes-ai-generator
printf '%s\n' '--- standalone subprocess timeout probe ---'
python3 - <<'PY'
import subprocess
import sys
child = [sys.executable, "-c", "import time; time.sleep(0.2)"]
for timeout in (0, -1):
try:
subprocess.run(child, timeout=timeout, check=True)
except BaseException as exc:
print(f"timeout={timeout}: {type(exc).__name__}: {exc}")
else:
print(f"timeout={timeout}: completed")
PYRepository: pingcap/docs
Length of output: 6741
Reject non-positive AI timeouts before creating the AI client.
When AI generation is enabled, call validate_positive_int("--ai-timeout", args.ai_timeout) before creating AzureOpenAIClient or CodexAIClient. A zero or negative value causes a running Codex subprocess to raise TimeoutExpired immediately.
| parser.add_argument( | ||
| "--output-release-file", | ||
| help=( | ||
| "Output Markdown file. Defaults to release-{version}-updated-by-ai.md. " | ||
| "The default never writes the canonical release-{version}.md, because " | ||
| "the generator only produces Improvements and Bug fixes, not a complete " | ||
| "release note." | ||
| ), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Restore the required default output-file selection.
The current implementation always writes release-<version>-updated-by-ai.md. The PR contract requires release-<version>.md when that file does not exist, and the AI-suffixed file only when it already exists. Update the help text and scripts/release-notes-ai-generator/release-notes-generator-readme.md Lines 149 and 163 to match the corrected behavior.
Proposed fix
def default_output_release_file(releases_dir: Path, version: str) -> Path:
- # Always write to the "-updated-by-ai" name, never the canonical
- # release-<version>.md. The generator only produces Improvements and Bug
- # fixes, not a complete formal release note, so the default output must not
- # be mistaken for the official file. This name is also skipped by the
- # historical-note scanner, so a re-run never treats the draft as published.
- return releases_dir / f"release-{version}-updated-by-ai.md"
+ canonical_file = releases_dir / f"release-{version}.md"
+ if canonical_file.exists():
+ return releases_dir / f"release-{version}-updated-by-ai.md"
+ return canonical_fileAlso applies to: 402-408
| def restore_row(sheet: Any, row_number: int, snapshot: dict[str, Any]) -> None: | ||
| row_dimension = sheet.row_dimensions[row_number] | ||
| row_dimension.height = snapshot["height"] | ||
| row_dimension.hidden = snapshot["hidden"] | ||
| row_dimension.outlineLevel = snapshot["outline_level"] | ||
| row_dimension.collapsed = snapshot["collapsed"] | ||
| for column, cell_snapshot in enumerate(snapshot["cells"], start=1): | ||
| cell = sheet.cell(row=row_number, column=column) | ||
| cell.value = cell_snapshot["value"] | ||
| cell._style = copy.copy(cell_snapshot["style"]) | ||
| cell.number_format = cell_snapshot["number_format"] | ||
| cell._hyperlink = copy.copy(cell_snapshot["hyperlink"]) if cell_snapshot["hyperlink"] else None | ||
| cell.comment = copy.copy(cell_snapshot["comment"]) if cell_snapshot["comment"] else None |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
openpyxl 3.1 Cell.hyperlink setter sets Hyperlink ref to cell coordinate
💡 Result:
Yes, the openpyxl 3.1 Cell.hyperlink setter does automatically set the Hyperlink reference (ref) to the cell's coordinate [1][2][3]. When you assign a value (such as a string URL) to the cell's hyperlink property, the setter logic performs the following steps: 1. It checks if the value is already an instance of the Hyperlink class [1][2]. 2. If it is not, it creates a new Hyperlink object, initializing it with the provided target [1][2]. 3. It then explicitly sets the Hyperlink object's ref attribute to the current cell's coordinate (e.g., 'A1') using self.coordinate [1][2][3]. This design ensures that the hyperlink object is correctly associated with the cell's position in the worksheet [1][2]. If you set the hyperlink property to None, the hyperlink is removed from the cell [1][2][3]. Top results: [1][2][3]
Citations:
- 1: https://openpyxl.readthedocs.io/en/stable/_modules/openpyxl/cell/cell.html
- 2: https://openpyxl.readthedocs.io/en/3.1/%5Fmodules/openpyxl/cell/cell.html
- 3: https://openpyxl.readthedocs.io/en/3.1.0/_modules/openpyxl/cell/cell.html
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- target file map ---'
ast-grep outline scripts/release-notes-ai-generator/excel_workbook.py
printf '%s\n' '--- relevant implementation ---'
sed -n '1,230p' scripts/release-notes-ai-generator/excel_workbook.py
printf '%s\n' '--- dependency declarations ---'
rg -n -i 'openpyxl|hyperlink|snapshot_cell|restore_row|sort' \
--glob '!*node_modules*' --glob '!dist/**' --glob '!build/**' .Repository: pingcap/docs
Length of output: 50369
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- release-note script files ---'
git ls-files scripts/release-notes-ai-generator | sed -n '1,120p'
printf '%s\n' '--- openpyxl references in the script area ---'
rg -n -i 'openpyxl|sort_sheet_rows_by_component|snapshot_row|restore_row|snapshot_cell' scripts/release-notes-ai-generator
printf '%s\n' '--- sort callers ---'
rg -n -C 4 'sort_sheet_rows_by_component\(' scripts/release-notes-ai-generator
printf '%s\n' '--- installed openpyxl behavior and source ---'
python3 - <<'PY'
try:
import inspect
import openpyxl
from openpyxl.cell.cell import Cell
from openpyxl.cell import hyperlink as hyperlink_module
from openpyxl.worksheet import _writer
print("version:", openpyxl.__version__)
print("Cell.hyperlink setter:")
print(inspect.getsource(Cell.hyperlink.fset))
print("worksheet hyperlink writer references:")
source = inspect.getsource(_writer)
for i, line in enumerate(source.splitlines(), 1):
if "hyperlink" in line.lower():
print(f"{i}: {line}")
print("Hyperlink class:")
print(inspect.getsource(hyperlink_module.Hyperlink))
except Exception as exc:
print(type(exc).__name__ + ":", exc)
PYRepository: pingcap/docs
Length of output: 4775
🌐 Web query:
openpyxl 3.1 worksheet _writer hyperlink ref cell.coordinate source
💡 Result:
In openpyxl 3.1, the relationship between a cell's hyperlink and its coordinate is established during the cell's hyperlink property assignment [1][2][3]. When you set a hyperlink on a cell (e.g., cell.hyperlink = "http://example.com"), the following occurs: 1. Cell Coordinate Assignment: The Cell.hyperlink setter automatically creates a Hyperlink object if one is not provided [1][3]. It then assigns the current cell's coordinate (e.g., 'A5') to the ref attribute of that Hyperlink instance using val.ref = self.coordinate [1][2][3]. 2. Hyperlink Structure: The Hyperlink class, defined in openpyxl.worksheet.hyperlink, uses the ref attribute to store the cell reference as a string [4][5]. 3. Writing Process: When the worksheet is saved, the _writer (or worksheet._writer) processes these hyperlinks [6]. It iterates through the worksheet's hyperlinks and writes them into the XML structure as a HyperlinkList, which contains the ref (the cell coordinate) and, if applicable, the relationship ID for the target [7][6]. In summary, the cell.coordinate property is derived from the cell's row and column attributes [1][2], and this string is explicitly injected into the Hyperlink.ref attribute during the setter process to ensure the XML output correctly maps the link to the specific cell [1][3].
Citations:
- 1: https://openpyxl.readthedocs.io/en/3.1/_modules/openpyxl/cell/cell.html
- 2: https://openpyxl.readthedocs.io/en/3.1.2/_modules/openpyxl/cell/cell.html
- 3: https://openpyxl.readthedocs.io/en/stable/_modules/openpyxl/cell/cell.html
- 4: https://openpyxl.readthedocs.io/en/3.1/_modules/openpyxl/worksheet/hyperlink.html
- 5: https://openpyxl.readthedocs.io/en/stable/api/openpyxl.worksheet.hyperlink.html
- 6: https://github.com/soxhub/openpyxl/blob/ec884e35/openpyxl/worksheet/_writer.py
- 7: https://openpyxl.readthedocs.io/en/2.5/%5Fmodules/openpyxl/writer/worksheet.html
🌐 Web query:
openpyxl 3.1.5 writer.py write_hyperlinks Hyperlink.ref source
💡 Result:
In openpyxl 3.1.5, the Hyperlink.ref attribute defines the cell range or single cell reference (e.g., 'A1' or 'A1:B2') to which the hyperlink is applied [1][2]. It is defined as a String type within the openpyxl.worksheet.hyperlink.Hyperlink class [2][3]. During the saving process, the write_hyperlinks method (found in the internal worksheet writer logic) processes the worksheet's collection of hyperlinks [4]. Specifically: 1. It iterates through the links attached to the worksheet (_hyperlinks) [4]. 2. For each link with a defined target, it creates an external relationship and assigns a relationship ID to the Hyperlink.id attribute [4]. 3. Finally, it uses the HyperlinkList class to convert the list of hyperlink objects into an XML tree structure for serialization, which includes the ref attribute as part of the tag [4][2].
Citations:
- 1: https://openpyxl.pages.heptapod.net/openpyxl/api/openpyxl.worksheet.hyperlink.html
- 2: https://openpyxl.readthedocs.io/en/3.1.0/_modules/openpyxl/worksheet/hyperlink.html
- 3: https://openpyxl.readthedocs.io/en/stable/_modules/openpyxl/worksheet/hyperlink.html
- 4: https://github.com/soxhub/openpyxl/blob/ec884e35/openpyxl/worksheet/_writer.py
Restore hyperlinks through the public cell.hyperlink setter.
sort_sheet_rows_by_component restores rows at new coordinates. Assigning cell._hyperlink preserves the old Hyperlink.ref, which can serialize the hyperlink against the wrong cell.
🧰 Tools
🪛 Ruff (0.16.1)
[warning] 172-172: Dynamically typed expressions (typing.Any) are disallowed in sheet
(ANN401)
| --excel </path/to/release-note-excel.xlsx> \ | ||
| --releases-dir </path/to/releases-folder> \ | ||
| --ai-provider azure | ||
| ``` | ||
|
|
||
| Use Codex CLI: | ||
|
|
||
| ```bash | ||
| cd scripts | ||
| python3 -m release-notes-ai-generator generate \ | ||
| --version 8.5.7 \ | ||
| --excel /path/to/release-note-excel.xlsx \ | ||
| --releases-dir </path/to/releases-folder> | ||
| ``` | ||
|
|
||
| ### Phase 2: Export Markdown from processed Excel | ||
|
|
||
| After Phase 1 is fully complete, export the Markdown: | ||
|
|
||
| ```bash | ||
| cd scripts | ||
| python3 -m release-notes-ai-generator export-markdown \ | ||
| --version <tidb-version, for example 8.5.7> \ | ||
| --excel </path/to/release-note-excel_processed.xlsx> \ | ||
| --releases-dir </path/to/releases-folder> \ | ||
| --release-date "<release date>" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use shell-safe placeholders in both command examples.
Line 89, Line 90, and Lines 111-114 use <...> placeholders. POSIX shells interpret these tokens as input redirections, so the copied commands fail before Python receives the arguments. Replace them with shell-safe literal examples such as /path/to/release-note-excel.xlsx and 8.5.7.
A committable suggestion is not suitable because the required replacements span noncontiguous command blocks. As per path instructions, a regular review comment is appropriate when a fix spans noncontiguous lines.
Source: Path instructions
| openpyxl>=3.1 | ||
| openai>=1.66 | ||
| requests>=2.31 | ||
| urllib3>=1.26 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n --hidden \
-g 'requirements*.txt' \
-g 'pyproject.toml' \
-g 'poetry.lock' \
-g 'uv.lock' \
-g 'Pipfile.lock' \
'urllib3' .
curl -sS https://api.osv.dev/v1/query \
-H 'content-type: application/json' \
--data '{"package":{"ecosystem":"PyPI","name":"urllib3"}}' |
jq '.vulns[] | select(.aliases[]? == "GHSA-g4mx-q9vg-27p4")'Repository: pingcap/docs
Length of output: 5228
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- requirements.txt ---'
cat -n scripts/release-notes-ai-generator/requirements.txt
printf '%s\n' '--- urllib3 usage ---'
rg -n --hidden -g '!node_modules' -g '!dist' -g '!build' \
'urllib3|requests|httpx|pip install|requirements\.txt' \
scripts/release-notes-ai-generator
printf '%s\n' '--- specifier behavior probe ---'
python3 - <<'PY'
from packaging.specifiers import SpecifierSet
from packaging.version import Version
for spec in ("urllib3>=1.26", "urllib3>=1.26.18"):
allowed_affected = [
str(v) for v in [Version("1.26.17"), Version("2.0.0"), Version("2.0.6"), Version("2.0.7")]
if Version(str(v)) in SpecifierSet(spec.removeprefix("urllib3"))
]
print(spec, "matches:", allowed_affected)
PYRepository: pingcap/docs
Length of output: 3515
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- github_client.py ---'
cat -n scripts/release-notes-ai-generator/github_client.py
printf '%s\n' '--- urllib3 requirement matches ---'
python3 - <<'PY'
from functools import total_ordering
`@total_ordering`
class V:
def __init__(self, text):
self.parts = tuple(int(x) for x in text.split("."))
def __eq__(self, other):
return self.parts == other.parts
def __lt__(self, other):
return self.parts < other.parts
def __str__(self):
return ".".join(map(str, self.parts))
for minimum in ("1.26", "1.26.18"):
print(f">={minimum}:")
for candidate in ("1.26.17", "1.26.18", "2.0.0", "2.0.6", "2.0.7"):
print(f" {candidate}: {V(candidate) >= V(minimum)}")
PY
printf '%s\n' '--- request method calls ---'
rg -n -C 2 \
'session\.(get|post|put|patch|delete|request)\(|requests\.(get|post|put|patch|delete|request)\(' \
scripts/release-notes-ai-generatorRepository: pingcap/docs
Length of output: 15560
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- get_session call graph ---'
rg -n --hidden -g '!node_modules' -g '!dist' -g '!build' \
'get_session|\.request\(|\.post\(|\.put\(|\.patch\(|\.delete\(' \
scripts/release-notes-ai-generator
printf '%s\n' '--- runtime and dependency context ---'
rg -n -C 2 \
'Python|python3|requests|urllib3|openai' \
scripts/release-notes-ai-generator/release-notes-generator-readme.md \
scripts/release-notes-ai-generator/requirements.txtRepository: pingcap/docs
Length of output: 5600
Exclude all affected urllib3 releases.
urllib3>=1.26.18 still permits vulnerable versions 2.0.0 through 2.0.6. Use the existing 1.x line unless the supported runtime allows urllib3>=2.0.7.
Proposed fix
-urllib3>=1.26
+urllib3>=1.26.18,<2.0📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| urllib3>=1.26 | |
| urllib3>=1.26.18,<2.0 |
Source: MCP tools
| def find_header_column(sheet: Any, header_name: str) -> int | None: | ||
| for column in range(1, sheet.max_column + 1): | ||
| if str_value(sheet.cell(row=1, column=column).value) == header_name: | ||
| return column | ||
| return None | ||
|
|
||
|
|
||
| def ensure_reason_header(source_sheet: Any, target_sheet: Any) -> int: | ||
| reason_col = find_header_column(target_sheet, REASON_HEADER) | ||
| if not reason_col: | ||
| reason_col = max(source_sheet.max_column, target_sheet.max_column) + 1 | ||
| copy_missing_header_cells(source_sheet, target_sheet) | ||
| target_sheet.cell(row=1, column=reason_col, value=REASON_HEADER) | ||
| return reason_col | ||
|
|
||
| while reason_col <= source_sheet.max_column: | ||
| target_sheet.insert_cols(reason_col) | ||
| reason_col += 1 | ||
|
|
||
| copy_missing_header_cells(source_sheet, target_sheet) | ||
| return reason_col | ||
|
|
||
|
|
||
| def copy_missing_header_cells(source_sheet: Any, target_sheet: Any) -> None: | ||
| for column in range(1, source_sheet.max_column + 1): | ||
| if not str_value(target_sheet.cell(row=1, column=column).value): | ||
| copy_cell( | ||
| source_sheet.cell(row=1, column=column), | ||
| target_sheet.cell(row=1, column=column), | ||
| ) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Duplicated header and version helpers across two modules. scope_filter.py and excel_workbook.py each define near-identical find_header_column, copy_missing_header_cells, reason-header, and semver-parsing helpers. The copies already diverge, which is the root cause of the positional row-copy defect reported at scripts/release-notes-ai-generator/scope_filter.py#L171-L186. Extract one implementation into utils.py and pass the reason header name as a parameter.
scripts/release-notes-ai-generator/scope_filter.py#L139-L168: removefind_header_column,ensure_reason_header, andcopy_missing_header_cells, and import the shared helpers withREASON_HEADERpassed as an argument.scripts/release-notes-ai-generator/excel_workbook.py#L535-L564: removeensure_same_series_reason_header,copy_missing_header_cells, andfind_header_column, and call the shared helpers withSAME_SERIES_REASON_HEADER.scripts/release-notes-ai-generator/scope_filter.py#L405-L413: dropparse_versionand buildVersionfrom the shared semver parser.scripts/release-notes-ai-generator/excel_workbook.py#L266-L274: moveparse_semver_tupleintoutils.pyas the single semver parser and import it here.
As per coding guidelines: "Reuse existing wording, structure, terminology, templates, scripts, workflows, and repository patterns whenever possible."
🧰 Tools
🪛 Ruff (0.16.1)
[warning] 139-139: Dynamically typed expressions (typing.Any) are disallowed in sheet
(ANN401)
[warning] 146-146: Dynamically typed expressions (typing.Any) are disallowed in source_sheet
(ANN401)
[warning] 146-146: Dynamically typed expressions (typing.Any) are disallowed in target_sheet
(ANN401)
[warning] 162-162: Dynamically typed expressions (typing.Any) are disallowed in source_sheet
(ANN401)
[warning] 162-162: Dynamically typed expressions (typing.Any) are disallowed in target_sheet
(ANN401)
📍 Affects 2 files
scripts/release-notes-ai-generator/scope_filter.py#L139-L168(this comment)scripts/release-notes-ai-generator/excel_workbook.py#L535-L564scripts/release-notes-ai-generator/scope_filter.py#L405-L413scripts/release-notes-ai-generator/excel_workbook.py#L266-L274
Source: Coding guidelines
| def append_row_with_reason(source_sheet: Any, target_sheet: Any, row_number: int, reason: str) -> None: | ||
| reason_col = ensure_reason_header(source_sheet, target_sheet) | ||
| target_row = target_sheet.max_row + 1 | ||
| source_dimension = source_sheet.row_dimensions[row_number] | ||
| target_dimension = target_sheet.row_dimensions[target_row] | ||
| target_dimension.height = source_dimension.height | ||
| target_dimension.hidden = source_dimension.hidden | ||
| target_dimension.outlineLevel = source_dimension.outlineLevel | ||
| target_dimension.collapsed = source_dimension.collapsed | ||
|
|
||
| for column in range(1, source_sheet.max_column + 1): | ||
| copy_cell( | ||
| source_sheet.cell(row=row_number, column=column), | ||
| target_sheet.cell(row=target_row, column=column), | ||
| ) | ||
| target_sheet.cell(row=target_row, column=reason_col, value=reason) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Copy cells by header name, not by column index.
ensure_out_of_scope_sheet accepts an existing PRs_not_in_scope sheet, and ensure_reason_header can insert columns into it. Positional copying then writes source values under the wrong target headers, and the archive sheet silently misreports data. excel_workbook.move_not_needed_rows_to_sheet (Lines 411-423) already maps columns by header name for the same task. Use the same mapping here.
🐛 Map columns by header name
- for column in range(1, source_sheet.max_column + 1):
- copy_cell(
- source_sheet.cell(row=row_number, column=column),
- target_sheet.cell(row=target_row, column=column),
- )
+ target_header = get_header(target_sheet)
+ for name, source_column in get_header(source_sheet).items():
+ target_column = target_header.get(name)
+ if not target_column:
+ continue
+ copy_cell(
+ source_sheet.cell(row=row_number, column=source_column),
+ target_sheet.cell(row=target_row, column=target_column),
+ )Reuse of the existing repository pattern is required here. As per coding guidelines: "Reuse existing wording, structure, terminology, templates, scripts, workflows, and repository patterns whenever possible."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def append_row_with_reason(source_sheet: Any, target_sheet: Any, row_number: int, reason: str) -> None: | |
| reason_col = ensure_reason_header(source_sheet, target_sheet) | |
| target_row = target_sheet.max_row + 1 | |
| source_dimension = source_sheet.row_dimensions[row_number] | |
| target_dimension = target_sheet.row_dimensions[target_row] | |
| target_dimension.height = source_dimension.height | |
| target_dimension.hidden = source_dimension.hidden | |
| target_dimension.outlineLevel = source_dimension.outlineLevel | |
| target_dimension.collapsed = source_dimension.collapsed | |
| for column in range(1, source_sheet.max_column + 1): | |
| copy_cell( | |
| source_sheet.cell(row=row_number, column=column), | |
| target_sheet.cell(row=target_row, column=column), | |
| ) | |
| target_sheet.cell(row=target_row, column=reason_col, value=reason) | |
| def append_row_with_reason(source_sheet: Any, target_sheet: Any, row_number: int, reason: str) -> None: | |
| reason_col = ensure_reason_header(source_sheet, target_sheet) | |
| target_row = target_sheet.max_row + 1 | |
| source_dimension = source_sheet.row_dimensions[row_number] | |
| target_dimension = target_sheet.row_dimensions[target_row] | |
| target_dimension.height = source_dimension.height | |
| target_dimension.hidden = source_dimension.hidden | |
| target_dimension.outlineLevel = source_dimension.outlineLevel | |
| target_dimension.collapsed = source_dimension.collapsed | |
| target_header = get_header(target_sheet) | |
| for name, source_column in get_header(source_sheet).items(): | |
| target_column = target_header.get(name) | |
| if not target_column: | |
| continue | |
| copy_cell( | |
| source_sheet.cell(row=row_number, column=source_column), | |
| target_sheet.cell(row=target_row, column=target_column), | |
| ) | |
| target_sheet.cell(row=target_row, column=reason_col, value=reason) |
🧰 Tools
🪛 Ruff (0.16.1)
[warning] 171-171: Dynamically typed expressions (typing.Any) are disallowed in source_sheet
(ANN401)
[warning] 171-171: Dynamically typed expressions (typing.Any) are disallowed in target_sheet
(ANN401)
Source: Coding guidelines
| from __future__ import annotations | ||
|
|
||
| import copy | ||
| from typing import Any, Iterable |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ruff check scripts/release-notes-ai-generator/utils.py --select UP035Repository: pingcap/docs
Length of output: 1129
Import Iterable from collections.abc.
Ruff reports UP035 for this import. Keep Any in typing.
Proposed fix
-from typing import Any, Iterable
+from collections.abc import Iterable
+from typing import Any📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| from typing import Any, Iterable | |
| from collections.abc import Iterable | |
| from typing import Any |
🧰 Tools
🪛 Ruff (0.16.1)
[warning] 4-4: Import from collections.abc instead: Iterable
Import from collections.abc
(UP035)
Source: Linters/SAST tools
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: a7ce6cf7-7a91-433e-938a-6bea1264a09e
📒 Files selected for processing (5)
scripts/release-notes-ai-generator/__main__.pyscripts/release-notes-ai-generator/cli.pyscripts/release-notes-ai-generator/excel_workbook.pyscripts/release-notes-ai-generator/release-notes-generator-readme.mdscripts/release-notes-ai-generator/tests/test_doc_impact.py
| def set_doc_impact_column_width(sheet: Any, header: dict[str, int]) -> None: | ||
| column_letter = get_column_letter(header[DOC_IMPACT_HEADER]) | ||
| sheet.column_dimensions[column_letter].width = DOC_IMPACT_COLUMN_WIDTH |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(pyproject\.toml|requirements[^/]*|[^/]*requirements[^/]*|setup\.cfg|tox\.ini|ruff[^/]*|excel_workbook\.py|test_doc_impact\.py)$' || true
printf '%s\n' '--- helper and imports ---'
file=$(git ls-files | rg '(^|/)scripts/release-notes-ai-generator/excel_workbook\.py$' | head -n 1)
if [ -n "$file" ]; then
nl -ba "$file" | sed -n '1,130p'
fi
printf '%s\n' '--- dependency and Ruff references ---'
rg -n -i -C 3 'openpyxl|ANN401|flake8-annotations|ruff' \
pyproject.toml setup.cfg tox.ini .ruff.toml ruff.toml requirements\*.txt 2>/dev/null || true
printf '%s\n' '--- relevant tests ---'
tests=$(git ls-files | rg '(^|/)test_doc_impact\.py$')
for file in $tests; do
nl -ba "$file"
doneRepository: pingcap/docs
Length of output: 634
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- source ---'
sed -n '1,130p' scripts/release-notes-ai-generator/excel_workbook.py
printf '%s\n' '--- requirements ---'
cat scripts/release-notes-ai-generator/requirements.txt
printf '%s\n' '--- tests ---'
sed -n '1,260p' scripts/release-notes-ai-generator/tests/test_doc_impact.py
printf '%s\n' '--- repository Ruff and annotation configuration ---'
git ls-files | while IFS= read -r file; do
case "$file" in
*.toml|*.cfg|*.ini|*.yaml|*.yml|*.txt|*.md)
grep -n -H -E 'ANN401|flake8-annotations|ruff|openpyxl' "$file" 2>/dev/null || true
;;
esac
doneRepository: pingcap/docs
Length of output: 12668
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
from importlib import metadata, util
dist_name = "openpyxl"
try:
print("installed_version:", metadata.version(dist_name))
except metadata.PackageNotFoundError:
print("installed_version: not-installed")
spec = util.find_spec("openpyxl.worksheet.worksheet")
print("worksheet_module_spec:", spec.origin if spec else "not-found")
if spec:
import openpyxl.worksheet.worksheet as worksheet
print("Worksheet_exported:", hasattr(worksheet, "Worksheet"))
if hasattr(worksheet, "Worksheet"):
print("Worksheet_module:", worksheet.Worksheet.__module__)
PY
printf '%s\n' '--- tracked workflow/config references ---'
git ls-files | rg '(^|/)(\.github/|.*(toml|cfg|ini|yaml|yml|json)$)' | while IFS= read -r file; do
grep -n -H -E 'ruff|ANN401|flake8-annotations|requirements\.txt' "$file" 2>/dev/null || true
doneRepository: pingcap/docs
Length of output: 435
🌐 Web query:
openpyxl 3.1.0 openpyxl.worksheet.worksheet Worksheet official documentation
💡 Result:
The official documentation for the Worksheet class in openpyxl 3.1.0 is located at the following URL: https://openpyxl.readthedocs.io/en/3.1.0/api/openpyxl.worksheet.worksheet.html [1]. In openpyxl, the Worksheet class (openpyxl.worksheet.worksheet.Worksheet) is the second-level container in an Excel workbook [1]. The documentation emphasizes that users should not instantiate the Worksheet class directly; instead, they should use the workbook's create_sheet method (openpyxl.workbook.Workbook.create_sheet) to generate new worksheets [1]. Key features and methods described in the documentation include: - Iteration: It provides methods such as iter_rows and iter_cols to iterate over cells in a specific range [1]. - Cell Manipulation: It includes support for methods like append, merge_cells, and unmerge_cells [1]. - Sheet Management: The class supports various worksheet properties and settings, including print configurations, dimension calculations, and cell data management [1]. The full API documentation for the openpyxl.worksheet package, which contains the Worksheet module, can be accessed at https://openpyxl.readthedocs.io/en/3.1.0/api/openpyxl.worksheet.html [2].
Citations:
- 1: https://openpyxl.readthedocs.io/en/3.1.0/api/openpyxl.worksheet.worksheet.html
- 2: https://openpyxl.readthedocs.io/en/3.1.0/api/openpyxl.worksheet.html
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- focused diff ---'
git diff -- scripts/release-notes-ai-generator/excel_workbook.py
printf '%s\n' '--- Ruff availability ---'
if command -v ruff >/dev/null 2>&1; then
ruff --version
else
echo 'ruff: not-installed'
fi
printf '%s\n' '--- Ruff invocations in tracked files ---'
git ls-files | while IFS= read -r file; do
case "$file" in
*.sh|*.bash|*.yml|*.yaml|*.toml|*.cfg|*.ini|*.md|Makefile|*.mk)
grep -n -H -E '(^|[^[:alnum:]_-])ruff([^[:alnum:]_-]|$)|ANN401' "$file" 2>/dev/null || true
;;
esac
doneRepository: pingcap/docs
Length of output: 251
🌐 Web query:
Ruff ANN401 any type annotation rule official documentation
💡 Result:
The Ruff rule ANN401, titled "any-type," is a linter rule derived from flake8-annotations that checks for the use of typing.Any in type annotations [1][2]. Official Documentation: The rule documentation can be found on the official Ruff website at: https://docs.astral.sh/ruff/rules/any-type/ [1] Key Details: - Purpose: It flags function arguments that are annotated with Any, encouraging developers to use more specific types instead [1]. - Rationale: Any acts as an unconstrained type, allowing all operations and bypassing type checking. Using explicit types is preferred for type safety, reserving Any only as a necessary "escape hatch" [1]. - Scope: In addition to direct usage (e.g., x: Any), the rule also detects Any within other type structures, such as Unions, Optional types, Annotated, and forward references [3]. - Known Limitation: Type aliases are currently unsupported by this rule and may cause false positives where the rule is triggered inadvertently [1]. The rule has been a stable part of the Ruff linter since version v0.0.108 [2][4].
Citations:
- 1: https://docs.astral.sh/ruff/rules/any-type/
- 2: https://docs.astral.sh/ruff/rules/
- 3: Check for
Anyin other types forANN401astral-sh/ruff#5601 - 4: https://docs.astral.sh/ruff/rules/?featured_on=talkpython
🏁 Script executed:
#!/bin/bash
set -eu
ruff check --isolated --select ANN401 scripts/release-notes-ai-generator/excel_workbook.pyRepository: pingcap/docs
Length of output: 30083
Use Worksheet for sheet.
Ruff flags sheet: Any with ANN401. Import Worksheet from openpyxl.worksheet.worksheet; it is available in the declared openpyxl>=3.1 dependency.
🧰 Tools
🪛 Ruff (0.16.1)
[warning] 96-96: Dynamically typed expressions (typing.Any) are disallowed in sheet
(ANN401)
Source: Linters/SAST tools
What is changed, added or deleted? (Required)
scripts/release_notes_generate_ai.py.scripts/release_notes_ai/package for workbook preprocessing, scope filtering, duplicate release note reuse, AI/non-AI generation, Markdown rendering, and GitHub API helpers.release-<version>.mdwhen the release note file does not exist, and torelease-<version>-updated-by-ai.mdwhen it already exists.Validation:
git diff --checkPYTHONPATH=scripts PYTHONDONTWRITEBYTECODE=1 python3 scripts/release_notes_generate_ai.py --helpPYTHONPATH=scripts PYTHONDONTWRITEBYTECODE=1 python3 - <<EOFcheck fordefault_output_release_fileWhich TiDB version(s) do your changes apply to? (Required)
What is the related PR or file link(s)?
Do your changes match any of the following descriptions?
Summary by CodeRabbit
New Features
Documentation