From ff46483e4fb9a282fcf906d2c1dd29e7cba29c71 Mon Sep 17 00:00:00 2001 From: Stephen Bolton Date: Tue, 15 Jul 2025 12:24:31 +0100 Subject: [PATCH 1/9] Batch 2 resources --- .../model/field/FieldTypeResource.java | 26 +- .../java/com/dotcms/rest/ContentResource.java | 336 ++- .../api/v1/categories/CategoriesResource.java | 273 +- .../api/v1/categories/CategoryImportData.java | 48 + .../content/ContentRelationshipsResource.java | 29 +- .../api/v1/content/ContentReportResource.java | 39 +- .../rest/api/v1/content/ContentResource.java | 129 +- .../v1/content/ContentVersionResource.java | 84 +- .../api/v1/content/ResourceLinkResource.java | 68 +- .../dotimport/ContentImportResource.java | 4 +- .../v1/contenttype/ContentTypeResource.java | 77 +- .../api/v1/contenttype/FieldResource.java | 321 ++- .../v1/contenttype/FieldVariableResource.java | 257 +- .../WorkflowActionMultipartSchema.java | 31 + .../api/v1/workflow/WorkflowResource.java | 275 +- .../dotcms/rest/api/v2/tags/TagResource.java | 293 ++- .../main/webapp/WEB-INF/openapi/openapi.yaml | 2258 +++++++++++++---- .../RestEndpointAnnotationComplianceTest.java | 169 +- 18 files changed, 3811 insertions(+), 906 deletions(-) create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/categories/CategoryImportData.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowActionMultipartSchema.java diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/model/field/FieldTypeResource.java b/dotCMS/src/main/java/com/dotcms/contenttype/model/field/FieldTypeResource.java index 1dc819e08a5a..1e04772a9fbc 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/model/field/FieldTypeResource.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/model/field/FieldTypeResource.java @@ -13,12 +13,18 @@ import com.dotcms.rest.ResponseEntityView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.google.common.collect.ImmutableList; import com.liferay.portal.model.User; import javax.servlet.http.HttpServletRequest; import java.util.List; import java.util.Map; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import static com.dotcms.util.CollectionsUtils.toImmutableList; @@ -26,8 +32,9 @@ /** * This end-point provides access to information associated to dotCMS FieldType. */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/fieldTypes") -@Tag(name = "Content Type Field", description = "Content type field definitions and configuration") +@Tag(name = "Content Type Field") public class FieldTypeResource { private final WebResource webResource; @@ -43,10 +50,23 @@ public FieldTypeResource(final WebResource webresource, FieldTypeAPI fieldTypeAP this.fieldTypeAPI = fieldTypeAPI; } + @Operation( + summary = "Get field types", + description = "Retrieves all available field types in dotCMS for content type configuration" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field types retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityFieldTypeListView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")) + }) @GET @JSONP @NoCache - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) + @Produces({ MediaType.APPLICATION_JSON }) public Response getFieldTypes(@Context final HttpServletRequest req) { final InitDataObject initData = this.webResource.init(null, true, req, true, null); @@ -56,6 +76,6 @@ public Response getFieldTypes(@Context final HttpServletRequest req) { .map(FieldType::toMap) .collect(toImmutableList()); - return Response.ok( new ResponseEntityView>>( fieldTypesMap ) ).build(); + return Response.ok( new ResponseEntityFieldTypeListView( fieldTypesMap ) ).build(); } } diff --git a/dotCMS/src/main/java/com/dotcms/rest/ContentResource.java b/dotCMS/src/main/java/com/dotcms/rest/ContentResource.java index 8506d0703840..20ee5d1e26d7 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/ContentResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/ContentResource.java @@ -7,6 +7,7 @@ import com.dotcms.exception.ExceptionUtil; import com.dotcms.rendering.velocity.viewtools.content.util.ContentUtils; import com.dotcms.repackage.org.apache.commons.httpclient.HttpStatus; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.api.v1.authentication.ResponseUtil; import com.dotcms.rest.exception.ForbiddenException; import com.dotcms.rest.exception.mapper.ExceptionMapperUtil; @@ -55,6 +56,13 @@ import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; @@ -112,6 +120,7 @@ * @author Daniel Silva * @since May 25th, 2012 */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/content") @Tag(name = "Content Delivery") public class ContentResource { @@ -155,12 +164,41 @@ public class ContentResource { * * @return json array of objects. each object with inode and identifier */ + @Operation( + summary = "Search content with advanced parameters", + description = "Performs a comprehensive content search using Lucene query syntax. " + + "Supports filtering, sorting, pagination, and depth-based relationship loading. " + + "Returns structured JSON with search metadata and contentlet results." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Search completed successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntitySearchView.class))), + @ApiResponse(responseCode = "400", + description = "Invalid search parameters or malformed query", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions to access content", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error during search", + content = @Content(mediaType = "application/json")) + }) @POST @Path("/_search") @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) public Response search(@Context HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Store query in session for Query Tool portlet", required = false) @QueryParam("rememberQuery") @DefaultValue("false") final boolean rememberQuery, + @RequestBody(description = "Search criteria including query, sort, pagination and filters", + required = true, + content = @Content(schema = @Schema(implementation = SearchForm.class))) final SearchForm searchForm) throws DotSecurityException, DotDataException { final InitDataObject initData = this.webResource.init @@ -213,16 +251,46 @@ public Response search(@Context HttpServletRequest request, * @param offset how many results skip * @return json array of objects. each object with inode and identifier */ + @Operation( + summary = "Search content index with URL parameters", + description = "Performs a direct Elasticsearch index search using Lucene query syntax. " + + "Returns a simplified JSON array containing only inode and identifier for each matching contentlet. " + + "This is a lighter-weight alternative to the POST search endpoint." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Index search completed successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Invalid query syntax or parameters", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions to search index", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error during index search", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/indexsearch/{query}/sortby/{sortby}/limit/{limit}/offset/{offset}") @Produces(MediaType.APPLICATION_JSON) public Response indexSearch(@Context HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Lucene query string (e.g., '+structurename:webpagecontent +live:true')", required = true) @PathParam("query") String query, - @PathParam("sortby") String sortBy, @PathParam("limit") int limit, + @Parameter(description = "Field to sort results by (e.g., 'modDate', 'title')", required = true) + @PathParam("sortby") String sortBy, + @Parameter(description = "Maximum number of results to return", required = true) + @PathParam("limit") int limit, + @Parameter(description = "Number of results to skip for pagination", required = true) @PathParam("offset") int offset, - @PathParam("type") String type, - @PathParam("callback") String callback) + @Parameter(description = "Response format type (optional)", required = false) + @QueryParam("type") String type, + @Parameter(description = "JSONP callback function name (optional)", required = false) + @QueryParam("callback") String callback) throws DotDataException, JSONException { InitDataObject initData = webResource.init(null, request, response, false, null); @@ -260,14 +328,40 @@ public Response indexSearch(@Context HttpServletRequest request, * @param query lucene query to count on * @return a string with the count */ + @Operation( + summary = "Count content matching query", + description = "Returns the total count of contentlets matching the specified Lucene query. " + + "This is useful for pagination calculations and understanding result set sizes " + + "without retrieving the actual content data." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Count retrieved successfully", + content = @Content(mediaType = "text/plain")), + @ApiResponse(responseCode = "400", + description = "Invalid query syntax", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions to query content", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error during count operation", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/indexcount/{query}") @Produces(MediaType.TEXT_PLAIN) public Response indexCount(@Context HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Lucene query string to count matching contentlets", required = true) @PathParam("query") String query, - @PathParam("type") String type, - @PathParam("callback") String callback) throws DotDataException { + @Parameter(description = "Response format type (optional)", required = false) + @QueryParam("type") String type, + @Parameter(description = "JSONP callback function name (optional)", required = false) + @QueryParam("callback") String callback) throws DotDataException { InitDataObject initData = webResource.init(null, request, response, false, null); @@ -295,12 +389,30 @@ public Response indexCount(@Context HttpServletRequest request, * @throws DotDataException * @throws JSONException */ + @Operation( + summary = "Lock content (deprecated)", + description = "Legacy endpoint for locking content. This endpoint is deprecated and will be removed in future versions. Use the versioned v1 content API instead.", + deprecated = true + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content locked successfully (deprecated endpoint)", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions or security error", + content = @Content(mediaType = "application/json")) + }) @Deprecated @PUT @Path("/lock/{params:.*}") @Produces(MediaType.APPLICATION_JSON) public Response lockContent(@Context HttpServletRequest request, - @Context HttpServletResponse response, @PathParam("params") String params) + @Context HttpServletResponse response, + @Parameter(description = "URL parameters containing content ID or inode", required = true) + @PathParam("params") String params) throws DotDataException, JSONException { InitDataObject initData = webResource.init(params, request, response, false, null); @@ -376,11 +488,28 @@ public Response lockContent(@Context HttpServletRequest request, * @throws DotDataException * @throws JSONException */ + @Operation( + summary = "Check if content can be locked (deprecated)", + description = "Legacy endpoint for checking lock status of content. This endpoint is deprecated and will be removed in future versions. Use the versioned v1 content API instead.", + deprecated = true + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Lock status checked successfully (deprecated endpoint)", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions or security error", + content = @Content(mediaType = "application/json")) + }) @Deprecated @PUT @Path("/canLock/{params:.*}") @Produces(MediaType.APPLICATION_JSON) public Response canLockContent(@Context HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "URL parameters containing content ID or inode", required = true) @PathParam("params") String params) throws DotDataException, JSONException { @@ -474,12 +603,30 @@ public Response canLockContent(@Context HttpServletRequest request, @Context fin * @throws DotDataException * @throws JSONException */ + @Operation( + summary = "Unlock content (deprecated)", + description = "Legacy endpoint for unlocking content. This endpoint is deprecated and will be removed in future versions. Use the versioned v1 content API instead.", + deprecated = true + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content unlocked successfully (deprecated endpoint)", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions or security error", + content = @Content(mediaType = "application/json")) + }) @Deprecated @PUT @Path("/unlock/{params:.*}") @Produces(MediaType.APPLICATION_JSON) public Response unlockContent(@Context HttpServletRequest request, - @Context HttpServletResponse response, @PathParam("params") String params) + @Context HttpServletResponse response, + @Parameter(description = "URL parameters containing content ID or inode", required = true) + @PathParam("params") String params) throws DotDataException, JSONException { InitDataObject initData = webResource.init(params, request, response, false, null); @@ -557,10 +704,37 @@ public Response unlockContent(@Context HttpServletRequest request, * 3 --> The contentlet object will contain the related contentlets, which in turn will contain a list of their related contentlets * null --> Relationships will not be sent in the response */ + @Operation( + summary = "Get content by ID, inode, or query", + description = "Retrieves contentlets using flexible URL parameters. Supports content lookup by identifier, inode, or Lucene query. " + + "Includes depth-based relationship loading (0-3 levels) and supports both JSON and XML output formats. " + + "This is a legacy endpoint - prefer using the versioned v1 content API for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content retrieved successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Invalid parameters or malformed query", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions to access content", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/{params:.*}") @Produces(MediaType.APPLICATION_JSON) public Response getContent(@Context HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "URL parameters in key/value format (e.g., 'id/abc123' or 'query/+structurename:News/limit/10')", required = true) @PathParam("params") String params) { final InitDataObject initData = this.webResource.init (params, request, response, false, null); @@ -1159,14 +1333,35 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co * @throws URISyntaxException * @throws DotDataException */ + @Operation( + summary = "Create/update content with multipart data (deprecated)", + description = "Legacy endpoint for creating or updating content using multipart form data. This endpoint is deprecated and will be removed in future versions. Use the WorkflowResource#fireActionDefaultMultipart instead.", + deprecated = true + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content saved successfully (deprecated endpoint)", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Invalid request data or parameters", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions or security error", + content = @Content(mediaType = "application/json")) + }) @Deprecated @PUT @Path("/{params:.*}") - @Produces({MediaType.APPLICATION_JSON, "application/javascript", MediaType.TEXT_PLAIN}) + @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Consumes(MediaType.MULTIPART_FORM_DATA) public Response multipartPUT(@Context HttpServletRequest request, @Context HttpServletResponse response, - FormDataMultiPart multipart, @PathParam("params") String params) + @RequestBody(description = "Multipart form data containing content fields and binary files", + required = true, + content = @Content(mediaType = "multipart/form-data")) + FormDataMultiPart multipart, + @Parameter(description = "URL parameters for content creation/update", required = true) + @PathParam("params") String params) throws URISyntaxException, DotDataException { return multipartPUTandPOST(request, response, multipart, params, "PUT"); } @@ -1183,6 +1378,22 @@ public Response multipartPUT(@Context HttpServletRequest request, * @throws URISyntaxException * @throws DotDataException */ + @Operation( + summary = "Create content with multipart data (deprecated)", + description = "Legacy endpoint for creating content using multipart form data via POST. This endpoint is deprecated and will be removed in future versions. Use the WorkflowResource#fireActionDefaultMultipart instead.", + deprecated = true + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content created successfully (deprecated endpoint)", + content = @Content(mediaType = "text/plain")), + @ApiResponse(responseCode = "400", + description = "Invalid request data or parameters", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions or security error", + content = @Content(mediaType = "application/json")) + }) @Deprecated @POST @Path("/{params:.*}") @@ -1190,7 +1401,12 @@ public Response multipartPUT(@Context HttpServletRequest request, @Consumes(MediaType.MULTIPART_FORM_DATA) public Response multipartPOST(@Context HttpServletRequest request, @Context HttpServletResponse response, - FormDataMultiPart multipart, @PathParam("params") String params) + @RequestBody(description = "Multipart form data containing content fields and binary files", + required = true, + content = @Content(mediaType = "multipart/form-data")) + FormDataMultiPart multipart, + @Parameter(description = "URL parameters for content creation", required = true) + @PathParam("params") String params) throws URISyntaxException, DotDataException { return multipartPUTandPOST(request, response, multipart, params, "POST"); } @@ -1227,9 +1443,11 @@ private Response multipartPUTandPOST(final HttpServletRequest request,final Http if (mediaType.equals(MediaType.APPLICATION_JSON_TYPE) || name.equals("json")) { try { - processJSON(contentlet, part.getEntityAs(InputStream.class)); + // Read and parse JSON once to avoid "Stream closed" error + final Map jsonMap = WebResource.processJSON(part.getEntityAs(InputStream.class)); + processMap(contentlet, jsonMap); try { - binaryFieldsInput = WebResource.processJSON(part.getEntityAs(InputStream.class)).get("binary_fields").toString(); + binaryFieldsInput = jsonMap.get("binary_fields").toString(); } catch (NullPointerException npe) { //empty on purpose } @@ -1369,14 +1587,46 @@ private void processFile(final Contentlet contentlet, /** * This method has been deprecated in favor of {@link com.dotcms.rest.api.v1.workflow.WorkflowResource#fireActionDefault(HttpServletRequest, HttpServletResponse, String, String, long, SystemAction, FireActionForm)} - * @param request - * @param response - * @param params + * + * Note: The requestBody parameter is for OpenAPI documentation only and is not used in the implementation. + * The actual request body is processed from request.getInputStream() in the singlePUTandPOST method. + * The @RequestBody annotation uses required=false to maintain backward compatibility with clients + * that may not send a request body, as per OpenAPI 3.0.3 specification (default is false). + * + * @param request HTTP servlet request + * @param response HTTP servlet response + * @param params URL parameters for content update + * @param requestBody Request body parameter for OpenAPI documentation only (not used in implementation) * @deprecated * @see {@link com.dotcms.rest.api.v1.workflow.WorkflowResource#fireActionDefault(HttpServletRequest, HttpServletResponse, String, String, long, SystemAction, FireActionForm)} - * @return - * @throws URISyntaxException + * @return Response containing the operation result + * @throws URISyntaxException if URL parameters are malformed */ + @Operation( + summary = "Update content with JSON/XML/form data (deprecated)", + description = "Legacy endpoint for updating content using JSON, XML, or form-encoded data. This endpoint is deprecated and will be removed in future versions. Use the WorkflowResource#fireActionDefault instead.", + deprecated = true, + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "Content data in JSON, XML, or form format", + required = true, + content = { + @Content(mediaType = "application/json"), + @Content(mediaType = "application/xml"), + @Content(mediaType = "application/x-www-form-urlencoded") + } + ) + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content updated successfully (deprecated endpoint)", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Invalid request data or parameters", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions or security error", + content = @Content(mediaType = "application/json")) + }) @PUT @Path("/{params:.*}") @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @@ -1384,22 +1634,56 @@ private void processFile(final Contentlet contentlet, MediaType.APPLICATION_XML}) @Deprecated public Response singlePUT(@Context HttpServletRequest request, - @Context HttpServletResponse response, @PathParam("params") String params) + @Context HttpServletResponse response, + @Parameter(description = "URL parameters for content update", required = true) + @PathParam("params") String params) throws URISyntaxException { return singlePUTandPOST(request, response, params, "PUT"); } /** * This method has been deprecated in favor of {@link com.dotcms.rest.api.v1.workflow.WorkflowResource#fireActionDefault(HttpServletRequest, HttpServletResponse, String, String, long, SystemAction, FireActionForm)} - * @param request - * @param response - * @param params + * + * Note: The requestBody parameter is for OpenAPI documentation only and is not used in the implementation. + * The actual request body is processed from request.getInputStream() in the singlePUTandPOST method. + * The @RequestBody annotation uses required=false to maintain backward compatibility with clients + * that may not send a request body, as per OpenAPI 3.0.3 specification (default is false). + * + * @param request HTTP servlet request + * @param response HTTP servlet response + * @param params URL parameters for content creation + * @param requestBody Request body parameter for OpenAPI documentation only (not used in implementation) * @deprecated * @see {@link com.dotcms.rest.api.v1.workflow.WorkflowResource#fireActionDefault(HttpServletRequest, HttpServletResponse, String, String, long, SystemAction, FireActionForm)} * - * @return - * @throws URISyntaxException + * @return Response containing the operation result + * @throws URISyntaxException if URL parameters are malformed */ + @Operation( + summary = "Create content with JSON/XML/form data (deprecated)", + description = "Legacy endpoint for creating content using JSON, XML, or form-encoded data. This endpoint is deprecated and will be removed in future versions. Use the WorkflowResource#fireActionDefault instead.", + deprecated = true, + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "Content data in JSON, XML, or form format", + required = true, + content = { + @Content(mediaType = "application/json"), + @Content(mediaType = "application/xml"), + @Content(mediaType = "application/x-www-form-urlencoded") + } + ) + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content created successfully (deprecated endpoint)", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Invalid request data or parameters", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions or security error", + content = @Content(mediaType = "application/json")) + }) @Deprecated @POST @Path("/{params:.*}") @@ -1407,7 +1691,9 @@ public Response singlePUT(@Context HttpServletRequest request, @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_XML}) public Response singlePOST(@Context HttpServletRequest request, - @Context HttpServletResponse response, @PathParam("params") String params) + @Context HttpServletResponse response, + @Parameter(description = "URL parameters for content creation", required = true) + @PathParam("params") String params) throws URISyntaxException { return singlePUTandPOST(request, response, params, "POST"); } @@ -1560,7 +1846,7 @@ protected Response saveContent(Contentlet contentlet, InitDataObject init) if (type.equals("jsonp")) { String callback = init.getParamsMap().get(RESTParams.CALLBACK.getValue()); - return Response.ok(callback + "(" + result + ")", "application/javascript") + return Response.ok(callback + "(" + result + ")") .location(new URI("content/inode/" + contentlet.getInode() + "/type/jsonp/callback/" + callback)) .header("inode", contentlet.getInode()) diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/categories/CategoriesResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/categories/CategoriesResource.java index 6cb9c1d5b2c3..a865e28692d5 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/categories/CategoriesResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/categories/CategoriesResource.java @@ -4,9 +4,10 @@ import com.dotcms.exception.ExceptionUtil; import com.dotcms.repackage.com.google.common.annotations.VisibleForTesting; import com.dotcms.rest.InitDataObject; -import com.dotcms.rest.ResponseEntityView; +import com.dotcms.rest.ResponseEntityBulkResultView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.api.BulkResultView; import com.dotcms.rest.api.FailedResultView; import com.dotcms.rest.api.v1.DotObjectMapperProvider; @@ -17,7 +18,6 @@ import com.dotcms.util.PaginationUtil; import com.dotcms.util.pagination.CategoriesPaginator; import com.dotcms.util.pagination.CategoryListDTOPaginator; -import com.dotcms.util.pagination.ChildrenCategoryListDTOPaginator; import com.dotcms.util.pagination.OrderDirection; import com.dotmarketing.beans.Host; import com.dotmarketing.business.APILocator; @@ -29,31 +29,29 @@ import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.portlets.categories.business.CategoryAPI; -import com.dotmarketing.portlets.categories.business.PaginatedCategories; import com.dotmarketing.portlets.categories.model.Category; -import com.dotmarketing.portlets.categories.model.HierarchyShortCategory; import com.dotmarketing.util.ActivityLogger; import com.dotmarketing.util.Logger; import com.dotmarketing.util.PageMode; -import com.dotmarketing.util.PaginatedArrayList; import com.dotmarketing.util.UtilMethods; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.liferay.portal.model.User; import com.liferay.util.StringPool; -import io.swagger.v3.oas.annotations.ExternalDocumentation; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import io.vavr.control.Try; import java.io.BufferedReader; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; -import java.io.StringReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Date; @@ -79,16 +77,16 @@ import javax.ws.rs.core.Response; import org.apache.commons.beanutils.BeanUtils; import org.glassfish.jersey.media.multipart.FormDataContentDisposition; -import org.glassfish.jersey.media.multipart.FormDataMultiPart; -import org.glassfish.jersey.media.multipart.FormDataParam; +import javax.ws.rs.BeanParam; import org.glassfish.jersey.server.JSONP; /** * This resource provides all the different end-points associated to information and actions that * the front-end can perform on the Categories. */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/categories") -@Tag(name = "Categories", description = "Content categorization and taxonomy") +@Tag(name = "Categories") public class CategoriesResource { private final WebResource webResource; @@ -154,18 +152,27 @@ public CategoriesResource(final WebResource webresource, final PaginationUtil pa * @param showChildrenCount * @return Response */ + @Operation( + summary = "Get categories with pagination", + description = "Retrieves a paginated list of categories with optional filtering, sorting, and children count information. Supports hierarchical category navigation and management." + ) + @ApiResponse(responseCode = "200", description = "Categories retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "500", description = "Internal server error retrieving categories") @GET @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) public final Response getCategories(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - @QueryParam(PaginationUtil.FILTER) final String filter, - @QueryParam(PaginationUtil.PAGE) final int page, - @QueryParam(PaginationUtil.PER_PAGE) final int perPage, - @DefaultValue("category_name") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, - @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction, - @QueryParam("showChildrenCount") final boolean showChildrenCount) { + @Parameter(description = "Filter text to search categories") @QueryParam(PaginationUtil.FILTER) final String filter, + @Parameter(description = "Page number for pagination") @QueryParam(PaginationUtil.PAGE) final int page, + @Parameter(description = "Number of items per page") @QueryParam(PaginationUtil.PER_PAGE) final int perPage, + @Parameter(description = "Field to order results by") @DefaultValue("category_name") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, + @Parameter(description = "Sort direction (ASC or DESC)") @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction, + @Parameter(description = "Whether to include children count for each category") @QueryParam("showChildrenCount") final boolean showChildrenCount) { final InitDataObject initData = webResource.init(null, httpRequest, httpResponse, true, null); @@ -239,22 +246,32 @@ public final Response getCategories(@Context final HttpServletRequest httpReques * @param inode * @return Response */ + @Operation( + summary = "Get category children", + description = "Retrieves child categories of a specified parent category with pagination and filtering options. Can include all nested levels and parent hierarchy information." + ) + @ApiResponse(responseCode = "200", description = "Child categories retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "404", description = "Parent category not found") + @ApiResponse(responseCode = "500", description = "Internal server error retrieving child categories") @GET @Path(("/children")) @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) public final Response getChildren(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - @QueryParam(PaginationUtil.FILTER) final String filter, - @QueryParam(PaginationUtil.PAGE) final int page, - @QueryParam(PaginationUtil.PER_PAGE) final int perPage, - @DefaultValue("category_name") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, - @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction, - @QueryParam("inode") final String inode, - @QueryParam("showChildrenCount") final boolean showChildrenCount, - @QueryParam("allLevels") final boolean allLevels, - @QueryParam("parentList") final boolean parentList) { + @Parameter(description = "Filter text to search child categories") @QueryParam(PaginationUtil.FILTER) final String filter, + @Parameter(description = "Page number for pagination") @QueryParam(PaginationUtil.PAGE) final int page, + @Parameter(description = "Number of items per page") @QueryParam(PaginationUtil.PER_PAGE) final int perPage, + @Parameter(description = "Field to order results by") @DefaultValue("category_name") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, + @Parameter(description = "Sort direction (ASC or DESC)") @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction, + @Parameter(description = "Parent category inode to get children for") @QueryParam("inode") final String inode, + @Parameter(description = "Whether to include children count for each category") @QueryParam("showChildrenCount") final boolean showChildrenCount, + @Parameter(description = "Whether to include all nested levels") @QueryParam("allLevels") final boolean allLevels, + @Parameter(description = "Whether to include parent list hierarchy") @QueryParam("parentList") final boolean parentList) { final InitDataObject initData = webResource.init(null, httpRequest, httpResponse, true, null); @@ -345,18 +362,25 @@ public final Response getChildren(@Context final HttpServletRequest httpRequest, * @throws DotDataException * @throws DotSecurityException */ + @Operation( + summary = "Get category hierarchy for multiple categories", + description = "Retrieves the parent hierarchy for a set of categories specified by their keys. Returns parent lists for each category, starting from the top-level parent down to the direct parent. Categories that don't exist are ignored." + ) + @ApiResponse(responseCode = "200", description = "Category hierarchies retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = HierarchyShortCategoriesResponseView.class))) + @ApiResponse(responseCode = "400", description = "Bad request - invalid category keys form") + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "500", description = "Internal server error retrieving hierarchies") @POST @Path(("/hierarchy")) @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON}) - @Operation(operationId = "getSchemes", summary = "Get the List of Parents from set of categories", - description = "Response with the list of parents for a specific set of categories. If any of the categories" + - "does not exists then it is just ignored" - ) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) public final HierarchyShortCategoriesResponseView getHierarchy(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - final CategoryKeysForm form) throws DotDataException { + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Category keys form containing array of category keys", required = true) final CategoryKeysForm form) throws DotDataException { Logger.debug(this, () -> "Getting the List of Parents for the follow categories: " + String.join(",", form.getKeys())); @@ -374,15 +398,25 @@ public final HierarchyShortCategoriesResponseView getHierarchy(@Context final Ht * @return CategoryView */ + @Operation( + summary = "Get category by ID or key", + description = "Retrieves a specific category by its unique identifier (inode) or key. Can optionally include child count information." + ) + @ApiResponse(responseCode = "200", description = "Category retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "404", description = "Category not found") + @ApiResponse(responseCode = "500", description = "Internal server error retrieving category") @GET @JSONP @Path("/{idOrKey}") @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) public final Response getCategoryByIdOrKey(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - @PathParam("idOrKey") final String idOrKey, - @QueryParam("showChildrenCount") final boolean showChildrenCount) + @Parameter(description = "Category ID (inode) or key", required = true) @PathParam("idOrKey") final String idOrKey, + @Parameter(description = "Whether to include children count") @QueryParam("showChildrenCount") final boolean showChildrenCount) throws DotSecurityException, DotDataException { final InitDataObject initData = new WebResource.InitBuilder(webResource) @@ -410,8 +444,8 @@ public final Response getCategoryByIdOrKey(@Context final HttpServletRequest htt "Category with idOrKey: " + idOrKey + " does not exist"); } - return showChildrenCount ? Response.ok(new ResponseEntityView(this.categoryHelper.toCategoryWithChildCountView(category, user))).build() : - Response.ok(new ResponseEntityView(this.categoryHelper.toCategoryView(category, user))).build(); + return showChildrenCount ? Response.ok(new ResponseEntityCategoryWithChildCountView(this.categoryHelper.toCategoryWithChildCountView(category, user))).build() : + Response.ok(new ResponseEntityCategoryView(this.categoryHelper.toCategoryView(category, user))).build(); } /** @@ -424,13 +458,25 @@ public final Response getCategoryByIdOrKey(@Context final HttpServletRequest htt * @throws DotDataException * @throws DotSecurityException */ + @Operation( + summary = "Create new category", + description = "Creates a new category with the specified properties. The category name is required, and optionally can be associated with a specific site." + ) + @ApiResponse(responseCode = "200", description = "Category created successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "400", description = "Bad request - missing category name or invalid form data") + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "403", description = "Forbidden - insufficient permissions to create categories") + @ApiResponse(responseCode = "500", description = "Internal server error creating category") @POST @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) public final Response saveNew(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - final CategoryForm categoryForm) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Category form with name and properties", required = true) final CategoryForm categoryForm) throws DotDataException, DotSecurityException { final InitDataObject initData = new WebResource.InitBuilder(webResource) @@ -447,7 +493,7 @@ public final Response saveNew(@Context final HttpServletRequest httpRequest, "The category name is required"); try { - return Response.ok(new ResponseEntityView(this.categoryHelper.toCategoryView( + return Response.ok(new ResponseEntityCategoryView(this.categoryHelper.toCategoryView( this.fillAndSave(categoryForm, user, host, pageMode, new Category()), user))).build(); } catch (InvocationTargetException | IllegalAccessException e) { Logger.error(this, e.getMessage(), e); @@ -466,13 +512,26 @@ public final Response saveNew(@Context final HttpServletRequest httpRequest, * @throws DotDataException * @throws DotSecurityException */ + @Operation( + summary = "Update existing category", + description = "Updates an existing category identified by its inode. All category properties can be modified including name, description, and hierarchy placement." + ) + @ApiResponse(responseCode = "200", description = "Category updated successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "400", description = "Bad request - missing inode or invalid form data") + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "403", description = "Forbidden - insufficient permissions to update categories") + @ApiResponse(responseCode = "404", description = "Category not found") + @ApiResponse(responseCode = "500", description = "Internal server error updating category") @PUT @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) public final Response save(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - final CategoryForm categoryForm) throws DotDataException, DotSecurityException { + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Category form with updated properties including inode", required = true) final CategoryForm categoryForm) throws DotDataException, DotSecurityException { final InitDataObject initData = new WebResource.InitBuilder(webResource) .requestAndResponse(httpRequest, httpResponse).rejectWhenNoUser(true).init(); @@ -496,7 +555,7 @@ public final Response save(@Context final HttpServletRequest httpRequest, } try { - return Response.ok(new ResponseEntityView(this.categoryHelper.toCategoryView( + return Response.ok(new ResponseEntityCategoryView(this.categoryHelper.toCategoryView( this.fillAndSave(categoryForm, user, host, pageMode, oldCategory, new Category()), user))).build(); } catch (InvocationTargetException | IllegalAccessException e) { @@ -516,14 +575,27 @@ public final Response save(@Context final HttpServletRequest httpRequest, * @throws DotDataException * @throws DotSecurityException */ + @Operation( + summary = "Update category sort order", + description = "Updates the sort order of categories. The request must contain category inode and sortOrder pairs. Can update multiple categories at once within a parent category." + ) + @ApiResponse(responseCode = "200", description = "Category sort order updated successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "400", description = "Bad request - missing category data or invalid sort order") + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "403", description = "Forbidden - insufficient permissions to update categories") + @ApiResponse(responseCode = "404", description = "Parent category not found") + @ApiResponse(responseCode = "500", description = "Internal server error updating sort order") @PUT @Path("/_sort") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) public final Response save(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - final CategoryEditForm categoryEditForm + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Category edit form with category data and sort order information", required = true) final CategoryEditForm categoryEditForm ) throws DotDataException, DotSecurityException { final InitDataObject initData = new WebResource.InitBuilder(webResource) @@ -571,13 +643,25 @@ public final Response save(@Context final HttpServletRequest httpRequest, * @throws DotDataException * @throws DotSecurityException */ + @Operation( + summary = "Delete categories", + description = "Deletes multiple categories by their inodes. Deletes both parent categories and their children. User needs Edit permissions on categories to delete them successfully." + ) + @ApiResponse(responseCode = "200", description = "Categories deleted successfully (may include partial failures)", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "400", description = "Bad request - missing category inodes") + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "403", description = "Forbidden - insufficient permissions to delete categories") + @ApiResponse(responseCode = "500", description = "Internal server error deleting categories") @DELETE @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) public final Response delete(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - final List categoriesToDelete) { + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "List of category inodes to delete", required = true) final List categoriesToDelete) { final InitDataObject initData = new WebResource.InitBuilder(webResource) .requestAndResponse(httpRequest, httpResponse).rejectWhenNoUser(true).init(); @@ -614,7 +698,7 @@ public final Response delete(@Context final HttpServletRequest httpRequest, Logger.debug(this, e.getMessage(), e); } - return Response.ok(new ResponseEntityView( + return Response.ok(new ResponseEntityBulkResultView( new BulkResultView(Long.valueOf(deletedIds.size()), 0L, failedToDelete))) .build(); } @@ -706,6 +790,16 @@ private Category fillAndSave(final CategoryForm categoryForm, * @param httpRequest * @return */ + @Operation( + summary = "Export categories to CSV", + description = "Exports categories to a CSV file format. Can filter by category name pattern and specify a context category inode. Returns a downloadable CSV file." + ) + @ApiResponse(responseCode = "200", description = "Categories exported successfully as CSV file", + content = @Content(mediaType = "text/csv")) + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "403", description = "Forbidden - insufficient permissions to export categories") + @ApiResponse(responseCode = "404", description = "Context category not found") + @ApiResponse(responseCode = "500", description = "Internal server error exporting categories") @GET @Path("/_export") @JSONP @@ -713,8 +807,8 @@ private Category fillAndSave(final CategoryForm categoryForm, @Produces({"text/csv"}) public final void export(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - @QueryParam("contextInode") final String contextInode, - @QueryParam(PaginationUtil.FILTER) final String filter) + @Parameter(description = "Context category inode to export from") @QueryParam("contextInode") final String contextInode, + @Parameter(description = "Filter pattern to match category names") @QueryParam(PaginationUtil.FILTER) final String filter) throws DotDataException, DotSecurityException, IOException { final InitDataObject initData = webResource.init(null, httpRequest, httpResponse, true, @@ -775,57 +869,54 @@ public final void export(@Context final HttpServletRequest httpRequest, } /** - * Exports a list of categories. + * Imports categories from a CSV file. * - * @param httpRequest - * @param httpResponse - * @param uploadedFile - * @param multiPart - * @param fileDetail - * @param filter - * @param exportType - * @param contextInode - * @return Response - * @throws DotDataException - * @throws DotSecurityException + * @param httpRequest HTTP request context + * @param httpResponse HTTP response context + * @param uploadedFile CSV file containing categories to import + * @param fileDetail File metadata and disposition information + * @param filter Filter pattern for categories + * @param exportType Import type: 'replace' or 'append' + * @param contextInode Context category inode to import into + * @return Response indicating success/failure + * @throws IOException if file reading fails */ + @Operation( + summary = "Import categories from CSV file", + description = "Imports categories from an uploaded CSV file. Supports 'replace' mode to replace existing categories or 'append' mode to add to existing categories. Can specify a context category and filter options." + ) + @RequestBody(required = true, + content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA, + schema = @Schema(implementation = CategoryImportData.class))) + @ApiResponse(responseCode = "200", description = "Categories imported successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "400", description = "Bad request - invalid file format or missing required parameters") + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "403", description = "Forbidden - insufficient permissions to import categories") + @ApiResponse(responseCode = "500", description = "Internal server error importing categories") @POST @Path("/_import") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Consumes({MediaType.MULTIPART_FORM_DATA}) public Response importCategories(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - @FormDataParam("file") final File uploadedFile, - FormDataMultiPart multiPart, - @FormDataParam("file") FormDataContentDisposition fileDetail, - @FormDataParam("filter") String filter, - @FormDataParam("exportType") String exportType, - @FormDataParam("contextInode") String contextInode) throws IOException { + @Parameter(hidden = true) @BeanParam final CategoryImportData form) throws IOException { - return processImport(httpRequest, httpResponse, uploadedFile, multiPart, fileDetail, filter, - exportType, contextInode); + return processImport(httpRequest, httpResponse, + form.getFileInputStream(), form.getFileDetail(), + form.getFilter(), form.getExportType(), form.getContextInode()); } - private String getContentFromFile(String path) throws IOException { - - String content = StringPool.BLANK; - - try (InputStream file = java.nio.file.Files.newInputStream(java.nio.file.Path.of(path))) { - byte[] uploadedFileBytes = file.readAllBytes(); - - content = new String(uploadedFileBytes); - } - return content; - } + @WrapInTransaction private Response processImport(final HttpServletRequest httpRequest, final HttpServletResponse httpResponse, - final File uploadedFile, - final FormDataMultiPart multiPart, + final InputStream fileInputStream, final FormDataContentDisposition fileDetail, final String filter, final String exportType, @@ -834,7 +925,6 @@ private Response processImport(final HttpServletRequest httpRequest, List unableToDeleteCats = null; final List failedToDelete = new ArrayList<>(); - StringReader stringReader = null; BufferedReader bufferedReader = null; try { @@ -849,10 +939,7 @@ private Response processImport(final HttpServletRequest httpRequest, contextInode, filter, exportType)); - String content = getContentFromFile(uploadedFile.getPath()); - - stringReader = new StringReader(content); - bufferedReader = new BufferedReader(stringReader); + bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream, StandardCharsets.UTF_8)); if (exportType.equals("replace")) { Logger.debug(this, () -> "Replacing categories"); @@ -884,10 +971,10 @@ private Response processImport(final HttpServletRequest httpRequest, } catch (Exception e) { Logger.error(this, "Error importing categories", e); } finally { - CloseUtils.closeQuietly(stringReader, bufferedReader); + CloseUtils.closeQuietly(bufferedReader); } - return Response.ok(new ResponseEntityView( + return Response.ok(new ResponseEntityBulkResultView( new BulkResultView(Long.valueOf(UtilMethods.isSet(unableToDeleteCats) ? 1 : 0), 0L, failedToDelete))) .build(); diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/categories/CategoryImportData.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/categories/CategoryImportData.java new file mode 100644 index 000000000000..701723d28280 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/categories/CategoryImportData.java @@ -0,0 +1,48 @@ +package com.dotcms.rest.api.v1.categories; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataParam; +import java.io.InputStream; + +/** + * Runtime binding bean for multipart form parts. + */ +@Schema(name = "CategoryImportFormSchema", description = "Category import form data with CSV file and processing parameters") +public class CategoryImportData { + + private InputStream fileInputStream; + private FormDataContentDisposition fileDetail; + private String filter; + private String exportType; + private String contextInode; + + @FormDataParam("file") + @JsonProperty("file") + @Schema(name = "file", description = "CSV file containing categories to import", type = "string", format = "binary") + public void setFileInputStream(final InputStream inputStream) { this.fileInputStream = inputStream; } + + @FormDataParam("file") + @Schema(hidden = true) + public void setFileDetail(final FormDataContentDisposition detail) { this.fileDetail = detail; } + + @FormDataParam("filter") + @Schema(description = "Filter pattern for categories") + public void setFilter(final String filter) { this.filter = filter; } + + @FormDataParam("exportType") + @Schema(description = "Import behavior", allowableValues = {"replace", "merge"}) + public void setExportType(final String exportType) { this.exportType = exportType; } + + @FormDataParam("contextInode") + @Schema(description = "Context category inode to import into") + public void setContextInode(final String contextInode) { this.contextInode = contextInode; } + + public InputStream getFileInputStream() { return fileInputStream; } + public FormDataContentDisposition getFileDetail() { return fileDetail; } + public String getFilter() { return filter; } + public String getExportType() { return exportType; } + public String getContextInode() { return contextInode; } +} + diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentRelationshipsResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentRelationshipsResource.java index 2f40f19b1536..195cf3488a0a 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentRelationshipsResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentRelationshipsResource.java @@ -4,18 +4,26 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import com.dotcms.rest.InitDataObject; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.exception.mapper.ExceptionMapperUtil; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.portlets.contentlet.model.Contentlet; import com.dotmarketing.util.Logger; import com.dotmarketing.util.json.JSONException; import com.liferay.portal.model.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import javax.servlet.http.HttpServletRequest; @@ -40,8 +48,9 @@ * 3 --> The contentlet object will contain the related contentlets, which in turn will contain a list of their related contentlets * null --> Relationships will not be sent in the response */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/contentrelationships") -@Tag(name = "Content", description = "Endpoints for managing content and contentlets") +@Tag(name = "Content") @Deprecated public class ContentRelationshipsResource { @@ -87,11 +96,29 @@ protected ContentRelationshipsResource(final ContentRelationshipsHelper * @param params A Map of parameters that will define the search criteria. * @return The list of associated contents. */ + @Operation( + summary = "Get content with relationships (deprecated)", + description = "Retrieves content with relationships based on query parameters, identifier, or inode. This endpoint is deprecated - use /v1/content with depth parameter instead." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content with relationships retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(type = "object", description = "Content with relationships in JSON format including contentlets and their related content"))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @NoCache @GET @Path("/{params: .*}") + @Produces(MediaType.APPLICATION_JSON) public Response getContent(@Context final HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Query parameters, identifier, or inode for content lookup", required = true) @PathParam("params") final String params) { final InitDataObject initData = this.webResource.init(params, request, response, false, null); final Map paramsMap = initData.getParamsMap(); diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentReportResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentReportResource.java index d44406a5dc39..c17877c573d2 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentReportResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentReportResource.java @@ -2,6 +2,7 @@ import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.api.v1.site.ResponseSiteVariablesEntityView; import com.dotcms.util.PaginationUtil; import com.dotcms.util.pagination.ContentReportPaginator; @@ -13,6 +14,7 @@ import com.google.common.annotations.VisibleForTesting; import com.liferay.portal.model.User; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -48,6 +50,7 @@ * @author Jose Castro * @since Mar 7th, 2024 */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/contentreport") @Tag(name = "Content Report") public class ContentReportResource { @@ -91,9 +94,10 @@ public ContentReportResource(final WebResource webResource) { @Path("/site/{site}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Operation(summary = "Generates a report of the different Content Types living under a Site, " + - "and the number of content items for each type", + @Produces({MediaType.APPLICATION_JSON}) + @Operation( + summary = "Generate site content report", + description = "Generates a detailed report of the different Content Types living under a Site and the number of content items for each type. Useful for data analysis and deletion planning.", responses = { @ApiResponse( responseCode = "200", @@ -106,11 +110,11 @@ public ContentReportResource(final WebResource webResource) { }) public Response getSiteContentReport(@Context final HttpServletRequest httpServletRequest, @Context final HttpServletResponse httpServletResponse, - @PathParam(ContentReportPaginator.SITE_PARAM) final String site, - @QueryParam(PaginationUtil.PAGE) final int page, - @QueryParam(PaginationUtil.PER_PAGE) final int perPage, - @DefaultValue("upper(name)") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, - @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction) { + @Parameter(description = "Site ID or key to generate the report for", required = true) @PathParam(ContentReportPaginator.SITE_PARAM) final String site, + @Parameter(description = "Page number for pagination") @QueryParam(PaginationUtil.PAGE) final int page, + @Parameter(description = "Number of items per page") @QueryParam(PaginationUtil.PER_PAGE) final int perPage, + @Parameter(description = "Field to order results by") @DefaultValue("upper(name)") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, + @Parameter(description = "Sort direction (ASC or DESC)") @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction) { final User user = new WebResource.InitBuilder(this.webResource) .requestAndResponse(httpServletRequest, httpServletResponse) .requiredBackendUser(true) @@ -154,9 +158,10 @@ public Response getSiteContentReport(@Context final HttpServletRequest httpServl @Path("/folder/{folder: .*}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Operation(summary = "Generates a report of the different Content Types living under a " + - "Folder, and the number of content items for each type", + @Produces({MediaType.APPLICATION_JSON}) + @Operation( + summary = "Generate folder content report", + description = "Generates a detailed report of the different Content Types living under a Folder and the number of content items for each type. Supports both folder ID and folder path (requires site parameter).", responses = { @ApiResponse( responseCode = "200", @@ -168,12 +173,12 @@ public Response getSiteContentReport(@Context final HttpServletRequest httpServl }) public Response getFolderContentReport(@Context final HttpServletRequest httpServletRequest, @Context final HttpServletResponse httpServletResponse, - @PathParam(ContentReportPaginator.FOLDER_PARAM) final String folder, - @QueryParam(ContentReportPaginator.SITE_PARAM) final String site, - @QueryParam(PaginationUtil.PAGE) final int page, - @QueryParam(PaginationUtil.PER_PAGE) final int perPage, - @DefaultValue("upper(name)") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, - @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction) { + @Parameter(description = "Folder ID or path to generate the report for", required = true) @PathParam(ContentReportPaginator.FOLDER_PARAM) final String folder, + @Parameter(description = "Site ID or key (required when using folder path)") @QueryParam(ContentReportPaginator.SITE_PARAM) final String site, + @Parameter(description = "Page number for pagination") @QueryParam(PaginationUtil.PAGE) final int page, + @Parameter(description = "Number of items per page") @QueryParam(PaginationUtil.PER_PAGE) final int perPage, + @Parameter(description = "Field to order results by") @DefaultValue("upper(name)") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, + @Parameter(description = "Sort direction (ASC or DESC)") @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction) { final User user = new WebResource.InitBuilder(this.webResource) .requestAndResponse(httpServletRequest, httpServletResponse) .requiredBackendUser(true) diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentResource.java index 07b1a748ff65..04b4851c8da5 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentResource.java @@ -10,8 +10,7 @@ import com.dotcms.rest.CountView; import com.dotcms.rest.InitDataObject; import com.dotcms.rest.MapToContentletPopulator; -import com.dotcms.rest.ResponseEntityBooleanView; -import com.dotcms.rest.ResponseEntityContentletView; +import com.dotcms.rest.ResponseEntityListMapView; import com.dotcms.rest.ResponseEntityCountView; import com.dotcms.rest.ResponseEntityMapView; import com.dotcms.rest.ResponseEntityView; @@ -19,6 +18,7 @@ import com.dotcms.rest.SearchView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.api.v1.content.search.LuceneQueryBuilder; import com.dotcms.util.DotPreconditions; import com.dotcms.uuid.shorty.ShortType; @@ -64,6 +64,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import io.vavr.Lazy; import io.vavr.control.Try; @@ -98,8 +99,9 @@ * Version 1 of the Content resource, to interact and retrieve contentlets * @author jsanca */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/content") -@Tag(name = "Content", description = "Endpoints for managing content and contentlets - the core data objects in dotCMS") +@Tag(name = "Content") public class ContentResource { private final WebResource webResource; @@ -154,8 +156,8 @@ public ContentResource(final WebResource webResource, @Path("/_draft") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation( operationId = "saveDraft", summary = "Saves a content draft", @@ -165,7 +167,7 @@ public ContentResource(final WebResource webResource, responses = { @ApiResponse(responseCode = "200", description = "Draft saved successfully", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityContentletView.class))), + schema = @Schema(implementation = ResponseEntityMapView.class))), @ApiResponse(responseCode = "400", description = "Bad request - Invalid content data"), @ApiResponse(responseCode = "401", description = "Unauthorized - User not authenticated"), @ApiResponse(responseCode = "403", description = "Forbidden - User lacks write permissions"), @@ -173,7 +175,7 @@ public ContentResource(final WebResource webResource, @ApiResponse(responseCode = "500", description = "Internal server error") } ) - public final ResponseEntityContentletView saveDraft(@Context final HttpServletRequest request, + public final ResponseEntityMapView saveDraft(@Context final HttpServletRequest request, @Parameter(description = "Content inode for existing content") @QueryParam("inode") final String inode, @Parameter(description = "Content identifier for existing content") @QueryParam("identifier") final String identifier, @Parameter(description = "Index policy (DEFER_UNTIL_PUBLISH, FORCE, WAIT_FOR)") @QueryParam("indexPolicy") final String indexPolicy, @@ -202,7 +204,7 @@ public final ResponseEntityContentletView saveDraft(@Context final HttpServletRe APILocator.getContentletAPI().saveDraft(contentlet, (ContentletRelationships) contentlet.get(Contentlet.RELATIONSHIP_KEY), categories.orElse(null), null, initDataObject.getUser(), false); - return new ResponseEntityContentletView( + return new ResponseEntityMapView( new DotTransformerBuilder().defaultOptions().content(contentlet).build().toMaps().stream().findFirst().orElse(Collections.emptyMap())); } @@ -326,7 +328,7 @@ private Contentlet populateContentlet(final ContentForm contentForm, final Conte responses = { @ApiResponse(responseCode = "200", description = "Contentlet retrieved successfully", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityView.class))), + schema = @Schema(implementation = ResponseEntityMapView.class))), @ApiResponse(responseCode = "400", description = "Bad request - Invalid identifier format"), @ApiResponse(responseCode = "401", description = "Unauthorized - User not authenticated"), @ApiResponse(responseCode = "403", description = "Forbidden - User lacks read permissions"), @@ -379,12 +381,29 @@ public Response getContent(@Context HttpServletRequest request, * * @return The {@link ResponseEntityCountView} */ + @Operation( + summary = "Get contentlet references count", + description = "Retrieves the total number of references to a specific contentlet by its identifier" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "References count retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCountView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - contentlet with identifier not found", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/{identifier}/references/count") @Produces(MediaType.APPLICATION_JSON) public ResponseEntityCountView getAllContentletReferencesCount( @Context HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Content identifier", required = true) @PathParam("identifier") final String identifier ) throws DotDataException { @@ -412,13 +431,31 @@ public ResponseEntityCountView getAllContentletReferencesCount( * * @return The {@link ResponseEntityView>} */ + @Operation( + summary = "Get contentlet references", + description = "Retrieves all references to a specific contentlet, including pages, containers, and personas that reference it" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "References retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityContentReferenceListView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - contentlet not found", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/{inodeOrIdentifier}/references") @Produces(MediaType.APPLICATION_JSON) - public ResponseEntityView> getContentletReferences( + public ResponseEntityContentReferenceListView getContentletReferences( @Context HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Content inode or identifier", required = true) @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, + @Parameter(description = "Language ID for content localization", example = "1") @DefaultValue("") @QueryParam("language") final String language ) throws DotDataException, DotSecurityException { @@ -445,7 +482,7 @@ public ResponseEntityView> getContentletReferences( (String) reference.get("personaName"))) .collect(Collectors.toList()): List.of(); - return new ResponseEntityView<>(contentReferenceViews); + return new ResponseEntityContentReferenceListView(contentReferenceViews); } /** @@ -478,7 +515,7 @@ public ResponseEntityView> getContentletReferences( responses = { @ApiResponse(responseCode = "200", description = "Successfully retrieved lock status", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityView.class) + schema = @Schema(implementation = ResponseEntityMapView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad request"), // invalid param string like `\` @@ -490,8 +527,8 @@ public ResponseEntityView> getContentletReferences( ) public ResponseEntityView> canLockContent(@Context HttpServletRequest request, @Context final HttpServletResponse response, - @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, - @DefaultValue("-1") @QueryParam("language") final String language) + @Parameter(description = "Contentlet inode or identifier", required = true) @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, + @Parameter(description = "Language ID for content localization") @DefaultValue("-1") @QueryParam("language") final String language) throws DotDataException, DotSecurityException { final User user = @@ -566,8 +603,8 @@ public ResponseEntityView> canLockContent(@Context HttpServl ) public ResponseEntityMapView unlockContent(@Context HttpServletRequest request, @Context final HttpServletResponse response, - @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, - @DefaultValue("-1") @QueryParam("language") final String language) + @Parameter(description = "Contentlet inode or identifier", required = true) @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, + @Parameter(description = "Language ID for content localization") @DefaultValue("-1") @QueryParam("language") final String language) throws DotDataException, DotSecurityException { final User user = @@ -624,8 +661,8 @@ public ResponseEntityMapView unlockContent(@Context HttpServletRequest request, ) public ResponseEntityMapView lockContent(@Context HttpServletRequest request, @Context HttpServletResponse response, - @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, - @DefaultValue("-1") @QueryParam("language") final String language) + @Parameter(description = "Contentlet inode or identifier", required = true) @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, + @Parameter(description = "Language ID for content localization") @DefaultValue("-1") @QueryParam("language") final String language) throws DotDataException, DotSecurityException { final User user = @@ -760,18 +797,33 @@ private Optional resolveContentlet (final String inodeOrIdentifier, @POST @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Path("related") - @Operation(summary = "Pull Related Content", - responses = { - @ApiResponse( - responseCode = "200", - content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityView.class))), - @ApiResponse(responseCode = "404", description = "Contentlet not found"), - @ApiResponse(responseCode = "400", description = "Contentlet does not have a relationship field")}) + @Operation( + summary = "Pull Related Content", + description = "Retrieves related content for a contentlet based on relationship field configuration and query conditions" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Related content retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityListMapView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - contentlet does not have a relationship field", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - contentlet not found", + content = @Content(mediaType = "application/json")) + }) public Response pullRelated(@Context final HttpServletRequest request, @Context final HttpServletResponse response, + @RequestBody(description = "Pull related content request parameters", + required = true, + content = @Content(schema = @Schema(implementation = PullRelatedForm.class))) final PullRelatedForm pullRelatedForm) throws DotDataException, DotSecurityException { final InitDataObject initData = new WebResource.InitBuilder(webResource) @@ -841,13 +893,29 @@ public Response pullRelated(@Context final HttpServletRequest request, * * @throws DotDataException An error occurred when interacting with the database. */ + @Operation( + summary = "Check content language versions", + description = "Retrieves all available languages for a contentlet and indicates which languages have content versions" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Language versions retrieved successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - contentlet identifier not found", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/{identifier}/languages") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityView> checkContentLanguageVersions(@Context final HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Content identifier", required = true) @PathParam("identifier") final String identifier) throws DotDataException { Logger.debug(this, () -> String.format("Check the languages that Contentlet '%s' is " + "available on", identifier)); @@ -916,7 +984,8 @@ private List getExistingLanguagesForContent( @POST @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON}) @Path("/search") @Operation(operationId = "search", summary = "Retrieves content from the dotCMS repository", description = "Abstracts the generation of the required Lucene query to look for user searchable fields " + @@ -937,6 +1006,8 @@ private List getExistingLanguagesForContent( ) public ResponseEntityView search(@Context final HttpServletRequest request, @Context final HttpServletResponse response, + @RequestBody(description = "Content search parameters", required = true, + content = @Content(schema = @Schema(implementation = ContentSearchForm.class))) final ContentSearchForm contentSearchForm) throws DotDataException, DotSecurityException { Logger.debug(this, () -> "Searching for contentlets with the following parameters: " + contentSearchForm); final User user = new WebResource.InitBuilder(webResource) diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentVersionResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentVersionResource.java index ee4ee8a7a3ed..c7c3f6244f5c 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentVersionResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentVersionResource.java @@ -1,10 +1,14 @@ package com.dotcms.rest.api.v1.content; +import static com.dotcms.rest.api.v1.authentication.ResponseUtil.getFormattedMessage; + import com.dotcms.rest.InitDataObject; +import com.dotcms.rest.ResponseEntityMapView; import com.dotcms.rest.ResponseEntityPaginatedDataView; import com.dotcms.rest.ResponseEntityView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.api.v1.authentication.ResponseUtil; import com.dotcms.rest.exception.BadRequestException; import com.dotcms.rest.exception.NotFoundException; @@ -39,6 +43,15 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.glassfish.jersey.server.JSONP; @@ -54,17 +67,6 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -import static com.dotcms.rest.api.v1.authentication.ResponseUtil.getFormattedMessage; /** * This REST Endpoint allows you to retrieve information related to versions of Contentlets in @@ -73,8 +75,9 @@ * @author Fabrizzio Araya * @since Dec 18th, 2018 */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/content/versions") -@Tag(name = "Content", description = "Endpoints for managing content and contentlets") +@Tag(name = "Content") public class ContentVersionResource { private static final String FIND_BY_ID_ERROR_MESSAGE_KEY = "Unable-to-find-contentlet-by-id"; @@ -116,6 +119,25 @@ public ContentVersionResource() { * @throws DotStateException * @throws DotSecurityException */ + @Operation( + summary = "Find content versions", + description = "Retrieves all versions for content by identifier or inodes, with optional grouping by language" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content versions retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - missing identifier/inodes or invalid parameters", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - content not found", + content = @Content(mediaType = "application/json")) + }) @GET @JSONP @NoCache @@ -123,8 +145,14 @@ public ContentVersionResource() { @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8") public Response findVersions(@Context final HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Comma-separated list of content inodes") @QueryParam("inodes") final String inodes, - @QueryParam("identifier") final String identifier, @QueryParam("groupByLang")final String groupByLangParam, @QueryParam("limit") final int limit) + @Parameter(description = "Content identifier (takes precedence over inodes)") + @QueryParam("identifier") final String identifier, + @Parameter(description = "Group results by language (true/1 or false/0)", example = "false") + @QueryParam("groupByLang")final String groupByLangParam, + @Parameter(description = "Maximum number of results (min: 20, max: 100)", example = "20") + @QueryParam("limit") final int limit) throws DotDataException, DotStateException, DotSecurityException { final boolean groupByLang = "1".equals(groupByLangParam) || BooleanUtils.toBoolean(groupByLangParam); @@ -145,12 +173,12 @@ public Response findVersions(@Context final HttpServletRequest request, if(groupByLang){ final Map>> versionsByLang = mapVersionsByLang(contentletAPI .findAllVersions(identifierObj, user, respectFrontendRoles), showing); - responseEntityView = new ResponseEntityView(ImmutableMap.of(VERSIONS, versionsByLang)); + responseEntityView = new ResponseEntityView<>(ImmutableMap.of(VERSIONS, versionsByLang)); } else { final List> versions = mapVersions(contentletAPI .findAllVersions(identifierObj, user, respectFrontendRoles), showing); - responseEntityView = new ResponseEntityView(ImmutableMap.of(VERSIONS, versions)); + responseEntityView = new ResponseEntityView<>(ImmutableMap.of(VERSIONS, versions)); } } else { final Set inodesSet = @@ -164,10 +192,10 @@ public Response findVersions(@Context final HttpServletRequest request, if(groupByLang){ final Map>> versionsByLang = mapVersionsByLang(findByInodes(user, inodesSet, respectFrontendRoles), showing); - responseEntityView = new ResponseEntityView(ImmutableMap.of(VERSIONS, versionsByLang)); + responseEntityView = new ResponseEntityView<>(ImmutableMap.of(VERSIONS, versionsByLang)); } else { final Map> versions = mapVersionsByInode(findByInodes(user, inodesSet, respectFrontendRoles), showing); - responseEntityView = new ResponseEntityView(ImmutableMap.of(VERSIONS, versions)); + responseEntityView = new ResponseEntityView<>(ImmutableMap.of(VERSIONS, versions)); } @@ -365,6 +393,25 @@ private Map>> mapVersionsByLang(final List>> mapVersionsByLang(final List(contentletToMap(contentlet)) ); return responseBuilder.build(); } catch (Exception ex) { diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ResourceLinkResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ResourceLinkResource.java index 4f9c2646335e..9f576eb3e3c0 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ResourceLinkResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ResourceLinkResource.java @@ -3,9 +3,12 @@ import com.dotcms.contenttype.model.field.BinaryField; import com.dotcms.contenttype.model.field.Field; import com.dotcms.rest.InitDataObject; +import com.dotcms.rest.ResponseEntityMapStringObjectView; +import com.dotcms.rest.ResponseEntityMapView; import com.dotcms.rest.ResponseEntityView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.util.DotPreconditions; import com.dotmarketing.business.APILocator; import com.dotmarketing.business.DotStateException; @@ -25,6 +28,12 @@ import com.liferay.portal.language.LanguageUtil; import com.liferay.portal.model.User; import org.glassfish.jersey.server.JSONP; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import javax.servlet.http.HttpServletRequest; @@ -48,8 +57,9 @@ * Exposes a {@link com.dotmarketing.portlets.contentlet.model.ResourceLink} by inode or id * @author jsanca */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/content/resourcelinks") -@Tag(name = "Content", description = "Endpoints for managing content and contentlets") +@Tag(name = "Content") public class ResourceLinkResource { private final WebResource webResource; @@ -80,6 +90,28 @@ protected ResourceLinkResource(final WebResource webResource, final ContentletAP * @throws DotStateException * @throws DotSecurityException */ + @Operation( + summary = "Get resource link for specific field", + description = "Retrieves a resource link for a specific field of a contentlet identified by inode or identifier" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Resource link retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapStringObjectView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - missing inode/identifier parameter", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - download restricted", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - contentlet or field not found", + content = @Content(mediaType = "application/json")) + }) @GET @JSONP @NoCache @@ -87,9 +119,13 @@ protected ResourceLinkResource(final WebResource webResource, final ContentletAP @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8") public Response findResourceLink(@Context final HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Field variable name", required = true) @PathParam("field") final String field, + @Parameter(description = "Content inode") @QueryParam("inode") final String inode, + @Parameter(description = "Content identifier") @QueryParam("identifier") final String identifier, + @Parameter(description = "Language ID", example = "1") @DefaultValue("-1") @QueryParam("language") final String language) throws DotStateException, DotSecurityException, DotDataException { if (!UtilMethods.isSet(inode) && !UtilMethods.isSet(identifier)) { @@ -121,10 +157,10 @@ public Response findResourceLink(@Context final HttpServletRequest request, throw new DotSecurityException("The Resource link to the contentlet is restricted."); } - return Response.ok(new ResponseEntityView(this.toMapView(contentlet, link))).build(); + return Response.ok(new ResponseEntityView<>(this.toMapView(contentlet, link))).build(); }catch (DoesNotExistException e) { - return Response.ok(new ResponseEntityView(Collections.emptyMap())).build(); + return Response.ok(new ResponseEntityView<>(Collections.emptyMap())).build(); } } @@ -187,14 +223,36 @@ private Contentlet getContentlet(final String inode, * @throws DotStateException * @throws DotSecurityException */ + @Operation( + summary = "Get all resource links for contentlet", + description = "Retrieves resource links for all binary fields of a contentlet identified by inode or identifier" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Resource links retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapStringObjectView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - missing inode/identifier parameter", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - download restricted", + content = @Content(mediaType = "application/json")) + }) @GET @JSONP @NoCache @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8") public Response findResourceLinks(@Context final HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Content inode") @QueryParam("inode") final String inode, + @Parameter(description = "Content identifier") @QueryParam("identifier") final String identifier, + @Parameter(description = "Language ID", example = "1") @DefaultValue("-1") @QueryParam("language") final String language) throws DotStateException, DotSecurityException, DotDataException { if (!UtilMethods.isSet(inode) && !UtilMethods.isSet(identifier)) { @@ -227,10 +285,10 @@ public Response findResourceLinks(@Context final HttpServletRequest request, resourceLinkMap.put(fieldName, this.toMapView(contentlet, link)); } - return Response.ok(new ResponseEntityView(resourceLinkMap)).build(); + return Response.ok(new ResponseEntityView<>(resourceLinkMap)).build(); } catch (DoesNotExistException e) { - return Response.ok(new ResponseEntityView(Collections.emptyList())).build(); + return Response.ok(new ResponseEntityView<>(Collections.emptyList())).build(); } } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/dotimport/ContentImportResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/dotimport/ContentImportResource.java index 4349f487437f..14d02632993e 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/dotimport/ContentImportResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/dotimport/ContentImportResource.java @@ -13,6 +13,7 @@ import com.dotcms.rest.ResponseEntityStringView; import com.dotcms.rest.ResponseEntityView; import com.dotcms.rest.WebResource; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.api.v1.job.SSEMonitorUtil; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.util.Logger; @@ -50,8 +51,9 @@ * REST resource for handling content import operations, including creating and enqueuing content import jobs. * This class provides endpoints for importing content from CSV files and processing them based on the provided parameters. */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/content/_import") -@Tag(name = "Content", description = "Endpoints for managing content and contentlets") +@Tag(name = "Content") public class ContentImportResource { private final WebResource webResource; diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java index 4b973551a9c7..823609f54aa9 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java @@ -16,11 +16,12 @@ import com.dotcms.rendering.velocity.services.PageRenderUtil; import com.dotcms.repackage.com.google.common.annotations.VisibleForTesting; import com.dotcms.rest.InitDataObject; -import com.dotcms.rest.ResponseEntityView; +import com.dotcms.rest.ResponseEntityListMapView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.InitRequestRequired; import com.dotcms.rest.annotation.NoCache; import com.dotcms.rest.annotation.PermissionsUtil; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.exception.BadRequestException; import com.dotcms.rest.exception.ForbiddenException; import com.dotcms.rest.exception.mapper.ExceptionMapperUtil; @@ -113,12 +114,9 @@ * @author Will Ezell * @since Sep 11th, 2016 */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/contenttype") -@Tag(name = "Content Type", - description = "Endpoints that perform operations related to content types.", - externalDocs = @ExternalDocumentation(description = "Additional Content Type API information", - url = "https://www.dotcms.com/docs/latest/content-type-api") -) +@Tag(name = "Content Type") public class ContentTypeResource implements Serializable { private static final String MAP_KEY_WORKFLOWS = "workflows"; @@ -159,7 +157,7 @@ public ContentTypeResource(final ContentTypeHelper contentletHelper, final WebRe @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation( operationId = "postContentTypeCopy", summary = "Copies a content type", @@ -224,7 +222,8 @@ public ContentTypeResource(final ContentTypeHelper contentletHelper, final WebRe " }\n" + "}" ) - } + }, + schema = @Schema(implementation = ResponseEntityContentTypeOperationView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad Request"), @@ -298,7 +297,7 @@ public final Response copyType(@Context final HttpServletRequest req, session.removeAttribute(SELECTED_STRUCTURE_KEY); } - response = Response.ok(new ResponseEntityView<>(responseMap)).build(); + response = Response.ok(new ResponseEntityContentTypeOperationView(responseMap)).build(); } catch (final IllegalArgumentException e) { final String errorMsg = String.format("Missing required information when copying Content Type " + "'%s': %s", baseVariableName, ExceptionUtil.getErrorMessage(e)); @@ -412,7 +411,7 @@ private ImmutableMap copyContentTypeAndDependencies(final Conten @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation( operationId = "postContentTypeCreate", summary = "Creates one or more content types", @@ -461,7 +460,8 @@ private ImmutableMap copyContentTypeAndDependencies(final Conten " \"permissions\": []\n" + "}" ) - } + }, + schema = @Schema(implementation = ResponseEntityListMapView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad Request"), @@ -542,7 +542,7 @@ public final Response createType(@Context final HttpServletRequest req, Logger.debug(this, ()->String.format("Creating Content Type(s): %s", form.getRequestJson())); final HttpSession session = req.getSession(false); final Iterable typesToSave = form.getIterable(); - final List> savedContentTypes = new ArrayList<>(); + final List> savedContentTypes = new ArrayList<>(); for (final ContentTypeForm.ContentTypeFormEntry entry : typesToSave) { final ContentType type = contentTypeHelper.evaluateContentTypeRequest( @@ -559,7 +559,7 @@ public final Response createType(@Context final HttpServletRequest req, entry.workflows, form.getSystemActions(), APILocator.getContentTypeAPI(user, true), true); final ContentType contentTypeSaved = tuple2._1; - final ImmutableMap responseMap = ImmutableMap.builder() + final ImmutableMap responseMap = ImmutableMap.builder() .putAll(contentTypeHelper.contentTypeToMap(contentTypeSaved, user)) .put(MAP_KEY_WORKFLOWS, this.workflowHelper.findSchemesByContentType(contentTypeSaved.id(), @@ -573,7 +573,7 @@ public final Response createType(@Context final HttpServletRequest req, session.removeAttribute(SELECTED_STRUCTURE_KEY); } } - return Response.ok(new ResponseEntityView<>(savedContentTypes)).build(); + return Response.ok(new ResponseEntityListMapView(savedContentTypes)).build(); } catch (final IllegalArgumentException e) { final String errorMsg = String.format("Missing required information when creating Content Type(s): " + "%s", ExceptionUtil.getErrorMessage(e)); @@ -611,7 +611,7 @@ public final Response createType(@Context final HttpServletRequest req, @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) + @Produces({ MediaType.APPLICATION_JSON }) @Operation( operationId = "putContentTypeUpdate", summary = "Updates a content type", @@ -659,7 +659,8 @@ public final Response createType(@Context final HttpServletRequest req, " \"permissions\": []\n" + "}" ) - } + }, + schema = @Schema(implementation = ResponseEntityContentTypeDetailView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad Request"), @@ -736,8 +737,8 @@ public Response updateType(@PathParam("idOrVar") @Parameter( this.saveContentTypeAndDependencies(contentType, user, form.getWorkflows(), form.getSystemActions(), contentTypeAPI, false); - final ImmutableMap.Builder builderMap = - ImmutableMap.builder() + final ImmutableMap.Builder builderMap = + ImmutableMap.builder() .putAll(contentTypeHelper.contentTypeToMap(tuple2._1, user)) .put(MAP_KEY_WORKFLOWS, this.workflowHelper.findSchemesByContentType( @@ -746,7 +747,7 @@ public Response updateType(@PathParam("idOrVar") @Parameter( .collect(Collectors.toMap( SystemActionWorkflowActionMapping::getSystemAction, mapping -> mapping))); - return Response.ok(new ResponseEntityView<>(builderMap.build())).build(); + return Response.ok(new ResponseEntityContentTypeDetailView(builderMap.build())).build(); } catch (final NotFoundInDbException e) { Logger.error(this, String.format("Content Type with ID or var name '%s' was not found", idOrVar), e); return ExceptionMapperUtil.createResponse(e, Response.Status.NOT_FOUND); @@ -972,7 +973,7 @@ private void handleUpdateFieldVariables( @Path("/id/{idOrVar}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation( operationId = "deleteContentType", summary = "Deletes a content type", @@ -993,7 +994,8 @@ private void handleUpdateFieldVariables( " \"permissions\": []\n" + "}" ) - } + }, + schema = @Schema(implementation = ResponseEntityContentTypeJsonView.class) ) ), @ApiResponse(responseCode = "403", description = "Forbidden"), @@ -1026,7 +1028,7 @@ public Response deleteType(@PathParam("idOrVar") @Parameter( JSONObject joe = new JSONObject(); joe.put("deleted", type.id()); - return Response.ok(new ResponseEntityView<>(joe.toString())).build(); + return Response.ok(new ResponseEntityContentTypeJsonView(joe.toString())).build(); } catch (final DotSecurityException e) { throw new ForbiddenException(e); } catch (final Exception e) { @@ -1039,7 +1041,7 @@ public Response deleteType(@PathParam("idOrVar") @Parameter( @Path("/id/{idOrVar}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation( operationId = "getContentTypeIdVar", summary = "Retrieves a single content type", @@ -1091,7 +1093,8 @@ public Response deleteType(@PathParam("idOrVar") @Parameter( " \"permissions\": []\n" + "}\n" ) - } + }, + schema = @Schema(implementation = ResponseEntityContentTypeDetailView.class) ) ), @ApiResponse(responseCode = "403", description = "Forbidden"), @@ -1146,7 +1149,7 @@ public Response getType( final ContentTypeInternationalization contentTypeInternationalization = languageId != null ? new ContentTypeInternationalization(languageId, live, user) : null; - final ImmutableMap resultMap = ImmutableMap.builder() + final ImmutableMap resultMap = ImmutableMap.builder() .putAll(contentTypeHelper.contentTypeToMap(type, contentTypeInternationalization, user)) .put(MAP_KEY_WORKFLOWS, this.workflowHelper.findSchemesByContentType( @@ -1158,8 +1161,9 @@ public Response getType( mapping -> mapping))).build(); response = ("true".equalsIgnoreCase(req.getParameter("include_permissions")))? - Response.ok(new ResponseEntityView<>(resultMap, PermissionsUtil.getInstance().getPermissionsArray(type, initData.getUser()))).build(): - Response.ok(new ResponseEntityView<>(resultMap)).build(); + Response.ok(new ResponseEntityContentTypeDetailView( + (Map) resultMap, PermissionsUtil.getInstance().getPermissionsArray(type, initData.getUser()))).build(): + Response.ok(new ResponseEntityContentTypeDetailView((Map) resultMap)).build(); } catch (final DotSecurityException e) { throw new ForbiddenException(e); } catch (final NotFoundInDbException nfdb2) { @@ -1203,7 +1207,7 @@ public Response getType( @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation( operationId = "postContentTypeFilter", summary = "Filters content types", @@ -1253,7 +1257,8 @@ public Response getType( " \"permissions\": []\n" + "}\n" ) - } + }, + schema = @Schema(implementation = ResponseEntityListContentTypeView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad Request"), @@ -1349,7 +1354,7 @@ public final Response filteredContentTypes(@Context final HttpServletRequest req @JSONP @InitRequestRequired @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation( operationId = "getContentTypeBaseTypes", summary = "Retrieves base content types", @@ -1375,7 +1380,8 @@ public final Response filteredContentTypes(@Context final HttpServletRequest req " \"permissions\": []\n" + "}" ) - } + }, + schema = @Schema(implementation = ResponseEntityBaseContentTypesView.class) ) ), @ApiResponse(responseCode = "500", description = "Internal Server Error") @@ -1385,7 +1391,7 @@ public final Response getRecentBaseTypes(@Context final HttpServletRequest reque Response response; try { final List types = contentTypeHelper.getTypes(request); - response = Response.ok(new ResponseEntityView<>(types)).build(); + response = Response.ok(new ResponseEntityBaseContentTypesView(types)).build(); } catch (Exception e) { // this is an unknown error, so we report as a 500. response = ExceptionMapperUtil.createResponse(e, Response.Status.INTERNAL_SERVER_ERROR); @@ -1412,9 +1418,8 @@ public final Response getRecentBaseTypes(@Context final HttpServletRequest reque * Variable Name, or Inode. You can pass down part of the characters. * @param page The selected results page, for pagination purposes. * @param perPage The number of results to return per page, for pagination purposes. - * @param orderByParam The column name that will be used to sort the paginated results. For - * reference, please check - * {@link com.dotmarketing.common.util.SQLUtil#ORDERBY_WHITELIST}. + * @param orderByParam The column name that will be used to sort the paginated results. + * . * @param direction The direction of the sorting. It can be either "ASC" or "DESC". * @param type The Velocity variable name of the Content Type to retrieve. * @param siteId The identifier of the Site where the requested Content Types live. @@ -1428,7 +1433,7 @@ public final Response getRecentBaseTypes(@Context final HttpServletRequest reque @GET @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation( operationId = "getContentType", summary = "Retrieves a list of content types", diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldResource.java index 5c051792dc85..245609b397a2 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldResource.java @@ -9,9 +9,14 @@ import com.dotcms.contenttype.transform.field.JsonFieldTransformer; import com.dotcms.repackage.com.google.common.annotations.VisibleForTesting; import com.dotcms.rest.InitDataObject; +import com.dotcms.rest.ResponseEntityMapStringObjectView; +import com.dotcms.rest.ResponseEntityMapView; +import com.dotcms.rest.ResponseEntityStringView; import com.dotcms.rest.ResponseEntityView; +import com.dotcms.rest.ResponseEntityListMapView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.exception.ForbiddenException; import com.dotcms.rest.exception.mapper.ExceptionMapperUtil; import com.dotmarketing.business.APILocator; @@ -37,14 +42,22 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.glassfish.jersey.server.JSONP; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; /** * @deprecated {@link com.dotcms.rest.api.v2.contenttype.FieldResource} should be used instead. Path:/v2/contenttype/{typeId}/fields */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Deprecated @Path("/v1/contenttype/{typeId}/fields") -@Tag(name = "Content Type Field", description = "Content type field management and configuration") +@Tag(name = "Content Type Field") public class FieldResource implements Serializable { private final WebResource webResource; private final FieldAPI fieldAPI; @@ -62,13 +75,40 @@ public FieldResource(final WebResource webresource, final FieldAPI fieldAPI) { private static final long serialVersionUID = 1L; + @Operation( + summary = "Update content type fields (deprecated)", + description = "Updates multiple fields for a content type. Use v2 API instead." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Fields updated successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityListMapView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - content type not found", + content = @Content(mediaType = "application/json")) + }) @PUT @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response updateFields(@PathParam("typeId") final String typeId, final String fieldsJson, - @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { + @Produces({ MediaType.APPLICATION_JSON }) + public Response updateFields(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @RequestBody(description = "Fields JSON data", + required = true, + content = @Content(schema = @Schema(implementation = String.class))) + final String fieldsJson, + @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); final User user = initData.getUser(); @@ -102,12 +142,39 @@ public Response updateFields(@PathParam("typeId") final String typeId, final Str return response; } + @Operation( + summary = "Create content type field (deprecated)", + description = "Creates a new field for a content type. Use v2 API instead." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field created successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapStringObjectView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - content type not found", + content = @Content(mediaType = "application/json")) + }) @POST @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response createContentTypeField(@PathParam("typeId") final String typeId, final String fieldJson, + @Produces({ MediaType.APPLICATION_JSON }) + public Response createContentTypeField(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @RequestBody(description = "Field JSON data", + required = true, + content = @Content(schema = @Schema(implementation = String.class))) + final String fieldJson, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); @@ -126,7 +193,7 @@ public Response createContentTypeField(@PathParam("typeId") final String typeId, field = fapi.save(field, user); - response = Response.ok(new ResponseEntityView(new JsonFieldTransformer(field).mapObject())).build(); + response = Response.ok(new ResponseEntityView<>(new JsonFieldTransformer(field).mapObject())).build(); } } catch (DotStateException e) { @@ -147,11 +214,33 @@ public Response createContentTypeField(@PathParam("typeId") final String typeId, return response; } + @Operation( + summary = "Get content type fields (deprecated)", + description = "Retrieves all fields for a specific content type. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Fields retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityListMapView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @GET @JSONP @NoCache - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public final Response getContentTypeFields(@PathParam("typeId") final String typeId, + @Produces({ MediaType.APPLICATION_JSON }) + public final Response getContentTypeFields(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, @Context final HttpServletRequest req) { final InitDataObject initData = this.webResource.init(null, true, req, true, null); @@ -182,13 +271,35 @@ public final Response getContentTypeFields(@PathParam("typeId") final String typ } + @Operation( + summary = "Get content type field by ID (deprecated)", + description = "Retrieves a specific field from a content type by its unique field ID. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapStringObjectView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type or field not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/id/{fieldId}") @JSONP @NoCache - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response getContentTypeFieldById(@PathParam("typeId") final String typeId, - @PathParam("fieldId") final String fieldId, @Context final HttpServletRequest req) + @Produces({ MediaType.APPLICATION_JSON }) + public Response getContentTypeFieldById(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, + @Parameter(description = "Field ID", required = true) @PathParam("fieldId") final String fieldId, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); @@ -199,7 +310,7 @@ public Response getContentTypeFieldById(@PathParam("typeId") final String typeId Field field = fapi.find(fieldId); - response = Response.ok(new ResponseEntityView(new JsonFieldTransformer(field).mapObject())).build(); + response = Response.ok(new ResponseEntityView<>(new JsonFieldTransformer(field).mapObject())).build(); } catch (NotFoundInDbException e) { @@ -213,13 +324,35 @@ public Response getContentTypeFieldById(@PathParam("typeId") final String typeId return response; } + @Operation( + summary = "Get content type field by variable name (deprecated)", + description = "Retrieves a specific field from a content type by its variable name. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapStringObjectView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type or field not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/var/{fieldVar}") @JSONP @NoCache - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response getContentTypeFieldByVar(@PathParam("typeId") final String typeId, - @PathParam("fieldVar") final String fieldVar, @Context final HttpServletRequest httpServletRequest, @Context final HttpServletResponse httpServletResponse) + @Produces({ MediaType.APPLICATION_JSON }) + public Response getContentTypeFieldByVar(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, + @Parameter(description = "Field variable name", required = true) @PathParam("fieldVar") final String fieldVar, @Context final HttpServletRequest httpServletRequest, @Context final HttpServletResponse httpServletResponse) throws DotDataException, DotSecurityException { this.webResource.init(null, httpServletRequest, httpServletResponse, false, null); @@ -230,7 +363,7 @@ public Response getContentTypeFieldByVar(@PathParam("typeId") final String typeI Field field = typeFieldAPI.byContentTypeIdAndVar(typeId, fieldVar); - response = Response.ok(new ResponseEntityView(new JsonFieldTransformer(field).mapObject())).build(); + response = Response.ok(new ResponseEntityView<>(new JsonFieldTransformer(field).mapObject())).build(); } catch (NotFoundInDbException e) { @@ -245,14 +378,39 @@ public Response getContentTypeFieldByVar(@PathParam("typeId") final String typeI } + @Operation( + summary = "Update content type field by ID (deprecated)", + description = "Updates a specific field in a content type by its field ID. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field updated successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapStringObjectView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field data or missing field ID", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type or field not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @PUT @Path("/id/{fieldId}") @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response updateContentTypeFieldById(@PathParam("typeId") final String typeId, @PathParam("fieldId") final String fieldId, - final String fieldJson, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { + @Produces({ MediaType.APPLICATION_JSON }) + public Response updateContentTypeFieldById(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, @Parameter(description = "Field ID to update", required = true) @PathParam("fieldId") final String fieldId, + @RequestBody(description = "Field JSON data with updates", required = true) final String fieldJson, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); final User user = initData.getUser(); @@ -278,7 +436,7 @@ public Response updateContentTypeFieldById(@PathParam("typeId") final String typ field = fapi.save(field, user); - response = Response.ok(new ResponseEntityView(new JsonFieldTransformer(field).mapObject())).build(); + response = Response.ok(new ResponseEntityView<>(new JsonFieldTransformer(field).mapObject())).build(); } } } catch (DotStateException e) { @@ -300,14 +458,39 @@ public Response updateContentTypeFieldById(@PathParam("typeId") final String typ return response; } + @Operation( + summary = "Update content type field by variable name (deprecated)", + description = "Updates a specific field in a content type by its variable name. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field updated successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapStringObjectView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type or field not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @PUT @Path("/var/{fieldVar}") @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response updateContentTypeFieldByVar(@PathParam("typeId") final String typeId, @PathParam("fieldVar") final String fieldVar, - final String fieldJson, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { + @Produces({ MediaType.APPLICATION_JSON }) + public Response updateContentTypeFieldByVar(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, @Parameter(description = "Field variable name to update", required = true) @PathParam("fieldVar") final String fieldVar, + @RequestBody(description = "Field JSON data with updates", required = true) final String fieldJson, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); final User user = initData.getUser(); @@ -333,7 +516,7 @@ public Response updateContentTypeFieldByVar(@PathParam("typeId") final String ty field = fapi.save(field, user); - response = Response.ok(new ResponseEntityView(new JsonFieldTransformer(field).mapObject())).build(); + response = Response.ok(new ResponseEntityView<>(new JsonFieldTransformer(field).mapObject())).build(); } } } catch (DotStateException e) { @@ -356,11 +539,37 @@ public Response updateContentTypeFieldByVar(@PathParam("typeId") final String ty } + @Operation( + summary = "Delete multiple fields (deprecated)", + description = "Deletes multiple fields from a content type by their field IDs. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Fields deleted successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field IDs", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type or one or more fields not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @DELETE @JSONP @NoCache - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response deleteFields(@PathParam("typeId") final String typeId, final String[] fieldsID, @Context final HttpServletRequest req) + @Consumes(MediaType.APPLICATION_JSON) + @Produces({ MediaType.APPLICATION_JSON }) + public Response deleteFields(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, @RequestBody(description = "Array of field IDs to delete", required = true) final String[] fieldsID, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); @@ -381,7 +590,7 @@ public Response deleteFields(@PathParam("typeId") final String typeId, final Str } final List contentTypeFields = fieldAPI.byContentTypeId(typeId); - response = Response.ok(new ResponseEntityView(imap("deletedIds", deletedIds, + response = Response.ok(new ResponseEntityView<>(imap("deletedIds", deletedIds, "fields", new JsonFieldTransformer(contentTypeFields).mapList()))).build(); } catch (DotSecurityException e) { @@ -395,13 +604,35 @@ public Response deleteFields(@PathParam("typeId") final String typeId, final Str return response; } + @Operation( + summary = "Delete content type field by ID (deprecated)", + description = "Deletes a specific field from a content type by its field ID. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field deleted successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityStringView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type or field not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @DELETE @Path("/id/{fieldId}") @JSONP @NoCache - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response deleteContentTypeFieldById(@PathParam("typeId") final String typeId, - @PathParam("fieldId") final String fieldId, @Context final HttpServletRequest req) + @Produces({ MediaType.APPLICATION_JSON }) + public Response deleteContentTypeFieldById(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, + @Parameter(description = "Field ID to delete", required = true) @PathParam("fieldId") final String fieldId, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); @@ -431,13 +662,35 @@ public Response deleteContentTypeFieldById(@PathParam("typeId") final String typ return response; } + @Operation( + summary = "Delete content type field by variable name (deprecated)", + description = "Deletes a specific field from a content type by its variable name. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field deleted successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityStringView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type or field not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @DELETE @Path("/var/{fieldVar}") @JSONP @NoCache - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response deleteContentTypeFieldByVar(@PathParam("typeId") final String typeId, - @PathParam("fieldVar") final String fieldVar, @Context final HttpServletRequest req) + @Produces({ MediaType.APPLICATION_JSON }) + public Response deleteContentTypeFieldByVar(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, + @Parameter(description = "Field variable name to delete", required = true) @PathParam("fieldVar") final String fieldVar, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldVariableResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldVariableResource.java index e942472aa489..e7b2167b0447 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldVariableResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldVariableResource.java @@ -11,12 +11,20 @@ import com.dotcms.rest.ResponseEntityView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotmarketing.business.APILocator; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.util.UtilMethods; import com.liferay.portal.model.User; import org.glassfish.jersey.server.JSONP; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import javax.servlet.http.HttpServletRequest; @@ -50,8 +58,9 @@ * @author Anibal Gomez * @since Apre 26th, 2017 */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/contenttype/{typeId}/fields") -@Tag(name = "Content Type Field", description = "Content type field management and configuration") +@Tag(name = "Content Type Field") public class FieldVariableResource implements Serializable { private final transient WebResource webResource; @@ -99,14 +108,40 @@ public FieldVariableResource(final WebResource webresource, final FieldAPI field * @throws DotSecurityException The current user does not have the necessary permissions to * execute this action. */ + @Operation( + summary = "Create field variable by field ID", + description = "Creates a new field variable for a specific field identified by its ID" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable created successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field variable data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field not found", + content = @Content(mediaType = "application/json")) + }) @POST @Path("/id/{fieldId}/variables") @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response createFieldVariableByFieldId(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public Response createFieldVariableByFieldId(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field ID", required = true) @PathParam("fieldId") final String fieldId, + @RequestBody(description = "Field variable data", + required = true, + content = @Content(schema = @Schema(implementation = String.class))) final String fieldVariableJson, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException, DotSecurityException { @@ -150,14 +185,40 @@ public Response createFieldVariableByFieldId(@PathParam("typeId") final String t * @throws DotSecurityException The current user does not have the necessary permissions to * execute this action. */ + @Operation( + summary = "Create field variable by field variable name", + description = "Creates a new field variable for a specific field identified by its velocity variable name" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable created successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field variable data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field not found", + content = @Content(mediaType = "application/json")) + }) @POST @Path("/var/{fieldVar}/variables") @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response createFieldVariableByFieldVar(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public Response createFieldVariableByFieldVar(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field velocity variable name", required = true) @PathParam("fieldVar") final String fieldVar, + @RequestBody(description = "Field variable data", + required = true, + content = @Content(schema = @Schema(implementation = String.class))) final String fieldVariableJson, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException, DotSecurityException { @@ -187,12 +248,26 @@ public Response createFieldVariableByFieldVar(@PathParam("typeId") final String * * @throws DotDataException An error occurred when accessing the database. */ + @Operation( + summary = "Get field variables by field ID", + description = "Retrieves all field variables for a specific field identified by its ID" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variables retrieved successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field not found", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/id/{fieldId}/variables") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public final Response getFieldVariablesByFieldId(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public final Response getFieldVariablesByFieldId(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field ID", required = true) @PathParam("fieldId") final String fieldId, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException { @@ -218,12 +293,26 @@ public final Response getFieldVariablesByFieldId(@PathParam("typeId") final Stri * * @throws DotDataException An error occurred when accessing the database. */ + @Operation( + summary = "Get field variables by field variable name", + description = "Retrieves all field variables for a specific field identified by its velocity variable name" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variables retrieved successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field not found", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/var/{fieldVar}/variables") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public final Response getFieldVariablesByFieldVar(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public final Response getFieldVariablesByFieldVar(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field velocity variable name", required = true) @PathParam("fieldVar") final String fieldVar, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException { @@ -250,13 +339,28 @@ public final Response getFieldVariablesByFieldVar(@PathParam("typeId") final Str * @throws DotDataException An error occurred when retrieving the Field Variable from the * database. */ + @Operation( + summary = "Get specific field variable by field ID", + description = "Retrieves a specific field variable by field ID and variable ID" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable retrieved successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field or field variable not found", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/id/{fieldId}/variables/id/{fieldVarId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response getFieldVariableByFieldId(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public Response getFieldVariableByFieldId(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field ID", required = true) @PathParam("fieldId") final String fieldId, + @Parameter(description = "Field variable ID", required = true) @PathParam("fieldVarId") final String fieldVarId, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException { @@ -284,13 +388,28 @@ public Response getFieldVariableByFieldId(@PathParam("typeId") final String type * @throws DotDataException An error occurred when retrieving the Field Variable from the * database. */ + @Operation( + summary = "Get specific field variable by field variable name", + description = "Retrieves a specific field variable by field velocity variable name and variable ID" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable retrieved successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field or field variable not found", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/var/{fieldVar}/variables/id/{fieldVarId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response getFieldVariableByFieldVar(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public Response getFieldVariableByFieldVar(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field velocity variable name", required = true) @PathParam("fieldVar") final String fieldVar, + @Parameter(description = "Field variable ID", required = true) @PathParam("fieldVarId") final String fieldVarId, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException { @@ -330,15 +449,42 @@ public Response getFieldVariableByFieldVar(@PathParam("typeId") final String typ * @throws DotSecurityException The current user does not have the necessary permissions to * execute this action. */ + @Operation( + summary = "Update field variable by field ID", + description = "Updates an existing field variable for a specific field identified by its ID" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable updated successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field variable data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field or field variable not found", + content = @Content(mediaType = "application/json")) + }) @PUT @Path("/id/{fieldId}/variables/id/{fieldVarId}") @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response updateFieldVariableByFieldId(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public Response updateFieldVariableByFieldId(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field ID", required = true) @PathParam("fieldId") final String fieldId, + @Parameter(description = "Field variable ID", required = true) @PathParam("fieldVarId") final String fieldVarId, + @RequestBody(description = "Updated field variable data", + required = true, + content = @Content(schema = @Schema(implementation = String.class))) final String fieldVariableJson, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException, DotSecurityException { @@ -383,15 +529,42 @@ public Response updateFieldVariableByFieldId(@PathParam("typeId") final String t * @throws DotSecurityException The current user does not have the necessary permissions to * execute this action. */ + @Operation( + summary = "Update field variable by field variable name", + description = "Updates an existing field variable for a specific field identified by its velocity variable name" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable updated successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field variable data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field or field variable not found", + content = @Content(mediaType = "application/json")) + }) @PUT @Path("/var/{fieldVar}/variables/id/{fieldVarId}") @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response updateFieldVariableByFieldVar(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public Response updateFieldVariableByFieldVar(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field velocity variable name", required = true) @PathParam("fieldVar") final String fieldVar, + @Parameter(description = "Field variable ID", required = true) @PathParam("fieldVarId") final String fieldVarId, + @RequestBody(description = "Updated field variable data", + required = true, + content = @Content(schema = @Schema(implementation = String.class))) final String fieldVariableJson, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException, DotSecurityException { @@ -423,13 +596,34 @@ public Response updateFieldVariableByFieldVar(@PathParam("typeId") final String * @throws DotDataException An error occurred when deleting the Field Variable from the * database. */ + @Operation( + summary = "Delete field variable by field ID", + description = "Deletes a specific field variable for a field identified by its ID" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable deleted successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field or field variable not found", + content = @Content(mediaType = "application/json")) + }) @DELETE @Path("/id/{fieldId}/variables/id/{fieldVarId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response deleteFieldVariableByFieldId(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public Response deleteFieldVariableByFieldId(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field ID", required = true) @PathParam("fieldId") final String fieldId, + @Parameter(description = "Field variable ID", required = true) @PathParam("fieldVarId") final String fieldVarId, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException, UniqueFieldValueDuplicatedException { @@ -463,13 +657,34 @@ public Response deleteFieldVariableByFieldId(@PathParam("typeId") final String t * @throws DotDataException An error occurred when deleting the Field Variable from the * database. */ + @Operation( + summary = "Delete field variable by field variable name", + description = "Deletes a specific field variable for a field identified by its velocity variable name" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable deleted successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field or field variable not found", + content = @Content(mediaType = "application/json")) + }) @DELETE @Path("/var/{fieldVar}/variables/id/{fieldVarId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response deleteFieldVariableByFieldVar(@PathParam("typeId") final String typeId, + @Produces({MediaType.APPLICATION_JSON}) + public Response deleteFieldVariableByFieldVar(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field velocity variable name", required = true) @PathParam("fieldVar") final String fieldVar, + @Parameter(description = "Field variable ID", required = true) @PathParam("fieldVarId") final String fieldVarId, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException, UniqueFieldValueDuplicatedException { diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowActionMultipartSchema.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowActionMultipartSchema.java new file mode 100644 index 000000000000..0e936136ae82 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowActionMultipartSchema.java @@ -0,0 +1,31 @@ +package com.dotcms.rest.api.v1.workflow; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import javax.ws.rs.FormParam; +import java.util.List; + +/** + * OpenAPI schema for workflow multipart requests. + * Runtime binding remains FormDataMultiPart; this class is only for documentation. + */ +@Schema(description = "Multipart form for workflow actions. Include a JSON 'contentlet' and one or more 'file' parts that map to binary field variables.") +public class WorkflowActionMultipartSchema { + + @FormParam("contentlet") + @Schema(description = "JSON object describing the contentlet values.", example = "{\\n \"contentType\": \"News\",\\n \"title\": \"My News\"\\n}") + public String contentlet; + + @FormParam("binaryFields") + @Schema(description = "List of binary field variables. The uploaded files in 'file' should correspond by index to these field variables.", example = "[\"binaryImage\", \"binaryDocument\"]") + public List binaryFields; + + @FormParam("file") + @ArraySchema( + arraySchema = @Schema(description = "Files to upload. Repeat the 'file' part for multiple files; order should match 'binaryFields'."), + schema = @Schema(type = "string", format = "binary") + ) + public List file; +} + + diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java index 634766a6615c..0b60d5cb8a5e 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java @@ -17,6 +17,7 @@ import com.dotcms.rest.*; import com.dotcms.rest.annotation.IncludePermissions; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.api.MultiPartUtils; import com.dotcms.rest.api.v1.DotObjectMapperProvider; import com.dotcms.rest.api.v1.authentication.RequestUtil; @@ -181,12 +182,9 @@ * @author jsanca * @since Dec 6th, 2017 */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/workflow") -@Tag(name = "Workflow", - description = "Endpoints that perform operations related to workflows.", - externalDocs = @ExternalDocumentation(description = "Additional Workflow API information", - url = "https://www.dotcms.com/docs/latest/workflow-rest-api") -) +@Tag(name = "Workflow") @ApiResponses( value = { // error codes only! @ApiResponse(responseCode = "401", description = "Invalid User"), // not logged in @@ -285,7 +283,7 @@ public WorkflowResource() { @Path("/schemes") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowSchemes", summary = "Find workflow schemes", description = "Returns workflow schemes. Can be filtered by content type and/or live status " + "through optional query parameters.", @@ -343,7 +341,7 @@ public final Response findSchemes(@Context final HttpServletRequest request, @Path("/actionlets") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowActionlets", summary = "Find all workflow actionlets", description = "Returns a list of all workflow actionlets — a.k.a. [workflow sub-actions]" + "(https://www.dotcms.com/docs/latest/workflow-sub-actions). " + @@ -385,7 +383,7 @@ public final Response findActionlets(@Context final HttpServletRequest request) @Path("/actions/{actionId}/actionlets") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowActionletsByActionId", summary = "Find workflow actionlets by workflow action", description = "Returns a list of the workflow actionlets — a.k.a. [workflow sub-actions](https://www.dotcms." + "com/docs/latest/workflow-sub-actions) — associated with a specified workflow action.", @@ -469,7 +467,7 @@ public final Response findActionletsByAction(@Context final HttpServletRequest r @Path("/schemes/schemescontenttypes/{contentTypeId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowSchemesByContentTypeId", summary = "Find workflow schemes by content type id", description = "Fetches [workflow schemes](https://www.dotcms.com/docs/latest/managing-workflows#Schemes) " + " associated with a content type by its identifier. Returns an entity containing two properties:\n\n" + @@ -508,7 +506,7 @@ public final Response findAllSchemesAndSchemesByContentType( final List schemes = this.workflowHelper.findSchemes(); final List contentTypeSchemes = this.workflowHelper.findSchemesByContentType(contentTypeId, initDataObject.getUser()); - return Response.ok(new ResponseEntityView( + return Response.ok(new ResponseEntityView<>( new SchemesAndSchemesContentTypeView(schemes, contentTypeSchemes))) .build(); // 200 } catch (Exception e) { @@ -531,7 +529,7 @@ public final Response findAllSchemesAndSchemesByContentType( @Path("/schemes/{schemeId}/steps") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowStepsBySchemeId", summary = "Find steps by workflow scheme ID", description = "Returns a list of [steps](https://www.dotcms.com/docs/latest/managing-workflows#Steps) " + "associated with a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes).", @@ -589,7 +587,7 @@ public final Response findStepsByScheme(@Context final HttpServletRequest reques @Path("/contentlet/{inode}/actions") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowActionsByContentletInode", summary = "Finds workflow actions by content inode", description = "Returns a list of [workflow actions](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + "associated with a [contentlet](https://www.dotcms.com/docs/latest/content#Contentlets) specified by inode.", @@ -732,8 +730,8 @@ private static List createActionInputViews (final WorkflowActio @Path("/contentlet/actions/bulk") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postBulkActions", summary = "Finds available bulk workflow actions for content", description = "Returns a list of bulk actions available for " + "[contentlets](https://www.dotcms.com/docs/latest/content#Contentlets) either by identifiers " + @@ -798,8 +796,8 @@ public final Response getBulkActions(@Context final HttpServletRequest request, @Path("/contentlet/actions/bulk/fire") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "putBulkActionsFire", summary = "Perform workflow actions on bulk content", description = "This operation allows you to specify a multiple content items (either by query or a list of " + "identifiers), a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + @@ -845,7 +843,7 @@ public final void fireBulkActions(@Context final HttpServletRequest request, try { final BulkActionsResultView view = workflowHelper .fireBulkActions(fireBulkActionsForm, initDataObject.getUser()); - return Response.ok( new ResponseEntityView(view)).build(); + return Response.ok( new ResponseEntityView<>(view)).build(); } catch (Exception e) { asyncResponse.resume(ResponseUtil.mapExceptionResponse(e)); } @@ -862,7 +860,7 @@ public final void fireBulkActions(@Context final HttpServletRequest request, @Path("/contentlet/actions/_bulkfire") @JSONP @Produces(SseFeature.SERVER_SENT_EVENTS) - @Consumes({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postBulkActionsFire", summary = "Perform workflow actions on bulk content", description = "This operation allows you to specify a multiple content items (either by query or a list of " + "identifiers), a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + @@ -961,7 +959,7 @@ public EventOutput fireBulkActions(@Context final HttpServletRequest request, @JSONP @NoCache @IncludePermissions - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowActionByActionId", summary = "Find action by ID", description = "Returns a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) object.", tags = {"Workflow"}, @@ -989,7 +987,7 @@ public final Response findAction(@Context final HttpServletRequest request, try { Logger.debug(this, ()->"Finding the workflow action " + actionId); final WorkflowAction action = this.workflowHelper.findAction(actionId, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(this.toWorkflowActionView(action))).build(); // 200 + return Response.ok(new ResponseEntityView<>(this.toWorkflowActionView(action))).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), "Exception on findAction, actionId: " + actionId + @@ -1010,7 +1008,7 @@ public final Response findAction(@Context final HttpServletRequest request, @JSONP @NoCache @IncludePermissions - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowConditionByActionId", summary = "Find condition by action ID", description = "Returns a string representing the \"condition\" on the selected action.\n\n" + "More specifically: if the workflow action has anything in its [Custom Code]" + @@ -1043,7 +1041,7 @@ public final Response evaluateActionCondition( Logger.debug(this, ()->"Finding the workflow action " + actionId); final String evaluated = workflowHelper.evaluateActionCondition(actionId, initDataObject.getUser(), request, response); - return Response.ok(new ResponseEntityView(evaluated)).build(); // 200 + return Response.ok(new ResponseEntityView<>(evaluated)).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), "Exception on evaluateActionCondition, actionId: " + actionId + @@ -1063,7 +1061,7 @@ public final Response evaluateActionCondition( @Path("/steps/{stepId}/actions/{actionId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowActionByStepActionId", summary = "Find a workflow action within a step", description = "Returns a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + "if it exists within a specific [step](https://www.dotcms.com/docs/latest/managing-workflows#Steps).", @@ -1099,7 +1097,7 @@ public final Response findActionByStep(@Context final HttpServletRequest request try { Logger.debug(this, "Getting the workflow action " + actionId + " for the step: " + stepId); final WorkflowAction action = this.workflowHelper.findAction(actionId, stepId, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(action)).build(); // 200 + return Response.ok(new ResponseEntityView<>(action)).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), "Exception on findAction, actionId: " + actionId + @@ -1120,7 +1118,7 @@ public final Response findActionByStep(@Context final HttpServletRequest request @Path("/steps/{stepId}/actions") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowActionsByStepId", summary = "Find all actions in a workflow step", description = "Returns a list of [workflow actions](https://www.dotcms.com/docs/latest/managing" + "-workflows#Actions) associated with a specified [workflow step](https://www.dotcms.com/" + @@ -1170,7 +1168,7 @@ public final Response findActionsByStep(@Context final HttpServletRequest reques @Path("/schemes/{schemeId}/actions") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowActionsBySchemeId", summary = "Find all actions in a workflow scheme", description = "Returns a list of [workflow actions](https://www.dotcms.com/docs/latest/managing-" + "workflows#Actions) associated with a specified [workflow scheme](https://www.dotcms.com/" + @@ -1220,8 +1218,8 @@ public final Response findActionsByScheme(@Context final HttpServletRequest requ @Path("/schemes/actions/{systemAction}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postFindActionsBySchemesAndSystemAction", summary = "Finds workflow actions by schemes and system action", description = "Returns a list of [workflow actions](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + "associated with [workflow schemes](https://www.dotcms.com/docs/latest/managing-workflows#Schemes), further " + @@ -1299,7 +1297,7 @@ public final Response findActionsBySchemesAndSystemAction(@Context final HttpSer @Path("/schemes/{schemeId}/system/actions") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getSystemActionMappingsBySchemeId", summary = "Find default system actions mapped to a workflow scheme", description = "Returns a list of [default system actions](https://www.dotcms.com/docs/latest/managing-" + "workflows#DefaultActions) associated with a specified [workflow scheme](https://www.dotcms.com" + @@ -1350,7 +1348,7 @@ public final Response findSystemActionsByScheme(@Context final HttpServletReques @Path("/contenttypes/{contentTypeVarOrId}/system/actions") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getSystemActionMappingsByContentType", summary = "Find default system actions mapped to a content type", description = "Returns a list of [default system actions](https://www.dotcms.com/docs/latest/managing-" + "workflows#DefaultActions) associated with a specified [content type](https://www.dotcms.com" + @@ -1403,7 +1401,7 @@ public final Response findSystemActionsByContentType(@Context final HttpServletR @Path("/system/actions/{workflowActionId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getSystemActionsByActionId", summary = "Find default system actions by workflow action id", description = "Returns a list of [default system actions]" + "(https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) associated with a " + @@ -1459,8 +1457,8 @@ public final Response getSystemActionsReferredByWorkflowAction(@Context final Ht @Path("/system/actions") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "putSaveSystemActions", summary = "Save a default system action mapping", description = "This operation allows you to save a [default system action]" + "(https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) mapping. This requires:\n\n" + @@ -1515,7 +1513,7 @@ public final Response saveSystemAction(@Context final HttpServletRequest request "scheme: " + workflowSystemActionForm.getSchemeId(): "var: " + workflowSystemActionForm.getContentTypeVariable())); - return Response.ok(new ResponseEntityView( + return Response.ok(new ResponseEntityView<>( this.workflowHelper.mapSystemActionToWorkflowAction(workflowSystemActionForm, initDataObject.getUser()))) .build(); // 200 } catch (final Exception e) { @@ -1540,7 +1538,7 @@ public final Response saveSystemAction(@Context final HttpServletRequest request @Path("/system/actions/{identifier}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "deleteSystemActionByActionId", summary = "Delete default system action binding by action id", description = "Deletes a [default system action]" + "(https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) binding.\n\n" + @@ -1578,7 +1576,7 @@ public final Response deletesSystemAction(@Context final HttpServletRequest requ Logger.debug(this, ()-> "Deleting system action: " + identifier); - return Response.ok(new ResponseEntityView( + return Response.ok(new ResponseEntityView<>( this.workflowHelper.deleteSystemAction(identifier, initDataObject.getUser()))) .build(); // 200 } catch (final Exception e) { @@ -1600,8 +1598,8 @@ public final Response deletesSystemAction(@Context final HttpServletRequest requ @Path("/actions") @JSONP @NoCache - @Consumes({MediaType.APPLICATION_JSON}) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "postActionsByWorkflowActionForm", summary = "Creates/saves a workflow action", description = "Creates or updates a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + "from the properties specified in the payload. Returns the created workflow action.", @@ -1668,7 +1666,7 @@ public final Response saveAction(@Context final HttpServletRequest request, DotPreconditions.notNull(workflowActionForm,"Expected Request body was empty."); Logger.debug(this, "Saving new workflow action: " + workflowActionForm.getActionName()); newAction = this.workflowHelper.saveAction(workflowActionForm, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(newAction)).build(); // 200 + return Response.ok(new ResponseEntityView<>(newAction)).build(); // 200 } catch (final Exception e) { @@ -1691,8 +1689,8 @@ public final Response saveAction(@Context final HttpServletRequest request, @Path("/actions/separator") @JSONP @NoCache - @Consumes({MediaType.APPLICATION_JSON}) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "addSeparatorAction", summary = "Creates workflow action separator", description = "Creates a [workflow action] separator(https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + "from the properties specified in the payload. Returns the created workflow action.", @@ -1748,8 +1746,8 @@ public final WorkflowActionView addSeparatorAction(@Context final HttpServletReq @Path("/actions/{actionId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "putSaveActionsByWorkflowActionForm", summary = "Update an existing workflow action", description = "Updates a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + "based on the payload properties.\n\nReturns updated workflow action.\n\n", @@ -1815,7 +1813,7 @@ public final Response updateAction(@Context final HttpServletRequest request, DotPreconditions.notNull(workflowActionForm,"Expected Request body was empty."); Logger.debug(this, "Updating action with id: " + actionId); final WorkflowAction workflowAction = this.workflowHelper.updateAction(actionId, workflowActionForm, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(workflowAction)).build(); // 200 + return Response.ok(new ResponseEntityView<>(workflowAction)).build(); // 200 } catch (final Exception e) { Logger.error(this.getClass(), "Exception on updateAction, actionId: " +actionId+", workflowActionForm: " + workflowActionForm + @@ -1835,8 +1833,8 @@ public final Response updateAction(@Context final HttpServletRequest request, @Path("/steps/{stepId}/actions") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postActionToStepById", summary = "Adds a workflow action to a workflow step", description = "Assigns a single [workflow action](https://www.dotcms.com/docs/latest" + "/managing-workflows#Actions) to a [workflow step](https://www.dotcms.com/docs" + @@ -1917,7 +1915,7 @@ public final Response saveActionToStep(@Context final HttpServletRequest request + " in to a step: " + stepId); this.workflowHelper.saveActionToStep(new WorkflowActionStepBean.Builder().stepId(stepId) .actionId(workflowActionStepForm.getActionId()).build(), initDataObject.getUser()); - return Response.ok(new ResponseEntityView(OK)).build(); // 200 + return Response.ok(new ResponseEntityView<>(OK)).build(); // 200 } catch (final Exception e) { Logger.error(this.getClass(), "Exception on saveActionToStep, stepId: "+stepId+", saveActionToStep: " + workflowActionStepForm + @@ -1937,8 +1935,8 @@ public final Response saveActionToStep(@Context final HttpServletRequest request @Path("/actions/{actionId}/actionlets") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postAddActionletToActionById", summary = "Adds an actionlet to a workflow action", description = "Adds an actionlet — a.k.a. a [workflow sub-action]" + "(https://www.dotcms.com/docs/latest/workflow-sub-actions) — to a [workflow action]" + @@ -2023,7 +2021,7 @@ public final Response saveActionletToAction(@Context final HttpServletRequest re .order(workflowActionletActionForm.getOrder()) .parameters(workflowActionletActionForm.getParameters()).build() , initDataObject.getUser()); - return Response.ok(new ResponseEntityView(OK)).build(); // 200 + return Response.ok(new ResponseEntityView<>(OK)).build(); // 200 } catch (final Exception e) { Logger.error(this.getClass(), "Exception on saveActionletToAction, actionId: "+actionId+", saveActionletToAction: " + workflowActionletActionForm + @@ -2043,7 +2041,7 @@ public final Response saveActionletToAction(@Context final HttpServletRequest re @Path("/steps/{stepId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "deleteWorkflowStepById", summary = "Delete a workflow step", description = "Deletes a [step](https://www.dotcms.com/docs/latest/managing-workflows#Steps) from a " + "[workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes).\n\n" + @@ -2090,7 +2088,7 @@ public final void deleteStep(@Context final HttpServletRequest request, @Path("/steps/{stepId}/actions/{actionId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "deleteWorkflowActionFromStepByActionId", summary = "Remove a workflow action from a step", description = "Deletes an [action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) from a " + "single [workflow step](https://www.dotcms.com/docs/latest/managing-workflows#Steps).\n\n" + @@ -2126,7 +2124,7 @@ public final Response deleteAction(@Context final HttpServletRequest request, Logger.debug(this, "Deleting the action: " + actionId + " for the step: " + stepId); this.workflowHelper.deleteAction(actionId, stepId, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(OK)).build(); // 200 + return Response.ok(new ResponseEntityView<>(OK)).build(); // 200 } catch (final Exception e) { Logger.error(this.getClass(), "Exception on deleteAction, actionId: "+actionId+", stepId: " + stepId + @@ -2146,7 +2144,7 @@ public final Response deleteAction(@Context final HttpServletRequest request, @Path("/actions/{actionId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "deleteWorkflowActionByActionId", summary = "Delete a workflow action", description = "Deletes a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + "from all [steps](https://www.dotcms.com/docs/latest/managing-workflows#Steps) in which it appears.\n\n" + @@ -2176,7 +2174,7 @@ public final Response deleteAction(@Context final HttpServletRequest request, Logger.debug(this, "Deleting the action: " + actionId); this.workflowHelper.deleteAction(actionId, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(OK)).build(); // 200 + return Response.ok(new ResponseEntityView<>(OK)).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), "Exception on deleteAction, action: " + actionId + @@ -2196,7 +2194,7 @@ public final Response deleteAction(@Context final HttpServletRequest request, @Path("/actionlets/{actionletId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "deleteWorkflowActionletFromAction", summary = "Remove an actionlet from a workflow action", description = "Removes an [actionlet](https://www.dotcms.com/docs/latest/workflow-sub-actions), or sub-action, " + "from a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions). This deletes " + @@ -2238,7 +2236,7 @@ public final Response deleteActionlet(@Context final HttpServletRequest request, DoesNotExistException.class); this.workflowAPI.deleteActionClass(actionClass, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(OK)).build(); // 200 + return Response.ok(new ResponseEntityView<>(OK)).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), "Exception on deleteActionlet, actionletId: " + actionletId + @@ -2259,8 +2257,7 @@ public final Response deleteActionlet(@Context final HttpServletRequest request, @Path("/reorder/step/{stepId}/order/{order}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "putReorderWorkflowStepsInScheme", summary = "Change the order of steps within a scheme", description = "Updates a [workflow step](https://www.dotcms.com/docs/latest/managing-workflows#Steps)'s " + "order within a [scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes) by " + @@ -2297,7 +2294,7 @@ public final Response reorderStep(@Context final HttpServletRequest request, Logger.debug(this, "Doing reordering of step: " + stepId + ", order: " + order); this.workflowHelper.reorderStep(stepId, order, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(OK)).build(); // 200 + return Response.ok(new ResponseEntityView<>(OK)).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), "WorkflowPortletAccessException on reorderStep, stepId: " + stepId + @@ -2318,8 +2315,8 @@ public final Response reorderStep(@Context final HttpServletRequest request, @Path("/steps/{stepId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "putUpdateWorkflowStepById", summary = "Update an existing workflow step", description = "Updates a [workflow step](https://www.dotcms.com/docs/latest/managing-workflows#Steps).\n\n" + "Returns an object representing the updated step.", @@ -2368,7 +2365,7 @@ public final Response updateStep(@Context final HttpServletRequest request, try { DotPreconditions.notNull(stepForm,"Expected Request body was empty."); final WorkflowStep step = this.workflowHelper.updateStep(stepId, stepForm, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(step)).build(); + return Response.ok(new ResponseEntityView<>(step)).build(); } catch (Exception e) { Logger.error(this.getClass(), "WorkflowPortletAccessException on updateStep, stepId: " + stepId + @@ -2387,8 +2384,8 @@ public final Response updateStep(@Context final HttpServletRequest request, @Path("/steps") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postAddWorkflowStep", summary = "Add a new workflow step", description = "Creates a [workflow step](https://www.dotcms.com/docs/latest/managing-workflows#Steps).\n\n" + "Returns an object representing the step.", @@ -2432,7 +2429,7 @@ public final Response addStep(@Context final HttpServletRequest request, final InitDataObject initDataObject = this.webResource.init(null, request, response, true, null); Logger.debug(this, "updating step for scheme with schemeId: " + schemeId); final WorkflowStep step = this.workflowHelper.addStep(newStepForm, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(step)).build(); + return Response.ok(new ResponseEntityView<>(step)).build(); } catch (final Exception e) { Logger.error(this.getClass(), "Exception on addStep, schemeId: " + schemeId + @@ -2452,7 +2449,7 @@ public final Response addStep(@Context final HttpServletRequest request, @Path("/steps/{stepId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "getFindWorkflowStepById", summary = "Retrieves a workflow step", description = "Returns a [workflow step](https://www.dotcms.com/docs/latest/managing-workflows#Steps) by identifier.", tags = {"Workflow"}, @@ -2478,7 +2475,7 @@ public final Response findStepById(@Context final HttpServletRequest request, Logger.debug(this, "finding step by id stepId: " + stepId); try { final WorkflowStep step = this.workflowHelper.findStepById(stepId); - return Response.ok(new ResponseEntityView(step)).build(); + return Response.ok(new ResponseEntityView<>(step)).build(); } catch (Exception e) { Logger.error(this.getClass(), "Exception on findStepById, stepId: " + stepId + @@ -2499,7 +2496,7 @@ public final Response findStepById(@Context final HttpServletRequest request, @Path("/actions/firemultipart") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Consumes(MediaType.MULTIPART_FORM_DATA) @Operation(operationId = "putFireActionByNameMultipart", summary = "Fire action by name (multipart form) \uD83D\uDEA7", description = "(**Construction notice:** Still needs request body documentation. Coming soon!)\n\n" + @@ -2512,7 +2509,7 @@ public final Response findStepById(@Context final HttpServletRequest request, responses = { @ApiResponse(responseCode = "200", description = "Fired action successfully", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityView.class) + schema = @Schema(implementation = ResponseEntityMapView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad request"), // invalid param string like `\` @@ -2557,13 +2554,14 @@ public final Response fireActionByNameMultipartNewPath(@Context final HttpServle description = "Language version of target content.", schema = @Schema(type = "string") ) final String language, - @RequestBody( - description = "Multipart form. More details to follow.", - required = true, - content = @Content( - schema = @Schema(implementation = FormDataMultiPart.class) - ) - ) final FormDataMultiPart multipart) { + @RequestBody( + description = "Multipart form containing a JSON 'contentlet' body and optional binary field parts.", + required = true, + content = @Content( + mediaType = MediaType.MULTIPART_FORM_DATA, + schema = @Schema(implementation = com.dotcms.rest.api.v1.workflow.WorkflowActionMultipartSchema.class) + ) + ) final FormDataMultiPart multipart) { return fireActionByNameMultipart(request, response, inode, identifier, indexPolicy, language, multipart); } @@ -2581,7 +2579,7 @@ public final Response fireActionByNameMultipartNewPath(@Context final HttpServle @Path("/actions/fire") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Consumes(MediaType.MULTIPART_FORM_DATA) @Hidden public final Response fireActionByNameMultipart(@Context final HttpServletRequest request, @@ -2644,8 +2642,8 @@ public final Response fireActionByNameMultipart(@Context final HttpServletReques @Path("/actions/fire") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "putFireActionByName", summary = "Fire workflow action by name", description = "Fires a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions), " + "specified by name, on a target contentlet.\n\nReturns a map of the resultant contentlet, " + @@ -2655,7 +2653,7 @@ public final Response fireActionByNameMultipart(@Context final HttpServletReques responses = { @ApiResponse(responseCode = "200", description = "Fired action successfully", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityView.class) + schema = @Schema(implementation = ResponseEntityMapView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad request"), // invalid param string like `\` @@ -2919,8 +2917,8 @@ private boolean needSave (final FireActionForm fireActionForm) { @Path("/actions/default/fire/{systemAction}") @JSONP @NoCache - @Consumes({MediaType.APPLICATION_JSON}) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "putFireDefaultSystemAction", summary = "Fire system action by name", description = "Fire a [default system action](https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) " + "by name on a target contentlet.\n\nReturns a map of the resultant contentlet, " + @@ -2930,7 +2928,7 @@ private boolean needSave (final FireActionForm fireActionForm) { responses = { @ApiResponse(responseCode = "200", description = "Fired action successfully", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityView.class) + schema = @Schema(implementation = ResponseEntityMapView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad request"), // invalid param string like `\` @@ -3139,9 +3137,8 @@ public final Response fireActionDefaultSinglePart(@Context final HttpServletRequ @Path("/actions/default/fire/{systemAction}") @JSONP @NoCache - //@Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Consumes(MediaType.APPLICATION_JSON) - @Produces("application/octet-stream") @Operation(operationId = "postFireSystemActionByNameMulti", summary = "Fire system action by name over multiple contentlets", description = "Fire a [default system action](https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) " + "by name on multiple target contentlets.\n\nReturns a list of resultant contentlet maps, each with an additional " + @@ -3151,7 +3148,7 @@ public final Response fireActionDefaultSinglePart(@Context final HttpServletRequ responses = { @ApiResponse(responseCode = "200", description = "Fired action successfully", content = @Content(mediaType = "application/octet-stream", - schema = @Schema(implementation = ResponseEntityView.class), + schema = @Schema(implementation = ResponseEntityMapView.class), examples = @ExampleObject(value = "{\n" + " \"entity\": {\n" + " \"results\": [\n" + @@ -3429,9 +3426,8 @@ private void saveMultipleContentletsByDefaultAction(final List(OK)).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), @@ -4804,8 +4808,8 @@ public final Response reorderAction(@Context final HttpServletRequest request, @Path("/schemes/import") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postImportScheme", summary = "Import a workflow scheme", description = "Import a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes).\n\n" + "Returns \"OK\" on success.", @@ -4862,7 +4866,7 @@ public final Response importScheme(@Context final HttpServletRequest httpServle exportObject, workflowSchemeImportForm.getPermissions(), initDataObject.getUser()); - response = Response.ok(new ResponseEntityView("OK")).build(); // 200 + response = Response.ok(new ResponseEntityView<>("OK")).build(); // 200 } catch (Exception e){ Logger.error(this.getClass(), @@ -4885,7 +4889,7 @@ public final Response importScheme(@Context final HttpServletRequest httpServle @Path("/schemes/{schemeIdOrVariable}/export") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "getExportScheme", summary = "Export a workflow scheme", description = "Export a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes).\n\n" + "Returns the full workflow scheme, along with steps, actions, permissions, etc., on success.", @@ -4893,7 +4897,7 @@ public final Response importScheme(@Context final HttpServletRequest httpServle responses = { @ApiResponse(responseCode = "200", description = "Exported workflow scheme successfully", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityView.class), + schema = @Schema(implementation = ResponseEntityWorkflowSchemeView.class), examples = @ExampleObject(value = "{\n" + " \"entity\": {\n" + " \"permissions\": [\n" + @@ -5071,7 +5075,7 @@ public final Response exportScheme(@Context final HttpServletRequest httpServle scheme = this.workflowAPI.findScheme(schemeIdOrVariable); exportObject = this.workflowImportExportUtil.buildExportObject(Arrays.asList(scheme)); permissions = this.workflowHelper.getActionsPermissions(exportObject.getActions()); - response = Response.ok(new ResponseEntityView( + response = Response.ok(new ResponseEntityView<>( Map.of("workflowObject", new WorkflowSchemeImportExportObjectView(VERSION, exportObject), "permissions", permissions))).build(); // 200 } catch (Exception e){ @@ -5096,8 +5100,8 @@ public final Response exportScheme(@Context final HttpServletRequest httpServle @Path("/schemes/{schemeId}/copy") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postCopyScheme", summary = "Copy a workflow scheme", description = "Copy a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes).\n\n " + "A name for the new scheme may be provided either by parameter or by POST body property; if no name " + @@ -5155,7 +5159,7 @@ public final Response copyScheme(@Context final HttpServletRequest httpServletRe ); Logger.debug(this, "Copying the workflow scheme: " + schemeId); - response = Response.ok(new ResponseEntityView( + response = Response.ok(new ResponseEntityView<>( this.workflowAPI.deepCopyWorkflowScheme( this.workflowAPI.findScheme(schemeId), initDataObject.getUser(), workflowName)) @@ -5179,7 +5183,7 @@ public final Response copyScheme(@Context final HttpServletRequest httpServletRe @Path("/defaultactions/contenttype/{contentTypeId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "getDefaultActionsByContentTypeId", summary = "Find possible default actions by content type", description = "Returns a list of actions that may be used as a [default action]" + "(https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) for a " + @@ -5224,7 +5228,7 @@ public final ResponseEntityDefaultWorkflowActionsView findAvailableDefaultAction @Path("/defaultactions/schemes") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "getDefaultActionsBySchemeIds", summary = "Find possible default actions by scheme(s)", description = "Returns a list of actions that are eligible to be used as a [default action]" + "(https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) for one or " + @@ -5278,7 +5282,7 @@ public final Response findAvailableDefaultActionsBySchemes( @Path("/initialactions/contenttype/{contentTypeId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "getInitialActionsByContentTypeId", summary = "Find initial actions by content type", description = "Returns a list of available actions of the initial/first step(s) of the workflow scheme(s) " + "associated with a [content type](https://www.dotcms.com/docs/latest/content-types).", @@ -5335,8 +5339,8 @@ public final Response findInitialAvailableActionsByContentType( @Path("/schemes") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postSaveScheme", summary = "Create a workflow scheme", description = "Create a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes).\n\n " + "Returns created workflow scheme on success.", @@ -5361,6 +5365,7 @@ public final Response saveScheme(@Context final HttpServletRequest request, "| `schemeDescription` | String | A description of the scheme. |\n" + "| `schemeArchived` | Boolean | If `true`, the scheme will be created " + "in an archived state. |\n", + required = true, content = @Content( schema = @Schema(implementation = WorkflowSchemeForm.class) ) @@ -5370,7 +5375,7 @@ public final Response saveScheme(@Context final HttpServletRequest request, DotPreconditions.notNull(workflowSchemeForm,"Expected Request body was empty."); Logger.debug(this, ()->"Saving scheme named: " + workflowSchemeForm.getSchemeName()); final WorkflowScheme scheme = this.workflowHelper.saveOrUpdate(null, workflowSchemeForm, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(scheme)).build(); // 200 + return Response.ok(new ResponseEntityView<>(scheme)).build(); // 200 } catch (Exception e) { final String schemeName = workflowSchemeForm == null ? "" : workflowSchemeForm.getSchemeName(); Logger.error(this.getClass(), "Exception on save, schema named: " + schemeName + ", exception message: " + e.getMessage(), e); @@ -5389,8 +5394,8 @@ public final Response saveScheme(@Context final HttpServletRequest request, @Path("/schemes/{schemeId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "putUpdateWorkflowScheme", summary = "Update a workflow scheme", description = "Updates a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes).\n\n" + "Returns updated scheme on success.", @@ -5432,7 +5437,7 @@ public final Response updateScheme(@Context final HttpServletRequest request, DotPreconditions.notNull(workflowSchemeForm,"Expected Request body was empty."); final User user = initDataObject.getUser(); final WorkflowScheme scheme = this.workflowHelper.saveOrUpdate(schemeId, workflowSchemeForm, user); - return Response.ok(new ResponseEntityView(scheme)).build(); // 200 + return Response.ok(new ResponseEntityView<>(scheme)).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), "Exception attempting to update schema identified by : " +schemeId + ", exception message: " + e.getMessage(), e); return ResponseUtil.mapExceptionResponse(e); @@ -5448,7 +5453,7 @@ public final Response updateScheme(@Context final HttpServletRequest request, @Path("/schemes/{schemeId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "deleteWorkflowSchemeById", summary = "Delete a workflow scheme", description = "Deletes a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes)\n\n" + "Scheme must already be in an archived state.\n\n" + @@ -5514,7 +5519,7 @@ public final void deleteScheme(@Context final HttpServletRequest request, @Path("/status/{contentletInode}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "getContentWorkflowStatusByInode", summary = "Find workflow status of content", description = "Checks the current workflow status of a contentlet by its [inode]" + "(https://www.dotcms.com/docs/latest/content-versions#IdentifiersInodes).\n\n" + @@ -5594,7 +5599,7 @@ public final ResponseContentletWorkflowStatusView getStatusForContentlet(@Contex @Path("/tasks/history/comments/{contentletIdentifier}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "getWorkflowTasksHistoryComments", summary = "Find workflow tasks history and comments of content", description = "Retrieve the workflow tasks comments of a contentlet by its [id]" + "(https://www.dotcms.com/docs/latest/content-versions#IdentifiersInodes).\n\n" + @@ -5671,9 +5676,9 @@ private WorkflowTimelineItemView toWorkflowTimelineItemView(final WorkflowTimeli @Path("/{contentletId}/comments") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) - @Operation(operationId = "postSaveScheme", summary = "Create a workflow comment", + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) + @Operation(operationId = "postSaveComment", summary = "Create a workflow comment", description = "Create a [workflow comment].\n\n " + "Returns created workflow comment on success.", tags = {"Workflow"}, diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v2/tags/TagResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v2/tags/TagResource.java index 7ee13fef0232..ffe4665049fd 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v2/tags/TagResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v2/tags/TagResource.java @@ -4,6 +4,7 @@ import com.dotcms.repackage.com.google.common.annotations.VisibleForTesting; import com.dotcms.rest.*; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.exception.BadRequestException; import com.dotcms.rest.exception.NotFoundException; import com.dotcms.rest.exception.ValidationException; @@ -68,8 +69,9 @@ * * @author jsanca */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v2/tags") -@io.swagger.v3.oas.annotations.tags.Tag(name = "Tags", description = "Content tagging and labeling") +@io.swagger.v3.oas.annotations.tags.Tag(name = "Tags", description = "Tag management operations for content categorization") public class TagResource { public static final String NO_TAGS_WERE_FOUND_BY_THE_INODE_S = "No tags with Inode %s were found."; @@ -112,13 +114,32 @@ protected TagResource(final TagAPI tagAPI, final HostAPI hostAPI, final FolderAP * @return The {@link ResponseEntityTagMapView} containing the list of Tags that match the * provided criteria. */ + @Operation( + summary = "List tags", + description = "Lists all tags based on provided criteria. If a tag name is provided, performs a search-by-name (like) operation that can be delimited by site ID. If no matches are found against the site ID, searches global tags." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Tags retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityTagMapView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions to access tags portlet", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @GET @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityTagMapView list(@Context final HttpServletRequest request, @Context final HttpServletResponse response, - @QueryParam("name") final String tagName, - @QueryParam("siteId") final String siteId) { + @Parameter(description = "Name of the tag to search for (optional)") @QueryParam("name") final String tagName, + @Parameter(description = "ID of the site where the tag lives (optional)") @QueryParam("siteId") final String siteId) { final InitDataObject initDataObject = new WebResource.InitBuilder(webResource) @@ -148,25 +169,25 @@ public ResponseEntityTagMapView list(@Context final HttpServletRequest request, * @return The {@link ResponseEntityTagMapView} containing the saved Tags. */ @Operation( - summary = "Create multiple tags", - description = "Creates multiple tags in bulk with optional owner assignment" + summary = "Create multiple tags", + description = "Creates multiple tags in bulk with optional owner assignment. Tags can be created with site-specific scoping." ) @ApiResponses(value = { - @ApiResponse(responseCode = "201", + @ApiResponse(responseCode = "200", description = "Tags created successfully", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityTagMapView.class))), - @ApiResponse(responseCode = "400", - description = "Bad Request - Invalid tag data", + schema = @Schema(implementation = ResponseEntityTagMapView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid tag data or parameters", content = @Content(mediaType = "application/json")), - @ApiResponse(responseCode = "401", - description = "Unauthorized - Authentication required", + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", content = @Content(mediaType = "application/json")), - @ApiResponse(responseCode = "403", - description = "Forbidden - User does not have access to Tags portlet", + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions to create tags", content = @Content(mediaType = "application/json")), - @ApiResponse(responseCode = "500", - description = "Internal Server Error - Database or system error", + @ApiResponse(responseCode = "500", + description = "Internal server error", content = @Content(mediaType = "application/json")) }) @POST @@ -174,14 +195,15 @@ public ResponseEntityTagMapView list(@Context final HttpServletRequest request, @Path("/_bulk") @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityTagMapView addTag( @Context final HttpServletRequest request, @Context final HttpServletResponse response, - @RequestBody(description = "Bulk tag data to create", - required = true, - content = @Content(schema = @Schema(implementation = TagForm.class))) - final TagForm tagForm) throws DotDataException, DotSecurityException { + @RequestBody( + description = "Tag form containing tags to create with optional owner assignment", + required = true, + content = @Content(schema = @Schema(implementation = TagForm.class)) + ) final TagForm tagForm) throws DotDataException, DotSecurityException { final InitDataObject initDataObject = getInitDataObject(request, response); @@ -233,7 +255,7 @@ public ResponseEntityTagMapView addTag( @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public Response createSingleTag( @Context final HttpServletRequest request, @Context final HttpServletResponse response, @@ -319,14 +341,44 @@ private void saveTags(final HttpServletRequest request, * * @return The {@link ResponseEntityTagMapView} containing the updated Tag information. */ + @Operation( + summary = "Update tag", + description = "Updates the information belonging to a specific tag. Requires tag ID, site ID, and new tag name." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Tag updated successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityTagMapView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid or incomplete tag data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions to update tags", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Tag not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @PUT @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityTagMapView updateTag( @Context final HttpServletRequest request, @Context final HttpServletResponse response, - final UpdateTagForm tagForm) throws DotDataException { + @RequestBody( + description = "Update tag form containing tag ID, site ID, and new tag name", + required = true, + content = @Content(schema = @Schema(implementation = UpdateTagForm.class)) + ) final UpdateTagForm tagForm) throws DotDataException { final InitDataObject initDataObject = getInitDataObject(request, response); @@ -372,14 +424,36 @@ public ResponseEntityTagMapView updateTag( * @return The {@link ResponseEntityTagMapView} containing the list of Tags that belong to a * User. */ + @Operation( + summary = "Get tags by user ID", + description = "Retrieves all tags owned by a specific user. Returns tags that were explicitly linked to the user during creation." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "User tags retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityTagMapView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions to access tags", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "No tags found for the specified user", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @GET @JSONP @Path("/user/{userId}") @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityTagMapView getTagsByUserId(@Context final HttpServletRequest request, @Context final HttpServletResponse response, - @PathParam("userId") final String userId) { + @Parameter(description = "User ID to retrieve tags for", required = true) @PathParam("userId") final String userId) { final InitDataObject initDataObject = getInitDataObject(request, response); final User user = initDataObject.getUser(); @@ -411,32 +485,32 @@ public ResponseEntityTagMapView getTagsByUserId(@Context final HttpServletReques * @return The {@link Response} containing the found Tag or error information. */ @Operation( - summary = "Get tag by name or ID", - description = "Retrieves a single tag by its name or UUID. For name-based searches, uses site context for disambiguation." + summary = "Get tag by name or ID", + description = "Retrieves a single tag by its name or UUID. For name-based searches, uses site context for disambiguation with SYSTEM_HOST fallback." ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", - description = "Tag found successfully", + @ApiResponse(responseCode = "200", + description = "Tag retrieved successfully", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityRestTagView.class))), - @ApiResponse(responseCode = "404", - description = "Tag not found", + schema = @Schema(implementation = ResponseEntityRestTagView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", content = @Content(mediaType = "application/json")), - @ApiResponse(responseCode = "401", - description = "Unauthorized - Authentication required", + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions to access tags", content = @Content(mediaType = "application/json")), - @ApiResponse(responseCode = "403", - description = "Forbidden - User does not have access to Tags portlet", + @ApiResponse(responseCode = "404", + description = "Tag not found by the specified name or ID", content = @Content(mediaType = "application/json")), - @ApiResponse(responseCode = "500", - description = "Internal Server Error", + @ApiResponse(responseCode = "500", + description = "Internal server error", content = @Content(mediaType = "application/json")) }) @GET @JSONP @Path("/{nameOrId}") @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityRestTagView getTagsByNameOrId(@Context final HttpServletRequest request, @Context final HttpServletResponse response, @Parameter(description = "Tag name or UUID", required = true) @@ -488,14 +562,36 @@ public ResponseEntityRestTagView getTagsByNameOrId(@Context final HttpServletReq * * @return A {@link ResponseEntityBooleanView} containing the result of the delete operation. */ + @Operation( + summary = "Delete tag", + description = "Deletes a tag based on its ID. The tag must exist and the user must have appropriate permissions." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Tag deleted successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityBooleanView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions to delete tags", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Tag not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @DELETE @JSONP @Path("/{tagId}") @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityBooleanView delete(@Context final HttpServletRequest request, @Context final HttpServletResponse response, - @PathParam("tagId") final String tagId) throws DotDataException { + @Parameter(description = "ID of the tag to delete", required = true) @PathParam("tagId") final String tagId) throws DotDataException { final InitDataObject initDataObject = getInitDataObject(request, response); final User user = initDataObject.getUser(); @@ -527,15 +623,40 @@ public ResponseEntityBooleanView delete(@Context final HttpServletRequest reques * * @return The {@link ResponseEntityTagInodesMapView} containing the list of linked Tags. */ + @Operation( + summary = "Link tags to inode", + description = "Binds tags with a given inode. Lookup can be done via tag name or ID. If tag name matches multiple tags, all matching tags will be bound. Use tag ID for specific binding." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Tags linked to inode successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityTagInodesMapView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid tag name/ID or inode", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions to link tags", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Tag not found by the specified name or ID", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @PUT @JSONP @Path("/tag/{nameOrId}/inode/{inode}") @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityTagInodesMapView linkTagsAndInode(@Context final HttpServletRequest request, @Context final HttpServletResponse response, - @PathParam("nameOrId") final String nameOrId, - @PathParam("inode") final String inode) throws DotDataException { + @Parameter(description = "Name or UUID of the tag to link", required = true) @PathParam("nameOrId") final String nameOrId, + @Parameter(description = "Inode to link the tag(s) to", required = true) @PathParam("inode") final String inode) throws DotDataException { final InitDataObject initDataObject = getInitDataObject(request, response); final User user = initDataObject.getUser(); @@ -578,14 +699,36 @@ public ResponseEntityTagInodesMapView linkTagsAndInode(@Context final HttpServle * @return The {@link ResponseEntityTagInodesMapView} containing the list of Tags that match the * specified Inode. */ + @Operation( + summary = "Get tags by inode", + description = "Retrieves all tags associated with a given inode. Returns tag-inode relationships for the specified content." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Tags retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityTagInodesMapView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions to access tags", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "No tags found for the specified inode", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @GET @JSONP @Path("/inode/{inode}") @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityTagInodesMapView findTagsByInode(@Context final HttpServletRequest request, @Context final HttpServletResponse response, - @PathParam("inode") final String inode) { + @Parameter(description = "Inode to retrieve tags for", required = true) @PathParam("inode") final String inode) { final InitDataObject initDataObject = getInitDataObject(request, response); final User user = initDataObject.getUser(); @@ -608,14 +751,36 @@ public ResponseEntityTagInodesMapView findTagsByInode(@Context final HttpServlet * * @return A {@link ResponseEntityBooleanView} containing the result of the delete operation. */ + @Operation( + summary = "Delete tag-inode associations", + description = "Breaks the link between an inode and all its associated tags. Removes all tag associations for the specified content but does not delete the tags themselves." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Tag-inode associations deleted successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityBooleanView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions to modify tag associations", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "No tag associations found for the specified inode", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @DELETE @JSONP @Path("/inode/{inode}") @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityBooleanView deleteTagInodesByInode(@Context final HttpServletRequest request, @Context final HttpServletResponse response, - @PathParam("inode") final String inode) throws DotDataException { + @Parameter(description = "Inode to remove tag associations from", required = true) @PathParam("inode") final String inode) throws DotDataException { final InitDataObject initDataObject = getInitDataObject(request, response); final User user = initDataObject.getUser(); @@ -670,16 +835,42 @@ private InitDataObject getInitDataObject(final HttpServletRequest request, * @throws DotSecurityException The specified user does not have the required permissions to * perform this operation. */ + @Operation( + summary = "Import tags from CSV", + description = "Imports tags from a CSV file. The CSV file should contain tag data in the expected format for bulk tag creation." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Tags imported successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityBooleanView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid CSV file format or data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions to import tags", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @POST @Path("/import") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON}) + @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.MULTIPART_FORM_DATA) public final ResponseEntityBooleanView importTags( @Context final HttpServletRequest request, @Context final HttpServletResponse response, - final FormDataMultiPart form + @RequestBody( + description = "CSV file containing tag data for import", + required = true, + content = @Content(mediaType = "multipart/form-data") + ) final FormDataMultiPart form ) throws DotDataException, IOException, DotSecurityException { final InitDataObject initDataObject = getInitDataObject(request, response); diff --git a/dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml b/dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml index 632b7d31237e..1a752504b870 100644 --- a/dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml +++ b/dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml @@ -8,8 +8,6 @@ servers: tags: - description: AI-powered content generation and analysis endpoints name: AI -- description: Content type field definitions and configuration - name: Content Type Field - description: JavaScript execution and server-side scripting name: JavaScript - description: Content bundle management and deployment @@ -36,12 +34,8 @@ tags: name: Authentication - description: File and folder browser tree operations name: Browser Tree -- description: Content categorization and taxonomy - name: Categories - description: Endpoints for managing Container objects and their content name: Containers -- description: Endpoints for managing content and contentlets - name: Content - description: Returns the content types valid for a page based on the container/types on the layout name: getPagesContentTypes @@ -78,11 +72,8 @@ tags: name: Templates - description: Endpoints for managing content variants name: Variants -- description: Endpoints that perform operations related to workflows. - externalDocs: - description: Additional Workflow API information - url: https://www.dotcms.com/docs/latest/workflow-rest-api - name: Workflow +- description: Content type field definitions and configuration + name: Content Type Field - description: System administration and management tools name: Administration - description: API token management and authentication @@ -90,8 +81,12 @@ tags: description: Additional API token information url: https://www.dotcms.com/docs/latest/rest-api-authentication#APIToken name: API Token +- description: Content categorization and taxonomy + name: Categories - description: Cluster nodes and distributed system management name: Cluster Management +- description: Endpoints for managing content and contentlets + name: Content - description: Content delivery and rendering name: Content Delivery - description: Content reporting and analytics @@ -156,6 +151,11 @@ tags: name: Web Assets - description: Widget development and rendering name: Widgets +- description: Endpoints that perform operations related to workflows. + externalDocs: + description: Additional Workflow API information + url: https://www.dotcms.com/docs/latest/workflow-rest-api + name: Workflow paths: /auditPublishing/get/{bundleId}: get: @@ -683,199 +683,372 @@ paths: - System Configuration /content/_search: post: + description: "Performs a comprehensive content search using Lucene query syntax.\ + \ Supports filtering, sorting, pagination, and depth-based relationship loading.\ + \ Returns structured JSON with search metadata and contentlet results." operationId: search parameters: - - in: query + - description: Store query in session for Query Tool portlet + in: query name: rememberQuery schema: type: boolean default: false requestBody: content: - '*/*': + application/json: schema: $ref: "#/components/schemas/SearchForm" + description: "Search criteria including query, sort, pagination and filters" + required: true responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntitySearchView" + description: Search completed successfully + "400": content: application/json: {} - description: default response + description: Invalid search parameters or malformed query + "401": + content: + application/json: {} + description: Authentication required + "403": + content: + application/json: {} + description: Insufficient permissions to access content + "500": + content: + application/json: {} + description: Internal server error during search + summary: Search content with advanced parameters tags: - Content Delivery /content/canLock/{params}: put: deprecated: true + description: Legacy endpoint for checking lock status of content. This endpoint + is deprecated and will be removed in future versions. Use the versioned v1 + content API instead. operationId: canLockContent parameters: - - in: path + - description: URL parameters containing content ID or inode + in: path name: params required: true schema: type: string pattern: .* responses: - default: + "200": content: application/json: {} - description: default response + description: Lock status checked successfully (deprecated endpoint) + "403": + content: + application/json: {} + description: Insufficient permissions or security error + "404": + content: + application/json: {} + description: Content not found + summary: Check if content can be locked (deprecated) tags: - Content Delivery /content/indexcount/{query}: get: + description: Returns the total count of contentlets matching the specified Lucene + query. This is useful for pagination calculations and understanding result + set sizes without retrieving the actual content data. operationId: indexCount_1 parameters: - - in: path + - description: Lucene query string to count matching contentlets + in: path name: query required: true schema: type: string - - in: path + - description: Response format type (optional) + in: query name: type - required: true schema: type: string - - in: path + - description: JSONP callback function name (optional) + in: query name: callback - required: true schema: type: string responses: - default: + "200": content: text/plain: {} - description: default response + description: Count retrieved successfully + "400": + content: + application/json: {} + description: Invalid query syntax + "401": + content: + application/json: {} + description: Authentication required + "403": + content: + application/json: {} + description: Insufficient permissions to query content + "500": + content: + application/json: {} + description: Internal server error during count operation + summary: Count content matching query tags: - Content Delivery /content/indexsearch/{query}/sortby/{sortby}/limit/{limit}/offset/{offset}: get: + description: Performs a direct Elasticsearch index search using Lucene query + syntax. Returns a simplified JSON array containing only inode and identifier + for each matching contentlet. This is a lighter-weight alternative to the + POST search endpoint. operationId: indexSearch parameters: - - in: path + - description: "Lucene query string (e.g., '+structurename:webpagecontent +live:true')" + in: path name: query required: true schema: type: string - - in: path + - description: "Field to sort results by (e.g., 'modDate', 'title')" + in: path name: sortby required: true schema: type: string - - in: path + - description: Maximum number of results to return + in: path name: limit required: true schema: type: integer format: int32 - - in: path + - description: Number of results to skip for pagination + in: path name: offset required: true schema: type: integer format: int32 - - in: path + - description: Response format type (optional) + in: query name: type - required: true schema: type: string - - in: path + - description: JSONP callback function name (optional) + in: query name: callback - required: true schema: type: string responses: - default: + "200": content: application/json: {} - description: default response + description: Index search completed successfully + "400": + content: + application/json: {} + description: Invalid query syntax or parameters + "401": + content: + application/json: {} + description: Authentication required + "403": + content: + application/json: {} + description: Insufficient permissions to search index + "500": + content: + application/json: {} + description: Internal server error during index search + summary: Search content index with URL parameters tags: - Content Delivery /content/lock/{params}: put: deprecated: true + description: Legacy endpoint for locking content. This endpoint is deprecated + and will be removed in future versions. Use the versioned v1 content API instead. operationId: lockContent parameters: - - in: path + - description: URL parameters containing content ID or inode + in: path name: params required: true schema: type: string pattern: .* responses: - default: + "200": content: application/json: {} - description: default response + description: Content locked successfully (deprecated endpoint) + "403": + content: + application/json: {} + description: Insufficient permissions or security error + "404": + content: + application/json: {} + description: Content not found + summary: Lock content (deprecated) tags: - Content Delivery /content/unlock/{params}: put: deprecated: true + description: Legacy endpoint for unlocking content. This endpoint is deprecated + and will be removed in future versions. Use the versioned v1 content API instead. operationId: unlockContent parameters: - - in: path + - description: URL parameters containing content ID or inode + in: path name: params required: true schema: type: string pattern: .* responses: - default: + "200": content: application/json: {} - description: default response + description: Content unlocked successfully (deprecated endpoint) + "403": + content: + application/json: {} + description: Insufficient permissions or security error + "404": + content: + application/json: {} + description: Content not found + summary: Unlock content (deprecated) tags: - Content Delivery /content/{params}: get: + description: "Retrieves contentlets using flexible URL parameters. Supports\ + \ content lookup by identifier, inode, or Lucene query. Includes depth-based\ + \ relationship loading (0-3 levels) and supports both JSON and XML output\ + \ formats. This is a legacy endpoint - prefer using the versioned v1 content\ + \ API for new implementations." operationId: getContent parameters: - - in: path + - description: "URL parameters in key/value format (e.g., 'id/abc123' or 'query/+structurename:News/limit/10')" + in: path name: params required: true schema: type: string pattern: .* responses: - default: + "200": content: application/json: {} - description: default response + description: Content retrieved successfully + "400": + content: + application/json: {} + description: Invalid parameters or malformed query + "401": + content: + application/json: {} + description: Authentication required + "403": + content: + application/json: {} + description: Insufficient permissions to access content + "404": + content: + application/json: {} + description: Content not found + "500": + content: + application/json: {} + description: Internal server error + summary: "Get content by ID, inode, or query" tags: - Content Delivery post: deprecated: true + description: "Legacy endpoint for creating content using JSON, XML, or form-encoded\ + \ data. This endpoint is deprecated and will be removed in future versions.\ + \ Use the WorkflowResource#fireActionDefault instead." operationId: singlePOST parameters: - - in: path + - description: URL parameters for content creation + in: path name: params required: true schema: type: string pattern: .* + requestBody: + content: + application/json: {} + application/x-www-form-urlencoded: {} + application/xml: {} + description: "Content data in JSON, XML, or form format" + required: true responses: - default: + "200": content: application/json: {} - text/plain: {} - description: default response + description: Content created successfully (deprecated endpoint) + "400": + content: + application/json: {} + description: Invalid request data or parameters + "403": + content: + application/json: {} + description: Insufficient permissions or security error + summary: Create content with JSON/XML/form data (deprecated) tags: - Content Delivery put: deprecated: true + description: "Legacy endpoint for updating content using JSON, XML, or form-encoded\ + \ data. This endpoint is deprecated and will be removed in future versions.\ + \ Use the WorkflowResource#fireActionDefault instead." operationId: singlePUT parameters: - - in: path + - description: URL parameters for content update + in: path name: params required: true schema: type: string pattern: .* + requestBody: + content: + application/json: {} + application/x-www-form-urlencoded: {} + application/xml: {} + description: "Content data in JSON, XML, or form format" + required: true responses: - default: + "200": content: application/json: {} - text/plain: {} - description: default response + description: Content updated successfully (deprecated endpoint) + "400": + content: + application/json: {} + description: Invalid request data or parameters + "403": + content: + application/json: {} + description: Insufficient permissions or security error + summary: Update content with JSON/XML/form data (deprecated) tags: - Content Delivery /environment: @@ -4117,233 +4290,380 @@ paths: - Cache Management /v1/categories: delete: + description: Deletes multiple categories by their inodes. Deletes both parent + categories and their children. User needs Edit permissions on categories to + delete them successfully. operationId: delete_7 requestBody: content: - '*/*': + application/json: schema: type: array items: type: string + description: List of category inodes to delete + required: true responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Categories deleted successfully (may include partial failures) + "400": + description: Bad request - missing category inodes + "401": + description: Unauthorized - user authentication required + "403": + description: Forbidden - insufficient permissions to delete categories + "500": + description: Internal server error deleting categories + summary: Delete categories tags: - Categories get: + description: "Retrieves a paginated list of categories with optional filtering,\ + \ sorting, and children count information. Supports hierarchical category\ + \ navigation and management." operationId: getCategories parameters: - - in: query + - description: Filter text to search categories + in: query name: filter schema: type: string - - in: query + - description: Page number for pagination + in: query name: page schema: type: integer format: int32 - - in: query + - description: Number of items per page + in: query name: per_page schema: type: integer format: int32 - - in: query + - description: Field to order results by + in: query name: orderby schema: type: string default: category_name - - in: query + - description: Sort direction (ASC or DESC) + in: query name: direction schema: type: string default: ASC - - in: query + - description: Whether to include children count for each category + in: query name: showChildrenCount schema: type: boolean responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Categories retrieved successfully + "401": + description: Unauthorized - user authentication required + "500": + description: Internal server error retrieving categories + summary: Get categories with pagination tags: - Categories post: + description: "Creates a new category with the specified properties. The category\ + \ name is required, and optionally can be associated with a specific site." operationId: saveNew requestBody: content: - '*/*': + application/json: schema: $ref: "#/components/schemas/CategoryForm" + description: Category form with name and properties + required: true responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Category created successfully + "400": + description: Bad request - missing category name or invalid form data + "401": + description: Unauthorized - user authentication required + "403": + description: Forbidden - insufficient permissions to create categories + "500": + description: Internal server error creating category + summary: Create new category tags: - Categories put: + description: "Updates an existing category identified by its inode. All category\ + \ properties can be modified including name, description, and hierarchy placement." operationId: save_1 requestBody: content: - '*/*': + application/json: schema: $ref: "#/components/schemas/CategoryForm" + description: Category form with updated properties including inode + required: true responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Category updated successfully + "400": + description: Bad request - missing inode or invalid form data + "401": + description: Unauthorized - user authentication required + "403": + description: Forbidden - insufficient permissions to update categories + "404": + description: Category not found + "500": + description: Internal server error updating category + summary: Update existing category tags: - Categories /v1/categories/_export: get: + description: Exports categories to a CSV file format. Can filter by category + name pattern and specify a context category inode. Returns a downloadable + CSV file. operationId: export parameters: - - in: query + - description: Context category inode to export from + in: query name: contextInode schema: type: string - - in: query + - description: Filter pattern to match category names + in: query name: filter schema: type: string responses: - default: + "200": content: text/csv: {} - description: default response + description: Categories exported successfully as CSV file + "401": + description: Unauthorized - user authentication required + "403": + description: Forbidden - insufficient permissions to export categories + "404": + description: Context category not found + "500": + description: Internal server error exporting categories + summary: Export categories to CSV tags: - Categories /v1/categories/_import: post: + description: Imports categories from an uploaded CSV file. Supports 'replace' + mode to replace existing categories or 'append' mode to add to existing categories. + Can specify a context category and filter options. operationId: importCategories requestBody: content: multipart/form-data: schema: - $ref: "#/components/schemas/FormDataMultiPart" + $ref: "#/components/schemas/CategoryImportFormSchema" + required: true responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Categories imported successfully + "400": + description: Bad request - invalid file format or missing required parameters + "401": + description: Unauthorized - user authentication required + "403": + description: Forbidden - insufficient permissions to import categories + "500": + description: Internal server error importing categories + summary: Import categories from CSV file tags: - Categories /v1/categories/_sort: put: + description: Updates the sort order of categories. The request must contain + category inode and sortOrder pairs. Can update multiple categories at once + within a parent category. operationId: save requestBody: content: - '*/*': + application/json: schema: $ref: "#/components/schemas/CategoryEditForm" + description: Category edit form with category data and sort order information + required: true responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Category sort order updated successfully + "400": + description: Bad request - missing category data or invalid sort order + "401": + description: Unauthorized - user authentication required + "403": + description: Forbidden - insufficient permissions to update categories + "404": + description: Parent category not found + "500": + description: Internal server error updating sort order + summary: Update category sort order tags: - Categories /v1/categories/children: get: + description: Retrieves child categories of a specified parent category with + pagination and filtering options. Can include all nested levels and parent + hierarchy information. operationId: getChildren parameters: - - in: query + - description: Filter text to search child categories + in: query name: filter schema: type: string - - in: query + - description: Page number for pagination + in: query name: page schema: type: integer format: int32 - - in: query + - description: Number of items per page + in: query name: per_page schema: type: integer format: int32 - - in: query + - description: Field to order results by + in: query name: orderby schema: type: string default: category_name - - in: query + - description: Sort direction (ASC or DESC) + in: query name: direction schema: type: string default: ASC - - in: query + - description: Parent category inode to get children for + in: query name: inode schema: type: string - - in: query + - description: Whether to include children count for each category + in: query name: showChildrenCount schema: type: boolean - - in: query + - description: Whether to include all nested levels + in: query name: allLevels schema: type: boolean - - in: query + - description: Whether to include parent list hierarchy + in: query name: parentList schema: type: boolean responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Child categories retrieved successfully + "401": + description: Unauthorized - user authentication required + "404": + description: Parent category not found + "500": + description: Internal server error retrieving child categories + summary: Get category children tags: - Categories /v1/categories/hierarchy: post: - description: Response with the list of parents for a specific set of categories. - If any of the categoriesdoes not exists then it is just ignored - operationId: getSchemes + description: "Retrieves the parent hierarchy for a set of categories specified\ + \ by their keys. Returns parent lists for each category, starting from the\ + \ top-level parent down to the direct parent. Categories that don't exist\ + \ are ignored." + operationId: getHierarchy requestBody: content: - '*/*': + application/json: schema: $ref: "#/components/schemas/CategoryKeysForm" + description: Category keys form containing array of category keys + required: true responses: - default: + "200": content: application/json: schema: $ref: "#/components/schemas/HierarchyShortCategoriesResponseView" - description: default response - summary: Get the List of Parents from set of categories + description: Category hierarchies retrieved successfully + "400": + description: Bad request - invalid category keys form + "401": + description: Unauthorized - user authentication required + "500": + description: Internal server error retrieving hierarchies + summary: Get category hierarchy for multiple categories tags: - Categories /v1/categories/{idOrKey}: get: + description: Retrieves a specific category by its unique identifier (inode) + or key. Can optionally include child count information. operationId: getCategoryByIdOrKey parameters: - - in: path + - description: Category ID (inode) or key + in: path name: idOrKey required: true schema: type: string - - in: query + - description: Whether to include children count + in: query name: showChildrenCount schema: type: boolean responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Category retrieved successfully + "401": + description: Unauthorized - user authentication required + "404": + description: Category not found + "500": + description: Internal server error retrieving category + summary: Get category by ID or key tags: - Categories /v1/changePassword: @@ -5033,12 +5353,14 @@ paths: be locked by the current user. operationId: canLockContent_1 parameters: - - in: path + - description: Contentlet inode or identifier + in: path name: inodeOrIdentifier required: true schema: type: string - - in: query + - description: Language ID for content localization + in: query name: language schema: type: string @@ -5048,7 +5370,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Successfully retrieved lock status "400": description: Bad request @@ -5102,7 +5424,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityContentletView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Draft saved successfully "400": description: Bad request - Invalid content data @@ -5667,12 +5989,14 @@ paths: \ inode or identifier, the contentlet will be locked." operationId: lockContent_1 parameters: - - in: path + - description: Contentlet inode or identifier + in: path name: inodeOrIdentifier required: true schema: type: string - - in: query + - description: Language ID for content localization + in: query name: language schema: type: string @@ -5703,12 +6027,14 @@ paths: \ inode or identifier, the contentlet will be unlocked." operationId: unlockContent_1 parameters: - - in: path + - description: Contentlet inode or identifier + in: path name: inodeOrIdentifier required: true schema: type: string - - in: query + - description: Language ID for content localization + in: query name: language schema: type: string @@ -5751,76 +6077,136 @@ paths: - File Assets /v1/content/related: post: + description: Retrieves related content for a contentlet based on relationship + field configuration and query conditions operationId: pullRelated requestBody: content: - '*/*': + application/json: schema: $ref: "#/components/schemas/PullRelatedForm" + description: Pull related content request parameters + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityListMapView" + description: Related content retrieved successfully "400": - description: Contentlet does not have a relationship field + content: + application/json: {} + description: Bad request - contentlet does not have a relationship field + "401": + content: + application/json: {} + description: Unauthorized - authentication required "404": - description: Contentlet not found + content: + application/json: {} + description: Not found - contentlet not found summary: Pull Related Content tags: - Content /v1/content/resourcelinks: get: + description: Retrieves resource links for all binary fields of a contentlet + identified by inode or identifier operationId: findResourceLinks parameters: - - in: query + - description: Content inode + in: query name: inode schema: type: string - - in: query + - description: Content identifier + in: query name: identifier schema: type: string - - in: query + - description: Language ID + example: 1 + in: query name: language schema: type: string default: "-1" responses: - default: + "200": content: - application/json;charset=UTF-8: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapStringObjectView" + description: Resource links retrieved successfully + "400": + content: + application/json: {} + description: Bad request - missing inode/identifier parameter + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - download restricted + summary: Get all resource links for contentlet tags: - Content /v1/content/resourcelinks/field/{field}: get: + description: Retrieves a resource link for a specific field of a contentlet + identified by inode or identifier operationId: findResourceLink parameters: - - in: path + - description: Field variable name + in: path name: field required: true schema: type: string - - in: query + - description: Content inode + in: query name: inode schema: type: string - - in: query + - description: Content identifier + in: query name: identifier schema: type: string - - in: query + - description: Language ID + example: 1 + in: query name: language schema: type: string default: "-1" responses: - default: + "200": content: - application/json;charset=UTF-8: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapStringObjectView" + description: Resource link retrieved successfully + "400": + content: + application/json: {} + description: Bad request - missing inode/identifier parameter + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - download restricted + "404": + content: + application/json: {} + description: Not found - contentlet or field not found + summary: Get resource link for specific field tags: - Content /v1/content/search: @@ -5830,9 +6216,11 @@ paths: operationId: search_1 requestBody: content: - '*/*': + application/json: schema: $ref: "#/components/schemas/ContentSearchForm" + description: Content search parameters + required: true responses: "200": content: @@ -5854,30 +6242,53 @@ paths: - Content /v1/content/versions: get: + description: "Retrieves all versions for content by identifier or inodes, with\ + \ optional grouping by language" operationId: findVersions parameters: - - in: query + - description: Comma-separated list of content inodes + in: query name: inodes schema: type: string - - in: query + - description: Content identifier (takes precedence over inodes) + in: query name: identifier schema: type: string - - in: query + - description: Group results by language (true/1 or false/0) + example: false + in: query name: groupByLang schema: type: string - - in: query + - description: "Maximum number of results (min: 20, max: 100)" + example: 20 + in: query name: limit schema: type: integer format: int32 responses: - default: + "200": content: - application/json;charset=UTF-8: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapView" + description: Content versions retrieved successfully + "400": + content: + application/json: {} + description: Bad request - missing identifier/inodes or invalid parameters + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "404": + content: + application/json: {} + description: Not found - content not found + summary: Find content versions tags: - Content /v1/content/versions/id/{identifier}/history: @@ -5954,57 +6365,93 @@ paths: - Content /v1/content/versions/{inode}: get: + description: Retrieves a specific content version by its inode operationId: findByInode parameters: - - in: path + - description: Content inode + in: path name: inode required: true schema: type: string responses: - default: + "200": content: - application/json;charset=UTF-8: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapView" + description: Content found successfully + "400": + content: + application/json: {} + description: Bad request - invalid inode format + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "404": + content: + application/json: {} + description: Not found - content with inode not found + summary: Find content by inode tags: - Content /v1/content/{identifier}/languages: get: + description: Retrieves all available languages for a contentlet and indicates + which languages have content versions operationId: checkContentLanguageVersions parameters: - - in: path + - description: Content identifier + in: path name: identifier required: true schema: type: string responses: - default: + "200": content: - application/javascript: - schema: - $ref: "#/components/schemas/ResponseEntityViewListExistingLanguagesForContentletView" - application/json: - schema: - $ref: "#/components/schemas/ResponseEntityViewListExistingLanguagesForContentletView" - description: default response + application/json: {} + description: Language versions retrieved successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "404": + content: + application/json: {} + description: Not found - contentlet identifier not found + summary: Check content language versions tags: - Content /v1/content/{identifier}/references/count: get: + description: Retrieves the total number of references to a specific contentlet + by its identifier operationId: getAllContentletReferencesCount parameters: - - in: path + - description: Content identifier + in: path name: identifier required: true schema: type: string responses: - default: + "200": content: application/json: schema: $ref: "#/components/schemas/ResponseEntityCountView" - description: default response + description: References count retrieved successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "404": + content: + application/json: {} + description: Not found - contentlet with identifier not found + summary: Get contentlet references count tags: - Content /v1/content/{inodeOrIdentifier}: @@ -6044,7 +6491,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Contentlet retrieved successfully "400": description: Bad request - Invalid identifier format @@ -6061,75 +6508,115 @@ paths: - Content /v1/content/{inodeOrIdentifier}/references: get: + description: "Retrieves all references to a specific contentlet, including pages,\ + \ containers, and personas that reference it" operationId: getContentletReferences parameters: - - in: path + - description: Content inode or identifier + in: path name: inodeOrIdentifier required: true schema: type: string - - in: query + - description: Language ID for content localization + example: 1 + in: query name: language schema: type: string default: "" responses: - default: + "200": content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityViewListContentReferenceView" - description: default response - tags: - - Content - /v1/contentrelationships/{params}: + $ref: "#/components/schemas/ResponseEntityContentReferenceListView" + description: References retrieved successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "404": + content: + application/json: {} + description: Not found - contentlet not found + summary: Get contentlet references + tags: + - Content + /v1/contentrelationships/{params}: get: deprecated: true + description: "Retrieves content with relationships based on query parameters,\ + \ identifier, or inode. This endpoint is deprecated - use /v1/content with\ + \ depth parameter instead." operationId: getContent_1 parameters: - - in: path + - description: "Query parameters, identifier, or inode for content lookup" + in: path name: params required: true schema: type: string pattern: .* responses: - default: + "200": content: - '*/*': {} - description: default response + application/json: + schema: + type: object + description: Content with relationships in JSON format including contentlets + and their related content + description: Content with relationships retrieved successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "500": + content: + application/json: {} + description: Internal server error + summary: Get content with relationships (deprecated) tags: - Content /v1/contentreport/folder/{folder}: get: + description: Generates a detailed report of the different Content Types living + under a Folder and the number of content items for each type. Supports both + folder ID and folder path (requires site parameter). operationId: getFolderContentReport parameters: - - in: path + - description: Folder ID or path to generate the report for + in: path name: folder required: true schema: type: string pattern: .* - - in: query + - description: Site ID or key (required when using folder path) + in: query name: site schema: type: string - - in: query + - description: Page number for pagination + in: query name: page schema: type: integer format: int32 - - in: query + - description: Number of items per page + in: query name: per_page schema: type: integer format: int32 - - in: query + - description: Field to order results by + in: query name: orderby schema: type: string default: upper(name) - - in: query + - description: Sort direction (ASC or DESC) + in: query name: direction schema: type: string @@ -6142,35 +6629,42 @@ paths: $ref: "#/components/schemas/ContentReportView" description: "Content Report for the specified Folder ID/path, or an empty\ \ list if either the Folder doesn't exist, or no content is found." - summary: "Generates a report of the different Content Types living under a Folder,\ - \ and the number of content items for each type" + summary: Generate folder content report tags: - Content Report /v1/contentreport/site/{site}: get: + description: Generates a detailed report of the different Content Types living + under a Site and the number of content items for each type. Useful for data + analysis and deletion planning. operationId: getSiteContentReport parameters: - - in: path + - description: Site ID or key to generate the report for + in: path name: site required: true schema: type: string - - in: query + - description: Page number for pagination + in: query name: page schema: type: integer format: int32 - - in: query + - description: Number of items per page + in: query name: per_page schema: type: integer format: int32 - - in: query + - description: Field to order results by + in: query name: orderby schema: type: string default: upper(name) - - in: query + - description: Sort direction (ASC or DESC) + in: query name: direction schema: type: string @@ -6183,8 +6677,7 @@ paths: $ref: "#/components/schemas/ResponseSiteVariablesEntityView" description: "Content Report for the specified Site ID/Key, or an empty\ \ list if either the Site doesn't exist, or no content is found." - summary: "Generates a report of the different Content Types living under a Site,\ - \ and the number of content items for each type" + summary: Generate site content report tags: - Content Report /v1/contenttype: @@ -6397,6 +6890,8 @@ paths: messages: [] pagination: null permissions: [] + schema: + $ref: "#/components/schemas/ResponseEntityListMapView" description: Content type(s) created successfully "400": description: Bad Request @@ -6476,6 +6971,8 @@ paths: perPage: 0 totalEntries: 0 permissions: [] + schema: + $ref: "#/components/schemas/ResponseEntityListContentTypeView" description: Content types filtered successfully "400": description: Bad Request @@ -6506,6 +7003,8 @@ paths: messages: [] pagination: null permissions: [] + schema: + $ref: "#/components/schemas/ResponseEntityBaseContentTypesView" description: Base content types retrieved successfully "500": description: Internal Server Error @@ -6537,6 +7036,8 @@ paths: messages: [] pagination: null permissions: [] + schema: + $ref: "#/components/schemas/ResponseEntityContentTypeJsonView" description: Content type deleted successfully "403": description: Forbidden @@ -6611,6 +7112,8 @@ paths: perPage: 0 totalEntries: 0 permissions: [] + schema: + $ref: "#/components/schemas/ResponseEntityContentTypeDetailView" description: Content type retrieved successfully "403": description: Forbidden @@ -6702,6 +7205,8 @@ paths: messages: [] pagination: null permissions: [] + schema: + $ref: "#/components/schemas/ResponseEntityContentTypeDetailView" description: Content type updated successfully "400": description: Bad Request @@ -6919,6 +7424,8 @@ paths: currentPage: 0 perPage: 0 totalEntries: 0 + schema: + $ref: "#/components/schemas/ResponseEntityContentTypeOperationView" description: Content type copied successfully "400": description: Bad Request @@ -6934,50 +7441,100 @@ paths: /v1/contenttype/{typeId}/fields: delete: deprecated: true + description: Deletes multiple fields from a content type by their field IDs. + Use v2 API instead for new implementations. operationId: deleteFields parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string requestBody: content: - '*/*': + application/json: schema: type: array items: type: string + description: Array of field IDs to delete + required: true responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapView" + description: Fields deleted successfully + "400": content: - application/javascript: {} application/json: {} - description: default response + description: Bad request - invalid field IDs + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type or one or more fields not found + "500": + content: + application/json: {} + description: Internal server error + summary: Delete multiple fields (deprecated) tags: - Content Type Field get: deprecated: true + description: Retrieves all fields for a specific content type. Use v2 API instead + for new implementations. operationId: getContentTypeFields parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityListMapView" + description: Fields retrieved successfully + "401": content: - application/javascript: {} application/json: {} - description: default response + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type not found + "500": + content: + application/json: {} + description: Internal server error + summary: Get content type fields (deprecated) tags: - Content Type Field post: deprecated: true + description: Creates a new field for a content type. Use v2 API instead. operationId: createContentTypeField parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: @@ -6987,19 +7544,41 @@ paths: application/json: schema: type: string + description: Field JSON data + required: true responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapStringObjectView" + description: Field created successfully + "400": content: - application/javascript: {} application/json: {} - description: default response + description: Bad request - invalid field data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - content type not found + summary: Create content type field (deprecated) tags: - Content Type Field put: deprecated: true + description: Updates multiple fields for a content type. Use v2 API instead. operationId: updateFields parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: @@ -7009,69 +7588,137 @@ paths: application/json: schema: type: string + description: Fields JSON data + required: true responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityListMapView" + description: Fields updated successfully + "400": content: - application/javascript: {} application/json: {} - description: default response + description: Bad request - invalid field data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - content type not found + summary: Update content type fields (deprecated) tags: - Content Type Field /v1/contenttype/{typeId}/fields/id/{fieldId}: delete: deprecated: true + description: Deletes a specific field from a content type by its field ID. Use + v2 API instead for new implementations. operationId: deleteContentTypeFieldById parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID to delete + in: path name: fieldId required: true schema: type: string responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityStringView" + description: Field deleted successfully + "401": content: - application/javascript: {} application/json: {} - description: default response + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type or field not found + "500": + content: + application/json: {} + description: Internal server error + summary: Delete content type field by ID (deprecated) tags: - Content Type Field get: deprecated: true + description: Retrieves a specific field from a content type by its unique field + ID. Use v2 API instead for new implementations. operationId: getContentTypeFieldById parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID + in: path name: fieldId required: true schema: type: string responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapStringObjectView" + description: Field retrieved successfully + "401": content: - application/javascript: {} application/json: {} - description: default response + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type or field not found + "500": + content: + application/json: {} + description: Internal server error + summary: Get content type field by ID (deprecated) tags: - Content Type Field put: deprecated: true + description: Updates a specific field in a content type by its field ID. Use + v2 API instead for new implementations. operationId: updateContentTypeFieldById parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID to update + in: path name: fieldId required: true schema: @@ -7081,45 +7728,81 @@ paths: application/json: schema: type: string + description: Field JSON data with updates + required: true responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapStringObjectView" + description: Field updated successfully + "400": content: - application/javascript: {} application/json: {} - description: default response + description: Bad request - invalid field data or missing field ID + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type or field not found + "500": + content: + application/json: {} + description: Internal server error + summary: Update content type field by ID (deprecated) tags: - Content Type Field /v1/contenttype/{typeId}/fields/id/{fieldId}/variables: get: + description: Retrieves all field variables for a specific field identified by + its ID operationId: getFieldVariablesByFieldId parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID + in: path name: fieldId required: true schema: type: string responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variables retrieved successfully + "404": + content: + application/json: {} + description: Not found - field not found + summary: Get field variables by field ID tags: - Content Type Field post: + description: Creates a new field variable for a specific field identified by + its ID operationId: createFieldVariableByFieldId parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID + in: path name: fieldId required: true schema: @@ -7129,81 +7812,129 @@ paths: application/json: schema: type: string + description: Field variable data + required: true responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable created successfully + "400": + content: + application/json: {} + description: Bad request - invalid field variable data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - field not found + summary: Create field variable by field ID tags: - Content Type Field /v1/contenttype/{typeId}/fields/id/{fieldId}/variables/id/{fieldVarId}: delete: + description: Deletes a specific field variable for a field identified by its + ID operationId: deleteFieldVariableByFieldId parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID + in: path name: fieldId required: true schema: type: string - - in: path + - description: Field variable ID + in: path name: fieldVarId required: true schema: type: string responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable deleted successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - field or field variable not found + summary: Delete field variable by field ID tags: - Content Type Field get: + description: Retrieves a specific field variable by field ID and variable ID operationId: getFieldVariableByFieldId parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID + in: path name: fieldId required: true schema: type: string - - in: path + - description: Field variable ID + in: path name: fieldVarId required: true schema: type: string responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable retrieved successfully + "404": + content: + application/json: {} + description: Not found - field or field variable not found + summary: Get specific field variable by field ID tags: - Content Type Field put: + description: Updates an existing field variable for a specific field identified + by its ID operationId: updateFieldVariableByFieldId parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID + in: path name: fieldId required: true schema: type: string - - in: path + - description: Field variable ID + in: path name: fieldVarId required: true schema: @@ -7213,69 +7944,135 @@ paths: application/json: schema: type: string + description: Updated field variable data + required: true responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable updated successfully + "400": + content: + application/json: {} + description: Bad request - invalid field variable data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - field or field variable not found + summary: Update field variable by field ID tags: - Content Type Field /v1/contenttype/{typeId}/fields/var/{fieldVar}: delete: deprecated: true + description: Deletes a specific field from a content type by its variable name. + Use v2 API instead for new implementations. operationId: deleteContentTypeFieldByVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field variable name to delete + in: path name: fieldVar required: true schema: type: string responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityStringView" + description: Field deleted successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type or field not found + "500": content: - application/javascript: {} application/json: {} - description: default response + description: Internal server error + summary: Delete content type field by variable name (deprecated) tags: - Content Type Field get: deprecated: true + description: Retrieves a specific field from a content type by its variable + name. Use v2 API instead for new implementations. operationId: getContentTypeFieldByVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field variable name + in: path name: fieldVar required: true schema: type: string responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapStringObjectView" + description: Field retrieved successfully + "401": content: - application/javascript: {} application/json: {} - description: default response + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type or field not found + "500": + content: + application/json: {} + description: Internal server error + summary: Get content type field by variable name (deprecated) tags: - Content Type Field put: deprecated: true + description: Updates a specific field in a content type by its variable name. + Use v2 API instead for new implementations. operationId: updateContentTypeFieldByVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field variable name to update + in: path name: fieldVar required: true schema: @@ -7285,45 +8082,81 @@ paths: application/json: schema: type: string + description: Field JSON data with updates + required: true responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapStringObjectView" + description: Field updated successfully + "400": content: - application/javascript: {} application/json: {} - description: default response + description: Bad request - invalid field data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type or field not found + "500": + content: + application/json: {} + description: Internal server error + summary: Update content type field by variable name (deprecated) tags: - Content Type Field /v1/contenttype/{typeId}/fields/var/{fieldVar}/variables: get: + description: Retrieves all field variables for a specific field identified by + its velocity variable name operationId: getFieldVariablesByFieldVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field velocity variable name + in: path name: fieldVar required: true schema: type: string responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variables retrieved successfully + "404": + content: + application/json: {} + description: Not found - field not found + summary: Get field variables by field variable name tags: - Content Type Field post: + description: Creates a new field variable for a specific field identified by + its velocity variable name operationId: createFieldVariableByFieldVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field velocity variable name + in: path name: fieldVar required: true schema: @@ -7333,81 +8166,130 @@ paths: application/json: schema: type: string + description: Field variable data + required: true responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable created successfully + "400": + content: + application/json: {} + description: Bad request - invalid field variable data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - field not found + summary: Create field variable by field variable name tags: - Content Type Field /v1/contenttype/{typeId}/fields/var/{fieldVar}/variables/id/{fieldVarId}: delete: + description: Deletes a specific field variable for a field identified by its + velocity variable name operationId: deleteFieldVariableByFieldVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field velocity variable name + in: path name: fieldVar required: true schema: type: string - - in: path + - description: Field variable ID + in: path name: fieldVarId required: true schema: type: string responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable deleted successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - field or field variable not found + summary: Delete field variable by field variable name tags: - Content Type Field get: + description: Retrieves a specific field variable by field velocity variable + name and variable ID operationId: getFieldVariableByFieldVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field velocity variable name + in: path name: fieldVar required: true schema: type: string - - in: path + - description: Field variable ID + in: path name: fieldVarId required: true schema: type: string responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable retrieved successfully + "404": + content: + application/json: {} + description: Not found - field or field variable not found + summary: Get specific field variable by field variable name tags: - Content Type Field put: + description: Updates an existing field variable for a specific field identified + by its velocity variable name operationId: updateFieldVariableByFieldVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field velocity variable name + in: path name: fieldVar required: true schema: type: string - - in: path + - description: Field variable ID + in: path name: fieldVarId required: true schema: @@ -7417,12 +8299,30 @@ paths: application/json: schema: type: string + description: Updated field variable data + required: true responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable updated successfully + "400": + content: + application/json: {} + description: Bad request - invalid field variable data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - field or field variable not found + summary: Update field variable by field variable name tags: - Content Type Field /v1/ema: @@ -8297,13 +9197,21 @@ paths: - Experiment /v1/fieldTypes: get: + description: Retrieves all available field types in dotCMS for content type + configuration operationId: getFieldTypes responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityFieldTypeListView" + description: Field types retrieved successfully + "401": content: - application/javascript: {} application/json: {} - description: default response + description: Unauthorized - authentication required + summary: Get field types tags: - Content Type Field /v1/folder/byPath: @@ -13771,7 +14679,7 @@ paths: messages: [] permissions: [] schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Contentlet(s) modified successfully "401": description: Invalid User @@ -13889,7 +14797,7 @@ paths: messages: [] permissions: [] schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Fired action successfully "400": description: Bad request @@ -14003,7 +14911,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Fired action successfully "400": description: Bad request @@ -14030,7 +14938,7 @@ paths: Fires a default [system action](https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) on target contentlet. Uses a multipart form to transmit its data. Returns a map of the resultant contentlet, with an additional `AUTO_ASSIGN_WORKFLOW` property, which can be referenced by delegate services that handle automatically assigning workflow schemes to content with none. - operationId: putFireActionByIdMultipart + operationId: putFireDefaultActionMultipart parameters: - description: Inode of the target content. in: query @@ -14084,13 +14992,16 @@ paths: content: multipart/form-data: schema: - $ref: "#/components/schemas/FormDataMultiPart" + $ref: "#/components/schemas/WorkflowActionMultipartSchema" + description: Multipart form containing a JSON 'contentlet' body and optional + binary field parts. + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Fired action successfully "400": description: Bad request @@ -14106,7 +15017,7 @@ paths: description: Unsupported Media Type "500": description: Internal Server Error - summary: Fire action by ID (multipart form) 🚧 + summary: Fire default action (multipart form) 🚧 tags: - Workflow /v1/workflow/actions/fire: @@ -14185,7 +15096,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Fired action successfully "400": description: Bad request @@ -14251,15 +15162,16 @@ paths: content: multipart/form-data: schema: - $ref: "#/components/schemas/FormDataMultiPart" - description: Multipart form. More details to follow. + $ref: "#/components/schemas/WorkflowActionMultipartSchema" + description: Multipart form containing a JSON 'contentlet' body and optional + binary field parts. required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Fired action successfully "400": description: Bad request @@ -14703,7 +15615,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Fired action successfully "400": description: Bad request @@ -14730,7 +15642,7 @@ paths: Fires a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions), specified by identifier, on a target contentlet. Uses a multipart form to transmit its data. Returns a map of the resultant contentlet, with an additional `AUTO_ASSIGN_WORKFLOW` property, which can be referenced by delegate services that handle automatically assigning workflow schemes to content with none. - operationId: putFireActionByIdMultipart_1 + operationId: putFireActionByIdMultipart parameters: - description: |- Identifier of a workflow action. @@ -14778,15 +15690,16 @@ paths: content: multipart/form-data: schema: - $ref: "#/components/schemas/FormDataMultiPart" - description: Multipart form. More details to follow. + $ref: "#/components/schemas/WorkflowActionMultipartSchema" + description: Multipart form containing a JSON 'contentlet' body and optional + binary field parts. required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Fired action successfully "400": description: Bad request @@ -15283,7 +16196,7 @@ paths: Create a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes). Returns created workflow scheme on success. - operationId: postSaveScheme_1 + operationId: postSaveScheme requestBody: content: application/json: @@ -15297,6 +16210,7 @@ paths: | `schemeName` | String | The workflow scheme's name. | | `schemeDescription` | String | A description of the scheme. | | `schemeArchived` | Boolean | If `true`, the scheme will be created in an archived state. | + required: true responses: "200": content: @@ -15592,7 +16506,7 @@ paths: pagination: null permissions: [] schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityWorkflowSchemeView" description: Exported workflow scheme successfully "400": description: Bad request @@ -16440,7 +17354,7 @@ paths: Create a [workflow comment]. Returns created workflow comment on success. - operationId: postSaveScheme + operationId: postSaveComment parameters: - description: Identifier of contentlet to add comment. in: path @@ -16949,26 +17863,41 @@ paths: - Internationalization /v2/tags: get: + description: "Lists all tags based on provided criteria. If a tag name is provided,\ + \ performs a search-by-name (like) operation that can be delimited by site\ + \ ID. If no matches are found against the site ID, searches global tags." operationId: list_14 parameters: - - in: query + - description: Name of the tag to search for (optional) + in: query name: name schema: type: string - - in: query + - description: ID of the site where the tag lives (optional) + in: query name: siteId schema: type: string responses: - default: + "200": content: - application/javascript: - schema: - $ref: "#/components/schemas/ResponseEntityTagMapView" application/json: schema: $ref: "#/components/schemas/ResponseEntityTagMapView" - description: default response + description: Tags retrieved successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions to access tags portlet + "500": + content: + application/json: {} + description: Internal server error + summary: List tags tags: - Tags post: @@ -17009,37 +17938,60 @@ paths: tags: - Tags put: + description: "Updates the information belonging to a specific tag. Requires\ + \ tag ID, site ID, and new tag name." operationId: updateTag_1 requestBody: content: - '*/*': + application/json: schema: $ref: "#/components/schemas/UpdateTagForm" + description: "Update tag form containing tag ID, site ID, and new tag name" + required: true responses: - default: + "200": content: - application/javascript: - schema: - $ref: "#/components/schemas/ResponseEntityTagMapView" application/json: schema: $ref: "#/components/schemas/ResponseEntityTagMapView" - description: default response + description: Tag updated successfully + "400": + content: + application/json: {} + description: Bad request - invalid or incomplete tag data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions to update tags + "404": + content: + application/json: {} + description: Tag not found + "500": + content: + application/json: {} + description: Internal server error + summary: Update tag tags: - Tags /v2/tags/_bulk: post: - description: Creates multiple tags in bulk with optional owner assignment + description: Creates multiple tags in bulk with optional owner assignment. Tags + can be created with site-specific scoping. operationId: addTag_1 requestBody: content: application/json: schema: $ref: "#/components/schemas/TagForm" - description: Bulk tag data to create + description: Tag form containing tags to create with optional owner assignment required: true responses: - "201": + "200": content: application/json: schema: @@ -17048,131 +18000,227 @@ paths: "400": content: application/json: {} - description: Bad Request - Invalid tag data + description: Bad request - invalid tag data or parameters "401": content: application/json: {} - description: Unauthorized - Authentication required + description: Unauthorized - authentication required "403": content: application/json: {} - description: Forbidden - User does not have access to Tags portlet + description: Forbidden - insufficient permissions to create tags "500": content: application/json: {} - description: Internal Server Error - Database or system error + description: Internal server error summary: Create multiple tags tags: - Tags /v2/tags/import: post: + description: Imports tags from a CSV file. The CSV file should contain tag data + in the expected format for bulk tag creation. operationId: importTags_1 requestBody: content: multipart/form-data: schema: $ref: "#/components/schemas/FormDataMultiPart" + description: CSV file containing tag data for import + required: true responses: - default: + "200": content: application/json: schema: $ref: "#/components/schemas/ResponseEntityBooleanView" - description: default response + description: Tags imported successfully + "400": + content: + application/json: {} + description: Bad request - invalid CSV file format or data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions to import tags + "500": + content: + application/json: {} + description: Internal server error + summary: Import tags from CSV tags: - Tags /v2/tags/inode/{inode}: delete: + description: Breaks the link between an inode and all its associated tags. Removes + all tag associations for the specified content but does not delete the tags + themselves. operationId: deleteTagInodesByInode_1 parameters: - - in: path + - description: Inode to remove tag associations from + in: path name: inode required: true schema: type: string responses: - default: + "200": content: - application/javascript: - schema: - $ref: "#/components/schemas/ResponseEntityBooleanView" application/json: schema: $ref: "#/components/schemas/ResponseEntityBooleanView" - description: default response + description: Tag-inode associations deleted successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions to modify tag associations + "404": + content: + application/json: {} + description: No tag associations found for the specified inode + "500": + content: + application/json: {} + description: Internal server error + summary: Delete tag-inode associations tags: - Tags get: + description: Retrieves all tags associated with a given inode. Returns tag-inode + relationships for the specified content. operationId: findTagsByInode_1 parameters: - - in: path + - description: Inode to retrieve tags for + in: path name: inode required: true schema: type: string responses: - default: + "200": content: - application/javascript: - schema: - $ref: "#/components/schemas/ResponseEntityTagInodesMapView" application/json: schema: $ref: "#/components/schemas/ResponseEntityTagInodesMapView" - description: default response + description: Tags retrieved successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions to access tags + "404": + content: + application/json: {} + description: No tags found for the specified inode + "500": + content: + application/json: {} + description: Internal server error + summary: Get tags by inode tags: - Tags /v2/tags/tag/{nameOrId}/inode/{inode}: put: + description: "Binds tags with a given inode. Lookup can be done via tag name\ + \ or ID. If tag name matches multiple tags, all matching tags will be bound.\ + \ Use tag ID for specific binding." operationId: linkTagsAndInode_1 parameters: - - in: path + - description: Name or UUID of the tag to link + in: path name: nameOrId required: true schema: type: string - - in: path + - description: Inode to link the tag(s) to + in: path name: inode required: true schema: type: string responses: - default: + "200": content: - application/javascript: - schema: - $ref: "#/components/schemas/ResponseEntityTagInodesMapView" application/json: schema: $ref: "#/components/schemas/ResponseEntityTagInodesMapView" - description: default response + description: Tags linked to inode successfully + "400": + content: + application/json: {} + description: Bad request - invalid tag name/ID or inode + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions to link tags + "404": + content: + application/json: {} + description: Tag not found by the specified name or ID + "500": + content: + application/json: {} + description: Internal server error + summary: Link tags to inode tags: - Tags /v2/tags/user/{userId}: get: + description: Retrieves all tags owned by a specific user. Returns tags that + were explicitly linked to the user during creation. operationId: getTagsByUserId_1 parameters: - - in: path + - description: User ID to retrieve tags for + in: path name: userId required: true schema: type: string responses: - default: + "200": content: - application/javascript: - schema: - $ref: "#/components/schemas/ResponseEntityTagMapView" application/json: schema: $ref: "#/components/schemas/ResponseEntityTagMapView" - description: default response + description: User tags retrieved successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions to access tags + "404": + content: + application/json: {} + description: No tags found for the specified user + "500": + content: + application/json: {} + description: Internal server error + summary: Get tags by user ID tags: - Tags /v2/tags/{nameOrId}: get: description: "Retrieves a single tag by its name or UUID. For name-based searches,\ - \ uses site context for disambiguation." + \ uses site context for disambiguation with SYSTEM_HOST fallback." operationId: getTagsByNameOrId_1 parameters: - description: Tag name or UUID @@ -17193,45 +18241,62 @@ paths: application/json: schema: $ref: "#/components/schemas/ResponseEntityRestTagView" - description: Tag found successfully + description: Tag retrieved successfully "401": content: application/json: {} - description: Unauthorized - Authentication required + description: Unauthorized - authentication required "403": content: application/json: {} - description: Forbidden - User does not have access to Tags portlet + description: Forbidden - insufficient permissions to access tags "404": content: application/json: {} - description: Tag not found + description: Tag not found by the specified name or ID "500": content: application/json: {} - description: Internal Server Error + description: Internal server error summary: Get tag by name or ID tags: - Tags /v2/tags/{tagId}: delete: + description: Deletes a tag based on its ID. The tag must exist and the user + must have appropriate permissions. operationId: delete_17 parameters: - - in: path + - description: ID of the tag to delete + in: path name: tagId required: true schema: type: string responses: - default: + "200": content: - application/javascript: - schema: - $ref: "#/components/schemas/ResponseEntityBooleanView" application/json: schema: $ref: "#/components/schemas/ResponseEntityBooleanView" - description: default response + description: Tag deleted successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions to delete tags + "404": + content: + application/json: {} + description: Tag not found + "500": + content: + application/json: {} + description: Internal server error + summary: Delete tag tags: - Tags /v3/contenttype/{typeIdOrVarName}/fields: @@ -18066,6 +19131,17 @@ components: required: - password - userId + BaseContentTypesView: + type: object + properties: + label: + type: string + name: + type: string + types: + type: array + items: + $ref: "#/components/schemas/ContentTypeView" BayesianResult: type: object properties: @@ -18438,6 +19514,26 @@ components: format: int32 required: - categoryName + CategoryImportFormSchema: + type: object + description: Category import form data with CSV file and processing parameters + properties: + contextInode: + type: string + description: Context category inode to import into + exportType: + type: string + description: Import behavior + enum: + - replace + - merge + file: + type: string + format: binary + description: CSV file containing categories to import + filter: + type: string + description: Filter pattern for categories CategoryKeysForm: type: object properties: @@ -18445,6 +19541,31 @@ components: type: array items: type: string + CategoryView: + type: object + properties: + active: + type: boolean + categoryName: + type: string + categoryVelocityVarName: + type: string + description: + type: string + inode: + type: string + key: + type: string + keywords: + type: string + modDate: + type: string + format: date-time + parent: + type: string + sortOrder: + type: integer + format: int32 ChangeLoggerForm: type: object properties: @@ -18999,6 +20120,19 @@ components: type: array items: $ref: "#/components/schemas/WorkflowFormEntry" + ContentTypeView: + type: object + properties: + action: + type: string + inode: + type: string + name: + type: string + type: + type: string + variable: + type: string ContentletWorkflowStatusView: type: object properties: @@ -21073,6 +22207,13 @@ components: properties: empty: type: boolean + ImmutableMapObjectObject: + type: object + additionalProperties: + type: object + properties: + empty: + type: boolean ImmutableMapStringListSampleData: type: object additionalProperties: @@ -22517,9 +23658,92 @@ components: entity: type: array items: - type: object - additionalProperties: - type: object + type: object + additionalProperties: + type: object + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string + ResetPasswordForm: + type: object + properties: + password: + type: string + token: + type: string + required: + - password + - token + ResponseContentletWorkflowStatusView: + type: object + properties: + entity: + $ref: "#/components/schemas/ContentletWorkflowStatusView" + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string + ResponseEntityApiTokenWithJwtView: + type: object + properties: + entity: + type: object + additionalProperties: + type: object + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string + ResponseEntityBaseContentTypesView: + type: object + properties: + entity: + type: array + items: + $ref: "#/components/schemas/BaseContentTypesView" errors: type: array items: @@ -22538,21 +23762,11 @@ components: type: array items: type: string - ResetPasswordForm: - type: object - properties: - password: - type: string - token: - type: string - required: - - password - - token - ResponseContentletWorkflowStatusView: + ResponseEntityBooleanView: type: object properties: entity: - $ref: "#/components/schemas/ContentletWorkflowStatusView" + type: boolean errors: type: array items: @@ -22571,13 +23785,34 @@ components: type: array items: type: string - ResponseEntityApiTokenWithJwtView: + ResponseEntityBulkActionView: type: object properties: entity: + $ref: "#/components/schemas/BulkActionView" + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: type: object additionalProperties: - type: object + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string + ResponseEntityBulkActionsResultView: + type: object + properties: + entity: + $ref: "#/components/schemas/BulkActionsResultView" errors: type: array items: @@ -22596,11 +23831,13 @@ components: type: array items: type: string - ResponseEntityBooleanView: + ResponseEntityBundleListView: type: object properties: entity: - type: boolean + type: array + items: + $ref: "#/components/schemas/BundleMap" errors: type: array items: @@ -22619,11 +23856,11 @@ components: type: array items: type: string - ResponseEntityBulkActionView: + ResponseEntityCategoryView: type: object properties: entity: - $ref: "#/components/schemas/BulkActionView" + $ref: "#/components/schemas/CategoryView" errors: type: array items: @@ -22642,11 +23879,13 @@ components: type: array items: type: string - ResponseEntityBulkActionsResultView: + ResponseEntityContainerView: type: object properties: entity: - $ref: "#/components/schemas/BulkActionsResultView" + type: array + items: + $ref: "#/components/schemas/Container" errors: type: array items: @@ -22665,13 +23904,13 @@ components: type: array items: type: string - ResponseEntityBundleListView: + ResponseEntityContentReferenceListView: type: object properties: entity: type: array items: - $ref: "#/components/schemas/BundleMap" + $ref: "#/components/schemas/ContentReferenceView" errors: type: array items: @@ -22690,13 +23929,36 @@ components: type: array items: type: string - ResponseEntityContainerView: + ResponseEntityContentTypeDetailView: type: object properties: entity: + type: object + additionalProperties: + type: object + errors: type: array items: - $ref: "#/components/schemas/Container" + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string + ResponseEntityContentTypeJsonView: + type: object + properties: + entity: + type: string errors: type: array items: @@ -22715,13 +23977,16 @@ components: type: array items: type: string - ResponseEntityContentletView: + ResponseEntityContentTypeOperationView: type: object properties: entity: type: object additionalProperties: type: object + properties: + empty: + type: boolean errors: type: array items: @@ -22955,6 +24220,33 @@ components: type: array items: type: string + ResponseEntityFieldTypeListView: + type: object + properties: + entity: + type: array + items: + type: object + additionalProperties: + type: object + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string ResponseEntityForgotPasswordView: type: object properties: @@ -23097,6 +24389,58 @@ components: type: array items: type: string + ResponseEntityListContentTypeView: + type: object + properties: + entity: + type: array + items: + $ref: "#/components/schemas/ContentType" + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string + ResponseEntityListMapView: + type: object + properties: + entity: + type: array + items: + type: object + additionalProperties: + type: object + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string ResponseEntityListUserView: type: object properties: @@ -23247,6 +24591,31 @@ components: type: array items: type: string + ResponseEntityMapStringObjectView: + type: object + properties: + entity: + type: object + additionalProperties: + type: object + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string ResponseEntityMapView: type: object properties: @@ -23539,6 +24908,29 @@ components: type: array items: type: string + ResponseEntitySearchView: + type: object + properties: + entity: + $ref: "#/components/schemas/SearchView" + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string ResponseEntitySingleExperimentView: type: object properties: @@ -23919,31 +25311,6 @@ components: type: array items: type: string - ResponseEntityViewListContentReferenceView: - type: object - properties: - entity: - type: array - items: - $ref: "#/components/schemas/ContentReferenceView" - errors: - type: array - items: - $ref: "#/components/schemas/ErrorEntity" - i18nMessagesMap: - type: object - additionalProperties: - type: string - messages: - type: array - items: - $ref: "#/components/schemas/MessageEntity" - pagination: - $ref: "#/components/schemas/Pagination" - permissions: - type: array - items: - type: string ResponseEntityViewListExistingLanguagesForContentletView: type: object properties: @@ -26531,6 +27898,35 @@ components: - actionRoleHierarchyForAssign - schemeId - showOn + WorkflowActionMultipartSchema: + type: object + description: Multipart form for workflow actions. Include a JSON 'contentlet' + and one or more 'file' parts that map to binary field variables. + properties: + binaryFields: + type: array + description: List of binary field variables. The uploaded files in 'file' + should correspond by index to these field variables. + example: + - binaryImage + - binaryDocument + items: + type: string + description: List of binary field variables. The uploaded files in 'file' + should correspond by index to these field variables. + example: "[\"binaryImage\",\"binaryDocument\"]" + contentlet: + type: string + description: JSON object describing the contentlet values. + example: "{\\n \"contentType\": \"News\",\\n \"title\": \"My News\"\\\ + n}" + file: + type: array + description: Files to upload. Repeat the 'file' part for multiple files; + order should match 'binaryFields'. + items: + type: string + format: binary WorkflowActionSeparatorForm: type: object properties: diff --git a/dotCMS/src/test/java/com/dotcms/rest/RestEndpointAnnotationComplianceTest.java b/dotCMS/src/test/java/com/dotcms/rest/RestEndpointAnnotationComplianceTest.java index 46c573a5b502..243f010edf01 100644 --- a/dotCMS/src/test/java/com/dotcms/rest/RestEndpointAnnotationComplianceTest.java +++ b/dotCMS/src/test/java/com/dotcms/rest/RestEndpointAnnotationComplianceTest.java @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -19,6 +20,7 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.BeanParam; import java.io.File; import java.io.IOException; import java.lang.reflect.Method; @@ -286,6 +288,9 @@ private void validateEndpointMethod(Class resourceClass, Method method) { // Validate parameter annotations validateParameterAnnotations(className, methodName, method); + // Validate multipart form documentation + validateMultipartFormDocumentation(className, methodName, method); + // Validate schema antipatterns validateSchemaAntipatterns(className, methodName, method); } @@ -424,20 +429,35 @@ private void validateParameterAnnotations(String className, String methodName, M } } - // Enhanced request body validation + // Validate @FormDataParam parameters should have @Parameter annotations + if (hasAnnotation(parameter, "org.glassfish.jersey.media.multipart.FormDataParam")) { + io.swagger.v3.oas.annotations.Parameter parameterAnnotation = + parameter.getAnnotation(io.swagger.v3.oas.annotations.Parameter.class); + + if (parameterAnnotation == null) { + addViolation(className, "FormData parameter " + parameter.getName() + + " in method " + methodName + " missing @Parameter annotation"); + } else if (parameterAnnotation.description() == null || + parameterAnnotation.description().trim().isEmpty()) { + addViolation(className, "FormData parameter " + parameter.getName() + + " in method " + methodName + " missing description"); + } + } + + // Enhanced request body validation (excluding FormDataMultiPart which should be documented at operation level) if (hasActualRequestBody(method) && !isContextParameter(parameter) && pathParamAnnotation == null && queryParamAnnotation == null && !isFrameworkParameter(parameter)) { // This parameter is likely a request body parameter - boolean hasRequestBodyAnnotation = parameter.getAnnotation(io.swagger.v3.oas.annotations.parameters.RequestBody.class) != null; + boolean hasRequestBodyAnnotation = parameter.getAnnotation(RequestBody.class) != null; if (!hasRequestBodyAnnotation) { addViolation(className, "Method " + methodName + " parameter '" + parameter.getType().getSimpleName() + "' appears to be request body but missing @RequestBody annotation"); } else { // Validate @RequestBody annotation content - io.swagger.v3.oas.annotations.parameters.RequestBody requestBodyAnnotation = - parameter.getAnnotation(io.swagger.v3.oas.annotations.parameters.RequestBody.class); + RequestBody requestBodyAnnotation = + parameter.getAnnotation(RequestBody.class); if (requestBodyAnnotation.description() == null || requestBodyAnnotation.description().trim().isEmpty()) { addViolation(className, "Method " + methodName + " @RequestBody annotation missing description"); @@ -446,6 +466,12 @@ private void validateParameterAnnotations(String className, String methodName, M // Check if request body should be required based on HTTP method and operation type if (requestBodyAnnotation.required() == false && (method.isAnnotationPresent(POST.class) || method.isAnnotationPresent(PUT.class))) { + // Skip deprecated methods that intentionally use required=false for backward compatibility + if (method.isAnnotationPresent(Deprecated.class)) { + // Deprecated methods may use required=false for backward compatibility + continue; + } + // Only flag as violation for operations that typically require a request body String methodNameLower = methodName.toLowerCase(); if (methodNameLower.contains("save") && !methodNameLower.contains("comment") || @@ -462,6 +488,75 @@ private void validateParameterAnnotations(String className, String methodName, M } } + /** + * Validate multipart form documentation for endpoints using @FormDataParam. + * These endpoints should use @RequestBody at operation level with multipart/form-data schema. + */ + private void validateMultipartFormDocumentation(String className, String methodName, Method method) { + // Check if method has @FormDataParam annotations + boolean hasFormDataParam = false; + java.lang.reflect.Parameter[] parameters = method.getParameters(); + + for (java.lang.reflect.Parameter parameter : parameters) { + if (parameter.isAnnotationPresent(org.glassfish.jersey.media.multipart.FormDataParam.class)) { + hasFormDataParam = true; + break; + } + } + + if (!hasFormDataParam) { + return; // This validation only applies to methods with @FormDataParam + } + + // Check for proper @RequestBody documentation at operation level + Operation operationAnnotation = method.getAnnotation(Operation.class); + if (operationAnnotation == null) { + return; // Already flagged by other validation + } + + RequestBody requestBodyAnnotation = operationAnnotation.requestBody(); + + if (requestBodyAnnotation == null || + requestBodyAnnotation.content() == null || + requestBodyAnnotation.content().length == 0) { + addViolation(className, "Method " + methodName + + " uses @FormDataParam but missing @RequestBody at operation level for proper multipart documentation"); + return; + } + + // Check if any content has multipart/form-data media type + boolean hasMultipartFormData = false; + for (Content content : requestBodyAnnotation.content()) { + if ("multipart/form-data".equals(content.mediaType())) { + hasMultipartFormData = true; + + // Check if schema is properly defined + Schema schema = content.schema(); + if (schema == null) { + addViolation(className, "Method " + methodName + + " has multipart/form-data content but missing schema definition"); + } else { + // Valid schema approaches for multipart/form-data: + // 1. type="object" with description + // 2. implementation class (specific or generic like MultipartFormDataSchema) + boolean hasValidSchema = "object".equals(schema.type()) || + (schema.implementation() != void.class && schema.implementation() != null); + + if (!hasValidSchema) { + addViolation(className, "Method " + methodName + + " multipart/form-data schema should use type='object' or specific implementation class"); + } + } + break; + } + } + + if (!hasMultipartFormData) { + addViolation(className, "Method " + methodName + + " uses @FormDataParam but @RequestBody content should include multipart/form-data media type"); + } + } + /** * Validate schema antipatterns */ @@ -665,8 +760,70 @@ private boolean isContextParameter(java.lang.reflect.Parameter parameter) { */ private boolean isFrameworkParameter(java.lang.reflect.Parameter parameter) { String typeName = parameter.getType().getSimpleName(); - return typeName.equals("AsyncResponse") || - parameter.isAnnotationPresent(javax.ws.rs.container.Suspended.class); + String fullTypeName = parameter.getType().getName(); + + // Check for AsyncResponse and @Suspended parameters + try { + if (typeName.equals("AsyncResponse") || + parameter.isAnnotationPresent(javax.ws.rs.container.Suspended.class)) { + return true; + } + } catch (Exception e) { + // Ignore if classes not available + } + + // Check for multipart form parameters - these use @FormDataParam annotation + try { + if (hasAnnotation(parameter, "org.glassfish.jersey.media.multipart.FormDataParam")) { + return true; + } + } catch (Exception e) { + // Ignore if classes not available + } + + // CRITICAL: FormDataMultiPart parameters are special framework types that: + // 1. Are automatically injected by Jersey multipart handling + // 2. MUST NOT have @Parameter annotations (causes injection conflicts) + // 3. MUST NOT have @RequestBody annotations (not JSON request bodies) + // 4. Should be documented using @RequestBody at operation level for proper OpenAPI documentation + if (typeName.equals("FormDataMultiPart") || + typeName.equals("FormDataContentDisposition") || + fullTypeName.equals("org.glassfish.jersey.media.multipart.FormDataMultiPart") || + fullTypeName.equals("org.glassfish.jersey.media.multipart.FormDataContentDisposition")) { + return true; + } + + // Check for @BeanParam parameters (used for grouping form parameters) + try { + if (parameter.isAnnotationPresent(javax.ws.rs.BeanParam.class)) { + return true; + } + } catch (Exception e) { + // Ignore if classes not available + } + + // Check for File parameters in multipart contexts (assume they don't need @RequestBody) + if (typeName.equals("File")) { + return true; + } + + return false; + } + + /** + * Check if parameter has a specific annotation by class name (safer than direct class reference) + */ + private boolean hasAnnotation(java.lang.reflect.Parameter parameter, String annotationClassName) { + try { + for (java.lang.annotation.Annotation annotation : parameter.getAnnotations()) { + if (annotation.annotationType().getName().equals(annotationClassName)) { + return true; + } + } + } catch (Exception e) { + // Ignore if annotation classes not available + } + return false; } /** From 8440e45da30d58364fd42196076d26169070c5be Mon Sep 17 00:00:00 2001 From: Jamie Mauro Date: Fri, 12 Dec 2025 13:34:18 -0500 Subject: [PATCH 2/9] Merged origin/main, resolved conflicts, ran test scripts, reviewed in app. --- .../BEST_PRACTICES_ASSESSMENT.md | 102 + .../BEST_PRACTICES_COMPLIANCE.md | 130 + .claude/skills/cicd-diagnostics/CHANGELOG.md | 384 + .../skills/cicd-diagnostics/ENHANCEMENTS.md | 351 + .../skills/cicd-diagnostics/ISSUE_TEMPLATE.md | 510 + .../skills/cicd-diagnostics/LOG_ANALYSIS.md | 399 + .claude/skills/cicd-diagnostics/README.md | 262 + .claude/skills/cicd-diagnostics/REFERENCE.md | 609 + .claude/skills/cicd-diagnostics/SKILL.md | 765 + .claude/skills/cicd-diagnostics/WORKFLOWS.md | 347 + .claude/skills/cicd-diagnostics/fetch-jobs.py | 51 + .claude/skills/cicd-diagnostics/fetch-logs.py | 103 + .../skills/cicd-diagnostics/fetch-metadata.py | 42 + .../cicd-diagnostics/init-diagnostic.py | 42 + .../skills/cicd-diagnostics/requirements.txt | 17 + .../skills/cicd-diagnostics/utils/README.md | 293 + .../skills/cicd-diagnostics/utils/__init__.py | 5 + .../skills/cicd-diagnostics/utils/evidence.py | 652 + .../cicd-diagnostics/utils/external_issues.py | 288 + .../cicd-diagnostics/utils/github_api.py | 346 + .../utils/tiered_extraction.py | 597 + .../cicd-diagnostics/utils/workspace.py | 270 + .claude/skills/sdk-analytics/SKILL.md | 959 ++ .cursor/rules/typescript-context.md | 221 +- .github/CODEOWNERS | 4 + .github/ISSUE_TEMPLATE/defect.yaml | 138 +- .github/ISSUE_TEMPLATE/epic.yml | 59 + .github/ISSUE_TEMPLATE/feature.yaml | 124 +- .github/ISSUE_TEMPLATE/pillar.yml | 47 + .github/ISSUE_TEMPLATE/spike.yaml | 66 + .github/ISSUE_TEMPLATE/task.yaml | 120 +- .../deploy-javascript-sdk/action.yml | 21 +- .../security/org-membership-check/README.md | 83 + .../security/org-membership-check/action.yml | 80 + .github/copilot-instructions.md | 193 + .github/frontend.instructions.md | 142 + .github/workflows/cicd_1-pr.yml | 2 +- .github/workflows/cicd_2-merge-queue.yml | 2 +- .github/workflows/cicd_3-trunk.yml | 1 + .github/workflows/cicd_4-nightly.yml | 1 + .github/workflows/cicd_5-lts.yml | 2 +- .github/workflows/cicd_6-release.yml | 178 + .../cicd_comp_cli-native-build-phase.yml | 2 +- .../workflows/cicd_comp_deployment-phase.yml | 27 +- .github/workflows/cicd_comp_release-phase.yml | 216 + .../cicd_comp_release-prepare-phase.yml | 182 + .github/workflows/cicd_comp_test-phase.yml | 4 +- .../issue_comment_claude-code-review.yaml | 79 + .../workflows/issue_comp_link-issue-to-pr.yml | 19 +- ...-release_comp_maven-build-docker-image.yml | 5 + ...cy-release_publish-dotcms-docker-image.yml | 5 + .gitignore | 19 +- .mise.md | 154 + .mise.toml | 31 + CLAUDE.md | 34 +- bom/application/pom.xml | 9 +- core-web/AGENTS.md | 13 + core-web/CLAUDE.md | 265 + .../apps/dotcdn/src/app/app.component.html | 196 +- core-web/apps/dotcdn/src/app/app.module.ts | 6 +- .../dotcms-block-editor/src/app/app.module.ts | 8 +- .../apps/dotcms-ui-e2e/playwright.config.ts | 16 +- core-web/apps/dotcms-ui/jest.config.ts | 34 + core-web/apps/dotcms-ui/karma.conf.js | 11 - core-web/apps/dotcms-ui/project.json | 25 +- core-web/apps/dotcms-ui/proxy-dev.conf.mjs | 11 + .../add-to-menu/add-to-menu.service.spec.ts | 14 +- .../api/services/dot-account-service.spec.ts | 8 +- .../dot-apps/dot-apps.service.spec.ts | 42 +- .../dot-custom-event-handler.service.spec.ts | 81 +- .../dot-custom-event-handler.service.ts | 8 +- .../app/api/services/dot-menu.service.spec.ts | 16 +- .../guards/auth-guard.service.spec.ts | 19 +- .../guards/contentlet-guard.service.spec.ts | 15 +- .../guards/edit-content.guard.spec.ts | 2 +- .../guards/ema-app/edit-page.guard.spec.ts | 40 +- .../guards/ema-app/edit-page.guard.ts | 2 +- .../guards/menu-guard.service.spec.ts | 39 +- .../guards/pages-guard.service.spec.ts | 4 +- .../guards/public-auth-guard.service.spec.ts | 17 +- .../dotcms-ui/src/app/app-routing.module.ts | 47 +- .../apps/dotcms-ui/src/app/app.component.html | 1 + .../dotcms-ui/src/app/app.component.spec.ts | 61 +- .../apps/dotcms-ui/src/app/app.component.ts | 9 +- core-web/apps/dotcms-ui/src/app/app.module.ts | 10 +- core-web/apps/dotcms-ui/src/app/components.ts | 55 +- core-web/apps/dotcms-ui/src/app/modules.ts | 43 +- ...onfiguration-detail-form.component.spec.ts | 35 +- ...pps-configuration-detail-form.component.ts | 55 +- ...t-apps-configuration-detail-form.module.ts | 40 - ...l-generated-string-field.component.spec.ts | 27 +- ...detail-generated-string-field.component.ts | 3 +- ...figuration-detail-resolver.service.spec.ts | 9 +- ...pps-configuration-detail.component.spec.ts | 87 +- ...dot-apps-configuration-detail.component.ts | 14 +- .../dot-apps-configuration-detail.module.ts | 35 - ...pps-configuration-header.component.spec.ts | 61 +- ...dot-apps-configuration-header.component.ts | 17 +- .../dot-apps-configuration-header.module.ts | 27 - ...-apps-configuration-item.component.spec.ts | 41 +- .../dot-apps-configuration-item.component.ts | 8 +- .../dot-apps-configuration-item.module.ts | 25 - ...-apps-configuration-list.component.spec.ts | 25 +- .../dot-apps-configuration-list.component.ts | 7 +- .../dot-apps-configuration-list.module.ts | 22 - ...pps-configuration-resolver.service.spec.ts | 24 +- ...dot-apps-configuration-resolver.service.ts | 15 +- .../dot-apps-configuration.component.spec.ts | 72 +- .../dot-apps-configuration.component.ts | 15 +- .../dot-apps-configuration.module.ts | 44 - ...pps-import-export-dialog.component.spec.ts | 41 +- ...dot-apps-import-export-dialog.component.ts | 20 +- .../dot-apps-import-export-dialog.module.ts | 36 - .../dot-apps-card.component.spec.ts | 58 +- .../dot-apps-card/dot-apps-card.component.ts | 21 +- .../dot-apps-card/dot-apps-card.module.ts | 31 - .../dot-apps-list-resolver.service.spec.ts | 16 +- .../dot-apps-list.component.spec.ts | 106 +- .../dot-apps-list/dot-apps-list.component.ts | 18 +- .../dot-apps-list/dot-apps-list.module.ts | 34 - .../app/portlets/dot-apps/dot-apps.module.ts | 16 - ...s-routing.module.ts => dot-apps.routes.ts} | 25 +- ...t-categories-create-edit-routing.module.ts | 17 - ...t-categories-create-edit.component.spec.ts | 31 +- .../dot-categories-create-edit.component.ts | 17 +- .../dot-categories-create-edit.module.ts | 28 - .../dot-categories-list-routing.module.ts | 17 - .../dot-categories-list.component.spec.ts | 15 +- .../dot-categories-list.component.ts | 32 +- .../dot-categories-list.module.ts | 44 - ...t-categories-permissions.component.spec.ts | 16 +- .../dot-categories-permissions.component.ts | 6 +- .../dot-categories-permissions.module.ts | 14 - .../dot-categories-routing.module.ts | 19 - .../dot-categories/dot-categories.module.ts | 9 - .../dot-categories/dot-categories.routes.ts | 14 + .../container-list.component.spec.ts | 154 +- .../container-list.component.ts | 56 +- .../container-list/container-list.module.ts | 55 - ...ing.module.ts => container-list.routes.ts} | 12 +- ...ot-container-list-resolver.service.spec.ts | 4 +- .../dot-add-variable.component.spec.ts | 9 +- .../dot-add-variable.component.ts | 10 +- .../dot-add-variable.module.ts | 18 - .../dot-container-code.component.spec.ts | 194 +- .../dot-container-code.component.ts | 25 +- .../dot-container-code.module.ts | 37 - .../dot-container-create-routing.module.ts | 19 - .../dot-container-create.component.spec.ts | 34 +- .../dot-container-create.component.ts | 20 +- .../dot-container-create.module.ts | 31 - .../dot-container-create.routes.ts | 12 + .../dot-container-history.component.spec.ts | 18 +- .../dot-container-history.component.ts | 3 +- .../dot-container-history.module.ts | 14 - ...ot-container-permissions.component.spec.ts | 21 +- .../dot-container-permissions.component.ts | 5 +- .../dot-container-permissions.module.ts | 14 - ...dot-container-properties.component.spec.ts | 45 +- .../dot-container-properties.component.ts | 49 +- .../dot-container-properties.module.ts | 50 - .../dot-loop-editor.component.spec.ts | 16 +- .../dot-loop-editor.component.ts | 17 +- .../dot-loop-editor/dot-loop-editor.module.ts | 24 - .../dot-container-edit.resolver.spec.ts | 38 +- .../resolvers/dot-container-edit.resolver.ts | 15 +- .../dot-containers/dot-containers.module.ts | 12 - ...ing.module.ts => dot-containers.routes.ts} | 26 +- .../dot-content-types.module.ts | 9 - ....module.ts => dot-content-types.routes.ts} | 15 +- ...dot-block-editor-sidebar.component.spec.ts | 27 +- .../dot-block-editor-sidebar.component.ts | 17 +- .../dot-block-editor-sidebar.module.ts | 27 - .../dot-edit-page-info.component.spec.ts | 16 +- .../dot-edit-page-info.component.ts | 19 +- .../dot-edit-page-info.module.ts | 32 - ...dot-palette-content-type.component.spec.ts | 36 +- .../dot-palette-content-type.component.ts | 3 +- .../dot-palette-content-type.module.ts | 22 - .../dot-palette-contentlets.component.spec.ts | 33 +- .../dot-palette-contentlets.component.ts | 4 +- .../dot-palette-contentlets.module.ts | 24 - ...dot-palette-input-filter.component.spec.ts | 13 +- .../dot-palette-input-filter.component.ts | 8 +- .../dot-palette-input-filter.module.ts | 23 - .../dot-palette/dot-palette.component.spec.ts | 119 +- .../dot-palette.component.stories.ts | 11 +- .../dot-palette/dot-palette.component.ts | 5 +- .../dot-palette/dot-palette.module.ts | 13 - .../store/dot-palette.store.spec.ts | 36 +- .../dot-edit-page-lock-info.component.spec.ts | 7 +- .../dot-edit-page-lock-info.component.ts | 3 +- ...it-page-state-controller.component.spec.ts | 66 +- ...ot-edit-page-state-controller.component.ts | 18 +- .../dot-edit-page-state-controller.module.ts | 33 - .../dot-edit-page-toolbar.component.spec.ts | 80 +- .../dot-edit-page-toolbar.component.ts | 35 +- .../dot-edit-page-toolbar.module.ts | 52 - ...-page-view-as-controller.component.spec.ts | 54 +- ...-edit-page-view-as-controller.component.ts | 21 +- ...dot-edit-page-view-as-controller.module.ts | 32 - ...t-page-workflows-actions.component.spec.ts | 25 +- ...t-edit-page-workflows-actions.component.ts | 5 +- .../dot-edit-page-workflows-actions.module.ts | 21 - .../dot-form-selector.component.spec.ts | 16 +- .../dot-form-selector.component.ts | 9 +- .../dot-form-selector.module.ts | 23 - .../dot-whats-changed.component.spec.ts | 88 +- .../dot-whats-changed.component.ts | 4 +- .../dot-whats-changed.module.ts | 15 - .../dot-edit-content.component.spec.ts | 537 +- .../content/dot-edit-content.component.ts | 58 +- .../content/dot-edit-content.module.ts | 102 - .../dot-container-contentlet.service.spec.ts | 15 +- .../dot-edit-content-html.service.spec.ts | 72 +- .../dot-edit-content-html.service.ts | 3 +- .../dot-drag-drop-api-html.service.spec.ts | 21 +- ...-edit-content-toolbar-html.service.spec.ts | 49 +- .../dot-edit-content-toolbar-html.service.ts | 2 +- .../html/libraries/inline-edit-mode.js.ts | 26 +- .../dot-edit-page/dot-edit-page.module.ts | 15 +- ...ting.module.ts => dot-edit-page.routes.ts} | 29 +- ...dditional-actions-iframe.component.spec.ts | 89 +- ...ate-additional-actions-iframe.component.ts | 4 +- ...mplate-additional-actions-iframe.module.ts | 15 - ...plate-additional-actions-routing.module.ts | 17 - .../dot-template-additional-actions.module.ts | 14 - .../layout/dot-edit-layout.module.ts | 28 - .../dot-edit-layout.component.spec.ts | 40 +- .../dot-edit-layout.component.ts | 6 +- .../dot-edit-page-main.component.spec.ts | 58 +- .../dot-edit-page-main.component.ts | 23 +- .../dot-edit-page-main.module.ts | 34 - .../directives/dot-edit-page-nav.directive.ts | 1 - .../dot-edit-page-nav.component.html | 2 +- .../dot-edit-page-nav.component.spec.ts | 124 +- .../dot-edit-page-nav.component.ts | 19 +- .../dot-edit-page-nav.module.ts | 25 - .../dot-edit-page-info-seo.component.ts | 3 +- ...-edit-page-lock-info-seo.component.spec.ts | 4 +- ...age-state-controller-seo.component.spec.ts | 67 +- ...ot-edit-page-toolbar-seo.component.spec.ts | 462 +- .../dot-edit-page-toolbar-seo.component.ts | 17 +- ...e-view-as-controller-seo.component.spec.ts | 33 +- ...t-page-view-as-controller-seo.component.ts | 13 +- .../dot-form-builder.component.spec.ts | 44 +- .../dot-form-builder.component.ts | 7 +- .../dot-form-builder.module.ts | 20 - ...g.module.ts => dot-form-builder.routes.ts} | 14 +- ...pages-create-page-dialog.component.spec.ts | 6 +- .../dot-pages-create-page-dialog.component.ts | 10 +- .../dot-pages-card.component.spec.ts | 32 +- .../dot-pages-card.component.ts | 15 +- .../dot-pages-card/dot-pages-card.module.ts | 24 - ...dot-pages-favorite-panel.component.spec.ts | 50 +- .../dot-pages-favorite-panel.component.ts | 8 +- .../dot-pages-favorite-panel.module.ts | 17 - .../dot-pages-listing-panel.component.spec.ts | 32 +- .../dot-pages-listing-panel.component.ts | 31 +- .../dot-pages-listing-panel.module.ts | 39 - .../dot-pages-store/dot-pages.store.spec.ts | 96 +- .../dot-pages-store/dot-pages.store.ts | 56 +- .../dot-pages/dot-pages.component.spec.ts | 334 +- .../portlets/dot-pages/dot-pages.component.ts | 46 +- .../portlets/dot-pages/dot-pages.module.ts | 59 - ...-routing.module.ts => dot-pages.routes.ts} | 19 +- .../dot-pages/guards/dot-pages.guard.spec.ts | 106 +- .../dot-contentlets.component.spec.ts | 64 +- .../dot-contentlets.component.ts | 4 +- .../dot-contentlets/dot-contentlets.module.ts | 13 - .../dot-portlet-detail.component.spec.ts | 81 +- .../dot-portlet-detail.component.ts | 5 +- .../dot-portlet-detail.module.ts | 25 - .../dot-workflow-task.component.spec.ts | 69 +- .../dot-workflow-task.component.ts | 4 +- .../dot-workflow-task.module.ts | 14 - .../dot-starter/dot-starter.component.spec.ts | 10 +- .../dot-starter/dot-starter.module.ts | 5 +- ...outing.module.ts => dot-starter.routes.ts} | 11 +- .../dot-template-advanced.component.spec.ts | 32 +- .../dot-template-advanced.component.ts | 16 +- .../dot-template-advanced.module.ts | 25 - .../dot-template-builder.component.spec.ts | 465 +- .../dot-template-builder.component.ts | 19 +- .../dot-template-builder.module.ts | 32 - ...dot-template-create-edit.component.spec.ts | 227 +- .../dot-template-create-edit.component.ts | 76 +- .../dot-template-create-edit.module.ts | 32 - ....ts => dot-template-create-edit.routes.ts} | 11 +- .../dot-template-new-routing.module.ts | 27 - .../dot-template-new.component.spec.ts | 85 +- .../dot-template-new.component.ts | 4 +- .../dot-template-new.module.ts | 15 - .../dot-template-new.routes.ts | 23 + .../guards/dot-template.guard.spec.ts | 3 +- .../dot-template-props.component.spec.ts | 51 +- .../dot-template-props.component.ts | 38 +- .../dot-template-props.module.ts | 36 - ...ot-template-thumbnail-field.component.html | 4 +- ...template-thumbnail-field.component.spec.ts | 41 +- .../dot-template-thumbnail-field.component.ts | 6 +- .../dot-template-thumbnail-field.module.ts | 16 - .../dot-template-create-edit.resolver.spec.ts | 10 +- .../store/dot-template.store.spec.ts | 47 +- ...dot-template-list-resolver.service.spec.ts | 4 +- .../dot-template-list.component.spec.ts | 391 +- .../dot-template-list.component.ts | 66 +- .../dot-template-list.module.ts | 51 - .../dot-templates/dot-templates.module.ts | 17 - ...ting.module.ts => dot-templates.routes.ts} | 40 +- .../dot-experiment-class.directive.ts | 1 - .../dot-binary-settings.component.html | 6 +- .../dot-binary-settings.component.spec.ts | 23 +- .../dot-block-editor-settings.component.html | 2 +- ...ot-block-editor-settings.component.spec.ts | 30 +- ...ot-convert-to-block-info.component.spec.ts | 8 +- ...ot-convert-wysiwyg-to-block.component.html | 2 +- ...convert-wysiwyg-to-block.component.spec.ts | 18 +- ...t-type-field-dragabble-item.component.html | 10 +- ...ype-field-dragabble-item.component.spec.ts | 10 +- ...content-type-fields-add-row.component.html | 6 +- ...tent-type-fields-add-row.component.spec.ts | 44 +- .../content-type-fields-add-row.component.ts | 7 +- .../content-type-fields-add-row.module.ts | 25 - .../content-type-fields-add-row/index.ts | 1 - ...ntent-type-fields-drop-zone.component.html | 20 +- ...nt-type-fields-drop-zone.component.spec.ts | 132 +- ...type-fields-properties-form.component.html | 4 +- ...e-fields-properties-form.component.spec.ts | 26 +- ...t-type-fields-properties-form.component.ts | 167 +- .../categories-property.component.spec.ts | 6 +- .../checkbox-property.component.html | 2 +- .../data-type-property.component.html | 2 +- .../data-type-property.component.spec.ts | 6 +- .../default-value-property.component.html | 2 +- .../dot-cardinality-selector.component.html | 2 +- ...dot-cardinality-selector.component.spec.ts | 4 +- .../dot-cardinality-selector.component.ts | 7 +- .../dot-edit-relationships.component.spec.ts | 39 +- .../dot-edit-relationships.component.ts | 9 +- .../dot-new-relationships.component.spec.ts | 116 +- .../dot-new-relationships.component.ts | 46 +- ...t-relationships-property.component.spec.ts | 42 +- .../dot-relationships-property.component.ts | 24 +- .../dot-relationships.module.ts | 45 - .../services/dot-relationship.service.spec.ts | 8 +- .../dynamic-field-property.directive.spec.ts | 7 +- .../dynamic-field-property.directive.ts | 69 +- .../field-properties/index.ts | 1 + .../name-property.component.html | 6 +- .../name-property.component.spec.ts | 4 +- .../new-render-mode-proptery/index.ts | 1 + .../new-render-mode-property.component.html | 30 + .../new-render-mode-property.component.scss | 68 + .../new-render-mode-property.component.ts | 64 + .../regex-check-property.component.html | 2 +- .../values-property.component.html | 8 +- .../values-property.component.spec.ts | 9 +- .../content-type-fields-row.component.html | 6 +- .../content-type-fields-row.component.spec.ts | 2 +- .../content-type-fields-tab.component.html | 2 +- .../content-type-fields-tab.component.spec.ts | 11 +- .../content-types-fields-list.component.html | 2 +- ...ontent-types-fields-list.component.spec.ts | 11 +- .../content-types-fields-list.component.ts | 5 +- ...nt-type-fields-variables.component.spec.ts | 23 +- ...content-type-fields-variables.component.ts | 4 +- ...ot-content-type-fields-variables.module.ts | 15 - .../dot-field-variables.service.spec.ts | 8 +- .../components/fields/index.ts | 1 - .../service/field-drag-drop.service.spec.ts | 14 +- .../service/field-properties.service.spec.ts | 412 +- .../service/field-properties.service.ts | 157 +- .../fields/service/field-property-info.ts | 8 +- .../form/content-types-form.component.html | 24 +- .../form/content-types-form.component.spec.ts | 807 +- .../form/content-types-form.component.ts | 123 +- .../content-types-layout.component.html | 27 +- .../content-types-layout.component.spec.ts | 141 +- .../layout/content-types-layout.component.ts | 37 +- ...ontent-types-edit-resolver.service.spec.ts | 58 +- ...dot-content-types-edit-resolver.service.ts | 24 +- .../dot-content-types-edit.component.html | 7 +- .../dot-content-types-edit.component.spec.ts | 94 +- .../dot-content-types-edit.component.ts | 12 +- .../dot-content-types-edit.module.ts | 130 +- ...le.ts => dot-content-types-edit.routes.ts} | 12 +- .../dot-add-to-menu.component.spec.ts | 96 +- .../dot-add-to-menu.component.ts | 32 +- .../dot-add-to-menu/dot-add-to-menu.module.ts | 42 - ...content-type-copy-dialog.component.spec.ts | 28 +- .../dot-content-type-copy-dialog.component.ts | 30 +- .../dot-content-type-copy-dialog.module.ts | 42 - .../dot-content-type.store.spec.ts | 11 +- .../dot-content-types-listing.module.ts | 33 - .../dot-content-types.component.spec.ts | 49 +- .../dot-content-types.component.ts | 25 +- .../dot-feature-flag-resolver.service.spec.ts | 4 +- core-web/apps/dotcms-ui/src/app/providers.ts | 63 +- .../dot-show-hide-feature.directive.spec.ts | 10 +- .../dot-show-hide-feature.directive.ts | 3 +- .../src/app/shared/dot-directives.module.ts | 6 +- .../dot-save-on-deactivate.service.spec.ts | 21 +- .../src/app/shared/dot-utils.spec.ts | 12 +- .../dotcms-ui/src/app/shared/shared.module.ts | 8 +- .../dotcms-ui/src/app/test/dot-test-bed.ts | 13 +- .../dot-action-button.component.spec.ts | 8 +- .../dot-action-button.component.ts | 5 +- .../dot-action-button.module.ts | 14 - .../dot-alert-confirm.spec.ts | 95 +- .../dot-alert-confirm/dot-alert-confirm.ts | 5 +- .../dot-autocomplete-tags.component.spec.ts | 36 +- .../dot-autocomplete-tags.component.ts | 9 +- .../dot-autocomplete-tags.module.ts | 27 - .../dot-bulk-information.component.spec.ts | 20 +- .../dot-bulk-information.component.ts | 4 +- .../dot-bulk-information.module.ts | 13 - .../dot-custom-time.component.ts | 4 +- .../dot-custom-time.module.ts | 13 - ...t-download-bundle-dialog.component.spec.ts | 57 +- .../dot-download-bundle-dialog.component.ts | 22 +- .../dot-download-bundle-dialog.module.ts | 36 - .../dot-empty-state.component.spec.ts | 13 +- .../dot-empty-state.component.ts | 4 +- .../dot-empty-state/dot-empty-state.module.ts | 13 - .../dot-empty-state.stories.ts | 3 +- ...generate-secure-password.component.spec.ts | 144 +- .../dot-generate-secure-password.component.ts | 7 +- .../dot-generate-secure-password.module.ts | 17 - .../dot-global-message.component.spec.ts | 7 +- .../dot-global-message.component.ts | 3 +- .../dot-global-message.module.ts | 15 - .../dot-inline-edit.component.spec.ts | 8 +- .../dot-inline-edit.component.ts | 6 +- .../dot-inline-edit/dot-inline-edit.module.ts | 15 - .../dot-md-icon-selector.component.spec.ts | 4 +- .../dot-md-icon-selector.component.ts | 5 +- .../dot-md-icon-selector.module.ts | 12 - .../dot-overlay-mask.component.spec.ts | 3 +- .../dot-overlay-mask.component.ts | 2 +- .../dot-overlay-mask.module.ts | 11 - .../dot-page-selector.component.spec.ts | 91 +- .../dot-page-selector.component.ts | 19 +- .../dot-page-selector.module.ts | 29 - .../service/dot-page-selector.service.spec.ts | 8 +- .../dot-push-publish-dialog.component.spec.ts | 58 +- .../dot-push-publish-dialog.component.ts | 25 +- .../dot-push-publish-dialog.module.ts | 35 - ...ush-publish-env-selector.component.spec.ts | 24 +- ...dot-push-publish-env-selector.component.ts | 8 +- .../dot-push-publish-env-selector.module.ts | 26 - .../dot-site-selector-field.component.spec.ts | 11 +- .../dot-site-selector-field.component.ts | 4 +- .../dot-site-selector-field.module.ts | 14 - .../dot-site-selector.component.spec.ts | 90 +- .../dot-site-selector.component.ts | 7 +- .../dot-site-selector.module.ts | 14 - .../dot-textarea-content.component.spec.ts | 30 +- .../dot-textarea-content.component.ts | 8 +- .../dot-textarea-content.module.ts | 16 - .../dot-wizard/dot-wizard.component.html | 1 + .../dot-wizard/dot-wizard.component.spec.ts | 43 +- .../dot-wizard/dot-wizard.component.ts | 21 +- .../_common/dot-wizard/dot-wizard.module.ts | 34 - ...s-actions-selector-field.component.spec.ts | 34 +- ...kflows-actions-selector-field.component.ts | 8 +- ...workflows-actions-selector-field.module.ts | 19 - ...ows-actions-selector-field.service.spec.ts | 10 +- ...ot-workflows-selector-field.component.html | 2 +- ...workflows-selector-field.component.spec.ts | 110 +- .../dot-workflows-selector-field.component.ts | 10 +- .../dot-workflows-selector-field.module.ts | 16 - ...-comment-and-assign-form.component.spec.ts | 11 +- .../dot-comment-and-assign-form.component.ts | 22 +- .../dot-comment-and-assign-form.module.ts | 31 - .../dot-push-publish-form.component.spec.ts | 30 +- .../dot-push-publish-form.component.ts | 33 +- .../dot-push-publish-form.module.ts | 45 - .../dot-loading-indicator.component.ts | 3 +- .../dot-loading-indicator.module.ts | 15 - .../iframe-component/iframe.component.spec.ts | 48 +- .../iframe-component/iframe.component.ts | 18 +- .../iframe-porlet-legacy.component.spec.ts | 23 +- .../iframe-porlet-legacy.component.ts | 11 +- .../_common/iframe/iframe.module.ts | 35 - .../view/components/_common/iframe/index.ts | 3 +- .../dot-safe-url/dot-safe-url.pipe.spec.ts | 11 +- .../pipes/dot-safe-url/dot-safe-url.pipe.ts | 2 +- ...ame-porlet-legacy-resolver.service.spec.ts | 24 +- .../searchable-dropdown.component.spec.ts | 61 +- .../searchable-dropdown.component.ts | 47 +- .../searchable-dropdown.module.ts | 36 - .../dot-add-persona-dialog.component.spec.ts | 42 +- .../dot-add-persona-dialog.component.ts | 3 +- .../dot-add-persona-dialog.module.ts | 22 - .../dot-create-persona-form.component.html | 2 +- .../dot-create-persona-form.component.spec.ts | 65 +- .../dot-create-persona-form.component.ts | 25 +- .../dot-create-persona-form.module.ts | 42 - .../dot-base-type-selector.component.spec.ts | 11 +- .../dot-base-type-selector.component.ts | 5 +- .../dot-base-type-selector.module.ts | 14 - .../dot-container-selector.component.spec.ts | 48 +- .../dot-container-selector.component.ts | 12 +- .../dot-container-selector.module.ts | 25 - ...ot-content-type-selector.component.spec.ts | 11 +- .../dot-content-type-selector.component.ts | 5 +- .../dot-content-type-selector.module.ts | 14 - .../dot-add-contentlet.component.spec.ts | 43 +- .../dot-add-contentlet.component.ts | 4 +- .../dot-contentlet-wrapper.component.spec.ts | 106 +- .../dot-contentlet-wrapper.component.ts | 17 +- .../dot-create-contentlet.component.spec.ts | 193 +- .../dot-create-contentlet.component.ts | 4 +- ...create-contentlet.resolver.service.spec.ts | 9 +- .../dot-edit-contentlet.component.spec.ts | 17 +- .../dot-edit-contentlet.component.ts | 4 +- .../dot-reorder-menu.component.spec.ts | 76 +- .../dot-reorder-menu.component.ts | 6 +- .../dot-contentlet-editor.module.ts | 40 - ...ule.ts => dot-contentlet-editor.routes.ts} | 11 +- .../dot-contentlet-editor.service.spec.ts | 35 +- .../services/dot-contentlet-editor.service.ts | 4 +- .../dot-copy-link.component.spec.ts | 19 +- .../dot-copy-link/dot-copy-link.component.ts | 6 +- .../dot-copy-link/dot-copy-link.module.ts | 17 - .../dot-crumbtrail.component.html | 2 +- .../dot-crumbtrail.component.spec.ts | 61 +- .../dot-crumbtrail.component.ts | 31 +- .../dot-crumbtrail/dot-crumbtrail.module.ts | 15 - .../service/dot-crumbtrail.service.spec.ts | 732 - .../service/dot-crumbtrail.service.ts | 202 - .../dot-device-selector.component.spec.ts | 38 +- .../dot-device-selector.component.ts | 7 +- .../dot-device-selector.module.ts | 18 - .../dot-field-helper.component.spec.ts | 10 +- .../dot-field-helper.component.ts | 5 +- .../dot-field-helper.module.ts | 14 - .../dot-iframe-dialog.component.spec.ts | 80 +- .../dot-iframe-dialog.component.ts | 4 +- .../dot-iframe-dialog.module.ts | 14 - .../dot-language-selector.component.spec.ts | 5 +- .../dot-language-selector.component.ts | 1 - ...ot-large-message-display.component.spec.ts | 10 +- .../dot-large-message-display.component.ts | 3 +- .../dot-large-message-display.module.ts | 13 - .../action-header.component.spec.ts | 8 +- .../action-header/action-header.component.ts | 6 +- .../action-header/action-header.module.ts | 25 - .../dot-listing-data-table.component.spec.ts | 78 +- .../dot-listing-data-table.component.ts | 40 +- .../dot-listing-data-table.module.ts | 46 - .../dot-message-display.component.spec.ts | 33 +- .../dot-message-display.component.ts | 4 +- .../dot-message-display.module.ts | 17 - .../dot-nav-header.component.spec.ts | 14 +- .../dot-nav-header.component.ts | 4 +- .../dot-nav-icon.component.spec.ts | 5 +- .../dot-nav-icon/dot-nav-icon.component.ts | 4 +- .../dot-nav-icon/dot-nav-icon.module.ts | 13 - .../dot-nav-item/dot-nav-item.component.html | 34 +- .../dot-nav-item/dot-nav-item.component.scss | 26 +- .../dot-nav-item.component.spec.ts | 251 +- .../dot-nav-item/dot-nav-item.component.ts | 66 +- .../dot-sub-nav/dot-sub-nav.component.html | 10 + .../dot-sub-nav/dot-sub-nav.component.scss | 18 +- .../dot-sub-nav/dot-sub-nav.component.spec.ts | 73 +- .../dot-sub-nav/dot-sub-nav.component.ts | 3 +- .../dot-navigation.component.html | 2 +- .../dot-navigation.component.spec.ts | 248 +- .../dot-navigation.component.ts | 101 +- .../dot-navigation/dot-navigation.module.ts | 40 - .../services/dot-navigation.service.spec.ts | 319 +- .../services/dot-navigation.service.ts | 434 +- ...ot-persona-selected-item.component.spec.ts | 43 +- .../dot-persona-selected-item.component.ts | 7 +- .../dot-persona-selected-item.module.ts | 30 - ...-persona-selector-option.component.spec.ts | 20 +- .../dot-persona-selector-option.component.ts | 15 +- .../dot-persona-selector-option.module.ts | 27 - .../dot-persona-selector.component.spec.ts | 280 +- .../dot-persona-selector.component.ts | 21 +- .../dot-persona.selector.module.ts | 39 - .../dot-portlet-box.component.spec.ts | 3 +- .../dot-portlet-box.component.ts | 2 +- .../dot-portlet-box/dot-portlet-box.module.ts | 11 - .../dot-portlet-toolbar.component.spec.ts | 35 +- .../dot-portlet-toolbar.component.ts | 8 +- .../dot-portlet-toolbar.module.ts | 17 - .../dot-portlet-base.component.html | 8 +- .../dot-portlet-base.component.spec.ts | 43 +- .../dot-portlet-base.component.ts | 5 +- .../dot-portlet-base.module.ts | 14 - .../dot-portlet-base.stories.ts | 3 +- .../dot-relationship-tree.component.spec.ts | 35 +- .../dot-relationship-tree.component.ts | 3 +- .../dot-relationship-tree.module.ts | 13 - .../dot-secondary-toolbar.component.spec.ts | 39 +- .../dot-secondary-toolbar.component.ts | 4 +- .../dot-secondary-toolbar.module.ts | 14 - ...-theme-selector-dropdown.component.spec.ts | 394 +- .../dot-theme-selector-dropdown.component.ts | 40 +- .../dot-theme-selector-dropdown.module.ts | 29 - .../dot-login-as.component.spec.ts | 110 +- .../dot-my-account.component.spec.ts | 143 +- .../dot-my-account.component.ts | 1 + ...ot-toolbar-announcements.component.spec.ts | 36 +- .../dot-toolbar-announcements.component.ts | 13 +- .../store/dot-announcements.store.spec.ts | 4 +- .../dot-notification-item.component.spec.ts | 2 +- .../dot-notification-item.component.ts | 4 +- .../dot-toolbar-notifications.component.ts | 12 +- .../dot-toolbar-notifications.module.ts | 29 - .../dot-toolbar-btn-overlay.component.spec.ts | 60 +- .../dot-toolbar-user.component.html | 1 + .../dot-toolbar-user.component.spec.ts | 110 +- .../store/dot-toolbar-user.store.spec.ts | 32 +- .../dot-toolbar/dot-toolbar.component.html | 2 +- .../dot-toolbar/dot-toolbar.component.scss | 4 + .../dot-toolbar/dot-toolbar.component.ts | 21 +- .../dot-toolbar/dot-toolbar.module.ts | 33 - .../dot-toolbar/dot-toolbar.spec.ts | 44 +- ...dot-workflow-task-detail.component.spec.ts | 122 +- .../dot-workflow-task-detail.component.ts | 6 +- .../dot-workflow-task-detail.module.ts | 15 - .../dot-workflow-task-detail.service.spec.ts | 10 +- .../dot-login.component.spec.ts | 35 +- .../dot-login.component.ts | 39 +- .../dot-login-component/dot-login.module.ts | 50 - .../dot-login-page-resolver.service.spec.ts | 13 +- .../login/dot-login-page-routing.module.ts | 35 - .../components/login/dot-login-page.module.ts | 13 - .../components/login/dot-login-page.routes.ts | 28 + .../forgot-password.component.spec.ts | 32 +- .../forgot-password.component.ts | 31 +- .../forgot-password.module.ts | 38 - .../main/dot-login-page.component.spec.ts | 6 +- .../login/main/dot-login-page.component.ts | 3 +- .../reset-password.component.spec.ts | 31 +- .../reset-password.component.ts | 31 +- .../reset-password.module.ts | 38 - .../dot-login-page-state.service.spec.ts | 9 +- .../main-legacy/main-legacy.component.spec.ts | 84 +- .../main-legacy/main-legacy.component.ts | 30 +- .../not-license/not-license.component.spec.ts | 4 +- .../dot-container-reference.directive.spec.ts | 2 +- .../dot-container-reference.directive.ts | 3 +- .../dot-container-reference.module.ts | 11 - .../dot-maxlength.directive.spec.ts | 5 +- .../dot-maxlength/dot-maxlength.directive.ts | 3 +- .../dot-maxlength/dot-maxlength.module.ts | 11 - .../ripple/ripple-effect.directive.ts | 3 +- .../directives/ripple/ripple-effect.module.ts | 11 - .../dot-filter/dot-filter-pipe.module.ts | 11 - .../view/pipes/dot-filter/dot-filter.pipe.ts | 3 +- .../dot-random-icon.pipe.module.ts | 11 - .../dot-radom-icon/dot-random-icon.pipe.ts | 3 +- core-web/apps/dotcms-ui/src/main.ts | 10 +- .../dotcms/menu/DotCrumbtrail.stories.ts | 29 +- .../primeng/messages/Toast.component.ts | 1 - .../stories/primeng/misc/Defer.component.ts | 1 - .../overlay/ConfirmDialog.component.ts | 1 - core-web/apps/dotcms-ui/src/test-setup.ts | 112 + core-web/apps/dotcms-ui/src/test.ts | 13 - core-web/apps/dotcms-ui/tsconfig.spec.json | 7 +- core-web/apps/mcp-server/CONTRIBUTING.md | 82 + core-web/apps/mcp-server/README.md | 32 +- .../src/services/contentype.spec.ts | 6 +- .../mcp-server/src/services/search.spec.ts | 228 +- .../apps/mcp-server/src/services/site.spec.ts | 201 + .../src/tools/_example-tool/formatters.ts | 47 + .../src/tools/_example-tool/handlers.ts | 76 + .../src/tools/_example-tool/index.ts | 75 + .../src/tools/content-types/index.ts | 2 +- .../mcp-server/src/types/contentype.spec.ts | 698 + .../apps/mcp-server/src/types/contentype.ts | 70 +- core-web/apps/mcp-server/src/types/search.ts | 32 +- core-web/apps/mcp-server/src/types/site.ts | 90 +- .../mcp-server/src/utils/response.spec.ts | 118 +- .../apps/mcp-server/src/utils/response.ts | 27 +- core-web/karma.conf.js | 66 - .../src/lib/block-editor.module.ts | 10 +- .../dot-block-editor.component.spec.ts | 1 - .../dot-block-editor.component.stories.ts | 4 +- .../dot-block-editor.component.ts | 14 +- .../lib/directive/drag-handle.directive.ts | 3 +- .../lib/directive/editor-modal.directive.ts | 1 - .../dot-bubble-menu.component.ts | 2 - .../dot-context-menu.component.html | 8 +- .../dot-context-menu.component.ts | 4 +- .../elements/dot-table/dot-table.extension.ts | 7 +- .../store/ai-content-prompt.store.ts | 6 +- .../asset-form/asset-form.module.ts | 6 +- .../dot-external-asset.component.ts | 11 +- .../dot-upload-asset.component.html | 37 +- .../dot-upload-asset.component.spec.ts | 10 +- .../dot-upload-asset.component.ts | 13 +- .../asset-uploader.extension.ts | 2 +- .../upload-placeholder.component.html | 2 +- .../upload-placeholder.component.ts | 6 +- .../bubble-form/bubble-form.component.ts | 5 +- .../extensions/dot-config/dot-config.types.ts | 7 + .../suggestions-list-item.component.ts | 12 +- .../suggestions/suggestions.component.ts | 11 +- .../directives/editor/editor.directive.ts | 9 +- .../floating/floating-menu.directive.ts | 4 +- .../suggestions/suggestions.service.ts | 4 +- core-web/libs/data-access/src/index.ts | 5 + .../add-to-bundle.service.spec.ts | 10 +- .../src/lib/dot-ai/dot-ai.service.ts | 2 +- .../dot-containers.service.spec.ts | 276 + .../dot-containers/dot-containers.service.ts | 91 +- .../dot-content-drive.service.spec.ts | 101 + .../dot-content-drive.service.ts | 19 + .../dot-content-type.service.spec.ts | 179 +- .../dot-content-type.service.ts | 101 +- .../dot-contentlet-locker.service.spec.ts | 8 +- .../dot-contentlet-locker.service.ts | 8 + .../dot-contentlet/dot-contentlet.service.ts | 4 +- .../src/lib/dot-crud/dot-crud.service.spec.ts | 8 +- .../dot-current-user.service.spec.ts | 8 +- .../dot-devices/dot-devices.service.spec.ts | 8 +- .../dot-edit-page-resolver.service.ts | 1 - .../dot-edit-page.service.spec.ts | 8 +- .../lib/dot-events/dot-events.service.spec.ts | 2 +- .../dot-favorite-contenttype.service.spec.ts | 582 + .../dot-favorite-contenttype.service.ts | 113 + .../lib/dot-folder/dot-folder.service.spec.ts | 283 + .../src/lib/dot-folder/dot-folder.service.ts | 65 + .../dot-global-message.service.spec.ts | 4 +- .../dot-languages/dot-languages.service.ts | 4 +- .../dot-license/dot-license.service.spec.ts | 8 +- .../dot-localstorage.service.spec.ts | 2 +- .../dot-page-contenttype.service.spec.ts | 958 ++ .../dot-page-contenttype.service.ts | 178 + .../dot-page-layout.service.spec.ts | 8 +- .../dot-page-render.service.spec.ts | 8 +- .../dot-page-state.service.spec.ts | 34 +- .../dot-personalize.service.spec.ts | 8 +- .../dot-personas/dot-personas.service.spec.ts | 8 +- .../dot-properties/dot-properties.service.ts | 11 +- .../dot-push-publish-filters.service.spec.ts | 8 +- .../dot-push-publish-filters.service.ts | 4 +- .../src/lib/dot-roles/dot-roles.service.ts | 4 +- .../lib/dot-router/dot-router.service.spec.ts | 56 +- .../src/lib/dot-router/dot-router.service.ts | 29 +- .../src/lib/dot-tags/dot-tags.service.spec.ts | 8 +- .../src/lib/dot-tags/dot-tags.service.ts | 4 +- .../lib/dot-themes/dot-themes.service.spec.ts | 8 +- .../dot-ui-colors.service.spec.ts | 27 +- .../dot-ui-colors/dot-ui-colors.service.ts | 11 +- .../dot-upload-file.service.spec.ts | 14 + .../dot-upload-file.service.ts | 16 +- .../dot-versionable.service.spec.ts | 51 +- .../dot-versionable.service.ts | 24 +- .../dot-workflow-actions-fire.service.ts | 27 +- .../lib/paginator/paginator.service.spec.ts | 14 +- .../dot-rules/src/lib/app.component.spec.ts | 8 +- .../libs/dot-rules/src/lib/app.component.ts | 2 +- .../src/lib/components/dropdown/dropdown.ts | 44 +- .../components/restdropdown/RestDropdown.ts | 8 +- .../serverside-condition.ts | 172 +- .../visitors-location.component.ts | 68 +- .../area-picker-dialog.component.ts | 10 +- .../src/lib/modal-dialog/dialog-component.ts | 11 +- .../add-to-bundle-dialog-component.ts | 11 +- .../src/lib/rule-action-component.ts | 54 +- .../libs/dot-rules/src/lib/rule-component.ts | 280 +- .../src/lib/rule-condition-component.ts | 74 +- .../src/lib/rule-condition-group-component.ts | 87 +- .../libs/dot-rules/src/lib/rule-engine.ts | 230 +- .../src/lib/services/GoogleMapService.ts | 8 +- .../libs/dot-rules/src/lib/services/Rule.ts | 2 +- .../dot-binary-file/dot-binary-file.e2e.tsx | 3 +- .../dot-binary-file/dot-binary-file.tsx | 6 +- .../dot-binary-text-field.tsx | 1 + .../dot-binary-upload-button.tsx | 1 + .../dot-checkbox/dot-checkbox.e2e.ts | 1 + .../components/dot-checkbox/dot-checkbox.tsx | 1 + .../dot-date-range/dot-date-range.e2e.ts | 1 + .../dot-date-range/dot-date-range.tsx | 3 +- .../dot-date-time/dot-date-time.e2e.ts | 1 + .../dot-date-time/dot-date-time.tsx | 9 +- .../src/components/dot-date/dot-date.e2e.ts | 1 + .../src/components/dot-date/dot-date.tsx | 3 +- .../dot-form-column/dot-form-column.e2e.ts | 1 + .../dot-form-column/dot-form-column.tsx | 4 +- .../dot-form/dot-form-row/dot-form-row.e2e.ts | 1 + .../dot-form/dot-form-row/dot-form-row.tsx | 1 + .../src/components/dot-form/dot-form.e2e.ts | 3 +- .../src/components/dot-form/dot-form.tsx | 10 +- .../src/components/dot-form/utils/fields.tsx | 4 +- .../components/dot-form/utils/index.spec.ts | 1 + .../src/components/dot-form/utils/index.ts | 6 +- .../dot-input-calendar/dot-input-calendar.tsx | 5 +- .../dot-key-value/dot-key-value.e2e.ts | 1 + .../dot-key-value/dot-key-value.tsx | 1 + .../key-value-form/key-value-form.tsx | 1 + .../key-value-table/key-value-table.tsx | 1 + .../src/components/dot-label/dot-label.tsx | 1 + .../dot-multi-select/dot-multi-select.e2e.ts | 1 + .../dot-multi-select/dot-multi-select.tsx | 1 + .../src/components/dot-radio/dot-radio.e2e.ts | 1 + .../src/components/dot-radio/dot-radio.tsx | 1 + .../components/dot-select/dot-select.e2e.ts | 1 + .../src/components/dot-select/dot-select.tsx | 1 + .../dot-tags/components/dot-chip/dot-chip.tsx | 2 +- .../src/components/dot-tags/dot-tags.e2e.ts | 1 + .../src/components/dot-tags/dot-tags.tsx | 1 + .../dot-textarea/dot-textarea.e2e.ts | 1 + .../components/dot-textarea/dot-textarea.tsx | 1 + .../dot-textfield/dot-texfield.e2e.ts | 1 + .../dot-textfield/dot-textfield.tsx | 1 + .../src/components/dot-time/dot-time.e2e.ts | 1 + .../src/components/dot-time/dot-time.tsx | 3 +- .../src/models/dot-field-event.model.ts | 2 +- .../src/utils/checkProp.tsx | 4 +- .../src/utils/props/DotFieldPropError.spec.ts | 3 +- .../src/utils/props/validators/date.spec.ts | 1 + .../src/utils/props/validators/date.ts | 2 +- .../src/utils/props/validators/props.spec.ts | 3 +- .../src/utils/props/validators/props.ts | 5 +- .../dotcms-field-elements/src/utils/utils.tsx | 3 +- .../src/lib/core/site.service.mock.ts | 25 +- .../dotcms-js/src/lib/core/site.service.ts | 45 +- core-web/libs/dotcms-models/src/index.ts | 3 + .../dotcms-models/src/lib/dot-api-response.ts | 20 + .../src/lib/dot-content-drive.model.ts | 178 +- .../src/lib/dot-content-types.model.ts | 30 +- .../src/lib/dot-contentlet.model.ts | 23 + .../dotcms-models/src/lib/dot-folder.model.ts | 40 + .../src/lib/dot-pagination.model.ts | 24 + .../dotcms-models/src/lib/dot-site.model.ts | 1 + .../dotcms-models/src/lib/navigation/index.ts | 3 + .../src/lib/navigation/menu-entity.model.ts | 18 + .../src/lib/navigation/menu-group.model.ts | 12 + .../src/lib/navigation/menu-item.model.ts | 5 + .../src/lib/navigation/menu.model.ts | 1 + .../src/lib/navigation/menu.slice.model.ts | 17 + .../navigation/navigate-to-options.model.ts | 1 + .../src/lib/navigation/portlet-nav.model.ts | 1 + .../dotcms-models/src/lib/shared-models.ts | 8 +- core-web/libs/dotcms-scss/angular/_forms.scss | 12 + .../dotcms-theme/components/_contextmenu.scss | 9 +- .../dotcms-theme/components/_dialog.scss | 4 + .../dotcms-theme/components/_skeleton.scss | 7 +- .../dotcms-theme/components/_table.scss | 3 +- .../dotcms-theme/components/_tabview.scss | 8 + .../components/buttons/_button.scss | 9 + .../components/form/_iconfield.scss | 16 + .../components/form/_multiselect.scss | 27 + .../dotcms-theme/components/form/common.scss | 8 +- .../components/messages/_message.scss | 4 + core-web/libs/dotcms-scss/jsp/css/dotcms.css | 6 +- .../dot-admin/portlets/_content-edit.scss | 1 - core-web/libs/dotcms-scss/shared/_colors.scss | 14 + core-web/libs/dotcms-scss/shared/_common.scss | 5 + .../libs/dotcms-scss/shared/_spacing.scss | 3 + .../dot-contentlet-icon.tsx | 84 +- .../dot-contentlet-thumbnail.tsx | 20 +- .../src/models/dot-contentlet-item.model.ts | 1 + .../dotcms-webcomponents/src/utils/utils.tsx | 7 +- .../libs/edit-content-bridge/package.json | 4 +- .../lib/bridges/angular-form-bridge.spec.ts | 119 +- .../src/lib/bridges/angular-form-bridge.ts | 46 +- .../src/lib/factories/form-bridge.factory.ts | 2 +- .../edit-content-bridge/tsconfig.lib.json | 2 +- .../libs/edit-content-bridge/vite.config.ts | 33 +- ...ot-create-content-dialog.component.spec.ts | 14 +- .../dot-edit-content-compare.component.html | 58 + .../dot-edit-content-compare.component.scss | 55 + .../dot-edit-content-compare.component.ts | 19 + .../dot-edit-content-field.component.html | 94 +- .../dot-edit-content-field.component.spec.ts | 487 +- .../dot-edit-content-field.component.ts | 58 +- .../dot-edit-content-form-resolutions.spec.ts | 197 +- .../dot-edit-content-form-resolutions.ts | 77 +- .../dot-edit-content-form.component.html | 81 +- .../dot-edit-content-form.component.spec.ts | 232 +- .../dot-edit-content-form.component.ts | 126 +- .../dot-edit-content.layout.component.html | 34 +- .../dot-edit-content.layout.component.spec.ts | 11 +- .../dot-edit-content.layout.component.ts | 12 +- ...-content-sidebar-activities.component.html | 1 + ...ntent-sidebar-activities.component.spec.ts | 6 +- .../dot-history-timeline-item.component.html | 124 + .../dot-history-timeline-item.component.scss | 197 + ...ot-history-timeline-item.component.spec.ts | 392 + .../dot-history-timeline-item.component.ts | 159 + ...t-pushpublish-timeline-item.component.html | 91 + ...t-pushpublish-timeline-item.component.scss | 167 + ...ushpublish-timeline-item.component.spec.ts | 161 + ...dot-pushpublish-timeline-item.component.ts | 55 + ...dit-content-sidebar-history.component.html | 123 + ...dit-content-sidebar-history.component.scss | 101 + ...-content-sidebar-history.component.spec.ts | 555 + ...-edit-content-sidebar-history.component.ts | 240 + .../dot-edit-content-sidebar.component.html | 24 + .../dot-edit-content-sidebar.component.scss | 9 +- ...dot-edit-content-sidebar.component.spec.ts | 183 +- .../dot-edit-content-sidebar.component.ts | 64 +- .../tab-view-insert.directive.spec.ts | 100 +- .../tab-view-insert.directive.ts | 3 +- .../dot-card-field-content.component.ts | 10 + .../dot-card-field-footer.component.ts | 10 + .../dot-card-field-label.component.html | 14 + .../dot-card-field-label.component.scss | 16 + .../dot-card-field-label.component.ts | 50 + .../dot-card-field.component.ts | 20 + .../dot-binary-field-editor.component.ts | 1 + .../dot-binary-field-preview.component.ts | 2 - .../dot-binary-field-url-mode.component.ts | 1 + .../dot-binary-field-wrapper.component.html | 36 + .../dot-binary-field-wrapper.component.ts | 59 + ...t-edit-content-binary-field.component.html | 10 +- ...dit-content-binary-field.component.spec.ts | 25 +- ...dot-edit-content-binary-field.component.ts | 9 +- ...t-edit-content-block-editor.component.html | 35 + ...dit-content-block-editor.component.spec.ts | 108 + ...dot-edit-content-block-editor.component.ts | 62 + .../calendar-field.component.html | 24 + .../calendar-field.component.scss} | 0 .../calendar-field.component.ts | 247 + .../calendar-field.util.spec.ts} | 7 +- .../calendar-field/calendar-field.util.ts} | 4 +- ...edit-content-calendar-field.component.html | 102 +- ...t-content-calendar-field.component.spec.ts | 700 +- ...t-edit-content-calendar-field.component.ts | 227 +- ...tegory-field-search-list.component.spec.ts | 5 - .../dot-category-field.component.html | 27 + .../dot-category-field.component.scss} | 0 .../dot-category-field.component.spec.ts} | 226 +- .../dot-category-field.component.ts | 152 + ...edit-content-category-field.component.html | 60 +- ...t-edit-content-category-field.component.ts | 165 +- ...edit-content-checkbox-field.component.html | 42 + ...t-content-checkbox-field.component.spec.ts | 374 +- ...t-edit-content-checkbox-field.component.ts | 123 +- ...t-edit-content-custom-field.component.html | 134 +- ...dit-content-custom-field.component.spec.ts | 193 +- ...dot-edit-content-custom-field.component.ts | 47 +- .../dot-file-field.component.html | 116 + .../dot-file-field.component.scss | 105 + .../dot-file-field.component.ts | 471 + ...dot-edit-content-file-field.component.html | 141 +- ...dot-edit-content-file-field.component.scss | 104 - ...-edit-content-file-field.component.spec.ts | 194 +- .../dot-edit-content-file-field.component.ts | 491 +- .../host-folder-field.component.html | 34 + .../host-folder-field.component.ts | 141 + ...t-content-host-folder-field.component.html | 63 +- ...ontent-host-folder-field.component.spec.ts | 147 +- ...dit-content-host-folder-field.component.ts | 108 +- ...dot-edit-content-json-field.component.html | 67 +- ...-edit-content-json-field.component.spec.ts | 219 +- .../dot-edit-content-json-field.component.ts | 36 +- .../key-value-field.component.html | 5 + .../key-value-field.component.ts | 78 + .../dot-edit-content-key-value.component.html | 36 +- ...t-edit-content-key-value.component.spec.ts | 265 +- .../dot-edit-content-key-value.component.ts | 90 +- ...-content-multi-select-field.component.html | 37 + ...ntent-multi-select-field.component.spec.ts | 152 +- ...it-content-multi-select-field.component.ts | 40 +- ...ot-edit-content-radio-field.component.html | 48 +- ...ot-edit-content-radio-field.component.scss | 7 - ...edit-content-radio-field.component.spec.ts | 259 +- .../dot-edit-content-radio-field.component.ts | 27 +- .../dot-relationship-field.component.html | 192 + .../dot-relationship-field.component.scss | 63 + .../dot-relationship-field.component.ts | 339 + .../search/search.component.spec.ts | 2 - ...-content-relationship-field.component.html | 196 +- ...ntent-relationship-field.component.spec.ts | 716 +- ...it-content-relationship-field.component.ts | 365 +- ...t-edit-content-select-field.component.html | 38 + ...dit-content-select-field.component.spec.ts | 156 +- ...dot-edit-content-select-field.component.ts | 71 +- .../tag-field/tag-field.component.html | 26 + .../tag-field/tag-field.component.ts | 211 + .../dot-edit-content-tag-field.component.html | 54 +- ...t-edit-content-tag-field.component.spec.ts | 205 +- .../dot-edit-content-tag-field.component.ts | 243 +- .../dot-edit-content-text-area.component.html | 88 +- ...t-edit-content-text-area.component.spec.ts | 593 +- .../dot-edit-content-text-area.component.ts | 34 +- ...dot-edit-content-text-field.component.html | 49 +- ...dot-edit-content-text-field.component.scss | 4 - ...-edit-content-text-field.component.spec.ts | 175 +- .../dot-edit-content-text-field.component.ts | 57 +- .../dot-wysiwyg-tinymce.component.html | 4 +- .../dot-wysiwyg-tinymce.component.spec.ts | 186 +- .../dot-wysiwyg-tinymce.component.ts | 4 + ...-edit-content-wysiwyg-field.component.html | 68 +- ...-edit-content-wysiwyg-field.component.scss | 2 +- ...it-content-wysiwyg-field.component.spec.ts | 231 +- ...ot-edit-content-wysiwyg-field.component.ts | 35 +- .../shared/base-control-value-accesor.ts | 49 + .../lib/fields/shared/base-wrapper-field.ts | 83 + .../src/lib/models/dot-edit-content.model.ts | 67 +- .../src/lib/pipes/contentlet-status.pipe.ts | 3 +- .../src/lib/pipes/language.pipe.ts | 3 +- .../src/lib/pipes/name-format.pipe.ts | 1 - .../src/lib/pipes/truncate-path.pipe.ts | 1 - .../services/dot-edit-content.service.spec.ts | 213 + .../lib/services/dot-edit-content.service.ts | 89 +- ...ntent-monaco-editor-control.component.html | 9 +- ...ntent-monaco-editor-control.component.scss | 6 + ...nt-monaco-editor-control.component.spec.ts | 92 +- ...content-monaco-editor-control.component.ts | 70 +- .../src/lib/store/edit-content.store.spec.ts | 38 +- .../src/lib/store/edit-content.store.ts | 67 +- .../features/content/content.feature.spec.ts | 26 +- .../store/features/content/content.feature.ts | 21 +- .../lib/store/features/form/form.feature.ts | 9 + .../features/history/history.feature.spec.ts | 1221 ++ .../store/features/history/history.feature.ts | 721 + .../workflow/workflow.feature.spec.ts | 14 +- .../src/lib/utils/functions.util.spec.ts | 3 + .../src/lib/utils/functions.util.ts | 1 + .../libs/edit-content/src/lib/utils/mocks.ts | 15 - core-web/libs/edit-content/src/test-setup.ts | 38 +- .../breadcrumb/breadcrumb.feature.spec.ts | 933 + .../features/breadcrumb/breadcrumb.feature.ts | 308 + .../breadcrumb/breadcrumb.utils.spec.ts | 233 + .../features/breadcrumb/breadcrumb.utils.ts | 183 + .../src/lib/features/menu/menu.slice.ts | 31 + .../features/menu/with-menu.feature.spec.ts | 588 + .../lib/features/menu/with-menu.feature.ts | 364 + .../with-system}/with-system.feature.ts | 0 core-web/libs/global-store/src/lib/store.ts | 50 +- ...t-analytics-dashboard-filters.component.ts | 3 +- .../dot-analytics-state-message.component.ts | 4 +- .../dot-analytics-dashboard.component.html | 26 + .../dot-analytics-dashboard.component.spec.ts | 88 +- .../dot-analytics-dashboard.component.ts | 25 +- .../dot-analytics-search.component.ts | 2 - .../portlet/src/lib.routes.ts | 4 +- ...content-drive-dialog-folder.component.html | 113 + ...content-drive-dialog-folder.component.scss | 71 + ...tent-drive-dialog-folder.component.spec.ts | 984 ++ ...t-content-drive-dialog-folder.component.ts | 367 + .../dot-content-drive-dropzone.component.html | 10 + .../dot-content-drive-dropzone.component.scss | 68 + ...t-content-drive-dropzone.component.spec.ts | 586 + .../dot-content-drive-dropzone.component.ts | 140 + .../dot-content-drive-sidebar.component.html | 17 + .../dot-content-drive-sidebar.component.scss | 29 + ...ot-content-drive-sidebar.component.spec.ts | 934 + .../dot-content-drive-sidebar.component.ts | 166 + ...nt-drive-base-type-selector.component.html | 5 +- ...drive-base-type-selector.component.spec.ts | 31 +- ...tent-drive-base-type-selector.component.ts | 20 +- ...nt-drive-content-type-field.component.html | 38 +- ...nt-drive-content-type-field.component.scss | 16 + ...drive-content-type-field.component.spec.ts | 708 +- ...tent-drive-content-type-field.component.ts | 332 +- ...ontent-drive-language-field.component.html | 18 + ...ntent-drive-language-field.component.scss} | 0 ...-content-drive-language-field.component.ts | 66 + .../dot-content-drive-language-field.spec.ts | 142 + ...-content-drive-search-input.component.html | 6 + ...ntent-drive-search-input.component.spec.ts | 56 +- ...ot-content-drive-search-input.component.ts | 9 +- ...tent-drive-workflow-actions.component.html | 10 + ...ent-drive-workflow-actions.component.scss} | 2 +- ...t-drive-workflow-actions.component.spec.ts | 801 + ...ontent-drive-workflow-actions.component.ts | 220 + .../dot-content-drive-toolbar.component.html | 47 +- .../dot-content-drive-toolbar.component.scss | 70 +- ...ot-content-drive-toolbar.component.spec.ts | 86 +- .../dot-content-drive-toolbar.component.ts | 162 +- ...ot-folder-list-context-menu.component.html | 1 + ...ot-folder-list-context-menu.component.scss | 3 + ...folder-list-context-menu.component.spec.ts | 964 ++ .../dot-folder-list-context-menu.component.ts | 356 + .../dot-content-drive-shell.component.html | 106 +- .../dot-content-drive-shell.component.scss | 32 +- .../dot-content-drive-shell.component.spec.ts | 1787 +- .../dot-content-drive-shell.component.ts | 469 +- .../portlet/src/lib/shared/constants.ts | 42 + .../portlet/src/lib/shared/mocks.ts | 38 +- .../portlet/src/lib/shared/models.ts | 43 +- ...t-content-drive-navigation.service.spec.ts | 297 + .../dot-content-drive-navigation.service.ts | 79 + .../portlet/src/lib/shared/services/index.ts | 1 + .../lib/store/dot-content-drive.store.spec.ts | 467 +- .../src/lib/store/dot-content-drive.store.ts | 183 +- .../context-menu/withContextMenu.spec.ts | 306 + .../features/context-menu/withContextMenu.ts | 38 + .../store/features/dialog/withDialog.spec.ts | 68 + .../lib/store/features/dialog/withDialog.ts | 26 + .../features/dragging/withDragging.spec.ts | 296 + .../store/features/dragging/withDragging.ts | 67 + .../features/sidebar/withSidebar.spec.ts | 395 + .../lib/store/features/sidebar/withSidebar.ts | 126 + .../portlet/src/lib/utils/functions.spec.ts | 916 +- .../portlet/src/lib/utils/functions.ts | 230 +- .../src/lib/utils/tree-folder.utils.spec.ts | 729 + .../src/lib/utils/tree-folder.utils.ts | 121 + .../src/lib/utils/workflow-actions.spec.ts | 1248 ++ .../portlet/src/lib/utils/workflow-actions.ts | 285 + .../portlet/src/test-setup.ts | 6 + .../dot-content-drive/ui/src/index.ts | 3 + .../dot-folder-list-view.component.html | 109 +- .../dot-folder-list-view.component.scss | 170 +- .../dot-folder-list-view.component.spec.ts | 968 +- .../dot-folder-list-view.component.ts | 347 +- .../dot-tree-folder.component.html | 49 + .../dot-tree-folder.component.scss | 125 + .../dot-tree-folder.component.spec.ts | 830 + .../dot-tree-folder.component.ts | 215 + .../ui/src/lib/shared/constants.ts | 41 +- .../ui/src/lib/shared/models.ts | 39 + ...s-configuration-goal-select.component.html | 10 +- ...riments-configuration-goals.component.html | 168 +- ...ents-configuration-goals.component.spec.ts | 10 + ...periments-configuration-goals.component.ts | 4 +- ...nts-configuration-items-count.component.ts | 3 +- ...onfiguration-scheduling-add.component.html | 5 +- ...ts-configuration-scheduling.component.html | 57 +- ...ents-configuration-scheduling.component.ts | 4 +- ...ments-configuration-targeting.component.ts | 1 - ...tion-traffic-allocation-add.component.html | 5 +- ...iguration-traffic-split-add.component.html | 71 +- ...ments-configuration-traffic.component.html | 5 +- ...riments-configuration-traffic.component.ts | 4 +- ...-configuration-variants-add.component.html | 6 +- ...ents-configuration-variants.component.html | 97 +- ...iments-configuration-variants.component.ts | 9 +- ...t-experiments-configuration.component.html | 25 +- ...xperiments-configuration.component.spec.ts | 10 +- ...dot-experiments-configuration.component.ts | 3 +- .../dot-experiments-create.component.html | 8 +- .../dot-experiments-list-table.component.html | 14 +- .../dot-experiments-list-table.component.ts | 6 +- .../dot-experiments-list.component.html | 25 +- .../dot-experiments-list.component.ts | 4 +- ...eriments-experiment-summary.component.html | 71 +- ...iments-report-daily-details.component.html | 46 +- ...eriments-report-daily-details.component.ts | 3 +- ...t-experiments-reports-chart.component.html | 34 +- ...dot-experiments-reports-chart.component.ts | 3 +- .../dot-experiments-reports.component.html | 16 +- .../dot-experiments-reports.component.ts | 3 +- ...periments-option-content-base.component.ts | 1 - .../dot-experiment-options.component.html | 40 +- ...t-experiments-details-table.component.html | 127 +- ...dot-experiments-details-table.component.ts | 4 +- ...al-configuration-reach-page.component.html | 129 +- ...goal-configuration-reach-page.component.ts | 2 - ...ion-url-parameter-component.component.html | 140 +- ...ation-url-parameter-component.component.ts | 2 - ...experiments-goals-coming-soon.component.ts | 3 +- .../dot-experiments-ui-header.component.html | 14 +- .../dot-experiments-ui-header.component.ts | 6 +- ...xperiments-inline-edit-text.component.html | 33 +- ...-experiments-inline-edit-text.component.ts | 2 - .../dot-locales-list.component.spec.ts | 44 +- .../DotLocaleConfirmationDialog.component.ts | 3 +- .../libs/portlets/dot-usage/.eslintrc.json | 24 + core-web/libs/portlets/dot-usage/README.md | 7 + .../libs/portlets/dot-usage/jest.config.ts | 22 + core-web/libs/portlets/dot-usage/project.json | 20 + core-web/libs/portlets/dot-usage/src/index.ts | 3 + .../libs/portlets/dot-usage/src/lib.routes.ts | 14 + .../dot-usage-shell.component.html | 346 + .../dot-usage-shell.component.scss | 288 + .../dot-usage-shell.component.spec.ts | 159 + .../dot-usage-shell.component.ts | 69 + .../lib/services/dot-usage.service.spec.ts | 245 + .../src/lib/services/dot-usage.service.ts | 170 + .../libs/portlets/dot-usage/src/test-setup.ts | 25 + .../libs/portlets/dot-usage/tsconfig.json | 27 + .../libs/portlets/dot-usage/tsconfig.lib.json | 12 + .../portlets/dot-usage/tsconfig.spec.json | 11 + .../dot-block-editor-sidebar.component.ts | 2 - .../dot-ema-dialog.component.spec.ts | 56 +- .../dot-ema-dialog.component.ts | 33 +- .../edit-ema-navigation-bar.component.html | 10 - .../edit-ema-navigation-bar.component.scss | 3 +- .../edit-ema-navigation-bar.component.spec.ts | 66 +- .../edit-ema-navigation-bar.component.ts | 4 - .../dot-ema-shell.component.html | 37 + .../dot-ema-shell.component.scss | 32 + .../dot-ema-shell.component.spec.ts | 34 +- .../dot-ema-shell/dot-ema-shell.component.ts | 61 +- .../dot-uve-contentlet-tools.component.html | 69 + .../dot-uve-contentlet-tools.component.scss} | 49 +- ...dot-uve-contentlet-tools.component.spec.ts | 676 + .../dot-uve-contentlet-tools.component.ts | 241 + .../dot-uve-lock-overlay.component.html | 9 + .../dot-uve-lock-overlay.component.scss | 48 + .../dot-uve-lock-overlay.component.spec.ts | 189 + .../dot-uve-lock-overlay.component.ts | 39 + .../dot-favorite-selector.component.html | 30 + .../dot-favorite-selector.component.scss | 18 + .../dot-favorite-selector.component.spec.ts | 467 + .../dot-favorite-selector.component.ts | 182 + .../dot-uve-palette-contentlet.component.html | 17 + .../dot-uve-palette-contentlet.component.scss | 107 + ...t-uve-palette-contentlet.component.spec.ts | 172 + .../dot-uve-palette-contentlet.component.ts | 41 + ...dot-uve-palette-contenttype.component.html | 26 + ...dot-uve-palette-contenttype.component.scss | 135 + ...-uve-palette-contenttype.component.spec.ts | 309 + .../dot-uve-palette-contenttype.component.ts | 51 + .../dot-uve-palette-list.component.html | 127 + .../dot-uve-palette-list.component.scss | 150 + .../dot-uve-palette-list.component.spec.ts | 635 + .../dot-uve-palette-list.component.ts | 423 + .../dot-uve-palette-list/store/store.spec.ts | 868 + .../dot-uve-palette-list/store/store.ts | 286 + .../dot-uve-palette.component.html | 72 + .../dot-uve-palette.component.scss | 61 + .../dot-uve-palette.component.spec.ts | 314 + .../dot-uve-palette.component.ts | 75 + .../components/dot-uve-palette/models.ts | 169 + .../components/dot-uve-palette/utils/index.ts | 394 + .../dot-uve-palette/utils/utils.spec.ts | 601 + .../assets/left_panel_close.svg | 1 + .../assets/left_panel_open.svg | 1 + ...dot-editor-mode-selector.component.spec.ts | 18 +- .../dot-editor-mode-selector.component.ts | 44 +- ...t-ema-running-experiment.component.spec.ts | 16 +- .../dot-toggle-lock-button.component.html | 30 + .../dot-toggle-lock-button.component.scss | 49 + .../dot-toggle-lock-button.component.spec.ts | 325 + .../dot-toggle-lock-button.component.ts | 55 + .../dot-uve-device-selector.component.spec.ts | 20 +- ...dot-uve-workflow-actions.component.spec.ts | 6 +- .../dot-uve-workflow-actions.component.ts | 2 +- .../dot-uve-toolbar.component.html | 57 +- .../dot-uve-toolbar.component.scss | 61 + .../dot-uve-toolbar.component.spec.ts | 439 +- .../dot-uve-toolbar.component.ts | 105 +- ...it-ema-palette-content-type.component.html | 50 - ...it-ema-palette-content-type.component.scss | 98 - ...ema-palette-content-type.component.spec.ts | 82 - ...edit-ema-palette-content-type.component.ts | 73 - ...dit-ema-palette-contentlets.component.html | 66 - ...dit-ema-palette-contentlets.component.scss | 97 - ...-ema-palette-contentlets.component.spec.ts | 98 - .../edit-ema-palette-contentlets.component.ts | 79 - .../edit-ema-palette.component.html | 20 - .../edit-ema-palette.component.scss | 7 - .../edit-ema-palette.component.spec.ts | 335 - .../edit-ema-palette.component.ts | 101 - .../store/edit-ema-palette.store.spec.ts | 259 - .../store/edit-ema-palette.store.ts | 302 - .../ema-contentlet-tools.component.html | 72 - .../ema-contentlet-tools.component.spec.ts | 458 - .../ema-contentlet-tools.component.ts | 251 - .../pipes/error/dot-error.pipe.ts | 3 +- .../pipes/position/dot-position.pipe.ts | 3 +- .../edit-ema-editor.component.html | 62 +- .../edit-ema-editor.component.scss | 32 +- .../edit-ema-editor.component.spec.ts | 392 +- .../edit-ema-editor.component.ts | 236 +- .../edit-ema-layout.component.spec.ts | 11 +- .../edit-ema-layout.component.ts | 5 +- .../lib/services/dot-page-api.service.spec.ts | 19 + .../src/lib/services/dot-page-api.service.ts | 46 +- .../edit-ema/portlet/src/lib/shared/consts.ts | 9 +- .../edit-ema/portlet/src/lib/shared/mocks.ts | 4 +- .../edit-ema/portlet/src/lib/shared/models.ts | 23 +- .../src/lib/store/dot-uve.store.spec.ts | 11 +- .../portlet/src/lib/store/dot-uve.store.ts | 77 +- .../store/features/client/withClient.spec.ts | 127 +- .../lib/store/features/client/withClient.ts | 5 +- .../src/lib/store/features/editor/models.ts | 62 +- .../editor/toolbar/withUVEToolbar.spec.ts | 6 +- .../features/editor/toolbar/withUVEToolbar.ts | 61 +- .../store/features/editor/withEditor.spec.ts | 421 +- .../lib/store/features/editor/withEditor.ts | 153 +- .../store/features/editor/withLock.spec.ts | 297 + .../src/lib/store/features/editor/withLock.ts | 134 + .../store/features/flags/withFlags.spec.ts | 11 - .../src/lib/store/features/flags/withFlags.ts | 25 +- .../store/features/layout/wihtLayout.spec.ts | 2 - .../lib/store/features/load/withLoad.spec.ts | 10 +- .../src/lib/store/features/load/withLoad.ts | 14 +- .../store/features/track/withTrack.spec.ts | 2 - .../src/lib/store/features/withPageContext.ts | 88 + .../features/workflow/withWorkflow.spec.ts | 2 - .../edit-ema/portlet/src/lib/store/models.ts | 2 - .../edit-ema/portlet/src/lib/utils/index.ts | 125 +- .../portlet/src/lib/utils/utils.spec.ts | 165 +- .../edit-ema/portlet/src/test-setup.ts | 5 + .../edit-ema/portlet/tsconfig.lib.json | 6 +- .../edit-ema/portlet/tsconfig.spec.json | 8 +- .../libs/portlets/edit-ema/ui/src/index.ts | 2 +- .../block-editor-mock.component.ts | 14 +- ...ontent-compare-block-editor.component.html | 21 +- ...ent-compare-block-editor.component.spec.ts | 75 +- ...-content-compare-block-editor.component.ts | 6 +- .../dot-content-compare-dialog.component.html | 2 +- ...t-content-compare-dialog.component.spec.ts | 48 +- .../dot-content-compare-dialog.component.ts | 6 +- .../dot-content-compare-table.component.html | 327 +- .../dot-content-compare-table.component.scss | 78 +- ...ot-content-compare-table.component.spec.ts | 12 +- .../dot-content-compare-table.component.ts | 27 +- ...ntent-compare-preview-field.component.html | 11 +- ...nt-compare-preview-field.component.spec.ts | 3 +- ...content-compare-preview-field.component.ts | 4 +- .../dot-content-compare.component.html | 20 +- .../dot-content-compare.component.spec.ts | 5 +- .../dot-content-compare.component.ts | 7 +- .../dot-content-compare.module.ts | 51 - .../dot-device-selector-seo.component.ts | 1 + .../dot-favorite-page.component.html | 172 +- .../dot-page-tools-seo.component.html | 68 +- .../dot-page-tools-seo.component.ts | 4 +- .../dot-results-seo-tool.component.html | 166 +- .../dot-results-seo-tool.component.ts | 16 +- .../dot-select-seo-tool.component.html | 7 +- .../dot-select-seo-tool.component.ts | 4 +- .../dot-seo-image-preview.component.html | 16 +- .../dot-seo-image-preview.component.ts | 3 +- core-web/libs/sdk/analytics/README.md | 676 +- core-web/libs/sdk/analytics/package.json | 8 +- core-web/libs/sdk/analytics/project.json | 3 +- .../lib/core/dot-analytics.content.spec.ts | 351 + .../src/lib/core/dot-analytics.content.ts | 136 + .../click/dot-analytics.click-tracker.spec.ts | 593 + .../click/dot-analytics.click-tracker.ts | 261 + .../click/dot-analytics.click.plugin.ts | 87 + .../click/dot-analytics.click.utils.spec.ts | 488 + .../plugin/click/dot-analytics.click.utils.ts | 100 + .../dot-analytics.enricher.plugin.spec.ts | 270 + .../enricher/dot-analytics.enricher.plugin.ts | 81 + ...nalytics.identity.activity-tracker.spec.ts | 226 + ...ot-analytics.identity.activity-tracker.ts} | 79 +- .../identity/dot-analytics.identity.plugin.ts | 23 +- .../dot-analytics.identity.utils.spec.ts | 189 +- .../identity/dot-analytics.identity.utils.ts | 36 + .../dot-analytics.impression-tracker.spec.ts | 980 ++ .../dot-analytics.impression-tracker.ts | 515 + .../dot-analytics.impression.plugin.spec.ts | 418 + .../dot-analytics.impression.plugin.ts | 100 + .../dot-analytics.impression.utils.spec.ts | 404 + .../dot-analytics.impression.utils.ts | 71 + .../src/lib/core/plugin/impression/index.ts | 2 + .../core/plugin/main/dot-analytics.plugin.ts | 224 + .../constants/dot-analytics.constants.ts | 190 + .../src/lib/core/shared/constants/index.ts | 5 + .../lib/core/shared/dot-analytics.logger.ts | 156 + .../shared/http/dot-analytics.http.spec.ts | 282 + .../core/shared/http/dot-analytics.http.ts | 67 + .../src/lib/core/shared/models/data.model.ts | 152 + .../src/lib/core/shared/models/event.model.ts | 217 + .../src/lib/core/shared/models/index.ts | 15 + .../lib/core/shared/models/library.model.ts | 289 + .../lib/core/shared/models/request.model.ts | 42 + .../queue/dot-analytics.queue.utils.spec.ts | 649 + .../shared/queue/dot-analytics.queue.utils.ts | 187 + .../src/lib/core/shared/queue/index.ts | 1 + .../lib/core/shared/queue/queue-utils.d.ts | 23 + .../lib/core/shared/queue/router-utils.d.ts | 17 + .../shared/utils/dot-analytics.utils.spec.ts} | 401 +- .../core/shared/utils/dot-analytics.utils.ts | 782 + .../dot-content-analytics.spec.ts | 220 - .../lib/dotAnalytics/dot-content-analytics.ts | 71 - .../plugin/dot-analytics.plugin.ts | 137 - .../dot-analytics.enricher.plugin.spec.ts | 310 - .../enricher/dot-analytics.enricher.plugin.ts | 50 - .../identity/dot-analytics.identity.utils.ts | 107 - .../shared/dot-content-analytics.constants.ts | 53 - .../shared/dot-content-analytics.http.spec.ts | 462 - .../shared/dot-content-analytics.http.ts | 47 - .../shared/dot-content-analytics.model.ts | 390 - .../shared/dot-content-analytics.utils.ts | 518 - .../react/components/DotContentAnalytics.tsx | 45 + .../DotContentAnalyticsProvider.spec.tsx | 102 - .../DotContentAnalyticsProvider.tsx | 45 - .../DotContentAnalyticsContext.spec.tsx | 49 - .../contexts/DotContentAnalyticsContext.tsx | 16 - .../react/hook/useContentAnalytics.spec.tsx | 179 +- .../src/lib/react/hook/useContentAnalytics.ts | 148 +- .../lib/react/hook/useRouteTracker.spec.tsx | 93 +- .../src/lib/react/hook/useRouterTracker.ts | 62 +- .../analytics/src/lib/react/internal/index.ts | 2 +- .../src/lib/react/internal/utils.spec.ts | 91 + .../analytics/src/lib/react/internal/utils.ts | 36 + .../src/lib/react/internal/uve.utils.spec.ts | 96 - .../src/lib/react/internal/uve.utils.ts | 23 - .../sdk/analytics/src/lib/react/public-api.ts | 4 +- .../libs/sdk/analytics/src/lib/standalone.ts | 56 +- core-web/libs/sdk/analytics/vite.config.mts | 26 +- .../sdk/analytics/vite.standalone.config.mts | 4 +- core-web/libs/sdk/angular/CHANGELOG.md | 177 + core-web/libs/sdk/angular/README.md | 167 +- core-web/libs/sdk/angular/package.json | 2 +- .../dotcms-block-editor-renderer/README.md | 57 +- .../blocks/code.component.ts | 2 - .../blocks/dot-contentlet.component.spec.ts | 144 +- .../blocks/dot-contentlet.component.ts | 8 +- .../blocks/image.component.ts | 1 - .../blocks/list.component.ts | 3 - .../blocks/text.component.ts | 3 - .../blocks/unknown.component.ts | 1 - .../blocks/video.component.ts | 1 - ...ms-block-editor-renderer.component.spec.ts | 6 +- .../dotcms-block-editor-renderer.component.ts | 4 +- .../dotcms-block-editor-item.component.html | 5 +- .../item/dotcms-block-editor-item.spec.ts | 5 +- .../components/row/row.component.spec.ts | 1 + .../components/row/row.component.ts | 2 +- .../dotcms-layout-body.component.ts | 2 +- .../dotcms-show-when.directive.ts | 3 +- .../dotcms-client.provider.spec.ts | 185 +- .../dotcms-client/dotcms-client.provider.ts | 112 +- .../dotcms-image_loader.spec.ts | 54 +- .../dotcms-image_loader.ts | 10 +- core-web/libs/sdk/client/README.md | 846 +- core-web/libs/sdk/client/package.json | 4 +- .../client/adapters/fetch-http-client.spec.ts | 365 + .../lib/client/adapters/fetch-http-client.ts | 118 + .../sdk/client/src/lib/client/ai/ai-api.ts | 106 + .../src/lib/client/ai/search/search.spec.ts | 782 + .../client/src/lib/client/ai/search/search.ts | 198 + .../client/src/lib/client/ai/shared/const.ts | 19 + .../client/src/lib/client/ai/shared/types.ts | 28 + .../sdk/client/src/lib/client/base/README.md | 70 + .../client/src/lib/client/base/base-api.ts | 88 + .../sdk/client/src/lib/client/client.spec.ts | 80 +- .../libs/sdk/client/src/lib/client/client.ts | 25 +- .../builders/collection/collection.spec.ts | 520 +- .../content/builders/collection/collection.ts | 174 +- .../src/lib/client/content/content-api.ts | 31 +- .../src/lib/client/content/shared/const.ts | 8 +- .../src/lib/client/content/shared/types.ts | 23 +- .../lib/client/content/shared/utils.spec.ts | 274 + .../src/lib/client/content/shared/utils.ts | 63 +- .../client/navigation/navigation-api.spec.ts | 129 +- .../lib/client/navigation/navigation-api.ts | 61 +- .../src/lib/client/page/page-api.spec.ts | 500 +- .../client/src/lib/client/page/page-api.ts | 115 +- .../sdk/client/src/lib/client/page/utils.ts | 51 +- .../src/lib/utils/graphql/transforms.spec.ts | 8 +- .../src/lib/utils/graphql/transforms.ts | 12 +- .../client/src/lib/utils/params/utils.spec.ts | 315 + .../sdk/client/src/lib/utils/params/utils.ts | 40 + core-web/libs/sdk/client/tsconfig.lib.json | 2 +- core-web/libs/sdk/experiments/README.md | 180 +- .../src/lib/hooks/useExperimentVariant.ts | 13 + core-web/libs/sdk/experiments/vite.config.ts | 4 +- core-web/libs/sdk/react/CHANGELOG.md | 235 + core-web/libs/sdk/react/README.md | 150 +- core-web/libs/sdk/react/package.json | 4 +- core-web/libs/sdk/react/src/index.ts | 11 +- .../__test__/components/Contentlet.test.tsx | 58 +- .../DotCMSBlockEditorRenderer.test.tsx | 21 +- .../next/__test__/hook/useAISearch.test.tsx | 525 + .../hook/useStyleEditorSchemas.test.tsx | 245 + .../next/components/Contentlet/Contentlet.tsx | 38 +- .../DotCMSBlockEditorRenderer.tsx | 42 +- .../DotCMSBlockEditorRenderer/README.md | 180 +- .../components/BlockEditorBlock.tsx | 2 +- .../components/blocks/DotContent.tsx | 6 +- .../react/src/lib/next/hooks/useAISearch.ts | 114 + .../lib/next/hooks/useIsAnalyticsActive.ts | 76 + .../lib/next/hooks/useStyleEditorSchemas.ts | 14 + .../sdk/react/src/lib/next/shared/types.ts | 32 + core-web/libs/sdk/types/README.md | 322 +- core-web/libs/sdk/types/package.json | 2 +- core-web/libs/sdk/types/src/index.ts | 4 + core-web/libs/sdk/types/src/internal.ts | 3 + .../libs/sdk/types/src/lib/ai/internal.ts | 58 + core-web/libs/sdk/types/src/lib/ai/public.ts | 220 + .../libs/sdk/types/src/lib/client/public.ts | 304 +- .../block-editor-renderer/public.ts | 13 +- .../src/lib/components/generic/public.ts | 12 + .../libs/sdk/types/src/lib/content/public.ts | 45 + .../libs/sdk/types/src/lib/editor/internal.ts | 22 + .../libs/sdk/types/src/lib/editor/public.ts | 4 + core-web/libs/sdk/types/src/lib/nav/public.ts | 85 + .../libs/sdk/types/src/lib/page/public.ts | 95 +- core-web/libs/sdk/uve/README.md | 24 +- core-web/libs/sdk/uve/package.json | 2 +- core-web/libs/sdk/uve/src/index.ts | 4 + core-web/libs/sdk/uve/src/internal.ts | 1 + .../libs/sdk/uve/src/internal/constants.ts | 6 + .../libs/sdk/uve/src/lib/core/core.spec.ts | 53 +- .../libs/sdk/uve/src/lib/core/core.utils.ts | 64 +- .../libs/sdk/uve/src/lib/dom/dom.utils.ts | 27 +- .../libs/sdk/uve/src/lib/editor/internal.ts | 25 +- .../sdk/uve/src/lib/style-editor/internal.ts | 231 + .../uve/src/lib/style-editor/public.spec.ts | 641 + .../sdk/uve/src/lib/style-editor/public.ts | 404 + .../sdk/uve/src/lib/style-editor/types.ts | 690 + core-web/libs/sdk/uve/src/script/utils.ts | 11 +- core-web/libs/sdk/uve/tsconfig.lib.json | 2 +- core-web/libs/template-builder/src/index.ts | 2 +- .../add-widget/add-widget.component.ts | 1 - ...t-layout-properties-item.component.spec.ts | 9 +- .../dot-layout-properties-item.component.ts | 3 +- .../dot-layout-properties-item.module.ts | 12 - .../dot-layout-properties.component.spec.ts | 11 +- .../dot-layout-properties.component.ts | 19 +- .../dot-layout-properties.module.ts | 29 - ...-layout-property-sidebar.component.spec.ts | 7 +- .../dot-layout-property-sidebar.component.ts | 7 +- .../dot-layout-property-sidebar.module.ts | 17 - .../template-builder-actions.component.ts | 4 +- ...-builder-background-columns.component.html | 2 +- .../template-builder-box.component.html | 6 +- .../template-builder-box.component.ts | 1 + .../template-builder-components.module.ts | 40 - .../template-builder/models/models.ts | 3 +- .../store/template-builder.store.spec.ts | 211 +- .../store/template-builder.store.ts | 31 +- .../template-builder.component.html | 16 +- .../template-builder.component.spec.ts | 71 +- .../template-builder.component.stories.ts | 27 +- .../template-builder.component.ts | 106 +- .../utils/gridstack-utils.spec.ts | 101 +- .../template-builder/utils/gridstack-utils.ts | 84 +- .../template-builder/utils/mocks.ts | 56 +- .../src/lib/template-builder.module.ts | 32 - core-web/libs/ui/src/index.ts | 13 +- .../dot-add-to-bundle.component.ts | 4 +- .../dot-action-menu-button.component.ts | 2 +- .../dot-asset-card-list.component.ts | 8 +- .../dot-asset-search.component.ts | 3 +- ...t-binary-option-selector.component.spec.ts | 6 +- .../dot-key-value-ng.component.spec.ts | 4 +- ...ot-language-variable-selector.component.ts | 1 - .../dot-not-license.component.spec.ts | 4 +- ...-favorite-page-empty-skeleton.component.ts | 1 - .../dot-severity-icon.component.html | 14 + .../dot-severity-icon.component.spec.ts | 112 + .../dot-severity-icon.component.ts | 26 + ...ot-sidebar-accordion-tab.component.spec.ts | 157 + .../dot-sidebar-accordion-tab.component.ts | 78 + .../dot-sidebar-accordion.component.html | 34 + .../dot-sidebar-accordion.component.scss | 202 + .../dot-sidebar-accordion.component.spec.ts | 637 + .../dot-sidebar-accordion.component.ts | 285 + .../components/dot-sidebar-accordion/index.ts | 3 + .../dot-workflow-actions.component.ts | 3 +- .../dot-autofocus/dot-autofocus.directive.ts | 3 +- .../dot-avatar/dot-avatar.directive.ts | 3 +- .../lib/directives/dot-dropdown.directive.ts | 1 - .../lib/directives/dot-dynamic.directive.ts | 1 - .../dot-gravatar.directive.spec.ts | 235 +- .../dot-gravatar/dot-gravatar.directive.ts | 121 +- .../lib/directives/dot-sidebar.directive.ts | 1 - .../dot-state-restore.directive.ts | 3 +- .../dot-string-template-outlet.directive.ts | 1 - .../dot-trim-input.directive.ts | 3 +- .../dot-container-options.directive.spec.ts | 3 +- .../dot-container-options.directive.ts | 3 +- .../dot-contentlet-status.pipe.ts | 1 - .../dot-field-required.directive.ts | 3 +- .../lib/dot-icon/dot-icon.component.spec.ts | 2 +- .../ui/src/lib/dot-icon/dot-icon.component.ts | 3 +- .../ui/src/lib/dot-icon/dot-icon.module.ts | 11 - .../src/lib/dot-message/dot-message.pipe.ts | 1 - .../dot-remove-confirm-popup.directive.ts | 3 +- .../dot-site-selector.directive.ts | 3 +- .../dot-spinner/dot-spinner.component.spec.ts | 4 +- .../lib/dot-spinner/dot-spinner.component.ts | 3 +- .../src/lib/dot-spinner/dot-spinner.module.ts | 12 - .../dot-dialog/dot-dialog.component.spec.ts | 8 +- .../dot-dialog/dot-dialog.component.ts | 5 +- .../modules/dot-dialog/dot-dialog.module.ts | 14 - .../src/lib/pipes/dot-diff/dot-diff.pipe.ts | 3 +- .../dot-file-size-format.pipe.ts | 3 +- .../dot-folder-name.pipe.spec.ts | 96 + .../dot-folder-name/dot-folder-name.pipe.ts | 18 + .../pipes/dot-highlight/dot-highlight.pipe.ts | 3 +- .../pipes/dot-iso-code/dot-iso-code.pipe.ts | 3 +- .../dot-locale-tag.pipe.spec.ts | 94 + .../dot-locale-tag/dot-locale-tag.pipe.ts | 35 + .../dot-relative-date.pipe.ts | 2 +- .../pipes/dot-safe-html/dot-safe-html.pipe.ts | 2 +- .../dot-string-format.pipe.ts | 3 +- .../dot-timestamp-to-date.pipe.ts | 1 - .../src/lib/pipes/safe-url/safe-url.pipe.ts | 3 +- .../services/clipboard/ClipboardUtil.spec.ts | 5 +- core-web/libs/ui/src/test-setup.ts | 4 +- core-web/libs/utils-testing/src/index.ts | 3 + .../src/lib/core-web.service.mock.ts | 5 + .../src/lib/dot-containers.service.mock.ts | 2 + .../src/lib/dot-content-types.mock.ts | 76 +- .../src/lib/dot-contentlet.mock.ts | 109 + .../utils-testing/src/lib/dot-device.mock.ts | 2 + .../utils-testing/src/lib/dot-folder.mock.ts | 13 + .../dot-http-error-manager.service.mock.ts | 4 +- .../src/lib/dot-message-mock.pipe.ts | 2 +- .../src/lib/dot-router-service.mock.ts | 58 +- .../utils-testing/src/lib/dot-site.mock.ts | 8 +- .../src/lib/dot-workflows-actions.mock.ts | 86 + .../utils-testing/src/lib/fake-event.mock.ts | 120 + .../src/lib/monaco-editor.mock.ts | 27 +- .../src/lib/site-service.mock.ts | 27 +- .../src/lib/test-setup-helpers.ts | 120 + core-web/libs/utils/src/lib/dot-utils.spec.ts | 192 +- core-web/libs/utils/src/lib/dot-utils.ts | 59 + .../libs/utils/src/lib/shared/FieldUtil.ts | 16 +- core-web/libs/utils/src/lib/shared/const.ts | 2 + core-web/libs/utils/tsconfig.lib.json | 2 +- core-web/migrations.json | 48 +- core-web/package.json | 45 +- core-web/pom.xml | 77 +- core-web/tsconfig.base.json | 3 +- core-web/yarn.lock | 14204 ++++++++-------- .../analytics/README.md | 294 + .../analytics/docker-compose.yml | 180 +- .../analytics/get-token.sh | 206 + .../analytics/setup/config/dev/cube/cube.js | 212 +- .../dev/cube/schema/ContentAttribution.js | 130 + .../config/dev/cube/schema/Conversion.js | 133 + .../config/dev/cube/schema/EventSummary.js | 131 + .../setup/config/dev/cube/schema/Events.js | 248 +- .../setup/config/dev/cube/schema/Request.js | 434 + .../setup/config/dev/keycloak/test-realm.json | 4 +- .../setup/db/clickhouse/init-scripts/init.sql | 555 +- .../analytics/start-analytics.sh | 112 + .../cluster-mode/docker-compose-node-1.yml | 2 +- .../cluster-mode/docker-compose-node-2.yml | 2 +- .../push-publish/docker-compose.yml | 4 +- .../single-node-debug-mode/docker-compose.yml | 2 +- .../single-node-demo-site/docker-compose.yml | 2 +- .../single-node/docker-compose.yml | 2 +- .../docker-compose.yml | 2 +- .../docker-compose-node-1.yml | 2 +- .../with-redis/docker-compose-node-1.yml | 2 +- .../with-redis/docker-compose-node-2.yml | 2 +- docker/java-base/Dockerfile | 10 +- .../k8s-aws-eks-manifest-examples/README.md | 15 +- docker/k8s-aws-eks-manifest-examples/alb.yaml | 51 +- .../statefulset.yaml | 30 +- docs/backend/TELEMETRY_IMPLEMENTATION.md | 301 + docs/frontend/ANGULAR_STANDARDS.md | 11 +- dotCMS/pom.xml | 5 + .../cache/provider/CacheProviderAPIImpl.java | 10 +- .../publishing/PublishDateUpdater.java | 184 +- .../remote/handler/CategoryFullHandler.java | 3 +- .../remote/handler/ContentHandler.java | 57 +- .../velocity/CacheBlockDirective.java | 3 +- .../velocity/CacheBlockDirective2.java | 3 +- .../srv/OVERRIDE/WEB-INF/log4j/log4j2.xml | 11 +- .../java/com/dotcms/ai/api/AIVisionAPI.java | 70 + .../ai/api/OpenAITranslationService.java | 466 + .../dotcms/ai/api/OpenAIVisionAPIImpl.java | 477 + .../ai/listener/AIVisionInitializer.java | 18 + .../OpenAIImageTaggingContentListener.java | 140 + .../main/java/com/dotcms/ai/util/AIUtil.java | 78 + .../ai/workflow/DotEmbeddingsActionlet.java | 2 +- .../ai/workflow/OpenAIAutoTagActionlet.java | 2 +- .../OpenAIContentPromptActionlet.java | 2 +- .../OpenAIGenerateImageActionlet.java | 2 +- .../workflow/OpenAITranslationActionlet.java | 231 + .../OpenAIVisionAutoTagActionlet.java | 79 + .../dotcms/analytics/app/AnalyticsApp.java | 4 +- .../attributes/CustomAttributeAPI.java | 93 + .../attributes/CustomAttributeAPIImpl.java | 203 +- .../CustomAttributeProcessingException.java | 19 + .../analytics/init/AnalyticsInitializer.java | 168 +- .../dotcms/analytics/metrics/EventType.java | 5 +- .../analytics/model/AnalyticsAppProperty.java | 8 +- .../track/AnalyticsTrackWebInterceptor.java | 29 +- .../analytics/web/AnalyticsWebAPIImpl.java | 28 +- .../providers/jwt/factories/ApiTokenAPI.java | 25 + .../providers/saml/v1/DotSamlResource.java | 10 + .../java/com/dotcms/browser/BrowserAPI.java | 35 + .../com/dotcms/browser/BrowserAPIImpl.java | 1450 +- .../java/com/dotcms/browser/BrowserQuery.java | 222 +- .../dotcms/business/APILocatorProducers.java | 20 + .../java/com/dotcms/cache/CacheValue.java | 17 +- .../java/com/dotcms/cache/CacheValueImpl.java | 36 + .../com/dotcms/cache/DynamicTTLCache.java | 26 +- .../ajax/ThreadMonitorTool.java | 197 +- .../config/DotInitializationService.java | 3 + .../business/ContentletIndexAPIImpl.java | 20 + .../business/DropOldContentletRunner.java | 241 + .../business/ESContentFactoryImpl.java | 213 +- .../business/ESContentletAPIImpl.java | 259 +- .../business/ESMappingAPIImpl.java | 157 +- .../business/IndiciesAPIImpl.java | 21 + .../business/SearchCriteria.java | 36 +- .../constants/ESMappingConstants.java | 7 +- .../contenttype/business/ContentTypeAPI.java | 148 +- .../business/ContentTypeAPIImpl.java | 269 +- .../business/ContentTypeFactory.java | 28 + .../business/ContentTypeFactoryImpl.java | 423 +- .../ContentTypeFieldLayoutAPIImpl.java | 172 +- .../business/DotAssetValidationException.java | 61 + .../dotcms/contenttype/business/FieldAPI.java | 86 +- .../contenttype/business/FieldAPIImpl.java | 138 +- .../business/StoryBlockAPIImpl.java | 67 +- .../UniqueFieldsValidationInitializer.java | 27 +- .../uniquefields/extratable/SqlQueries.java | 17 +- .../extratable/UniqueFieldDataBaseUtil.java | 140 +- .../contenttype/model/field/CustomField.java | 23 +- .../model/type/BaseContentType.java | 18 + .../contenttype/model/type/ContentType.java | 12 +- .../transform/field/JsonFieldTransformer.java | 110 +- .../util/ContentTypeFieldNames.java | 198 + .../contenttype/util/StoryBlockUtil.java | 115 + .../dotcms/featureflag/FeatureFlagName.java | 4 + .../business/PageAPIGraphQLTypesProvider.java | 3 + .../datafetcher/ContentMapDataFetcher.java | 16 +- .../AbstractIntegrityChecker.java | 10 +- .../dotcms/jitsu/AnalyticsEventsPayload.java | 3 +- .../com/dotcms/jitsu/EventLogRunnable.java | 61 +- .../java/com/dotcms/jitsu/EventsPayload.java | 3 +- .../dotcms/jitsu/ExperimentEventsPayload.java | 3 +- .../jitsu/ValidAnalyticsEventPayload.java | 4 +- ...ValidAnalyticsEventPayloadTransformer.java | 29 +- .../AnalyticsValidatorProcessor.java | 8 +- .../jitsu/validators/NumberTypeValidator.java | 42 + ...yValidator.java => SiteAuthValidator.java} | 28 +- .../jitsu/validators/ValidationErrorCode.java | 13 +- .../LegacyLangVarMigrationHelper.java | 47 +- .../PushedAssetHistoryTransformer.java | 86 + .../assets/business/PushedAssetsAPI.java | 29 +- .../assets/business/PushedAssetsAPIImpl.java | 19 + .../assets/business/PushedAssetsFactory.java | 44 +- .../business/PushedAssetsFactoryImpl.java | 95 +- .../publisher/business/PublishAuditAPI.java | 4 + .../business/PublishAuditAPIImpl.java | 46 +- .../publisher/business/PublisherQueueJob.java | 163 +- .../ajax/PublishingEndpointAjaxAction.java | 21 + .../dotcms/publisher/util/PublisherUtil.java | 20 +- .../velocity/directive/DotCacheDirective.java | 3 +- .../velocity/directive/DotParse.java | 4 +- .../velocity/services/ContainerLoader.java | 7 +- .../velocity/services/PageRenderUtil.java | 17 +- .../velocity/servlet/VelocityLiveMode.java | 365 +- .../rendering/velocity/util/VelocityUtil.java | 9 +- .../velocity/viewtools/FolderWebAPI.java | 4 + .../velocity/viewtools/StructuresWebAPI.java | 71 +- .../viewtools/content/ContentMap.java | 22 +- .../viewtools/content/ContentTool.java | 57 + .../viewtools/dotcache/DotCacheTool.java | 3 +- .../dotcms/rest/AuditPublishingResource.java | 28 +- .../java/com/dotcms/rest/ContentHelper.java | 12 + .../com/dotcms/rest/IntegrityResource.java | 60 +- .../rest/ResponseEntityRestTagListView.java | 18 + .../rest/ResponseEntityTagOperationView.java | 4 +- .../java/com/dotcms/rest/WebResource.java | 6 +- .../content/ContentAnalyticsResource.java | 183 +- .../content/util/ContentAnalyticsUtil.java | 47 +- .../dotcms/rest/api/v1/apps/AppsHelper.java | 9 +- .../AbstractAssetArchiveRequestForm.java | 25 + .../AbstractAssetDeletionRequestForm.java | 25 + .../asset/AbstractAssetInfoRequestForm.java | 8 + .../asset/AbstractAssetLookupRequestForm.java | 25 + .../v1/asset/AbstractAssetsRequestForm.java | 18 + .../AbstractFolderDeletionRequestForm.java | 25 + .../api/v1/asset/AbstractFolderDetail.java | 64 + .../rest/api/v1/asset/AbstractFolderForm.java | 20 + .../api/v1/asset/AbstractNewFolderForm.java | 15 + .../asset/AbstractResolvedAssetAndPath.java | 7 +- .../v1/asset/AbstractUpdateFolderDetail.java | 31 + .../v1/asset/AbstractUpdateFolderForm.java | 15 + .../rest/api/v1/asset/AssetPathResolver.java | 37 +- .../rest/api/v1/asset/FileUploadData.java | 5 +- .../rest/api/v1/asset/WebAssetHelper.java | 249 +- .../rest/api/v1/asset/WebAssetResource.java | 273 +- .../v1/authentication/ApiTokenResource.java | 6 +- .../api/v1/authentication/RequestUtil.java | 18 + .../content/ContentPushHistoryPaginator.java | 64 + .../rest/api/v1/content/ContentResource.java | 100 +- .../v1/content/ContentVersionResource.java | 23 +- .../api/v1/content/PushedAssetHistory.java | 67 + .../api/v1/content/SiteContentReport.java | 2 +- .../v1/contenttype/BaseContentTypesView.java | 12 +- .../api/v1/contenttype/ContentTypeHelper.java | 158 +- .../v1/contenttype/ContentTypeResource.java | 483 +- .../v1/drive/AbstractDriveRequestForm.java | 344 + .../api/v1/drive/AbstractQueryFilters.java | 29 + .../rest/api/v1/drive/ContentDriveHelper.java | 257 + .../api/v1/drive/ContentDriveResource.java | 100 + .../rest/api/v1/folder/FolderHelper.java | 15 +- .../rest/api/v1/folder/FolderResource.java | 61 +- .../rest/api/v1/job/JobParamsSchema.java | 46 + .../dotcms/rest/api/v1/job/JobQueueDocs.java | 17 + .../rest/api/v1/job/JobQueueResource.java | 554 +- .../dotcms/rest/api/v1/menu/MenuHelper.java | 21 +- .../rest/api/v1/page/CopyContentletForm.java | 18 +- .../rest/api/v1/page/PageResourceHelper.java | 10 +- .../api/v1/system/ConfigurationResource.java | 4 +- .../AbstractPermissionMetadataView.java | 51 + .../AbstractUserPermissionAssetView.java | 133 + .../system/permission/PermissionResource.java | 233 +- .../permission/PermissionSaveHelper.java | 427 + .../v1/system/permission/PermissionUtils.java | 74 + .../ResponseEntityPermissionMetadataView.java | 21 + ...ResponseEntitySaveUserPermissionsView.java | 21 + .../permission/SaveUserPermissionsForm.java | 109 + .../permission/SaveUserPermissionsView.java | 85 + .../dotcms/rest/api/v1/temp/TempFileAPI.java | 8 +- .../usage/ResponseEntityUsageSummaryView.java | 13 + .../rest/api/v1/usage/UsageResource.java | 240 + .../rest/api/v1/usage/UsageSummary.java | 262 + .../ResponseEntityUserPermissionsView.java | 14 + .../com/dotcms/rest/api/v1/user/UserForm.java | 3 - .../dotcms/rest/api/v1/user/UserHelper.java | 1 + .../api/v1/user/UserPermissionHelper.java | 262 + .../dotcms/rest/api/v1/user/UserResource.java | 262 +- .../rest/api/v1/user/UserResourceHelper.java | 37 + .../ResponseEntityWorkflowTasksView.java | 15 + .../api/v1/workflow/WorkflowResource.java | 119 +- .../workflow/WorkflowSearchResultsView.java | 26 + .../api/v1/workflow/WorkflowSearcherForm.java | 199 + .../api/v1/workflow/WorkflowTaskView.java | 85 + .../v1/workflow/WorkflowTimelineItemView.java | 18 +- .../v2/tags/TagForm.java} | 37 +- .../dotcms/rest/api/v2/tags/TagResource.java | 737 +- .../rest/api/v2/tags/TagValidationHelper.java | 42 + .../rest/api/v2/tags/UpdateTagForm.java | 93 + .../api/v3/contenttype/FieldResource.java | 392 +- .../api/v3/contenttype/MoveFieldsForm.java | 50 +- .../rest/exception/ConflictException.java | 21 + .../mapper/DotConflictExceptionMapper.java | 19 + .../dotcms/rest/tag/TagsResourceHelper.java | 360 +- .../com/dotcms/security/apps/AppsAPIImpl.java | 50 +- .../dotcms/security/apps/AppsCacheImpl.java | 19 +- .../apps/SecretCachedKeyStoreImpl.java | 32 +- .../IllegalFileExtensionsValidator.java | 5 +- .../IllegalTraversalFilePathValidator.java | 21 +- .../com/dotcms/telemetry/DashboardMetric.java | 55 + .../telemetry/business/MetricsAPIImpl.java | 8 +- .../collectors/DashboardMetricsProvider.java | 201 + .../collectors/MetricStatsCollector.java | 326 +- .../ai/TotalEmbeddingsIndexesMetricType.java | 2 + .../ai/TotalSitesUsingDotaiMetricType.java | 2 + ...sWithAutoIndexContentConfigMetricType.java | 2 + ...ontainersInLivePageDatabaseMetricType.java | 2 + ...ainersInWorkingPageDatabaseMetricType.java | 2 + ...ontainersInLivePageDatabaseMetricType.java | 2 + ...ainersInWorkingPageDatabaseMetricType.java | 2 + ...portContentletsJobTriggeredMetricType.java | 34 + .../LastContentEditedDatabaseMetricType.java | 5 + ...ultLanguageContentsDatabaseMetricType.java | 2 + ...centlyEditedContentDatabaseMetricType.java | 5 + .../TotalContentsDatabaseMetricType.java | 4 + ...ultLanguageContentsDatabaseMetricType.java | 2 + .../CountOfBinaryFieldsMetricType.java | 2 + .../CountOfBlockEditorFieldsMetricType.java | 2 + .../CountOfCategoryFieldsMetricType.java | 2 + .../CountOfCheckboxFieldsMetricType.java | 2 + .../CountOfColumnsFieldsMetricType.java | 2 + .../CountOfConstantFieldsMetricType.java | 2 + .../CountOfDateFieldsMetricType.java | 2 + .../CountOfDateTimeFieldsMetricType.java | 2 + .../CountOfFileFieldsMetricType.java | 2 + .../CountOfHiddenFieldsMetricType.java | 2 + .../CountOfImageFieldsMetricType.java | 2 + .../CountOfJSONFieldsMetricType.java | 2 + .../CountOfKeyValueFieldsMetricType.java | 2 + .../CountOfLineDividersFieldsMetricType.java | 2 + .../CountOfMultiselectFieldsMetricType.java | 2 + .../CountOfPermissionsFieldsMetricType.java | 2 + .../CountOfRadioFieldsMetricType.java | 2 + .../CountOfRelationshipFieldsMetricType.java | 2 + .../CountOfRowsFieldsMetricType.java | 2 + .../CountOfSelectFieldsMetricType.java | 2 + .../CountOfSiteOrFolderFieldsMetricType.java | 2 + .../CountOfTabFieldsMetricType.java | 2 + .../CountOfTagFieldsMetricType.java | 2 + .../CountOfTextAreaFieldsMetricType.java | 2 + .../CountOfTextFieldsMetricType.java | 2 + .../CountOfTimeFieldsMetricType.java | 2 + .../CountOfWYSIWYGFieldsMetricType.java | 2 + ...imentsEditedInThePast30DaysMetricType.java | 2 + ...perimentsWithBounceRateGoalMetricType.java | 2 + ...ExperimentsWithExitRateGoalMetricType.java | 2 + ...xperimentsWithReachPageGoalMetricType.java | 2 + ...rimentsWithURLParameterGoalMetricType.java | 2 + ...agesWithAllEndedExperimentsMetricType.java | 2 + ...agesWithArchivedExperimentsMetricType.java | 2 + ...ntPagesWithDraftExperimentsMetricType.java | 2 + ...PagesWithRunningExperimentsMetricType.java | 2 + ...gesWithScheduledExperimentsMetricType.java | 2 + ...ntsInAllArchivedExperimentsMetricType.java | 2 + ...riantsInAllDraftExperimentsMetricType.java | 2 + ...riantsInAllEndedExperimentsMetricType.java | 2 + ...antsInAllRunningExperimentsMetricType.java | 2 + ...tsInAllScheduledExperimentsMetricType.java | 2 + .../ExperimentFeatureFlagMetricType.java | 42 - .../CountOfContentAssetImageBEAPICalls.java | 2 + .../CountOfContentAssetImageFEAPICalls.java | 2 + .../image/CountOfDAImageBEAPICalls.java | 2 + .../image/CountOfDAImageFEAPICalls.java | 2 + ...ngeDefaultLanguagesDatabaseMetricType.java | 2 + .../OldStyleLanguagesVarialeMetricType.java | 2 + .../TotalLanguagesDatabaseMetricType.java | 4 + ...eLanguagesVariablesDatabaseMetricType.java | 2 + ...otalUniqueLanguagesDatabaseMetricType.java | 2 + ...gLanguagesVariablesDatabaseMetricType.java | 2 + ...fLiveSitesWithSiteVariablesMetricType.java | 2 + ...esWithIndividualPermissionsMetricType.java | 2 + .../CountOfSitesWithThumbnailsMetricType.java | 2 + ...rkingSitesWithSiteVariablesMetricType.java | 2 + ...NoDefaultTagStorageDatabaseMetricType.java | 2 + ...sWithNoSystemFieldsDatabaseMetricType.java | 2 + ...tesWithRunDashboardDatabaseMetricType.java | 2 + .../TotalActiveSitesDatabaseMetricType.java | 4 + ...lAliasesActiveSitesDatabaseMetricType.java | 2 + ...otalAliasesAllSitesDatabaseMetricType.java | 4 + .../site/TotalSitesDatabaseMetricType.java | 4 + .../CountSiteSearchDocumentMetricType.java | 2 + .../CountSiteSearchIndicesMetricType.java | 2 + .../TotalSizeSiteSearchIndicesMetricType.java | 2 + ...alAdvancedTemplatesDatabaseMetricType.java | 2 + ...talBuilderTemplatesDatabaseMetricType.java | 4 + .../TotalTemplatesDatabaseMetricType.java | 4 + ...emplatesInLivePagesDatabaseMetricType.java | 2 + ...latesInWorkingPagesDatabaseMetricType.java | 2 + .../theme/TotalFilesInThemeMetricType.java | 2 + .../TotalLiveContainerDatabaseMetricType.java | 4 + .../TotalLiveFilesInThemeMetricType.java | 2 + .../TotalSizeOfFilesPerThemeMetricType.java | 2 + .../theme/TotalThemeMetricType.java | 2 + ...talThemeUsedInLiveTemplatesMetricType.java | 2 + ...ThemeUsedInWorkingTemplatesMetricType.java | 2 + ...talWorkingContainerDatabaseMetricType.java | 2 + ...tentTypesWithUrlMapDatabaseMetricType.java | 2 + ...LiveContentInUrlMapDatabaseMetricType.java | 2 + ...terWithTwoVariablesDatabaseMetricType.java | 2 + ...kingContentInUrlMapDatabaseMetricType.java | 2 + .../user/ActiveUsersDatabaseMetricType.java | 4 + .../user/LastLoginDatabaseMetricType.java | 4 + .../user/LastLoginUserDatabaseMetric.java | 3 + .../user/TotalUsersDatabaseMetricType.java | 40 + .../workflow/ActionsDatabaseMetricType.java | 2 + .../ContentTypesDatabaseMetricType.java | 4 + .../workflow/SchemesDatabaseMetricType.java | 4 + .../workflow/StepsDatabaseMetricType.java | 4 + .../SubActionsDatabaseMetricType.java | 2 + .../UniqueSubActionsDatabaseMetricType.java | 2 + .../dotcms/telemetry/job/MetricsStatsJob.java | 4 +- .../telemetry/rest/TelemetryResource.java | 13 +- .../com/dotcms/util/HttpRequestDataUtil.java | 39 +- .../java/com/dotcms/util/PaginationUtil.java | 2 +- .../java/com/dotcms/util/SecurityUtils.java | 100 +- .../util/pagination/ContainerPaginator.java | 12 +- .../pagination/ContentHistoryPaginator.java | 26 +- .../pagination/ContentTypesPaginator.java | 138 +- .../dotcms/util/pagination/TagsPaginator.java | 97 + .../workflow/helper/WorkflowHelper.java | 68 + .../java/com/dotmarketing/beans/Host.java | 3 +- .../com/dotmarketing/beans/MultiTree.java | 36 +- .../beans/transform/MultiTreeTransformer.java | 16 +- .../com/dotmarketing/business/APILocator.java | 15 +- .../business/BlockDirectiveCache.java | 3 +- .../business/BlockDirectiveCacheImpl.java | 34 +- .../business/BlockDirectiveCacheObject.java | 24 +- .../business/BlockPageCacheImpl.java | 132 - .../dotmarketing/business/CacheLocator.java | 10 +- .../business/PageCacheParameters.java | 149 +- .../dotmarketing/business/PermissionAPI.java | 124 + .../business/PermissionBitAPIImpl.java | 106 +- ...ockPageCache.java => StaticPageCache.java} | 2 +- .../business/StaticPageCacheImpl.java | 114 + .../dotmarketing/business/UserAPIImpl.java | 26 +- .../com/dotmarketing/business/UserHelper.java | 4 +- .../dotmarketing/business/ajax/RoleAjax.java | 16 +- .../cache/provider/CacheProvider.java | 3 + .../business/cache/provider/h22/H22Cache.java | 108 +- .../provider/h22/H22CacheCleanupThread.java | 34 +- .../business/portal/DotPortlet.java | 32 +- .../GenericMapFieldComparator.java | 362 + .../factories/MultiTreeAPIImpl.java | 45 +- .../factories/PersonalizedContentlet.java | 21 +- .../com/dotmarketing/loggers/Log4jUtil.java | 52 +- .../portlets/browser/BrowserUtil.java | 2 +- .../business/CategoryFactoryImpl.java | 33 +- .../factories/CMSMaintenanceFactory.java | 19 +- .../business/ContainerFactoryImpl.java | 119 +- .../business/ContentletFactory.java | 43 + .../DotContentletValidationException.java | 97 +- .../contentlet/business/HostAPIImpl.java | 10 +- .../contentlet/business/HostCacheImpl.java | 10 +- .../contentlet/business/HostFactoryImpl.java | 30 + .../DotFolderTransformerBuilder.java | 28 +- .../transform/DotFolderTransformerImpl.java | 92 +- .../transform/DotTransformerBuilder.java | 4 +- .../strategy/HistoryViewStrategy.java | 38 +- .../folders/business/FolderAPIImpl.java | 31 +- .../page/HTMLPageAssetRenderedBuilder.java | 41 + .../business/LanguageAPI.java | 11 + .../business/LanguageAPIImpl.java | 26 +- .../portlets/user/ajax/UserAjax.java | 4 +- .../actionlet/ResetApproversActionlet.java | 6 +- .../workflows/business/WorkflowAPIImpl.java | 129 +- .../business/WorkflowFactoryImpl.java | 2 +- .../workflows/model/WorkflowSearcher.java | 4 + .../quartz/job/DropOldContentVersionsJob.java | 14 +- .../quartz/job/ResetPermissionsJob.java | 7 +- ...sk250826AddIndexesToUniqueFieldsTable.java | 62 +- ...910AddAnalyticsDashboardPortletToMenu.java | 139 + ...eContentTypesLegacyPortletFromLayouts.java | 33 + ...03AddStylePropertiesColumnInMultiTree.java | 41 + .../com/dotmarketing/tag/business/TagAPI.java | 10 + .../dotmarketing/tag/business/TagAPIImpl.java | 9 + .../dotmarketing/tag/business/TagFactory.java | 11 + .../tag/business/TagFactoryImpl.java | 171 +- .../java/com/dotmarketing/util/Constants.java | 66 +- .../com/dotmarketing/util/ImportUtil.java | 18 +- .../java/com/dotmarketing/util/PortletID.java | 7 +- .../dotmarketing/util/TaskLocatorUtil.java | 7 +- .../com/dotmarketing/util/UtilMethods.java | 18 +- .../importer/ImportLineValidationCodes.java | 5 + .../dotmarketing/util/json/JSONObject.java | 6 + .../com/liferay/portal/model/Portlet.java | 34 +- .../main/java/com/liferay/util/MapUtil.java | 33 + .../apache/velocity/app/VelocityEngine.java | 66 +- .../resources/analytics/validators/all.json | 27 +- .../analytics/validators/content_click.json | 80 + .../validators/content_impression.json | 50 + .../analytics/validators/conversion.json | 27 + .../analytics/validators/pageview.json | 25 +- .../apps/dotContentAnalytics-config.yml | 10 +- dotCMS/src/main/resources/ca/ca-lib.js | 2 - .../resources/ca/html/analytics_head.html | 2 +- .../ai/prompts/default-vision-prompt.json | 21 + .../com/dotcms/ai/prompts/prompts.properties | 9 + .../resources/container/tomcat9/bin/setenv.sh | 17 +- .../container/tomcat9/conf/server.xml | 10 +- dotCMS/src/main/resources/db.properties | 53 +- .../resources/dotmarketing-config.properties | 15 + .../main/resources/es-content-mapping.json | 2 +- .../runtime/defaults/velocity.properties | 10 +- dotCMS/src/main/resources/portal.properties | 9 +- dotCMS/src/main/resources/postgres.sql | 1 + dotCMS/src/main/resources/sql/rag_schema.sql | 28 + .../WEB-INF/messages/Language.properties | 201 +- .../WEB-INF/messages/Language_de.properties | 5 + .../main/webapp/WEB-INF/openapi/openapi.yaml | 2546 ++- dotCMS/src/main/webapp/WEB-INF/portlet.xml | 255 +- dotCMS/src/main/webapp/WEB-INF/web.xml | 25 +- dotCMS/src/main/webapp/ext/uve/dot-uve.js | 2 +- .../main/webapp/html/common/bottom_inc.jsp | 27 +- .../webapp/html/css/dijit-dotcms/dotcms.css | 6 +- .../webapp/html/error/custom-error-page.jsp | 21 +- .../ext/browser/view_browser_js_inc.jsp | 1 - .../ext/cmsconfig/remotePublishing.jsp | 10 +- .../cmsmaintenance/view_cms_maintenance.jsp | 10 +- .../contentlet/edit_contentlet_references.jsp | 3 +- .../publishing/view_unpushed_bundles.jsp | 3 +- .../AnalyticsTrackWebInterceptorTest.java | 2 + .../analytics/web/AnalyticsWebAPITest.java | 84 +- .../architecture/CodingStandardsArchTest.java | 70 + .../jitsu/ValidAnalyticsEventPayloadTest.java | 19 +- .../com/dotcms/util/SecurityUtilsTest.java | 144 +- .../business/BlockDirectiveCacheImplTest.java | 11 +- .../business/PageCacheParametersTest.java | 307 + .../cache/provider/h22/H22CacheTest.java | 96 +- .../business/portal/DotPortletTest.java | 5 +- .../business/portal/PortletUrlTest.java | 298 + .../portal/SerializationHelperTest.java | 2 +- .../dotmarketing/util/UtilMethodsTest.java | 194 + .../java/com/liferay/util/MapUtilTest.java | 92 + dotFrontendOnboarding.md | 12 +- .../src/test/java/com/dotcms/MainSuite2b.java | 73 +- .../src/test/java/com/dotcms/MainSuite3a.java | 19 +- .../ai/api/OpenAIVisionAPIImplTest.java | 502 + .../dotcms/ai/viewtool/AIViewToolTest.java | 32 +- .../CustomAttributeAPIImplTest.java | 1359 ++ .../analytics/track/RequestMatcherTest.java | 2 + .../AsyncVanitiesCollectorTest.java | 2 + .../collectors/BasicProfileCollectorTest.java | 2 + .../FilesCollectorIntegrationTest.java | 2 + .../track/collectors/FilesCollectorTest.java | 2 + .../collectors/PageDetailCollectorTest.java | 2 + .../track/collectors/PagesCollectorTest.java | 2 + .../collectors/SyncVanitiesCollectorTest.java | 2 + .../WebEventsCollectorServiceImplTest.java | 2 + .../com/dotcms/browser/BrowserAPITest.java | 939 +- .../business/ESContentFactoryImplTest.java | 156 +- .../business/ESContentletAPIImplTest.java | 41 + ...BaseTypeToContentTypeStrategyImplTest.java | 4 +- .../business/StoryBlockAPITest.java | 316 +- .../business/StoryBlockValidationTest.java | 808 + .../UniqueFieldDataBaseUtilTest.java | 120 +- .../contenttype/test/ContentResourceTest.java | 6 +- .../test/ContentTypeResourceTest.java | 4 +- .../contenttype/test/ContentTypeTest.java | 29 + .../contenttype/test/StoryBlockUtilTest.java | 415 + .../com/dotcms/datagen/BundleDataGen.java | 9 +- .../com/dotcms/datagen/FolderDataGen.java | 14 + .../com/dotcms/datagen/TestDataUtils.java | 4 +- .../page/ContentMapDataFetcherTest.java | 103 +- .../dotcms/http/CircuitBreakerUrlTest.java | 3 +- .../AnalyticsValidatorUtilTest.java | 382 +- .../business/PushedAssetsFactoryTest.java | 289 + .../publisher/business/PublisherTest.java | 229 +- .../publishing/job/SiteSearchJobImplTest.java | 34 +- .../velocity/RecycledHttpServletRequest.java | 53 + .../services/HTMLPageAssetRenderedTest.java | 13 +- .../viewtools/content/ContentMapTest.java | 32 + .../viewtools/content/ContentToolTest.java | 26 + .../com/dotcms/rest/BundleResourceTest.java | 4 +- .../rest/api/v1/apps/AppsResourceTest.java | 30 + .../asset/WebAssetHelperIntegrationTest.java | 339 +- ...ContentVersionResourceIntegrationTest.java | 34 +- .../contenttype/ContentTypeResourceTest.java | 42 +- .../api/v1/contenttype/FieldResourceTest.java | 4 +- ...riveHelperContentletAPIComparisonTest.java | 557 + .../rest/api/v1/page/NavResourceTest.java | 4 +- .../rest/api/v1/page/PageResourceTest.java | 174 + .../PushPublishFilterResourceTest.java | 4 +- .../RelationshipsResourceTest.java | 4 +- .../PermissionResourceIntegrationTest.java | 532 + .../v1/user/UserResourceIntegrationTest.java | 237 +- .../WorkflowResourceIntegrationTest.java | 4 +- .../api/v2/contenttype/FieldResourceTest.java | 4 +- .../api/v3/contenttype/FieldResourceTest.java | 4 +- .../ESContentResourcePortletTest.java | 4 +- .../dotcms/security/apps/AppsAPIImplTest.java | 198 + .../apps/SecretsStoreKeyStoreImplTest.java | 100 +- .../multipart/SecureFileValidatorTest.java | 31 +- .../ExperimentFeatureFlagMetricTypeTest.java | 34 - .../java/com/dotcms/util/ImportUtilTest.java | 294 +- .../pagination/ContentTypesPaginatorTest.java | 213 +- .../business/PermissionAPITest.java | 45 +- .../SecondaryCategoryPermissionTest.java | 537 + .../cache/MultiTreeCacheTest.java | 46 +- .../factories/MultiTreeAPITest.java | 202 +- .../com/dotmarketing/filters/FiltersTest.java | 69 +- ...GenericBundleActivatorIntegrationTest.java | 47 +- .../business/ContainerFactoryImplTest.java | 6 +- .../business/ContentletAPITest.java | 10 +- .../contentlet/business/HostAPITest.java | 54 + ...geAssetRenderedAPIImplIntegrationTest.java | 109 +- .../ResetApproversActionletTest.java | 301 + .../quartz/DotStatefulJobTest.java | 16 +- .../dotmarketing/quartz/MyStatefulJob.java | 5 +- .../quartz/job/ResetPermissionsJobTest.java | 187 + .../servlets/BinaryExporterServletTest.java | 4 +- ...306MigrateLegacyLanguageVariablesTest.java | 353 + .../Task250604UpdateFolderInodesTest.java | 43 +- ...0826AddIndexesToUniqueFieldsTableTest.java | 4 + ...ddAnalyticsDashboardPortletToMenuTest.java | 104 + ...tentTypesLegacyPortletFromLayoutsTest.java | 232 + ...dStylePropertiesColumnInMultiTreeTest.java | 180 + .../portal/ejb/UserLocalManagerTest.java | 13 +- .../lang-vars/cms_language_en.properties | 5 +- .../lang-vars/cms_language_en_US.properties | 2 +- .../lang-vars/cms_language_es_ES.properties | 10 +- .../lang-vars/cms_language_fr.properties | 16 +- .../lang-vars/cms_language_fr_FR.properties | 16 +- ...ntentDriveResource.postman_collection.json | 1658 ++ .../ContentTypePages.postman_collection.json | 711 +- .../postman/ContentTypeResourceTests.json | 3854 +++-- .../Content_Analytics.postman_collection.json | 1321 +- .../Content_Resource.postman_collection.json | 20 +- ...t_Version_Resource.postman_collection.json | 140 +- .../FolderResource.postman_collection.json | 57 + .../main/resources/postman/GraphQLTests.json | 306 +- ...ueResourceAPITests.postman_collection.json | 4 +- .../resources/postman/PagesResourceTests.json | 4 + ...ermission_Resource.postman_collection.json | 101 + .../Tags_Resource_V2.postman_collection.json | 4048 ++++- .../UserResource.postman_collection.json | 993 +- .../postman/WebAssets.postman_collection.json | 719 + .../postman/Workflow_Resource_Tests.json | 446 +- .../postman/resources/csv_with_errors.csv | 6 + e2e/dotcms-e2e-node/frontend/package.json | 1 + .../src/pages/newEditContentForm.page.ts | 12 +- .../frontend/src/requests/contentType.ts | 86 + .../frontend/src/requests/schemas.ts | 34 + .../frontend/src/requests/sites.ts | 65 + .../frontend/src/requests/workflowActions.ts | 40 + .../src/tests/components/breadcrumb.spec.ts | 49 +- .../contentSearch/contentEditing.spec.ts | 60 +- .../frontend/src/tests/login/login.spec.ts | 3 +- .../siteOrFolder/select-default.spec.ts | 37 + .../fields/siteOrFolderField.spec.ts | 2 +- e2e/dotcms-e2e-node/pom.xml | 15 +- examples/angular-ssr/.editorconfig | 17 + examples/angular-ssr/.env.example | 15 + examples/angular-ssr/.gitignore | 47 + examples/angular-ssr/.postcssrc.json | 5 + examples/angular-ssr/CLAUDE.md | 126 + examples/angular-ssr/README.md | 147 + examples/angular-ssr/angular.json | 98 + examples/angular-ssr/api/index.js | 7 + examples/angular-ssr/package-lock.json | 10735 ++++++++++++ examples/angular-ssr/package.json | 64 + examples/angular-ssr/public/favicon.ico | Bin 0 -> 15086 bytes .../angular-ssr/src/app/app.config.server.ts | 12 + examples/angular-ssr/src/app/app.config.ts | 31 + .../angular-ssr/src/app/app.css | 0 examples/angular-ssr/src/app/app.html | 9 + .../angular-ssr/src/app/app.routes.server.ts | 8 + examples/angular-ssr/src/app/app.routes.ts | 28 + examples/angular-ssr/src/app/app.spec.ts | 23 + examples/angular-ssr/src/app/app.ts | 105 + .../edit-contentlet-button.component.ts | 39 + .../components/blogs/blogs.component.ts | 19 + .../destinations/destinations.component.ts | 21 + .../recommended-card.component.ts | 56 + .../components/footer/footer.component.css | 0 .../app/components/footer/footer.component.ts | 49 + .../components/header/header.component.css} | 0 .../app/components/header/header.component.ts | 25 + .../components/loading/loading.component.ts | 187 + .../navigation/navigation.component.css | 0 .../navigation/navigation.component.ts | 28 + .../reorder-button.component.ts | 74 + .../activity/activity.component.css | 0 .../components/activity/activity.component.ts | 44 + .../banner-carousel.component.html | 74 + .../banner-carousel.component.ts | 58 + .../components}/banner/banner.component.css | 0 .../components/banner/banner.component.ts | 47 + .../category-filter.component.ts | 40 + .../components}/image/image.component.css | 0 .../components/image/image.component.ts | 32 + .../contact-us/contact-us.component.html | 99 + .../contact-us/contact-us.component.ts | 45 + .../page-form/page-form.component.ts | 31 + .../components}/product/product.component.css | 0 .../components/product/product.component.ts | 71 + .../simple-widget/simple-widget.component.ts | 48 + .../store-product-list.component.html | 57 + .../store-product-list.component.ts | 52 + .../destination-listing.component.html | 62 + .../destination-listing.component.ts | 26 + .../vtl-include/vtl-include.component.ts | 38 + .../web-page-content.component.css | 2 +- .../web-page-content.component.ts | 33 + .../activity-detail.component.css | 1 + .../activity-detail.component.html | 28 + .../activity-detail.component.ts | 22 + .../pages/activity/activity.component.html | 3 + .../pages/activity/activity.component.ts | 74 + .../blog-listing/blog-listing.component.html | 31 + .../blog-listing/blog-listing.component.ts | 174 + .../blog-card/blog-card.component.html | 49 + .../blog-card/blog-card.component.ts | 33 + .../components/search/search.component.html | 28 + .../components/search/search.component.ts | 34 + .../blog/blog-post/blog-post.component.css | 0 .../blog/blog-post/blog-post.component.html | 22 + .../blog/blog-post/blog-post.component.ts | 48 + .../activity/activity.component.ts | 21 + .../paragraph/paragraph.component.ts | 34 + .../app/dotcms/pages/blog/blog.component.html | 3 + .../app/dotcms/pages/blog/blog.component.ts | 71 + .../src/app/dotcms/pages/page/page.css | 4 + .../src/app/dotcms/pages/page/page.html | 5 + .../src/app/dotcms/pages/page/page.spec.ts | 23 + .../src/app/dotcms/pages/page/page.ts | 100 + .../src/app/dotcms/types/contentlet.model.ts | 149 + examples/angular-ssr/src/indexFile.html | 13 + examples/angular-ssr/src/main.server.ts | 8 + examples/angular-ssr/src/main.ts | 6 + examples/angular-ssr/src/server.ts | 111 + examples/angular-ssr/src/styles.css | 2 + examples/angular-ssr/tsconfig.app.json | 17 + examples/angular-ssr/tsconfig.json | 34 + examples/angular-ssr/tsconfig.spec.json | 14 + examples/angular-ssr/vercel.json | 15 + examples/angular/README.md | 197 +- examples/angular/angular.json | 66 +- examples/angular/package.json | 88 +- examples/angular/proxy.conf.json | 16 +- examples/angular/src/app/app.component.ts | 10 - examples/angular/src/app/app.config.server.ts | 12 + examples/angular/src/app/app.config.ts | 59 +- examples/angular/src/app/app.css | 0 examples/angular/src/app/app.html | 9 + examples/angular/src/app/app.routes.server.ts | 8 + examples/angular/src/app/app.routes.ts | 44 +- examples/angular/src/app/app.spec.ts | 23 + examples/angular/src/app/app.ts | 101 + .../edit-contentlet-button.component.ts | 39 + .../components/blogs/blogs.component.ts | 19 + .../destinations/destinations.component.ts | 21 + .../recommended-card.component.ts | 56 + .../footer/footer.component.css} | 0 .../app/components/footer/footer.component.ts | 49 + .../components/header/header.component.css | 0 .../app/components/header/header.component.ts | 25 + .../components/loading/loading.component.ts | 187 + .../navigation/navigation.component.css | 0 .../navigation/navigation.component.ts | 28 + .../reorder-button.component.ts | 74 + .../activity/activity.component.ts | 33 - .../banner-carousel.component.html | 63 - .../banner-carousel.component.ts | 50 - .../content-types/banner/banner.component.ts | 40 - .../category-filter.component.ts | 35 - .../custom-no-component.component.ts | 18 - .../content-types/image/image.component.ts | 34 - .../contact-us/contact-us.component.html | 82 - .../contact-us/contact-us.component.ts | 46 - .../page-form/page-form.component.ts | 28 - .../product/product.component.ts | 63 - .../simple-widget/simple-widget.component.ts | 42 - .../store-product-list.component.html | 47 - .../store-product-list.component.ts | 47 - .../destination-listing.component.html | 58 - .../destination-listing.component.ts | 23 - .../vtl-include/vtl-include.component.ts | 35 - .../web-page-content.component.ts | 33 - .../activity/activity.component.css} | 0 .../components/activity/activity.component.ts | 44 + .../banner-carousel.component.html | 74 + .../banner-carousel.component.ts | 58 + .../components/banner/banner.component.css} | 0 .../components/banner/banner.component.ts | 46 + .../category-filter.component.ts | 40 + .../components/image/image.component.css | 3 + .../components/image/image.component.ts | 32 + .../contact-us/contact-us.component.html | 99 + .../contact-us/contact-us.component.ts | 45 + .../page-form/page-form.component.ts | 31 + .../components/product/product.component.css | 3 + .../components/product/product.component.ts | 71 + .../simple-widget/simple-widget.component.ts | 48 + .../store-product-list.component.html | 57 + .../store-product-list.component.ts | 52 + .../destination-listing.component.html | 62 + .../destination-listing.component.ts | 26 + .../vtl-include/vtl-include.component.ts | 38 + .../web-page-content.component.css | 8 + .../web-page-content.component.ts | 33 + .../activity-detail.component.css | 1 + .../activity-detail.component.html | 28 + .../activity-detail.component.ts | 22 + .../pages/activity/activity.component.html | 3 + .../pages/activity/activity.component.ts | 76 + .../blog-listing/blog-listing.component.html | 31 + .../blog-listing/blog-listing.component.ts | 168 + .../blog-card/blog-card.component.html | 49 + .../blog-card/blog-card.component.ts | 33 + .../components/search/search.component.html | 28 + .../components/search/search.component.ts | 34 + .../blog/blog-post/blog-post.component.css | 0 .../blog/blog-post/blog-post.component.html | 22 + .../blog/blog-post/blog-post.component.ts | 48 + .../activity/activity.component.ts | 26 + .../paragraph/paragraph.component.ts | 31 + .../app/dotcms/pages/blog/blog.component.html | 3 + .../app/dotcms/pages/blog/blog.component.ts | 71 + .../src/app/dotcms/pages/page/page.css | 4 + .../src/app/dotcms/pages/page/page.html | 5 + .../src/app/dotcms/pages/page/page.spec.ts | 23 + .../angular/src/app/dotcms/pages/page/page.ts | 100 + .../src/app/dotcms/types/contentlet.model.ts | 149 + .../blog-listing/blog-listing.component.html | 47 - .../blog-listing/blog-listing.component.ts | 137 - .../blog-card/blog-card.component.html | 42 - .../blog-card/blog-card.component.ts | 34 - .../components/search/search.component.html | 24 - .../components/search/search.component.ts | 18 - .../blog/blog-post/blog-post.component.html | 17 - .../blog/blog-post/blog-post.component.ts | 49 - .../activity/activity.component.ts | 21 - .../paragraph/paragraph.component.ts | 34 - .../src/app/pages/blog/blog.component.html | 28 - .../src/app/pages/blog/blog.component.ts | 55 - .../dot-cms-page/dot-cms-page.component.html | 28 - .../dot-cms-page/dot-cms-page.component.ts | 50 - .../src/app/services/editable-page.service.ts | 137 - .../angular/src/app/services/page.service.ts | 43 - .../edit-contentlet-button.component.ts | 37 - .../components/error/error.component.ts | 38 - .../components/blogs/blogs.component.ts | 19 - .../destinations/destinations.component.ts | 22 - .../recommended-card.component.ts | 52 - .../components/footer/footer.component.ts | 45 - .../components/header/header.component.ts | 23 - .../components/loading/loading.component.ts | 188 - .../navigation/navigation.component.ts | 39 - .../reorder-button.component.ts | 66 - .../src/app/shared/contentlet.model.ts | 134 - .../src/app/shared/dynamic-components.ts | 34 - examples/angular/src/app/shared/models.ts | 20 - examples/angular/src/app/shared/queries.ts | 83 - .../environments/environment.development.ts | 12 +- .../angular/src/environments/environment.ts | 8 +- examples/angular/src/index.html | 20 +- examples/angular/src/main.ts | 7 +- examples/angular/src/styles.css | 2 +- examples/angular/tailwind.config.js | 11 +- examples/angular/tsconfig.app.json | 8 +- examples/angular/tsconfig.json | 2 +- examples/angular/tsconfig.spec.json | 9 +- examples/astro/src/types/page.model.ts | 4 +- examples/astro/src/views/DetailPage.tsx | 18 +- examples/nextjs/.gitignore | 2 +- examples/nextjs/package.json | 6 +- examples/nextjs/src/views/DetailPage.js | 18 +- parent/pom.xml | 3 +- starter/empty_20251006.zip | Bin 0 -> 47681 bytes .../java/graphql/ftm/deleteFolder.feature | 38 +- tools/dotcms-cli/README.md | 54 + .../src/test/resources/docker-compose.yaml | 2 +- tools/dotcms-cli/cli/pom.xml | 6 + .../traversal/LocalTraversalServiceImpl.java | 10 + ...bstractLocalFolderTraversalTaskParams.java | 9 + .../task/LocalFolderTraversalTask.java | 20 +- .../com/dotcms/cli/common/DotCliIgnore.java | 409 + .../cli/common/DotCliIgnoreFileFilter.java | 48 + .../src/main/resources/.dotcliignore.example | 66 + .../api/client/files/PushServiceIT.java | 127 +- .../contenttype/ContentTypeCommandIT.java | 201 +- .../dotcms/cli/common/DotCliIgnoreTest.java | 417 + .../src/test/resources/docker-compose.yaml | 2 +- yarn.lock | 670 + 2411 files changed, 173480 insertions(+), 44844 deletions(-) create mode 100644 .claude/skills/cicd-diagnostics/BEST_PRACTICES_ASSESSMENT.md create mode 100644 .claude/skills/cicd-diagnostics/BEST_PRACTICES_COMPLIANCE.md create mode 100644 .claude/skills/cicd-diagnostics/CHANGELOG.md create mode 100644 .claude/skills/cicd-diagnostics/ENHANCEMENTS.md create mode 100644 .claude/skills/cicd-diagnostics/ISSUE_TEMPLATE.md create mode 100644 .claude/skills/cicd-diagnostics/LOG_ANALYSIS.md create mode 100644 .claude/skills/cicd-diagnostics/README.md create mode 100644 .claude/skills/cicd-diagnostics/REFERENCE.md create mode 100644 .claude/skills/cicd-diagnostics/SKILL.md create mode 100644 .claude/skills/cicd-diagnostics/WORKFLOWS.md create mode 100755 .claude/skills/cicd-diagnostics/fetch-jobs.py create mode 100755 .claude/skills/cicd-diagnostics/fetch-logs.py create mode 100755 .claude/skills/cicd-diagnostics/fetch-metadata.py create mode 100755 .claude/skills/cicd-diagnostics/init-diagnostic.py create mode 100644 .claude/skills/cicd-diagnostics/requirements.txt create mode 100644 .claude/skills/cicd-diagnostics/utils/README.md create mode 100755 .claude/skills/cicd-diagnostics/utils/__init__.py create mode 100755 .claude/skills/cicd-diagnostics/utils/evidence.py create mode 100644 .claude/skills/cicd-diagnostics/utils/external_issues.py create mode 100755 .claude/skills/cicd-diagnostics/utils/github_api.py create mode 100755 .claude/skills/cicd-diagnostics/utils/tiered_extraction.py create mode 100755 .claude/skills/cicd-diagnostics/utils/workspace.py create mode 100644 .claude/skills/sdk-analytics/SKILL.md create mode 100644 .github/ISSUE_TEMPLATE/epic.yml create mode 100644 .github/ISSUE_TEMPLATE/pillar.yml create mode 100644 .github/ISSUE_TEMPLATE/spike.yaml create mode 100644 .github/actions/security/org-membership-check/README.md create mode 100644 .github/actions/security/org-membership-check/action.yml create mode 100644 .github/copilot-instructions.md create mode 100644 .github/frontend.instructions.md create mode 100644 .github/workflows/cicd_6-release.yml create mode 100644 .github/workflows/cicd_comp_release-phase.yml create mode 100644 .github/workflows/cicd_comp_release-prepare-phase.yml create mode 100644 .github/workflows/issue_comment_claude-code-review.yaml create mode 100644 .mise.md create mode 100644 .mise.toml create mode 100644 core-web/AGENTS.md create mode 100644 core-web/CLAUDE.md create mode 100644 core-web/apps/dotcms-ui/jest.config.ts delete mode 100644 core-web/apps/dotcms-ui/karma.conf.js delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration-detail/dot-apps-configuration-detail-form/dot-apps-configuration-detail-form.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration-detail/dot-apps-configuration-detail.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration-header/dot-apps-configuration-header.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration/dot-apps-configuration-list/dot-apps-configuration-item/dot-apps-configuration-item.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration/dot-apps-configuration-list/dot-apps-configuration-list.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration/dot-apps-configuration.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-import-export-dialog/dot-apps-import-export-dialog.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-list/dot-apps-card/dot-apps-card.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-list/dot-apps-list.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps.module.ts rename core-web/apps/dotcms-ui/src/app/portlets/dot-apps/{dot-apps-routing.module.ts => dot-apps.routes.ts} (73%) delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-create-edit/dot-categories-create-edit-routing.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-create-edit/dot-categories-create-edit.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/dot-categories-list-routing.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/dot-categories-list.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-permissions/dot-categories-permissions.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-routing.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories.module.ts create mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories.routes.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.module.ts rename core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/{container-list-routing.module.ts => container-list.routes.ts} (55%) delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-code/dot-add-variable/dot-add-variable.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-code/dot-container-code.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-create-routing.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-create.module.ts create mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-create.routes.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-history/dot-container-history.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-permissions/dot-container-permissions.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-properties/dot-container-properties.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-loop-editor/dot-loop-editor.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-containers.module.ts rename core-web/apps/dotcms-ui/src/app/portlets/dot-containers/{dot-containers-routing.module.ts => dot-containers.routes.ts} (53%) delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-content-types/dot-content-types.module.ts rename core-web/apps/dotcms-ui/src/app/portlets/dot-content-types/{dot-content-types-routing.module.ts => dot-content-types.routes.ts} (71%) delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-block-editor-sidebar/dot-block-editor-sidebar.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-edit-page-info/dot-edit-page-info.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette-content-type/dot-palette-content-type.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette-contentlets/dot-palette-contentlets.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette-input-filter/dot-palette-input-filter.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-toolbar/dot-edit-page-toolbar.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-view-as-controller/dot-edit-page-view-as-controller.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-workflows-actions/dot-edit-page-workflows-actions.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-form-selector/dot-form-selector.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-whats-changed/dot-whats-changed.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/dot-edit-content.module.ts rename core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/{dot-edit-page-routing.module.ts => dot-edit-page.routes.ts} (73%) delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/components/dot-template-additional-actions/dot-legacy-template-additional-actions-iframe/dot-legacy-template-additional-actions-iframe.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/components/dot-template-additional-actions/dot-template-additional-actions-routing.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/components/dot-template-additional-actions/dot-template-additional-actions.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/main/dot-edit-page-main/dot-edit-page-main.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/main/dot-edit-page-nav/dot-edit-page-nav.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-form-builder/dot-form-builder.module.ts rename core-web/apps/dotcms-ui/src/app/portlets/dot-form-builder/{dot-form-builder-routing.module.ts => dot-form-builder.routes.ts} (79%) delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-favorite-panel/dot-pages-card/dot-pages-card.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-favorite-panel/dot-pages-favorite-panel.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-listing-panel/dot-pages-listing-panel.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages.module.ts rename core-web/apps/dotcms-ui/src/app/portlets/dot-pages/{dot-pages-routing.module.ts => dot-pages.routes.ts} (74%) delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-contentlets/dot-contentlets.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-workflow-task/dot-workflow-task.module.ts rename core-web/apps/dotcms-ui/src/app/portlets/dot-starter/{dot-starter-routing.module.ts => dot-starter.routes.ts} (52%) delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-advanced/dot-template-advanced.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.module.ts rename core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/{dot-template-create-edit-routing.module.ts => dot-template-create-edit.routes.ts} (52%) delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-new/dot-template-new-routing.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-new/dot-template-new.module.ts create mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-new/dot-template-new.routes.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-props/dot-template-props.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-props/dot-template-thumbnail-field/dot-template-thumbnail-field.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-templates.module.ts rename core-web/apps/dotcms-ui/src/app/portlets/dot-templates/{dot-templates-routing.module.ts => dot-templates.routes.ts} (56%) delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-add-row/content-type-fields-add-row.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-add-row/index.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/dot-relationships.module.ts create mode 100644 core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/new-render-mode-proptery/index.ts create mode 100644 core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/new-render-mode-proptery/new-render-mode-property.component.html create mode 100644 core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/new-render-mode-proptery/new-render-mode-property.component.scss create mode 100644 core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/new-render-mode-proptery/new-render-mode-property.component.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/dot-content-type-fields-variables/dot-content-type-fields-variables.module.ts rename core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/{dot-content-types-edit-routing.module.ts => dot-content-types-edit.routes.ts} (60%) delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/components/dot-add-to-menu/dot-add-to-menu.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/components/dot-content-type-copy-dialog/dot-content-type-copy-dialog.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/dot-content-types-listing.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-action-button/dot-action-button.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-autocomplete-tags/dot-autocomplete-tags.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-bulk-information/dot-bulk-information.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-custom-time.component/dot-custom-time.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-download-bundle-dialog/dot-download-bundle-dialog.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-empty-state/dot-empty-state.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-generate-secure-password/dot-generate-secure-password.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-global-message/dot-global-message.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-inline-edit/dot-inline-edit.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-md-icon-selector/dot-md-icon-selector.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-overlay-mask/dot-overlay-mask.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-page-selector/dot-page-selector.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-push-publish-dialog/dot-push-publish-dialog.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-push-publish-env-selector/dot-push-publish-env-selector.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-site-selector-field/dot-site-selector-field.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-site-selector/dot-site-selector.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-textarea-content/dot-textarea-content.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-wizard/dot-wizard.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-workflows-actions-selector-field/dot-workflows-actions-selector-field.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/dot-workflows-selector-field/dot-workflows-selector-field.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/forms/dot-comment-and-assign-form/dot-comment-and-assign-form.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/forms/dot-push-publish-form/dot-push-publish-form.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/dot-loading-indicator/dot-loading-indicator.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/searchable-dropdown.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-add-persona-dialog.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-create-persona-form/dot-create-persona-form.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-base-type-selector/dot-base-type-selector.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector/dot-container-selector.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-content-type-selector/dot-content-type-selector.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/dot-contentlet-editor.module.ts rename core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/{dot-contentlet-editor.routing.module.ts => dot-contentlet-editor.routes.ts} (65%) delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-copy-link/dot-copy-link.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-crumbtrail/dot-crumbtrail.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-crumbtrail/service/dot-crumbtrail.service.spec.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-crumbtrail/service/dot-crumbtrail.service.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-device-selector/dot-device-selector.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-field-helper/dot-field-helper.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-iframe-dialog/dot-iframe-dialog.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-large-message-display/dot-large-message-display.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-listing-data-table/action-header/action-header.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-listing-data-table/dot-listing-data-table.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-message-display/dot-message-display.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/components/dot-nav-icon/dot-nav-icon.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/dot-navigation.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selected-item/dot-persona-selected-item.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector-option/dot-persona-selector-option.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona.selector.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-portlet-base/components/dot-portlet-box/dot-portlet-box.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-portlet-base/components/dot-portlet-toolbar/dot-portlet-toolbar.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-portlet-base/dot-portlet-base.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-relationship-tree/dot-relationship-tree.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-secondary-toolbar/dot-secondary-toolbar.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-theme-selector-dropdown/dot-theme-selector-dropdown.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-notifications/dot-toolbar-notifications.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-workflow-task-detail/dot-workflow-task-detail.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/login/dot-login-component/dot-login.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/login/dot-login-page-routing.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/login/dot-login-page.module.ts create mode 100644 core-web/apps/dotcms-ui/src/app/view/components/login/dot-login-page.routes.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/login/forgot-password-component/forgot-password.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/login/reset-password-component/reset-password.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/directives/dot-container-reference/dot-container-reference.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/directives/dot-maxlength/dot-maxlength.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/directives/ripple/ripple-effect.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/pipes/dot-filter/dot-filter-pipe.module.ts delete mode 100644 core-web/apps/dotcms-ui/src/app/view/pipes/dot-radom-icon/dot-random-icon.pipe.module.ts create mode 100644 core-web/apps/dotcms-ui/src/test-setup.ts delete mode 100644 core-web/apps/dotcms-ui/src/test.ts create mode 100644 core-web/apps/mcp-server/CONTRIBUTING.md create mode 100644 core-web/apps/mcp-server/src/tools/_example-tool/formatters.ts create mode 100644 core-web/apps/mcp-server/src/tools/_example-tool/handlers.ts create mode 100644 core-web/apps/mcp-server/src/tools/_example-tool/index.ts create mode 100644 core-web/apps/mcp-server/src/types/contentype.spec.ts delete mode 100644 core-web/karma.conf.js create mode 100644 core-web/libs/block-editor/src/lib/extensions/dot-config/dot-config.types.ts create mode 100644 core-web/libs/data-access/src/lib/dot-containers/dot-containers.service.spec.ts create mode 100644 core-web/libs/data-access/src/lib/dot-content-drive/dot-content-drive.service.spec.ts create mode 100644 core-web/libs/data-access/src/lib/dot-content-drive/dot-content-drive.service.ts create mode 100644 core-web/libs/data-access/src/lib/dot-favorite-contenttype/dot-favorite-contenttype.service.spec.ts create mode 100644 core-web/libs/data-access/src/lib/dot-favorite-contenttype/dot-favorite-contenttype.service.ts create mode 100644 core-web/libs/data-access/src/lib/dot-folder/dot-folder.service.spec.ts create mode 100644 core-web/libs/data-access/src/lib/dot-folder/dot-folder.service.ts create mode 100644 core-web/libs/data-access/src/lib/dot-page-contenttype/dot-page-contenttype.service.spec.ts create mode 100644 core-web/libs/data-access/src/lib/dot-page-contenttype/dot-page-contenttype.service.ts rename core-web/{apps/dotcms-ui/src/app/api/services => libs/data-access/src/lib}/dot-ui-colors/dot-ui-colors.service.spec.ts (94%) rename core-web/{apps/dotcms-ui/src/app/api/services => libs/data-access/src/lib}/dot-ui-colors/dot-ui-colors.service.ts (94%) create mode 100644 core-web/libs/dotcms-models/src/lib/dot-api-response.ts create mode 100644 core-web/libs/dotcms-models/src/lib/dot-folder.model.ts create mode 100644 core-web/libs/dotcms-models/src/lib/dot-pagination.model.ts create mode 100644 core-web/libs/dotcms-models/src/lib/navigation/menu-entity.model.ts create mode 100644 core-web/libs/dotcms-models/src/lib/navigation/menu-group.model.ts create mode 100644 core-web/libs/dotcms-models/src/lib/navigation/menu.slice.model.ts create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-compare/dot-edit-content-compare.component.html create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-compare/dot-edit-content-compare.component.scss create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-compare/dot-edit-content-compare.component.ts create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-history/components/dot-history-timeline-item/dot-history-timeline-item.component.html create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-history/components/dot-history-timeline-item/dot-history-timeline-item.component.scss create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-history/components/dot-history-timeline-item/dot-history-timeline-item.component.spec.ts create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-history/components/dot-history-timeline-item/dot-history-timeline-item.component.ts create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-history/components/dot-pushpublish-timeline-item/dot-pushpublish-timeline-item.component.html create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-history/components/dot-pushpublish-timeline-item/dot-pushpublish-timeline-item.component.scss create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-history/components/dot-pushpublish-timeline-item/dot-pushpublish-timeline-item.component.spec.ts create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-history/components/dot-pushpublish-timeline-item/dot-pushpublish-timeline-item.component.ts create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-history/dot-edit-content-sidebar-history.component.html create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-history/dot-edit-content-sidebar-history.component.scss create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-history/dot-edit-content-sidebar-history.component.spec.ts create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-history/dot-edit-content-sidebar-history.component.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-card-field/components/dot-card-field-content.component.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-card-field/components/dot-card-field-footer.component.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-card-field/components/dot-card-field-label/dot-card-field-label.component.html create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-card-field/components/dot-card-field-label/dot-card-field-label.component.scss create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-card-field/components/dot-card-field-label/dot-card-field-label.component.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-card-field/dot-card-field.component.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/components/dot-binary-field-wrapper/dot-binary-field-wrapper.component.html create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/components/dot-binary-field-wrapper/dot-binary-field-wrapper.component.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-block-editor/dot-edit-content-block-editor.component.html create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-block-editor/dot-edit-content-block-editor.component.spec.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-block-editor/dot-edit-content-block-editor.component.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-calendar-field/components/calendar-field/calendar-field.component.html rename core-web/libs/edit-content/src/lib/fields/dot-edit-content-calendar-field/{dot-edit-content-calendar-field.component.scss => components/calendar-field/calendar-field.component.scss} (100%) create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-calendar-field/components/calendar-field/calendar-field.component.ts rename core-web/libs/edit-content/src/lib/fields/dot-edit-content-calendar-field/{dot-edit-content-calendar-field.util.spec.ts => components/calendar-field/calendar-field.util.spec.ts} (99%) rename core-web/libs/edit-content/src/lib/fields/dot-edit-content-calendar-field/{dot-edit-content-calendar-field.util.ts => components/calendar-field/calendar-field.util.ts} (99%) create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field/dot-category-field.component.html rename core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/{dot-edit-content-category-field.component.scss => components/dot-category-field/dot-category-field.component.scss} (100%) rename core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/{dot-edit-content-category-field.component.spec.ts => components/dot-category-field/dot-category-field.component.spec.ts} (53%) create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field/dot-category-field.component.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component.html create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-file-field/dot-file-field.component.html create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-file-field/dot-file-field.component.scss create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-file-field/dot-file-field.component.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/components/host-folder-field/host-folder-field.component.html create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/components/host-folder-field/host-folder-field.component.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-key-value/components/key-value-field/key-value-field.component.html create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-key-value/components/key-value-field/key-value-field.component.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.html delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-radio-field/dot-edit-content-radio-field.component.scss create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-relationship-field/dot-relationship-field.component.html create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-relationship-field/dot-relationship-field.component.scss create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-relationship-field/dot-relationship-field.component.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-select-field/dot-edit-content-select-field.component.html create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/components/tag-field/tag-field.component.html create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/components/tag-field/tag-field.component.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-text-field/dot-edit-content-text-field.component.scss create mode 100644 core-web/libs/edit-content/src/lib/fields/shared/base-control-value-accesor.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/shared/base-wrapper-field.ts create mode 100644 core-web/libs/edit-content/src/lib/store/features/history/history.feature.spec.ts create mode 100644 core-web/libs/edit-content/src/lib/store/features/history/history.feature.ts create mode 100644 core-web/libs/global-store/src/lib/features/breadcrumb/breadcrumb.feature.spec.ts create mode 100644 core-web/libs/global-store/src/lib/features/breadcrumb/breadcrumb.feature.ts create mode 100644 core-web/libs/global-store/src/lib/features/breadcrumb/breadcrumb.utils.spec.ts create mode 100644 core-web/libs/global-store/src/lib/features/breadcrumb/breadcrumb.utils.ts create mode 100644 core-web/libs/global-store/src/lib/features/menu/menu.slice.ts create mode 100644 core-web/libs/global-store/src/lib/features/menu/with-menu.feature.spec.ts create mode 100644 core-web/libs/global-store/src/lib/features/menu/with-menu.feature.ts rename core-web/libs/global-store/src/lib/{ => features/with-system}/with-system.feature.ts (100%) create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dialogs/dot-content-drive-dialog-folder/dot-content-drive-dialog-folder.component.html create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dialogs/dot-content-drive-dialog-folder/dot-content-drive-dialog-folder.component.scss create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dialogs/dot-content-drive-dialog-folder/dot-content-drive-dialog-folder.component.spec.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dialogs/dot-content-drive-dialog-folder/dot-content-drive-dialog-folder.component.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-dropzone/dot-content-drive-dropzone.component.html create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-dropzone/dot-content-drive-dropzone.component.scss create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-dropzone/dot-content-drive-dropzone.component.spec.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-dropzone/dot-content-drive-dropzone.component.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-sidebar/dot-content-drive-sidebar.component.html create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-sidebar/dot-content-drive-sidebar.component.scss create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-sidebar/dot-content-drive-sidebar.component.spec.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-sidebar/dot-content-drive-sidebar.component.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-language-field/dot-content-drive-language-field.component.html rename core-web/{apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/store/dot-categories-list-store.spec.ts => libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-language-field/dot-content-drive-language-field.component.scss} (100%) create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-language-field/dot-content-drive-language-field.component.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-language-field/dot-content-drive-language-field.spec.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-workflow-actions/dot-content-drive-workflow-actions.component.html rename core-web/libs/{edit-content/src/lib/fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component.scss => portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-workflow-actions/dot-content-drive-workflow-actions.component.scss} (72%) create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-workflow-actions/dot-content-drive-workflow-actions.component.spec.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-workflow-actions/dot-content-drive-workflow-actions.component.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-folder-list-context-menu/dot-folder-list-context-menu.component.html create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-folder-list-context-menu/dot-folder-list-context-menu.component.scss create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-folder-list-context-menu/dot-folder-list-context-menu.component.spec.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-folder-list-context-menu/dot-folder-list-context-menu.component.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/shared/services/dot-content-drive-navigation.service.spec.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/shared/services/dot-content-drive-navigation.service.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/shared/services/index.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/store/features/context-menu/withContextMenu.spec.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/store/features/context-menu/withContextMenu.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/store/features/dialog/withDialog.spec.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/store/features/dialog/withDialog.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/store/features/dragging/withDragging.spec.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/store/features/dragging/withDragging.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/store/features/sidebar/withSidebar.spec.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/store/features/sidebar/withSidebar.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/utils/tree-folder.utils.spec.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/utils/tree-folder.utils.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/utils/workflow-actions.spec.ts create mode 100644 core-web/libs/portlets/dot-content-drive/portlet/src/lib/utils/workflow-actions.ts create mode 100644 core-web/libs/portlets/dot-content-drive/ui/src/lib/dot-tree-folder/dot-tree-folder.component.html create mode 100644 core-web/libs/portlets/dot-content-drive/ui/src/lib/dot-tree-folder/dot-tree-folder.component.scss create mode 100644 core-web/libs/portlets/dot-content-drive/ui/src/lib/dot-tree-folder/dot-tree-folder.component.spec.ts create mode 100644 core-web/libs/portlets/dot-content-drive/ui/src/lib/dot-tree-folder/dot-tree-folder.component.ts create mode 100644 core-web/libs/portlets/dot-usage/.eslintrc.json create mode 100644 core-web/libs/portlets/dot-usage/README.md create mode 100644 core-web/libs/portlets/dot-usage/jest.config.ts create mode 100644 core-web/libs/portlets/dot-usage/project.json create mode 100644 core-web/libs/portlets/dot-usage/src/index.ts create mode 100644 core-web/libs/portlets/dot-usage/src/lib.routes.ts create mode 100644 core-web/libs/portlets/dot-usage/src/lib/dot-usage-shell/dot-usage-shell.component.html create mode 100644 core-web/libs/portlets/dot-usage/src/lib/dot-usage-shell/dot-usage-shell.component.scss create mode 100644 core-web/libs/portlets/dot-usage/src/lib/dot-usage-shell/dot-usage-shell.component.spec.ts create mode 100644 core-web/libs/portlets/dot-usage/src/lib/dot-usage-shell/dot-usage-shell.component.ts create mode 100644 core-web/libs/portlets/dot-usage/src/lib/services/dot-usage.service.spec.ts create mode 100644 core-web/libs/portlets/dot-usage/src/lib/services/dot-usage.service.ts create mode 100644 core-web/libs/portlets/dot-usage/src/test-setup.ts create mode 100644 core-web/libs/portlets/dot-usage/tsconfig.json create mode 100644 core-web/libs/portlets/dot-usage/tsconfig.lib.json create mode 100644 core-web/libs/portlets/dot-usage/tsconfig.spec.json create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-contentlet-tools/dot-uve-contentlet-tools.component.html rename core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/{ema-contentlet-tools/ema-contentlet-tools.component.scss => dot-uve-contentlet-tools/dot-uve-contentlet-tools.component.scss} (59%) create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-contentlet-tools/dot-uve-contentlet-tools.component.spec.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-contentlet-tools/dot-uve-contentlet-tools.component.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-lock-overlay/dot-uve-lock-overlay.component.html create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-lock-overlay/dot-uve-lock-overlay.component.scss create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-lock-overlay/dot-uve-lock-overlay.component.spec.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-lock-overlay/dot-uve-lock-overlay.component.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-favorite-selector/dot-favorite-selector.component.html create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-favorite-selector/dot-favorite-selector.component.scss create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-favorite-selector/dot-favorite-selector.component.spec.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-favorite-selector/dot-favorite-selector.component.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-contentlet/dot-uve-palette-contentlet.component.html create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-contentlet/dot-uve-palette-contentlet.component.scss create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-contentlet/dot-uve-palette-contentlet.component.spec.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-contentlet/dot-uve-palette-contentlet.component.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-contenttype/dot-uve-palette-contenttype.component.html create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-contenttype/dot-uve-palette-contenttype.component.scss create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-contenttype/dot-uve-palette-contenttype.component.spec.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-contenttype/dot-uve-palette-contenttype.component.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-list/dot-uve-palette-list.component.html create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-list/dot-uve-palette-list.component.scss create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-list/dot-uve-palette-list.component.spec.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-list/dot-uve-palette-list.component.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-list/store/store.spec.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-list/store/store.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/dot-uve-palette.component.html create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/dot-uve-palette.component.scss create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/dot-uve-palette.component.spec.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/dot-uve-palette.component.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/models.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/utils/index.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/utils/utils.spec.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/assets/left_panel_close.svg create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/assets/left_panel_open.svg create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/components/dot-toggle-lock-button/dot-toggle-lock-button.component.html create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/components/dot-toggle-lock-button/dot-toggle-lock-button.component.scss create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/components/dot-toggle-lock-button/dot-toggle-lock-button.component.spec.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/components/dot-toggle-lock-button/dot-toggle-lock-button.component.ts delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-content-type/edit-ema-palette-content-type.component.html delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-content-type/edit-ema-palette-content-type.component.scss delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-content-type/edit-ema-palette-content-type.component.spec.ts delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-content-type/edit-ema-palette-content-type.component.ts delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-contentlets/edit-ema-palette-contentlets.component.html delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-contentlets/edit-ema-palette-contentlets.component.scss delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-contentlets/edit-ema-palette-contentlets.component.spec.ts delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-contentlets/edit-ema-palette-contentlets.component.ts delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/edit-ema-palette.component.html delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/edit-ema-palette.component.scss delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/edit-ema-palette.component.spec.ts delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/edit-ema-palette.component.ts delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/store/edit-ema-palette.store.spec.ts delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/store/edit-ema-palette.store.ts delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-contentlet-tools/ema-contentlet-tools.component.html delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-contentlet-tools/ema-contentlet-tools.component.spec.ts delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-contentlet-tools/ema-contentlet-tools.component.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withLock.spec.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withLock.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/withPageContext.ts delete mode 100644 core-web/libs/portlets/edit-ema/ui/src/lib/dot-content-compare/dot-content-compare.module.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/dot-analytics.content.spec.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/dot-analytics.content.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/click/dot-analytics.click-tracker.spec.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/click/dot-analytics.click-tracker.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/click/dot-analytics.click.plugin.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/click/dot-analytics.click.utils.spec.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/click/dot-analytics.click.utils.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/enricher/dot-analytics.enricher.plugin.spec.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/enricher/dot-analytics.enricher.plugin.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/identity/dot-analytics.identity.activity-tracker.spec.ts rename core-web/libs/sdk/analytics/src/lib/{dotAnalytics/shared/dot-content-analytics.activity-tracker.ts => core/plugin/identity/dot-analytics.identity.activity-tracker.ts} (72%) rename core-web/libs/sdk/analytics/src/lib/{dotAnalytics => core}/plugin/identity/dot-analytics.identity.plugin.ts (76%) rename core-web/libs/sdk/analytics/src/lib/{dotAnalytics => core}/plugin/identity/dot-analytics.identity.utils.spec.ts (64%) create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/identity/dot-analytics.identity.utils.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/impression/dot-analytics.impression-tracker.spec.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/impression/dot-analytics.impression-tracker.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/impression/dot-analytics.impression.plugin.spec.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/impression/dot-analytics.impression.plugin.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/impression/dot-analytics.impression.utils.spec.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/impression/dot-analytics.impression.utils.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/impression/index.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/plugin/main/dot-analytics.plugin.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/constants/dot-analytics.constants.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/constants/index.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/dot-analytics.logger.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/http/dot-analytics.http.spec.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/http/dot-analytics.http.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/models/data.model.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/models/event.model.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/models/index.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/models/library.model.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/models/request.model.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/queue/dot-analytics.queue.utils.spec.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/queue/dot-analytics.queue.utils.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/queue/index.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/queue/queue-utils.d.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/queue/router-utils.d.ts rename core-web/libs/sdk/analytics/src/lib/{dotAnalytics/shared/dot-content-analytics.utils.spec.ts => core/shared/utils/dot-analytics.utils.spec.ts} (68%) create mode 100644 core-web/libs/sdk/analytics/src/lib/core/shared/utils/dot-analytics.utils.ts delete mode 100644 core-web/libs/sdk/analytics/src/lib/dotAnalytics/dot-content-analytics.spec.ts delete mode 100644 core-web/libs/sdk/analytics/src/lib/dotAnalytics/dot-content-analytics.ts delete mode 100644 core-web/libs/sdk/analytics/src/lib/dotAnalytics/plugin/dot-analytics.plugin.ts delete mode 100644 core-web/libs/sdk/analytics/src/lib/dotAnalytics/plugin/enricher/dot-analytics.enricher.plugin.spec.ts delete mode 100644 core-web/libs/sdk/analytics/src/lib/dotAnalytics/plugin/enricher/dot-analytics.enricher.plugin.ts delete mode 100644 core-web/libs/sdk/analytics/src/lib/dotAnalytics/plugin/identity/dot-analytics.identity.utils.ts delete mode 100644 core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.constants.ts delete mode 100644 core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.http.spec.ts delete mode 100644 core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.http.ts delete mode 100644 core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.model.ts delete mode 100644 core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.utils.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/react/components/DotContentAnalytics.tsx delete mode 100644 core-web/libs/sdk/analytics/src/lib/react/components/DotContentAnalyticsProvider.spec.tsx delete mode 100644 core-web/libs/sdk/analytics/src/lib/react/components/DotContentAnalyticsProvider.tsx delete mode 100644 core-web/libs/sdk/analytics/src/lib/react/contexts/DotContentAnalyticsContext.spec.tsx delete mode 100644 core-web/libs/sdk/analytics/src/lib/react/contexts/DotContentAnalyticsContext.tsx create mode 100644 core-web/libs/sdk/analytics/src/lib/react/internal/utils.spec.ts create mode 100644 core-web/libs/sdk/analytics/src/lib/react/internal/utils.ts delete mode 100644 core-web/libs/sdk/analytics/src/lib/react/internal/uve.utils.spec.ts delete mode 100644 core-web/libs/sdk/analytics/src/lib/react/internal/uve.utils.ts create mode 100644 core-web/libs/sdk/angular/CHANGELOG.md create mode 100644 core-web/libs/sdk/client/src/lib/client/adapters/fetch-http-client.spec.ts create mode 100644 core-web/libs/sdk/client/src/lib/client/adapters/fetch-http-client.ts create mode 100644 core-web/libs/sdk/client/src/lib/client/ai/ai-api.ts create mode 100644 core-web/libs/sdk/client/src/lib/client/ai/search/search.spec.ts create mode 100644 core-web/libs/sdk/client/src/lib/client/ai/search/search.ts create mode 100644 core-web/libs/sdk/client/src/lib/client/ai/shared/const.ts create mode 100644 core-web/libs/sdk/client/src/lib/client/ai/shared/types.ts create mode 100644 core-web/libs/sdk/client/src/lib/client/base/README.md create mode 100644 core-web/libs/sdk/client/src/lib/client/base/base-api.ts create mode 100644 core-web/libs/sdk/client/src/lib/client/content/shared/utils.spec.ts create mode 100644 core-web/libs/sdk/client/src/lib/utils/params/utils.spec.ts create mode 100644 core-web/libs/sdk/client/src/lib/utils/params/utils.ts create mode 100644 core-web/libs/sdk/react/CHANGELOG.md create mode 100644 core-web/libs/sdk/react/src/lib/next/__test__/hook/useAISearch.test.tsx create mode 100644 core-web/libs/sdk/react/src/lib/next/__test__/hook/useStyleEditorSchemas.test.tsx create mode 100644 core-web/libs/sdk/react/src/lib/next/hooks/useAISearch.ts create mode 100644 core-web/libs/sdk/react/src/lib/next/hooks/useIsAnalyticsActive.ts create mode 100644 core-web/libs/sdk/react/src/lib/next/hooks/useStyleEditorSchemas.ts create mode 100644 core-web/libs/sdk/react/src/lib/next/shared/types.ts create mode 100644 core-web/libs/sdk/types/src/lib/ai/internal.ts create mode 100644 core-web/libs/sdk/types/src/lib/ai/public.ts create mode 100644 core-web/libs/sdk/types/src/lib/components/generic/public.ts create mode 100644 core-web/libs/sdk/types/src/lib/content/public.ts create mode 100644 core-web/libs/sdk/types/src/lib/nav/public.ts create mode 100644 core-web/libs/sdk/uve/src/lib/style-editor/internal.ts create mode 100644 core-web/libs/sdk/uve/src/lib/style-editor/public.spec.ts create mode 100644 core-web/libs/sdk/uve/src/lib/style-editor/public.ts create mode 100644 core-web/libs/sdk/uve/src/lib/style-editor/types.ts delete mode 100644 core-web/libs/template-builder/src/lib/components/template-builder/components/dot-layout-properties/dot-layout-properties-item/dot-layout-properties-item.module.ts delete mode 100644 core-web/libs/template-builder/src/lib/components/template-builder/components/dot-layout-properties/dot-layout-properties.module.ts delete mode 100644 core-web/libs/template-builder/src/lib/components/template-builder/components/dot-layout-properties/dot-layout-property-sidebar/dot-layout-property-sidebar.module.ts delete mode 100644 core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-components.module.ts delete mode 100644 core-web/libs/template-builder/src/lib/template-builder.module.ts create mode 100644 core-web/libs/ui/src/lib/components/dot-severity-icon/dot-severity-icon.component.html create mode 100644 core-web/libs/ui/src/lib/components/dot-severity-icon/dot-severity-icon.component.spec.ts create mode 100644 core-web/libs/ui/src/lib/components/dot-severity-icon/dot-severity-icon.component.ts create mode 100644 core-web/libs/ui/src/lib/components/dot-sidebar-accordion/components/dot-sidebar-accordion-tab/dot-sidebar-accordion-tab.component.spec.ts create mode 100644 core-web/libs/ui/src/lib/components/dot-sidebar-accordion/components/dot-sidebar-accordion-tab/dot-sidebar-accordion-tab.component.ts create mode 100644 core-web/libs/ui/src/lib/components/dot-sidebar-accordion/dot-sidebar-accordion.component.html create mode 100644 core-web/libs/ui/src/lib/components/dot-sidebar-accordion/dot-sidebar-accordion.component.scss create mode 100644 core-web/libs/ui/src/lib/components/dot-sidebar-accordion/dot-sidebar-accordion.component.spec.ts create mode 100644 core-web/libs/ui/src/lib/components/dot-sidebar-accordion/dot-sidebar-accordion.component.ts create mode 100644 core-web/libs/ui/src/lib/components/dot-sidebar-accordion/index.ts delete mode 100644 core-web/libs/ui/src/lib/dot-icon/dot-icon.module.ts delete mode 100644 core-web/libs/ui/src/lib/dot-spinner/dot-spinner.module.ts delete mode 100644 core-web/libs/ui/src/lib/modules/dot-dialog/dot-dialog.module.ts create mode 100644 core-web/libs/ui/src/lib/pipes/dot-folder-name/dot-folder-name.pipe.spec.ts create mode 100644 core-web/libs/ui/src/lib/pipes/dot-folder-name/dot-folder-name.pipe.ts create mode 100644 core-web/libs/ui/src/lib/pipes/dot-locale-tag/dot-locale-tag.pipe.spec.ts create mode 100644 core-web/libs/ui/src/lib/pipes/dot-locale-tag/dot-locale-tag.pipe.ts create mode 100644 core-web/libs/utils-testing/src/lib/dot-folder.mock.ts create mode 100644 core-web/libs/utils-testing/src/lib/fake-event.mock.ts create mode 100644 core-web/libs/utils-testing/src/lib/test-setup-helpers.ts create mode 100644 docker/docker-compose-examples/analytics/README.md create mode 100755 docker/docker-compose-examples/analytics/get-token.sh create mode 100644 docker/docker-compose-examples/analytics/setup/config/dev/cube/schema/ContentAttribution.js create mode 100644 docker/docker-compose-examples/analytics/setup/config/dev/cube/schema/Conversion.js create mode 100644 docker/docker-compose-examples/analytics/setup/config/dev/cube/schema/EventSummary.js create mode 100644 docker/docker-compose-examples/analytics/setup/config/dev/cube/schema/Request.js create mode 100755 docker/docker-compose-examples/analytics/start-analytics.sh create mode 100644 docs/backend/TELEMETRY_IMPLEMENTATION.md create mode 100644 dotCMS/src/main/java/com/dotcms/ai/api/AIVisionAPI.java create mode 100644 dotCMS/src/main/java/com/dotcms/ai/api/OpenAITranslationService.java create mode 100644 dotCMS/src/main/java/com/dotcms/ai/api/OpenAIVisionAPIImpl.java create mode 100644 dotCMS/src/main/java/com/dotcms/ai/listener/AIVisionInitializer.java create mode 100644 dotCMS/src/main/java/com/dotcms/ai/listener/OpenAIImageTaggingContentListener.java create mode 100644 dotCMS/src/main/java/com/dotcms/ai/util/AIUtil.java create mode 100644 dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAITranslationActionlet.java create mode 100644 dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIVisionAutoTagActionlet.java create mode 100644 dotCMS/src/main/java/com/dotcms/analytics/attributes/CustomAttributeProcessingException.java create mode 100644 dotCMS/src/main/java/com/dotcms/cache/CacheValueImpl.java create mode 100644 dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/DropOldContentletRunner.java create mode 100644 dotCMS/src/main/java/com/dotcms/contenttype/business/DotAssetValidationException.java create mode 100644 dotCMS/src/main/java/com/dotcms/contenttype/util/ContentTypeFieldNames.java create mode 100644 dotCMS/src/main/java/com/dotcms/contenttype/util/StoryBlockUtil.java create mode 100644 dotCMS/src/main/java/com/dotcms/jitsu/validators/NumberTypeValidator.java rename dotCMS/src/main/java/com/dotcms/jitsu/validators/{SiteKeyValidator.java => SiteAuthValidator.java} (70%) create mode 100644 dotCMS/src/main/java/com/dotcms/publisher/assets/business/PushedAssetHistoryTransformer.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/ResponseEntityRestTagListView.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/asset/AbstractAssetArchiveRequestForm.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/asset/AbstractAssetDeletionRequestForm.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/asset/AbstractAssetLookupRequestForm.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/asset/AbstractFolderDeletionRequestForm.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/asset/AbstractFolderDetail.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/asset/AbstractFolderForm.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/asset/AbstractNewFolderForm.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/asset/AbstractUpdateFolderDetail.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/asset/AbstractUpdateFolderForm.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentPushHistoryPaginator.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/content/PushedAssetHistory.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/drive/AbstractDriveRequestForm.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/drive/AbstractQueryFilters.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/drive/ContentDriveHelper.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/drive/ContentDriveResource.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/job/JobParamsSchema.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/job/JobQueueDocs.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/system/permission/AbstractPermissionMetadataView.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/system/permission/AbstractUserPermissionAssetView.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/system/permission/PermissionSaveHelper.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/system/permission/PermissionUtils.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/system/permission/ResponseEntityPermissionMetadataView.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/system/permission/ResponseEntitySaveUserPermissionsView.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/system/permission/SaveUserPermissionsForm.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/system/permission/SaveUserPermissionsView.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/usage/ResponseEntityUsageSummaryView.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/usage/UsageResource.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/usage/UsageSummary.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/user/ResponseEntityUserPermissionsView.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/user/UserPermissionHelper.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/ResponseEntityWorkflowTasksView.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowSearchResultsView.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowSearcherForm.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowTaskView.java rename dotCMS/src/main/java/com/dotcms/rest/{tag/SingleTagForm.java => api/v2/tags/TagForm.java} (50%) create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v2/tags/TagValidationHelper.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v2/tags/UpdateTagForm.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/exception/ConflictException.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/exception/mapper/DotConflictExceptionMapper.java create mode 100644 dotCMS/src/main/java/com/dotcms/telemetry/DashboardMetric.java create mode 100644 dotCMS/src/main/java/com/dotcms/telemetry/collectors/DashboardMetricsProvider.java create mode 100644 dotCMS/src/main/java/com/dotcms/telemetry/collectors/content/ImportContentletsJobTriggeredMetricType.java delete mode 100644 dotCMS/src/main/java/com/dotcms/telemetry/collectors/experiment/ExperimentFeatureFlagMetricType.java create mode 100644 dotCMS/src/main/java/com/dotcms/telemetry/collectors/user/TotalUsersDatabaseMetricType.java create mode 100644 dotCMS/src/main/java/com/dotcms/util/pagination/TagsPaginator.java delete mode 100644 dotCMS/src/main/java/com/dotmarketing/business/BlockPageCacheImpl.java rename dotCMS/src/main/java/com/dotmarketing/business/{BlockPageCache.java => StaticPageCache.java} (95%) create mode 100644 dotCMS/src/main/java/com/dotmarketing/business/StaticPageCacheImpl.java create mode 100644 dotCMS/src/main/java/com/dotmarketing/comparators/GenericMapFieldComparator.java create mode 100644 dotCMS/src/main/java/com/dotmarketing/startup/runonce/Task250910AddAnalyticsDashboardPortletToMenu.java create mode 100644 dotCMS/src/main/java/com/dotmarketing/startup/runonce/Task251029RemoveContentTypesLegacyPortletFromLayouts.java create mode 100644 dotCMS/src/main/java/com/dotmarketing/startup/runonce/Task251103AddStylePropertiesColumnInMultiTree.java create mode 100644 dotCMS/src/main/resources/analytics/validators/content_click.json create mode 100644 dotCMS/src/main/resources/analytics/validators/content_impression.json create mode 100644 dotCMS/src/main/resources/analytics/validators/conversion.json delete mode 100644 dotCMS/src/main/resources/ca/ca-lib.js create mode 100644 dotCMS/src/main/resources/com/dotcms/ai/prompts/default-vision-prompt.json create mode 100644 dotCMS/src/main/resources/com/dotcms/ai/prompts/prompts.properties create mode 100644 dotCMS/src/main/resources/sql/rag_schema.sql create mode 100644 dotCMS/src/test/java/com/dotcms/architecture/CodingStandardsArchTest.java create mode 100644 dotCMS/src/test/java/com/dotmarketing/business/PageCacheParametersTest.java create mode 100644 dotCMS/src/test/java/com/dotmarketing/business/portal/PortletUrlTest.java create mode 100644 dotCMS/src/test/java/com/liferay/util/MapUtilTest.java create mode 100644 dotcms-integration/src/test/java/com/dotcms/ai/api/OpenAIVisionAPIImplTest.java create mode 100644 dotcms-integration/src/test/java/com/dotcms/contenttype/business/StoryBlockValidationTest.java create mode 100644 dotcms-integration/src/test/java/com/dotcms/contenttype/test/StoryBlockUtilTest.java create mode 100644 dotcms-integration/src/test/java/com/dotcms/publisher/assets/business/PushedAssetsFactoryTest.java create mode 100644 dotcms-integration/src/test/java/com/dotcms/rendering/velocity/RecycledHttpServletRequest.java create mode 100644 dotcms-integration/src/test/java/com/dotcms/rest/api/v1/drive/ContentDriveHelperContentletAPIComparisonTest.java create mode 100644 dotcms-integration/src/test/java/com/dotcms/rest/api/v1/system/permission/PermissionResourceIntegrationTest.java delete mode 100644 dotcms-integration/src/test/java/com/dotcms/telemetry/collectors/experiment/ExperimentFeatureFlagMetricTypeTest.java create mode 100644 dotcms-integration/src/test/java/com/dotmarketing/business/SecondaryCategoryPermissionTest.java create mode 100644 dotcms-integration/src/test/java/com/dotmarketing/portlets/workflows/actionlet/ResetApproversActionletTest.java create mode 100644 dotcms-integration/src/test/java/com/dotmarketing/quartz/job/ResetPermissionsJobTest.java create mode 100644 dotcms-integration/src/test/java/com/dotmarketing/startup/runonce/Task250910AddAnalyticsDashboardPortletToMenuTest.java create mode 100644 dotcms-integration/src/test/java/com/dotmarketing/startup/runonce/Task251029RemoveContentTypesLegacyPortletFromLayoutsTest.java create mode 100644 dotcms-integration/src/test/java/com/dotmarketing/startup/runonce/Task251103AddStylePropertiesColumnInMultiTreeTest.java create mode 100644 dotcms-postman/src/main/resources/postman/ContentDriveResource.postman_collection.json create mode 100644 dotcms-postman/src/main/resources/postman/resources/csv_with_errors.csv create mode 100644 e2e/dotcms-e2e-node/frontend/src/requests/contentType.ts create mode 100644 e2e/dotcms-e2e-node/frontend/src/requests/schemas.ts create mode 100644 e2e/dotcms-e2e-node/frontend/src/requests/sites.ts create mode 100644 e2e/dotcms-e2e-node/frontend/src/requests/workflowActions.ts create mode 100644 e2e/dotcms-e2e-node/frontend/src/tests/newEditContent/fields/siteOrFolder/select-default.spec.ts create mode 100644 examples/angular-ssr/.editorconfig create mode 100644 examples/angular-ssr/.env.example create mode 100644 examples/angular-ssr/.gitignore create mode 100644 examples/angular-ssr/.postcssrc.json create mode 100644 examples/angular-ssr/CLAUDE.md create mode 100644 examples/angular-ssr/README.md create mode 100644 examples/angular-ssr/angular.json create mode 100644 examples/angular-ssr/api/index.js create mode 100644 examples/angular-ssr/package-lock.json create mode 100644 examples/angular-ssr/package.json create mode 100644 examples/angular-ssr/public/favicon.ico create mode 100644 examples/angular-ssr/src/app/app.config.server.ts create mode 100644 examples/angular-ssr/src/app/app.config.ts rename core-web/libs/edit-content/src/lib/fields/dot-edit-content-key-value/dot-edit-content-key-value.component.css => examples/angular-ssr/src/app/app.css (100%) create mode 100644 examples/angular-ssr/src/app/app.html create mode 100644 examples/angular-ssr/src/app/app.routes.server.ts create mode 100644 examples/angular-ssr/src/app/app.routes.ts create mode 100644 examples/angular-ssr/src/app/app.spec.ts create mode 100644 examples/angular-ssr/src/app/app.ts create mode 100644 examples/angular-ssr/src/app/components/edit-contentlet-button/edit-contentlet-button.component.ts create mode 100644 examples/angular-ssr/src/app/components/footer/components/blogs/blogs.component.ts create mode 100644 examples/angular-ssr/src/app/components/footer/components/destinations/destinations.component.ts create mode 100644 examples/angular-ssr/src/app/components/footer/components/recommended-card/recommended-card.component.ts rename examples/{angular/src/app/shared => angular-ssr/src/app}/components/footer/footer.component.css (100%) create mode 100644 examples/angular-ssr/src/app/components/footer/footer.component.ts rename examples/{angular/src/app/pages/blog/blog-post/blog-post.component.css => angular-ssr/src/app/components/header/header.component.css} (100%) create mode 100644 examples/angular-ssr/src/app/components/header/header.component.ts create mode 100644 examples/angular-ssr/src/app/components/loading/loading.component.ts create mode 100644 examples/angular-ssr/src/app/components/navigation/navigation.component.css create mode 100644 examples/angular-ssr/src/app/components/navigation/navigation.component.ts create mode 100644 examples/angular-ssr/src/app/components/reorder-button/reorder-button.component.ts rename examples/{angular/src/app/content-types => angular-ssr/src/app/dotcms/components}/activity/activity.component.css (100%) create mode 100644 examples/angular-ssr/src/app/dotcms/components/activity/activity.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/components/banner-carousel/banner-carousel.component.html create mode 100644 examples/angular-ssr/src/app/dotcms/components/banner-carousel/banner-carousel.component.ts rename examples/{angular/src/app/content-types => angular-ssr/src/app/dotcms/components}/banner/banner.component.css (100%) create mode 100644 examples/angular-ssr/src/app/dotcms/components/banner/banner.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/components/category-filter/category-filter.component.ts rename examples/{angular/src/app/content-types => angular-ssr/src/app/dotcms/components}/image/image.component.css (100%) create mode 100644 examples/angular-ssr/src/app/dotcms/components/image/image.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/components/page-form/components/contact-us/contact-us.component.html create mode 100644 examples/angular-ssr/src/app/dotcms/components/page-form/components/contact-us/contact-us.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/components/page-form/page-form.component.ts rename examples/{angular/src/app/content-types => angular-ssr/src/app/dotcms/components}/product/product.component.css (100%) create mode 100644 examples/angular-ssr/src/app/dotcms/components/product/product.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/components/simple-widget/simple-widget.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/components/store-product-list/store-product-list.component.html create mode 100644 examples/angular-ssr/src/app/dotcms/components/store-product-list/store-product-list.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/components/vtl-include/components/destination-listing/destination-listing.component.html create mode 100644 examples/angular-ssr/src/app/dotcms/components/vtl-include/components/destination-listing/destination-listing.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/components/vtl-include/vtl-include.component.ts rename examples/{angular/src/app/content-types => angular-ssr/src/app/dotcms/components}/web-page-content/web-page-content.component.css (97%) create mode 100644 examples/angular-ssr/src/app/dotcms/components/web-page-content/web-page-content.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/pages/activity/activity-detail/activity-detail.component.css create mode 100644 examples/angular-ssr/src/app/dotcms/pages/activity/activity-detail/activity-detail.component.html create mode 100644 examples/angular-ssr/src/app/dotcms/pages/activity/activity-detail/activity-detail.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/pages/activity/activity.component.html create mode 100644 examples/angular-ssr/src/app/dotcms/pages/activity/activity.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/pages/blog-listing/blog-listing.component.html create mode 100644 examples/angular-ssr/src/app/dotcms/pages/blog-listing/blog-listing.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/pages/blog-listing/components/blog-card/blog-card.component.html create mode 100644 examples/angular-ssr/src/app/dotcms/pages/blog-listing/components/blog-card/blog-card.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/pages/blog-listing/components/search/search.component.html create mode 100644 examples/angular-ssr/src/app/dotcms/pages/blog-listing/components/search/search.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/pages/blog/blog-post/blog-post.component.css create mode 100644 examples/angular-ssr/src/app/dotcms/pages/blog/blog-post/blog-post.component.html create mode 100644 examples/angular-ssr/src/app/dotcms/pages/blog/blog-post/blog-post.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/pages/blog/blog-post/customRenderers/activity/activity.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/pages/blog/blog-post/customRenderers/paragraph/paragraph.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/pages/blog/blog.component.html create mode 100644 examples/angular-ssr/src/app/dotcms/pages/blog/blog.component.ts create mode 100644 examples/angular-ssr/src/app/dotcms/pages/page/page.css create mode 100644 examples/angular-ssr/src/app/dotcms/pages/page/page.html create mode 100644 examples/angular-ssr/src/app/dotcms/pages/page/page.spec.ts create mode 100644 examples/angular-ssr/src/app/dotcms/pages/page/page.ts create mode 100644 examples/angular-ssr/src/app/dotcms/types/contentlet.model.ts create mode 100644 examples/angular-ssr/src/indexFile.html create mode 100644 examples/angular-ssr/src/main.server.ts create mode 100644 examples/angular-ssr/src/main.ts create mode 100644 examples/angular-ssr/src/server.ts create mode 100644 examples/angular-ssr/src/styles.css create mode 100644 examples/angular-ssr/tsconfig.app.json create mode 100644 examples/angular-ssr/tsconfig.json create mode 100644 examples/angular-ssr/tsconfig.spec.json create mode 100644 examples/angular-ssr/vercel.json delete mode 100644 examples/angular/src/app/app.component.ts create mode 100644 examples/angular/src/app/app.config.server.ts create mode 100644 examples/angular/src/app/app.css create mode 100644 examples/angular/src/app/app.html create mode 100644 examples/angular/src/app/app.routes.server.ts create mode 100644 examples/angular/src/app/app.spec.ts create mode 100644 examples/angular/src/app/app.ts create mode 100644 examples/angular/src/app/components/edit-contentlet-button/edit-contentlet-button.component.ts create mode 100644 examples/angular/src/app/components/footer/components/blogs/blogs.component.ts create mode 100644 examples/angular/src/app/components/footer/components/destinations/destinations.component.ts create mode 100644 examples/angular/src/app/components/footer/components/recommended-card/recommended-card.component.ts rename examples/angular/src/app/{shared/components/error/error.component.css => components/footer/footer.component.css} (100%) create mode 100644 examples/angular/src/app/components/footer/footer.component.ts create mode 100644 examples/angular/src/app/components/header/header.component.css create mode 100644 examples/angular/src/app/components/header/header.component.ts create mode 100644 examples/angular/src/app/components/loading/loading.component.ts create mode 100644 examples/angular/src/app/components/navigation/navigation.component.css create mode 100644 examples/angular/src/app/components/navigation/navigation.component.ts create mode 100644 examples/angular/src/app/components/reorder-button/reorder-button.component.ts delete mode 100644 examples/angular/src/app/content-types/activity/activity.component.ts delete mode 100644 examples/angular/src/app/content-types/banner-carousel/banner-carousel.component.html delete mode 100644 examples/angular/src/app/content-types/banner-carousel/banner-carousel.component.ts delete mode 100644 examples/angular/src/app/content-types/banner/banner.component.ts delete mode 100644 examples/angular/src/app/content-types/category-filter/category-filter.component.ts delete mode 100644 examples/angular/src/app/content-types/custom-no-component/custom-no-component.component.ts delete mode 100644 examples/angular/src/app/content-types/image/image.component.ts delete mode 100644 examples/angular/src/app/content-types/page-form/components/contact-us/contact-us.component.html delete mode 100644 examples/angular/src/app/content-types/page-form/components/contact-us/contact-us.component.ts delete mode 100644 examples/angular/src/app/content-types/page-form/page-form.component.ts delete mode 100644 examples/angular/src/app/content-types/product/product.component.ts delete mode 100644 examples/angular/src/app/content-types/simple-widget/simple-widget.component.ts delete mode 100644 examples/angular/src/app/content-types/store-product-list/store-product-list.component.html delete mode 100644 examples/angular/src/app/content-types/store-product-list/store-product-list.component.ts delete mode 100644 examples/angular/src/app/content-types/vtl-include/components/destination-listing/destination-listing.component.html delete mode 100644 examples/angular/src/app/content-types/vtl-include/components/destination-listing/destination-listing.component.ts delete mode 100644 examples/angular/src/app/content-types/vtl-include/vtl-include.component.ts delete mode 100644 examples/angular/src/app/content-types/web-page-content/web-page-content.component.ts rename examples/angular/src/app/{shared/components/header/header.component.css => dotcms/components/activity/activity.component.css} (100%) create mode 100644 examples/angular/src/app/dotcms/components/activity/activity.component.ts create mode 100644 examples/angular/src/app/dotcms/components/banner-carousel/banner-carousel.component.html create mode 100644 examples/angular/src/app/dotcms/components/banner-carousel/banner-carousel.component.ts rename examples/angular/src/app/{shared/components/navigation/navigation.component.css => dotcms/components/banner/banner.component.css} (100%) create mode 100644 examples/angular/src/app/dotcms/components/banner/banner.component.ts create mode 100644 examples/angular/src/app/dotcms/components/category-filter/category-filter.component.ts create mode 100644 examples/angular/src/app/dotcms/components/image/image.component.css create mode 100644 examples/angular/src/app/dotcms/components/image/image.component.ts create mode 100644 examples/angular/src/app/dotcms/components/page-form/components/contact-us/contact-us.component.html create mode 100644 examples/angular/src/app/dotcms/components/page-form/components/contact-us/contact-us.component.ts create mode 100644 examples/angular/src/app/dotcms/components/page-form/page-form.component.ts create mode 100644 examples/angular/src/app/dotcms/components/product/product.component.css create mode 100644 examples/angular/src/app/dotcms/components/product/product.component.ts create mode 100644 examples/angular/src/app/dotcms/components/simple-widget/simple-widget.component.ts create mode 100644 examples/angular/src/app/dotcms/components/store-product-list/store-product-list.component.html create mode 100644 examples/angular/src/app/dotcms/components/store-product-list/store-product-list.component.ts create mode 100644 examples/angular/src/app/dotcms/components/vtl-include/components/destination-listing/destination-listing.component.html create mode 100644 examples/angular/src/app/dotcms/components/vtl-include/components/destination-listing/destination-listing.component.ts create mode 100644 examples/angular/src/app/dotcms/components/vtl-include/vtl-include.component.ts create mode 100644 examples/angular/src/app/dotcms/components/web-page-content/web-page-content.component.css create mode 100644 examples/angular/src/app/dotcms/components/web-page-content/web-page-content.component.ts create mode 100644 examples/angular/src/app/dotcms/pages/activity/activity-detail/activity-detail.component.css create mode 100644 examples/angular/src/app/dotcms/pages/activity/activity-detail/activity-detail.component.html create mode 100644 examples/angular/src/app/dotcms/pages/activity/activity-detail/activity-detail.component.ts create mode 100644 examples/angular/src/app/dotcms/pages/activity/activity.component.html create mode 100644 examples/angular/src/app/dotcms/pages/activity/activity.component.ts create mode 100644 examples/angular/src/app/dotcms/pages/blog-listing/blog-listing.component.html create mode 100644 examples/angular/src/app/dotcms/pages/blog-listing/blog-listing.component.ts create mode 100644 examples/angular/src/app/dotcms/pages/blog-listing/components/blog-card/blog-card.component.html create mode 100644 examples/angular/src/app/dotcms/pages/blog-listing/components/blog-card/blog-card.component.ts create mode 100644 examples/angular/src/app/dotcms/pages/blog-listing/components/search/search.component.html create mode 100644 examples/angular/src/app/dotcms/pages/blog-listing/components/search/search.component.ts create mode 100644 examples/angular/src/app/dotcms/pages/blog/blog-post/blog-post.component.css create mode 100644 examples/angular/src/app/dotcms/pages/blog/blog-post/blog-post.component.html create mode 100644 examples/angular/src/app/dotcms/pages/blog/blog-post/blog-post.component.ts create mode 100644 examples/angular/src/app/dotcms/pages/blog/blog-post/customRenderers/activity/activity.component.ts create mode 100644 examples/angular/src/app/dotcms/pages/blog/blog-post/customRenderers/paragraph/paragraph.component.ts create mode 100644 examples/angular/src/app/dotcms/pages/blog/blog.component.html create mode 100644 examples/angular/src/app/dotcms/pages/blog/blog.component.ts create mode 100644 examples/angular/src/app/dotcms/pages/page/page.css create mode 100644 examples/angular/src/app/dotcms/pages/page/page.html create mode 100644 examples/angular/src/app/dotcms/pages/page/page.spec.ts create mode 100644 examples/angular/src/app/dotcms/pages/page/page.ts create mode 100644 examples/angular/src/app/dotcms/types/contentlet.model.ts delete mode 100644 examples/angular/src/app/pages/blog-listing/blog-listing.component.html delete mode 100644 examples/angular/src/app/pages/blog-listing/blog-listing.component.ts delete mode 100644 examples/angular/src/app/pages/blog-listing/components/blog-card/blog-card.component.html delete mode 100644 examples/angular/src/app/pages/blog-listing/components/blog-card/blog-card.component.ts delete mode 100644 examples/angular/src/app/pages/blog-listing/components/search/search.component.html delete mode 100644 examples/angular/src/app/pages/blog-listing/components/search/search.component.ts delete mode 100644 examples/angular/src/app/pages/blog/blog-post/blog-post.component.html delete mode 100644 examples/angular/src/app/pages/blog/blog-post/blog-post.component.ts delete mode 100644 examples/angular/src/app/pages/blog/blog-post/customRenderers/activity/activity.component.ts delete mode 100644 examples/angular/src/app/pages/blog/blog-post/customRenderers/paragraph/paragraph.component.ts delete mode 100644 examples/angular/src/app/pages/blog/blog.component.html delete mode 100644 examples/angular/src/app/pages/blog/blog.component.ts delete mode 100644 examples/angular/src/app/pages/dot-cms-page/dot-cms-page.component.html delete mode 100644 examples/angular/src/app/pages/dot-cms-page/dot-cms-page.component.ts delete mode 100644 examples/angular/src/app/services/editable-page.service.ts delete mode 100644 examples/angular/src/app/services/page.service.ts delete mode 100644 examples/angular/src/app/shared/components/edit-contentlet-button/edit-contentlet-button.component.ts delete mode 100644 examples/angular/src/app/shared/components/error/error.component.ts delete mode 100644 examples/angular/src/app/shared/components/footer/components/blogs/blogs.component.ts delete mode 100644 examples/angular/src/app/shared/components/footer/components/destinations/destinations.component.ts delete mode 100644 examples/angular/src/app/shared/components/footer/components/recommended-card/recommended-card.component.ts delete mode 100644 examples/angular/src/app/shared/components/footer/footer.component.ts delete mode 100644 examples/angular/src/app/shared/components/header/header.component.ts delete mode 100644 examples/angular/src/app/shared/components/loading/loading.component.ts delete mode 100644 examples/angular/src/app/shared/components/navigation/navigation.component.ts delete mode 100644 examples/angular/src/app/shared/components/reorder-button/reorder-button.component.ts delete mode 100644 examples/angular/src/app/shared/contentlet.model.ts delete mode 100644 examples/angular/src/app/shared/dynamic-components.ts delete mode 100644 examples/angular/src/app/shared/models.ts delete mode 100644 examples/angular/src/app/shared/queries.ts create mode 100644 starter/empty_20251006.zip create mode 100644 tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/common/DotCliIgnore.java create mode 100644 tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/common/DotCliIgnoreFileFilter.java create mode 100644 tools/dotcms-cli/cli/src/main/resources/.dotcliignore.example create mode 100644 tools/dotcms-cli/cli/src/test/java/com/dotcms/cli/common/DotCliIgnoreTest.java diff --git a/.claude/skills/cicd-diagnostics/BEST_PRACTICES_ASSESSMENT.md b/.claude/skills/cicd-diagnostics/BEST_PRACTICES_ASSESSMENT.md new file mode 100644 index 000000000000..22aab133e0b4 --- /dev/null +++ b/.claude/skills/cicd-diagnostics/BEST_PRACTICES_ASSESSMENT.md @@ -0,0 +1,102 @@ +# Skill Best Practices Assessment + +## ✅ Best Practices Compliance + +### Required Metadata (All Present) +- ✅ **name**: `cicd-diagnostics` (15 chars, under 64 limit) +- ✅ **description**: 199 characters (under 200 limit) - concise and specific +- ✅ **version**: `2.0.0` (tracking versions) +- ✅ **dependencies**: `python>=3.8` (clearly specified) + +### Best Practice Guidelines + +#### ✅ Focused on One Workflow +The skill is focused on CI/CD failure diagnosis - a single, well-defined task. + +#### ✅ Clear Instructions +The skill provides comprehensive instructions for: +- When to use the skill (extensive trigger list) +- How to use the skill (step-by-step workflow) +- What utilities are available +- Examples throughout + +#### ✅ Examples Included +The skill includes: +- Code examples for Python utilities +- Example prompts that trigger the skill +- Example analysis outputs +- Example diagnostic reports + +#### ✅ Defines When to Use +Extensive "When to Use This Skill" section with: +- Primary triggers (always use) +- Context indicators (use when mentioned) +- Don't use scenarios (when NOT to use) + +### âš ï¸ Areas for Improvement + +#### 1. File Length +- **Current**: 1,130 lines +- **Best Practice**: Keep concise (<500 lines recommended) +- **Issue**: SKILL.md is very comprehensive but verbose +- **Recommendation**: Consider moving detailed sections to reference files (REFERENCE.md) + +#### 2. Duplicate Files +- **Issue**: Both `Skill.md` and `SKILL.md` exist (appear identical) +- **Recommendation**: Use only `SKILL.md` (uppercase) per Claude conventions + +#### 3. Structure Alignment +- **Current**: Single large SKILL.md with all content +- **Best Practice**: Use progressive disclosure with reference files +- **Recommendation**: Move detailed technical content to REFERENCE.md + +### Comparison with Example Skills + +#### Similarities to Examples: +- ✅ YAML frontmatter with required fields +- ✅ Clear description under 200 chars +- ✅ Version tracking +- ✅ Dependencies specified +- ✅ Python scripts for utilities +- ✅ Clear when-to-use guidance + +#### Differences from Examples: +- âš ï¸ Much longer than typical examples (examples are usually 200-500 lines) +- âš ï¸ More comprehensive/verbose than typical +- âš ï¸ Could benefit from progressive disclosure (main SKILL.md + REFERENCE.md) + +### Recommendations + +1. **Keep SKILL.md focused on core workflow** (<500 lines) + - Move detailed technical content to REFERENCE.md + - Keep examples concise + - Focus on "how to use" not "everything about" + +2. **Remove duplicate file** + - Keep only `SKILL.md` (uppercase) + - Delete `Skill.md` if identical + +3. **Maintain current strengths** + - Excellent description (199 chars, specific) + - Clear Python implementation + - Good examples + - Well-defined triggers + +### Overall Assessment + +**Score: 8/10** + +**Strengths:** +- ✅ Excellent metadata (all required fields, proper length) +- ✅ Clear Python implementation (best practice) +- ✅ Comprehensive examples +- ✅ Well-defined use cases +- ✅ Version tracking + +**Areas for Improvement:** +- âš ï¸ File length (too verbose for SKILL.md) +- âš ï¸ Consider progressive disclosure structure +- âš ï¸ Remove duplicate file + +**Conclusion:** The skill follows most best practices well, especially the critical ones (description length, Python implementation, clear triggers). The main improvement would be to make SKILL.md more concise by moving detailed content to reference files, following the progressive disclosure pattern recommended in best practices. + diff --git a/.claude/skills/cicd-diagnostics/BEST_PRACTICES_COMPLIANCE.md b/.claude/skills/cicd-diagnostics/BEST_PRACTICES_COMPLIANCE.md new file mode 100644 index 000000000000..5f8f15565d0d --- /dev/null +++ b/.claude/skills/cicd-diagnostics/BEST_PRACTICES_COMPLIANCE.md @@ -0,0 +1,130 @@ +# Best Practices Compliance Assessment + +Based on: https://docs.claude.com/en/docs/agents-and-tools/agent-skills/best-practices + +## ✅ Fully Compliant + +### 1. Naming Conventions +- ✅ **SKILL.md** (uppercase) - Correct convention +- ✅ **name**: `cicd-diagnostics` (lowercase, hyphens, under 64 chars) +- ✅ **File naming**: Descriptive names (workspace.py, github_api.py, evidence.py) + +### 2. YAML Frontmatter +- ✅ **name**: Present, valid format (lowercase, hyphens) +- ✅ **description**: Present, 199 chars (under 1024 limit) +- ✅ **version**: Present (2.0.0) - optional but good practice +- ✅ **dependencies**: Present (python>=3.8) - optional but good practice + +### 3. Description Quality +- ✅ Describes what the skill does +- ✅ Describes when to use it +- ✅ Includes key terms (CI/CD, GitHub Actions, DotCMS, failures, tests) +- ✅ Concise and specific + +### 4. File Structure +- ✅ Uses forward slashes (no Windows paths) +- ✅ Descriptive file names +- ✅ Organized directory structure (utils/ subdirectory) +- ✅ Reference files exist (WORKFLOWS.md, LOG_ANALYSIS.md, etc.) + +### 5. Code and Scripts +- ✅ Python scripts solve problems (don't punt to Claude) +- ✅ Clear documentation in scripts +- ✅ No Windows-style paths +- ✅ Dependencies clearly listed + +## âš ï¸ Areas Needing Improvement + +### 1. SKILL.md Length (CRITICAL) +- **Current**: 1,042 lines +- **Best Practice**: Under 500 lines for optimal performance +- **Issue**: SKILL.md is too verbose - exceeds recommended length by 2x +- **Impact**: Higher token usage, slower loading, harder for Claude to navigate + +**Recommendation**: Apply progressive disclosure pattern: +- Keep core workflow in SKILL.md (<500 lines) +- Move detailed technical content to REFERENCE.md +- Move extensive examples to EXAMPLES.md +- Keep "When to Use" section but make it more concise + +### 2. Progressive Disclosure +- **Current**: Some reference files exist but SKILL.md still contains too much detail +- **Best Practice**: SKILL.md should be high-level guide pointing to reference files +- **Recommendation**: Refactor to follow Pattern 1 (High-level guide with references) + +### 3. Concise Content +- **Current**: Some sections explain things Claude already knows +- **Best Practice**: "Default assumption: Claude is already very smart" +- **Recommendation**: Remove explanations of basic concepts (what GitHub Actions is, what Python is, etc.) + +## 📋 Detailed Checklist + +### Core Quality +- ✅ Description is specific and includes key terms +- ✅ Description includes both what and when to use +- ⌠SKILL.md body is under 500 lines (currently 1,042) +- âš ï¸ Additional details are in separate files (partially - need more) +- ✅ No time-sensitive information +- ✅ Consistent terminology throughout +- ✅ Examples are concrete, not abstract +- ✅ File references are one level deep +- âš ï¸ Progressive disclosure used appropriately (needs improvement) +- ✅ Workflows have clear steps + +### Code and Scripts +- ✅ Scripts solve problems rather than punt to Claude +- ✅ Error handling is explicit and helpful +- ✅ No "voodoo constants" (all values justified) +- ✅ Required packages listed in instructions +- ✅ Scripts have clear documentation +- ✅ No Windows-style paths (all forward slashes) +- ✅ Validation/verification steps for critical operations +- ✅ Feedback loops included for quality-critical tasks + +### Structure Alignment +- ✅ YAML frontmatter correct +- ✅ File naming follows conventions +- âš ï¸ SKILL.md should be more concise (progressive disclosure) +- ✅ Reference files exist +- ✅ Utils directory organized + +## Recommendations + +### High Priority +1. **Refactor SKILL.md to <500 lines** + - Move detailed technical expertise to `REFERENCE.md` + - Move extensive examples to `EXAMPLES.md` + - Keep only core workflow and essential instructions in SKILL.md + - Use progressive disclosure pattern + +2. **Apply "Concise is Key" principle** + - Remove explanations Claude already knows + - Challenge each paragraph: "Does Claude really need this?" + - Assume Claude knows GitHub Actions, Python, CI/CD basics + +### Medium Priority +3. **Enhance progressive disclosure** + - SKILL.md should be a high-level guide + - Reference files should contain detailed content + - Clear navigation between files + +4. **Optimize description** (optional) + - Current description is good (199 chars) + - Could potentially expand to include more key terms if needed + - But current length is fine + +## Overall Score: 7.5/10 + +**Strengths:** +- ✅ Excellent naming and structure +- ✅ Good description +- ✅ Proper Python implementation +- ✅ Clear file organization +- ✅ No Windows paths or anti-patterns + +**Critical Issue:** +- ⌠SKILL.md is 1,042 lines (should be <500) + +**Conclusion:** The skill follows most best practices well, but needs refactoring to reduce SKILL.md length using progressive disclosure. This is the most important improvement needed to align with best practices. + + diff --git a/.claude/skills/cicd-diagnostics/CHANGELOG.md b/.claude/skills/cicd-diagnostics/CHANGELOG.md new file mode 100644 index 000000000000..05387394d5e0 --- /dev/null +++ b/.claude/skills/cicd-diagnostics/CHANGELOG.md @@ -0,0 +1,384 @@ +# CI/CD Diagnostics Skill - Changelog + +## Version 2.2.2 - 2025-11-10 (Parameter Validation Improvement) + +### Problem +The `fetch-logs.py` script's parameter validation was too simplistic, causing false positives when the workspace path ended with a run ID (e.g., `.claude/diagnostics/run-19219835536`). The validation checked if the workspace parameter was all digits, but didn't account for long run IDs appearing in valid paths. + +### Solution +Improved the validation logic to distinguish between: +- **Valid workspace paths** that may contain digits (e.g., `/path/to/run-19219835536`) +- **Job IDs** that are purely numeric and typically 11+ digits long + +### Changes Made +- Updated `fetch-logs.py` line 39: Changed validation from `workspace_path.isdigit()` to `workspace_path.isdigit() and len(workspace_path) > 10` +- This allows paths containing run IDs to pass validation while still catching parameter order mistakes + +### Before +```python +if workspace_path.isdigit(): + # Would incorrectly trigger on paths like "run-19219835536" +``` + +### After +```python +if workspace_path.isdigit() and len(workspace_path) > 10: + # Only triggers on pure job IDs (11+ digits), not paths with numbers +``` + +### Impact +- **Fixed false positives** - Valid workspace paths with run IDs no longer trigger validation errors +- **Maintained error detection** - Still catches actual parameter order mistakes (e.g., swapping workspace and job ID) +- **Better user experience** - Clear error messages when parameters are truly in wrong order +- **No breaking changes** - All correct usage continues to work + +### Testing +Validated with: +- ✅ Correct order: `fetch-logs.py 19219835536 /path/to/run-19219835536 54939324205` (works) +- ✅ Wrong order detection: `fetch-logs.py /path/to/workspace 54939324205` (correctly caught) +- ✅ Path with run ID: `.claude/diagnostics/run-19219835536` (no longer false positive) + +--- + +## Version 2.2.1 - 2025-11-10 (Parameter Consistency Documentation Fix) + +### Problem +The SKILL.md documentation showed a complex Python code block for calling `fetch-logs.py`, which made it easy to confuse parameter order. The error occurred because: +- Documentation showed nested Python subprocess calls instead of direct Bash +- Parameter order wasn't emphasized clearly +- Inconsistent presentation across different scripts + +### Solution +1. **Simplified documentation** - Replaced complex Python examples with straightforward Bash commands +2. **Added parameter order emphasis** - Clearly stated "All scripts follow the same pattern: [optional]" +3. **Added error prevention tips** - Documented common error and how to fix it +4. **Consistent examples** - All three scripts now show consistent usage + +### Changes Made +- Updated SKILL.md section "3. Download Failed Job Logs" to use simple Bash syntax +- Updated SKILL.md section "2. Fetch Workflow Data" to emphasize consistent parameter order +- Added parameter order documentation and tips + +### Before +```python +# Complex Python code calling subprocess +subprocess.run([ + "python3", ".claude/skills/cicd-diagnostics/fetch-logs.py", + "19131365567", # RUN_ID + str(WORKSPACE), # WORKSPACE path + str(failed_job_id) # JOB_ID (optional) +]) +``` + +### After +```bash +# Simple, clear Bash command +python3 .claude/skills/cicd-diagnostics/fetch-logs.py \ + "$RUN_ID" \ + "$WORKSPACE" \ + 54939324205 # JOB_ID from fetch-jobs.py output +``` + +### Impact +- **No code changes required** - The actual Python scripts were already correct +- **Documentation clarity improved** - Easier to understand and use correctly +- **Error prevention** - Clear parameter order reduces mistakes +- **Consistency** - All three scripts now documented the same way + +--- + +## Version 2.2.0 - 2025-11-10 (Flexibility & AI-Driven Investigation) + +### Philosophy Change: From Checklist to Investigation + +**Problem:** Previous version (2.1.0) had numbered steps (0-10) that felt prescriptive and rigid. Risk of the AI following steps mechanically rather than adapting to findings. + +**Solution:** Redesigned as an adaptive, evidence-driven investigation framework. + +### Major Changes + +#### 1. Investigation Decision Tree (NEW) + +Added visual decision tree to guide investigation approach based on failure type: + +``` +Test Failure → Check code changes + Known issues +Deployment Failure → CHECK EXTERNAL ISSUES FIRST +Infrastructure Failure → Check logs + Patterns +``` + +**Decision points at key stages:** +- After evidence: External issue or internal? +- After known issues: Duplicate or new? +- After analysis: Confidence HIGH/MEDIUM/LOW? + +#### 2. Removed Rigid Step Numbers + +**Before:** +``` +### 0. Setup and Load Utilities +### 1. Identify Target +### 2. Fetch Workflow Data +... +### 10. Create Issue +``` + +**After:** +``` +## Investigation Toolkit + +Use these techniques flexibly: + +### Setup and Load Utilities (Always Start Here) +### Identify Target and Create Workspace +### Fetch Workflow Data +... +### Create Issue (if needed) +``` + +**Impact:** AI can now skip irrelevant steps, reorder techniques, and adapt depth based on findings. + +#### 3. Conditional Guidance Added + +Every major technique now has "When to use" guidance: + +**Example - Check Known Issues:** +``` +Check External Issues when evidence suggests: +- 🔴 HIGH Priority - Authentication errors + service names +- 🟡 MEDIUM Priority - Infrastructure errors + timing +- ⚪ LOW Priority - Test failures with clear assertions + +Skip external checks if: +- Test assertion failure with obvious code bug +- Known flaky test already documented +``` + +#### 4. Enhanced Key Principles + +**New Principle: Tool Selection Based on Failure Type** + +| Failure Type | Primary Tools | Skip | +|--------------|---------------|------| +| Deployment/Auth | external_issues.py, WebSearch | Deep log analysis | +| Test assertion | Code changes, test history | External checks | +| Flaky test | Run history, timing patterns | External checks | + +**Updated Principle: Adaptive Investigation Depth** + +``` +Quick Win (30 sec - 2 min) → Known issue? Clear error? +Standard Investigation (2-10 min) → Gather, hypothesize, test +Deep Dive (10+ min) → Unclear patterns, multiple theories +``` + +**Don't always do everything - Stop when confident.** + +#### 5. Natural Reporting Guidelines + +**Before:** Fixed template with 8 required sections + +**After:** Write naturally with relevant sections: +- Core sections (always): Summary, Root Cause, Evidence, Recommendations +- Optional sections: Known Issues, Timeline, Test Fingerprint (when relevant) + +**Guideline:** "A deployment authentication error doesn't need a 'Test Fingerprint' section." + +### Success Criteria Updated + +**Changed focus from checklist completion to investigation quality:** + +**Investigation Quality:** +- ✅ Used adaptive investigation depth (stopped when confident) +- ✅ Let evidence guide technique selection (didn't use every tool blindly) +- ✅ Made appropriate use of external validation (when patterns suggest it) + +**Removed rigid requirements:** +- ⌠"Checked known issues" → ✅ "Assessed whether this is a known issue (when relevant)" +- ⌠"Validated external dependencies" → ✅ "Made appropriate use of external validation" + +### Examples of Improved Flexibility + +**Scenario 1: Clear Test Assertion Failure** +- **Old behavior:** Still checks external issues, runs full diagnostic +- **New behavior:** Quickly identifies code change, checks internal issues, done + +**Scenario 2: NPM Authentication Error** +- **Old behavior:** Goes through all 10 steps sequentially +- **New behavior:** Decision tree → Deployment failure → Check external FIRST → Find npm security update → Done + +**Scenario 3: Unclear Pattern** +- **Old behavior:** Might stop at step 7 without deep analysis +- **New behavior:** Recognizes low confidence → Gathers more context → Compares runs → Forms conclusion + +### Backward Compatibility + +✅ All utilities unchanged - still work the same way +✅ Evidence extraction unchanged - same quality +✅ External issue detection - still available when needed +✅ No breaking changes to existing functionality + +### Documentation Impact + +- **SKILL.md:** Complete restructure (~200 lines changed) +- **Philosophy section:** New 6-point investigation pattern +- **Decision tree:** New visual guide +- **Key Principles:** Rewritten with flexibility focus +- **Success Criteria:** Shifted from compliance to quality + +--- + +## Version 2.1.0 - 2025-11-10 + +### Major Enhancements + +#### 1. External Issue Detection (NEW) + +**Problem Solved:** Skill was missing critical external service changes (like npm security updates) that cause CI/CD failures. + +**Solution:** Added comprehensive external issue detection system. + +**New Capabilities:** +- **Automated pattern detection** for npm, Docker, GitHub Actions errors +- **Likelihood assessment** (LOW/MEDIUM/HIGH) for external causes +- **Targeted web search generation** based on error patterns +- **Service-specific checks** with direct links to status pages +- **Timeline correlation** to detect service change impacts + +**New Files:** +- `utils/external_issues.py` - External issue detection utilities + - `extract_error_indicators()` - Parse logs for external error patterns + - `generate_search_queries()` - Create targeted web searches + - `suggest_external_checks()` - Recommend which services to verify + - `format_external_issue_report()` - Generate markdown report section + +**Updated Files:** +- `SKILL.md` - Added Step 5: "Check Known Issues (Internal and External)" + - Automated detection using new utility + - Internal GitHub issue searches + - External web searches for high-likelihood issues + - Correlation analysis with red flags + +**Success Criteria Updated:** +- ✅ **Checked known issues - internal (GitHub) AND external (service changes)** +- ✅ **Validated external dependencies (npm, Docker, GitHub Actions) if relevant** +- ✅ Generated comprehensive natural report **with external context** + +#### 2. Improved Error Detection in Logs + +**Problem Solved:** NPM OTP errors and other critical deployment failures were buried under transient Docker errors. + +**Solution:** Enhanced evidence extraction to prioritize and properly detect critical errors. + +**Changes to `utils/evidence.py`:** +- **Enhanced error keyword detection:** + - Added `npm ERR!`, `::error::`, `##[error]` + - Added `FAILURE:`, `Failed to`, `Cannot`, `Unable to` + +- **Smart filtering:** + - Skip false positives (`.class` files, `.jar` references) + - Distinguish between recoverable vs. fatal errors + +- **Prioritization:** + - Scan entire log (not just first 100 lines) + - Show **last 10 error groups** (final/fatal errors) + - Provide more context (10 lines vs 6 lines after error) + +- **Two-pass strategy:** + - First pass: Critical deployment/infrastructure errors + - Second pass: Test errors (if no critical errors found) + +**Before:** +``` +ERROR MESSAGES === +[Shows first 100 lines of Docker blob errors, stops] +[NPM OTP error at line 38652 never shown] +``` + +**After:** +``` +ERROR MESSAGES === +[Shows last 10 critical error groups from entire log] +[NPM OTP error properly captured and displayed] +``` + +### Bug Fixes + +1. **Path handling in Python scripts** - Scripts now work correctly when called from any directory +2. **Step numbering** - Fixed duplicate step 6, renumbered workflow steps (5-10) +3. **Evidence limit** - Increased from 100 to 150 lines to capture more context +4. **Smart file listing filter** - Fixed overly aggressive `.class` file filtering: + - **Before:** Skipped ANY line containing `.class` (would miss real errors like `ERROR: Failed to load class MyClass`) + - **After:** Only skip lines that are pure file listings (tar/zip output) without error keywords + - **Logic:** Skip line ONLY if it contains `.class` AND path pattern (`maven/dotserver`) AND NO error keywords (`ERROR:`, `FAILURE:`, `Failed`, `Exception:`) + - **Result:** Now captures real Java class loading errors while filtering file listings + +### Documentation Updates + +**README.md:** +- Added external issue detection to capabilities +- Updated examples to show external validation + +**SKILL.md:** +- Restructured diagnostic workflow (0-10 steps) +- Added detailed Step 5 with external issue checking +- Updated success criteria +- Added external_issues.py utility reference + +### Examples Added + +**NPM Security Update (November 2025):** +- Demonstrates detecting npm classic token revocation +- Shows correlation with failure timeline +- Provides migration path recommendations + +**Detection Pattern:** +``` +🔴 External Cause Likelihood: HIGH + +Indicators: +- NPM authentication errors (EOTP/ENEEDAUTH) often caused by + npm registry policy changes +- Multiple consecutive failures suggest external change + +Recommended Web Searches: +- npm EOTP authentication error November 2025 +- npm classic token revoked 2025 +``` + +### Migration Notes + +**For existing diagnostics:** +1. Re-run skill on historical failures to check for external causes +2. Update any diagnosis reports to include external validation +3. Use new utility for future diagnostics + +**No breaking changes** - All existing functionality preserved. + +### Testing + +Validated with: +- Run 19219835536 (nightly build failure Nov 10, 2025) +- Successfully identified npm EOTP error +- Detected npm security update as external cause +- Generated accurate timeline correlation +- Provided actionable migration recommendations + +### Future Enhancements + +Potential additions for future versions: +- Expand external_issues.py to detect more service patterns +- Add caching for web search results +- Create database of known external service changes +- Add Slack/email notifications for external issues +- Integration with service status APIs + +--- + +## Version 2.0.0 - 2025-11-07 + +Initial Python-based implementation with evidence-driven analysis. + +## Version 1.0.0 - 2025-10-15 + +Initial bash-based implementation. diff --git a/.claude/skills/cicd-diagnostics/ENHANCEMENTS.md b/.claude/skills/cicd-diagnostics/ENHANCEMENTS.md new file mode 100644 index 000000000000..53864aef4c48 --- /dev/null +++ b/.claude/skills/cicd-diagnostics/ENHANCEMENTS.md @@ -0,0 +1,351 @@ +# CI/CD Diagnostics Skill Enhancements + +**Date:** 2025-11-06 +**Status:** ✅ Tiered Extraction and Retry Analysis Complete + +--- + +## Problem Statement + +The original error extraction approach had a critical limitation: + +``` +Error: File content (33,985 tokens) exceeds maximum allowed tokens (25,000) +``` + +Even after extracting "error sections only" from an 11.5MB log file, the resulting file was still **too large to process in a single Read operation**. This made it impossible for the AI to analyze the evidence without manual chunking. + +--- + +## Solution: Tiered Evidence Extraction + +### Core Innovation + +Instead of a single extraction level, we now create **three progressively detailed levels** that allow the AI to: + +1. **Start with a quick overview** (Level 1 - always fits in context) +2. **Get detailed errors** (Level 2 - moderate detail) +3. **Deep dive if needed** (Level 3 - comprehensive context) + +### Implementation + +**New File:** `.claude/skills/cicd-diagnostics/utils/tiered-extraction.sh` + +#### Level 1: Test Summary (~1,500 tokens) +```bash +extract_level1_summary LOG_FILE OUTPUT_FILE +``` + +**Contents:** +- Overall test results (pass/fail counts) +- List of failed test names (no details) +- Retry patterns summary +- Classification hints (timeout count, assertion count, NPE count, infra errors) + +**Size:** ~6,222 bytes (~1,555 tokens) - **Always readable** + +**Use Case:** Quick triage - "What failed and why might it have failed?" + +#### Level 2: Unique Failures (~6,000 tokens) +```bash +extract_level2_unique_failures LOG_FILE OUTPUT_FILE +``` + +**Contents:** +- Deterministic failures with retry counts (4/4 failed = blocking bug) +- Flaky tests with pass/fail breakdown (2/4 failed = timing issue) +- First occurrence of each unique error type: + - ConditionTimeoutException (Awaitility failures) + - AssertionError / ComparisonFailure + - NullPointerException + - Other exceptions + +**Size:** ~24,624 bytes (~6,156 tokens) - **Fits in context** + +**Use Case:** Detailed analysis - "What's the actual error message and pattern?" + +#### Level 3: Full Context (~21,000 tokens) +```bash +extract_level3_full_context LOG_FILE OUTPUT_FILE +``` + +**Contents:** +- Complete retry analysis with all attempts +- All error sections with full stack traces +- Timing correlation (errors with timestamps) +- Infrastructure events (Docker, DB, ES failures) +- Test execution timeline for failed tests + +**Size:** ~86,624 bytes (~21,656 tokens) - **Just fits in context** + +**Use Case:** Deep investigation - "Show me everything about this failure" + +### Auto-Tiered Extraction + +```bash +auto_extract_tiered LOG_FILE WORKSPACE +``` + +**Smart behavior:** +- Always creates Level 1 (summary) +- Always creates Level 2 (unique failures) +- Only creates Level 3 if log > 5MB (for complex cases) + +**Output:** +``` +=== Auto-Tiered Extraction === +Log size: 11 MB + +Creating Level 1 (Summary)... +✓ Level 1 created: 6222 bytes (~1555 tokens) + +Creating Level 2 (Unique Failures)... +✓ Level 2 created: 24624 bytes (~6156 tokens) + +Creating Level 3 (Full Context) - large log detected... +✓ Level 3 created: 86624 bytes (~21656 tokens) + +=== Tiered Extraction Complete === +Analysis workflow: +1. Read Level 1 for quick overview and classification hints +2. Read Level 2 for detailed error messages and retry patterns +3. Read Level 3 (if exists) for deep dive analysis +``` + +--- + +## Enhancement 2: Automated Retry Pattern Analysis + +### Problem + +The original diagnosis required manual analysis to distinguish: +- **Deterministic failures** (test fails 100% of the time = real bug) +- **Flaky tests** (test fails sometimes = timing/concurrency issue) + +This distinction is **critical** for proper diagnosis and prioritization. + +### Solution + +**New File:** `.claude/skills/cicd-diagnostics/utils/retry-analyzer.sh` + +```bash +analyze_simple_retry_patterns LOG_FILE +``` + +**Output:** +``` +================================================================================ +RETRY PATTERN ANALYSIS +================================================================================ + +Surefire retry mechanism detected + +=== DETERMINISTIC FAILURES (All Retries Failed) === + • com.dotcms.publisher.business.PublisherTest.autoUnpublishContent - Failed 4/4 retries (100% failure rate) + +=== FLAKY TESTS (Passed Some Retries) === + • com.dotcms.publisher.business.PublisherTest.testPushArchivedAndMultiLanguageContent - Failed 2/4 retries (50% failure rate, 2 passed) + • com.dotcms.publisher.business.PublisherTest.testPushContentWithUniqueField - Failed 2/4 retries (50% failure rate, 2 passed) + • com.dotmarketing.startup.runonce.Task240306MigrateLegacyLanguageVariablesTest.testBothFilesMapToSameLanguageWithPriorityHandling - Failed 1/2 retries (50% failure rate, 1 passed) + +=== SUMMARY === +Deterministic failures: 1 test(s) +Flaky tests: 3 test(s) +Total problematic tests: 4 + +âš ï¸ BLOCKING: 1 deterministic failure(s) detected + These tests failed ALL retry attempts - indicates real bugs or incomplete fixes +âš ï¸ WARNING: 3 flaky test(s) detected + These tests passed some retries - indicates timing/concurrency issues + +================================================================================ +``` + +### Key Benefits + +1. **Immediate Classification:** Instantly see which failures are blocking vs flaky +2. **Retry Context:** Understand failure rates (4/4 vs 2/4 tells completely different stories) +3. **Actionable Guidance:** Clear labeling of BLOCKING vs WARNING severity +4. **No Manual Counting:** Automatically parses Surefire retry summary format + +--- + +## Impact Assessment + +### Before Enhancements + +**Problem:** Error extraction created 80KB file (33,985 tokens) +``` +Read(.claude/diagnostics/run-19147272508/error-sections.txt) + ⎿ Error: File content (33,985 tokens) exceeds maximum allowed tokens (25,000) +``` + +**Workaround Required:** +- Manual grep commands to extract specific sections +- Multiple Read operations with offset/limit parameters +- Slow, iterative analysis +- Easy to miss critical information + +### After Enhancements + +**Solution:** Tiered extraction with guaranteed-readable sizes + +**Level 1:** 1,555 tokens - Quick overview +```bash +cat .claude/diagnostics/run-19147272508/evidence-level1-summary.txt +# Always readable, instant triage +``` + +**Level 2:** 6,156 tokens - Detailed errors +```bash +cat .claude/diagnostics/run-19147272508/evidence-level2-unique.txt +# First occurrence of each error type with context +``` + +**Level 3:** 21,656 tokens - Full context +```bash +cat .claude/diagnostics/run-19147272508/evidence-level3-full.txt +# Complete investigation details +``` + +**Retry Analysis:** Automated classification +```bash +source .claude/skills/cicd-diagnostics/utils/retry-analyzer.sh +analyze_simple_retry_patterns "$LOG_FILE" +# Instant deterministic vs flaky distinction +``` + +--- + +## Usage Examples + +### Example 1: Quick Triage (30 seconds) + +```bash +# Initialize and extract +RUN_ID=19147272508 +bash .claude/skills/cicd-diagnostics/init-diagnostic.sh "$RUN_ID" +source .claude/skills/cicd-diagnostics/utils/tiered-extraction.sh + +WORKSPACE="/path/to/.claude/diagnostics/run-$RUN_ID" +LOG_FILE="$WORKSPACE/failed-job-*.txt" + +# Create tiered extractions +auto_extract_tiered "$LOG_FILE" "$WORKSPACE" + +# Read Level 1 (always fits) +cat "$WORKSPACE/evidence-level1-summary.txt" + +# Result: Instant answer to "what failed?" +``` + +### Example 2: Detailed Analysis (2 minutes) + +```bash +# After Level 1 triage, read Level 2 for error details +cat "$WORKSPACE/evidence-level2-unique.txt" + +# Get retry pattern analysis +source .claude/skills/cicd-diagnostics/utils/retry-analyzer.sh +analyze_simple_retry_patterns "$LOG_FILE" + +# Result: Know exact error messages and whether failures are deterministic or flaky +``` + +### Example 3: Deep Investigation (5 minutes) + +```bash +# For complex cases, read Level 3 +cat "$WORKSPACE/evidence-level3-full.txt" + +# Result: Complete stack traces, timing correlation, infrastructure events +``` + +--- + +## Performance Comparison + +| Metric | Before | After | Improvement | +|--------|--------|-------|-------------| +| **Extraction Time** | ~5 seconds | ~5 seconds | Same | +| **File Size (error sections)** | 80KB (33,985 tokens) | Level 1: 6KB (1,555 tokens) | **95% reduction** | +| **Readability** | ⌠Too large | ✅ Always readable | **Fixed** | +| **Analysis Speed** | 5+ min (manual chunks) | 30sec - 2min (progressive) | **60-80% faster** | +| **Retry Classification** | Manual counting | Automated | **100% automation** | +| **Accuracy** | Prone to counting errors | Algorithmic parsing | **More reliable** | + +--- + +## Test Results (Run 19147272508) + +### Tiered Extraction +``` +✓ Level 1 created: 6,222 bytes (~1,555 tokens) - READABLE +✓ Level 2 created: 24,624 bytes (~6,156 tokens) - READABLE +✓ Level 3 created: 86,624 bytes (~21,656 tokens) - READABLE +``` + +### Retry Pattern Analysis +``` +✓ Correctly identified 1 deterministic failure (4/4 retries failed) +✓ Correctly identified 3 flaky tests with pass/fail breakdowns +✓ Accurate failure rate calculations (50%, 50%, 50%) +✓ Clear blocking vs warning classification +``` + +### AI Analysis Workflow +``` +1. Read Level 1 → Identified PublisherTest failures and timing issues (10 sec) +2. Read Level 2 → Saw ConditionTimeout pattern for IdentifierDateJob (30 sec) +3. Run retry analysis → Confirmed 1 deterministic, 3 flaky (5 sec) +4. Read Level 3 → Got full stack traces for deep dive (60 sec) + +Total: ~2 minutes from log download to full diagnosis +``` + +--- + +## Next Steps (Future Enhancements) + +### High Priority (Recommended by ANALYSIS_EVALUATION.md) + +1. **PR Diff Integration** + - Automatically fetch PR diff when analyzing PR failures + - Show code changes that may have caused failure + - Implementation: `fetch_pr_diff()` utility function + +2. **Background Job Execution Tracing** + - Extract logs specifically for background jobs (Quartz, IdentifierDateJob, etc.) + - Help diagnose request context issues + - Implementation: `trace_job_execution()` utility function + +3. **Automated Known Issue Search** + - Search GitHub issues for matching test names/patterns + - Instant detection of known flaky tests + - Implementation: `find_related_issues()` utility function + +### Medium Priority + +4. **Timing Correlation Analysis** + - Correlate error timestamps to detect cascades + - Identify primary vs secondary failures + - Implementation: `correlate_error_timing()` utility function + +5. **Infrastructure Event Detection** + - Parse Docker/DB/ES logs for root cause + - Detect environment issues vs code issues + - Implementation: `extract_infrastructure_events()` utility function + +--- + +## Conclusion + +The tiered extraction system successfully solves the "file too large" problem while providing a **better analysis workflow**: + +- ✅ **Level 1 always readable** - No more token limit errors +- ✅ **Progressive detail** - Start fast, go deep only when needed +- ✅ **Automated retry analysis** - Instant deterministic vs flaky classification +- ✅ **60-80% faster** - Less manual work, clearer insights +- ✅ **More reliable** - Algorithmic parsing vs manual counting + +**Impact:** The skill can now handle large CI/CD logs efficiently and provide instant triage, making it suitable for production use in automated diagnostics workflows. diff --git a/.claude/skills/cicd-diagnostics/ISSUE_TEMPLATE.md b/.claude/skills/cicd-diagnostics/ISSUE_TEMPLATE.md new file mode 100644 index 000000000000..9a16cc838a82 --- /dev/null +++ b/.claude/skills/cicd-diagnostics/ISSUE_TEMPLATE.md @@ -0,0 +1,510 @@ +# GitHub Issue Templates for CI/CD Failures + +Standard templates for documenting build failures. + +## Template Selection Guide + +**New Build Failure** → Use "Build Failure Report" template +**Flaky Test** → Use "Flaky Test Report" template +**Infrastructure Issue** → Use "Infrastructure Issue" template +**Add to existing issue** → Use "Failure Update Comment" template + +## Build Failure Report Template + +Use when creating a new issue for a consistent build failure. + +```markdown +## Build Failure Report + +**Workflow Run**: [workflow-name #run-id](run-url) +**Failed Job**: `job-name` +**Commit**: [`short-sha`](commit-url) - commit message +**Branch**: `branch-name` +**PR**: #pr-number (if applicable) +**Date**: YYYY-MM-DD HH:MM UTC + +### Failure Summary + +Brief description of what failed (1-2 sentences). + +### Failed Test(s) + +If test failure, list test class and method: +``` +com.dotcms.contenttype.business.ContentTypeAPIImplTest.testCreateContentType +``` + +If build failure, describe the build phase: +``` +Maven compilation phase - Java syntax error in ContentTypeResource.java +``` + +### Error Message + +``` +[Insert relevant error message] +Example: +java.lang.AssertionError: Expected content type to be created + Expected: ContentType{name='test', baseType=CONTENT} + Actual: null +``` + +### Stack Trace + +``` +[Insert relevant stack trace, focus on com.dotcms.* lines] +Example: +java.lang.NullPointerException: Cannot invoke method on null object + at com.dotcms.contenttype.business.ContentTypeAPIImpl.save(ContentTypeAPIImpl.java:456) + at com.dotcms.contenttype.business.ContentTypeAPIImplTest.testCreateContentType(ContentTypeAPIImplTest.java:123) +``` + +### Root Cause + +**Category**: [Code Change | Test Issue | Infrastructure | External Dependency] + +**Analysis**: +Explain the identified root cause with evidence (changed files, recent commits, historical pattern). + +Example: +"The failure was introduced in commit abc1234 which refactored the ContentType save logic. The test expects the save method to return the created object, but the refactored code returns null when validation fails." + +### Classification + +- **Type**: [New Failure | Regression | Test Gap] +- **Introduced in**: commit-sha or "unknown" +- **First failed**: run-id and date +- **Reproducibility**: [Always | Sometimes | Once] +- **Affects workflows**: [PR | Merge Queue | Trunk | Nightly] + +### Related Changes + +Commits between last success and this failure: +- `abc1234` - Refactor ContentType API by @author (YYYY-MM-DD) +- `def5678` - Update test fixtures by @author (YYYY-MM-DD) + +### Reproduction Steps + +Steps to reproduce locally (if known): +```bash +./mvnw test -Dtest=ContentTypeAPIImplTest#testCreateContentType +``` + +Or mark as: +``` +Cannot reproduce locally - CI environment specific +``` + +### Recommendations + +1. **Immediate action**: [Specific fix or workaround] + ```bash + [Command or code snippet if applicable] + ``` + +2. **Verification**: [How to verify the fix] + ```bash + [Test command] + ``` + +3. **Prevention**: [How to prevent similar issues] + [Description] + +### Related Issues + +- Related to #issue-number +- Similar to #issue-number +- Depends on #issue-number + +### Additional Context + +[Any other relevant information: environment details, configuration, external factors] + +--- +*Generated by CI/CD Diagnostics Skill* +``` + +**Labels to add**: +- `bug` (always) +- `ci-cd` (always) +- Workflow-specific: `pr-workflow`, `merge-queue`, `trunk-workflow`, or `nightly` +- Type-specific: `test-failure`, `build-failure`, `deployment-failure` + +**gh CLI command**: +```bash +gh issue create \ + --title "[CI/CD] Brief description of failure" \ + --body "$(cat issue-body.md)" \ + --label "bug,ci-cd,pr-workflow" +``` + +## Flaky Test Report Template + +Use when documenting a test that fails intermittently. + +```markdown +## Flaky Test Report + +**Test**: `com.dotcms.package.TestClass.testMethod` +**Failure Rate**: X failures out of Y runs (Z%) +**Date Range**: YYYY-MM-DD to YYYY-MM-DD +**Workflows Affected**: [PR | Merge Queue | Nightly] + +### Failure Pattern + +**Frequency**: +- Last 30 days: X failures / Y runs (Z%) +- Last 7 days: X failures / Y runs (Z%) + +**Time pattern** (if any): +- Random failures: No time pattern detected +- OR: Tends to fail during high load / specific time of day + +**Workflow pattern**: +- Fails in: [which workflows] +- Always passes in: [which workflows] +- Pattern: [describe any pattern] + +### Example Failures + +**Recent failure 1**: +- Run: [run-name #run-id](run-url) +- Date: YYYY-MM-DD +- Error: `brief error message` + +**Recent failure 2**: +- Run: [run-name #run-id](run-url) +- Date: YYYY-MM-DD +- Error: `brief error message` + +**Recent failure 3**: +- Run: [run-name #run-id](run-url) +- Date: YYYY-MM-DD +- Error: `brief error message` + +### Error Messages + +Common error patterns seen: +``` +[Error message variant 1] +``` + +``` +[Error message variant 2] +``` + +### Suspected Root Cause + +**Hypothesis**: [Your hypothesis about why it's flaky] + +Examples: +- Race condition in async operation +- Timing dependency on external service +- Resource contention (database connections, ports) +- Non-deterministic test data +- Cleanup issue leaving state for next test + +**Evidence**: +- [Supporting evidence for hypothesis] +- [Stack trace analysis] +- [Timing information] + +### Test Code Location + +- File: `src/test/java/com/dotcms/package/TestClass.java` +- Method: `testMethod` (line XXX) +- Related code: [Files tested by this test] + +### Mitigation Options + +**Option 1: Fix the root cause** (preferred) +- [ ] Identify race condition +- [ ] Add proper synchronization/waiting +- [ ] Improve test isolation +- [ ] Fix cleanup issues + +**Option 2: Improve test resilience** (temporary) +- [ ] Add retry logic +- [ ] Increase timeouts +- [ ] Add explicit waits +- [ ] Improve assertions + +**Option 3: Quarantine** (last resort) +- [ ] Mark with `@Flaky` annotation +- [ ] Exclude from CI runs temporarily +- [ ] Track in separate test suite +- [ ] Create investigation task + +### Recommended Actions + +1. [Specific action 1] +2. [Specific action 2] +3. [Specific action 3] + +### Related Issues + +- Similar flaky test: #issue-number +- Related to: #issue-number + +--- +*Generated by CI/CD Diagnostics Skill* +``` + +**Labels to add**: +- `flaky-test` (always) +- `test-failure` +- `ci-cd` +- Severity: `high-priority` if >20% failure rate, `medium-priority` if 5-20%, `low-priority` if <5% + +**gh CLI command**: +```bash +gh issue create \ + --title "[Flaky Test] TestClass.testMethod - X% failure rate" \ + --body "$(cat flaky-test.md)" \ + --label "flaky-test,test-failure,ci-cd,high-priority" +``` + +## Infrastructure Issue Template + +Use for issues related to CI/CD infrastructure, not code. + +```markdown +## CI/CD Infrastructure Issue + +**Affected Workflows**: [PR | Merge Queue | Trunk | Nightly | All] +**Issue Type**: [Timeout | Connectivity | Resource | Service Outage] +**First Observed**: YYYY-MM-DD HH:MM UTC +**Status**: [Ongoing | Resolved | Intermittent] + +### Symptom + +Brief description of the infrastructure issue. + +Example: +"Multiple workflow runs timing out during Elasticsearch startup phase" + +### Affected Runs + +Recent runs experiencing this issue: +- [workflow #run-id](run-url) - YYYY-MM-DD - timeout after 15 minutes +- [workflow #run-id](run-url) - YYYY-MM-DD - connection refused +- [workflow #run-id](run-url) - YYYY-MM-DD - rate limit exceeded + +### Error Patterns + +``` +[Common error message 1] +``` + +``` +[Common error message 2] +``` + +### Investigation + +**External Service Status**: +- GitHub Actions status: [Link to status page] +- Maven Central: [Status] +- Docker Hub: [Status] +- Other services: [Status] + +**Runner Information**: +- Runner OS: [ubuntu-latest, macos-latest, etc.] +- Runner version: [if known] +- Resource limits: [if relevant] + +**Timing**: +- Time of day pattern: [if any] +- Duration of issue: [how long observed] +- Frequency: [always, intermittent, rare] + +### Root Cause + +**Identified cause** (if known): +[Description of root cause] + +**Suspected cause** (if investigating): +[Hypothesis about cause] + +### Impact + +- **Workflows blocked**: X runs failed +- **PRs affected**: Y PRs unable to merge +- **Duration**: Started YYYY-MM-DD, ongoing/resolved YYYY-MM-DD +- **Severity**: [Critical | High | Medium | Low] + +### Workaround + +**Temporary workaround** (if available): +```bash +[Commands or config changes] +``` + +Or: +``` +No workaround available - must wait for service restoration +``` + +### Resolution + +**Status**: [Investigating | Waiting for external fix | Fixed] + +**Actions taken**: +1. [Action 1] +2. [Action 2] +3. [Action 3] + +**Permanent fix** (if applicable): +[Description of fix implemented] + +### Related Issues + +- Related to #issue-number +- Duplicate of #issue-number +- External issue: [link to GitHub Actions, service status, etc.] + +--- +*Generated by CI/CD Diagnostics Skill* +``` + +**Labels to add**: +- `ci-cd` +- `infrastructure` +- Severity based on impact: `critical`, `high-priority`, `medium-priority` +- Type: `timeout`, `connectivity`, `resource-constraint` + +## Failure Update Comment Template + +Use when adding information to an existing issue. + +```markdown +### Additional Failure - YYYY-MM-DD + +**Run**: [workflow #run-id](run-url) +**Commit**: `short-sha` +**Workflow**: [PR | Merge Queue | Trunk | Nightly] + +**Status**: [Same error | Slightly different | Related] + +**Error**: +``` +[Error message if different] +``` + +**Notes**: +[Any new observations or patterns] + +**Failure count**: Now X failures out of Y observed runs +``` + +**gh CLI command**: +```bash +gh issue comment ISSUE_NUMBER --body "$(cat update-comment.md)" +``` + +## Label Standards + +**Workflow labels** (one): +- `pr-workflow` - cicd_1-pr.yml +- `merge-queue` - cicd_2-merge-queue.yml +- `trunk-workflow` - cicd_3-trunk.yml +- `nightly` - cicd_4-nightly.yml + +**Type labels** (one or more): +- `test-failure` - Test failed +- `build-failure` - Compilation/build failed +- `deployment-failure` - Deployment step failed +- `flaky-test` - Intermittent test failure +- `infrastructure` - Infrastructure/external issue + +**Severity labels** (one): +- `critical` - Blocking all builds +- `high-priority` - Affecting multiple PRs/runs +- `medium-priority` - Intermittent or limited impact +- `low-priority` - Rare or minor issue + +**Always include**: +- `bug` (for failures) +- `ci-cd` (for all CI/CD issues) + +## Title Conventions + +**Build Failure**: +``` +[CI/CD] Brief description of what failed +``` +Examples: +- `[CI/CD] ContentTypeAPIImplTest.testCreate fails with NPE` +- `[CI/CD] Maven compilation error in ContentTypeResource` +- `[CI/CD] Docker build timeout in trunk workflow` + +**Flaky Test**: +``` +[Flaky Test] TestClass.testMethod - X% failure rate +``` +Examples: +- `[Flaky Test] ContentTypeAPIImplTest.testConcurrent - 15% failure rate` +- `[Flaky Test] WorkflowAPITest.testTransition - intermittent timeout` + +**Infrastructure**: +``` +[Infrastructure] Brief description of issue +``` +Examples: +- `[Infrastructure] Elasticsearch startup timeouts in nightly builds` +- `[Infrastructure] Maven Central connectivity issues` + +## Quick Issue Creation Commands + +**New build failure**: +```bash +gh issue create \ + --title "[CI/CD] Test/Build description" \ + --label "bug,ci-cd,pr-workflow,test-failure" \ + --assignee "@me" \ + --body "$(cat < failed-job.log + +# Much smaller than full archive! +``` + +### 3. Progressive Log Extraction + +```bash +# Download full archive +gh run download $RUN_ID --dir ./logs + +# List contents first (don't extract) +unzip -l logs.zip | head -50 + +# Identify structure +# Typical structure: +# - 1_Job Name/ +# - 2_Step Name.txt +# - 3_Another Step.txt + +# Extract ONLY failed job directory +unzip logs.zip "*/Failed Job Name/*" -d extracted/ + +# Or stream search without extracting +unzip -p logs.zip "**/[0-9]*_*.txt" | grep "pattern" | head -100 +``` + +## Pattern Matching Strategies + +### Maven Build Failures + +**Primary indicators** (check these first): +```bash +# Maven errors (most reliable) +unzip -p logs.zip "**/[0-9]*_*.txt" | grep -A 10 -B 3 "\[ERROR\]" | head -100 + +# Build failure summary +unzip -p logs.zip "**/[0-9]*_*.txt" | grep -A 20 "BUILD FAILURE" | head -100 + +# Compilation errors +unzip -p logs.zip "**/[0-9]*_*.txt" | grep -A 15 "COMPILATION ERROR" | head -50 +``` + +**What to look for**: +- `[ERROR] Failed to execute goal` - Maven plugin failures +- `[ERROR] COMPILATION ERROR` - Java compilation issues +- `[ERROR] There are test failures` - Test failures +- `[ERROR] Could not resolve dependencies` - Dependency issues + +### Test Failures + +**Test failure markers** (surefire/failsafe): +```bash +# Test failure summary +unzip -p logs.zip "**/[0-9]*_*.txt" | grep -E "Tests run:.*Failures: [1-9]" | head -20 + +# Individual test failures +unzip -p logs.zip "**/[0-9]*_*.txt" | grep -A 25 "<<< FAILURE!" | head -200 + +# Test errors (crashes) +unzip -p logs.zip "**/[0-9]*_*.txt" | grep -A 25 "<<< ERROR!" | head -200 +``` + +**Test failure structure**: +``` +[ERROR] Tests run: 150, Failures: 2, Errors: 0, Skipped: 5 +... +[ERROR] testMethodName(com.dotcms.TestClass) Time elapsed: 1.234 s <<< FAILURE! +java.lang.AssertionError: Expected X but was Y + at org.junit.Assert.fail(Assert.java:88) + at com.dotcms.TestClass.testMethodName(TestClass.java:123) +``` + +**Extract failure details**: +```bash +# Get test class and method +grep "<<< FAILURE!" logs.txt | sed 's/.*\(test[A-Za-z]*\)(\([^)]*\).*/\2.\1/' + +# Get exception type and message +grep -A 5 "<<< FAILURE!" logs.txt | grep -E "^[a-zA-Z.]*Exception|^java.lang.AssertionError" +``` + +### Stack Trace Analysis + +**Find relevant stack traces**: +```bash +# Find DotCMS code in stack traces (ignore framework) +unzip -p logs.zip "**/[0-9]*_*.txt" | \ + grep -A 50 "Exception:" | \ + grep -E "at com\.(dotcms|dotmarketing)\." | \ + head -100 +``` + +**Stack trace structure**: +``` +java.lang.NullPointerException: Cannot invoke method on null object + at com.dotcms.MyClass.myMethod(MyClass.java:456) ↠Target this + at com.dotcms.OtherClass.caller(OtherClass.java:123) ↠And this + at org.junit.internal.runners... ↠Ignore framework + at sun.reflect... ↠Ignore JVM +``` + +**Priority**: Lines starting with `at com.dotcms` or `at com.dotmarketing` + +### Infrastructure Issues + +**Patterns to search**: +```bash +# Timeout issues +grep -i "timeout\|timed out\|deadline exceeded" logs.txt | head -20 + +# Connection issues +grep -i "connection refused\|connection reset\|unable to connect" logs.txt | head -20 + +# Rate limiting +grep -i "rate limit\|too many requests\|429" logs.txt | head -20 + +# Resource exhaustion +grep -i "out of memory\|cannot allocate\|disk.*full" logs.txt | head -20 + +# Docker issues +grep -i "docker.*error\|failed to pull\|image not found" logs.txt | head -20 +``` + +### Dependency Issues + +**Patterns**: +```bash +# Dependency resolution failures +grep -i "could not resolve\|failed to resolve\|artifact not found" logs.txt | head -30 + +# Version conflicts +grep -i "version conflict\|duplicate\|incompatible" logs.txt | head -20 + +# Download issues +grep -i "failed to download\|connection to.*refused" logs.txt | head-20 +``` + +## Test Report XML Analysis + +**Structure** (surefire/failsafe XML): +```xml + + + + + + + +``` + +**Parse with Read tool or xmllint**: +```bash +# Extract test results only +unzip logs.zip "**/*surefire-reports/*.xml" -d test-results/ + +# Count failures per test suite +find test-results -name "*.xml" -exec grep -H "failures=" {} \; | grep -v 'failures="0"' + +# Extract failure messages +xmllint --xpath "//failure/@message" test-results/*.xml +``` + +## Efficient Search Workflow + +### Step-by-Step Process + +**1. Quick Status Check (30 seconds)**: +```bash +gh run view $RUN_ID --json conclusion,jobs \ + --jq '{conclusion, failed_jobs: [.jobs[] | select(.conclusion == "failure") | .name]}' +``` + +**2. Failed Job Details (1 minute)**: +```bash +gh api "/repos/dotCMS/core/actions/runs/$RUN_ID/jobs" \ + --jq '.jobs[] | select(.conclusion == "failure") | + {name, failed_steps: [.steps[] | select(.conclusion == "failure") | .name]}' +``` + +**3. Check Test Artifacts (1 minute)**: +```bash +# List test result artifacts +gh api "/repos/dotCMS/core/actions/runs/$RUN_ID/artifacts" \ + --jq '.artifacts[] | select(.name | contains("test-results")) | {name, id, size_in_bytes}' + +# Download if small (< 10 MB) +# Skip if large or expired +``` + +**4. Job-Specific Logs (2-3 minutes)**: +```bash +# Download only failed job logs +FAILED_JOB_ID= +gh api "/repos/dotCMS/core/actions/jobs/$FAILED_JOB_ID/logs" > failed-job.log + +# Search for Maven errors +grep -A 10 "\[ERROR\]" failed-job.log | head -100 + +# Search for test failures +grep -A 25 "<<< FAILURE!" failed-job.log | head -200 +``` + +**5. Full Archive Analysis (5+ minutes, only if needed)**: +```bash +# Download full logs +gh run download $RUN_ID --name logs --dir ./logs + +# List contents +unzip -l logs/*.zip | grep -E "\.txt$" | head -50 + +# Stream search (no extraction) +unzip -p logs/*.zip "**/[0-9]*_*.txt" | grep -E "\[ERROR\]|<<< FAILURE!" | head -300 +``` + +## Pattern Recognition Guide + +### Error Type Identification + +**Compilation Error**: +``` +[ERROR] COMPILATION ERROR +[ERROR] /path/to/File.java:[123,45] cannot find symbol +``` +→ Code syntax error, missing import, type mismatch + +**Test Failure (Assertion)**: +``` +<<< FAILURE! +java.lang.AssertionError: expected: but was: +``` +→ Test expectation not met, code behavior changed + +**Test Error (Exception)**: +``` +<<< ERROR! +java.lang.NullPointerException + at com.dotcms.MyClass.method(MyClass.java:123) +``` +→ Unexpected exception, code defect + +**Timeout**: +``` +org.junit.runners.model.TestTimedOutException: test timed out after 30000 milliseconds +``` +→ Test hung, infinite loop, or infrastructure slow + +**Connection/Infrastructure**: +``` +java.net.ConnectException: Connection refused +Could not resolve host: repository.example.com +``` +→ Network issue, external service down, infrastructure problem + +**Dependency Issue**: +``` +[ERROR] Failed to collect dependencies +Could not resolve dependencies for project com.dotcms:dotcms-core +``` +→ Maven repository issue, version conflict, missing artifact + +## Context Window Optimization + +**Problem**: Cannot load 500 MB of logs into context + +**Solutions**: + +1. **Targeted extraction**: Get only relevant sections +```bash +# Extract just the error summary from a 500 MB log +unzip -p logs.zip "**/5_Test.txt" | \ + grep -A 50 "\[ERROR\] Tests run:" | \ + head -200 +# Result: ~10 KB instead of 500 MB +``` + +2. **Layered analysis**: + - First: Maven ERROR lines (usually < 100 lines) + - Second: Specific test failure (usually < 50 lines) + - Third: Stack trace for that test (usually < 30 lines) + - Total: ~200 lines instead of millions + +3. **Use structured data when possible**: + - XML test reports: Parse for failures only + - JSON from gh CLI: Filter with jq + - Grep with line limits: Never more than needed + +## Common Pitfalls + +⌠**Don't do this**: +```bash +# Downloads and extracts EVERYTHING (5-10 min, huge context) +gh run download $RUN_ID +unzip -q logs.zip +cat **/*.txt > all-logs.txt # 1 GB+ file +``` + +✅ **Do this instead**: +```bash +# Targeted search (30 sec, minimal context) +gh run download $RUN_ID --name logs +unzip -p logs/*.zip "**/[0-9]*_*.txt" | grep -A 10 "\[ERROR\]" | head -100 +``` + +⌠**Don't do this**: +```bash +# Read entire log file +Read: /path/to/5-Test-step.txt # 200 MB file +``` + +✅ **Do this instead**: +```bash +# Use Bash grep to extract relevant lines first +grep -A 20 "<<< FAILURE!" /path/to/5-Test-step.txt | head -200 > failures-only.txt +# Then read the small extracted file +Read: failures-only.txt # 10 KB file +``` + +## Quick Reference Commands + +### Fastest Diagnosis Commands +```bash +# 1. Which job failed? (10 sec) +gh run view $RUN_ID --json jobs --jq '.jobs[] | select(.conclusion == "failure") | .name' + +# 2. What step failed? (10 sec) +gh api "/repos/dotCMS/core/actions/runs/$RUN_ID/jobs" --jq '.jobs[] | select(.conclusion == "failure") | .steps[] | select(.conclusion == "failure") | .name' + +# 3. Get that job's logs (30 sec) +FAILED_JOB_ID=$(gh api "/repos/dotCMS/core/actions/runs/$RUN_ID/jobs" --jq '.jobs[] | select(.conclusion == "failure") | .id' | head -1) +gh api "/repos/dotCMS/core/actions/jobs/$FAILED_JOB_ID/logs" > job.log + +# 4. Find Maven errors (5 sec) +grep -A 10 "\[ERROR\]" job.log | head -100 + +# 5. Find test failures (5 sec) +grep -A 25 "<<< FAILURE!" job.log | head -200 +``` + +**Total time**: ~60 seconds to identify most failures + +## Log Analysis Checklist + +When analyzing logs: +- [ ] Start with job-level logs via API (fastest) +- [ ] Look for Maven `[ERROR]` markers first +- [ ] Search for test failure markers: `<<< FAILURE!`, `<<< ERROR!` +- [ ] Extract stack traces with DotCMS code only +- [ ] Check for infrastructure patterns if no code errors +- [ ] Use grep line limits (`head`, `tail`) religiously +- [ ] Only download full archive if absolutely necessary +- [ ] Never try to read entire log files without filtering \ No newline at end of file diff --git a/.claude/skills/cicd-diagnostics/README.md b/.claude/skills/cicd-diagnostics/README.md new file mode 100644 index 000000000000..7ba8291f53a7 --- /dev/null +++ b/.claude/skills/cicd-diagnostics/README.md @@ -0,0 +1,262 @@ +# CI/CD Diagnostics Skill + +Expert diagnostic tool for analyzing DotCMS CI/CD build failures in GitHub Actions. + +## Skill Overview + +This skill provides automated diagnosis of CI/CD failures across all DotCMS workflows: +- **cicd_1-pr.yml** - Pull Request validation +- **cicd_2-merge-queue.yml** - Pre-merge full validation +- **cicd_3-trunk.yml** - Post-merge deployment +- **cicd_4-nightly.yml** - Scheduled full test runs + +## Capabilities + +### 🔠Intelligent Failure Analysis +- Identifies failed jobs and steps +- Extracts relevant errors from large log files efficiently +- Classifies failures (new, flaky, infrastructure, test filtering) +- Compares workflow results (PR vs merge queue) +- Checks historical patterns across runs + +### 📊 Root Cause Determination +- New failures introduced by specific commits +- Flaky tests with failure rate calculation +- Infrastructure issues (timeouts, connectivity) +- Test filtering discrepancies between workflows +- External dependency changes + +### 🔗 GitHub Integration +- Searches existing issues for known problems +- Creates detailed GitHub issues with proper labels +- Links failures to related PRs and commits +- Provides actionable recommendations + +### âš¡ Efficiency Optimized +- Progressive disclosure of log analysis +- Streaming search without full extraction +- Job-specific log downloads +- Pattern-based error detection +- Context window optimized + +## Skill Structure + +``` +cicd-diagnostics/ +├── SKILL.md # Main skill instructions (concise, <300 lines) +├── WORKFLOWS.md # Detailed workflow documentation +├── LOG_ANALYSIS.md # Advanced log analysis techniques +├── ISSUE_TEMPLATE.md # GitHub issue templates +└── README.md # This file +``` + +## Usage + +The skill activates automatically when you ask questions like: + +- "Why did the build fail?" +- "Check CI/CD status" +- "Analyze run 19131365567" +- "Is ContentTypeAPIImplTest flaky?" +- "Why did my PR pass but merge queue fail?" +- "What's blocking the merge queue?" +- "Debug the nightly build failure" + +Or invoke explicitly: +```bash +/cicd-diagnostics +``` + +## Example Scenarios + +### Scenario 1: Analyze Specific Run +``` +You: "Analyze https://github.com/dotCMS/core/actions/runs/19131365567" + +Skill: +1. Extracts run ID and fetches run details +2. Identifies failed jobs and steps +3. Downloads and analyzes logs efficiently +4. Determines root cause with evidence +5. Checks for known issues +6. Provides actionable recommendations +``` + +### Scenario 2: Check Current PR +``` +You: "Check my PR build status" + +Skill: +1. Gets current branch name +2. Finds associated PR +3. Gets latest PR workflow runs +4. Analyzes any failures +5. Reports status and recommendations +``` + +### Scenario 3: Flaky Test Investigation +``` +You: "Is ContentTypeAPIImplTest flaky?" + +Skill: +1. Searches nightly build history +2. Counts failures vs successes +3. Calculates failure rate +4. Checks existing flaky test issues +5. Recommends action (fix vs quarantine) +``` + +### Scenario 4: Workflow Comparison +``` +You: "Why did PR pass but merge queue fail?" + +Skill: +1. Gets PR workflow results +2. Gets merge queue results for same commit +3. Identifies test filtering differences +4. Explains discrepancy +5. Recommends fixing the filtered tests +``` + +## Key Principles + +### Efficiency First +- Start with high-level status (30 sec) +- Progress to detailed logs only if needed (5+ min) +- Use streaming and filtering for large files +- Target specific patterns based on failure type + +### Workflow Context Matters +- **PR failures** → Usually code issues or filtered tests +- **Merge queue failures** → Test filtering, conflicts, or flaky tests +- **Trunk failures** → Deployment/artifact issues +- **Nightly failures** → Flaky tests or infrastructure + +### Progressive Investigation +1. Run status → Failed jobs (30 sec) +2. Maven errors → Test failures (2 min) +3. Full log analysis (5+ min, only if needed) +4. Historical comparison (2 min) +5. Issue creation (2 min, if needed) + +## Reference Files + +### SKILL.md +Main skill instructions with: +- Core workflow types +- 7-step diagnostic approach +- Key principles and efficiency tips +- Success criteria + +**Use**: Core instructions loaded when skill activates + +### WORKFLOWS.md +Detailed workflow documentation: +- Each workflow's purpose and triggers +- Common failure patterns with detection methods +- Test strategies and typical durations +- Cross-cutting failure causes +- Diagnostic decision tree + +**Use**: Reference when you need detailed workflow-specific information + +### LOG_ANALYSIS.md +Advanced log analysis techniques: +- Smart download strategies +- Pattern matching for different error types +- Efficient search workflows +- Context window optimization +- Quick reference commands + +**Use**: Reference when analyzing logs to find specific patterns efficiently + +### ISSUE_TEMPLATE.md +GitHub issue templates: +- Build Failure Report +- Flaky Test Report +- Infrastructure Issue Report +- Failure Update Comment +- Label standards and conventions + +**Use**: Reference when creating or updating GitHub issues + +## Best Practices + +### Do ✅ +- Start with job status before downloading logs +- Use streaming (`unzip -p`) for large archives +- Search for Maven `[ERROR]` first +- Check test filtering differences (PR vs merge queue) +- Compare with historical runs +- Search existing issues before creating new ones +- Provide specific, actionable recommendations + +### Don't ⌠+- Download entire log archives unnecessarily +- Try to read full logs without filtering +- Assume PR passing means all tests pass (filtering!) +- Create duplicate issues without searching +- Provide vague recommendations +- Ignore workflow context + +## Integration with GitHub CLI + +All commands use `gh` CLI for: +- Workflow run queries +- Job and step details +- Log downloads +- Artifact management +- Issue search and creation +- PR status checks + +**Required**: `gh` CLI installed and authenticated + +## Output Format + +Standard diagnostic report structure: +```markdown +## CI/CD Failure Diagnosis: [workflow] #[run-id] + +**Root Cause**: [Category] - [Explanation] +**Confidence**: [High/Medium/Low] + +### Failure Details +[Specific job, step, test information] + +### Classification +[Type, frequency, related issues] + +### Evidence +[Key log excerpts, commits, patterns] + +### Recommendations +[Actionable steps with commands/links] +``` + +## Success Criteria + +A successful diagnosis provides: +1. ✅ Specific failure point (job, step, test) +2. ✅ Root cause category with evidence +3. ✅ New vs recurring classification +4. ✅ Known issue status +5. ✅ Actionable recommendations +6. ✅ Issue creation if needed + +## Contributing + +When updating this skill: +1. Keep SKILL.md concise (<500 lines) +2. Move detailed content to reference files +3. Maintain one level of reference depth +4. Test with real failure scenarios +5. Update examples with actual patterns +6. Keep commands up-to-date with gh CLI + +## Version History + +- **v1.0** (2025-11-06) - Initial skill creation + - Four workflow support + - Progressive disclosure structure + - Efficient log analysis + - GitHub issue integration \ No newline at end of file diff --git a/.claude/skills/cicd-diagnostics/REFERENCE.md b/.claude/skills/cicd-diagnostics/REFERENCE.md new file mode 100644 index 000000000000..74f038f37c76 --- /dev/null +++ b/.claude/skills/cicd-diagnostics/REFERENCE.md @@ -0,0 +1,609 @@ +# CI/CD Diagnostics Reference Guide + +Detailed technical expertise and diagnostic patterns for DotCMS CI/CD failure analysis. + +## Table of Contents + +1. [Core Expertise & Approach](#core-expertise--approach) +2. [Specialized Diagnostic Skills](#specialized-diagnostic-skills) +3. [Design Philosophy](#design-philosophy) +4. [Detailed Analysis Patterns](#detailed-analysis-patterns) +5. [Report Templates](#report-templates) +6. [User Collaboration Examples](#user-collaboration-examples) +7. [Comparison with Old Approach](#comparison-with-old-approach) + +## Core Expertise & Approach + +### Technical Depth + +**GitHub Actions:** +- Runner environments, workflow dispatch patterns, matrix builds +- Test filtering strategies, artifact propagation +- Caching strategies and optimization + +**DotCMS Architecture:** +- Java/Maven build system +- Docker containers, PostgreSQL/Elasticsearch dependencies +- Integration test infrastructure + +**Testing Frameworks:** +- JUnit 5, Postman collections, Karate scenarios, Playwright E2E tests + +**Log Analysis:** +- Efficient parsing of multi-GB logs +- Error cascade detection +- Timing correlation +- Infrastructure failure patterns + +## Specialized Diagnostic Skills + +### Timing & Race Condition Recognition + +**Clock precision issues:** +- Second-level timestamps causing non-deterministic ordering (e.g., modDate sorting failures) +- Pattern indicators: Boolean flip assertions, intermittent ordering failures + +**Test execution timing:** +- Rapid test execution causing identical timestamps +- sleep() vs Awaitility patterns +- Pattern indicators: Tests that fail faster on faster CI runners + +**Database timing:** +- Transaction isolation, commit timing +- Optimistic locking failures + +**Async operation timing:** +- Background jobs, scheduled tasks +- Publish/expire date updates + +**Cache timing:** +- TTL expiration races +- Cache invalidation timing + +### Async Testing Anti-Patterns (CRITICAL) + +**Thread.sleep() anti-pattern:** +- Fixed delays causing flaky tests (too short = intermittent failure, too long = slow tests) +- Pattern indicators: + - `Thread.sleep(1000)` or `Thread.sleep(5000)` in test code + - Intermittent failures with timing-related assertions + - Tests that fail faster on faster CI runners + - "Expected X but was Y" where Y is intermediate state + - Flakiness that increases under load or on slower machines + +**Correct Async Testing Patterns:** + +```java +// ⌠WRONG: Fixed sleep (flaky and slow) +publishContent(content); +Thread.sleep(5000); // Hope it's done by now! +assertTrue(isPublished(content)); + +// ✅ CORRECT: Awaitility with timeout and polling +publishContent(content); +await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(100)) + .untilAsserted(() -> assertTrue(isPublished(content))); + +// ✅ CORRECT: With meaningful error message +await() + .atMost(10, SECONDS) + .pollDelay(100, MILLISECONDS) + .untilAsserted(() -> { + assertThat(getContentStatus(content)) + .describedAs("Content %s should be published", content.getId()) + .isEqualTo(Status.PUBLISHED); + }); + +// ✅ CORRECT: Await condition (more efficient than untilAsserted) +await() + .atMost(Duration.ofSeconds(10)) + .until(() -> isPublished(content)); +``` + +**When to recommend Awaitility:** +- Any test with `Thread.sleep()` followed by assertions +- Any test checking async operation results (publish, index, cache update) +- Any test with timing-dependent behavior +- Any test that fails intermittently with state-related assertions + +### Threading & Concurrency Issues + +**Thread safety violations:** +- Shared mutable state, non-atomic operations +- Race conditions on counters/maps + +**Deadlock patterns:** +- Circular lock dependencies +- Database connection pool exhaustion + +**Thread pool problems:** +- Executor queue overflow, thread starvation, improper shutdown + +**Quartz job context:** +- Background jobs running in separate thread pools +- Different lifecycle than HTTP requests + +**Concurrent modification:** +- ConcurrentModificationException +- Iterator failures during parallel access + +**Pattern indicators:** +- NullPointerException in background threads +- "user" is null errors +- Intermittent failures under load + +### Request Context Issues (CRITICAL for DotCMS) + +**Servlet lifecycle boundaries:** +- HTTP request/response lifecycle vs background thread execution + +**ThreadLocal anti-patterns:** +- HttpServletRequestThreadLocal accessed from Quartz jobs +- Scheduled tasks or thread pools accessing request context + +**Request object recycling:** +- Tomcat request object reuse after response completion + +**User context propagation:** +- Failure to pass User object to background operations +- Bundle publishing, permission jobs + +**Session scope leakage:** +- Session-scoped beans accessed from background threads + +**Pattern indicators:** +- `Cannot invoke "com.liferay.portal.model.User.getUserId()" because "user" is null` +- `HttpServletRequest` accessed after response completion +- NullPointerException in `PublisherQueueJob`, `IdentifierDateJob`, `CascadePermissionsJob` +- Failures in bundle publishing, content push, or scheduled background tasks + +**Common DotCMS Request Context Patterns:** + +```java +// ⌠WRONG: Accessing HTTP request in background thread (Quartz job) +User user = HttpServletRequestThreadLocal.INSTANCE.getRequest().getUser(); // NPE! + +// ✅ CORRECT: Pass user context explicitly +PublisherConfig config = new PublisherConfig(); +config.setUser(systemUser); // Or user from bundle metadata +``` + +### Analytical Methodology + +1. **Progressive Investigation:** Start with high-level patterns (30s), drill down only when needed (up to 10+ min for complex issues) +2. **Evidence-Based Reasoning:** Facts are facts, hypotheses are clearly labeled as such +3. **Multiple Hypothesis Testing:** Consider competing explanations before committing to root cause +4. **Efficient Resource Use:** Extract minimal necessary log context (99%+ size reduction for large files) + +### Problem-Solving Philosophy + +- **Adaptive Intelligence:** Recognize new failure patterns without pre-programmed rules +- **Skeptical Validation:** Don't accept first obvious answer; validate through evidence +- **User Collaboration:** When multiple paths exist, present options and ask user preference +- **Fact Discipline:** Known facts labeled as facts, theories labeled as theories, confidence levels explicit + +## Design Philosophy + +This skill follows an **AI-guided, utility-assisted** approach: + +- **Utilities** handle data access, caching, and extraction (Python modules) +- **AI** (you, the senior engineer) handles pattern recognition, classification, and reasoning + +**Why this works:** +- Senior engineers excel at recognizing new patterns and explaining reasoning +- Utilities excel at fast, cached data access and log extraction +- Avoids brittle hardcoded classification logic +- Adapts to new failure modes without code changes + +## Detailed Analysis Patterns + +### Example AI Analysis + +```markdown +## Failure Analysis + +**Test**: ContentTypeCommandIT.Test_Command_Content_Filter_Order_By_modDate_Ascending +**Pattern**: Boolean flip assertion on modDate ordering +**Match**: Issue #33746 - modDate precision timing + +**Classification**: Flaky Test (High Confidence) + +**Reasoning**: +1. Test compares modDate ordering (second-level precision) +2. Assertion shows intermittent true/false flip +3. Exact match with documented issue #33746 +4. Not a functional bug (would fail consistently) + +**Fingerprint**: +- test: ContentTypeCommandIT.Test_Command_Content_Filter_Order_By_modDate_Ascending +- pattern: modDate-ordering +- assertion: boolean-flip +- line: 477 +- known-issue: #33746 + +**Recommendation**: Known flaky test tracked in #33746. Fixes in progress. +``` + +## Report Templates + +### DIAGNOSIS.md Template + +```markdown +# CI/CD Failure Diagnosis - Run {RUN_ID} + +**Analysis Date:** {DATE} +**Run URL:** {URL} +**Workflow:** {WORKFLOW_NAME} +**Event:** {EVENT_TYPE} +**Conclusion:** {CONCLUSION} +**Analyzed By:** cicd-diagnostics skill with AI-guided analysis + +--- + +## Executive Summary +[2-3 sentence overview of the failure] + +--- + +## Failure Details +[Specific failure information with line numbers and context] + +### Failed Job +- **Name:** {JOB_NAME} +- **Job ID:** {JOB_ID} +- **Duration:** {DURATION} + +### Specific Test Failure +- **Test:** {TEST_NAME} +- **Location:** Line {LINE_NUMBER} +- **Error Type:** {ERROR_TYPE} +- **Assertion:** {ASSERTION_MESSAGE} + +--- + +## Root Cause Analysis + +### Classification: **{CATEGORY}** ({CONFIDENCE} Confidence) + +### Evidence Supporting Diagnosis +[Detailed evidence-based reasoning] + +### Why This Is/Isn't a Code Defect +[Clear explanation] + +--- + +## Test Fingerprint + +**Natural Language Description:** +[Human-readable description of failure pattern] + +**Matching Criteria for Future Failures:** +[How to identify similar failures] + +--- + +## Impact Assessment + +### Severity: **{SEVERITY}** + +### Business Impact +- **Blocking:** {YES/NO} +- **False Positive:** {YES/NO} +- **Developer Friction:** {LEVEL} +- **CI/CD Reliability:** {IMPACT_DESCRIPTION} + +### Frequency Analysis +[Historical failure data] + +### Risk Assessment +[Risk levels for different categories] + +--- + +## Recommendations + +### Immediate Actions (Unblock) +1. [Specific action with command/link] + +### Short-term Solutions (Reduce Issues) +2. [Solution with explanation] + +### Long-term Improvements (Prevent Recurrence) +3. [Systemic improvement suggestion] + +--- + +## Related Context + +### GitHub Issues +[Related open/closed issues] + +### Recent Workflow History +[Pattern analysis from recent runs] + +### Related PR/Branch +[Context about what triggered this run] + +--- + +## Diagnostic Artifacts + +All diagnostic data saved to: `{WORKSPACE_PATH}` + +### Files Generated +- `run-metadata.json` - Workflow run metadata +- `jobs-detailed.json` - All job details +- `failed-job-*.txt` - Complete job logs +- `error-sections.txt` - Extracted error sections +- `evidence.txt` - Structured evidence +- `DIAGNOSIS.md` - This report +- `ANALYSIS_EVALUATION.md` - Skill effectiveness evaluation + +--- + +## Conclusion +[Final summary with action items] + +**Action Required:** +1. [Priority action] +2. [Follow-up action] + +**Status:** [Ready for retry | Needs code fix | Investigation needed] +``` + +### ANALYSIS_EVALUATION.md Template + +```markdown +# Skill Effectiveness Evaluation - Run {RUN_ID} + +**Purpose:** Meta-analysis of cicd-diagnostics skill performance for continuous improvement. + +--- + +## Analysis Summary + +- **Run Analyzed:** {RUN_ID} +- **Time to Diagnosis:** {DURATION} +- **Cached Data Used:** {YES/NO} +- **Evidence Size:** {LOG_SIZE} → {EXTRACTED_SIZE} +- **Classification:** {CATEGORY} ({CONFIDENCE} confidence) + +--- + +## What Worked Well + +### 1. {Category} ✅ +[Specific success with examples] + +### 2. {Category} ✅ +[Specific success with examples] + +--- + +## AI Adaptive Analysis Strengths + +The skill successfully demonstrated AI-guided analysis by: + +1. **Natural Pattern Recognition** + [How AI identified patterns without hardcoded rules] + +2. **Contextual Reasoning** + [How AI connected evidence to root cause] + +3. **Cross-Reference Synthesis** + [How AI linked to related issues/history] + +4. **Confidence Assessment** + [How AI provided reasoning for confidence level] + +5. **Comprehensive Recommendations** + [How AI generated actionable solutions] + +**Key Insight:** The AI adapted to evidence rather than following rigid rules, enabling: +- [Specific capability 1] +- [Specific capability 2] +- [Specific capability 3] + +--- + +## What Could Be Improved + +### 1. {Area for Improvement} +- **Gap:** [What was missing] +- **Impact:** [Effect on analysis] +- **Suggestion:** [Specific improvement idea] + +### 2. {Area for Improvement} +- **Gap:** [What was missing] +- **Impact:** [Effect on analysis] +- **Suggestion:** [Specific improvement idea] + +--- + +## Performance Metrics + +### Speed +- **Data Fetching:** {TIME} +- **Evidence Extraction:** {TIME} +- **AI Analysis:** {TIME} +- **Total Duration:** {TIME} +- **vs Manual Analysis:** {COMPARISON} + +### Accuracy +- **Root Cause Correct:** {YES/NO/PARTIAL} +- **Known Issue Match:** {YES/NO/PARTIAL} +- **Classification Accuracy:** {CONFIDENCE_LEVEL} + +### Completeness +- [x] Identified specific failure point +- [x] Determined root cause with reasoning +- [x] Created natural test fingerprint +- [x] Assessed frequency/history +- [x] Checked known issues +- [x] Provided actionable recommendations +- [x] Saved diagnostic artifacts + +--- + +## Design Validation + +### AI-Guided Approach ✅/⌠+[How well the evidence-driven AI analysis worked] + +### Utility Functions ✅/⌠+[How well the Python utilities performed] + +### Caching Strategy ✅/⌠+[How well the workspace caching worked] + +--- + +## Recommendations for Skill Enhancement + +### High Priority +1. [Specific improvement with rationale] +2. [Specific improvement with rationale] + +### Medium Priority +3. [Specific improvement with rationale] +4. [Specific improvement with rationale] + +### Low Priority +5. [Specific improvement with rationale] + +--- + +## Comparison with Previous Approaches + +### Before (Hardcoded Logic) +[Issues with rule-based classification] + +### After (AI-Guided) +[Benefits of evidence-driven analysis] + +### Impact +- **Accuracy:** [Improvement] +- **Flexibility:** [Improvement] +- **Maintainability:** [Improvement] + +--- + +## Conclusion + +[Overall assessment of skill effectiveness] + +**Key Strengths:** +- [Strength 1] +- [Strength 2] +- [Strength 3] + +**Areas for Growth:** +- [Area 1] +- [Area 2] + +**Ready for production use:** {YES/NO} +**Recommended next steps:** [Action items] +``` + +## User Collaboration Examples + +### When to Ask for User Input: + +**1. Multiple plausible root causes** with similar evidence weight: +``` +I've identified two equally plausible explanations: + +1. **Test filtering discrepancy** - Test may be filtered in PR but runs in merge queue +2. **Environmental timing issue** - Race condition in test setup + +Would you like me to: +A) Deep dive into test filtering configuration (5 min analysis) +B) Analyze test timing patterns across recent runs (5 min analysis) +C) Investigate both in parallel (10 min analysis) +``` + +**2. Insufficient information** requiring deeper investigation: +``` +**FACT**: Test failed with NullPointerException at line 234 +**HYPOTHESIS**: Could be either (a) data initialization race or (b) mock configuration issue +**NEED**: Additional log context around test setup (lines 200-240) + +Would you like me to extract and analyze the full setup context? This will add ~2 min. +``` + +**3. Trade-offs between investigation paths:** +``` +I can either: +- **Quick path** (2 min): Verify this matches known flaky test pattern → recommend retry +- **Thorough path** (10 min): Analyze why test is flaky → identify potential fix + +What's your priority: unblock immediately or understand root cause? +``` + +**4. Recommendation requires user context:** +``` +This appears to be a genuine code defect in the new pagination logic. + +Options: +1. Revert PR and investigate offline +2. Push fix commit to existing PR +3. Merge with known issue and create follow-up + +What's the team's current priority: stability or feature velocity? +``` + +## Comparison with Old Approach + +### Before (Hardcoded Logic) + +```bash +# 100+ lines of pattern matching +detect_flaky_patterns() { + if grep -qi "modDate"; then indicators+=("date_ordering"); fi + if grep -E "expected: but was: "; then indicators+=("boolean_flip"); fi + # ... 20 more hardcoded rules +} + +classify_root_cause() { + if [ "$has_known_issue" = true ]; then category="flaky_test"; fi + # ... 50 more lines of brittle logic +} +``` + +**Problems:** +- Misses new patterns +- Can't explain reasoning +- Hard to maintain +- macOS incompatible + +### After (AI-Guided) + +```python +# Present evidence to AI +evidence = present_complete_diagnostic(log_file) + +# AI analyzes and explains: +# "This is ContentTypeCommandIT with modDate ordering (line 477), +# boolean flip assertion, matching known issue #33746. +# Classification: Flaky Test (high confidence)" +``` + +**Benefits:** +- Recognizes new patterns +- Explains reasoning clearly +- Easy to maintain +- Works on all platforms +- More accurate + +## Additional Context + +For more information: +- [WORKFLOWS.md](WORKFLOWS.md) - Detailed workflow descriptions and failure patterns +- [LOG_ANALYSIS.md](LOG_ANALYSIS.md) - Advanced log analysis techniques +- [utils/README.md](utils/README.md) - Utility function reference +- [ISSUE_TEMPLATE.md](ISSUE_TEMPLATE.md) - Issue creation template + + diff --git a/.claude/skills/cicd-diagnostics/SKILL.md b/.claude/skills/cicd-diagnostics/SKILL.md new file mode 100644 index 000000000000..aa1b4a0e2c84 --- /dev/null +++ b/.claude/skills/cicd-diagnostics/SKILL.md @@ -0,0 +1,765 @@ +--- +name: cicd-diagnostics +description: Diagnoses DotCMS GitHub Actions failures (PR builds, merge queue, nightly, trunk). Analyzes failed tests, root causes, compares runs. Use for "fails in GitHub", "merge queue failure", "PR build failed", "nightly build issue". +version: 2.2.0 +dependencies: python>=3.8 +--- + +# CI/CD Build Diagnostics + +**Persona: Senior Platform Engineer - CI/CD Specialist** + +You are an experienced platform engineer specializing in DotCMS CI/CD failure diagnosis. See [REFERENCE.md](REFERENCE.md) for detailed technical expertise and diagnostic patterns. + +## Core Workflow Types + +- **cicd_1-pr.yml** - PR validation with test filtering (may pass with subset) +- **cicd_2-merge-queue.yml** - Full test suite before merge (catches filtered tests) +- **cicd_3-trunk.yml** - Post-merge deployment (uses artifacts, no test re-run) +- **cicd_4-nightly.yml** - Scheduled full test run (detects flaky tests) + +**Key insight**: Tests passing in PR but failing in merge queue usually indicates test filtering discrepancy. + +## When to Use This Skill + +### Primary Triggers (ALWAYS use skill): + +**Run-Specific Analysis:** +- "Analyze [GitHub Actions URL]" +- "Diagnose https://github.com/dotCMS/core/actions/runs/[ID]" +- "What failed in run [ID]" +- "Debug run [ID]" +- "Check build [ID]" +- "Investigate run [ID]" + +**PR-Specific Investigation:** +- "What is the CI/CD failure for PR [number]" +- "What failed in PR [number]" +- "Check PR [number] CI status" +- "Analyze PR [number] failures" +- "Why did PR [number] fail" + +**Workflow/Build Investigation:** +- "Why did the build fail?" +- "What's wrong with the CI?" +- "Check CI/CD status" +- "Debug [workflow-name] failure" +- "What's failing in CI?" + +**Comparative Analysis:** +- "Why did PR pass but merge queue fail?" +- "Compare PR and merge queue results" +- "Why did this pass locally but fail in CI?" + +**Flaky Test Investigation:** +- "Is [test] flaky?" +- "Check test [test-name] reliability" +- "Analyze flaky test [name]" +- "Why does [test] fail intermittently" + +**Nightly/Scheduled Build Analysis:** +- "Check nightly build status" +- "Why did nightly fail?" +- "Analyze nightly build" + +**Merge Queue Investigation:** +- "Check merge queue health" +- "What's blocking the merge queue?" +- "Why is merge queue failing?" + +### Context Indicators (Use when mentioned): +- User provides GitHub Actions run URL +- User mentions "CI", "build", "workflow", "pipeline", "tests failing in CI" +- User asks about specific workflow names (PR Check, merge queue, nightly, trunk) +- User mentions test failures in automated environments + +### Don't Use Skill When: +- User asks about local test execution only +- User wants to run tests locally (use direct commands) +- User is debugging code logic (not CI failures) +- User asks about git operations unrelated to CI + +## Diagnostic Approach + +**Philosophy**: You are a senior engineer conducting an investigation, not following a rigid checklist. Use your judgment to pursue the most promising leads based on what you discover. The steps below are tools and techniques, not a mandatory sequence. + +**Core Investigation Pattern**: +1. **Understand the context** - What failed? When? How often? +2. **Gather evidence** - Logs, errors, timeline, patterns +3. **Form hypotheses** - What are the possible causes? +4. **Test hypotheses** - Which evidence supports/refutes each? +5. **Draw conclusions** - Root cause with confidence level +6. **Provide recommendations** - How to fix, prevent, or investigate further + +--- + +## Investigation Decision Tree + +**Use this to guide your investigation approach based on initial findings:** + +``` +Start → Identify what failed → Gather evidence → What type of failure? + +├─ Test Failure? +│ ├─ Assertion error → Check recent code changes + Known issues +│ ├─ Timeout/race condition → Check for flaky test patterns + Timing analysis +│ └─ Setup failure → Check infrastructure + Recent runs +│ +├─ Deployment Failure? +│ ├─ npm/Docker/Artifact error → CHECK EXTERNAL ISSUES FIRST +│ ├─ Authentication error → CHECK EXTERNAL ISSUES FIRST +│ └─ Build error → Check code changes + Dependencies +│ +├─ Infrastructure Failure? +│ ├─ Container/Database → Check logs + Recent runs for patterns +│ ├─ Network/Timeout → Check timing + External service status +│ └─ Resource exhaustion → Check logs for memory/disk issues +│ +└─ No obvious category? + → Gather more evidence → Present complete diagnostic → AI analysis +``` + +**Key Decision Points:** + +1. **After gathering evidence** → Does this look like external service issue? + - YES → Run external_issues.py, check service status, search web + - NO → Focus on code changes, test patterns, internal issues + +2. **After checking known issues** → Is this a duplicate? + - YES → Link to existing issue, assess if new information + - NO → Continue investigation + +3. **After initial analysis** → Confidence level? + - HIGH → Write diagnosis, create issue if needed + - MEDIUM/LOW → Gather more context, compare runs, deep dive logs + +--- + +## Investigation Toolkit + +Use these techniques flexibly based on your decision tree path: + +### Setup and Load Utilities (Always Start Here) + +**CRITICAL**: All commands must run from repository root. Never use `cd` to change directories. + +**CRITICAL**: This skill uses Python 3.8+ for all utility scripts. Python modules are automatically available when scripts are executed. + +**🚨 CRITICAL - SCRIPT PARAMETER ORDER 🚨** + +**ALL fetch-*.py scripts use the SAME parameter order:** + +``` +fetch-metadata.py +fetch-jobs.py +fetch-logs.py [JOB_ID] +``` + +**Remember: RUN_ID is ALWAYS first, WORKSPACE is ALWAYS second!** + +Initialize the diagnostic workspace: + +```bash +# Use the Python init script to set up workspace +RUN_ID=19131365567 +python3 .claude/skills/cicd-diagnostics/init-diagnostic.py "$RUN_ID" +# Outputs: WORKSPACE=/path/to/.claude/diagnostics/run-{RUN_ID} + +# IMPORTANT: Extract and set WORKSPACE variable from output +WORKSPACE="/Users/stevebolton/git/core2/.claude/diagnostics/run-${RUN_ID}" +``` + +**Available Python utilities** (imported automatically): +- **workspace.py** - Diagnostic workspace with automatic caching +- **github_api.py** - GitHub API wrappers for runs/jobs/logs +- **evidence.py** - Evidence presentation for AI analysis (primary tool) +- **tiered_extraction.py** - Tiered log extraction (Level 1/2/3) + +All utilities use Python standard library and GitHub CLI (gh). No external Python packages required. + +### Identify Target and Create Workspace + +**Extract run ID from URL or PR:** + +```bash +# From URL: https://github.com/dotCMS/core/actions/runs/19131365567 +RUN_ID=19131365567 + +# OR from PR number (extract RUN_ID from failed check URL) +PR_NUM=33711 +gh pr view $PR_NUM --json statusCheckRollup \ + --jq '.statusCheckRollup[] | select(.conclusion == "FAILURE") | .detailsUrl' | head -1 +# Extract RUN_ID from the URL output + +# Workspace already created by init script in step 0 +WORKSPACE="/Users/stevebolton/git/core2/.claude/diagnostics/run-${RUN_ID}" +``` + +### 2. Fetch Workflow Data (with caching) + +**Use Python helper scripts - remember: RUN_ID first, WORKSPACE second:** + +```bash +# ✅ CORRECT PARAMETER ORDER: + +# Example values for reference: +# RUN_ID=19131365567 +# WORKSPACE="/Users/stevebolton/git/core2/.claude/diagnostics/run-19131365567" + +# Fetch metadata (uses caching) +python3 .claude/skills/cicd-diagnostics/fetch-metadata.py "$RUN_ID" "$WORKSPACE" +# ^^^^^^^^ ^^^^^^^^^^ +# FIRST SECOND + +# Fetch jobs (uses caching) +python3 .claude/skills/cicd-diagnostics/fetch-jobs.py "$RUN_ID" "$WORKSPACE" +# ^^^^^^^^ ^^^^^^^^^^ +# FIRST SECOND + +# Set file paths +METADATA="$WORKSPACE/run-metadata.json" +JOBS="$WORKSPACE/jobs-detailed.json" +``` + +### 3. Download Failed Job Logs + +The fetch-jobs.py script displays failed job IDs. Use those to download logs: + +```bash +# ✅ CORRECT PARAMETER ORDER: [JOB_ID] + +# Example values for reference: +# RUN_ID=19131365567 +# WORKSPACE="/Users/stevebolton/git/core2/.claude/diagnostics/run-19131365567" +# FAILED_JOB_ID=54939324205 + +# Download logs for specific failed job +python3 .claude/skills/cicd-diagnostics/fetch-logs.py "$RUN_ID" "$WORKSPACE" "$FAILED_JOB_ID" +# ^^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^^^^^ +# FIRST SECOND THIRD (optional) + +# Or download all failed job logs (omit JOB_ID) +python3 .claude/skills/cicd-diagnostics/fetch-logs.py "$RUN_ID" "$WORKSPACE" +``` + +**⌠COMMON MISTAKES TO AVOID:** + +```bash +# ⌠WRONG - Missing RUN_ID (only 2 params when you need 3) +python3 .claude/skills/cicd-diagnostics/fetch-logs.py "$WORKSPACE" "$FAILED_JOB_ID" + +# ⌠WRONG - Swapped RUN_ID and WORKSPACE +python3 .claude/skills/cicd-diagnostics/fetch-logs.py "$WORKSPACE" "$RUN_ID" "$FAILED_JOB_ID" + +# ⌠WRONG - Job ID in second position +python3 .claude/skills/cicd-diagnostics/fetch-logs.py "$RUN_ID" "$FAILED_JOB_ID" "$WORKSPACE" +``` + +**Parameter order**: RUN_ID, WORKSPACE, JOB_ID (optional) +- If you get "WORKSPACE parameter appears to be a job ID" error, you likely forgot RUN_ID or swapped parameters +- All three scripts (fetch-metadata.py, fetch-jobs.py, fetch-logs.py) use the same order +- **Mnemonic: Think "Run → Where → What" (Run ID → Workspace → Job ID)** + +### 4. Present Evidence to AI (KEY STEP!) + +**This is where AI-guided analysis begins.** Use Python `evidence.py` to present raw data: + +```python +from pathlib import Path +import sys +sys.path.insert(0, str(Path(".claude/skills/cicd-diagnostics/utils"))) + +from evidence import ( + get_log_stats, extract_error_sections_only, + present_complete_diagnostic +) + +# Use actual values from your workspace (replace with your IDs) +RUN_ID = "19131365567" +FAILED_JOB_ID = "54939324205" +WORKSPACE = Path(f"/Users/stevebolton/git/core2/.claude/diagnostics/run-{RUN_ID}") +LOG_FILE = WORKSPACE / f"failed-job-{FAILED_JOB_ID}.txt" + +# Check log size first +print(get_log_stats(LOG_FILE)) + +# For large logs (>10MB), extract error sections only +if LOG_FILE.stat().st_size > 10485760: + print("Large log detected - extracting error sections...") + ERROR_FILE = WORKSPACE / "error-sections.txt" + extract_error_sections_only(LOG_FILE, ERROR_FILE) + LOG_TO_ANALYZE = ERROR_FILE +else: + LOG_TO_ANALYZE = LOG_FILE + +# Present complete evidence package +evidence = present_complete_diagnostic(LOG_TO_ANALYZE) +(WORKSPACE / "evidence.txt").write_text(evidence) + +# Display evidence for AI analysis +print(evidence) +``` + +**What this shows:** +- Failed tests (JUnit, E2E, Postman) +- Error messages with context +- Assertion failures (expected vs actual) +- Stack traces +- Timing indicators (timeouts, race conditions) +- Infrastructure indicators (Docker, DB, ES) +- First error context (for cascade detection) +- Failure timeline +- Known issues matching test name + +### Check Known Issues (Guided by Evidence) + +**Decision Point: When should you check for known issues?** + +**Check Internal GitHub Issues when:** +- Error message/test name suggests a known pattern +- After identifying the failure type (test, deployment, infrastructure) +- Quick search can save deep analysis time + +**Check External Issues when evidence suggests:** +- 🔴 **HIGH Priority** - Authentication errors + service names (npm, Docker, GitHub) +- 🟡 **MEDIUM Priority** - Infrastructure errors + timing correlation +- ⚪ **LOW Priority** - Test failures with clear assertions + +**Skip external checks if:** +- Test assertion failure with obvious code bug +- Known flaky test already documented +- Recent PR introduced clear breaking change + +#### A. Automated External Issue Detection (Use When Warranted) + +**The external_issues.py utility helps decide if external investigation is needed:** + +```python +from pathlib import Path +import sys +sys.path.insert(0, str(Path(".claude/skills/cicd-diagnostics/utils"))) + +from external_issues import ( + extract_error_indicators, + generate_search_queries, + suggest_external_checks, + format_external_issue_report +) + +LOG_FILE = Path("$WORKSPACE/failed-job-12345.txt") +log_content = LOG_FILE.read_text(encoding='utf-8', errors='ignore') + +# Extract error patterns +indicators = extract_error_indicators(log_content) + +# Generate targeted search queries +search_queries = generate_search_queries(indicators, "2025-11-10") + +# Get specific recommendations +recent_runs = [ + ("2025-11-10", "failure"), + ("2025-11-09", "failure"), + ("2025-11-08", "failure"), + ("2025-11-07", "failure"), + ("2025-11-06", "success") +] +suggestions = suggest_external_checks(indicators, recent_runs) + +# Print formatted report +print(format_external_issue_report(indicators, search_queries, suggestions)) +``` + +**This utility automatically:** +- Detects npm, Docker, GitHub Actions errors +- Identifies authentication/token issues +- Assesses likelihood of external cause (LOW/MEDIUM/HIGH) +- Generates targeted web search queries +- Suggests specific external sources to check + +#### B. Search Internal GitHub Issues + +```bash +# Search for error-specific keywords from evidence +gh issue list --search "npm ERR" --state all --limit 10 --json number,title,state,createdAt,labels + +# Search for component-specific issues +gh issue list --search "docker build" --state all --limit 10 +gh issue list --label "ci-cd" --state all --limit 20 + +# Look for recently closed issues (may have resurfaced) +gh issue list --search "authentication token" --state closed --limit 10 +``` + +**Pattern matching:** +- Extract key error codes (e.g., `EOTP`, `ENEEDAUTH`, `ERR_CONNECTION_REFUSED`) +- Search for component names (e.g., `npm`, `docker`, `elasticsearch`) +- Look for similar failure patterns in issue descriptions + +#### C. Execute Web Searches for High-Likelihood External Issues + +**When the utility suggests HIGH likelihood of external cause:** + +Use the generated search queries from step A with WebSearch tool: + +```python +# Execute top priority searches +for query in search_queries[:3]: # Top 3 most relevant + print(f"\n🔠Searching: {query}\n") + # Use WebSearch tool with the query +``` + +**Key external sources to check:** +1. **npm registry**: https://github.blog/changelog/ (search: "npm security token") +2. **GitHub Actions status**: https://www.githubstatus.com/ +3. **Docker Hub status**: https://status.docker.com/ +4. **Service changelogs**: Check breaking changes in major versions + +**When to use WebFetch:** +- To read specific changelog pages identified by searches +- To validate exact dates of service changes +- To get detailed migration instructions + +```python +# Example: Fetch npm security update details +WebFetch( + url="https://github.blog/changelog/2025-11-05-npm-security-update...", + prompt="Extract the key dates, changes to npm tokens, and impact on CI/CD workflows" +) +``` + +#### D. Correlation Analysis + +**Red flags for external issues:** +- ✅ Failure started on specific date with no code changes +- ✅ Error mentions external service (npm, Docker Hub, GitHub) +- ✅ Authentication/authorization errors +- ✅ Multiple unrelated projects affected (search reveals community reports) +- ✅ Error message suggests policy change ("requires 2FA", "token expired") + +**Document findings:** +```markdown +## Known Issues + +### Internal (dotCMS Repository) +- Issue #XXXXX: Similar error, status, resolution + +### External (Service Provider Changes) +- Service: +- Change Date: +- Impact: +- Source: +- Timeline: +``` + +### Senior Engineer Analysis (Evidence-Based Reasoning) + +**As a senior engineer, analyze the evidence systematically:** + +#### A. Initial Hypothesis Generation +Consider **multiple competing hypotheses**: +- **Code Defect** - New bug introduced by recent changes? +- **Flaky Test - Timing Issue** - Race condition, clock precision, async timing? +- **Flaky Test - Concurrency Issue** - Thread safety violation, deadlock, shared state? +- **Request Context Issue** - ThreadLocal accessed from background thread? User null in Quartz job? +- **Infrastructure Issue** - Docker/DB/ES environment problem? +- **Test Filtering** - PR test subset passed, full merge queue suite failed? +- **Cascading Failure** - Primary error triggering secondary failures? + +**Apply specialized diagnostic lens** (see [REFERENCE.md](REFERENCE.md) for detailed patterns): +- Look for timing patterns: Identical timestamps, boolean flips, ordering failures +- Check thread context: Background jobs (Quartz), async operations, thread pool execution +- Identify request lifecycle: HTTP request boundary vs background execution +- Examine concurrency: Shared state, locks, atomic operations + +#### B. Evidence Evaluation +For each hypothesis, assess supporting/contradicting evidence: +- **FACT**: What the logs definitively show (error messages, line numbers, stack traces) +- **HYPOTHESIS**: What this might indicate (must be labeled as theory) +- **CONFIDENCE**: How certain are you (High/Medium/Low with reasoning) + +#### C. Differential Diagnosis +Apply systematic elimination: +1. Check recent code changes vs failure (correlation ≠ causation) +2. Search known issues for matching patterns (exact matches = high confidence) +3. Analyze recent run history (consistent vs intermittent) +4. Examine error timing and cascades (primary vs secondary failures) + +#### D. Log Context Extraction (Efficient) +**For large logs (>10MB):** +- Extract only relevant error sections (99%+ reduction) +- Identify specific line numbers and context (±10 lines) +- Note timing patterns (timestamps show cascade vs independent) +- Track infrastructure events (Docker, DB connections, ES indices) + +**When you need more context from logs:** +```python +from pathlib import Path +import re + +LOG_FILE = Path("$WORKSPACE/failed-job-12345.txt") +lines = LOG_FILE.read_text(encoding='utf-8', errors='ignore').split('\n') + +# Extract specific context around an error (lines 450-480) +print('\n'.join(lines[449:480])) + +# Search for related errors by pattern +for i, line in enumerate(lines, 1): + if "ContentTypeCommandIT" in line: + print(f"{i}: {line}") + if i >= 20: + break + +# Get timing correlation for cascade analysis +timestamp_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}') +for line in lines[:50]: + if timestamp_pattern.match(line) and ("ERROR" in line or "FAILURE" in line): + print(line) +``` + +#### E. Final Classification +Provide evidence-based conclusion: + +1. **Root Cause Classification** + - Category: New failure / Flaky test / Infrastructure / Test filtering + - Confidence: High / Medium / Low (with reasoning) + - Competing hypotheses considered and why rejected + +2. **Test Fingerprint** (natural language) + - Test name and exact location (file:line) + - Failure pattern (assertion type, timing characteristics, error signature) + - Key identifiers for matching similar failures + +3. **Known Issue Matching** + - Exact matches with open GitHub issues + - Pattern matches with documented flaky tests + - If no match: clearly state "No known issue found" + +4. **Impact Assessment** + - Blocking status (is this blocking merge/deploy?) + - False positive likelihood (should retry help?) + - Frequency analysis (first occurrence vs recurring) + - Developer friction impact + +### 7. Get Additional Context (if needed) + +**For comparative analysis or frequency checks:** + +```python +import sys +from pathlib import Path +sys.path.insert(0, str(Path(".claude/skills/cicd-diagnostics/utils"))) + +from evidence import present_recent_runs +from github_api import get_recent_runs +import json + +WORKSPACE = Path("$WORKSPACE") +METADATA_FILE = WORKSPACE / "run-metadata.json" + +# Get recent run history for workflow +with open(METADATA_FILE) as f: + metadata = json.load(f) +workflow_name = metadata.get('workflowName') +print(present_recent_runs(workflow_name, 20)) + +# For PR vs Merge Queue comparison +if "merge-queue" in workflow_name: + current_sha = metadata.get('headSha') + pr_runs = get_recent_runs("cicd_1-pr.yml", 1) + if pr_runs and pr_runs[0].get('headSha') == current_sha: + pr_result = pr_runs[0].get('conclusion') + if pr_result == "success": + print("âš ï¸ Test Filtering Issue: PR passed but merge queue failed") + print("This suggests test was filtered in PR but ran in merge queue") +``` + +### 8. Generate Comprehensive Report + +**AI writes report naturally** (not a template): + +**CRITICAL**: Generate TWO separate reports: +1. **DIAGNOSIS.md** - User-facing failure diagnosis (no skill evaluation) +2. **ANALYSIS_EVALUATION.md** - Skill effectiveness evaluation (meta-analysis) + +See [REFERENCE.md](REFERENCE.md) for report templates and structure. + +**IMPORTANT**: +- **DIAGNOSIS.md** = User-facing failure analysis (what failed, why, how to fix) +- **ANALYSIS_EVALUATION.md** = Internal skill evaluation (how well the skill performed) +- DO NOT mix skill effectiveness evaluation into DIAGNOSIS.md +- Users should not see skill meta-analysis in their failure reports + +### 9. Collaborate with User (When Multiple Paths Exist) + +**As a senior engineer, when you encounter decision points or uncertainty, engage the user:** + +#### When to Ask for User Input: +1. **Multiple plausible root causes** with similar evidence weight +2. **Insufficient information** requiring deeper investigation +3. **Trade-offs between investigation paths** +4. **Recommendation requires user context** + +See [REFERENCE.md](REFERENCE.md) for examples of user collaboration patterns. + +### 10. Create Issue (if needed) + +**After analysis, determine if issue creation is warranted:** + +```python +import subprocess +import json + +# Senior engineer judgment call based on: +# - Is this already tracked? (check known issues) +# - Is this a new failure? (check recent history) +# - Is this blocking development? (impact assessment) +# - Would an issue help track/fix it? (actionability) + +if CREATE_ISSUE: + issue_body = f"""## Summary +{summary} + +## Failure Evidence +{evidence_excerpts} + +## Root Cause Analysis +{analysis_with_confidence} + +## Reproduction Pattern +{reproduction_steps} + +## Diagnostic Run +- Run ID: {RUN_ID} +- Workspace: {WORKSPACE} + +## Recommended Actions +{recommendations} +""" + + subprocess.run([ + "gh", "issue", "create", + "--title", f"[CI/CD] {brief_description}", + "--label", "bug,ci-cd,Flakey Test", + "--body", issue_body + ]) +``` + +## Key Principles + +### 1. Evidence-Driven, Not Rule-Based + +**Don't hardcode classification logic**. Present evidence and let AI reason: + +⌠**Bad** (rigid rules): +```python +if "modDate" in log_content: + return "flaky_test" +if "npm" in log_content: + check_external_always() # Wasteful +``` + +✅ **Good** (AI interprets evidence): +```python +evidence = present_complete_diagnostic(log_file) +# AI sees "modDate + boolean flip + issue #33746" → concludes "flaky test" +# AI sees "npm ERR! + EOTP + timing correlation" → checks external issues +# AI sees "AssertionError + recent PR" → focuses on code changes +``` + +### 2. Adaptive Investigation Depth + +**Let findings guide how deep you go:** + +``` +Quick Win (30 sec - 2 min) +└─ Known issue? → Link and done +└─ Clear error? → Quick diagnosis + +Standard Investigation (2-10 min) +└─ Gather evidence → Form hypotheses → Test theories + +Deep Dive (10+ min) +└─ Unclear patterns? → Compare runs, check history, analyze timing +└─ Multiple theories? → Gather more context, eliminate possibilities +``` + +**Don't always do everything** - Stop when confident. + +### 3. Context Shapes Interpretation + +**Same error, different meaning in different workflows:** + +``` +"Test timeout" in PR workflow → Might be code issue, check changes +"Test timeout" in nightly → Likely flaky test, check history +"npm ERR!" in deployment → Check external issues FIRST +"npm ERR!" in build → Check package.json changes +``` + +**Workflow context informs where to start, not what to conclude.** + +### 4. Tool Selection Based on Failure Type + +**Don't use every tool every time:** + +| Failure Type | Primary Tools | Skip | +|--------------|---------------|------| +| Deployment/Auth | external_issues.py, WebSearch | Deep log analysis | +| Test assertion | Code changes, test history | External checks | +| Flaky test | Run history, timing patterns | External checks | +| Infrastructure | Recent runs, log patterns | Code changes | + +### 5. Leverage Caching + +Workspace automatically caches: +- Run metadata +- Job details +- Downloaded logs +- Evidence extraction + +**Rerunning the skill uses cached data** (much faster!) + +## Output Format + +**Write naturally, like a senior engineer writing to a colleague.** Include relevant sections based on what you discovered: + +**Core sections (always):** +- **Executive Summary** - What failed and why (2-3 sentences) +- **Root Cause** - Your conclusion with confidence level and reasoning +- **Evidence** - Key findings that support your conclusion +- **Recommendations** - What should happen next + +**Additional sections (as relevant):** +- **Known Issues** - Internal or external issues found (if checked) +- **Timeline Analysis** - When it started failing (if relevant) +- **Test Fingerprint** - Pattern for matching (if test failure) +- **Impact Assessment** - Blocking status, frequency (if important) +- **Competing Hypotheses** - Theories you ruled out (if multiple possibilities) + +**Don't force sections that don't add value.** A deployment authentication error doesn't need a "Test Fingerprint" section. + +## Success Criteria + +**Investigation Quality:** +✅ Identified specific failure point with evidence +✅ Determined root cause with reasoning (not just labels) +✅ Assessed whether this is a known issue (when relevant) +✅ Made appropriate use of external validation (when patterns suggest it) +✅ Provided actionable recommendations + +**Process Quality:** +✅ Used adaptive investigation depth (stopped when confident) +✅ Let evidence guide technique selection (didn't use every tool blindly) +✅ Explained confidence level and competing theories +✅ Saved diagnostic artifacts in workspace +✅ Wrote natural, contextual report (not template-filled) + +## Reference Files + +For detailed information: +- [REFERENCE.md](REFERENCE.md) - Detailed technical expertise, diagnostic patterns, and examples +- [WORKFLOWS.md](WORKFLOWS.md) - Workflow descriptions and patterns +- [LOG_ANALYSIS.md](LOG_ANALYSIS.md) - Advanced log analysis techniques +- [utils/README.md](utils/README.md) - Utility function reference +- [ISSUE_TEMPLATE.md](ISSUE_TEMPLATE.md) - Issue creation template +- [README.md](README.md) - Quick reference and examples diff --git a/.claude/skills/cicd-diagnostics/WORKFLOWS.md b/.claude/skills/cicd-diagnostics/WORKFLOWS.md new file mode 100644 index 000000000000..6d00e95205ab --- /dev/null +++ b/.claude/skills/cicd-diagnostics/WORKFLOWS.md @@ -0,0 +1,347 @@ +# DotCMS CI/CD Workflows Reference + +Complete documentation of workflow behaviors and failure patterns. + +## cicd_1-pr.yml - Pull Request Validation + +**Purpose**: Fast feedback on PR changes with optimized test selection + +**Triggers**: +- Pull request opened/synchronized +- Re-run requested + +**Test Strategy**: +- **Filtered tests**: Runs subset based on changed files +- **Optimization goal**: Fast feedback (5-15 min typical) +- **Trade-off**: May miss integration issues caught in full suite + +**Common Failure Patterns**: + +1. **Code Compilation Errors** + - Pattern: `[ERROR] COMPILATION ERROR` + - Cause: Syntax errors, missing imports, type errors + - Log location: Maven build output, early in job + - Action: Fix compilation errors in PR + +2. **Unit Test Failures** + - Pattern: `Tests run:.*Failures: [1-9]` + - Cause: Breaking changes in code + - Log location: Surefire reports + - Action: Fix failing tests or revert breaking change + +3. **Lint/Format Violations** + - Pattern: `Checkstyle violations`, `PMD violations` + - Cause: Code style issues + - Log location: Static analysis step + - Action: Run `mvn spotless:apply` locally + +4. **Filtered Test Passes (False Positive)** + - Pattern: PR passes, merge queue fails + - Cause: Integration test not run in PR due to filtering + - Detection: Compare PR vs merge queue results for same commit + - Action: Run full test suite locally or wait for merge queue + +**Typical Duration**: 5-20 minutes + +**Workflow URL**: https://github.com/dotCMS/core/actions/workflows/cicd_1-pr.yml + +## cicd_2-merge-queue.yml - Pre-Merge Full Validation + +**Purpose**: Comprehensive validation before merging to main branch + +**Triggers**: +- PR added to merge queue (manual or automated) +- Required status checks passed + +**Test Strategy**: +- **Full test suite**: ALL tests run (integration, unit, E2E) +- **No filtering**: Catches issues missed in PR workflow +- **Duration**: 30-60 minutes typical + +**Common Failure Patterns**: + +1. **Test Filtering Discrepancy** + - Pattern: PR passed ✓, merge queue failed ✗ + - Cause: Test filtered in PR, failed in full suite + - Detection: Same commit, different outcomes + - Action: Fix the test that was filtered out + - Prevention: Run full suite locally before merge + +2. **Multiple PR Conflicts** + - Pattern: PR A passes, PR B passes, merge queue with both fails + - Cause: Conflicting changes between PRs + - Detection: Multiple PRs in queue, all passing individually + - Log pattern: Integration test failures, database state issues + - Action: Rebase one PR on the other, re-test + +3. **Previous PR Failure Contamination** + - Pattern: PR fails immediately after another PR failure + - Cause: Shared state or resources from previous run + - Detection: Check previous run in queue + - Action: Re-run the workflow (no code changes needed) + +4. **Branch Not Synchronized** + - Pattern: Tests fail that pass on main + - Cause: PR branch behind main, missing recent fixes + - Detection: `gh pr view $PR --json mergeable` shows `BEHIND` + - Action: Merge main into PR branch, re-test + +5. **Flaky Tests** + - Pattern: Intermittent failures, passes on re-run + - Cause: Test has race conditions, timing dependencies + - Detection: Same test fails/passes across runs + - Action: Investigate test, add to flaky test tracking + - Labels: `flaky-test` + +6. **Infrastructure Timeouts** + - Pattern: `timeout`, `connection refused`, `rate limit exceeded` + - Cause: GitHub Actions infrastructure, external services + - Detection: No code changes, external error messages + - Action: Re-run workflow, check GitHub status + +**Typical Duration**: 30-90 minutes + +**Critical Checks Before Merge**: +```bash +# Verify PR is up to date +gh pr view $PR_NUMBER --json mergeStateStatus + +# Check for other PRs in queue +gh pr list --search "is:open base:main label:merge-queue" + +# Review recent merge queue runs +gh run list --workflow=cicd_2-merge-queue.yml --limit 10 +``` + +**Workflow URL**: https://github.com/dotCMS/core/actions/workflows/cicd_2-merge-queue.yml + +## cicd_3-trunk.yml - Post-Merge Deployment + +**Purpose**: Deploy merged changes, publish artifacts, build Docker images + +**Triggers**: +- Successful merge to main branch +- Uses artifacts from merge queue (no test re-run) + +**Key Operations**: +1. Retrieve build artifacts from merge queue +2. Deploy to staging environment +3. Build and push Docker images +4. Run CLI smoke tests +5. Update documentation sites + +**Common Failure Patterns**: + +1. **Artifact Retrieval Failure** + - Pattern: `artifact not found`, `download failed` + - Cause: Merge queue artifacts expired or missing + - Detection: Early failure in artifact download step + - Action: Re-run merge queue to regenerate artifacts + +2. **Docker Build Failure** + - Pattern: `failed to build`, `COPY failed`, `image too large` + - Cause: Dockerfile changes, dependency updates, resource limits + - Log location: Docker build step + - Action: Review Dockerfile changes, check layer sizes + +3. **Docker Push Failure** + - Pattern: `denied: access forbidden`, `rate limit`, `timeout` + - Cause: Registry authentication, network, rate limits + - Detection: Build succeeds, push fails + - Action: Check registry credentials, retry after rate limit + +4. **CLI Tool Failures** + - Pattern: CLI command errors, integration failures + - Cause: API changes breaking CLI, environment config + - Log location: CLI test/validation steps + - Action: Review CLI compatibility with API changes + +5. **Deployment Configuration Issues** + - Pattern: Configuration errors, environment variable issues + - Cause: Missing secrets, config changes + - Detection: Deployment step failures + - Action: Verify environment configuration in GitHub secrets + +**Important Notes**: +- Tests are NOT re-run (assumes merge queue validation) +- Test failures here indicate artifact corruption or environment issues +- Deployment failures don't necessarily mean code issues + +**Typical Duration**: 15-30 minutes + +**Workflow URL**: https://github.com/dotCMS/core/actions/workflows/cicd_3-trunk.yml + +## cicd_4-nightly.yml - Scheduled Full Validation + +**Purpose**: Detect flaky tests, infrastructure issues, external dependency changes + +**Triggers**: +- Scheduled (nightly, e.g., 2 AM UTC) +- Manual trigger via workflow dispatch + +**Test Strategy**: +- Full test suite against main branch +- Latest dependencies (detects upstream breaking changes) +- Longer timeout thresholds +- Multiple test runs for flaky detection (optional) + +**Common Failure Patterns**: + +1. **Flaky Test Detection** + - Pattern: Test fails occasionally, not consistently + - Cause: Race conditions, timing dependencies, resource contention + - Detection: Failure rate < 100% over multiple nights + - Analysis: Track test across 20-30 nightly runs + - Action: Mark as flaky, investigate root cause + - Threshold: >5% failure rate = needs attention + +2. **External Dependency Changes** + - Pattern: Tests fail after dependency update + - Cause: Upstream library using `latest` or mutable version + - Detection: No code changes in repo, failure starts suddenly + - Log pattern: `NoSuchMethodError`, API compatibility errors + - Action: Pin dependency versions, update code for compatibility + +3. **GitHub Actions Version Changes** + - Pattern: Workflow steps fail, GitHub Actions behavior changed + - Cause: GitHub Actions runner or action version updated + - Detection: Workflow YAML unchanged, runner behavior different + - Log pattern: Action warnings, deprecation notices + - Action: Update action versions explicitly in workflow + +4. **Infrastructure Degradation** + - Pattern: Timeouts, slow tests, resource exhaustion + - Cause: GitHub Actions infrastructure issues + - Detection: Tests pass but take much longer, timeouts + - Action: Check GitHub Actions status, wait for resolution + +5. **Database/Elasticsearch State Issues** + - Pattern: Tests fail with data inconsistencies + - Cause: Cleanup issues, state leakage between tests + - Detection: Tests pass individually, fail in suite + - Action: Improve test isolation, add cleanup + +6. **Time-Dependent Test Failures** + - Pattern: Tests fail at specific times (timezone, daylight saving) + - Cause: Hard-coded dates, timezone assumptions + - Detection: Failure coincides with date/time changes + - Action: Use relative dates, mock time in tests + +**Flaky Test Analysis Process**: +```bash +# Get last 30 nightly runs +gh run list --workflow=cicd_4-nightly.yml --limit 30 --json databaseId,conclusion,createdAt + +# For specific test, count failures +# (requires parsing test report artifacts across runs) + +# Calculate flaky percentage +# Flaky if: 5% < failure rate < 95% +# Consistently failing if: failure rate >= 95% +# Stable if: failure rate < 5% +``` + +**Typical Duration**: 45-90 minutes + +**Workflow URL**: https://github.com/dotCMS/core/actions/workflows/cicd_4-nightly.yml + +## Cross-Cutting Failure Causes + +These affect all workflows: + +### Reproducibility Issues + +**External Dependencies with Mutable Versions**: +- Maven dependencies using version ranges or `LATEST` +- Docker base images using `latest` tag +- GitHub Actions without pinned versions (@v2 vs @v2.1.0) +- NPM dependencies without lock file or using `^` ranges + +**Detection**: +- Failures start suddenly without code changes +- Different results across runs with same code +- Dependency resolution messages in logs + +**Prevention**: +- Pin all dependency versions explicitly +- Use lock files (package-lock.json, yarn.lock) +- Pin GitHub Actions to commit SHA: `uses: actions/checkout@a12b3c4` +- Avoid `latest` tags for Docker images + +### Infrastructure Issues + +**GitHub Actions Platform**: +- Runner outages or degraded performance +- Artifact storage issues +- Registry rate limits +- Network connectivity issues + +**Detection**: +```bash +# Check GitHub status +curl -s https://www.githubstatus.com/api/v2/status.json | jq '.status.description' + +# Look for infrastructure patterns in logs +grep -i "timeout\|rate limit\|connection refused\|runner.*fail" logs.txt +``` + +**Action**: Wait for GitHub resolution, retry workflow + +**External Services**: +- Maven Central unavailable +- Docker Hub rate limits +- NPM registry issues +- Elasticsearch download failures + +**Detection**: +- `Could not resolve`, `connection timeout`, `rate limit` +- Service-specific error messages + +**Action**: Wait for service resolution, use mirrors/caches + +### Resource Constraints + +**Memory/Disk Issues**: +- Pattern: `OutOfMemoryError`, `No space left on device` +- Cause: Large test suite, memory leaks, artifact accumulation +- Action: Optimize test memory, clean up artifacts, split jobs + +**Timeout Issues**: +- Pattern: Job cancelled, timeout reached +- Cause: Tests running longer than expected, hung processes +- Action: Investigate slow tests, increase timeout, optimize + +## Workflow Comparison Matrix + +| Aspect | PR | Merge Queue | Trunk | Nightly | +|--------|-----|-------------|--------|---------| +| **Tests** | Filtered subset | Full suite | None (reuses) | Full suite | +| **Duration** | 5-20 min | 30-90 min | 15-30 min | 45-90 min | +| **Purpose** | Fast feedback | Validation | Deployment | Stability | +| **Failure = Code Issue?** | Usually yes | Usually yes | Maybe no | Maybe no | +| **Retry Safe?** | Yes | Yes (check queue) | Yes | Yes | + +## Diagnostic Decision Tree + +``` +Build failed? +├─ Which workflow? +│ ├─ PR → Check compilation, unit tests, lint +│ ├─ Merge Queue → Compare with PR results +│ │ ├─ PR passed → Test filtering issue +│ │ ├─ PR failed → Same issue, expected +│ │ └─ First failure → Check queue, branch sync +│ ├─ Trunk → Check artifact retrieval, deployment +│ └─ Nightly → Likely flaky or infrastructure +│ +├─ Error type? +│ ├─ Compilation → Code issue, fix in PR +│ ├─ Test failure → Check if new or flaky +│ ├─ Timeout → Infrastructure or slow test +│ └─ Dependency → External issue or reproducibility +│ +└─ Historical pattern? + ├─ First time → New issue, recent change + ├─ Intermittent → Flaky test, track + └─ Always fails → Consistent issue, needs fix +``` \ No newline at end of file diff --git a/.claude/skills/cicd-diagnostics/fetch-jobs.py b/.claude/skills/cicd-diagnostics/fetch-jobs.py new file mode 100755 index 000000000000..d5aecfe65c54 --- /dev/null +++ b/.claude/skills/cicd-diagnostics/fetch-jobs.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Fetch job details with caching.""" + +import sys +import json +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent / "utils")) +from github_api import get_jobs_detailed + + +def main(): + if len(sys.argv) < 3: + print("Usage: python fetch-jobs.py ", file=sys.stderr) + sys.exit(1) + + run_id = sys.argv[1] + workspace = Path(sys.argv[2]) + + if not workspace: + print("ERROR: WORKSPACE parameter is required", file=sys.stderr) + sys.exit(1) + + jobs_file = workspace / "jobs-detailed.json" + + # Fetch jobs if not cached + if not jobs_file.exists(): + print("Fetching job details...") + get_jobs_detailed(run_id, jobs_file) + print(f"✓ Job details saved to {jobs_file}") + else: + print(f"✓ Using cached jobs: {jobs_file}") + + # Display failed jobs + print("") + print("=== Failed Jobs ===") + jobs_data = json.loads(jobs_file.read_text(encoding='utf-8')) + jobs = jobs_data.get('jobs', []) + + for job in jobs: + if job.get('conclusion') == 'failure': + print(f"Name: {job.get('name')}") + print(f"ID: {job.get('id')}") + print(f"Conclusion: {job.get('conclusion')}") + print("") + + +if __name__ == "__main__": + main() + + diff --git a/.claude/skills/cicd-diagnostics/fetch-logs.py b/.claude/skills/cicd-diagnostics/fetch-logs.py new file mode 100755 index 000000000000..311dc32fd2ed --- /dev/null +++ b/.claude/skills/cicd-diagnostics/fetch-logs.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Fetch failed job logs with caching.""" + +import sys +import json +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent / "utils")) +from github_api import download_job_logs, get_failed_jobs + + +def format_size(size_bytes: int) -> str: + """Format size in human-readable format.""" + for unit in ['B', 'KB', 'MB', 'GB']: + if size_bytes < 1024.0: + return f"{size_bytes:.1f}{unit}" + size_bytes /= 1024.0 + return f"{size_bytes:.1f}TB" + + +def main(): + if len(sys.argv) < 3: + print("Usage: python fetch-logs.py [JOB_ID]", file=sys.stderr) + print("", file=sys.stderr) + print("Example:", file=sys.stderr) + print(" python fetch-logs.py 19219835536 /path/to/workspace", file=sys.stderr) + print(" python fetch-logs.py 19219835536 /path/to/workspace 54939324205", file=sys.stderr) + sys.exit(1) + + run_id = sys.argv[1] + workspace_path = sys.argv[2] + + # Optional job ID parameter + specific_job_id = sys.argv[3] if len(sys.argv) > 3 else None + + # Validate parameters are not swapped (workspace should be a path, not just digits) + # A workspace path will contain slashes or be a relative path like "workspace" + # A job ID will be only digits + if workspace_path.isdigit() and len(workspace_path) > 10: + print(f"ERROR: WORKSPACE parameter appears to be a job ID: {workspace_path}", file=sys.stderr) + print("", file=sys.stderr) + print("Correct usage: python fetch-logs.py [JOB_ID]", file=sys.stderr) + print(f" RUN_ID: {run_id}", file=sys.stderr) + print(f" WORKSPACE_PATH: should be a directory path (e.g., /path/to/workspace)", file=sys.stderr) + print(f" JOB_ID (optional): {workspace_path} <- you may have meant this as job ID", file=sys.stderr) + sys.exit(1) + + workspace = Path(workspace_path) + + if not workspace.exists(): + print(f"ERROR: Workspace directory does not exist: {workspace}", file=sys.stderr) + print(f"", file=sys.stderr) + print(f"Make sure the workspace path is correct. You passed:", file=sys.stderr) + print(f" RUN_ID: {run_id}", file=sys.stderr) + print(f" WORKSPACE: {workspace_path}", file=sys.stderr) + if specific_job_id: + print(f" JOB_ID: {specific_job_id}", file=sys.stderr) + sys.exit(1) + + jobs_file = workspace / "jobs-detailed.json" + if not jobs_file.exists(): + print(f"ERROR: Jobs file not found: {jobs_file}", file=sys.stderr) + print("Run fetch-jobs.py first to get job details.", file=sys.stderr) + sys.exit(1) + + # Get failed jobs + failed_jobs = get_failed_jobs(jobs_file) + + if not failed_jobs: + print("No failed jobs found.") + return + + # If specific job ID provided, filter to that job + if specific_job_id: + failed_jobs = [job for job in failed_jobs if str(job['id']) == specific_job_id] + if not failed_jobs: + print(f"ERROR: Job {specific_job_id} not found or not failed", file=sys.stderr) + sys.exit(1) + + # Download logs for each failed job + for job in failed_jobs: + job_id = str(job['id']) + job_name = job.get('name', 'Unknown') + log_file = workspace / f"failed-job-{job_id}.txt" + + # Download logs if not cached or empty + if not log_file.exists() or log_file.stat().st_size == 0: + print(f"Downloading logs for job {job_id} ({job_name})...") + try: + download_job_logs(job_id, log_file) + size = log_file.stat().st_size + print(f"✓ Downloaded: {format_size(size)} -> {log_file}") + except Exception as e: + print(f"✗ Failed to download logs for job {job_id}: {e}", file=sys.stderr) + else: + size = log_file.stat().st_size + print(f"✓ Using cached logs: {format_size(size)} -> {log_file}") + + +if __name__ == "__main__": + main() + + diff --git a/.claude/skills/cicd-diagnostics/fetch-metadata.py b/.claude/skills/cicd-diagnostics/fetch-metadata.py new file mode 100755 index 000000000000..e49d890322c7 --- /dev/null +++ b/.claude/skills/cicd-diagnostics/fetch-metadata.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Fetch workflow metadata with caching.""" + +import sys +import json +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent / "utils")) +from github_api import get_run_metadata + + +def main(): + if len(sys.argv) < 3: + print("Usage: python fetch-metadata.py ", file=sys.stderr) + sys.exit(1) + + run_id = sys.argv[1] + workspace = Path(sys.argv[2]) + + if not workspace: + print("ERROR: WORKSPACE parameter is required", file=sys.stderr) + sys.exit(1) + + metadata_file = workspace / "run-metadata.json" + + # Fetch metadata if not cached + if not metadata_file.exists(): + print("Fetching run metadata...") + get_run_metadata(run_id, metadata_file) + print(f"✓ Metadata saved to {metadata_file}") + else: + print(f"✓ Using cached metadata: {metadata_file}") + + # Display metadata + metadata = json.loads(metadata_file.read_text(encoding='utf-8')) + print(json.dumps(metadata, indent=2)) + + +if __name__ == "__main__": + main() + + diff --git a/.claude/skills/cicd-diagnostics/init-diagnostic.py b/.claude/skills/cicd-diagnostics/init-diagnostic.py new file mode 100755 index 000000000000..7ca67e03483a --- /dev/null +++ b/.claude/skills/cicd-diagnostics/init-diagnostic.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Initialize diagnostic environment. + +Usage: python init-diagnostic.py +Returns: Sets WORKSPACE environment variable and loads all utilities +""" + +import sys +import os +from pathlib import Path + +# Add utils to path +script_dir = Path(__file__).parent +sys.path.insert(0, str(script_dir / "utils")) + +from workspace import get_diagnostic_workspace + + +def main(): + if len(sys.argv) < 2: + print("ERROR: Run ID required", file=sys.stderr) + print("Usage: python init-diagnostic.py ", file=sys.stderr) + sys.exit(1) + + run_id = sys.argv[1] + + # Create workspace + workspace = get_diagnostic_workspace(run_id) + + print("✅ Diagnostic environment initialized") + print(f" RUN_ID: {run_id}") + print(f" WORKSPACE: {workspace}") + + # Export for shell usage + print(f"\nexport RUN_ID={run_id}") + print(f"export WORKSPACE={workspace}") + + +if __name__ == "__main__": + main() + + diff --git a/.claude/skills/cicd-diagnostics/requirements.txt b/.claude/skills/cicd-diagnostics/requirements.txt new file mode 100644 index 000000000000..7e58241804ba --- /dev/null +++ b/.claude/skills/cicd-diagnostics/requirements.txt @@ -0,0 +1,17 @@ +# Python dependencies for cicd-diagnostics skill +# No external dependencies required - uses standard library and GitHub CLI + +# Note: This skill uses the GitHub CLI (gh) which must be installed separately +# The skill uses Python 3.8+ standard library modules: +# - subprocess (for GitHub CLI calls) +# - json (for JSON parsing) +# - re (for regex) +# - pathlib (for file operations) +# - os, sys (standard system modules) + +# If you need to install GitHub CLI: +# macOS: brew install gh +# Linux: See https://github.com/cli/cli/blob/trunk/docs/install_linux.md +# Windows: See https://github.com/cli/cli/blob/trunk/docs/install_windows.md + + diff --git a/.claude/skills/cicd-diagnostics/utils/README.md b/.claude/skills/cicd-diagnostics/utils/README.md new file mode 100644 index 000000000000..182bd7ca6e81 --- /dev/null +++ b/.claude/skills/cicd-diagnostics/utils/README.md @@ -0,0 +1,293 @@ +# CI/CD Diagnostics Utility Functions + +Reusable Python utility modules for CI/CD failure analysis. + +## Overview + +This directory contains modular Python utility modules extracted from the cicd-diagnostics skill. These modules can be imported and used by the skill or other automation scripts. + +## Files + +### github_api.py +GitHub API and CLI wrapper functions for fetching workflow, job, and issue data. + +**Key Functions:** +- `extract_run_id(url)` - Extract run ID from GitHub Actions URL +- `extract_pr_number(input)` - Extract PR number from URL or branch name +- `get_run_metadata(run_id, output_file)` - Fetch workflow run details +- `get_jobs_detailed(run_id, output_file)` - Get all jobs with step information +- `get_failed_jobs(jobs_file)` - Filter failed jobs from jobs file +- `download_job_logs(job_id, output_file)` - Download job logs +- `get_pr_info(pr_num, output_file)` - Get PR details and status checks +- `find_failed_run_from_pr(pr_info_file)` - Find failed run from PR data +- `get_recent_runs(workflow_name, limit, output_file)` - Fetch workflow history +- `search_issues(query, output_file)` - Search GitHub issues +- `compare_commits(base_sha, head_sha, output_file)` - Compare commit ranges + +**Usage Example:** +```python +import sys +from pathlib import Path +sys.path.insert(0, str(Path(".claude/skills/cicd-diagnostics/utils"))) + +from github_api import extract_run_id, get_run_metadata + +run_id = extract_run_id("https://github.com/dotCMS/core/actions/runs/19118302390") +get_run_metadata(run_id, Path("run-metadata.json")) +``` + +### workspace.py +Diagnostic workspace management with caching and artifact organization. + +**Key Functions:** +- `create_diagnostic_workspace(run_id)` - Create workspace directory +- `find_existing_diagnostic(run_id)` - Check for cached diagnostics +- `get_diagnostic_workspace(run_id, force_clean=False)` - Get or create workspace (with caching) +- `save_artifact(diagnostic_dir, filename, content)` - Save artifact to workspace +- `artifact_exists(diagnostic_dir, filename)` - Check if artifact is cached +- `get_or_fetch_artifact(diagnostic_dir, filename, fetch_command)` - Cache-aware fetching +- `ensure_gitignore_diagnostics()` - Add diagnostic dirs to .gitignore +- `list_diagnostic_workspaces()` - List all diagnostic sessions +- `clean_old_diagnostics(max_age_hours=168, max_count=50)` - Cleanup old workspaces +- `get_workspace_summary(diagnostic_dir)` - Display workspace details + +**Usage Example:** +```python +import sys +from pathlib import Path +sys.path.insert(0, str(Path(".claude/skills/cicd-diagnostics/utils"))) + +from workspace import get_diagnostic_workspace, save_artifact + +diagnostic_dir = get_diagnostic_workspace("19118302390") +save_artifact(diagnostic_dir, "notes.txt", "Analysis in progress...") +``` + +### evidence.py +Evidence presentation for AI analysis - simple data extraction without classification logic. + +**Key Functions:** +- `present_failure_evidence(log_file)` - Present all failure evidence (supports JUnit, E2E, **Postman**) +- `get_first_error_context(log_file, before=30, after=20)` - Get context around first error +- `get_failure_timeline(log_file)` - Get timeline of all failures +- `present_known_issues(test_name, error_keywords="")` - Search and present known issues +- `present_recent_runs(workflow, limit=10)` - Get recent workflow run history +- `extract_test_name(log_file)` - Extract test name from log file (JUnit/E2E/Postman) +- `extract_error_keywords(log_file)` - Extract error keywords for pattern matching +- `present_complete_diagnostic(log_file)` - Present complete diagnostic package +- `extract_error_sections_only(log_file, output_file)` - Extract only error sections for large files +- `get_log_stats(log_file)` - Get log file statistics + +**Postman Test Support (NEW in v2.1)**: +- Detects `[INFO] \d+\. AssertionError` patterns +- Extracts "expected [...] to deeply equal [...]" assertions +- Identifies failing collections and test names +- Provides context around Postman failures + +**Usage Example:** +```python +import sys +from pathlib import Path +sys.path.insert(0, str(Path(".claude/skills/cicd-diagnostics/utils"))) + +from evidence import present_complete_diagnostic, get_log_stats + +log_file = Path("job-logs.txt") +print(get_log_stats(log_file)) +evidence = present_complete_diagnostic(log_file) +print(evidence) +``` + +### tiered_extraction.py +Tiered evidence extraction - creates multiple levels of detail for progressive analysis. + +**Key Functions:** +- `extract_level1_summary(log_file, output_file)` - Level 1: Test Summary (~500 tokens) +- `extract_level2_unique_failures(log_file, output_file)` - Level 2: Unique Failures (~5000 tokens) +- `extract_level3_full_context(log_file, output_file)` - Level 3: Full Context (~15000 tokens) +- `extract_failed_test_names(log_file)` - Extract failed test names (JUnit/E2E/Postman) +- `auto_extract_tiered(log_file, workspace)` - Auto-tiered extraction based on log size +- `analyze_retry_patterns(log_file)` - Analyze retry patterns (deterministic vs flaky) +- `extract_postman_failures(log_file, output_file)` - **NEW**: Postman-specific extraction + +**Postman Extraction (NEW in v2.1)**: +- Parses Newman/Postman test output format +- Extracts test summary table (executed/failed counts) +- Identifies failed collections +- Provides detailed failure context with line numbers +- Lists all failed test names from "inside" patterns + +**Usage Example:** +```python +import sys +from pathlib import Path +sys.path.insert(0, str(Path(".claude/skills/cicd-diagnostics/utils"))) + +from tiered_extraction import auto_extract_tiered, analyze_retry_patterns + +log_file = Path("job-logs.txt") +workspace = Path(".claude/diagnostics/run-12345") + +auto_extract_tiered(log_file, workspace) +print(analyze_retry_patterns(log_file)) +``` + +## Integration with cicd-diagnostics Skill + +The main SKILL.md references these utilities throughout the diagnostic workflow: + +```python +import sys +from pathlib import Path +sys.path.insert(0, str(Path(".claude/skills/cicd-diagnostics/utils"))) + +from workspace import get_diagnostic_workspace +from github_api import get_run_metadata +from evidence import present_complete_diagnostic + +# Initialize workspace +diagnostic_dir = get_diagnostic_workspace("19118302390") + +# Fetch metadata +get_run_metadata("19118302390", diagnostic_dir / "run-metadata.json") + +# Analyze logs +log_file = diagnostic_dir / "failed-job-12345.txt" +evidence = present_complete_diagnostic(log_file) +``` + +## Benefits of Modular Design + +1. **Reusability** - Modules can be used by other skills or scripts +2. **Testability** - Each utility can be tested independently +3. **Maintainability** - Changes isolated to specific utility files +4. **Clarity** - Main skill logic is cleaner and more readable +5. **Composability** - Functions can be combined in different workflows +6. **Cross-platform** - Python works on macOS, Linux, and Windows + +## Platform Compatibility + +All utilities use Python standard library (Python 3.8+): +- `pathlib` for cross-platform file paths +- `subprocess` for GitHub CLI calls +- `json` for JSON parsing +- `re` for regex operations +- No external Python dependencies required + +## Error Handling + +All utilities use Python exception handling: +- Functions raise exceptions on errors +- Type hints for better IDE support +- Clear error messages for debugging + +## Dependencies + +- Python 3.8 or higher +- GitHub CLI (gh) - must be installed separately +- Standard library only - no external Python packages required + +## Script Organization & Best Practices + +### Directory Structure +``` +cicd-diagnostics/ +├── init-diagnostic.py # ✅ Entry Point: CLI script +├── fetch-metadata.py # ✅ Entry Point: CLI script +├── fetch-jobs.py # ✅ Entry Point: CLI script +├── fetch-logs.py # ✅ Entry Point: CLI script +│ +└── utils/ # ✅ Library: Reusable utilities + ├── __init__.py + ├── github_api.py # GitHub API wrappers + ├── evidence.py # Evidence extraction + ├── tiered_extraction.py # Multi-level analysis + └── workspace.py # Workspace management +``` + +### Design Principles + +**✅ Root Level = Entry Points (User-Facing)** +- Accept command-line arguments +- Show usage messages +- Orchestrate workflows +- Import from utils/ +- Exit with status codes + +**✅ utils/ = Library (Developer-Facing)** +- Pure functions +- No CLI argument parsing +- Raise exceptions (don't exit) +- Type hints and docstrings +- Fully testable + +### Example Comparison + +**⌠BAD: Mixing Concerns** +```python +# utils/github_api.py (WRONG - has CLI parsing) +def download_logs(): + if len(sys.argv) < 2: + print("Usage: ...") # ⌠CLI logic in library + sys.exit(1) # ⌠Exit from library + job_id = sys.argv[1] # ⌠Argument parsing in library + ... +``` + +**✅ GOOD: Separation of Concerns** +```python +# utils/github_api.py (CORRECT - pure function) +def download_job_logs(job_id: str, output_file: Path) -> None: + """Download logs for a specific job. + + Args: + job_id: GitHub Actions job ID + output_file: Path to save logs + + Raises: + subprocess.CalledProcessError: If gh CLI fails + """ + result = subprocess.run([...], check=True) + output_file.write_text(result.stdout) + +# fetch-logs.py (CORRECT - CLI orchestration) +def main(): + if len(sys.argv) < 3: + print("Usage: python fetch-logs.py ") + sys.exit(1) + + from utils.github_api import download_job_logs + download_job_logs(sys.argv[1], Path(sys.argv[2])) + +if __name__ == "__main__": + main() +``` + +### Why This Structure? + +| Aspect | Entry Points (Root) | Utilities (utils/) | +|--------|--------------------|--------------------| +| **Purpose** | User interface | Reusable logic | +| **Testability** | Hard (needs CLI mocking) | Easy (pure functions) | +| **Reusability** | Low (specific to one workflow) | High (used by multiple scripts) | +| **Complexity** | Simple orchestration | Complex business logic | +| **Error Handling** | Print & exit | Raise exceptions | +| **Documentation** | Usage messages | Docstrings + type hints | + +### Version History + +**v2.1.0** (Current) +- ✅ Enhanced Postman/Newman test detection +- ✅ Added `extract_postman_failures()` to tiered_extraction.py +- ✅ Fixed `fetch-logs.py` argument parsing (now supports optional job ID) +- ✅ Improved assertion detection for API tests in evidence.py + +**v2.0.0** +- ✅ Converted from Bash to Python +- ✅ Separated entry points from utilities +- ✅ Added tiered extraction for large logs +- ✅ Enhanced known issue searching + +**v1.0.0** (Legacy Bash) +- Basic log extraction +- Limited test framework support diff --git a/.claude/skills/cicd-diagnostics/utils/__init__.py b/.claude/skills/cicd-diagnostics/utils/__init__.py new file mode 100755 index 000000000000..50fbaee4dcb2 --- /dev/null +++ b/.claude/skills/cicd-diagnostics/utils/__init__.py @@ -0,0 +1,5 @@ +"""CI/CD Diagnostics Utilities - Python modules for GitHub Actions failure analysis.""" + +__version__ = "2.1.0" + + diff --git a/.claude/skills/cicd-diagnostics/utils/evidence.py b/.claude/skills/cicd-diagnostics/utils/evidence.py new file mode 100755 index 000000000000..f3ca5fd85406 --- /dev/null +++ b/.claude/skills/cicd-diagnostics/utils/evidence.py @@ -0,0 +1,652 @@ +#!/usr/bin/env python3 +"""Evidence Presentation for AI Analysis. + +Simple data extraction without classification logic. +""" + +import json +import re +import subprocess +from pathlib import Path +from typing import Optional + + +def present_failure_evidence(log_file: Path) -> str: + """Present all failure evidence for AI analysis. + + Args: + log_file: Path to log file + + Returns: + Formatted evidence string + """ + log_content = log_file.read_text(encoding='utf-8', errors='ignore') + lines = log_content.split('\n') + + output = [] + output.append("=" * 80) + output.append("FAILURE EVIDENCE FOR ANALYSIS") + output.append("=" * 80) + output.append("") + + # Test Failures + output.append("=== FAILED TESTS ===") + output.append("") + failed_tests = [ + line for line in lines + if "<<< FAILURE!" in line or "::error file=" in line + ][:10] + + # Add Postman failures + postman_failures = [] + for i, line in enumerate(lines): + if re.search(r'\[INFO\]\s+\d+\.\s+(AssertionError|AssertionFailure)', line): + # Get context around the failure + start = max(0, i - 2) + end = min(len(lines), i + 5) + postman_failures.extend(lines[start:end]) + postman_failures.append("") # Add separator + if len(postman_failures) >= 50: + break + + if failed_tests or postman_failures: + if failed_tests: + output.append("JUnit/E2E Failures:") + output.extend(failed_tests) + output.append("") + if postman_failures: + output.append("Postman/API Test Failures:") + output.extend(postman_failures[:50]) + else: + output.append("No test failures found") + + output.append("") + output.append("=== ERROR MESSAGES ===") + output.append("") + errors = [] + + # Enhanced error detection for NPM, Docker, and GitHub Actions errors + # Prioritize critical deployment/build errors + critical_keywords = [ + "npm ERR!", "::error::", "##[error]", + "FAILURE:", "Failed to", "Cannot", "Unable to", + "Error:", "ERROR:" + ] + + test_error_keywords = [ + "[ERROR]", "AssertionError", "Exception" + ] + + # First pass: capture critical deployment/infrastructure errors + # Scan entire log for critical errors (don't stop early) + critical_errors = [] + for i, line in enumerate(lines): + # Skip false positives: file listings from tar/zip archives + # These are lines that ONLY list filenames without actual error context + # Pattern: timestamp + path + filename.class (no error keywords) + is_file_listing = ( + ('.class' in line or '.jar' in line) and + ('maven/dotserver' in line or 'webapps/ROOT' in line) and + not any(err_word in line for err_word in ['ERROR:', 'FAILURE:', 'Failed', 'Exception:']) + ) + + if is_file_listing: + continue + + if any(keyword in line for keyword in critical_keywords): + start = max(0, i - 5) + end = min(len(lines), i + 10) # More context for deployment errors + critical_errors.append((i, lines[start:end])) + + # Prioritize later errors (usually final failures) and unique error types + if critical_errors: + # Take last 5 error groups (most recent/final errors) + for _, error_lines in critical_errors[-10:]: + errors.extend(error_lines) + errors.append("") # Separator + + # Second pass: if no critical errors found, look for test errors + if not errors: + for i, line in enumerate(lines): + # Same file listing filter as first pass + is_file_listing = ( + ('.class' in line or '.jar' in line) and + ('maven/dotserver' in line or 'webapps/ROOT' in line) and + not any(err_word in line for err_word in ['ERROR:', 'FAILURE:', 'Failed', 'Exception:']) + ) + + if is_file_listing: + continue + + if any(keyword in line for keyword in test_error_keywords): + start = max(0, i - 3) + end = min(len(lines), i + 6) + errors.extend(lines[start:end]) + if len(errors) >= 100: + break + + if errors: + output.extend(errors[:150]) # Allow more errors to be shown + else: + output.append("No explicit errors found") + + output.append("") + output.append("=== ASSERTION DETAILS ===") + output.append("") + assertions = [ + line for line in lines + if "expected:" in line and "but was:" in line or "AssertionFailedError" in line + ][:10] + + # Add Postman assertion details + postman_assertions = [] + for i, line in enumerate(lines): + if re.search(r'(expected.*to deeply equal|expected.*to be|expected.*but was)', line, re.IGNORECASE): + postman_assertions.append(line) + if len(postman_assertions) >= 10: + break + + if assertions or postman_assertions: + if assertions: + output.append("JUnit Assertions:") + output.extend(assertions) + output.append("") + if postman_assertions: + output.append("Postman Assertions:") + output.extend(postman_assertions) + else: + output.append("No assertion failures found") + + output.append("") + output.append("=== STACK TRACES ===") + output.append("") + stack_pattern = re.compile(r'at [a-zA-Z0-9.]+\([A-Za-z0-9]+\.java:\d+\)') + stacks = [line for line in lines if stack_pattern.search(line)][:30] + if stacks: + output.extend(stacks) + else: + output.append("No Java stack traces found") + + output.append("") + output.append("=== TIMING INDICATORS ===") + output.append("") + timing_keywords = ["timeout", "timed out", "Thread.sleep", "Awaitility", "race condition", "concurrent"] + timing = [ + line for line in lines + if any(keyword.lower() in line.lower() for keyword in timing_keywords) + ][:10] + if timing: + output.extend(timing) + else: + output.append("No obvious timing indicators") + + output.append("") + output.append("=== INFRASTRUCTURE INDICATORS ===") + output.append("") + infra_keywords = ["connection refused", "docker", "container", "failed", "elasticsearch", "exception", "database", "error"] + infra = [ + line for line in lines + if any(keyword.lower() in line.lower() for keyword in infra_keywords) + ][:10] + if infra: + output.extend(infra) + else: + output.append("No obvious infrastructure issues") + + output.append("") + output.append("=" * 80) + + return "\n".join(output) + + +def get_first_error_context(log_file: Path, before: int = 30, after: int = 20) -> str: + """Get context around first error (for cascade detection). + + Args: + log_file: Path to log file + before: Number of lines before error + after: Number of lines after error + + Returns: + Context string + """ + log_content = log_file.read_text(encoding='utf-8', errors='ignore') + lines = log_content.split('\n') + + first_error_line = None + for i, line in enumerate(lines, 1): + if any(keyword in line for keyword in ["[ERROR]", "FAILURE!", "::error"]): + first_error_line = i + break + + if first_error_line is None: + return "No errors found in log" + + start = max(0, first_error_line - before - 1) + end = min(len(lines), first_error_line + after) + + output = [f"=== FIRST ERROR AT LINE {first_error_line} ===", ""] + for i, line in enumerate(lines[start:end], start=start + 1): + output.append(f"{i:6d}: {line}") + + return "\n".join(output) + + +def get_failure_timeline(log_file: Path) -> str: + """Get timeline of all failures (for cascade analysis). + + Args: + log_file: Path to log file + + Returns: + Timeline string + """ + log_content = log_file.read_text(encoding='utf-8', errors='ignore') + lines = log_content.split('\n') + + output = ["=== FAILURE TIMELINE ===", ""] + + failures = [] + for i, line in enumerate(lines, 1): + if any(keyword in line for keyword in ["[ERROR]", "FAILURE!", "::error"]): + content = line[:100] if len(line) > 100 else line + failures.append((i, content)) + if len(failures) >= 20: + break + + for line_num, content in failures: + output.append(f"Line {line_num}: {content}") + + return "\n".join(output) + + +def present_known_issues(test_name: str, error_keywords: str = "") -> str: + """Present known issues for comparison (ENHANCED). + + Args: + test_name: Name of the test + error_keywords: Optional error keywords for pattern matching + + Returns: + Formatted issues string + """ + output = [] + output.append("=== KNOWN ISSUES SEARCH ===") + output.append("") + output.append(f"Searching for: {test_name}") + if error_keywords: + output.append(f"Error keywords: {error_keywords}") + output.append("") + + # Strategy 1: Exact test name match + output.append("Strategy 1: Exact test name match") + try: + result = subprocess.run( + [ + "gh", "issue", "list", + "--search", f'"{test_name}" in:body', + "--state", "all", + "--label", "Flakey Test", + "--json", "number,title,state", + "--limit", "5" + ], + capture_output=True, + text=True, + check=True + ) + exact_match = json.loads(result.stdout) if result.stdout else [] + except (subprocess.CalledProcessError, json.JSONDecodeError): + exact_match = [] + + if exact_match: + output.append(" EXACT MATCHES:") + for issue in exact_match: + output.append(f" - Issue #{issue['number']}: {issue['title']} [{issue['state']}]") + else: + output.append(" No exact matches") + output.append("") + + # Strategy 2: Test class name match + output.append("Strategy 2: Test class name match") + test_class = test_name.split('.')[0] if '.' in test_name else test_name + try: + result = subprocess.run( + [ + "gh", "issue", "list", + "--search", f'"{test_class}" in:body', + "--state", "all", + "--label", "Flakey Test", + "--json", "number,title,state", + "--limit", "10" + ], + capture_output=True, + text=True, + check=True + ) + class_match = json.loads(result.stdout) if result.stdout else [] + except (subprocess.CalledProcessError, json.JSONDecodeError): + class_match = [] + + # Deduplicate with exact matches + exact_numbers = {issue['number'] for issue in exact_match} + new_class_matches = [issue for issue in class_match if issue['number'] not in exact_numbers] + + if new_class_matches: + output.append(" CLASS NAME MATCHES:") + for issue in new_class_matches: + output.append(f" - Issue #{issue['number']}: {issue['title']} [{issue['state']}]") + else: + output.append(" No additional class matches") + output.append("") + + # Strategy 3: Error pattern/keyword match + if error_keywords: + output.append(f"Strategy 3: Error pattern match ({error_keywords})") + try: + result = subprocess.run( + [ + "gh", "issue", "list", + "--search", f"{error_keywords} in:body", + "--state", "all", + "--label", "Flakey Test", + "--json", "number,title,state,body", + "--limit", "15" + ], + capture_output=True, + text=True, + check=True + ) + pattern_match = json.loads(result.stdout) if result.stdout else [] + except (subprocess.CalledProcessError, json.JSONDecodeError): + pattern_match = [] + + # Deduplicate + all_numbers = exact_numbers | {issue['number'] for issue in new_class_matches} + new_pattern_matches = [issue for issue in pattern_match if issue['number'] not in all_numbers] + + if new_pattern_matches: + output.append(" PATTERN MATCHES:") + for issue in new_pattern_matches: + output.append(f" - Issue #{issue['number']}: {issue['title']} [{issue['state']}]") + output.append("") + output.append(" Pattern match details (showing first 200 chars from body):") + for issue in new_pattern_matches: + body_preview = issue.get('body', '')[:200].replace('\n', ' ') + output.append(f" #{issue['number']}: {body_preview}...") + else: + output.append(" No additional pattern matches") + output.append("") + + # Strategy 4: CLI test issues + if "cli" in test_name.lower() or "command" in test_name.lower(): + output.append("Strategy 4: CLI-related flaky tests") + try: + result = subprocess.run( + [ + "gh", "issue", "list", + "--search", "cli in:body", + "--state", "all", + "--label", "Flakey Test", + "--json", "number,title,state", + "--limit", "10" + ], + capture_output=True, + text=True, + check=True + ) + cli_match = json.loads(result.stdout) if result.stdout else [] + except (subprocess.CalledProcessError, json.JSONDecodeError): + cli_match = [] + + if cli_match: + output.append(" CLI-RELATED:") + for issue in cli_match: + output.append(f" - Issue #{issue['number']}: {issue['title']} [{issue['state']}]") + else: + output.append(" No CLI-related matches") + output.append("") + + # Summary + total_exact = len(exact_match) + total_class = len(new_class_matches) + total_pattern = len(new_pattern_matches) if error_keywords else 0 + total = total_exact + total_class + total_pattern + + output.append("=== SEARCH SUMMARY ===") + output.append(f"Total potential matches: {total}") + output.append(f" - Exact matches: {total_exact}") + output.append(f" - Class matches: {total_class}") + if error_keywords: + output.append(f" - Pattern matches: {total_pattern}") + output.append("") + + return "\n".join(output) + + +def present_recent_runs(workflow: str, limit: int = 10) -> str: + """Get recent workflow run history. + + Args: + workflow: Workflow name + limit: Maximum number of runs to fetch + + Returns: + Formatted runs string + """ + try: + result = subprocess.run( + [ + "gh", "run", "list", + "--workflow", workflow, + "--limit", str(limit), + "--json", "databaseId,conclusion,displayTitle,createdAt" + ], + capture_output=True, + text=True, + check=True + ) + runs = json.loads(result.stdout) if result.stdout else [] + except (subprocess.CalledProcessError, json.JSONDecodeError): + runs = [] + + output = [] + output.append(f"=== RECENT RUNS: {workflow} ===") + output.append("") + + if not runs: + output.append("No recent runs found") + else: + for run in runs: + output.append( + f"{run['databaseId']} | {run['conclusion']} | {run['displayTitle']} | {run['createdAt']}" + ) + + output.append("") + + # Calculate failure rate + if runs: + total = len(runs) + failures = sum(1 for run in runs if run.get('conclusion') == 'failure') + if total > 0: + rate = (failures * 100) // total + output.append(f"Failure rate: {failures}/{total} ({rate}%)") + + return "\n".join(output) + + +def extract_test_name(log_file: Path) -> str: + """Extract test name from log file. + + Args: + log_file: Path to log file + + Returns: + Test name or empty string + """ + log_content = log_file.read_text(encoding='utf-8', errors='ignore') + lines = log_content.split('\n') + + # Try JUnit test + for line in lines: + if "<<< FAILURE!" in line: + match = re.search(r'\[ERROR\] ([^\s]+)', line) + if match: + return match.group(1).split('.')[0] + + # Try E2E test + for line in lines: + if "::error file=" in line: + match = re.search(r'file=([^,]+)', line) + if match: + file_path = match.group(1) + return Path(file_path).stem.replace('.spec', '') + + # Try Postman + for line in lines: + if "Collection" in line and "had failures" in line: + match = re.search(r'Collection ([^\s]+) had failures', line) + if match: + return match.group(1) + + return "" + + +def extract_error_keywords(log_file: Path) -> str: + """Extract error keywords for pattern matching. + + Args: + log_file: Path to log file + + Returns: + Space-separated keywords + """ + log_content = log_file.read_text(encoding='utf-8', errors='ignore').lower() + + keywords = [] + + if "moddate" in log_content or "modification date" in log_content: + keywords.append("modDate") + if "createddate" in log_content or "created date" in log_content or "creationdate" in log_content: + keywords.append("createdDate") + if "race condition" in log_content or "concurrent" in log_content or "synchronization" in log_content: + keywords.append("timing") + if "timeout" in log_content or "timed out" in log_content: + keywords.append("timeout") + if "ordering" in log_content or "order by" in log_content or "sorted" in log_content: + keywords.append("ordering") + if re.search(r'boolean.*flip|expected:.*true.*but was:.*false|expected:.*false.*but was:.*true', log_content): + keywords.append("assertion") + + return " ".join(keywords) + + +def present_complete_diagnostic(log_file: Path) -> str: + """Present complete diagnostic package for AI. + + Args: + log_file: Path to log file + + Returns: + Complete diagnostic string + """ + output = [] + output.append("=" * 80) + output.append("COMPLETE DIAGNOSTIC EVIDENCE") + output.append("=" * 80) + output.append("") + + # 1. Failure evidence + output.append(present_failure_evidence(log_file)) + output.append("") + output.append("") + + # 2. First error context + output.append(get_first_error_context(log_file)) + output.append("") + output.append("") + + # 3. Timeline + output.append(get_failure_timeline(log_file)) + output.append("") + output.append("") + + # 4. Known issues + test_name = extract_test_name(log_file) + if test_name: + error_keywords = extract_error_keywords(log_file) + output.append(present_known_issues(test_name, error_keywords)) + + output.append("") + output.append("=" * 80) + output.append("END DIAGNOSTIC EVIDENCE - READY FOR AI ANALYSIS") + output.append("=" * 80) + + return "\n".join(output) + + +def extract_error_sections_only(log_file: Path, output_file: Path) -> None: + """Extract only error sections for large files (performance optimization). + + Args: + log_file: Path to input log file + output_file: Path to output file + """ + log_content = log_file.read_text(encoding='utf-8', errors='ignore') + lines = log_content.split('\n') + + output = [] + output.append("=== ERRORS AND FAILURES ===") + + # Get context around errors + error_lines = [] + for i, line in enumerate(lines): + if any(keyword in line for keyword in ["[ERROR]", "FAILURE!", "::error"]): + start = max(0, i - 20) + end = min(len(lines), i + 21) + error_lines.extend(lines[start:end]) + if len(error_lines) >= 2000: + break + + output.extend(error_lines[:2000]) + output.append("") + output.append("=== FIRST 200 LINES ===") + output.extend(lines[:200]) + output.append("") + output.append("=== LAST 200 LINES ===") + output.extend(lines[-200:]) + + output_file.write_text("\n".join(output), encoding='utf-8') + + +def get_log_stats(log_file: Path) -> str: + """Get log file stats. + + Args: + log_file: Path to log file + + Returns: + Stats string + """ + size = log_file.stat().st_size + size_mb = size / 1048576 + lines = len(log_file.read_text(encoding='utf-8', errors='ignore').split('\n')) + + log_content = log_file.read_text(encoding='utf-8', errors='ignore') + error_count = log_content.count("[ERROR]") + failure_count = log_content.count("FAILURE!") + + output = [ + "=== LOG FILE STATISTICS ===", + f"File: {log_file}", + f"Size: {size} bytes ({size_mb:.2f} MB)", + f"Lines: {lines}", + f"Errors: {error_count}", + f"Failures: {failure_count}", + "" + ] + + if size_mb > 10: + output.append("âš ï¸ Large file detected. Consider using extract_error_sections_only() for faster analysis.") + + return "\n".join(output) + diff --git a/.claude/skills/cicd-diagnostics/utils/external_issues.py b/.claude/skills/cicd-diagnostics/utils/external_issues.py new file mode 100644 index 000000000000..a2a6a0664cf5 --- /dev/null +++ b/.claude/skills/cicd-diagnostics/utils/external_issues.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +"""External issue detection for CI/CD failures. + +Identifies when CI/CD failures are caused by external service changes +rather than code issues. +""" + +import re +from datetime import datetime +from typing import Dict, List, Optional, Tuple + + +def extract_error_indicators(log_content: str) -> Dict[str, List[str]]: + """Extract key indicators from logs that suggest external issues. + + Args: + log_content: Full log file content + + Returns: + Dictionary mapping indicator type to list of matches + """ + indicators = { + 'npm_errors': [], + 'docker_errors': [], + 'auth_errors': [], + 'network_errors': [], + 'service_names': set() + } + + lines = log_content.split('\n') + + for line in lines: + # NPM specific errors + if 'npm ERR!' in line: + indicators['npm_errors'].append(line.strip()) + indicators['service_names'].add('npm') + + # Extract error codes + if 'code E' in line: + match = re.search(r'code (E\w+)', line) + if match: + indicators['npm_errors'].append(f"Error code: {match.group(1)}") + + # Docker errors + if 'ERROR:' in line and any(keyword in line.lower() for keyword in ['docker', 'blob', 'image', 'registry']): + indicators['docker_errors'].append(line.strip()) + indicators['service_names'].add('docker') + + # Authentication errors (generic) + auth_keywords = [ + 'authentication', 'authorization', 'OTP', '2FA', 'token', + 'ENEEDAUTH', 'EOTP', 'unauthorized', 'forbidden', 'access denied' + ] + if any(keyword.lower() in line.lower() for keyword in auth_keywords): + if any(error in line for error in ['ERR!', 'ERROR:', '::error::', 'FAILURE:']): + indicators['auth_errors'].append(line.strip()) + + # Network/connectivity errors + network_keywords = [ + 'connection refused', 'timeout', 'cannot connect', + 'network error', 'ECONNREFUSED', 'ETIMEDOUT' + ] + if any(keyword.lower() in line.lower() for keyword in network_keywords): + indicators['network_errors'].append(line.strip()) + + # Convert set to list for JSON serialization + indicators['service_names'] = list(indicators['service_names']) + + return indicators + + +def generate_search_queries(indicators: Dict[str, List[str]], + failure_date: Optional[str] = None) -> List[str]: + """Generate web search queries based on error indicators. + + Args: + indicators: Error indicators from extract_error_indicators() + failure_date: Date of failure (YYYY-MM-DD format) + + Returns: + List of search query strings + """ + queries = [] + + # Extract month/year from failure date + date_context = "" + if failure_date: + try: + dt = datetime.strptime(failure_date, "%Y-%m-%d") + date_context = f"{dt.strftime('%B %Y')}" + except ValueError: + pass + + # NPM specific searches + if indicators['npm_errors']: + npm_codes = [line for line in indicators['npm_errors'] if 'Error code:' in line] + if npm_codes: + # Extract error code + for code_line in npm_codes: + code = code_line.split('Error code: ')[1] + queries.append(f'npm {code} authentication error {date_context}') + + # Check for token/2FA issues + if any('OTP' in err or '2FA' in err or 'token' in err.lower() + for err in indicators['npm_errors']): + queries.append(f'npm classic token revoked {date_context}') + queries.append(f'npm 2FA authentication CI/CD {date_context}') + + # Docker specific searches + if indicators['docker_errors']: + if any('blob' in err.lower() for err in indicators['docker_errors']): + queries.append(f'docker blob not found error {date_context}') + if any('registry' in err.lower() for err in indicators['docker_errors']): + queries.append(f'docker registry authentication {date_context}') + + # GitHub Actions searches + if any('actions' in err.lower() for err in + indicators['auth_errors'] + indicators['network_errors']): + queries.append(f'GitHub Actions runner issues {date_context}') + + # Generic service change searches + for service in indicators['service_names']: + queries.append(f'{service} breaking changes {date_context}') + queries.append(f'{service} security update {date_context}') + + return queries + + +def suggest_external_checks(indicators: Dict[str, List[str]], + failure_timeline: List[Tuple[str, str]]) -> Dict[str, any]: + """Suggest which external sources to check based on failure patterns. + + Args: + indicators: Error indicators from extract_error_indicators() + failure_timeline: List of (date, status) tuples showing failure history + + Returns: + Dictionary with suggested checks and reasoning + """ + suggestions = { + 'likelihood': 'low', # low, medium, high + 'checks': [], + 'reasoning': [] + } + + # Check if failures started on a specific date with no recovery + if len(failure_timeline) >= 3: + recent_failures = [status for _, status in failure_timeline[:5]] + if all(status == 'failure' for status in recent_failures): + suggestions['likelihood'] = 'medium' + suggestions['reasoning'].append( + "Multiple consecutive failures suggest external change or persistent issue" + ) + + # NPM authentication errors strongly suggest external changes + if indicators['npm_errors']: + if any('EOTP' in err or 'ENEEDAUTH' in err for err in indicators['npm_errors']): + suggestions['likelihood'] = 'high' + suggestions['checks'].append({ + 'source': 'npm registry changelog', + 'url': 'https://github.blog/changelog/', + 'search_for': 'npm security token authentication 2FA' + }) + suggestions['reasoning'].append( + "NPM authentication errors (EOTP/ENEEDAUTH) often caused by npm registry policy changes" + ) + + # Docker authentication/registry errors + if indicators['docker_errors'] and indicators['auth_errors']: + suggestions['likelihood'] = 'high' if suggestions['likelihood'] != 'high' else 'high' + suggestions['checks'].append({ + 'source': 'Docker Hub status', + 'url': 'https://status.docker.com/', + 'search_for': 'Docker Hub registry authentication' + }) + suggestions['reasoning'].append( + "Docker authentication errors may indicate Docker Hub policy changes or outages" + ) + + # Generic authentication without specific service + if indicators['auth_errors'] and not indicators['service_names']: + suggestions['checks'].append({ + 'source': 'GitHub Actions status', + 'url': 'https://www.githubstatus.com/', + 'search_for': 'GitHub Actions runner authentication' + }) + + return suggestions + + +def format_external_issue_report(indicators: Dict[str, List[str]], + search_queries: List[str], + suggestions: Dict[str, any]) -> str: + """Format external issue detection report for inclusion in diagnosis. + + Args: + indicators: Error indicators + search_queries: Generated search queries + suggestions: Suggested checks + + Returns: + Formatted markdown report section + """ + report = [] + + report.append("## External Issue Detection\n") + + # Likelihood assessment + likelihood_emoji = { + 'low': '⚪', + 'medium': '🟡', + 'high': '🔴' + } + emoji = likelihood_emoji.get(suggestions['likelihood'], '⚪') + report.append(f"**External Cause Likelihood:** {emoji} {suggestions['likelihood'].upper()}\n") + + # Reasoning + if suggestions['reasoning']: + report.append("**Indicators:**") + for reason in suggestions['reasoning']: + report.append(f"- {reason}") + report.append("") + + # Service-specific errors + if indicators['npm_errors']: + report.append("**NPM Errors Detected:**") + for err in indicators['npm_errors'][:5]: # Show first 5 + report.append(f"- `{err}`") + report.append("") + + if indicators['docker_errors']: + report.append("**Docker Errors Detected:**") + for err in indicators['docker_errors'][:3]: + report.append(f"- `{err}`") + report.append("") + + if indicators['auth_errors']: + report.append("**Authentication Errors Detected:**") + for err in indicators['auth_errors'][:3]: + report.append(f"- `{err}`") + report.append("") + + # Recommended searches + if search_queries: + report.append("**Recommended Web Searches:**") + for query in search_queries[:5]: # Top 5 queries + report.append(f"- `{query}`") + report.append("") + + # Specific checks + if suggestions['checks']: + report.append("**Suggested External Checks:**") + for check in suggestions['checks']: + report.append(f"- **{check['source']}**: {check['url']}") + report.append(f" Search for: `{check['search_for']}`") + report.append("") + + return '\n'.join(report) + + +if __name__ == "__main__": + # Example usage + import sys + from pathlib import Path + + if len(sys.argv) < 2: + print("Usage: python external_issues.py ") + sys.exit(1) + + log_file = Path(sys.argv[1]) + if not log_file.exists(): + print(f"Error: Log file not found: {log_file}") + sys.exit(1) + + log_content = log_file.read_text(encoding='utf-8', errors='ignore') + + indicators = extract_error_indicators(log_content) + queries = generate_search_queries(indicators, "2025-11-10") + suggestions = suggest_external_checks(indicators, [ + ("2025-11-10", "failure"), + ("2025-11-09", "failure"), + ("2025-11-08", "failure"), + ("2025-11-07", "failure"), + ("2025-11-06", "success") + ]) + + report = format_external_issue_report(indicators, queries, suggestions) + print(report) diff --git a/.claude/skills/cicd-diagnostics/utils/github_api.py b/.claude/skills/cicd-diagnostics/utils/github_api.py new file mode 100755 index 000000000000..c6697f25c408 --- /dev/null +++ b/.claude/skills/cicd-diagnostics/utils/github_api.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python3 +"""GitHub API Utility Functions for CI/CD Diagnostics. + +Provides reusable functions for interacting with GitHub API and CLI. +""" + +import re +import subprocess +import json +from typing import Optional, Dict, Any, List +from pathlib import Path + + +def extract_run_id(url: str) -> Optional[str]: + """Extract run ID from GitHub Actions URL. + + Args: + url: GitHub Actions run URL + + Returns: + Run ID or None if not found + """ + match = re.search(r'/runs/(\d+)', url) + return match.group(1) if match else None + + +def extract_pr_number(input_str: str) -> Optional[str]: + """Extract PR number from URL or branch name. + + Args: + input_str: PR URL or branch name + + Returns: + PR number or None if not found + """ + # Try pull URL pattern + match = re.search(r'/pull/(\d+)', input_str) + if match: + return match.group(1) + + # Try branch name pattern (issue-123-feature-name) + match = re.search(r'issue-(\d+)', input_str) + if match: + return match.group(1) + + return None + + +def get_run_metadata(run_id: str, output_file: Path) -> None: + """Get workflow run metadata. + + Args: + run_id: GitHub Actions run ID + output_file: Path to save JSON output + """ + result = subprocess.run( + [ + "gh", "run", "view", run_id, + "--json", "conclusion,status,event,headBranch,headSha,workflowName,url,createdAt,updatedAt,displayTitle" + ], + capture_output=True, + text=True, + check=True + ) + output_file.write_text(result.stdout, encoding='utf-8') + + +def get_jobs_detailed(run_id: str, output_file: Path) -> None: + """Get all jobs for a workflow run with detailed step information. + + Args: + run_id: GitHub Actions run ID + output_file: Path to save JSON output + """ + result = subprocess.run( + [ + "gh", "api", + f"/repos/dotCMS/core/actions/runs/{run_id}/jobs", + "--paginate" + ], + capture_output=True, + text=True, + check=True + ) + output_file.write_text(result.stdout, encoding='utf-8') + + +def get_failed_jobs(jobs_file: Path) -> List[Dict[str, Any]]: + """Get failed jobs from detailed jobs file. + + Args: + jobs_file: Path to jobs JSON file + + Returns: + List of failed job dictionaries + """ + jobs_data = json.loads(jobs_file.read_text(encoding='utf-8')) + return [job for job in jobs_data.get('jobs', []) if job.get('conclusion') == 'failure'] + + +def get_canceled_jobs(jobs_file: Path) -> List[Dict[str, Any]]: + """Get canceled jobs from detailed jobs file. + + Args: + jobs_file: Path to jobs JSON file + + Returns: + List of canceled job dictionaries + """ + jobs_data = json.loads(jobs_file.read_text(encoding='utf-8')) + return [job for job in jobs_data.get('jobs', []) if job.get('conclusion') == 'cancelled'] + + +def download_job_logs(job_id: str, output_file: Path) -> None: + """Download logs for a specific job. + + Args: + job_id: GitHub Actions job ID + output_file: Path to save logs + """ + result = subprocess.run( + [ + "gh", "api", + f"/repos/dotCMS/core/actions/jobs/{job_id}/logs" + ], + capture_output=True, + text=True, + check=True + ) + output_file.write_text(result.stdout, encoding='utf-8') + + +def get_pr_info(pr_num: str, output_file: Path) -> None: + """Get PR information including status check rollup. + + Args: + pr_num: PR number + output_file: Path to save JSON output + """ + result = subprocess.run( + [ + "gh", "pr", "view", pr_num, + "--json", "number,headRefOid,headRefName,title,author,statusCheckRollup" + ], + capture_output=True, + text=True, + check=True + ) + output_file.write_text(result.stdout, encoding='utf-8') + + +def find_failed_run_from_pr(pr_info_file: Path) -> Optional[str]: + """Find failed run from PR info. + + Args: + pr_info_file: Path to PR info JSON file + + Returns: + Run ID or None if not found + """ + pr_data = json.loads(pr_info_file.read_text(encoding='utf-8')) + + status_checks = pr_data.get('statusCheckRollup', []) + for check in status_checks: + if (check.get('conclusion') == 'FAILURE' and + check.get('workflowName') == '-1 PR Check'): + details_url = check.get('detailsUrl', '') + return extract_run_id(details_url) + + return None + + +def get_recent_runs(workflow_name: str, limit: int = 20, output_file: Optional[Path] = None) -> List[Dict[str, Any]]: + """Get recent workflow runs. + + Args: + workflow_name: Name of the workflow + limit: Maximum number of runs to fetch + output_file: Optional path to save JSON output + + Returns: + List of run dictionaries + """ + result = subprocess.run( + [ + "gh", "run", "list", + "--workflow", workflow_name, + "--limit", str(limit), + "--json", "databaseId,conclusion,headSha,displayTitle,createdAt" + ], + capture_output=True, + text=True, + check=True + ) + + runs = json.loads(result.stdout) + + if output_file: + output_file.write_text(result.stdout, encoding='utf-8') + + return runs + + +def get_artifacts(run_id: str, output_file: Path) -> None: + """Get artifacts for a workflow run. + + Args: + run_id: GitHub Actions run ID + output_file: Path to save JSON output + """ + result = subprocess.run( + [ + "gh", "api", + f"/repos/dotCMS/core/actions/runs/{run_id}/artifacts", + "--jq", ".artifacts[] | {name, id, size_in_bytes, expired}" + ], + capture_output=True, + text=True, + check=True + ) + output_file.write_text(result.stdout, encoding='utf-8') + + +def search_issues(query: str, output_file: Optional[Path] = None) -> List[Dict[str, Any]]: + """Search for related GitHub issues. + + Args: + query: Search query + output_file: Optional path to save JSON output + + Returns: + List of issue dictionaries + """ + result = subprocess.run( + [ + "gh", "issue", "list", + "--search", query, + "--json", "number,title,state,labels,createdAt", + "--limit", "10" + ], + capture_output=True, + text=True, + check=True + ) + + issues = json.loads(result.stdout) + + if output_file: + output_file.write_text(result.stdout, encoding='utf-8') + + return issues + + +def get_issue(issue_num: str, output_file: Path) -> None: + """Get issue details. + + Args: + issue_num: Issue number + output_file: Path to save JSON output + """ + result = subprocess.run( + [ + "gh", "issue", "view", issue_num, + "--json", "title,body,labels,author" + ], + capture_output=True, + text=True, + check=True + ) + output_file.write_text(result.stdout, encoding='utf-8') + + +def compare_commits(base_sha: str, head_sha: str, output_file: Path) -> None: + """Compare two commits. + + Args: + base_sha: Base commit SHA + head_sha: Head commit SHA + output_file: Path to save JSON output + """ + result = subprocess.run( + [ + "gh", "api", + f"/repos/dotCMS/core/compare/{base_sha}...{head_sha}", + "--jq", ".commits[] | {sha: .sha[:7], message: .commit.message, author: .commit.author.name}" + ], + capture_output=True, + text=True, + check=True + ) + output_file.write_text(result.stdout, encoding='utf-8') + + +def get_prs_for_branch(branch: str, output_file: Path) -> None: + """Get PR list for current branch. + + Args: + branch: Branch name + output_file: Path to save JSON output + """ + result = subprocess.run( + [ + "gh", "pr", "list", + "--head", branch, + "--json", "number,url,headRefOid,title,author" + ], + capture_output=True, + text=True, + check=True + ) + output_file.write_text(result.stdout, encoding='utf-8') + + +def get_runs_for_commit(workflow_name: str, commit_sha: str, limit: int = 5) -> List[Dict[str, Any]]: + """Get workflow runs for specific commit. + + Args: + workflow_name: Name of the workflow + commit_sha: Commit SHA + limit: Maximum number of runs to fetch + + Returns: + List of run dictionaries + """ + result = subprocess.run( + [ + "gh", "run", "list", + "--workflow", workflow_name, + "--commit", commit_sha, + "--limit", str(limit), + "--json", "databaseId,conclusion,status,displayTitle" + ], + capture_output=True, + text=True, + check=True + ) + + return json.loads(result.stdout) + + +def is_macos() -> bool: + """Check if running on macOS.""" + import platform + return platform.system() == "Darwin" + + diff --git a/.claude/skills/cicd-diagnostics/utils/tiered_extraction.py b/.claude/skills/cicd-diagnostics/utils/tiered_extraction.py new file mode 100755 index 000000000000..764ed567b6aa --- /dev/null +++ b/.claude/skills/cicd-diagnostics/utils/tiered_extraction.py @@ -0,0 +1,597 @@ +#!/usr/bin/env python3 +"""Tiered Evidence Extraction. + +Creates multiple levels of detail for progressive analysis. +""" + +import re +from pathlib import Path +from typing import List + + +def extract_level1_summary(log_file: Path, output_file: Path) -> None: + """Level 1: Test Summary Only (ALWAYS fits in context - ~500 tokens max). + + Purpose: Quick overview of what failed + + Args: + log_file: Path to log file + output_file: Path to output file + """ + log_content = log_file.read_text(encoding='utf-8', errors='ignore') + lines = log_content.split('\n') + + output = [] + output.append("=" * 80) + output.append("LEVEL 1: TEST SUMMARY (Quick Overview)") + output.append("=" * 80) + output.append("") + + # Overall test results + output.append("=== OVERALL TEST RESULTS ===") + test_results = [ + line for line in lines + if "Tests run:" in line and ("Failures:" in line or "Errors:" in line) or "BUILD SUCCESS" in line or "BUILD FAILURE" in line + ][-5:] + output.extend(test_results) + output.append("") + + # List of failed tests + output.append("=== FAILED TESTS (Names Only) ===") + failed_tests = [] + for line in lines: + if "[ERROR]" in line and "Test." in line: + match = re.search(r'\[ERROR\] ([^\s]+)', line) + if match: + failed_tests.append(match.group(1)) + output.extend(list(set(failed_tests))[:20]) + output.append("") + + # Retry patterns + output.append("=== RETRY PATTERNS ===") + has_retries = any("Run " in line and ":" in line for line in lines) + if has_retries: + output.append("Tests were retried (Surefire rerunFailingTestsCount active)") + retry_lines = [ + line for line in lines + if "[ERROR]" in line or ("Run " in line and ":" in line) + ][:15] + output.extend(retry_lines) + output.append("") + flake_lines = [ + line for line in lines + if "[WARNING]" in line or ("Run " in line and ":" in line) + ][:15] + output.extend(flake_lines) + else: + output.append("No retry patterns detected") + output.append("") + + # Quick classification hints + output.append("=== CLASSIFICATION HINTS ===") + log_lower = log_content.lower() + has_timeout = "timeout" in log_lower or "conditiontimeout" in log_lower + has_assertion = "assertionerror" in log_lower or "expected:" in log_lower and "but was:" in log_lower + has_npe = "nullpointerexception" in log_lower + has_infra = any(kw in log_lower for kw in ["connection refused", "docker", "failed", "container", "error"]) + + output.append(f"Timeout errors: {sum(1 for _ in [True] if has_timeout)}") + output.append(f"Assertion errors: {sum(1 for _ in [True] if has_assertion)}") + output.append(f"NullPointerException: {sum(1 for _ in [True] if has_npe)}") + output.append(f"Infrastructure errors: {sum(1 for _ in [True] if has_infra)}") + output.append("") + + output.append("=" * 80) + output.append("Use extract_level2_unique_failures() for detailed error messages") + output.append("=" * 80) + + output_file.write_text("\n".join(output), encoding='utf-8') + + +def extract_level2_unique_failures(log_file: Path, output_file: Path) -> None: + """Level 2: Unique Failures (Moderate detail - ~5000 tokens max). + + Purpose: First occurrence of each unique failure with error messages + + Args: + log_file: Path to log file + output_file: Path to output file + """ + log_content = log_file.read_text(encoding='utf-8', errors='ignore') + lines = log_content.split('\n') + + output = [] + output.append("=" * 80) + output.append("LEVEL 2: UNIQUE FAILURES (Detailed Error Messages)") + output.append("=" * 80) + output.append("") + + # Parse retry summary + output.append("=== DETERMINISTIC FAILURES (Failed All Retries) ===") + if "Errors:" in log_content: + # Extract error section + error_start = None + for i, line in enumerate(lines): + if "[ERROR] Errors:" in line: + error_start = i + break + + if error_start is not None: + error_section = lines[error_start:error_start + 50] + output.extend(error_section[:100]) + else: + output.append("No deterministic failures (all retries failed)") + output.append("") + + output.append("=== FLAKY FAILURES (Passed Some Retries) ===") + if "Flakes:" in log_content: + flake_start = None + for i, line in enumerate(lines): + if "[WARNING] Flakes:" in line: + flake_start = i + break + + if flake_start is not None: + flake_section = lines[flake_start:flake_start + 50] + output.extend(flake_section[:100]) + else: + output.append("No flaky tests detected") + output.append("") + + # Get first occurrence of each unique error message + output.append("=== UNIQUE ERROR MESSAGES (First Occurrence) ===") + + # ConditionTimeoutException + if "ConditionTimeoutException" in log_content: + output.append("--- Awaitility Timeout ---") + for i, line in enumerate(lines): + if "ConditionTimeoutException" in line: + start = max(0, i - 5) + end = min(len(lines), i + 16) + output.extend(lines[start:end]) + if len(output) >= 40: + break + output.append("") + + # AssertionError / ComparisonFailure + if "AssertionError" in log_content or "ComparisonFailure" in log_content: + output.append("--- Assertion Failures ---") + for i, line in enumerate(lines): + if "AssertionError" in line or "ComparisonFailure" in line: + start = max(0, i - 3) + end = min(len(lines), i + 11) + output.extend(lines[start:end]) + if len(output) >= 50: + break + output.append("") + + # NullPointerException + if "NullPointerException" in log_content: + output.append("--- NullPointerException ---") + for i, line in enumerate(lines): + if "NullPointerException" in line: + start = max(0, i - 5) + end = min(len(lines), i + 11) + output.extend(lines[start:end]) + if len(output) >= 30: + break + output.append("") + + # Other exceptions + output.append("--- Other Exceptions (First 3) ---") + exception_count = 0 + for i, line in enumerate(lines): + if "Exception:" in line and "ConditionTimeout" not in line and "AssertionError" not in line and "NullPointer" not in line: + start = max(0, i - 3) + end = min(len(lines), i + 9) + output.extend(lines[start:end]) + exception_count += 1 + if exception_count >= 3: + break + output.append("") + + output.append("=" * 80) + output.append("Use extract_level3_full_context() for complete stack traces and timing") + output.append("=" * 80) + + output_file.write_text("\n".join(output), encoding='utf-8') + + +def extract_level3_full_context(log_file: Path, output_file: Path) -> None: + """Level 3: Full Context (Comprehensive - ~15000 tokens max). + + Purpose: Complete stack traces, timing correlation, all retry attempts + + Args: + log_file: Path to log file + output_file: Path to output file + """ + log_content = log_file.read_text(encoding='utf-8', errors='ignore') + lines = log_content.split('\n') + + output = [] + output.append("=" * 80) + output.append("LEVEL 3: FULL CONTEXT (Complete Details)") + output.append("=" * 80) + output.append("") + + # Complete retry analysis + output.append("=== COMPLETE RETRY ANALYSIS ===") + results_start = None + for i, line in enumerate(lines): + if "[INFO] Results:" in line: + results_start = i + break + + if results_start is not None: + output.extend(lines[results_start:results_start + 300]) + output.append("") + + # All error sections with full stack traces + output.append("=== ALL ERROR SECTIONS WITH STACK TRACES ===") + error_contexts = [] + for i, line in enumerate(lines): + if "[ERROR]" in line and "Test." in line: + start = max(0, i - 10) + end = min(len(lines), i + 31) + error_contexts.extend(lines[start:end]) + if len(error_contexts) >= 500: + break + output.extend(error_contexts[:500]) + output.append("") + + # Timing correlation + output.append("=== TIMING CORRELATION ===") + timestamp_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}') + timing_lines = [ + line for line in lines + if timestamp_pattern.match(line) and ("ERROR" in line or "FAILURE" in line or "Exception" in line) + ][:100] + output.extend(timing_lines) + output.append("") + + # Infrastructure events + output.append("=== INFRASTRUCTURE EVENTS ===") + infra_keywords = ["docker", "container", "elasticsearch", "database", "connection"] + infra_lines = [ + line for line in lines + if any(kw.lower() in line.lower() for kw in infra_keywords) and + any(kw in line.lower() for kw in ["error", "failed", "refused", "timeout"]) + ][:50] + output.extend(infra_lines) + output.append("") + + output.append("=" * 80) + output.append("This is the most detailed extraction level available") + output.append("=" * 80) + + output_file.write_text("\n".join(output), encoding='utf-8') + + +def extract_failed_test_names(log_file: Path) -> List[str]: + """Extract failed test names. + + Args: + log_file: Path to log file + + Returns: + List of test names + """ + log_content = log_file.read_text(encoding='utf-8', errors='ignore') + lines = log_content.split('\n') + + test_names = set() + + # E2E test names + for line in lines: + if "::error file=" in line: + match = re.search(r'file=([^,]+)', line) + if match: + file_path = match.group(1) + test_name = Path(file_path).stem.replace('.spec', '') + test_names.add(test_name) + + # JUnit/Maven test names + for line in lines: + if "<<< FAILURE!" in line: + match = re.search(r'\[ERROR\] ([^\s]+)', line) + if match: + test_names.add(match.group(1)) + + # Postman collection failures + for line in lines: + if "Collection" in line and "had failures" in line: + match = re.search(r'Collection ([^\s]+) had failures', line) + if match: + test_names.add(match.group(1)) + + return sorted(test_names) + + +def extract_postman_failures(log_file: Path, output_file: Path) -> None: + """Extract Postman test failures with full details. + + Purpose: Parse Postman/Newman test output for API test failures + + Args: + log_file: Path to log file + output_file: Path to output file + """ + log_content = log_file.read_text(encoding='utf-8', errors='ignore') + lines = log_content.split('\n') + + output = [] + output.append("=" * 80) + output.append("POSTMAN/NEWMAN TEST FAILURES") + output.append("=" * 80) + output.append("") + + # Find test summary + output.append("=== TEST SUMMARY ===") + for i, line in enumerate(lines): + if re.search(r'│\s+(executed|iterations|requests|test-scripts)', line): + output.append(line) + # Get surrounding lines for context + if i + 1 < len(lines) and '│' in lines[i + 1]: + continue + output.append("") + + # Find collection that failed + output.append("=== FAILED COLLECTIONS ===") + for line in lines: + if "Collection" in line and "had failures" in line: + output.append(line) + output.append("") + + # Extract individual failure details + output.append("=== FAILURE DETAILS ===") + in_failure_section = False + failure_count = 0 + + for i, line in enumerate(lines): + # Start of failure section + if re.search(r'\[INFO\]\s+#\s+failure\s+detail', line): + in_failure_section = True + output.append(line) + continue + + # In failure section + if in_failure_section: + # Individual failure entry + if re.search(r'\[INFO\]\s+\d+\.\s+(AssertionError|AssertionFailure|Error)', line): + failure_count += 1 + output.append("") + output.append(f"--- Failure #{failure_count} ---") + + # Extract failure details (next 10 lines) + for j in range(i, min(i + 12, len(lines))): + output.append(lines[j]) + if lines[j].strip() == "" or (j > i and re.search(r'\[INFO\]\s+\d+\.', lines[j])): + break + + # End of failure section + if "Collection" in line and "had failures" in line: + in_failure_section = False + break + + if failure_count >= 10: # Limit to first 10 failures + output.append("") + output.append("(Additional failures truncated...)") + break + + if failure_count == 0: + output.append("No Postman failures detected") + output.append("") + + # Extract test names from failure section + output.append("=== FAILED TEST NAMES ===") + failed_tests = set() + for line in lines: + # Pattern: inside "Collection Name / Test Name / Sub Test" + match = re.search(r'inside "(([^"]+) / ([^"]+))"', line) + if match: + failed_tests.add(match.group(1)) + + if failed_tests: + for test in sorted(failed_tests): + output.append(f" • {test}") + else: + output.append(" None found") + output.append("") + + output.append("=" * 80) + output.append(f"Total Postman Failures Extracted: {failure_count}") + output.append("=" * 80) + + output_file.write_text("\n".join(output), encoding='utf-8') + + +def auto_extract_tiered(log_file: Path, workspace: Path) -> None: + """Auto-tiered extraction (chooses appropriate level based on log size). + + Args: + log_file: Path to log file + workspace: Workspace directory + """ + size = log_file.stat().st_size + size_mb = size / 1048576 + + print("=== Auto-Tiered Extraction ===") + print(f"Log size: {size_mb:.2f} MB") + print("") + + # Always create Level 1 + print("Creating Level 1 (Summary)...") + level1_file = workspace / "evidence-level1-summary.txt" + extract_level1_summary(log_file, level1_file) + l1_size = level1_file.stat().st_size + print(f"✓ Level 1 created: {l1_size} bytes (~{l1_size // 4} tokens)") + print("") + + # Create Level 2 + print("Creating Level 2 (Unique Failures)...") + level2_file = workspace / "evidence-level2-unique.txt" + extract_level2_unique_failures(log_file, level2_file) + l2_size = level2_file.stat().st_size + print(f"✓ Level 2 created: {l2_size} bytes (~{l2_size // 4} tokens)") + print("") + + # Create Level 3 only if needed + if size_mb > 5: + print("Creating Level 3 (Full Context) - large log detected...") + level3_file = workspace / "evidence-level3-full.txt" + extract_level3_full_context(log_file, level3_file) + l3_size = level3_file.stat().st_size + print(f"✓ Level 3 created: {l3_size} bytes (~{l3_size // 4} tokens)") + else: + print("Skipping Level 3 (log is small enough for Level 2 analysis)") + print("") + + print("=== Tiered Extraction Complete ===") + print("Analysis workflow:") + print("1. Read Level 1 for quick overview and classification hints") + print("2. Read Level 2 for detailed error messages and retry patterns") + print("3. Read Level 3 (if exists) for deep dive analysis") + print("") + + +def analyze_retry_patterns(log_file: Path) -> str: + """Analyze retry patterns (deterministic vs flaky). + + Args: + log_file: Path to log file + + Returns: + Analysis string + """ + log_content = log_file.read_text(encoding='utf-8', errors='ignore') + lines = log_content.split('\n') + + output = [] + output.append("=" * 80) + output.append("RETRY PATTERN ANALYSIS") + output.append("=" * 80) + output.append("") + + # Check if retries are enabled + has_retries = any("Run " in line and ":" in line for line in lines) + if not has_retries: + output.append("No retry patterns detected (Surefire rerunFailingTestsCount not enabled)") + return "\n".join(output) + + output.append("Surefire retry mechanism detected") + output.append("") + + # Parse errors (deterministic failures) + output.append("=== DETERMINISTIC FAILURES (All Retries Failed) ===") + + error_section_start = None + for i, line in enumerate(lines): + if "[ERROR] Errors:" in line: + error_section_start = i + break + + if error_section_start is not None: + # Extract error section until flakes section + error_section = [] + for i in range(error_section_start, min(len(lines), error_section_start + 100)): + line = lines[i] + if "[WARNING] Flakes:" in line: + break + error_section.append(line) + + # Find test names + test_names = set() + for line in error_section: + if "[ERROR]" in line and "com." in line and "Run " not in line: + match = re.search(r'\[ERROR\]\s+([^\s]+)', line) + if match: + test_names.add(match.group(1)) + + if test_names: + for test in sorted(test_names): + test_simple = test.split('.')[-1] + retry_count = sum(1 for line in error_section if f"Run " in line and test_simple in line) + if retry_count == 0: + output.append(f" • {test} - Failed on first attempt (no retries or all 4 attempts failed)") + else: + output.append(f" • {test} - Failed {retry_count}/{retry_count} retries (100% failure rate)") + else: + output.append(" None") + else: + output.append(" None") + output.append("") + + # Parse flakes (intermittent failures) + output.append("=== FLAKY TESTS (Passed Some Retries) ===") + + flake_section_start = None + for i, line in enumerate(lines): + if "[WARNING] Flakes:" in line: + flake_section_start = i + break + + if flake_section_start is not None: + flake_section = lines[flake_section_start:flake_section_start + 200] + + # Find test names + test_names = set() + for line in flake_section: + if "[WARNING]" in line and "com." in line: + match = re.search(r'\[WARNING\]\s+([^\s]+)', line) + if match: + test_names.add(match.group(1)) + + if test_names: + for test in sorted(test_names): + test_simple = test.split('.')[-1] + # Find section for this test + test_section = [] + in_test = False + for line in flake_section: + if f"[WARNING] {test}" in line: + in_test = True + if in_test: + test_section.append(line) + if line.strip() == "" or ("[INFO]" in line and "[WARNING]" not in line): + break + + pass_count = sum(1 for line in test_section if "PASS" in line) + error_count = sum(1 for line in test_section if "[ERROR]" in line and "Run " in line) + total_runs = pass_count + error_count + + if total_runs > 0: + failure_rate = (error_count * 100) // total_runs + output.append(f" • {test} - Failed {error_count}/{total_runs} retries ({failure_rate}% failure rate, {pass_count} passed)") + else: + output.append(f" • {test} - Unable to parse retry counts") + else: + output.append(" None") + else: + output.append(" None") + output.append("") + + # Summary statistics + error_count = sum(1 for line in error_section if "[ERROR]" in line and "com." in line and "Run " not in line) if error_section_start else 0 + flake_count = sum(1 for line in flake_section if "[WARNING]" in line and "com." in line) if flake_section_start else 0 + + output.append("=== SUMMARY ===") + output.append(f"Deterministic failures: {error_count} test(s)") + output.append(f"Flaky tests: {flake_count} test(s)") + output.append(f"Total problematic tests: {error_count + flake_count}") + output.append("") + + # Classification guidance + if error_count > 0: + output.append(f"âš ï¸ BLOCKING: {error_count} deterministic failure(s) detected") + output.append(" These tests fail consistently and indicate real bugs or incomplete fixes") + + if flake_count > 0: + output.append(f"âš ï¸ WARNING: {flake_count} flaky test(s) detected") + output.append(" These tests pass sometimes, indicating timing/concurrency issues") + output.append("") + + output.append("=" * 80) + + return "\n".join(output) + diff --git a/.claude/skills/cicd-diagnostics/utils/workspace.py b/.claude/skills/cicd-diagnostics/utils/workspace.py new file mode 100755 index 000000000000..aaf29cf5884a --- /dev/null +++ b/.claude/skills/cicd-diagnostics/utils/workspace.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +"""Diagnostic Workspace Management Utilities. + +Handles creation, caching, and organization of diagnostic artifacts. +""" + +import os +import subprocess +import stat +from pathlib import Path +from typing import Optional + + +def get_repo_root() -> Path: + """Get repository root (works from any subdirectory).""" + try: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=True + ) + return Path(result.stdout.strip()) + except (subprocess.CalledProcessError, FileNotFoundError): + return Path(".").resolve() + + +def create_diagnostic_workspace(run_id: str) -> Path: + """Create diagnostic workspace (no timestamp - reusable by run ID). + + Args: + run_id: GitHub Actions run ID + + Returns: + Path to diagnostic directory + """ + repo_root = get_repo_root() + diagnostic_dir = repo_root / ".claude" / "diagnostics" / f"run-{run_id}" + diagnostic_dir.mkdir(parents=True, exist_ok=True) + return diagnostic_dir + + +def find_existing_diagnostic(run_id: str) -> Optional[Path]: + """Find existing diagnostic workspace for a run ID. + + Args: + run_id: GitHub Actions run ID + + Returns: + Path to existing directory or None + """ + repo_root = get_repo_root() + diagnostic_dir = repo_root / ".claude" / "diagnostics" / f"run-{run_id}" + + if diagnostic_dir.exists() and diagnostic_dir.is_dir(): + return diagnostic_dir + return None + + +def get_diagnostic_workspace(run_id: str, force_clean: bool = False) -> Path: + """Get or create diagnostic workspace (with caching). + + Args: + run_id: GitHub Actions run ID + force_clean: If True, remove existing workspace and start fresh + + Returns: + Path to diagnostic directory (existing or new) + """ + repo_root = get_repo_root() + diagnostic_dir = repo_root / ".claude" / "diagnostics" / f"run-{run_id}" + + # Clean existing workspace if requested + if force_clean and diagnostic_dir.exists(): + print(f"ðŸ—‘ï¸ Cleaning existing workspace: {diagnostic_dir}", file=os.sys.stderr) + import shutil + shutil.rmtree(diagnostic_dir) + + if diagnostic_dir.exists(): + print(f"✓ Reusing existing diagnostic workspace: {diagnostic_dir}", file=os.sys.stderr) + print(" (Cached logs and metadata will be reused)", file=os.sys.stderr) + return diagnostic_dir + else: + diagnostic_dir.mkdir(parents=True, exist_ok=True) + print(f"✓ Created new diagnostic workspace: {diagnostic_dir}", file=os.sys.stderr) + return diagnostic_dir + + +def save_artifact(diagnostic_dir: Path, filename: str, content: str) -> None: + """Save artifact to diagnostic workspace. + + Args: + diagnostic_dir: Diagnostic workspace directory + filename: Name of the file to save + content: Content to write + """ + artifact_path = diagnostic_dir / filename + artifact_path.write_text(content, encoding='utf-8') + + +def artifact_exists(diagnostic_dir: Path, filename: str) -> bool: + """Check if artifact exists in workspace. + + Args: + diagnostic_dir: Diagnostic workspace directory + filename: Name of the file to check + + Returns: + True if exists and non-empty, False otherwise + """ + artifact_path = diagnostic_dir / filename + return artifact_path.exists() and artifact_path.stat().st_size > 0 + + +def get_or_fetch_artifact(diagnostic_dir: Path, filename: str, fetch_command: list) -> Path: + """Get cached artifact or fetch new. + + Args: + diagnostic_dir: Diagnostic workspace directory + filename: Name of the artifact file + fetch_command: Command to run if artifact doesn't exist (list of args) + + Returns: + Path to artifact file + """ + artifact_path = diagnostic_dir / filename + + if artifact_exists(diagnostic_dir, filename): + print(f"✓ Using cached artifact: {filename}", file=os.sys.stderr) + return artifact_path + else: + print(f"Fetching {filename}...", file=os.sys.stderr) + result = subprocess.run( + fetch_command, + capture_output=True, + text=True, + check=True + ) + artifact_path.write_text(result.stdout, encoding='utf-8') + print(f"✓ Saved to: {artifact_path}", file=os.sys.stderr) + return artifact_path + + +def ensure_gitignore_diagnostics() -> None: + """Ensure .gitignore includes diagnostic directories.""" + repo_root = get_repo_root() + gitignore_path = repo_root / ".gitignore" + + gitignore_content = "" + if gitignore_path.exists(): + gitignore_content = gitignore_path.read_text(encoding='utf-8') + + if ".claude/diagnostics/" not in gitignore_content: + gitignore_content += "\n# Claude Code diagnostic outputs\n.claude/diagnostics/\n" + gitignore_path.write_text(gitignore_content, encoding='utf-8') + print("✓ Added .claude/diagnostics/ to .gitignore", file=os.sys.stderr) + + +def list_diagnostic_workspaces() -> list[Path]: + """List all diagnostic workspaces. + + Returns: + List of workspace paths, sorted by name (most recent first) + """ + repo_root = get_repo_root() + diagnostics_dir = repo_root / ".claude" / "diagnostics" + + if not diagnostics_dir.exists(): + return [] + + workspaces = [ + p for p in diagnostics_dir.iterdir() + if p.is_dir() and p.name.startswith("run-") + ] + return sorted(workspaces, reverse=True) + + +def get_workspace_age(diagnostic_dir: Path) -> int: + """Get workspace age in hours. + + Args: + diagnostic_dir: Diagnostic workspace directory + + Returns: + Age in hours, or -1 if directory doesn't exist + """ + if not diagnostic_dir.exists(): + return -1 + + dir_timestamp = diagnostic_dir.stat().st_mtime + current_timestamp = os.path.getmtime(diagnostic_dir) + age_seconds = current_timestamp - dir_timestamp + age_hours = int(age_seconds / 3600) + + return age_hours + + +def clean_old_diagnostics(max_age_hours: int = 168, max_count: int = 50) -> int: + """Clean old diagnostic workspaces. + + Args: + max_age_hours: Maximum age in hours (default: 168 = 7 days) + max_count: Maximum number to keep (default: 50) + + Returns: + Number of workspaces removed + """ + print(f"Cleaning diagnostic workspaces older than {max_age_hours} hours...", file=os.sys.stderr) + + workspaces = list_diagnostic_workspaces() + removed = 0 + + for i, workspace in enumerate(workspaces, 1): + age = get_workspace_age(workspace) + + if age >= max_age_hours or i > max_count: + print(f" Removing: {workspace} (age: {age}h)", file=os.sys.stderr) + import shutil + shutil.rmtree(workspace) + removed += 1 + + print(f"✓ Cleaned {removed} old diagnostic workspace(s)", file=os.sys.stderr) + return removed + + +def get_workspace_summary(diagnostic_dir: Path) -> str: + """Get workspace summary. + + Args: + diagnostic_dir: Diagnostic workspace directory + + Returns: + Summary string + """ + if not diagnostic_dir.exists(): + return f"Workspace not found: {diagnostic_dir}" + + import shutil + age = get_workspace_age(diagnostic_dir) + size = shutil.disk_usage(diagnostic_dir).used + + lines = [ + "=== Diagnostic Workspace Summary ===", + f"Path: {diagnostic_dir}", + f"Age: {age} hours", + f"Size: {size} bytes", + "Files:" + ] + + for file_path in sorted(diagnostic_dir.iterdir()): + if file_path.is_file(): + size_str = f"{file_path.stat().st_size:,} bytes" + lines.append(f" {file_path.name:<40} {size_str:>10}") + + return "\n".join(lines) + + +def init_diagnostic_structure(diagnostic_dir: Path) -> None: + """Create standard diagnostic file structure. + + Args: + diagnostic_dir: Diagnostic workspace directory + """ + diagnostic_dir.mkdir(parents=True, exist_ok=True) + (diagnostic_dir / "error-summary.txt").touch() + (diagnostic_dir / "analysis-notes.txt").touch() + + print(f"✓ Initialized diagnostic structure in {diagnostic_dir}", file=os.sys.stderr) + + diff --git a/.claude/skills/sdk-analytics/SKILL.md b/.claude/skills/sdk-analytics/SKILL.md new file mode 100644 index 000000000000..d6f04c8afe05 --- /dev/null +++ b/.claude/skills/sdk-analytics/SKILL.md @@ -0,0 +1,959 @@ +--- +name: SDK Analytics Installer +description: Use this skill when the user asks to install, configure, or set up @dotcms/analytics, sdk-analytics, analytics SDK, add analytics tracking, or mentions installing analytics in Next.js or React projects +allowed-tools: Read, Write, Edit, Bash, Grep, Glob +version: 1.0.0 +--- + +# DotCMS SDK Analytics Installation Guide + +This skill provides step-by-step instructions for installing and configuring the `@dotcms/analytics` SDK in the Next.js example project at `/core/examples/nextjs`. + +## Overview + +The `@dotcms/analytics` SDK is dotCMS's official JavaScript library for tracking content-aware events and analytics. It provides: + +- Automatic page view tracking +- Conversion tracking (purchases, downloads, sign-ups, etc.) +- Custom event tracking +- Session management (30-minute timeout) +- Anonymous user identity tracking +- UTM campaign parameter tracking +- Event batching/queuing for performance + +## 🚨 Important: Understanding the Analytics Components + +**CRITICAL**: `useContentAnalytics()` **ALWAYS requires config as a parameter**. The hook does NOT use React Context. + +### Component Roles + +1. **``** - Auto Page View Tracker + + - Only purpose: Automatically track pageviews on route changes + - **NOT a React Context Provider** + - Does **NOT** provide config to child components + - Place in root layout for automatic pageview tracking + +2. **`useContentAnalytics(config)`** - Manual Tracking Hook + - Used for custom event tracking + - **ALWAYS requires config parameter** + - Import centralized config in each component that uses it + +### Correct Usage Pattern + +```javascript +// 1. Create centralized config file (once) +// /src/config/analytics.config.js +export const analyticsConfig = { + siteAuth: process.env.NEXT_PUBLIC_DOTCMS_ANALYTICS_SITE_KEY, + server: process.env.NEXT_PUBLIC_DOTCMS_ANALYTICS_HOST, + autoPageView: true, + debug: process.env.NEXT_PUBLIC_DOTCMS_ANALYTICS_DEBUG === "true", +}; + +// 2. Add DotContentAnalytics to layout for auto pageview tracking (optional) +// /src/app/layout.js +import { DotContentAnalytics } from "@dotcms/analytics/react"; +import { analyticsConfig } from "@/config/analytics.config"; + +; + +// 3. Import config in every component that uses the hook +// /src/components/MyComponent.js +import { useContentAnalytics } from "@dotcms/analytics/react"; +import { analyticsConfig } from "@/config/analytics.config"; + +const { track } = useContentAnalytics(analyticsConfig); // ✅ Config required! +``` + +**Why centralize config?** While you must import it in each component, centralizing prevents duplication and makes updates easier. + +## Quick Setup Summary + +Here's the complete setup flow: + +``` +1. Install package + └─> npm install @dotcms/analytics + +2. Create centralized config file + └─> /src/config/analytics.config.js + └─> export const analyticsConfig = { siteAuth, server, debug, ... } + +3. (Optional) Add DotContentAnalytics for auto pageview tracking + └─> /src/app/layout.js + └─> import { analyticsConfig } from "@/config/analytics.config" + └─> + +4. Import config in EVERY component that uses the hook + └─> /src/components/MyComponent.js + └─> import { analyticsConfig } from "@/config/analytics.config" + └─> const { track } = useContentAnalytics(analyticsConfig) // ✅ Config required! +``` + +**Key Benefits of Centralized Config**: + +- ✅ Single source of truth for configuration values +- ✅ Easy to update environment variables in one place +- ✅ Consistent config across all components +- ✅ Better than duplicating config in every file + +## Installation Steps + +### 1. Install the Package + +Navigate to the Next.js example directory and install the package: + +```bash +cd /core/examples/nextjs +npm install @dotcms/analytics +``` + +### 2. Verify Installation + +Check that the package was added to `package.json`: + +```bash +grep "@dotcms/analytics" package.json +``` + +Expected output: `"@dotcms/analytics": "latest"` or similar version. + +### 3. Create Centralized Analytics Configuration + +Create a dedicated configuration file to centralize your analytics settings. This makes it easier to maintain and reuse across your application. + +**File**: `/core/examples/nextjs/src/config/analytics.config.js` + +```javascript +/** + * Centralized analytics configuration for dotCMS Content Analytics + * + * This configuration is used by: + * - DotContentAnalytics provider in layout.js + * - useContentAnalytics() hook when used standalone (optional) + * + * Environment variables required: + * - NEXT_PUBLIC_DOTCMS_ANALYTICS_SITE_KEY + * - NEXT_PUBLIC_DOTCMS_ANALYTICS_HOST + * - NEXT_PUBLIC_DOTCMS_ANALYTICS_DEBUG (optional) + */ +export const analyticsConfig = { + siteAuth: process.env.NEXT_PUBLIC_DOTCMS_ANALYTICS_SITE_KEY, + server: process.env.NEXT_PUBLIC_DOTCMS_ANALYTICS_HOST, + autoPageView: true, // Automatically track page views on route changes + debug: process.env.NEXT_PUBLIC_DOTCMS_ANALYTICS_DEBUG === "true", + queue: { + eventBatchSize: 15, // Send when 15 events are queued + flushInterval: 5000, // Or send every 5 seconds (ms) + }, +}; +``` + +**Benefits of this approach**: + +- ✅ Single source of truth for analytics configuration +- ✅ Easy to import and reuse across components +- ✅ Centralized environment variable management +- ✅ Type-safe and IDE autocomplete friendly +- ✅ Easy to test and mock in unit tests + +### 4. Configure Analytics in Next.js Layout + +Update the root layout file to include the analytics provider using the centralized config. + +**File**: `/core/examples/nextjs/src/app/layout.js` + +```javascript +import { Inter } from "next/font/google"; +import "./globals.css"; + +const inter = Inter({ subsets: ["latin"] }); + +export default function RootLayout({ children }) { + return ( + + {children} + + ); +} +``` + +**Updated with Analytics** (using centralized config): + +```javascript +import { Inter } from "next/font/google"; +import { DotContentAnalytics } from "@dotcms/analytics/react"; +import { analyticsConfig } from "@/config/analytics.config"; +import "./globals.css"; + +const inter = Inter({ subsets: ["latin"] }); + +export default function RootLayout({ children }) { + return ( + + + + {children} + + + ); +} +``` + +### 4. Add Environment Variables + +Create or update `.env.local` file in the Next.js project root: + +**File**: `/core/examples/nextjs/.env.local` + +```bash +# dotCMS Analytics Configuration +NEXT_PUBLIC_DOTCMS_SITE_AUTH=your_site_auth_key_here +NEXT_PUBLIC_DOTCMS_SERVER=https://your-dotcms-server.com +``` + +**Important**: Replace `your_site_auth_key_here` with your actual dotCMS Analytics site auth key. This can be obtained from the Analytics app in your dotCMS instance. + +### 5. Add `.env.local` to `.gitignore` + +Ensure the environment file is not committed to version control: + +```bash +# Check if already ignored +grep ".env.local" /core/examples/nextjs/.gitignore + +# If not present, add it +echo ".env.local" >> /core/examples/nextjs/.gitignore +``` + +## Usage Examples + +### Basic Setup (Automatic Page Views) + +With the configuration above, page views are automatically tracked on every route change. No additional code needed! + +### Manual Page View with Custom Data + +Track page views with additional context: + +```javascript +"use client"; + +import { useEffect } from "react"; +import { useContentAnalytics } from "@dotcms/analytics/react"; +import { analyticsConfig } from "@/config/analytics.config"; + +function MyComponent() { + // ✅ ALWAYS pass config - import from centralized config file + const { pageView } = useContentAnalytics(analyticsConfig); + + useEffect(() => { + // Track page view with custom data + pageView({ + contentType: "blog", + category: "technology", + author: "john-doe", + wordCount: 1500, + }); + }, []); + + return
Content here
; +} +``` + +### Track Custom Events + +Track specific user interactions: + +```javascript +"use client"; + +import { useContentAnalytics } from "@dotcms/analytics/react"; +import { analyticsConfig } from "@/config/analytics.config"; + +function CallToActionButton() { + // ✅ ALWAYS pass config - import from centralized config file + const { track } = useContentAnalytics(analyticsConfig); + + const handleClick = () => { + // Track custom event + track("cta-click", { + button: "Buy Now", + location: "hero-section", + price: 299.99, + }); + }; + + return ; +} +``` + +### Form Submission Tracking + +```javascript +"use client"; + +import { useContentAnalytics } from "@dotcms/analytics/react"; +import { analyticsConfig } from "@/config/analytics.config"; + +function ContactForm() { + const { track } = useContentAnalytics(analyticsConfig); + + const handleSubmit = async (e) => { + e.preventDefault(); + + // Track form submission + track("form-submit", { + formName: "contact-form", + formType: "lead-gen", + source: "homepage", + }); + + // Submit form... + }; + + return
{/* Form fields */}
; +} +``` + +### Video/Media Interaction Tracking + +```javascript +"use client"; + +import { useContentAnalytics } from "@dotcms/analytics/react"; +import { analyticsConfig } from "@/config/analytics.config"; + +function VideoPlayer({ videoId }) { + const { track } = useContentAnalytics(analyticsConfig); + + const handlePlay = () => { + track("video-play", { + videoId, + duration: 120, + autoplay: false, + }); + }; + + const handleComplete = () => { + track("video-complete", { + videoId, + watchPercentage: 100, + }); + }; + + return ( + + ); +} +``` + +### E-commerce Product View Tracking + +```javascript +"use client"; + +import { useEffect } from "react"; +import { useContentAnalytics } from "@dotcms/analytics/react"; +import { analyticsConfig } from "@/config/analytics.config"; + +function ProductPage({ product }) { + const { track } = useContentAnalytics(analyticsConfig); + + useEffect(() => { + // Track product view + track("product-view", { + productId: product.sku, + productName: product.title, + category: product.category, + price: product.price, + inStock: product.inventory > 0, + }); + }, [product]); + + return
{/* Product details */}
; +} +``` + +### Conversion Tracking (E-commerce Purchase) + +```javascript +"use client"; + +import { useContentAnalytics } from "@dotcms/analytics/react"; +import { analyticsConfig } from "@/config/analytics.config"; + +function CheckoutButton({ product, quantity }) { + const { conversion } = useContentAnalytics(analyticsConfig); + + const handlePurchase = () => { + // Process checkout logic here... + // After successful payment confirmation: + + // Track conversion ONLY after successful purchase + conversion("purchase", { + value: product.price * quantity, + currency: "USD", + productId: product.sku, + productName: product.title, + quantity: quantity, + category: product.category, + }); + }; + + return ; +} +``` + +### Conversion Tracking (Lead Generation) + +```javascript +"use client"; + +import { useContentAnalytics } from "@dotcms/analytics/react"; +import { analyticsConfig } from "@/config/analytics.config"; + +function DownloadWhitepaper() { + const { conversion } = useContentAnalytics(analyticsConfig); + + const handleDownload = () => { + // Trigger download logic here... + // After download is successfully completed: + + // Track conversion ONLY after successful download + conversion("download", { + fileType: "pdf", + fileName: "whitepaper-2024.pdf", + category: "lead-magnet", + }); + }; + + return ( + + ); +} +``` + +## Configuration Options + +### Analytics Config Object + +| Option | Type | Required | Default | Description | +| -------------- | ----------------------------- | -------- | ---------------------- | ---------------------------------------------------------------- | +| `siteAuth` | `string` | Yes | - | Site authentication key from dotCMS Analytics | +| `server` | `string` | Yes | - | Your dotCMS server URL | +| `debug` | `boolean` | No | `false` | Enable verbose logging for debugging | +| `autoPageView` | `boolean` | No | `true` (React) | Automatically track page views on route changes | +| `queue` | `QueueConfig \| false` | No | Default queue settings | Event batching configuration | +| `impressions` | `ImpressionConfig \| boolean` | No | `false` | Content impression tracking (disabled by default) | +| `clicks` | `boolean` | No | `false` | Content click tracking with 300ms throttle (disabled by default) | + +### Queue Configuration + +Controls how events are batched and sent: + +| Option | Type | Default | Description | +| ---------------- | -------- | ------- | ---------------------------------------------- | +| `eventBatchSize` | `number` | `15` | Max events per batch - auto-sends when reached | +| `flushInterval` | `number` | `5000` | Time in ms between flushes | + +**Disable Queuing** (send immediately): + +```javascript +const analyticsConfig = { + siteAuth: "your_key", + server: "https://your-server.com", + queue: false, // Send events immediately +}; +``` + +### Impression Tracking Configuration + +Controls automatic tracking of content visibility: + +| Option | Type | Default | Description | +| --------------------- | -------- | ------- | ----------------------------------------- | +| `visibilityThreshold` | `number` | `0.5` | Min percentage visible (0.0 to 1.0) | +| `dwellMs` | `number` | `750` | Min time visible in milliseconds | +| `maxNodes` | `number` | `1000` | Max elements to track (performance limit) | + +**Enable with defaults:** + +```javascript +const analyticsConfig = { + siteAuth: "your_key", + server: "https://your-server.com", + impressions: true, // 50% visible, 750ms dwell, 1000 max nodes +}; +``` + +**Custom thresholds:** + +```javascript +const analyticsConfig = { + siteAuth: "your_key", + server: "https://your-server.com", + impressions: { + visibilityThreshold: 0.7, // Require 70% visible + dwellMs: 1000, // Must be visible for 1 second + maxNodes: 500, // Track max 500 elements + }, +}; +``` + +**How it works:** + +- ✅ Tracks contentlets marked with `dotcms-analytics-contentlet` class and `data-dot-analytics-*` attributes +- ✅ Uses Intersection Observer API for high performance and battery efficiency +- ✅ Only fires when element is ≥50% visible for ≥750ms (configurable) +- ✅ Only tracks during active tab (respects page visibility) +- ✅ One impression per contentlet per session (no duplicates) +- ✅ Automatically disabled in dotCMS editor mode + +### Click Tracking Configuration + +Controls automatic tracking of user interactions with content elements. + +**Enable click tracking:** + +```javascript +const analyticsConfig = { + siteAuth: "your_key", + server: "https://your-server.com", + clicks: true, // Enable with 300ms throttle (fixed) +}; +``` + +**How it works:** + +- ✅ Tracks clicks on `` and ` +``` + +**Complete Configuration Example:** + +```javascript +// /config/analytics.config.js +export const analyticsConfig = { + siteAuth: process.env.NEXT_PUBLIC_DOTCMS_ANALYTICS_SITE_KEY, + server: process.env.NEXT_PUBLIC_DOTCMS_ANALYTICS_HOST, + autoPageView: true, + debug: process.env.NEXT_PUBLIC_DOTCMS_ANALYTICS_DEBUG === "true", + queue: { + eventBatchSize: 15, + flushInterval: 5000, + }, + impressions: { + visibilityThreshold: 0.5, // 50% visible + dwellMs: 750, // 750ms dwell time + maxNodes: 1000, // Track up to 1000 elements + }, + clicks: true, // Enable click tracking (300ms throttle, fixed) +}; +``` + +## Data Captured Automatically + +The SDK automatically enriches events with: + +### Page View Events + +- **Page Data**: URL, title, referrer, path, protocol, search params, hash +- **Device Data**: Screen resolution, viewport size, language, user agent +- **UTM Parameters**: Campaign tracking (source, medium, campaign, term, content) +- **Context**: Site key, session ID, user ID, timestamp + +### Custom Events + +- **Context**: Site key, session ID, user ID +- **Device Data**: Screen resolution, language, viewport dimensions +- **Custom Properties**: Any data you pass to `track()` + +## Session Management + +- **Duration**: 30-minute timeout of inactivity +- **Reset Conditions**: + - At midnight UTC + - When UTM campaign changes +- **Storage**: Uses `dot_analytics_session_id` in localStorage + +## Identity Tracking + +- **Anonymous User ID**: Persisted across sessions +- **Storage Key**: `dot_analytics_user_id` +- **Behavior**: Generated automatically on first visit, reused on subsequent visits + +## Testing & Debugging + +### Enable Debug Mode + +Set `debug: true` in config to see verbose logging: + +```javascript +const analyticsConfig = { + siteAuth: "your_key", + server: "https://your-server.com", + debug: true, // Enable debug logging +}; +``` + +### Verify Events in Network Tab + +1. Open browser DevTools � Network tab +2. Filter by: `/api/v1/analytics/content/event` +3. Perform actions in your app +4. Check request payloads to see captured data + +### Check Storage + +Open browser DevTools � Application � Local Storage: + +- `dot_analytics_user_id` - Anonymous user identifier +- `dot_analytics_session_id` - Current session ID +- `dot_analytics_session_utm` - UTM campaign data +- `dot_analytics_session_start` - Session start timestamp + +## Troubleshooting + +### Events Not Appearing + +1. **Verify Configuration**: + + - Check `siteAuth` and `server` are correct + - Enable `debug: true` to see console logs + +2. **Check Network Requests**: + + - Look for requests to `/api/v1/analytics/content/event` + - Verify they're returning 200 status + +3. **Editor Mode Detection**: + + - Analytics are automatically disabled inside dotCMS editor + - Test in preview or published mode + +4. **Environment Variables**: + - Ensure `.env.local` is loaded (restart dev server if needed) + - Verify variable names start with `NEXT_PUBLIC_` + +### Queue Not Flushing + +- Check `eventBatchSize` - might not be reaching threshold +- Verify `flushInterval` is appropriate for your use case +- Events auto-flush on page navigation/close via `visibilitychange` + +### Session Not Persisting + +- Check localStorage is enabled in browser +- Verify no browser extensions are blocking storage +- Check console for storage-related errors + +### Config File Issues + +1. **Import Path Not Found**: + + ```javascript + // ⌠Error: Cannot find module '@/config/analytics.config' + ``` + + - Verify the file exists at `/src/config/analytics.config.js` + - Check your `jsconfig.json` or `tsconfig.json` has the `@` alias configured: + ```json + { + "compilerOptions": { + "paths": { + "@/*": ["./src/*"] + } + } + } + ``` + +2. **Undefined Config Values**: + + ```javascript + // Config shows undefined for siteAuth or server + ``` + + - Verify environment variables are set in `.env.local` + - Restart dev server after changing `.env.local` + - Check variable names start with `NEXT_PUBLIC_` + +3. **Config Not Updated**: + - Clear Next.js cache: `rm -rf .next` + - Restart dev server: `npm run dev` + +## Integration with Existing Next.js Example + +The Next.js example at `/core/examples/nextjs` already uses other dotCMS SDK packages: + +- `@dotcms/client` - Core API client +- `@dotcms/experiments` - A/B testing +- `@dotcms/react` - React components +- `@dotcms/types` - TypeScript types +- `@dotcms/uve` - Universal Visual Editor + +Adding analytics complements these by providing: + +- Usage tracking across all content types +- User behavior insights +- Campaign performance metrics +- Content engagement analytics + +## API Reference + +### Component: `DotContentAnalytics` + +```typescript +interface AnalyticsConfig { + siteAuth: string; + server: string; + debug?: boolean; + autoPageView?: boolean; + queue?: QueueConfig | false; +} + +interface QueueConfig { + eventBatchSize?: number; + flushInterval?: number; +} + +; +``` + +### Hook: `useContentAnalytics` + +```typescript +interface ContentAnalyticsHook { + pageView: (customData?: Record) => void; + track: (eventName: string, properties?: Record) => void; + conversion: (name: string, options?: Record) => void; +} + +// ✅ CORRECT: Always pass config - import from centralized config file +import { analyticsConfig } from "@/config/analytics.config"; +const { pageView, track, conversion } = useContentAnalytics(analyticsConfig); +``` + +**CRITICAL**: The hook **ALWAYS requires config as a parameter**. There is no provider pattern for the hook - `` is only for auto pageview tracking and does NOT provide context to child components. + +**Always import and pass the centralized config** from `/config/analytics.config.js` to ensure consistency. + +### Methods + +#### `pageView(customData?)` + +Track a page view with optional custom data. Automatically captures page, device, UTM, and context data. + +**Parameters**: + +- `customData` (optional): Object with custom properties to attach + +**Example**: + +```javascript +pageView({ + contentType: "product", + category: "electronics", +}); +``` + +#### `track(eventName, properties?)` + +Track a custom event with optional properties. + +**Parameters**: + +- `eventName` (required): String identifier for the event (cannot be "pageview" or "conversion") +- `properties` (optional): Object with event-specific data + +**Example**: + +```javascript +track("button-click", { + label: "Subscribe", + location: "sidebar", +}); +``` + +#### `conversion(name, options?)` + +Track a conversion event (purchase, download, sign-up, etc.) with optional metadata. + +**âš ï¸ IMPORTANT: Conversion events are business events that should only be tracked after a successful action or completed goal.** Tracking conversions on clicks or attempts (before success) diminishes their value as conversion metrics. Only track conversions when: + +- ✅ Purchase is completed and payment is confirmed +- ✅ Download is successfully completed +- ✅ Sign-up form is submitted and account is created +- ✅ Form submission is successful and data is saved +- ✅ Any business goal is actually achieved + +**Parameters**: + +- `name` (required): String identifier for the conversion (e.g., "purchase", "download", "signup") +- `options` (optional): Object with conversion metadata (all properties go into `custom` object) + +**Examples**: + +```javascript +// Basic conversion (after successful download) +conversion("download"); + +// Conversion with custom metadata (after successful purchase) +conversion("purchase", { + value: 99.99, + currency: "USD", + productId: "SKU-12345", +}); + +// Conversion with additional context (after successful signup) +conversion("signup", { + source: "homepage", + plan: "premium", +}); +``` + +## Best Practices + +1. **Centralize Configuration**: Create a dedicated config file (`/config/analytics.config.js`) for all analytics settings + + ```javascript + // ✅ GOOD: Centralized config file + // /config/analytics.config.js + export const analyticsConfig = { + siteAuth: process.env.NEXT_PUBLIC_DOTCMS_ANALYTICS_SITE_KEY, + server: process.env.NEXT_PUBLIC_DOTCMS_ANALYTICS_HOST, + debug: process.env.NEXT_PUBLIC_DOTCMS_ANALYTICS_DEBUG === "true", + autoPageView: true, + }; + + // ⌠BAD: Inline config in multiple files + // component1.js + const config = { siteAuth: "...", server: "..." }; + // component2.js + const config = { siteAuth: "...", server: "..." }; // Duplicate! + ``` + +2. **Always Import and Pass Config**: The hook requires config as a parameter + + ```javascript + // ✅ CORRECT: Import centralized config in every component + // MyComponent.js + import { analyticsConfig } from "@/config/analytics.config"; + const { track } = useContentAnalytics(analyticsConfig); + + // ⌠WRONG: Inline config duplication + // MyComponent.js + const { track } = useContentAnalytics({ + siteAuth: "...", // Duplicated! + server: "...", // Duplicated! + }); + ``` + +3. **Use DotContentAnalytics for Auto PageViews**: Add to layout for automatic tracking + + ```javascript + // layout.js - For automatic pageview tracking only + import { analyticsConfig } from "@/config/analytics.config"; + ; + ``` + +4. **Environment Variables**: Always use environment variables for sensitive config (siteAuth) + +5. **Event Naming**: Use consistent, descriptive event names (e.g., `cta-click`, not just `click`) + +6. **Custom Data**: Include relevant context in event properties + +7. **Queue Configuration**: Use default queue settings unless you have specific performance needs + +8. **Debug Mode**: Enable only in development, disable in production + +9. **Auto Page Views**: Keep enabled for SPAs (Next.js) to track route changes + +## Related Resources + +- Analytics SDK README: `/core/core-web/libs/sdk/analytics/README.md` +- Package Location: `/core/core-web/libs/sdk/analytics/` +- Next.js Example: `/core/examples/nextjs/` + +## Quick Command Reference + +```bash +# Install package +cd /core/examples/nextjs +npm install @dotcms/analytics + +# Start Next.js dev server +npm run dev + +# Build for production +npm run build + +# Start production server +npm run start + +# Verify installation +npm list @dotcms/analytics +``` diff --git a/.cursor/rules/typescript-context.md b/.cursor/rules/typescript-context.md index 5060ace22d9e..c849370bb049 100644 --- a/.cursor/rules/typescript-context.md +++ b/.cursor/rules/typescript-context.md @@ -1,66 +1,172 @@ --- description: Angular frontend development context - loads only for Angular files -globs: ["core-web/**/*.ts", "core-web/**/*.html", "core-web/**/*.scss"] +globs: ["core-web/**/*.{ts,html,scss,css}"] alwaysApply: false --- # Angular Frontend Context -## Immediate Patterns (Copy-Paste Ready) +This project adheres to modern Angular best practices, emphasizing maintainability, performance, accessibility, and scalability. + +## TypeScript Best Practices + +* **Strict Type Checking:** Always enable and adhere to strict type checking. This helps catch errors early and improves code quality. +* **Prefer Type Inference:** Allow TypeScript to infer types when they are obvious from the context. This reduces verbosity while maintaining type safety. + * **Bad:** + ```typescript + let name: string = 'Angular'; + ``` + * **Good:** + ```typescript + let name = 'Angular'; + ``` +* **Avoid `any`:** Do not use the `any` type unless absolutely necessary as it bypasses type checking. Prefer `unknown` when a type is uncertain and you need to handle it safely. +* **Don't allow use enums, use `as const` instead, example:** + ```typescript + const MyEnum = { + VALUE1: 'value1', + VALUE2: 'value2', + } as const; + ``` +* **Private properties:** Use `#` prefix to indicate that a property is private, example: `#myPrivateProperty`. + * **Bad:** + ```typescript + private myPrivateProperty = 'private'; + ``` + * **Good:** + ```typescript + #myPrivateProperty = 'private'; + ``` + +## Angular Best Practices + +* **Standalone Components:** Always use standalone components, directives, and pipes. Avoid using `NgModules` for new features or refactoring existing ones. +* **Implicit Standalone:** When creating standalone components, you do not need to explicitly set `standalone: true` inside the `@Component`, `@Directive` and `@Pipe` decorators, as it is implied by default. + * **Bad:** + ```typescript + @Component({ + standalone: true, + // ... + }) + export class MyComponent {} + ``` + * **Good:** + ```typescript + @Component({ + // `standalone: true` is implied + // ... + }) + export class MyComponent {} + ``` +* **Signals for State Management:** Utilize Angular Signals for reactive state management within components and services. +* **Lazy Loading:** Implement lazy loading for feature routes to improve initial load times of your application. +* **NgOptimizedImage:** Use `NgOptimizedImage` for all static images to automatically optimize image loading and performance. +* **Host bindings:** Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead. + +## Components + +* **Single Responsibility:** Keep components small, focused, and responsible for a single piece of functionality. +* **`input()` and `output()` Functions:** Prefer `input()` and `output()` functions over the `@Input()` and `@Output()` decorators for defining component inputs and outputs. + * **Old Decorator Syntax:** + ```typescript + @Input() userId!: string; + @Output() userSelected = new EventEmitter(); + ``` + * **New Function Syntax:** + ```typescript + import { input, output } from '@angular/core'; + + // ... + $userId = input(''); + $userSelected = output(); + ``` +* **`computed()` for Derived State:** Use the `computed()` function from `@angular/core` for derived state based on signals. +* **`ChangeDetectionStrategy.OnPush`:** Always set `changeDetection: ChangeDetectionStrategy.OnPush` in the `@Component` decorator for performance benefits by reducing unnecessary change detection cycles. +* **Reactive Forms:** Prefer Reactive forms over Template-driven forms for complex forms, validation, and dynamic controls due to their explicit, immutable, and synchronous nature. +* **No `ngClass` / `NgClass`:** Do not use the `ngClass` directive. Instead, use native `class` bindings for conditional styling. + * **Bad:** + ```html +
+ ``` + * **Good:** + ```html +
+
+
+ ``` +* **No `ngStyle` / `NgStyle`:** Do not use the `ngStyle` directive. Instead, use native `style` bindings for conditional inline styles. + * **Bad:** + ```html +
+ ``` + * **Good:** + ```html +
+
+ ``` +* **File Structure:** Follow the file structure below for components. + * component-name/ + * component-name.component.ts # Logic + * component-name.component.html # Template + * component-name.component.scss # Styles + * component-name.component.spec.ts # Tests +* **For signals**, use the `$` prefix to indicate that it is a signal, example: `$mySignal` +* **For observables**, use the `$` suffix to indicate that it is an observable, example: `myObservable$` + +## State Management + +* **Signals for Local State:** Use signals for managing local component state. +* **`computed()` for Derived State:** Leverage `computed()` for any state that can be derived from other signals. +* **Pure and Predictable Transformations:** Ensure state transformations are pure functions (no side effects) and predictable. +* **Signal value updates:** Do NOT use `mutate` on signals, use `update` or `set` instead. +* **Signal Store:** For complex state management, use the Signal Store pattern, learn more here https://ngrx.io/guide/signals + +## Templates + +* **Simple Templates:** Keep templates as simple as possible, avoiding complex logic directly in the template. Delegate complex logic to the component's TypeScript code. +* **Native Control Flow:** Use the new built-in control flow syntax (`@if`, `@for`, `@switch`) instead of the older structural directives (`*ngIf`, `*ngFor`, `*ngSwitch`). + * **Old Syntax:** + ```html +
Content
+
{{ item }}
+ ``` + * **New Syntax:** + ```html + @if (isVisible) { +
Content
+ } + @for (item of items; track item.id) { +
{{ item }}
+ } + ``` +* **Async Pipe:** Use the `async` pipe to handle observables in templates. This automatically subscribes and unsubscribes, preventing memory leaks. + +## Services + +* **Single Responsibility:** Design services around a single, well-defined responsibility. +* **`providedIn: 'root'`:** Use the `providedIn: 'root'` option when declaring injectable services to ensure they are singletons and tree-shakable. +* **`inject()` Function:** Prefer the `inject()` function over constructor injection when injecting dependencies, especially within `provide` functions, `computed` properties, or outside of constructor context. + * **Old Constructor Injection:** + ```typescript + constructor(private myService: MyService) {} + ``` + * **New `inject()` Function:** + ```typescript + import { inject } from '@angular/core'; + + export class MyComponent { + private myService = inject(MyService); + // ... + } + ``` -### Modern Template Syntax (REQUIRED) -```html - -@if (isLoading()) { - -} @else { - -} +### Testing Patterns (CRITICAL) - -@for (item of items(); track item.id) { -
{{item.name}}
-} @empty { - -} +Always use Spectator with jest or Vitest for testing using @ngneat/spectator/jest package. - -@switch (status()) { - @case ('loading') { } - @case ('error') { } - @default { } -} -``` - -### Component Structure (REQUIRED) ```typescript -@Component({ - selector: 'dot-my-component', - standalone: true, // REQUIRED - imports: [CommonModule], - templateUrl: './my-component.html', - styleUrls: ['./my-component.scss'], // Note: plural - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class MyComponent { - // Input/Output signals (REQUIRED) - data = input(); // NOT @Input() - config = input(); - change = output(); // NOT @Output() - - // State signals - loading = signal(false); - - // Computed signals - isValid = computed(() => this.data() && this.loading()); - - // Dependency injection - private service = inject(MyService); -} -``` +import { createComponentFactory, Spectator, byTestId, mockProvider } from '@ngneat/spectator/jest'; -### Testing Patterns (CRITICAL) -```typescript // Spectator setup const createComponent = createComponentFactory({ component: MyComponent, @@ -104,15 +210,6 @@ spectator.typeInElement('test', byTestId('name-input')); .feature-list__item--active { } ``` -### File Structure (REQUIRED) -``` -component-name/ -├── component-name.component.ts # Logic -├── component-name.component.html # Template -├── component-name.component.scss # Styles -└── component-name.component.spec.ts # Tests -``` - ## Build Commands ```bash # Development server @@ -126,10 +223,10 @@ cd core-web && yarn install # NOT npm install ``` ## Tech Stack -- **Angular**: 18.2.3 standalone components +- **Angular**: 20.3.9 standalone components - **UI**: PrimeNG 17.18.11, PrimeFlex 3.3.1 - **State**: NgRx Signals, Component Store -- **Build**: Nx 19.6.5 +- **Build**: Nx 20.5.1 - **Testing**: Jest + Spectator (REQUIRED) ## On-Demand Documentation diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bd78a7eb5481..758f143ee497 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,9 @@ # Require workflow changes to be approved by developers who understand the codebase # and security implications of changes +# Most things are related to CI/CD or actions and should have workflow devs approve /.github/ @dotCMS/core-workflow-developers +# Democratize the PR and Issue templates +/.github/PULL_REQUEST_TEMPLATE/ @dotCMS/dotDevelopers @dotCMS/dotProduct +/.github/ISSUE_TEMPLATE/ @dotCMS/dotDevelopers @dotCMS/dotProduct diff --git a/.github/ISSUE_TEMPLATE/defect.yaml b/.github/ISSUE_TEMPLATE/defect.yaml index ed3596670a49..323376b70e29 100644 --- a/.github/ISSUE_TEMPLATE/defect.yaml +++ b/.github/ISSUE_TEMPLATE/defect.yaml @@ -1,133 +1,71 @@ name: Defect -description: I need to report a bug or issue with dotCMS. -labels: ['Type : Defect',Triage] -projects: ["dotCMS/7"] +description: Report a bug or issue with existing functionality +title: "[DEFECT] " +labels: ["Triage","OKR : Customer Support"] +type: bug +assignees: [] body: - type: markdown attributes: value: | - If you have any questions about how to use this form, check out [How dotCMS uses GitHub](https://docs.google.com/presentation/d/1C1oCESIL9Z84xXo1DPWQZh48c4BSGlVRAn1DYGEFMUw). + **Note: This template is intended for Engineering team use.** - type: textarea id: problem-statement attributes: - label: "Problem Statement" - description: "Explain the problem. How common is this issue? Who does it impact? How severely does it affect them? If this is a front-end issue, please include the Browser & OS." - placeholder: - value: + label: Problem Statement + description: Explain the problem. How common is this issue? Who does it impact? How severely does it affect them? If this is a front-end issue, please include the Browser & OS. + placeholder: "Describe what happened and the impact..." validations: required: true - + - type: textarea id: steps-to-reproduce attributes: - label: "Steps to Reproduce" - description: "How do we reproduce this bug? Ideally, please include a screencast link." - placeholder: - value: + label: Steps to Reproduce + description: Please provide a video screencast showing the issue. If video is not possible, provide detailed steps. + placeholder: | + **Video screencast:** [Upload or link to video showing the issue] + + **Or detailed steps:** + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See error validations: required: true - type: textarea - id: acceptance-criteria + id: acceptance-criteria attributes: - label: "Acceptance Criteria" - description: "What objective needs to be met in order to resolve this?" - placeholder: - value: + label: Acceptance Criteria + description: What objective needs to be met in order to resolve this? + placeholder: | + - [ ] Criteria 1 + - [ ] Criteria 2 + - [ ] Criteria 3 validations: required: true - type: textarea - id: dotCMS-version - attributes: - label: "dotCMS Version" - description: "What version, or environment, is this problem related to?" - placeholder: - value: - validations: - required: true - - - type: dropdown - id: proposed-objective + id: dotcms-version attributes: - label: "Proposed Objective" - description: - options: - - "Please Select" - - "Application Performance" - - "Business" - - "Cloud Engineering" - - "Code Maintenance" - - "Core Features" - - "Customer Success" - - "Customer Support" - - "Documentation" - - "Integrations" - - "Marketing" - - "Quality Assurance" - - "Reliability" - - "Sales" - - "Security & Privacy" - - "Technical User Experience" - - "User Experience" + label: dotCMS Version + description: What version, or environment, is this problem related to? + placeholder: "e.g., 23.10.1, Latest from main branch, Cloud environment" validations: required: true - type: dropdown - id: proposed-prioritiy + id: severity attributes: - label: "Proposed Priority" - description: + label: Severity + description: How severe is this defect? options: - - "Please Select" - - "Priority 1 - Show Stopper" - - "Priority 2 - Important" - - "Priority 3 - Average" - - "Priority 4 - Trivial" + - Critical - System unusable + - High - Major functionality broken + - Medium - Some functionality impacted + - Low - Minor issue or cosmetic validations: required: true - - - type: textarea - id: external-links - attributes: - label: "External Links... Slack Conversations, Support Tickets, Figma Designs, etc." - description: "Provide links to any support tickets or Slack conversations that help explain the problem or desired outcome." - placeholder: - value: - validations: - required: false - - - type: textarea - id: assumptions - attributes: - label: "Assumptions & Initiation Needs" - description: "List relevant assumptions, pre-requisite steps, or issues that need to be completed before this issue can be worked on." - placeholder: - value: - validations: - required: false - - - type: textarea - id: qa-note - attributes: - label: "Quality Assurance Notes & Workarounds" - description: "Add any additional notes for QA you would like; this field is also for use by the QA Team once the issue is in progress." - placeholder: - value: - validations: - required: false - - - type: textarea - id: sub-tasks - attributes: - label: "Sub-Tasks & Estimates" - description: "Use a task-list format, and feel free to @ people responsible for completion." - placeholder: | - - [ ] Some Task Related to this Issue (4 points) - - [ ] Some Task for Damen (2 points) @damen-dotcms - value: - validations: - required: false - diff --git a/.github/ISSUE_TEMPLATE/epic.yml b/.github/ISSUE_TEMPLATE/epic.yml new file mode 100644 index 000000000000..c7e0300ceb76 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic.yml @@ -0,0 +1,59 @@ +name: EPIC +description: Wide area of functionality that delivers significant value within a theme +title: "[EPIC] " +labels: [] +type: epic +assignees: [] + +body: + - type: markdown + attributes: + value: | + **Note: This template is intended for Product team use only.** + + EPICs represent wide areas of functionality that solve specific problems and deliver significant value. + + - type: textarea + id: description + attributes: + label: Description + description: Explain the problem this EPIC solves and how it will benefit customers + placeholder: "Describe the user problem, proposed solution, and customer value..." + validations: + required: true + + - type: textarea + id: desired-outcome + attributes: + label: Desired Outcome + description: How will we would know that this featured is successful? This will be used to determine if a feature "Landed" or not + placeholder: "Describe what data or behaviour would result in a successful landing of this feature." + validations: + required: true + + - type: checkboxes + id: personas + attributes: + label: Target Personas + description: Select the personas this EPIC affects + options: + - label: Developer teams + - label: Content teams + - label: DevOps teams + - label: System administrators (dotCMS) + + - type: textarea + id: links + attributes: + label: Links + description: Related resources + placeholder: | + - [Kickoff deck](url) + - [GitHub](url) + - [Walkthrough](url) + - [Figma](url) + - [Slack Channel](url) + - [EPIC Folder in Google Drive](url) + - [POC](url) + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml index 9e59846e8007..01b50183f3a1 100644 --- a/.github/ISSUE_TEMPLATE/feature.yaml +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -1,113 +1,87 @@ name: Feature -description: I need to change the way something works, or create new functionality. -labels: ['Type : New Functionality', Triage] -projects: ["dotCMS/7"] +description: Specific functionality within an EPIC that provides particular value +title: "[FEATURE] " +labels: [] +type: feature +assignees: [] body: - - type: markdown attributes: value: | - If you have any questions about how to use this form, check out [How dotCMS uses GitHub](https://docs.google.com/presentation/d/1C1oCESIL9Z84xXo1DPWQZh48c4BSGlVRAn1DYGEFMUw). + **Note: This template is intended for Product team use only.** + + Features are specific groupings of functionality within an EPIC that provide particular value. - type: textarea - id: user-story + id: problem attributes: - label: "User Story" - description: "As a ___, I want to be able to ___, so I can ___." - placeholder: - value: + label: Problem + description: Explain in detail what user problem this feature will solve + placeholder: "Describe the specific user pain point..." validations: required: true - type: textarea - id: acceptance-criteria + id: goal attributes: - label: "Acceptance Criteria" - description: "What objective needs to be met in order to resolve this?" - placeholder: - value: + label: Goal + description: Describe the value this delivers to the user or dotCMS. Be specific. + placeholder: "Users will be able to... which will result in..." validations: required: true - - type: dropdown - id: proposed-objective + - type: checkboxes + id: personas attributes: - label: "Proposed Objective" - description: + label: Target Personas + description: Select the personas this feature affects options: - - "Please Select" - - "Application Performance" - - "Cloud Engineering" - - "Code Maintenance" - - "Core Features" - - "Customer Success" - - "Customer Support" - - "Documentation" - - "Integrations" - - "Marketing" - - "Quality Assurance" - - "Reliability" - - "Sales" - - "Security & Privacy" - - "Technical User Experience" - - "User Experience" - validations: - required: true - - - type: dropdown - id: proposed-prioritiy - attributes: - label: "Proposed Priority" - description: - options: - - "Please Select" - - "Priority 1 - Show Stopper" - - "Priority 2 - Important" - - "Priority 3 - Average" - - "Priority 4 - Trivial" - validations: - required: true + - label: Developer teams + - label: Content teams + - label: DevOps teams + - label: System administrators (dotCMS) - type: textarea - id: external-links + id: demo-expectations attributes: - label: "External Links... Slack Conversations, Support Tickets, Figma Designs, etc." - description: "Provide links to any support tickets or Slack conversations that help explain the problem or desired outcome." - placeholder: - value: + label: Demo Expectations + description: Describe exactly what someone can show in a bi-weekly demo to prove the value is delivered + placeholder: "In the demo, we will show..." validations: - required: false + required: true - type: textarea - id: assumptions + id: acceptance-criteria attributes: - label: "Assumptions & Initiation Needs" - description: "List relevant assumptions, pre-requisite steps, or issues that need to be completed before this issue can be worked on." - placeholder: - value: + label: Acceptance Criteria + description: Specific, measurable criteria for feature completion + placeholder: | + - [ ] Criteria 1 + - [ ] Criteria 2 + - [ ] Criteria 3 validations: required: false - type: textarea - id: qa-note + id: user-stories attributes: - label: "Quality Assurance Notes & Workarounds" - description: "Add any additional notes for QA you would like; this field is also for use by the QA Team once the issue is in progress." - placeholder: - value: + label: User Stories + description: Break down into specific user stories + placeholder: | + - As a [persona], I want [functionality], so that [benefit] + - As a [persona], I want [functionality], so that [benefit] validations: required: false - type: textarea - id: sub-tasks + id: links attributes: - label: "Sub-Tasks & Estimates" - description: "Use a task-list format, and feel free to @ people responsible for completion." + label: Links + description: Related resources placeholder: | - - [ ] Some Task Related to this Issue (4 points) - - [ ] Some Task for Damen (2 points) @damen-dotcms - value: + - [Design mockups](url) + - [Technical specs](url) + - [Research](url) validations: - required: false - + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/pillar.yml b/.github/ISSUE_TEMPLATE/pillar.yml new file mode 100644 index 000000000000..d9a364b25ef3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/pillar.yml @@ -0,0 +1,47 @@ +name: Pillar +description: High-level strategic initiative that groups related EPICs +title: "[PILLAR] " +labels: [] +type: pillar +assignees: [] + +body: + - type: markdown + attributes: + value: | + **Note: This template is intended for Product team use only.** + + ## Pillar Overview + Pillars represent broad strategic initiatives that align with product vision and encompass multiple EPICs. + + - type: textarea + id: vision + attributes: + label: Vision + description: Explain in detail how this pillar will benefit our customers + placeholder: "Describe the strategic value and customer impact..." + validations: + required: true + + - type: checkboxes + id: personas + attributes: + label: Target Personas + description: Select the personas this pillar primarily affects + options: + - label: Developer teams + - label: Content teams + - label: DevOps teams + - label: System administrators (dotCMS) + + - type: textarea + id: links + attributes: + label: Links + description: Related resources (design docs, research, etc.) + placeholder: | + - [Kickoff deck](url) + - [Research document](url) + - [Design files](url) + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/spike.yaml b/.github/ISSUE_TEMPLATE/spike.yaml new file mode 100644 index 000000000000..b1933dba18af --- /dev/null +++ b/.github/ISSUE_TEMPLATE/spike.yaml @@ -0,0 +1,66 @@ +name: Spike +description: Timeboxed research to turn unknowns into knowns +title: "[SPIKE] " +labels: [""] +type: spike +assignees: [] + +body: + - type: markdown + attributes: + value: | + **Note: This template is intended for Engineering team use.** + + - type: textarea + id: research-question + attributes: + label: Research Question + description: What specific question or unknown needs to be investigated? + placeholder: "What we need to learn or understand..." + validations: + required: true + + - type: dropdown + id: timebox + attributes: + label: Timebox + description: How much time should be allocated to this research? + options: + - 2h + - 4h + - 8h + validations: + required: true + + - type: textarea + id: acceptance-criteria + attributes: + label: Acceptance Criteria + description: What deliverables or outcomes define completion of this spike? + placeholder: | + - [ ] Document findings on approach A vs approach B + - [ ] Provide recommendation with pros/cons + - [ ] Create proof of concept if feasible + validations: + required: true + + - type: textarea + id: context + attributes: + label: Context + description: Background information and why this research is needed + placeholder: "Provide context on why this spike is necessary..." + validations: + required: false + + - type: textarea + id: links + attributes: + label: Links + description: Related resources, documentation, or references + placeholder: | + - [Related documentation](url) + - [Similar implementations](url) + - [Technical specs](url) + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/task.yaml b/.github/ISSUE_TEMPLATE/task.yaml index f5e00dff04ba..8a7298e31e17 100644 --- a/.github/ISSUE_TEMPLATE/task.yaml +++ b/.github/ISSUE_TEMPLATE/task.yaml @@ -1,115 +1,53 @@ name: Task -description: I just need to create a simple to-do for someone on the team. -labels: ['Type : Task',Triage] -projects: ["dotCMS/7"] +description: Technical task or improvement that needs to be completed +title: "[TASK] " +labels: ["Triage"] +type: task +assignees: [] body: - - type: markdown attributes: value: | - If you have any questions about how to use this form, check out [How dotCMS uses GitHub](https://docs.google.com/presentation/d/1C1oCESIL9Z84xXo1DPWQZh48c4BSGlVRAn1DYGEFMUw). - + **Note: This template is intended for Engineering team use.** + - type: textarea - id: task-body + id: description attributes: - label: "Task" - description: "What's this issue meant to track?" - placeholder: "Maintenance on XYZ... Refactoring ABC... Library updates... etc." - value: + label: Description + description: Detailed description of what needs to be done + placeholder: "Describe the task, including context and rationale..." validations: required: true - - type: dropdown - id: proposed-objective + - type: textarea + id: acceptance-criteria attributes: - label: "Proposed Objective" - description: - options: - - "Please Select" - - "Same as Parent Issue" - - "Application Performance" - - "Cloud Engineering" - - "Code Maintenance" - - "Core Features" - - "Customer Success" - - "Customer Support" - - "Documentation" - - "Integrations" - - "Marketing" - - "Quality Assurance" - - "Reliability" - - "Sales" - - "Security & Privacy" - - "Technical User Experience" - - "User Experience" + label: Acceptance Criteria + description: Specific criteria that must be met for this task to be considered complete + placeholder: | + - [ ] Criteria 1 + - [ ] Criteria 2 + - [ ] Criteria 3 validations: required: true - type: dropdown - id: proposed-prioritiy + id: priority attributes: - label: "Proposed Priority" - description: + label: Priority + description: Priority level for this task options: - - "Please Select" - - "Same as Parent Issue" - - "Priority 1 - Show Stopper" - - "Priority 2 - Important" - - "Priority 3 - Average" - - "Priority 4 - Trivial" - validations: - required: true - - - type: textarea - id: acceptance-criteria - attributes: - label: "Acceptance Criteria" - description: "What objective needs to be met in order to resolve this?" - placeholder: - value: + - High + - Medium + - Low validations: required: false - type: textarea - id: external-links + id: additional-context attributes: - label: "External Links... Slack Conversations, Support Tickets, Figma Designs, etc." - description: "Provide links to any support tickets or Slack conversations that help explain the problem or desired outcome." - placeholder: - value: + label: Additional Context + description: Any additional information that might be helpful validations: - required: false - - - type: textarea - id: assumptions - attributes: - label: "Assumptions & Initiation Needs" - description: "List relevant assumptions, pre-requisite steps, or issues that need to be completed before this issue can be worked on." - placeholder: - value: - validations: - required: false - - - type: textarea - id: qa-note - attributes: - label: "Quality Assurance Notes & Workarounds" - description: "Add any additional notes for QA you would like; this field is also for use by the QA Team once the issue is in progress." - placeholder: - value: - validations: - required: false - - - type: textarea - id: sub-tasks - attributes: - label: "Sub-Tasks & Estimates" - description: "Use a task-list format, and feel free to @ people responsible for completion." - placeholder: | - - [ ] Some Task Related to this Issue (4 points) - - [ ] Some Task for Damen (2 points) @damen-dotcms - value: - validations: - required: false - + required: false \ No newline at end of file diff --git a/.github/actions/core-cicd/deployment/deploy-javascript-sdk/action.yml b/.github/actions/core-cicd/deployment/deploy-javascript-sdk/action.yml index b9d55fb4dbaf..d36148cb38a9 100644 --- a/.github/actions/core-cicd/deployment/deploy-javascript-sdk/action.yml +++ b/.github/actions/core-cicd/deployment/deploy-javascript-sdk/action.yml @@ -97,6 +97,7 @@ runs: # Check the status of each package and find the highest version HIGHEST_VERSION="0.0.0" + HIGHEST_NEXT_VERSION="" VERSION_SOURCE="none" PACKAGES_STATUS="" @@ -108,8 +109,9 @@ runs: if npm view @dotcms/${sdk} >/dev/null 2>&1; then # Package exists, get current versions STABLE_VERSION=$(npm view @dotcms/${sdk} dist-tags --json 2>/dev/null | jq -r '.latest // empty') + NEXT_VERSION=$(npm view @dotcms/${sdk} dist-tags --json 2>/dev/null | jq -r '.next // empty') - echo " ✅ Package exists - Latest: ${STABLE_VERSION:-'none'}" + echo " ✅ Package exists - Latest: ${STABLE_VERSION:-'none'}, Next: ${NEXT_VERSION:-'none'}" # Update highest version if this package has a higher stable version if [ -n "$STABLE_VERSION" ] && [ "$STABLE_VERSION" != "null" ] && [ "$STABLE_VERSION" != "empty" ]; then @@ -121,6 +123,18 @@ runs: fi fi + # Track highest next version across packages + if [ -n "$NEXT_VERSION" ] && [ "$NEXT_VERSION" != "null" ] && [ "$NEXT_VERSION" != "empty" ]; then + if [ -z "$HIGHEST_NEXT_VERSION" ]; then + HIGHEST_NEXT_VERSION="$NEXT_VERSION" + else + # Compare next versions to find the highest + if [ "$(printf '%s\n' "$NEXT_VERSION" "$HIGHEST_NEXT_VERSION" | sort -V | tail -n1)" = "$NEXT_VERSION" ]; then + HIGHEST_NEXT_VERSION="$NEXT_VERSION" + fi + fi + fi + PACKAGES_STATUS="${PACKAGES_STATUS}${sdk}:exists," else echo " 📦 Package doesn't exist yet (first-time publication)" @@ -150,6 +164,7 @@ runs: echo "" echo "=== PACKAGE STATUS SUMMARY ===" echo "Base version: $CURRENT_VERSION" + echo "Highest next version: $HIGHEST_NEXT_VERSION" echo "Version source: $VERSION_SOURCE" echo "===============================" @@ -157,7 +172,7 @@ runs: echo "version_source=$VERSION_SOURCE" >> $GITHUB_OUTPUT echo "current_stable=$CURRENT_STABLE" >> $GITHUB_OUTPUT echo "current_beta=" >> $GITHUB_OUTPUT - echo "current_next=" >> $GITHUB_OUTPUT + echo "current_next=$HIGHEST_NEXT_VERSION" >> $GITHUB_OUTPUT echo "::endgroup::" shell: bash @@ -747,4 +762,4 @@ runs: echo "✅ Outputs set:" echo " published: $PUBLISHED" echo " npm_package_version: $NPM_PACKAGE_VERSION" - shell: bash \ No newline at end of file + shell: bash diff --git a/.github/actions/security/org-membership-check/README.md b/.github/actions/security/org-membership-check/README.md new file mode 100644 index 000000000000..56dac0cdff49 --- /dev/null +++ b/.github/actions/security/org-membership-check/README.md @@ -0,0 +1,83 @@ +# Organization Membership Check Action + +This composite action checks if a GitHub user is a member of the dotCMS organization. It's used as a security gate to ensure only dotCMS organization members can trigger sensitive workflows like Claude code reviews. + +## Security Features + +- **Hardcoded Organization**: The organization name "dotCMS" is hardcoded and cannot be overridden +- **All Organization Members**: Detects both public and private organization members +- **Simple Token Usage**: Uses default GITHUB_TOKEN without additional secrets +- **Graceful Error Handling**: Returns clear status without exposing internal API details + +## Inputs + +| Input | Description | Required | Default | +|-------|-------------|----------|---------| +| `username` | GitHub username to check | Yes | N/A | + +## Outputs + +| Output | Description | Possible Values | +|--------|-------------|-----------------| +| `is_member` | Boolean indicating membership | `true` or `false` | +| `membership_status` | Detailed status | `member`, `non-member`, or `error` | + +## Usage + +```yaml +- name: Check organization membership + id: membership-check + uses: ./.github/actions/security/org-membership-check + with: + username: ${{ github.actor }} + +- name: Conditional step based on membership + if: steps.membership-check.outputs.is_member == 'true' + run: echo "User is authorized" +``` + +## Implementation Details + +The action uses the GitHub CLI (`gh`) with the repository's `GITHUB_TOKEN` to check organization membership via the GitHub API endpoint `GET /orgs/dotCMS/members/{username}`. + +**API Behavior** + +The GitHub organization membership API works for both public and private members: + +- **HTTP 204 No Content**: User is a member (public or private) → **AUTHORIZED** +- **HTTP 404 Not Found**: User is not a member → **BLOCKED** + +This approach successfully detects all dotCMS organization members regardless of their membership visibility setting, using only the default GITHUB_TOKEN without requiring additional secrets or configuration. + +## Troubleshooting + +If you're a dotCMS team member but getting blocked by the security gate: + +### Step 1: Verify Organization Membership +1. Visit: https://github.com/orgs/dotCMS/people +2. Look for your username in the member list +3. If you're not listed, you need to be added to the organization + +### Step 2: Check Membership Visibility +If you are listed but still blocked: +1. Look for a "Make public" button next to your name +2. Click it to make your membership public +3. This allows the workflow to detect your membership + +### Step 3: Contact Organization Owners +If you're not a member: +- Contact a dotCMS organization owner to be added +- Only organization members can trigger Claude workflows + +### Common Issues +- **Private membership**: Most common cause - make membership public +- **Not a member**: Contact org owners to be added +- **Recent changes**: GitHub API may take a few minutes to reflect visibility changes + +## Security Considerations + +- Only checks membership in the dotCMS organization (hardcoded) +- Authorizes organization members (requires public membership visibility) +- Logs authorization results without sensitive details +- Uses default GITHUB_TOKEN (no additional secrets required) +- Provides clear troubleshooting guidance for blocked users \ No newline at end of file diff --git a/.github/actions/security/org-membership-check/action.yml b/.github/actions/security/org-membership-check/action.yml new file mode 100644 index 000000000000..2e9111b82f54 --- /dev/null +++ b/.github/actions/security/org-membership-check/action.yml @@ -0,0 +1,80 @@ +name: 'Organization Membership Check' +description: 'Checks if a user is a member of the dotCMS organization' + +inputs: + username: + description: 'GitHub username to check' + required: true + +outputs: + is_member: + description: 'true if user is a member, false otherwise' + value: ${{ steps.check-membership.outputs.is_member }} + membership_status: + description: 'Membership status (member, non-member, error)' + value: ${{ steps.check-membership.outputs.membership_status }} + +runs: + using: 'composite' + steps: + - name: Check Organization Membership + id: check-membership + shell: bash + run: | + echo "Checking organization membership for user: ${{ inputs.username }} in dotCMS organization" + + # Use GitHub CLI to check PUBLIC organization membership + # This uses the default GITHUB_TOKEN and only detects public members + + set +e # Don't exit on error, we want to handle it gracefully + + # Check organization membership using GitHub API + # + # API Behavior: + # - HTTP 204 No Content (exit code 0) = User is a member (public or private) + # - HTTP 404 Not Found (exit code 1) = User is not a member + # + # Note: The API returns the same response for both public and private members, + # so this actually works for both visibility settings. + + echo "Checking organization membership for ${{ inputs.username }} in dotCMS..." + + response=$(gh api orgs/dotCMS/members/${{ inputs.username }} 2>/dev/null) + api_exit_code=$? + + if [ $api_exit_code -eq 0 ]; then + # HTTP 204: User is a member (public or private) + echo "✅ User ${{ inputs.username }} is a member of dotCMS" + echo "is_member=true" >> $GITHUB_OUTPUT + echo "membership_status=member" >> $GITHUB_OUTPUT + else + # HTTP 404: Not a member OR private membership not visible to GITHUB_TOKEN + echo "⌠User ${{ inputs.username }} is not authorized to trigger Claude workflows" + echo "" + echo "🔠TROUBLESHOOTING STEPS:" + echo "1. Verify you are a member of the dotCMS organization:" + echo " → Visit: https://github.com/orgs/dotCMS/people" + echo " → You should see your username in the list" + echo "" + echo "2. If you are a member but have PRIVATE visibility:" + echo " → Click 'Make public' next to your name" + echo " → This allows the workflow to detect your membership" + echo "" + echo "3. If you are not a member:" + echo " → Contact a dotCMS organization owner to be added" + echo " → Only dotCMS organization members can trigger Claude workflows" + echo "" + echo "is_member=false" >> $GITHUB_OUTPUT + echo "membership_status=non-member" >> $GITHUB_OUTPUT + fi + + # Log the result for debugging (without leaking membership details) + membership_result=$(if [ "$(cat $GITHUB_OUTPUT | grep 'is_member=true')" ]; then echo "AUTHORIZED"; else echo "UNAUTHORIZED"; fi) + + if [ "$membership_result" = "UNAUTHORIZED" ]; then + echo "::notice::⌠BLOCKED: ${{ inputs.username }} failed organization membership check. If you're a dotCMS team member, visit https://github.com/orgs/dotCMS/people and ensure your membership is PUBLIC." + else + echo "::notice::✅ AUTHORIZED: ${{ inputs.username }} is a dotCMS organization member" + fi + env: + GITHUB_TOKEN: ${{ github.token }} \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000000..fecad5d1b9a6 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,193 @@ +# Copilot Coding Agent Instructions for dotCMS Core + +## Repository Overview +dotCMS is a **Universal Content Management System** - a large-scale enterprise CMS built with Java (backend) and Angular (frontend). The repository is a Maven multi-module project with an Nx monorepo for frontend code. + +**Tech Stack:** +- **Backend**: Java 21 runtime (Java 11 syntax for core), Maven, JAX-RS REST APIs +- **Frontend**: Angular 19+, TypeScript, Nx workspace, PrimeNG +- **Infrastructure**: Docker, PostgreSQL, Elasticsearch + +## Build Commands (Validated & Essential) + +### Quick Reference +```bash +# FASTEST build for simple backend changes (~2-3 min) +./mvnw install -pl :dotcms-core -DskipTests + +# Full build without Docker (~5-8 min) +./mvnw clean install -DskipTests -Ddocker.skip + +# Full build with Docker image (~8-15 min) +./mvnw clean install -DskipTests +``` + +### Testing Commands +**âš ï¸ CRITICAL: Never run full integration suite (60+ min). Always target specific tests:** +```bash +# Specific integration test class (~2-10 min) +./mvnw verify -pl :dotcms-integration -Dcoreit.test.skip=false -Dit.test=ContentTypeAPIImplTest + +# Specific test method +./mvnw verify -pl :dotcms-integration -Dcoreit.test.skip=false -Dit.test=MyTest#testMethod + +# JVM unit tests only +./mvnw test -pl :dotcms-core + +# Postman API tests (specific collection) +./mvnw verify -pl :dotcms-postman -Dpostman.test.skip=false -Dpostman.collections=ai +``` + +### Frontend Commands +```bash +cd core-web +yarn install # Install dependencies +nx run dotcms-ui:serve # Development server +nx run dotcms-ui:test # Run tests +nx run dotcms-ui:lint # Lint code +nx affected -t test # Test affected projects +``` + +## Project Structure + +``` +core/ +├── dotCMS/ # Main backend Java code +│ └── src/main/java/com/ # Java source files +├── core-web/ # Frontend (Angular/Nx monorepo) +│ ├── apps/dotcms-ui/ # Main admin UI +│ └── libs/ # Shared libraries and SDKs +├── dotcms-integration/ # Integration tests +├── dotcms-postman/ # Postman API tests +├── bom/application/pom.xml # Dependency versions (ADD versions here) +├── parent/pom.xml # Plugin management +└── .github/workflows/ # CI/CD pipelines +``` + +## Critical Patterns (Always Follow) + +### Maven Dependency Management +**ALWAYS add dependency versions to `bom/application/pom.xml`, NEVER to module POMs:** +```xml + + + 1.2.3 + + + + + com.example + my-library + ${my-library.version} + + + +``` + +### Java Coding Patterns +```java +// Configuration - ALWAYS use Config class +import com.dotmarketing.util.Config; +String value = Config.getStringProperty("key", "default"); + +// Logging - ALWAYS use Logger class +import com.dotmarketing.util.Logger; +Logger.info(this, "message"); + +// Services - ALWAYS use APILocator +import com.dotcms.api.system.APILocator; +ContentletAPI contentletAPI = APILocator.getContentletAPI(); + +// Null checking - ALWAYS use UtilMethods +import com.dotmarketing.util.UtilMethods; +if (UtilMethods.isSet(myString)) { } +``` + +### REST API Patterns +```java +@Path("/v1/resource") +@Tag(name = "Resource", description = "Resource operations") +public class ResourceEndpoint { + private final WebResource webResource = new WebResource(); + + @GET @Path("/{id}") + @Operation(summary = "Get by ID") + @ApiResponse(responseCode = "200", content = @Content( + schema = @Schema(implementation = ResponseEntityResourceView.class))) + @Produces(MediaType.APPLICATION_JSON) + public Response getById(@Context HttpServletRequest request, + @Context HttpServletResponse response, @PathParam("id") String id) { + InitDataObject initData = webResource.init(request, response, true); + // Business logic + } +} +``` + +### Angular/Frontend Patterns +```typescript +// Modern control flow (REQUIRED) +@if (condition()) { } +@for (item of items(); track item.id) { } + +// Modern inputs/outputs (REQUIRED) +data = input(); +onChange = output(); + +// Testing - use data-testid + +spectator.setInput('prop', value); // ALWAYS use setInput +``` + +## CI/CD and Validation + +### What Triggers CI +Changes to these paths trigger builds (from `.github/filters.yaml`): +- **Backend**: `dotCMS/**`, `bom/**`, `parent/**`, `pom.xml`, `dotcms-integration/**` +- **Frontend**: `core-web/**` +- **CLI**: `tools/dotcms-cli/**` + +### Required Test Flags +Tests are skipped by default. Enable with explicit flags: +```bash +-Dcoreit.test.skip=false # Integration tests +-Dpostman.test.skip=false # Postman tests +-Dkarate.test.skip=false # Karate tests +``` + +### Validation Checklist +Before committing: +1. Run relevant tests for changed code +2. Check no hardcoded secrets or sensitive data +3. Verify dependency versions are in `bom/application/pom.xml` +4. For REST endpoints: include Swagger/OpenAPI annotations + +## Key Files Reference + +| Purpose | Location | +|---------|----------| +| Backend source | `dotCMS/src/main/java/com/dotcms/` | +| Frontend source | `core-web/apps/dotcms-ui/`, `core-web/libs/` | +| Dependency versions | `bom/application/pom.xml` | +| Plugin versions | `parent/pom.xml` | +| Integration tests | `dotcms-integration/src/test/java/` | +| CI workflows | `.github/workflows/cicd_*.yml` | +| Change detection | `.github/filters.yaml` | + +## Common Issues and Solutions + +| Issue | Solution | +|-------|----------| +| Build fails with Java version | Requires Java 21. Set with SDKMAN: `sdk env install` | +| Tests skipped silently | Add `-D.test.skip=false` flag | +| Frontend build fails | Run `yarn install` first, requires Node 22.15+ | +| Dependency version conflict | Check `bom/application/pom.xml`, run `./mvnw dependency:tree` | +| Docker build fails | Use `-Ddocker.skip` for non-Docker builds | + +## Environment Requirements +- **Java**: 21.0.8+ (via SDKMAN with `.sdkmanrc`) +- **Node.js**: 22.15.0+ (via NVM with `.nvmrc`) +- **Maven**: 3.9+ (wrapper included: `./mvnw`) +- **Docker**: Required for integration tests + +--- +**Trust these instructions.** Only search the codebase if information here is incomplete or incorrect. diff --git a/.github/frontend.instructions.md b/.github/frontend.instructions.md new file mode 100644 index 000000000000..a2dc5a2b46e6 --- /dev/null +++ b/.github/frontend.instructions.md @@ -0,0 +1,142 @@ +--- +description: Frontend development instructions +applyTo: "core-web/**/*.{ts,html,scss,css}" +--- + +# Persona + +You are a dedicated Angular developer who thrives on leveraging the absolute latest features of the framework to build cutting-edge applications. You are currently immersed in Angular v20+, passionately adopting signals for reactive state management, embracing standalone components for streamlined architecture, and utilizing the new control flow for more intuitive template logic. Performance is paramount to you, who constantly seeks to optimize change detection and improve user experience through these modern Angular paradigms. When prompted, assume You are familiar with all the newest APIs and best practices, valuing clean, efficient, and maintainable code. + +## Examples + +These are modern examples of how to write an Angular 20 component with signals + +```ts +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; + + +@Component({ + selector: '{{tag-name}}-root', + templateUrl: '{{tag-name}}.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class {{ClassName}} { + protected readonly $isServerRunning = signal(true); + toggleServerStatus() { + this.$isServerRunning.update(isServerRunning => !isServerRunning); + } +} +``` + +```css +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + + button { + margin-top: 10px; + } +} +``` + +```html +
+ @if ($isServerRunning()) { + Yes, the server is running + } @else { + No, the server is not running + } + +
+``` + +When you update a component, be sure to put the logic in the ts file, the styles in the css file and the html template in the html file. + +## Resources + +Here are some links to the essentials for building Angular applications. Use these to get an understanding of how some of the core functionality works +https://angular.dev/essentials/components +https://angular.dev/essentials/signals +https://angular.dev/essentials/templates +https://angular.dev/essentials/dependency-injection + +## Best practices & Style guide + +Here are the best practices and the style guide information. + +### Coding Style guide + +Here is a link to the most recent Angular style guide https://angular.dev/style-guide + +### TypeScript Best Practices + +- Use strict type checking +- Prefer type inference when the type is obvious +- Avoid the `any` type; use `unknown` when type is uncertain +- Don't allow use enums, use `as const` instead. +- Use `#` prefix to indicate that a property is private, example: `#myPrivateProperty`. + +### Angular Best Practices + +- Always use standalone components over `NgModules` +- Do NOT set `standalone: true` inside the `@Component`, `@Directive` and `@Pipe` decorators +- Use signals for state management +- Implement lazy loading for feature routes +- Use `NgOptimizedImage` for all static images. +- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead +- For signals, use the `$` prefix to indicate that it is a signal, example: `$mySignal` +- For observables, use the `$` suffix to indicate that it is an observable, example: `myObservable$` + +### Components + +- Keep components small and focused on a single responsibility +- Use `input()` signal instead of decorators, learn more here https://angular.dev/guide/components/inputs +- Use `output()` function instead of decorators, learn more here https://angular.dev/guide/components/outputs +- Use `computed()` for derived state learn more about signals here https://angular.dev/guide/signals. +- Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator +- Prefer inline templates for small components +- Prefer Reactive forms instead of Template-driven ones +- Do NOT use `ngClass`, use `class` bindings instead, for context: https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings +- Do NOT use `ngStyle`, use `style` bindings instead, for context: https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings +- Do NOT use `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead + +### State Management + +- Use signals for local component state +- Use `computed()` for derived state +- Keep state transformations pure and predictable +- Do NOT use `mutate` on signals, use `update` or `set` instead +- For complex state management, use the Signal Store pattern, learn more here https://ngrx.io/guide/signals + +### Templates + +- Keep templates simple and avoid complex logic +- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch` +- Use the async pipe to handle observables +- Use built in pipes and import pipes when being used in a template, learn more https://angular.dev/guide/templates/pipes# + +### Services + +- Design services around a single responsibility +- Use the `providedIn: 'root'` option for singleton services +- Use the `inject()` function instead of constructor injection + +### Testing + +- Always use Spectator with jest or Vitest for testing using `@ngneat/spectator` package. +- Use the `createComponentFactory` function to create a component factory. +- Use the `createDirectiveFactory` function to create a directive factory. +- Use the `createPipeFactory` function to create a pipe factory. +- Use the `createServiceFactory` function to create a service factory. +- Use the `createHostFactory` function to create a host factory. +- Use the `createRoutingFactory` function to create a routing factory. +- Use the `createHttpFactory` function to create a http factory. +- Use the `Spectator` class to create a spectator instance. +- Use the `byTestId` function to select a component by its test id. +- Use the `mockProvider` function to mock a service. +- Use the `detectChanges` function to trigger change detection. +- Use the `setInput` function to set an input value. +- Use the `click` function to click an element. \ No newline at end of file diff --git a/.github/workflows/cicd_1-pr.yml b/.github/workflows/cicd_1-pr.yml index cadf48c8f3af..ef08b68ce454 100644 --- a/.github/workflows/cicd_1-pr.yml +++ b/.github/workflows/cicd_1-pr.yml @@ -70,7 +70,7 @@ jobs: karate: ${{ needs.initialize.outputs.backend == 'true' }} frontend: ${{ needs.initialize.outputs.frontend == 'true' }} cli: ${{ needs.initialize.outputs.cli == 'true' }} - e2e: ${{ needs.initialize.outputs.build == 'true' }} + e2e: false secrets: DOTCMS_LICENSE: ${{ secrets.DOTCMS_LICENSE }} diff --git a/.github/workflows/cicd_2-merge-queue.yml b/.github/workflows/cicd_2-merge-queue.yml index 1542418ccfe8..6867bc21cf70 100644 --- a/.github/workflows/cicd_2-merge-queue.yml +++ b/.github/workflows/cicd_2-merge-queue.yml @@ -27,7 +27,7 @@ jobs: karate: ${{ needs.initialize.outputs.backend == 'true' }} frontend: ${{ needs.initialize.outputs.frontend == 'true' }} cli: ${{ needs.initialize.outputs.cli == 'true' }} - e2e: ${{ needs.initialize.outputs.build == 'true' }} + e2e: false secrets: DOTCMS_LICENSE: ${{ secrets.DOTCMS_LICENSE }} finalize: diff --git a/.github/workflows/cicd_3-trunk.yml b/.github/workflows/cicd_3-trunk.yml index 028fa7416211..60e742e255fb 100644 --- a/.github/workflows/cicd_3-trunk.yml +++ b/.github/workflows/cicd_3-trunk.yml @@ -70,6 +70,7 @@ jobs: with: run-all-tests: ${{ inputs.run-all-tests || false }} artifact-run-id: ${{ needs.initialize.outputs.artifact-run-id }} + e2e: false secrets: DOTCMS_LICENSE: ${{ secrets.DOTCMS_LICENSE }} permissions: diff --git a/.github/workflows/cicd_4-nightly.yml b/.github/workflows/cicd_4-nightly.yml index abddfd02d462..9f921dccef4c 100644 --- a/.github/workflows/cicd_4-nightly.yml +++ b/.github/workflows/cicd_4-nightly.yml @@ -63,6 +63,7 @@ jobs: with: run-all-tests: ${{ inputs.run-all-tests || true }} artifact-run-id: ${{ needs.initialize.outputs.artifact-run-id }} + e2e: false secrets: DOTCMS_LICENSE: ${{ secrets.DOTCMS_LICENSE }} permissions: diff --git a/.github/workflows/cicd_5-lts.yml b/.github/workflows/cicd_5-lts.yml index e9bcb045573d..0b798ddd4629 100644 --- a/.github/workflows/cicd_5-lts.yml +++ b/.github/workflows/cicd_5-lts.yml @@ -57,7 +57,7 @@ jobs: karate: ${{ needs.initialize.outputs.backend == 'true' }} frontend: ${{ needs.initialize.outputs.frontend == 'true' }} cli: ${{ needs.initialize.outputs.cli == 'true' }} - e2e: ${{ needs.initialize.outputs.build == 'true' }} + e2e: false secrets: DOTCMS_LICENSE: ${{ secrets.DOTCMS_LICENSE }} diff --git a/.github/workflows/cicd_6-release.yml b/.github/workflows/cicd_6-release.yml new file mode 100644 index 000000000000..0a81dab522d4 --- /dev/null +++ b/.github/workflows/cicd_6-release.yml @@ -0,0 +1,178 @@ +# +# Release Workflow +# +# This workflow handles the complete release process for dotCMS following the established +# phase pattern: initialize -> build -> deployment -> finalize +# +# Key features: +# - Release preparation (branch creation, version setting) +# - Standard build phase for artifact generation +# - Release-specific deployment (Artifactory, Javadocs, plugins) +# - Docker image deployment via standard deployment phase +# - SBOM generation +# - GitHub label management +# - Release notifications +# +# This workflow follows the modular phase pattern established in the CICD architecture +# and replaces the legacy-release_maven-release-process.yml workflow +# + +name: '-6 Release Process' + +on: + workflow_dispatch: + inputs: + release_version: + description: 'Release Version (yy.mm.dd-## or yy.mm.dd_lts_v##] ##: counter)' + required: true + release_commit: + description: 'Commit Hash (default to latest commit)' + required: false + deploy_artifact: + description: 'Deploy Artifact to Artifactory' + type: boolean + default: true + required: false + update_plugins: + description: 'Update Plugins' + type: boolean + default: true + required: false + upload_javadocs: + description: 'Upload Javadocs to S3' + type: boolean + default: true + required: false + update_github_labels: + description: 'Update GitHub labels' + type: boolean + default: true + required: false + notify_slack: + description: 'Notify Slack' + type: boolean + default: true + required: false + +# No concurrency control - releases should complete without interruption +concurrency: + group: release-${{ github.event.inputs.release_version }} + cancel-in-progress: false + +jobs: + # Initialize - standard initialization phase (always first) + initialize: + name: Initialize + uses: ./.github/workflows/cicd_comp_initialize-phase.yml + with: + validation-level: 'none' + + # Release Prepare - validates version, creates release branch, sets version + release-prepare: + name: Release Prepare + needs: [ initialize ] + uses: ./.github/workflows/cicd_comp_release-prepare-phase.yml + with: + release_version: ${{ github.event.inputs.release_version }} + release_commit: ${{ github.event.inputs.release_commit }} + secrets: + CI_MACHINE_TOKEN: ${{ secrets.CI_MACHINE_TOKEN }} + CI_MACHINE_USER: ${{ secrets.CI_MACHINE_USER }} + + # Build - standard build phase for artifact generation + build: + name: Build + needs: [ release-prepare, initialize ] + if: always() && !failure() && !cancelled() + uses: ./.github/workflows/cicd_comp_build-phase.yml + with: + core-build: true + run-pr-checks: false + ref: ${{ needs.release-prepare.outputs.release_tag }} + validate: false + version: ${{ needs.release-prepare.outputs.release_version }} + generate-docker: true + permissions: + contents: read + packages: write + + # Deployment - standard deployment phase for Docker images and NPM + deployment: + name: Deployment + needs: [ release-prepare, initialize, build ] + if: always() && !failure() && !cancelled() + uses: ./.github/workflows/cicd_comp_deployment-phase.yml + with: + environment: ${{ needs.release-prepare.outputs.release_version }} + artifact-run-id: ${{ github.run_id }} + latest: ${{ needs.release-prepare.outputs.is_latest == 'true' }} + deploy-dev-image: true + reuse-previous-build: false + publish-npm-cli: false + publish-npm-sdk-libs: false + secrets: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} + EE_REPO_USERNAME: ${{ secrets.EE_REPO_USERNAME }} + EE_REPO_PASSWORD: ${{ secrets.EE_REPO_PASSWORD }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + DEV_REQUEST_TOKEN: ${{ secrets.DEV_REQUEST_TOKEN }} + + # Release - release-specific operations (Artifactory, Javadocs, Plugins, SBOM, Labels) + # Waits for deployment to complete to safely update labels only if both succeed + release: + name: Release + needs: [ release-prepare, initialize, build, deployment ] + if: always() && !failure() && !cancelled() + uses: ./.github/workflows/cicd_comp_release-phase.yml + with: + release_version: ${{ needs.release-prepare.outputs.release_version }} + release_tag: ${{ needs.release-prepare.outputs.release_tag }} + artifact_run_id: ${{ github.run_id }} + deploy_artifact: ${{ github.event.inputs.deploy_artifact }} + upload_javadocs: ${{ github.event.inputs.upload_javadocs }} + update_plugins: ${{ github.event.inputs.update_plugins }} + update_github_labels: ${{ github.event.inputs.update_github_labels }} + secrets: + EE_REPO_USERNAME: ${{ secrets.EE_REPO_USERNAME }} + EE_REPO_PASSWORD: ${{ secrets.EE_REPO_PASSWORD }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + CI_MACHINE_TOKEN: ${{ secrets.CI_MACHINE_TOKEN }} + + # Finalize - standard finalization phase (required for phase pattern) + finalize: + name: Finalize + if: always() + needs: [ initialize, build, deployment, release ] + uses: ./.github/workflows/cicd_comp_finalize-phase.yml + with: + artifact-run-id: ${{ github.run_id }} + needsData: ${{ toJson(needs) }} + + # Report - send release notification to Slack + report: + name: Report + runs-on: ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }} + needs: [ release-prepare, deployment, finalize ] + if: always() + steps: + - name: Checkout core + uses: actions/checkout@v4 + with: + ref: main + + - uses: ./.github/actions/core-cicd/cleanup-runner + + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.RELEASE_SLACK_WEBHOOK }} + SLACK_USERNAME: dotBot + SLACK_TITLE: "Important news!" + SLACK_MSG_AUTHOR: " " + MSG_MINIMAL: true + SLACK_FOOTER: "" + SLACK_ICON: https://avatars.slack-edge.com/temp/2021-12-08/2830145934625_e4e464d502865ff576e4.png + SLACK_MESSAGE: " This automated script is excited to announce the release of a new version of dotCMS `${{ needs.release-prepare.outputs.release_version }}` :rocket:\n:docker: Produced images: [${{ needs.deployment.outputs.formatted_tags || needs.deployment.outputs.docker_tags }}]" + if: success() && github.event.inputs.notify_slack == 'true' \ No newline at end of file diff --git a/.github/workflows/cicd_comp_cli-native-build-phase.yml b/.github/workflows/cicd_comp_cli-native-build-phase.yml index 25237b6a876a..a8c5a2c36819 100644 --- a/.github/workflows/cicd_comp_cli-native-build-phase.yml +++ b/.github/workflows/cicd_comp_cli-native-build-phase.yml @@ -64,7 +64,7 @@ jobs: id: set-os run: | if [[ "${{ inputs.buildNativeImage }}" == "true" ]]; then - RUNNERS='[{ "os": "ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }}", "label": "Linux", "platform": "linux-x86_64" }, { "os": "macos-13", "label": "macOS-Intel", "platform": "osx-x86_64" }, { "os": "macos-14", "label": "macOS-Silicon", "platform": "osx-aarch_64" }]' + RUNNERS='[{ "os": "ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }}", "label": "Linux", "platform": "linux-x86_64" }, { "os": "macos-${{ vars.MACOS_INTEL_RUNNER_VERSION || '15-intel' }}", "label": "macOS-Intel", "platform": "osx-x86_64" }, { "os": "macos-${{ vars.MACOS_SILICON_RUNNER_VERSION || '14' }}", "label": "macOS-Silicon", "platform": "osx-aarch_64" }]' else RUNNERS='[{ "os": "ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }}", "label": "Linux", "platform": "linux-x86_64" }]' fi diff --git a/.github/workflows/cicd_comp_deployment-phase.yml b/.github/workflows/cicd_comp_deployment-phase.yml index 96ee509b0b02..76ad7ab3fcfb 100644 --- a/.github/workflows/cicd_comp_deployment-phase.yml +++ b/.github/workflows/cicd_comp_deployment-phase.yml @@ -37,7 +37,14 @@ on: type: boolean publish-npm-sdk-libs: default: false - type: boolean + type: boolean + outputs: + docker_tags: + description: 'Docker image tags that were built' + value: ${{ jobs.deployment.outputs.docker_tags }} + formatted_tags: + description: 'Formatted Docker tags for notifications' + value: ${{ jobs.deployment.outputs.formatted_tags }} secrets: DOCKER_USERNAME: required: false @@ -67,6 +74,9 @@ jobs: # Use of Docker environments to enable per-deployment environment secrets # This allows for different secrets to be used based on the deployment environment environment: ${{ inputs.environment }} + outputs: + docker_tags: ${{ steps.docker_build.outputs.tags }} + formatted_tags: ${{ steps.format-tags.outputs.formatted_tags }} steps: # Checkout the repository - uses: actions/checkout@v4 @@ -108,6 +118,21 @@ jobs: DOTCMS_DOCKER_TAG=${{ inputs.environment }} SDKMAN_JAVA_VERSION=${{ steps.get-sdkman-version.outputs.SDKMAN_JAVA_VERSION }} + # Format tags for Slack notifications + - name: Format Tags + id: format-tags + run: | + tags='' + tags_arr=( ${{ steps.docker_build.outputs.tags }} ) + + for tag in "${tags_arr[@]}" + do + [[ -n "${tags}" ]] && tags="${tags}, " + tags="${tags}\`${tag}\`" + done + + echo "formatted_tags=${tags}" >> $GITHUB_OUTPUT + # Build and push the dev Docker image (if required) - name: Build/Push Docker Dev Image id: docker_build_dev diff --git a/.github/workflows/cicd_comp_release-phase.yml b/.github/workflows/cicd_comp_release-phase.yml new file mode 100644 index 000000000000..7770a7d42062 --- /dev/null +++ b/.github/workflows/cicd_comp_release-phase.yml @@ -0,0 +1,216 @@ +# Release Phase Workflow +# +# This reusable workflow handles release-specific finalization operations: +# - Deploying artifacts to Artifactory (Maven repository) +# - Generating and uploading Javadocs to S3 +# - Triggering plugin repository updates +# - Generating and uploading SBOM (Software Bill of Materials) +# - Updating GitHub issue labels for release tracking +# +# This phase runs after the standard deployment phase (which handles Docker/NPM) +# and focuses on release-specific operations. +# +# Key features: +# - Configurable release operations (artifacts, javadocs, plugins, labels) +# - SBOM generation and GitHub release asset upload +# - GitHub issue label management for release tracking +# - AWS S3 integration for javadoc hosting + +name: Release Phase + +on: + workflow_call: + inputs: + release_version: + description: 'Release version' + required: true + type: string + release_tag: + description: 'Release tag' + required: true + type: string + artifact_run_id: + description: 'Artifact run ID' + required: false + type: string + default: ${{ github.run_id }} + deploy_artifact: + description: 'Deploy artifact to Artifactory' + type: boolean + default: true + upload_javadocs: + description: 'Upload Javadocs to S3' + type: boolean + default: true + update_plugins: + description: 'Update Plugins' + type: boolean + default: true + update_github_labels: + description: 'Update GitHub labels' + type: boolean + default: true + secrets: + EE_REPO_USERNAME: + required: false + description: 'Artifactory username' + EE_REPO_PASSWORD: + required: false + description: 'Artifactory password' + AWS_ACCESS_KEY_ID: + required: false + description: 'AWS access key ID' + AWS_SECRET_ACCESS_KEY: + required: false + description: 'AWS secret access key' + CI_MACHINE_TOKEN: + required: false + description: 'CI machine token for GitHub API operations' + +jobs: + # Deploy release artifacts to Artifactory and S3 + release-artifacts: + name: Release Artifacts + runs-on: ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }} + env: + AWS_REGION: us-east-1 + JVM_TEST_MAVEN_OPTS: '-e -B -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn' + steps: + - name: Checkout core + uses: actions/checkout@v4 + with: + ref: ${{ inputs.release_tag }} + + - uses: ./.github/actions/core-cicd/cleanup-runner + + - name: Setup Java + id: setup-java + uses: ./.github/actions/core-cicd/setup-java + + - name: Restore Maven Repository + uses: actions/download-artifact@v4 + with: + name: maven-repo + path: ~/.m2/repository + + - name: Configure Maven Settings + uses: whelk-io/maven-settings-xml-action@v20 + with: + servers: '[{ "id": "dotcms-libs-local", "username": "${{ secrets.EE_REPO_USERNAME }}", "password": "${{ secrets.EE_REPO_PASSWORD }}" }]' + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + if: inputs.upload_javadocs == true + + - name: Deploy Release Artifacts to Artifactory + run: | + ./mvnw -ntp \ + "${JVM_TEST_MAVEN_OPTS}" \ + -Dprod=true \ + -DskipTests=true \ + deploy + if: inputs.deploy_artifact == true + + - name: Generate and Upload Javadocs + run: | + ./mvnw -ntp \ + "${JVM_TEST_MAVEN_OPTS}" \ + javadoc:javadoc \ + -pl :dotcms-core + rc=$? + if [[ $rc != 0 ]]; then + echo "Javadoc generation failed with exit code $rc" + exit $rc + fi + + site_dir=./dotCMS/target/site + javadoc_dir=${site_dir}/javadocs + s3_uri=s3://static.dotcms.com/docs/${{ inputs.release_version }}/javadocs + + mv ${site_dir}/apidocs ${javadoc_dir} + echo "Running: aws s3 cp ${javadoc_dir} ${s3_uri} --recursive" + aws s3 cp ${javadoc_dir} ${s3_uri} --recursive + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + if: inputs.upload_javadocs == true + + - name: Trigger Plugin Repository Update + env: + RELEASE_VERSION: ${{ inputs.release_version }} + CI_MACHINE_TOKEN: ${{ secrets.CI_MACHINE_TOKEN }} + run: | + # shellcheck disable=SC2153 + release_version="${RELEASE_VERSION}" + response=$(curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${CI_MACHINE_TOKEN}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/dotCMS/plugin-seeds/dispatches \ + -d "{\"event_type\": \"on-plugins-release\", \"client_payload\": {\"release_version\": \"${release_version}\"}}" \ + -w "\n%{http_code}" \ + -s) + http_code=$(echo "$response" | tail -n1) + if [ "${http_code}" != "204" ]; then + echo "Failed to dispatch workflow. HTTP code: $http_code" + echo "Response: $response" + fi + if: inputs.update_plugins == true + + # Generate and upload SBOM to GitHub release + release-sbom: + name: Release SBOM + runs-on: ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }} + continue-on-error: true + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/legacy-release/sbom-generator + id: sbom-generator + with: + dotcms_version: ${{ inputs.release_version }} + github_token: ${{ secrets.CI_MACHINE_TOKEN }} + + - name: Download SBOM Artifacts + uses: actions/download-artifact@v4 + with: + path: ${{ github.workspace }}/artifacts + pattern: ${{ steps.sbom-generator.outputs.sbom-artifact }} + + - name: Upload SBOM to GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.CI_MACHINE_TOKEN }} + run: | + echo "::group::Upload SBOM Asset" + ARTIFACT_NAME=${{ steps.sbom-generator.outputs.sbom-artifact }} + SBOM="./artifacts/${ARTIFACT_NAME}/${ARTIFACT_NAME}.json" + + if [ -f "${SBOM}" ]; then + echo "SBOM: ${SBOM}" + cat "${SBOM}" + + zip "${ARTIFACT_NAME}.zip" "${SBOM}" + gh release upload "${{ inputs.release_tag }}" "${ARTIFACT_NAME}.zip" + else + echo "SBOM artifact not found." + fi + echo "::endgroup::" + + # Update GitHub labels for release tracking + # Only updates labels if release-artifacts (Artifactory/Javadocs) succeeded. + # The calling workflow's dependency chain ensures deployment also succeeded. + release-labeling: + name: Release Labeling + needs: [ release-artifacts ] + if: success() && inputs.update_github_labels == true + uses: ./.github/workflows/issue_comp_release-labeling.yml + with: + new_label: 'Release : ${{ inputs.release_version }}' + rename_label: 'Next Release' + secrets: + CI_MACHINE_TOKEN: ${{ secrets.CI_MACHINE_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/cicd_comp_release-prepare-phase.yml b/.github/workflows/cicd_comp_release-prepare-phase.yml new file mode 100644 index 000000000000..320d83552024 --- /dev/null +++ b/.github/workflows/cicd_comp_release-prepare-phase.yml @@ -0,0 +1,182 @@ +# Release Prepare Phase Workflow +# +# This reusable workflow is responsible for preparing a release by: +# - Validating release version format +# - Creating release branch +# - Setting release version in maven.config +# - Updating LICENSE file Change Date +# - Creating initial GitHub release +# - Caching build artifacts for subsequent phases +# +# Key features: +# - Version validation (standard and LTS formats) +# - Automatic branch and tag management +# - Maven configuration setup for production builds +# - Build artifact caching for reuse +# - GitHub release creation + +name: Release Prepare Phase + +on: + workflow_call: + inputs: + release_version: + description: 'Release Version (yy.mm.dd-## or yy.mm.dd_lts_v##)' + required: true + type: string + release_commit: + description: 'Commit Hash (default to latest commit)' + required: false + type: string + default: '' + secrets: + CI_MACHINE_TOKEN: + required: false + description: 'CI machine token for GitHub operations (defaults to GITHUB_TOKEN)' + CI_MACHINE_USER: + required: false + description: 'CI machine user for git commits (defaults to github-actions[bot])' + outputs: + release_version: + value: ${{ jobs.prepare.outputs.release_version }} + release_tag: + value: ${{ jobs.prepare.outputs.release_tag }} + release_branch: + value: ${{ jobs.prepare.outputs.release_branch }} + release_commit: + value: ${{ jobs.prepare.outputs.release_commit }} + release_hash: + value: ${{ jobs.prepare.outputs.release_hash }} + is_lts: + value: ${{ jobs.prepare.outputs.is_lts }} + is_latest: + value: ${{ jobs.prepare.outputs.is_latest }} + date: + value: ${{ jobs.prepare.outputs.date }} + +jobs: + prepare: + name: Prepare Release + runs-on: ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }} + outputs: + release_version: ${{ steps.set-version.outputs.release_version }} + release_tag: ${{ steps.set-version.outputs.release_tag }} + release_branch: ${{ steps.set-version.outputs.release_branch }} + release_commit: ${{ steps.set-version.outputs.release_commit }} + release_hash: ${{ steps.set-version.outputs.release_hash }} + is_lts: ${{ steps.set-version.outputs.is_lts }} + is_latest: ${{ steps.set-version.outputs.is_latest }} + date: ${{ steps.set-version.outputs.date }} + steps: + - name: Validate Release Version Format + env: + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + # shellcheck disable=SC2153 + release_version="${RELEASE_VERSION}" + if [[ ! ${release_version} =~ ^[0-9]{2}.[0-9]{2}.[0-9]{2}(-[0-9]{1,2}|_lts_v[0-9]{1,2})$ ]]; then + echo 'Release version must be in the format yy.mm.dd-counter or yy.mm.dd_lts_v##' + exit 1 + fi + + - run: echo 'GitHub context' + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + + - name: Checkout core + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.CI_MACHINE_TOKEN || github.token }} + + - uses: ./.github/actions/core-cicd/cleanup-runner + + - name: Set Version Variables + id: set-version + env: + RELEASE_VERSION: ${{ inputs.release_version }} + RELEASE_COMMIT: ${{ inputs.release_commit }} + CI_MACHINE_USER: ${{ secrets.CI_MACHINE_USER || 'github-actions[bot]' }} + run: | + git config user.name "${CI_MACHINE_USER}" + git config user.email "${CI_MACHINE_USER}@users.noreply.github.com" + + # shellcheck disable=SC2153 + release_version="${RELEASE_VERSION}" + release_branch="release-${release_version}" + release_tag="v${release_version}" + # shellcheck disable=SC2153 + release_commit="${RELEASE_COMMIT}" + if [[ -z "${release_commit}" ]]; then + release_commit=$(git log -1 --pretty=%H) + fi + release_hash=${release_commit::7} + is_lts=false + is_latest=false + [[ ${release_version} =~ ^[0-9]{2}.[0-9]{2}.[0-9]{2}_lts_v[0-9]{1,2}$ ]] && is_lts=true + [[ ${release_version} =~ ^[0-9]{2}.[0-9]{2}.[0-9]{2}-[0-9]{1,2}$ ]] && is_latest=true + + { + echo "release_version=${release_version}" + echo "release_branch=${release_branch}" + echo "release_tag=${release_tag}" + echo "release_commit=${release_commit}" + echo "release_hash=${release_hash}" + echo "is_lts=${is_lts}" + echo "is_latest=${is_latest}" + echo "date=$(/bin/date -u "+%Y-%m")" + } >> "$GITHUB_OUTPUT" + + - name: Create Release Branch and Tag + id: create-branch + run: | + release_tag=${{ steps.set-version.outputs.release_tag }} + if git rev-parse "${release_tag}" >/dev/null 2>&1; then + echo "Tag ${release_tag} exists, removing it" + git push origin :refs/tags/${release_tag} + fi + + git reset --hard ${{ steps.set-version.outputs.release_commit }} + release_version=${{ steps.set-version.outputs.release_version }} + release_branch=${{ steps.set-version.outputs.release_branch }} + + remote=$(git ls-remote --heads https://github.com/dotCMS/core.git "${release_branch}" | wc -l | tr -d '[:space:]') + if [[ "${remote}" == '1' ]]; then + echo "Release branch ${release_branch} already exists, removing it" + git push origin :${release_branch} + fi + git checkout -b ${release_branch} + + # set version in .mvn/maven.config + echo "-Dprod=true" > .mvn/maven.config + echo "-Drevision=${release_version}" >> .mvn/maven.config + echo "-Dchangelist=" >> .mvn/maven.config + + git add .mvn/maven.config + + # Update LICENSE file Change Date + chmod +x .github/actions/update-license-date.sh + .github/actions/update-license-date.sh + + # Add LICENSE file if it was modified + if ! git diff --quiet HEAD -- LICENSE; then + echo "LICENSE file was updated, adding to commit" + git add LICENSE + fi + + git status + git commit -a -m "ðŸ Publishing release version [${release_version}]" + git push origin ${release_branch} + + release_commit=$(git log -1 --pretty=%H) + echo "release_commit=${release_commit}" >> "$GITHUB_OUTPUT" + + - name: Create GitHub Release + run: | + curl -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + -H "Authorization: Bearer ${{ secrets.CI_MACHINE_TOKEN || github.token }}" \ + https://api.github.com/repos/${{ github.repository }}/releases \ + -d '{"tag_name": "${{ steps.set-version.outputs.release_tag }}", "name": "Release ${{ steps.set-version.outputs.release_version }}", "target_commitish": "${{ steps.create-branch.outputs.release_commit }}", "draft": false, "prerelease": false, "generate_release_notes": false}' + if: success() \ No newline at end of file diff --git a/.github/workflows/cicd_comp_test-phase.yml b/.github/workflows/cicd_comp_test-phase.yml index 3111d8cf0f1a..69812e9fd1ca 100644 --- a/.github/workflows/cicd_comp_test-phase.yml +++ b/.github/workflows/cicd_comp_test-phase.yml @@ -102,7 +102,9 @@ jobs: // Process each test type for (const [testType, testConfig] of Object.entries(config.test_types)) { - const shouldRun = inputs['run-all-tests'] || inputs[testConfig.condition_input]; + // Check if explicitly disabled (false) - this overrides run-all-tests + const isExplicitlyDisabled = inputs[testConfig.condition_input] === false; + const shouldRun = !isExplicitlyDisabled && (inputs['run-all-tests'] || inputs[testConfig.condition_input]); if (!shouldRun) { console.log(`Skipping ${testType} tests - not enabled`); diff --git a/.github/workflows/issue_comment_claude-code-review.yaml b/.github/workflows/issue_comment_claude-code-review.yaml new file mode 100644 index 000000000000..aa09ff499648 --- /dev/null +++ b/.github/workflows/issue_comment_claude-code-review.yaml @@ -0,0 +1,79 @@ +name: Claude-Code When Mentioned + +# Concurrency control to prevent multiple jobs running for the same PR/issue +concurrency: + group: claude-${{ github.event.pull_request.number || github.event.issue.number || 'manual' }} + cancel-in-progress: false + +on: + workflow_dispatch: + inputs: + test_mode: + description: 'Test mode for debugging' + required: false + type: boolean + default: false + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +jobs: + # Security gate: Check if user is dotCMS organization member + # + # REQUIREMENTS FOR CLAUDE ACCESS: + # 1. Must be a member of the dotCMS organization + # 2. Membership must be set to PUBLIC visibility + # + # TROUBLESHOOTING: If blocked, visit https://github.com/orgs/dotCMS/people + # and ensure your membership is public (click "Make public" if needed) + security-check: + runs-on: ubuntu-latest + permissions: + contents: read # Allow repository checkout + # Note: Organization membership checking uses fine-grained token + # so no additional GITHUB_TOKEN permissions needed for that API + outputs: + authorized: ${{ steps.membership-check.outputs.is_member }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check organization membership + id: membership-check + uses: ./.github/actions/security/org-membership-check + with: + username: ${{ github.event.comment.user.login || github.actor }} + + - name: Log security decision + run: | + if [ "${{ steps.membership-check.outputs.is_member }}" = "true" ]; then + echo "✅ Access granted: User is a dotCMS organization member" + else + echo "⌠Access denied: User failed dotCMS organization membership check" + echo "" + echo "📋 TROUBLESHOOTING: If you are a dotCMS team member:" + echo " 1. Visit https://github.com/orgs/dotCMS/people" + echo " 2. Ensure your membership is set to 'Public'" + echo " 3. If you're not listed, contact an organization owner" + echo "" + echo "::warning::Unauthorized user attempted to trigger Claude workflow: ${{ github.event.comment.user.login || github.actor }}" + fi + + # Interactive Claude mentions (simplified using centralized logic) + claude-interactive: + needs: security-check + if: needs.security-check.outputs.authorized == 'true' + uses: dotCMS/ai-workflows/.github/workflows/claude-orchestrator.yml@v1.0.0 + with: + trigger_mode: interactive + allowed_tools: | + Bash(git status) + Bash(git diff) + timeout_minutes: 15 + runner: ubuntu-latest + enable_mention_detection: true # Uses built-in @claude mention detection + # custom_trigger_condition: | # Optional: Override default mention detection + # your custom condition here + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/.github/workflows/issue_comp_link-issue-to-pr.yml b/.github/workflows/issue_comp_link-issue-to-pr.yml index 680a02751ce1..fba84216f752 100644 --- a/.github/workflows/issue_comp_link-issue-to-pr.yml +++ b/.github/workflows/issue_comp_link-issue-to-pr.yml @@ -141,23 +141,24 @@ jobs: - name: Get existing issue comments id: get_comments run: | - comments=$(curl -s \ + # Fetch comments and write to temp file to avoid multiline JSON issues in GitHub Actions outputs + curl -s \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \ -H "X-GitHub-Api-Version: 2022-11-28" \ https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.determine_issue.outputs.final_issue_number }}/comments \ - | jq -c .) - - echo "comments=$comments" >> "$GITHUB_OUTPUT" + | jq -c . > /tmp/issue_comments.json + + echo "comments_file=/tmp/issue_comments.json" >> "$GITHUB_OUTPUT" - name: Check if comment already exists id: check_comment run: | - comments='${{ steps.get_comments.outputs.comments }}' + comments_file="${{ steps.get_comments.outputs.comments_file }}" pr_url="${{ inputs.pr_url }}" - - # Check if our bot comment already exists - existing_comment=$(echo "$comments" | jq -r '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("PRs linked to this issue"))) | .id' | head -1) + + # Check if our bot comment already exists (read from file instead of env var) + existing_comment=$(jq -r '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("PRs linked to this issue"))) | .id' "$comments_file" | head -1) if [[ -n "$existing_comment" && "$existing_comment" != "null" ]]; then echo "Found existing comment: $existing_comment" @@ -169,7 +170,7 @@ jobs: # Get existing PR list from the comment if it exists if [[ -n "$existing_comment" && "$existing_comment" != "null" ]]; then - existing_body=$(echo "$comments" | jq -r --arg id "$existing_comment" '.[] | select(.id == ($id | tonumber)) | .body') + existing_body=$(jq -r --arg id "$existing_comment" '.[] | select(.id == ($id | tonumber)) | .body' "$comments_file") # Extract existing PR lines (lines starting with "- [") existing_pr_lines=$(echo "$existing_body" | grep "^- \[" | sort -u) diff --git a/.github/workflows/legacy-release_comp_maven-build-docker-image.yml b/.github/workflows/legacy-release_comp_maven-build-docker-image.yml index 4a9fe0845602..b021e453e456 100644 --- a/.github/workflows/legacy-release_comp_maven-build-docker-image.yml +++ b/.github/workflows/legacy-release_comp_maven-build-docker-image.yml @@ -25,6 +25,10 @@ on: required: false type: boolean default: false + custom_tag: + description: 'Custom Docker Image Tag' + required: false + type: string secrets: docker_io_username: description: 'Docker.io username' @@ -248,6 +252,7 @@ jobs: type=raw,value=${{ steps.set-common-vars.outputs.version }},enable=${{ steps.set-common-vars.outputs.is_release }} type=raw,value=latest,enable=${{ steps.set-common-vars.outputs.is_latest }} type=raw,value={{sha}},enable=${{ steps.set-common-vars.outputs.is_custom }} + type=raw,value=${{ inputs.custom_tag }},enable=${{ inputs.custom_tag != '' }} if: success() - name: Debug Docker Metadata diff --git a/.github/workflows/legacy-release_publish-dotcms-docker-image.yml b/.github/workflows/legacy-release_publish-dotcms-docker-image.yml index ddeea10d60f3..2eb475603012 100644 --- a/.github/workflows/legacy-release_publish-dotcms-docker-image.yml +++ b/.github/workflows/legacy-release_publish-dotcms-docker-image.yml @@ -16,6 +16,10 @@ on: - 'GHCR.IO' - 'BOTH' default: 'DOCKER.IO' + custom_tag: + description: 'Custom Docker Image Tag' + required: false + type: string jobs: prepare-build: name: Prepare build @@ -45,6 +49,7 @@ jobs: ref: ${{ needs.prepare-build.outputs.ref }} docker_platforms: ${{ needs.prepare-build.outputs.docker_platforms }} docker_registry: ${{ inputs.docker_registry }} + custom_tag: ${{ inputs.custom_tag }} secrets: docker_io_username: ${{ secrets.DOCKER_USERNAME }} docker_io_token: ${{ secrets.DOCKER_TOKEN }} diff --git a/.gitignore b/.gitignore index 63116c4fc68f..47af9ca53d7f 100644 --- a/.gitignore +++ b/.gitignore @@ -189,4 +189,21 @@ dotCMS/dependencies.gradle # Examples package-lock.json examples/nextjs/package-lock.json examples/angular/package-lock.json -examples/astro/package-lock.json \ No newline at end of file +examples/astro/package-lock.json + +local/ + +**/.yalc/ + +# Claude Code diagnostic outputs +.claude/diagnostics/ + +# Python cache files +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +*.egg diff --git a/.mise.md b/.mise.md new file mode 100644 index 000000000000..316f33c41b53 --- /dev/null +++ b/.mise.md @@ -0,0 +1,154 @@ +# Mise Configuration for dotCMS + +This repository uses [mise](https://mise.jdx.dev/) for managing development tool versions, including GitHub CLI and Python. + +## What is Mise? + +Mise (formerly rtx) is a polyglot tool version manager. It automatically installs and manages versions of tools like Python, Node.js, GitHub CLI, and many others. + +## Quick Start + +### 1. Install Mise + +```bash +# macOS +brew install mise + +# Or using the official installer +curl https://mise.run | sh +``` + +### 2. Activate Mise in Your Shell + +Add to your `~/.zshrc` (for zsh) or `~/.bashrc` (for bash): + +```bash +eval "$(mise activate zsh)" # for zsh +eval "$(mise activate bash)" # for bash +``` + +Then reload your shell: + +```bash +source ~/.zshrc # or source ~/.bashrc +# Or restart your terminal +``` + +### 3. Install Tools + +Navigate to the repository and mise will automatically install configured tools: + +```bash +cd /path/to/dotcms/core +mise install +``` + +Or let mise auto-install when you enter the directory: + +```bash +cd /path/to/dotcms/core +# Tools will install automatically if auto_install is enabled +``` + +## Configured Tools + +The `.mise.toml` file configures these tools: + +- **gh (GitHub CLI)** - `latest` version + - Used for issue and PR management + - Commands: `gh issue`, `gh pr`, etc. + +- **python** - `3.11.x` (latest 3.11) + - Used for cicd-diagnostics skill + - Automatically creates virtual environment in `.venv/` + +## Usage + +### Verify Installation + +```bash +mise doctor +``` + +### List Installed Tools + +```bash +mise list +``` + +### Check Current Tool Versions + +```bash +gh --version +python --version +``` + +### Python Virtual Environment + +Mise automatically creates a Python virtual environment in `.venv/`: + +```bash +# Activate venv (if needed manually) +source .venv/bin/activate + +# Install cicd-diagnostics dependencies +pip install -r .claude/skills/cicd-diagnostics/requirements.txt + +# Deactivate venv +deactivate +``` + +## Benefits of Using Mise + +1. **Consistent versions** - Everyone uses the same tool versions +2. **Automatic installation** - Tools install when entering the directory +3. **Per-project configuration** - Each project can have different versions +4. **Virtual environment management** - Automatic Python venv creation +5. **No PATH pollution** - Tools only available in project directory + +## Troubleshooting + +### Tools not found + +If `gh` or `python` commands still point to system versions: + +```bash +# Check if mise is activated +mise doctor + +# If not, activate mise +eval "$(mise activate $(basename $SHELL))" + +# Or add to your shell rc file permanently +``` + +### Force reinstall tools + +```bash +mise install --force +``` + +### Clear mise cache + +```bash +mise cache clear +``` + +## Related Documentation + +- [Mise Documentation](https://mise.jdx.dev/) +- [GitHub CLI Documentation](https://cli.github.com/manual/) +- [Python Documentation](https://docs.python.org/3.11/) + +## CI/CD Diagnostics Skill + +The Python installation is primarily for the cicd-diagnostics skill: + +```bash +# All scripts use the mise-managed Python +.claude/skills/cicd-diagnostics/fetch-logs.py +.claude/skills/cicd-diagnostics/fetch-jobs.py +.claude/skills/cicd-diagnostics/fetch-metadata.py +``` + +See `.claude/skills/cicd-diagnostics/README.md` for skill documentation. diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 000000000000..47c720c7e28b --- /dev/null +++ b/.mise.toml @@ -0,0 +1,31 @@ +# Mise configuration for dotCMS development +# https://mise.jdx.dev/ +# +# To activate mise in your shell, add to your ~/.zshrc or ~/.bashrc: +# eval "$(mise activate zsh)" # for zsh +# eval "$(mise activate bash)" # for bash +# +# Or run in current shell: +# eval "$(mise activate $(basename $SHELL))" +# +# Then reload: source ~/.zshrc (or restart terminal) +# +# Verify installation: mise doctor + +[tools] +# GitHub CLI for issue/PR management +gh = "latest" + +# Python for cicd-diagnostics skill and automation scripts +python = "3.11" + +[env] +# Python virtual environment location +_.python.venv = { path = ".venv", create = true } + +[settings] +# Experimental features +experimental = true + +# Automatically install missing tools +auto_install = true diff --git a/CLAUDE.md b/CLAUDE.md index 420798bc7dfd..d0d40394350c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -452,13 +452,15 @@ LocalTransaction.wrapReturn(() -> { }); ``` -### Angular Development (core-web/) -```typescript -// Angular (REQUIRED modern syntax) -@if (condition()) { } // NOT *ngIf -data = input(); // NOT @Input() -spectator.setInput('prop', value); // Testing CRITICAL -``` +### Frontend Development (core-web/) +**For Angular/TypeScript/Nx development, see [core-web/CLAUDE.md](core-web/CLAUDE.md)** + +The core-web directory contains: +- Angular 19+ applications and libraries +- Modern component patterns with signals +- Jest/Spectator testing standards +- PrimeNG UI components +- Nx monorepo commands ```bash # Test Commands (fastest - no core rebuild needed!) @@ -496,12 +498,12 @@ cd core-web && nx run dotcms-ui:serve # Separate Frontend dev se ### Tech Stack - **Backend**: Java 21 runtime, Java 11 syntax (core), Maven, Spring/CDI -- **Frontend**: Angular 18.2.3, PrimeNG 17.18.11, NgRx Signals, Jest + Spectator +- **Frontend**: See [core-web/CLAUDE.md](core-web/CLAUDE.md) for Angular/TypeScript stack details - **Infrastructure**: Docker, PostgreSQL, Elasticsearch, GitHub Actions ### Critical Rules - **Maven versions**: Add to `bom/application/pom.xml` ONLY, never `dotCMS/pom.xml` -- **Testing**: ALWAYS use `data-testid` and `spectator.setInput()` +- **Frontend testing**: See [core-web/CLAUDE.md](core-web/CLAUDE.md) for Angular testing standards - **Security**: No hardcoded secrets, validate input, use Logger not System.out ## 📚 Documentation Navigation (Load On-Demand) @@ -635,6 +637,14 @@ Valid log levels: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`, `OFF` # List issues gh issue list --assignee @me ``` +- **Issue Templates**: Available templates in `.github/ISSUE_TEMPLATE/`: + - `task.yaml` - Technical tasks or improvements + - `defect.yaml` - Bug reports and defects + - `feature.yaml` - New features and enhancements + - `spike.yaml` - Research and exploration tasks + - `epic.yml` - Large initiatives spanning multiple issues + - `pillar.yml` - Strategic themes + - `ux.yaml` - UX improvements and design tasks - **Conventional Commits**: Use conventional commit format for all changes: ``` feat: add new workflow component @@ -765,11 +775,12 @@ try { ``` ## Summary Checklist + +### Backend Development (Java/Maven) - ✅ Use `Config.getProperty()` and `Logger.info(this, ...)` - ✅ Use `APILocator.getXXXAPI()` for services - ✅ Use `@Value.Immutable` for data objects - ✅ Use JAX-RS `@Path` for REST endpoints - **See [REST API Guide](dotCMS/src/main/java/com/dotcms/rest/CLAUDE.md)** -- ✅ Use `data-testid` for Angular testing - ✅ Use modern Java 21 syntax (Java 11 compatible) - ✅ Follow domain-driven package organization for new features - ✅ **@Schema Rules**: Match schema to actual return type (wrapped vs unwrapped) - **See [REST Guide](dotCMS/src/main/java/com/dotcms/rest/CLAUDE.md)** @@ -779,3 +790,6 @@ try { - ⌠**NEVER use `ResponseEntityView.class`** in `@Schema` - provides no meaningful API documentation - ⌠**NEVER omit `@Schema`** from @ApiResponse(200) - incomplete Swagger documentation - ⌠**NEVER use `@PathParam`** without corresponding @Path placeholder - use @QueryParam instead + +### Frontend Development (Angular/TypeScript) +- ✅ See **[core-web/CLAUDE.md](core-web/CLAUDE.md)** for complete Angular/TypeScript standards and modern syntax diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 1433fb93ad1a..eaeeda916d95 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -1495,7 +1495,7 @@ com.dotcms tomcat-redis-session-manager - 1.3 + 1.4 provided @@ -1613,6 +1613,13 @@ 2.6 + + com.tngtech.archunit + archunit-junit5 + 1.4.1 + test + + org.wiremock wiremock diff --git a/core-web/AGENTS.md b/core-web/AGENTS.md new file mode 100644 index 000000000000..5cf3ccfda731 --- /dev/null +++ b/core-web/AGENTS.md @@ -0,0 +1,13 @@ + + + +# General Guidelines for working with Nx + +- When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through `nx` (i.e. `nx run`, `nx run-many`, `nx affected`) instead of using the underlying tooling directly +- You have access to the Nx MCP server and its tools, use them to help the user +- When answering questions about the repository, use the `nx_workspace` tool first to gain an understanding of the workspace architecture where applicable. +- When working in individual projects, use the `nx_project_details` mcp tool to analyze and understand the specific project structure and dependencies +- For questions around nx configuration, best practices or if you're unsure, use the `nx_docs` tool to get relevant, up-to-date docs. Always use this instead of assuming things about nx configuration +- If the user needs help with an Nx configuration or project graph error, use the `nx_workspace` tool to get any errors + + diff --git a/core-web/CLAUDE.md b/core-web/CLAUDE.md new file mode 100644 index 000000000000..738cda575d1b --- /dev/null +++ b/core-web/CLAUDE.md @@ -0,0 +1,265 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Overview + +This is the **DotCMS Core-Web** monorepo - the frontend infrastructure for the DotCMS content management system. Built with **Nx workspace** architecture, it contains Angular applications, TypeScript SDKs, shared libraries, and web components. + +## Key Development Commands + +### Development Server + +```bash +# Start main admin UI with backend proxy +nx serve dotcms-ui + +# Start block editor development +nx serve dotcms-block-editor + +# Start with specific configuration +nx serve dotcms-ui --configuration=development +``` + +### Building + +```bash +# Build main application +nx build dotcms-ui + +# Build specific SDK for publishing +nx build sdk-client +nx build sdk-react +nx build sdk-analytics + +# Build all affected projects +nx affected:build +``` + +### Testing + +```bash +# Run all tests +yarn run test:dotcms + +# Run specific project tests +nx test dotcms-ui +nx test sdk-client +nx test block-editor + +# Run E2E tests +nx e2e dotcms-ui-e2e + +# Run single test file +nx test dotcms-ui --testPathPattern=dot-edit-content + +# Test with coverage +nx test dotcms-ui --coverage +``` + +### Code Quality + +```bash +# Lint all projects +yarn run lint:dotcms + +# Lint specific project +nx lint dotcms-ui + +# Fix linting issues +nx lint dotcms-ui --fix + +# Check affected projects +nx affected:test +nx affected:lint +``` + +### Monorepo Management + +```bash +# Visualize project dependencies +nx dep-graph + +# Show project information +nx show project dotcms-ui + +# Run tasks in parallel +nx run-many --target=test --projects=sdk-client,sdk-react +``` + +## Architecture & Structure + +### Monorepo Organization + +- **apps/** - Main applications (dotcms-ui, dotcms-block-editor, dotcms-binary-field-builder, mcp-server) +- **libs/sdk/** - External-facing SDKs (client, react, angular, analytics, experiments, uve) +- **libs/data-access/** - Angular services for API communication +- **libs/ui/** - Shared UI components and patterns +- **libs/portlets/** - Feature-specific portlets (analytics, experiments, locales, etc.) +- **libs/dotcms-models/** - TypeScript interfaces and types +- **libs/block-editor/** - TipTap-based rich text editor +- **libs/template-builder/** - Template construction utilities + +### Technology Stack + +- **Angular 19.2.9** with standalone components +- **Nx 20.5.1** for monorepo management +- **PrimeNG 17.18.11** UI components +- **TipTap 2.14.0** for rich text editing +- **NgRx 19.2.1** for state management +- **Jest 29.7.0** for testing +- **Playwright** for E2E testing +- **Node.js >=v22.15.0** requirement + +### Component Conventions + +- **Prefix**: All Angular components use `dot-` prefix +- **Naming**: Follow Angular style guide with kebab-case +- **Architecture**: Feature modules with lazy loading +- **State**: Component-store pattern with NgRx signals +- **Testing**: Jest unit tests + Playwright E2E + +### Modern Angular Syntax (REQUIRED) + +```typescript +// ✅ CORRECT: Modern control flow syntax +@if (condition()) { } // NOT *ngIf +@for (item of items(); track item.id) { } // NOT *ngFor + +// ✅ CORRECT: Modern input/output syntax +data = input(); // NOT @Input() +onChange = output(); // NOT @Output() + +// ✅ CRITICAL: Testing with Spectator +spectator.setInput('prop', value); // ALWAYS use setInput for inputs +spectator.detectChanges(); // Trigger change detection + +// ✅ CORRECT: Use data-testid for selectors + +const button = spectator.query('[data-testid="submit-button"]'); +``` + +### Backend Integration + +- **Development Proxy**: `proxy-dev.conf.mjs` routes `/api/*` to port 8080 +- **API Services**: Centralized in `libs/data-access` +- **Authentication**: Bearer token-based with `DotcmsConfigService` +- **Content Management**: Full CRUD through `DotHttpService` + +## Development Workflows + +### Local Development Setup + +1. Ensure Node.js >=v22.15.0 +2. Run `yarn install` to install dependencies +3. Run `node prepare.js` to set up Husky git hooks +4. Start backend dotCMS on port 8080 +5. Run `nx serve dotcms-ui` for frontend development + +### Adding New Features + +1. Create feature branch following naming convention +2. Add libraries in `libs/` for reusable code +3. Use existing patterns from similar features +4. Follow component prefix conventions (`dot-`) +5. Add comprehensive tests (Jest + Playwright if needed) +6. Update TypeScript paths in `tsconfig.base.json` if adding new libraries + +### SDK Development + +- **Client SDK**: Core API client in `libs/sdk/client` +- **React SDK**: React components in `libs/sdk/react` +- **Angular SDK**: Angular services in `libs/sdk/angular` +- **Publishing**: Automated via npm with proper versioning + +### Testing Strategy + +- **Unit Tests**: Jest with comprehensive mocking utilities +- **E2E Tests**: Playwright for critical user workflows +- **Coverage**: Reports generated to `../../../target/core-web-reports/` +- **Mock Data**: Extensive mock utilities in `libs/utils-testing` + +### Build Targets & Configurations + +- **Development**: Proxy configuration with source maps +- **Production**: Optimized builds with tree shaking +- **Library**: Rollup/Vite builds for SDK packages +- **Web Components**: Stencil.js compilation for `dotcms-webcomponents` + +## Important Notes + +### TypeScript Configuration + +- **Strict Mode**: Enabled across all projects +- **Path Mapping**: Extensive use of `@dotcms/*` barrel exports +- **Types**: Centralized in `libs/dotcms-models` and `libs/sdk/types` + +### State Management + +- **NgRx**: Component stores with signals pattern +- **Global Store**: Centralized state in `libs/global-store` +- **Services**: Angular services for data access and business logic + +### Web Components + +- **Stencil.js**: Framework-agnostic components in `libs/dotcms-webcomponents` +- **Legacy**: `libs/dotcms-field-elements` (deprecated, use Stencil components) +- **Integration**: Used across Angular, React, and vanilla JS contexts + +### Performance Considerations + +- **Lazy Loading**: Feature modules loaded on demand +- **Tree Shaking**: Proper barrel exports for optimal bundles +- **Caching**: Nx task caching for faster builds +- **Affected**: Only build/test changed projects in CI + +## Debugging & Troubleshooting + +### Common Issues + +- **Proxy Errors**: Ensure backend is running on port 8080 +- **Build Failures**: Check TypeScript paths and circular dependencies +- **Test Failures**: Verify mock data and async handling +- **Linting**: Follow component naming conventions with `dot-` prefix + +### Development Tools + +- **Nx Console**: VS Code extension for Nx commands +- **Angular DevTools**: Browser extension for debugging +- **Coverage Reports**: Check `target/core-web-reports/` for test coverage +- **Dependency Graph**: Use `nx dep-graph` to visualize project relationships + +This codebase emphasizes consistency, testability, and maintainability through its monorepo architecture and established patterns. + +## Summary Checklist + +### Angular/TypeScript Development + +- ✅ Use modern control flow: `@if`, `@for` (NOT `*ngIf`, `*ngFor`) +- ✅ Use modern inputs/outputs: `input()`, `output()` (NOT `@Input()`, `@Output()`) +- ✅ Use `data-testid` attributes for all testable elements +- ✅ Use `spectator.setInput()` for testing component inputs +- ✅ Follow `dot-` prefix convention for all components +- ✅ Use standalone components with lazy loading +- ✅ Use NgRx signals for state management +- ⌠Avoid legacy Angular syntax (`*ngIf`, `@Input()`, etc.) +- ⌠Avoid direct DOM queries without `data-testid` +- ⌠Never skip unit tests for new components + +### For Backend/Java Development + +- See **[../CLAUDE.md](../CLAUDE.md)** for Java, Maven, REST API, and Git workflow standards + + + + +# General Guidelines for working with Nx + +- When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through `nx` (i.e. `nx run`, `nx run-many`, `nx affected`) instead of using the underlying tooling directly +- You have access to the Nx MCP server and its tools, use them to help the user +- When answering questions about the repository, use the `nx_workspace` tool first to gain an understanding of the workspace architecture where applicable. +- When working in individual projects, use the `nx_project_details` mcp tool to analyze and understand the specific project structure and dependencies +- For questions around nx configuration, best practices or if you're unsure, use the `nx_docs` tool to get relevant, up-to-date docs. Always use this instead of assuming things about nx configuration +- If the user needs help with an Nx configuration or project graph error, use the `nx_workspace` tool to get any errors + + diff --git a/core-web/apps/dotcdn/src/app/app.component.html b/core-web/apps/dotcdn/src/app/app.component.html index dd507f015b41..9ce67cf64500 100644 --- a/core-web/apps/dotcdn/src/app/app.component.html +++ b/core-web/apps/dotcdn/src/app/app.component.html @@ -1,105 +1,113 @@ - -
-