Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ We use [semantic versioning](http://semver.org/):
- PATCH version when you make backwards compatible bug fixes.

# Next version
- [feature] _maven-plugin_: Auto-detect commit from CI/CD environment variables (Jenkins, GitHub Actions, GitLab CI, Azure DevOps, etc.)

# 36.3.0
- [feature] _teamscale-client_: User-Agent header now includes the specific component performing the request (e.g., "Teamscale Gradle Plugin", "Teamscale Maven Plugin") and version number
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,24 @@ object ProcessUtils {
class ProcessExecutor(private val commands: List<String>) {
private var workingDirectory: File? = null
private var input: String? = null
private var environmentVariables: Map<String, String>? = null
private var removeEnvironmentVariables: List<String> = emptyList()

/**
* Sets the working directory for the process.
*/
fun directory(dir: File): ProcessExecutor = apply { workingDirectory = dir }

/**
* Merges the given variables into the inherited environment of the subprocess.
*/
fun setEnvironmentVariables(variables: Map<String, String>): ProcessExecutor = apply { environmentVariables = variables }

/**
* Removes the given environment variable names from the inherited environment of the subprocess.
*/
fun removeEnvironmentVariables(names: List<String>): ProcessExecutor = apply { removeEnvironmentVariables = names }

/**
* Executes the process and returns the result.
*/
Expand Down Expand Up @@ -107,6 +119,8 @@ object ProcessUtils {
*/
fun build(): ProcessBuilder = ProcessBuilder(commands).apply {
workingDirectory?.let { directory(it) }
environmentVariables?.let { environment().putAll(it) }
removeEnvironmentVariables.forEach { environment().remove(it) }
}
}

Expand Down
30 changes: 15 additions & 15 deletions system-tests/api-changing-settings-should-dump/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
plugins {
com.teamscale.`kotlin-convention`
com.teamscale.`system-test-convention`
com.teamscale.`kotlin-convention`
com.teamscale.`system-test-convention`
}

tasks.test {
/** These ports must match what is configured in the SystemTest class. */
teamscaleAgent(
mapOf(
"http-server-port" to "$agentPort",
"teamscale-server-url" to "http://localhost:$teamscalePort",
"teamscale-user" to "fake",
"teamscale-access-token" to "fake",
"teamscale-project" to "p",
"teamscale-partition" to "partition_before_change",
"teamscale-commit" to "master:12345",
"includes" to "**SystemUnderTest**",
)
)
/** These ports must match what is configured in the SystemTest class. */
teamscaleAgent(
mapOf(
"http-server-port" to "$agentPort",
"teamscale-server-url" to "http://localhost:$teamscalePort",
"teamscale-user" to "fake",
"teamscale-access-token" to "fake",
"teamscale-project" to "p",
"teamscale-partition" to "partition_before_change",
"teamscale-commit" to "master:12345",
"includes" to "*SystemUnderTest*",
)
)
}
32 changes: 16 additions & 16 deletions system-tests/default-excludes-test/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
plugins {
com.teamscale.`kotlin-convention`
com.teamscale.`system-test-convention`
com.teamscale.`kotlin-convention`
com.teamscale.`system-test-convention`
}

tasks.test {
/** These ports must match what is configured in the SystemTest class. */
teamscaleAgent(
mapOf(
"http-server-port" to "$agentPort",
"teamscale-server-url" to "http://localhost:$teamscalePort",
"teamscale-user" to "fake",
"teamscale-access-token" to "fake",
"teamscale-project" to "p",
"teamscale-partition" to "part",
"teamscale-commit" to "master:12345",
"includes" to "**",
"excludes" to "*foo.*"
)
)
/** These ports must match what is configured in the SystemTest class. */
teamscaleAgent(
mapOf(
"http-server-port" to "$agentPort",
"teamscale-server-url" to "http://localhost:$teamscalePort",
"teamscale-user" to "fake",
"teamscale-access-token" to "fake",
"teamscale-project" to "p",
"teamscale-partition" to "part",
"teamscale-commit" to "master:12345",
"includes" to "*",
"excludes" to "*foo.*"
)
)
}
2 changes: 1 addition & 1 deletion system-tests/http-redirect-test/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ tasks.test {
"teamscale-project" to "p",
"teamscale-partition" to "part",
"teamscale-commit" to "master:12345",
"includes" to "**"
"includes" to "*"
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,6 @@
<groupId>com.teamscale</groupId>
<artifactId>teamscale-maven-plugin</artifactId>
<version>${tia.agent.version}</version>
<executions>
<execution>
<goals>
<goal>upload-coverage</goal>
</goals>
</execution>
</executions>
<configuration>
<teamscaleUrl>http://localhost:${tia.teamscale.fake.port}</teamscaleUrl>
<projectId>m</projectId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.teamscale.upload

import com.teamscale.client.EReportFormat
import com.teamscale.client.EnvironmentVariableChecker
import com.teamscale.client.FileSystemUtils
import com.teamscale.client.SystemUtils
import com.teamscale.test.commons.ProcessUtils
Expand Down Expand Up @@ -32,15 +33,23 @@ class MavenExternalUploadSystemTest {
teamscaleMockServer?.reset()
}

private fun runCoverageUploadGoal(projectPath: String): ProcessUtils.ProcessResult? {
private fun runCoverageUploadGoal(
projectPath: String,
environment: Map<String, String>? = null,
removeEnvironmentVariables: List<String> = emptyList()
): ProcessUtils.ProcessResult? {
val workingDirectory = File(projectPath)
var executable = "./mvnw"
if (SystemUtils.IS_OS_WINDOWS) {
executable = Paths.get(projectPath, "mvnw.cmd").toUri().getPath()
}
try {
return ProcessUtils.processBuilder(executable, MAVEN_COVERAGE_UPLOAD_GOAL).directory(workingDirectory)
.execute()
val builder = ProcessUtils.processBuilder(executable, MAVEN_COVERAGE_UPLOAD_GOAL).directory(workingDirectory)
if (environment != null) {
builder.setEnvironmentVariables(environment)
}
builder.removeEnvironmentVariables(removeEnvironmentVariables)
return builder.execute()
} catch (e: IOException) {
Assertions.fail(e.toString())
}
Expand Down Expand Up @@ -91,6 +100,30 @@ class MavenExternalUploadSystemTest {
assertThat(session.getRevision()).matches("[a-f0-9]{40}")
}

/**
* When no commit or revision is configured and no git repo is available, but a CI environment variable
* is set, the plugin should use the commit from that variable for the upload (TS-45104).
*/
@Test
@Throws(IOException::class)
fun testCiEnvironmentVariableCommitResolution(@TempDir tmpDir: Path) {
val fakeCommit = "abc123def456abc123def456abc123def456abc1"
FileSystemUtils.copyFiles(File("missing-commit-project"), tmpDir.toFile()) { true }
tmpDir.resolve("mvnw").toFile().setExecutable(true)
val projectPath = tmpDir.toAbsolutePath().toString()
runMavenTests(projectPath)
val result = runCoverageUploadGoal(
projectPath,
environment = mapOf("GITHUB_SHA" to fakeCommit)
)
assertThat(result).isNotNull()
assertThat(result!!.exitCode).isEqualTo(0)

val session = teamscaleMockServer!!.getSession("My Custom Unit Tests Partition")
assertThat(session.getReports(EReportFormat.JACOCO)).hasSize(1)
assertThat(session.getRevision()).isEqualTo(fakeCommit)
}

/**
* When no commit is given and no git repo is available, which is the usual fallback, a helpful error message should
* be shown (TS-40425).
Expand All @@ -102,12 +135,12 @@ class MavenExternalUploadSystemTest {
tmpDir.resolve("mvnw").toFile().setExecutable(true)
val projectPath = tmpDir.toAbsolutePath().toString()
runMavenTests(projectPath)
val result = runCoverageUploadGoal(projectPath)
val result = runCoverageUploadGoal(projectPath, removeEnvironmentVariables = EnvironmentVariableChecker.COMMIT_ENVIRONMENT_VARIABLES)
assertThat(result).isNotNull()
assertThat(result!!.exitCode).isNotEqualTo(0)
assertThat(teamscaleMockServer!!.getSessions()).isEmpty()
assertThat(result.stdout)
.contains("There is no <revision> or <commit> configured in the pom.xml and it was not possible to determine the current revision")
.contains("There is no <revision> or <commit> configured in the pom.xml, no CI environment variable was found, and it was not possible to determine the current revision")
}

companion object {
Expand Down
2 changes: 1 addition & 1 deletion system-tests/teamscale-properties-test/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ tasks.test {
"teamscale-project" to "p",
"teamscale-partition" to "part",
"teamscale-commit" to "master:12345",
"includes" to "**"
"includes" to "*"
)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.teamscale.client

import java.util.logging.Logger

/**
* Checks well-known environment variables for commit infos.
*/
object EnvironmentVariableChecker {

private val logger = Logger.getLogger("EnvironmentVariableChecker")

/**
* A list of environment variable names that may hold the commit hash in various
* CI/CD environments and version control systems.
*/
val COMMIT_ENVIRONMENT_VARIABLES: List<String> = mutableListOf( // user-specified as a fallback
"COMMIT", // Git
"GIT_COMMIT", // Jenkins
// https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/Complete-Jenkins-Git-environment-variables-list-for-batch-jobs-and-shell-script-builds
"Build.SourceVersion", // Azure DevOps
// https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables
"CIRCLE_SHA1", // Circle CI
// https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables
"TRAVIS_COMMIT", // Travis CI
// https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
"BITBUCKET_COMMIT", // Bitbucket Pipelines
// https://confluence.atlassian.com/bitbucket/environment-variables-794502608.html
"CI_COMMIT_SHA", // GitLab Pipelines
// https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
"APPVEYOR_REPO_COMMIT", // AppVeyor https://www.appveyor.com/docs/environment-variables/
"GITHUB_SHA", // GitHub actions
// https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables
// SVN
"SVN_REVISION", // Jenkins
// https://stackoverflow.com/questions/43780145/no-svn-revision-in-jenkins-environment-variable
// https://issues.jenkins-ci.org/browse/JENKINS-14797
// Both
"build_vcs_number" // TeamCity
// https://confluence.jetbrains.com/display/TCD8/Predefined+Build+Parameters
// https://stackoverflow.com/questions/2882953/how-to-get-branch-specific-svn-revision-numbers-in-teamcity
)

/**
* Returns either a commit that was found in an environment variable (Git SHA1
* or SVN revision number or TFS changeset number) or null if none was found.
*/
@JvmStatic
fun findCommit(): String? {
for (variable in COMMIT_ENVIRONMENT_VARIABLES) {
val commit = System.getenv(variable)
if (commit != null) {
logger.fine("Using commit/revision/changeset $commit from environment variable $variable")
return commit
}
}

logger.fine("Found no commit/revision/changeset info in any environment variables.")
return null
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.teamscale.config

import com.teamscale.client.EnvironmentVariableChecker
import org.eclipse.jgit.api.Git
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.ValueSource
Expand Down Expand Up @@ -36,55 +37,3 @@ abstract class GitRevisionValueSource : ValueSource<String, GitRevisionValueSour
}
}
}

/**
* Checks well-known environment variables for commit infos.
*/
private object EnvironmentVariableChecker {

private val logger = Logger.getLogger("EnvironmentVariableChecker")

private val COMMIT_ENVIRONMENT_VARIABLES: List<String> = mutableListOf( // user-specified as a fallback
"COMMIT", // Git
"GIT_COMMIT", // Jenkins
// https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/Complete-Jenkins-Git-environment-variables-list-for-batch-jobs-and-shell-script-builds
"Build.SourceVersion", // Azure DevOps
// https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables
"CIRCLE_SHA1", // Circle CI
// https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables
"TRAVIS_COMMIT", // Travis CI
// https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
"BITBUCKET_COMMIT", // Bitbucket Pipelines
// https://confluence.atlassian.com/bitbucket/environment-variables-794502608.html
"CI_COMMIT_SHA", // GitLab Pipelines
// https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
"APPVEYOR_REPO_COMMIT", // AppVeyor https://www.appveyor.com/docs/environment-variables/
"GITHUB_SHA", // GitHub actions
// https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables
// SVN
"SVN_REVISION", // Jenkins
// https://stackoverflow.com/questions/43780145/no-svn-revision-in-jenkins-environment-variable
// https://issues.jenkins-ci.org/browse/JENKINS-14797
// Both
"build_vcs_number" // TeamCity
// https://confluence.jetbrains.com/display/TCD8/Predefined+Build+Parameters
// https://stackoverflow.com/questions/2882953/how-to-get-branch-specific-svn-revision-numbers-in-teamcity
)

/**
* Returns either a commit that was found in an environment variable (Git SHA1
* or SVN revision number or TFS changeset number) or null if none was found.
*/
fun findCommit(): String? {
for (variable in COMMIT_ENVIRONMENT_VARIABLES) {
val commit = System.getenv(variable)
if (commit != null) {
logger.fine("Using commit/revision/changeset $commit from environment variable $variable")
return commit
}
}

logger.fine("Found no commit/revision/changeset info in any environment variables.")
return null
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.teamscale.maven;

import com.teamscale.client.EnvironmentVariableChecker;
import com.teamscale.client.StringUtils;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Plugin;
Expand Down Expand Up @@ -105,9 +106,8 @@ public void execute() throws MojoExecutionException, MojoFailureException {

/**
* Sets the <code>resolvedRevision</code> or <code>resolvedCommit</code>. If not provided, try to determine the
* revision via the GitCommit class.
*
* @see GitCommitUtils
* revision from CI/CD environment variables via {@link EnvironmentVariableChecker} or from the Git repository via
* {@link GitCommitUtils}.
*/
protected void resolveCommitOrRevision() throws MojoFailureException {
if (!StringUtils.isEmpty(revision)) {
Expand All @@ -118,11 +118,21 @@ protected void resolveCommitOrRevision() throws MojoFailureException {
resolvedCommit = commit;
return;
}

// Check CI/CD environment variables first
String envCommit = EnvironmentVariableChecker.findCommit();
if (envCommit != null) {
resolvedRevision = envCommit;
return;
}

// Fall back to Git repository detection
Path basedir = session.getCurrentProject().getBasedir().toPath();
try {
resolvedRevision = GitCommitUtils.getGitHeadRevision(basedir);
} catch (IOException e) {
throw new MojoFailureException("There is no <revision> or <commit> configured in the pom.xml" +
throw new MojoFailureException("There is no <revision> or <commit> configured in the pom.xml," +
" no CI environment variable was found," +
" and it was not possible to determine the current revision in " + basedir + " from Git", e);
}
}
Expand Down
Loading
Loading