Skip to content

Commit df54d67

Browse files
committed
Merge branch '8.0.x' into feat/grails-jacoco-plugin
2 parents 359de88 + d7452fb commit df54d67

13 files changed

Lines changed: 820 additions & 12 deletions

File tree

.github/release-drafter.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,27 @@ categories:
130130
exclude-labels:
131131
- 'skip-changelog'
132132
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
133+
# Multi-branch isolation:
134+
# - filter-by-commitish: only consider releases targeting this branch (e.g. 7.0.x)
135+
# - tag-prefix: only consider releases tagged with the "v" prefix (defense-in-depth)
136+
# - include-pre-releases: REQUIRED for Apache Grails - releases are marked
137+
# prerelease=true on GitHub during the ASF vote process. Without this,
138+
# release-drafter excludes them when finding the "last release", causing it
139+
# to either bump from a stale baseline (e.g. v7.0.10 instead of v7.0.11) or,
140+
# on branches where ALL releases are prereleases (e.g. 8.0.x at v8.0.0-M1),
141+
# report "No last release" and fall back to walking unbounded git history,
142+
# which exhausts the GitHub API rate limit and produces no draft at all.
133143
filter-by-commitish: true
144+
tag-prefix: v
145+
include-pre-releases: true
146+
# Bound API usage when no prior release matches the filters above (e.g. on a
147+
# brand-new release branch like 7.2.x before its first tag). Without this,
148+
# release-drafter walks the entire commit history and exhausts the rate limit.
149+
# - initial-commits-since: hard date floor used only when no last release is
150+
# found. Set just before 7.1.1 / 7.0.11 / 8.0.0-M1 (all published 2026-04-30)
151+
# so newly-cut branches walk only days, not years, of history. Bump this
152+
# when forking a new release line to a date close to the fork point.
153+
initial-commits-since: '2026-04-29T00:00:00Z'
134154
version-resolver:
135155
major:
136156
labels:

.github/workflows/release-notes.yml

