Skip to content

Commit 2e598df

Browse files
authoredNov 1, 2024··
Add ArtifactoryPromoteBuild, ArtifactoryPromoteDockerImage, ArtifactoryPushDockerImageAndPublishBuild (#1)
1 parent 0a211d8 commit 2e598df

34 files changed

+1341
-5
lines changed
 

‎.github/release.yml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# .github/release.yml
2+
3+
changelog:
4+
categories:
5+
- title: New features and enhancements
6+
labels:
7+
- '*'
8+
exclude:
9+
labels:
10+
- dependencies
11+
- bug
12+
- title: Bugfixes
13+
labels:
14+
- bug
15+
- title: Dependencies
16+
labels:
17+
- dependencies

‎.github/workflows/build.yml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
name: Gradle Build
2+
3+
on: [push, pull_request, workflow_dispatch]
4+
5+
jobs:
6+
build:
7+
uses: octopusden/octopus-base/.github/workflows/common-java-gradle-build.yml@v2.1.8
8+
with:
9+
flow-type: hybrid
10+
java-version: '21'
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: Check for artifact and register release
2+
3+
on:
4+
workflow_run:
5+
workflows: ["Gradle Release"]
6+
types:
7+
- completed
8+
9+
jobs:
10+
build:
11+
uses: octopusden/octopus-base/.github/workflows/common-check-and-register-release.yml@v2.1.8
12+
if: "${{ github.event.workflow_run.conclusion == 'success' }}"
13+
with:
14+
artifact-pattern: "octopus/automation/teamcity/octopus-artifactory-automation/_VER_/octopus-artifactory-automation-_VER_.jar"
15+
secrets: inherit

‎.github/workflows/release.yml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: Gradle Release
2+
3+
on:
4+
repository_dispatch:
5+
types: release
6+
7+
jobs:
8+
build:
9+
uses: octopusden/octopus-base/.github/workflows/common-java-gradle-release.yml@v2.1.8
10+
with:
11+
flow-type: hybrid
12+
java-version: '21'
13+
commit-hash: ${{ github.event.client_payload.commit }}
14+
build-version: ${{ github.event.client_payload.project_version }}
15+
secrets: inherit

‎.gitignore

+16-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
1-
target
21
.gradle
3-
out
4-
build
5-
.idea
2+
**/build/
3+
!src/**/build/
4+
5+
# Ignore Gradle GUI config
6+
gradle-app.setting
7+
8+
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
9+
!gradle-wrapper.jar
10+
11+
# Avoid ignore Gradle wrappper properties
12+
!gradle-wrapper.properties
13+
14+
# Cache of project
15+
.gradletasknamecache
16+
17+
.idea

‎README.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
# octopus-artifactory-automation
1+
# octopus-artifactory-automation
2+
3+
## Prerequisites
4+
Teamcity parameters listed below should be accessible on level of meta-runner usage
5+
- ARTIFACTORY_URL
6+
- ARTIFACTORY_USER + ARTIFACTORY_PASSWORD or ARTIFACTORY_TOKEN
7+
8+
Meta-runner 'Push and publish Artifactory Docker Image' depends on 'jfrog' cli utility. The utility must be accessible on using agents

‎build.gradle.kts

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import com.avast.gradle.dockercompose.ComposeExtension
2+
import java.time.Duration
3+
import org.gradle.api.tasks.testing.logging.TestLogEvent
4+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
5+
import org.octopusden.octopus.task.ConfigureMockServer
6+
7+
plugins {
8+
id("org.jetbrains.kotlin.jvm")
9+
application
10+
id("com.github.johnrengelman.shadow")
11+
id("com.avast.gradle.docker-compose")
12+
`maven-publish`
13+
id("io.github.gradle-nexus.publish-plugin")
14+
signing
15+
}
16+
17+
group = "org.octopusden.octopus.automation.artifactory"
18+
description = "Octopus Artifactory Automation"
19+
20+
tasks.withType<KotlinCompile>().configureEach {
21+
kotlinOptions {
22+
suppressWarnings = true
23+
jvmTarget = "1.8"
24+
}
25+
}
26+
27+
java.targetCompatibility = JavaVersion.VERSION_1_8
28+
29+
repositories {
30+
mavenCentral()
31+
}
32+
33+
tasks.withType<Test> {
34+
dependsOn("configureMockServer")
35+
useJUnitPlatform()
36+
testLogging {
37+
info.events = setOf(TestLogEvent.FAILED, TestLogEvent.PASSED, TestLogEvent.SKIPPED)
38+
}
39+
systemProperties["jar"] = configurations["shadow"].artifacts.files.asPath
40+
}
41+
42+
configure<ComposeExtension> {
43+
useComposeFiles.add(layout.projectDirectory.file("docker/docker-compose.yml").asFile.path)
44+
waitForTcpPorts.set(true)
45+
captureContainersOutputToFiles.set(layout.buildDirectory.dir("docker-logs"))
46+
environment.putAll(
47+
mapOf(
48+
"DOCKER_REGISTRY" to properties["docker.registry"],
49+
"MOCK_SERVER_VERSION" to properties["mock-server.version"],
50+
)
51+
)
52+
}
53+
54+
tasks {
55+
val configureMockServer by registering(ConfigureMockServer::class)
56+
}
57+
58+
tasks.named("configureMockServer") {
59+
dependsOn("composeUp")
60+
}
61+
62+
dependencies {
63+
implementation("org.slf4j:slf4j-api:2.0.13")
64+
implementation("ch.qos.logback:logback-classic:1.3.14")
65+
implementation("com.github.ajalt.clikt:clikt:4.4.0")
66+
implementation("org.octopusden.octopus.octopus-external-systems-clients:artifactory-client:${properties["artifactory-client.version"]}")
67+
68+
testImplementation("org.junit.jupiter:junit-jupiter-api:${properties["junit.version"]}")
69+
testImplementation("org.junit.jupiter:junit-jupiter-params:${properties["junit.version"]}")
70+
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${properties["junit.version"]}")
71+
testImplementation("it.skrape:skrapeit:1.2.2")
72+
testImplementation("org.mock-server:mockserver-client-java:${properties["mock-server.version"]}")
73+
}
74+
75+
application {
76+
mainClass = "$group.ApplicationKt"
77+
}
78+
79+
tasks.jar {
80+
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
81+
manifest { attributes(mapOf("Main-Class" to application.mainClass)) }
82+
}
83+
84+
java {
85+
withJavadocJar()
86+
withSourcesJar()
87+
}
88+
89+
tasks.register<Zip>("zipMetarunners") {
90+
archiveFileName = "metarunners.zip"
91+
from(layout.projectDirectory.dir("metarunners")) {
92+
expand(properties)
93+
}
94+
}
95+
96+
configurations {
97+
create("distributions")
98+
}
99+
100+
val metarunners = artifacts.add(
101+
"distributions",
102+
layout.buildDirectory.file("distributions/metarunners.zip").get().asFile
103+
) {
104+
classifier = "metarunners"
105+
type = "zip"
106+
builtBy("zipMetarunners")
107+
}
108+
109+
nexusPublishing {
110+
repositories {
111+
sonatype {
112+
nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
113+
snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
114+
username.set(System.getenv("MAVEN_USERNAME"))
115+
password.set(System.getenv("MAVEN_PASSWORD"))
116+
}
117+
}
118+
transitionCheckOptions {
119+
maxRetries.set(60)
120+
delayBetween.set(Duration.ofSeconds(30))
121+
}
122+
}
123+
124+
publishing {
125+
publications {
126+
create<MavenPublication>("maven") {
127+
from(components["java"])
128+
artifact(metarunners)
129+
pom {
130+
name.set(project.name)
131+
description.set(project.description)
132+
url.set("https://github.com/octopusden/${project.name}.git")
133+
licenses {
134+
license {
135+
name.set("The Apache License, Version 2.0")
136+
url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
137+
}
138+
}
139+
scm {
140+
url.set("https://github.com/octopusden/${project.name}.git")
141+
connection.set("scm:git://github.com/octopusden/${project.name}.git")
142+
}
143+
developers {
144+
developer {
145+
id.set("octopus")
146+
name.set("octopus")
147+
}
148+
}
149+
}
150+
}
151+
}
152+
}
153+
154+
signing {
155+
isRequired = System.getenv().containsKey("ORG_GRADLE_PROJECT_signingKey") && System.getenv()
156+
.containsKey("ORG_GRADLE_PROJECT_signingPassword")
157+
val signingKey: String? by project
158+
val signingPassword: String? by project
159+
useInMemoryPgpKeys(signingKey, signingPassword)
160+
sign(publishing.publications["maven"])
161+
}
162+
163+
tasks.distZip.get().isEnabled = false
164+
tasks.shadowDistZip.get().isEnabled = false
165+
tasks.distTar.get().isEnabled = false
166+
tasks.shadowDistTar.get().isEnabled = false

‎buildSrc/build.gradle.kts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
plugins {
2+
kotlin("jvm") version "2.0.21"
3+
}
4+
5+
repositories {
6+
mavenCentral()
7+
}
8+
9+
dependencies {
10+
implementation(gradleApi())
11+
implementation("org.mock-server:mockserver-client-java:${properties["mock-server.version"]}")
12+
implementation("org.octopusden.octopus.octopus-external-systems-clients:artifactory-client:${properties["artifactory-client.version"]}")
13+
}

‎buildSrc/gradle.properties

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
mock-server.version=5.15.0
2+
artifactory-client.version=2.0.50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package org.octopusden.octopus.task
2+
3+
import com.fasterxml.jackson.core.type.TypeReference
4+
import com.fasterxml.jackson.databind.ObjectMapper
5+
import com.fasterxml.jackson.module.kotlin.KotlinModule
6+
import org.gradle.api.DefaultTask
7+
import org.gradle.api.tasks.TaskAction
8+
import org.mockserver.client.MockServerClient
9+
import org.mockserver.model.HttpRequest
10+
import org.mockserver.model.HttpResponse
11+
import org.mockserver.model.MediaType
12+
import org.octopusden.octopus.infrastructure.artifactory.client.dto.ArtifactoryResponse
13+
import org.octopusden.octopus.infrastructure.artifactory.client.dto.PromoteDockerImageRequest
14+
15+
16+
abstract class ConfigureMockServer : DefaultTask() {
17+
private val mockServerClient = MockServerClient("localhost", 1080)
18+
19+
@TaskAction
20+
fun configureMockServer() {
21+
mockServerClient.reset()
22+
val builds = load("builds.json", object : TypeReference<Map<String, Map<String, Any>>>() {})
23+
24+
mockServerClient.`when`(
25+
HttpRequest.request().withMethod("GET")
26+
.withPath("/artifactory/api/system/version")
27+
).respond {
28+
val version = load("version.json", object : TypeReference<Any>() {})
29+
HttpResponse.response().withContentType(MediaType.APPLICATION_JSON_UTF_8).withStatusCode(200)
30+
.withBody(mapper.writeValueAsString(version))
31+
}
32+
33+
mockServerClient.`when`(
34+
HttpRequest.request().withMethod("GET")
35+
.withPath("/artifactory/api/build/{buildName}/{buildNumber}")
36+
.withPathParameter("buildName")
37+
.withPathParameter("buildNumber")
38+
).respond {
39+
val buildName = it.getFirstPathParameter("buildName")
40+
val buildNumber = it.getFirstPathParameter("buildNumber")
41+
42+
builds[buildName]?.get(buildNumber)?.let { buildInfo ->
43+
HttpResponse.response().withContentType(MediaType.APPLICATION_JSON_UTF_8).withStatusCode(200)
44+
.withBody(mapper.writeValueAsString(buildInfo))
45+
} ?: run {
46+
val error = load("build-not-found-error.json", object : TypeReference<Any>() {})
47+
HttpResponse.response().withContentType(MediaType.APPLICATION_JSON_UTF_8).withStatusCode(404)
48+
.withBody(mapper.writeValueAsString(error))
49+
}
50+
}
51+
52+
mockServerClient.`when`(
53+
HttpRequest.request().withMethod("POST")
54+
.withPath("/artifactory/api/build/promote/{buildName}/{buildNumber}")
55+
.withPathParameter("buildName")
56+
.withPathParameter("buildNumber")
57+
).respond {
58+
val buildName = it.getFirstPathParameter("buildName")
59+
val buildNumber = it.getFirstPathParameter("buildNumber")
60+
61+
builds[buildName]?.get(buildNumber)?.let { buildInfo ->
62+
HttpResponse.response().withContentType(MediaType.APPLICATION_JSON_UTF_8).withStatusCode(200)
63+
.withBody(mapper.writeValueAsString(ArtifactoryResponse(emptyList())))
64+
} ?: run {
65+
val error = load("build-not-found-error.json", object : TypeReference<Any>() {})
66+
HttpResponse.response().withContentType(MediaType.APPLICATION_JSON_UTF_8).withStatusCode(404)
67+
.withBody(mapper.writeValueAsString(error))
68+
}
69+
}
70+
71+
val dockerRepositories =
72+
load("docker-repositories.json", object : TypeReference<Map<String, Map<String, Map<String, String?>>>>() {})
73+
74+
mockServerClient.`when`(
75+
HttpRequest.request().withMethod("POST")
76+
.withPath("/artifactory/api/docker/{repoKey}/v2/promote")
77+
.withPathParameter("repoKey")
78+
).respond {
79+
try {
80+
val repoKey = it.getFirstPathParameter("repoKey")
81+
val promoteDockerImageRequest =
82+
mapper.readValue(it.body.rawBytes, PromoteDockerImageRequest::class.java)
83+
val images =
84+
dockerRepositories[repoKey] ?: throw MockException(400, "docker-repository-not-found-error.json")
85+
val tags = images[promoteDockerImageRequest.dockerRepository] ?: throw MockException(404, "docker-image-not-found-error.json")
86+
val targetRepository = tags[promoteDockerImageRequest.tag] ?: throw MockException(404, "docker-image-not-found-error.json")
87+
if (targetRepository != promoteDockerImageRequest.targetRepo) {
88+
throw MockException(400, "docker-repository-not-found-error.json")
89+
}
90+
HttpResponse.response().withStatusCode(200)
91+
} catch (e: MockException) {
92+
val error = load(e.responseFileName, object : TypeReference<Any>() {})
93+
HttpResponse.response().withContentType(MediaType.APPLICATION_JSON_UTF_8).withStatusCode(e.code)
94+
.withBody(mapper.writeValueAsString(error))
95+
}
96+
}
97+
}
98+
99+
private class MockException(val code: Int, val responseFileName: String) : RuntimeException()
100+
101+
private fun <T> load(filename: String, typeReference: TypeReference<T>): T {
102+
val buildPath = project.rootDir.resolve("src").resolve("test").resolve("resources").resolve("mockserver")
103+
.resolve(filename)
104+
return mapper.readValue(buildPath, typeReference)
105+
}
106+
107+
companion object {
108+
private val mapper = with(ObjectMapper()) {
109+
registerModules(KotlinModule.Builder().build())
110+
this
111+
}
112+
}
113+
}

‎docker/docker-compose.yml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version: '3'
2+
3+
services:
4+
mockserver:
5+
container_name: artifactory-mockserver
6+
image: ${DOCKER_REGISTRY}/mockserver/mockserver:mockserver-${MOCK_SERVER_VERSION}
7+
ports:
8+
- "1080:1080"

‎gradle.properties

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
version=2.0-SNAPSHOT
2+
artifactory-client.version=2.0.50
3+
junit.version=5.9.2
4+
mock-server.version=5.15.0
5+
docker.registry=

‎gradle/libs.versions.toml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# This file was generated by the Gradle 'init' task.
2+
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format

‎gradle/wrapper/gradle-wrapper.jar

42.4 KB
Binary file not shown.
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
distributionBase=GRADLE_USER_HOME
2+
distributionPath=wrapper/dists
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
4+
networkTimeout=10000
5+
validateDistributionUrl=true
6+
zipStoreBase=GRADLE_USER_HOME
7+
zipStorePath=wrapper/dists

‎gradlew

+249
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
#!/bin/sh
2+
3+
#
4+
# Copyright © 2015-2021 the original authors.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# https://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
##############################################################################
20+
#
21+
# Gradle start up script for POSIX generated by Gradle.
22+
#
23+
# Important for running:
24+
#
25+
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26+
# noncompliant, but you have some other compliant shell such as ksh or
27+
# bash, then to run this script, type that shell name before the whole
28+
# command line, like:
29+
#
30+
# ksh Gradle
31+
#
32+
# Busybox and similar reduced shells will NOT work, because this script
33+
# requires all of these POSIX shell features:
34+
# * functions;
35+
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36+
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37+
# * compound commands having a testable exit status, especially «case»;
38+
# * various built-in commands including «command», «set», and «ulimit».
39+
#
40+
# Important for patching:
41+
#
42+
# (2) This script targets any POSIX shell, so it avoids extensions provided
43+
# by Bash, Ksh, etc; in particular arrays are avoided.
44+
#
45+
# The "traditional" practice of packing multiple parameters into a
46+
# space-separated string is a well documented source of bugs and security
47+
# problems, so this is (mostly) avoided, by progressively accumulating
48+
# options in "$@", and eventually passing that to Java.
49+
#
50+
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51+
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52+
# see the in-line comments for details.
53+
#
54+
# There are tweaks for specific operating systems such as AIX, CygWin,
55+
# Darwin, MinGW, and NonStop.
56+
#
57+
# (3) This script is generated from the Groovy template
58+
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59+
# within the Gradle project.
60+
#
61+
# You can find Gradle at https://github.com/gradle/gradle/.
62+
#
63+
##############################################################################
64+
65+
# Attempt to set APP_HOME
66+
67+
# Resolve links: $0 may be a link
68+
app_path=$0
69+
70+
# Need this for daisy-chained symlinks.
71+
while
72+
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73+
[ -h "$app_path" ]
74+
do
75+
ls=$( ls -ld "$app_path" )
76+
link=${ls#*' -> '}
77+
case $link in #(
78+
/*) app_path=$link ;; #(
79+
*) app_path=$APP_HOME$link ;;
80+
esac
81+
done
82+
83+
# This is normally unused
84+
# shellcheck disable=SC2034
85+
APP_BASE_NAME=${0##*/}
86+
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
87+
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
88+
89+
# Use the maximum available, or set MAX_FD != -1 to use that value.
90+
MAX_FD=maximum
91+
92+
warn () {
93+
echo "$*"
94+
} >&2
95+
96+
die () {
97+
echo
98+
echo "$*"
99+
echo
100+
exit 1
101+
} >&2
102+
103+
# OS specific support (must be 'true' or 'false').
104+
cygwin=false
105+
msys=false
106+
darwin=false
107+
nonstop=false
108+
case "$( uname )" in #(
109+
CYGWIN* ) cygwin=true ;; #(
110+
Darwin* ) darwin=true ;; #(
111+
MSYS* | MINGW* ) msys=true ;; #(
112+
NONSTOP* ) nonstop=true ;;
113+
esac
114+
115+
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
116+
117+
118+
# Determine the Java command to use to start the JVM.
119+
if [ -n "$JAVA_HOME" ] ; then
120+
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
121+
# IBM's JDK on AIX uses strange locations for the executables
122+
JAVACMD=$JAVA_HOME/jre/sh/java
123+
else
124+
JAVACMD=$JAVA_HOME/bin/java
125+
fi
126+
if [ ! -x "$JAVACMD" ] ; then
127+
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
128+
129+
Please set the JAVA_HOME variable in your environment to match the
130+
location of your Java installation."
131+
fi
132+
else
133+
JAVACMD=java
134+
if ! command -v java >/dev/null 2>&1
135+
then
136+
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137+
138+
Please set the JAVA_HOME variable in your environment to match the
139+
location of your Java installation."
140+
fi
141+
fi
142+
143+
# Increase the maximum file descriptors if we can.
144+
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
145+
case $MAX_FD in #(
146+
max*)
147+
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
148+
# shellcheck disable=SC2039,SC3045
149+
MAX_FD=$( ulimit -H -n ) ||
150+
warn "Could not query maximum file descriptor limit"
151+
esac
152+
case $MAX_FD in #(
153+
'' | soft) :;; #(
154+
*)
155+
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
156+
# shellcheck disable=SC2039,SC3045
157+
ulimit -n "$MAX_FD" ||
158+
warn "Could not set maximum file descriptor limit to $MAX_FD"
159+
esac
160+
fi
161+
162+
# Collect all arguments for the java command, stacking in reverse order:
163+
# * args from the command line
164+
# * the main class name
165+
# * -classpath
166+
# * -D...appname settings
167+
# * --module-path (only if needed)
168+
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
169+
170+
# For Cygwin or MSYS, switch paths to Windows format before running java
171+
if "$cygwin" || "$msys" ; then
172+
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
173+
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
174+
175+
JAVACMD=$( cygpath --unix "$JAVACMD" )
176+
177+
# Now convert the arguments - kludge to limit ourselves to /bin/sh
178+
for arg do
179+
if
180+
case $arg in #(
181+
-*) false ;; # don't mess with options #(
182+
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183+
[ -e "$t" ] ;; #(
184+
*) false ;;
185+
esac
186+
then
187+
arg=$( cygpath --path --ignore --mixed "$arg" )
188+
fi
189+
# Roll the args list around exactly as many times as the number of
190+
# args, so each arg winds up back in the position where it started, but
191+
# possibly modified.
192+
#
193+
# NB: a `for` loop captures its iteration list before it begins, so
194+
# changing the positional parameters here affects neither the number of
195+
# iterations, nor the values presented in `arg`.
196+
shift # remove old arg
197+
set -- "$@" "$arg" # push replacement arg
198+
done
199+
fi
200+
201+
202+
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203+
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204+
205+
# Collect all arguments for the java command:
206+
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207+
# and any embedded shellness will be escaped.
208+
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209+
# treated as '${Hostname}' itself on the command line.
210+
211+
set -- \
212+
"-Dorg.gradle.appname=$APP_BASE_NAME" \
213+
-classpath "$CLASSPATH" \
214+
org.gradle.wrapper.GradleWrapperMain \
215+
"$@"
216+
217+
# Stop when "xargs" is not available.
218+
if ! command -v xargs >/dev/null 2>&1
219+
then
220+
die "xargs is not available"
221+
fi
222+
223+
# Use "xargs" to parse quoted args.
224+
#
225+
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
226+
#
227+
# In Bash we could simply go:
228+
#
229+
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
230+
# set -- "${ARGS[@]}" "$@"
231+
#
232+
# but POSIX shell has neither arrays nor command substitution, so instead we
233+
# post-process each arg (as a line of input to sed) to backslash-escape any
234+
# character that might be a shell metacharacter, then use eval to reverse
235+
# that process (while maintaining the separation between arguments), and wrap
236+
# the whole thing up as a single "set" statement.
237+
#
238+
# This will of course break if any of these variables contains a newline or
239+
# an unmatched quote.
240+
#
241+
242+
eval "set -- $(
243+
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
244+
xargs -n1 |
245+
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
246+
tr '\n' ' '
247+
)" '"$@"'
248+
249+
exec "$JAVACMD" "$@"

