Skip to content

Instantly share code, notes, and snippets.

@ZackDeRose
Created October 22, 2025 20:52
Show Gist options
  • Select an option

  • Save ZackDeRose/d57a094cfb709b5701af8233673981de to your computer and use it in GitHub Desktop.

Select an option

Save ZackDeRose/d57a094cfb709b5701af8233673981de to your computer and use it in GitHub Desktop.
#!/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