add nextflow d30e48d

This commit is contained in:
2026-04-29 23:01:54 +02:00
parent d0b12d668d
commit 97cc9058d3
2840 changed files with 730250 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
# AWS CodeCommit plugin for Nextflow
## Summary
The AWS CodeCommit plugin provides integration with AWS CodeCommit. It enables Nextflow to pull pipeline scripts directly from CodeCommit repositories.
## Get Started
To use this plugin, add it to your `nextflow.config`:
```groovy
plugins {
id 'nf-codecommit'
}
```
The plugin enables Nextflow to recognize CodeCommit repository URLs and authenticate using your AWS credentials.
Run a pipeline directly from CodeCommit:
```bash
nextflow run codecommit://my-repo/main.nf
```
## Examples
### Running a Pipeline from CodeCommit
```bash
nextflow run codecommit://my-pipeline-repo/main.nf
```
### Specifying a Branch or Tag
```bash
nextflow run codecommit://my-pipeline-repo/main.nf -r develop
```
### Using a Specific AWS Region
```bash
nextflow run codecommit://my-pipeline-repo/main.nf -hub codecommit -hub-opts region=eu-west-1
```
### Configuration with AWS Region
```groovy
plugins {
id 'nf-codecommit'
}
aws {
region = 'us-east-1'
}
```
### Using AWS Profiles
Set the AWS profile via environment variable:
```bash
export AWS_PROFILE=my-profile
nextflow run codecommit://my-repo/main.nf
```
## Resources
- [AWS CodeCommit Documentation](https://docs.aws.amazon.com/codecommit/)
- [Nextflow Pipeline Sharing](https://nextflow.io/docs/latest/sharing.html)
## License
[Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0)

View File

@@ -0,0 +1 @@
0.5.1

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id 'io.nextflow.nextflow-plugin' version "${nextflowPluginVersion}"
id 'java-test-fixtures'
}
nextflowPlugin {
nextflowVersion = '25.09.0-edge'
provider = "${nextflowPluginProvider}"
description = 'Provides seamless integration with AWS CodeCommit repositories for workflow source code management and version control'
className = 'nextflow.cloud.aws.codecommit.AwsCodeCommitPlugin'
useDefaultDependencies = false
generateSpec = false
extensionPoints = [
'nextflow.cloud.aws.codecommit.AwsCodeCommitFactory'
]
}
sourceSets {
main.java.srcDirs = []
main.groovy.srcDirs = ['src/main']
main.resources.srcDirs = ['src/resources']
test.groovy.srcDirs = ['src/test']
test.java.srcDirs = []
test.resources.srcDirs = ['src/testResources']
}
configurations {
// see https://docs.gradle.org/4.1/userguide/dependency_management.html#sub:exclude_transitive_dependencies
runtimeClasspath.exclude group: 'org.slf4j', module: 'slf4j-api'
}
dependencies {
compileOnly project(':nextflow')
compileOnly 'org.slf4j:slf4j-api:2.0.17'
compileOnly 'org.pf4j:pf4j:3.14.1'
api ('javax.xml.bind:jaxb-api:2.4.0-b180830.0359')
api ('software.amazon.awssdk:codecommit:2.31.64')
api ('software.amazon.awssdk:sso:2.31.64')
api ('software.amazon.awssdk:ssooidc:2.31.64')
// address security vulnerabilities
runtimeOnly 'io.netty:netty-codec-http:4.1.132.Final'
testImplementation(testFixtures(project(":nextflow")))
testImplementation project(':nextflow')
testImplementation "org.apache.groovy:groovy:4.0.31"
testImplementation "org.apache.groovy:groovy-nio:4.0.31"
}

View File

@@ -0,0 +1,56 @@
nf-codecommit changelog
=======================
0.5.1 - 26 Mar 2026
- Fix netty and jackson vulnerabilities (#6955) [8dafdd95d]
0.5.0 - 8 Oct 2025
- Add listDirectory traversal API to RepositoryProvider abstraction (#6430) [1449fdfec]
0.3.0 - 15 Aug 2025
- Bump groovy 4.0.28 (#6304) [a468f8ef]
- Revert "Update nf-codecommit to AWS SDK v2 (#6263)" [5542351c]
- Update nf-codecommit to AWS SDK v2 (#6263) [9e9476f2]
- Update nf-codecommit to AWS SDK v2 with corrected test (#6293) [1557a91a]
0.2.3 - 20 Jan 2025
- Bump logback 1.5.13 + slf4j 2.0.16 [cc0163ac]
- Bump groovy 4.0.24 missing deps [40670f7e]
0.2.2 - 5 Aug 2024
- Bump pf4j to version 3.12.0 [96117b9a]
0.2.1 - 1 Aug 2024
- Bump amazon sdk to version 1.12.766 [5ce42b79]
- Bump pf4j to version 3.12.0 [1a8f086a]
0.2.0 - 5 Feb 2024
- Bump Groovy 4 (#4443) [9d32503b]
0.1.6 - 24 Nov 2023
- Fix security vulnerabilities (#4513) [a310c777]
- Bump javax.xml.bind:jaxb-api:2.4.0-b180830.0359
0.1.5-patch2 - 30 Jul 2024
- Bump amazon sdk to version 1.12.766 [189f58ed]
- Bump pf4j to version 3.12.0 [8dfa4076]
0.1.5-patch1 - 28 May 2024
- Bump dependency with Nextflow 23.10.2
0.1.5 - 15 May 2023
- Update logging libraries [d7eae86e]
0.1.4 - 15 Apr 2023
- Bump aws-java-sdk-s3:1.12.429 [b71ee0a3]
0.1.3 - 14 Jan 2023
- Bump groovy 3.0.14 [6f3ed6e8]
0.1.2 - 17 Jun 2022
- Fix CodeCommit credentials usage
0.1.1 - 16 Jun 2022
- [INVALID]
0.1.0 - 9 Jun 2022
- AWS CodeCommit initial release

View File

@@ -0,0 +1,324 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.cloud.aws.codecommit
import software.amazon.awssdk.auth.credentials.AwsCredentials
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import javax.security.auth.login.CredentialException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.text.SimpleDateFormat
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import nextflow.exception.AbortOperationException
import org.eclipse.jgit.errors.UnsupportedCredentialItem
import org.eclipse.jgit.transport.CredentialItem
import org.eclipse.jgit.transport.CredentialsProvider
import org.eclipse.jgit.transport.URIish
/**
* Provides a jgit {@link CredentialsProvider} implementation that can provide the
* appropriate credentials to connect to an AWS CodeCommit repository.
*
* From the command line, you can configure git to use AWS CodeCommit with a credential
* helper. Although jgit does not support credential helper commands, it provides
* a CredentialsProvider abstract class we can extend. Connecting to an AWS CodeCommit
* (codecommit) repository requires an AWS access key and secret key. These are used to
* calculate a signature for the git request. The AWS access key is used as the codecommit
* username, and the calculated signature is used as the password. The process for
* calculating this signature is documented at
* https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html.
*
* @author Don Laidlaw
*
* This class is mostly a direct port of the AwsCodeCommitCredentialProvider class provided by the
* spring framework in org.springframwork.cloud.config.server.support, with some minor
* modifications and simplifications that leverage the Groovy language.
*
* @author W. Lee Pang <wleepang@gmail.com>
*/
@Slf4j
@CompileStatic
final class AwsCodeCommitCredentialProvider extends CredentialsProvider {
private static final String SHA_256 = "SHA-256"
private static final String UTF8 = "UTF8"
private static final String HMAC_SHA256 = "HmacSHA256"
private String username
private String password
/**
* Calculate the AWS CodeCommit password for the provided URI and AWS secret key. This
* uses the algorithm published by AWS at
* https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
* @param uri the codecommit repository uri
* @param awsSecretKey the aws secret key
* @return the password to use in the git request
*/
protected static String calculateCodeCommitPassword(URIish uri, String awsSecretKey, Date now) {
String[] split = uri.getHost().split("\\.")
if (split.length < 4) {
throw new CredentialException("Cannot detect AWS region from URI $uri")
}
String region = split[1]
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss")
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"))
String dateStamp = dateFormat.format(now)
String shortDateStamp = dateStamp.substring(0, 8)
String codeCommitPassword
try {
def stringToSign = "AWS4-HMAC-SHA256\n${dateStamp}\n${shortDateStamp}/$region/codecommit/aws4_request\n${bytesToHexString(canonicalRequestDigest(uri))}"
def signedRequest = bytesToHexString(
sign(awsSecretKey, shortDateStamp, region, stringToSign)
)
codeCommitPassword = "${dateStamp}Z${signedRequest}"
}
catch (Exception e) {
throw new RuntimeException("Error calculating AWS CodeCommit password", e)
}
return codeCommitPassword
}
private static byte[] hmacSha256(String data, byte[] key) {
String algorithm = HMAC_SHA256
Mac mac = Mac.getInstance(algorithm)
mac.init(new SecretKeySpec(key, algorithm))
return mac.doFinal(data.getBytes(UTF8))
}
private static byte[] sign(String secret, String shortDateStamp, String region, String toSign) {
byte[] kSecret = ("AWS4" + secret).getBytes(UTF8)
byte[] kDate = hmacSha256(shortDateStamp, kSecret)
byte[] kRegion = hmacSha256(region, kDate)
byte[] kService = hmacSha256("codecommit", kRegion)
byte[] kSigning = hmacSha256("aws4_request", kService)
return hmacSha256(toSign, kSigning)
}
/**
* Creates a message digest.
* @param uri uri to process
* @return a message digest
* @throws NoSuchAlgorithmException when the SHA 256 algorithm is not found
*/
private static byte[] canonicalRequestDigest(URIish uri) {
def canonicalRequest = ""
// this could be done faster with a templated multi-line GString, but is broken out here
// to maintain documentation of each part
canonicalRequest += "GIT\n" // codecommit uses GIT as the request method
canonicalRequest += "${uri.getPath()}\n" // URI request path
canonicalRequest += "\n" // Query string, always empty for codecommit
// Next are canonical headers — codecommit only requires the host header
canonicalRequest += "host:${uri.getHost()}\n\n" // canonical headers are alays terminated with \n
canonicalRequest += "host\n" // The list of canonical headers, only one for codecommit
MessageDigest digest = MessageDigest.getInstance(SHA_256);
return digest.digest(canonicalRequest.getBytes());
}
/**
* Convert bytes to a hex string.
* @param bytes the bytes
* @return a string of hex characters encoding the bytes.
*/
private static String bytesToHexString(byte[] bytes) { bytes.encodeHex().toString() }
/**
* This provider can handle uris like
* https://git-codecommit.$AWS_REGION.amazonaws.com/v1/repos/$REPO .
* @param uri uri to parse
* @return {@code true} if the URI can be handled
*/
static boolean canHandle(String uri) {
if ( !uri?.trim() ) {
return false
}
try {
URL url = new URL(uri)
URI u = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(),
url.getPort(), url.getPath(), url.getQuery(), url.getRef())
if (u.getScheme().equals("https")) {
String host = u.getHost()
if (host.endsWith(".amazonaws.com")
&& host.startsWith("git-codecommit.")) {
return true
}
}
}
catch (Throwable t) {
// ignore all, we can't handle it
log.debug "AWS CodeCommit cannot handle uri: $uri - Reason: ${t.message ?: t}"
}
return false
}
/**
* Get the AWSCredentials. If an AwsCredentialProvider was specified, use that,
* otherwise, create a new AwsCredentialsProvider. If the username and password are
* provided, use those directly as AwsCredentials. Otherwise, use the
* {@link DefaultCredentialsProvider} as is standard with AWS applications.
* @return the AWS credentials.
*/
private AwsCredentials retrieveAwsCredentials() {
AwsCredentialsProvider credsProvider
if ( username && password ) {
log.debug "Creating a static AWS credentials provider"
credsProvider = StaticCredentialsProvider.create( AwsBasicCredentials.create(username,password) )
}
else {
log.debug "Creating a default AWS credentials provider chain"
credsProvider = DefaultCredentialsProvider.builder().build()
}
return credsProvider.resolveCredentials()
}
/**
* This credentials provider cannot run interactively.
* @return false
* @see org.eclipse.jgit.transport.CredentialsProvider#isInteractive()
*/
@Override
boolean isInteractive() { false }
/**
* We support username and password credential items only.
* @see org.eclipse.jgit.transport.CredentialsProvider#supports(org.eclipse.jgit.transport.CredentialItem[])
*/
@Override
boolean supports(CredentialItem... items) {
for ( i in items ) {
if (i instanceof CredentialItem.Username) {
continue
}
else if (i instanceof CredentialItem.Password) {
continue
}
else {
return false
}
}
return true
}
/**
* Get the username and password to use for the given uri.
* @see org.eclipse.jgit.transport.CredentialsProvider#get(org.eclipse.jgit.transport.URIish,
* org.eclipse.jgit.transport.CredentialItem[])
*/
@Override
boolean get(URIish uri, CredentialItem... items) {
String codeCommitPassword
String awsAccessKey
String awsSecretKey
try {
AwsCredentials awsCredentials = retrieveAwsCredentials()
StringBuilder awsKey = new StringBuilder();
awsKey.append(awsCredentials.accessKeyId());
awsSecretKey = awsCredentials.secretAccessKey();
if (awsCredentials instanceof AwsSessionCredentials) {
AwsSessionCredentials sessionCreds = (AwsSessionCredentials) awsCredentials;
if ( sessionCreds.sessionToken() ) {
awsKey.append('%').append(sessionCreds.sessionToken())
}
}
awsAccessKey = awsKey.toString()
}
catch (Exception e) {
throw new AbortOperationException("Unable to retrieve AWS Credentials", e)
}
try {
codeCommitPassword = calculateCodeCommitPassword(uri, awsSecretKey, new Date());
}
catch (Exception e) {
throw new AbortOperationException("Error calculating AWS CodeCommit password", e)
}
for ( i in items ) {
if (i instanceof CredentialItem.Username) {
((CredentialItem.Username) i).setValue(awsAccessKey);
log.trace("Returning username " + awsAccessKey);
continue;
}
if (i instanceof CredentialItem.Password) {
((CredentialItem.Password) i).setValue(codeCommitPassword.toCharArray());
log.trace("Returning password " + codeCommitPassword);
continue;
}
if (i instanceof CredentialItem.StringType
&& i.getPromptText().equals("Password: ")) {
((CredentialItem.StringType) i).setValue(codeCommitPassword);
log.trace("Returning password string " + codeCommitPassword);
continue;
}
throw new UnsupportedCredentialItem(uri, i.getClass().getName() + ":" + i.getPromptText());
}
return true;
}
/**
* Throw out cached data and force retrieval of AWS credentials.
* @param uri This parameter is not used in this implementation.
*/
@Override
void reset(URIish uri) {
// Should throw out cached info.
// Note that even though the credentials (password) we calculate here is
// valid for 15 minutes, we do not cache it. Instead, we re-calculate
// it each time it is needed. However, the AWSCredentialProvider will cache
// its AWSCredentials object.
}
/**
* @param awsCredentialProvider the awsCredentialProvider to set
*/
void setAwsCredentialsProvider(AwsCredentialsProvider awsCredentialsProvider) {
this.awsCredentialsProvider = awsCredentialsProvider
}
String getUsername() {
return username
}
String getPassword() {
return password
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.cloud.aws.codecommit
import groovy.util.logging.Slf4j
import nextflow.plugin.Priority
import nextflow.scm.GitUrl
import nextflow.scm.ProviderConfig
import nextflow.scm.RepositoryFactory
import nextflow.scm.RepositoryProvider
/**
* Implements a factory to create an instance of {@link AwsCodeCommitRepositoryProvider}
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
@Slf4j
@Priority(-10) // <-- lower is higher, this is needed to override default provider behavior
class AwsCodeCommitFactory extends RepositoryFactory {
@Override
protected RepositoryProvider createProviderInstance(ProviderConfig config, String project) {
return config.platform=='codecommit'
? new AwsCodeCommitRepositoryProvider(project,config)
: null
}
@Override
protected ProviderConfig getConfig(List<ProviderConfig> providers, GitUrl url) {
// do not care about non AWS codecommit url
if( !url.domain.startsWith('git-codecommit.') || !url.domain.endsWith('.amazonaws.com') )
return null
// CodeCommit hostname vary depending the AWS region
// try to find the config for the specified region
def config = providers.find( it -> it.domain==url.domain )
if( config ) {
log.debug "Git url=$url (1) -> config=$config"
return config
}
// fallback on the platform name
config = providers.find( it -> it.platform=='codecommit' && !it.server )
if( config ) {
config.setServer("${url.protocol}://${url.domain}")
log.debug "Git url=$url (2) -> config=$config"
return config
}
// still nothing, create a new instance
config = new AwsCodeCommitProviderConfig(url.domain)
if( url.user ) {
log.debug "Git url=$url (3) -> config=$config"
config.setUser(url.user)
}
return config
}
@Override
protected ProviderConfig createConfigInstance(String name, Map attrs) {
final copy = new HashMap(attrs)
if( name == 'codecommit' ) {
copy.platform = 'codecommit'
}
return copy.platform == 'codecommit'
? new AwsCodeCommitProviderConfig(name, copy)
: null
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.cloud.aws.codecommit
import nextflow.plugin.BasePlugin
import org.pf4j.PluginWrapper
/**
* AWS CodeCommit plugin entry point
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class AwsCodeCommitPlugin extends BasePlugin {
AwsCodeCommitPlugin(PluginWrapper wrapper) {
super(wrapper)
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.cloud.aws.codecommit
import groovy.transform.CompileStatic
import nextflow.scm.ProviderConfig
/**
* A {@link ProviderConfig} specialised for AWS CodeCommit
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
@CompileStatic
class AwsCodeCommitProviderConfig extends ProviderConfig {
AwsCodeCommitProviderConfig(String host) {
super('codecommit', [platform:'codecommit', server: "https://$host"])
assert host =~ /git-codecommit\.[a-z0-9-]+\.amazonaws\.com/, "Invalid AWS CodeCommit hostname: '$host'"
}
AwsCodeCommitProviderConfig(String name, Map attributes) {
super(name, attributes)
assert attributes.platform=='codecommit', "Invalid AWS CodeCommit platform value: '$attributes.platform'"
}
String getRegion() {
final host = getDomain()
if( !host )
throw new IllegalStateException("Missing AWS CodeCommit repository name")
final result = host.tokenize('.')[1]
if( !result )
throw new IllegalStateException("Invalid AWS CodeCommit hostname: '${host}'")
return result
}
@Override
protected String resolveProjectName(String path) {
assert path
assert !path.startsWith('/')
final repoName = path.tokenize('/')[-1]
if( !repoName )
throw new IllegalArgumentException("Invalid AWS CodeCommit repository path: $path")
return "codecommit-$region/$repoName"
}
String toString() {
return "AwsCodeCommitProviderConfig[name=$name; platform=$platform; server=$server]"
}
}

View File

@@ -0,0 +1,277 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.cloud.aws.codecommit
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.core.exception.SdkException
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.codecommit.CodeCommitClient
import software.amazon.awssdk.services.codecommit.model.CodeCommitException
import software.amazon.awssdk.services.codecommit.model.GetFileRequest
import software.amazon.awssdk.services.codecommit.model.GetFolderRequest
import software.amazon.awssdk.services.codecommit.model.GetRepositoryRequest
import software.amazon.awssdk.services.codecommit.model.RepositoryMetadata
import groovy.transform.CompileStatic
import groovy.transform.Memoized
import groovy.util.logging.Slf4j
import nextflow.exception.AbortOperationException
import nextflow.exception.MissingCredentialsException
import nextflow.scm.ProviderConfig
import nextflow.scm.RepositoryProvider
import nextflow.scm.RepositoryProvider.RepositoryEntry
import nextflow.util.StringUtils
import org.eclipse.jgit.api.errors.TransportException
import org.eclipse.jgit.transport.CredentialsProvider
/**
* Implements a repository provider for AWS CodeCommit
*
* @author W. Lee Pang <wleepang@gmail.com>
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
@Slf4j
@CompileStatic
class AwsCodeCommitRepositoryProvider extends RepositoryProvider {
AwsCodeCommitRepositoryProvider(String project, ProviderConfig config) {
assert config instanceof AwsCodeCommitProviderConfig
this.project = project // expect: "codecommit-<region>/<repository>"
this.config = config
this.region = config.region
this.repositoryName = project.tokenize('/')[-1]
this.client = createClient(config)
}
private String region
private CodeCommitClient client
private String repositoryName
protected CodeCommitClient createClient(AwsCodeCommitProviderConfig config) {
final builder = CodeCommitClient.builder()
.region(Region.of(region))
if( config.user && config.password ) {
final creds = AwsBasicCredentials.create(config.user, config.password)
log.debug "AWS CodeCommit using username=$config.user; password=${StringUtils.redact(config.password)}"
builder.credentialsProvider( StaticCredentialsProvider.create(creds) )
}
else {
log.debug "AWS CodeCommit using default credentials chain"
builder.credentialsProvider( DefaultCredentialsProvider.builder().build() )
}
builder.build()
}
/** {@inheritDoc} **/
@Memoized
@Override
CredentialsProvider getGitCredentials() {
return new AwsCodeCommitCredentialProvider(username: user, password: password)
}
private RepositoryMetadata getRepositoryMetadata() {
final request = GetRepositoryRequest.builder()
.repositoryName(repositoryName)
.build()
return client
.getRepository(request)
.repositoryMetadata()
}
/** {@inheritDoc} **/
// called by AssetManager
// used to set credentials for a clone, pull, fetch, operation
@Override
boolean hasCredentials() {
// set to true
// uses AWS Credentials instead of username : password
// see getGitCredentials()
return true
}
/** {@inheritDoc} **/
@Override
String getName() { "CodeCommit" }
/** {@inheritDoc} **/
@Override
String getEndpointUrl() {
"https://git-codecommit.${region}.amazonaws.com/v1/repos/${repositoryName}"
}
/** {@inheritDoc} **/
// not used, but the abstract method needs to be overridden
@Override
String getContentUrl( String path ) {
throw new UnsupportedOperationException()
}
/** {@inheritDoc} **/
// called by AssetManager
@Override
String getCloneUrl() { getEndpointUrl() }
/** {@inheritDoc} **/
// called by AssetManager
@Override
String getRepositoryUrl() { getEndpointUrl() }
/** {@inheritDoc} **/
// called by AssetManager
// called by RepositoryProvider.readText()
@Override
byte[] readBytes( String path ) {
final builder = GetFileRequest.builder()
.repositoryName(repositoryName)
.filePath(path)
if( revision )
builder.commitSpecifier(revision)
try {
return client
.getFile( builder.build() )
.fileContent()?.asByteArray()
}
catch (Exception e) {
checkMissingCredsException(e)
log.debug "AWS CodeCommit unable to retrieve file: $path from repo: $repositoryName"
return null
}
}
/** {@inheritDoc} **/
@Override
List<RepositoryEntry> listDirectory(String path, int depth) {
try {
// AWS CodeCommit doesn't have a dedicated directory listing API like GitHub
// We would need to use GetFolder API, but it has limitations
def request = GetFolderRequest.builder()
.repositoryName(repositoryName)
.folderPath(path ?: "/")
.commitSpecifier(revision ?: "HEAD")
.build()
def response = client.getFolder(request)
List<RepositoryEntry> entries = []
// Add files
response.files()?.each { file ->
entries.add(new RepositoryEntry(
name: file.relativePath().split('/').last(),
path: ensureAbsolutePath(file.relativePath()),
type: RepositoryProvider.EntryType.FILE,
sha: file.blobId(),
size: null // AWS CodeCommit API doesn't provide file size in folder response
))
}
// Add subdirectories - but CodeCommit API has limited support for deep traversal
response.subFolders()?.each { folder ->
entries.add(new RepositoryEntry(
name: folder.relativePath().split('/').last(),
path: ensureAbsolutePath(folder.relativePath()),
type: RepositoryProvider.EntryType.DIRECTORY,
sha: null, // CodeCommit doesn't provide SHA for directories
size: null
))
// For recursive listing, we would need additional API calls
// However, this can be expensive and slow for large repositories
if (depth != 0 && depth != 1) {
try {
def subEntries = listDirectory(folder.relativePath(), depth == -1 ? -1 : depth - 1)
entries.addAll(subEntries)
} catch (Exception e) {
// Continue with other directories if one fails
}
}
}
return entries.sort { it.name }
} catch (Exception e) {
checkMissingCredsException(e)
throw new UnsupportedOperationException("Directory listing failed for AWS CodeCommit path: $path - ${e.message}", e)
}
}
protected void checkMissingCredsException(Exception e) {
final errs = [
"Failed to connect to service endpoint",
"Unable to load AWS credentials",
"The security token included in the request is invalid",
"The request signature we calculated does not match the signature you provided"]
if( e !instanceof SdkException )
return
if( e instanceof CodeCommitException && e.message?.startsWith("Could not find path") ) {
// it cannot find the request file
return
}
if( errs.find(it-> e.message?.startsWith(it))) {
final msg = e.message?.split(/\.|\(|:/)[0].trim()
throw new MissingCredentialsException("Missing or invalid AWS CodeCommit credentials - $msg", e)
}
else {
throw new AbortOperationException("Unexpected error while connecting repository - $e.message", e)
}
}
/** {@inheritDoc} **/
// called by AssetManager
@Override
void validateRepo() {
try {
getRepositoryMetadata()
}
catch( IOException e ) {
throw new AbortOperationException("Cannot access ${getEndpointUrl()} - Make sure a repository exists for it in AWS CodeCommit")
}
}
private String errMsg(Exception e) {
def msg = "Unable to access Git repository"
if( e.message )
msg + " - ${e.message}"
else
msg += ": " + getCloneUrl()
return msg
}
@Override
List<BranchInfo> getBranches() {
try {
return super.getBranches()
}
catch (TransportException e) {
throw new AbortOperationException(errMsg(e), e)
}
}
@Override
List<TagInfo> getTags() {
try {
return super.getTags()
}
catch (TransportException e) {
throw new AbortOperationException(errMsg(e), e)
}
}
}

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.cloud.aws.codecommit
import nextflow.scm.GitUrl
import nextflow.scm.ProviderConfig
import spock.lang.Specification
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class AwsCodeCommitFactoryTest extends Specification {
def 'should create a new provider instance' () {
given:
def factory = new AwsCodeCommitFactory()
and:
def config = Mock(ProviderConfig)
when:
def result = factory.createProviderInstance(config, 'some name')
then:
result == null
and:
config.getPlatform() >> 'any'
}
def 'should create config instance' () {
given:
def factory = new AwsCodeCommitFactory()
when:
def result = factory.createConfigInstance('foo', [:])
then:
result == null
when:
result = factory.createConfigInstance('codecommit', [:])
then:
result instanceof AwsCodeCommitProviderConfig
result.platform == 'codecommit'
result.name == 'codecommit'
when:
result = factory.createConfigInstance('my-aws-repo', [platform: 'codecommit', user:'foo', password:'xyz'])
then:
result instanceof AwsCodeCommitProviderConfig
result.platform == 'codecommit'
result.name == 'my-aws-repo'
result.user == 'foo'
result.password == 'xyz'
}
def 'should get a config' () {
given:
def factory = new AwsCodeCommitFactory()
when:
def configs = [
new ProviderConfig('github', [:]),
new AwsCodeCommitProviderConfig('codecommit', [platform:'codecommit'])
]
and:
def result = factory.getConfig(configs, new GitUrl('https://github.com/this/that'))
then:
result == null
/*
* A CodeCommit config is given without any server specification
* => attributes should be taken and server path updated
*/
when:
configs = [
new ProviderConfig('github', [:]),
new AwsCodeCommitProviderConfig('codecommit', [platform:'codecommit', 'user':'foo', password: 'xxx'])
]
and:
result = factory.getConfig(configs, new GitUrl('https://git-codecommit.eu-west-1.amazonaws.com/v1/repos/my-repo'))
then:
result instanceof AwsCodeCommitProviderConfig
and:
result.name == 'codecommit'
result.platform == 'codecommit'
result.server == 'https://git-codecommit.eu-west-1.amazonaws.com'
result.region == 'eu-west-1'
result.user == 'foo'
result.password == 'xxx'
and:
// just modifies the instance in the provided list
result in configs
/*
* No config is given => a new one should be created
*/
when:
configs = []
and:
result = factory.getConfig(configs, new GitUrl('https://git-codecommit.eu-west-1.amazonaws.com/v1/repos/my-repo'))
then:
result instanceof AwsCodeCommitProviderConfig
and:
result.name == 'codecommit'
result.platform == 'codecommit'
result.server == 'https://git-codecommit.eu-west-1.amazonaws.com'
result.region == 'eu-west-1'
and:
// creates a new instance
result !in configs
/*
* A CodeCommit config with a matching server is given => it should be used
*/
when:
configs = [
new ProviderConfig('github', [:]),
new AwsCodeCommitProviderConfig('my-repo', [platform:'codecommit', server: 'https://git-codecommit.eu-west-1.amazonaws.com'])
]
and:
result = factory.getConfig(configs, new GitUrl('https://git-codecommit.eu-west-1.amazonaws.com/v1/repos/my-repo'))
then:
result instanceof AwsCodeCommitProviderConfig
and:
result.name == 'my-repo'
result.platform == 'codecommit'
result.server == 'https://git-codecommit.eu-west-1.amazonaws.com'
result.region == 'eu-west-1'
and:
// just modifies the instance in the provided list
result in configs
/*
* A CodeCommit config is given for a different region/server
* => a new config should be created
*/
when:
configs = [
new ProviderConfig('github', [:]),
new AwsCodeCommitProviderConfig('my-repo', [platform:'codecommit', server: 'https://git-codecommit.us-east-1.amazonaws.com', user: 'foo'])
]
and:
result = factory.getConfig(configs, new GitUrl('https://myself@git-codecommit.eu-west-1.amazonaws.com/v1/repos/my-repo'))
then:
result instanceof AwsCodeCommitProviderConfig
and:
result.name == 'codecommit'
result.platform == 'codecommit'
result.server == 'https://git-codecommit.eu-west-1.amazonaws.com'
result.region == 'eu-west-1'
result.user == 'myself'
and:
result !in configs
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.cloud.aws.codecommit
import spock.lang.Specification
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class AwsCodeCommitProviderConfigTest extends Specification {
def 'should create config' () {
given:
def HOST = 'git-codecommit.eu-west-1.amazonaws.com'
when:
def config = new AwsCodeCommitProviderConfig(HOST)
then:
config.name == 'codecommit'
config.platform == 'codecommit'
config.region == 'eu-west-1'
config.domain == 'git-codecommit.eu-west-1.amazonaws.com'
config.server == 'https://git-codecommit.eu-west-1.amazonaws.com'
config.endpoint == 'https://git-codecommit.eu-west-1.amazonaws.com'
expect:
config.resolveProjectName('https://git-codecommit.eu-west-1.amazonaws.com/v1/repos/my-repo') == 'codecommit-eu-west-1/my-repo'
}
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.cloud.aws.codecommit
import nextflow.scm.RepositoryProvider
import spock.lang.IgnoreIf
import spock.lang.Requires
import spock.lang.Specification
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
@IgnoreIf({System.getenv('NXF_SMOKE')})
@Requires({System.getenv('AWS_ACCESS_KEY_ID') && System.getenv('AWS_SECRET_ACCESS_KEY')})
class AwsCodeCommitRepositoryProviderTest extends Specification {
def 'should get repo url' () {
given:
def config = new AwsCodeCommitProviderConfig('git-codecommit.eu-west-1.amazonaws.com')
and:
def provider = new AwsCodeCommitRepositoryProvider('codecommit-eu-west-1/my-repo', config)
expect:
provider.getCloneUrl() == 'https://git-codecommit.eu-west-1.amazonaws.com/v1/repos/my-repo'
and:
provider.getRepositoryUrl() == "https://git-codecommit.eu-west-1.amazonaws.com/v1/repos/my-repo"
}
def 'should read content' () {
given:
def config = new AwsCodeCommitProviderConfig('git-codecommit.eu-west-1.amazonaws.com')
and:
def provider = new AwsCodeCommitRepositoryProvider('codecommit-eu-west-1/my-repo', config)
expect:
provider.readText('main.nf') == '''\
nextflow.enable.dsl=2
workflow {
sayHello()
}
process sayHello {
/echo Hello world/
}
'''.stripIndent().rightTrim()
}
def 'should read content with revision' () {
given:
def config = new AwsCodeCommitProviderConfig('git-codecommit.eu-west-1.amazonaws.com')
and:
def provider = new AwsCodeCommitRepositoryProvider('codecommit-eu-west-1/my-repo', config)
and:
provider.revision = 'dev1'
expect:
provider.readText('main.nf') == 'println "Hello world in dev branch!"\n'
}
def 'should fetch repo tags'() {
given:
def config = new AwsCodeCommitProviderConfig('git-codecommit.eu-west-1.amazonaws.com')
and:
def provider = new AwsCodeCommitRepositoryProvider('codecommit-eu-west-1/my-repo', config)
when:
// uses repo at
// https://git-codecommit.eu-west-1.amazonaws.com/v1/repos/my-repo
def result = provider.getTags() as Set
then:
result == [
new RepositoryProvider.TagInfo('v0.1', '1a88516b5e382d0d68bfa01c18eab6c2067c0595'),
new RepositoryProvider.TagInfo('v0.2', 'c673d3d55be190c54db2056690b71e285fe5b3d8')] as Set
}
def 'should fetch repo branches'() {
given:
def config = new AwsCodeCommitProviderConfig('git-codecommit.eu-west-1.amazonaws.com')
and:
def provider = new AwsCodeCommitRepositoryProvider('codecommit-eu-west-1/my-repo', config)
when:
// uses repo at
// https://git-codecommit.eu-west-1.amazonaws.com/v1/repos/my-repo
def result = provider.getBranches() as Set
then:
result == [
new RepositoryProvider.BranchInfo('master', 'c820e0904d9ce4404e005e3cc910502300b36ba3'),
new RepositoryProvider.BranchInfo('dev1', 'c90422a1b4823f1c0980bbf8cab261e45a351622')] as Set
}
def 'should list root directory contents'() {
given:
def config = new AwsCodeCommitProviderConfig('git-codecommit.eu-west-1.amazonaws.com')
def provider = new AwsCodeCommitRepositoryProvider('codecommit-eu-west-1/my-repo', config)
when:
def entries = provider.listDirectory("/", 1)
then:
entries.size() > 0
and:
entries.any { it.name == 'main.nf' && it.type == RepositoryProvider.EntryType.FILE }
and:
entries.every { it.path && it.name && it.sha }
// Should only include immediate children for depth=1
entries.every { it.path.split('/').length <= 2 }
}
def 'should list directory contents recursively'() {
given:
def config = new AwsCodeCommitProviderConfig('git-codecommit.eu-west-1.amazonaws.com')
def provider = new AwsCodeCommitRepositoryProvider('codecommit-eu-west-1/my-repo', config)
when:
def entries = provider.listDirectory("/", 10)
then:
entries.size() > 0
and:
// Should include files from root and potentially subdirectories
entries.any { it.name == 'main.nf' && it.type == RepositoryProvider.EntryType.FILE }
and:
entries.every { it.path && it.name && it.sha }
}
def 'should list directory contents with depth 2'() {
given:
def config = new AwsCodeCommitProviderConfig('git-codecommit.eu-west-1.amazonaws.com')
def provider = new AwsCodeCommitRepositoryProvider('codecommit-eu-west-1/my-repo', config)
when:
def depthOne = provider.listDirectory("/", 1)
def depthTwo = provider.listDirectory("/", 2)
then:
depthOne.size() > 0
depthTwo.size() >= depthOne.size()
and:
depthOne.every { it.path && it.name && it.sha }
depthTwo.every { it.path && it.name && it.sha }
}
}