diff --git a/src/main/java/io/jenkins/plugins/checks/github/GitSCMChecksContext.java b/src/main/java/io/jenkins/plugins/checks/github/GitSCMChecksContext.java index 358c0dc5..58909d89 100644 --- a/src/main/java/io/jenkins/plugins/checks/github/GitSCMChecksContext.java +++ b/src/main/java/io/jenkins/plugins/checks/github/GitSCMChecksContext.java @@ -48,6 +48,17 @@ class GitSCMChecksContext extends GitHubChecksContext { @Override public String getHeadSha() { + // When checkout fails, BuildData is either absent from the current run or + // carries a stale build number from a previous build. In both cases, + // GIT_COMMIT from run.getEnvironment() is also unreliable because + // GitSCM.buildEnvironment() walks back through previous builds' BuildData. + BuildData gitBuildData = run.getAction(BuildData.class); + if (gitBuildData == null + || gitBuildData.lastBuild == null + || gitBuildData.lastBuild.getBuildNumber() != run.getNumber()) { + return StringUtils.EMPTY; + } + try { String head = getGitCommitEnvironment(); if (StringUtils.isNotBlank(head)) { diff --git a/src/test/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherFactoryTest.java b/src/test/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherFactoryTest.java index 8e111835..097468fd 100644 --- a/src/test/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherFactoryTest.java +++ b/src/test/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherFactoryTest.java @@ -6,6 +6,8 @@ import hudson.model.TaskListener; import hudson.plugins.git.GitSCM; import hudson.plugins.git.UserRemoteConfig; +import hudson.plugins.git.util.Build; +import hudson.plugins.git.util.BuildData; import jenkins.scm.api.SCMHead; import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials; @@ -78,6 +80,12 @@ void shouldCreateGitHubChecksPublisherFromRunForProjectWithValidGitSCM() throws SCMFacade scmFacade = mock(SCMFacade.class); EnvVars envVars = mock(EnvVars.class); + BuildData buildData = mock(BuildData.class); + Build lastBuild = mock(Build.class); + buildData.lastBuild = lastBuild; + when(lastBuild.getBuildNumber()).thenReturn(1); + when(run.getNumber()).thenReturn(1); + when(run.getAction(BuildData.class)).thenReturn(buildData); when(run.getParent()).thenReturn(job); when(run.getEnvironment(TaskListener.NULL)).thenReturn(envVars); when(envVars.get("GIT_COMMIT")).thenReturn("a1b2c3"); diff --git a/src/test/java/io/jenkins/plugins/checks/github/GitSCMChecksContextITest.java b/src/test/java/io/jenkins/plugins/checks/github/GitSCMChecksContextITest.java index c5450551..d1f68d1b 100644 --- a/src/test/java/io/jenkins/plugins/checks/github/GitSCMChecksContextITest.java +++ b/src/test/java/io/jenkins/plugins/checks/github/GitSCMChecksContextITest.java @@ -82,4 +82,35 @@ void shouldRetrieveContextFromPipeline(JenkinsRule j) throws Exception { assertThat(gitSCMChecksContext.getCredentialsId()).isEqualTo(CREDENTIALS_ID); assertThat(gitSCMChecksContext.getHeadSha()).isEqualTo(EXISTING_HASH); } + + /** + * Verifies that when a checkout fails on a subsequent build, {@link GitSCMChecksContext#getHeadSha()} + * returns empty rather than returning the stale SHA from the previous successful build. + * This prevents checks from being posted against the wrong commit when checkout fails + * (e.g. due to network flakiness). + */ + @Test + void shouldReturnEmptyHeadShaWhenCheckoutFails(JenkinsRule j) throws Exception { + FreeStyleProject job = j.createFreeStyleProject(); + + // First build: successful checkout populates BuildData with the correct SHA + GitSCM scm = new GitSCM(GitSCM.createRepoList(HTTP_URL, CREDENTIALS_ID), + Collections.singletonList(new BranchSpec(EXISTING_HASH)), + null, null, Collections.emptyList()); + job.setScm(scm); + Run successfulRun = buildSuccessfully(j, job); + assertThat(new GitSCMChecksContext(successfulRun, URL_NAME).getHeadSha()).isEqualTo(EXISTING_HASH); + + // Second build: use a non-existent SHA to simulate checkout failure. + // The Git plugin carries over BuildData from the previous build, but since + // the checkout never completes, lastBuild.hudsonBuildNumber still refers + // to build #1. + job.setScm(new GitSCM(GitSCM.createRepoList(HTTP_URL, CREDENTIALS_ID), + Collections.singletonList(new BranchSpec("0000000000000000000000000000000000000001")), + null, null, Collections.emptyList())); + Run failedRun = j.assertBuildStatus(Result.FAILURE, job.scheduleBuild2(0, new Action[0])); + + // Must return empty, not the previous build's SHA + assertThat(new GitSCMChecksContext(failedRun, URL_NAME).getHeadSha()).isEmpty(); + } }