diff --git a/.chloggen/TEMPLATE.yaml b/.chloggen/TEMPLATE.yaml new file mode 100644 index 000000000..4487b5f6d --- /dev/null +++ b/.chloggen/TEMPLATE.yaml @@ -0,0 +1,12 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: +# The name of the component (e.g. jmx-metrics, resource-providers, aws-xray) +component: +# A brief description of the change. +note: +# One or more tracking issues related to the change. Use the PR number if no issue exists. +issues: [] +# Optional additional context. +subtext: +# 'user' if relevant to end users, 'api' if there is a library API change. +change_logs: [user] \ No newline at end of file diff --git a/.chloggen/config.yaml b/.chloggen/config.yaml new file mode 100644 index 000000000..064417223 --- /dev/null +++ b/.chloggen/config.yaml @@ -0,0 +1,12 @@ +change_types: + - breaking + - deprecation + - new_component + - enhancement + - bug_fix +entries_dir: .chloggen +changelog_md: CHANGELOG.md +template_yaml: .chloggen/TEMPLATE.yaml +change_logs: + user: CHANGELOG.md +default_change_logs: [user] \ No newline at end of file diff --git a/.chloggen/pr-2837.yaml b/.chloggen/pr-2837.yaml new file mode 100644 index 000000000..ced5eabcd --- /dev/null +++ b/.chloggen/pr-2837.yaml @@ -0,0 +1,6 @@ +change_type: enhancement +component: +note: Automate changelog generation using fragment pattern +issues: [2787] +subtext: +change_logs: [user] diff --git a/.github/workflows/changelog-fragment.yml b/.github/workflows/changelog-fragment.yml new file mode 100644 index 000000000..5f6e02de4 --- /dev/null +++ b/.github/workflows/changelog-fragment.yml @@ -0,0 +1,190 @@ +name: Changelog Fragments + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled + +permissions: + contents: write + issues: write + pull-requests: write + models: read + +jobs: + generate-fragment: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.head_ref }} + token: ${{ github.token }} + fetch-depth: 0 + + - name: Install yq + run: | + go install github.com/mikefarah/yq/v4@v4.44.3 + + - name: Check skip conditions + id: skip + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_LABELS: ${{ toJson(github.event.pull_request.labels.*.name) }} + PR_USER_LOGIN: ${{ github.event.pull_request.user.login }} + run: | + skip=false + + if [[ "$PR_USER_LOGIN" == "dependabot[bot]" || "$PR_USER_LOGIN" == "renovate[bot]" ]]; then + skip=true + fi + + if [[ "$PR_TITLE" == \[chore\]* ]]; then + skip=true + fi + + if jq -e 'index("Skip Changelog")' <<<"$PR_LABELS" >/dev/null; then + skip=true + fi + + echo "skip=$skip" >> "$GITHUB_OUTPUT" + + - name: Check existing fragment + id: fragment + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + fragment=".chloggen/pr-${PR_NUMBER}.yaml" + + if [[ -f "$fragment" ]]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Fetch base branch + if: ${{ steps.skip.outputs.skip != 'true' && steps.fragment.outputs.exists != 'true' && github.event.pull_request.head.repo.full_name == github.repository }} + run: | + git fetch origin "${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }}" --depth=1 + + - name: Generate fragment + if: ${{ steps.skip.outputs.skip != 'true' && steps.fragment.outputs.exists != 'true' && github.event.pull_request.head.repo.full_name == github.repository }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: | + changed_files="$(git diff --name-only "origin/${BASE_REF}"...HEAD)" + + request_payload="$(jq -n \ + --arg title "$PR_TITLE" \ + --arg body "$PR_BODY" \ + --arg changed_files "$changed_files" \ + '{ + model: "gpt-4o-mini", + response_format: {type: "json_object"}, + messages: [ + { + role: "system", + content: "Generate a single JSON object for a changelog fragment. Return only valid JSON with these keys: change_type, component, note, issues, subtext, change_logs. change_type must be one of breaking, deprecation, new_component, enhancement, bug_fix. component must be blank if the PR does not clearly map to one component. note must be a brief end-user facing summary. issues must be an array of issue numbers without #. subtext is optional and may be null or omitted. change_logs must be [\"user\"]. Do not add markdown or extra keys." + }, + { + role: "user", + content: "PR title: \($title)\n\nPR body:\n\($body)\n\nChanged files:\n\($changed_files)" + } + ] + }')" + + response="$(curl --fail-with-body --silent --show-error https://models.github.ai/inference/chat/completions \ + -H "Accept: application/vnd.github+json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${GITHUB_TOKEN}" \ + -d "$request_payload")" + + jq -r '.choices[0].message.content | fromjson' <<<"$response" | "$(go env GOPATH)/bin/yq" -P - > ".chloggen/pr-${PR_NUMBER}.yaml" + + - name: Validate AI Output + if: ${{ steps.skip.outputs.skip != 'true' && steps.fragment.outputs.exists != 'true' && github.event.pull_request.head.repo.full_name == github.repository }} + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + fragment=".chloggen/pr-${PR_NUMBER}.yaml" + + if ! "$(go env GOPATH)/bin/yq" -e 'has("change_type") and has("component") and has("note")' "$fragment" >/dev/null; then + echo "missing required keys: change_type, component, note" + rm -f "$fragment" + exit 1 + fi + + - name: Commit + if: ${{ steps.skip.outputs.skip != 'true' && steps.fragment.outputs.exists != 'true' && github.event.pull_request.head.repo.full_name == github.repository }} + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_BRANCH: ${{ github.head_ref }} + run: | + git config user.name opentelemetrybot + git config user.email 107717825+opentelemetrybot@users.noreply.github.com + git add ".chloggen/pr-${PR_NUMBER}.yaml" + git commit -m "Add changelog fragment for PR #${PR_NUMBER}" + git push origin HEAD:"${PR_BRANCH}" + + validate-fragment: + needs: generate-fragment + if: always() + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.head_ref }} + token: ${{ github.token }} + fetch-depth: 0 + + - name: Check skip conditions + id: skip + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_LABELS: ${{ toJson(github.event.pull_request.labels.*.name) }} + PR_USER_LOGIN: ${{ github.event.pull_request.user.login }} + run: | + skip=false + + if [[ "$PR_USER_LOGIN" == "dependabot[bot]" || "$PR_USER_LOGIN" == "renovate[bot]" ]]; then + skip=true + fi + + if [[ "$PR_TITLE" == \[chore\]* ]]; then + skip=true + fi + + if jq -e 'index("Skip Changelog")' <<<"$PR_LABELS" >/dev/null; then + skip=true + fi + + echo "skip=$skip" >> "$GITHUB_OUTPUT" + + - name: Exit if skipped + if: ${{ steps.skip.outputs.skip == 'true' }} + run: exit 0 + + - name: Validate fragment + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + fragment=".chloggen/pr-${PR_NUMBER}.yaml" + + if [[ ! -f "$fragment" ]]; then + echo "❌ Missing changelog fragment: $fragment" + echo "If you are contributing from a fork, you must create this file manually." + echo "Copy the contents of .chloggen/TEMPLATE.yaml into a new file named $fragment, fill it out, and commit it to your branch." + exit 1 + fi + + go install go.opentelemetry.io/build-tools/chloggen@v0.15.0 + "$(go env GOPATH)/bin/chloggen" validate --config .chloggen/config.yaml diff --git a/.github/workflows/draft-release-changelog.yml b/.github/workflows/draft-release-changelog.yml new file mode 100644 index 000000000..c0fa31c79 --- /dev/null +++ b/.github/workflows/draft-release-changelog.yml @@ -0,0 +1,55 @@ +name: Draft Release Changelog + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g. 1.57.0)' + required: true + type: string + +jobs: + draft-changelog: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate version input + run: | + if [[ ! "${{ inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "❌ Invalid version format: ${{ inputs.version }}. Must be X.Y.Z" + exit 1 + fi + + - name: Install chloggen + run: go install go.opentelemetry.io/build-tools/chloggen@v0.15.0 + + - name: Dry run (preview) + run: $(go env GOPATH)/bin/chloggen update --dry + + - name: Compile changelog + env: + VERSION: ${{ inputs.version }} + run: $(go env GOPATH)/bin/chloggen update --version "$VERSION" + + - name: Create PR for Release Captain review + uses: peter-evans/create-pull-request@v6 + with: + # Strictly require the bot token for EasyCLA compliance + token: ${{ secrets.OPENTELEMETRYBOT_GITHUB_TOKEN }} + commit-message: "[chore] update changelog for release ${{ inputs.version }}" + committer: "opentelemetrybot <107717825+opentelemetrybot@users.noreply.github.com>" + author: "opentelemetrybot <107717825+opentelemetrybot@users.noreply.github.com>" + title: "[chore] update changelog for release ${{ inputs.version }}" + body: | + Automated changelog compilation for release `${{ inputs.version }}`. + + **Release Captain:** Review the entries below, make any manual corrections, then merge. + branch: "chore/changelog-${{ inputs.version }}" + base: main \ No newline at end of file