diff --git a/dist/index.js b/dist/index.js index 6c37a23..52957e6 100644 --- a/dist/index.js +++ b/dist/index.js @@ -95707,10 +95707,41 @@ var __webpack_exports__ = {}; }); return runsResponse.data; } + async hasArtifactsForCommit(commitHash) { + try { + const workflowRuns = await this.findAllWorkflowRunsByCommit(commitHash); + for (const workflowRun of workflowRuns)try { + const runArtifacts = await this.listArtifactsForWorkflowRun(workflowRun.id); + if (runArtifacts.artifacts && runArtifacts.artifacts.length > 0) return true; + } catch (error) { + continue; + } + return false; + } catch (error) { + return false; + } + } + async getParentCommit(commitHash) { + const { owner, repo } = this.repository; + try { + const commitResponse = await this.octokit.rest.repos.getCommit({ + owner, + repo, + ref: commitHash + }); + if (commitResponse.data.parents && commitResponse.data.parents.length > 0) return commitResponse.data.parents[0].sha.substring(0, 10); + return null; + } catch (error) { + const apiError = error; + console.warn(`āš ļø Failed to get parent commit for ${commitHash}: ${apiError.message}`); + return null; + } + } async getTargetBranchLatestCommit() { const targetBranch = this.getTargetBranch(); console.log(`šŸ” Attempting to get latest commit for target branch: ${targetBranch}`); console.log(`šŸ“‹ Repository: ${this.repository.owner}/${this.repository.repo}`); + let latestCommitHash = null; try { console.log(`šŸ“” Trying to get latest commit from GitHub API...`); const { owner, repo } = this.repository; @@ -95721,9 +95752,8 @@ var __webpack_exports__ = {}; branch: targetBranch }); if (branchResponse.data && branchResponse.data.commit) { - const commitHash = branchResponse.data.commit.sha.substring(0, 10); - console.log(`āœ… Found commit hash from GitHub API: ${commitHash}`); - return commitHash; + latestCommitHash = branchResponse.data.commit.sha.substring(0, 10); + console.log(`āœ… Found commit hash from GitHub API: ${latestCommitHash}`); } } catch (error) { const apiError = error; @@ -95741,67 +95771,121 @@ var __webpack_exports__ = {}; branch: altBranch }); if (altResponse.data && altResponse.data.commit) { - const commitHash = altResponse.data.commit.sha.substring(0, 10); - console.log(`āœ… Found commit hash from alternative branch ${altBranch}: ${commitHash}`); - return commitHash; + latestCommitHash = altResponse.data.commit.sha.substring(0, 10); + console.log(`āœ… Found commit hash from alternative branch ${altBranch}: ${latestCommitHash}`); + break; } } catch (error) { const altError = error; console.log(`āŒ Alternative branch ${altBranch} also failed: ${altError.message}`); } } - console.log(`šŸ“‹ Trying to get from workflow runs...`); - try { - const runs = await this.listWorkflowRuns({ - branch: targetBranch, - status: 'completed', - limit: 10 - }); - if (runs.workflow_runs && runs.workflow_runs.length > 0) { - console.log(`Found ${runs.workflow_runs.length} workflow runs for ${targetBranch}`); - const successfulRun = runs.workflow_runs.find((run)=>'success' === run.conclusion); - if (successfulRun) { - console.log(`āœ… Found successful workflow run for ${targetBranch}: ${successfulRun.head_sha}`); - return successfulRun.head_sha.substring(0, 10); + if (!latestCommitHash) { + console.log(`šŸ“‹ Trying to get from workflow runs...`); + try { + const runs = await this.listWorkflowRuns({ + branch: targetBranch, + status: 'completed', + limit: 10 + }); + if (runs.workflow_runs && runs.workflow_runs.length > 0) { + console.log(`Found ${runs.workflow_runs.length} workflow runs for ${targetBranch}`); + const successfulRun = runs.workflow_runs.find((run)=>'success' === run.conclusion); + if (successfulRun) { + latestCommitHash = successfulRun.head_sha.substring(0, 10); + console.log(`āœ… Found successful workflow run for ${targetBranch}: ${latestCommitHash}`); + } else { + const latestRun = runs.workflow_runs[0]; + latestCommitHash = latestRun.head_sha.substring(0, 10); + console.log(`āš ļø No successful runs found, using latest workflow run for ${targetBranch}: ${latestCommitHash}`); + } } - const latestRun = runs.workflow_runs[0]; - console.log(`āš ļø No successful runs found, using latest workflow run for ${targetBranch}: ${latestRun.head_sha}`); - return latestRun.head_sha.substring(0, 10); + } catch (error) { + const workflowError = error; + console.warn(`āš ļø Failed to get workflow runs: ${workflowError.message}`); } - } catch (error) { - const workflowError = error; - console.warn(`āš ļø Failed to get workflow runs: ${workflowError.message}`); } - console.log(`šŸ”§ No workflow runs found for ${targetBranch}, trying to fetch from remote...`); - try { - console.log(`šŸ“„ Running: git fetch origin`); - (0, external_child_process_.execSync)('git fetch origin', { - encoding: 'utf8' - }); - console.log(`šŸ“„ Running: git rev-parse --short=10 origin/${targetBranch}`); - const commitHash = (0, external_child_process_.execSync)(`git rev-parse --short=10 origin/${targetBranch}`, { - encoding: 'utf8' - }).trim(); - console.log(`āœ… Found commit hash from git: ${commitHash}`); - return commitHash; - } catch (gitError) { - console.warn(`āŒ Git fetch failed: ${gitError}`); + if (!latestCommitHash) { + console.log(`šŸ”§ No workflow runs found for ${targetBranch}, trying to fetch from remote...`); try { - console.log(`šŸ“„ Trying alternative: git ls-remote origin ${targetBranch}`); - const remoteRef = (0, external_child_process_.execSync)(`git ls-remote origin ${targetBranch}`, { + console.log(`šŸ“„ Running: git fetch origin`); + (0, external_child_process_.execSync)('git fetch origin', { + encoding: 'utf8' + }); + console.log(`šŸ“„ Running: git rev-parse --short=10 origin/${targetBranch}`); + latestCommitHash = (0, external_child_process_.execSync)(`git rev-parse --short=10 origin/${targetBranch}`, { encoding: 'utf8' }).trim(); - if (remoteRef) { - const commitHash = remoteRef.split('\t')[0].substring(0, 10); - console.log(`āœ… Found commit hash from git ls-remote: ${commitHash}`); - return commitHash; + console.log(`āœ… Found commit hash from git: ${latestCommitHash}`); + } catch (gitError) { + console.warn(`āŒ Git fetch failed: ${gitError}`); + try { + console.log(`šŸ“„ Trying alternative: git ls-remote origin ${targetBranch}`); + const remoteRef = (0, external_child_process_.execSync)(`git ls-remote origin ${targetBranch}`, { + encoding: 'utf8' + }).trim(); + if (remoteRef) { + latestCommitHash = remoteRef.split('\t')[0].substring(0, 10); + console.log(`āœ… Found commit hash from git ls-remote: ${latestCommitHash}`); + } + } catch (altError) { + console.warn(`āŒ Alternative git command failed: ${altError}`); } - } catch (altError) { - console.warn(`āŒ Alternative git command failed: ${altError}`); } } - console.error(`āŒ All methods to get target branch commit have failed`); - throw new Error(`Unable to get target branch (${targetBranch}) commit hash. Please ensure the branch exists and you have correct permissions.`); + if (!latestCommitHash) { + console.error(`āŒ All methods to get target branch commit have failed`); + throw new Error(`Unable to get target branch (${targetBranch}) commit hash. Please ensure the branch exists and you have correct permissions.`); + } + console.log(`šŸ” Checking if commit ${latestCommitHash} has baseline artifacts...`); + const hasArtifacts = await this.hasArtifactsForCommit(latestCommitHash); + if (hasArtifacts) { + console.log(`āœ… Commit ${latestCommitHash} has baseline artifacts`); + return { + commitHash: latestCommitHash, + usedFallbackCommit: false + }; + } + console.log(`āš ļø Commit ${latestCommitHash} does not have baseline artifacts`); + console.log(`šŸ” Looking for previous commits with baseline artifacts...`); + let currentCommit = latestCommitHash; + let checkedCommits = [ + currentCommit + ]; + const maxDepth = 5; + for(let depth = 0; depth < maxDepth; depth++){ + const parentCommit = await this.getParentCommit(currentCommit); + if (!parentCommit) { + console.log(`āš ļø Reached the beginning of the branch, no more parent commits`); + break; + } + if (checkedCommits.includes(parentCommit)) { + console.log(`āš ļø Detected circular reference, stopping search`); + break; + } + checkedCommits.push(parentCommit); + console.log(`šŸ” Checking parent commit ${parentCommit}...`); + const parentHasArtifacts = await this.hasArtifactsForCommit(parentCommit); + if (parentHasArtifacts) { + console.log(`āœ… Found commit ${parentCommit} with baseline artifacts`); + console.log(`\nāš ļø Note: The latest commit (${latestCommitHash}) does not have baseline artifacts.`); + console.log(` Using commit ${parentCommit} for baseline comparison instead.`); + console.log(" If this seems incorrect, please wait a few minutes and try rerunning the workflow."); + return { + commitHash: parentCommit, + usedFallbackCommit: true, + latestCommitHash: latestCommitHash + }; + } + currentCommit = parentCommit; + } + console.log(`\nāš ļø No commits with baseline artifacts found in the last ${maxDepth} commits.`); + console.log(` Using latest commit ${latestCommitHash} anyway.`); + console.log(" Note: If baseline comparison fails, please wait a few minutes and try rerunning the workflow."); + return { + commitHash: latestCommitHash, + usedFallbackCommit: false + }; } catch (error) { console.error(`āŒ Failed to get target branch commit: ${error}`); console.error(`Repository: ${this.repository.owner}/${this.repository.repo}`); @@ -96212,18 +96296,26 @@ var __webpack_exports__ = {}; } function calculateDiff(current, baseline) { if (!baseline || 0 === baseline || isNaN(baseline)) return { - value: 'N/A', + value: '0', emoji: 'ā“' }; if (isNaN(current)) return { - value: 'N/A', + value: '0', emoji: 'ā“' }; const diff = current - baseline; + if (0 === diff) return { + value: '0', + emoji: '' + }; const percent = diff / baseline * 100; - if (Math.abs(percent) < 1) return { + if (Math.abs(percent) < 1) if (diff > 0) return { + value: `+${formatBytes(diff)} (${percent.toFixed(1)}%)`, + emoji: '' + }; + else return { value: `${formatBytes(diff)} (${percent.toFixed(1)}%)`, - emoji: 'āž”ļø' + emoji: '' }; if (diff > 0) return { value: `+${formatBytes(diff)} (+${percent.toFixed(1)}%)`, @@ -96304,7 +96396,7 @@ var __webpack_exports__ = {}; header: false }, { - data: baseline ? calculateDiff(current.totalSize, baseline.totalSize).value : 'N/A', + data: baseline ? calculateDiff(current.totalSize, baseline.totalSize).value : '0', header: false } ], @@ -96322,7 +96414,7 @@ var __webpack_exports__ = {}; header: false }, { - data: baseline ? calculateDiff(current.jsSize, baseline.jsSize).value : 'N/A', + data: baseline ? calculateDiff(current.jsSize, baseline.jsSize).value : '0', header: false } ], @@ -96340,7 +96432,7 @@ var __webpack_exports__ = {}; header: false }, { - data: baseline ? calculateDiff(current.cssSize, baseline.cssSize).value : 'N/A', + data: baseline ? calculateDiff(current.cssSize, baseline.cssSize).value : '0', header: false } ], @@ -96358,7 +96450,7 @@ var __webpack_exports__ = {}; header: false }, { - data: baseline ? calculateDiff(current.htmlSize, baseline.htmlSize).value : 'N/A', + data: baseline ? calculateDiff(current.htmlSize, baseline.htmlSize).value : '0', header: false } ], @@ -96376,7 +96468,7 @@ var __webpack_exports__ = {}; header: false }, { - data: baseline ? calculateDiff(current.otherSize, baseline.otherSize).value : 'N/A', + data: baseline ? calculateDiff(current.otherSize, baseline.otherSize).value : '0', header: false } ] @@ -96412,7 +96504,7 @@ var __webpack_exports__ = {}; header: false }, { - data: baseline ? formatBytes(baseline.totalSize) : 'N/A', + data: baseline ? formatBytes(baseline.totalSize) : '0', header: false } ] @@ -96533,7 +96625,7 @@ var __webpack_exports__ = {}; } return pathParts[0] || 'root'; } - async function processSingleFile(fullPath, currentCommitHash, targetCommitHash) { + async function processSingleFile(fullPath, currentCommitHash, targetCommitHash, baselineUsedFallback, baselineLatestCommitHash) { const fileName = external_path_default().basename(fullPath); const relativePath = external_path_default().relative(process.cwd(), fullPath); const pathParts = relativePath.split(external_path_default().sep); @@ -96565,6 +96657,8 @@ var __webpack_exports__ = {}; if (baselineBundleAnalysis) { report.baseline = baselineBundleAnalysis; report.baselineCommitHash = targetCommitHash; + report.baselineUsedFallback = baselineUsedFallback; + report.baselineLatestCommitHash = baselineLatestCommitHash; try { const githubService = new GitHubService(); baselinePRs = await githubService.findPRsByCommit(targetCommitHash); @@ -96655,10 +96749,16 @@ var __webpack_exports__ = {}; const currentCommitHash = githubService.getCurrentCommitHash(); console.log(`Current commit hash: ${currentCommitHash}`); let targetCommitHash = null; + let baselineUsedFallback = false; + let baselineLatestCommitHash; if (isPullRequestEvent()) try { console.log('šŸ” Getting target branch commit hash...'); - targetCommitHash = await githubService.getTargetBranchLatestCommit(); + const commitInfo = await githubService.getTargetBranchLatestCommit(); + targetCommitHash = commitInfo.commitHash; + baselineUsedFallback = commitInfo.usedFallbackCommit; + baselineLatestCommitHash = commitInfo.latestCommitHash; console.log(`āœ… Target branch commit hash: ${targetCommitHash}`); + if (baselineUsedFallback && baselineLatestCommitHash) console.log(`āš ļø Using fallback commit: ${targetCommitHash} (latest: ${baselineLatestCommitHash})`); } catch (error) { console.error(`āŒ Failed to get target branch commit: ${error}`); console.log('šŸ“ No baseline data available for comparison'); @@ -96702,14 +96802,19 @@ var __webpack_exports__ = {}; } else if (isPR) { console.log('šŸ“„ Detected pull request event - processing files'); for (const fullPath of matchedFiles){ - const report = await processSingleFile(fullPath, currentCommitHash, targetCommitHash); + const report = await processSingleFile(fullPath, currentCommitHash, targetCommitHash, baselineUsedFallback, baselineLatestCommitHash); projectReports.push(report); } if (projectReports.length > 0) if (1 === projectReports.length) { const report = projectReports[0]; - if (report.current) await generateBundleAnalysisReport(report.current, report.baseline || void 0, true, report.baselineCommitHash, report.baselinePRs); + if (report.current) { + if (report.baselineUsedFallback && report.baselineLatestCommitHash) await core.summary.addRaw(`> āš ļø **Note:** The latest commit (\`${report.baselineLatestCommitHash}\`) does not have baseline artifacts. Using commit \`${report.baselineCommitHash}\` for baseline comparison instead. If this seems incorrect, please wait a few minutes and try rerunning the workflow.\n\n`); + await generateBundleAnalysisReport(report.current, report.baseline || void 0, true, report.baselineCommitHash, report.baselinePRs); + } } else { await core.summary.addHeading('šŸ“¦ Monorepo Bundle Analysis', 2); + const firstReport = projectReports.find((r)=>r.current); + if (firstReport?.baselineUsedFallback && firstReport?.baselineLatestCommitHash) await core.summary.addRaw(`> āš ļø **Note:** The latest commit (\`${firstReport.baselineLatestCommitHash}\`) does not have baseline artifacts. Using commit \`${firstReport.baselineCommitHash}\` for baseline comparison instead. If this seems incorrect, please wait a few minutes and try rerunning the workflow.\n\n`); for (const report of projectReports)if (report.current) { await core.summary.addHeading(`šŸ“ ${report.projectName}`, 3); await core.summary.addRaw(`**Path:** \`${report.filePath}\``); @@ -96721,13 +96826,85 @@ var __webpack_exports__ = {}; if (isPR && projectReports.length > 0) { const { context } = __webpack_require__("./node_modules/.pnpm/@actions+github@4.0.0/node_modules/@actions/github/lib/github.js"); let commentBody = '## Rsdoctor Bundle Diff Analysis\n\n'; - if (projectReports.length > 1) commentBody += `Found ${projectReports.length} project(s) in monorepo.\n\n`; - for (const report of projectReports)if (report.current) { - commentBody += generateProjectMarkdown(report.projectName, report.filePath, report.current, report.baseline || void 0, report.baselineCommitHash, report.baselinePRs); - if (report.diffHtmlArtifactId) { - const artifactDownloadLink = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts/${report.diffHtmlArtifactId}`; - commentBody += `\nšŸ“¦ **Download Diff Report**: [${report.projectName} Bundle Diff](${artifactDownloadLink})\n\n`; + const firstReport = projectReports.find((r)=>r.current); + if (firstReport?.baselineUsedFallback && firstReport?.baselineLatestCommitHash) commentBody += `> āš ļø **Note:** The latest commit (\`${firstReport.baselineLatestCommitHash}\`) does not have baseline artifacts. Using commit \`${firstReport.baselineCommitHash}\` for baseline comparison instead. If this seems incorrect, please wait a few minutes and try rerunning the workflow.\n\n`; + const reportsWithCurrent = projectReports.filter((r)=>r.current); + if (reportsWithCurrent.length > 1) { + let projectsWithChanges = 0; + for (const report of reportsWithCurrent){ + if (!report.current) continue; + if (!report.baseline) { + projectsWithChanges++; + continue; + } + const currentSize = report.current.totalSize; + const baselineSize = report.baseline.totalSize; + if (0 === baselineSize || isNaN(baselineSize)) continue; + const diff = currentSize - baselineSize; + if (0 !== diff) projectsWithChanges++; + } + const totalProjects = reportsWithCurrent.length; + const projectWord = 1 === totalProjects ? 'project' : 'projects'; + const changeWord = 1 === projectsWithChanges ? 'project' : 'projects'; + commentBody += `Found ${totalProjects} ${projectWord} in monorepo, ${projectsWithChanges} ${changeWord} with changes.\n\n`; + } + if (reportsWithCurrent.length > 0) { + let hasChanges = false; + for (const report of reportsWithCurrent){ + if (!report.current) continue; + if (!report.baseline) { + hasChanges = true; + break; + } + const currentSize = report.current.totalSize; + const baselineSize = report.baseline.totalSize; + if (0 === baselineSize || isNaN(baselineSize)) continue; + const diff = currentSize - baselineSize; + if (0 !== diff) { + hasChanges = true; + break; + } + } + const detailsTag = hasChanges ? '
\n' : '
\n'; + commentBody += `${detailsTag}šŸ“Š Quick Summary\n\n`; + commentBody += '| Project | Total Size | Change |\n'; + commentBody += '|---------|------------|--------|\n'; + for (const report of reportsWithCurrent){ + if (!report.current) continue; + const currentSize = report.current.totalSize; + const baselineSize = report.baseline?.totalSize || 0; + const diff = report.baseline ? calculateDiff(currentSize, baselineSize) : { + value: '-', + emoji: '' + }; + const sizeStr = formatBytes(currentSize); + commentBody += `| ${report.projectName} | ${sizeStr} | ${diff.emoji} ${diff.value} |\n`; + } + commentBody += '\n
\n\n'; + } + const hasSignificantChanges = (report)=>{ + if (!report.current) return false; + if (!report.baseline) return true; + const currentSize = report.current.totalSize; + const baselineSize = report.baseline.totalSize; + if (0 === baselineSize || isNaN(baselineSize)) return false; + const diff = currentSize - baselineSize; + return 0 !== diff; + }; + const reportsWithChanges = projectReports.filter((report)=>{ + if (!report.current) return false; + return hasSignificantChanges(report); + }); + if (reportsWithChanges.length > 0) { + commentBody += '
\nšŸ“‹ Detailed Reports (Click to expand)\n\n'; + for (const report of reportsWithChanges){ + commentBody += generateProjectMarkdown(report.projectName, report.filePath, report.current, report.baseline || void 0, report.baselineCommitHash, report.baselinePRs); + if (report.diffHtmlArtifactId) { + const artifactDownloadLink = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts/${report.diffHtmlArtifactId}`; + commentBody += `\nšŸ“¦ **Download Diff Report**: [${report.projectName} Bundle Diff](${artifactDownloadLink})\n\n`; + } } + if (reportsWithChanges.length > 1) commentBody += '
\n\n'; } commentBody += '*Generated by [Rsdoctor GitHub Action](https://rsdoctor.rs/guide/start/action)*'; try { diff --git a/examples/rsbuild-demo2/src/App.tsx b/examples/rsbuild-demo2/src/App.tsx index 1894fd5..bfaada1 100644 --- a/examples/rsbuild-demo2/src/App.tsx +++ b/examples/rsbuild-demo2/src/App.tsx @@ -21,6 +21,10 @@ const App = () => {
+
+

Fast Build

+

Rsbuild provides lightning-fast build times with modern tooling.

+

Fast Build

Rsbuild provides lightning-fast build times with modern tooling.

diff --git a/src/__tests__/github.test.ts b/src/__tests__/github.test.ts index c7f8f32..d15427d 100644 --- a/src/__tests__/github.test.ts +++ b/src/__tests__/github.test.ts @@ -39,8 +39,89 @@ describe('GitHub Service', () => { }, }); - const commit = await githubService.getTargetBranchLatestCommit(); - expect(commit).toBe(mockCommitSha); + // Mock workflow runs check (no artifacts found) + nock('https://api.github.com') + .get('/repos/web-infra-dev/rsdoctor-action/actions/runs') + .query({ head_sha: mockCommitSha, status: 'completed', per_page: 30 }) + .reply(200, { + workflow_runs: [], + }); + + // Mock get parent commit (no parent, reached beginning) + nock('https://api.github.com') + .get(`/repos/web-infra-dev/rsdoctor-action/commits/${mockCommitSha}`) + .reply(200, { + sha: mockCommitSha + '0123456789', + parents: [], + }); + + const result = await githubService.getTargetBranchLatestCommit(); + expect(result).toHaveProperty('commitHash'); + expect(result).toHaveProperty('usedFallbackCommit'); + expect(result.commitHash).toBe(mockCommitSha); + expect(result.usedFallbackCommit).toBe(false); + }); + + it('should return object with fallback info when latest commit has no artifacts', async () => { + const mockCommitSha = 'abcdef1234'; + const mockParentSha = 'parent1234'; + nock('https://api.github.com') + .get('/repos/web-infra-dev/rsdoctor-action/branches/main') + .reply(200, { + commit: { + sha: mockCommitSha + '0123456789', + }, + }); + + // Mock workflow runs check for latest commit (no artifacts) + nock('https://api.github.com') + .get('/repos/web-infra-dev/rsdoctor-action/actions/runs') + .query({ head_sha: mockCommitSha, status: 'completed', per_page: 30 }) + .reply(200, { + workflow_runs: [], + }); + + // Mock get parent commit + nock('https://api.github.com') + .get(`/repos/web-infra-dev/rsdoctor-action/commits/${mockCommitSha}`) + .reply(200, { + sha: mockCommitSha + '0123456789', + parents: [ + { sha: mockParentSha + '0123456789' }, + ], + }); + + // Mock workflow runs check for parent commit (has artifacts) + nock('https://api.github.com') + .get('/repos/web-infra-dev/rsdoctor-action/actions/runs') + .query({ head_sha: mockParentSha, status: 'completed', per_page: 30 }) + .reply(200, { + workflow_runs: [ + { + id: 123, + name: 'CI', + status: 'completed', + conclusion: 'success', + }, + ], + }); + + // Mock artifacts for parent commit + nock('https://api.github.com') + .get('/repos/web-infra-dev/rsdoctor-action/actions/runs/123/artifacts') + .reply(200, { + artifacts: [ + { id: 1, name: 'test-artifact' }, + ], + }); + + const result = await githubService.getTargetBranchLatestCommit(); + expect(result).toHaveProperty('commitHash'); + expect(result).toHaveProperty('usedFallbackCommit'); + expect(result).toHaveProperty('latestCommitHash'); + expect(result.commitHash).toBe(mockParentSha); + expect(result.usedFallbackCommit).toBe(true); + expect(result.latestCommitHash).toBe(mockCommitSha); }); }); }); diff --git a/src/github.ts b/src/github.ts index 7a26f15..508b8df 100644 --- a/src/github.ts +++ b/src/github.ts @@ -77,11 +77,64 @@ export class GitHubService { return runsResponse.data; } - async getTargetBranchLatestCommit(): Promise { + /** + * Check if a commit has any artifacts by checking its workflow runs + */ + async hasArtifactsForCommit(commitHash: string): Promise { + try { + const workflowRuns = await this.findAllWorkflowRunsByCommit(commitHash); + + for (const workflowRun of workflowRuns) { + try { + const runArtifacts = await this.listArtifactsForWorkflowRun(workflowRun.id); + if (runArtifacts.artifacts && runArtifacts.artifacts.length > 0) { + return true; + } + } catch (error) { + // Continue checking other workflow runs + continue; + } + } + + return false; + } catch (error) { + // If we can't check, assume no artifacts + return false; + } + } + + /** + * Get parent commit hash + */ + async getParentCommit(commitHash: string): Promise { + const { owner, repo } = this.repository; + + try { + const commitResponse = await this.octokit.rest.repos.getCommit({ + owner, + repo, + ref: commitHash + }); + + if (commitResponse.data.parents && commitResponse.data.parents.length > 0) { + return commitResponse.data.parents[0].sha.substring(0, 10); + } + + return null; + } catch (error) { + const apiError = error as ApiError; + console.warn(`āš ļø Failed to get parent commit for ${commitHash}: ${apiError.message}`); + return null; + } + } + + async getTargetBranchLatestCommit(): Promise<{ commitHash: string; usedFallbackCommit: boolean; latestCommitHash?: string }> { const targetBranch = this.getTargetBranch(); console.log(`šŸ” Attempting to get latest commit for target branch: ${targetBranch}`); console.log(`šŸ“‹ Repository: ${this.repository.owner}/${this.repository.repo}`); + let latestCommitHash: string | null = null; + try { console.log(`šŸ“” Trying to get latest commit from GitHub API...`); const { owner, repo } = this.repository; @@ -94,9 +147,8 @@ export class GitHubService { }); if (branchResponse.data && branchResponse.data.commit) { - const commitHash = branchResponse.data.commit.sha.substring(0, 10); - console.log(`āœ… Found commit hash from GitHub API: ${commitHash}`); - return commitHash; + latestCommitHash = branchResponse.data.commit.sha.substring(0, 10); + console.log(`āœ… Found commit hash from GitHub API: ${latestCommitHash}`); } } catch (error) { const apiError = error as ApiError; @@ -114,9 +166,9 @@ export class GitHubService { }); if (altResponse.data && altResponse.data.commit) { - const commitHash = altResponse.data.commit.sha.substring(0, 10); - console.log(`āœ… Found commit hash from alternative branch ${altBranch}: ${commitHash}`); - return commitHash; + latestCommitHash = altResponse.data.commit.sha.substring(0, 10); + console.log(`āœ… Found commit hash from alternative branch ${altBranch}: ${latestCommitHash}`); + break; } } catch (error) { const altError = error as ApiError; @@ -126,59 +178,125 @@ export class GitHubService { } } - console.log(`šŸ“‹ Trying to get from workflow runs...`); - try { - const runs = await this.listWorkflowRuns({ - branch: targetBranch, - status: 'completed', - limit: 10 - }); + if (!latestCommitHash) { + console.log(`šŸ“‹ Trying to get from workflow runs...`); + try { + const runs = await this.listWorkflowRuns({ + branch: targetBranch, + status: 'completed', + limit: 10 + }); + + if (runs.workflow_runs && runs.workflow_runs.length > 0) { + console.log(`Found ${runs.workflow_runs.length} workflow runs for ${targetBranch}`); + + const successfulRun = runs.workflow_runs.find((run: WorkflowRun) => run.conclusion === 'success'); + if (successfulRun) { + latestCommitHash = successfulRun.head_sha.substring(0, 10); + console.log(`āœ… Found successful workflow run for ${targetBranch}: ${latestCommitHash}`); + } else { + const latestRun = runs.workflow_runs[0] as WorkflowRun; + latestCommitHash = latestRun.head_sha.substring(0, 10); + console.log(`āš ļø No successful runs found, using latest workflow run for ${targetBranch}: ${latestCommitHash}`); + } + } + } catch (error) { + const workflowError = error as ApiError; + console.warn(`āš ļø Failed to get workflow runs: ${workflowError.message}`); + } + } - if (runs.workflow_runs && runs.workflow_runs.length > 0) { - console.log(`Found ${runs.workflow_runs.length} workflow runs for ${targetBranch}`); + if (!latestCommitHash) { + console.log(`šŸ”§ No workflow runs found for ${targetBranch}, trying to fetch from remote...`); + try { + console.log(`šŸ“„ Running: git fetch origin`); + execSync('git fetch origin', { encoding: 'utf8' }); - const successfulRun = runs.workflow_runs.find((run: WorkflowRun) => run.conclusion === 'success'); - if (successfulRun) { - console.log(`āœ… Found successful workflow run for ${targetBranch}: ${successfulRun.head_sha}`); - return successfulRun.head_sha.substring(0, 10); - } + console.log(`šŸ“„ Running: git rev-parse --short=10 origin/${targetBranch}`); + latestCommitHash = execSync(`git rev-parse --short=10 origin/${targetBranch}`, { encoding: 'utf8' }).trim(); + console.log(`āœ… Found commit hash from git: ${latestCommitHash}`); + } catch (gitError) { + console.warn(`āŒ Git fetch failed: ${gitError}`); - const latestRun = runs.workflow_runs[0] as WorkflowRun; - console.log(`āš ļø No successful runs found, using latest workflow run for ${targetBranch}: ${latestRun.head_sha}`); - return latestRun.head_sha.substring(0, 10); + try { + console.log(`šŸ“„ Trying alternative: git ls-remote origin ${targetBranch}`); + const remoteRef = execSync(`git ls-remote origin ${targetBranch}`, { encoding: 'utf8' }).trim(); + if (remoteRef) { + latestCommitHash = remoteRef.split('\t')[0].substring(0, 10); + console.log(`āœ… Found commit hash from git ls-remote: ${latestCommitHash}`); + } + } catch (altError) { + console.warn(`āŒ Alternative git command failed: ${altError}`); + } } - } catch (error) { - const workflowError = error as ApiError; - console.warn(`āš ļø Failed to get workflow runs: ${workflowError.message}`); } - console.log(`šŸ”§ No workflow runs found for ${targetBranch}, trying to fetch from remote...`); - try { - console.log(`šŸ“„ Running: git fetch origin`); - execSync('git fetch origin', { encoding: 'utf8' }); + if (!latestCommitHash) { + console.error(`āŒ All methods to get target branch commit have failed`); + throw new Error(`Unable to get target branch (${targetBranch}) commit hash. Please ensure the branch exists and you have correct permissions.`); + } + + // Check if the latest commit has artifacts, if not, look for previous commits + console.log(`šŸ” Checking if commit ${latestCommitHash} has baseline artifacts...`); + const hasArtifacts = await this.hasArtifactsForCommit(latestCommitHash); + + if (hasArtifacts) { + console.log(`āœ… Commit ${latestCommitHash} has baseline artifacts`); + return { + commitHash: latestCommitHash, + usedFallbackCommit: false + }; + } + + // Latest commit doesn't have artifacts, look for previous commits + console.log(`āš ļø Commit ${latestCommitHash} does not have baseline artifacts`); + console.log(`šŸ” Looking for previous commits with baseline artifacts...`); + + let currentCommit = latestCommitHash; + let checkedCommits: string[] = [currentCommit]; + const maxDepth = 5; + + for (let depth = 0; depth < maxDepth; depth++) { + const parentCommit = await this.getParentCommit(currentCommit); - console.log(`šŸ“„ Running: git rev-parse --short=10 origin/${targetBranch}`); - const commitHash = execSync(`git rev-parse --short=10 origin/${targetBranch}`, { encoding: 'utf8' }).trim(); - console.log(`āœ… Found commit hash from git: ${commitHash}`); - return commitHash; - } catch (gitError) { - console.warn(`āŒ Git fetch failed: ${gitError}`); + if (!parentCommit) { + console.log(`āš ļø Reached the beginning of the branch, no more parent commits`); + break; + } - try { - console.log(`šŸ“„ Trying alternative: git ls-remote origin ${targetBranch}`); - const remoteRef = execSync(`git ls-remote origin ${targetBranch}`, { encoding: 'utf8' }).trim(); - if (remoteRef) { - const commitHash = remoteRef.split('\t')[0].substring(0, 10); - console.log(`āœ… Found commit hash from git ls-remote: ${commitHash}`); - return commitHash; - } - } catch (altError) { - console.warn(`āŒ Alternative git command failed: ${altError}`); + if (checkedCommits.includes(parentCommit)) { + console.log(`āš ļø Detected circular reference, stopping search`); + break; } + + checkedCommits.push(parentCommit); + console.log(`šŸ” Checking parent commit ${parentCommit}...`); + + const parentHasArtifacts = await this.hasArtifactsForCommit(parentCommit); + + if (parentHasArtifacts) { + console.log(`āœ… Found commit ${parentCommit} with baseline artifacts`); + console.log(`\nāš ļø Note: The latest commit (${latestCommitHash}) does not have baseline artifacts.`); + console.log(` Using commit ${parentCommit} for baseline comparison instead.`); + console.log(` If this seems incorrect, please wait a few minutes and try rerunning the workflow.`); + return { + commitHash: parentCommit, + usedFallbackCommit: true, + latestCommitHash: latestCommitHash + }; + } + + currentCommit = parentCommit; } - - console.error(`āŒ All methods to get target branch commit have failed`); - throw new Error(`Unable to get target branch (${targetBranch}) commit hash. Please ensure the branch exists and you have correct permissions.`); + + // No commits with artifacts found + console.log(`\nāš ļø No commits with baseline artifacts found in the last ${maxDepth} commits.`); + console.log(` Using latest commit ${latestCommitHash} anyway.`); + console.log(` Note: If baseline comparison fails, please wait a few minutes and try rerunning the workflow.`); + return { + commitHash: latestCommitHash, + usedFallbackCommit: false + }; } catch (error) { console.error(`āŒ Failed to get target branch commit: ${error}`); diff --git a/src/index.ts b/src/index.ts index a2597a3..bc8b48b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { setFailed, getInput, summary } from '@actions/core'; import { uploadArtifact, hashPath } from './upload'; import { downloadArtifactByCommitHash } from './download'; import { GitHubService } from './github'; -import { loadSizeData, generateSizeReport, parseRsdoctorData, generateBundleAnalysisReport, BundleAnalysis, generateProjectMarkdown } from './report'; +import { loadSizeData, generateSizeReport, parseRsdoctorData, generateBundleAnalysisReport, BundleAnalysis, generateProjectMarkdown, formatBytes, calculateDiff } from './report'; import path from 'path'; import * as fs from 'fs'; import { execFile } from 'child_process'; @@ -84,6 +84,8 @@ interface ProjectReport { baselinePRs?: Array<{ number: number; title: string; url: string }>; diffHtmlPath?: string; diffHtmlArtifactId?: number; + baselineUsedFallback?: boolean; + baselineLatestCommitHash?: string; } function extractProjectName(filePath: string): string { @@ -122,6 +124,8 @@ async function processSingleFile( fullPath: string, currentCommitHash: string, targetCommitHash: string | null, + baselineUsedFallback?: boolean, + baselineLatestCommitHash?: string, ): Promise { const fileName = path.basename(fullPath); const relativePath = path.relative(process.cwd(), fullPath); @@ -162,6 +166,8 @@ async function processSingleFile( if (baselineBundleAnalysis) { report.baseline = baselineBundleAnalysis; report.baselineCommitHash = targetCommitHash; + report.baselineUsedFallback = baselineUsedFallback; + report.baselineLatestCommitHash = baselineLatestCommitHash; // Try to find associated PRs for the baseline commit try { @@ -276,11 +282,20 @@ async function processSingleFile( console.log(`Current commit hash: ${currentCommitHash}`); let targetCommitHash: string | null = null; + let baselineUsedFallback = false; + let baselineLatestCommitHash: string | undefined = undefined; + if (isPullRequestEvent()) { try { console.log('šŸ” Getting target branch commit hash...'); - targetCommitHash = await githubService.getTargetBranchLatestCommit(); + const commitInfo = await githubService.getTargetBranchLatestCommit(); + targetCommitHash = commitInfo.commitHash; + baselineUsedFallback = commitInfo.usedFallbackCommit; + baselineLatestCommitHash = commitInfo.latestCommitHash; console.log(`āœ… Target branch commit hash: ${targetCommitHash}`); + if (baselineUsedFallback && baselineLatestCommitHash) { + console.log(`āš ļø Using fallback commit: ${targetCommitHash} (latest: ${baselineLatestCommitHash})`); + } } catch (error) { console.error(`āŒ Failed to get target branch commit: ${error}`); console.log('šŸ“ No baseline data available for comparison'); @@ -351,7 +366,7 @@ async function processSingleFile( console.log('šŸ“„ Detected pull request event - processing files'); for (const fullPath of matchedFiles) { - const report = await processSingleFile(fullPath, currentCommitHash, targetCommitHash); + const report = await processSingleFile(fullPath, currentCommitHash, targetCommitHash, baselineUsedFallback, baselineLatestCommitHash); projectReports.push(report); } @@ -359,11 +374,21 @@ async function processSingleFile( if (projectReports.length === 1) { const report = projectReports[0]; if (report.current) { + // Add fallback notice if applicable + if (report.baselineUsedFallback && report.baselineLatestCommitHash) { + await summary.addRaw(`> āš ļø **Note:** The latest commit (\`${report.baselineLatestCommitHash}\`) does not have baseline artifacts. Using commit \`${report.baselineCommitHash}\` for baseline comparison instead. If this seems incorrect, please wait a few minutes and try rerunning the workflow.\n\n`); + } await generateBundleAnalysisReport(report.current, report.baseline || undefined, true, report.baselineCommitHash, report.baselinePRs); } } else { await summary.addHeading('šŸ“¦ Monorepo Bundle Analysis', 2); + // Add fallback notice if applicable (check first report) + const firstReport = projectReports.find(r => r.current); + if (firstReport?.baselineUsedFallback && firstReport?.baselineLatestCommitHash) { + await summary.addRaw(`> āš ļø **Note:** The latest commit (\`${firstReport.baselineLatestCommitHash}\`) does not have baseline artifacts. Using commit \`${firstReport.baselineCommitHash}\` for baseline comparison instead. If this seems incorrect, please wait a few minutes and try rerunning the workflow.\n\n`); + } + for (const report of projectReports) { if (!report.current) continue; @@ -384,19 +409,113 @@ async function processSingleFile( let commentBody = '## Rsdoctor Bundle Diff Analysis\n\n'; - if (projectReports.length > 1) { - commentBody += `Found ${projectReports.length} project(s) in monorepo.\n\n`; + // Add fallback notice if applicable (check first report) + const firstReport = projectReports.find(r => r.current); + if (firstReport?.baselineUsedFallback && firstReport?.baselineLatestCommitHash) { + commentBody += `> āš ļø **Note:** The latest commit (\`${firstReport.baselineLatestCommitHash}\`) does not have baseline artifacts. Using commit \`${firstReport.baselineCommitHash}\` for baseline comparison instead. If this seems incorrect, please wait a few minutes and try rerunning the workflow.\n\n`; } - for (const report of projectReports) { - if (!report.current) continue; + // Generate summary (always visible) + const reportsWithCurrent = projectReports.filter(r => r.current); + if (reportsWithCurrent.length > 1) { + // Count projects with changes + let projectsWithChanges = 0; + for (const report of reportsWithCurrent) { + if (!report.current) continue; + if (!report.baseline) { + projectsWithChanges++; + continue; + } + const currentSize = report.current.totalSize; + const baselineSize = report.baseline.totalSize; + if (baselineSize === 0 || isNaN(baselineSize)) continue; + const diff = currentSize - baselineSize; + if (diff !== 0) { + projectsWithChanges++; + } + } + + const totalProjects = reportsWithCurrent.length; + const projectWord = totalProjects === 1 ? 'project' : 'projects'; + const changeWord = projectsWithChanges === 1 ? 'project' : 'projects'; + commentBody += `Found ${totalProjects} ${projectWord} in monorepo, ${projectsWithChanges} ${changeWord} with changes.\n\n`; + } + + // Generate summary table for quick overview + if (reportsWithCurrent.length > 0) { + // Check if any project has changes (any non-zero change) + let hasChanges = false; + for (const report of reportsWithCurrent) { + if (!report.current) continue; + if (!report.baseline) { + hasChanges = true; // No baseline means we can't compare, show it + break; + } + const currentSize = report.current.totalSize; + const baselineSize = report.baseline.totalSize; + if (baselineSize === 0 || isNaN(baselineSize)) continue; + const diff = currentSize - baselineSize; + // Show if there's any non-zero change + if (diff !== 0) { + hasChanges = true; + break; + } + } - commentBody += generateProjectMarkdown(report.projectName, report.filePath, report.current, report.baseline || undefined, report.baselineCommitHash, report.baselinePRs); + // Use 'open' attribute if there are changes, otherwise keep it collapsed + const detailsTag = hasChanges ? '
\n' : '
\n'; + commentBody += `${detailsTag}šŸ“Š Quick Summary\n\n`; + commentBody += '| Project | Total Size | Change |\n'; + commentBody += '|---------|------------|--------|\n'; + + for (const report of reportsWithCurrent) { + if (!report.current) continue; + const currentSize = report.current.totalSize; + const baselineSize = report.baseline?.totalSize || 0; + const diff = report.baseline ? calculateDiff(currentSize, baselineSize) : { value: '-', emoji: '' }; + const sizeStr = formatBytes(currentSize); + commentBody += `| ${report.projectName} | ${sizeStr} | ${diff.emoji} ${diff.value} |\n`; + } + + commentBody += '\n
\n\n'; + } + + // Helper function to check if a report has significant changes + + const hasSignificantChanges = (report: ProjectReport): boolean => { + if (!report.current) return false; + if (!report.baseline) return true; // No baseline means we can't compare, show it + const currentSize = report.current.totalSize; + const baselineSize = report.baseline.totalSize; + if (baselineSize === 0 || isNaN(baselineSize)) return false; + const diff = currentSize - baselineSize; + // Show detailed report if there's any change (not zero) + return diff !== 0; + }; + + // Filter reports with changes + const reportsWithChanges = projectReports.filter(report => { + if (!report.current) return false; + return hasSignificantChanges(report); + }); + + // Generate detailed reports only for projects with changes + if (reportsWithChanges.length > 0) { + // Only add collapse wrapper if there are multiple reports with changes + commentBody += '
\nšŸ“‹ Detailed Reports (Click to expand)\n\n'; + + for (const report of reportsWithChanges) { + commentBody += generateProjectMarkdown(report.projectName, report.filePath, report.current!, report.baseline || undefined, report.baselineCommitHash, report.baselinePRs); + + // Add diff HTML link if available + if (report.diffHtmlArtifactId) { + const artifactDownloadLink = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts/${report.diffHtmlArtifactId}`; + commentBody += `\nšŸ“¦ **Download Diff Report**: [${report.projectName} Bundle Diff](${artifactDownloadLink})\n\n`; + } + } - // Add diff HTML link if available - if (report.diffHtmlArtifactId) { - const artifactDownloadLink = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts/${report.diffHtmlArtifactId}`; - commentBody += `\nšŸ“¦ **Download Diff Report**: [${report.projectName} Bundle Diff](${artifactDownloadLink})\n\n`; + if (reportsWithChanges.length > 1) { + commentBody += '
\n\n'; } } diff --git a/src/report.ts b/src/report.ts index 480e32c..c379d12 100644 --- a/src/report.ts +++ b/src/report.ts @@ -49,7 +49,7 @@ export interface BundleAnalysis { }>; } -function formatBytes(bytes: number): string { +export function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; @@ -155,20 +155,31 @@ export function loadSizeData(filePath: string): SizeData | null { } } -function calculateDiff(current: number, baseline: number): { value: string; emoji: string } { +export function calculateDiff(current: number, baseline: number): { value: string; emoji: string } { if (!baseline || baseline === 0 || isNaN(baseline)) { - return { value: 'N/A', emoji: 'ā“' }; + return { value: '0', emoji: 'ā“' }; } if (isNaN(current)) { - return { value: 'N/A', emoji: 'ā“' }; + return { value: '0', emoji: 'ā“' }; } const diff = current - baseline; + + // If diff is 0, just return "0" + if (diff === 0) { + return { value: '0', emoji: '' }; + } + const percent = (diff / baseline) * 100; if (Math.abs(percent) < 1) { - return { value: `${formatBytes(diff)} (${percent.toFixed(1)}%)`, emoji: 'āž”ļø' }; + // For small changes, still show + sign if it's an increase + if (diff > 0) { + return { value: `+${formatBytes(diff)} (${percent.toFixed(1)}%)`, emoji: ''}; + } else { + return { value: `${formatBytes(diff)} (${percent.toFixed(1)}%)`, emoji: ''}; + } } else if (diff > 0) { return { value: `+${formatBytes(diff)} (+${percent.toFixed(1)}%)`, emoji: 'šŸ“ˆ' }; } else { @@ -256,31 +267,31 @@ export async function generateBundleAnalysisReport( { data: 'šŸ“Š Total Size', header: false }, { data: formatBytes(current.totalSize), header: false }, { data: baseline ? formatBytes(baseline.totalSize) : formatBytes(current.totalSize), header: false }, - { data: baseline ? calculateDiff(current.totalSize, baseline.totalSize).value : 'N/A', header: false } + { data: baseline ? calculateDiff(current.totalSize, baseline.totalSize).value : '0', header: false } ], [ { data: 'šŸ“„ JavaScript', header: false }, { data: formatBytes(current.jsSize), header: false }, { data: baseline ? formatBytes(baseline.jsSize) : formatBytes(current.jsSize), header: false }, - { data: baseline ? calculateDiff(current.jsSize, baseline.jsSize).value : 'N/A', header: false } + { data: baseline ? calculateDiff(current.jsSize, baseline.jsSize).value : '0', header: false } ], [ { data: 'šŸŽØ CSS', header: false }, { data: formatBytes(current.cssSize), header: false }, { data: baseline ? formatBytes(baseline.cssSize) : formatBytes(current.cssSize), header: false }, - { data: baseline ? calculateDiff(current.cssSize, baseline.cssSize).value : 'N/A', header: false } + { data: baseline ? calculateDiff(current.cssSize, baseline.cssSize).value : '0', header: false } ], [ { data: '🌐 HTML', header: false }, { data: formatBytes(current.htmlSize), header: false }, { data: baseline ? formatBytes(baseline.htmlSize) : formatBytes(current.htmlSize), header: false }, - { data: baseline ? calculateDiff(current.htmlSize, baseline.htmlSize).value : 'N/A', header: false } + { data: baseline ? calculateDiff(current.htmlSize, baseline.htmlSize).value : '0', header: false } ], [ { data: 'šŸ“ Other Assets', header: false }, { data: formatBytes(current.otherSize), header: false }, { data: baseline ? formatBytes(baseline.otherSize) : formatBytes(current.otherSize), header: false }, - { data: baseline ? calculateDiff(current.otherSize, baseline.otherSize).value : 'N/A', header: false } + { data: baseline ? calculateDiff(current.otherSize, baseline.otherSize).value : '0', header: false } ] ]; @@ -310,7 +321,7 @@ export async function generateSizeReport(current: SizeData, baseline?: SizeData) [ { data: 'šŸ“Š Total Size', header: false }, { data: formatBytes(current.totalSize), header: false }, - { data: baseline ? formatBytes(baseline.totalSize) : 'N/A', header: false } + { data: baseline ? formatBytes(baseline.totalSize) : '0', header: false } ] ];