This is an automated email from the ASF dual-hosted git repository.
mridulpathak pushed a commit to branch release24.09
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/release24.09 by this push:
new 09cd441570 Implemented: Generate release notes with GitHub and label
pull requests by type from their title prefix (OFBIZ-13696)
09cd441570 is described below
commit 09cd441570190347994a1afce95a2ff49bd6e845
Author: Mridul Pathak <[email protected]>
AuthorDate: Sun Oct 18 01:09:21 2026 +0530
Implemented: Generate release notes with GitHub and label pull requests by
type from their title prefix (OFBIZ-13696)
- Add .github/release.yml, which groups the generated release notes into
Bug fixes, Improvements, New features, Documentation and Other changes, and
leaves out Dependabot pull requests and those labeled dependencies.
- Add the PR Type Label workflow, which sets the bug, enhancement, feature
or documentation label of a pull request from its title prefix (Fixed,
Improved, Implemented, Documented), including backport titles, and swaps the
label when the title changes. It only uses actions/github-script and never
checks out the pull request. A manual run labels the pull requests merged since
a given date.
---
.github/release.yml | 47 +++++++++++
.github/workflows/pr-type-label.yml | 151 ++++++++++++++++++++++++++++++++++++
2 files changed, 198 insertions(+)
diff --git a/.github/release.yml b/.github/release.yml
new file mode 100644
index 0000000000..fc7940b217
--- /dev/null
+++ b/.github/release.yml
@@ -0,0 +1,47 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+# Configuration of the release notes GitHub generates for a release.
+# GitHub reads this file from the tag being released, so it has to be on the
+# release branch before the tag is cut.
+# The type labels are set from the PR title prefix by pr-type-label.yml.
+# See
https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes
+
+changelog:
+ exclude:
+ labels:
+ - dependencies
+ authors:
+ - dependabot[bot]
+ categories:
+ - title: Bug fixes
+ labels:
+ - bug
+ - title: Improvements
+ labels:
+ - enhancement
+ - title: New features
+ labels:
+ - feature
+ - title: Documentation
+ labels:
+ - documentation
+ - title: Other changes
+ labels:
+ - "*"
diff --git a/.github/workflows/pr-type-label.yml
b/.github/workflows/pr-type-label.yml
new file mode 100644
index 0000000000..db11eb7fca
--- /dev/null
+++ b/.github/workflows/pr-type-label.yml
@@ -0,0 +1,151 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+# Sets the type label (bug, enhancement, feature, documentation) of a pull
+# request from the prefix of its title, so that the release notes GitHub
+# generates (see .github/release.yml) can group pull requests by type.
+# It never checks out nor runs code of the pull request.
+# Run it manually (workflow_dispatch) to label pull requests already merged.
+
+name: "PR Type Label"
+
+on:
+ pull_request_target:
+ types: [ opened, edited, reopened ]
+ branches: [ trunk, release* ]
+ workflow_dispatch:
+ inputs:
+ since:
+ description: "Label pull requests merged on or after this date
(YYYY-MM-DD)"
+ required: true
+ dry_run:
+ description: "Only log the labels, without changing the pull requests"
+ type: boolean
+ default: true
+
+permissions: {}
+
+concurrency:
+ group: pr-type-label-${{ github.event.pull_request.number || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ label:
+ if: >-
+ github.event_name == 'workflow_dispatch' ||
+ (github.event.pull_request.user.login != 'dependabot[bot]' &&
+ (github.event.action != 'edited' || github.event.changes.title))
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ steps:
+ - name: Set the type label from the title prefix
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #
v9.0.0
+ with:
+ script: |
+ const TYPE_LABELS = ['bug', 'enhancement', 'feature',
'documentation'];
+ const PREFIXES = [
+ [/^(fixed|fix|fixing)(?=[\s:])/i, 'bug'],
+ [/^(improved|completed)(?=[\s:])/i, 'enhancement'],
+ [/^implemented(?=[\s:])/i, 'feature'],
+ [/^documented(?=[\s:])/i, 'documentation'],
+ ];
+ // e.g. "Backport PR #1360: 'Fixed: ...'", "Backport pr 1157
(Improved: ...)", "Backported: ..."
+ const BACKPORT =
/^backport(?:ed)?\b\s*(?:pr\b)?\s*#?\s*(\d*)\s*[:\-–]?\s*['"(]?\s*/i;
+ const { owner, repo } = context.repo;
+
+ function labelFromPrefix(title) {
+ const match = PREFIXES.find(([prefix]) => prefix.test(title));
+ return match ? match[1] : null;
+ }
+
+ async function typeLabel(pr) {
+ const title = (pr.title || '').trim();
+ const label = labelFromPrefix(title);
+ if (label) {
+ return label;
+ }
+ const preamble = title.match(BACKPORT);
+ if (!preamble && !/\bbackport/i.test(pr.body || '')) {
+ return null;
+ }
+ const stripped = title.slice(preamble ? preamble[0].length : 0);
+ const strippedLabel = labelFromPrefix(stripped);
+ if (strippedLabel) {
+ return strippedLabel;
+ }
+ // fall back to the title of the backported pull request
+ const ref = (preamble && preamble[1] ? preamble : null) ||
title.match(/#\s*(\d+)/)
+ || title.match(/\bpr\s*(\d+)/i) || (pr.body ||
'').match(/#(\d+)/);
+ if (!ref || Number(ref[1]) === pr.number) {
+ return null;
+ }
+ try {
+ const { data: original } = await github.rest.pulls.get({
owner, repo, pull_number: Number(ref[1]) });
+ return labelFromPrefix((original.title || '').trim());
+ } catch (error) {
+ core.info(`#${pr.number}: cannot read #${ref[1]}:
${error.message}`);
+ return null;
+ }
+ }
+
+ async function setTypeLabel(pr, dryRun) {
+ const label = await typeLabel(pr);
+ const current = pr.labels.map(l => l.name);
+ const stale = label ? current.filter(name =>
TYPE_LABELS.includes(name) && name !== label) : [];
+ const add = label && !current.includes(label);
+ core.info(`#${pr.number} [${label || 'no type'}]${stale.length ?
` -${stale.join(' -')}` : ''}${add ? ` +${label}` : ''} ${pr.title}`);
+ if (dryRun) {
+ return;
+ }
+ for (const name of stale) {
+ await github.rest.issues.removeLabel({ owner, repo,
issue_number: pr.number, name });
+ }
+ if (add) {
+ await github.rest.issues.addLabels({ owner, repo,
issue_number: pr.number, labels: [label] });
+ }
+ }
+
+ if (context.eventName === 'pull_request_target') {
+ // read the pull request again, a previous run may have changed
its labels
+ const { data: pr } = await github.rest.pulls.get({ owner, repo,
pull_number: context.payload.pull_request.number });
+ await setTypeLabel(pr, false);
+ return;
+ }
+
+ const { since } = context.payload.inputs;
+ const dryRun = context.payload.inputs.dry_run !== false &&
context.payload.inputs.dry_run !== 'false';
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(since)) {
+ core.setFailed(`since must be a date (YYYY-MM-DD), got:
${since}`);
+ return;
+ }
+ const sinceTime = Date.parse(`${since}T00:00:00Z`);
+ const merged = await github.paginate(github.rest.pulls.list,
+ { owner, repo, state: 'closed', sort: 'updated', direction:
'desc', per_page: 100 },
+ (response, done) => {
+ if (response.data.some(pr => Date.parse(pr.updated_at) <
sinceTime)) {
+ done();
+ }
+ return response.data.filter(pr => pr.merged_at &&
Date.parse(pr.merged_at) >= sinceTime
+ && pr.user.login !== 'dependabot[bot]');
+ });
+ core.info(`${merged.length} pull requests merged since
${since}${dryRun ? ' (dry run)' : ''}`);
+ for (const pr of merged) {
+ await setTypeLabel(pr, dryRun);
+ }