From df3e5e2715b481f3e421b2df742d24857dc2f15f Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 25 Oct 2026 10:28:44 -0400 Subject: [PATCH 01/13] Migrate to Jackson 3 with compatible mapper defaults --- README.md | 8 ++++ pom.xml | 12 ++--- .../java/com/networknt/aws/lambda/Main.java | 9 ++-- .../middleware/limit/LimitMiddleware.java | 2 +- .../specification/OpenApiMiddleware.java | 4 +- .../Jackson3NativeCompatibilityTest.java | 44 +++++++++++++++++++ .../com/networknt/aws/lambda/TestUtils.java | 13 +++--- .../validator/ValidatorMiddlewareTest.java | 2 +- 8 files changed, 74 insertions(+), 20 deletions(-) create mode 100644 src/test/java/com/networknt/aws/lambda/Jackson3NativeCompatibilityTest.java diff --git a/README.md b/README.md index e61068e..e0db2ae 100644 --- a/README.md +++ b/README.md @@ -122,3 +122,11 @@ software.amazon.awssdk.services.lambda.model.ResourceNotFoundException: Function ### [AuditMiddleware](doc/audit.md) ### [LambdaProxyMiddleware](doc/proxy.md) + +## Jackson 3 migration + +This source line uses Jackson 3.2.3 and json-schema-validator 3.0.8. Build the Jackson 3 light-4j, http-client, REST and AWS Lambda prerequisites first; older snapshot binaries are not compatible. Jackson annotations retain their com.fasterxml.jackson.annotation package. + +The runtime mapper uses Jackson2Defaults with its existing permissive unknown-property override. Mapper instances are immutable. Java time and Optional support are built in. Classic SnakeYAML compatibility pins are retained until the stack-wide cleanup. + +Local qualification includes the full JVM suite, sanitized event/validation parity against the Jackson 2 baseline, and an offline GraalVM 25 native-image build. A native-image build does not establish deployed AWS behavior or complete reflection coverage for every middleware/configuration. The separate conformance worktree is deferred. diff --git a/pom.xml b/pom.xml index 18fd315..1be1d97 100644 --- a/pom.xml +++ b/pom.xml @@ -82,8 +82,8 @@ ${java.version} UTF-8 2.4.2-SNAPSHOT - 2.0.8 - 2.22.3 + 3.0.8 + 3.2.3 2.0.20 1.5.0 1.4.0 @@ -114,7 +114,7 @@ import - com.fasterxml.jackson + tools.jackson jackson-bom ${version.jackson} pom @@ -356,15 +356,15 @@ ${version.lambda-awssdk} - com.fasterxml.jackson.core + tools.jackson.core jackson-databind - com.fasterxml.jackson.core + tools.jackson.core jackson-core - com.fasterxml.jackson.dataformat + tools.jackson.dataformat jackson-dataformat-yaml diff --git a/src/main/java/com/networknt/aws/lambda/Main.java b/src/main/java/com/networknt/aws/lambda/Main.java index 0a12600..2b04598 100644 --- a/src/main/java/com/networknt/aws/lambda/Main.java +++ b/src/main/java/com/networknt/aws/lambda/Main.java @@ -2,16 +2,17 @@ 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 com.networknt.config.Jackson2Defaults; import com.networknt.aws.lambda.app.LambdaApp; import java.io.IOException; public class Main { - 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 { diff --git a/src/main/java/com/networknt/aws/lambda/handler/middleware/limit/LimitMiddleware.java b/src/main/java/com/networknt/aws/lambda/handler/middleware/limit/LimitMiddleware.java index 953637c..d014982 100644 --- a/src/main/java/com/networknt/aws/lambda/handler/middleware/limit/LimitMiddleware.java +++ b/src/main/java/com/networknt/aws/lambda/handler/middleware/limit/LimitMiddleware.java @@ -2,7 +2,7 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import com.networknt.aws.lambda.handler.MiddlewareHandler; import com.networknt.aws.lambda.LightLambdaExchange; import com.networknt.aws.lambda.utility.HeaderKey; diff --git a/src/main/java/com/networknt/aws/lambda/handler/middleware/specification/OpenApiMiddleware.java b/src/main/java/com/networknt/aws/lambda/handler/middleware/specification/OpenApiMiddleware.java index a9118ec..da332c4 100644 --- a/src/main/java/com/networknt/aws/lambda/handler/middleware/specification/OpenApiMiddleware.java +++ b/src/main/java/com/networknt/aws/lambda/handler/middleware/specification/OpenApiMiddleware.java @@ -1,6 +1,6 @@ package com.networknt.aws.lambda.handler.middleware.specification; -import com.fasterxml.jackson.core.JsonProcessingException; +import tools.jackson.core.JacksonException; import com.networknt.aws.lambda.handler.MiddlewareHandler; import com.networknt.aws.lambda.LightLambdaExchange; import com.networknt.config.Config; @@ -40,7 +40,7 @@ public OpenApiMiddleware() { String openapiString = Config.getInstance().getMapper().writeValueAsString(openapi); LOG.trace("OpenApiMiddleware openapiString: {}", openapiString); helper = new OpenApiHelper(openapiString); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { LOG.error("merge specification failed"); throw new RuntimeException("merge specification failed"); } diff --git a/src/test/java/com/networknt/aws/lambda/Jackson3NativeCompatibilityTest.java b/src/test/java/com/networknt/aws/lambda/Jackson3NativeCompatibilityTest.java new file mode 100644 index 0000000..f0266a3 --- /dev/null +++ b/src/test/java/com/networknt/aws/lambda/Jackson3NativeCompatibilityTest.java @@ -0,0 +1,44 @@ +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.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.*; + +class Jackson3NativeCompatibilityTest { + private ObjectMapper mainMapper() throws Exception { + Field field = Main.class.getDeclaredField("OBJECT_MAPPER"); + field.setAccessible(true); + return (ObjectMapper) field.get(null); + } + + @Test + void runtimeMapperKeepsPermissiveEventBinding() throws Exception { + APIGatewayProxyRequestEvent event = mainMapper().readValue( + "{\"path\":\"/v1/pets\",\"httpMethod\":\"POST\",\"futureField\":true} trailing", + APIGatewayProxyRequestEvent.class); + assertEquals("/v1/pets", event.getPath()); + assertEquals("POST", event.getHttpMethod()); + } + + @Test + void responseRetainsJackson2ShapeAndRoundTrips() throws Exception { + ObjectMapper mapper = mainMapper(); + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() + .withStatusCode(200).withBody("{\"ok\":true}").withIsBase64Encoded(false); + String json = mapper.writeValueAsString(response); + assertEquals("{\"statusCode\":200,\"headers\":null,\"multiValueHeaders\":null," + + "\"body\":\"{\\\"ok\\\":true}\",\"isBase64Encoded\":false}", json); + assertEquals(response, mapper.readValue(json, APIGatewayProxyResponseEvent.class)); + } + + @Test + void runtimeMapperRetainsJackson2StreamDepth() throws Exception { + String nested = "[".repeat(600) + "0" + "]".repeat(600); + assertDoesNotThrow(() -> mainMapper().readTree(nested)); + } +} diff --git a/src/test/java/com/networknt/aws/lambda/TestUtils.java b/src/test/java/com/networknt/aws/lambda/TestUtils.java index 7a05410..fa39bb7 100644 --- a/src/test/java/com/networknt/aws/lambda/TestUtils.java +++ b/src/test/java/com/networknt/aws/lambda/TestUtils.java @@ -1,9 +1,10 @@ package com.networknt.aws.lambda; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import com.networknt.config.Jackson2Defaults; public class TestUtils { @@ -132,14 +133,14 @@ public class TestUtils { " }\n" + "}"; - 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 APIGatewayProxyRequestEvent createTestRequestEvent() { APIGatewayProxyRequestEvent apiGatewayProxyRequestEvent; try { apiGatewayProxyRequestEvent = OBJECT_MAPPER.readValue(TEST_EVENT, APIGatewayProxyRequestEvent.class); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { throw new RuntimeException(e); } diff --git a/src/test/java/com/networknt/aws/lambda/middleware/validator/ValidatorMiddlewareTest.java b/src/test/java/com/networknt/aws/lambda/middleware/validator/ValidatorMiddlewareTest.java index b8cc67e..997f102 100644 --- a/src/test/java/com/networknt/aws/lambda/middleware/validator/ValidatorMiddlewareTest.java +++ b/src/test/java/com/networknt/aws/lambda/middleware/validator/ValidatorMiddlewareTest.java @@ -3,7 +3,7 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.fasterxml.jackson.databind.JsonNode; +import tools.jackson.databind.JsonNode; import com.networknt.aws.lambda.InvocationResponse; import com.networknt.aws.lambda.LambdaContext; import com.networknt.aws.lambda.TestUtils; From f144ad717f5876fa1899faa5503eedab52ebf925 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 25 Oct 2026 12:40:44 -0400 Subject: [PATCH 02/13] Invoke native Runtime smoke and include YAML charsets --- .github/workflows/jackson3-source-build.yml | 125 ++++++++++++++++++++ README.md | 15 +++ native-image-linux.sh | 15 ++- tests/native_runtime_smoke.py | 100 ++++++++++++++++ tests/test_native_image_script.py | 1 + 5 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/jackson3-source-build.yml create mode 100644 tests/native_runtime_smoke.py diff --git a/.github/workflows/jackson3-source-build.yml b/.github/workflows/jackson3-source-build.yml new file mode 100644 index 0000000..13e3b1a --- /dev/null +++ b/.github/workflows/jackson3-source-build.yml @@ -0,0 +1,125 @@ +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", "light-aws-lambda")) 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 + resolve light-aws-lambda 168 + - 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 + - uses: actions/checkout@v4 + with: + repository: networknt/light-aws-lambda + ref: ${{ steps.prerequisites.outputs.light_aws_lambda }} + path: sources/light-aws-lambda + - 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 sources/light-aws-lambda/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: Test native packaging script + working-directory: consumer + run: python3 tests/test_native_image_script.py + - name: Build and invoke native Runtime locally + working-directory: consumer + env: + MAVEN_REPO_LOCAL: ${{ runner.temp }}/m2 + run: | + bash native-image-linux.sh + python3 tests/native_runtime_smoke.py target/custom-runtime/lambda-native + - 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 e0db2ae..4a70bdb 100644 --- a/README.md +++ b/README.md @@ -130,3 +130,18 @@ This source line uses Jackson 3.2.3 and json-schema-validator 3.0.8. Build the J The runtime mapper uses Jackson2Defaults with its existing permissive unknown-property override. Mapper instances are immutable. Java time and Optional support are built in. Classic SnakeYAML compatibility pins are retained until the stack-wide cleanup. Local qualification includes the full JVM suite, sanitized event/validation parity against the Jackson 2 baseline, and an offline GraalVM 25 native-image build. A native-image build does not establish deployed AWS behavior or complete reflection coverage for every middleware/configuration. The separate conformance worktree is deferred. + +Native qualification now invokes the executable against a synthetic loopback Lambda Runtime API: + +```sh +MAVEN_REPO_LOCAL=/path/to/isolated/m2 bash native-image-linux.sh +python3 tests/native_runtime_smoke.py target/custom-runtime/lambda-native +``` + +The smoke check binds an API Gateway event, runs the local health handler, and verifies success and builder-based error serialization. It uses no AWS endpoint or credentials. `-H:+AddAllCharsets` is required by SnakeYAML Engine's UTF-32 input detection. The check does not qualify every middleware chain or deployed AWS behavior. + +### 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/native-image-linux.sh b/native-image-linux.sh index 99eb3b5..b523a42 100755 --- a/native-image-linux.sh +++ b/native-image-linux.sh @@ -3,13 +3,21 @@ set -euo pipefail cd -- "$(dirname -- "${BASH_SOURCE[0]}")" +maven_args=() +if [[ -n ${MAVEN_REPO_LOCAL:-} ]]; then + maven_args+=("-Dmaven.repo.local=$MAVEN_REPO_LOCAL") +fi +if [[ ${MAVEN_OFFLINE:-0} == 1 ]]; then + maven_args+=(-o) +fi + # Build the shaded JAR, then obtain its name and Java level from Maven. -mvn clean install +mvn clean install "${maven_args[@]}" metadata_dir=$(mktemp -d) trap 'rm -rf -- "$metadata_dir"' EXIT -mvn -q help:evaluate -Dexpression=project.build.finalName \ +mvn -q help:evaluate "${maven_args[@]}" -Dexpression=project.build.finalName \ -Doutput="$metadata_dir/final-name" -mvn -q help:evaluate -Dexpression=maven.compiler.target \ +mvn -q help:evaluate "${maven_args[@]}" -Dexpression=maven.compiler.target \ -Doutput="$metadata_dir/java-version" jar_name=$(cat "$metadata_dir/final-name") java_version=$(cat "$metadata_dir/java-version") @@ -24,6 +32,7 @@ docker run --rm --user "$(id -u):$(id -g)" \ "$graalvm_image" \ --enable-url-protocols=http,https \ --no-fallback \ + -H:+AddAllCharsets \ --verbose \ --initialize-at-build-time=org.slf4j,ch.qos.logback \ --initialize-at-run-time=io.netty \ diff --git a/tests/native_runtime_smoke.py b/tests/native_runtime_smoke.py new file mode 100644 index 0000000..74891cf --- /dev/null +++ b/tests/native_runtime_smoke.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Exercise the native Runtime against a loopback-only synthetic Lambda Runtime API.""" +import argparse +import http.server +import json +import os +from pathlib import Path +import queue +import subprocess +import tempfile +import threading + + +def invoke(binary, handler, expected_endpoint): + received = queue.Queue() + event = {"path": "/health", "httpMethod": "GET", "body": None, + "isBase64Encoded": False, "headers": {}, "multiValueHeaders": {}, + "requestContext": {"requestId": "native-smoke", "identity": {"sourceIp": "127.0.0.1"}}} + + class RuntimeApi(http.server.BaseHTTPRequestHandler): + def log_message(self, *args): + pass + + def do_GET(self): + assert self.path == "/2018-06-01/runtime/invocation/next", self.path + payload = json.dumps(event).encode() + self.send_response(200) + self.send_header("lambda-runtime-aws-request-id", "native-smoke") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def do_POST(self): + payload = self.rfile.read(int(self.headers["Content-Length"])) + received.put((self.path, json.loads(payload))) + self.send_response(202) + self.send_header("Content-Length", "0") + self.end_headers() + + with tempfile.TemporaryDirectory(prefix="native-runtime-smoke-") as directory: + config = Path(directory) + (config / "handler.yml").write_text("""enabled: true +handlers: + - com.networknt.aws.lambda.handler.health.HealthCheckHandler@health +chains: {} +paths: + - path: /health + method: get + exec: [health] +defaultHandlers: [health] +""") + (config / "health.yml").write_text("enabled: true\ndownstreamEnabled: false\n") + (config / "lambda-app.yml").write_text("lambdaAppId: native-smoke\nencodeBase64Request: false\nencodeBase64Response: false\n") + (config / "values.yml").write_text("{}\n") + server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), RuntimeApi) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + env = os.environ.copy() + env.update(AWS_LAMBDA_RUNTIME_API=f"127.0.0.1:{server.server_port}", _HANDLER=handler) + with (config / "runtime.log").open("w+") as log: + process = subprocess.Popen([str(binary), f"-Dlight-4j-config-dir={config}"], + env=env, stdout=log, stderr=subprocess.STDOUT) + try: + endpoint, payload = received.get(timeout=45) + assert endpoint.endswith("/native-smoke/" + expected_endpoint), endpoint + return payload + except Exception: + log.seek(0) + diagnostic = log.read() + binary.with_name("native-runtime-smoke-failure.log").write_text(diagnostic) + log.seek(0) + raise AssertionError("Native Runtime smoke failed:\n" + log.read()[-12000:]) + finally: + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) + server.shutdown() + server.server_close() + thread.join(timeout=5) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("binary", type=Path) + args = parser.parse_args() + binary = args.binary.resolve(strict=True) + response = invoke(binary, "com.networknt.aws.lambda.app.LambdaApp", "response") + assert response["statusCode"] == 200, response + assert response["body"] == "OK", response + assert response["isBase64Encoded"] is False, response + error = invoke(binary, "native.smoke.MissingHandler", "error") + assert "native.smoke.MissingHandler" in error["message"], error + print("Native Runtime smoke passed: API Gateway binding, health response and builder error serialization") + + +if __name__ == "__main__": + main() diff --git a/tests/test_native_image_script.py b/tests/test_native_image_script.py index f6faad3..d81720a 100644 --- a/tests/test_native_image_script.py +++ b/tests/test_native_image_script.py @@ -70,6 +70,7 @@ def test_packages_pom_named_jar_with_https_and_java_25(self): self.assertIn("ghcr.io/graalvm/native-image-community:25", args) self.assertIn("/workspace/target/customer-runtime-9.9.jar", args) self.assertIn("--enable-url-protocols=http,https", args) + self.assertIn("-H:+AddAllCharsets", args) with ZipFile(self.root / "lambda-native-custom-runtime.zip") as archive: self.assertEqual(set(archive.namelist()), {"lambda-native", "bootstrap"}) for info in archive.infolist(): From f72dd9523a6c42e7d9fb4a1c2bd720994de6d335 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 25 Oct 2026 12:54:12 -0400 Subject: [PATCH 03/13] 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 13e3b1a..dbf6be6 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", "light-aws-lambda")) 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", "light-aws-lambda")) 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 resolve light-aws-lambda 168 - uses: actions/checkout@v4 with: @@ -80,6 +81,11 @@ jobs: repository: networknt/light-aws-lambda ref: ${{ steps.prerequisites.outputs.light_aws_lambda }} path: sources/light-aws-lambda + - 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: | @@ -101,6 +107,7 @@ 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 sources/light-aws-lambda/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: Test native packaging script @@ -113,6 +120,27 @@ jobs: run: | bash native-image-linux.sh python3 tests/native_runtime_smoke.py target/custom-runtime/lambda-native + - 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 682ceba3e3da8cf0728505d283c2a1fcd83c99e9 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 25 Oct 2026 13:01:28 -0400 Subject: [PATCH 04/13] 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 dbf6be6..f9da6b9 100644 --- a/.github/workflows/jackson3-source-build.yml +++ b/.github/workflows/jackson3-source-build.yml @@ -124,22 +124,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() @@ -149,5 +166,6 @@ jobs: source-build.log source-commits.txt source-refs.json + resolved-networknt.txt **/target/surefire-reports/** **/target/*smoke*.log From f8c4912875ef570facef539de9ba1bb4bc0bf40e Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 25 Oct 2026 14:22:32 -0400 Subject: [PATCH 05/13] Use canonical mapper policy and qualified source CI --- .github/workflows/jackson3-source-build.yml | 46 +++++-------------- README.md | 6 +++ .../java/com/networknt/aws/lambda/Main.java | 4 +- .../com/networknt/aws/lambda/TestUtils.java | 4 +- 4 files changed, 21 insertions(+), 39 deletions(-) diff --git a/.github/workflows/jackson3-source-build.yml b/.github/workflows/jackson3-source-build.yml index f9da6b9..b3b99fa 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 @@ -98,6 +99,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 @@ -120,44 +124,14 @@ jobs: run: | bash native-image-linux.sh python3 tests/native_runtime_smoke.py target/custom-runtime/lambda-native - - 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: @@ -166,6 +140,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 4a70bdb..2194433 100644 --- a/README.md +++ b/README.md @@ -145,3 +145,9 @@ The smoke check binds an API Gateway event, runs the local health handler, and v `.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/src/main/java/com/networknt/aws/lambda/Main.java b/src/main/java/com/networknt/aws/lambda/Main.java index 2b04598..276910d 100644 --- a/src/main/java/com/networknt/aws/lambda/Main.java +++ b/src/main/java/com/networknt/aws/lambda/Main.java @@ -4,14 +4,14 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.ObjectMapper; -import com.networknt.config.Jackson2Defaults; +import com.networknt.jackson.JacksonDefaults; import com.networknt.aws.lambda.app.LambdaApp; import java.io.IOException; public class Main { - 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/src/test/java/com/networknt/aws/lambda/TestUtils.java b/src/test/java/com/networknt/aws/lambda/TestUtils.java index fa39bb7..e36c1c9 100644 --- a/src/test/java/com/networknt/aws/lambda/TestUtils.java +++ b/src/test/java/com/networknt/aws/lambda/TestUtils.java @@ -4,7 +4,7 @@ import tools.jackson.core.JacksonException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.ObjectMapper; -import com.networknt.config.Jackson2Defaults; +import com.networknt.jackson.JacksonDefaults; public class TestUtils { @@ -133,7 +133,7 @@ public class TestUtils { " }\n" + "}"; - 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 APIGatewayProxyRequestEvent createTestRequestEvent() { From edaf61bbe8dd656c2bf86a07a94b98ab291e1ab4 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 25 Oct 2026 21:32:27 -0400 Subject: [PATCH 06/13] Resolve migration sources by branch and require retained core evidence --- .github/workflows/jackson3-source-build.yml | 22 +++++++++++++-------- README.md | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/jackson3-source-build.yml b/.github/workflows/jackson3-source-build.yml index b3b99fa..3fea384 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,25 +34,30 @@ 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", "light-aws-lambda")) 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 - resolve light-aws-lambda 168 + resolve light-aws-lambda migrate-jackson-3 - uses: actions/checkout@v4 with: repository: networknt/yaml-rule diff --git a/README.md b/README.md index 2194433..e24e3c0 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,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 ab46d875618ae48a8097aa295f2b1568129df674 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 25 Oct 2026 23:52:29 -0400 Subject: [PATCH 07/13] 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 3fea384..6af5aad 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 @@ -107,13 +119,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 2cc4193cc470dc15c6fdefdf641f79abcd483148 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Mon, 26 Oct 2026 16:48:31 -0400 Subject: [PATCH 08/13] Pin source CI to reviewed Jackson 3 qualification --- .github/workflows/jackson3-source-build.yml | 39 ++++----------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/.github/workflows/jackson3-source-build.yml b/.github/workflows/jackson3-source-build.yml index 6af5aad..a722a8d 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,43 +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", "light-aws-lambda")) 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,"light-aws-lambda":"migrate-jackson-3"}' --source-refs "$SOURCE_REFS" \ --report core-qualification.json --github-output "$GITHUB_OUTPUT" - resolve json-overlay - resolve openapi-parser - resolve light-rest-4j - resolve light-aws-lambda migrate-jackson-3 - uses: actions/checkout@v4 with: repository: networknt/yaml-rule From 91c4fd7eb33b48414561109bce49849cd4eb3e91 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Mon, 26 Oct 2026 16:51:52 -0400 Subject: [PATCH 09/13] Document adopted core qualification pins --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e24e3c0..869234d 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ The smoke check binds an API Gateway event, runs the local health handler, and v ### 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 bf69fedcd25887ec294a10c2e80251e5e345ff1f Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Mon, 26 Oct 2026 17:26:06 -0400 Subject: [PATCH 10/13] Pin complete source-build prerequisite closure --- .github/workflows/jackson3-source-build.yml | 3 ++- README.md | 2 +- ci/jackson3-source-refs.json | 9 +++++++++ 3 files changed, 12 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 a722a8d..0ab78c7 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,"light-aws-lambda":"migrate-jackson-3"}' --source-refs "$SOURCE_REFS" \ + --repositories '{"json-overlay":null,"openapi-parser":null,"light-rest-4j":null,"light-aws-lambda":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 869234d..9e0d9a5 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ The smoke check binds an API Gateway event, runs the local health handler, and v ### 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 0000000..a1fd3ac --- /dev/null +++ b/ci/jackson3-source-refs.json @@ -0,0 +1,9 @@ +{ + "http-client": "5ecee29dbb08d813512f5379c2e1b902d553c5d4", + "json-overlay": "e309fd63f95317067c9b13a4bed7ac16727bfea5", + "light-4j": "97b2359b92ed3e1d05cac04f2da8760aa954e49d", + "light-aws-lambda": "d68d2b32f6f72ddcd82ebf0ebfd1cf753d3886d0", + "light-rest-4j": "a31955945036d9108b7e9bb72167415cc9cfc287", + "openapi-parser": "9ae849056e49f7c796fe28bfe6093fbe4847cc5c", + "yaml-rule": "c237cc3a25720f43e57bf571e25d5fa06687aa3b" +} From 8d6db07c95f812617a2842717d7d1e46f6eb64b2 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Mon, 26 Oct 2026 17:30:15 -0400 Subject: [PATCH 11/13] 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 0ab78c7..15938a3 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-aws-lambda","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" \ From 79d5c11789844fc0bdddc3434ea2ea4bddf0b0da Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Mon, 26 Oct 2026 18:06:38 -0400 Subject: [PATCH 12/13] docs: define post-merge sibling source pin promotion --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 9e0d9a5..114f223 100644 --- a/README.md +++ b/README.md @@ -151,3 +151,14 @@ The optional `source_refs` workflow-dispatch input replays recorded full SHAs. G 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. + + +### Updating Jackson 3 sibling source pins after merge + +The source manifest deliberately pins reviewed commits, including sibling PR sources. It does not follow branch heads. Later CI-only or documentation commits in a sibling do not require repeatedly chasing its head. + +Before merging this dependent PR, merge networknt/light-aws-lambda#168. Resolve each actual merge commit through GitHub (`gh pr view --repo networknt/ --json mergeCommit`); do not substitute the old PR head or a moving branch. For codegen, merge and requalify the Lambda native dependency after AWS first. + +Fetch each sibling repository’s master and verify the selected commit is its ancestor with `git merge-base --is-ancestor origin/master`. Replace the corresponding values in `ci/jackson3-source-refs.json` with those full 40-character merge SHAs in a reviewed dependent change. Keep the remaining closure pins and the qualified core/policy SHAs unchanged. Run fresh source-build CI on that dependent commit and review its resolved-source artifact before merging. A squash or merge commit must be qualified even when its runtime tree matches the earlier pin. + +For later sibling updates after this PR merges, use the same manifest-update PR and fresh dependent CI procedure. Never silently advance pins or infer qualification from the sibling’s own green build. From 6e4737e40242e2f45e071dd3130cd48fbdec5c04 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Mon, 26 Oct 2026 21:56:02 -0400 Subject: [PATCH 13/13] Pin merged AWS Lambda Jackson 3 migration --- ci/jackson3-source-refs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/jackson3-source-refs.json b/ci/jackson3-source-refs.json index a1fd3ac..ad31c77 100644 --- a/ci/jackson3-source-refs.json +++ b/ci/jackson3-source-refs.json @@ -2,7 +2,7 @@ "http-client": "5ecee29dbb08d813512f5379c2e1b902d553c5d4", "json-overlay": "e309fd63f95317067c9b13a4bed7ac16727bfea5", "light-4j": "97b2359b92ed3e1d05cac04f2da8760aa954e49d", - "light-aws-lambda": "d68d2b32f6f72ddcd82ebf0ebfd1cf753d3886d0", + "light-aws-lambda": "dddbbc9e7f6f690f8688ceac024728b6a84f4808", "light-rest-4j": "a31955945036d9108b7e9bb72167415cc9cfc287", "openapi-parser": "9ae849056e49f7c796fe28bfe6093fbe4847cc5c", "yaml-rule": "c237cc3a25720f43e57bf571e25d5fa06687aa3b"