agent-ecosystem/scripts/ci/verify-sentry-release.cjs
2026-05-18 01:57:16 +03:00

131 lines
3.7 KiB
JavaScript

#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const repoRoot = path.resolve(__dirname, '..', '..');
const pkg = require(path.join(repoRoot, 'package.json'));
const REQUIRED_ENV = ['SENTRY_DSN', 'SENTRY_AUTH_TOKEN', 'SENTRY_ORG', 'SENTRY_PROJECT'];
const OUTPUT_DIRS = ['dist-electron/main', 'out/renderer'];
const SENTRY_DEBUG_ID_RE = /\/\/# debugId=[a-fA-F0-9-]+/;
function fail(message) {
console.error(`[sentry-release] ${message}`);
process.exit(1);
}
function isTaggedRelease() {
return /^refs\/tags\/v/.test(process.env.GITHUB_REF ?? '');
}
function assertTaggedReleaseEnv() {
if (!isTaggedRelease()) {
console.log('[sentry-release] skipped: not a tag release');
return false;
}
const missing = REQUIRED_ENV.filter((name) => !String(process.env[name] ?? '').trim());
if (missing.length > 0) {
fail(`missing required env for source map upload: ${missing.join(', ')}`);
}
if (!String(process.env.SENTRY_DSN).startsWith('https://')) {
fail('SENTRY_DSN must be an https DSN');
}
const tagVersion = String(process.env.GITHUB_REF).replace(/^refs\/tags\/v/, '');
if (pkg.version !== tagVersion) {
fail(`package version ${pkg.version} does not match release tag v${tagVersion}`);
}
return true;
}
function walkFiles(relativeDir) {
const absoluteDir = path.join(repoRoot, relativeDir);
if (!fs.existsSync(absoluteDir)) {
return [];
}
const files = [];
const stack = [absoluteDir];
while (stack.length > 0) {
const current = stack.pop();
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const entryPath = path.join(current, entry.name);
if (entry.isDirectory()) {
stack.push(entryPath);
} else if (entry.isFile()) {
files.push(entryPath);
}
}
}
return files;
}
function prebuild() {
if (!assertTaggedReleaseEnv()) return;
console.log(
`[sentry-release] prebuild ok: release=agent-teams-ai@${pkg.version}, project=${process.env.SENTRY_ORG}/${process.env.SENTRY_PROJECT}`
);
}
function postbuild() {
if (!assertTaggedReleaseEnv()) return;
const jsFilesByOutputDir = new Map();
for (const outputDir of OUTPUT_DIRS) {
const jsFiles = walkFiles(outputDir).filter((file) => /\.(?:js|cjs|mjs)$/.test(file));
if (jsFiles.length === 0) {
fail(`no built JavaScript files found in ${outputDir}`);
}
jsFilesByOutputDir.set(outputDir, jsFiles);
}
const jsFiles = [...jsFilesByOutputDir.values()].flat();
if (jsFiles.length === 0) {
fail(`no built JavaScript files found in ${OUTPUT_DIRS.join(', ')}`);
}
const missingDebugIdDirs = [];
for (const [outputDir, files] of jsFilesByOutputDir.entries()) {
const hasDebugId = files.some((file) => SENTRY_DEBUG_ID_RE.test(fs.readFileSync(file, 'utf8')));
if (!hasDebugId) {
missingDebugIdDirs.push(outputDir);
}
}
if (missingDebugIdDirs.length > 0) {
fail(
[
'Sentry debug IDs were not injected into built JavaScript artifacts',
...missingDebugIdDirs.map((dir) => ` - ${dir}`),
].join('\n')
);
}
const mapFiles = OUTPUT_DIRS.flatMap(walkFiles).filter((file) => file.endsWith('.map'));
if (mapFiles.length > 0) {
fail(
[
'source maps still exist after build; expected Sentry upload to delete them',
...mapFiles.slice(0, 20).map((file) => ` - ${path.relative(repoRoot, file)}`),
].join('\n')
);
}
console.log(
`[sentry-release] postbuild ok: ${jsFiles.length} JS artifacts built, debug IDs were injected, and source maps were removed after upload`
);
}
const command = process.argv[2] ?? 'prebuild';
if (command === 'prebuild') {
prebuild();
} else if (command === 'postbuild') {
postbuild();
} else {
fail(`unknown command: ${command}`);
}