jdaugherty commented on code in PR #15805: URL: https://github.com/apache/grails-core/pull/15805#discussion_r4122934418
########## grails-skills/developer/skills/grails-developer/SKILL.md: ########## @@ -0,0 +1,870 @@ +--- +name: grails-developer +description: Comprehensive guide for current Grails development, covering web applications, REST APIs, GORM, controllers, services, views, plugins, and testing with Spock and Geb +license: Apache-2.0 +--- +<!-- +SPDX-License-Identifier: Apache-2.0 + +Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements; and to You under the Apache License, Version 2.0. +--> + +## What I Do + +- Provide detailed guidance for building current Grails web applications and REST APIs. +- Assist with GORM for data modeling, controllers for request handling, services for business logic, and views (GSP, JSON, Markup). +- Support testing with Spock 2.4 (unit/integration tests) and Geb for browser automation. Review Comment: Removed. The Technology Stack section still gives the Spock version. ########## grails-skills/upgrade-guide-8/skills/grails-8-upgrade/SKILL.md: ########## @@ -0,0 +1,336 @@ +--- +name: grails-8-upgrade +description: Guide for upgrading Grails applications from Grails 7.x to Grails 8, covering Java 21, Spring Boot 4.1, Spring Framework 7, dependency management, Micronaut, Jackson 3, Hibernate 7, TagLibs, testing, content negotiation, and validation behavior changes +license: Apache-2.0 +--- + +<!-- +SPDX-License-Identifier: Apache-2.0 + +Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements; and to You under the Apache License, Version 2.0. +--> + +## What I Do + +- Guide upgrades of Grails applications from Grails 7.x to Grails 8. +- Turn the Grails 8 upgrade guide into a practical migration checklist. +- Identify Spring Boot 4.1, Spring Framework 7, Jackson 3, Gradle platform, Micronaut, Hibernate, TagLib, testing, validation, and content negotiation changes that can break an application. +- Keep migration work focused on public application behavior, not internal Grails framework implementation details. + +## When to Use Me + +Activate this skill when: + +- Updating an application to Grails 8. +- Reviewing a Grails 8 upgrade branch or pull request. +- Fixing tests, build failures, runtime failures, or behavior changes after moving from Grails 7.x to Grails 8. +- Deciding whether an upgrade should remain on Hibernate 5 or opt in to Hibernate 7. +- Updating application build files, configuration, custom Spring Boot integration, TagLibs, JSON rendering, or content negotiation behavior for Grails 8. + +## Primary Sources + +Use these repository docs as the source of truth before changing an application: Review Comment: Replaced them with the published docs, as `https://grails.apache.org/docs/<version>/...` URLs. The skill tells the agent to use the Grails 8 version the application is upgrading to, such as `grailsVersion` in `gradle.properties`, or `snapshot`. I checked every page and anchor against the 8.0.0-RC1 docs. `conf/micronaut.adoc` isn't published because it isn't in `toc.yml`, so that row points at the upgrade guide's Micronaut section. ########## grails-skills/upgrade-guide-8/skills/grails-8-upgrade/SKILL.md: ########## @@ -0,0 +1,336 @@ +--- +name: grails-8-upgrade +description: Guide for upgrading Grails applications from Grails 7.x to Grails 8, covering Java 21, Spring Boot 4.1, Spring Framework 7, dependency management, Micronaut, Jackson 3, Hibernate 7, TagLibs, testing, content negotiation, and validation behavior changes +license: Apache-2.0 +--- + +<!-- +SPDX-License-Identifier: Apache-2.0 + +Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements; and to You under the Apache License, Version 2.0. +--> + +## What I Do + +- Guide upgrades of Grails applications from Grails 7.x to Grails 8. +- Turn the Grails 8 upgrade guide into a practical migration checklist. +- Identify Spring Boot 4.1, Spring Framework 7, Jackson 3, Gradle platform, Micronaut, Hibernate, TagLib, testing, validation, and content negotiation changes that can break an application. +- Keep migration work focused on public application behavior, not internal Grails framework implementation details. + +## When to Use Me + +Activate this skill when: + +- Updating an application to Grails 8. +- Reviewing a Grails 8 upgrade branch or pull request. +- Fixing tests, build failures, runtime failures, or behavior changes after moving from Grails 7.x to Grails 8. +- Deciding whether an upgrade should remain on Hibernate 5 or opt in to Hibernate 7. +- Updating application build files, configuration, custom Spring Boot integration, TagLibs, JSON rendering, or content negotiation behavior for Grails 8. + +## Primary Sources + +Use these repository docs as the source of truth before changing an application: + +| Path | Use for | +|------|---------| +| `grails-doc/src/en/guide/upgrading/upgrading80x.adoc` | Main Grails 7 to Grails 8 upgrade guide | +| `grails-doc/src/en/guide/introduction/whatsNew.adoc` | Grails 8 feature and platform overview | +| `grails-doc/src/en/guide/introduction/whatsNew/dependencyUpgrades.adoc` | Current platform dependency baseline | +| `grails-doc/src/en/ref/Dependency Versions.adoc` | BOM variants and dependency-version entry points | +| `grails-doc/src/en/guide/conf/micronaut.adoc` | Micronaut BOM usage, Hibernate-specific Micronaut BOMs, and JDK 25 requirements | +| `grails-doc/src/en/guide/theWebLayer/contentNegotiation.adoc` | MIME defaults and Accept header behavior | +| `grails-data-hibernate7/docs/src/docs/asciidoc/introduction/upgradeNotes.adoc` | Hibernate 7 GORM query and tenant-schema notes | +| `grails-data-hibernate7/dbmigration/README.md` | Database migration plugin version line for Grails 8 and Hibernate 7 | + +When checking published docs, use explicit Grails 8 URLs such as `https://grails.apache.org/docs/8.0.0-M1/guide/upgrading.html` or `https://grails.apache.org/docs/snapshot/guide/upgrading.html`. Do not assume `https://grails.apache.org/docs/latest/guide/upgrading.html` points at Grails 8. + +## Upgrade Baseline + +Start every Grails 8 upgrade by checking these platform requirements: + +- Use JDK 21 or later to build and run ordinary Grails 8 applications. +- Use JDK 25 or later if the application uses `grails-micronaut`, `micronaut-http-client`, or other Micronaut features. +- Update the Gradle wrapper to the Grails 8 managed line. Current 8.0.x snapshot docs use Gradle 9.7.1, while milestone docs may show an earlier Gradle 9.x version. +- Expect Spring Boot 4.1.x, Spring Framework 7.0.x, Spring Security 7.1.x, Spring Data 2026.0.x, Micrometer 1.17.x, Jackson 3.1.x, Tomcat 11.0.x, and Jakarta Servlet 6.1. +- Keep using `jakarta.*` APIs. Do not reintroduce `javax.*` packages. +- Add `runtimeOnly 'org.springframework.boot:spring-boot-properties-migrator'` temporarily during the migration, boot once, fix reported configuration properties, then remove it. + +## Migration Workflow + +Use this order so failures are isolated and reversible: + +1. Move the build, CI, and deployment runtime to the required JDK. +2. Update the Grails version and regenerate or compare a fresh Grails 8 application for build-file conventions. +3. Boot with the Spring Boot properties migrator and fix configuration warnings. +4. Fix dependency management and starter changes before code changes. +5. Compile and fix direct Spring Boot, Spring Framework, Jackson, Hibernate, and testing API removals. +6. Run application tests and manually exercise public behavior that depends on rendering, validation, content negotiation, persistence, and custom Spring integration. +7. Remove temporary migration dependencies and compatibility toggles once the application is clean. + +## Build and Dependency Management + +Grails 8 no longer applies the `io.spring.dependency-management` plugin by default. + +- Grails now uses Gradle native `platform()` dependency management with the Grails BOM. +- The Grails Gradle Plugin auto-applies the selected Grails BOM to declarable configurations. +- Existing version overrides in `gradle.properties` and `ext['property.version']` still work through the bundled `org.apache.grails.gradle.bom-property-overrides` plugin. +- Replace `grails { springDependencyManagement = false }` with `grails { bom = null }` for new builds that intentionally opt out. +- If Spring DM was used to pin arbitrary dependencies, replace it with direct dependencies or Gradle `resolutionStrategy.force` where a transitive version must be forced. +- If strict BOM behavior is required, explicitly use `enforcedPlatform("org.apache.grails:grails-bom:$grailsVersion")`. + +Spring Boot 4 renamed common starters: + +| Spring Boot 3 starter | Spring Boot 4 starter | +|-----------------------|-----------------------| +| `spring-boot-starter-web` | `spring-boot-starter-webmvc` | +| `spring-boot-starter-aop` | `spring-boot-starter-aspectj` | +| `spring-boot-starter-web-services` | `spring-boot-starter-webservices` | +| `spring-boot-starter-oauth2-authorization-server` | `spring-boot-starter-security-oauth2-authorization-server` | +| `spring-boot-starter-oauth2-client` | `spring-boot-starter-security-oauth2-client` | +| `spring-boot-starter-oauth2-resource-server` | `spring-boot-starter-security-oauth2-resource-server` | + +For WAR deployment to an external servlet container, replace `providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'` with `providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat-runtime'`. + +Undertow is not available in Grails 8 until Undertow supports Servlet 6.1. Use Tomcat or Jetty. Review Comment: It was. The skill now matches what Grails Forge generates for `--servlet=undertow`: `implementation 'org.apache.grails:grails-undertow'` in place of `spring-boot-starter-undertow`, with no `spring-boot-starter-tomcat` or `spring-boot-tomcat`. I checked that against a generated 8.0.0-RC1 app. It also says `server.undertow.*` properties keep working and that `max-http-post-size` now defaults to 2MB. ########## build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/AgentSkillsPlugin.groovy: ########## @@ -0,0 +1,130 @@ +/* + * 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.apache.grails.buildsrc + +import groovy.transform.CompileStatic + +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.CopySpec +import org.gradle.api.file.Directory +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.Sync +import org.gradle.api.tasks.TaskProvider + +import static org.apache.grails.buildsrc.GradleUtils.lookupProperty + +/** + * Packages the agent skills of a project into its jar with the SkillsJars layout. + * + * <p>Each directory under {@code skills/} is one skill: a {@code SKILL.md} whose frontmatter + * {@code name} is the directory name, and any files it ships beside it. The skills are packaged at + * {@code META-INF/skills/<githubSlug>/<skill>/}, where {@code githubSlug} is the same + * {@code <org>/<repo>} the publish plugin writes into the POM, so SkillsJars tooling extracts the + * jar the same way it would a SkillsJars catalog jar of that repository. + */ +@CompileStatic +class AgentSkillsPlugin implements Plugin<Project> { + + static final String SKILLS_DIRECTORY = 'skills' + static final String SKILLS_RESOURCE_ROOT = 'META-INF/skills' + static final String SKILL_FILE = 'SKILL.md' + static final String DEFAULT_GITHUB_SLUG = 'apache/grails-core' + + @Override + void apply(Project project) { + project.plugins.withId('java') { + configureSkillResources(project) + } + } + + private static void configureSkillResources(Project project) { + Directory skillsDirectory = project.layout.projectDirectory.dir(SKILLS_DIRECTORY) + String projectPath = project.path + // Checked while configuring: with nothing to copy the task below is skipped as NO-SOURCE, + // and the project would publish an empty jar. + if (!skillDirectories(skillsDirectory.asFile)) { + throw new GradleException( + "${projectPath} applies the agent skills plugin but has no skills: expected ${SKILLS_DIRECTORY}/<skill>/${SKILL_FILE}" + ) + } + + TaskProvider<Sync> skillResources = project.tasks.register('agentSkillResources', Sync) + skillResources.configure { Sync task -> + task.group = 'build' + task.description = "Packages the agent skills under ${SKILLS_DIRECTORY}/ into ${SKILLS_RESOURCE_ROOT}/." + task.from(skillsDirectory) { CopySpec spec -> + spec.into(project.provider { + "${SKILLS_RESOURCE_ROOT}/${lookupProperty(project, 'githubSlug', DEFAULT_GITHUB_SLUG)}" as String + }) + } + task.into(project.layout.buildDirectory.dir('generated/agent-skills')) + task.doFirst { + validateSkills(projectPath, skillsDirectory.asFile) + } + } + + project.extensions.getByType(SourceSetContainer).named(SourceSet.MAIN_SOURCE_SET_NAME) { SourceSet sourceSet -> + sourceSet.resources.srcDir(skillResources) + } + } + + /** + * A skill agents cannot load must not be published: every directory under {@code skills/} needs a + * {@code SKILL.md} whose frontmatter names the directory, which is how agents identify it. + */ + static void validateSkills(String projectPath, File skillsDirectory) { + skillDirectories(skillsDirectory).each { File skill -> + File skillFile = new File(skill, SKILL_FILE) + if (!skillFile.file) { + throw new GradleException("Agent skill '${skill.name}' of ${projectPath} has no ${SKILL_FILE}") Review Comment: Changed. ########## build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/AgentSkillsPlugin.groovy: ########## @@ -0,0 +1,130 @@ +/* + * 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.apache.grails.buildsrc + +import groovy.transform.CompileStatic + +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.CopySpec +import org.gradle.api.file.Directory +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.Sync +import org.gradle.api.tasks.TaskProvider + +import static org.apache.grails.buildsrc.GradleUtils.lookupProperty + +/** + * Packages the agent skills of a project into its jar with the SkillsJars layout. + * + * <p>Each directory under {@code skills/} is one skill: a {@code SKILL.md} whose frontmatter + * {@code name} is the directory name, and any files it ships beside it. The skills are packaged at + * {@code META-INF/skills/<githubSlug>/<skill>/}, where {@code githubSlug} is the same + * {@code <org>/<repo>} the publish plugin writes into the POM, so SkillsJars tooling extracts the + * jar the same way it would a SkillsJars catalog jar of that repository. + */ +@CompileStatic +class AgentSkillsPlugin implements Plugin<Project> { + + static final String SKILLS_DIRECTORY = 'skills' + static final String SKILLS_RESOURCE_ROOT = 'META-INF/skills' + static final String SKILL_FILE = 'SKILL.md' + static final String DEFAULT_GITHUB_SLUG = 'apache/grails-core' + + @Override + void apply(Project project) { + project.plugins.withId('java') { + configureSkillResources(project) + } + } + + private static void configureSkillResources(Project project) { + Directory skillsDirectory = project.layout.projectDirectory.dir(SKILLS_DIRECTORY) + String projectPath = project.path + // Checked while configuring: with nothing to copy the task below is skipped as NO-SOURCE, + // and the project would publish an empty jar. + if (!skillDirectories(skillsDirectory.asFile)) { + throw new GradleException( + "${projectPath} applies the agent skills plugin but has no skills: expected ${SKILLS_DIRECTORY}/<skill>/${SKILL_FILE}" + ) + } + + TaskProvider<Sync> skillResources = project.tasks.register('agentSkillResources', Sync) + skillResources.configure { Sync task -> + task.group = 'build' + task.description = "Packages the agent skills under ${SKILLS_DIRECTORY}/ into ${SKILLS_RESOURCE_ROOT}/." + task.from(skillsDirectory) { CopySpec spec -> + spec.into(project.provider { + "${SKILLS_RESOURCE_ROOT}/${lookupProperty(project, 'githubSlug', DEFAULT_GITHUB_SLUG)}" as String + }) + } + task.into(project.layout.buildDirectory.dir('generated/agent-skills')) + task.doFirst { + validateSkills(projectPath, skillsDirectory.asFile) + } + } + + project.extensions.getByType(SourceSetContainer).named(SourceSet.MAIN_SOURCE_SET_NAME) { SourceSet sourceSet -> + sourceSet.resources.srcDir(skillResources) + } + } + + /** + * A skill agents cannot load must not be published: every directory under {@code skills/} needs a + * {@code SKILL.md} whose frontmatter names the directory, which is how agents identify it. + */ + static void validateSkills(String projectPath, File skillsDirectory) { + skillDirectories(skillsDirectory).each { File skill -> + File skillFile = new File(skill, SKILL_FILE) + if (!skillFile.file) { + throw new GradleException("Agent skill '${skill.name}' of ${projectPath} has no ${SKILL_FILE}") + } + String declaredName = frontmatterValue(skillFile, 'name') + if (declaredName != skill.name) { + throw new GradleException( Review Comment: Changed. The spec now checks the exception type for each validation failure. ########## build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/AgentSkillsPlugin.groovy: ########## @@ -0,0 +1,130 @@ +/* + * 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.apache.grails.buildsrc + +import groovy.transform.CompileStatic + +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.CopySpec +import org.gradle.api.file.Directory +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.Sync +import org.gradle.api.tasks.TaskProvider + +import static org.apache.grails.buildsrc.GradleUtils.lookupProperty + +/** + * Packages the agent skills of a project into its jar with the SkillsJars layout. + * + * <p>Each directory under {@code skills/} is one skill: a {@code SKILL.md} whose frontmatter + * {@code name} is the directory name, and any files it ships beside it. The skills are packaged at + * {@code META-INF/skills/<githubSlug>/<skill>/}, where {@code githubSlug} is the same + * {@code <org>/<repo>} the publish plugin writes into the POM, so SkillsJars tooling extracts the + * jar the same way it would a SkillsJars catalog jar of that repository. + */ +@CompileStatic +class AgentSkillsPlugin implements Plugin<Project> { + + static final String SKILLS_DIRECTORY = 'skills' + static final String SKILLS_RESOURCE_ROOT = 'META-INF/skills' + static final String SKILL_FILE = 'SKILL.md' + static final String DEFAULT_GITHUB_SLUG = 'apache/grails-core' + + @Override + void apply(Project project) { + project.plugins.withId('java') { + configureSkillResources(project) + } + } + + private static void configureSkillResources(Project project) { + Directory skillsDirectory = project.layout.projectDirectory.dir(SKILLS_DIRECTORY) + String projectPath = project.path + // Checked while configuring: with nothing to copy the task below is skipped as NO-SOURCE, + // and the project would publish an empty jar. + if (!skillDirectories(skillsDirectory.asFile)) { + throw new GradleException( + "${projectPath} applies the agent skills plugin but has no skills: expected ${SKILLS_DIRECTORY}/<skill>/${SKILL_FILE}" + ) + } + + TaskProvider<Sync> skillResources = project.tasks.register('agentSkillResources', Sync) Review Comment: Inlined into `register`. ########## grails-skills/developer/skills/grails-developer/SKILL.md: ########## @@ -0,0 +1,870 @@ +--- +name: grails-developer +description: Comprehensive guide for current Grails development, covering web applications, REST APIs, GORM, controllers, services, views, plugins, and testing with Spock and Geb Review Comment: Removed "current" here and in the other places it described Grails: "What I Do", "When to Use Me", and the tech-stack heading. ########## build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/AgentSkillsPlugin.groovy: ########## @@ -0,0 +1,130 @@ +/* + * 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.apache.grails.buildsrc + +import groovy.transform.CompileStatic + +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.CopySpec +import org.gradle.api.file.Directory +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.Sync +import org.gradle.api.tasks.TaskProvider + +import static org.apache.grails.buildsrc.GradleUtils.lookupProperty + +/** + * Packages the agent skills of a project into its jar with the SkillsJars layout. + * + * <p>Each directory under {@code skills/} is one skill: a {@code SKILL.md} whose frontmatter + * {@code name} is the directory name, and any files it ships beside it. The skills are packaged at + * {@code META-INF/skills/<githubSlug>/<skill>/}, where {@code githubSlug} is the same + * {@code <org>/<repo>} the publish plugin writes into the POM, so SkillsJars tooling extracts the + * jar the same way it would a SkillsJars catalog jar of that repository. + */ +@CompileStatic +class AgentSkillsPlugin implements Plugin<Project> { + + static final String SKILLS_DIRECTORY = 'skills' + static final String SKILLS_RESOURCE_ROOT = 'META-INF/skills' + static final String SKILL_FILE = 'SKILL.md' + static final String DEFAULT_GITHUB_SLUG = 'apache/grails-core' + + @Override + void apply(Project project) { + project.plugins.withId('java') { + configureSkillResources(project) + } + } + + private static void configureSkillResources(Project project) { + Directory skillsDirectory = project.layout.projectDirectory.dir(SKILLS_DIRECTORY) + String projectPath = project.path + // Checked while configuring: with nothing to copy the task below is skipped as NO-SOURCE, + // and the project would publish an empty jar. + if (!skillDirectories(skillsDirectory.asFile)) { + throw new GradleException( Review Comment: Changed to `InvalidUserDataException`, along with the other two. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
