diff --git a/.buildkite/pipeline.extra-steps.gradle b/.buildkite/pipeline.extra-steps.gradle index 4e8f65d..bd92d91 100644 --- a/.buildkite/pipeline.extra-steps.gradle +++ b/.buildkite/pipeline.extra-steps.gradle @@ -52,8 +52,6 @@ blockStep(':rocket: Release!') { } } -triggerStep 'other-pipeline' - triggerStep 'other-pipeline', { branches 'master' async true diff --git a/buildSrc/src/main/groovy/com/widen/plugins/buildkite/BuildkiteExtension.groovy b/buildSrc/src/main/groovy/com/widen/plugins/buildkite/BuildkiteExtension.groovy new file mode 100644 index 0000000..8893d99 --- /dev/null +++ b/buildSrc/src/main/groovy/com/widen/plugins/buildkite/BuildkiteExtension.groovy @@ -0,0 +1,63 @@ +package com.widen.plugins.buildkite + +import groovy.transform.PackageScope +import org.gradle.api.Project + +class BuildkiteExtension { + @PackageScope + Project project + + @PackageScope + final Map> pipelines = [:] + + @PackageScope + final Map pluginVersions = [ + docker: 'v3.2.0', + 'docker-compose': 'v3.0.3', + ] + + /** + * The default agent queue name to use for steps that do not specify one. + */ + String defaultAgentQueue = 'builder' + + /** + * Whether detected pipeline script files should be included automatically. + */ + boolean includeScripts = true + + /** + * Set the default agent queue name. + */ + void defaultAgentQueue(String queueName) { + defaultAgentQueue = queueName + } + + /** + * Specify the version of a Buildkite plugin that should be used inside pipelines if no version is specified. + */ + void pluginVersion(String name, String version) { + pluginVersions[name] = version + } + + /** + * Defines the default pipeline. + */ + void pipeline(@DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = BuildkitePipeline) Closure closure) { + pipeline('default', closure) + } + + /** + * Defines a named pipeline. + */ + void pipeline( + String name, + @DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = BuildkitePipeline) Closure closure + ) { + pipelines[name] = { + def pipeline = new BuildkitePipeline(project, this) + pipeline.with(closure) + return pipeline + } + } +} diff --git a/buildSrc/src/main/groovy/com/widen/plugins/buildkite/BuildkitePipeline.groovy b/buildSrc/src/main/groovy/com/widen/plugins/buildkite/BuildkitePipeline.groovy index b1a755f..5075d7b 100644 --- a/buildSrc/src/main/groovy/com/widen/plugins/buildkite/BuildkitePipeline.groovy +++ b/buildSrc/src/main/groovy/com/widen/plugins/buildkite/BuildkitePipeline.groovy @@ -2,17 +2,20 @@ package com.widen.plugins.buildkite import groovy.json.JsonBuilder import groovy.json.JsonOutput +import org.gradle.api.Project import java.nio.file.Files import java.time.Duration class BuildkitePipeline implements ConfigurableEnvironment { - private final BuildkitePlugin.Config pluginConfig + private final Project project + private final BuildkiteExtension buildkite private final Map env = [:] - private final List steps = [] + private final List steps = [] - BuildkitePipeline(BuildkitePlugin.Config pluginConfig) { - this.pluginConfig = pluginConfig + BuildkitePipeline(Project project, BuildkiteExtension buildkite) { + this.project = project + this.buildkite = buildkite } /** @@ -68,7 +71,7 @@ class BuildkitePipeline implements ConfigurableEnvironment { class CommandStep extends Step implements ConfigurableEnvironment { // Set defaults. { - agentQueue pluginConfig.defaultAgentQueue + agentQueue buildkite.defaultAgentQueue } /** @@ -216,8 +219,8 @@ class BuildkitePipeline implements ConfigurableEnvironment { } // If no version is given and a default version is defined, set it. - if (!name.contains("#") && pluginConfig.pluginVersions.containsKey(name)) { - name += "#${pluginConfig.pluginVersions[name]}" + if (!name.contains("#") && buildkite.pluginVersions.containsKey(name)) { + name += "#${buildkite.pluginVersions[name]}" } model.get('plugins', []) << [ @@ -321,7 +324,7 @@ class BuildkitePipeline implements ConfigurableEnvironment { // Pre-populate some config files. ['docker-compose.yml', 'docker-compose.buildkite.yml'].each { - if (Files.exists(pluginConfig.rootDir.toPath().resolve(it))) { + if (Files.exists(project.rootDir.toPath().resolve(it))) { config.composeFile(it) } } @@ -619,7 +622,7 @@ class BuildkitePipeline implements ConfigurableEnvironment { closure = (Closure) closure.clone() closure.delegate = map closure.resolveStrategy = Closure.OWNER_FIRST - closure() + closure.call() model.get('build', [:]) .get('meta_data', [:]) diff --git a/buildSrc/src/main/groovy/com/widen/plugins/buildkite/BuildkitePlugin.groovy b/buildSrc/src/main/groovy/com/widen/plugins/buildkite/BuildkitePlugin.groovy index 14c7021..2cb3e85 100644 --- a/buildSrc/src/main/groovy/com/widen/plugins/buildkite/BuildkitePlugin.groovy +++ b/buildSrc/src/main/groovy/com/widen/plugins/buildkite/BuildkitePlugin.groovy @@ -3,19 +3,21 @@ package com.widen.plugins.buildkite import org.codehaus.groovy.control.CompilerConfiguration import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.Task class BuildkitePlugin implements Plugin { + private static final String GROUP = 'Buildkite' + @Override void apply(Project project) { - def extension = project.extensions.create('buildkite', Extension) + def extension = project.extensions.create('buildkite', BuildkiteExtension) + extension.project = project - extension.config = new Config().with { - rootDir = project.rootDir - it - } + project.task('pipelines') { Task task -> + task.group = GROUP + task.description = "List all Buildkite pipelines." - project.task('pipelines') { - doLast { + task.doLast { extension.pipelines.each { println it.key } @@ -25,95 +27,45 @@ class BuildkitePlugin implements Plugin { // Run anything that needs to be done after plugin configuration has been evaluated. project.afterEvaluate { if (extension.includeScripts) { - def shell = new GroovyShell(project.buildscript.classLoader, new Binding(project: project), new - CompilerConfiguration( - scriptBaseClass: PipelineScript.class.name - )) - - project.fileTree(project.rootDir) { + loadPipelineScripts(project, extension, project.fileTree(project.rootDir) { include '.buildkite/pipeline*.gradle' - }.each { file -> - def pipelineName = file.name.find(/pipeline\.([^.]+)\.gradle/) { x, name -> - name.replaceAll(/[^a-zA-Z0-9]+([a-zA-Z0-9]+)/) { y, word -> - word.capitalize() - } - } ?: 'default' - - def script = (PipelineScript) shell.parse(file) - - extension.pipeline(pipelineName) { BuildkitePipeline pipeline -> - println(pipeline) - script.setPipeline(pipeline) - script.setBuildkite(extension) - script.setProject(project) - script.run() - } - } + }) } extension.pipelines.each { name, config -> def taskName = name == 'default' ? 'uploadPipeline' : "upload${name.capitalize()}Pipeline" - project.tasks.create(taskName, UploadPipelineTask) { - pipelineConfigure = config + project.tasks.create(taskName, UploadPipelineTask) { task -> + task.group = GROUP + task.description = "Upload the $name pipeline to the current job." + task.pipelineConfig = config } } } } - static class Config { - String defaultAgentQueue = 'builder' - - final Map pluginVersions = [ - docker: 'v3.2.0', - 'docker-compose': 'v3.0.3', - ] - - File rootDir - } - - static class Extension { - protected final Map> pipelines = [:] - protected Config config - - /** - * Whether detected pipeline script files should be included automatically. - */ - boolean includeScripts = true - - /** - * Set the default agent queue name to use for steps that do not specify one. - */ - void defaultAgentQueue(String queueName) { - config.defaultAgentQueue = queueName - } - - /** - * Specify the version of a Buildkite plugin that should be used inside pipelines if no version is specified. - */ - void pluginVersion(String name, String version) { - config.pluginVersions[name] = version - } - - /** - * Defines the default pipeline. - */ - void pipeline(@DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = BuildkitePipeline) Closure closure) { - pipeline('default', closure) + private static loadPipelineScripts(Project project, BuildkiteExtension extension, Iterable files) { + def shell = new GroovyShell(project.buildscript.classLoader, new CompilerConfiguration( + scriptBaseClass: PipelineScript.class.name + )) + + files.each { file -> + extension.pipeline(pipelineNameFromFile(file)) { BuildkitePipeline pipeline -> + // Avoid loading the file until the pipeline spec is actually requested. + def script = (PipelineScript) shell.parse(file) + script.setProject(project) + script.setBuildkite(extension) + script.setPipeline(pipeline) + script.run() + } } + } - /** - * Defines a named pipeline. - */ - void pipeline( - String name, - @DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = BuildkitePipeline) Closure closure - ) { - pipelines[name] = { - def pipeline = new BuildkitePipeline(config) - pipeline.with(closure) - return pipeline + private static String pipelineNameFromFile(File file) { + return file.name.find(/pipeline\.([^.]+)\.gradle/) { x, name -> + name.replaceAll(/[^a-zA-Z0-9]+([a-zA-Z0-9]+)/) { y, word -> + word.capitalize() } - } + } ?: 'default' } } diff --git a/buildSrc/src/main/groovy/com/widen/plugins/buildkite/PipelineScript.groovy b/buildSrc/src/main/groovy/com/widen/plugins/buildkite/PipelineScript.groovy index 95bea93..6dc7250 100644 --- a/buildSrc/src/main/groovy/com/widen/plugins/buildkite/PipelineScript.groovy +++ b/buildSrc/src/main/groovy/com/widen/plugins/buildkite/PipelineScript.groovy @@ -3,21 +3,19 @@ package com.widen.plugins.buildkite import org.gradle.api.Project /** - * Base class for scripts + * Base class for scripts that define a Buildkite pipeline. */ abstract class PipelineScript extends Script { - BuildkitePipeline pipeline - BuildkitePlugin.Extension buildkite Project project + BuildkiteExtension buildkite + BuildkitePipeline pipeline Object getProperty(String property) { - if ('buildkite' == property) { - return buildkite + try { + return getMetaClass().getProperty(this, property) + } catch (MissingPropertyException e) { + return pipeline.getProperty(property) } - if ('project' == property) { - return project - } - return pipeline.getProperty(property) } void setProperty(String property, Object newValue) { @@ -25,6 +23,10 @@ abstract class PipelineScript extends Script { } Object invokeMethod(String name, Object args) { - return pipeline.invokeMethod(name, args) + try { + return getMetaClass().invokeMethod(this, name, args) + } catch (MissingMethodException e) { + return pipeline.invokeMethod(name, args) + } } } diff --git a/buildSrc/src/main/groovy/com/widen/plugins/buildkite/UploadPipelineTask.groovy b/buildSrc/src/main/groovy/com/widen/plugins/buildkite/UploadPipelineTask.groovy index 3dd375b..43d1b15 100644 --- a/buildSrc/src/main/groovy/com/widen/plugins/buildkite/UploadPipelineTask.groovy +++ b/buildSrc/src/main/groovy/com/widen/plugins/buildkite/UploadPipelineTask.groovy @@ -1,6 +1,7 @@ package com.widen.plugins.buildkite import groovy.json.JsonOutput +import groovy.transform.PackageScope import org.gradle.api.DefaultTask import org.gradle.api.tasks.TaskAction @@ -8,16 +9,17 @@ import org.gradle.api.tasks.TaskAction * A Gradle task that uploads a pipeline during a build. */ class UploadPipelineTask extends DefaultTask { - Closure pipelineConfigure + @PackageScope + Closure pipelineConfig /** * Upload the pipeline to Buildkite to be executed. */ @TaskAction void upload() { - def pipeline = pipelineConfigure.call() + def pipeline = pipelineConfig.call() - if (System.env.BUILDKITE || System.env.CI) { + if (System.getenv('BUILDKITE') || System.getenv('CI')) { def cmd = ['buildkite-agent', 'pipeline', 'upload'] if (!pipeline.interpolate) { @@ -34,7 +36,7 @@ class UploadPipelineTask extends DefaultTask { it << pipeline.toJson() } if (process.waitFor() != 0) { - throw new RuntimeException() + throw new RuntimeException("buildkite-agent returned exit code ${process.exitValue()}") } } else { print(JsonOutput.prettyPrint(pipeline.toJson()))