Skip to content

Commit 7cad67f

Browse files
committed
add script to find breaking changes
1 parent 8c11228 commit 7cad67f

File tree

2 files changed

+302
-6
lines changed

2 files changed

+302
-6
lines changed

.github/workflows/nightly-build.yml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,21 @@ jobs:
2929
exit 1
3030
fi
3131
32+
- name: Set up Node.js
33+
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 #v5.0.0
34+
with:
35+
node-version: '18'
36+
37+
- name: Check for breaking changes
38+
id: breaking_changes
39+
run: node scripts/find_breaking_changes.js
40+
3241
- name: Configure git and create branch
3342
run: |
3443
git config --local user.email "[email protected]"
3544
git config --local user.name "GitHub Action"
3645
git checkout -b "$BRANCH_NAME"
3746
38-
- name: Set up Node.js
39-
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 #v5.0.0
40-
with:
41-
node-version: '18'
42-
4347
- name: Install dependencies
4448
run: npm install
4549

@@ -62,7 +66,10 @@ jobs:
6266
6367
gh pr create \
6468
--title "Nightly dependency update: OpenTelemetry packages to latest versions" \
65-
--body "Automated update of OpenTelemetry dependencies to their latest available versions." \
69+
--body "Automated update of OpenTelemetry dependencies to their latest available versions.
70+
71+
**Upstream releases with breaking changes:**
72+
${{ steps.breaking_changes.outputs.breaking_changes_info }}" \
6673
--base main \
6774
--head "$BRANCH_NAME"
6875
fi

