From f1ce86188618b73a37c42ed6426f6ef1b35b564e Mon Sep 17 00:00:00 2001 From: Tyler Crawford Date: Thu, 25 Sep 2025 09:29:52 -0400 Subject: [PATCH 1/3] fix(branching): cross-repo PR builds fail --- .../com/figure/gradle/semver/internal/command/Branch.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/figure/gradle/semver/internal/command/Branch.kt b/src/main/kotlin/com/figure/gradle/semver/internal/command/Branch.kt index 34a185a..d07766c 100644 --- a/src/main/kotlin/com/figure/gradle/semver/internal/command/Branch.kt +++ b/src/main/kotlin/com/figure/gradle/semver/internal/command/Branch.kt @@ -34,7 +34,9 @@ class Branch( Env.isCI -> Env.githubHeadRef ?: Env.githubRefName else -> git.repository.branch } - return branchList.find(refName) ?: error("Could not find current branch: $refName") + return branchList.find(refName) + ?: headRef.takeIf { !it.target.name.startsWith("refs/heads/") } + ?: error("Could not find current branch: $refName") } fun isOnMainBranch(providedMainBranch: String? = null): Boolean = From 71fbdc43cb76287b9e7d378235f9f01e2f433175 Mon Sep 17 00:00:00 2001 From: Tyler Crawford Date: Thu, 25 Sep 2025 09:52:36 -0400 Subject: [PATCH 2/3] Try adding a SyntheticRe --- .../specs/CrossRepositoryPullRequestSpec.kt | 74 +++++++++++++++++++ .../gradle/semver/internal/command/Branch.kt | 43 ++++++++++- .../semver/internal/command/BranchList.kt | 1 + 3 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 src/functionalTest/kotlin/com/figure/gradle/semver/specs/CrossRepositoryPullRequestSpec.kt diff --git a/src/functionalTest/kotlin/com/figure/gradle/semver/specs/CrossRepositoryPullRequestSpec.kt b/src/functionalTest/kotlin/com/figure/gradle/semver/specs/CrossRepositoryPullRequestSpec.kt new file mode 100644 index 0000000..bb59046 --- /dev/null +++ b/src/functionalTest/kotlin/com/figure/gradle/semver/specs/CrossRepositoryPullRequestSpec.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 Figure Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.figure.gradle.semver.specs + +import com.figure.gradle.semver.internal.environment.Env +import com.figure.gradle.semver.kotest.GradleProjectsExtension +import com.figure.gradle.semver.kotest.shouldOnlyHave +import com.figure.gradle.semver.projects.RegularProject +import com.figure.gradle.semver.projects.SettingsProject +import com.figure.gradle.semver.projects.SubprojectProject +import io.kotest.core.extensions.install +import io.kotest.core.spec.style.FunSpec +import io.kotest.extensions.system.OverrideMode +import io.kotest.extensions.system.withEnvironment +import org.gradle.util.GradleVersion + +class CrossRepositoryPullRequestSpec : FunSpec({ + val projects = install( + GradleProjectsExtension( + RegularProject(projectName = "regular-project"), + SettingsProject(projectName = "settings-project"), + SubprojectProject(projectName = "subproject-project"), + ), + ) + + val mainBranch = "main" + val developmentBranch = "develop" + val forkedFeatureBranch = "fix/builds-fail-cross-repo" + + test("should calculate next version for cross-repository/forked pull request") { + withEnvironment( + environment = mapOf( + Env.CI to "true", + Env.GITHUB_HEAD_REF to forkedFeatureBranch, + ), + mode = OverrideMode.SetOrOverride, + ) { + // Given: Simulate a cross-repo PR scenario where the feature branch doesn't exist locally + // This mimics what happens when GitHub Actions checks out a forked pull request + projects.git { + initialBranch = mainBranch + actions = actions { + commit(message = "1 commit on $mainBranch", tag = "1.0.0") + + checkout(developmentBranch) + commit(message = "1 commit on $developmentBranch") + + // Simulate being on main branch (as in cross-repo PRs) but with GITHUB_HEAD_REF set + // to the forked branch name that doesn't exist locally + checkout(mainBranch) + } + } + + // When: Building should not fail even though the branch name from GITHUB_HEAD_REF doesn't exist locally + projects.build(GradleVersion.current()) + + // Then: Should generate a version using the forked branch name from GITHUB_HEAD_REF + projects.versions shouldOnlyHave "1.0.1-fix-builds-fail-cross-repo.0" + } + } +}) diff --git a/src/main/kotlin/com/figure/gradle/semver/internal/command/Branch.kt b/src/main/kotlin/com/figure/gradle/semver/internal/command/Branch.kt index d07766c..d52492c 100644 --- a/src/main/kotlin/com/figure/gradle/semver/internal/command/Branch.kt +++ b/src/main/kotlin/com/figure/gradle/semver/internal/command/Branch.kt @@ -19,6 +19,7 @@ import com.figure.gradle.semver.internal.command.extension.shortName import com.figure.gradle.semver.internal.environment.Env import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.Constants +import org.eclipse.jgit.lib.ObjectId import org.eclipse.jgit.lib.Ref class Branch( @@ -34,9 +35,20 @@ class Branch( Env.isCI -> Env.githubHeadRef ?: Env.githubRefName else -> git.repository.branch } - return branchList.find(refName) - ?: headRef.takeIf { !it.target.name.startsWith("refs/heads/") } - ?: error("Could not find current branch: $refName") + val foundRef = branchList.find(refName) + if (foundRef != null) { + return foundRef + } + + // Handle cross-repo PRs where the branch doesn't exist locally + // but we still want to use the branch name from GITHUB_HEAD_REF + return if (Env.isCI && Env.githubHeadRef != null && refName == Env.githubHeadRef) { + // Create a synthetic ref object that preserves the original branch name + SyntheticRef(name = "refs/heads/$refName", target = headRef.objectId) + } else { + headRef.takeIf { !it.target.name.startsWith("refs/heads/") } + ?: headRef + } } fun isOnMainBranch(providedMainBranch: String? = null): Boolean = @@ -53,3 +65,28 @@ class Branch( .setForce(true) .call() } + +/** + * Synthetic Ref implementation for cross-repository PRs where the branch doesn't exist locally + * but we want to preserve the original branch name for version calculation + */ +private class SyntheticRef( + private val name: String, + private val target: ObjectId, +) : Ref { + override fun getName(): String = name + + override fun isSymbolic(): Boolean = false + + override fun getLeaf(): Ref = this + + override fun getTarget(): Ref? = null + + override fun getObjectId(): ObjectId = target + + override fun isPeeled(): Boolean = true + + override fun getPeeledObjectId(): ObjectId? = target + + override fun getStorage(): Ref.Storage = Ref.Storage.LOOSE +} diff --git a/src/main/kotlin/com/figure/gradle/semver/internal/command/BranchList.kt b/src/main/kotlin/com/figure/gradle/semver/internal/command/BranchList.kt index 4610674..58e6f69 100644 --- a/src/main/kotlin/com/figure/gradle/semver/internal/command/BranchList.kt +++ b/src/main/kotlin/com/figure/gradle/semver/internal/command/BranchList.kt @@ -93,6 +93,7 @@ class BranchList( ?: git.repository.resolve(baseBranchName) val targetBranch: ObjectId = git.repository.resolve(targetBranchName) + ?: git.repository.resolve("HEAD") // Fall back to HEAD for synthetic refs in cross-repo PRs return git.revWalk { revWalk -> revWalk.apply { From 92867b541e9538f0db9e1fd07ed04a9e85d16d68 Mon Sep 17 00:00:00 2001 From: Tyler Crawford Date: Thu, 25 Sep 2025 10:24:27 -0400 Subject: [PATCH 3/3] Try another change --- .../specs/CrossRepositoryPullRequestSpec.kt | 53 +++++++++++++++++++ .../gradle/semver/internal/command/Branch.kt | 13 +++-- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/functionalTest/kotlin/com/figure/gradle/semver/specs/CrossRepositoryPullRequestSpec.kt b/src/functionalTest/kotlin/com/figure/gradle/semver/specs/CrossRepositoryPullRequestSpec.kt index bb59046..45080e2 100644 --- a/src/functionalTest/kotlin/com/figure/gradle/semver/specs/CrossRepositoryPullRequestSpec.kt +++ b/src/functionalTest/kotlin/com/figure/gradle/semver/specs/CrossRepositoryPullRequestSpec.kt @@ -71,4 +71,57 @@ class CrossRepositoryPullRequestSpec : FunSpec({ projects.versions shouldOnlyHave "1.0.1-fix-builds-fail-cross-repo.0" } } + + test("should handle cross-repo PR with both GITHUB_HEAD_REF and GITHUB_REF_NAME set") { + withEnvironment( + environment = mapOf( + Env.CI to "true", + Env.GITHUB_HEAD_REF to forkedFeatureBranch, + Env.GITHUB_REF_NAME to "123/merge", // Typical PR merge ref + ), + mode = OverrideMode.SetOrOverride, + ) { + // Given: Similar setup but with both environment variables set + projects.git { + initialBranch = mainBranch + actions = actions { + commit(message = "1 commit on $mainBranch", tag = "1.0.0") + checkout(mainBranch) // Stay on main to simulate cross-repo PR checkout + } + } + + // When: Building should prioritize GITHUB_HEAD_REF over GITHUB_REF_NAME + projects.build(GradleVersion.current()) + + // Then: Should use the forked branch name, not the merge ref + projects.versions shouldOnlyHave "1.0.1-fix-builds-fail-cross-repo.0" + } + } + + test("should fallback to GITHUB_REF_NAME when GITHUB_HEAD_REF is empty") { + withEnvironment( + environment = mapOf( + Env.CI to "true", + Env.GITHUB_HEAD_REF to "", // Empty but present + Env.GITHUB_REF_NAME to "feature-branch-fallback", + ), + mode = OverrideMode.SetOrOverride, + ) { + // Given: GITHUB_HEAD_REF is empty (not a PR) but GITHUB_REF_NAME is set + projects.git { + initialBranch = mainBranch + actions = actions { + commit(message = "1 commit on $mainBranch", tag = "1.0.0") + checkout("feature-branch-fallback") + commit(message = "1 commit on feature branch") + } + } + + // When: Building should use GITHUB_REF_NAME since GITHUB_HEAD_REF is empty + projects.build(GradleVersion.current()) + + // Then: Should use the ref name from GITHUB_REF_NAME with correct commit count + projects.versions shouldOnlyHave "1.0.1-feature-branch-fallback.1" + } + } }) diff --git a/src/main/kotlin/com/figure/gradle/semver/internal/command/Branch.kt b/src/main/kotlin/com/figure/gradle/semver/internal/command/Branch.kt index d52492c..7297d14 100644 --- a/src/main/kotlin/com/figure/gradle/semver/internal/command/Branch.kt +++ b/src/main/kotlin/com/figure/gradle/semver/internal/command/Branch.kt @@ -41,13 +41,16 @@ class Branch( } // Handle cross-repo PRs where the branch doesn't exist locally - // but we still want to use the branch name from GITHUB_HEAD_REF - return if (Env.isCI && Env.githubHeadRef != null && refName == Env.githubHeadRef) { + // This happens when GitHub Actions checks out the target repository but the + // branch name from GITHUB_HEAD_REF (from forked repo) doesn't exist locally + return if (Env.isCI && Env.githubHeadRef != null) { // Create a synthetic ref object that preserves the original branch name - SyntheticRef(name = "refs/heads/$refName", target = headRef.objectId) + // This ensures version calculation uses the actual feature branch name + // instead of falling back to HEAD or the merge ref + SyntheticRef(name = "refs/heads/${Env.githubHeadRef}", target = headRef.objectId) } else { - headRef.takeIf { !it.target.name.startsWith("refs/heads/") } - ?: headRef + // For non-CI environments or when not in a PR, use HEAD + headRef } }