Skip to content

Commit 378cda3

Browse files
Read ref from GITHUB_HEAD_REF env variable if it's present (#776)
* Read ref from GITHUB_HEAD_REF env variable if it's present * Clear Github Actions env variables in tests * Implement annotation-driven extension @WithEnvironment and use it in test for GITHUB_HEAD_REF * Extract ClearGithubEnvVariablesExtension * Refactor WithEnvironmentExtension * Add JavaDoc * Support other Github event types * Go back to reading only GITHUB_HEAD_REF, provide documentation and JavaDocs
1 parent 7fd4882 commit 378cda3

File tree

8 files changed

+195
-54
lines changed

8 files changed

+195
-54
lines changed

docs/configuration/version.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ collection type used (default for `[:]` is LinkedHashMap).
297297

298298
#### versionWithBranch [default]
299299

300+
This is the default version creator since version 1.18.0 of the plugin.
301+
300302
scmVersion {
301303
versionCreator('versionWithBranch')
302304
}
@@ -307,7 +309,13 @@ This version creator appends branch name to version unless you are on
307309
decorate(version: '0.1.0', branch: 'master') == 0.1.0
308310
decorate(version: '0.1.0', branch: 'my-special-branch') == 0.1.0-my-special-branch
309311

310-
This is the default version creator since version 1.18.0 of the plugin.
312+
If your Gradle build is executed within workflow on GitHub Actions and that workflow is triggered by either
313+
`pull_request` or `pull_request_target` event, GitHub will not check out the source branch of the pull request but
314+
a custom reference `refs/pull/<pr_number>/merge` (known as "merge branch"). Consequently, the repository will be in
315+
detached-HEAD state.
316+
317+
In that case, the branch name will be taken from `GITHUB_HEAD_REF` environment variable
318+
[provided by GitHub](https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables).
311319

312320
#### simple
313321

src/main/java/pl/allegro/tech/build/axion/release/infrastructure/git/GitRepository.java

Lines changed: 56 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,26 @@
11
package pl.allegro.tech.build.axion.release.infrastructure.git;
22

3-
import org.eclipse.jgit.api.AddCommand;
4-
import org.eclipse.jgit.api.FetchCommand;
5-
import org.eclipse.jgit.api.Git;
6-
import org.eclipse.jgit.api.LogCommand;
7-
import org.eclipse.jgit.api.PushCommand;
8-
import org.eclipse.jgit.api.Status;
3+
import org.eclipse.jgit.api.*;
94
import org.eclipse.jgit.api.errors.GitAPIException;
105
import org.eclipse.jgit.api.errors.NoHeadException;
116
import org.eclipse.jgit.diff.DiffFormatter;
127
import org.eclipse.jgit.errors.RepositoryNotFoundException;
13-
import org.eclipse.jgit.lib.AnyObjectId;
14-
import org.eclipse.jgit.lib.BranchTrackingStatus;
15-
import org.eclipse.jgit.lib.Config;
16-
import org.eclipse.jgit.lib.Constants;
17-
import org.eclipse.jgit.lib.ObjectId;
18-
import org.eclipse.jgit.lib.Ref;
19-
import org.eclipse.jgit.lib.Repository;
20-
import org.eclipse.jgit.lib.StoredConfig;
8+
import org.eclipse.jgit.lib.*;
219
import org.eclipse.jgit.revwalk.RevCommit;
2210
import org.eclipse.jgit.revwalk.RevSort;
2311
import org.eclipse.jgit.revwalk.RevWalk;
24-
import org.eclipse.jgit.transport.PushResult;
25-
import org.eclipse.jgit.transport.RemoteConfig;
26-
import org.eclipse.jgit.transport.RemoteRefUpdate;
27-
import org.eclipse.jgit.transport.TagOpt;
28-
import org.eclipse.jgit.transport.URIish;
12+
import org.eclipse.jgit.transport.*;
2913
import org.eclipse.jgit.treewalk.filter.PathFilter;
3014
import org.eclipse.jgit.util.SystemReader;
3115
import org.eclipse.jgit.util.io.DisabledOutputStream;
3216
import org.gradle.api.logging.Logger;
3317
import org.gradle.api.logging.Logging;
34-
import pl.allegro.tech.build.axion.release.domain.scm.ScmException;
35-
import pl.allegro.tech.build.axion.release.domain.scm.ScmIdentity;
36-
import pl.allegro.tech.build.axion.release.domain.scm.ScmPosition;
37-
import pl.allegro.tech.build.axion.release.domain.scm.ScmProperties;
38-
import pl.allegro.tech.build.axion.release.domain.scm.ScmPushOptions;
39-
import pl.allegro.tech.build.axion.release.domain.scm.ScmPushResult;
40-
import pl.allegro.tech.build.axion.release.domain.scm.ScmRepository;
41-
import pl.allegro.tech.build.axion.release.domain.scm.ScmRepositoryUnavailableException;
42-
import pl.allegro.tech.build.axion.release.domain.scm.TagsOnCommit;
18+
import pl.allegro.tech.build.axion.release.domain.scm.*;
4319

