Created
October 22, 2025 20:52
-
-
Save ZackDeRose/d57a094cfb709b5701af8233673981de to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| /** | |
| * JENKINS DISCOVERY SCRIPT | |
| * | |
| * This script inspects a Jenkins job to discover ALL available data fields | |
| * in the build responses. Use this to understand what data your Jenkins | |
| * instance provides, then write a custom extraction script based on the results. | |
| * | |
| * USAGE: | |
| * node jenkins-discovery.js | |
| * | |
| * REQUIRED ENVIRONMENT VARIABLES: | |
| * JENKINS_URL - Jenkins server base URL (e.g., https://jenkins.mycompany.com) | |
| * JENKINS_JOB_NAME - Jenkins job name to inspect | |
| * JENKINS_USER - Jenkins username for authentication | |
| * JENKINS_TOKEN - Jenkins API token for authentication | |
| * | |
| * OPTIONAL ENVIRONMENT VARIABLES: | |
| * SAMPLE_SIZE - Number of recent builds to inspect (default: 3) | |
| * | |
| * EXAMPLES: | |
| * # Basic usage | |
| * JENKINS_URL=https://jenkins.mycompany.com JENKINS_JOB_NAME=MyProject \ | |
| * JENKINS_USER=myuser JENKINS_TOKEN=xxx \ | |
| * node jenkins-discovery.js | |
| * | |
| * # Inspect more builds | |
| * SAMPLE_SIZE=5 \ | |
| * JENKINS_URL=https://jenkins.mycompany.com JENKINS_JOB_NAME=MyProject \ | |
| * JENKINS_USER=myuser JENKINS_TOKEN=xxx \ | |
| * node jenkins-discovery.js | |
| * | |
| * OUTPUT: | |
| * - Displays complete build data structure for sample builds | |
| * - Shows all available fields and their actual values | |
| * - Highlights fields that commonly contain Git commit info | |
| * - Saves detailed JSON report for analysis | |
| * - Use this data to write custom extraction logic per client | |
| */ | |
| const { writeFileSync } = require('fs'); | |
| const { join } = require('path'); | |
| // ============================================================================ | |
| // CONFIGURATION | |
| // ============================================================================ | |
| const JENKINS_URL = process.env.JENKINS_URL; | |
| const JENKINS_JOB_NAME = process.env.JENKINS_JOB_NAME; | |
| const JENKINS_USER = process.env.JENKINS_USER; | |
| const JENKINS_TOKEN = process.env.JENKINS_TOKEN; | |
| const SAMPLE_SIZE = parseInt(process.env.SAMPLE_SIZE || '3', 10); | |
| // ============================================================================ | |
| // UTILITY FUNCTIONS | |
| // ============================================================================ | |
| /** | |
| * Make Jenkins API request | |
| * @param {string} url | |
| * @returns {Promise<any>} | |
| */ | |
| async function makeJenkinsRequest(url) { | |
| const authString = Buffer.from(`${JENKINS_USER}:${JENKINS_TOKEN}`).toString( | |
| 'base64' | |
| ); | |
| const response = await fetch(url, { | |
| headers: { | |
| Authorization: `Basic ${authString}`, | |
| Accept: 'application/json', | |
| }, | |
| }); | |
| if (!response.ok) { | |
| throw new Error( | |
| `Jenkins API request failed: ${response.status} ${response.statusText}` | |
| ); | |
| } | |
| return response.json(); | |
| } | |
| /** | |
| * Pretty print JSON with indentation | |
| * @param {any} obj | |
| * @param {number} indent | |
| * @returns {string} | |
| */ | |
| function prettyPrintJSON(obj, indent = 2) { | |
| return JSON.stringify(obj, null, indent); | |
| } | |
| /** | |
| * Recursively find all paths to leaf values in an object | |
| * @param {any} obj | |
| * @param {string} prefix | |
| * @returns {Array<{path: string, value: any, type: string}>} | |
| */ | |
| function getAllPaths(obj, prefix = '') { | |
| const paths = []; | |
| if (obj === null || obj === undefined) { | |
| paths.push({ path: prefix, value: obj, type: typeof obj }); | |
| return paths; | |
| } | |
| if (typeof obj !== 'object') { | |
| paths.push({ path: prefix, value: obj, type: typeof obj }); | |
| return paths; | |
| } | |
| if (Array.isArray(obj)) { | |
| if (obj.length === 0) { | |
| paths.push({ path: prefix, value: '[]', type: 'empty-array' }); | |
| } else { | |
| obj.forEach((item, index) => { | |
| const itemPaths = getAllPaths(item, `${prefix}[${index}]`); | |
| paths.push(...itemPaths); | |
| }); | |
| } | |
| } else { | |
| const keys = Object.keys(obj); | |
| if (keys.length === 0) { | |
| paths.push({ path: prefix, value: '{}', type: 'empty-object' }); | |
| } else { | |
| keys.forEach(key => { | |
| const newPrefix = prefix ? `${prefix}.${key}` : key; | |
| const childPaths = getAllPaths(obj[key], newPrefix); | |
| paths.push(...childPaths); | |
| }); | |
| } | |
| } | |
| return paths; | |
| } | |
| /** | |
| * Check if a value looks like a Git commit SHA | |
| * @param {any} value | |
| * @returns {boolean} | |
| */ | |
| function looksLikeCommitSHA(value) { | |
| if (typeof value !== 'string') return false; | |
| // 40-char hex string | |
| return /^[0-9a-f]{40}$/i.test(value.trim()); | |
| } | |
| /** | |
| * Check if a path or value suggests commit/SHA information | |
| * @param {string} path | |
| * @param {any} value | |
| * @returns {boolean} | |
| */ | |
| function looksLikeCommitField(path, value) { | |
| const pathLower = path.toLowerCase(); | |
| const containsCommitKeywords = | |
| pathLower.includes('commit') || | |
| pathLower.includes('sha') || | |
| pathLower.includes('revision') || | |
| pathLower.includes('git'); | |
| return containsCommitKeywords || looksLikeCommitSHA(value); | |
| } | |
| /** | |
| * Analyze a build and display all available data | |
| * @param {any} build | |
| * @param {number} index | |
| * @returns {Object} | |
| */ | |
| function analyzeBuild(build, index) { | |
| console.log(`\n${'='.repeat(80)}`); | |
| console.log(`BUILD #${build.number} (Sample ${index + 1}/${SAMPLE_SIZE})`); | |
| console.log(`${'='.repeat(80)}`); | |
| console.log(`\n๐ Basic Info:`); | |
| console.log(` Result: ${build.result || 'N/A'}`); | |
| console.log(` Building: ${build.building}`); | |
| console.log(` Duration: ${Math.round(build.duration / 1000)}s`); | |
| console.log(` Timestamp: ${new Date(build.timestamp).toISOString()}`); | |
| console.log(` URL: ${build.url || 'N/A'}`); | |
| // Get all paths in the build object | |
| const allPaths = getAllPaths(build); | |
| // Find fields that look like commit info | |
| const commitFields = allPaths.filter(p => looksLikeCommitField(p.path, p.value)); | |
| if (commitFields.length > 0) { | |
| console.log(`\n๐ POTENTIAL COMMIT/SHA FIELDS (${commitFields.length} found):`); | |
| commitFields.forEach(field => { | |
| const valuePreview = typeof field.value === 'string' && field.value.length > 60 | |
| ? field.value.substring(0, 60) + '...' | |
| : field.value; | |
| const isSHA = looksLikeCommitSHA(field.value); | |
| const marker = isSHA ? 'โ [COMMIT SHA]' : '๐'; | |
| console.log(` ${marker} ${field.path}`); | |
| console.log(` Value: ${valuePreview}`); | |
| console.log(` Type: ${field.type}`); | |
| }); | |
| } else { | |
| console.log(`\nโ ๏ธ NO COMMIT/SHA FIELDS DETECTED`); | |
| console.log(` This may mean:`); | |
| console.log(` - Commits are stored in unexpected field names`); | |
| console.log(` - This Jenkins job doesn't track Git commits`); | |
| console.log(` - Need to inspect full data structure below`); | |
| } | |
| // Show complete structure in collapsed format | |
| console.log(`\n๐ COMPLETE DATA STRUCTURE (all ${allPaths.length} fields):`); | |
| console.log(` (First 50 fields shown, full data in JSON report)`); | |
| const fieldsToShow = allPaths.slice(0, 50); | |
| fieldsToShow.forEach(field => { | |
| const valuePreview = typeof field.value === 'string' && field.value.length > 40 | |
| ? field.value.substring(0, 40) + '...' | |
| : JSON.stringify(field.value); | |
| console.log(` ${field.path}: ${valuePreview}`); | |
| }); | |
| if (allPaths.length > 50) { | |
| console.log(` ... and ${allPaths.length - 50} more fields (see JSON report)`); | |
| } | |
| return { | |
| buildNumber: build.number, | |
| totalFields: allPaths.length, | |
| commitFieldsFound: commitFields.length, | |
| commitFields: commitFields.map(f => ({ path: f.path, value: f.value })), | |
| allFields: allPaths, | |
| }; | |
| } | |
| /** | |
| * Main execution function | |
| */ | |
| async function runDiscovery() { | |
| console.log('๐ Jenkins Discovery Tool'); | |
| console.log('=' .repeat(80)); | |
| // Validate environment variables | |
| if (!JENKINS_URL) { | |
| throw new Error('JENKINS_URL environment variable is required'); | |
| } | |
| if (!JENKINS_JOB_NAME) { | |
| throw new Error('JENKINS_JOB_NAME environment variable is required'); | |
| } | |
| if (!JENKINS_USER) { | |
| throw new Error('JENKINS_USER environment variable is required'); | |
| } | |
| if (!JENKINS_TOKEN) { | |
| throw new Error('JENKINS_TOKEN environment variable is required'); | |
| } | |
| console.log(`\n๐ Configuration:`); | |
| console.log(` Jenkins URL: ${JENKINS_URL}`); | |
| console.log(` Job Name: ${JENKINS_JOB_NAME}`); | |
| console.log(` Sample Size: ${SAMPLE_SIZE} builds`); | |
| // Fetch job info | |
| console.log(`\n๐ Fetching job information...`); | |
| const jobUrl = `${JENKINS_URL}/job/${JENKINS_JOB_NAME}/api/json?tree=builds[number,result,building,duration,timestamp,url,actions[parameters[name,value],causes[_class,userId,userName,shortDescription]],changeSet[items[commitId,author[fullName]]]]`; | |
| let jobData; | |
| try { | |
| jobData = await makeJenkinsRequest(jobUrl); | |
| } catch (error) { | |
| console.error(`\nโ Failed to fetch job data:`); | |
| console.error(` ${error instanceof Error ? error.message : String(error)}`); | |
| console.error(`\n๐ก Troubleshooting tips:`); | |
| console.error(` 1. Verify JENKINS_URL is correct: ${JENKINS_URL}`); | |
| console.error(` 2. Verify job name is correct: ${JENKINS_JOB_NAME}`); | |
| console.error(` 3. Check that your Jenkins token has read permissions`); | |
| console.error(` 4. Ensure the Jenkins job exists and is accessible`); | |
| process.exit(1); | |
| } | |
| if (!jobData.builds || jobData.builds.length === 0) { | |
| console.log(`\nโ ๏ธ No builds found for job: ${JENKINS_JOB_NAME}`); | |
| console.log(` This job may not have been run yet.`); | |
| process.exit(0); | |
| } | |
| console.log(`โ Found ${jobData.builds.length} total builds`); | |
| // Analyze sample builds | |
| const buildsToAnalyze = jobData.builds.slice(0, SAMPLE_SIZE); | |
| console.log(`\n๐ฌ Analyzing ${buildsToAnalyze.length} recent builds...`); | |
| const analysisResults = buildsToAnalyze.map((build, index) => | |
| analyzeBuild(build, index) | |
| ); | |
| // Summary | |
| console.log(`\n\n${'='.repeat(80)}`); | |
| console.log(`DISCOVERY SUMMARY`); | |
| console.log(`${'='.repeat(80)}`); | |
| // Aggregate commit fields across all builds | |
| const allCommitFields = new Map(); // path -> count | |
| analysisResults.forEach(result => { | |
| result.commitFields.forEach(field => { | |
| const count = allCommitFields.get(field.path) || 0; | |
| allCommitFields.set(field.path, count + 1); | |
| }); | |
| }); | |
| const stats = { | |
| totalAnalyzed: analysisResults.length, | |
| totalBuildsInJob: jobData.builds.length, | |
| avgFieldsPerBuild: Math.round( | |
| analysisResults.reduce((sum, r) => sum + r.totalFields, 0) / analysisResults.length | |
| ), | |
| buildsWithCommitFields: analysisResults.filter(r => r.commitFieldsFound > 0).length, | |
| uniqueCommitFieldPaths: allCommitFields.size, | |
| }; | |
| console.log(`\n๐ Statistics:`); | |
| console.log(` Total builds in job: ${stats.totalBuildsInJob}`); | |
| console.log(` Builds analyzed: ${stats.totalAnalyzed}`); | |
| console.log(` Average fields per build: ${stats.avgFieldsPerBuild}`); | |
| console.log(` Builds with potential commit fields: ${stats.buildsWithCommitFields}/${stats.totalAnalyzed}`); | |
| console.log(` Unique commit-related field paths: ${stats.uniqueCommitFieldPaths}`); | |
| // Show most common commit field paths | |
| if (allCommitFields.size > 0) { | |
| console.log(`\n๐ฏ MOST COMMON COMMIT FIELD PATHS:`); | |
| const sortedFields = Array.from(allCommitFields.entries()) | |
| .sort((a, b) => b[1] - a[1]) | |
| .slice(0, 10); | |
| sortedFields.forEach(([path, count]) => { | |
| const percentage = Math.round((count / stats.totalAnalyzed) * 100); | |
| console.log(` ${percentage}% (${count}/${stats.totalAnalyzed}) - ${path}`); | |
| }); | |
| console.log(`\n๐ก NEXT STEPS:`); | |
| console.log(` 1. Review the JSON report for full data structure`); | |
| console.log(` 2. Identify which field paths contain the actual Git commit SHAs`); | |
| console.log(` 3. Create a custom extraction function based on these paths`); | |
| console.log(` 4. Update github-with-jenkins-ttg.js with your custom logic`); | |
| } else { | |
| console.log(`\nโ ๏ธ NO COMMIT-RELATED FIELDS FOUND`); | |
| console.log(`\n๐ก NEXT STEPS:`); | |
| console.log(` 1. Review the JSON report to manually inspect the data structure`); | |
| console.log(` 2. Look for fields that might contain commit information`); | |
| console.log(` 3. This Jenkins job may not track Git commits at all`); | |
| console.log(` 4. Consider configuring Jenkins to store commit SHA in build parameters`); | |
| } | |
| // Save detailed report | |
| const reportPath = join(process.cwd(), `jenkins-discovery-${JENKINS_JOB_NAME}-${Date.now()}.json`); | |
| const report = { | |
| timestamp: new Date().toISOString(), | |
| jenkins_url: JENKINS_URL, | |
| job_name: JENKINS_JOB_NAME, | |
| sample_size: SAMPLE_SIZE, | |
| total_builds_in_job: stats.totalBuildsInJob, | |
| statistics: stats, | |
| commit_field_frequencies: Object.fromEntries(allCommitFields), | |
| builds_analyzed: analysisResults, | |
| raw_builds_sample: buildsToAnalyze, // Include ALL sample builds | |
| }; | |
| writeFileSync(reportPath, JSON.stringify(report, null, 2)); | |
| console.log(`\n๐ Detailed JSON report saved to: ${reportPath}`); | |
| console.log(` Use this file to create custom extraction logic for this client.`); | |
| console.log(`\nโ Discovery complete!`); | |
| } | |
| // ============================================================================ | |
| // SCRIPT EXECUTION | |
| // ============================================================================ | |
| // Run the discovery | |
| runDiscovery().catch((error) => { | |
| console.error('\nโ Discovery failed:', error); | |
| process.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment