Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b0e0990
Make SiteMesh 3 the default GSP layout, mutually exclusive with grail…
codeconsole Jun 2, 2026
769e971
Merge branch 'deps/sitemesh3-3.3.0-snapshot' into feat/sitemesh3-defa…
jdaugherty Jun 3, 2026
d561d7d
Undo revert
jdaugherty Jun 3, 2026
9784068
Merge branch '8.0.x' into feat/sitemesh3-default-layout
jamesfredley Jun 3, 2026
a553b47
Model the GSP layout choice as a one-of option to fix the forge UI gl…
codeconsole Jun 4, 2026
021059d
Run CI test apps against SiteMesh 3 by default
codeconsole Jun 5, 2026
920c12c
Fix empty SiteMesh 3 output for undecorated GSP responses
codeconsole Jun 6, 2026
81d9147
Fix SiteMesh 3 applyLayout for embedded/nested field rendering
codeconsole Jun 6, 2026
6178aeb
Drop redundant grails-layout from starter-web test apps
codeconsole Jun 6, 2026
8a1fbcb
Don't apply a SiteMesh 3 layout to template/partial renders
codeconsole Jun 6, 2026
8b5db52
Default test apps to SiteMesh 3, opt into SiteMesh 2 via flag
codeconsole Jun 10, 2026
692ee12
Apply gspLayout option in the project generation endpoints
codeconsole Jun 10, 2026
852d132
Drop redundant user-agent param from the GitHub create endpoint
codeconsole Jun 10, 2026
db817eb
Fix if/else spacing in the SiteMesh test-app build files
codeconsole Jun 10, 2026
51d5e88
Make SiteMesh 3 the silent forge default, SiteMesh 2 a selectable fea…
codeconsole Jun 10, 2026
bb94c8c
Merge branch '8.0.x' into feat/sitemesh3-default-layout
jamesfredley Jun 10, 2026
a61691e
Re-enable the gsp-sitemesh3 test example
codeconsole Jun 10, 2026
6e83b25
Fix SiteMesh 3 layout chaining, error-page decoration and JSP rendering
codeconsole Jun 10, 2026
e7390ac
Drop SiteMesh 3 plugin workarounds superseded by the updated starter
codeconsole Jun 11, 2026
53ee6ca
Fix stale SiteMesh toggle in the hibernate7 test examples
codeconsole Jun 11, 2026
3589ba6
Don't mis-slice head content around title-prefixed elements
codeconsole Jun 11, 2026
751a937
Match SiteMesh 2's default layout config and application fallback
codeconsole Jun 11, 2026
159f812
Run the jetty and scaffolding examples against SiteMesh 3 by default
codeconsole Jun 11, 2026
8e6a989
Publish grails-gsp-spring-boot and re-enable its example
codeconsole Jun 11, 2026
7040b86
Support the full applyLayout attribute surface under SiteMesh 3
codeconsole Jun 11, 2026
c7a29df
Merge remote-tracking branch 'apache/8.0.x' into feat/sitemesh3-defau…
codeconsole Jun 11, 2026
14da8e4
Apply the published-module build conventions to grails-gsp-spring-boot
codeconsole Jun 11, 2026
5e636fd
Leave the view resolver unwrapped when the SiteMesh 3 beans are absent
codeconsole Jun 11, 2026
536da4d
Defer to SiteMesh 2's layout resolver instead of double-wrapping it
codeconsole Jun 11, 2026
cb6edf7
Merge branch '8.0.x' into feat/sitemesh3-default-layout
codeconsole Jun 16, 2026
8ae337f
Merge branch '8.0.x' into feat/sitemesh3-default-layout
jdaugherty Jun 17, 2026
6de6f5d
Merge branch '8.0.x' into feat/sitemesh3-default-layout
codeconsole Jun 17, 2026
3381711
Add a CI lane that runs the test suite against SiteMesh 2
jamesfredley Jun 17, 2026
d7667d5
Merge branch '8.0.x' into feat/sitemesh3-default-layout
jamesfredley Jun 17, 2026
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
127 changes: 127 additions & 0 deletions .github/workflows/sitemesh2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# 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
#
# https://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.

