Skip to content
Draft
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
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Figure Technologies
* Copyright (C) 2024-2025 Figure Technologies
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Figure Technologies
* Copyright (C) 2024-2025 Figure Technologies
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* 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"
}
}

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"
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -34,7 +35,23 @@ class Branch(
Env.isCI -> Env.githubHeadRef ?: Env.githubRefName
else -> git.repository.branch
}
return branchList.find(refName) ?: 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
// 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
// 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 {
// For non-CI environments or when not in a PR, use HEAD
headRef
}
}

fun isOnMainBranch(providedMainBranch: String? = null): Boolean =
Expand All @@ -51,3 +68,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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading