diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 220d3fd..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,89 +0,0 @@ -version: 2 -jobs: - build: - machine: true - steps: - - checkout - - restore_cache: - keys: - - v1-dependencies-{{ checksum "build.gradle.kts" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - run: ./gradlew build - - run: - name: Save test results - command: | - mkdir -p ~/test-results/junit/ - find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/junit/ \; - when: always - - store_test_results: - path: ~/test-results - - store_artifacts: - path: ~/test-results/junit - - save_cache: - paths: - - ~/.gradle - key: v1-dependencies-{{ checksum "build.gradle.kts" }} - publish-plugin: - docker: - - image: circleci/openjdk:8-jdk - working_directory: ~/repo - environment: - JVM_OPTS: -Xmx3200m - TERM: dumb - steps: - - checkout - - restore_cache: - keys: - - v1-dependencies-{{ checksum "build.gradle.kts" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - run: ./gradlew versionDisplay - - run: ./gradlew publishPlugins - - save_cache: - paths: - - ~/.gradle - key: v1-dependencies-{{ checksum "build.gradle.kts" }} - integration-test: - machine: true - steps: - - checkout - - restore_cache: - keys: - - v1-dependencies-{{ checksum "build.gradle.kts" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - run: - name: Install octo cli - command: | - sudo apt update && sudo apt install --no-install-recommends gnupg curl ca-certificates apt-transport-https && \ - curl -sSfL https://apt.octopus.com/public.key | sudo apt-key add - && \ - sudo sh -c "echo deb https://apt.octopus.com/ stable main > /etc/apt/sources.list.d/octopus.com.list" && \ - sudo apt update && sudo apt install octopuscli - - run: ./gradlew integrationTest - - save_cache: - paths: - - ~/.gradle - key: v1-dependencies-{{ checksum "build.gradle.kts" }} - -workflows: - version: 2 - all: - jobs: - - build: - filters: - tags: - only: /.*/ - - integration-test: - filters: - tags: - only: /.*/ - - publish-plugin: - requires: - - build - - integration-test - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..f1773ad --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,25 @@ +name: Pre Merge Checks + +on: + push: + branches: + - main + pull_request: + branches: + - '*' + +jobs: + gradle: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: +# needed for FirstCommitHashTaskTest + fetch-depth: 0 + - name: Cache + uses: gradle/gradle-build-action@v2 + - name: Build + run: ./gradlew build + - name: Validate + run: ./gradlew check validatePlugins --continue diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..573bdd1 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,27 @@ +name: Publish to Gradle Plugin Portal + +on: + push: + tags: + - '*' + +jobs: + gradle: + runs-on: ubuntu-latest + env: + GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} + GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + # needed for FirstCommitHashTaskTest + fetch-depth: 0 + - name: Cache + uses: gradle/gradle-build-action@v2 + - name: Build + run: ./gradlew build + - name: Validate + run: ./gradlew validatePlugins --continue + - name: Publish + run: ./gradlew publishPlugins diff --git a/src/main/kotlin/com/liftric/octopusdeploy/OctopusDeployPlugin.kt b/src/main/kotlin/com/liftric/octopusdeploy/OctopusDeployPlugin.kt index f35d2b0..7695e4b 100644 --- a/src/main/kotlin/com/liftric/octopusdeploy/OctopusDeployPlugin.kt +++ b/src/main/kotlin/com/liftric/octopusdeploy/OctopusDeployPlugin.kt @@ -7,6 +7,7 @@ import org.gradle.api.Project internal const val extensionName = "octopus" class OctopusDeployPlugin : Plugin { + @Suppress("UNUSED_VARIABLE") override fun apply(project: Project) { val extension = project.extensions.create(extensionName, OctopusDeployExtension::class.java, project) extension.outputDir.apply { @@ -59,6 +60,26 @@ class OctopusDeployPlugin : Plugin { buildInformationAddition = extension.buildInformationAddition } } + val createBuildInformationMarkdownTask = + project.tasks.create("createBuildInformationMarkdown", CreateBuildInformationMarkdownTask::class.java) + .apply { + val task = this + project.afterEvaluate { + if (extension.generateChangelogSinceLastTag) { + dependsOn(commitsSinceLastTagTask) + } + commits = emptyList() + packageName.set(extension.packageName) + issueTrackerName.set(extension.issueTrackerName) + parseCommitsForJiraIssues.set(extension.parseCommitsForJiraIssues) + jiraBaseBrowseUrl.set(extension.jiraBaseBrowseUrl) + task.version.set(extension.version) + } + doFirst { + commits = commitsSinceLastTagTask.commits + buildInformationAddition = extension.buildInformationAddition + } + } val uploadBuildInformationTask = project.tasks.create("uploadBuildInformation", UploadBuildInformationTask::class.java).apply { dependsOn(createBuildInformationTask) @@ -94,6 +115,10 @@ class OctopusDeployPlugin : Plugin { httpLogLevel.set(extension.httpLogLevel) } } + project.tasks.withType(CreateReleaseTask::class.java) { + apiKey.convention(extension.apiKey) + octopusUrl.convention(extension.serverUrl) + } } } diff --git a/src/main/kotlin/com/liftric/octopusdeploy/api/Progression.kt b/src/main/kotlin/com/liftric/octopusdeploy/api/Progression.kt index 7419528..83ca236 100644 --- a/src/main/kotlin/com/liftric/octopusdeploy/api/Progression.kt +++ b/src/main/kotlin/com/liftric/octopusdeploy/api/Progression.kt @@ -218,85 +218,85 @@ data class DeploymentDeploymentLinks( ) data class Task( - @get:JsonProperty("Id", required = true) @field:JsonProperty("Id", required = true) - val id: String, + @get:JsonProperty("Id") @field:JsonProperty("Id") + val id: String? = null, - @get:JsonProperty("SpaceId", required = true) @field:JsonProperty("SpaceId", required = true) - val spaceID: String, + @get:JsonProperty("SpaceId") @field:JsonProperty("SpaceId") + val spaceID: String? = null, - @get:JsonProperty("Name", required = true) @field:JsonProperty("Name", required = true) - val name: String, + @get:JsonProperty("Name") @field:JsonProperty("Name") + val name: String? = null, - @get:JsonProperty("Description", required = true) @field:JsonProperty("Description", required = true) - val description: String, + @get:JsonProperty("Description") @field:JsonProperty("Description") + val description: String? = null, - @get:JsonProperty("Arguments", required = true) @field:JsonProperty("Arguments", required = true) - val arguments: Arguments, + @get:JsonProperty("Arguments") @field:JsonProperty("Arguments") + val arguments: Arguments? = null, - @get:JsonProperty("State", required = true) @field:JsonProperty("State", required = true) - val state: String, + @get:JsonProperty("State") @field:JsonProperty("State") + val state: String? = null, - @get:JsonProperty("QueueTime", required = true) @field:JsonProperty("QueueTime", required = true) - val queueTime: String, + @get:JsonProperty("QueueTime") @field:JsonProperty("QueueTime") + val queueTime: String? = null, @get:JsonProperty("QueueTimeExpiry") @field:JsonProperty("QueueTimeExpiry") val queueTimeExpiry: Any? = null, - @get:JsonProperty("StartTime", required = true) @field:JsonProperty("StartTime", required = true) - val startTime: String, + @get:JsonProperty("StartTime") @field:JsonProperty("StartTime") + val startTime: String? = null, - @get:JsonProperty("LastUpdatedTime", required = true) @field:JsonProperty("LastUpdatedTime", required = true) - val lastUpdatedTime: String, + @get:JsonProperty("LastUpdatedTime") @field:JsonProperty("LastUpdatedTime") + val lastUpdatedTime: String? = null, @get:JsonProperty("CompletedTime") @field:JsonProperty("CompletedTime") val completedTime: Any? = null, - @get:JsonProperty("ServerNode", required = true) @field:JsonProperty("ServerNode", required = true) - val serverNode: String, + @get:JsonProperty("ServerNode") @field:JsonProperty("ServerNode") + val serverNode: String? = null, - @get:JsonProperty("Duration", required = true) @field:JsonProperty("Duration", required = true) - val duration: String, + @get:JsonProperty("Duration") @field:JsonProperty("Duration") + val duration: String? = null, - @get:JsonProperty("ErrorMessage", required = true) @field:JsonProperty("ErrorMessage", required = true) - val errorMessage: String, + @get:JsonProperty("ErrorMessage") @field:JsonProperty("ErrorMessage") + val errorMessage: String? = null, - @get:JsonProperty("HasBeenPickedUpByProcessor", required = true) @field:JsonProperty( + @get:JsonProperty("HasBeenPickedUpByProcessor") @field:JsonProperty( "HasBeenPickedUpByProcessor", required = true ) - val hasBeenPickedUpByProcessor: Boolean, + val hasBeenPickedUpByProcessor: Boolean? = null, - @get:JsonProperty("IsCompleted", required = true) @field:JsonProperty("IsCompleted", required = true) - val isCompleted: Boolean, + @get:JsonProperty("IsCompleted") @field:JsonProperty("IsCompleted") + val isCompleted: Boolean? = null, - @get:JsonProperty("FinishedSuccessfully", required = true) @field:JsonProperty( + @get:JsonProperty("FinishedSuccessfully") @field:JsonProperty( "FinishedSuccessfully", required = true ) - val finishedSuccessfully: Boolean, + val finishedSuccessfully: Boolean? = null, - @get:JsonProperty("HasPendingInterruptions", required = true) @field:JsonProperty( + @get:JsonProperty("HasPendingInterruptions") @field:JsonProperty( "HasPendingInterruptions", required = true ) - val hasPendingInterruptions: Boolean, + val hasPendingInterruptions: Boolean? = null, - @get:JsonProperty("CanRerun", required = true) @field:JsonProperty("CanRerun", required = true) - val canRerun: Boolean, + @get:JsonProperty("CanRerun") @field:JsonProperty("CanRerun") + val canRerun: Boolean? = null, - @get:JsonProperty("HasWarningsOrErrors", required = true) @field:JsonProperty( + @get:JsonProperty("HasWarningsOrErrors") @field:JsonProperty( "HasWarningsOrErrors", required = true ) - val hasWarningsOrErrors: Boolean, + val hasWarningsOrErrors: Boolean? = null, - @get:JsonProperty("Links", required = true) @field:JsonProperty("Links", required = true) - val links: TaskLinks + @get:JsonProperty("Links") @field:JsonProperty("Links") + val links: TaskLinks? = null ) data class Arguments( - @get:JsonProperty("DeploymentId", required = true) @field:JsonProperty("DeploymentId", required = true) - val deploymentID: String + @get:JsonProperty("DeploymentId") @field:JsonProperty("DeploymentId") + val deploymentID: String? = null ) data class TaskLinks( diff --git a/src/main/kotlin/com/liftric/octopusdeploy/buildinformation.kt b/src/main/kotlin/com/liftric/octopusdeploy/buildinformation.kt new file mode 100644 index 0000000..3ee43f3 --- /dev/null +++ b/src/main/kotlin/com/liftric/octopusdeploy/buildinformation.kt @@ -0,0 +1,26 @@ +package com.liftric.octopusdeploy + +import com.liftric.octopusdeploy.api.CommitCli +import com.liftric.octopusdeploy.api.WorkItem + +fun parseCommitsForJira( + commits: List, + jiraBaseBrowseUrl: String +): List { + val jiraIssues = commits + .mapNotNull { it.Comment } + .map { jiraKeyRegex.findAll(it).map { it.groupValues[1] }.toList() } + .flatten() + .toSet() + println("parseCommitsForJira: found $jiraIssues") + return jiraIssues.map { + WorkItem( + Id = it, + LinkUrl = "${jiraBaseBrowseUrl.removeSuffix("/")}/$it", + Description = "some placeholder text" + ) + } +} + +// from https://confluence.atlassian.com/stashkb/integrating-with-custom-jira-issue-key-313460921.html +private val jiraKeyRegex = Regex("((? = project.objects.property() + + @Input + val version: Property = project.objects.property() + + @Input + lateinit var commits: List + + @Input + var buildInformationAddition: BuildInformationCli.() -> Unit = {} + + @OutputFile + @Optional + var outputFile: File? = null + + @Input + @Optional + val issueTrackerName: Property = project.objects.property() + + @Input + @Optional + val parseCommitsForJiraIssues: Property = project.objects.property() + + @Input + @Optional + val jiraBaseBrowseUrl: Property = project.objects.property() +} diff --git a/src/main/kotlin/com/liftric/octopusdeploy/task/CreateBuildInformationMarkdownTask.kt b/src/main/kotlin/com/liftric/octopusdeploy/task/CreateBuildInformationMarkdownTask.kt new file mode 100644 index 0000000..37b9e86 --- /dev/null +++ b/src/main/kotlin/com/liftric/octopusdeploy/task/CreateBuildInformationMarkdownTask.kt @@ -0,0 +1,53 @@ +package com.liftric.octopusdeploy.task + +import com.liftric.octopusdeploy.api.BuildInformationCli +import com.liftric.octopusdeploy.api.WorkItem +import com.liftric.octopusdeploy.extensionName +import com.liftric.octopusdeploy.parseCommitsForJira +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +open class CreateBuildInformationMarkdownTask : AbstractBuildInformationTask() { + init { + group = "octopus" + description = "Creates a markdown file freom the build-information for octo cli release creation." + outputs.upToDateWhen { false } + } + + @OutputDirectory + val outputDirectory: DirectoryProperty = project.objects.directoryProperty().convention(project.layout.buildDirectory.dir(extensionName)) + + @OutputFile + val outputMarkdown: RegularFileProperty = project.objects.fileProperty().convention(outputDirectory.file("build-information.md")) + + @TaskAction + fun execute() { + val workItems: List? = if (parseCommitsForJiraIssues.getOrElse(false)) { + parseCommitsForJira(commits, jiraBaseBrowseUrl.getOrElse("")) + } else { + null + } + val buildInformationCli = BuildInformationCli().apply(buildInformationAddition) + outputMarkdown.get().asFile.apply { + writeText("") + appendText("# ${packageName.get()}: ${version.get()}\n") + appendText("[VCS Root](${buildInformationCli.VcsRoot})\n\n") + appendText("CI: [${buildInformationCli.VcsCommitNumber}](${buildInformationCli.VcsCommitUrl})\n\n") + appendText("[BuildEnvironment: ${buildInformationCli.BuildEnvironment}](${buildInformationCli.BuildUrl})\n\n") + appendText("last modified by: ${buildInformationCli.LastModifiedBy}\n\n") + if (commits.isNotEmpty()) + appendText("## Commits\n") + commits.forEach { + appendText(" - ${it.Id}: [${it.Comment}](${it.LinkUrl})\n") + } + if (workItems?.isNotEmpty() == true) + appendText("## WorkItems\n") + workItems?.forEach { + appendText(" - [${it.Id}](${it.LinkUrl})\n") + } + } + } +} diff --git a/src/main/kotlin/com/liftric/octopusdeploy/task/CreateBuildInformationTask.kt b/src/main/kotlin/com/liftric/octopusdeploy/task/CreateBuildInformationTask.kt index c29458a..9652e79 100644 --- a/src/main/kotlin/com/liftric/octopusdeploy/task/CreateBuildInformationTask.kt +++ b/src/main/kotlin/com/liftric/octopusdeploy/task/CreateBuildInformationTask.kt @@ -4,52 +4,22 @@ import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.liftric.octopusdeploy.api.BuildInformationCli -import com.liftric.octopusdeploy.api.CommitCli import com.liftric.octopusdeploy.api.WorkItem -import org.gradle.api.DefaultTask -import org.gradle.api.provider.Property -import org.gradle.api.tasks.* -import org.gradle.kotlin.dsl.property +import com.liftric.octopusdeploy.parseCommitsForJira +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction import java.io.File -open class CreateBuildInformationTask : DefaultTask() { +open class CreateBuildInformationTask : AbstractBuildInformationTask() { init { group = "octopus" description = "Creates the octopus build-information file." outputs.upToDateWhen { false } } - @Input - val packageName: Property = project.objects.property() - - @Input - val version: Property = project.objects.property() - - @Input - lateinit var commits: List - - @Input - var buildInformationAddition: BuildInformationCli.() -> Unit = {} - @OutputDirectory lateinit var outputDir: File - @OutputFile - @Optional - var outputFile: File? = null - - @Input - @Optional - val issueTrackerName: Property = project.objects.property() - - @Input - @Optional - val parseCommitsForJiraIssues: Property = project.objects.property() - - @Input - @Optional - val jiraBaseBrowseUrl: Property = project.objects.property() - @TaskAction fun execute() { val workItems: List? = if (parseCommitsForJiraIssues.getOrElse(false)) { @@ -74,26 +44,4 @@ open class CreateBuildInformationTask : DefaultTask() { ) } } - - private fun parseCommitsForJira( - commits: List, - jiraBaseBrowseUrl: String - ): List { - val jiraIssues = commits - .mapNotNull { it.Comment } - .map { jiraKeyRegex.findAll(it).map { it.groupValues[1] }.toList() } - .flatten() - .toSet() - println("parseCommitsForJira: found $jiraIssues") - return jiraIssues.map { - WorkItem( - Id = it, - LinkUrl = "${jiraBaseBrowseUrl.removeSuffix("/")}/$it", - Description = "some placeholder text" - ) - } - } } - -// from https://confluence.atlassian.com/stashkb/integrating-with-custom-jira-issue-key-313460921.html -private val jiraKeyRegex = Regex("((? = project.objects.property() + + @Input + val apiKey: Property = project.objects.property() + + @Input + val projectName: Property = project.objects.property() + + @Input + @Optional + val releaseNumber: Property = project.objects.property() + + @Input + @Optional + val dryRun: Property = project.objects.property() + + @Input + @Optional + val waitForReleaseDeployments: Property = project.objects.property() + + @Optional + @InputFile + val releaseNoteFile: RegularFileProperty = project.objects.fileProperty() + + /** + * Version number to use for a package + * in the release. Format: StepName:Version or + * PackageID:Version or + * StepName:PackageName:Version. StepName, + * PackageID, and PackageName can be replaced with + * an asterisk. An asterisk will be assumed for + * StepName, PackageID, or PackageName if they are + * omitted. + */ + @Input + @Optional + val packages: ListProperty = project.objects.listProperty() + + @TaskAction + fun execute() { + val dryRun = if(dryRun.getOrElse(false)) { + "--whatif" + } else { + null + } + val waitForReleaseDeployments = if(waitForReleaseDeployments.getOrElse(false)) { + "--waitForDeployment" + } else { + null + } + val releaseNotes = releaseNoteFile.orNull + println(" DEBUG: releaseNotes=$releaseNotes") + val (exitCode, inputText, errorText) = listOfNotNull( + "octo", + "create-release", + "--server=${octopusUrl.get()}", + "--apiKey=${apiKey.get()}", + "--project=\"${projectName.get()}\"", + releaseNumber.orNull?.let { "--releaseNumber=\"$it\"" }, + *packages.orNull?.map { "--package=\"$it\"" }?.toTypedArray() ?: emptyArray(), + releaseNotes?.let { "--releaseNoteFile=\"${it.asFile.absolutePath}\"" }, + waitForReleaseDeployments, + dryRun, + ).joinToString(" ").let { shell(it, logger) } + if (exitCode == 0) { + println(inputText) + println(errorText) + } else { + logger.error("octo create-release returned non-zero exitCode: $exitCode") + logger.error(inputText) + throw IllegalStateException("octo create-release exitCode: $exitCode") + } + } +} diff --git a/src/main/kotlin/com/liftric/octopusdeploy/task/PromoteReleaseTask.kt b/src/main/kotlin/com/liftric/octopusdeploy/task/PromoteReleaseTask.kt index 34a4cc6..4bbc242 100644 --- a/src/main/kotlin/com/liftric/octopusdeploy/task/PromoteReleaseTask.kt +++ b/src/main/kotlin/com/liftric/octopusdeploy/task/PromoteReleaseTask.kt @@ -80,7 +80,7 @@ open class PromoteReleaseTask : DefaultTask() { "promote-release", "--server=$octopusUrlValue", "--apiKey=$apiKeyValue", - "--project=$projectNameValue", + "--project=\"$projectNameValue\"", "--from=$fromValue", "--to=$toValue" ).joinToString(" ").let { shell(it, logger) } diff --git a/src/main/kotlin/com/liftric/octopusdeploy/task/UploadBuildInformationTask.kt b/src/main/kotlin/com/liftric/octopusdeploy/task/UploadBuildInformationTask.kt index ce3d358..aeb7fe2 100644 --- a/src/main/kotlin/com/liftric/octopusdeploy/task/UploadBuildInformationTask.kt +++ b/src/main/kotlin/com/liftric/octopusdeploy/task/UploadBuildInformationTask.kt @@ -47,7 +47,7 @@ open class UploadBuildInformationTask : DefaultTask() { "--file", buildInformation?.absolutePath ?: error("couldn't find build-information.json"), "--package-id", - packageName.get(), + "\"${packageName.get()}\"", "--version=${version.get()}", overwriteMode?.let { "--overwrite-mode=$it" } ).filterNotNull().joinToString(" ").let { shell(it, logger) } diff --git a/src/test/kotlin/com/liftric/octopusdeploy/task/FirstCommitHashTaskTest.kt b/src/test/kotlin/com/liftric/octopusdeploy/task/FirstCommitHashTaskTest.kt index 87e436e..a859fca 100644 --- a/src/test/kotlin/com/liftric/octopusdeploy/task/FirstCommitHashTaskTest.kt +++ b/src/test/kotlin/com/liftric/octopusdeploy/task/FirstCommitHashTaskTest.kt @@ -28,6 +28,7 @@ class FirstCommitHashTaskTest { assertEquals(TaskOutcome.SUCCESS, result.task(":firstCommitHash")?.outcome) File("${testProjectDir.root.absolutePath}/build/$extensionName/firstCommitHash").apply { assertTrue(exists()) + // this is the first commit hash of the octopus-deploy-plugin git repo itself assertEquals("9c82501b25fd6c03bd6f3074739496b498cf3938", readText()) } } @@ -46,6 +47,13 @@ octopus { packageName.set("whatever") serverUrl.set("whatever") } +tasks { + val firstCommitHash by existing { + doLast { + println("firstCommitHash=${'$'}{file("build/octopus/firstCommitHash").readText()}") + } + } +} """ ) }