Skip to content

Commit 7a6647f

Browse files
pditommasoclaude
andcommitted
Add releasePluginIfNotExists task with HTTP 409 handling
- Add new RegistryReleaseIfNotExistsTask that treats HTTP 409 errors as non-failing - Add releaseIfNotExists method to RegistryClient with duplicate detection - Register releasePluginIfNotExists and releasePluginToRegistryIfNotExists tasks - Refactor RegistryClient to eliminate code duplication between release methods - Extract common HTTP request logic into shared sendReleaseRequest method 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 76fe68a commit 7a6647f

File tree

3 files changed

+134
-5
lines changed

3 files changed

+134
-5
lines changed

src/main/groovy/io/nextflow/gradle/NextflowPlugin.groovy

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.nextflow.gradle
22

3+
import io.nextflow.gradle.registry.RegistryReleaseIfNotExistsTask
34
import io.nextflow.gradle.registry.RegistryReleaseTask
45
import org.gradle.api.Plugin
56
import org.gradle.api.Project
@@ -113,6 +114,17 @@ class NextflowPlugin implements Plugin<Project> {
113114
description = 'Release plugin to configured destination'
114115
})
115116
project.tasks.releasePlugin.dependsOn << project.tasks.releasePluginToRegistry
117+
118+
// Always create registry release if not exists task - handles duplicates gracefully
119+
project.tasks.register('releasePluginToRegistryIfNotExists', RegistryReleaseIfNotExistsTask)
120+
project.tasks.releasePluginToRegistryIfNotExists.dependsOn << project.tasks.packagePlugin
121+
122+
// Always create the main release if not exists task
123+
project.tasks.register('releasePluginIfNotExists', {
124+
group = 'Nextflow Plugin'
125+
description = 'Release plugin to configured destination, skipping if already exists'
126+
})
127+
project.tasks.releasePluginIfNotExists.dependsOn << project.tasks.releasePluginToRegistryIfNotExists
116128
}
117129
}
118130