‎gradlew.bat

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
@rem
2+
@rem Copyright 2015 the original author or authors.
3+
@rem
4+
@rem Licensed under the Apache License, Version 2.0 (the "License");
5+
@rem you may not use this file except in compliance with the License.
6+
@rem You may obtain a copy of the License at
7+
@rem
8+
@rem https://www.apache.org/licenses/LICENSE-2.0
9+
@rem
10+
@rem Unless required by applicable law or agreed to in writing, software
11+
@rem distributed under the License is distributed on an "AS IS" BASIS,
12+
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
@rem See the License for the specific language governing permissions and
14+
@rem limitations under the License.
15+
@rem
16+
17+
@if "%DEBUG%"=="" @echo off
18+
@rem ##########################################################################
19+
@rem
20+
@rem Gradle startup script for Windows
21+
@rem
22+
@rem ##########################################################################
23+
24+
@rem Set local scope for the variables with windows NT shell
25+
if "%OS%"=="Windows_NT" setlocal
26+
27+
set DIRNAME=%~dp0
28+
if "%DIRNAME%"=="" set DIRNAME=.
29+
@rem This is normally unused
30+
set APP_BASE_NAME=%~n0
31+
set APP_HOME=%DIRNAME%
32+
33+
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
34+
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35+
36+
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37+
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38+
39+
@rem Find java.exe
40+
if defined JAVA_HOME goto findJavaFromJavaHome
41+
42+
set JAVA_EXE=java.exe
43+
%JAVA_EXE% -version >NUL 2>&1
44+
if %ERRORLEVEL% equ 0 goto execute
45+
46+
echo. 1>&2
47+
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
48+
echo. 1>&2
49+
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
50+
echo location of your Java installation. 1>&2
51+
52+
goto fail
53+
54+
:findJavaFromJavaHome
55+
set JAVA_HOME=%JAVA_HOME:"=%
56+
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57+
58+
if exist "%JAVA_EXE%" goto execute
59+
60+
echo. 1>&2
61+
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
62+
echo. 1>&2
63+
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
64+
echo location of your Java installation. 1>&2
65+
66+
goto fail
67+
68+
:execute
69+
@rem Setup the command line
70+
71+
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72+
73+
74+
@rem Execute Gradle
75+
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76+
77+
:end
78+
@rem End local scope for the variables with windows NT shell
79+
if %ERRORLEVEL% equ 0 goto mainEnd
80+
81+
:fail
82+
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83+
rem the _cmd.exe /c_ return code!
84+
set EXIT_CODE=%ERRORLEVEL%
85+
if %EXIT_CODE% equ 0 set EXIT_CODE=1
86+
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87+
exit /b %EXIT_CODE%
88+
89+
:mainEnd
90+
if "%OS%"=="Windows_NT" endlocal
91+
92+
:omega
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<meta-runner name="Promote Artifactory Build">
3+
<description>Promote Artifactory build</description>
4+
<settings>
5+
<parameters>
6+
<param name="ARTIFACTORY_BUILD_NAME" value="%ARTIFACTORY_BUILD_NAME%"
7+
spec="text description='Artifactory Build Name to promote.' validationMode='not_empty' display='normal'"/>
8+
<param name="ARTIFACTORY_BUILD_NUMBER" value="%BUILD_VERSION%"
9+
spec="text description='Artifactory Build Number to promote.' validationMode='not_empty' display='normal'"/>
10+
<param name="ARTIFACTORY_TARGET_STATUS" value="release"
11+
spec="text description='Artifactory status to promote build to.' validationMode='not_empty' display='normal'"/>
12+
<param name="ARTIFACTORY_TARGET_REPOSITORY" value=""
13+
spec="text description='Artifactory repository to promote build to.' validationMode='not_empty' display='normal'"/>
14+
<param name="ARTIFACTORY_IGNORE_NOT_FOUND" value="true"
15+
spec="checkbox description='Exit silently if build is not found' uncheckedValue='false' checkedValue='true' display='normal'" />
16+
<param name="ARTIFACTORY_FORCE_PROMOTE" value="false"
17+
spec="checkbox description='Do promotion whether or not build has target status assigned already' uncheckedValue='false' checkedValue='true' display='normal'" />
18+
</parameters>
19+
<build-runners>
20+
<runner name="promote build" type="OctopusTeamcityAutomation">
21+
<parameters>
22+
<param name="ARGS"
23+
value="--url=%ARTIFACTORY_URL% --user=%ARTIFACTORY_USER% --password=%ARTIFACTORY_PASSWORD% promote-build --build-name=%ARTIFACTORY_BUILD_NAME% --build-number=%ARTIFACTORY_BUILD_NUMBER% --target-repository=%ARTIFACTORY_TARGET_REPOSITORY% --target-status=%ARTIFACTORY_TARGET_STATUS% --ignore-not-found=%ARTIFACTORY_IGNORE_NOT_FOUND% --force=%ARTIFACTORY_FORCE_PROMOTE%"/>
24+
</parameters>
25+
</runner>
26+
</build-runners>
27+
<requirements/>
28+
</settings>
29+
</meta-runner>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<meta-runner name="Promote Artifactory Docker Image">
3+
<description>Promote Artifactory Docker image</description>
4+
<settings>
5+
<parameters>
6+
<param name="ARTIFACTORY_DOCKER_SOURCE_REPOSITORY" value="%DOCKER_REPO_SOURCE%"
7+
spec="text description='Artifactory image source repository' validationMode='not_empty' display='normal'"/>
8+
<param name="ARTIFACTORY_DOCKER_TARGET_REPOSITORY" value="%DOCKER_REPO_RELEASE%"
9+
spec="text description='Artifactory image source repository' validationMode='not_empty' display='normal'"/>
10+
<param name="ARTIFACTORY_DOCKER_IMAGE" value="%ARTIFACTORY_DOCKER_IMAGE%"
11+
spec="text description='Artifactory image name promote' validationMode='not_empty' display='normal'"/>
12+
<param name="ARTIFACTORY_DOCKER_TAG" value="%ARTIFACTORY_DOCKER_TAG%"
13+
spec="text description='Artifactory image tag promote' validationMode='not_empty' display='normal'"/>
14+
<param name="ARTIFACTORY_IGNORE_NOT_FOUND" value="true"
15+
spec="checkbox description='Exit silently if build is not found' uncheckedValue='false' checkedValue='true' display='normal'" />
16+
</parameters>
17+
<build-runners>
18+
<runner name="promote docker image" type="OctopusTeamcityAutomation">
19+
<parameters>
20+
<param name="ARGS"
21+
value="--url=%ARTIFACTORY_URL% --user=%ARTIFACTORY_USER% --password=%ARTIFACTORY_PASSWORD% promote-docker-image --source-repository=%ARTIFACTORY_DOCKER_SOURCE_REPOSITORY% --target-repository=%ARTIFACTORY_DOCKER_TARGET_REPOSITORY% --image=%ARTIFACTORY_DOCKER_IMAGE% --tag=%ARTIFACTORY_DOCKER_TAG% --ignore-not-found=%ARTIFACTORY_IGNORE_NOT_FOUND%"/>
22+
</parameters>
23+
</runner>
24+
</build-runners>
25+
<requirements/>
26+
</settings>
27+
</meta-runner>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<meta-runner name="Push and publish Artifactory Docker Image">
2+
<description>Create and publish Artifactory build and push docker image to it</description>
3+
<settings>
4+
<parameters>
5+
<param name="DOCKER_REGISTRY" value="%DOCKER_REGISTRY%" />
6+
<param name="ARTIFACTORY_DOCKER_IMAGE" value="%DISTRIBUTION_DOCKER_IMAGE_NAME%" />
7+
<param name="ARTIFACTORY_DOCKER_BUILD_NUMBER" value="%BUILD_VERSION%" />
8+
<param name="ARTIFACTORY_DOCKER_REPOSITORY" value="%DOCKER_REPO_DEV%" />
9+
<param name="ARTIFACTORY_DOCKER_BUILD_NAME" value="%ARTIFACTORY_DOCKER_BUILD_NAME%" />
10+
</parameters>
11+
<build-runners>
12+
<runner name="Push Image (Docker)" type="simpleRunner">
13+
<conditions>
14+
<does-not-equal name="DISTRIBUTION_DOCKER_IMAGE_NAME" value="" />
15+
<equals name="container.engine" value="docker" />
16+
</conditions>
17+
<parameters>
18+
<param name="org.jfrog.artifactory.selectedDeployableServer.downloadSpecSource" value="Job configuration" />
19+
<param name="org.jfrog.artifactory.selectedDeployableServer.uploadSpecSource" value="Job configuration" />
20+
<param name="org.jfrog.artifactory.selectedDeployableServer.useSpecs" value="false" />
21+
<param name="script.content"><![CDATA[jfrog rt docker-push %DOCKER_REGISTRY%/%ARTIFACTORY_DOCKER_IMAGE%:%ARTIFACTORY_DOCKER_BUILD_NUMBER% %ARTIFACTORY_DOCKER_REPOSITORY% --build-name=%ARTIFACTORY_DOCKER_BUILD_NAME% --build-number=%ARTIFACTORY_DOCKER_BUILD_NUMBER%]]></param>
22+
<param name="teamcity.step.mode" value="default" />
23+
<param name="use.custom.script" value="true" />
24+
</parameters>
25+
</runner>
26+
<runner name="Push Image (Podman)" type="simpleRunner">
27+
<conditions>
28+
<does-not-equal name="DISTRIBUTION_DOCKER_IMAGE_NAME" value="" />
29+
<equals name="container.engine" value="podman" />
30+
</conditions>
31+
<parameters>
32+
<param name="org.jfrog.artifactory.selectedDeployableServer.downloadSpecSource" value="Job configuration" />
33+
<param name="org.jfrog.artifactory.selectedDeployableServer.uploadSpecSource" value="Job configuration" />
34+
<param name="org.jfrog.artifactory.selectedDeployableServer.useSpecs" value="false" />
35+
<param name="script.content"><![CDATA[jfrog rt podman-push %DOCKER_REGISTRY%/%ARTIFACTORY_DOCKER_IMAGE%:%ARTIFACTORY_DOCKER_BUILD_NUMBER% %ARTIFACTORY_DOCKER_REPOSITORY% --build-name=%ARTIFACTORY_DOCKER_BUILD_NAME% --build-number=%ARTIFACTORY_DOCKER_BUILD_NUMBER%]]></param>
36+
<param name="teamcity.step.mode" value="default" />
37+
<param name="use.custom.script" value="true" />
38+
</parameters>
39+
</runner>
40+
<runner name="Publish Artifactory Build" type="simpleRunner">
41+
<conditions>
42+
<does-not-equal name="DISTRIBUTION_DOCKER_IMAGE_NAME" value="" />
43+
</conditions>
44+
<parameters>
45+
<param name="org.jfrog.artifactory.selectedDeployableServer.downloadSpecSource" value="Job configuration" />
46+
<param name="org.jfrog.artifactory.selectedDeployableServer.uploadSpecSource" value="Job configuration" />
47+
<param name="org.jfrog.artifactory.selectedDeployableServer.useSpecs" value="false" />
48+
<param name="script.content"><![CDATA[jfrog rt bp %ARTIFACTORY_DOCKER_BUILD_NAME% %ARTIFACTORY_DOCKER_BUILD_NUMBER%]]></param>
49+
<param name="teamcity.step.mode" value="default" />
50+
<param name="use.custom.script" value="true" />
51+
</parameters>
52+
</runner>
53+
</build-runners>
54+
<requirements />
55+
</settings>
56+
</meta-runner>

