From 2755d5d0ab459606eeaa1172def50d4f6cdb9283 Mon Sep 17 00:00:00 2001 From: Pratik Mahalle Date: Sun, 22 Feb 2026 11:59:00 +0530 Subject: [PATCH] [chore] Auto-generate PR from changelog (#42123) Add GitHub workflow to parse .chloggen/*.yaml and populate PR title/body when title matches 'as per changelog'. --- .../workflows/generate-pr-from-changelog.yml | 40 ++++ .../scripts/generate-pr-from-changelog.sh | 215 ++++++++++++++++++ CONTRIBUTING.md | 5 +- 3 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/generate-pr-from-changelog.yml create mode 100755 .github/workflows/scripts/generate-pr-from-changelog.sh diff --git a/.github/workflows/generate-pr-from-changelog.yml b/.github/workflows/generate-pr-from-changelog.yml new file mode 100644 index 0000000000000..b8b7a0e4d7ca6 --- /dev/null +++ b/.github/workflows/generate-pr-from-changelog.yml @@ -0,0 +1,40 @@ +# This workflow generates a PR title and body from changelog entry YAML files +# when the PR title contains "as per changelog" (case-insensitive). +# +# This allows contributors to write their changelog entry first, then simply +# set the PR title to "as per changelog" to have the PR information auto-populated. +# +# See: https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/42123 + +name: 'Generate PR from changelog' +on: + pull_request_target: + types: + - opened + - edited + +permissions: read-all + +jobs: + generate-pr-from-changelog: + permissions: + pull-requests: write + contents: read + runs-on: ubuntu-24.04 + if: >- + github.actor != 'dependabot[bot]' + && github.repository_owner == 'open-telemetry' + && (contains(github.event.pull_request.title, 'as per changelog') + || contains(github.event.pull_request.title, 'as per chloggen')) + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Generate PR title and body from changelog + run: ./.github/workflows/scripts/generate-pr-from-changelog.sh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR: ${{ github.event.number }} + PR_HEAD: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/scripts/generate-pr-from-changelog.sh b/.github/workflows/scripts/generate-pr-from-changelog.sh new file mode 100755 index 0000000000000..af8660aa5bdf7 --- /dev/null +++ b/.github/workflows/scripts/generate-pr-from-changelog.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +# +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +# +# Generates a PR title and body from changelog entry YAML files +# when the PR title contains "as per changelog" (case-insensitive). +# +# Required environment variables: +# REPO - The GitHub repository (e.g. "open-telemetry/opentelemetry-collector-contrib") +# PR - The PR number +# PR_HEAD - The HEAD SHA of the PR + +set -euo pipefail + +if [[ -z "${REPO:-}" || -z "${PR:-}" || -z "${PR_HEAD:-}" ]]; then + echo "One or more of REPO, PR, and PR_HEAD have not been set, please ensure each is set." + exit 0 +fi + +# Map change_type values to human-readable prefixes +change_type_label() { + case "$1" in + breaking) echo "breaking change" ;; + deprecation) echo "deprecation" ;; + new_component) echo "new component" ;; + enhancement) echo "enhancement" ;; + bug_fix) echo "bug fix" ;; + *) echo "$1" ;; + esac +} + +main() { + # Find changelog YAML files added in this PR (excluding TEMPLATE.yaml, config.yaml) + ADDED_FILES=$(git diff --diff-filter=A --name-only "$(git merge-base origin/main "${PR_HEAD}")" "${PR_HEAD}" ./.chloggen/ \ + | grep -E '\.yaml$' \ + | grep -v 'TEMPLATE\.yaml' \ + | grep -v 'config\.yaml' \ + || true) + + if [[ -z "${ADDED_FILES}" ]]; then + echo "No changelog entry YAML files found in this PR. Cannot generate PR info from changelog." + exit 0 + fi + + echo "Found changelog entries:" + echo "${ADDED_FILES}" + + # Collect information from all changelog entries + COMPONENTS=() + NOTES=() + CHANGE_TYPES=() + ALL_ISSUES=() + SUBTEXTS=() + + for FILE in ${ADDED_FILES}; do + if [[ ! -f "${FILE}" ]]; then + echo "Warning: File ${FILE} not found, skipping." + continue + fi + + # Parse YAML fields using grep + sed (avoids requiring yq as a dependency) + # Extract change_type + CHANGE_TYPE=$(grep -E '^change_type:' "${FILE}" | head -1 | sed "s/^change_type:[[:space:]]*//" | sed "s/['\"]//g" | xargs) + + # Extract component + COMPONENT=$(grep -E '^component:' "${FILE}" | head -1 | sed "s/^component:[[:space:]]*//" | sed "s/['\"]//g" | xargs) + + # Extract note + NOTE=$(grep -E '^note:' "${FILE}" | head -1 | sed "s/^note:[[:space:]]*//" | sed "s/['\"]//g" | xargs) + + # Extract issues (format: issues: [1234] or issues: [1234, 5678]) + ISSUES=$(grep -E '^issues:' "${FILE}" | head -1 | sed "s/^issues:[[:space:]]*//" | tr -d '[]' | xargs) + + # Extract subtext (can be multiline with pipe |) + SUBTEXT_LINE=$(grep -n '^subtext:' "${FILE}" | head -1) + SUBTEXT="" + if [[ -n "${SUBTEXT_LINE}" ]]; then + LINE_NUM=$(echo "${SUBTEXT_LINE}" | cut -d: -f1) + SUBTEXT_VALUE=$(echo "${SUBTEXT_LINE}" | sed "s/^[0-9]*:subtext:[[:space:]]*//" | xargs) + + if [[ "${SUBTEXT_VALUE}" == "|" ]]; then + # Multiline subtext: read indented lines after the pipe + SUBTEXT=$(awk -v start="${LINE_NUM}" ' + NR > start { + if (/^[[:space:]]+[^#]/) { + sub(/^[[:space:]]{2}/, ""); print + } else if (/^[[:space:]]*$/) { + print "" + } else { + exit + } + } + ' "${FILE}") + elif [[ -n "${SUBTEXT_VALUE}" ]]; then + # shellcheck disable=SC2001 + SUBTEXT=$(echo "${SUBTEXT_VALUE}" | sed "s/['\"]//g") + fi + fi + + if [[ -n "${COMPONENT}" ]]; then + COMPONENTS+=("${COMPONENT}") + fi + if [[ -n "${NOTE}" ]]; then + NOTES+=("${NOTE}") + fi + if [[ -n "${CHANGE_TYPE}" ]]; then + CHANGE_TYPES+=("${CHANGE_TYPE}") + fi + if [[ -n "${ISSUES}" ]]; then + # Split comma-separated issues + IFS=',' read -ra ISSUE_ARRAY <<< "${ISSUES}" + for ISSUE in "${ISSUE_ARRAY[@]}"; do + ISSUE=$(echo "${ISSUE}" | xargs) + if [[ -n "${ISSUE}" ]]; then + ALL_ISSUES+=("${ISSUE}") + fi + done + fi + if [[ -n "${SUBTEXT}" ]]; then + SUBTEXTS+=("${SUBTEXT}") + fi + done + + if [[ ${#NOTES[@]} -eq 0 ]]; then + echo "No valid changelog notes found. Cannot generate PR info." + exit 0 + fi + + # --- Generate PR Title --- + # Format: [component] note (for single entry) + # Format: [component1, component2] first note (+N more) (for multiple entries) + + # Deduplicate components + mapfile -t UNIQUE_COMPONENTS < <(echo "${COMPONENTS[@]}" | tr ' ' '\n' | sort -u) + COMPONENT_PREFIX="" + if [[ ${#UNIQUE_COMPONENTS[@]} -gt 0 ]]; then + COMPONENT_PREFIX="[$(IFS=', '; echo "${UNIQUE_COMPONENTS[*]}")] " + fi + + if [[ ${#NOTES[@]} -eq 1 ]]; then + NEW_TITLE="${COMPONENT_PREFIX}${NOTES[0]}" + else + # Multiple entries: use the first note, mentioning there are more + REMAINING=$(( ${#NOTES[@]} - 1 )) + NEW_TITLE="${COMPONENT_PREFIX}${NOTES[0]} (+${REMAINING} more)" + fi + + # Truncate title to 256 characters (GitHub limit) + if [[ ${#NEW_TITLE} -gt 256 ]]; then + NEW_TITLE="${NEW_TITLE:0:253}..." + fi + + echo "" + echo "Generated PR title: ${NEW_TITLE}" + + # --- Generate PR Body --- + BODY="#### Description"$'\n' + + for i in "${!NOTES[@]}"; do + CHANGE_LABEL="" + if [[ -n "${CHANGE_TYPES[$i]:-}" ]]; then + CHANGE_LABEL=$(change_type_label "${CHANGE_TYPES[$i]}") + fi + + if [[ ${#NOTES[@]} -eq 1 ]]; then + BODY+=$'\n'"**${CHANGE_LABEL^}**: ${NOTES[$i]}"$'\n' + else + BODY+=$'\n'"- **${CHANGE_LABEL^}** (${COMPONENTS[$i]:-unknown}): ${NOTES[$i]}"$'\n' + fi + done + + # Add subtext if present + if [[ ${#SUBTEXTS[@]} -gt 0 ]]; then + BODY+=$'\n'"**Details:**"$'\n' + for SUBTEXT in "${SUBTEXTS[@]}"; do + if [[ -n "${SUBTEXT}" ]]; then + BODY+=$'\n'"${SUBTEXT}"$'\n' + fi + done + fi + + # Add tracking issues + BODY+=$'\n'"#### Link to tracking issue"$'\n' + if [[ ${#ALL_ISSUES[@]} -gt 0 ]]; then + mapfile -t UNIQUE_ISSUES < <(echo "${ALL_ISSUES[@]}" | tr ' ' '\n' | sort -un) + for ISSUE in "${UNIQUE_ISSUES[@]}"; do + BODY+="Fixes #${ISSUE}"$'\n' + done + else + BODY+="Fixes "$'\n' + fi + + BODY+=$'\n'"#### Testing"$'\n\n' + BODY+="#### Documentation"$'\n\n' + + BODY+="---"$'\n' + BODY+="*This PR description was auto-generated from the changelog entry.*"$'\n' + + echo "" + echo "Generated PR body:" + echo "${BODY}" + + # --- Update the PR --- + echo "" + echo "Updating PR #${PR}..." + + gh pr edit "${PR}" --title "${NEW_TITLE}" --body "${BODY}" + + echo "PR #${PR} updated successfully." +} + +# We don't want this workflow to ever fail and block a PR, +# so ensure all errors are caught. +main || echo "Failed to run $0" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 641c547c7dbca..d2a6ebe74ea4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -115,6 +115,10 @@ change. For instance: [processor/tailsampling] fix AND policy +Alternatively, if you have already written a changelog entry, you can set your PR title to `as per changelog` and a +GitHub Action will automatically generate the PR title and description from your changelog entry YAML file(s). This +avoids duplicating effort between the changelog entry and the PR description. + ### Description guidelines When linking to an open issue, if your PR is meant to close said issue, please prefix your issue with one of the @@ -122,7 +126,6 @@ following keywords: `Resolves`, `Fixes`, or `Closes`. More information on this f [here](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword). This will automatically close the issue once your PR has been merged. - ## Issue Triaging See [issue-triaging.md](./issue-triaging.md) for more information on the issue triaging process.