src/main/groovy/io/nextflow/gradle/registry/RegistryClient.groovy

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,53 @@ class RegistryClient {
5151
* @throws RegistryReleaseException if the upload fails or returns an error
5252
*/
5353
def release(String id, String version, File file) {
54+
def response = sendReleaseRequest(id, version, file)
55+
56+
if (response.statusCode() != 200) {
57+
throw new RegistryReleaseException(getErrorMessage(response, url.resolve("v1/plugins/release")))
58+
}
59+
}
60+
61+
/**
62+
* Releases a plugin to the registry with duplicate handling.
63+
*
64+
* Uploads the plugin zip file along with metadata to the registry
65+
* using a multipart HTTP POST request to the v1/plugins/release endpoint.
66+
* Unlike the regular release method, this handles HTTP 409 "DUPLICATE_PLUGIN"
67+
* errors as non-failing conditions.
68+
*
69+
* @param id The plugin identifier/name
70+
* @param version The plugin version (must be valid semver)
71+
* @param file The plugin zip file to upload
72+
* @return Map with keys: success (boolean), skipped (boolean), message (String)
73+
* @throws RegistryReleaseException if the upload fails for reasons other than duplicates
74+
*/
75+
def releaseIfNotExists(String id, String version, File file) {
76+
def response = sendReleaseRequest(id, version, file)
77+
78+
if (response.statusCode() == 200) {
79+
return [success: true, skipped: false, message: null]
80+
}
81+
82+
if (response.statusCode() == 409) {
83+
// HTTP 409 indicates plugin already exists - treat as non-failing
84+
return [success: true, skipped: true, message: response.body()]
85+
}
86+
87+
// For all other errors, throw exception as usual
88+
throw new RegistryReleaseException(getErrorMessage(response, url.resolve("v1/plugins/release")))
89+
}
90+
91+
/**
92+
* Sends the HTTP release request to the registry.
93+
*
94+
* @param id The plugin identifier/name
95+
* @param version The plugin version
96+
* @param file The plugin zip file to upload
97+
* @return HttpResponse from the registry
98+
* @throws RegistryReleaseException if the request fails due to network issues
99+
*/
100+
private HttpResponse<String> sendReleaseRequest(String id, String version, File file) {
54101
def client = HttpClient.newBuilder()
55102
.connectTimeout(Duration.ofSeconds(30))
56103
.build()
@@ -68,11 +115,7 @@ class RegistryClient {
68115
.build()
69116

70117
try {
71-
def response = client.send(request, HttpResponse.BodyHandlers.ofString())
72-
73-
if (response.statusCode() != 200) {
74-
throw new RegistryReleaseException(getErrorMessage(response, requestUri))
75-
}
118+
return client.send(request, HttpResponse.BodyHandlers.ofString())
76119
} catch (InterruptedException e) {
77120
Thread.currentThread().interrupt()
78121
throw new RegistryReleaseException("Plugin release to ${requestUri} was interrupted: ${e.message}", e)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.nextflow.gradle.registry
2+
3+
import groovy.transform.CompileStatic
4+
import io.nextflow.gradle.NextflowPluginConfig
5+
import org.gradle.api.DefaultTask
6+
import org.gradle.api.file.RegularFileProperty
7+
import org.gradle.api.tasks.InputFile
8+
import org.gradle.api.tasks.TaskAction
9+
10+
/**
11+
* Gradle task for releasing a Nextflow plugin to a registry with duplicate handling.
12+
*
13+
* This task uploads the assembled plugin zip file to a configured registry
14+
* using the registry's REST API. Unlike the regular release task, this task
15+
* treats HTTP 409 "DUPLICATE_PLUGIN" errors as non-failing conditions and
16+
* logs an info message instead of failing the build.
17+
*/
18+
@CompileStatic
19+
class RegistryReleaseIfNotExistsTask extends DefaultTask {
20+
/**
21+
* The plugin zip file to be uploaded to the registry.
22+
* By default, this points to the zip file created by the packagePlugin task.
23+
*/
24+
@InputFile
25+
final RegularFileProperty zipFile
26+
27+
RegistryReleaseIfNotExistsTask() {
28+
group = 'Nextflow Plugin'
29+
description = 'Release the assembled plugin to the registry, skipping if already exists'
30+
31+
final buildDir = project.layout.buildDirectory.get()
32+
zipFile = project.objects.fileProperty()
33+
zipFile.convention(project.provider {
34+
buildDir.file("distributions/${project.name}-${project.version}.zip")
35+
})
36+
}
37+
38+
/**
39+
* Executes the registry release task with duplicate handling.
40+
*
41+
* This method retrieves the plugin configuration and creates a RegistryClient
42+
* to upload the plugin zip file to the configured registry endpoint.
43+
* If the plugin already exists (HTTP 409 DUPLICATE_PLUGIN), it logs an info
44+
* message and continues without failing.
45+
*
46+
* @throws RegistryReleaseException if the upload fails for reasons other than duplicates
47+
*/
48+
@TaskAction
49+
def run() {
50+
final version = project.version.toString()
51+
final plugin = project.extensions.getByType(NextflowPluginConfig)
52+
53+
// Get or create registry configuration
54+
def registryConfig
55+
if (plugin.registry) {
56+
registryConfig = plugin.registry
57+
} else {
58+
// Create default registry config that will use fallback values
59+
registryConfig = new RegistryReleaseConfig(project)
60+
}
61+
62+
def registryUri = new URI(registryConfig.resolvedUrl)
63+
def client = new RegistryClient(registryUri, registryConfig.resolvedAuthToken)
64+
def result = client.releaseIfNotExists(project.name, version, project.file(zipFile)) as Map<String, Object>
65+
66+
if (result.skipped as Boolean) {
67+
// Plugin already exists - log info message and continue
68+
project.logger.lifecycle("ℹ️ Plugin '${project.name}' version ${version} already exists in registry [${registryUri}] - skipping upload")
69+
} else {
70+
// Celebrate successful plugin upload! 🎉
71+
project.logger.lifecycle("🎉 SUCCESS! Plugin '${project.name}' version ${version} has been successfully released to Nextflow Registry [${registryUri}]!")
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)