[feat] Add CI checks for OSS license compliance and telemetry tree-shaking

Implements automated verification to ensure the OSS distribution:
1. Only includes open-source licensed dependencies
2. Properly tree-shakes proprietary fonts (ABCROM)
3. Removes telemetry code (Mixpanel) from OSS builds

New scripts:
- scripts/verify-licenses.js - Validates production dependency licenses
- scripts/verify-oss-build.js - Checks dist/ for violations

New CI workflow:
- .github/workflows/ci-oss-compliance.yaml - Runs compliance checks

New npm scripts:
- pnpm verify:licenses - Check dependency licenses
- pnpm verify:oss - Verify OSS build compliance
- pnpm verify:compliance - Run all checks
- pnpm build:oss - Build OSS distribution

Documentation:
- docs/OSS_COMPLIANCE.md - Complete guide for compliance checks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
snomiao
2025-11-20 06:12:36 +00:00
parent 87d6d18c57
commit 8dcdcfce5b
5 changed files with 750 additions and 0 deletions

225
scripts/verify-licenses.js Normal file
View File

@@ -0,0 +1,225 @@
/**
* CI Script: Verify License Compliance
*
* This script verifies that all production dependencies use open-source compatible licenses.
* It checks against a list of approved licenses and flags any non-compliant dependencies.
*
* Usage: node scripts/verify-licenses.js
*
* Exit codes:
* - 0: All licenses are compliant
* - 1: Non-compliant licenses found
*/
import { execSync } from 'child_process'
const COLORS = {
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
reset: '\x1b[0m'
}
// Approved open-source licenses
// Based on OSI-approved licenses and common permissive licenses
const APPROVED_LICENSES = new Set([
'MIT',
'Apache-2.0',
'BSD-2-Clause',
'BSD-3-Clause',
'ISC',
'CC0-1.0',
'CC-BY-3.0',
'CC-BY-4.0',
'Unlicense',
'WTFPL',
'0BSD',
'BlueOak-1.0.0',
'Python-2.0',
'Zlib',
// GPL is acceptable for libraries as long as we're GPL-3.0-only
'GPL-2.0',
'GPL-3.0',
'GPL-3.0-only',
'LGPL-2.1',
'LGPL-3.0',
'MPL-2.0',
// Public domain
'Public Domain',
'Unlicensed'
])
// Known problematic licenses
const PROBLEMATIC_LICENSES = new Set([
'UNLICENSED',
'CUSTOM',
'SEE LICENSE IN LICENSE',
'PROPRIETARY'
])
/**
* Parse pnpm licenses output
*/
function getLicenses() {
console.log(
`${COLORS.blue}Fetching production dependency licenses...${COLORS.reset}`
)
try {
const output = execSync('pnpm licenses list --json --prod', {
encoding: 'utf-8',
maxBuffer: 10 * 1024 * 1024 // 10MB buffer
})
const licenses = JSON.parse(output)
return licenses
} catch (err) {
console.error(
`${COLORS.red}Error fetching licenses: ${err.message}${COLORS.reset}`
)
process.exit(1)
}
}
/**
* Normalize license names for comparison
*/
function normalizeLicense(license) {
if (!license) return 'UNKNOWN'
// Handle common variations
const normalized = license.trim().replace(/\s+/g, '-').toUpperCase()
// Handle "OR" clauses - take the first license
if (normalized.includes(' OR ')) {
return normalized.split(' OR ')[0].trim()
}
// Handle "AND" clauses - if any license is approved, consider it approved
if (normalized.includes(' AND ')) {
const licenses = normalized.split(' AND ')
for (const lic of licenses) {
const trimmed = lic.trim()
if (APPROVED_LICENSES.has(trimmed)) {
return trimmed
}
}
}
return normalized
}
/**
* Check if a license is approved
*/
function isLicenseApproved(license) {
const normalized = normalizeLicense(license)
// Check exact match
if (APPROVED_LICENSES.has(normalized)) {
return true
}
// Check if any approved license is a substring (handles variations)
for (const approved of APPROVED_LICENSES) {
if (normalized.includes(approved.toUpperCase())) {
return true
}
}
return false
}
/**
* Main verification function
*/
function main() {
console.log(
`${COLORS.blue}========================================${COLORS.reset}`
)
console.log(`${COLORS.blue}License Compliance Verification${COLORS.reset}`)
console.log(
`${COLORS.blue}========================================${COLORS.reset}\n`
)
const licenses = getLicenses()
const violations = []
const warnings = []
let totalPackages = 0
// Check each license group
for (const [license, packages] of Object.entries(licenses)) {
for (const pkg of packages) {
totalPackages++
const isApproved = isLicenseApproved(license)
const isProblematic = PROBLEMATIC_LICENSES.has(normalizeLicense(license))
if (isProblematic || !isApproved) {
violations.push({
package: pkg.name,
version: pkg.versions[0],
license: license,
isProblematic
})
} else if (license === 'UNKNOWN' || !license) {
warnings.push({
package: pkg.name,
version: pkg.versions[0],
license: 'UNKNOWN'
})
}
}
}
// Report warnings
if (warnings.length > 0) {
console.log(
`${COLORS.yellow}⚠ Packages with unknown licenses (${warnings.length}):${COLORS.reset}`
)
warnings.forEach(({ package: name, version }) => {
console.log(` ${COLORS.yellow}- ${name}@${version}${COLORS.reset}`)
})
console.log()
}
// Report violations
if (violations.length > 0) {
console.log(
`${COLORS.red}✗ Found ${violations.length} package(s) with non-compliant licenses:${COLORS.reset}\n`
)
violations.forEach(({ package: name, version, license, isProblematic }) => {
console.log(` ${COLORS.red}Package: ${name}@${version}${COLORS.reset}`)
console.log(` ${COLORS.red}License: ${license}${COLORS.reset}`)
if (isProblematic) {
console.log(
` ${COLORS.red}⚠ This license is known to be problematic${COLORS.reset}`
)
}
console.log()
})
console.log(
`${COLORS.blue}========================================${COLORS.reset}`
)
console.log(`${COLORS.red}✗ License verification failed!${COLORS.reset}`)
console.log(
`${COLORS.red}Please review and update dependencies with non-compliant licenses.${COLORS.reset}\n`
)
process.exit(1)
}
// Success
console.log(
`${COLORS.blue}========================================${COLORS.reset}`
)
console.log(
`${COLORS.green}✓ All ${totalPackages} production dependencies use approved licenses!${COLORS.reset}\n`
)
process.exit(0)
}
main()

