This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch 
feature/staleness-test-coverage-and-release-notes
in repository https://gitbox.apache.org/repos/asf/skywalking-graalvm-distro.git

commit 8ddae1f763b21a15c4ad118d71b4137be4be7ba8
Author: Wu Sheng <[email protected]>
AuthorDate: Sun Mar 15 19:55:55 2026 +0800

    Add replacement class coverage check and 0.2.0 release notes
    
    - ReplacementClassStalenessTest: add auto-discovery pass that scans
      oap-libs-for-graalvm/ for untracked same-FQCN replacements
    - release.sh: verify changes/changes.md has release notes before proceeding
    - Add 0.2.0 changes section with upstream sync commit reference
---
 changes/changes.md                                 |  42 +++++++
 .../graalvm/ReplacementClassStalenessTest.java     | 132 +++++++++++++++++++--
 release/release.sh                                 |  12 ++
 3 files changed, 176 insertions(+), 10 deletions(-)

diff --git a/changes/changes.md b/changes/changes.md
index 89f1294..9836004 100644
--- a/changes/changes.md
+++ b/changes/changes.md
@@ -1,5 +1,47 @@
 # Changes
 
+## 0.2.0
+
+### Highlights
+
+Upgrade to the latest Apache SkyWalking OAP server, with documentation 
restructure and CI/CD improvements.
+
+### Upstream Sync
+
+- Sync SkyWalking submodule to upstream commit `64a1795d8a`.
+
+### Documentation
+
+- Add user-facing docs: Quick Start, Supported Features, FAQ.
+- Move internal build-time docs to `docs/internals/`.
+- Update `docs/README.md` with "For Users" / "For Contributors" sections and 
official doc site link.
+- Add Docker Hub README (`docker/DOCKERHUB_README.md`).
+- Add release guide (`docs/release-guide.md`).
+- Update root `README.md` with project intro, quick start, and image registry 
table.
+
+### CI/CD
+
+- Push Docker images to Docker Hub (release only) in addition to GHCR.
+- Docker Hub only receives `latest` and version tags — no commit SHA tags.
+- Add `.asf.yaml` branch protection.
+- PR-only `cancel-in-progress` to avoid cancelling release builds.
+
+### Release Tooling
+
+- `release/release.sh`: auto-create SVN `graalvm-distro` directory if it 
doesn't exist.
+- `release/full-release.sh`: end-to-end release script.
+- Generate vote email template with GPG signer info and submodule commit IDs.
+
+### Testing
+
+- Replacement class staleness detector: add auto-discovery coverage check for 
untracked same-FQCN replacements in `oap-libs-for-graalvm/`.
+
+### E2E Tests
+
+- Update BanyanDB to `e1ba421` (fixes Zipkin `minDuration` trace query).
+- Bump Istio to 1.28.0.
+- Add Baseline e2e test case.
+
 ## 0.1.0
 
 ### Highlights
diff --git 
a/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/ReplacementClassStalenessTest.java
 