‎settings.gradle.kts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
pluginManagement {
2+
plugins {
3+
id("org.jetbrains.kotlin.jvm") version ("2.0.21")
4+
id("com.github.johnrengelman.shadow") version ("8.1.1")
5+
id("com.avast.gradle.docker-compose") version ("0.16.9")
6+
id("io.github.gradle-nexus.publish-plugin") version ("1.1.0")
7+
}
8+
}
9+
10+
rootProject.name = "octopus-artifactory-automation"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.octopusden.octopus.automation.artifactory
2+
3+
import com.github.ajalt.clikt.core.subcommands
4+
5+
const val SPLIT_SYMBOLS = "[,;]"
6+
7+
fun main(args: Array<String>) {
8+
ArtifactoryCommand().subcommands(
9+
ArtifactoryPromoteBuild(),
10+
ArtifactoryPromoteDockerImage()
11+
).main(args)
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package org.octopusden.octopus.automation.artifactory
2+
3+
import com.github.ajalt.clikt.core.CliktCommand
4+
import com.github.ajalt.clikt.core.findOrSetObject
5+
import com.github.ajalt.clikt.parameters.options.check
6+
import com.github.ajalt.clikt.parameters.options.convert
7+
import com.github.ajalt.clikt.parameters.options.option
8+
import com.github.ajalt.clikt.parameters.options.required
9+
import com.github.ajalt.clikt.parameters.options.validate
10+
import org.octopusden.octopus.infrastructure.artifactory.client.ArtifactoryClassicClient
11+
import org.octopusden.octopus.infrastructure.artifactory.client.ArtifactoryClient
12+
import org.octopusden.octopus.infrastructure.client.commons.ClientParametersProvider
13+
import org.octopusden.octopus.infrastructure.client.commons.CredentialProvider
14+
import org.octopusden.octopus.infrastructure.client.commons.StandardBasicCredCredentialProvider
15+
import org.octopusden.octopus.infrastructure.client.commons.StandardBearerTokenCredentialProvider
16+
import org.slf4j.LoggerFactory
17+
18+
class ArtifactoryCommand : CliktCommand(name = "") {
19+
private val url by option(URL_OPTION, help = "Artifactory URL").convert { it.trim() }.required()
20+
.check("$URL_OPTION is empty") { it.isNotEmpty() }
21+
private val user by option(USER_OPTION, help = "Artifactory user").convert { it.trim() }
22+
.check("$USER_OPTION is empty") { it.isNotEmpty() }
23+
private val password by option(PASSWORD_OPTION, help = "Artifactory password").convert { it.trim() }.validate {
24+
require(it.isNotBlank() && user?.isNotBlank() == true || user?.isBlank() == true) {
25+
"Password must not be blank with basic authentication"
26+
}
27+
}
28+
private val token by option(TOKEN_OPTION, help = "Artifactory token").convert { it.trim() }.validate {
29+
/*require(user?.isBlank() == false && password?.isBlank() == false || it.isNotBlank()) {
30+
"Either $TOKEN_OPTION or $USER_OPTION/$PASSWORD_OPTION must be set"
31+
}*/
32+
}
33+
34+
private val context by findOrSetObject { mutableMapOf<String, Any>() }
35+
36+
override fun run() {
37+
val log = LoggerFactory.getLogger(ArtifactoryCommand::class.java.`package`.name)
38+
val client: ArtifactoryClient = ArtifactoryClassicClient(object : ClientParametersProvider {
39+
override fun getApiUrl() = url
40+
override fun getAuth(): CredentialProvider =
41+
token?.let { t -> StandardBearerTokenCredentialProvider(t) }
42+
?: user?.let { u -> password?.let { p -> StandardBasicCredCredentialProvider(u, p) } }
43+
?: throw IllegalArgumentException("Artifactory credentials not found")
44+
})
45+
46+
val resultUser = token?.let { client.getTokens().tokens.map { t -> t.subject.substringAfterLast("/") } }
47+
?.firstOrNull { it.isNotBlank() }
48+
?: user
49+
?: throw IllegalStateException("Artifactory user not found")
50+
51+
val version = client.getVersion()
52+
log.info("Artifactory server: ${version.license}:${version.version}")
53+
context[LOG] = log
54+
context[CLIENT] = client
55+
context[USERNAME] = resultUser
56+
}
57+
58+
companion object {
59+
const val URL_OPTION = "--url"
60+
const val USER_OPTION = "--user"
61+
const val PASSWORD_OPTION = "--password"
62+
const val TOKEN_OPTION = "--token"
63+
const val LOG = "log"
64+
const val CLIENT = "client"
65+
const val USERNAME = "username"
66+
private const val AUTH_NO_CREDENTIAL_ERROR_MESSAGE = "Either username/password or token must be set"
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package org.octopusden.octopus.automation.artifactory
2+
3+
import com.github.ajalt.clikt.core.CliktCommand
4+
import com.github.ajalt.clikt.core.requireObject
5+
import com.github.ajalt.clikt.parameters.options.check
6+
import com.github.ajalt.clikt.parameters.options.convert
7+
import com.github.ajalt.clikt.parameters.options.default
8+
import com.github.ajalt.clikt.parameters.options.option
9+
import com.github.ajalt.clikt.parameters.options.required
10+
import org.octopusden.octopus.infrastructure.artifactory.client.ArtifactoryClient
11+
import org.octopusden.octopus.infrastructure.artifactory.client.dto.BuildInfo
12+
import org.octopusden.octopus.infrastructure.artifactory.client.dto.PromoteBuildRequest
13+
import org.slf4j.Logger
14+
15+
class ArtifactoryPromoteBuild : CliktCommand(name = COMMAND) {
16+
private val context by requireObject<MutableMap<String, Any>>()
17+
18+
private val buildName by option(BUILD_NAME, help = "Artifactory build name")
19+
.convert { it.trim() }.required()
20+
.check("$BUILD_NAME is empty") { it.isNotEmpty() }
21+
22+
private val buildNumber by option(BUILD_NUMBER, help = "Artifactory build version")
23+
.convert { it.trim() }.required()
24+
.check("$BUILD_NUMBER is empty") { it.isNotEmpty() }
25+
26+
private val targetRepository by option(TARGET_REPOSITORY, help = "Target Artifactory repository")
27+
.convert { it.trim() }.required()
28+
.check("$TARGET_REPOSITORY is empty") { it.isNotEmpty() }
29+
30+
private val targetStatus by option(TARGET_STATUS, help = "Target promotion status (e.g. 'release')")
31+
.convert { it.trim() }.required()
32+
.check("$TARGET_STATUS is empty") { it.isNotEmpty() }
33+
34+
private val ignoreNotFound by option(IGNORE_NOT_FOUND, help = "Ignore errors when build is not found")
35+
.convert { it.trim().toBoolean() }.default(true)
36+
37+
private val force by option(FORCE, help = "Force promotion").convert { it.trim().toBoolean() }
38+
.default(false)
39+
40+
private val client by lazy { context[ArtifactoryCommand.CLIENT] as ArtifactoryClient }
41+
private val log by lazy { context[ArtifactoryCommand.LOG] as Logger }
42+
private val username by lazy { context[ArtifactoryCommand.USERNAME] as String }
43+
44+
override fun run() {
45+
log.info("Promote Artifactory build: '$buildName:$buildNumber', to repository: '$targetRepository', target status: '$targetStatus'")
46+
promoteBuild()
47+
}
48+
49+
private fun promoteBuild() {
50+
getBuildInfo(buildName, buildNumber)?.let { buildInfo ->
51+
promote(buildInfo, targetRepository, targetStatus, force)
52+
}
53+
}
54+
55+
private fun getBuildInfo(buildName: String, buildNumber: String): BuildInfo? {
56+
return Util.handleNotFoundException(ignoreNotFound) {
57+
client.getBuildInfo(buildName, buildNumber).buildInfo.also { buildInfo ->
58+
if (buildInfo.modules.isNullOrEmpty()) {
59+
log.warn("The build $buildName:$buildNumber found but has no modules. Check creating and publishing build artifacts.")
60+
}
61+
}
62+
}
63+
}
64+
65+
private fun promote(buildInfo: BuildInfo, targetRepository: String, targetStatus: String, forcePromote: Boolean) {
66+
if (forcePromote || (buildInfo.statuses?.find { it.status == targetStatus } == null)) {
67+
val promote = PromoteBuildRequest(username, targetRepository, targetStatus)
68+
client.promoteBuild(buildInfo.name, buildInfo.number, promote).also {
69+
it.messages.joinToString { artifactoryMessage -> artifactoryMessage.message }.let { message ->
70+
log.info("Build $buildName:$buildNumber promoted with message: $message")
71+
}
72+
}
73+
} else {
74+
log.info("Build $buildName:$buildNumber already promoted to $targetStatus")
75+
}
76+
}
77+
78+
companion object {
79+
const val COMMAND = "promote-build"
80+
const val BUILD_NAME = "--build-name"
81+
const val BUILD_NUMBER = "--build-number"
82+
const val TARGET_REPOSITORY = "--target-repository"
83+
const val TARGET_STATUS = "--target-status"
84+
const val IGNORE_NOT_FOUND = "--ignore-not-found"
85+
const val FORCE = "--force"
86+
}
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package org.octopusden.octopus.automation.artifactory
2+
3+
import com.github.ajalt.clikt.core.CliktCommand
4+
import com.github.ajalt.clikt.core.requireObject
5+
import com.github.ajalt.clikt.parameters.options.check
6+
import com.github.ajalt.clikt.parameters.options.convert
7+
import com.github.ajalt.clikt.parameters.options.default
8+
import com.github.ajalt.clikt.parameters.options.option
9+
import com.github.ajalt.clikt.parameters.options.required
10+
import org.octopusden.octopus.infrastructure.artifactory.client.ArtifactoryClient
11+
import org.octopusden.octopus.infrastructure.artifactory.client.dto.PromoteDockerImageRequest
12+
import org.slf4j.Logger
13+
14+
class ArtifactoryPromoteDockerImage : CliktCommand(name = COMMAND) {
15+
private val context by requireObject<MutableMap<String, Any>>()
16+
17+
private val sourceRepository by option(SOURCE_REPOSITORY, help = "Source Artifactory docker repository key")
18+
.convert { it.trim() }.required()
19+
.check("$SOURCE_REPOSITORY is empty") { it.isNotEmpty() }
20+
21+
private val targetRepository by option(TARGET_REPOSITORY, help = "Target Artifactory docker repository key")
22+
.convert { it.trim() }.required()
23+
.check("$TARGET_REPOSITORY is empty") { it.isNotEmpty() }
24+
25+
private val image by option(IMAGE, help = "Docker image")
26+
.convert { it.trim() }.required()
27+
.check("$IMAGE is empty") { it.isNotEmpty() }
28+
29+
private val tag by option(TAG, help = "Artifactory build version")
30+
.convert { it.trim() }.required()
31+
.check("$TAG is empty") { it.isNotEmpty() }
32+
33+
private val ignoreNotFound by option(IGNORE_NOT_FOUND, help = "Ignore errors when build is not found")
34+
.convert { it.trim().toBoolean() }.default(false)
35+
36+
private val client by lazy { context[ArtifactoryCommand.CLIENT] as ArtifactoryClient }
37+
private val log by lazy { context[ArtifactoryCommand.LOG] as Logger }
38+
39+
override fun run() {
40+
log.info("Promote Docker image: '$image:$tag' to repository: '$targetRepository'")
41+
promoteDockerImage()
42+
}
43+
44+
private fun promoteDockerImage() {
45+
Util.handleNotFoundException(ignoreNotFound) {
46+
client.promoteDockerImage(
47+
sourceRepository,
48+
PromoteDockerImageRequest(image, tag, targetRepository)
49+
)
50+
}
51+
}
52+
53+
companion object {
54+
const val COMMAND = "promote-docker-image"
55+
const val SOURCE_REPOSITORY = "--source-repository"
56+
const val TARGET_REPOSITORY = "--target-repository"
57+
const val IMAGE = "--image"
58+
const val TAG = "--tag"
59+
const val IGNORE_NOT_FOUND = "--ignore-not-found"
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.octopusden.octopus.automation.artifactory
2+
3+
import org.octopusden.octopus.infrastructure.artifactory.client.exception.NotFoundException
4+
5+
class Util private constructor() {
6+
companion object {
7+
fun <T> handleNotFoundException(ignore: Boolean, function: () -> T): T? {
8+
return try {
9+
function()
10+
} catch (e: NotFoundException) {
11+
if (!ignore) {
12+
throw e
13+
}
14+
null
15+
}
16+
}
17+
}
18+
}

‎src/main/resources/logback.xml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<configuration>
3+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
4+
<encoder>
5+
<pattern>%-5level - %msg%n</pattern>
6+
</encoder>
7+
</appender>
8+
9+
<logger name="org.octopusden.octopus.automation" level="INFO"/>
10+
11+
<root level="INFO">
12+
<appender-ref ref="STDOUT" />
13+
</root>
14+
</configuration>

‎src/test/kotlin/ApplicationTest.kt

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import java.io.File
2+
import java.util.stream.Stream
3+
import org.junit.jupiter.api.Assertions
4+
import org.junit.jupiter.params.ParameterizedTest
5+
import org.junit.jupiter.params.provider.Arguments
6+
import org.junit.jupiter.params.provider.MethodSource
7+
import org.octopusden.octopus.automation.artifactory.ArtifactoryCommand
8+
import org.octopusden.octopus.automation.artifactory.ArtifactoryPromoteBuild
9+
import org.octopusden.octopus.automation.artifactory.ArtifactoryPromoteDockerImage
10+
11+
12+
class ApplicationTest {
13+
private val jar = System.getProperty("jar") ?: throw IllegalStateException("System property 'jar' must be provided")
14+
15+
@ParameterizedTest
16+
@MethodSource("commands")
17+
fun parametrizedTest(name: String, expectedExitCode: Int, command: Array<String>) {
18+
Assertions.assertEquals(expectedExitCode, execute(name, *ARTIFACTORY_OPTIONS, *command))
19+
}
20+
21+
private fun execute(name: String, vararg command: String) =
22+
ProcessBuilder("java", "-jar", jar, *command)
23+
.redirectErrorStream(true)
24+
.redirectOutput(
25+
File("").resolve("build").resolve("logs").resolve("$name.log").also { it.parentFile.mkdirs() })
26+
.start()
27+
.waitFor()
28+
29+
companion object {
30+
const val HELP_OPTION = "-h"
31+
32+
const val ARTIFACTORY_URL = "http://localhost:1080"
33+
const val ARTIFACTORY_USER = "admin"
34+
const val ARTIFACTORY_PASSWORD = "password"
35+
36+
val ARTIFACTORY_OPTIONS = arrayOf(
37+
"${ArtifactoryCommand.URL_OPTION}=$ARTIFACTORY_URL",
38+
"${ArtifactoryCommand.USER_OPTION}=$ARTIFACTORY_USER",
39+
"${ArtifactoryCommand.PASSWORD_OPTION}=$ARTIFACTORY_PASSWORD"
40+
)
41+
42+
//<editor-fold defaultstate="collapsed" desc="Test Data">
43+
@JvmStatic
44+
private fun commands(): Stream<Arguments> = Stream.of(
45+
46+
Arguments.of(
47+
"Test help", 0, arrayOf(HELP_OPTION)
48+
),
49+
Arguments.of(
50+
"Test promote-build help", 0, arrayOf(ArtifactoryPromoteBuild.COMMAND, HELP_OPTION)
51+
),
52+
Arguments.of(
53+
"Test promote-build", 0, arrayOf(
54+
ArtifactoryPromoteBuild.COMMAND,
55+
"${ArtifactoryPromoteBuild.BUILD_NAME}=existed-build-name",
56+
"${ArtifactoryPromoteBuild.BUILD_NUMBER}=existed-build-number1",
57+
"${ArtifactoryPromoteBuild.TARGET_REPOSITORY}=release",
58+
"${ArtifactoryPromoteBuild.TARGET_STATUS}=release",
59+
"${ArtifactoryPromoteBuild.IGNORE_NOT_FOUND}=false",
60+
)
61+
),
62+
Arguments.of(
63+
"Test promote-build with not existed build", 1, arrayOf(
64+
ArtifactoryPromoteBuild.COMMAND,
65+
"${ArtifactoryPromoteBuild.BUILD_NAME}=not-existed-build-name",
66+
"${ArtifactoryPromoteBuild.BUILD_NUMBER}=not-existed-build-version",
67+
"${ArtifactoryPromoteBuild.TARGET_REPOSITORY}=release",
68+
"${ArtifactoryPromoteBuild.TARGET_STATUS}=release",
69+
"${ArtifactoryPromoteBuild.IGNORE_NOT_FOUND}=false",
70+
)
71+
),
72+
Arguments.of(
73+
"Test promote-build with not existed build with ignore-not-found", 0, arrayOf(
74+
ArtifactoryPromoteBuild.COMMAND,
75+
"${ArtifactoryPromoteBuild.BUILD_NAME}=not-existed-build-name",
76+
"${ArtifactoryPromoteBuild.BUILD_NUMBER}=not-existed-build-version",
77+
"${ArtifactoryPromoteBuild.TARGET_REPOSITORY}=release",
78+
"${ArtifactoryPromoteBuild.TARGET_STATUS}=release",
79+
"${ArtifactoryPromoteBuild.IGNORE_NOT_FOUND}=true"
80+
)
81+
),
82+
Arguments.of(
83+
"Test promote-docker-image help", 0, arrayOf(ArtifactoryPromoteDockerImage.COMMAND, HELP_OPTION)
84+
),
85+
Arguments.of(
86+
"Test promote-docker-image", 0, arrayOf(
87+
ArtifactoryPromoteDockerImage.COMMAND,
88+
"${ArtifactoryPromoteDockerImage.SOURCE_REPOSITORY}=docker-dev-repository",
89+
"${ArtifactoryPromoteDockerImage.TARGET_REPOSITORY}=docker-release-repository",
90+
"${ArtifactoryPromoteDockerImage.IMAGE}=docker.example.com/existed-image",
91+
"${ArtifactoryPromoteDockerImage.TAG}=existed-tag"
92+
)
93+
),
94+
Arguments.of(
95+
"Test promote-docker-image with not existed source docker repository", 1, arrayOf(
96+
ArtifactoryPromoteDockerImage.COMMAND,
97+
"${ArtifactoryPromoteDockerImage.SOURCE_REPOSITORY}=not-existed-docker-dev-repository",
98+
"${ArtifactoryPromoteDockerImage.TARGET_REPOSITORY}=docker-release-repository",
99+
"${ArtifactoryPromoteDockerImage.IMAGE}=docker.example.com/existed-image",
100+
"${ArtifactoryPromoteDockerImage.TAG}=existed-tag",
101+
"${ArtifactoryPromoteDockerImage.IGNORE_NOT_FOUND}=false"
102+
)
103+
),
104+
Arguments.of(
105+
"Test promote-docker-image with not existed target docker repository", 1, arrayOf(
106+
ArtifactoryPromoteDockerImage.COMMAND,
107+
"${ArtifactoryPromoteDockerImage.SOURCE_REPOSITORY}=docker-dev-repository",
108+
"${ArtifactoryPromoteDockerImage.TARGET_REPOSITORY}=not-existed-docker-release-repository",
109+
"${ArtifactoryPromoteDockerImage.IMAGE}=docker.example.com/existed-image",
110+
"${ArtifactoryPromoteDockerImage.TAG}=existed-tag",
111+
"${ArtifactoryPromoteDockerImage.IGNORE_NOT_FOUND}=false"
112+
)
113+
),
114+
Arguments.of(
115+
"Test promote-docker-image with not existed docker image", 1, arrayOf(
116+
ArtifactoryPromoteDockerImage.COMMAND,
117+
"${ArtifactoryPromoteDockerImage.SOURCE_REPOSITORY}=docker-dev-repository",
118+
"${ArtifactoryPromoteDockerImage.TARGET_REPOSITORY}=docker-release-repository",
119+
"${ArtifactoryPromoteDockerImage.IMAGE}=docker.example.com/not-existed-image",
120+
"${ArtifactoryPromoteDockerImage.TAG}=existed-tag",
121+
"${ArtifactoryPromoteDockerImage.IGNORE_NOT_FOUND}=false"
122+
)
123+
),
124+
Arguments.of(
125+
"Test promote-docker-image with not existed docker image with ignore-not-found", 0, arrayOf(
126+
ArtifactoryPromoteDockerImage.COMMAND,
127+
"${ArtifactoryPromoteDockerImage.SOURCE_REPOSITORY}=docker-dev-repository",
128+
"${ArtifactoryPromoteDockerImage.TARGET_REPOSITORY}=docker-release-repository",
129+
"${ArtifactoryPromoteDockerImage.IMAGE}=docker.example.com/not-existed-image",
130+
"${ArtifactoryPromoteDockerImage.TAG}=existed-tag",
131+
"${ArtifactoryPromoteDockerImage.IGNORE_NOT_FOUND}=true"
132+
)
133+
)
134+
)
135+
//</editor-fold>
136+
}
137+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"errors": [
3+
{
4+
"status": "404",
5+
"message": "No build was found for build name: $buildName, build number: $buildNumber"
6+
}
7+
]
8+
}
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"existed-build-name": {
3+
"existed-build-number1": {
4+
"buildInfo": {
5+
"version": "1.0.1",
6+
"name": "existed-build-name",
7+
"number": "existed-build-number1",
8+
"buildAgent": {
9+
"name": "Maven",
10+
"version": "3.6.0"
11+
},
12+
"agent": {
13+
"name": "artifactory-maven-plugin",
14+
"version": "3.2.1"
15+
},
16+
"started": "2024-10-19T15:07:54.915+0300",
17+
"durationMillis": 276272,
18+
"artifactoryPrincipal": "tcagent",
19+
"vcs": [],
20+
"modules": [
21+
{
22+
"properties": {
23+
},
24+
"id": "org.octopusden.octopus.automation.artifactory:octopus-artifactory-automation:existed-build-number1",
25+
"artifacts": [
26+
{
27+
"type": "jar",
28+
"sha1": "b9f3c4256bd04842e7e906c430b5fdf0d46750a6",
29+
"sha256": "05065e8a9b6b2006492c69e74996491d08b536c7d5980eec823c864f0aab60b8",
30+
"md5": "7f71b32a41891e548840ace56f112b49",
31+
"name": "octopus-artifactory-automation-existed-build-number1.jar"
32+
}
33+
],
34+
"dependencies": []
35+
}
36+
]
37+
},
38+
"uri": "https://localhost:1080:443/artifactory/api/build/existed-build-name/existed-build-number1?buildRepo=artifactory-build-info"
39+
}
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"errors": [
3+
{
4+
"status": "404",
5+
"message": "Unable to find 'docker.example.com/not-existed-image:existed-tag'. Promotion aborted"
6+
}
7+
]
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"docker-dev-repository" : {
3+
"docker.example.com/existed-image": {
4+
"existed-tag" : "docker-release-repository"
5+
}
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"errors": [
3+
{
4+
"status": "400",
5+
"message": "Unsupported V2 repository request for 'docker-release-repository'"
6+
}
7+
]
8+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"version" : "7.71.23",
3+
"revision" : "77123900",
4+
"addons" : [ "build", "docker", "helmoci", "oci", "packages-archive", "vagrant", "replication", "filestore", "curation", "plugins", "worker", "gems", "composer", "npm", "bower", "git-lfs", "nuget", "debian", "opkg", "rpm", "cocoapods", "conan", "vcs", "pypi", "release-bundle", "jf-connect", "jf-event", "replicator", "keys", "alpine", "analytics", "cargo", "chef", "federated", "git", "observability", "onboarding", "pub", "rest", "swift", "lead-artifact-detector", "conda", "terraform", "tracker", "license", "puppet", "ldap", "sso", "layouts", "properties", "search", "securityresourceaddon", "filtered-resources", "p2", "watch", "webstart", "support", "xray", "retention" ],
5+
"license" : "bd93b6c4e05c86fec431f74cc1dc94b91f84abed2",
6+
"entitlements" : {
7+
"EVENT_BASED_PULL_REPLICATION" : false,
8+
"SMART_REMOTE_TARGET_FOR_EDGE" : false,
9+
"REPO_REPLICATION" : true,
10+
"MULTIPUSH_REPLICATION" : false
11+
}
12+
}

0 commit comments

Comments
 (0)
Please sign in to comment.