From f6ad8c75c3c8a860e60350bb7496f0b99a716429 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 25 Oct 2026 10:28:44 -0400 Subject: [PATCH 01/11] Migrate to Jackson 3 with compatible mapper defaults --- README.md | 8 +++ custom-runtime/pom.xml | 8 +++ .../networknt/aws/lambda/DefaultResponse.java | 4 +- .../aws/lambda/InvocationResponse.java | 4 +- .../com/networknt/aws/lambda/Runtime.java | 10 +-- .../Jackson3RuntimeCompatibilityTest.java | 64 +++++++++++++++++++ env-config/pom.xml | 7 +- .../aws/lambda/ConfigurationTest.java | 6 +- lambda-interceptor/pom.xml | 6 +- .../aws/lambda/LightLambdaInterceptor.java | 6 +- lambda-invoker/pom.xml | 2 +- .../aws/lambda/LambdaInvokerConfig.java | 2 +- lambda-validator/pom.xml | 2 +- .../aws/lambda/LambdaSchemaValidator.java | 4 +- ...ackson3LambdaValidationRegressionTest.java | 44 +++++++++++++ .../aws/lambda/LambdaSchemaValidatorTest.java | 6 +- pom.xml | 6 +- request-handler/pom.xml | 2 +- schema-validator/pom.xml | 2 +- .../lambda/validator/RequestValidator.java | 12 ++-- .../aws/lambda/validator/SchemaValidator.java | 4 +- .../lambda/validator/SchemaValidatorTest.java | 20 +++--- scope-verifier/pom.xml | 2 +- .../aws/lambda/ScopeVerifierTest.java | 6 +- 24 files changed, 191 insertions(+), 46 deletions(-) create mode 100644 custom-runtime/src/test/java/com/networknt/aws/lambda/Jackson3RuntimeCompatibilityTest.java create mode 100644 lambda-validator/src/test/java/com/networknt/aws/lambda/Jackson3LambdaValidationRegressionTest.java diff --git a/README.md b/README.md index 1596b086..42d24980 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,11 @@ This is a Lambda extension that will collect the metrics of the Lambda function `env-config` uses SnakeYAML Engine with YAML 1.2 Core resolution. See the [configuration migration notes](env-config/README.md) for scalar changes, duplicate-key handling, and dependency compatibility before rollout. + +## Jackson 3 migration + +This source line requires Jackson 3.2.3, json-schema-validator 3.0.8 and the Jackson 3 light-4j/http-client prerequisites. Public mapper and JsonNode types use tools.jackson; annotations retain com.fasterxml.jackson.annotation. Build those prerequisites before this reactor. Do not mix published Jackson 2 snapshot binaries with this source line. + +Mappers use the shared Jackson2Defaults profile and preserve existing per-mapper overrides. Mappers are immutable; Java time and Optional support are built into Jackson 3. Classic SnakeYAML compatibility pins remain until the stack-wide cleanup. + +Local qualification covers JVM tests and sanitized API Gateway/event and validation parity. It does not qualify deployed AWS execution or GraalVM native images. diff --git a/custom-runtime/pom.xml b/custom-runtime/pom.xml index 4c1f60a2..5f2a82ce 100644 --- a/custom-runtime/pom.xml +++ b/custom-runtime/pom.xml @@ -31,6 +31,14 @@ A custom runtime for GraalVM native image to improve code start performance + + tools.jackson.core + jackson-databind + + + com.networknt + config + com.networknt utility diff --git a/custom-runtime/src/main/java/com/networknt/aws/lambda/DefaultResponse.java b/custom-runtime/src/main/java/com/networknt/aws/lambda/DefaultResponse.java index df11e128..5389aa7b 100644 --- a/custom-runtime/src/main/java/com/networknt/aws/lambda/DefaultResponse.java +++ b/custom-runtime/src/main/java/com/networknt/aws/lambda/DefaultResponse.java @@ -1,7 +1,7 @@ package com.networknt.aws.lambda; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonPOJOBuilder; @JsonDeserialize(builder = DefaultResponse.Builder.class) public class DefaultResponse { diff --git a/custom-runtime/src/main/java/com/networknt/aws/lambda/InvocationResponse.java b/custom-runtime/src/main/java/com/networknt/aws/lambda/InvocationResponse.java index b3464d39..bcdccc24 100644 --- a/custom-runtime/src/main/java/com/networknt/aws/lambda/InvocationResponse.java +++ b/custom-runtime/src/main/java/com/networknt/aws/lambda/InvocationResponse.java @@ -1,8 +1,8 @@ package com.networknt.aws.lambda; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonPOJOBuilder; @JsonDeserialize(builder = InvocationResponse.Builder.class) public class InvocationResponse { diff --git a/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java b/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java index b111f618..7373bf2f 100644 --- a/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java +++ b/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java @@ -1,10 +1,12 @@ package com.networknt.aws.lambda; +import com.networknt.config.Jackson2Defaults; + import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; import java.io.IOException; import java.lang.reflect.Method; @@ -18,8 +20,8 @@ public class Runtime { private static final String REQUEST_ID_HEADER = "lambda-runtime-aws-request-id"; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + private static final ObjectMapper OBJECT_MAPPER = Jackson2Defaults.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build(); public static void main(String[] args) throws IOException { while(true) { diff --git a/custom-runtime/src/test/java/com/networknt/aws/lambda/Jackson3RuntimeCompatibilityTest.java b/custom-runtime/src/test/java/com/networknt/aws/lambda/Jackson3RuntimeCompatibilityTest.java new file mode 100644 index 00000000..387d202a --- /dev/null +++ b/custom-runtime/src/test/java/com/networknt/aws/lambda/Jackson3RuntimeCompatibilityTest.java @@ -0,0 +1,64 @@ +package com.networknt.aws.lambda; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.ObjectMapper; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class Jackson3RuntimeCompatibilityTest { + private ObjectMapper runtimeMapper() throws Exception { + var field = Runtime.class.getDeclaredField("OBJECT_MAPPER"); + field.setAccessible(true); + return (ObjectMapper) field.get(null); + } + + @Test + void bindsAwsGatewayEventsWithoutJackson2Types() throws Exception { + var mapper = runtimeMapper(); + var request = mapper.readValue("{\"path\":\"/pets\",\"httpMethod\":\"POST\"," + + "\"body\":\"雪\",\"isBase64Encoded\":true,\"requestContext\":{\"requestId\":\"fixture\"}," + + "\"multiValueHeaders\":{\"X-Test\":[\"one\",\"two\"]}}", APIGatewayProxyRequestEvent.class); + assertEquals("/pets", request.getPath()); + assertEquals("fixture", request.getRequestContext().getRequestId()); + assertEquals(List.of("one", "two"), request.getMultiValueHeaders().get("X-Test")); + assertTrue(request.getIsBase64Encoded()); + assertEquals(request, mapper.readValue(mapper.writeValueAsString(request), APIGatewayProxyRequestEvent.class)); + } + + @Test + void preservesTheMeasuredResponseWireShape() throws Exception { + var response = new APIGatewayProxyResponseEvent().withStatusCode(201).withBody("雪\nbody") + .withIsBase64Encoded(true).withHeaders(Map.of("Content-Type", "application/json")) + .withMultiValueHeaders(Map.of("Set-Cookie", List.of("a=1", "b=2"))); + assertEquals("{\"statusCode\":201,\"headers\":{\"Content-Type\":\"application/json\"}," + + "\"multiValueHeaders\":{\"Set-Cookie\":[\"a=1\",\"b=2\"]},\"body\":\"雪\\nbody\"," + + "\"isBase64Encoded\":true}", runtimeMapper().writeValueAsString(response)); + } + + @Test + void retainsBuilderBindingAndNullPrimitiveHandling() throws Exception { + var mapper = runtimeMapper(); + var response = mapper.readValue("{\"code\":null,\"message\":\"fixture error\"}", DefaultResponse.class); + assertEquals(0, response.getCode()); + assertEquals("{\"code\":0,\"message\":\"fixture error\"}", mapper.writeValueAsString(response)); + var invocation = mapper.readValue("{\"requestId\":\"fixture\",\"event\":{\"path\":\"/pets\"}}", InvocationResponse.class); + assertEquals("fixture", invocation.getRequestId()); + assertEquals("/pets", invocation.getEvent().getPath()); + } + + @Test + void retainsPermissiveRuntimeBinding() throws Exception { + var mapper = runtimeMapper(); + var request = mapper.readValue("{\"futureField\":1,\"isBase64Encoded\":false} {}", APIGatewayProxyRequestEvent.class); + assertNull(request.getPath()); + assertFalse(request.getIsBase64Encoded()); + } +} diff --git a/env-config/pom.xml b/env-config/pom.xml index 5bda6212..6fea8960 100644 --- a/env-config/pom.xml +++ b/env-config/pom.xml @@ -31,6 +31,11 @@ Environmental Config for Lambda for seldom changed properties + + com.networknt + config + test + com.amazonaws aws-lambda-java-core @@ -40,7 +45,7 @@ aws-lambda-java-events - com.fasterxml.jackson.core + tools.jackson.core jackson-databind diff --git a/env-config/src/test/java/com/networknt/aws/lambda/ConfigurationTest.java b/env-config/src/test/java/com/networknt/aws/lambda/ConfigurationTest.java index a3c6f346..b3793898 100644 --- a/env-config/src/test/java/com/networknt/aws/lambda/ConfigurationTest.java +++ b/env-config/src/test/java/com/networknt/aws/lambda/ConfigurationTest.java @@ -1,7 +1,9 @@ package com.networknt.aws.lambda; +import com.networknt.config.Jackson2Defaults; + import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -9,7 +11,7 @@ import java.util.Map; public class ConfigurationTest { - ObjectMapper objectMapper = new ObjectMapper(); + ObjectMapper objectMapper = Jackson2Defaults.mapper(); @Test @Disabled public void thatConfigurationValuesAreLoaded() throws Exception { diff --git a/lambda-interceptor/pom.xml b/lambda-interceptor/pom.xml index 05306a2c..77fd32fb 100644 --- a/lambda-interceptor/pom.xml +++ b/lambda-interceptor/pom.xml @@ -31,6 +31,10 @@ The interceptor to pre-process request and post-process response when light-proxy is used + + com.networknt + config + com.networknt slf4j-logback @@ -44,7 +48,7 @@ aws-lambda-java-events - com.fasterxml.jackson.core + tools.jackson.core jackson-databind diff --git a/lambda-interceptor/src/main/java/com/networknt/aws/lambda/LightLambdaInterceptor.java b/lambda-interceptor/src/main/java/com/networknt/aws/lambda/LightLambdaInterceptor.java index 70be294d..352b897e 100644 --- a/lambda-interceptor/src/main/java/com/networknt/aws/lambda/LightLambdaInterceptor.java +++ b/lambda-interceptor/src/main/java/com/networknt/aws/lambda/LightLambdaInterceptor.java @@ -1,15 +1,17 @@ package com.networknt.aws.lambda; +import com.networknt.config.Jackson2Defaults; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.LambdaLogger; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LightLambdaInterceptor { - private static ObjectMapper objectMapper = new ObjectMapper(); + private static ObjectMapper objectMapper = Jackson2Defaults.mapper(); private static final Logger logger = LoggerFactory.getLogger(LightLambdaInterceptor.class); public APIGatewayProxyRequestEvent interceptRequest(final APIGatewayProxyRequestEvent input, final Context context) { diff --git a/lambda-invoker/pom.xml b/lambda-invoker/pom.xml index 5ea049cc..76be8a04 100644 --- a/lambda-invoker/pom.xml +++ b/lambda-invoker/pom.xml @@ -77,7 +77,7 @@ sts - com.fasterxml.jackson.core + tools.jackson.core jackson-databind diff --git a/lambda-invoker/src/main/java/com/networknt/aws/lambda/LambdaInvokerConfig.java b/lambda-invoker/src/main/java/com/networknt/aws/lambda/LambdaInvokerConfig.java index f10a5819..d2f39cd0 100644 --- a/lambda-invoker/src/main/java/com/networknt/aws/lambda/LambdaInvokerConfig.java +++ b/lambda-invoker/src/main/java/com/networknt/aws/lambda/LambdaInvokerConfig.java @@ -1,6 +1,6 @@ package com.networknt.aws.lambda; -import com.fasterxml.jackson.core.type.TypeReference; +import tools.jackson.core.type.TypeReference; import com.networknt.config.Config; import com.networknt.config.ConfigException; import com.networknt.config.schema.ConfigSchema; diff --git a/lambda-validator/pom.xml b/lambda-validator/pom.xml index bebc2819..6b7c7d68 100644 --- a/lambda-validator/pom.xml +++ b/lambda-validator/pom.xml @@ -60,7 +60,7 @@ aws-lambda-java-events - com.fasterxml.jackson.core + tools.jackson.core jackson-databind diff --git a/lambda-validator/src/main/java/com/networknt/aws/lambda/LambdaSchemaValidator.java b/lambda-validator/src/main/java/com/networknt/aws/lambda/LambdaSchemaValidator.java index 0f341a91..f9274e23 100644 --- a/lambda-validator/src/main/java/com/networknt/aws/lambda/LambdaSchemaValidator.java +++ b/lambda-validator/src/main/java/com/networknt/aws/lambda/LambdaSchemaValidator.java @@ -2,7 +2,7 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.fasterxml.jackson.core.JsonProcessingException; +import tools.jackson.core.JacksonException; import com.networknt.aws.lambda.validator.RequestValidator; import com.networknt.aws.lambda.validator.SchemaValidator; import com.networknt.config.Config; @@ -48,7 +48,7 @@ public LambdaSchemaValidator() { String openapiString = Config.getInstance().getMapper().writeValueAsString(openapi); if(logger.isTraceEnabled()) logger.trace("OpenApiMiddleware openapiString: " + openapiString); helper = new OpenApiHelper(openapiString); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { logger.error("merge specification failed"); throw new RuntimeException("merge specification failed"); } diff --git a/lambda-validator/src/test/java/com/networknt/aws/lambda/Jackson3LambdaValidationRegressionTest.java b/lambda-validator/src/test/java/com/networknt/aws/lambda/Jackson3LambdaValidationRegressionTest.java new file mode 100644 index 00000000..6db1cf7b --- /dev/null +++ b/lambda-validator/src/test/java/com/networknt/aws/lambda/Jackson3LambdaValidationRegressionTest.java @@ -0,0 +1,44 @@ +package com.networknt.aws.lambda; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.networknt.config.Config; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class Jackson3LambdaValidationRegressionTest { + @Test + void validatesAnEventBoundByTheFrameworkMapper() { + var request = Config.getInstance().getMapper().readValue( + "{\"path\":\"/pets\",\"httpMethod\":\"GET\",\"queryStringParameters\":{\"includeCode\":\"true\"}}", + APIGatewayProxyRequestEvent.class); + assertNull(new LambdaSchemaValidator().validateRequest(request)); + } + + @Test + void keepsMalformedBodyErrorsHandled() { + var request = new APIGatewayProxyRequestEvent().withPath("/pets").withHttpMethod("POST") + .withHeaders(Map.of("Content-Type", "application/json")).withBody("{"); + var response = new LambdaSchemaValidator().validateRequest(request); + assertNotNull(response); + assertEquals(400, response.getStatusCode()); + assertTrue(response.getBody().contains("ERR10015"), response.getBody()); + } + + @Test + void keepsValidationMessagesAndMethodErrorsStable() { + var validator = new LambdaSchemaValidator(); + var request = new APIGatewayProxyRequestEvent().withPath("/pets").withHttpMethod("GET") + .withQueryStringParameters(Map.of("includeCode", "wrong")); + var response = validator.validateRequest(request); + assertEquals(400, response.getStatusCode()); + assertTrue(response.getBody().contains("/includeCode: string found, boolean expected"), response.getBody()); + request.setHttpMethod("PATCH"); + assertEquals(405, validator.validateRequest(request).getStatusCode()); + } +} diff --git a/lambda-validator/src/test/java/com/networknt/aws/lambda/LambdaSchemaValidatorTest.java b/lambda-validator/src/test/java/com/networknt/aws/lambda/LambdaSchemaValidatorTest.java index bf590f3e..c02d08e0 100644 --- a/lambda-validator/src/test/java/com/networknt/aws/lambda/LambdaSchemaValidatorTest.java +++ b/lambda-validator/src/test/java/com/networknt/aws/lambda/LambdaSchemaValidatorTest.java @@ -1,9 +1,11 @@ package com.networknt.aws.lambda; +import com.networknt.config.Jackson2Defaults; + import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -13,7 +15,7 @@ import java.util.Map; public class LambdaSchemaValidatorTest { - ObjectMapper objectMapper = new ObjectMapper(); + ObjectMapper objectMapper = Jackson2Defaults.mapper(); private final LambdaSchemaValidator validator = new LambdaSchemaValidator(); APIGatewayProxyRequestEvent requestEvent; diff --git a/pom.xml b/pom.xml index cc7bd09f..ce8b4aac 100644 --- a/pom.xml +++ b/pom.xml @@ -99,7 +99,7 @@ ${java.version} UTF-8 2.4.2-SNAPSHOT - 2.22.3 + 3.2.3 2.0.20 1.4.0 3.16.1 @@ -116,7 +116,7 @@ 2.4.4.Final 3.27.7 2.23.0 - 2.0.8 + 3.0.8 3.12.0 1.7.0 3.6.0 @@ -147,7 +147,7 @@ import - com.fasterxml.jackson + tools.jackson jackson-bom ${version.jackson} pom diff --git a/request-handler/pom.xml b/request-handler/pom.xml index 77f13f78..ea81c974 100644 --- a/request-handler/pom.xml +++ b/request-handler/pom.xml @@ -52,7 +52,7 @@ aws-lambda-java-events - com.fasterxml.jackson.core + tools.jackson.core jackson-databind diff --git a/schema-validator/pom.xml b/schema-validator/pom.xml index 83e0bc1a..30a73650 100644 --- a/schema-validator/pom.xml +++ b/schema-validator/pom.xml @@ -60,7 +60,7 @@ aws-lambda-java-events - com.fasterxml.jackson.core + tools.jackson.core jackson-databind diff --git a/schema-validator/src/main/java/com/networknt/aws/lambda/validator/RequestValidator.java b/schema-validator/src/main/java/com/networknt/aws/lambda/validator/RequestValidator.java index fc786188..f08202ef 100644 --- a/schema-validator/src/main/java/com/networknt/aws/lambda/validator/RequestValidator.java +++ b/schema-validator/src/main/java/com/networknt/aws/lambda/validator/RequestValidator.java @@ -1,8 +1,8 @@ package com.networknt.aws.lambda.validator; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.TextNode; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.StringNode; import com.networknt.config.Config; import com.networknt.jsonoverlay.Overlay; import com.networknt.oas.model.Parameter; @@ -144,7 +144,7 @@ private Status validatePathParameters(final APIGatewayProxyRequestEvent requestE LOG.info("Path parameter cannot be decoded, it will be used directly"); } - return schemaValidator.validate(new TextNode(paramValue), Overlay.toJson((SchemaImpl)(parameter.get().getSchema())), paramName); + return schemaValidator.validate(new StringNode(paramValue), Overlay.toJson((SchemaImpl)(parameter.get().getSchema())), paramName); } } return status; @@ -179,7 +179,7 @@ private Status validateQueryParameter(final APIGatewayProxyRequestEvent requestE return new Status(VALIDATOR_REQUEST_PARAMETER_QUERY_MISSING, queryParameter.getName(), openApiOperation.getPathString().original()); } } else { - return schemaValidator.validate(new TextNode(queryParameterValue), Overlay.toJson((SchemaImpl)queryParameter.getSchema()), queryParameter.getName()); + return schemaValidator.validate(new StringNode(queryParameterValue), Overlay.toJson((SchemaImpl)queryParameter.getSchema()), queryParameter.getName()); } return null; } @@ -233,7 +233,7 @@ private Status validateHeader(final APIGatewayProxyRequestEvent requestEvent, return new Status(VALIDATOR_REQUEST_PARAMETER_HEADER_MISSING, headerParameter.getName(), openApiOperation.getPathString().original()); } } else { - return headerParameter.getSchema() != null ? schemaValidator.validate(new TextNode(headerValue.get()), Overlay.toJson((SchemaImpl)headerParameter.getSchema()), headerParameter.getName()) : null; + return headerParameter.getSchema() != null ? schemaValidator.validate(new StringNode(headerValue.get()), Overlay.toJson((SchemaImpl)headerParameter.getSchema()), headerParameter.getName()) : null; } return null; } @@ -249,7 +249,7 @@ private ValidationResult validateDeserializedValues(final APIGatewayProxyRequest if (null==deserializedValue) { validationResult.addSkipped(p); }else { - Status s = schemaValidator.validate(new TextNode(deserializedValue), Overlay.toJson((SchemaImpl)(p.getSchema())), p.getName()); + Status s = schemaValidator.validate(new StringNode(deserializedValue), Overlay.toJson((SchemaImpl)(p.getSchema())), p.getName()); validationResult.addStatus(s); } }); diff --git a/schema-validator/src/main/java/com/networknt/aws/lambda/validator/SchemaValidator.java b/schema-validator/src/main/java/com/networknt/aws/lambda/validator/SchemaValidator.java index a5e45217..52ed6493 100644 --- a/schema-validator/src/main/java/com/networknt/aws/lambda/validator/SchemaValidator.java +++ b/schema-validator/src/main/java/com/networknt/aws/lambda/validator/SchemaValidator.java @@ -16,8 +16,8 @@ package com.networknt.aws.lambda.validator; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ObjectNode; import com.networknt.jsonoverlay.Overlay; import com.networknt.oas.model.OpenApi3; import com.networknt.oas.model.impl.OpenApi3Impl; diff --git a/schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaValidatorTest.java b/schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaValidatorTest.java index 33abbbcf..c30d4ab7 100644 --- a/schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaValidatorTest.java +++ b/schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaValidatorTest.java @@ -1,9 +1,11 @@ package com.networknt.aws.lambda.validator; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.NullNode; -import com.fasterxml.jackson.databind.node.TextNode; +import com.networknt.config.Jackson2Defaults; + +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.NullNode; +import tools.jackson.databind.node.StringNode; import com.networknt.status.Status; import org.junit.jupiter.api.Test; @@ -14,7 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class SchemaValidatorTest { - private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final ObjectMapper MAPPER = Jackson2Defaults.mapper(); @Test void shouldConstructWithoutAnApi() { @@ -26,8 +28,8 @@ void shouldKeepLooseAndStrictTypeProfilesSeparate() throws Exception { SchemaValidator validator = new SchemaValidator(); JsonNode schema = MAPPER.readTree("{\"type\":\"integer\"}"); - assertNull(validator.validate(new TextNode("42"), schema, true, false)); - assertNotNull(validator.validate(new TextNode("42"), schema, false, false)); + assertNull(validator.validate(new StringNode("42"), schema, true, false)); + assertNotNull(validator.validate(new StringNode("42"), schema, false, false)); } @Test @@ -56,7 +58,7 @@ void shouldAllowStandardNonDefaultDialects() throws Exception { SchemaValidator validator = new SchemaValidator(); JsonNode schema = MAPPER.readTree("{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"integer\"}"); - Status status = validator.validate(new TextNode("invalid"), schema, false, false); + Status status = validator.validate(new StringNode("invalid"), schema, false, false); assertNotNull(status); assertEquals(SchemaValidator.VALIDATOR_SCHEMA, status.getCode()); @@ -67,7 +69,7 @@ void shouldFailClosedWhenSchemaCannotBeBuilt() throws Exception { SchemaValidator validator = new SchemaValidator(); JsonNode schema = MAPPER.readTree("{\"type\":\"string\",\"pattern\":\"[\"}"); - Status status = validator.validate(new TextNode("value"), schema, false, false); + Status status = validator.validate(new StringNode("value"), schema, false, false); assertNotNull(status); assertEquals(SchemaValidator.VALIDATOR_SCHEMA_INVALID_JSON, status.getCode()); diff --git a/scope-verifier/pom.xml b/scope-verifier/pom.xml index f32f475a..578cbdb6 100644 --- a/scope-verifier/pom.xml +++ b/scope-verifier/pom.xml @@ -44,7 +44,7 @@ aws-lambda-java-events - com.fasterxml.jackson.core + tools.jackson.core jackson-databind diff --git a/scope-verifier/src/test/java/com/networknt/aws/lambda/ScopeVerifierTest.java b/scope-verifier/src/test/java/com/networknt/aws/lambda/ScopeVerifierTest.java index 8c902480..f0d0e93e 100644 --- a/scope-verifier/src/test/java/com/networknt/aws/lambda/ScopeVerifierTest.java +++ b/scope-verifier/src/test/java/com/networknt/aws/lambda/ScopeVerifierTest.java @@ -1,8 +1,10 @@ package com.networknt.aws.lambda; +import com.networknt.config.Jackson2Defaults; + import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import com.networknt.utility.StringUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -11,7 +13,7 @@ import java.util.List; public class ScopeVerifierTest { - ObjectMapper objectMapper = new ObjectMapper(); + ObjectMapper objectMapper = Jackson2Defaults.mapper(); @Test public void testSuccessRequest() { ScopeVerifier verifier = new ScopeVerifier(); From fb93372fe8ab5d47df9ef3498c62230edb3ac563 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 25 Oct 2026 12:40:44 -0400 Subject: [PATCH 02/11] Use lightweight shared Jackson policy and add source CI --- .github/workflows/jackson3-source-build.yml | 108 ++++++++++++++++++ README.md | 6 + custom-runtime/pom.xml | 2 +- .../com/networknt/aws/lambda/Runtime.java | 11 +- lambda-interceptor/pom.xml | 2 +- .../aws/lambda/LightLambdaInterceptor.java | 5 +- pom.xml | 5 + 7 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/jackson3-source-build.yml diff --git a/.github/workflows/jackson3-source-build.yml b/.github/workflows/jackson3-source-build.yml new file mode 100644 index 00000000..2254479b --- /dev/null +++ b/.github/workflows/jackson3-source-build.yml @@ -0,0 +1,108 @@ +name: Jackson 3 source build +on: + pull_request: + workflow_dispatch: + inputs: + source_refs: + description: JSON map of prerequisite repositories to full commit SHAs + type: string + default: '{}' + push: + branches: [master] +permissions: + contents: read +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + with: + path: consumer + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '25' + - name: Resolve prerequisite commits + id: prerequisites + env: + GH_TOKEN: ${{ github.token }} + SOURCE_REFS: ${{ inputs.source_refs || '{}' }} + shell: bash + run: | + jq -e 'type == "object" and all(to_entries[]; (.key | IN("yaml-rule", "http-client", "light-4j", "json-overlay", "openapi-parser")) and (.value | type == "string" and test("^[0-9a-f]{40}$")))' <<< "$SOURCE_REFS" + resolve() { + local repo=$1 pr=${2:-} ref + ref=$(jq -r --arg repo "$repo" '.[$repo] // empty' <<< "$SOURCE_REFS") + if [[ -z "$ref" ]]; then + if [[ -n "$pr" ]]; then + ref=$(gh api "repos/networknt/$repo/pulls/$pr" --jq 'if .merged then .base.ref elif .state == "open" then .head.sha else error("Prerequisite PR closed unmerged") end') + else + ref=master + fi + fi + ref=$(gh api "repos/networknt/$repo/commits/$ref" --jq .sha) + echo "${repo//-/_}=$ref" >> "$GITHUB_OUTPUT" + } + resolve yaml-rule + resolve http-client + resolve light-4j 2821 + resolve json-overlay + resolve openapi-parser + - uses: actions/checkout@v4 + with: + repository: networknt/yaml-rule + ref: ${{ steps.prerequisites.outputs.yaml_rule }} + path: sources/yaml-rule + - uses: actions/checkout@v4 + with: + repository: networknt/http-client + ref: ${{ steps.prerequisites.outputs.http_client }} + path: sources/http-client + - uses: actions/checkout@v4 + with: + repository: networknt/light-4j + ref: ${{ steps.prerequisites.outputs.light_4j }} + path: sources/light-4j + - uses: actions/checkout@v4 + with: + repository: networknt/json-overlay + ref: ${{ steps.prerequisites.outputs.json_overlay }} + path: sources/json-overlay + - uses: actions/checkout@v4 + with: + repository: networknt/openapi-parser + ref: ${{ steps.prerequisites.outputs.openapi_parser }} + path: sources/openapi-parser + - name: Record source commits + shell: bash + run: | + for source in consumer sources/*; do + echo "$source $(git -C "$source" rev-parse HEAD)" + done | tee source-commits.txt + for source in sources/*; do + printf '{"key":"%s","value":"%s"}\n' "${source##*/}" "$(git -C "$source" rev-parse HEAD)" + done | jq -s from_entries | tee source-refs.json + - name: Build prerequisites and run full tests + shell: bash + env: + JACKSON3_MAVEN_REPO: ${{ runner.temp }}/m2 + JACKSON3_YAML_RULE: ${{ github.workspace }}/sources/yaml-rule + JACKSON3_HTTP_CLIENT: ${{ github.workspace }}/sources/http-client + run: | + set -euo pipefail + bash sources/light-4j/scripts/verify-jackson3-bootstrap.sh 2>&1 | tee source-build.log + mvn -f sources/yaml-rule/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log + mvn -f sources/json-overlay/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log + mvn -f sources/openapi-parser/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log + mvn -f consumer/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log + - uses: actions/upload-artifact@v4 + if: always() + with: + name: source-build-reports + path: | + source-build.log + source-commits.txt + source-refs.json + **/target/surefire-reports/** + **/target/*smoke*.log diff --git a/README.md b/README.md index 42d24980..98edda2d 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,9 @@ This source line requires Jackson 3.2.3, json-schema-validator 3.0.8 and the Jac Mappers use the shared Jackson2Defaults profile and preserve existing per-mapper overrides. Mappers are immutable; Java time and Optional support are built into Jackson 3. Classic SnakeYAML compatibility pins remain until the stack-wide cleanup. Local qualification covers JVM tests and sanitized API Gateway/event and validation parity. It does not qualify deployed AWS execution or GraalVM native images. + +### Reproducible Jackson 3 source build + +`.github/workflows/jackson3-source-build.yml` builds the prerequisite source stack in a fresh isolated Maven repository and runs its full test suites. It resolves every prerequisite to a full commit SHA once and uploads `source-refs.json`, `source-commits.txt` and Surefire reports. The shared mapper policy prerequisite is light-4j #2821; merge that before its consumers. + +The optional `source_refs` workflow-dispatch input replays recorded full SHAs. GitHub enables dispatch only after the workflow exists on the default branch; before merge, reproduce from the recorded source checkouts locally. A green local suite is separate from a successful remote source-build run. diff --git a/custom-runtime/pom.xml b/custom-runtime/pom.xml index 5f2a82ce..962be8fe 100644 --- a/custom-runtime/pom.xml +++ b/custom-runtime/pom.xml @@ -37,7 +37,7 @@ com.networknt - config + jackson-defaults com.networknt diff --git a/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java b/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java index 7373bf2f..486fcd28 100644 --- a/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java +++ b/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java @@ -1,21 +1,18 @@ package com.networknt.aws.lambda; -import com.networknt.config.Jackson2Defaults; - import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import tools.jackson.databind.DeserializationFeature; -import tools.jackson.databind.ObjectMapper; - +import com.networknt.jackson.Jackson2Defaults; +import com.networknt.utility.NioUtils; import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.net.HttpURLConnection; - -import com.networknt.utility.NioUtils; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; public class Runtime { private static final String REQUEST_ID_HEADER = "lambda-runtime-aws-request-id"; diff --git a/lambda-interceptor/pom.xml b/lambda-interceptor/pom.xml index 77fd32fb..e86716e5 100644 --- a/lambda-interceptor/pom.xml +++ b/lambda-interceptor/pom.xml @@ -33,7 +33,7 @@ com.networknt - config + jackson-defaults com.networknt diff --git a/lambda-interceptor/src/main/java/com/networknt/aws/lambda/LightLambdaInterceptor.java b/lambda-interceptor/src/main/java/com/networknt/aws/lambda/LightLambdaInterceptor.java index 352b897e..36366080 100644 --- a/lambda-interceptor/src/main/java/com/networknt/aws/lambda/LightLambdaInterceptor.java +++ b/lambda-interceptor/src/main/java/com/networknt/aws/lambda/LightLambdaInterceptor.java @@ -1,14 +1,13 @@ package com.networknt.aws.lambda; -import com.networknt.config.Jackson2Defaults; - import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.LambdaLogger; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import tools.jackson.databind.ObjectMapper; +import com.networknt.jackson.Jackson2Defaults; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.databind.ObjectMapper; public class LightLambdaInterceptor { private static ObjectMapper objectMapper = Jackson2Defaults.mapper(); diff --git a/pom.xml b/pom.xml index ce8b4aac..43c13982 100644 --- a/pom.xml +++ b/pom.xml @@ -139,6 +139,11 @@ + + com.networknt + jackson-defaults + ${version.light-4j} + org.junit junit-bom From 5dde45e9ab8dee4484fb0ae1d1d4079029d36cbf Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 25 Oct 2026 12:54:12 -0400 Subject: [PATCH 03/11] Require source-installed snapshots in fresh-cache CI --- .github/workflows/jackson3-source-build.yml | 30 ++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/.github/workflows/jackson3-source-build.yml b/.github/workflows/jackson3-source-build.yml index 2254479b..b696d746 100644 --- a/.github/workflows/jackson3-source-build.yml +++ b/.github/workflows/jackson3-source-build.yml @@ -30,7 +30,7 @@ jobs: SOURCE_REFS: ${{ inputs.source_refs || '{}' }} shell: bash run: | - jq -e 'type == "object" and all(to_entries[]; (.key | IN("yaml-rule", "http-client", "light-4j", "json-overlay", "openapi-parser")) and (.value | type == "string" and test("^[0-9a-f]{40}$")))' <<< "$SOURCE_REFS" + jq -e 'type == "object" and all(to_entries[]; (.key | IN("yaml-rule", "http-client", "light-4j", "json-overlay", "openapi-parser", "light-rest-4j")) and (.value | type == "string" and test("^[0-9a-f]{40}$")))' <<< "$SOURCE_REFS" resolve() { local repo=$1 pr=${2:-} ref ref=$(jq -r --arg repo "$repo" '.[$repo] // empty' <<< "$SOURCE_REFS") @@ -49,6 +49,7 @@ jobs: resolve light-4j 2821 resolve json-overlay resolve openapi-parser + resolve light-rest-4j - uses: actions/checkout@v4 with: repository: networknt/yaml-rule @@ -74,6 +75,11 @@ jobs: repository: networknt/openapi-parser ref: ${{ steps.prerequisites.outputs.openapi_parser }} path: sources/openapi-parser + - uses: actions/checkout@v4 + with: + repository: networknt/light-rest-4j + ref: ${{ steps.prerequisites.outputs.light_rest_4j }} + path: sources/light-rest-4j - name: Record source commits shell: bash run: | @@ -95,7 +101,29 @@ jobs: mvn -f sources/yaml-rule/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log mvn -f sources/json-overlay/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log mvn -f sources/openapi-parser/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log + mvn -f sources/light-rest-4j/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log mvn -f consumer/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log + - name: Reject remotely resolved NetworkNT snapshots + env: + SOURCE_MAVEN_REPO: ${{ runner.temp }}/m2 + run: | + python3 - <<'PYTHON' + import os + from pathlib import Path + root = Path(os.environ['SOURCE_MAVEN_REPO']) / 'com/networknt' + failures = [] + versions = list(root.glob('*/*-SNAPSHOT')) + assert versions, 'No source-installed NetworkNT snapshots found' + for version in versions: + artifact = version.parent.name + '-' + version.name + extension = 'jar' if (version / (artifact + '.jar')).exists() else 'pom' + tracking = version / '_remote.repositories' + local_marker = artifact + '.' + extension + '>=' + if not tracking.exists() or local_marker not in tracking.read_text().splitlines(): + failures.append(str(version.relative_to(root))) + assert not failures, 'Snapshots not installed from source: ' + ', '.join(failures) + print('All resolved NetworkNT snapshots were installed from source') + PYTHON - uses: actions/upload-artifact@v4 if: always() with: From 668233069dc87aa302736193e1f5d9df46f09a8c Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 25 Oct 2026 13:01:28 -0400 Subject: [PATCH 04/11] Audit resolved reactor snapshots rather than unrelated cache entries --- .github/workflows/jackson3-source-build.yml | 38 +++++++++++++++------ 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/.github/workflows/jackson3-source-build.yml b/.github/workflows/jackson3-source-build.yml index b696d746..d60dc8b8 100644 --- a/.github/workflows/jackson3-source-build.yml +++ b/.github/workflows/jackson3-source-build.yml @@ -107,22 +107,39 @@ jobs: env: SOURCE_MAVEN_REPO: ${{ runner.temp }}/m2 run: | + set -euo pipefail + mvn -B -Dstyle.color=never -f consumer/pom.xml "-Dmaven.repo.local=$SOURCE_MAVEN_REPO" dependency:tree -Dincludes=com.networknt:* 2>&1 | tee resolved-networknt.txt python3 - <<'PYTHON' import os + import re from pathlib import Path root = Path(os.environ['SOURCE_MAVEN_REPO']) / 'com/networknt' failures = [] - versions = list(root.glob('*/*-SNAPSHOT')) - assert versions, 'No source-installed NetworkNT snapshots found' - for version in versions: - artifact = version.parent.name + '-' + version.name - extension = 'jar' if (version / (artifact + '.jar')).exists() else 'pom' - tracking = version / '_remote.repositories' - local_marker = artifact + '.' + extension + '>=' - if not tracking.exists() or local_marker not in tracking.read_text().splitlines(): - failures.append(str(version.relative_to(root))) + checked = set() + for line in Path('resolved-networknt.txt').read_text().splitlines(): + match = re.search(r'com[.]networknt:([^: ]+):([^ ]+)', line) + if not match: + continue + name, coordinates = match.groups() + parts = coordinates.split(':') + versions = [i for i, part in enumerate(parts) if part.endswith('-SNAPSHOT')] + if not versions: + continue + index = versions[0] + version = parts[index] + extension = 'pom' if parts[0] == 'pom' else 'jar' + classifier = '-' + parts[1] if index > 1 else '-tests' if parts[0] == 'test-jar' else '' + artifact = name + '-' + version + classifier + '.' + extension + key = name + '/' + version + '/' + artifact + if key in checked: + continue + checked.add(key) + tracking = root / name / version / '_remote.repositories' + if not tracking.exists() or artifact + '>=' not in tracking.read_text().splitlines(): + failures.append(key) + assert checked, 'No resolved NetworkNT snapshots found' assert not failures, 'Snapshots not installed from source: ' + ', '.join(failures) - print('All resolved NetworkNT snapshots were installed from source') + print('All resolved NetworkNT snapshots were installed from source:', len(checked)) PYTHON - uses: actions/upload-artifact@v4 if: always() @@ -132,5 +149,6 @@ jobs: source-build.log source-commits.txt source-refs.json + resolved-networknt.txt **/target/surefire-reports/** **/target/*smoke*.log From 1865ee9d2750530de1e4920bf6290178454e6cf5 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 25 Oct 2026 14:22:32 -0400 Subject: [PATCH 05/11] Use canonical mapper policy and qualified source CI --- .github/workflows/jackson3-source-build.yml | 46 +++++-------------- README.md | 6 +++ .../com/networknt/aws/lambda/Runtime.java | 4 +- .../aws/lambda/ConfigurationTest.java | 4 +- .../aws/lambda/LightLambdaInterceptor.java | 4 +- .../aws/lambda/LambdaSchemaValidatorTest.java | 4 +- .../lambda/validator/SchemaValidatorTest.java | 4 +- .../aws/lambda/ScopeVerifierTest.java | 4 +- 8 files changed, 29 insertions(+), 47 deletions(-) diff --git a/.github/workflows/jackson3-source-build.yml b/.github/workflows/jackson3-source-build.yml index d60dc8b8..eed85fe6 100644 --- a/.github/workflows/jackson3-source-build.yml +++ b/.github/workflows/jackson3-source-build.yml @@ -11,6 +11,7 @@ on: branches: [master] permissions: contents: read + actions: read jobs: test: runs-on: ubuntu-latest @@ -92,6 +93,9 @@ jobs: - name: Build prerequisites and run full tests shell: bash env: + GH_TOKEN: ${{ github.token }} + JACKSON3_REUSE_CORE_QUALIFICATION: 1 + JACKSON3_CORE_QUALIFICATION_REPORT: ${{ github.workspace }}/core-qualification.json JACKSON3_MAVEN_REPO: ${{ runner.temp }}/m2 JACKSON3_YAML_RULE: ${{ github.workspace }}/sources/yaml-rule JACKSON3_HTTP_CLIENT: ${{ github.workspace }}/sources/http-client @@ -103,44 +107,14 @@ jobs: mvn -f sources/openapi-parser/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log mvn -f sources/light-rest-4j/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log mvn -f consumer/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log - - name: Reject remotely resolved NetworkNT snapshots + - name: Audit resolved artifacts, including releases and Jackson boundaries env: SOURCE_MAVEN_REPO: ${{ runner.temp }}/m2 + shell: bash run: | set -euo pipefail - mvn -B -Dstyle.color=never -f consumer/pom.xml "-Dmaven.repo.local=$SOURCE_MAVEN_REPO" dependency:tree -Dincludes=com.networknt:* 2>&1 | tee resolved-networknt.txt - python3 - <<'PYTHON' - import os - import re - from pathlib import Path - root = Path(os.environ['SOURCE_MAVEN_REPO']) / 'com/networknt' - failures = [] - checked = set() - for line in Path('resolved-networknt.txt').read_text().splitlines(): - match = re.search(r'com[.]networknt:([^: ]+):([^ ]+)', line) - if not match: - continue - name, coordinates = match.groups() - parts = coordinates.split(':') - versions = [i for i, part in enumerate(parts) if part.endswith('-SNAPSHOT')] - if not versions: - continue - index = versions[0] - version = parts[index] - extension = 'pom' if parts[0] == 'pom' else 'jar' - classifier = '-' + parts[1] if index > 1 else '-tests' if parts[0] == 'test-jar' else '' - artifact = name + '-' + version + classifier + '.' + extension - key = name + '/' + version + '/' + artifact - if key in checked: - continue - checked.add(key) - tracking = root / name / version / '_remote.repositories' - if not tracking.exists() or artifact + '>=' not in tracking.read_text().splitlines(): - failures.append(key) - assert checked, 'No resolved NetworkNT snapshots found' - assert not failures, 'Snapshots not installed from source: ' + ', '.join(failures) - print('All resolved NetworkNT snapshots were installed from source:', len(checked)) - PYTHON + mvn -B -Dstyle.color=never -f consumer/pom.xml "-Dmaven.repo.local=$SOURCE_MAVEN_REPO" dependency:tree 2>&1 | tee resolved-dependencies.txt + python3 sources/light-4j/scripts/jackson3_artifact_audit.py --repository "$SOURCE_MAVEN_REPO" --tree resolved-dependencies.txt --report artifact-audit.json --require-jackson3-only - uses: actions/upload-artifact@v4 if: always() with: @@ -149,6 +123,8 @@ jobs: source-build.log source-commits.txt source-refs.json - resolved-networknt.txt + resolved-dependencies.txt + artifact-audit.json + core-qualification.json **/target/surefire-reports/** **/target/*smoke*.log diff --git a/README.md b/README.md index 98edda2d..70301d54 100644 --- a/README.md +++ b/README.md @@ -49,3 +49,9 @@ Local qualification covers JVM tests and sanitized API Gateway/event and validat `.github/workflows/jackson3-source-build.yml` builds the prerequisite source stack in a fresh isolated Maven repository and runs its full test suites. It resolves every prerequisite to a full commit SHA once and uploads `source-refs.json`, `source-commits.txt` and Surefire reports. The shared mapper policy prerequisite is light-4j #2821; merge that before its consumers. The optional `source_refs` workflow-dispatch input replays recorded full SHAs. GitHub enables dispatch only after the workflow exists on the default branch; before merge, reproduce from the recorded source checkouts locally. A green local suite is separate from a successful remote source-build run. + +### Exact-source CI qualification + +Consumer CI requires the latest full-core workflow for the exact resolved light-4j SHA to have completed successfully. Pending, failed or missing core qualification blocks the build. The core workflow continues to run its full reactor tests; consumers install that qualified source and run their own full tests and other prerequisite suites. `core-qualification.json` records the accepted run. + +The full dependency ledger is audited across all groups. Every NetworkNT release and snapshot must be source-installed, apart from the explicitly SHA-256-pinned Jackson 3 validator release. Known Jackson 2 yaml-rule and chaos-monkey releases are rejected even with a local-install marker. Jackson 2 dependencies in other groups are reported; ready migrations reject them except shared annotations. diff --git a/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java b/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java index 486fcd28..0b99f2d2 100644 --- a/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java +++ b/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java @@ -3,7 +3,7 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.networknt.jackson.Jackson2Defaults; +import com.networknt.jackson.JacksonDefaults; import com.networknt.utility.NioUtils; import java.io.IOException; import java.lang.reflect.Method; @@ -17,7 +17,7 @@ public class Runtime { private static final String REQUEST_ID_HEADER = "lambda-runtime-aws-request-id"; - private static final ObjectMapper OBJECT_MAPPER = Jackson2Defaults.builder() + private static final ObjectMapper OBJECT_MAPPER = JacksonDefaults.builder() .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build(); public static void main(String[] args) throws IOException { diff --git a/env-config/src/test/java/com/networknt/aws/lambda/ConfigurationTest.java b/env-config/src/test/java/com/networknt/aws/lambda/ConfigurationTest.java index b3793898..7cf40dd0 100644 --- a/env-config/src/test/java/com/networknt/aws/lambda/ConfigurationTest.java +++ b/env-config/src/test/java/com/networknt/aws/lambda/ConfigurationTest.java @@ -1,6 +1,6 @@ package com.networknt.aws.lambda; -import com.networknt.config.Jackson2Defaults; +import com.networknt.jackson.JacksonDefaults; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import tools.jackson.databind.ObjectMapper; @@ -11,7 +11,7 @@ import java.util.Map; public class ConfigurationTest { - ObjectMapper objectMapper = Jackson2Defaults.mapper(); + ObjectMapper objectMapper = JacksonDefaults.mapper(); @Test @Disabled public void thatConfigurationValuesAreLoaded() throws Exception { diff --git a/lambda-interceptor/src/main/java/com/networknt/aws/lambda/LightLambdaInterceptor.java b/lambda-interceptor/src/main/java/com/networknt/aws/lambda/LightLambdaInterceptor.java index 36366080..ccf6b2be 100644 --- a/lambda-interceptor/src/main/java/com/networknt/aws/lambda/LightLambdaInterceptor.java +++ b/lambda-interceptor/src/main/java/com/networknt/aws/lambda/LightLambdaInterceptor.java @@ -4,13 +4,13 @@ import com.amazonaws.services.lambda.runtime.LambdaLogger; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.networknt.jackson.Jackson2Defaults; +import com.networknt.jackson.JacksonDefaults; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import tools.jackson.databind.ObjectMapper; public class LightLambdaInterceptor { - private static ObjectMapper objectMapper = Jackson2Defaults.mapper(); + private static ObjectMapper objectMapper = JacksonDefaults.mapper(); private static final Logger logger = LoggerFactory.getLogger(LightLambdaInterceptor.class); public APIGatewayProxyRequestEvent interceptRequest(final APIGatewayProxyRequestEvent input, final Context context) { diff --git a/lambda-validator/src/test/java/com/networknt/aws/lambda/LambdaSchemaValidatorTest.java b/lambda-validator/src/test/java/com/networknt/aws/lambda/LambdaSchemaValidatorTest.java index c02d08e0..f1e4bded 100644 --- a/lambda-validator/src/test/java/com/networknt/aws/lambda/LambdaSchemaValidatorTest.java +++ b/lambda-validator/src/test/java/com/networknt/aws/lambda/LambdaSchemaValidatorTest.java @@ -1,6 +1,6 @@ package com.networknt.aws.lambda; -import com.networknt.config.Jackson2Defaults; +import com.networknt.jackson.JacksonDefaults; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; @@ -15,7 +15,7 @@ import java.util.Map; public class LambdaSchemaValidatorTest { - ObjectMapper objectMapper = Jackson2Defaults.mapper(); + ObjectMapper objectMapper = JacksonDefaults.mapper(); private final LambdaSchemaValidator validator = new LambdaSchemaValidator(); APIGatewayProxyRequestEvent requestEvent; diff --git a/schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaValidatorTest.java b/schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaValidatorTest.java index c30d4ab7..a257a181 100644 --- a/schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaValidatorTest.java +++ b/schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaValidatorTest.java @@ -1,6 +1,6 @@ package com.networknt.aws.lambda.validator; -import com.networknt.config.Jackson2Defaults; +import com.networknt.jackson.JacksonDefaults; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; @@ -16,7 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class SchemaValidatorTest { - private static final ObjectMapper MAPPER = Jackson2Defaults.mapper(); + private static final ObjectMapper MAPPER = JacksonDefaults.mapper(); @Test void shouldConstructWithoutAnApi() { diff --git a/scope-verifier/src/test/java/com/networknt/aws/lambda/ScopeVerifierTest.java b/scope-verifier/src/test/java/com/networknt/aws/lambda/ScopeVerifierTest.java index f0d0e93e..e686d8dc 100644 --- a/scope-verifier/src/test/java/com/networknt/aws/lambda/ScopeVerifierTest.java +++ b/scope-verifier/src/test/java/com/networknt/aws/lambda/ScopeVerifierTest.java @@ -1,6 +1,6 @@ package com.networknt.aws.lambda; -import com.networknt.config.Jackson2Defaults; +import com.networknt.jackson.JacksonDefaults; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; @@ -13,7 +13,7 @@ import java.util.List; public class ScopeVerifierTest { - ObjectMapper objectMapper = Jackson2Defaults.mapper(); + ObjectMapper objectMapper = JacksonDefaults.mapper(); @Test public void testSuccessRequest() { ScopeVerifier verifier = new ScopeVerifier(); From e250f071dd8557e045b37b2ac980789ca51d5e94 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 25 Oct 2026 21:32:27 -0400 Subject: [PATCH 06/11] Resolve migration sources by branch and require retained core evidence --- .github/workflows/jackson3-source-build.yml | 20 +++++++++++++------- README.md | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/jackson3-source-build.yml b/.github/workflows/jackson3-source-build.yml index eed85fe6..0f34beb3 100644 --- a/.github/workflows/jackson3-source-build.yml +++ b/.github/workflows/jackson3-source-build.yml @@ -20,6 +20,7 @@ jobs: - uses: actions/checkout@v4 with: path: consumer + ref: ${{ github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-java@v4 with: distribution: temurin @@ -33,21 +34,26 @@ jobs: run: | jq -e 'type == "object" and all(to_entries[]; (.key | IN("yaml-rule", "http-client", "light-4j", "json-overlay", "openapi-parser", "light-rest-4j")) and (.value | type == "string" and test("^[0-9a-f]{40}$")))' <<< "$SOURCE_REFS" resolve() { - local repo=$1 pr=${2:-} ref + local repo=$1 branch=${2:-} ref pulls ref=$(jq -r --arg repo "$repo" '.[$repo] // empty' <<< "$SOURCE_REFS") - if [[ -z "$ref" ]]; then - if [[ -n "$pr" ]]; then - ref=$(gh api "repos/networknt/$repo/pulls/$pr" --jq 'if .merged then .base.ref elif .state == "open" then .head.sha else error("Prerequisite PR closed unmerged") end') - else - ref=master + if [[ -z "$ref" && -n "$branch" ]]; then + pulls=$(gh api --paginate --slurp "repos/networknt/$repo/pulls?head=networknt:$branch&state=all&per_page=100") + ref=$(jq -r '[.[][] | select(.state == "open")] | if length == 1 then .[0].head.sha elif length > 1 then error("Ambiguous source branch") else empty end' <<< "$pulls") + if [[ -z "$ref" ]]; then + jq -e '[.[][]] | length == 0 or any(.[]; .merged_at != null)' <<< "$pulls" >/dev/null || { + echo "Prerequisite branch $repo:$branch was closed without merging" >&2; exit 1; + } fi fi + if [[ -z "$ref" ]]; then + ref=$(gh api "repos/networknt/$repo" --jq .default_branch) + fi ref=$(gh api "repos/networknt/$repo/commits/$ref" --jq .sha) echo "${repo//-/_}=$ref" >> "$GITHUB_OUTPUT" } resolve yaml-rule resolve http-client - resolve light-4j 2821 + resolve light-4j fix-jackson3-mapper-policy resolve json-overlay resolve openapi-parser resolve light-rest-4j diff --git a/README.md b/README.md index 70301d54..dc708daa 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,6 @@ The optional `source_refs` workflow-dispatch input replays recorded full SHAs. G ### Exact-source CI qualification -Consumer CI requires the latest full-core workflow for the exact resolved light-4j SHA to have completed successfully. Pending, failed or missing core qualification blocks the build. The core workflow continues to run its full reactor tests; consumers install that qualified source and run their own full tests and other prerequisite suites. `core-qualification.json` records the accepted run. +Consumer CI requires retained successful full-reactor evidence for the actual checked-out light-4j SHA and the same yaml-rule/http-client source commits. Core PR CI explicitly checks out the PR head, not its merge ref. Replay dispatches cannot qualify consumers. The attempt-specific evidence artifact records the actual checkout, prerequisite SHAs and test totals. A later pending, failed or cancelled run does not invalidate earlier valid evidence. All API pages and retained attempts are considered; missing or expired evidence blocks reuse. `core-qualification.json` records the accepted run and artifact. The full dependency ledger is audited across all groups. Every NetworkNT release and snapshot must be source-installed, apart from the explicitly SHA-256-pinned Jackson 3 validator release. Known Jackson 2 yaml-rule and chaos-monkey releases are rejected even with a local-install marker. Jackson 2 dependencies in other groups are reported; ready migrations reject them except shared annotations. From d5b9426c93cf36777eb45e9c126fda1e16f60127 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 25 Oct 2026 23:52:29 -0400 Subject: [PATCH 07/11] Pin independent core policy and consume qualified prerequisite SHAs --- .github/workflows/jackson3-source-build.yml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/jackson3-source-build.yml b/.github/workflows/jackson3-source-build.yml index 0f34beb3..86e8ab95 100644 --- a/.github/workflows/jackson3-source-build.yml +++ b/.github/workflows/jackson3-source-build.yml @@ -12,6 +12,9 @@ on: permissions: contents: read actions: read +env: + # Proposed immutable policy pin; promotion requires review of this commit. + JACKSON3_QUALIFIER_REVISION: 27aed6a44a91d40a4a590be563b3d6e2fbc3415b jobs: test: runs-on: ubuntu-latest @@ -25,6 +28,11 @@ jobs: with: distribution: temurin java-version: '25' + - uses: actions/checkout@v4 + with: + repository: networknt/light-4j + ref: ${{ env.JACKSON3_QUALIFIER_REVISION }} + path: qualification-policy - name: Resolve prerequisite commits id: prerequisites env: @@ -50,10 +58,14 @@ jobs: fi ref=$(gh api "repos/networknt/$repo/commits/$ref" --jq .sha) echo "${repo//-/_}=$ref" >> "$GITHUB_OUTPUT" + if [[ "$repo" == light-4j ]]; then resolved_core=$ref; fi } - resolve yaml-rule - resolve http-client resolve light-4j fix-jackson3-mapper-policy + core_overrides=$(jq -c 'with_entries(select(.key == "yaml-rule" or .key == "http-client"))' <<< "$SOURCE_REFS") + python3 qualification-policy/scripts/jackson3_core_qualification.py \ + --source-sha "$resolved_core" --overrides "$core_overrides" \ + --trusted-revision "$JACKSON3_QUALIFIER_REVISION" \ + --report core-qualification.json --github-output "$GITHUB_OUTPUT" resolve json-overlay resolve openapi-parser resolve light-rest-4j @@ -101,13 +113,14 @@ jobs: env: GH_TOKEN: ${{ github.token }} JACKSON3_REUSE_CORE_QUALIFICATION: 1 + JACKSON3_CORE_SOURCE: ${{ github.workspace }}/sources/light-4j JACKSON3_CORE_QUALIFICATION_REPORT: ${{ github.workspace }}/core-qualification.json JACKSON3_MAVEN_REPO: ${{ runner.temp }}/m2 JACKSON3_YAML_RULE: ${{ github.workspace }}/sources/yaml-rule JACKSON3_HTTP_CLIENT: ${{ github.workspace }}/sources/http-client run: | set -euo pipefail - bash sources/light-4j/scripts/verify-jackson3-bootstrap.sh 2>&1 | tee source-build.log + bash qualification-policy/scripts/verify-jackson3-bootstrap.sh 2>&1 | tee source-build.log mvn -f sources/yaml-rule/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log mvn -f sources/json-overlay/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log mvn -f sources/openapi-parser/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log From 13f734fd1f13047a184dc04ad6f6a30b446c729b Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Mon, 26 Oct 2026 16:48:29 -0400 Subject: [PATCH 08/11] Pin source CI to reviewed Jackson 3 qualification --- .github/workflows/jackson3-source-build.yml | 38 ++++----------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/.github/workflows/jackson3-source-build.yml b/.github/workflows/jackson3-source-build.yml index 86e8ab95..90e763e0 100644 --- a/.github/workflows/jackson3-source-build.yml +++ b/.github/workflows/jackson3-source-build.yml @@ -13,8 +13,9 @@ permissions: contents: read actions: read env: - # Proposed immutable policy pin; promotion requires review of this commit. - JACKSON3_QUALIFIER_REVISION: 27aed6a44a91d40a4a590be563b3d6e2fbc3415b + # Reviewed master policy and separately qualified candidate; both pins are immutable. + JACKSON3_QUALIFIER_REVISION: f0d2100f972eec765b2b276e7ace38922394f0a6 + JACKSON3_CORE_REVISION: 97b2359b92ed3e1d05cac04f2da8760aa954e49d jobs: test: runs-on: ubuntu-latest @@ -33,42 +34,17 @@ jobs: repository: networknt/light-4j ref: ${{ env.JACKSON3_QUALIFIER_REVISION }} path: qualification-policy - - name: Resolve prerequisite commits + - name: Select qualified core and resolve remaining prerequisites id: prerequisites env: GH_TOKEN: ${{ github.token }} SOURCE_REFS: ${{ inputs.source_refs || '{}' }} shell: bash run: | - jq -e 'type == "object" and all(to_entries[]; (.key | IN("yaml-rule", "http-client", "light-4j", "json-overlay", "openapi-parser", "light-rest-4j")) and (.value | type == "string" and test("^[0-9a-f]{40}$")))' <<< "$SOURCE_REFS" - resolve() { - local repo=$1 branch=${2:-} ref pulls - ref=$(jq -r --arg repo "$repo" '.[$repo] // empty' <<< "$SOURCE_REFS") - if [[ -z "$ref" && -n "$branch" ]]; then - pulls=$(gh api --paginate --slurp "repos/networknt/$repo/pulls?head=networknt:$branch&state=all&per_page=100") - ref=$(jq -r '[.[][] | select(.state == "open")] | if length == 1 then .[0].head.sha elif length > 1 then error("Ambiguous source branch") else empty end' <<< "$pulls") - if [[ -z "$ref" ]]; then - jq -e '[.[][]] | length == 0 or any(.[]; .merged_at != null)' <<< "$pulls" >/dev/null || { - echo "Prerequisite branch $repo:$branch was closed without merging" >&2; exit 1; - } - fi - fi - if [[ -z "$ref" ]]; then - ref=$(gh api "repos/networknt/$repo" --jq .default_branch) - fi - ref=$(gh api "repos/networknt/$repo/commits/$ref" --jq .sha) - echo "${repo//-/_}=$ref" >> "$GITHUB_OUTPUT" - if [[ "$repo" == light-4j ]]; then resolved_core=$ref; fi - } - resolve light-4j fix-jackson3-mapper-policy - core_overrides=$(jq -c 'with_entries(select(.key == "yaml-rule" or .key == "http-client"))' <<< "$SOURCE_REFS") - python3 qualification-policy/scripts/jackson3_core_qualification.py \ - --source-sha "$resolved_core" --overrides "$core_overrides" \ - --trusted-revision "$JACKSON3_QUALIFIER_REVISION" \ + python3 qualification-policy/scripts/resolve_jackson3_consumer_sources.py \ + --core-sha "$JACKSON3_CORE_REVISION" \ + --repositories '{"json-overlay":null,"openapi-parser":null,"light-rest-4j":null}' --source-refs "$SOURCE_REFS" \ --report core-qualification.json --github-output "$GITHUB_OUTPUT" - resolve json-overlay - resolve openapi-parser - resolve light-rest-4j - uses: actions/checkout@v4 with: repository: networknt/yaml-rule From d68d2b32f6f72ddcd82ebf0ebfd1cf753d3886d0 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Mon, 26 Oct 2026 16:51:48 -0400 Subject: [PATCH 09/11] Document adopted core qualification pins --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dc708daa..bbdddecf 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Local qualification covers JVM tests and sanitized API Gateway/event and validat ### Reproducible Jackson 3 source build -`.github/workflows/jackson3-source-build.yml` builds the prerequisite source stack in a fresh isolated Maven repository and runs its full test suites. It resolves every prerequisite to a full commit SHA once and uploads `source-refs.json`, `source-commits.txt` and Surefire reports. The shared mapper policy prerequisite is light-4j #2821; merge that before its consumers. +`.github/workflows/jackson3-source-build.yml` builds the prerequisite source stack in an isolated Maven repository, reuses independently qualified full-core test evidence, and runs the prerequisite and full consumer suites. It pins reviewed policy `f0d2100f972eec765b2b276e7ace38922394f0a6` and candidate `97b2359b92ed3e1d05cac04f2da8760aa954e49d`; yaml-rule/http-client are selected from that verified evidence. Other prerequisites resolve once to full SHAs. The workflow uploads `source-refs.json`, `source-commits.txt`, `core-qualification.json` and Surefire reports. Core registration and workflow-token acceptance are merged in light-4j #2825 and #2826. The optional `source_refs` workflow-dispatch input replays recorded full SHAs. GitHub enables dispatch only after the workflow exists on the default branch; before merge, reproduce from the recorded source checkouts locally. A green local suite is separate from a successful remote source-build run. From 29c6c4631685e4b6438ff96c0a9622775ebcabb1 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Mon, 26 Oct 2026 17:26:05 -0400 Subject: [PATCH 10/11] Pin complete source-build prerequisite closure --- .github/workflows/jackson3-source-build.yml | 3 ++- README.md | 2 +- ci/jackson3-source-refs.json | 8 ++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 ci/jackson3-source-refs.json diff --git a/.github/workflows/jackson3-source-build.yml b/.github/workflows/jackson3-source-build.yml index 90e763e0..3148d81f 100644 --- a/.github/workflows/jackson3-source-build.yml +++ b/.github/workflows/jackson3-source-build.yml @@ -41,9 +41,10 @@ jobs: SOURCE_REFS: ${{ inputs.source_refs || '{}' }} shell: bash run: | + source_refs=$(jq -c --argjson overrides "$SOURCE_REFS" '. * $overrides' consumer/ci/jackson3-source-refs.json) python3 qualification-policy/scripts/resolve_jackson3_consumer_sources.py \ --core-sha "$JACKSON3_CORE_REVISION" \ - --repositories '{"json-overlay":null,"openapi-parser":null,"light-rest-4j":null}' --source-refs "$SOURCE_REFS" \ + --repositories '{"json-overlay":null,"openapi-parser":null,"light-rest-4j":null}' --source-refs "$source_refs" \ --report core-qualification.json --github-output "$GITHUB_OUTPUT" - uses: actions/checkout@v4 with: diff --git a/README.md b/README.md index bbdddecf..cc005a18 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Local qualification covers JVM tests and sanitized API Gateway/event and validat ### Reproducible Jackson 3 source build -`.github/workflows/jackson3-source-build.yml` builds the prerequisite source stack in an isolated Maven repository, reuses independently qualified full-core test evidence, and runs the prerequisite and full consumer suites. It pins reviewed policy `f0d2100f972eec765b2b276e7ace38922394f0a6` and candidate `97b2359b92ed3e1d05cac04f2da8760aa954e49d`; yaml-rule/http-client are selected from that verified evidence. Other prerequisites resolve once to full SHAs. The workflow uploads `source-refs.json`, `source-commits.txt`, `core-qualification.json` and Surefire reports. Core registration and workflow-token acceptance are merged in light-4j #2825 and #2826. +`.github/workflows/jackson3-source-build.yml` builds the prerequisite source stack in an isolated Maven repository, reuses independently qualified full-core test evidence, and runs the prerequisite and full consumer suites. It pins reviewed policy `f0d2100f972eec765b2b276e7ace38922394f0a6` and candidate `97b2359b92ed3e1d05cac04f2da8760aa954e49d`; yaml-rule/http-client are selected from that verified evidence. The complete prerequisite closure is pinned in `ci/jackson3-source-refs.json`, initialized from the successful source-build manifest, with reviewed updates expressed as full SHAs; no default-branch or migration-branch lookup is used. Explicit `source_refs` replay overrides are merged into that map and remain subject to core qualification. The workflow uploads `source-refs.json`, `source-commits.txt`, `core-qualification.json` and Surefire reports. Core registration and workflow-token acceptance are merged in light-4j #2825 and #2826. The optional `source_refs` workflow-dispatch input replays recorded full SHAs. GitHub enables dispatch only after the workflow exists on the default branch; before merge, reproduce from the recorded source checkouts locally. A green local suite is separate from a successful remote source-build run. diff --git a/ci/jackson3-source-refs.json b/ci/jackson3-source-refs.json new file mode 100644 index 00000000..399f89a4 --- /dev/null +++ b/ci/jackson3-source-refs.json @@ -0,0 +1,8 @@ +{ + "http-client": "5ecee29dbb08d813512f5379c2e1b902d553c5d4", + "json-overlay": "e309fd63f95317067c9b13a4bed7ac16727bfea5", + "light-4j": "97b2359b92ed3e1d05cac04f2da8760aa954e49d", + "light-rest-4j": "a31955945036d9108b7e9bb72167415cc9cfc287", + "openapi-parser": "9ae849056e49f7c796fe28bfe6093fbe4847cc5c", + "yaml-rule": "c237cc3a25720f43e57bf571e25d5fa06687aa3b" +} From 86a8fa4efc12c82fad4311ae28acb49bc59f6197 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Mon, 26 Oct 2026 17:30:13 -0400 Subject: [PATCH 11/11] Reject incomplete prerequisite pin manifests --- .github/workflows/jackson3-source-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/jackson3-source-build.yml b/.github/workflows/jackson3-source-build.yml index 3148d81f..56895886 100644 --- a/.github/workflows/jackson3-source-build.yml +++ b/.github/workflows/jackson3-source-build.yml @@ -41,6 +41,7 @@ jobs: SOURCE_REFS: ${{ inputs.source_refs || '{}' }} shell: bash run: | + jq -e --arg core "$JACKSON3_CORE_REVISION" --argjson expected '["http-client","json-overlay","light-4j","light-rest-4j","openapi-parser","yaml-rule"]' '.["light-4j"] == $core and keys == $expected and all(.[]; type == "string" and test("^[0-9a-f]{40}$"))' consumer/ci/jackson3-source-refs.json >/dev/null source_refs=$(jq -c --argjson overrides "$SOURCE_REFS" '. * $overrides' consumer/ci/jackson3-source-refs.json) python3 qualification-policy/scripts/resolve_jackson3_consumer_sources.py \ --core-sha "$JACKSON3_CORE_REVISION" \