250
scripts/verify-oss-build.js Normal file
View File

@@ -0,0 +1,250 @@
/**
* CI Script: Verify OSS Build Compliance
*
* This script verifies that the OSS build (DISTRIBUTION=localhost) does not contain:
* 1. Proprietary licensed files (e.g., ABCROM font)
* 2. Telemetry code (e.g., mixpanel library references)
*
* Usage: node scripts/verify-oss-build.js
*
* Exit codes:
* - 0: All checks passed
* - 1: Violations found
*/
import { readFileSync, readdirSync, statSync } from 'fs'
import { join, extname, dirname } from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const DIST_DIR = join(__dirname, '..', 'dist')
const COLORS = {
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
reset: '\x1b[0m'
}
// Patterns to check for violations
const VIOLATION_PATTERNS = {
// Proprietary font checks
font: {
patterns: [/ABCROM/gi, /ABCROMExtended/gi, /ABC\s*ROM/gi],
description: 'ABCROM proprietary font references'
},
// Telemetry checks
telemetry: {
patterns: [
/mixpanel/gi,
/MixpanelTelemetryProvider/gi,
/mp\.comfy\.org/gi,
/mixpanel-browser/gi,
/trackWorkflow/g,
/trackEvent/g,
/\.track\s*\(/g
],
description: 'Mixpanel telemetry code'
}
}
// File extensions to check
const JS_EXTENSIONS = ['.js', '.mjs', '.cjs']
const FONT_EXTENSIONS = ['.woff', '.woff2', '.ttf', '.otf']
/**
* Recursively get all files in a directory
*/
function getAllFiles(dir, extensions = null) {
const files = []
try {
const items = readdirSync(dir)
for (const item of items) {
const fullPath = join(dir, item)
const stat = statSync(fullPath)
if (stat.isDirectory()) {
files.push(...getAllFiles(fullPath, extensions))
} else if (stat.isFile()) {
if (!extensions || extensions.includes(extname(fullPath))) {
files.push(fullPath)
}
}
}
} catch (err) {
console.error(
`${COLORS.red}Error reading directory ${dir}: ${err.message}${COLORS.reset}`
)
}
return files
}
/**
* Check if file content contains violation patterns
*/
function checkFileForViolations(filePath, violationConfig) {
try {
const content = readFileSync(filePath, 'utf-8')
const violations = []
for (const pattern of violationConfig.patterns) {
const matches = content.match(pattern)
if (matches && matches.length > 0) {
violations.push({
pattern: pattern.toString(),
matches: matches.length,
sample: matches[0]
})
}
}
return violations
} catch (err) {
// Binary files or read errors - skip
return []
}
}
/**
* Check for proprietary font files
*/
function checkForFontFiles() {
console.log(
`\n${COLORS.blue}Checking for proprietary font files...${COLORS.reset}`
)
const fontFiles = getAllFiles(DIST_DIR, FONT_EXTENSIONS)
const violations = []
for (const fontFile of fontFiles) {
const fileName = fontFile.toLowerCase()
if (fileName.includes('abcrom')) {
violations.push(fontFile)
}
}
if (violations.length > 0) {
console.log(
`${COLORS.red}✗ Found ${violations.length} proprietary font file(s):${COLORS.reset}`
)
violations.forEach((file) => {
console.log(` ${COLORS.red}- ${file}${COLORS.reset}`)
})
return false
} else {
console.log(
`${COLORS.green}✓ No proprietary font files found${COLORS.reset}`
)
return true
}
}
/**
* Check JavaScript files for code violations
*/
function checkJavaScriptFiles() {
console.log(
`\n${COLORS.blue}Checking JavaScript files for code violations...${COLORS.reset}`
)
const jsFiles = getAllFiles(DIST_DIR, JS_EXTENSIONS)
const allViolations = {}
for (const [violationType, config] of Object.entries(VIOLATION_PATTERNS)) {
allViolations[violationType] = []
for (const jsFile of jsFiles) {
const violations = checkFileForViolations(jsFile, config)
if (violations.length > 0) {
allViolations[violationType].push({
file: jsFile,
violations
})
}
}
}
let hasViolations = false
for (const [violationType, config] of Object.entries(VIOLATION_PATTERNS)) {
const violations = allViolations[violationType]
if (violations.length > 0) {
hasViolations = true
console.log(
`\n${COLORS.red}✗ Found ${config.description} in ${violations.length} file(s):${COLORS.reset}`
)
violations.forEach(({ file, violations: fileViolations }) => {
console.log(`\n ${COLORS.yellow}${file}${COLORS.reset}`)
fileViolations.forEach(({ pattern, matches, sample }) => {
console.log(` ${COLORS.red}Pattern: ${pattern}${COLORS.reset}`)
console.log(` ${COLORS.red}Matches: ${matches}${COLORS.reset}`)
console.log(` ${COLORS.red}Sample: "${sample}"${COLORS.reset}`)
})
})
} else {
console.log(
`${COLORS.green}✓ No ${config.description} found${COLORS.reset}`
)
}
}
return !hasViolations
}
/**
* Main verification function
*/
function main() {
console.log(
`${COLORS.blue}========================================${COLORS.reset}`
)
console.log(`${COLORS.blue}OSS Build Verification${COLORS.reset}`)
console.log(
`${COLORS.blue}========================================${COLORS.reset}`
)
console.log(`${COLORS.blue}Checking: ${DIST_DIR}${COLORS.reset}`)
// Check if dist directory exists
try {
statSync(DIST_DIR)
} catch (err) {
console.error(
`\n${COLORS.red}Error: dist/ directory not found. Please run 'pnpm build' first.${COLORS.reset}`
)
process.exit(1)
}
// Run checks
const fontCheckPassed = checkForFontFiles()
const codeCheckPassed = checkJavaScriptFiles()
// Summary
console.log(
`\n${COLORS.blue}========================================${COLORS.reset}`
)
console.log(`${COLORS.blue}Verification Summary${COLORS.reset}`)
console.log(
`${COLORS.blue}========================================${COLORS.reset}`
)
if (fontCheckPassed && codeCheckPassed) {
console.log(
`${COLORS.green}✓ All checks passed! OSS build is compliant.${COLORS.reset}\n`
)
process.exit(0)
} else {
console.log(
`${COLORS.red}✗ Verification failed! Please fix the violations above.${COLORS.reset}\n`
)
process.exit(1)
}
}
main()