Lines changed: 127 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,41 +13,159 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16+
# Maintains one draft GitHub Release per active release branch (7.0.x, 7.1.x,
17+
# 7.2.x, 8.0.x, ...). Each branch produces an independent draft because the
18+
# release-drafter config in .github/release-drafter.yml combines
19+
# `filter-by-commitish: true`, `filter-by-range: ~MAJOR.MINOR.0`, and
20+
# `tag-prefix: v` so that drafts created on one branch never leak into another.
21+
#
22+
# Companion config: .github/release-drafter.yml
1623
name: "Release - Drafter"
1724
on:
18-
issues:
19-
types: [closed,reopened]
25+
# Runs on every push to a release branch so the draft for that branch is
26+
# always up to date with the latest merged PRs.
2027
push:
2128
branches:
2229
- '[0-9]+.[0-9]+.x'
30+
# Runs on PRs whose BASE branch is a release branch so the autolabeler can
31+
# apply labels (bug/feature/docs/...) and the draft picks up new PRs as soon
32+
# as they are opened. Feature-to-feature PRs (e.g. fix/foo -> feat/bar)
33+
# are intentionally excluded - they cannot affect any release.
2334
pull_request:
2435
types: [opened, reopened, synchronize, labeled]
36+
branches:
37+
- '[0-9]+.[0-9]+.x'
38+
# Manual recovery: rerun against any branch (e.g. to recreate a draft after
39+
# one was accidentally deleted, or to seed an initial draft on a new branch).
2540
workflow_dispatch:
26-
# queue jobs and only allow 1 run per branch due to the likelihood of hitting GitHub resource limits
41+
42+
# Per-branch concurrency. Critically: this group MUST NOT collide with the
43+
# `release-pipeline-${branch}` group used by .github/workflows/release.yml.
44+
# The release pipeline has manual approval gates (`environment: release`,
45+
# `environment: docs`, `environment: sdkman`) which routinely keep a release
46+
# run in `waiting` state for HOURS or DAYS until a maintainer approves the
47+
# next stage. When the drafter shared that group, every push to a release
48+
# branch queued behind those waiting runs - producing drafter runs of
49+
# 1400-2000+ minutes that ultimately got cancelled, leaving drafts stale.
50+
#
51+
# Drafter and release.yml never touch the same release object: the drafter
52+
# maintains a DRAFT for the *next* version (e.g. v7.0.12), while release.yml
53+
# uploads assets to the *current published* tag (e.g. v7.0.11). Splitting the
54+
# concurrency groups is therefore safe.
55+
#
56+
# `cancel-in-progress: true`: if multiple pushes land on the same branch in
57+
# quick succession, only the latest matters - the latest run sees every PR
58+
# the older one would have seen, so cancelling pending runs is correct.
2759
concurrency:
28-
group: release-pipeline-${{ github.event.pull_request.base.ref || github.ref_name }}
29-
cancel-in-progress: false
60+
group: release-drafter-${{ github.event.pull_request.base.ref || github.ref_name }}
61+
cancel-in-progress: true
62+
3063
jobs:
3164
update_release_draft:
65+
name: "Update Release Draft"
3266
permissions:
33-
# write permission is required to create a github release
67+
# Required to create or update the draft GitHub Release
3468
contents: write
35-
# write permission is required for autolabeler
69+
# Required for the autolabeler to add labels to PRs
3670
pull-requests: write
3771
runs-on: ubuntu-latest
3872
steps:
73+
# release-drafter's `filter-by-range` keeps the action looking only at
74+
# releases whose tag falls inside this branch's MAJOR.MINOR series. This
75+
# is what prevents 7.0.x's draft from being computed against 7.1.x's
76+
# tags (or vice versa). It is derived dynamically from the branch name
77+
# so this workflow file works identically on every release branch.
3978
- name: "🔢 Derive semver range from branch"
4079
id: version
4180
run: |
81+
set -euo pipefail
4282
BRANCH="${{ github.event.pull_request.base.ref || github.ref_name }}"
43-
if [[ "$BRANCH" =~ ^[0-9]+\.[0-9]+\.x$ ]]; then
44-
echo "range=~${BRANCH%.x}.0" >> "$GITHUB_OUTPUT"
83+
if [[ "$BRANCH" =~ ^([0-9]+)\.([0-9]+)\.x$ ]]; then
84+
RANGE="~${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.0"
85+
echo "Branch $BRANCH -> filter-by-range $RANGE"
86+
echo "range=$RANGE" >> "$GITHUB_OUTPUT"
87+
else
88+
echo "Branch $BRANCH is not a release branch; skipping range filter (will only match the configured prerelease/commitish filters)"
89+
echo "range=" >> "$GITHUB_OUTPUT"
4590
fi
91+
92+
# Pinned to v7.3.1 by commit SHA per the ASF security policy (matches
93+
# the pinning convention already used by other ASF-approved actions in
94+
# this repo). v7.2.1 or later is required because it ships PR #1593 - the
95+
# bug fix for `initial-commits-since` being silently ignored when set only
96+
# in the release-drafter.yml config (not also as a workflow input).
97+
# Our release-drafter.yml relies on that exact config-only path to
98+
# bound history walking on brand-new release branches like 7.2.x.
99+
#
100+
# Earlier minor releases also contributed key options we depend on:
101+
# - v7.1.0 (PR #1451): adds the `initial-commits-since` config option.
102+
# - v7.0.0 (PR #1470): adds the `history-limit` config option.
103+
#
104+
# Bump checklist: when updating, verify the new tag is signed, read the
105+
# release notes for any breaking changes to the config schema, and
106+
# update both the SHA and the `# v...` comment together. Resolve the
107+
# commit SHA via:
108+
# gh api repos/release-drafter/release-drafter/git/tags/<tag-sha> \
109+
# --jq '.object.sha'
46110
- name: "📝 Update Release Draft"
111+
id: drafter
47112
uses: release-drafter/release-drafter@e1247478eabc9f6d9cf5ec2b3547469b0e1d2767 # v7.3.1
113+
# Drafting release notes is a best-effort, non-critical task - a
114+
# transient GitHub API hiccup must not turn every PR check red. The
115+
# explicit verification step below catches the case where the action
116+
# silently produced no draft (e.g. rate-limit exhaustion).
48117
continue-on-error: true
49118
with:
119+
# Explicit `commitish` is critical on `pull_request` events: without
120+
# it, release-drafter would default to `refs/pull/N/merge` (a
121+
# virtual ref) which the GitHub API rejects when creating a release,
122+
# producing the "Validation Failed: target_commitish invalid" error
123+
# historically seen on PRs (see INFRA-27602).
50124
commitish: ${{ github.event.pull_request.base.ref || github.ref_name }}
51125
filter-by-range: ${{ steps.version.outputs.range }}
52126
env:
53127
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
128+
129+
# Surface failures that `continue-on-error` would otherwise hide. The
130+
# release-drafter action exposes the resulting release id as an output;
131+
# an empty value means it failed to create or update a draft. We log a
132+
# loud warning (visible in the workflow summary and as a GitHub Actions
133+
# annotation) but do NOT fail the job - PR checks must stay green for
134+
# transient API issues, while still alerting maintainers something is
135+
# wrong if drafts go missing for multiple consecutive runs.
136+
- name: "🔎 Verify draft was created or updated"
137+
if: always()
138+
env:
139+
DRAFT_ID: ${{ steps.drafter.outputs.id }}
140+
DRAFT_TAG: ${{ steps.drafter.outputs.tag_name }}
141+
DRAFT_NAME: ${{ steps.drafter.outputs.name }}
142+
DRAFT_URL: ${{ steps.drafter.outputs.html_url }}
143+
DRAFT_OUTCOME: ${{ steps.drafter.outcome }}
144+
BRANCH: ${{ github.event.pull_request.base.ref || github.ref_name }}
145+
run: |
146+
set -euo pipefail
147+
{
148+
echo "## Release Drafter Result"
149+
echo ""
150+
echo "- Branch: \`${BRANCH}\`"
151+
echo "- Step outcome: \`${DRAFT_OUTCOME}\`"
152+
} >> "$GITHUB_STEP_SUMMARY"
153+
if [[ -z "${DRAFT_ID:-}" ]]; then
154+
{
155+
echo "- Status: ⚠️ **No draft created/updated**"
156+
echo ""
157+
echo "release-drafter ran but did not produce a draft release id."
158+
echo "Common causes: GitHub API rate limit exhausted, no prior"
159+
echo "release matched the commitish/range/tag-prefix filters, or"
160+
echo "the action errored. Check the previous step's logs."
161+
} >> "$GITHUB_STEP_SUMMARY"
162+
echo "::warning title=Release draft missing::release-drafter produced no draft for branch ${BRANCH}. Step outcome was '${DRAFT_OUTCOME}'. See workflow summary."
163+
else
164+
{
165+
echo "- Status: ✅ Draft maintained"
166+
echo "- Tag: \`${DRAFT_TAG}\`"
167+
echo "- Name: ${DRAFT_NAME}"
168+
echo "- URL: ${DRAFT_URL}"
169+
} >> "$GITHUB_STEP_SUMMARY"
170+
echo "Draft ${DRAFT_TAG} (id ${DRAFT_ID}) maintained for branch ${BRANCH}: ${DRAFT_URL}"
171+
fi