scripts/find_breaking_changes.js

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
6+
async function httpsGet(url) {
7+
const https = require('https');
8+
9+
return new Promise((resolve, reject) => {
10+
const options = {
11+
timeout: 30000,
12+
headers: {
13+
'User-Agent': 'Mozilla/5.0 (compatible; Node.js script)'
14+
}
15+
};
16+
17+
const request = https.get(url, options, (response) => {
18+
let data = '';
19+
response.on('data', (chunk) => data += chunk);
20+
response.on('end', () => {
21+
try {
22+
if (response.statusCode === 200) {
23+
resolve(JSON.parse(data));
24+
} else {
25+
console.warn(`Warning: HTTP ${response.statusCode} for ${url}`);
26+
resolve(null);
27+
}
28+
} catch (parseError) {
29+
console.warn(`Warning: Could not parse response for ${url}: ${parseError.message}`);
30+
resolve(null);
31+
}
32+
});
33+
});
34+
35+
request.on('error', (requestError) => {
36+
console.warn(`Warning: Request failed for ${url}: ${requestError.message}`);
37+
resolve(null);
38+
});
39+
40+
request.on('timeout', () => {
41+
request.destroy();
42+
console.warn(`Warning: Timeout for ${url}`);
43+
resolve(null);
44+
});
45+
});
46+
}
47+
48+
function getCurrentVersionsFromPackageJson() {
49+
try {
50+
const packageJsonPath = path.join('aws-distro-opentelemetry-node-autoinstrumentation', 'package.json');
51+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
52+
53+
const dependencies = packageJson.dependencies || {};
54+
55+
// Find representative versions for each category
56+
const currentVersions = {};
57+
58+
// API version
59+
if (dependencies['@opentelemetry/api']) {
60+
currentVersions.api = dependencies['@opentelemetry/api'];
61+
}
62+
63+
// Core version (use sdk-trace-base as representative)
64+
if (dependencies['@opentelemetry/sdk-trace-base']) {
65+
currentVersions.core = dependencies['@opentelemetry/sdk-trace-base'];
66+
}
67+
68+
// Experimental version (use sdk-node as representative)
69+
if (dependencies['@opentelemetry/sdk-node']) {
70+
currentVersions.experimental = dependencies['@opentelemetry/sdk-node'];
71+
}
72+
73+
// Semconv version
74+
if (dependencies['@opentelemetry/semantic-conventions']) {
75+
currentVersions.semconv = dependencies['@opentelemetry/semantic-conventions'];
76+
}
77+
78+
// Get all contrib packages we actually depend on
79+
const contribPackages = {};
80+
for (const [packageName, version] of Object.entries(dependencies)) {
81+
if (packageName.startsWith('@opentelemetry/') &&
82+
!['@opentelemetry/api', '@opentelemetry/sdk-trace-base', '@opentelemetry/sdk-node', '@opentelemetry/semantic-conventions'].includes(packageName)) {
83+
// Check if it's likely a contrib package (not in core/experimental categories)
84+
const componentName = packageName.replace('@opentelemetry/', '');
85+
contribPackages[componentName] = version;
86+
}
87+
}
88+
89+
currentVersions.contrib = contribPackages;
90+
91+
return currentVersions;
92+
93+
} catch (error) {
94+
console.warn(`Error reading current versions: ${error.message}`);
95+
return {};
96+
}
97+
}
98+
99+
function compareVersions(current, target) {
100+
// Simple version comparison - assumes semver format
101+
const currentParts = current.split('.').map(Number);
102+
const targetParts = target.split('.').map(Number);
103+
104+
for (let i = 0; i < Math.max(currentParts.length, targetParts.length); i++) {
105+
const currentPart = currentParts[i] || 0;
106+
const targetPart = targetParts[i] || 0;
107+
108+
if (currentPart < targetPart) return -1;
109+
if (currentPart > targetPart) return 1;
110+
}
111+
112+
return 0;
113+
}
114+
115+
async function findBreakingChangesInReleases(repoName, currentVersion, newVersion, releasePattern) {
116+
try {
117+
const releases = await httpsGet(`https://api.github.com/repos/open-telemetry/${repoName}/releases?per_page=100`);
118+
if (!releases) return [];
119+
120+
const breakingReleases = [];
121+
122+
for (const release of releases) {
123+
const tagName = release.tag_name;
124+
let releaseVersion = null;
125+
126+
// Extract version based on pattern
127+
if (releasePattern === 'core' && /^v\d+\.\d+\.\d+$/.test(tagName)) {
128+
releaseVersion = tagName.substring(1);
129+
} else if (releasePattern === 'experimental' && tagName.startsWith('experimental/v')) {
130+
releaseVersion = tagName.substring('experimental/v'.length);
131+
} else if (releasePattern === 'api' && tagName.startsWith('api/v')) {
132+
releaseVersion = tagName.substring('api/v'.length);
133+
} else if (releasePattern === 'semconv' && tagName.startsWith('semconv/v')) {
134+
releaseVersion = tagName.substring('semconv/v'.length);
135+
}
136+
137+
if (releaseVersion) {
138+
// Check if this release is between current and new version
139+
if (compareVersions(releaseVersion, currentVersion) > 0 &&
140+
compareVersions(releaseVersion, newVersion) <= 0) {
141+
142+
// Check if release notes mention breaking changes
143+
const body = release.body || '';
144+
if (body.includes('💥 Breaking Changes')) {
145+
breakingReleases.push({
146+
version: releaseVersion,
147+
name: release.name || tagName,
148+
url: release.html_url
149+
});
150+
}
151+
}
152+
}
153+
}
154+
155+
return breakingReleases;
156+
157+
} catch (error) {
158+
console.warn(`Warning: Could not get releases for ${repoName}: ${error.message}`);
159+
return [];
160+
}
161+
}
162+
163+
async function findContribBreakingChanges(currentContribPackages, newContribVersions) {
164+
try {
165+
const releases = await httpsGet('https://api.github.com/repos/open-telemetry/opentelemetry-js-contrib/releases?per_page=100');
166+
if (!releases) return [];
167+
168+
const breakingReleases = [];
169+
170+
for (const release of releases) {
171+
const tagName = release.tag_name;
172+
173+
// Extract component name and version from releases like "auto-instrumentations-node: v0.64.4"
174+
const match = tagName.match(/^([^:]+):\s*v(.+)$/);
175+
if (match) {
176+
const componentName = match[1];
177+
const releaseVersion = match[2];
178+
179+
// Check if this is a package we depend on
180+
if (currentContribPackages[componentName]) {
181+
const currentVersion = currentContribPackages[componentName];
182+
const newVersion = newContribVersions[componentName];
183+
184+
if (newVersion &&
185+
compareVersions(releaseVersion, currentVersion) > 0 &&
186+
compareVersions(releaseVersion, newVersion) <= 0) {
187+
188+
// Check if release notes mention breaking changes
189+
const body = release.body || '';
190+
if (body.includes('⚠ BREAKING CHANGES')) {
191+
breakingReleases.push({
192+
component: componentName,
193+
version: releaseVersion,
194+
name: release.name || tagName,
195+
url: release.html_url
196+
});
197+
}
198+
}
199+
}
200+
}
201+
}
202+
203+
return breakingReleases;
204+
205+
} catch (error) {
206+
console.warn(`Warning: Could not get contrib releases: ${error.message}`);
207+
return [];
208+
}
209+
}
210+
async function main() {
211+
const newCoreVersion = process.env.OTEL_CORE_VERSION;
212+
const newExperimentalVersion = process.env.OTEL_EXPERIMENTAL_VERSION;
213+
const newApiVersion = process.env.OTEL_API_VERSION;
214+
const newSemconvVersion = process.env.OTEL_SEMCONV_VERSION;
215+
216+
if (!newCoreVersion && !newExperimentalVersion && !newApiVersion && !newSemconvVersion) {
217+
console.error('Error: At least one version environment variable required');
218+
process.exit(1);
219+
}
220+
221+
const currentVersions = getCurrentVersionsFromPackageJson();
222+
223+
console.log('Checking for breaking changes in JS releases...');
224+
225+
let breakingInfo = '';
226+
227+
// Check core releases
228+
if (newCoreVersion && currentVersions.core) {
229+
const coreBreaking = await findBreakingChangesInReleases(
230+
'opentelemetry-js',
231+
currentVersions.core,
232+
newCoreVersion,
233+
'core'
234+
);
235+
236+
if (coreBreaking.length > 0) {
237+
breakingInfo += '**opentelemetry-js (core):**\n';
238+
for (const release of coreBreaking) {
239+
breakingInfo += `- [${release.name}](${release.url})\n`;
240+
}
241+
}
242+
}
243+
244+
// Check experimental releases
245+
if (newExperimentalVersion && currentVersions.experimental) {
246+
const experimentalBreaking = await findBreakingChangesInReleases(
247+
'opentelemetry-js',
248+
currentVersions.experimental,
249+
newExperimentalVersion,
250+
'experimental'
251+
);
252+
253+
if (experimentalBreaking.length > 0) {
254+
breakingInfo += '**opentelemetry-js (experimental):**\n';
255+
for (const release of experimentalBreaking) {
256+
breakingInfo += `- [${release.name}](${release.url})\n`;
257+
}
258+
}
259+
}
260+
261+
// Check contrib releases for packages we actually depend on
262+
if (currentVersions.contrib) {
263+
// We need to get the new contrib versions from the update script
264+
// For now, we'll check all contrib packages we depend on
265+
const contribBreaking = await findContribBreakingChanges(currentVersions.contrib, {});
266+
267+
if (contribBreaking.length > 0) {
268+
breakingInfo += '**opentelemetry-js-contrib:**\n';
269+
for (const release of contribBreaking) {
270+
breakingInfo += `- [${release.name}](${release.url})\n`;
271+
}
272+
}
273+
}
274+
275+
// Set GitHub output
276+
if (process.env.GITHUB_OUTPUT) {
277+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `breaking_changes_info<<EOF\n${breakingInfo}EOF\n`);
278+
}
279+
280+
if (breakingInfo) {
281+
console.log('Breaking changes found');
282+
} else {
283+
console.log('No breaking changes found');
284+
}
285+
}
286+
287+
if (require.main === module) {
288+
main().catch(console.error);
289+
}

0 commit comments

Comments
 (0)