diff --git a/CHANGELOG.md b/CHANGELOG.md index ef71371d8..cb33d57bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/common-system-test/src/main/kotlin/com/teamscale/test/commons/ProcessUtils.kt b/common-system-test/src/main/kotlin/com/teamscale/test/commons/ProcessUtils.kt index 3497037a9..74b59825f 100644 --- a/common-system-test/src/main/kotlin/com/teamscale/test/commons/ProcessUtils.kt +++ b/common-system-test/src/main/kotlin/com/teamscale/test/commons/ProcessUtils.kt @@ -70,12 +70,24 @@ object ProcessUtils { class ProcessExecutor(private val commands: List) { private var workingDirectory: File? = null private var input: String? = null + private var environmentVariables: Map? = null + private var removeEnvironmentVariables: List = 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): ProcessExecutor = apply { environmentVariables = variables } + + /** + * Removes the given environment variable names from the inherited environment of the subprocess. + */ + fun removeEnvironmentVariables(names: List): ProcessExecutor = apply { removeEnvironmentVariables = names } + /** * Executes the process and returns the result. */ @@ -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) } } } diff --git a/system-tests/api-changing-settings-should-dump/build.gradle.kts b/system-tests/api-changing-settings-should-dump/build.gradle.kts index 641f4624f..a6081bc88 100644 --- a/system-tests/api-changing-settings-should-dump/build.gradle.kts +++ b/system-tests/api-changing-settings-should-dump/build.gradle.kts @@ -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*", + ) + ) } diff --git a/system-tests/default-excludes-test/build.gradle.kts b/system-tests/default-excludes-test/build.gradle.kts index 2d2497064..2ddde15a9 100644 --- a/system-tests/default-excludes-test/build.gradle.kts +++ b/system-tests/default-excludes-test/build.gradle.kts @@ -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.*" + ) + ) } diff --git a/system-tests/http-redirect-test/build.gradle.kts b/system-tests/http-redirect-test/build.gradle.kts index bbf11cb93..6ab09bef1 100644 --- a/system-tests/http-redirect-test/build.gradle.kts +++ b/system-tests/http-redirect-test/build.gradle.kts @@ -16,7 +16,7 @@ tasks.test { "teamscale-project" to "p", "teamscale-partition" to "part", "teamscale-commit" to "master:12345", - "includes" to "**" + "includes" to "*" ) ) } diff --git a/system-tests/maven-external-upload-test/missing-commit-project/pom.xml b/system-tests/maven-external-upload-test/missing-commit-project/pom.xml index b04911945..efd36d8c1 100644 --- a/system-tests/maven-external-upload-test/missing-commit-project/pom.xml +++ b/system-tests/maven-external-upload-test/missing-commit-project/pom.xml @@ -61,13 +61,6 @@ com.teamscale teamscale-maven-plugin ${tia.agent.version} - - - - upload-coverage - - - http://localhost:${tia.teamscale.fake.port} m diff --git a/system-tests/maven-external-upload-test/src/test/kotlin/com/teamscale/upload/MavenExternalUploadSystemTest.kt b/system-tests/maven-external-upload-test/src/test/kotlin/com/teamscale/upload/MavenExternalUploadSystemTest.kt index 3a082b3d5..fbb5c7b7c 100644 --- a/system-tests/maven-external-upload-test/src/test/kotlin/com/teamscale/upload/MavenExternalUploadSystemTest.kt +++ b/system-tests/maven-external-upload-test/src/test/kotlin/com/teamscale/upload/MavenExternalUploadSystemTest.kt @@ -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 @@ -32,15 +33,23 @@ class MavenExternalUploadSystemTest { teamscaleMockServer?.reset() } - private fun runCoverageUploadGoal(projectPath: String): ProcessUtils.ProcessResult? { + private fun runCoverageUploadGoal( + projectPath: String, + environment: Map? = null, + removeEnvironmentVariables: List = 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()) } @@ -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). @@ -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 or configured in the pom.xml and it was not possible to determine the current revision") + .contains("There is no or configured in the pom.xml, no CI environment variable was found, and it was not possible to determine the current revision") } companion object { diff --git a/system-tests/teamscale-properties-test/build.gradle.kts b/system-tests/teamscale-properties-test/build.gradle.kts index c0901971a..1857b9fed 100644 --- a/system-tests/teamscale-properties-test/build.gradle.kts +++ b/system-tests/teamscale-properties-test/build.gradle.kts @@ -27,7 +27,7 @@ tasks.test { "teamscale-project" to "p", "teamscale-partition" to "part", "teamscale-commit" to "master:12345", - "includes" to "**" + "includes" to "*" ) ) } diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/EnvironmentVariableChecker.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/EnvironmentVariableChecker.kt new file mode 100644 index 000000000..38b363ebd --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/EnvironmentVariableChecker.kt @@ -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 = 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 + } +} diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/GitRevisionValueSource.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/GitRevisionValueSource.kt index 104b42cdb..e4cc4b106 100644 --- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/GitRevisionValueSource.kt +++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/GitRevisionValueSource.kt @@ -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 @@ -36,55 +37,3 @@ abstract class GitRevisionValueSource : ValueSource = 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 - } -} diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/TeamscaleMojoBase.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/TeamscaleMojoBase.java index 378fd7149..6f9b29dff 100644 --- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/TeamscaleMojoBase.java +++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/TeamscaleMojoBase.java @@ -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; @@ -105,9 +106,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { /** * Sets the resolvedRevision or resolvedCommit. 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)) { @@ -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 or configured in the pom.xml" + + throw new MojoFailureException("There is no or 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); } } diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TestwiseCoverageReportMojo.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TestwiseCoverageReportMojo.java index 53818e7bb..f43dcd9ad 100644 --- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TestwiseCoverageReportMojo.java +++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TestwiseCoverageReportMojo.java @@ -44,7 +44,7 @@ public class TestwiseCoverageReportMojo extends AbstractMojo { /** * Wildcard include patterns to apply during JaCoCo's traversal of class files. */ - @Parameter(defaultValue = "**") + @Parameter(defaultValue = "*") public String[] includes; /** * Wildcard exclude patterns to apply during JaCoCo's traversal of class files. diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java index 280c4f995..c18bd5e1a 100644 --- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java +++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java @@ -82,7 +82,7 @@ public abstract class TiaMojoBase extends TeamscaleMojoBase { /** * You can optionally specify which code should be included in the coverage instrumentation. Each pattern is applied - * to the fully qualified class names of the profiled system. Use {@code *} to match any number characters and + * to the fully qualified class names of the profiled system. Use {@code *} to match any number of characters and * {@code ?} to match any single character. *

* Classes that match any of the include patterns are included, unless any exclude pattern excludes them.