#!/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) { console.warn( [ '[sentry-release] warning: Sentry debug ID comments were not found in 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 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}`); }