diff --git a/docs/configuration/basic_usage.md b/docs/configuration/basic_usage.md index 6996b8a2..624846e6 100644 --- a/docs/configuration/basic_usage.md +++ b/docs/configuration/basic_usage.md @@ -180,18 +180,7 @@ Version calculation rules: ### GitHub workflow context -If `release` task is executed in GitHub workflow it will generate an output variable `released-version` -that you can access later on in your workflow steps. - -``` -jobs: - build: - runs-on: ubuntu-latest - steps: - - id: release - run: ./gradlew release - - run: echo ${{steps.release.outputs.released-version}} -``` +See [GitHub outputs](ci_servers.md#github-outputs) ## Accessing previous version diff --git a/docs/configuration/ci_servers.md b/docs/configuration/ci_servers.md index d614c1fe..a6e3e61a 100644 --- a/docs/configuration/ci_servers.md +++ b/docs/configuration/ci_servers.md @@ -29,10 +29,47 @@ This behavior is experimental and has been tested on the following CI servers: `axion-release` has dedicated support for GitHub Actions and you don't need any custom configs to make it working. -Here's what Axion does for you under the hood: - -- gets the original branch name for workflows triggered by `pull_request` - - see [versionWithBranch](version.md#versionwithbranch-default) +### GitHub outputs + +To make it easier for you to chain jobs in a workflow, `axion-release` will provide some information as GitHub outputs. + +| name | description | +|---------------------|---------------------------------------------| +| `released-version` | Provided after executing the `release` task | +| `published-version` | Provided after executing the `publish` task | + +#### Multi-version builds + +If all your Gradle modules use the same version, the output will be a single value, such as: +``` +1.0.0 +``` + +However, if each module has its own version, the output will be in JSON format, for example: +```json +{"root-project":"1.0.0","sub-project-1":"2.0.0","sub-project-2":"3.0.0"} +``` +where `root-project`, `sub-project-1` and `sub-project-2` are project names from Gradle. + +#### Example + +```yaml +jobs: + build: + steps: + - id: release + run: ./gradlew release + + # for single-version builds + - run: | + echo ${{ steps.release.outputs.released-version }} + + # for multi-version builds + - run: | + echo ${{ fromJson(steps.release.outputs.released-version).root-project }} + echo ${{ fromJson(steps.release.outputs.released-version).sub-project-1 }} + echo ${{ fromJson(steps.release.outputs.released-version).sub-project-2 }} +``` ## Jenkins diff --git a/src/integration/groovy/pl/allegro/tech/build/axion/release/BaseIntegrationTest.groovy b/src/integration/groovy/pl/allegro/tech/build/axion/release/BaseIntegrationTest.groovy index c0fc5bad..1e67a0a2 100644 --- a/src/integration/groovy/pl/allegro/tech/build/axion/release/BaseIntegrationTest.groovy +++ b/src/integration/groovy/pl/allegro/tech/build/axion/release/BaseIntegrationTest.groovy @@ -16,23 +16,34 @@ class BaseIntegrationTest extends RepositoryBasedTest { } void buildFile(String contents) { - new FileTreeBuilder(temporaryFolder).file("build.gradle", """ - plugins { - id 'pl.allegro.tech.build.axion-release' - } - - """ + contents + + new FileTreeBuilder(temporaryFolder).file("build.gradle", """ + plugins { + id 'pl.allegro.tech.build.axion-release' + } - project.version = scmVersion.version - scmVersion.ignoreGlobalGitConfig = true - """) + $contents + + project.version = scmVersion.version + scmVersion.ignoreGlobalGitConfig = true + """ + ) } void vanillaBuildFile(String contents) { new FileTreeBuilder(temporaryFolder).file("build.gradle", contents) } + void vanillaSubprojectBuildFile(String subprojectName, String contents) { + new FileTreeBuilder(temporaryFolder).dir(subprojectName) { + file("build.gradle", contents) + } + } + + void vanillaSettingsFile(String contents) { + new FileTreeBuilder(temporaryFolder).file("settings.gradle", contents) + } + GradleRunner gradle() { return GradleRunner.create() .withProjectDir(temporaryFolder) diff --git a/src/integration/groovy/pl/allegro/tech/build/axion/release/SimpleIntegrationTest.groovy b/src/integration/groovy/pl/allegro/tech/build/axion/release/SimpleIntegrationTest.groovy index 626c7a04..6446e468 100644 --- a/src/integration/groovy/pl/allegro/tech/build/axion/release/SimpleIntegrationTest.groovy +++ b/src/integration/groovy/pl/allegro/tech/build/axion/release/SimpleIntegrationTest.groovy @@ -40,23 +40,122 @@ class SimpleIntegrationTest extends BaseIntegrationTest { result.task(":currentVersion").outcome == TaskOutcome.SUCCESS } - def "should define a github output when running release task in github workflow context"() { + def "should set released-version github output after release task"(String task, + String rootProjectVersion, + String subprojectVersion, + String output) { given: def outputFile = File.createTempFile("github-outputs", ".tmp") environmentVariablesRule.set("GITHUB_ACTIONS", "true") environmentVariablesRule.set("GITHUB_OUTPUT", outputFile.getAbsolutePath()) - buildFile('') + vanillaSettingsFile(""" + rootProject.name = 'root-project' + + include 'sub-project' + """ + ) + + vanillaBuildFile(""" + plugins { + id 'pl.allegro.tech.build.axion-release' + } + + scmVersion { + tag { + prefix = 'root-project-' + } + } + """ + ) + + vanillaSubprojectBuildFile("sub-project", """ + plugins { + id 'pl.allegro.tech.build.axion-release' + } + + scmVersion { + tag { + prefix = 'sub-project-' + } + } + """ + ) + + repository.tag("root-project-$rootProjectVersion") + repository.tag("sub-project-$subprojectVersion") + repository.commit(['.'], 'Some commit') when: - runGradle('release', '-Prelease.version=1.0.0', '-Prelease.localOnly', '-Prelease.disableChecks') + runGradle(task, '-Prelease.localOnly', '-Prelease.disableChecks') then: def definedEnvVariables = outputFile.getText().lines().collect(toList()) - definedEnvVariables.contains('released-version=1.0.0') + definedEnvVariables.contains(output) cleanup: environmentVariablesRule.clear("GITHUB_ACTIONS", "GITHUB_OUTPUT") + + where: + task | rootProjectVersion | subprojectVersion || output + 'release' | "1.0.0" | "1.0.0" || 'released-version=1.0.1' + 'release' | "1.0.0" | "2.0.0" || 'released-version={"root-project":"1.0.1","sub-project":"2.0.1"}' + ':release' | "1.0.0" | "2.0.0" || 'released-version=1.0.1' + ':sub-project:release' | "1.0.0" | "2.0.0" || 'released-version=2.0.1' + } + + def "should set published-version github output after publish task"(String task, + String rootProjectVersion, + String subprojectVersion, + String output) { + given: + def outputFile = File.createTempFile("github-outputs", ".tmp") + environmentVariablesRule.set("GITHUB_ACTIONS", "true") + environmentVariablesRule.set("GITHUB_OUTPUT", outputFile.getAbsolutePath()) + + vanillaSettingsFile(""" + rootProject.name = 'root-project' + + include 'sub-project' + """ + ) + + vanillaBuildFile(""" + plugins { + id 'pl.allegro.tech.build.axion-release' + id 'maven-publish' + } + + version = '$rootProjectVersion' + """ + ) + + vanillaSubprojectBuildFile("sub-project", """ + plugins { + id 'pl.allegro.tech.build.axion-release' + id 'maven-publish' + } + + version = '$subprojectVersion' + """ + ) + + when: + runGradle(task) + + then: + def definedEnvVariables = outputFile.getText().lines().collect(toList()) + definedEnvVariables.contains(output) + + cleanup: + environmentVariablesRule.clear("GITHUB_ACTIONS", "GITHUB_OUTPUT") + + where: + task | rootProjectVersion | subprojectVersion || output + 'publish' | "1.0.0" | "1.0.0" || 'published-version=1.0.0' + 'publish' | "1.0.0" | "2.0.0" || 'published-version={"root-project":"1.0.0","sub-project":"2.0.0"}' + ':publish' | "1.0.0" | "2.0.0" || 'published-version=1.0.0' + ':sub-project:publish' | "1.0.0" | "2.0.0" || 'published-version=2.0.0' } def "should return released version on calling cV on repo with release commit"() { diff --git a/src/main/groovy/pl/allegro/tech/build/axion/release/ReleasePlugin.groovy b/src/main/groovy/pl/allegro/tech/build/axion/release/ReleasePlugin.groovy index 0ea42002..a43ec627 100644 --- a/src/main/groovy/pl/allegro/tech/build/axion/release/ReleasePlugin.groovy +++ b/src/main/groovy/pl/allegro/tech/build/axion/release/ReleasePlugin.groovy @@ -2,9 +2,11 @@ package pl.allegro.tech.build.axion.release import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.provider.Provider import pl.allegro.tech.build.axion.release.domain.SnapshotDependenciesChecker import pl.allegro.tech.build.axion.release.domain.VersionConfig import pl.allegro.tech.build.axion.release.infrastructure.di.VersionResolutionContext +import pl.allegro.tech.build.axion.release.infrastructure.github.GithubService import pl.allegro.tech.build.axion.release.util.FileLoader abstract class ReleasePlugin implements Plugin { @@ -21,7 +23,10 @@ abstract class ReleasePlugin implements Plugin { void apply(Project project) { FileLoader.setRoot(project.rootDir) - def versionConfig = project.extensions.create(VERSION_EXTENSION, VersionConfig, project.rootProject.layout.projectDirectory) + Provider githubService = project.gradle.sharedServices + .registerIfAbsent("github", GithubService) {} + + VersionConfig versionConfig = project.extensions.create(VERSION_EXTENSION, VersionConfig, project.rootProject.layout.projectDirectory) project.tasks.withType(BaseAxionTask).configureEach({ it.versionConfig = versionConfig @@ -42,6 +47,8 @@ abstract class ReleasePlugin implements Plugin { group = 'Release' description = 'Performs release - creates tag and pushes it to remote.' dependsOn(VERIFY_RELEASE_TASK) + it.projectName = project.name + it.githubService = githubService } project.tasks.register(CREATE_RELEASE_TASK, CreateReleaseTask) { @@ -65,9 +72,25 @@ abstract class ReleasePlugin implements Plugin { description = 'Prints current project version extracted from SCM.' } + setGithubOutputsAfterPublishTask(project, githubService) + maybeDisableReleaseTasks(project, versionConfig) } + private static setGithubOutputsAfterPublishTask(Project project, Provider githubService) { + String projectName = project.name + Provider projectVersion = project.provider { project.version.toString() } + + project.plugins.withId('maven-publish') { + project.tasks.named('publish') { task -> + task.usesService(githubService) + task.doLast { + githubService.get().setOutput('published-version', projectName, projectVersion.get()) + } + } + } + } + private static void maybeDisableReleaseTasks(Project project, VersionConfig versionConfig) { project.afterEvaluate { def context = VersionResolutionContext.create(versionConfig, project.layout.projectDirectory) diff --git a/src/main/groovy/pl/allegro/tech/build/axion/release/ReleaseTask.groovy b/src/main/groovy/pl/allegro/tech/build/axion/release/ReleaseTask.groovy index 05f93945..4733d4a1 100644 --- a/src/main/groovy/pl/allegro/tech/build/axion/release/ReleaseTask.groovy +++ b/src/main/groovy/pl/allegro/tech/build/axion/release/ReleaseTask.groovy @@ -1,17 +1,23 @@ package pl.allegro.tech.build.axion.release +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction import pl.allegro.tech.build.axion.release.domain.Releaser import pl.allegro.tech.build.axion.release.domain.scm.ScmPushResult import pl.allegro.tech.build.axion.release.domain.scm.ScmPushResultOutcome import pl.allegro.tech.build.axion.release.infrastructure.di.VersionResolutionContext - -import java.nio.file.Files -import java.nio.file.Paths -import java.nio.file.StandardOpenOption +import pl.allegro.tech.build.axion.release.infrastructure.github.GithubService abstract class ReleaseTask extends BaseAxionTask { + @Input + abstract Property getProjectName() + + @Internal + abstract Property getGithubService() + @TaskAction void release() { VersionResolutionContext context = resolutionContext() @@ -28,13 +34,8 @@ abstract class ReleaseTask extends BaseAxionTask { } if (result.outcome === ScmPushResultOutcome.SUCCESS) { - if (System.getenv().containsKey('GITHUB_ACTIONS')) { - Files.write( - Paths.get(System.getenv('GITHUB_OUTPUT')), - "released-version=${versionConfig.uncached.decoratedVersion}\n".getBytes(), - StandardOpenOption.APPEND - ) - } + String version = versionConfig.uncached.decoratedVersion + githubService.get().setOutput("released-version", projectName.get(), version) } } } diff --git a/src/main/java/pl/allegro/tech/build/axion/release/infrastructure/github/GithubService.java b/src/main/java/pl/allegro/tech/build/axion/release/infrastructure/github/GithubService.java new file mode 100644 index 00000000..986e78cd --- /dev/null +++ b/src/main/java/pl/allegro/tech/build/axion/release/infrastructure/github/GithubService.java @@ -0,0 +1,65 @@ +package pl.allegro.tech.build.axion.release.infrastructure.github; + +import groovy.json.JsonBuilder; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; +import org.gradle.api.services.BuildService; +import org.gradle.api.services.BuildServiceParameters; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static java.util.stream.Collectors.toList; + +public abstract class GithubService implements BuildService, AutoCloseable { + + private static final Logger logger = Logging.getLogger(GithubService.class); + private static final String GITHUB_ACTIONS = "GITHUB_ACTIONS"; + private static final String GITHUB_OUTPUT = "GITHUB_OUTPUT"; + + private final Map> outputs = Collections.synchronizedMap(new LinkedHashMap<>()); + + public void setOutput(String name, String projectName, String value) { + outputs.putIfAbsent(name, Collections.synchronizedMap(new LinkedHashMap<>())); + outputs.get(name).put(projectName, value); + } + + @Override + public void close() { + if (System.getenv().containsKey(GITHUB_ACTIONS)) { + + outputs.forEach((name, valuePerProject) -> { + List distinctValues = valuePerProject.values().stream() + .distinct() + .collect(toList()); + + if (distinctValues.size() == 1) { + String singleValue = distinctValues.get(0); + writeOutput(name, singleValue); + } else { + String jsonValue = new JsonBuilder(valuePerProject).toString(); + logger.warn("Multiple values provided for the '{}' GitHub output, it will be formatted as JSON: {}", name, jsonValue); + writeOutput(name, jsonValue); + } + }); + } + } + + private static void writeOutput(String name, String value) { + try { + Files.write( + Paths.get(System.getenv(GITHUB_OUTPUT)), + String.format("%s=%s\n", name, value).getBytes(), + StandardOpenOption.APPEND + ); + } catch (IOException e) { + logger.warn("Unable to the set '{}' GitHub output, cause: {}", name, e.getMessage()); + } + } +}