grails-datamapping-core-test/src/test/groovy/org/grails/datastore/gorm/OrderBySpec.groovy

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,40 @@ class OrderBySpec extends GrailsDataTckSpec<GrailsDataCoreTckManager> {
7474
then:
7575
45 == results[0].age
7676
}
77+
78+
def 'Test multi-field sort ascending with a Map passed to list()'() {
79+
given: 'additional people sharing an age so the secondary sort field is observable'
80+
new TestEntity(name: "Amy", age: 40, child: new ChildEntity(name: "Amy Child")).save()
81+
new TestEntity(name: "Zoe", age: 40, child: new ChildEntity(name: "Zoe Child")).save()
82+
83+
when: 'sorting by age ascending and then name ascending'
84+
def results = TestEntity.list(sort: [age: 'asc', name: 'asc'])
85+
86+
then: 'entries with the same age are ordered by name ascending'
87+
results*.name == ["Amy", "Bob", "Zoe", "Fred", "Barney", "Frank", "Joe", "Ernie"]
88+
}
89+
90+
def 'Test multi-field sort with mixed directions in a Map passed to list()'() {
91+
given: 'additional people sharing an age so the secondary sort field is observable'
92+
new TestEntity(name: "Amy", age: 40, child: new ChildEntity(name: "Amy Child")).save()
93+
new TestEntity(name: "Zoe", age: 40, child: new ChildEntity(name: "Zoe Child")).save()
94+
95+
when: 'sorting by age ascending and then name descending'
96+
def results = TestEntity.list(sort: [age: 'asc', name: 'desc'])
97+
98+
then: 'entries with the same age are ordered by name descending'
99+
results*.name == ["Zoe", "Bob", "Amy", "Fred", "Barney", "Frank", "Joe", "Ernie"]
100+
}
101+
102+
def 'Test multi-field sort with a Map passed to a dynamic finder'() {
103+
given: 'additional people sharing an age so the secondary sort field is observable'
104+
new TestEntity(name: "Amy", age: 40, child: new ChildEntity(name: "Amy Child")).save()
105+
new TestEntity(name: "Zoe", age: 40, child: new ChildEntity(name: "Zoe Child")).save()
106+
107+
when: 'sorting by age ascending and then name descending via a dynamic finder'
108+
def results = TestEntity.findAllByAgeGreaterThanEquals(40, [sort: [age: 'asc', name: 'desc']])
109+
110+
then: 'the multi-field Map sort is applied to the dynamic finder results'
111+
results*.name == ["Zoe", "Bob", "Amy", "Fred", "Barney", "Frank", "Joe", "Ernie"]
112+
}
77113
}

grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/config/groovy/DefaultMappingConfigurationBuilder.groovy

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package org.grails.datastore.mapping.config.groovy
2020

2121
import groovy.transform.CompileStatic
22+
import groovy.util.logging.Slf4j
2223

2324
import org.springframework.beans.MutablePropertyValues
2425
import org.springframework.validation.DataBinder
@@ -31,6 +32,7 @@ import org.grails.datastore.mapping.reflect.NameUtils
3132
* @author Graeme Rocher
3233
* @since 1.0
3334
*/
35+
@Slf4j
3436
class DefaultMappingConfigurationBuilder implements MappingConfigurationBuilder {
3537

3638
public static final String VERSION_KEY = 'VERSION_KEY'
@@ -45,9 +47,38 @@ class DefaultMappingConfigurationBuilder implements MappingConfigurationBuilder
4547
propertyClass.metaClass.propertyMissing = { String name, val -> }
4648
}
4749

50+
/**
51+
* Merges the builder's own property map (populated by {@link #invokeMethod}
52+
* during evaluation of the mapping/constraints closures) with
53+
* {@code target.propertyConfigs} (populated by alternate paths such as
54+
* {@code Entity.property(name, Map)} or constraint evaluators that write
55+
* directly to the entity's property configs).
56+
*
57+
* <p>When the same key is present in both maps, the builder's instance
58+
* wins. This protects mapping-configured settings such as
59+
* {@code index: true} and {@code indexAttributes} from being silently
60+
* overwritten by a less-configured instance in {@code propertyConfigs}.
61+
*
62+
* <p>The dual-store design (this builder's {@code properties} field vs.
63+
* the entity's {@code propertyConfigs}) is a structural legacy; collapsing
64+
* the two into a single canonical store would let this method go away.
65+
* See <a href="https://github.com/apache/grails-core/issues/15680">#15680</a>.
66+
*/
4867
Map<String, Property> getProperties() {
4968
if (!target.propertyConfigs.isEmpty()) {
50-
properties.putAll(target.propertyConfigs)
69+
for (Map.Entry entry : target.propertyConfigs.entrySet()) {
70+
if (properties.containsKey(entry.key)) {
71+
if (log.isDebugEnabled()) {
72+
log.debug("Property '{}' is configured in both the mapping closure and via Entity.propertyConfigs " +
73+
'(typically a constraint evaluator or direct Entity.property() call). The mapping-side ' +
74+
'configuration takes precedence; the propertyConfigs entry is being ignored to avoid ' +
75+
'overwriting mapping-set fields such as index/indexAttributes. See grails-core#15680.',
76+
entry.key)
77+
}
78+
} else {
79+
properties.put(entry.key, entry.value)
80+
}
81+
}
5182
}
5283
return properties
5384
}

0 commit comments

Comments
 (0)