From ac489a748e3cbdc52563ce63f379ef4b56b588dc Mon Sep 17 00:00:00 2001 From: Andrew Oberstar Date: Sun, 14 Aug 2022 00:23:29 -0500 Subject: [PATCH] minor: Add settings plugin To ensure that we reckon can get configured before anyone else will use the project versions, we're adding a settings plugin. As long as no one applies another settings plugin that happens to also interact with versions this should resolve any other issues that woud crop up. Fixes #173 --- README.md | 10 + reckon-gradle/build.gradle.kts | 6 + .../gradle/CompositeBuildCompatTest.groovy | 2 - .../reckon/gradle/SettingsCompatTest.groovy | 369 ++++++++++++++++++ .../reckon/gradle/ReckonSettingsPlugin.java | 102 +++++ 5 files changed, 487 insertions(+), 2 deletions(-) create mode 100644 reckon-gradle/src/compatTest/groovy/org/ajoberstar/reckon/gradle/SettingsCompatTest.groovy create mode 100644 reckon-gradle/src/main/java/org/ajoberstar/reckon/gradle/ReckonSettingsPlugin.java diff --git a/README.md b/README.md index 78ef48a..5432ab9 100644 --- a/README.md +++ b/README.md @@ -117,11 +117,21 @@ Reckon can alternately use SNAPSHOT versions instead of the stage concept. #### Apply the plugin +**IMPORTANT:** It is recommended to apply reckon as a Settings plugin (in settings.gradle/settings.gradle.kts) to ensure it is configured before any other plugin tries to use the project version. + ```groovy + +// if applying in settings.gradle(.kts) +plugins { + id 'org.ajoberstar.reckon.settings' version '' +} + +// if applying in build.gradle(.kts) plugins { id 'org.ajoberstar.reckon' version '' } +// in either case reckon { // START As of 0.16.0 // what stages are allowed diff --git a/reckon-gradle/build.gradle.kts b/reckon-gradle/build.gradle.kts index 8ba695f..31605eb 100644 --- a/reckon-gradle/build.gradle.kts +++ b/reckon-gradle/build.gradle.kts @@ -82,5 +82,11 @@ gradlePlugin { description = "Infer a project's version from your Git repository." implementationClass = "org.ajoberstar.reckon.gradle.ReckonPlugin" } + create("settings") { + id = "org.ajoberstar.reckon.settings" + displayName = "Reckon Settings Plugin" + description = "Infer a build's version from your Git repository." + implementationClass = "org.ajoberstar.reckon.gradle.ReckonSettingsPlugin" + } } } diff --git a/reckon-gradle/src/compatTest/groovy/org/ajoberstar/reckon/gradle/CompositeBuildCompatTest.groovy b/reckon-gradle/src/compatTest/groovy/org/ajoberstar/reckon/gradle/CompositeBuildCompatTest.groovy index ba55ac4..9ad52ea 100644 --- a/reckon-gradle/src/compatTest/groovy/org/ajoberstar/reckon/gradle/CompositeBuildCompatTest.groovy +++ b/reckon-gradle/src/compatTest/groovy/org/ajoberstar/reckon/gradle/CompositeBuildCompatTest.groovy @@ -8,8 +8,6 @@ import org.ajoberstar.grgit.Grgit import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.BuildResult -// Composite builds were added in 3.1 -@IgnoreIf({ System.properties['compat.gradle.version'] == '3.0' }) class CompositeBuildCompatTest extends Specification { @TempDir File tempDir File project1Dir diff --git a/reckon-gradle/src/compatTest/groovy/org/ajoberstar/reckon/gradle/SettingsCompatTest.groovy b/reckon-gradle/src/compatTest/groovy/org/ajoberstar/reckon/gradle/SettingsCompatTest.groovy new file mode 100644 index 0000000..8668246 --- /dev/null +++ b/reckon-gradle/src/compatTest/groovy/org/ajoberstar/reckon/gradle/SettingsCompatTest.groovy @@ -0,0 +1,369 @@ +package org.ajoberstar.reckon.gradle + +import spock.lang.Specification +import spock.lang.TempDir + +import org.ajoberstar.grgit.Grgit +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.TaskOutcome + +class SettingsCompatTest extends Specification { + @TempDir File tempDir + File projectDir + File settingsFile + File buildFile + Grgit remote + Grgit remote2 + + def setup() { + projectDir = new File(tempDir, 'project') + settingsFile = projectFile('settings.gradle') + buildFile = projectFile('build.gradle') + + def remoteDir = new File(tempDir, 'remote') + remote = Grgit.init(dir: remoteDir) + + remoteFile('.gitignore') << '.gradle/\nbuild/\n' + remoteFile('master.txt') << 'contents here' + remote.add(patterns: ['.']) + remote.commit(message: 'first commit') + remote.tag.add(name: '1.0.0', message: '1.0.0') + remote.tag.add(name: 'project-a/9.0.0', message: '9.0.0') + remoteFile('master.txt') << 'contents here2' + remote.add(patterns: ['.']) + remote.commit(message: 'major: second commit') + + def remote2Dir = new File(tempDir, 'remote2') + remote2 = Grgit.clone(dir: remote2Dir, uri: remote.repository.rootDir) + } + + def 'if no git repo found, version is defaulted'() { + given: + settingsFile << """ +plugins { + id 'org.ajoberstar.reckon.settings' +} + +reckon { + scopeFromProp() + stageFromProp('alpha','beta', 'final') +} +""" + buildFile << """ +task printVersion { + doLast { + println version + } +} +""" + when: + def result = build('printVersion', '-q', '--configuration-cache') + then: + // version will end with a timestamp, so don't try to validate the whole thing + result.output.normalize().startsWith('0.1.0-alpha.0.0+') + } + + def 'if no strategies specified, version is unspecified'() { + given: + Grgit.clone(dir: projectDir, uri: remote.repository.rootDir) + + settingsFile << """ +plugins { + id 'org.ajoberstar.reckon.settings' +} +""" + buildFile << """ +task printVersion { + doLast { + println version + } +} +""" + when: + def result = build('printVersion', '-q', '--configuration-cache') + then: + result.output.contains('unspecified') + } + + def 'if reckoned version has build metadata no tag created'() { + given: + def local = Grgit.clone(dir: projectDir, uri: remote.repository.rootDir) + + settingsFile << """ +plugins { + id 'org.ajoberstar.reckon.settings' +} + +reckon { + scopeFromProp() + stageFromProp('alpha','beta', 'final') + defaultInferredScope = 'patch' +} +""" + local.add(patterns: ['settings.gradle']) + local.commit(message: 'Build file') + when: + def result = build('reckonTagPush', '--configuration-cache') + then: + result.output.contains('Reckoned version: 1.0.1-alpha.0') + result.task(':reckonTagCreate').outcome == TaskOutcome.UP_TO_DATE + result.task(':reckonTagPush').outcome == TaskOutcome.UP_TO_DATE + } + + def 'if reckoned version is SNAPSHOT no tag created'() { + given: + def local = Grgit.clone(dir: projectDir, uri: remote.repository.rootDir) + + settingsFile << """ +plugins { + id 'org.ajoberstar.reckon.settings' +} + +reckon { + scopeFromProp() + snapshotFromProp() +} +""" + local.add(patterns: ['settings.gradle']) + local.commit(message: 'Build file') + when: + def result = build('reckonTagPush', '--configuration-cache') + then: + result.output.contains('Reckoned version: 1.1.0-SNAPSHOT') + result.task(':reckonTagCreate').outcome == TaskOutcome.UP_TO_DATE + result.task(':reckonTagPush').outcome == TaskOutcome.UP_TO_DATE + } + + def 'if reckoned version is significant tag created and pushed'() { + given: + def local = Grgit.clone(dir: projectDir, uri: remote.repository.rootDir) + + settingsFile << """ +plugins { + id 'org.ajoberstar.reckon.settings' +} + +reckon { + scopeFromProp() + stageFromProp('alpha','beta', 'final') +} +""" + local.add(patterns: ['settings.gradle']) + local.commit(message: 'Build file') + when: + def result = build('reckonTagPush', '-Preckon.stage=alpha', '--configuration-cache') + then: + result.output.contains('Reckoned version: 1.1.0-alpha.1') + result.task(':reckonTagCreate').outcome == TaskOutcome.SUCCESS + result.task(':reckonTagPush').outcome == TaskOutcome.SUCCESS + and: + remote.tag.list().find { it.name == '1.1.0-alpha.1' } + } + + def 'can use commit messages for scope and if reckoned version is significant tag created and pushed'() { + given: + def local = Grgit.clone(dir: projectDir, uri: remote.repository.rootDir) + + settingsFile << """ +plugins { + id 'org.ajoberstar.reckon.settings' +} + +reckon { + stages('alpha','beta', 'final') + scopeCalc = calcScopeFromProp().or(calcScopeFromCommitMessages()) + stageCalc = calcStageFromProp() +} +""" + local.add(patterns: ['settings.gradle']) + local.commit(message: 'Build file') + when: + def result = build('reckonTagPush', '-Preckon.stage=alpha', '--configuration-cache') + then: + result.output.contains('Reckoned version: 2.0.0-alpha.1') + result.task(':reckonTagCreate').outcome == TaskOutcome.SUCCESS + result.task(':reckonTagPush').outcome == TaskOutcome.SUCCESS + and: + remote.tag.list().find { it.name == '2.0.0-alpha.1' } + } + + def 'can use commit messages for scope but override with prop and if reckoned version is significant tag created and pushed'() { + given: + def local = Grgit.clone(dir: projectDir, uri: remote.repository.rootDir) + + settingsFile << """ +plugins { + id 'org.ajoberstar.reckon.settings' +} + +reckon { + stages('alpha','beta', 'final') + scopeCalc = calcScopeFromProp().or(calcScopeFromCommitMessages()) + stageCalc = calcStageFromProp() +} +""" + local.add(patterns: ['settings.gradle']) + local.commit(message: 'Build file') + when: + def result = build('reckonTagPush', '-Preckon.scope=patch', '-Preckon.stage=alpha', '--configuration-cache') + then: + result.output.contains('Reckoned version: 1.0.1-alpha.1') + result.task(':reckonTagCreate').outcome == TaskOutcome.SUCCESS + result.task(':reckonTagPush').outcome == TaskOutcome.SUCCESS + and: + remote.tag.list().find { it.name == '1.0.1-alpha.1' } + } + + def 'remote can be overridden and if reckoned version is significant tag created and pushed'() { + given: + def local = Grgit.clone(dir: projectDir, uri: remote.repository.rootDir) + + settingsFile << """ +plugins { + id 'org.ajoberstar.reckon.settings' +} + +reckon { + scopeFromProp() + stageFromProp('alpha','beta', 'final') + remote = 'other-remote' +} +""" + local.add(patterns: ['settings.gradle']) + local.commit(message: 'Build file') + local.remote.add(name: 'other-remote', url: remote2.getRepository().getRootDir()) + when: + def result = build('reckonTagPush', '-Preckon.stage=alpha', '--configuration-cache') + then: + result.output.contains('Reckoned version: 1.1.0-alpha.1') + result.task(':reckonTagCreate').outcome == TaskOutcome.SUCCESS + result.task(':reckonTagPush').outcome == TaskOutcome.SUCCESS + and: + !remote.tag.list().find { it.name == '1.1.0-alpha.1' } + remote2.tag.list().find { it.name == '1.1.0-alpha.1' } + } + + def 'tag parser/writer can be overridden and reckoned version is significant tag created and pushed'() { + given: + def local = Grgit.clone(dir: projectDir, uri: remote.repository.rootDir) + + settingsFile << """ +plugins { + id 'org.ajoberstar.reckon.settings' +} + +reckon { + scopeFromProp() + stageFromProp('alpha','beta', 'final') + + tagParser = tagName -> java.util.Optional.of(tagName) + .filter(name -> name.startsWith("project-a/")) + .map(name -> name.replace("project-a/", "")) + .flatMap(name -> org.ajoberstar.reckon.core.Version.parse(name)) + tagWriter = version -> "project-a/" + version +} +""" + local.add(patterns: ['settings.gradle']) + local.commit(message: 'Build file') + when: + def result = build('reckonTagPush', '-Preckon.stage=alpha', '--configuration-cache') + then: + result.output.contains('Reckoned version: 9.1.0-alpha.1') + result.task(':reckonTagCreate').outcome == TaskOutcome.SUCCESS + result.task(':reckonTagPush').outcome == TaskOutcome.SUCCESS + and: + remote.tag.list().find { it.name == 'project-a/9.1.0-alpha.1' } + } + + def 'tag message can be overridden and if reckoned version is significant tag created and pushed'() { + given: + def local = Grgit.clone(dir: projectDir, uri: remote.repository.rootDir) + + settingsFile << """ +plugins { + id 'org.ajoberstar.reckon.settings' +} + +reckon { + scopeFromProp() + stageFromProp('alpha','beta', 'final') + tagMessage = version.map(v -> "Version " + v) +} +""" + local.add(patterns: ['settings.gradle']) + local.commit(message: 'Build file') + when: + def result = build('reckonTagPush', '-Preckon.stage=alpha', '--configuration-cache') + then: + result.output.contains('Reckoned version: 1.1.0-alpha.1') + result.task(':reckonTagCreate').outcome == TaskOutcome.SUCCESS + result.task(':reckonTagPush').outcome == TaskOutcome.SUCCESS + and: + remote.tag.list().find { it.name == '1.1.0-alpha.1' && it.shortMessage == 'Version 1.1.0-alpha.1' } + } + + def 'if reckoned version is rebuild, skip tag create, but push'() { + given: + def local = Grgit.clone(dir: projectDir, uri: remote.repository.rootDir) + + + settingsFile << """ +plugins { + id 'org.ajoberstar.reckon.settings' +} + +reckon { + scopeFromProp() + stageFromProp('alpha', 'beta', 'final') +} +""" + local.add(patterns: ['settings.gradle']) + local.commit(message: 'Build file') + local.tag.add(name: '1.1.0', message: '1.1.0') + when: + def result = build('reckonTagPush', '--configuration-cache') + then: + result.output.contains('Reckoned version: 1.1.0') + result.task(':reckonTagCreate').outcome == TaskOutcome.UP_TO_DATE + result.task(':reckonTagPush').outcome == TaskOutcome.SUCCESS + } + + private BuildResult build(String... args = []) { + return GradleRunner.create() + .withGradleVersion(System.properties['compat.gradle.version']) + .withPluginClasspath() + .withProjectDir(projectDir) + .forwardOutput() + .withArguments((args + '--stacktrace') as String[]) + .build() + } + + private BuildResult buildAndFail(String... args = []) { + return GradleRunner.create() + .withGradleVersion(System.properties['compat.gradle.version']) + .withPluginClasspath() + .withProjectDir(projectDir) + .forwardOutput() + .withArguments((args + '--stacktrace') as String[]) + .buildAndFail() + } + + private File remoteFile(String path) { + File file = new File(remote.repository.rootDir, path) + file.parentFile.mkdirs() + return file + } + + private File remote2File(String path) { + File file = new File(remote2.repository.rootDir, path) + file.parentFile.mkdirs() + return file + } + + private File projectFile(String path) { + File file = new File(projectDir, path) + file.parentFile.mkdirs() + return file + } +} diff --git a/reckon-gradle/src/main/java/org/ajoberstar/reckon/gradle/ReckonSettingsPlugin.java b/reckon-gradle/src/main/java/org/ajoberstar/reckon/gradle/ReckonSettingsPlugin.java new file mode 100644 index 0000000..e317af0 --- /dev/null +++ b/reckon-gradle/src/main/java/org/ajoberstar/reckon/gradle/ReckonSettingsPlugin.java @@ -0,0 +1,102 @@ +package org.ajoberstar.reckon.gradle; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.ajoberstar.grgit.gradle.GrgitService; +import org.ajoberstar.reckon.core.Version; +import org.ajoberstar.reckon.core.VersionTagParser; +import org.ajoberstar.reckon.core.VersionTagWriter; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.initialization.Settings; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.TaskProvider; + +public class ReckonSettingsPlugin implements Plugin { + private static Logger logger = Logging.getLogger(ReckonSettingsPlugin.class); + + public static final String TAG_TASK = "reckonTagCreate"; + public static final String PUSH_TASK = "reckonTagPush"; + + private static final String SCOPE_PROP = "reckon.scope"; + private static final String STAGE_PROP = "reckon.stage"; + + @Override + public void apply(Settings settings) { + Provider grgitService = settings.getGradle().getSharedServices().registerIfAbsent("reckon-grgit", GrgitService.class, spec -> { + spec.getParameters().getCurrentDirectory().set(settings.getSettingsDir()); + spec.getParameters().getInitIfNotExists().set(false); + spec.getMaxParallelUsages().set(1); + }); + + var extension = settings.getExtensions().create("reckon", ReckonExtension.class); + extension.getGrgitService().set(grgitService); + extension.setTagParser(VersionTagParser.getDefault()); + extension.setTagWriter(VersionTagWriter.getDefault()); + extension.getTagMessage().convention(extension.getVersion().map(Version::toString)); + + // composite builds have a parent Gradle build and can't trust the values of these properties + if (settings.getGradle().getParent() == null) { + extension.getScope().set(settings.getProviders().gradleProperty(SCOPE_PROP).forUseAtConfigurationTime()); + extension.getStage().set(settings.getProviders().gradleProperty(STAGE_PROP).forUseAtConfigurationTime()); + } + + var sharedVersion = new DelayedVersion(extension.getVersion()); + settings.getGradle().allprojects(prj -> { + prj.setVersion(sharedVersion); + }); + + settings.getGradle().projectsLoaded(gradle -> { + var tag = createTagTask(settings.getGradle().getRootProject(), extension); + var push = createPushTask(settings.getGradle().getRootProject(), extension); + push.configure(t -> t.dependsOn(tag)); + }); + } + + private TaskProvider createTagTask(Project project, ReckonExtension extension) { + return project.getTasks().register(TAG_TASK, ReckonCreateTagTask.class, task -> { + task.setDescription("Tag version inferred by reckon."); + task.setGroup("publishing"); + task.getGrgitService().set(extension.getGrgitService()); + task.getVersion().set(extension.getVersion()); + task.getTagWriter().set(extension.getTagWriter()); + task.getTagMessage().set(extension.getTagMessage()); + }); + } + + private TaskProvider createPushTask(Project project, ReckonExtension extension) { + return project.getTasks().register(PUSH_TASK, ReckonPushTagTask.class, task -> { + task.setDescription("Push version tag created by reckon."); + task.setGroup("publishing"); + task.getGrgitService().set(extension.getGrgitService()); + task.getRemote().set(extension.getRemote()); + task.getVersion().set(extension.getVersion()); + task.getTagWriter().set(extension.getTagWriter()); + }); + } + + private static class DelayedVersion { + private final Provider versionProvider; + private final AtomicBoolean warned; + + public DelayedVersion(Provider versionProvider) { + this.versionProvider = versionProvider; + this.warned = new AtomicBoolean(false); + } + + @Override + public String toString() { + try { + return versionProvider.get().toString(); + } catch (Exception e) { + if (warned.compareAndSet(false, true)) { + logger.warn("Project version evaluated before reckon was configured. Run with --info to see cause."); + } + logger.info("Project version evaluated before reckon was configured.", e); + return "unspecified"; + } + } + } +}