b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/ReplacementClassStalenessTest.java
index d49d15f..dc8e7f7 100644
--- 
a/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/ReplacementClassStalenessTest.java
+++ 
b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/ReplacementClassStalenessTest.java
@@ -24,8 +24,11 @@ import java.nio.file.Path;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Stream;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -34,10 +37,18 @@ import static org.junit.jupiter.api.Assertions.fail;
 /**
  * Staleness detector for same-FQCN replacement classes.
  *
- * <p>Tracks the SHA-256 of each upstream source file that has a same-FQCN
- * replacement in this project. If an upstream file changes (e.g. after a
- * {@code skywalking/} submodule update), this test fails with a clear message
- * indicating which replacement(s) need review and update.
+ * <p>Two verification passes:
+ * <ol>
+ *   <li><b>SHA-256 check</b>: For every entry in
+ *       {@code replacement-source-sha256.properties}, verify the upstream 
source
+ *       file still exists at the recorded path and its content hash matches.
+ *       A mismatch means the replacement needs review and update.</li>
+ *   <li><b>Coverage check</b>: Scan all {@code .java} files under
+ *       {@code oap-libs-for-graalvm/} with {@code org.apache.skywalking} 
package,
+ *       map each to its upstream counterpart in {@code skywalking/}, and 
verify
+ *       it is tracked in the properties file. An untracked replacement means
+ *       someone added a same-FQCN override without recording its upstream 
SHA.</li>
+ * </ol>
  *
  * <p>SHA-256 hashes are recorded in {@code 
replacement-source-sha256.properties}
  * (same pattern as {@code precompiled-yaml-sha256.properties}).
@@ -46,6 +57,12 @@ class ReplacementClassStalenessTest {
 
     private static final String PROPS_RESOURCE = 
"replacement-source-sha256.properties";
 
+    /**
+     * Base package prefix for same-FQCN replacements. Only classes under this
+     * package in oap-libs-for-graalvm are expected to have upstream 
counterparts.
+     */
+    private static final String SKYWALKING_PACKAGE = "org/apache/skywalking/";
+
     @Test
     void allReplacementSourcesMatchRecordedSha256() throws Exception {
         Properties props = loadProperties();
@@ -75,17 +92,21 @@ class ReplacementClassStalenessTest {
         }
 
         StringBuilder msg = new StringBuilder();
+        if (!missing.isEmpty()) {
+            msg.append("Upstream source files no longer exist — replacement is 
orphaned:\n");
+            for (String m : missing) {
+                msg.append("  ").append(m).append('\n');
+            }
+            msg.append("  Action: check if upstream renamed/moved/deleted the 
class,\n");
+            msg.append("  then update or remove the replacement and its 
properties entry.\n\n");
+        }
         if (!mismatches.isEmpty()) {
             msg.append("Upstream source files changed — review and update 
these replacements:\n");
             for (String m : mismatches) {
                 msg.append(m).append('\n');
             }
-        }
-        if (!missing.isEmpty()) {
-            msg.append("Upstream source files not found:\n");
-            for (String m : missing) {
-                msg.append("  ").append(m).append('\n');
-            }
+            msg.append("  Action: diff the upstream change, update the 
replacement class,\n");
+            msg.append("  then run: shasum -a 256 <path> to update the 
properties file.\n\n");
         }
 
         if (msg.length() > 0) {
@@ -93,6 +114,97 @@ class ReplacementClassStalenessTest {
         }
     }
 
+    /**
+     * Scan all replacement .java files under oap-libs-for-graalvm/ with
+     * org.apache.skywalking package and verify each has a corresponding entry
+     * in replacement-source-sha256.properties. This catches new replacements
+     * that were added without recording the upstream SHA.
+     */
+    @Test
+    void allReplacementClassesAreTracked() throws Exception {
+        Properties props = loadProperties();
+        Path projectRoot = Path.of(System.getProperty("user.dir")).getParent();
+        Path libsDir = projectRoot.resolve("oap-libs-for-graalvm");
+
+        if (!Files.isDirectory(libsDir)) {
+            return; // skip if directory doesn't exist (e.g. clean checkout)
+        }
+
+        // Collect all tracked upstream paths for quick lookup
+        Set<String> trackedUpstreamPaths = new HashSet<>();
+        for (String key : props.stringPropertyNames()) {
+            trackedUpstreamPaths.add(key);
+        }
+
+        // Scan oap-libs-for-graalvm for replacement .java files
+        List<String> untracked = new ArrayList<>();
+        try (Stream<Path> walk = Files.walk(libsDir)) {
+            walk.filter(p -> p.toString().endsWith(".java"))
+                .filter(p -> p.toString().contains("src/main/java/"))
+                .forEach(replacementFile -> {
+                    // Extract the relative path after src/main/java/
+                    String fullPath = replacementFile.toString();
+                    int srcIdx = fullPath.indexOf("src/main/java/");
+                    if (srcIdx < 0) return;
+                    String classRelPath = fullPath.substring(srcIdx + 
"src/main/java/".length());
+
+                    // Only check org.apache.skywalking classes
+                    if (!classRelPath.startsWith(SKYWALKING_PACKAGE)) return;
+
+                    // Try to find the upstream source file
+                    String upstreamPath = resolveUpstreamPath(projectRoot, 
classRelPath);
+                    if (upstreamPath != null && 
!trackedUpstreamPaths.contains(upstreamPath)) {
+                        untracked.add(String.format(
+                            "  replacement: %s%n  upstream:    %s",
+                            projectRoot.relativize(replacementFile), 
upstreamPath));
+                    }
+                });
+        }
+
+        if (!untracked.isEmpty()) {
+            StringBuilder msg = new StringBuilder();
+            msg.append("Replacement classes not tracked in 
").append(PROPS_RESOURCE).append(":\n");
+            for (String u : untracked) {
+                msg.append(u).append('\n');
+            }
+            msg.append("  Action: add the upstream path and its SHA-256 to 
").append(PROPS_RESOURCE).append('\n');
+            msg.append("  Run: shasum -a 256 <upstream-path>\n");
+            fail(msg.toString());
+        }
+    }
+
+    /**
+     * Resolve the upstream source path for a replacement class.
+     * Searches under skywalking/ for a .java file matching the same relative
+     * class path (e.g. org/apache/skywalking/.../Foo.java).
+     *
+     * @return relative path from project root (e.g. 
skywalking/oap-server/.../Foo.java),
+     *         or null if no upstream source found
+     */
+    private static String resolveUpstreamPath(Path projectRoot, String 
classRelPath) {
+        Path skywalking = projectRoot.resolve("skywalking");
+        if (!Files.isDirectory(skywalking)) return null;
+
+        try (Stream<Path> walk = Files.walk(skywalking)) {
+            return walk
+                .filter(p -> p.toString().endsWith(".java"))
+                .filter(p -> !p.toString().contains("/target/"))
+                .filter(p -> !p.toString().contains("/test/"))
+                .filter(p -> {
+                    String s = p.toString();
+                    int idx = s.indexOf("src/main/java/");
+                    if (idx < 0) return false;
+                    String rel = s.substring(idx + "src/main/java/".length());
+                    return rel.equals(classRelPath);
+                })
+                .map(p -> projectRoot.relativize(p).toString())
+                .findFirst()
+                .orElse(null);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
     private Properties loadProperties() throws IOException {
         Properties props = new Properties();
         try (InputStream is = getClass().getClassLoader()
diff --git a/release/release.sh b/release/release.sh
index 4458e9d..02d2b3f 100755
--- a/release/release.sh
+++ b/release/release.sh
@@ -112,6 +112,18 @@ git rev-parse "${TAG}" >/dev/null 2>&1 \
 gh release view "${TAG}" --repo "${REPO}" >/dev/null 2>&1 \
     || error "GitHub Release for ${TAG} not found. Run the CI release workflow 
first."
 
+# Verify changes/changes.md contains a section for this version
+CHANGES_FILE="${REPO_ROOT}/changes/changes.md"
+[[ -f "${CHANGES_FILE}" ]] || error "changes/changes.md not found"
+if ! grep -q "^## ${VERSION}$" "${CHANGES_FILE}"; then
+    error "changes/changes.md does not contain a '## ${VERSION}' section. Add 
release notes before releasing."
+fi
+# Verify the section has content (at least one non-empty line before next ## 
or EOF)
+SECTION_CONTENT=$(sed -n "/^## ${VERSION}$/,/^## /p" "${CHANGES_FILE}" | sed 
'1d;/^## /d' | grep -v '^$' | head -1)
+[[ -n "${SECTION_CONTENT}" ]] \
+    || error "changes/changes.md has a '## ${VERSION}' section but it is 
empty. Add release notes."
+log "changes/changes.md: found release notes for ${VERSION}"
+
 # ─── Step 1: Prepare release directory 
────────────────────────────────────────
 log "Preparing release directory..."
 rm -rf "${SCRIPT_DIR}/${RELEASE_DIR}"

Reply via email to