From aa5282796c1bf668ca604720363f9ceb445fe753 Mon Sep 17 00:00:00 2001 From: Mostafa Farrag Date: Mon, 20 Jul 2026 23:28:52 +0200 Subject: [PATCH 1/5] fix(release): detect changelog_file anywhere in the commitizen section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The action read changelog_file with a fixed `grep -A 20` window from the [tool.commitizen] header. A config that places changelog_file further down — e.g. after a long version_files array or comment block, as in the earthlens uv workspace — fell outside the window, so detection returned empty, defaulted to CHANGELOG.md, and the release failed with "Changelog file not found". Replace the grep in both the validate step and the bump step with a section-aware awk: scan from the [tool.commitizen] header to the next [...] table and take the first double-quoted value of changelog_file (which also drops any inline # comment). The key is now found regardless of how far below the header it sits. Add a regression job (test-release-changelog-far-from-header) that builds a fixture placing changelog_file ~33 lines below the header and asserts the release uses docs/change-log.md instead of falling back to CHANGELOG.md. --- .github/workflows/test-release-github.yml | 113 ++++++++++++++++++++++ actions/release/github/action.yml | 26 ++++- 2 files changed, 137 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-release-github.yml b/.github/workflows/test-release-github.yml index 00f4ff4..b4f981f 100644 --- a/.github/workflows/test-release-github.yml +++ b/.github/workflows/test-release-github.yml @@ -1963,3 +1963,116 @@ jobs: fi done echo "[OK] pip: no lockfile created; fold step correctly skipped" + + # Regression test: changelog_file must be found no matter how far below the + # [tool.commitizen] header it sits. The old detection used a fixed + # `grep -A 20` window, so a config with a long version_files array or comment + # block before changelog_file (as in the earthlens uv workspace) fell through + # to the CHANGELOG.md default and failed with "Changelog file not found". + # This fixture places changelog_file ~30 lines below the header on purpose. + test-release-changelog-far-from-header: + name: Test release - changelog_file far below the commitizen header + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Copy test fixture + run: | + cp tests/data/release-github/test-basic-uv/pyproject.toml . + cp tests/data/release-github/test-basic-uv/uv.lock . + + - name: Rewrite commitizen config so changelog_file is far below the header + run: | + # test-basic-uv keeps [tool.commitizen] as the last section, so drop it + # and append a version that pushes changelog_file well past 20 lines + # below the header (reproducing the earthlens layout). + sed -i '/^\[tool\.commitizen\]/,$d' pyproject.toml + { + echo '[tool.commitizen]' + echo 'name = "cz_conventional_commits"' + echo 'version = "0.1.0"' + echo 'tag_format = "$version"' + echo 'update_changelog_on_bump = true' + echo 'version_files = [' + echo ' "pyproject.toml:version"' + echo ']' + for i in $(seq 1 25); do + echo "# padding line $i pushing changelog_file past the old grep -A 20 window" + done + echo 'changelog_file = "docs/change-log.md"' + } >> pyproject.toml + + mkdir -p docs + echo "# Changelog" > docs/change-log.md + + HEADER=$(grep -n '^\[tool\.commitizen\]' pyproject.toml | tail -1 | cut -d: -f1) + CLINE=$(grep -n '^changelog_file' pyproject.toml | tail -1 | cut -d: -f1) + echo "header at line $HEADER, changelog_file at line $CLINE (delta $((CLINE - HEADER)))" + if [ $((CLINE - HEADER)) -le 20 ]; then + echo "::error::test setup invalid: changelog_file is not far enough below the header" + exit 1 + fi + echo "[OK] changelog_file sits $((CLINE - HEADER)) lines below the header (> 20)" + + - name: Clean up repository tags + run: git tag -l | xargs -r git tag -d + + - name: Setup fake git remote for testing + uses: ./.github/workflows/test-helpers/setup-fake-remote + + - name: Normalize local branch to main (dispatch-testability) + shell: bash + run: | + git checkout -B main + git config push.default current + + - name: Create initial commit with a prior tag + run: | + git config user.name "Test Bot" + git config user.email "test@example.com" + + git add pyproject.toml uv.lock docs/change-log.md + git commit -m "chore: initial commit" + git tag 0.1.0 + git push origin main + git push origin 0.1.0 + + echo "feature" > feature.py + git add feature.py + git commit -m "feat: trigger a minor bump" + git push origin main + + - name: Run release action + uses: ./actions/release/github + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + increment: 'minor' + prerelease-type: 'none' + package-manager: 'uv' + python-version: '3.12' + install-groups: 'groups: dev docs' + skip-github-release: 'true' + + - name: Verify the release used the custom changelog, not the CHANGELOG.md default + run: | + # The action must have found docs/change-log.md. If detection had fallen + # back to CHANGELOG.md the validate step would have failed the job, and no + # CHANGELOG.md should exist. + if [ -f CHANGELOG.md ]; then + echo "::error::CHANGELOG.md exists — detection fell back to the default" + exit 1 + fi + if ! git rev-parse 0.2.0 >/dev/null 2>&1; then + echo "::error::tag 0.2.0 was not created" + git tag -l + exit 1 + fi + if ! grep -q "0.2.0" docs/change-log.md; then + echo "::error::docs/change-log.md was not updated with the 0.2.0 entry" + cat docs/change-log.md + exit 1 + fi + echo "[OK] release found changelog_file far below the header; docs/change-log.md updated" diff --git a/actions/release/github/action.yml b/actions/release/github/action.yml index 5da34d7..38add63 100644 --- a/actions/release/github/action.yml +++ b/actions/release/github/action.yml @@ -240,7 +240,18 @@ runs: CHANGELOG_FILE="" if [ -f "pyproject.toml" ]; then - CHANGELOG_FILE=$(grep -A 20 "^\[tool\.commitizen\]" pyproject.toml | grep "^changelog_file" | head -1 | sed 's/.*=\s*"\(.*\)"/\1/' || true) + # Read changelog_file from the [tool.commitizen] section, section-aware: + # scan from the header to the next "[...]" table so the key is found no + # matter how far below the header it sits (e.g. after a long version_files + # array). match() grabs the first double-quoted value, which also drops + # any inline "# comment". Empty result falls back to CHANGELOG.md below. + CHANGELOG_FILE=$(awk ' + /^[[:space:]]*\[tool\.commitizen\][[:space:]]*$/ { inblock = 1; next } + inblock && /^[[:space:]]*\[/ { exit } + inblock && /^[[:space:]]*changelog_file[[:space:]]*=/ && match($0, /"[^"]*"/) { + print substr($0, RSTART + 1, RLENGTH - 2); exit + } + ' pyproject.toml 2>/dev/null || true) fi # Default to CHANGELOG.md if not found @@ -303,7 +314,18 @@ runs: # Read changelog_file from commitizen config (needed for recovery path) CHANGELOG_FILE="" if [ -f "pyproject.toml" ]; then - CHANGELOG_FILE=$(grep -A 20 "^\[tool\.commitizen\]" pyproject.toml | grep "^changelog_file" | head -1 | sed 's/.*=\s*"\(.*\)"/\1/' || true) + # Read changelog_file from the [tool.commitizen] section, section-aware: + # scan from the header to the next "[...]" table so the key is found no + # matter how far below the header it sits (e.g. after a long version_files + # array). match() grabs the first double-quoted value, which also drops + # any inline "# comment". Empty result falls back to CHANGELOG.md below. + CHANGELOG_FILE=$(awk ' + /^[[:space:]]*\[tool\.commitizen\][[:space:]]*$/ { inblock = 1; next } + inblock && /^[[:space:]]*\[/ { exit } + inblock && /^[[:space:]]*changelog_file[[:space:]]*=/ && match($0, /"[^"]*"/) { + print substr($0, RSTART + 1, RLENGTH - 2); exit + } + ' pyproject.toml 2>/dev/null || true) fi if [ -z "$CHANGELOG_FILE" ]; then CHANGELOG_FILE="CHANGELOG.md" From ce2b66ae313589c573a1c9d82cff70a935c800e5 Mon Sep 17 00:00:00 2001 From: Mostafa Farrag Date: Mon, 20 Jul 2026 23:49:53 +0200 Subject: [PATCH 2/5] fix(release): harden changelog_file detection against TOML variations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-1 review follow-ups to the section-aware changelog detection: - Header with a trailing comment (`[tool.commitizen] # ...`) is valid TOML but the end-anchored header regex missed it, silently falling back to CHANGELOG.md — a regression versus the old unanchored grep. Allow an optional trailing comment on the header line. - Accept a single-quoted value (`changelog_file = 'x'`), not only double-quoted. A literal single quote is passed in via `awk -v q` so the awk program stays free of embedded single quotes. - Drop `2>/dev/null` so a real awk/regex failure surfaces in the logs instead of being masked into a misleading "Changelog file not found". Applied identically at both detection sites (validate step and bump step). --- actions/release/github/action.yml | 36 +++++++++++++++++++------------ 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/actions/release/github/action.yml b/actions/release/github/action.yml index 38add63..a7826c3 100644 --- a/actions/release/github/action.yml +++ b/actions/release/github/action.yml @@ -243,15 +243,19 @@ runs: # Read changelog_file from the [tool.commitizen] section, section-aware: # scan from the header to the next "[...]" table so the key is found no # matter how far below the header it sits (e.g. after a long version_files - # array). match() grabs the first double-quoted value, which also drops - # any inline "# comment". Empty result falls back to CHANGELOG.md below. - CHANGELOG_FILE=$(awk ' - /^[[:space:]]*\[tool\.commitizen\][[:space:]]*$/ { inblock = 1; next } + # array). Tolerates a trailing comment on the header and accepts either a + # double- or single-quoted value; an inline "# comment" after the value is + # dropped. Empty result falls back to CHANGELOG.md below. `q` carries a + # literal single quote so the awk program needs no embedded single quote. + CHANGELOG_FILE=$(awk -v q="'" ' + /^[[:space:]]*\[tool\.commitizen\][[:space:]]*(#.*)?$/ { inblock = 1; next } inblock && /^[[:space:]]*\[/ { exit } - inblock && /^[[:space:]]*changelog_file[[:space:]]*=/ && match($0, /"[^"]*"/) { - print substr($0, RSTART + 1, RLENGTH - 2); exit + inblock && /^[[:space:]]*changelog_file[[:space:]]*=/ { + if (match($0, /"[^"]*"/)) { print substr($0, RSTART + 1, RLENGTH - 2); exit } + s = $0; a = index(s, q) + if (a > 0) { rest = substr(s, a + 1); b = index(rest, q); if (b > 0) { print substr(rest, 1, b - 1); exit } } } - ' pyproject.toml 2>/dev/null || true) + ' pyproject.toml || true) fi # Default to CHANGELOG.md if not found @@ -317,15 +321,19 @@ runs: # Read changelog_file from the [tool.commitizen] section, section-aware: # scan from the header to the next "[...]" table so the key is found no # matter how far below the header it sits (e.g. after a long version_files - # array). match() grabs the first double-quoted value, which also drops - # any inline "# comment". Empty result falls back to CHANGELOG.md below. - CHANGELOG_FILE=$(awk ' - /^[[:space:]]*\[tool\.commitizen\][[:space:]]*$/ { inblock = 1; next } + # array). Tolerates a trailing comment on the header and accepts either a + # double- or single-quoted value; an inline "# comment" after the value is + # dropped. Empty result falls back to CHANGELOG.md below. `q` carries a + # literal single quote so the awk program needs no embedded single quote. + CHANGELOG_FILE=$(awk -v q="'" ' + /^[[:space:]]*\[tool\.commitizen\][[:space:]]*(#.*)?$/ { inblock = 1; next } inblock && /^[[:space:]]*\[/ { exit } - inblock && /^[[:space:]]*changelog_file[[:space:]]*=/ && match($0, /"[^"]*"/) { - print substr($0, RSTART + 1, RLENGTH - 2); exit + inblock && /^[[:space:]]*changelog_file[[:space:]]*=/ { + if (match($0, /"[^"]*"/)) { print substr($0, RSTART + 1, RLENGTH - 2); exit } + s = $0; a = index(s, q) + if (a > 0) { rest = substr(s, a + 1); b = index(rest, q); if (b > 0) { print substr(rest, 1, b - 1); exit } } } - ' pyproject.toml 2>/dev/null || true) + ' pyproject.toml || true) fi if [ -z "$CHANGELOG_FILE" ]; then CHANGELOG_FILE="CHANGELOG.md" From ac402aba0b4f968c83d6474f0332a3e38a5e56c3 Mon Sep 17 00:00:00 2001 From: Mostafa Farrag Date: Mon, 20 Jul 2026 23:50:33 +0200 Subject: [PATCH 3/5] test(release): use the hardened awk detection in inline changelog lookups Three existing jobs (recovery-no-duplicates, notes-from-changelog, notes-custom-changelog-path) reimplemented changelog_file detection inline with the old `grep -A 20` window. Replace those copies with the same section-aware awk the action now uses, so the test suite and the action share one detection behaviour and the copies can catch future regressions. --- .github/workflows/test-release-github.yml | 30 ++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-release-github.yml b/.github/workflows/test-release-github.yml index b4f981f..feca395 100644 --- a/.github/workflows/test-release-github.yml +++ b/.github/workflows/test-release-github.yml @@ -1117,7 +1117,15 @@ jobs: echo "=== Running recovery path ===" # Read changelog_file from commitizen config (same as action does) - CHANGELOG_FILE=$(grep -A 20 "^\[tool\.commitizen\]" pyproject.toml | grep "^changelog_file" | head -1 | sed 's/.*=\s*"\(.*\)"/\1/' || true) + CHANGELOG_FILE=$(awk -v q="'" ' + /^[[:space:]]*\[tool\.commitizen\][[:space:]]*(#.*)?$/ { inblock = 1; next } + inblock && /^[[:space:]]*\[/ { exit } + inblock && /^[[:space:]]*changelog_file[[:space:]]*=/ { + if (match($0, /"[^"]*"/)) { print substr($0, RSTART + 1, RLENGTH - 2); exit } + s = $0; a = index(s, q) + if (a > 0) { rest = substr(s, a + 1); b = index(rest, q); if (b > 0) { print substr(rest, 1, b - 1); exit } } + } + ' pyproject.toml || true) if [ -z "$CHANGELOG_FILE" ]; then CHANGELOG_FILE="CHANGELOG.md" fi @@ -1287,7 +1295,15 @@ jobs: uv run cz bump --yes --increment minor # Read changelog file path - CHANGELOG_FILE=$(grep -A 20 "^\[tool\.commitizen\]" pyproject.toml | grep "^changelog_file" | head -1 | sed 's/.*=\s*"\(.*\)"/\1/' || true) + CHANGELOG_FILE=$(awk -v q="'" ' + /^[[:space:]]*\[tool\.commitizen\][[:space:]]*(#.*)?$/ { inblock = 1; next } + inblock && /^[[:space:]]*\[/ { exit } + inblock && /^[[:space:]]*changelog_file[[:space:]]*=/ { + if (match($0, /"[^"]*"/)) { print substr($0, RSTART + 1, RLENGTH - 2); exit } + s = $0; a = index(s, q) + if (a > 0) { rest = substr(s, a + 1); b = index(rest, q); if (b > 0) { print substr(rest, 1, b - 1); exit } } + } + ' pyproject.toml || true) if [ -z "$CHANGELOG_FILE" ]; then CHANGELOG_FILE="CHANGELOG.md" fi @@ -1414,7 +1430,15 @@ jobs: - name: Verify release notes extraction from custom path run: | - CHANGELOG_FILE=$(grep -A 20 "^\[tool\.commitizen\]" pyproject.toml | grep "^changelog_file" | head -1 | sed 's/.*=\s*"\(.*\)"/\1/' || true) + CHANGELOG_FILE=$(awk -v q="'" ' + /^[[:space:]]*\[tool\.commitizen\][[:space:]]*(#.*)?$/ { inblock = 1; next } + inblock && /^[[:space:]]*\[/ { exit } + inblock && /^[[:space:]]*changelog_file[[:space:]]*=/ { + if (match($0, /"[^"]*"/)) { print substr($0, RSTART + 1, RLENGTH - 2); exit } + s = $0; a = index(s, q) + if (a > 0) { rest = substr(s, a + 1); b = index(rest, q); if (b > 0) { print substr(rest, 1, b - 1); exit } } + } + ' pyproject.toml || true) if [ -z "$CHANGELOG_FILE" ]; then CHANGELOG_FILE="CHANGELOG.md" fi From c6a0cfbc3f385e73c52105beb0253942f3043c2c Mon Sep 17 00:00:00 2001 From: Mostafa Farrag Date: Mon, 20 Jul 2026 23:51:55 +0200 Subject: [PATCH 4/5] test(release): remove only the commitizen section when building the far-header fixture The far-from-header job deleted from [tool.commitizen] to EOF, which assumed that section is last in the shared test-basic-uv fixture. Replace it with a section-scoped awk that removes only the [tool.commitizen] table (header plus body up to the next table), so the job no longer depends on section ordering in a fixture shared by other jobs. --- .github/workflows/test-release-github.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-release-github.yml b/.github/workflows/test-release-github.yml index feca395..044c385 100644 --- a/.github/workflows/test-release-github.yml +++ b/.github/workflows/test-release-github.yml @@ -2010,10 +2010,15 @@ jobs: - name: Rewrite commitizen config so changelog_file is far below the header run: | - # test-basic-uv keeps [tool.commitizen] as the last section, so drop it - # and append a version that pushes changelog_file well past 20 lines - # below the header (reproducing the earthlens layout). - sed -i '/^\[tool\.commitizen\]/,$d' pyproject.toml + # Remove ONLY the existing [tool.commitizen] section (header + body up to + # the next table) so this does not depend on it being the last section of + # the shared fixture. Then append a version that pushes changelog_file well + # past 20 lines below the header (reproducing the earthlens layout). + awk ' + /^\[tool\.commitizen\]/ { skip = 1; next } + skip && /^\[/ { skip = 0 } + !skip { print } + ' pyproject.toml > pyproject.toml.tmp && mv pyproject.toml.tmp pyproject.toml { echo '[tool.commitizen]' echo 'name = "cz_conventional_commits"' From e748f8e43c981e8559335feafc2a59888b85de1d Mon Sep 17 00:00:00 2001 From: Mostafa Farrag Date: Tue, 21 Jul 2026 18:22:10 +0200 Subject: [PATCH 5/5] fix(release): anchor changelog_file extraction to the value's own quote MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-2 review: the previous extractor tried a double-quote match across the whole line first, so a single-quoted value with a double-quoted token in a trailing comment (e.g. changelog_file = 'x.md' # see "y") extracted the comment token instead of the value. Anchor extraction to the first char after `= ` — the value's own opening quote — and read to its matching close, which also keeps a `#` inside a double-quoted value intact. Apply identically to both action sites and the three inline test copies. Also add a caveat comment noting the line-based scanner assumes the ordinary commitizen layout (scalar keys; no multi-line-string or nested-array key that opens a line with "[" ahead of changelog_file). --- .github/workflows/test-release-github.yml | 33 ++++++++++++----- actions/release/github/action.yml | 44 +++++++++++++++-------- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/.github/workflows/test-release-github.yml b/.github/workflows/test-release-github.yml index 044c385..c138f1f 100644 --- a/.github/workflows/test-release-github.yml +++ b/.github/workflows/test-release-github.yml @@ -1121,9 +1121,14 @@ jobs: /^[[:space:]]*\[tool\.commitizen\][[:space:]]*(#.*)?$/ { inblock = 1; next } inblock && /^[[:space:]]*\[/ { exit } inblock && /^[[:space:]]*changelog_file[[:space:]]*=/ { - if (match($0, /"[^"]*"/)) { print substr($0, RSTART + 1, RLENGTH - 2); exit } - s = $0; a = index(s, q) - if (a > 0) { rest = substr(s, a + 1); b = index(rest, q); if (b > 0) { print substr(rest, 1, b - 1); exit } } + rhs = $0 + sub(/^[^=]*=[[:space:]]*/, "", rhs) # rhs now starts at the value + qc = substr(rhs, 1, 1) # opening quote char of the value + if (qc == "\"" || qc == q) { # anchor to the value quote, not any quote in a comment + rest = substr(rhs, 2); p = index(rest, qc) + if (p > 0) { print substr(rest, 1, p - 1); exit } + } + exit } ' pyproject.toml || true) if [ -z "$CHANGELOG_FILE" ]; then @@ -1299,9 +1304,14 @@ jobs: /^[[:space:]]*\[tool\.commitizen\][[:space:]]*(#.*)?$/ { inblock = 1; next } inblock && /^[[:space:]]*\[/ { exit } inblock && /^[[:space:]]*changelog_file[[:space:]]*=/ { - if (match($0, /"[^"]*"/)) { print substr($0, RSTART + 1, RLENGTH - 2); exit } - s = $0; a = index(s, q) - if (a > 0) { rest = substr(s, a + 1); b = index(rest, q); if (b > 0) { print substr(rest, 1, b - 1); exit } } + rhs = $0 + sub(/^[^=]*=[[:space:]]*/, "", rhs) # rhs now starts at the value + qc = substr(rhs, 1, 1) # opening quote char of the value + if (qc == "\"" || qc == q) { # anchor to the value quote, not any quote in a comment + rest = substr(rhs, 2); p = index(rest, qc) + if (p > 0) { print substr(rest, 1, p - 1); exit } + } + exit } ' pyproject.toml || true) if [ -z "$CHANGELOG_FILE" ]; then @@ -1434,9 +1444,14 @@ jobs: /^[[:space:]]*\[tool\.commitizen\][[:space:]]*(#.*)?$/ { inblock = 1; next } inblock && /^[[:space:]]*\[/ { exit } inblock && /^[[:space:]]*changelog_file[[:space:]]*=/ { - if (match($0, /"[^"]*"/)) { print substr($0, RSTART + 1, RLENGTH - 2); exit } - s = $0; a = index(s, q) - if (a > 0) { rest = substr(s, a + 1); b = index(rest, q); if (b > 0) { print substr(rest, 1, b - 1); exit } } + rhs = $0 + sub(/^[^=]*=[[:space:]]*/, "", rhs) # rhs now starts at the value + qc = substr(rhs, 1, 1) # opening quote char of the value + if (qc == "\"" || qc == q) { # anchor to the value quote, not any quote in a comment + rest = substr(rhs, 2); p = index(rest, qc) + if (p > 0) { print substr(rest, 1, p - 1); exit } + } + exit } ' pyproject.toml || true) if [ -z "$CHANGELOG_FILE" ]; then diff --git a/actions/release/github/action.yml b/actions/release/github/action.yml index a7826c3..25d24a9 100644 --- a/actions/release/github/action.yml +++ b/actions/release/github/action.yml @@ -243,17 +243,25 @@ runs: # Read changelog_file from the [tool.commitizen] section, section-aware: # scan from the header to the next "[...]" table so the key is found no # matter how far below the header it sits (e.g. after a long version_files - # array). Tolerates a trailing comment on the header and accepts either a - # double- or single-quoted value; an inline "# comment" after the value is - # dropped. Empty result falls back to CHANGELOG.md below. `q` carries a - # literal single quote so the awk program needs no embedded single quote. + # array). Tolerates a trailing comment on the header and accepts a double- + # or single-quoted value, anchoring extraction to the value's own opening + # quote so an inline "# comment" (even one containing quotes) is ignored. + # Empty result falls back to CHANGELOG.md below. `q` carries a literal + # single quote so the awk program needs no embedded single quote. Assumes + # the ordinary commitizen layout: scalar keys, no multi-line string or + # nested-array key that starts a line with "[" ahead of changelog_file. CHANGELOG_FILE=$(awk -v q="'" ' /^[[:space:]]*\[tool\.commitizen\][[:space:]]*(#.*)?$/ { inblock = 1; next } inblock && /^[[:space:]]*\[/ { exit } inblock && /^[[:space:]]*changelog_file[[:space:]]*=/ { - if (match($0, /"[^"]*"/)) { print substr($0, RSTART + 1, RLENGTH - 2); exit } - s = $0; a = index(s, q) - if (a > 0) { rest = substr(s, a + 1); b = index(rest, q); if (b > 0) { print substr(rest, 1, b - 1); exit } } + rhs = $0 + sub(/^[^=]*=[[:space:]]*/, "", rhs) # rhs now starts at the value + qc = substr(rhs, 1, 1) # opening quote char of the value + if (qc == "\"" || qc == q) { # anchor to the value quote, not any quote in a comment + rest = substr(rhs, 2); p = index(rest, qc) + if (p > 0) { print substr(rest, 1, p - 1); exit } + } + exit } ' pyproject.toml || true) fi @@ -321,17 +329,25 @@ runs: # Read changelog_file from the [tool.commitizen] section, section-aware: # scan from the header to the next "[...]" table so the key is found no # matter how far below the header it sits (e.g. after a long version_files - # array). Tolerates a trailing comment on the header and accepts either a - # double- or single-quoted value; an inline "# comment" after the value is - # dropped. Empty result falls back to CHANGELOG.md below. `q` carries a - # literal single quote so the awk program needs no embedded single quote. + # array). Tolerates a trailing comment on the header and accepts a double- + # or single-quoted value, anchoring extraction to the value's own opening + # quote so an inline "# comment" (even one containing quotes) is ignored. + # Empty result falls back to CHANGELOG.md below. `q` carries a literal + # single quote so the awk program needs no embedded single quote. Assumes + # the ordinary commitizen layout: scalar keys, no multi-line string or + # nested-array key that starts a line with "[" ahead of changelog_file. CHANGELOG_FILE=$(awk -v q="'" ' /^[[:space:]]*\[tool\.commitizen\][[:space:]]*(#.*)?$/ { inblock = 1; next } inblock && /^[[:space:]]*\[/ { exit } inblock && /^[[:space:]]*changelog_file[[:space:]]*=/ { - if (match($0, /"[^"]*"/)) { print substr($0, RSTART + 1, RLENGTH - 2); exit } - s = $0; a = index(s, q) - if (a > 0) { rest = substr(s, a + 1); b = index(rest, q); if (b > 0) { print substr(rest, 1, b - 1); exit } } + rhs = $0 + sub(/^[^=]*=[[:space:]]*/, "", rhs) # rhs now starts at the value + qc = substr(rhs, 1, 1) # opening quote char of the value + if (qc == "\"" || qc == q) { # anchor to the value quote, not any quote in a comment + rest = substr(rhs, 2); p = index(rest, qc) + if (p > 0) { print substr(rest, 1, p - 1); exit } + } + exit } ' pyproject.toml || true) fi