This is an automated email from the ASF dual-hosted git repository.
janhoy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new 91c4ed4e7ed GH workflow validate-changelog: Make the /no-changelog
command actually clear the failed changelog check (#5099)
91c4ed4e7ed is described below
commit 91c4ed4e7edda68f9fbc871ea302c44f63930725
Author: Jan Høydahl <[email protected]>
AuthorDate: Sat Oct 24 16:01:46 2026 +0200
GH workflow validate-changelog: Make the /no-changelog command actually
clear the failed changelog check (#5099)
---
.github/workflows/changelog-comment-command.yml | 103 +++++++++++++++----
.github/workflows/validate-changelog.yml | 127 ++++++++++++------------
dev-docs/changelog.adoc | 7 +-
3 files changed, 147 insertions(+), 90 deletions(-)
diff --git a/.github/workflows/changelog-comment-command.yml
b/.github/workflows/changelog-comment-command.yml
index 4d227b9f8cc..afad1968750 100644
--- a/.github/workflows/changelog-comment-command.yml
+++ b/.github/workflows/changelog-comment-command.yml
@@ -1,19 +1,15 @@
name: Changelog Comment Commands
-# Lets a PR author (or any committer) toggle the 'no-changelog' label by
-# commenting on the PR, without needing the repo write access that setting a
-# label directly requires:
+# Lets a PR author or a committer toggle the 'no-changelog' label by
commenting:
#
# /no-changelog - mark the PR as not needing a changelog entry
# /needs-changelog - undo that
#
-# The command applies/removes the label, and the label change re-triggers the
-# "Validate Changelog" workflow (which listens for labeled/unlabeled events),
-# so the red check turns green on its own.
+# The label change is followed by an explicit re-run of "Validate Changelog": a
+# label set with the GITHUB_TOKEN triggers no workflow of its own.
#
-# This workflow deliberately never checks out or executes PR code: it only
-# reads the comment payload and calls the labels API, so running with a
-# write-scoped token is safe.
+# This workflow never checks out or executes PR code, and the comment body
+# reaches the shell only through 'env'.
on:
issue_comment:
types:
@@ -22,14 +18,11 @@ on:
concurrency:
group: changelog-comment-command-${{ github.event.issue.number }}
-permissions:
- issues: write # add/remove labels, add a reaction, post a reply
- pull-requests: write
+permissions: {}
jobs:
handle-command:
- # Pull request comments only, from a human, that mention one of our
commands.
- # The exact (line-anchored) match happens in the parse step below.
+ # Pull request comments from a human; the exact match happens in the parse
step.
if: |
github.event.issue.pull_request &&
github.event.comment.user.type != 'Bot' &&
@@ -37,6 +30,13 @@ jobs:
contains(github.event.comment.body, '/needs-changelog'))
runs-on: ubuntu-latest
+ permissions:
+ issues: write # add/remove labels, add a reaction, post a reply
+ pull-requests: write
+
+ outputs:
+ rerun: ${{ steps.apply.outputs.rerun }}
+
steps:
- name: Parse command
id: parse
@@ -44,8 +44,7 @@ jobs:
BODY: ${{ github.event.comment.body }}
run: |
set -euo pipefail
- # A command must be alone on its own line, so that quoted text (">
/no-changelog")
- # or a mention inside a sentence does not trigger it.
+ # A command must stand alone on its line, so quoted text or prose
does not trigger it.
command=none
if printf '%s\n' "$BODY" | grep -iE
'^[[:space:]]*/no-changelog[[:space:]]*\r?$' > /dev/null; then
command=add
@@ -69,8 +68,7 @@ jobs:
ASSOCIATION: ${{ github.event.comment.author_association }}
run: |
set -euo pipefail
- # The PR author may always toggle it on their own PR; otherwise the
- # commenter needs write access, which author_association reflects.
+ # The PR author may always toggle it; anyone else needs write access.
if [ "$COMMENTER" = "$PR_AUTHOR" ]; then
allowed=true
else
@@ -83,6 +81,7 @@ jobs:
echo "Commenter '${COMMENTER}' (${ASSOCIATION}) allowed: ${allowed}"
- name: Apply label change
+ id: apply
if: steps.authz.outputs.allowed == 'true' &&
(steps.parse.outputs.command == 'add' || steps.parse.outputs.command ==
'remove')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -99,6 +98,7 @@ jobs:
if [ "$COMMAND" = "remove" ]; then
if [ "$has_label" = "true" ]; then
gh api -X DELETE
"repos/${REPO}/issues/${PR_NUMBER}/labels/${LABEL}" --silent
+ echo "rerun=true" >> "$GITHUB_OUTPUT"
echo "::notice::Removed '${LABEL}' from PR#${PR_NUMBER}"
else
echo "::notice::PR#${PR_NUMBER} does not have '${LABEL}';
nothing to do"
@@ -106,12 +106,14 @@ jobs:
exit 0
fi
+ # Re-run even if the label is already set; the check can still be
red.
if [ "$has_label" = "true" ]; then
- echo "::notice::PR#${PR_NUMBER} already has '${LABEL}'; nothing to
do"
- exit 0
+ echo "::notice::PR#${PR_NUMBER} already has '${LABEL}'"
+ else
+ gh api -X POST "repos/${REPO}/issues/${PR_NUMBER}/labels" -f
"labels[]=${LABEL}" --silent
+ echo "::notice::Added '${LABEL}' to PR#${PR_NUMBER}"
fi
- gh api -X POST "repos/${REPO}/issues/${PR_NUMBER}/labels" -f
"labels[]=${LABEL}" --silent
- echo "::notice::Added '${LABEL}' to PR#${PR_NUMBER}"
+ echo "rerun=true" >> "$GITHUB_OUTPUT"
- name: Acknowledge with a reaction
if: steps.authz.outputs.allowed == 'true' &&
(steps.parse.outputs.command == 'add' || steps.parse.outputs.command ==
'remove')
@@ -148,3 +150,60 @@ jobs:
> reply.md
fi
gh pr comment "$PR_NUMBER" --repo "$REPO" --body-file reply.md
+
+ # Separate job, so the one handling the comment body never holds 'actions:
write'.
+ rerun-check:
+ name: Re-run the changelog check
+ needs: handle-command
+ if: needs.handle-command.outputs.rerun == 'true'
+ runs-on: ubuntu-latest
+ continue-on-error: true
+
+ permissions:
+ actions: write # re-run the Validate Changelog workflow
+ pull-requests: read # read the PR's head SHA
+
+ steps:
+ - name: Re-run the latest Validate Changelog run
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REPO: ${{ github.repository }}
+ PR_NUMBER: ${{ github.event.issue.number }}
+ run: |
+ set -euo pipefail
+ WORKFLOW=validate-changelog.yml
+
+ head_sha=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid
--jq .headRefOid)
+
+ # An in-flight run may have read the labels before the change, and
only a
+ # completed run can be re-run, so wait it out.
+ deadline=$(( $(date +%s) + 600 ))
+ while true; do
+ run=$(gh api
"repos/${REPO}/actions/workflows/${WORKFLOW}/runs?head_sha=${head_sha}&per_page=1"
\
+ --jq '.workflow_runs[0] // empty | "\(.id) \(.status)
\(.conclusion)"')
+
+ if [ -z "$run" ]; then
+ echo "::notice::No ${WORKFLOW} run found for ${head_sha};
nothing to re-run"
+ exit 0
+ fi
+ read -r run_id run_status run_conclusion <<< "$run"
+ echo "Latest ${WORKFLOW} run for ${head_sha}: ${run_id}
(${run_status}/${run_conclusion})"
+
+ # Approval starts a fresh run, which reads the label as it is now.
+ if [ "$run_conclusion" = "action_required" ]; then
+ echo "::notice::Run ${run_id} awaits workflow approval; the
label applies once approved"
+ exit 0
+ fi
+ if [ "$run_status" = "completed" ]; then
+ break
+ fi
+ if [ "$(date +%s)" -ge "$deadline" ]; then
+ echo "::notice::Run ${run_id} is still ${run_status} after
waiting; leaving it alone"
+ exit 0
+ fi
+ echo "Run ${run_id} is ${run_status}; waiting for it to finish"
+ sleep 15
+ done
+
+ gh api -X POST "repos/${REPO}/actions/runs/${run_id}/rerun" --silent
+ echo "::notice::Re-ran ${WORKFLOW} run ${run_id} for PR#${PR_NUMBER}"
diff --git a/.github/workflows/validate-changelog.yml
b/.github/workflows/validate-changelog.yml
index af43d05b4d5..8f64cf32aeb 100644
--- a/.github/workflows/validate-changelog.yml
+++ b/.github/workflows/validate-changelog.yml
@@ -2,8 +2,8 @@ name: Validate Changelog
on:
pull_request:
- # 'labeled'/'unlabeled' are included so that adding or removing the
- # 'no-changelog' label re-runs this check on an already-failed PR.
+ # 'labeled'/'unlabeled' re-run this check when a human toggles
'no-changelog';
+ # the comment command re-runs it explicitly, see
changelog-comment-command.yml.
types:
- opened
- reopened
@@ -17,12 +17,13 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number ||
github.ref }}
cancel-in-progress: true
+permissions: {}
+
jobs:
validate-changelog:
name: Check changelog entry
- # Skip validation for Renovate PRs (solrbot); dependency changes are
summarized at release time.
- # On label events, only re-run for the 'no-changelog' label; other label
churn
- # (e.g. from the labeler workflow) leaves the existing check result
untouched.
+ # Renovate PRs (solrbot) are summarized at release time, and of the label
+ # events only 'no-changelog' is of interest here.
if: |
github.event.pull_request.user.login != 'solrbot' &&
(
@@ -31,46 +32,55 @@ jobs:
)
runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- with:
- fetch-depth: 0
+ permissions:
+ contents: read
+ issues: read # the PR's labels
+ pull-requests: read # the PR's changed files
+
+ # This job runs no PR code: it reads the PR through the API, keeps that
data
+ # outside the workspace, and only runs the validator from the base commit.
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REPO: ${{ github.repository }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ steps:
- name: Check for no-changelog label
id: check-label
- env:
- HAS_NO_CHANGELOG_LABEL: ${{
contains(github.event.pull_request.labels.*.name, 'no-changelog') }}
run: |
- if [ "$HAS_NO_CHANGELOG_LABEL" = "true" ]; then
- echo "skip=true" >> "$GITHUB_OUTPUT"
- else
- echo "skip=false" >> "$GITHUB_OUTPUT"
+ set -euo pipefail
+ # Not github.event.pull_request.labels: a re-run replays the original
payload.
+ has_label=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/labels" \
+ --jq '[.[].name] | index("no-changelog") != null')
+ if [ "$has_label" = "true" ]; then
+ echo "::notice::PR#${PR_NUMBER} has the 'no-changelog' label;
skipping validation"
fi
+ echo "skip=${has_label}" >> "$GITHUB_OUTPUT"
- name: Check for changelog entry
+ id: entry
if: steps.check-label.outputs.skip == 'false'
run: |
- # Get the list of changed files
- CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref
}}...HEAD)
+ set -euo pipefail
+ files="$RUNNER_TEMP/pr-files.tsv"
+ entries="$RUNNER_TEMP/entry-paths.txt"
- # Check if any files were added to changelog/unreleased/
- if echo "$CHANGED_FILES" | grep -q "^changelog/unreleased/"; then
+ gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/files" \
+ --jq '.[] | [.status, .filename] | @tsv' > "$files"
+
+ # Entries that are still there to validate; a deleted one has nothing
to read.
+ awk -F'\t' '$1 != "removed" && $2 ~
/^changelog\/unreleased\/.+\.ya?ml$/ { print $2 }' \
+ "$files" > "$entries"
+
+ if [ -s "$entries" ]; then
echo "✓ Changelog entry found"
+ echo "validate=true" >> "$GITHUB_OUTPUT"
exit 0
fi
- # Check if only docs/tests/comments were changed (common exceptions)
- HAS_NON_DOCS_CHANGES=false
- while IFS= read -r file; do
- # Skip changelog, docs, tests, CI config and certain config files
- if ! echo "$file" | grep -qE
"(^changelog/|^solr/solr-ref-guide/|^dev-docs/|^\.github/|\.md$|\.adoc$|^solr/.*/test|\.gradle$|\.properties$|README|NOTICE|LICENSE)";
then
- HAS_NON_DOCS_CHANGES=true
- break
- fi
- done <<< "$CHANGED_FILES"
-
- if [ "$HAS_NON_DOCS_CHANGES" = false ]; then
+ # Docs, tests, build files and CI config need no entry.
+ if ! cut -f2 "$files" | grep -qvE
"(^changelog/|^solr/solr-ref-guide/|^dev-docs/|^\.github/|\.md$|\.adoc$|^solr/.*/test|\.gradle$|\.properties$|README|NOTICE|LICENSE)";
then
echo "✓ No code changes detected (docs/tests only)"
exit 0
fi
@@ -89,41 +99,30 @@ jobs:
echo "This check then re-runs automatically. To undo, comment
'/needs-changelog'."
exit 1
+ # Only the validator script, only from the base commit, only one commit
deep.
+ - name: Checkout the validator
+ if: steps.entry.outputs.validate == 'true'
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ github.event.pull_request.base.sha }}
+ sparse-checkout: dev-tools/scripts
+ fetch-depth: 1
+ persist-credentials: false
+
- name: Validate changelog YAML structure
- if: steps.check-label.outputs.skip == 'false'
+ if: steps.entry.outputs.validate == 'true'
run: |
- # Get the list of changed files
- CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref
}}...HEAD)
-
- # Find all YAML files added to changelog/unreleased/
- YAML_FILES=$(echo "$CHANGED_FILES" | grep
"^changelog/unreleased/.*\.ya\?ml$" || true)
-
- if [ -z "$YAML_FILES" ]; then
- exit 0
- fi
-
- echo "Validating changelog YAML files..."
- VALIDATION_FAILED=false
+ set -euo pipefail
+ entries="$RUNNER_TEMP/entry-paths.txt"
+ yamls="$RUNNER_TEMP/entries"
+ mkdir -p "$yamls"
+ # PR-authored data: fetched outside the workspace and only ever parsed.
while IFS= read -r file; do
- if [ -z "$file" ] || [ ! -f "$file" ]; then
- continue
- fi
+ echo "Fetching $file"
+ gh api -H "Accept: application/vnd.github.raw" \
+ "repos/${REPO}/contents/${file}?ref=${HEAD_SHA}" >
"${yamls}/$(basename "$file")"
+ done < "$entries"
- echo ""
- echo "Validating: $file"
-
- # Validate using a Python script
- python3 dev-tools/scripts/validate-changelog-yaml.py "$file"
-
- if [ $? -ne 0 ]; then
- VALIDATION_FAILED=true
- fi
-
- done <<< "$YAML_FILES"
-
- if [ "$VALIDATION_FAILED" = true ]; then
- echo "Please see dev-docs/changelog.adoc for more info."
-
- exit 1
- fi
+ python3 dev-tools/scripts/validate-changelog-yaml.py "$yamls" \
+ || { echo "Please see dev-docs/changelog.adoc for more info."; exit
1; }
diff --git a/dev-docs/changelog.adoc b/dev-docs/changelog.adoc
index ac6f0504ff5..05ae92728b5 100644
--- a/dev-docs/changelog.adoc
+++ b/dev-docs/changelog.adoc
@@ -122,10 +122,9 @@ TIP: Aliases for the `writeChangelog` task are `changelog`
and `newChangelog`.
== 4. Changelog Validation in Pull Requests
-The `validate-changelog` GitHub workflow automatically checks that:
-
-. **CHANGES.txt is not edited directly** - All changes must use the YAML
fragment approach
-. **A changelog entry is added** - Code changes must include a corresponding
YAML file in `changelog/unreleased/`
+The `validate-changelog` GitHub workflow checks that code changes come with a
+changelog entry, and that any entry added under `changelog/unreleased/` is
+structurally valid.
Pull requests that only touch documentation, tests, build files or CI
configuration
are exempt and pass without an entry. Subjectively, some minor changes don't
need one either.