Skip to content

Commit 2d88f86

Browse files
committed
Issue #109 Add fingerprint-based issue deduplication and auto-issue creation
- Add generateFingerprint() to create SHA256-based 7-char hash of API diffs - Add generateSummary() to create markdown summary for issue body - Add hasDifferences() helper method - Update ApiTrackerRunner to write outputs to target/api-tracker/ - Update Java 25 workflow to: - Upload report artifacts - Check for existing issues with matching fingerprint - Create new issue only if no match found
1 parent 9116a33 commit 2d88f86

File tree

3 files changed

+222
-1
lines changed

3 files changed

+222
-1
lines changed

.github/workflows/api-tracker-java25.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,68 @@ jobs:
3232
run: mvn clean install
3333

3434
- name: Run API Tracker
35+
id: tracker
3536
run: |
3637
mvn exec:java \
3738
-pl json-java21-api-tracker \
3839
-Dexec.mainClass="io.github.simbo1905.tracker.ApiTrackerRunner" \
3940
-Dexec.args="INFO" \
4041
-Djava.util.logging.ConsoleHandler.level=INFO
42+
43+
# Read outputs into environment
44+
echo "fingerprint=$(cat json-java21-api-tracker/target/api-tracker/fingerprint.txt)" >> $GITHUB_OUTPUT
45+
echo "has_differences=$(cat json-java21-api-tracker/target/api-tracker/has-differences.txt)" >> $GITHUB_OUTPUT
46+
47+
- name: Upload API report artifact
48+
uses: actions/upload-artifact@v4
49+
with:
50+
name: api-tracker-report
51+
path: json-java21-api-tracker/target/api-tracker/
52+
retention-days: 90
53+
54+
- name: Check for existing issue
55+
if: steps.tracker.outputs.has_differences == 'true'
56+
id: check_issue
57+
env:
58+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59+
run: |
60+
FINGERPRINT="${{ steps.tracker.outputs.fingerprint }}"
61+
echo "Looking for existing issue with hash:${FINGERPRINT}"
62+
63+
# Search for open issues with this fingerprint
64+
EXISTING=$(gh issue list --state open --search "hash:${FINGERPRINT} in:title" --json number --jq '.[0].number // empty')
65+
66+
if [ -n "$EXISTING" ]; then
67+
echo "Found existing issue #${EXISTING}"
68+
echo "issue_exists=true" >> $GITHUB_OUTPUT
69+
echo "existing_issue=${EXISTING}" >> $GITHUB_OUTPUT
70+
else
71+
echo "No existing issue found"
72+
echo "issue_exists=false" >> $GITHUB_OUTPUT
73+
fi
74+
75+
- name: Create issue for API differences
76+
if: steps.tracker.outputs.has_differences == 'true' && steps.check_issue.outputs.issue_exists == 'false'
77+
env:
78+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
79+
run: |
80+
FINGERPRINT="${{ steps.tracker.outputs.fingerprint }}"
81+
SUMMARY=$(cat json-java21-api-tracker/target/api-tracker/summary.md)
82+
83+
# Create issue body
84+
cat > /tmp/issue_body.md << EOF
85+
${SUMMARY}
86+
87+
## Details
88+
89+
- **Fingerprint**: \`hash:${FINGERPRINT}\`
90+
- **Workflow Run**: [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
91+
- **Artifact**: Download the full JSON report from the workflow artifacts
92+
93+
This issue was auto-generated by the API Tracker workflow.
94+
EOF
95+
96+
gh issue create \
97+
--title "API drift detected [hash:${FINGERPRINT}]" \
98+
--body-file /tmp/issue_body.md \
99+
--label "api-tracking,upstream-sync"

json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import java.net.http.HttpRequest;
1414
import java.net.http.HttpResponse;
1515
import java.nio.charset.StandardCharsets;
16+
import java.security.MessageDigest;
17+
import java.security.NoSuchAlgorithmException;
1618
import java.time.Duration;
1719
import java.time.Instant;
1820
import java.util.*;
@@ -947,4 +949,117 @@ static String fetchUpstreamSource(String className) {
947949
FETCH_CACHE.put(className, source);
948950
return source;
949951
}
952+
953+
/// Generates a SHA256 fingerprint of the differences (first 7 chars)
954+
/// Used for deduplicating GitHub issues
955+
/// @param report the full comparison report
956+
/// @return 7-character fingerprint or "0000000" if no differences
957+
static String generateFingerprint(JsonObject report) {
958+
final var summary = (JsonObject) report.members().get("summary");
959+
final var differentApi = ((JsonNumber) summary.members().get("differentApi")).toNumber().longValue();
960+
961+
if (differentApi == 0) {
962+
return "0000000";
963+
}
964+
965+
// Extract just the differences array for fingerprinting
966+
final var differences = (JsonArray) report.members().get("differences");
967+
final var diffsOnly = differences.values().stream()
968+
.filter(v -> {
969+
final var obj = (JsonObject) v;
970+
final var status = ((JsonString) obj.members().get("status")).value();
971+
return "DIFFERENT".equals(status);
972+
})
973+
.toList();
974+
975+
// Serialize to stable JSON string for hashing
976+
final var jsonString = JsonArray.of(diffsOnly).toString();
977+
978+
try {
979+
final var digest = MessageDigest.getInstance("SHA-256");
980+
final var hash = digest.digest(jsonString.getBytes(StandardCharsets.UTF_8));
981+
final var hexString = new StringBuilder();
982+
for (final var b : hash) {
983+
hexString.append(String.format("%02x", b));
984+
}
985+
return hexString.substring(0, 7);
986+
} catch (NoSuchAlgorithmException e) {
987+
LOGGER.warning("SHA-256 not available, using fallback fingerprint");
988+
return String.format("%07d", jsonString.hashCode() & 0xFFFFFFF);
989+
}
990+
}
991+
992+
/// Generates a terse human-readable summary of the API differences
993+
/// Suitable for GitHub issue body
994+
/// @param report the full comparison report
995+
/// @return markdown-formatted summary
996+
static String generateSummary(JsonObject report) {
997+
final var sb = new StringBuilder();
998+
final var summary = (JsonObject) report.members().get("summary");
999+
final var differences = (JsonArray) report.members().get("differences");
1000+
1001+
final var totalClasses = ((JsonNumber) summary.members().get("totalClasses")).toNumber().longValue();
1002+
final var matchingClasses = ((JsonNumber) summary.members().get("matchingClasses")).toNumber().longValue();
1003+
final var differentApi = ((JsonNumber) summary.members().get("differentApi")).toNumber().longValue();
1004+
final var missingUpstream = ((JsonNumber) summary.members().get("missingUpstream")).toNumber().longValue();
1005+
1006+
sb.append("## API Comparison Summary\n\n");
1007+
sb.append("| Metric | Count |\n");
1008+
sb.append("|--------|-------|\n");
1009+
sb.append("| Total Classes | ").append(totalClasses).append(" |\n");
1010+
sb.append("| Matching | ").append(matchingClasses).append(" |\n");
1011+
sb.append("| Different | ").append(differentApi).append(" |\n");
1012+
sb.append("| Missing Upstream | ").append(missingUpstream).append(" |\n\n");
1013+
1014+
if (differentApi > 0) {
1015+
sb.append("## Changes Detected\n\n");
1016+
1017+
for (final var diff : differences.values()) {
1018+
final var diffObj = (JsonObject) diff;
1019+
final var status = ((JsonString) diffObj.members().get("status")).value();
1020+
1021+
if (!"DIFFERENT".equals(status)) continue;
1022+
1023+
final var className = ((JsonString) diffObj.members().get("className")).value();
1024+
sb.append("### ").append(className).append("\n\n");
1025+
1026+
final var classDiffs = (JsonArray) diffObj.members().get("differences");
1027+
if (classDiffs != null) {
1028+
for (final var change : classDiffs.values()) {
1029+
final var changeObj = (JsonObject) change;
1030+
final var type = ((JsonString) changeObj.members().get("type")).value();
1031+
final var methodValue = changeObj.members().get("method");
1032+
final var method = methodValue instanceof JsonString js ? js.value() : "unknown";
1033+
1034+
final var emoji = switch (type) {
1035+
case "methodRemoved" -> "➖";
1036+
case "methodAdded" -> "➕";
1037+
case "methodChanged" -> "🔄";
1038+
case "inheritanceChanged" -> "🔗";
1039+
case "fieldsChanged" -> "📦";
1040+
case "constructorsChanged" -> "🏗️";
1041+
default -> "❓";
1042+
};
1043+
1044+
sb.append("- ").append(emoji).append(" **").append(type).append("**: `").append(method).append("`\n");
1045+
}
1046+
}
1047+
sb.append("\n");
1048+
}
1049+
}
1050+
1051+
sb.append("---\n");
1052+
sb.append("*Generated by API Tracker on ").append(Instant.now().toString().split("T")[0]).append("*\n");
1053+
1054+
return sb.toString();
1055+
}
1056+
1057+
/// Checks if there are any API differences in the report
1058+
/// @param report the comparison report
1059+
/// @return true if differentApi > 0
1060+
static boolean hasDifferences(JsonObject report) {
1061+
final var summary = (JsonObject) report.members().get("summary");
1062+
final var differentApi = ((JsonNumber) summary.members().get("differentApi")).toNumber().longValue();
1063+
return differentApi > 0;
1064+
}
9501065
}

json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import jdk.sandbox.java.util.json.Json;
44

5+
import java.io.IOException;
6+
import java.nio.file.Files;
7+
import java.nio.file.Path;
58
import java.util.logging.ConsoleHandler;
69
import java.util.logging.Level;
710
import java.util.logging.Logger;
@@ -43,7 +46,26 @@ public static void main(String[] args) {
4346

4447
// Pretty print the report
4548
System.out.println("=== Comparison Report ===");
46-
System.out.println(Json.toDisplayString(report, 2));
49+
final var jsonOutput = Json.toDisplayString(report, 2);
50+
System.out.println(jsonOutput);
51+
52+
// Generate fingerprint and summary
53+
final var fingerprint = ApiTracker.generateFingerprint(report);
54+
final var summary = ApiTracker.generateSummary(report);
55+
final var hasDiffs = ApiTracker.hasDifferences(report);
56+
57+
System.out.println();
58+
System.out.println("=== Fingerprint ===");
59+
System.out.println("hash:" + fingerprint);
60+
61+
if (hasDiffs) {
62+
System.out.println();
63+
System.out.println("=== Summary ===");
64+
System.out.println(summary);
65+
}
66+
67+
// Write outputs to files for workflow artifact upload
68+
writeOutputFiles(jsonOutput, fingerprint, summary, hasDiffs);
4769

4870
} catch (Exception e) {
4971
System.err.println("Error during comparison: " + e.getMessage());
@@ -53,6 +75,31 @@ public static void main(String[] args) {
5375
}
5476
}
5577

78+
private static void writeOutputFiles(String jsonOutput, String fingerprint, String summary, boolean hasDiffs) {
79+
try {
80+
// Create output directory
81+
final var outputDir = Path.of("target", "api-tracker");
82+
Files.createDirectories(outputDir);
83+
84+
// Write full JSON report
85+
Files.writeString(outputDir.resolve("report.json"), jsonOutput);
86+
87+
// Write fingerprint
88+
Files.writeString(outputDir.resolve("fingerprint.txt"), fingerprint);
89+
90+
// Write summary markdown
91+
Files.writeString(outputDir.resolve("summary.md"), summary);
92+
93+
// Write has-differences flag for workflow
94+
Files.writeString(outputDir.resolve("has-differences.txt"), String.valueOf(hasDiffs));
95+
96+
System.out.println();
97+
System.out.println("Output files written to: " + outputDir.toAbsolutePath());
98+
} catch (IOException e) {
99+
System.err.println("Warning: Could not write output files: " + e.getMessage());
100+
}
101+
}
102+
56103
private static void configureLogging(Level level) {
57104
// Get root logger
58105
final var rootLogger = Logger.getLogger("");

0 commit comments

Comments
 (0)