Skip to content

Commit 942017f

Browse files
Add replay stubs check to danger workflow
1 parent c94a927 commit 942017f

File tree

3 files changed

+210
-1
lines changed

3 files changed

+210
-1
lines changed

.github/workflows/danger.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@ jobs:
88
danger:
99
runs-on: ubuntu-latest
1010
steps:
11-
- uses: getsentry/github-workflows/danger@v3
11+
- uses: lucas-zimerman/sentry-github-workflows/danger@lz/ext-danger
12+
with:
13+
extra-dangerfile: scripts/check-replay-stubs.js
14+
extra-install-packages: "curl unzip openjdk-17-jre-headless"
75.3 KB
Binary file not shown.

scripts/check-replay-stubs.js

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
const { execFileSync } = require("child_process");
2+
const fs = require("fs");
3+
const path = require("path");
4+
5+
const createSectionWarning = (title, content, icon = "🤖") => {
6+
return `### ${icon} ${title}\n\n${content}\n`;
7+
};
8+
9+
function validatePath(dirPath) {
10+
const resolved = path.resolve(dirPath);
11+
const cwd = process.cwd();
12+
if (!resolved.startsWith(cwd)) {
13+
throw new Error(`Invalid path: ${dirPath} is outside working directory`);
14+
}
15+
return resolved;
16+
}
17+
18+
function getFilesSha(dirPath, prefix = '') {
19+
const crypto = require('crypto');
20+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
21+
const results = [];
22+
23+
for (const entry of entries) {
24+
const fullPath = path.join(dirPath, entry.name);
25+
const relativePath = path.join(prefix, entry.name);
26+
27+
if (entry.isDirectory()) {
28+
results.push(...getFilesSha(fullPath, relativePath).split('\n').filter(Boolean));
29+
} else if (entry.isFile()) {
30+
const fileContent = fs.readFileSync(fullPath, 'utf8');
31+
const hash = crypto.createHash('sha256').update(fileContent).digest('hex');
32+
results.push(`${relativePath} : ${hash}`);
33+
}
34+
}
35+
return results.sort().join('\n');
36+
}
37+
38+
function getStubDiffMessage(oldHashMap, newHashMap, oldSrc, newSrc) {
39+
let fileDiffs = [];
40+
41+
// Check for added, removed, and modified files
42+
const allFiles = new Set([...oldHashMap.keys(), ...newHashMap.keys()]);
43+
44+
for (const file of allFiles) {
45+
const oldHash = oldHashMap.get(file);
46+
const newHash = newHashMap.get(file);
47+
48+
if (!oldHash && newHash) {
49+
// File added
50+
fileDiffs.push(`**Added:** \`${file}\``);
51+
const newFilePath = path.join(newSrc, file);
52+
if (fs.existsSync(newFilePath)) {
53+
const content = fs.readFileSync(newFilePath, 'utf8');
54+
fileDiffs.push('```java\n' + content + '\n```\n');
55+
}
56+
} else if (oldHash && !newHash) {
57+
// File removed
58+
fileDiffs.push(`**Removed:** \`${file}\``);
59+
const oldFilePath = path.join(oldSrc, file);
60+
if (fs.existsSync(oldFilePath)) {
61+
const content = fs.readFileSync(oldFilePath, 'utf8');
62+
fileDiffs.push('```java\n' + content + '\n```\n');
63+
}
64+
} else if (oldHash !== newHash) {
65+
// File modified - show diff
66+
fileDiffs.push(`**Modified:** \`${file}\``);
67+
const oldFilePath = path.join(oldSrc, file);
68+
const newFilePath = path.join(newSrc, file);
69+
70+
// Create temp files for diff if originals don't exist
71+
const oldExists = fs.existsSync(oldFilePath);
72+
const newExists = fs.existsSync(newFilePath);
73+
74+
if (oldExists && newExists) {
75+
try {
76+
const diff = execFileSync("diff", ["-u", oldFilePath, newFilePath], { encoding: 'utf8' });
77+
fileDiffs.push('```diff\n' + diff + '\n```\n');
78+
} catch (error) {
79+
// diff returns exit code 1 when files differ
80+
if (error.stdout) {
81+
fileDiffs.push('```diff\n' + error.stdout + '\n```\n');
82+
} else {
83+
fileDiffs.push('_(Could not generate diff)_\n');
84+
}
85+
}
86+
} else {
87+
fileDiffs.push(`_(File missing: old=${oldExists}, new=${newExists})_\n`);
88+
}
89+
}
90+
}
91+
92+
return fileDiffs.join('\n');
93+
}
94+
95+
module.exports = async function ({ fail, warn, __, ___, danger }) {
96+
const replayJarChanged = danger.git.modified_files.includes(
97+
"packages/core/android/libs/replay-stubs.jar"
98+
);
99+
100+
if (!replayJarChanged) {
101+
console.log("replay-stubs.jar not changed, skipping check.");
102+
return;
103+
}
104+
105+
console.log("Running replay stubs check...");
106+
107+
const jsDist = validatePath(path.join(process.cwd(), "js-dist"));
108+
const newSrc = validatePath(path.join(process.cwd(), "replay-stubs-src"));
109+
const oldSrc = validatePath(path.join(process.cwd(), "replay-stubs-old-src"));
110+
111+
[jsDist, newSrc, oldSrc].forEach(dir => {
112+
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
113+
});
114+
115+
// Cleanup handler for temporary files (defined inside so it has access to the variables)
116+
const cleanup = () => {
117+
[jsDist, newSrc, oldSrc].forEach(dir => {
118+
if (fs.existsSync(dir)) {
119+
fs.rmSync(dir, { recursive: true, force: true });
120+
}
121+
});
122+
};
123+
124+
process.on('exit', cleanup);
125+
process.on('SIGINT', cleanup);
126+
process.on('SIGTERM', cleanup);
127+
128+
// Tool for decompiling JARs.
129+
execFileSync("curl", ["-L", "-o", `${jsDist}/jd-cli.zip`, "https://github.com/intoolswetrust/jd-cli/releases/download/jd-cli-1.2.0/jd-cli-1.2.0-dist.zip"]);
130+
execFileSync("unzip", ["-o", `${jsDist}/jd-cli.zip`, "-d", jsDist]);
131+
132+
const newJarPath = path.join(jsDist, "replay-stubs.jar");
133+
fs.copyFileSync("packages/core/android/libs/replay-stubs.jar", newJarPath);
134+
135+
const baseJarPath = path.join(jsDist, "replay-stubs-old.jar");
136+
137+
// Validate git ref to prevent command injection
138+
const baseRef = danger.github.pr.base.ref;
139+
if (!/^[a-zA-Z0-9/_-]+$/.test(baseRef)) {
140+
throw new Error(`Invalid git ref: ${baseRef}`);
141+
}
142+
143+
try {
144+
const baseJarUrl = `https://github.com/getsentry/sentry-react-native/raw/${baseRef}/packages/core/android/libs/replay-stubs.jar`;
145+
console.log(`Downloading baseline jar from: ${baseJarUrl}`);
146+
execFileSync("curl", ["-L", "-o", baseJarPath, baseJarUrl]);
147+
} catch (error) {
148+
console.log('⚠️ Warning: Could not retrieve baseline replay-stubs.jar. Using empty file as fallback.');
149+
fs.writeFileSync(baseJarPath, '');
150+
}
151+
152+
const newJarSize = fs.statSync(newJarPath).size;
153+
const baseJarSize = fs.existsSync(baseJarPath) ? fs.statSync(baseJarPath).size : 0;
154+
155+
console.log(`File sizes - New: ${newJarSize} bytes, Baseline: ${baseJarSize} bytes`);
156+
157+
if (baseJarSize === 0) {
158+
console.log('⚠️ Baseline jar is empty, skipping decompilation comparison.');
159+
warn(createSectionWarning("Replay Stubs Check", "⚠️ Could not retrieve baseline replay-stubs.jar for comparison. This may be the first time this file is being added."));
160+
return;
161+
}
162+
163+
console.log(`Decompiling Stubs.`);
164+
try {
165+
execFileSync("java", ["-jar", `${jsDist}/jd-cli.jar`, "-od", newSrc, newJarPath]);
166+
execFileSync("java", ["-jar", `${jsDist}/jd-cli.jar`, "-od", oldSrc, baseJarPath]);
167+
} catch (error) {
168+
console.log('Error during decompilation:', error.message);
169+
warn(createSectionWarning("Replay Stubs Check", `❌ Error during JAR decompilation: ${error.message}`));
170+
return;
171+
}
172+
173+
console.log(`Comparing Stubs.`);
174+
175+
// Get complete directory listings with all details
176+
const newListing = getFilesSha(newSrc);
177+
const oldListing = getFilesSha(oldSrc);
178+
179+
if (oldListing !== newListing) {
180+
// Structural changes detected - show actual file diffs
181+
console.log("🚨 Structural changes detected in replay-stubs.jar");
182+
183+
const oldHashes = oldListing.split('\n').filter(Boolean);
184+
const newHashes = newListing.split('\n').filter(Boolean);
185+
186+
// Parse hash listings into maps
187+
const oldHashMap = new Map(oldHashes.map(line => {
188+
const [file, hash] = line.split(' : ');
189+
return [file, hash];
190+
}));
191+
192+
const newHashMap = new Map(newHashes.map(line => {
193+
const [file, hash] = line.split(' : ');
194+
return [file, hash];
195+
}));
196+
197+
let diffMessage = '🚨 **Structural changes detected** in replay-stubs.jar:\n\n'
198+
+ getStubDiffMessage(oldHashMap, newHashMap, oldSrc, newSrc);
199+
200+
warn(createSectionWarning("Replay Stubs Check", diffMessage));
201+
} else {
202+
console.log("✅ replay-stubs.jar content is identical (same SHA-256 hashes)");
203+
warn(createSectionWarning("Replay Stubs Check", `✅ **No changes detected** in replay-stubs.jar\n\nAll file contents are identical (verified by SHA-256 hash comparison).`));
204+
}
205+
};
206+

0 commit comments

Comments
 (0)