Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/api/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ export function buildBuildPayload(options) {
payload.commit_message = options.message || options.commit_message;
}

if (options.commit_author_name) {
payload.commit_author_name = options.commit_author_name;
}

if (options.commit_author_email) {
payload.commit_author_email = options.commit_author_email;
}

if (options.pullRequestNumber || options.github_pull_request_number) {
payload.github_pull_request_number =
options.pullRequestNumber || options.github_pull_request_number;
Expand Down
5 changes: 5 additions & 0 deletions src/commands/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
import {
detectBranch as defaultDetectBranch,
detectCommit as defaultDetectCommit,
detectCommitAuthor as defaultDetectCommitAuthor,
detectCommitMessage as defaultDetectCommitMessage,
detectPullRequestNumber as defaultDetectPullRequestNumber,
generateBuildNameWithGit as defaultGenerateBuildNameWithGit,
Expand Down Expand Up @@ -119,6 +120,7 @@ export async function runCommand(
runTests = defaultRunTests,
detectBranch = defaultDetectBranch,
detectCommit = defaultDetectCommit,
detectCommitAuthor = defaultDetectCommitAuthor,
detectCommitMessage = defaultDetectCommitMessage,
detectPullRequestNumber = defaultDetectPullRequestNumber,
generateBuildNameWithGit = defaultGenerateBuildNameWithGit,
Expand Down Expand Up @@ -245,6 +247,7 @@ export async function runCommand(
let commit = await detectCommit(options.commit || config.build.commit);
let message =
options.message || config.build.message || (await detectCommitMessage());
let commitAuthor = await detectCommitAuthor();
let buildName = await generateBuildNameWithGit(
options.buildName || configuredBuildName
);
Expand Down Expand Up @@ -306,6 +309,8 @@ export async function runCommand(
branch,
commit,
message,
commitAuthorName: commitAuthor.name,
commitAuthorEmail: commitAuthor.email,
environment: config.build.environment,
threshold: config.comparison.threshold,
minClusterSize: config.comparison.minClusterSize,
Expand Down
5 changes: 5 additions & 0 deletions src/commands/tdd.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
import {
detectBranch as defaultDetectBranch,
detectCommit as defaultDetectCommit,
detectCommitAuthor as defaultDetectCommitAuthor,
detectCommitMessage as defaultDetectCommitMessage,
detectPullRequestNumber as defaultDetectPullRequestNumber,
generateBuildNameWithGit as defaultGenerateBuildNameWithGit,
Expand Down Expand Up @@ -75,6 +76,7 @@ export async function tddCommand(
runTests = defaultRunTests,
detectBranch = defaultDetectBranch,
detectCommit = defaultDetectCommit,
detectCommitAuthor = defaultDetectCommitAuthor,
detectCommitMessage = defaultDetectCommitMessage,
detectPullRequestNumber = defaultDetectPullRequestNumber,
generateBuildNameWithGit = defaultGenerateBuildNameWithGit,
Expand Down Expand Up @@ -136,6 +138,7 @@ export async function tddCommand(
let commit = await detectCommit(options.commit || config.build.commit);
let message =
options.message || config.build.message || (await detectCommitMessage());
let commitAuthor = await detectCommitAuthor();
let buildName = await generateBuildNameWithGit(
options.buildName || configuredBuildName
);
Expand Down Expand Up @@ -191,6 +194,8 @@ export async function tddCommand(
branch,
commit,
message,
commitAuthorName: commitAuthor.name,
commitAuthorEmail: commitAuthor.email,
environment: config.build.environment,
pullRequestNumber,
parallelId: config.parallelId,
Expand Down
5 changes: 5 additions & 0 deletions src/commands/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
import {
detectBranch as defaultDetectBranch,
detectCommit as defaultDetectCommit,
detectCommitAuthor as defaultDetectCommitAuthor,
detectCommitMessage as defaultDetectCommitMessage,
detectPullRequestNumber as defaultDetectPullRequestNumber,
generateBuildNameWithGit as defaultGenerateBuildNameWithGit,
Expand Down Expand Up @@ -77,6 +78,7 @@ export async function uploadCommand(
createUploader = defaultCreateUploader,
detectBranch = defaultDetectBranch,
detectCommit = defaultDetectCommit,
detectCommitAuthor = defaultDetectCommitAuthor,
detectCommitMessage = defaultDetectCommitMessage,
detectPullRequestNumber = defaultDetectPullRequestNumber,
generateBuildNameWithGit = defaultGenerateBuildNameWithGit,
Expand Down Expand Up @@ -121,6 +123,7 @@ export async function uploadCommand(
let commit = await detectCommit(options.commit || config.build.commit);
let message =
options.message || config.build.message || (await detectCommitMessage());
let commitAuthor = await detectCommitAuthor();
let buildName = await generateBuildNameWithGit(
options.buildName || configuredBuildName
);
Expand Down Expand Up @@ -148,6 +151,8 @@ export async function uploadCommand(
branch,
commit,
message,
commitAuthorName: commitAuthor.name,
commitAuthorEmail: commitAuthor.email,
environment: config.build.environment,
threshold: config.comparison.threshold,
minClusterSize: config.comparison.minClusterSize,
Expand Down
2 changes: 2 additions & 0 deletions src/test-runner/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ export function buildApiBuildPayload(options, comparisonConfig = null) {
environment: options.environment || 'test',
commit_sha: options.commit,
commit_message: options.message,
commit_author_name: options.commitAuthorName,
commit_author_email: options.commitAuthorEmail,
github_pull_request_number: options.pullRequestNumber,
parallel_id: options.parallelId,
};
Expand Down
4 changes: 4 additions & 0 deletions src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ export interface UploadOptions {
branch?: string;
commit?: string;
message?: string;
commitAuthorName?: string;
commitAuthorEmail?: string;
environment?: string;
threshold?: number;
minClusterSize?: number;
Expand Down Expand Up @@ -517,6 +519,8 @@ export interface BuildOptions {
commit_sha?: string;
message?: string;
commit_message?: string;
commitAuthorName?: string;
commitAuthorEmail?: string;
environment?: string;
threshold?: number;
eager?: boolean;
Expand Down
2 changes: 2 additions & 0 deletions src/uploader/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ export function buildBuildInfo(options, defaultBranch = 'main') {
branch: options.branch || defaultBranch || 'main',
commit_sha: options.commit,
commit_message: options.message,
commit_author_name: options.commitAuthorName,
commit_author_email: options.commitAuthorEmail,
environment: options.environment || 'production',
threshold: options.threshold,
github_pull_request_number: options.pullRequestNumber,
Expand Down
41 changes: 39 additions & 2 deletions src/utils/ci-env.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,25 @@ export function getCommit() {
}

/**
* Get the commit message from CI environment variables
* Get the commit message from CI environment variables.
*
* For GitHub Actions pull_request events, the checkout is a synthetic merge
* commit whose message is "Merge <sha> into <sha>", so the PR title from the
* event payload is used instead.
*
* @returns {string|null} Commit message or null if not available
*/
export function getCommitMessage() {
if (process.env.VIZZLY_COMMIT_MESSAGE) {
return process.env.VIZZLY_COMMIT_MESSAGE;
}

if (process.env.GITHUB_ACTIONS) {
let title = getGitHubEvent().pull_request?.title;
if (title) return title;
}

return (
process.env.VIZZLY_COMMIT_MESSAGE || // Vizzly override
process.env.CI_COMMIT_MESSAGE || // GitLab CI
process.env.TRAVIS_COMMIT_MESSAGE || // Travis CI
process.env.BUILDKITE_MESSAGE || // Buildkite
Expand All @@ -147,6 +160,30 @@ export function getCommitMessage() {
);
}

/**
* Parse a "Name <email>" author string (GitLab's CI_COMMIT_AUTHOR format)
* @param {string|undefined} value - Author string
* @returns {{ name: string|null, email: string|null }}
*/
function parseAuthorString(value) {
let match = value?.match(/^(.*?)\s*<([^>]*)>\s*$/);
if (!match) return { name: value?.trim() || null, email: null };
return { name: match[1] || null, email: match[2] || null };
}

/**
* Get the commit author from CI environment variables
* @returns {{ name: string|null, email: string|null }} Author name and email
*/
export function getCommitAuthor() {
let gitlab = parseAuthorString(process.env.CI_COMMIT_AUTHOR);

return {
name: process.env.VIZZLY_COMMIT_AUTHOR_NAME || gitlab.name || null,
email: process.env.VIZZLY_COMMIT_AUTHOR_EMAIL || gitlab.email || null,
};
}

function parsePullRequestNumber(value) {
if (!/^\d+$/.test(value)) {
return null;
Expand Down
55 changes: 50 additions & 5 deletions src/utils/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { promisify } from 'node:util';
import {
getBranch as getCIBranch,
getCommit as getCICommit,
getCommitAuthor as getCICommitAuthor,
getCommitMessage as getCICommitMessage,
getPullRequestNumber,
} from './ci-env.js';
Expand Down Expand Up @@ -151,8 +152,52 @@ export async function detectCommitMessage(
let ciCommitMessage = getCICommitMessage();
if (ciCommitMessage) return ciCommitMessage;

// Fallback to regular git log
return await getCommitMessage(cwd);
// Fallback to git, reading the same commit reported as commit_sha
return await readCommit('%B', cwd);
}

/**
* Read a formatted field from the detected build commit.
*
* Uses detectCommit() so CI checkouts (e.g. GitHub's synthetic PR merge
* commit) describe the PR head rather than HEAD. Falls back to HEAD when that
* commit is not in local history (shallow clones).
*
* @param {string} format - git log --format string
* @param {string} cwd - Working directory
* @returns {Promise<string|null>} Formatted output or null
*/
async function readCommit(format, cwd = process.cwd()) {
let sha = await detectCommit(null, cwd);
let refs = /^[0-9a-f]{4,64}$/i.test(sha || '') ? [sha, 'HEAD'] : ['HEAD'];

for (let ref of refs) {
try {
return await runGit(['log', '-1', `--format=${format}`, ref], cwd);
} catch {
// Commit not available locally, try the next ref
}
}

return null;
}

/**
* Detect commit author with environment variable support, falling back to git
* @param {string} cwd - Working directory
* @returns {Promise<{ name: string|null, email: string|null }>}
*/
export async function detectCommitAuthor(cwd = process.cwd()) {
let author = getCICommitAuthor();
if (author.name && author.email) return author;

let stdout = await readCommit('%an%x00%ae', cwd);
let [gitName, gitEmail] = stdout ? stdout.split('\0') : [];

return {
name: author.name || gitName || null,
email: author.email || gitEmail || null,
};
}

/**
Expand Down Expand Up @@ -241,10 +286,10 @@ export async function generateBuildNameWithGit(
) {
if (override) return override;

let branch = await getCurrentBranch(cwd);
let shortSha = await getCurrentCommitSha(cwd);
let branch = await detectBranch(null, cwd);
let shortSha = await detectCommit(null, cwd);

if (branch && shortSha) {
if (branch && branch !== 'unknown' && shortSha) {
let shortCommit = shortSha.substring(0, 7);
return `${branch}-${shortCommit}`;
}
Expand Down
19 changes: 19 additions & 0 deletions tests/api/core.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,25 @@ describe('api/core', () => {
});

describe('buildBuildPayload', () => {
it('includes commit author fields only when present', () => {
let base = { name: 'Build', branch: 'main', environment: 'test' };

let withAuthor = buildBuildPayload({
...base,
commit_author_name: 'Ada Lovelace',
commit_author_email: 'ada@example.com',
});
let withoutAuthor = buildBuildPayload({
...base,
commit_author_name: null,
commit_author_email: undefined,
});

assert.strictEqual(withAuthor.commit_author_name, 'Ada Lovelace');
assert.strictEqual(withAuthor.commit_author_email, 'ada@example.com');
assert.deepStrictEqual(withoutAuthor, base);
});

it('builds basic payload with name, branch, environment', () => {
let result = buildBuildPayload({
name: 'My Build',
Expand Down
Loading
Loading