4420
import java.io.File;
4521
import java.io.IOException;
4622
import java.net.URISyntaxException;
47-
import java.util.ArrayList;
48-
import java.util.HashMap;
49-
import java.util.List;
50-
import java.util.Map;
51-
import java.util.Set;
52-
import java.util.Optional;
23+
import java.util.*;
5324
import java.util.regex.Pattern;
5425
import java.util.stream.Collectors;
5526
import java.util.stream.StreamSupport;
@@ -333,37 +304,72 @@ private void assertPathExists(String path) {
333304
}
334305

335306
public ScmPosition currentPosition() {
307+
String revision = getRevision();
308+
String branchName = branchName();
309+
boolean isClean = !checkUncommittedChanges();
310+
return new ScmPosition(revision, branchName, isClean);
311+
}
312+
313+
private String getRevision() {
336314
try {
337315
String revision = "";
338316
if (hasCommits()) {
339317
ObjectId head = head();
340318
revision = head.name();
341319
}
342-
343-
boolean isClean = !checkUncommittedChanges();
344-
345-
String branchName = branchName();
346-
return new ScmPosition(revision, branchName, isClean);
320+
return revision;
347321
} catch (IOException e) {
348322
throw new ScmException(e);
349323
}
350324
}
351325

326+
private String branchName() {
327+
return branchNameFromGithubEnvVariable().orElseGet(this::branchNameFromGit);
328+
}
329+
352330
/**
353-
* @return branch name or 'HEAD' when in detached state, unless it is overridden by 'overriddenBranchName'
331+
* <p>If executed within workflow on GitHub Actions and that workflow is triggered by pull_request (or pull_request_target)
332+
* event, GitHub will not check out the source branch of the PR, but some custom ref: 'refs/pull/<pr_number>/merge'
333+
* (they call it a "merge branch"). It effectively means that repository is in detached-HEAD state and axion-release
334+
* is not able to get the branch name from HEAD ref.</p>
335+
*
336+
* <p>For pull_request and pull_request_target events, GitHub sets a special environment variable GITHUB_HEAD_REF.
337+
* It contains the head ref of source branch of the PR which triggered the workflow. This variable is not set
338+
* for other event types.</p>
339+
*
340+
* <p>If the build is not executed on GitHub Actions, this method will always return Optional.empty.</p>
341+
*
342+
* @return source branch of the PR which triggered GitHub Actions workflow
354343
*/
355-
private String branchName() throws IOException {
356-
// this returns HEAD as branch name when in detached state
357-
Optional<Ref> ref = Optional.ofNullable(jgitRepository.getRepository().exactRef(Constants.HEAD));
358-
String branchName = ref.map(r -> r.getTarget().getName())
359-
.map(Repository::shortenRefName)
360-
.orElse(null);
361-
362-
if ("HEAD".equals(branchName) && properties.getOverriddenBranchName() != null && !properties.getOverriddenBranchName().isEmpty()) {
363-
branchName = Repository.shortenRefName(properties.getOverriddenBranchName());
344+
private Optional<String> branchNameFromGithubEnvVariable() {
345+
if (env("GITHUB_ACTIONS").isPresent()) {
346+
return env("GITHUB_HEAD_REF");
364347
}
348+
return Optional.empty();
349+
}
350+
351+
private Optional<String> env(String name) {
352+
return Optional.ofNullable(System.getenv(name));
353+
}
365354

366-
return branchName;
355+
/**
356+
* @return branch name or 'HEAD' when in detached state, unless it is overridden by 'overriddenBranchName'
357+
*/
358+
private String branchNameFromGit() {
359+
try {
360+
// this returns HEAD as branch name when in detached state
361+
Optional<Ref> ref = Optional.ofNullable(jgitRepository.getRepository().exactRef(Constants.HEAD));
362+
String branchName = ref.map(r -> r.getTarget().getName())
363+
.map(Repository::shortenRefName)
364+
.orElse(null);
365+
366+
if ("HEAD".equals(branchName) && properties.getOverriddenBranchName() != null && !properties.getOverriddenBranchName().isEmpty()) {
367+
branchName = Repository.shortenRefName(properties.getOverriddenBranchName());
368+
}
369+
return branchName;
370+
} catch (IOException e) {
371+
throw new ScmException(e);
372+
}
367373
}
368374

369375
@Override

src/test/groovy/pl/allegro/tech/build/axion/release/infrastructure/git/GitRepositoryTest.groovy

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,23 @@ import org.eclipse.jgit.lib.Constants
99
import org.eclipse.jgit.transport.RemoteConfig
1010
import org.eclipse.jgit.transport.URIish
1111
import org.gradle.testfixtures.ProjectBuilder
12-
import pl.allegro.tech.build.axion.release.domain.scm.*
12+
import pl.allegro.tech.build.axion.release.util.WithEnvironment
13+
import pl.allegro.tech.build.axion.release.domain.scm.ScmException
14+
import pl.allegro.tech.build.axion.release.domain.scm.ScmIdentity
15+
import pl.allegro.tech.build.axion.release.domain.scm.ScmPosition
16+
import pl.allegro.tech.build.axion.release.domain.scm.ScmProperties
17+
import pl.allegro.tech.build.axion.release.domain.scm.ScmPushOptions
18+
import pl.allegro.tech.build.axion.release.domain.scm.ScmRepositoryUnavailableException
19+
import pl.allegro.tech.build.axion.release.domain.scm.TagsOnCommit
1320
import spock.lang.Specification
1421

1522
import java.nio.file.Files
1623
import java.nio.file.Path
1724
import java.util.regex.Pattern
1825

1926
import static java.util.regex.Pattern.compile
20-
import static pl.allegro.tech.build.axion.release.TagPrefixConf.fullPrefix
2127
import static pl.allegro.tech.build.axion.release.TagPrefixConf.defaultPrefix
28+
import static pl.allegro.tech.build.axion.release.TagPrefixConf.fullPrefix
2229
import static pl.allegro.tech.build.axion.release.domain.scm.ScmPropertiesBuilder.scmProperties
2330

2431
class GitRepositoryTest extends Specification {
@@ -622,11 +629,36 @@ class GitRepositoryTest extends Specification {
622629
position.revision == headSubDirAChanged
623630
}
624631

632+
@WithEnvironment([
633+
'GITHUB_ACTIONS=true',
634+
'GITHUB_EVENT_NAME=pull_request',
635+
'GITHUB_HEAD_REF=pr-source-branch'
636+
])
637+
def "should get branch name on Github Actions if pull_request triggered the workflow"() {
638+
when:
639+
ScmPosition position = repository.currentPosition()
640+
641+
then:
642+
position.branch == 'pr-source-branch'
643+
}
644+
645+
@WithEnvironment([
646+
'GITHUB_ACTIONS=true',
647+
'GITHUB_EVENT_NAME=pull_request_target',
648+
'GITHUB_HEAD_REF=pr-source-branch'
649+
])
650+
def "should get branch name on Github Actions if pull_request_target triggered the workflow"() {
651+
when:
652+
ScmPosition position = repository.currentPosition()
653+
654+
then:
655+
position.branch == 'pr-source-branch'
656+
}
657+
625658
private void commitFile(String subDir, String fileName) {
626659
String fileInA = "${subDir}/${fileName}"
627660
new File(repositoryDir, subDir).mkdirs()
628661
new File(repositoryDir, fileInA).createNewFile()
629662
repository.commit([fileInA], "Add file ${fileName} in ${subDir}")
630663
}
631-
632664
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package pl.allegro.tech.build.axion.release.util
2+
3+
import org.spockframework.runtime.extension.IGlobalExtension
4+
5+
import static java.util.stream.Collectors.toList
6+
7+
class ClearGithubEnvVariablesExtension implements IGlobalExtension {
8+
9+
@Override
10+
void start() {
11+
def keysToRemove = TestEnvironment.getEnvVariableNames().stream()
12+
.filter { variableName -> variableName.startsWith("GITHUB_") }
13+
.collect(toList())
14+
keysToRemove.forEach(TestEnvironment::unsetEnvVariable)
15+
}
16+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package pl.allegro.tech.build.axion.release.util
2+
3+
/**
4+
* <p>Helper class for manipulating environment variables.</p>
5+
*
6+
* Credits to Kotest's <a href="https://github.com/kotest/kotest/blob/master/kotest-extensions/src/jvmMain/kotlin/io/kotest/extensions/system/SystemEnvironmentExtensions.kt#L26">withEnvironment</a>
7+
*/
8+
class TestEnvironment {
9+
10+
private static Map<String, String> MUTABLE_ENV_MAP
11+
12+
static {
13+
extractMutableMapFromSystemEnv()
14+
}
15+
16+
private static void extractMutableMapFromSystemEnv() {
17+
def envMap = System.getenv()
18+
def envMapClass = envMap.getClass()
19+
def internalMapField = envMapClass.getDeclaredField("m")
20+
internalMapField.setAccessible(true)
21+
MUTABLE_ENV_MAP = (Map<String, String>) internalMapField.get(envMap)
22+
}
23+
24+
static void setEnvVariable(String name, String value) {
25+
MUTABLE_ENV_MAP.put(name, value)
26+
}
27+
28+
static void unsetEnvVariable(String name) {
29+
MUTABLE_ENV_MAP.remove(name)
30+
}
31+
32+
static Set<String> getEnvVariableNames() {
33+
return MUTABLE_ENV_MAP.keySet()
34+
}
35+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package pl.allegro.tech.build.axion.release.util
2+
3+
4+
import org.spockframework.runtime.extension.ExtensionAnnotation
5+
6+
import java.lang.annotation.ElementType
7+
import java.lang.annotation.Retention
8+
import java.lang.annotation.RetentionPolicy
9+
import java.lang.annotation.Target
10+
11+
@Retention(RetentionPolicy.RUNTIME)
12+
@Target(ElementType.METHOD)
13+
@ExtensionAnnotation(WithEnvironmentExtension.class)
14+
@interface WithEnvironment {
15+
16+
String[] value()
17+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package pl.allegro.tech.build.axion.release.util
2+
3+
import org.spockframework.runtime.extension.IAnnotationDrivenExtension
4+
import org.spockframework.runtime.model.FeatureInfo
5+
import org.spockframework.util.Pair
6+
7+
import static java.util.stream.Collectors.toList
8+
import static TestEnvironment.setEnvVariable
9+
import static TestEnvironment.unsetEnvVariable
10+
11+
class WithEnvironmentExtension implements IAnnotationDrivenExtension<WithEnvironment> {
12+
13+
@Override
14+
void visitFeatureAnnotation(WithEnvironment annotation, FeatureInfo feature) {
15+
feature.getFeatureMethod().addInterceptor { invocation ->
16+
List<Pair<String, String>> envVarDefinitions = annotation.value().toList().stream()
17+
.map { it.split("=") }
18+
.map { Pair.of(it[0], it[1]) }
19+
.collect(toList())
20+
21+
envVarDefinitions.forEach { setEnvVariable(it.first(), it.second()) }
22+
invocation.proceed()
23+
envVarDefinitions.forEach { unsetEnvVariable(it.first()) }
24+
}
25+
}
26+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pl.allegro.tech.build.axion.release.util.ClearGithubEnvVariablesExtension

0 commit comments

Comments
 (0)