Repository: jfrog/terraform-provider-artifactory

Issue:
Add 'releasebundles' federated repository support
**Context:** some organizations want to create federated release bundles, shared between two instances. When release bundle is created, Artifactory automatically creates local repository with package type `releasebundles` with name pattern `<project_name>-release-bundles-v2`, one repository per project. 
This local repository can be converted to a federated repository after creation, but if there are thousands of them it's not feasible. 
**Solution:** to create federated repository with package type `releasebundles` before actual release bundle is created for this project. 
It's impossible to manually create repository (local or federated) of this package type in the UI, but possible to do it with REST API. I've tested the e2e flow on RT 7.126 cloud, there is no conflict when you create repository first, then create release bundle v2. When use publish the release bundle it get successfully federated between the two instances. 
Repository name has a strict name convention - `<project_name>-release-bundles-v2` and attribute `project_key` must be set. If the `project_key` is empty, it will try to create the repository in teh `default` project and fail. So, even though we recommend to manage project repositories with a separate project provider, in this case we must set project in the repository resource. 

To address this issue I will add a new federated repository type. Using this ticket to track the progress. 

<img width="1215" height="182" alt="Image" src="https://github.com/user-attachments/assets/a49397ea-e387-4023-8c48-ff9cfa805580" />



Discussion:
@srinivasgowda097: Feature available in 12.11.0 release

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6cf42724..853faa49 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,13 @@
-### 12.11.0 (Nov 14 2025).
+### 12.11.0 (Nov 14 2025). Tested on Artifactory 7.125.6 with Terraform 1.13.5 and OpenTofu 1.10.7
 
 BUG FIXES: 
 
 * resource/artifactory_oauth_settings: Fix explicitly disable OAuth integration on resource deletion
 
+IMPROVEMENTS:
+
+* resource/artifactory_federated_releasebundles_repository: New resource added. Issue: [#1335](https://github.com/jfrog/terraform-provider-artifactory/issues/1335). PR: [#1340](https://github.com/jfrog/terraform-provider-artifactory/pull/1340).
+
 ### 12.10.2 (Oct 28, 2025). Tested on Artifactory 7.117.19 with Terraform 1.13.4 and OpenTofu 1.10.6
 
 BUG FIXES: 
diff --git a/docs/resources/federated_releasebundles_repository.md b/docs/resources/federated_releasebundles_repository.md
new file mode 100644
index 00000000..ac1fa4b5
--- /dev/null
+++ b/docs/resources/federated_releasebundles_repository.md
@@ -0,0 +1,62 @@
+---
+subcategory: "Federated Repositories"
+---
+# Artifactory Federated Release Bundles Repository Resource
+
+Creates a federated Release Bundles repository. This resource should be used only when you require release bundle federation and are using Artifactory Projects.
+It is not possible to create repository with this package type in the UI. 
+
+Default Behavior vs. Recommended Use
+**Default Behavior**: When you create the first release bundle within a project, Artifactory automatically provisions a local repository named `<project_name>-release-bundles-v2`.
+
+**The Problem**: This local repository must then be manually converted to a federated repository.
+
+**Recommended Solution**: To avoid this manual step (especially with hundred of projects), use this resource to create the federated `releasebundles` repository before creating any release bundles in the project.
+
+This proactive approach ensures all new release bundle assets are written directly to the federated repository.
+
+~>Note: Federation doesn't work correctly for environments, and if member repository will be created by the federation, it will only have one environment - `DEV` disregard on the values on the source repository. As a workaround, user can create repositories on both instances with correct environments, then modify the configuration by adding members to each of the repos on each instance. 
+
+## Example Usage
+
+```hcl
+resource "artifactory_federated_releasebundles_repository" "terraform-federated-test-releasebundles-repo" {
+  key           = "terraform-federated-test-releasebundles-repo"
+  project_key   = project.test.key
+
+  member {
+    url     = "https://tempurl.org/artifactory/project-release-bundles-v2"
+    enabled = true
+  }
+
+  member {
+    url     = "https://tempurl2.org/artifactory/project-release-bundles-v2"
+    enabled = true
+  }
+}
+```
+
+## Argument Reference
+
+The following attributes are supported, along with the [list of attributes from the local generic repository](local_generic_repository.md):
+
+* `key` - (Required) the identity key of the repo. Repository name for this package type myst chave following format: `<project_name>-release-bundles-v2`. Project myst exist before the repository was created. 
+* `project_key` - (Required) Project key for assigning this repository to. Must be 2 - 32 lowercase alphanumeric and hyphen characters. When assigning repository to a project, repository key must be prefixed with project key, separated by a dash. Even thought we don't recommend using this attribute to assign all other repository types to the project, in the scope of this specific package type, it is necessary to have theis attribute.
+* `member` - (Required) The list of Federated members and must contain this repository URL (configured base URL
+  `/artifactory/` + repo `key`). Note that each of the federated members will need to have a base URL set.
+  Please follow the [instruction](https://www.jfrog.com/confluence/display/JFROG/Working+with+Federated+Repositories#WorkingwithFederatedRepositories-SettingUpaFederatedRepository)
+  to set up Federated repositories correctly.
+    * `url` - (Required) Full URL to ending with the repository name.
+    * `enabled` - (Required) Represents the active state of the federated member. It is supported to change the enabled
+      status of my own member. The config will be updated on the other federated members automatically.
+    * `access_token` - (Optional) Admin access token for this member Artifactory instance. Used in conjunction with `cleanup_on_delete` attribute when Access Federation for access tokens is not enabled.
+* `cleanup_on_delete` - (Optional) Delete all federated members on `terraform destroy` if set to `true`. Default is `false`. This attribute is added to match Terrform logic, so all the resources, created by the provider, must be removed on cleanup. Artifactory's behavior for the federated repositories is different, all the federated repositories stay after the user deletes the initial federated repository. **Caution**: if set to `true` all the repositories in the federation will be deleted, including repositories on other Artifactory instances in the "Circle of trust". This operation can not be reversed.
+* `proxy` - (Optional) Proxy key from Artifactory Proxies settings. Default is empty field. Can't be set if `disable_proxy = true`.
+* `disable_proxy` - (Optional, Default: `false`) When set to `true`, the proxy is disabled, and not returned in the API response body. If there is a default proxy set for the Artifactory instance, it will be ignored, too.
+
+## Import
+
+Federated repositories can be imported using their name, e.g.
+```
+$ terraform import artifactory_federated_rpm_repository.terraform-federated-test-rpm-repo terraform-federated-test-rpm-repo
+```
diff --git a/pkg/artifactory/resource/repository/default_repo_layout_map.go b/pkg/artifactory/resource/repository/default_repo_layout_map.go
index 6e46de60..64d93c75 100644
--- a/pkg/artifactory/resource/repository/default_repo_layout_map.go
+++ b/pkg/artifactory/resource/repository/default_repo_layout_map.go
@@ -280,6 +280,13 @@ var defaultRepoLayoutMap = map[string]SupportedRepoClasses{
 			"federated": true,
 		},
 	},
+	ReleasebundlesPackageType: {
+		RepoLayoutRef: "maven-2-default",
+		SupportedRepoTypes: map[string]bool{
+			"local":     true,
+			"federated": true,
+		},
+	},
 	RPMPackageType: {
 		RepoLayoutRef: "simple-default",
 		SupportedRepoTypes: map[string]bool{
diff --git a/pkg/artifactory/resource/repository/federated/federated.go b/pkg/artifactory/resource/repository/federated/federated.go
index 5e6d08d0..c2313ef1 100644
--- a/pkg/artifactory/resource/repository/federated/federated.go
+++ b/pkg/artifactory/resource/repository/federated/federated.go
@@ -42,6 +42,7 @@ var PackageTypesLikeGeneric = []string{
 	repository.PyPiPackageType,
 	repository.SwiftPackageType,
 	repository.VagrantPackageType,
+	repository.ReleasebundlesPackageType,
 }
 
 type RepoParams struct {
@@ -256,9 +257,15 @@ func createRepo(unpack unpacker.UnpackFunc, read schema.ReadContextFunc) schema.
 			return ds
 		}
 
-		ds = append(ds, configSync(ctx, d.Id(), m)...)
-		if ds.HasError() {
-			return ds
+		// Skip synchronization for `releasebundles` package type to prevent creation of the member repo on target.
+		// There is a bug on 7.126, which prevents correct sync for environments, and only DEV will be added, if the
+		// repo on the target instance will be created by federation.
+		packageType, ok := d.GetOk("package_type")
+		if !ok || packageType.(string) != repository.ReleasebundlesPackageType {
+			ds = append(ds, configSync(ctx, d.Id(), m)...)
+			if ds.HasError() {
+				return ds
+			}
 		}
 
 		return append(ds, read(ctx, d, m)...)
@@ -414,6 +421,7 @@ func mkResourceSchema(skeema map[string]*schema.Schema, packer packer.PackFunc,
 		CustomizeDiff: customdiff.All(
 			repository.ProjectEnvironmentsDiff,
 			repository.VerifyDisableProxy,
+			repository.VerifyReleasebundlesKey,
 		),
 	}
 }
diff --git a/pkg/artifactory/resource/repository/federated/resource_artifactory_federated_releasebundles_repository.go b/pkg/artifactory/resource/repository/federated/resource_artifactory_federated_releasebundles_repository.go
new file mode 100644
index 00000000..190b1680
--- /dev/null
+++ b/pkg/artifactory/resource/repository/federated/resource_artifactory_federated_releasebundles_repository.go
@@ -0,0 +1,59 @@
+package federated
+
+import (
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/repository"
+	"github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/repository/local"
+	"github.com/jfrog/terraform-provider-shared/packer"
+	"github.com/jfrog/terraform-provider-shared/predicate"
+	"github.com/samber/lo"
+)
+
+type ReleasebundlesRepositoryParams struct {
+	local.RepositoryBaseParams
+	Members []Member `hcl:"member" json:"members"`
+	RepoParams
+}
+
+func ResourceArtifactoryFederatedReleasebundlesRepository(packageType string) *schema.Resource {
+	var genericSchema = lo.Assign(
+		local.GetGenericSchemas(packageType)[local.CurrentSchemaVersion],
+		federatedSchemaV4,
+		repository.RepoLayoutRefSDKv2Schema(Rclass, packageType),
+	)
+
+	var unpackFederatedRepository = func(data *schema.ResourceData) (interface{}, string, error) {
+		repo := ReleasebundlesRepositoryParams{
+			RepositoryBaseParams: local.UnpackBaseRepo(Rclass, data, packageType),
+			Members:              unpackMembers(data),
+			RepoParams:           unpackRepoParams(data),
+		}
+		return repo, repo.Id(), nil
+	}
+
+	var packGenericMembers = func(repo interface{}, d *schema.ResourceData) error {
+		members := repo.(*ReleasebundlesRepositoryParams).Members
+		return PackMembers(members, d)
+	}
+
+	pkr := packer.Compose(
+		packer.Universal(
+			predicate.All(
+				predicate.NoClass,
+				predicate.Ignore("member", "terraform_type"),
+			),
+		),
+		packGenericMembers,
+	)
+
+	constructor := func() (interface{}, error) {
+		return &ReleasebundlesRepositoryParams{
+			RepositoryBaseParams: local.RepositoryBaseParams{
+				PackageType: local.GetPackageType(packageType),
+				Rclass:      Rclass,
+			},
+		}, nil
+	}
+
+	return mkResourceSchema(genericSchema, pkr, unpackFederatedRepository, constructor)
+}
diff --git a/pkg/artifactory/resource/repository/repository.go b/pkg/artifactory/resource/repository/repository.go
index 0c7cd1bc..73d7e104 100644
--- a/pkg/artifactory/resource/repository/repository.go
+++ b/pkg/artifactory/resource/repository/repository.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"net/http"
 	"reflect"
+	"strings"
 
 	"github.com/go-resty/resty/v2"
 	"github.com/hashicorp/go-cty/cty"
@@ -23,6 +24,7 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/types"
 	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
 	sdkv2_diag "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
 	sdkv2_schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
 	"github.com/jfrog/terraform-provider-shared/client"
@@ -70,6 +72,7 @@ const (
 	PubPackageType               = "pub"
 	PuppetPackageType            = "puppet"
 	PyPiPackageType              = "pypi"
+	ReleasebundlesPackageType    = "releasebundles"
 	RPMPackageType               = "rpm"
 	SBTPackageType               = "sbt"
 	SwiftPackageType             = "swift"
@@ -1001,6 +1004,30 @@ func VerifyDisableProxy(_ context.Context, diff *sdkv2_schema.ResourceDiff, _ in
 	return nil
 }
 
+func VerifyReleasebundlesKey(_ context.Context, diff *sdkv2_schema.ResourceDiff, _ interface{}) error {
+	packageType, ok := diff.GetOk("package_type")
+	if !ok {
+		// package_type might not be set yet, skip validation
+		return nil
+	}
+
+	if packageType.(string) == ReleasebundlesPackageType {
+		key, ok := diff.GetOk("key")
+		if !ok {
+			// key might not be set yet, skip validation
+			return nil
+		}
+
+		keyStr := key.(string)
+		requiredSuffix := "release-bundles-v2"
+		if !strings.HasSuffix(keyStr, requiredSuffix) {
+			return fmt.Errorf("when `package_type` is `%s`, the `key` attribute must end with `%s`, got: `%s`", ReleasebundlesPackageType, requiredSuffix, keyStr)
+		}
+	}
+
+	return nil
+}
+
 func MkResourceSchema(skeemas map[int16]map[string]*sdkv2_schema.Schema, packer packer.PackFunc, unpack unpacker.UnpackFunc, constructor Constructor) *sdkv2_schema.Resource {
 	var reader = MkRepoRead(packer, constructor)
 	return &sdkv2_schema.Resource{
@@ -1026,7 +1053,10 @@ func MkResourceSchema(skeemas map[int16]map[string]*sdkv2_schema.Schema, packer
 			},
 		},
 
-		CustomizeDiff: ProjectEnvironmentsDiff,
+		CustomizeDiff: customdiff.All(
+			ProjectEnvironmentsDiff,
+			VerifyReleasebundlesKey,
+		),
 	}
 }
 



Test patch:
diff --git a/pkg/artifactory/resource/repository/federated/resource_artifactory_federated_repository_test.go b/pkg/artifactory/resource/repository/federated/resource_artifactory_federated_repository_test.go
index 5f9cce45..90909b85 100644
--- a/pkg/artifactory/resource/repository/federated/resource_artifactory_federated_repository_test.go
+++ b/pkg/artifactory/resource/repository/federated/resource_artifactory_federated_repository_test.go
@@ -109,7 +109,7 @@ func TestAccFederatedRepoWithMembers(t *testing.T) {
 				ImportState:             true,
 				ImportStateVerify:       true,
 				ImportStateCheck:        validator.CheckImportState(name, "key"),
-				ImportStateVerifyIgnore: []string{"cleanup_on_delete"},
+				ImportStateVerifyIgnore: []string{"cleanup_on_delete", "member.0.access_token", "member.1.access_token"},
 			},
 		},
 	})
