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
75 changes: 74 additions & 1 deletion .github/workflows/changelog-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@
# `check-only` - Render the changelog but do NOT commit. Fail the job if
# the on-disk CHANGELOG.md disagrees with the regenerated
# output. Use as a `pull_request` gate.
#
# Loop-prevention (surfaced by maa-framework PR #219): two complementary guards
# prevent the workflow from reacting to its own output and opening an infinite
# chain of changelog PRs:
# 1. The canonical cliff.toml has a `skip = true` parser for the bot's own
# "chore(changelog): regenerate from conventional commits" subjects. This
# is the primary fix because it survives GitHub squash-merge (which uses
# the PR title as the new subject).
# 2. Every commit/PR this workflow produces carries a [skip changelog]
# marker in its body, and the `guard` job below bails out entirely when
# the triggering HEAD commit already has that marker. This protects
# `commit-back` mode callers (where the auto commit lands directly on
# main without a squash step) and provides defense-in-depth for pr-back.

on:
workflow_call:
Expand All @@ -50,11 +63,46 @@
default: '2.6.1'

permissions:
actions: read

Check warning on line 66 in .github/workflows/changelog-reusable.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move this read permission from workflow level to job level.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDJzDdNRmSTHiV9lIah&open=AaDJzDdNRmSTHiV9lIah&pullRequest=988
contents: read

jobs:
guard:
name: Check for self-trigger loop
runs-on: ${{ inputs.runs-on }}
outputs:
skip: ${{ steps.loopguard.outputs.skip }}
steps:
- name: Checkout caller (HEAD only is enough for the guard)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ github.repository }}
ref: ${{ github.ref }}
fetch-depth: 1
path: caller

- name: Loop guard
id: loopguard
working-directory: caller
run: |
set -euo pipefail
# If the push that triggered us is itself a changelog regeneration
# (commit-back's direct commit, or a squash-merged pr-back PR whose
# body still contains the [skip changelog] marker placed below),
# skip the whole generate job. cliff.toml's subject-matcher is the
# primary fix; this guard handles cases where the marker is on the
# body (e.g. commit-back mode, rebase-merges, mergers who preserve
# the PR body).
if git log -1 --pretty=%B | grep -q '\[skip changelog\]'; then
echo "HEAD commit carries [skip changelog]; skipping generate job to break the regenerate loop."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

generate:
needs: guard
if: ${{ needs.guard.outputs.skip != 'true' }}
timeout-minutes: 10
name: Generate CHANGELOG.md
runs-on: ${{ inputs.runs-on }}
Expand Down Expand Up @@ -151,6 +199,8 @@
fi
git commit -m "chore(changelog): regenerate from conventional commits

[skip changelog]

Auto-generated by hyperpolymath/standards changelog-reusable.yml.
See standards/templates/cliff.toml for the canonical config.

Expand All @@ -174,12 +224,35 @@
git config user.name "github-actions[bot]"
git checkout -b "$branch"
git add CHANGELOG.md
git commit -m "chore(changelog): regenerate from conventional commits"
git commit -m "chore(changelog): regenerate from conventional commits

[skip changelog]"
git push -u origin "$branch"
# Before opening a new PR, close any older open bot/changelog-* PRs
# against the same base so we don't accumulate duplicate PRs if a
# maintainer hasn't merged the previous one before another push lands.
for pr in $(gh pr list --repo "${{ github.repository }}" \
--base "${{ github.ref_name }}" \
--head 'bot/changelog-' \
--state open \
--json number,headRefName \
--jq '.[] | select(.headRefName != "'"$branch"'") | .number'); do
echo "Closing superseded changelog PR #$pr"
gh pr close "$pr" --repo "${{ github.repository }}" \
--comment "Superseded by a newer changelog regeneration PR."
done
gh pr create \
--repo "${{ github.repository }}" \
--title "chore(changelog): regenerate from conventional commits" \
--body "Auto-generated by hyperpolymath/standards changelog-reusable.yml.

[skip changelog]

This PR body carries the [skip changelog] marker so that a
squash-merge of this PR will not re-trigger the changelog
workflow (loop-prevention added in response to maa-framework#219).
Please do not remove that marker when editing the description.

Closes part of the 2026-05-26 CHANGELOG gap (standards#197 audit)." \
--base "${{ github.ref_name }}" \
--head "$branch"
Expand Down
9 changes: 9 additions & 0 deletions templates/cliff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ commit_parsers = [
{ message = "^docs", group = "Documentation" },
{ message = "^test", group = "Tests" },
{ message = "^ci", group = "CI" },
# Never re-emit the auto-changelog's own commits; that causes an infinite
# regenerate loop (surfaced by maa-framework PR #219). Must come BEFORE
# the generic ^chore matcher because commit_parsers is first-match-wins.
# The regex matches both the bot's branch commit subject and the default
# GitHub squash-merge subject ("... (#NNN)"), which is critical because
# in pr-back mode the squash-merge subject (not the branch commit body)
# survives into main.
{ message = "^chore\\(changelog\\):\\s*regenerate from conventional commits", skip = true },
{ body = "\\[skip changelog\\]", skip = true },
{ message = "^chore", group = "Chores" },
{ message = "^style", skip = true },
{ message = "^revert", group = "Reverted" },
Expand Down
Loading