Skip to content

Commit 298a673

Browse files
mwarkentinclaude
andauthored
fix(node,vercel-edge,bundler-plugins): INC-2566 - Ignore commit env vars set to their own name (#25244)
Some builds have `VERCEL_GIT_COMMIT_SHA` set to the literal string `VERCEL_GIT_COMMIT_SHA` instead of a SHA. Release auto-detection took that value as the release name, so every build of the app uploaded its source maps into, and reported events against, one release that never changes. This is not theoretical: Sentry has received source map uploads and events whose release is the literal string `VERCEL_GIT_COMMIT_SHA` from a number of organizations deploying on Vercel. Some were large enough to contribute to [INC-2566](https://firetower.getsentry.net/INC-2566). How those builds end up with that value is still being investigated; getsentry/sentry#127101 logs the placeholder name at upload time to help with that. This guard stops the value from becoming the release either way. Release detection in the bundler plugins and the Node and Vercel Edge SDKs now skips a commit env var whose value is its own name, warns, and moves on to the next candidate. Candidates are read in order and only until one is found, so a placeholder is only warned about when it would otherwise have become the release. In the bundler plugins, the Vercel commit association in `setCommits` uses the same check, so a placeholder SHA falls back to automatic commit association instead of being sent as the commit. Claude-Session: https://claude.ai/code/session_01WgDm3ijhQZQUHGAFFqQzKQ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 50ed82b commit 298a673

8 files changed

Lines changed: 380 additions & 148 deletions

File tree

‎packages/bundler-plugins/src/core/options-mapping.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
ModuleMetadata,
1010
ModuleMetadataCallback,
1111
} from './types';
12-
import { determineReleaseName } from './utils';
12+
import { determineReleaseName, readCommitEnvVar } from './utils';
1313

1414
export type NormalizedOptions = {
1515
org: string | undefined;
@@ -135,9 +135,10 @@ export function normalizeUserOptions(userOptions: UserOptions): NormalizedOption
135135
};
136136

137137
if (options.release.setCommits === undefined) {
138+
const vercelCommitSha = readCommitEnvVar('VERCEL_GIT_COMMIT_SHA');
138139
if (
139140
process.env['VERCEL'] &&
140-
process.env['VERCEL_GIT_COMMIT_SHA'] &&
141+
vercelCommitSha &&
141142
process.env['VERCEL_GIT_REPO_SLUG'] &&
142143
process.env['VERCEL_GIT_REPO_OWNER'] &&
143144
// We only want to set commits for the production env because Sentry becomes extremely noisy (eg on slack) for
@@ -147,8 +148,8 @@ export function normalizeUserOptions(userOptions: UserOptions): NormalizedOption
147148
) {
148149
options.release.setCommits = {
149150
shouldNotThrowOnFailure: true,
150-
commit: process.env['VERCEL_GIT_COMMIT_SHA'],
151-
previousCommit: process.env['VERCEL_GIT_PREVIOUS_SHA'],
151+
commit: vercelCommitSha,
152+
previousCommit: readCommitEnvVar('VERCEL_GIT_PREVIOUS_SHA'),
152153
repo: `${process.env['VERCEL_GIT_REPO_OWNER']}/${process.env['VERCEL_GIT_REPO_SLUG']}`,
153154
ignoreEmpty: true,
154155
ignoreMissing: true,

‎packages/bundler-plugins/src/core/utils.ts‎

Lines changed: 74 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -221,93 +221,120 @@ export function determineReleaseName(): string | undefined {
221221
// 1. Git providers
222222
// 2. CI providers with specific environment variables (has the provider name in the variable name)
223223
// 3. CI providers with generic environment variables (checked for last to prevent possible false positives)
224+
// Each group is only read if the earlier ones found nothing, so a placeholder value is only
225+
// warned about when it would otherwise have become the release.
224226

225-
const possibleReleaseNameOfGitProvider =
227+
const possibleReleaseNameOfGitProvider = (): string | undefined =>
226228
// GitHub Actions - https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables
227-
process.env['GITHUB_SHA'] ||
229+
readCommitEnvVar('GITHUB_SHA') ||
228230
// GitLab CI - https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
229-
process.env['CI_MERGE_REQUEST_SOURCE_BRANCH_SHA'] ||
230-
process.env['CI_BUILD_REF'] ||
231-
process.env['CI_COMMIT_SHA'] ||
231+
readCommitEnvVar('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA') ||
232+
readCommitEnvVar('CI_BUILD_REF') ||
233+
readCommitEnvVar('CI_COMMIT_SHA') ||
232234
// Bitbucket - https://support.atlassian.com/bitbucket-cloud/docs/variables-and-secrets/
233-
process.env['BITBUCKET_COMMIT'];
235+
readCommitEnvVar('BITBUCKET_COMMIT');
234236

235-
const possibleReleaseNameOfCiProvidersWithSpecificEnvVar =
237+
const possibleReleaseNameOfCiProvidersWithSpecificEnvVar = (): string | undefined =>
236238
// AppVeyor - https://www.appveyor.com/docs/environment-variables/
237-
process.env['APPVEYOR_PULL_REQUEST_HEAD_COMMIT'] ||
238-
process.env['APPVEYOR_REPO_COMMIT'] ||
239+
readCommitEnvVar('APPVEYOR_PULL_REQUEST_HEAD_COMMIT') ||
240+
readCommitEnvVar('APPVEYOR_REPO_COMMIT') ||
239241
// AWS CodeBuild - https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html
240-
process.env['CODEBUILD_RESOLVED_SOURCE_VERSION'] ||
242+
readCommitEnvVar('CODEBUILD_RESOLVED_SOURCE_VERSION') ||
241243
// AWS Amplify - https://docs.aws.amazon.com/amplify/latest/userguide/environment-variables.html
242-
process.env['AWS_COMMIT_ID'] ||
244+
readCommitEnvVar('AWS_COMMIT_ID') ||
243245
// Azure Pipelines - https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml
244-
process.env['BUILD_SOURCEVERSION'] ||
246+
readCommitEnvVar('BUILD_SOURCEVERSION') ||
245247
// Bitrise - https://devcenter.bitrise.io/builds/available-environment-variables/
246-
process.env['GIT_CLONE_COMMIT_HASH'] ||
248+
readCommitEnvVar('GIT_CLONE_COMMIT_HASH') ||
247249
// Buddy CI - https://buddy.works/docs/pipelines/environment-variables#default-environment-variables
248-
process.env['BUDDY_EXECUTION_REVISION'] ||
250+
readCommitEnvVar('BUDDY_EXECUTION_REVISION') ||
249251
// Builtkite - https://buildkite.com/docs/pipelines/environment-variables
250-
process.env['BUILDKITE_COMMIT'] ||
252+
readCommitEnvVar('BUILDKITE_COMMIT') ||
251253
// CircleCI - https://circleci.com/docs/variables/
252-
process.env['CIRCLE_SHA1'] ||
254+
readCommitEnvVar('CIRCLE_SHA1') ||
253255
// Cirrus CI - https://cirrus-ci.org/guide/writing-tasks/#environment-variables
254-
process.env['CIRRUS_CHANGE_IN_REPO'] ||
256+
readCommitEnvVar('CIRRUS_CHANGE_IN_REPO') ||
255257
// Codefresh - https://codefresh.io/docs/docs/codefresh-yaml/variables/
256-
process.env['CF_REVISION'] ||
258+
readCommitEnvVar('CF_REVISION') ||
257259
// Codemagic - https://docs.codemagic.io/yaml-basic-configuration/environment-variables/
258-
process.env['CM_COMMIT'] ||
260+
readCommitEnvVar('CM_COMMIT') ||
259261
// Cloudflare Pages - https://developers.cloudflare.com/pages/platform/build-configuration/#environment-variables
260-
process.env['CF_PAGES_COMMIT_SHA'] ||
262+
readCommitEnvVar('CF_PAGES_COMMIT_SHA') ||
261263
// Drone - https://docs.drone.io/pipeline/environment/reference/
262-
process.env['DRONE_COMMIT_SHA'] ||
264+
readCommitEnvVar('DRONE_COMMIT_SHA') ||
263265
// Flightcontrol - https://www.flightcontrol.dev/docs/guides/flightcontrol/environment-variables#built-in-environment-variables
264-
process.env['FC_GIT_COMMIT_SHA'] ||
266+
readCommitEnvVar('FC_GIT_COMMIT_SHA') ||
265267
// Heroku #1 https://devcenter.heroku.com/articles/heroku-ci
266-
process.env['HEROKU_TEST_RUN_COMMIT_VERSION'] ||
268+
readCommitEnvVar('HEROKU_TEST_RUN_COMMIT_VERSION') ||
267269
// Heroku #2 https://docs.sentry.io/product/integrations/deployment/heroku/#configure-releases
268-
process.env['HEROKU_SLUG_COMMIT'] ||
270+
readCommitEnvVar('HEROKU_SLUG_COMMIT') ||
269271
// Railway - https://docs.railway.app/reference/variables#git-variables
270-
process.env['RAILWAY_GIT_COMMIT_SHA'] ||
272+
readCommitEnvVar('RAILWAY_GIT_COMMIT_SHA') ||
271273
// Render - https://render.com/docs/environment-variables
272-
process.env['RENDER_GIT_COMMIT'] ||
274+
readCommitEnvVar('RENDER_GIT_COMMIT') ||
273275
// Semaphore CI - https://docs.semaphoreci.com/ci-cd-environment/environment-variables
274-
process.env['SEMAPHORE_GIT_SHA'] ||
276+
readCommitEnvVar('SEMAPHORE_GIT_SHA') ||
275277
// TravisCI - https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
276-
process.env['TRAVIS_PULL_REQUEST_SHA'] ||
278+
readCommitEnvVar('TRAVIS_PULL_REQUEST_SHA') ||
277279
// Vercel - https://vercel.com/docs/v2/build-step#system-environment-variables
278-
process.env['VERCEL_GIT_COMMIT_SHA'] ||
279-
process.env['VERCEL_GITHUB_COMMIT_SHA'] ||
280-
process.env['VERCEL_GITLAB_COMMIT_SHA'] ||
281-
process.env['VERCEL_BITBUCKET_COMMIT_SHA'] ||
280+
readCommitEnvVar('VERCEL_GIT_COMMIT_SHA') ||
281+
readCommitEnvVar('VERCEL_GITHUB_COMMIT_SHA') ||
282+
readCommitEnvVar('VERCEL_GITLAB_COMMIT_SHA') ||
283+
readCommitEnvVar('VERCEL_BITBUCKET_COMMIT_SHA') ||
282284
// Zeit (now known as Vercel)
283-
process.env['ZEIT_GITHUB_COMMIT_SHA'] ||
284-
process.env['ZEIT_GITLAB_COMMIT_SHA'] ||
285-
process.env['ZEIT_BITBUCKET_COMMIT_SHA'];
285+
readCommitEnvVar('ZEIT_GITHUB_COMMIT_SHA') ||
286+
readCommitEnvVar('ZEIT_GITLAB_COMMIT_SHA') ||
287+
readCommitEnvVar('ZEIT_BITBUCKET_COMMIT_SHA');
286288

287-
const possibleReleaseNameOfCiProvidersWithGenericEnvVar =
289+
const possibleReleaseNameOfCiProvidersWithGenericEnvVar = (): string | undefined =>
288290
// CloudBees CodeShip - https://docs.cloudbees.com/docs/cloudbees-codeship/latest/pro-builds-and-configuration/environment-variables
289-
process.env['CI_COMMIT_ID'] ||
291+
readCommitEnvVar('CI_COMMIT_ID') ||
290292
// Coolify - https://coolify.io/docs/knowledge-base/environment-variables
291-
process.env['SOURCE_COMMIT'] ||
293+
readCommitEnvVar('SOURCE_COMMIT') ||
292294
// Heroku #3 https://devcenter.heroku.com/changelog-items/630
293-
process.env['SOURCE_VERSION'] ||
295+
readCommitEnvVar('SOURCE_VERSION') ||
294296
// Jenkins - https://plugins.jenkins.io/git/#environment-variables
295-
process.env['GIT_COMMIT'] ||
297+
readCommitEnvVar('GIT_COMMIT') ||
296298
// Netlify - https://docs.netlify.com/configure-builds/environment-variables/#build-metadata
297-
process.env['COMMIT_REF'] ||
299+
readCommitEnvVar('COMMIT_REF') ||
298300
// TeamCity - https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html
299-
process.env['BUILD_VCS_NUMBER'] ||
300-
// Woodpecker CI - https://woodpecker-ci.org/docs/usage/environment
301-
process.env['CI_COMMIT_SHA'];
301+
readCommitEnvVar('BUILD_VCS_NUMBER');
302+
// Woodpecker CI (https://woodpecker-ci.org/docs/usage/environment) sets CI_COMMIT_SHA, which the
303+
// GitLab check above already reads.
302304

303305
return (
304-
possibleReleaseNameOfGitProvider ||
305-
possibleReleaseNameOfCiProvidersWithSpecificEnvVar ||
306-
possibleReleaseNameOfCiProvidersWithGenericEnvVar ||
306+
possibleReleaseNameOfGitProvider() ||
307+
possibleReleaseNameOfCiProvidersWithSpecificEnvVar() ||
308+
possibleReleaseNameOfCiProvidersWithGenericEnvVar() ||
307309
gitRevision()
308310
);
309311
}
310312

313+
// Release detection and commit association can read the same variable in one build.
314+
const warnedPlaceholderEnvVars = new Set<string>();
315+
316+
/**
317+
* Reads an environment variable that should hold a commit SHA. A value that is the variable's own
318+
* name, such as `VERCEL_GIT_COMMIT_SHA=VERCEL_GIT_COMMIT_SHA`, is ignored: it never changes, so as
319+
* a release name it would put every build into one release.
320+
*/
321+
export function readCommitEnvVar(name: string): string | undefined {
322+
const value = process.env[name];
323+
if (value?.trim() === name) {
324+
if (!warnedPlaceholderEnvVars.has(name)) {
325+
warnedPlaceholderEnvVars.add(name);
326+
// This runs before the plugin logger exists, so `silent: true` does not suppress it. That is
327+
// deliberate: a warning that is easy to miss is how this misconfiguration went unnoticed.
328+
// eslint-disable-next-line no-console
329+
console.warn(
330+
`[@sentry/bundler-plugins] Ignoring the ${name} environment variable as a release name, because its value is its own name. Set the release explicitly, or fix the variable in your build environment.`,
331+
);
332+
}
333+
return undefined;
334+
}
335+
return value;
336+
}
337+
311338
function stringifyForCodeInjection(value: unknown): string | undefined {
312339
// Bundles may be inlined in HTML, where JSON quoting does not escape script tags or HTML comments.
313340
return JSON.stringify(value)?.replace(/</g, '\\u003c');

‎packages/bundler-plugins/test/core/option-mappings.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,36 @@ describe('normalizeUserOptions()', () => {
179179
expect(normalizedOptions.release.deploy).toEqual(manualDeployConfig);
180180
});
181181

182+
test('should associate commits from Vercel env vars on production builds', () => {
183+
process.env['VERCEL'] = '1';
184+
process.env['VERCEL_TARGET_ENV'] = 'production';
185+
process.env['VERCEL_GIT_COMMIT_SHA'] = 'abc123';
186+
process.env['VERCEL_GIT_PREVIOUS_SHA'] = 'def456';
187+
process.env['VERCEL_GIT_REPO_OWNER'] = 'my-owner';
188+
process.env['VERCEL_GIT_REPO_SLUG'] = 'my-repo';
189+
190+
const normalizedOptions = normalizeUserOptions({ release: { name: 'my-release' } });
191+
192+
expect(normalizedOptions.release.setCommits).toEqual(
193+
expect.objectContaining({ commit: 'abc123', previousCommit: 'def456', repo: 'my-owner/my-repo' }),
194+
);
195+
});
196+
197+
test('should fall back to automatic commit association when VERCEL_GIT_COMMIT_SHA is its own name', () => {
198+
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
199+
process.env['VERCEL'] = '1';
200+
process.env['VERCEL_TARGET_ENV'] = 'production';
201+
process.env['VERCEL_GIT_COMMIT_SHA'] = 'VERCEL_GIT_COMMIT_SHA';
202+
process.env['VERCEL_GIT_REPO_OWNER'] = 'my-owner';
203+
process.env['VERCEL_GIT_REPO_SLUG'] = 'my-repo';
204+
205+
const normalizedOptions = normalizeUserOptions({ release: { name: 'my-release' } });
206+
207+
expect(normalizedOptions.release.setCommits).toEqual(expect.objectContaining({ auto: true }));
208+
expect(normalizedOptions.release.setCommits).not.toHaveProperty('commit');
209+
vi.restoreAllMocks();
210+
});
211+
182212
test('should not create deploy config when Vercel env vars are missing', () => {
183213
const userOptions: Options = {
184214
org: 'my-org',

‎packages/bundler-plugins/test/core/utils.test.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,4 +345,45 @@ describe('determineReleaseName', () => {
345345
execSyncSpy.mockRestore();
346346
}
347347
});
348+
it('ignores a commit env var whose value is its own name', () => {
349+
const originalEnv = process.env;
350+
process.env = { VERCEL_GIT_COMMIT_SHA: 'VERCEL_GIT_COMMIT_SHA', COMMIT_REF: 'abc123' };
351+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
352+
353+
try {
354+
expect(determineReleaseName()).toBe('abc123');
355+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('VERCEL_GIT_COMMIT_SHA'));
356+
} finally {
357+
process.env = originalEnv;
358+
warnSpy.mockRestore();
359+
}
360+
});
361+
it('does not warn about a placeholder checked after the variable that supplies the release', () => {
362+
const originalEnv = process.env;
363+
process.env = { VERCEL_GIT_COMMIT_SHA: 'abc123', COMMIT_REF: 'COMMIT_REF' };
364+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
365+
366+
try {
367+
expect(determineReleaseName()).toBe('abc123');
368+
expect(warnSpy).not.toHaveBeenCalled();
369+
} finally {
370+
process.env = originalEnv;
371+
warnSpy.mockRestore();
372+
}
373+
});
374+
it('warns once about a CI_COMMIT_SHA placeholder', () => {
375+
const originalEnv = process.env;
376+
process.env = { CI_COMMIT_SHA: 'CI_COMMIT_SHA' };
377+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
378+
const execSyncSpy = vi.spyOn(childProcess, 'execSync').mockReturnValue(Buffer.from('0'.repeat(40)));
379+
380+
try {
381+
expect(determineReleaseName()).toBe('0'.repeat(40));
382+
expect(warnSpy).toHaveBeenCalledTimes(1);
383+
} finally {
384+
process.env = originalEnv;
385+
warnSpy.mockRestore();
386+
execSyncSpy.mockRestore();
387+
}
388+
});
348389
});

0 commit comments

Comments
 (0)