@@ -246,6 +246,10 @@ func genericTestCase(repoType string, t *testing.T) (*testing.T, resource.TestCa
 
 func TestAccFederatedRepoGenericTypes(t *testing.T) {
 	for _, packageType := range federated.PackageTypesLikeGeneric {
+		// Skip releasebundles as it has its own dedicated test and repo name convention is different for this type
+		if packageType == repository.ReleasebundlesPackageType {
+			continue
+		}
 		t.Run(packageType, func(t *testing.T) {
 			resource.Test(genericTestCase(packageType, t))
 		})
@@ -1544,3 +1548,166 @@ func TestAccFederatedTerraformRepositories(t *testing.T) {
 		})
 	}
 }
+
+func TestAccFederatedReleasebundlesRepository(t *testing.T) {
+	if skip, reason := skipFederatedRepo(); skip {
+		t.Skip(reason)
+	}
+
+	projectName := fmt.Sprintf("t%d", testutil.RandomInt())
+	projectKey := projectName
+	projectEnv := testutil.RandSelect("DEV", "PROD").(string)
+	repoName := fmt.Sprintf("%s-release-bundles-v2", projectName)
+
+	_, fqrn, name := testutil.MkNames(repoName, "artifactory_federated_releasebundles_repository")
+	projectFqrn := "project.test"
+	federatedMember1Url := fmt.Sprintf("%s/artifactory/%s", acctest.GetArtifactoryUrl(t), repoName)
+	federatedMember2Url := fmt.Sprintf("%s/artifactory/%s", os.Getenv("ARTIFACTORY_URL_2"), repoName)
+
+	configWithOneMember := util.ExecuteTemplate("TestAccFederatedReleasebundlesRepositoryConfig", `
+		terraform {
+			required_providers {
+				project = {
+					source  = "jfrog/project"
+					version = "~> 1.9"
+				}
+			}
+		}
+
+		resource "project" "test" {
+			  key          = "{{ .projectKey }}"
+			  display_name = "Test Project {{ .projectKey }}"
+			  description  = "Test project for federated releasebundles repository"
+			  admin_privileges {
+				manage_members   = true
+				manage_resources = true
+				index_resources  = true
+			  }
+			  max_storage_in_gibibytes   = 10
+			  block_deployments_on_limit = false
+			  email_notification         = true
+		}
+
+		resource "artifactory_federated_releasebundles_repository" "{{ .name }}" {
+			key                  = "{{ .repoName }}"
+			description          = "Test federated releasebundles repo for {{ .repoName }}"
+			notes                = "Test federated releasebundles repo for {{ .repoName }}"
+			project_key          = project.test.key
+			project_environments = ["{{ .projectEnv }}"]
+
+			member {
+				url     = "{{ .member1Url }}"
+				enabled = true
+			}
+		}
+	`, map[string]interface{}{
+		"name":       name,
+		"repoName":   repoName,
+		"projectKey": projectKey,
+		"projectEnv": projectEnv,
+		"member1Url": federatedMember1Url,
+	})
+
+	configWithTwoMembers := util.ExecuteTemplate("TestAccFederatedReleasebundlesRepositoryConfig", `
+		terraform {
+			required_providers {
+				project = {
+					source  = "jfrog/project"
+					version = "~> 1.9"
+				}
+			}
+		}
+	
+		resource "project" "test" {
+			  key          = "{{ .projectKey }}"
+			  display_name = "Test Project {{ .projectKey }}"
+			  description  = "Test project for federated releasebundles repository"
+			  admin_privileges {
+				manage_members   = true
+				manage_resources = true
+				index_resources  = true
+			  }
+			  max_storage_in_gibibytes   = 10
+			  block_deployments_on_limit = false
+			  email_notification         = true
+		}
+	
+		resource "artifactory_federated_releasebundles_repository" "{{ .name }}" {
+			key                  = "{{ .repoName }}"
+			description          = "Test federated releasebundles repo for {{ .repoName }}"
+			notes                = "Test federated releasebundles repo for {{ .repoName }}"
+			project_key          = project.test.key
+			project_environments = ["{{ .projectEnv }}"]
+	
+			member {
+				url     = "{{ .member1Url }}"
+				enabled = true
+			}
+	
+			member {
+				url     = "{{ .member2Url }}"
+				enabled = true
+			}
+		}
+	`, map[string]interface{}{
+		"name":       name,
+		"repoName":   repoName,
+		"projectKey": projectKey,
+		"projectEnv": projectEnv,
+		"member1Url": federatedMember1Url,
+		"member2Url": federatedMember2Url,
+	})
+
+	resource.Test(t, resource.TestCase{
+		PreCheck:          func() { acctest.PreCheck(t) },
+		ProviderFactories: acctest.ProviderFactories,
+		ExternalProviders: map[string]resource.ExternalProvider{
+			"project": {
+				VersionConstraint: "~> 1.9",
+				Source:            "jfrog/project",
+			},
+		},
+		CheckDestroy: acctest.VerifyDeleted(t, fqrn, "key", acctest.CheckRepo),
+		Steps: []resource.TestStep{
+			{
+				Config: configWithOneMember,
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr(projectFqrn, "key", projectKey),
+					resource.TestCheckResourceAttr(fqrn, "key", repoName),
+					resource.TestCheckResourceAttr(fqrn, "package_type", repository.ReleasebundlesPackageType),
+					resource.TestCheckResourceAttr(fqrn, "description", fmt.Sprintf("Test federated releasebundles repo for %s", repoName)),
+					resource.TestCheckResourceAttr(fqrn, "notes", fmt.Sprintf("Test federated releasebundles repo for %s", repoName)),
+					resource.TestCheckResourceAttr(fqrn, "project_key", projectKey),
+					resource.TestCheckResourceAttr(fqrn, "project_environments.#", "1"),
+					resource.TestCheckResourceAttr(fqrn, "project_environments.0", projectEnv),
+					resource.TestCheckResourceAttr(fqrn, "member.#", "1"),
+					resource.TestCheckResourceAttr(fqrn, "member.0.url", federatedMember1Url),
+					resource.TestCheckResourceAttr(fqrn, "member.0.enabled", "true"),
+				),
+			},
+			{
+				Config: configWithTwoMembers,
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr(projectFqrn, "key", projectKey),
+					resource.TestCheckResourceAttr(fqrn, "key", repoName),
+					resource.TestCheckResourceAttr(fqrn, "package_type", repository.ReleasebundlesPackageType),
+					resource.TestCheckResourceAttr(fqrn, "project_key", projectKey),
+					resource.TestCheckResourceAttr(fqrn, "project_environments.#", "1"),
+					resource.TestCheckResourceAttr(fqrn, "project_environments.0", projectEnv),
+					resource.TestCheckResourceAttr(fqrn, "member.#", "2"),
+					resource.TestCheckResourceAttr(fqrn, "member.0.enabled", "true"),
+					resource.TestCheckResourceAttr(fqrn, "member.1.enabled", "true"),
+					resource.TestMatchResourceAttr(fqrn, "member.0.url", regexp.MustCompile(regexp.QuoteMeta(federatedMember1Url)+"|"+regexp.QuoteMeta(federatedMember2Url))),
+					resource.TestMatchResourceAttr(fqrn, "member.1.url", regexp.MustCompile(regexp.QuoteMeta(federatedMember1Url)+"|"+regexp.QuoteMeta(federatedMember2Url))),
+				),
+			},
+			{
+				ResourceName:            fqrn,
+				ImportState:             true,
+				ImportStateVerify:       true,
+				ImportStateCheck:        validator.CheckImportState(repoName, "key"),
+				ImportStateVerifyIgnore: []string{"cleanup_on_delete", "member.0.access_token", "member.1.access_token"},
+			},
+		},
+	})
+}