Skip to content

Commit 1fc635a

Browse files
Merge 14b365d into 93137d1
2 parents 93137d1 + 14b365d commit 1fc635a

File tree

3 files changed

+268
-1
lines changed

3 files changed

+268
-1
lines changed

.github/workflows/danger.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,9 @@ on:
66

77
jobs:
88
danger:
9-
uses: getsentry/github-workflows/.github/workflows/danger.yml@v2
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: lucas-zimerman/sentry-github-workflows/danger@lz/ext-danger
12+
with:
13+
extra-dangerfile: scripts/check-replay-stubs.js
14+
# test3
-1.17 KB
Binary file not shown.

scripts/check-replay-stubs.js

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

0 commit comments

Comments
 (0)