tdpw
Version:
CLI tool for uploading Playwright test reports to TestDino platform with TestDino storage support
972 lines • 44.2 kB
JavaScript
;
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