UNPKG

tdpw

Version:

CLI tool for uploading Playwright test reports to TestDino platform with TestDino storage support

972 lines 44.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GitCollector = void 0; const tslib_1 = require("tslib"); const simple_git_1 = tslib_1.__importDefault(require("simple-git")); const version_1 = require("../version"); const verbose_1 = require("../utils/verbose"); /** * Collector for Git metadata */ class GitCollector { git; repoPath; constructor(repoPath = process.cwd()) { this.repoPath = repoPath; this.git = (0, simple_git_1.default)({ baseDir: repoPath }); } /** * Configure git to work properly in CI/CD environments * Fixes "dubious ownership" errors in GitHub Actions and other CI/CD platforms */ async configureGitForCI() { const env = process.env; const isCICD = env.GITHUB_ACTIONS === 'true' || env.GITLAB_CI === 'true' || env.CIRCLECI === 'true' || env.TF_BUILD === 'True' || env.JENKINS_URL || !!env.CI; // Generic CI detection if (isCICD) { try { // Add current directory to git safe.directory to allow git operations // This fixes "fatal: detected dubious ownership in repository" errors await this.git.raw([ 'config', '--global', '--add', 'safe.directory', this.repoPath, ]); } catch { // If config fails, silently continue - git commands will fallback to env vars } } } /** * Gather Git metadata: branch, latest commit, and remote repository info * Uses git commands as primary source, falls back to environment variables in CI/CD */ async getMetadata() { // Configure git for CI/CD environments first await this.configureGitForCI(); // Try to get metadata from git commands first const gitMetadata = await this.getGitCommandMetadata(); // Get environment variable metadata as fallback const envMetadata = await this.getEnvironmentMetadata(); // Build metadata with required fields and defaults // For GitLab CI, CircleCI, Azure DevOps, and all GitHub Actions events, prioritize environment variables over git commands const isGitLabCI = process.env.GITLAB_CI === 'true'; const isCircleCI = process.env.CIRCLECI === 'true'; const isAzureDevOps = process.env.TF_BUILD === 'True' || !!process.env.AZURE_HTTP_USER_AGENT; const isGitHubActions = process.env.GITHUB_ACTIONS === 'true'; const shouldPrioritizeEnv = isGitLabCI || isCircleCI || isAzureDevOps || isGitHubActions; // Build commit object with optional authorId const commitAuthorId = shouldPrioritizeEnv ? envMetadata.authorId || gitMetadata.commit?.authorId : gitMetadata.commit?.authorId || envMetadata.authorId; const commitData = { hash: shouldPrioritizeEnv ? envMetadata.commitHash || gitMetadata.commit?.hash || 'unknown' : gitMetadata.commit?.hash || envMetadata.commitHash || 'unknown', message: shouldPrioritizeEnv ? envMetadata.commitMessage || gitMetadata.commit?.message || '' : gitMetadata.commit?.message || envMetadata.commitMessage || '', author: shouldPrioritizeEnv ? envMetadata.author || gitMetadata.commit?.author || '' : gitMetadata.commit?.author || envMetadata.author || '', email: shouldPrioritizeEnv ? envMetadata.email || gitMetadata.commit?.email || '' : gitMetadata.commit?.email || envMetadata.email || '', timestamp: gitMetadata.commit?.timestamp || new Date().toISOString(), }; // Only add authorId if it has a value if (commitAuthorId) { commitData.authorId = commitAuthorId; } const metadata = { branch: shouldPrioritizeEnv ? envMetadata.branch || gitMetadata.branch || 'unknown' : gitMetadata.branch || envMetadata.branch || 'unknown', commit: commitData, repository: { name: gitMetadata.repository?.name || envMetadata.repoName || 'unknown', url: shouldPrioritizeEnv ? envMetadata.repoUrl || gitMetadata.repository?.url || '' : gitMetadata.repository?.url || envMetadata.repoUrl || '', }, pr: { id: gitMetadata.pr?.id || envMetadata.prId || '', title: gitMetadata.pr?.title || envMetadata.prTitle || '', url: gitMetadata.pr?.url || envMetadata.prUrl || '', status: this.normalizeStatus(gitMetadata.pr?.status || envMetadata.prStatus || ''), }, }; // Debug logging when verbose mode is enabled if ((0, verbose_1.isVerboseMode)()) { const gitSuccess = Object.keys(gitMetadata).length > 0; const envFallback = Object.keys(envMetadata).length > 0; console.log(`🔍 Git metadata: ${metadata.branch}, ${gitSuccess ? 'git commands' : envFallback ? 'environment' : 'fallback'}`); } return metadata; } /** * Normalize PR status to match server enum */ normalizeStatus(status) { const normalizedStatus = status.toLowerCase(); switch (normalizedStatus) { case 'open': return 'open'; case 'draft': return 'draft'; case 'ready_for_review': return 'ready_for_review'; case 'changes_requested': return 'changes_requested'; case 'approved': return 'approved'; case 'merged': return 'merged'; case 'closed': return 'closed'; default: return ''; } } /** * Attempt to gather metadata using git commands with comprehensive error handling * CRITICAL: Ensures no data loss - always collects what's available */ async getGitCommandMetadata() { const metadata = {}; const errors = []; // Branch name with multiple fallback strategies try { const branchSummary = await this.git.branch(); metadata.branch = branchSummary.current; } catch (error) { const errMsg = error instanceof Error ? error.message : 'unknown'; errors.push(`branch: ${errMsg}`); // Fallback 1: Try git symbolic-ref try { const result = await this.git.raw(['symbolic-ref', '--short', 'HEAD']); metadata.branch = result.trim(); } catch { // Fallback 2: Try git rev-parse try { const result = await this.git.raw([ 'rev-parse', '--abbrev-ref', 'HEAD', ]); metadata.branch = result.trim(); } catch { // No branch data available - will use environment fallback } } } // Latest commit with fallback strategies try { const log = await this.git.log({ maxCount: 1 }); if (log.latest) { metadata.commit = { hash: log.latest.hash, message: log.latest.message, author: log.latest.author_name, email: log.latest.author_email, timestamp: log.latest.date, }; } } catch (error) { const errMsg = error instanceof Error ? error.message : 'unknown'; errors.push(`commit: ${errMsg}`); // Fallback: Try to get just the commit hash try { const hash = await this.git.raw(['rev-parse', 'HEAD']); metadata.commit = { hash: hash.trim(), message: '', author: '', email: '', timestamp: new Date().toISOString(), }; } catch { // No commit data available - will use environment fallback } } // Remote repo URL with comprehensive fallbacks try { const remotes = await this.git.getRemotes(true); if (remotes.length) { const origin = remotes.find(r => r.name === 'origin') || remotes[0]; if (origin) { const url = origin.refs.push || origin.refs.fetch; metadata.repository = { url, name: this.extractRepoNameFromUrl(url), }; } } } catch (error) { const errMsg = error instanceof Error ? error.message : 'unknown'; errors.push(`remote: ${errMsg}`); // Fallback: Try git config try { const url = await this.git.raw([ 'config', '--get', 'remote.origin.url', ]); const cleanUrl = url.trim(); metadata.repository = { url: cleanUrl, name: this.extractRepoNameFromUrl(cleanUrl), }; } catch { // No remote data available - will use environment fallback } } // Initialize empty PR metadata (filled by environment variables) metadata.pr = { id: '', status: '', title: '', url: '', }; // Log collection status if (errors.length > 0 && (0, verbose_1.isVerboseMode)()) { console.warn(`⚠️ Git command issues (using fallbacks): ${errors.join(', ')}`); } return metadata; } /** * Extract git metadata from environment variables (CI/CD contexts) * * Author Resolution Priority (highest to lowest): * ============================================ * For Pull Request Events: * 1. GitHub Branch Commit API (data.author.login) - Most accurate, includes user ID * 2. PR Head User Login (event file: pull_request.head.user.login) * 3. Git Author Name (git show %an) - Fallback when GitHub login unavailable * * For Non-PR Events (push, workflow_dispatch, etc.): * 1. GitHub Branch Commit API (data.author.login) - Most accurate, includes user ID * 2. GitHub Users API (fetched by username) - Fallback if commit API unavailable * 3. Git Author Name (git config user.name) - Fallback when APIs unavailable * * Rationale: GitHub login usernames are preferred for consistent author deduplication * across the platform, as git config names can vary (e.g., "John Doe" vs "John" vs "jdoe"). */ async getEnvironmentMetadata() { const env = process.env; const envInfo = {}; // GitHub Actions if (env.GITHUB_ACTIONS === 'true') { // For Pull Requests, prioritize GITHUB_HEAD_REF (source branch) if (env.GITHUB_EVENT_NAME === 'pull_request' && env.GITHUB_HEAD_REF) { envInfo.branch = env.GITHUB_HEAD_REF; // This gives us the actual source branch } else { // For push events, extract from GITHUB_REF const extractedBranch = this.extractBranchFromRef(env.GITHUB_REF); if (extractedBranch) envInfo.branch = extractedBranch; } // For PRs, try to get data from GitHub event file (works for private repos without token) if (env.GITHUB_EVENT_NAME === 'pull_request' && env.GITHUB_EVENT_PATH) { try { const fs = await Promise.resolve().then(() => tslib_1.__importStar(require('fs/promises'))); const eventData = JSON.parse(await fs.readFile(env.GITHUB_EVENT_PATH, 'utf-8')); const pullRequest = eventData?.pull_request; if (pullRequest) { // Extract PR head SHA (actual commit, not merge commit) if (pullRequest.head?.sha) { envInfo.commitHash = pullRequest.head.sha; } // Extract PR title and status if (pullRequest.title) { envInfo.prTitle = pullRequest.title; } const state = pullRequest.state; const draft = pullRequest.draft; const merged = pullRequest.merged; if (draft) { envInfo.prStatus = 'draft'; } else if (merged) { envInfo.prStatus = 'merged'; } else if (state === 'open') { envInfo.prStatus = 'open'; } else if (state === 'closed') { envInfo.prStatus = 'closed'; } // Extract commit details from the PR head commit using git show // This commit SHA is already available in the checked-out repository const prHead = pullRequest.head; if (prHead?.sha) { try { // Use git show to get commit details directly (no fetch needed - commit is local) // Format: %B (commit body/message), %an (author name), %ae (author email) const commitInfo = await this.git.show([ prHead.sha, '--no-patch', '--format=%B%n%an%n%ae', ]); const lines = commitInfo.trim().split('\n'); if (lines.length >= 3) { // Format: message (can be multiple lines), author name, author email const messageLines = lines.slice(0, -2); const message = messageLines.join('\n').trim(); const authorName = lines[lines.length - 2]; const authorEmail = lines[lines.length - 1]; // Set commit message, author, and email from git commit data if (message) { envInfo.commitMessage = message; } if (authorName) { envInfo.author = authorName; } if (authorEmail) { envInfo.email = authorEmail; } if ((0, verbose_1.isVerboseMode)()) { console.log('✓ Extracted commit metadata from git'); } } } catch (gitShowError) { // If git show fails, fallback to PR title and attempt GitHub API if ((0, verbose_1.isVerboseMode)()) { console.warn('⚠️ Could not extract commit from git:', gitShowError instanceof Error ? gitShowError.message : 'unknown'); } if (pullRequest.title && !envInfo.commitMessage) { envInfo.commitMessage = pullRequest.title; } } } } } catch (error) { // Log parsing errors when verbose mode is enabled if ((0, verbose_1.isVerboseMode)()) { console.warn('⚠️ Failed to parse GitHub event file:', error instanceof Error ? error.message : 'unknown'); } } } if (!envInfo.commitHash && env.GITHUB_SHA) envInfo.commitHash = env.GITHUB_SHA; if (env.GITHUB_REPOSITORY) envInfo.repoName = env.GITHUB_REPOSITORY; if (env.GITHUB_REPOSITORY) envInfo.repoUrl = `https://github.com/${env.GITHUB_REPOSITORY}`; // For all non-PR events, try to fetch author info from APIs if (env.GITHUB_EVENT_NAME !== 'pull_request') { // [Priority 1 - Non-PR] Try to get commit info from Branch Commit API (most accurate) if (env.GITHUB_REPOSITORY && envInfo.branch) { const branchCommitInfo = await this.fetchGitHubBranchCommit(env.GITHUB_REPOSITORY, envInfo.branch); if (branchCommitInfo) { // Use commit message from API if not already set if (branchCommitInfo.message && !envInfo.commitMessage) { envInfo.commitMessage = branchCommitInfo.message; } // Use email from API if not already set if (branchCommitInfo.email && !envInfo.email) { envInfo.email = branchCommitInfo.email; } // Always use GitHub username from Branch Commit API (highest priority) if (branchCommitInfo.author) { envInfo.author = branchCommitInfo.author; } // Save GitHub user ID for deduplication (highest priority) if (branchCommitInfo.authorId) { envInfo.authorId = branchCommitInfo.authorId; } } } // [Priority 2 - Non-PR] Fallback to GitHub Users API if Branch Commit API didn't provide login // This works for both public and private repos since we use the public Users API if (env.GITHUB_ACTOR && !envInfo.author) { const userInfo = await this.fetchGitHubUserInfo(env.GITHUB_ACTOR); if (userInfo) { // Use GitHub username from Users API (fallback if Branch Commit API unavailable) if (userInfo.login) { envInfo.author = userInfo.login; } // Save GitHub user ID for deduplication (fallback) if (userInfo.id && !envInfo.authorId) { envInfo.authorId = userInfo.id; } } } } // Pull Request info if (env.GITHUB_EVENT_NAME === 'pull_request') { // Extract PR number from GITHUB_REF (refs/pull/3279/merge -> 3279) if (env.GITHUB_REF) { const prMatch = env.GITHUB_REF.match(/refs\/pull\/(\d+)\//); if (prMatch?.[1]) { envInfo.prId = prMatch[1]; if (env.GITHUB_REPOSITORY) { envInfo.prUrl = `https://github.com/${env.GITHUB_REPOSITORY}/pull/${prMatch[1]}`; } } } // Try to get PR details from GitHub API for PRs if (env.GITHUB_REPOSITORY && envInfo.prId) { const prDetails = await this.fetchGitHubPullRequestDetails(env.GITHUB_REPOSITORY, envInfo.prId); if (prDetails) { envInfo.prTitle = prDetails.title; envInfo.prStatus = prDetails.status; } } // Try to enhance with GitHub API data for PRs (optional - for GitHub user ID) // This is an enhancement layer only - git data takes precedence for accuracy if (env.GITHUB_REPOSITORY && env.GITHUB_HEAD_REF && !envInfo.authorId) { // Only fetch from API if we don't already have authorId and if git data extraction succeeded const branchCommitInfo = await this.fetchGitHubBranchCommit(env.GITHUB_REPOSITORY, env.GITHUB_HEAD_REF); if (branchCommitInfo) { // Only use API data to fill in missing fields (don't override git data) if (!envInfo.commitMessage && branchCommitInfo.message) { envInfo.commitMessage = branchCommitInfo.message; } if (!envInfo.email && branchCommitInfo.email) { envInfo.email = branchCommitInfo.email; } if (!envInfo.commitHash && branchCommitInfo.sha) { envInfo.commitHash = branchCommitInfo.sha; } // GitHub user ID is valuable for deduplication - always use if available if (branchCommitInfo.authorId) { envInfo.authorId = branchCommitInfo.authorId; } // Only use GitHub username if git data is not available if (!envInfo.author && branchCommitInfo.author) { envInfo.author = branchCommitInfo.author; } } } } } // GitLab CI else if (env.GITLAB_CI === 'true') { // Branch name - prefer CI_COMMIT_BRANCH over CI_COMMIT_REF_NAME for accuracy if (env.CI_COMMIT_BRANCH) { envInfo.branch = env.CI_COMMIT_BRANCH; } else if (env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME) { // For merge requests, use the source branch envInfo.branch = env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME; } else if (env.CI_COMMIT_REF_NAME) { envInfo.branch = env.CI_COMMIT_REF_NAME; } if (env.CI_COMMIT_SHA) envInfo.commitHash = env.CI_COMMIT_SHA; if (env.CI_COMMIT_MESSAGE) envInfo.commitMessage = env.CI_COMMIT_MESSAGE; if (env.CI_COMMIT_AUTHOR) envInfo.author = env.CI_COMMIT_AUTHOR; if (env.CI_COMMIT_AUTHOR_EMAIL) envInfo.email = env.CI_COMMIT_AUTHOR_EMAIL; if (env.CI_PROJECT_PATH) envInfo.repoName = env.CI_PROJECT_PATH; // Clean the repository URL to remove CI token if (env.CI_REPOSITORY_URL) { // Remove the gitlab-ci-token from the URL envInfo.repoUrl = this.cleanGitLabUrl(env.CI_REPOSITORY_URL); } else if (env.CI_PROJECT_URL) { envInfo.repoUrl = env.CI_PROJECT_URL; } // Merge Request info if (env.CI_MERGE_REQUEST_IID) { envInfo.prId = env.CI_MERGE_REQUEST_IID; if (env.CI_MERGE_REQUEST_TITLE) envInfo.prTitle = env.CI_MERGE_REQUEST_TITLE; if (env.CI_PROJECT_URL) { envInfo.prUrl = `${env.CI_PROJECT_URL}/-/merge_requests/${env.CI_MERGE_REQUEST_IID}`; } } } // Azure DevOps else if (env.TF_BUILD === 'True' || env.AZURE_HTTP_USER_AGENT) { // Debug Azure DevOps environment variables if ((0, verbose_1.isVerboseMode)() || process.env.DEBUG) { console.log('🔍 Azure DevOps Branch Debug:'); console.log(' BUILD_SOURCEBRANCH:', env.BUILD_SOURCEBRANCH); console.log(' BUILD_SOURCEBRANCHNAME:', env.BUILD_SOURCEBRANCHNAME); console.log(' BUILD_REASON:', env.BUILD_REASON); console.log(' SYSTEM_PULLREQUEST_SOURCEBRANCH:', env.SYSTEM_PULLREQUEST_SOURCEBRANCH); console.log(' SYSTEM_PULLREQUEST_TARGETBRANCH:', env.SYSTEM_PULLREQUEST_TARGETBRANCH); } // Branch handling - prioritize PR source branch for pull requests if (env.BUILD_REASON === 'PullRequest' && env.SYSTEM_PULLREQUEST_SOURCEBRANCH) { // For PRs, use the source branch envInfo.branch = env.SYSTEM_PULLREQUEST_SOURCEBRANCH.startsWith('refs/heads/') ? env.SYSTEM_PULLREQUEST_SOURCEBRANCH.replace('refs/heads/', '') : env.SYSTEM_PULLREQUEST_SOURCEBRANCH; } else if (env.BUILD_SOURCEBRANCH) { envInfo.branch = env.BUILD_SOURCEBRANCH.startsWith('refs/heads/') ? env.BUILD_SOURCEBRANCH.replace('refs/heads/', '') : env.BUILD_SOURCEBRANCH; } else if (env.BUILD_SOURCEBRANCHNAME) { envInfo.branch = env.BUILD_SOURCEBRANCHNAME; } if ((0, verbose_1.isVerboseMode)() || process.env.DEBUG) { console.log(' Extracted branch:', envInfo.branch); } // Commit info if (env.BUILD_SOURCEVERSION) envInfo.commitHash = env.BUILD_SOURCEVERSION; if (env.BUILD_SOURCEVERSIONMESSAGE) envInfo.commitMessage = env.BUILD_SOURCEVERSIONMESSAGE; // Author info if (env.BUILD_REQUESTEDFOR) envInfo.author = env.BUILD_REQUESTEDFOR; if (env.BUILD_REQUESTEDFOREMAIL) envInfo.email = env.BUILD_REQUESTEDFOREMAIL; // Repository info with URL cleaning if (env.BUILD_REPOSITORY_NAME) { envInfo.repoName = env.BUILD_REPOSITORY_NAME; } if (env.BUILD_REPOSITORY_URI) { envInfo.repoUrl = this.cleanAzureDevOpsUrl(env.BUILD_REPOSITORY_URI); // Extract clean repo name if not already set if (!envInfo.repoName) { envInfo.repoName = this.extractRepoNameFromUrl(envInfo.repoUrl); } } // Pull Request info if (env.SYSTEM_PULLREQUEST_PULLREQUESTID) { envInfo.prId = env.SYSTEM_PULLREQUEST_PULLREQUESTID; if (env.SYSTEM_PULLREQUEST_PULLREQUESTTITLE) envInfo.prTitle = env.SYSTEM_PULLREQUEST_PULLREQUESTTITLE; if (env.SYSTEM_PULLREQUEST_SOURCEREPOSITORYURI) { envInfo.prUrl = `${env.SYSTEM_PULLREQUEST_SOURCEREPOSITORYURI}/pullrequest/${env.SYSTEM_PULLREQUEST_PULLREQUESTID}`; } } } // Jenkins else if (env.JENKINS_URL) { const gitBranch = env.BRANCH_NAME || env.GIT_BRANCH; if (gitBranch) envInfo.branch = gitBranch.replace('origin/', ''); if (env.GIT_COMMIT) envInfo.commitHash = env.GIT_COMMIT; if (env.GIT_AUTHOR_NAME) envInfo.author = env.GIT_AUTHOR_NAME; if (env.GIT_AUTHOR_EMAIL) envInfo.email = env.GIT_AUTHOR_EMAIL; if (env.GIT_URL) envInfo.repoUrl = env.GIT_URL; if (env.GIT_URL) envInfo.repoName = this.extractRepoNameFromUrl(env.GIT_URL); // Pull Request info (if using GitHub Pull Request Builder plugin) if (env.ghprbPullId) { envInfo.prId = env.ghprbPullId; if (env.ghprbPullTitle) envInfo.prTitle = env.ghprbPullTitle; if (env.ghprbPullLink) envInfo.prUrl = env.ghprbPullLink; } } // CircleCI else if (env.CIRCLECI === 'true') { if (env.CIRCLE_BRANCH) envInfo.branch = env.CIRCLE_BRANCH; if (env.CIRCLE_SHA1) envInfo.commitHash = env.CIRCLE_SHA1; // CircleCI commit message and author info if (env.CIRCLE_USERNAME) envInfo.author = env.CIRCLE_USERNAME; // Note: CircleCI doesn't provide commit message directly, will fall back to git commands // Repository info - construct clean URLs if (env.CIRCLE_PROJECT_USERNAME && env.CIRCLE_PROJECT_REPONAME) { envInfo.repoName = `${env.CIRCLE_PROJECT_USERNAME}/${env.CIRCLE_PROJECT_REPONAME}`; // Construct clean HTTPS URL - CircleCI supports GitHub, Bitbucket, etc. if (env.CIRCLE_REPOSITORY_URL) { envInfo.repoUrl = this.cleanCircleCIUrl(env.CIRCLE_REPOSITORY_URL); } else { // Default to GitHub if no repository URL provided envInfo.repoUrl = `https://github.com/${env.CIRCLE_PROJECT_USERNAME}/${env.CIRCLE_PROJECT_REPONAME}`; } } else if (env.CIRCLE_REPOSITORY_URL) { // Clean up the repository URL (remove SSH format, credentials, etc.) envInfo.repoUrl = this.cleanCircleCIUrl(env.CIRCLE_REPOSITORY_URL); envInfo.repoName = this.extractRepoNameFromUrl(envInfo.repoUrl); } // Pull Request info - enhanced handling if (env.CIRCLE_PULL_REQUEST || env.CIRCLE_PR_NUMBER) { // Extract PR number from URL if not directly available if (env.CIRCLE_PR_NUMBER) { envInfo.prId = env.CIRCLE_PR_NUMBER; } else if (env.CIRCLE_PULL_REQUEST) { // Extract PR number from URL like: https://github.com/owner/repo/pull/123 const prMatch = env.CIRCLE_PULL_REQUEST.match(/\/pull\/(\d+)$/); if (prMatch?.[1]) { envInfo.prId = prMatch[1]; } } envInfo.prUrl = env.CIRCLE_PULL_REQUEST || ''; // Determine PR status based on available info if (env.CIRCLE_TAG) { // If there's a tag, it might be a release PR that was merged envInfo.prStatus = ''; } else if (env.CIRCLE_PULL_REQUEST) { envInfo.prStatus = 'open'; // Assume open if we have PR info } } } // Generic CI environment variables (fallback) if (!envInfo.branch) { const fallbackBranch = env.CI_BRANCH || env.BRANCH_NAME || env.GIT_BRANCH; if (fallbackBranch) envInfo.branch = fallbackBranch; } if (!envInfo.commitHash) { const fallbackCommit = env.CI_COMMIT || env.COMMIT_SHA || env.GIT_COMMIT; if (fallbackCommit) envInfo.commitHash = fallbackCommit; } if (!envInfo.author) { const fallbackAuthor = env.CI_AUTHOR || env.GIT_AUTHOR || env.COMMIT_AUTHOR; if (fallbackAuthor) envInfo.author = fallbackAuthor; } if (!envInfo.repoUrl) { const fallbackUrl = env.CI_REPOSITORY_URL || env.REPOSITORY_URL || env.GIT_URL; if (fallbackUrl) envInfo.repoUrl = fallbackUrl; } return envInfo; } /** * Extract branch name from git ref (e.g., "refs/heads/main" -> "main") */ extractBranchFromRef(ref) { if (!ref) return undefined; if (ref.startsWith('refs/heads/')) { return ref.replace('refs/heads/', ''); } if (ref.startsWith('refs/tags/')) { return ref.replace('refs/tags/', ''); } return ref; } /** * Extract repository name from URL (e.g., "https://github.com/user/repo.git" -> "user/repo") * Special handling for Azure DevOps: "https://dev.azure.com/org/project/_git/repo" -> "project/repo" */ extractRepoNameFromUrl(url) { if (!url) return ''; try { // First clean GitLab URLs that might contain CI tokens let cleanUrl = url; if (url.includes('gitlab-ci-token')) { cleanUrl = this.cleanGitLabUrl(url); } // Remove .git suffix cleanUrl = cleanUrl.replace(/\.git$/, ''); // Special handling for Azure DevOps URLs if (cleanUrl.includes('dev.azure.com') || cleanUrl.includes('visualstudio.com')) { // Azure DevOps format: https://dev.azure.com/org/project/_git/repo const azureMatch = cleanUrl.match(/\/([^/]+)\/_git\/([^/]+)$/); if (azureMatch?.[1] && azureMatch[2]) { // Decode URL encoding for display (spaces, etc.) const project = decodeURIComponent(azureMatch[1]); const repo = decodeURIComponent(azureMatch[2]); return `${project}/${repo}`; } } // Default: Extract the last two parts of the path const parts = cleanUrl.split('/').filter(part => part.length > 0); if (parts.length >= 2) { try { return decodeURIComponent(parts.slice(-2).join('/')); } catch { // Fallback if decoding fails return parts.slice(-2).join('/'); } } return cleanUrl; } catch { return url; } } /** * Clean GitLab repository URL by removing CI token * Converts: https://gitlab-ci-token:xxxxx@gitlab.com/user/repo.git * To: https://gitlab.com/user/repo.git */ cleanGitLabUrl(url) { try { // Parse URL to remove credentials const urlObj = new URL(url); // Remove any auth info (username:password) urlObj.username = ''; urlObj.password = ''; return urlObj.toString(); } catch { // Fallback: regex-based cleaning return url.replace(/gitlab-ci-token:[^@]+@/, ''); } } /** * Clean CircleCI repository URL by removing SSH format and converting to HTTPS * Converts: git@github.com:user/repo.git -> https://github.com/user/repo.git * Also removes any credentials and normalizes URLs */ cleanCircleCIUrl(url) { if (!url || typeof url !== 'string') { return ''; } try { // Handle SSH format: git@github.com:user/repo.git if (url.startsWith('git@')) { const sshMatch = url.match(/git@([^:]+):([^/]+)\/(.+?)(?:\.git)?$/); if (sshMatch && sshMatch.length >= 4) { const [, host, user, repo] = sshMatch; return `https://${host}/${user}/${repo}`; } } // Handle HTTPS URLs - clean credentials const urlObj = new URL(url); urlObj.username = ''; urlObj.password = ''; // Remove .git suffix if present if (urlObj.pathname.endsWith('.git')) { urlObj.pathname = urlObj.pathname.slice(0, -4); } return urlObj.toString(); } catch { // Fallback: basic cleaning for malformed URLs try { return url .replace(/^git@([^:]+):/, 'https://$1/') .replace(/\.git$/, '') .replace(/\/+$/, ''); } catch { return url; } } } /** * Clean Azure DevOps repository URL by removing credentials * Converts: https://user@dev.azure.com/org/Project%20Name/_git/Repo%20Name * To: https://dev.azure.com/org/Project%20Name/_git/Repo%20Name * Note: Keeps URL encoding intact for valid URL format */ cleanAzureDevOpsUrl(url) { if (!url || typeof url !== 'string') { return ''; } try { const urlObj = new URL(url); // Remove credentials urlObj.username = ''; urlObj.password = ''; // Keep the pathname encoded for valid URL format // Azure DevOps needs encoded spaces in URLs return urlObj.toString(); } catch { // Fallback: remove credentials only try { return url.replace(/\/\/[^@]+@/, '//'); } catch { return url; } } } /** * Fetch GitHub user info using the public Users API * This works for both public and private repos since it only queries user data */ async fetchGitHubUserInfo(username) { try { const url = `https://api.github.com/users/${username}`; if ((0, verbose_1.isVerboseMode)() || process.env.LOG_LEVEL === 'debug') { console.log(`🔍 Fetching user info from GitHub API: ${url}`); } const response = await fetch(url, { method: 'GET', headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': `tdpw/${version_1.VERSION}`, }, // Increased timeout for reliable metadata collection signal: AbortSignal.timeout(15000), }); if (response.ok) { const data = (await response.json()); const login = data?.login; const id = data?.id ? String(data.id) : undefined; const result = {}; if (login) result.login = login; if (id) result.id = id; return Object.keys(result).length > 0 ? result : null; } else { return null; } } catch (_error) { return null; } } /** * Fetch the HEAD commit information from a specific branch to get the actual commit message * This avoids merge commit messages in PR scenarios */ async fetchGitHubBranchCommit(repository, branch) { try { // Use GitHub's API to fetch the latest commit from the specific branch const url = `https://api.github.com/repos/${repository}/commits/${branch}`; if (process.env.LOG_LEVEL === 'debug') { console.log(`🔍 Fetching branch HEAD commit from GitHub API: ${url}`); } const response = await fetch(url, { method: 'GET', headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': `tdpw/${version_1.VERSION}`, // If GITHUB_TOKEN is available, use it for higher rate limits ...(process.env.GITHUB_TOKEN && { Authorization: `token ${process.env.GITHUB_TOKEN}`, }), }, // Increased timeout for reliable metadata collection signal: AbortSignal.timeout(15000), }); if (response.ok) { const data = (await response.json()); const sha = data?.sha; const message = data?.commit?.message; const email = data?.commit?.author?.email; const authorLogin = data?.author?.login; // GitHub username const authorId = data?.author?.id ? String(data.author.id) : undefined; if (sha && message) { const result = { sha, message, email: email || '', }; if (authorLogin) { result.author = authorLogin; } if (authorId) { result.authorId = authorId; } return result; } return null; } else { return null; } } catch (error) { if (process.env.LOG_LEVEL === 'debug') { console.log(`❌ Error fetching GitHub branch commit:`, error); } return null; } } /** * Fetch pull request details from GitHub API to get PR title and status */ async fetchGitHubPullRequestDetails(repository, prId) { try { // Use GitHub's API to fetch PR details const url = `https://api.github.com/repos/${repository}/pulls/${prId}`; if (process.env.LOG_LEVEL === 'debug') { console.log(`🔍 Fetching PR details from GitHub API: ${url}`); } const response = await fetch(url, { method: 'GET', headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': `tdpw/${version_1.VERSION}`, // If GITHUB_TOKEN is available, use it for higher rate limits ...(process.env.GITHUB_TOKEN && { Authorization: `token ${process.env.GITHUB_TOKEN}`, }), }, // Increased timeout for reliable metadata collection signal: AbortSignal.timeout(15000), }); if (response.ok) { const data = (await response.json()); const title = data?.title; const state = data?.state; const draft = data?.draft; // Determine PR status based on state and draft status let status = ''; if (draft) { status = 'draft'; } else if (state === 'open') { status = 'open'; } else if (state === 'closed') { status = 'closed'; } else if (state === 'merged') { status = 'merged'; } if (process.env.LOG_LEVEL === 'debug') { console.log(`✅ Successfully fetched PR details - Title: ${title}, Status: ${status}`); } if (title) { return { title, status }; } return null; } else { if (process.env.LOG_LEVEL === 'debug') { console.log(`❌ GitHub API request for PR details failed: ${response.status} ${response.statusText}`); } return null; } } catch (error) { if (process.env.LOG_LEVEL === 'debug') { console.log(`❌ Error fetching GitHub PR details:`, error); } return null; } } } exports.GitCollector = GitCollector; //# sourceMappingURL=git.js.map