# SiteMesh 3 is the default GSP layout engine. This separate lane exercises the
# legacy SiteMesh 2 (grails-layout) path so that regressions affecting apps that
# still rely on it are caught. Setting SITEMESH2_TESTING_ENABLED=true flips the
# starter, the uber unit suite and the functional test examples back onto
# :grails-layout instead of :grails-sitemesh3.
name: "SiteMesh 2 Compatibility"
on:
workflow_dispatch:
schedule:
- cron: '0 6 * * 1' # Weekly, Mondays 06:00 UTC
push:
branches:
- '[0-9]+.[0-9]+.x'
- '8.0.x-hibernate7.*'
paths:
- 'grails-gsp/**'
- 'grails-test-suite-uber/**'
- 'grails-test-examples/**'
- 'grails-dependencies/**'
- '.github/workflows/sitemesh2.yml'
pull_request:
paths:
- 'grails-gsp/**'
- 'grails-test-suite-uber/**'
- 'grails-test-examples/**'
- 'grails-dependencies/**'
- '.github/workflows/sitemesh2.yml'
# Queue jobs - cancel in-progress PR runs when new commits pushed, but allow branch builds to complete
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
coreTests:
name: "SiteMesh 2 Core Tests (Java ${{ matrix.java }})"
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
strategy:
fail-fast: false
matrix:
java: [ 21 ]
runs-on: ubuntu-24.04
env:
SITEMESH2_TESTING_ENABLED: 'true'
steps:
- name: "Output Agent IP" # in the event RAO blocks this agent, this can be used to debug it
run: curl -s https://api.ipify.org
- name: "📥 Checkout repository"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: "☕️ Setup JDK"
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: liberica
java-version: ${{ matrix.java }}
- name: "🐘 Setup Gradle"
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use)
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
- name: "🔍 Setup TestLens"
uses: testlens-app/setup-testlens@d96a555133c275a00949d2cc77b70fe9a4242ebf # v1.9.2
- name: "🔨 Run Core Tests against SiteMesh 2"
# --rerun-tasks forces the tests to execute against the grails-layout
# classpath instead of reusing outputs cached from a SiteMesh 3 build.
run: >
./gradlew build
--continue
--rerun-tasks
--stacktrace
-PonlyCoreTests
-PskipCodeStyle
functionalTests:
name: "SiteMesh 2 Functional Tests (Java ${{ matrix.java }}, indy=${{ matrix.indy }})"
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
strategy:
fail-fast: false
matrix:
java: [ 21 ]
indy: [ false ]
runs-on: ubuntu-24.04
env:
SITEMESH2_TESTING_ENABLED: 'true'
steps:
- name: "Output Agent IP" # in the event RAO blocks this agent, this can be used to debug it
run: curl -s https://api.ipify.org
- name: "📥 Checkout repository"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: "☕️ Setup JDK"
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: liberica
java-version: ${{ matrix.java }}
- name: "🐘 Setup Gradle"
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use)
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
- name: "🔍 Setup TestLens"
uses: testlens-app/setup-testlens@d96a555133c275a00949d2cc77b70fe9a4242ebf # v1.9.2
- name: "🏃 Run Functional Tests against SiteMesh 2"
# --rerun-tasks forces the tests to execute against the grails-layout
# classpath instead of reusing outputs cached from a SiteMesh 3 build.
run: >
./gradlew bootJar check
--continue
--rerun-tasks
--stacktrace
-PgebAtCheckWaiting
-PgrailsIndy=${{ matrix.indy }}
-PonlyFunctionalTests
-PskipCodeStyle
-PskipHibernate5Tests
-PskipHibernate7Tests
-PskipMongodbTests
1 change: 1 addition & 0 deletions gradle/publish-root-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def publishedProjects = [
'grails-geb',
'grails-gsp',
'grails-gsp-core',
'grails-gsp-spring-boot',
'grails-i18n',
'grails-interceptors',
'grails-layout',
Expand Down
2 changes: 1 addition & 1 deletion grails-dependencies/starter-web/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def configurations = [
':grails-events',
':grails-gsp',
':grails-interceptors',
System.getenv('SITEMESH3_TESTING_ENABLED') == 'true' ? ':grails-sitemesh3' : ':grails-layout',
System.getenv('SITEMESH2_TESTING_ENABLED') == 'true' ? ':grails-layout' : ':grails-sitemesh3',
':grails-logging',
':grails-rest-transforms',
':grails-services',
Expand Down
5 changes: 4 additions & 1 deletion grails-doc/src/en/ref/Tags - GSP/applyLayout.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,12 @@ Attributes
* `name` - The name of the layout
* `template` - (optional) The template to apply the layout to
* `url` - (optional) The URL to retrieve the content from and apply a layout to
* `action` - (optional) The action whose output to include and apply a layout to, used together with `controller`
* `controller` - (optional) The controller whose action output to include and apply a layout to
* `contentType` (optional) - The content type to use, default is "text/html"
* `encoding` (optional) - The encoding to use
* `params` (optional) - The params to pass onto the page object (retrievable with the xref:pageProperty.html[pageProperty] tag)
* `params` (optional) - The params to pass onto the page object (retrievable with the xref:pageProperty.html[pageProperty] tag); for `action`/`controller` content they are also passed as request parameters to the include
* `model` (optional) - The model (as java.util.Map) to pass to the view and layout templates
* `parse` (optional) - If `true`, forces the content to be parsed for head, title and body sections instead of reusing an already captured page


Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,22 @@
import org.grails.forge.application.ApplicationType;
import org.grails.forge.application.generator.GeneratorContext;
import org.grails.forge.build.dependencies.Dependency;
import org.grails.forge.feature.Category;
import org.grails.forge.feature.DefaultFeature;
import org.grails.forge.feature.Feature;
import org.grails.forge.feature.view.GspLayout;
import org.grails.forge.options.Options;

@Singleton
public class Sitemesh3 implements Feature {
import java.util.Set;

public Sitemesh3() {
}
/**
* Default GSP layout decorator, backed by SiteMesh 3 ({@code grails-sitemesh3}).
* Applied automatically to web applications unless another {@link GspLayout}
* (e.g. {@code grails-layout}) is explicitly selected. Not visible: SiteMesh 3
* is silently the default, mirroring how the legacy {@code grails-layout} was
* silently the default before it.
*/
@Singleton
public class Sitemesh3 extends GspLayout implements DefaultFeature {

@Override
public String getName() {
Expand All @@ -38,29 +46,30 @@ public String getName() {

@Override
public String getTitle() {
return "Sitemesh 3";
return "GSP SiteMesh 3 Layouts";
}

@Override
public String getDescription() {
return "Adds support for Sitemesh3 based layouts instead of Sitemesh 2";
return "Adds support for SiteMesh 3 based GSP layouts";
}

@Override
public void apply(GeneratorContext generatorContext) {
generatorContext.addDependency(Dependency.builder()
.groupId("org.apache.grails")
.artifactId("grails-sitemesh3")
.implementation());
public boolean isVisible() {
return false;
}

@Override
public boolean supports(ApplicationType applicationType) {
return true;
public boolean shouldApply(ApplicationType applicationType, Options options, Set<Feature> selectedFeatures) {
return supports(applicationType) &&
selectedFeatures.stream().noneMatch(GspLayout.class::isInstance);
}

@Override
public String getCategory() {
return Category.VIEW;
public void apply(GeneratorContext generatorContext) {
generatorContext.addDependency(Dependency.builder()
.groupId("org.apache.grails")
.artifactId("grails-sitemesh3")
.implementation());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import org.grails.forge.feature.DefaultFeature;
import org.grails.forge.feature.Feature;
import org.grails.forge.feature.FeatureContext;
import org.grails.forge.feature.sitemesh3.Sitemesh3;
import org.grails.forge.feature.web.GrailsWeb;
import org.grails.forge.options.Options;
import org.grails.forge.template.URLTemplate;
Expand Down Expand Up @@ -110,12 +109,6 @@ public void apply(GeneratorContext generatorContext) {
.groupId("org.apache.grails")
.artifactId("grails-gsp")
.implementation());
if (!generatorContext.isFeaturePresent(Sitemesh3.class)) {
generatorContext.addDependency(Dependency.builder()
.groupId("org.apache.grails")
.artifactId("grails-layout")
.implementation());
}

final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
generatorContext.addTemplate("mainLayout", new URLTemplate(getViewFolderPath() + "layouts/main.gsp", classLoader.getResource("gsp/main.gsp")));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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
*
* https://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.
*/
package org.grails.forge.feature.view;

import jakarta.inject.Singleton;
import org.grails.forge.application.generator.GeneratorContext;
import org.grails.forge.build.dependencies.Dependency;

/**
* Opt-in GSP layout decorator backed by the legacy SiteMesh 2 based
* {@code grails-layout} plugin. Mutually exclusive with {@code sitemesh3};
* selecting this feature replaces the default SiteMesh 3 decorator.
*/
@Singleton
public class GrailsLayout extends GspLayout {

@Override
public String getName() {
return "grails-layout";
}

@Override
public String getTitle() {
return "GSP SiteMesh 2 Layouts";
}

@Override
public String getDescription() {
return "Adds support for legacy SiteMesh 2 based GSP layouts (grails-layout) instead of SiteMesh 3";
}

@Override
public void apply(GeneratorContext generatorContext) {
generatorContext.addDependency(Dependency.builder()
.groupId("org.apache.grails")
.artifactId("grails-layout")
.implementation());
}
}
Original file line number Diff line number Diff line change
@@ -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
*
* https://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.
*/
package org.grails.forge.feature.view;

import org.grails.forge.application.ApplicationType;
import org.grails.forge.feature.Category;
import org.grails.forge.feature.OneOfFeature;

/**
* Common parent for the mutually exclusive GSP layout decorators: SiteMesh 3
* ({@code grails-sitemesh3}) and the legacy SiteMesh 2 based {@code grails-layout}.
* Because they share the same {@link #getFeatureClass()}, only one of them may be
* selected for a given application (enforced by the one-of feature validator).
*/
public abstract class GspLayout implements OneOfFeature {

@Override
public Class<?> getFeatureClass() {
return GspLayout.class;
}

@Override
public boolean supports(ApplicationType applicationType) {
return applicationType == ApplicationType.WEB || applicationType == ApplicationType.WEB_PLUGIN;
}

@Override
public String getCategory() {
return Category.VIEW;
}
}
Loading
Loading