Repository: erpc/erpc
Pull request #264: feat: add tenderly vendor
Author: login: zekiblue
Created: 2025-05-06T16:15:51Z | Merged: 2025-05-08T09:17:25Z

- Adds tenderly vendor


Diff:
diff --git a/common/defaults.go b/common/defaults.go
index 5f9123313..52672c817 100644
--- a/common/defaults.go
+++ b/common/defaults.go
@@ -976,6 +976,10 @@ func buildProviderSettings(vendorName string, endpoint *url.URL) (VendorSettings
 		return VendorSettings{
 			"registryUrl": spec,
 		}, nil
+	case "tenderly", "evm+tenderly":
+		return VendorSettings{
+			"apiKey": endpoint.Host,
+		}, nil
 	case "repository", "evm+repository":
 		return VendorSettings{
 			"repositoryUrl": "https://" + endpoint.Host + "/" + strings.TrimPrefix(endpoint.Path, "/") + "?" + endpoint.RawQuery,
diff --git a/docs/pages/config/projects/providers.mdx b/docs/pages/config/projects/providers.mdx
index cbf8f08b4..e538c97cc 100644
--- a/docs/pages/config/projects/providers.mdx
+++ b/docs/pages/config/projects/providers.mdx
@@ -512,6 +512,46 @@ export default createConfig({
 </Tabs.Tab>
 </Tabs>
 
+#### `tenderly`
+
+Built for [Tenderly](https://tenderly.co) 3rd-party provider to make it easier to import "all supported evm chains" with just an API key.
+
+<Tabs items={["yaml", "typescript"]} defaultIndex={0} storageKey="GlobalConfigTypeTabIndex">
+  <Tabs.Tab>
+```yaml filename="erpc.yaml"
+# ...
+projects:
+  - id: main
+    # ...
+    upstreams:
+      - endpoint: tenderly://YOUR_TENDERLY_API_KEY
+        # ...
+```
+</Tabs.Tab>
+  <Tabs.Tab>
+```ts filename="erpc.ts"
+import { createConfig } from "@erpc-cloud/config";
+
+export default createConfig({
+  projects: [
+    {
+      id: "main",
+      // ...
+      upstreams: [
+        {
+          endpoint: "tenderly://YOUR_TENDERLY_API_KEY",
+          // ...
+        },
+      ],
+    },
+  ],
+});
+```
+</Tabs.Tab>
+</Tabs>
+
+For production traffic consult with Tenderly team about the chains you are going to use and amount of traffic you expect to handle.
+
 ## Advanced config
 
 You can use dedicated `providers:` config to customize per-network configurations (e.g. different config for Alchemy eth-mainnet vs Alchemy polygon) as follows:
@@ -658,4 +698,7 @@ providers:
     settings:
       registryUrl: "github.com/ethereum-optimism/superchain-registry/main/chainList.json"
       recheckInterval: 24h # (optional) How often to recheck the registry for newly added chains (default: 24h)
+  - vendor: tenderly
+    settings:
+      apiKey: xxxxx # Your Tenderly API key
 ```
diff --git a/erpc.dist.yaml b/erpc.dist.yaml
index 5a7384575..86fe8225f 100644
--- a/erpc.dist.yaml
+++ b/erpc.dist.yaml
@@ -93,6 +93,20 @@ projects:
             backoffMaxDelay: 10s
             backoffFactor: 0.3
             jitter: 500ms
+      - id: tenderly-example-1
+        endpoint: tenderly://YOUR_TENDERLY_API_KEY
+        rateLimitBudget: global-tenderly
+        evm:
+          chainId: 1
+        failsafe:
+          timeout:
+            duration: 15s
+          retry:
+            maxAttempts: 2
+            delay: 1000ms
+            backoffMaxDelay: 10s
+            backoffFactor: 0.3
+            jitter: 500ms
       - id: infura-multi-chain-example-1
         endpoint: infura://${YOUR_INFURA_API_KEY}
         rateLimitBudget: default-budget
@@ -163,6 +177,11 @@ projects:
             jitter: 500ms
 rateLimiters:
   budgets:
+    - id: global-tenderly
+      rules:
+        - method: '*'
+          maxCount: 10000
+          period: 1s
     - id: default-budget
       rules:
         - method: '*'
diff --git a/thirdparty/tenderly.go b/thirdparty/tenderly.go
new file mode 100644
index 000000000..f6fe86ff9
--- /dev/null
+++ b/thirdparty/tenderly.go
@@ -0,0 +1,138 @@
+package thirdparty
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+
+	"github.com/erpc/erpc/common"
+	"github.com/rs/zerolog"
+)
+
+var tenderlyNetworkNames = map[int64]string{
+	1:         "mainnet",
+	10:        "optimism",
+	100:       "gnosis",
+	130:       "unichain",
+	10200:     "chiado",
+	1088:      "metis",
+	1101:      "polygon-zkevm",
+	11155111:  "sepolia",
+	11155420:  "optimism-sepolia",
+	137:       "polygon",
+	168587773: "blast-sepolia",
+	17000:     "holesky",
+	204:       "opbnb",
+	2442:      "polygon-zkevm-cardona",
+	250:       "fantom",
+	300:       "zksync-sepolia",
+	324:       "zksync",
+	4002:      "fantom-testnet",
+	42161:     "arbitrum",
+	421614:    "arbitrum-sepolia",
+	42170:     "arbitrum-nova",
+	43113:     "avalanche-fuji",
+	43114:     "avalanche",
+	5000:      "mantle",
+	56:        "bsc",
+	5611:      "opbnb-testnet",
+	59141:     "linea-sepolia",
+	59144:     "linea",
+	592:       "astar",
+	7000:      "zetachain",
+	7001:      "zetachain-testnet",
+	7777777:   "zora",
+	80002:     "polygon-amoy",
+	81457:     "blast",
+	8453:      "base",
+	84532:     "base-sepolia",
+	97:        "bsc-testnet",
+	999999999: "zora-sepolia",
+	534351:    "scroll-sepolia",
+	534352:    "scroll",
+}
+
+type TenderlyVendor struct {
+	common.Vendor
+}
+
+func CreateTenderlyVendor() common.Vendor {
+	return &TenderlyVendor{}
+}
+
+func (v *TenderlyVendor) Name() string {
+	return "tenderly"
+}
+
+func (v *TenderlyVendor) SupportsNetwork(ctx context.Context, logger *zerolog.Logger, settings common.VendorSettings, networkId string) (bool, error) {
+	if !strings.HasPrefix(networkId, "evm:") {
+		return false, nil
+	}
+
+	chainID, err := strconv.ParseInt(strings.TrimPrefix(networkId, "evm:"), 10, 64)
+	if err != nil {
+		return false, err
+	}
+	_, ok := tenderlyNetworkNames[chainID]
+	return ok, nil
+}
+
+func (v *TenderlyVendor) GenerateConfigs(upstream *common.UpstreamConfig, settings common.VendorSettings) ([]*common.UpstreamConfig, error) {
+	if upstream.JsonRpc == nil {
+		upstream.JsonRpc = &common.JsonRpcUpstreamConfig{}
+	}
+
+	if upstream.Endpoint == "" {
+		if apiKey, ok := settings["apiKey"].(string); ok && apiKey != "" {
+			if upstream.Evm == nil {
+				return nil, fmt.Errorf("tenderly vendor requires upstream.evm to be defined")
+			}
+			chainID := upstream.Evm.ChainId
+			if chainID == 0 {
+				return nil, fmt.Errorf("tenderly vendor requires upstream.evm.chainId to be defined")
+			}
+			netName, ok := tenderlyNetworkNames[chainID]
+			if !ok {
+				return nil, fmt.Errorf("unsupported network chain ID for Tenderly: %d", chainID)
+			}
+			tenderlyURL := fmt.Sprintf("https://%s.gateway.tenderly.co/%s", netName, apiKey)
+			parsedURL, err := url.Parse(tenderlyURL)
+			if err != nil {
+				return nil, err
+			}
+
+			upstream.Endpoint = parsedURL.String()
+			upstream.Type = common.UpstreamTypeEvm
+		} else {
+			return nil, fmt.Errorf("apiKey is required in tenderly settings")
+		}
+	}
+
+	return []*common.UpstreamConfig{upstream}, nil
+}
+
+func (v *TenderlyVendor) GetVendorSpecificErrorIfAny(req *common.NormalizedRequest, resp *http.Response, jrr interface{}, details map[string]interface{}) error {
+	bodyMap, ok := jrr.(*common.JsonRpcResponse)
+	if !ok {
+		return nil
+	}
+
+	err := bodyMap.Error
+	if err.Data != "" {
+		details["data"] = err.Data
+	}
+
+	// Other errors can be properly handled by generic error handling
+	return nil
+}
+
+func (v *TenderlyVendor) OwnsUpstream(ups *common.UpstreamConfig) bool {
+	if strings.HasPrefix(ups.Endpoint, "tenderly://") || strings.HasPrefix(ups.Endpoint, "evm+tenderly://") {
+		return true
+	}
+
+	return strings.Contains(ups.Endpoint, ".gateway.tenderly.co")
+}
\ No newline at end of file
diff --git a/thirdparty/vendors_registry.go b/thirdparty/vendors_registry.go
index c73d2b8af..74319f23e 100644
--- a/thirdparty/vendors_registry.go
+++ b/thirdparty/vendors_registry.go
@@ -23,6 +23,7 @@ func NewVendorsRegistry() *VendorsRegistry {
 	r.Register(CreateThirdwebVendor())
 	r.Register(CreateRepositoryVendor())
 	r.Register(CreateSuperchainVendor())
+	r.Register(CreateTenderlyVendor())
 
 	return r
 }
diff --git a/typescript/config/lib/types/generic.d.ts b/typescript/config/lib/types/generic.d.ts
index c8d0552f7..4be156a57 100644
--- a/typescript/config/lib/types/generic.d.ts
+++ b/typescript/config/lib/types/generic.d.ts
@@ -43,7 +43,7 @@ export type ConnectorConfig = {
 /**
  * Supported upstream type
  */
-export type UpstreamType = "evm" | "evm+alchemy" | "evm+drpc" | "evm+blastapi" | "evm+envio" | "evm+etherspot" | "evm+infura" | "evm+pimlico" | "evm+thirdweb";
+export type UpstreamType = "evm" | "evm+alchemy" | "evm+drpc" | "evm+blastapi" | "evm+envio" | "evm+etherspot" | "evm+infura" | "evm+pimlico" | "evm+thirdweb" | "evm+tenderly";
 /**
  * Supported auth type
  */


Reviews:
user: login: aramalipoor
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T17:06:29Z
user: login: aramalipoor
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T17:07:53Z
user: login: aramalipoor
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T17:10:25Z
user: login: zekiblue
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T08:12:02Z
user: login: aramalipoor
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-08T07:56:09Z
user: login: aramalipoor
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T08:46:36Z
user: login: aramalipoor
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T08:46:55Z
user: login: zekiblue
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T08:48:08Z

Review comments:
diff_hunk: @@ -93,6 +93,20 @@ projects:
             backoffMaxDelay: 10s
             backoffFactor: 0.3
             jitter: 500ms
+      - id: tenderly-chain-example-1
+        endpoint: https://mainnet.tenderly.co/xxxxxxxxxxxxxxxxxxxxxx
path: erpc.dist.yaml
user: login: aramalipoor
body: ```suggestion
      - id: tenderly-example-1
        endpoint: tenderly://YOUR_TENDERLY_API_KEY
```

We could suggest the provider in this example
created_at: 2025-05-06T17:06:29Z
author_association: MEMBER
diff_hunk: @@ -93,6 +93,20 @@ projects:
             backoffMaxDelay: 10s
             backoffFactor: 0.3
             jitter: 500ms
+      - id: tenderly-chain-example-1
+        endpoint: https://mainnet.tenderly.co/xxxxxxxxxxxxxxxxxxxxxx
+        rateLimitBudget: global
path: erpc.dist.yaml
user: login: aramalipoor
body: ```suggestion
        rateLimitBudget: global-tenderly
```

So that user can define a rate limit based on their Tenderly plan. We'd need to add the rate limit budget at the bottom of this yaml as well.
created_at: 2025-05-06T17:07:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,138 @@
+package thirdparty
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+
+	"github.com/erpc/erpc/common"
+	"github.com/rs/zerolog"
+)
+
+var tenderlyNetworkNames = map[int64]string{
+	1:         "mainnet",
+	10:        "optimism",
+	100:       "gnosis",
+	130:       "unichain",
+	10200:     "chiado",
+	1088:      "metis",
+	1101:      "polygon-zkevm",
+	11155111:  "sepolia",
+	11155420:  "optimism-sepolia",
+	137:       "polygon",
+	168587773: "blast-sepolia",
+	17000:     "holesky",
+	204:       "opbnb",
+	2442:      "polygon-zkevm-cardona",
+	250:       "fantom",
+	300:       "zksync-sepolia",
+	324:       "zksync",
+	4002:      "fantom-testnet",
+	42161:     "arbitrum",
+	421614:    "arbitrum-sepolia",
+	42170:     "arbitrum-nova",
+	43113:     "avalanche-fuji",
+	43114:     "avalanche",
+	5000:      "mantle",
+	56:        "bsc",
+	5611:      "opbnb-testnet",
+	59141:     "linea-sepolia",
+	59144:     "linea",
+	592:       "astar",
+	7000:      "zetachain",
+	7001:      "zetachain-testnet",
+	7777777:   "zora",
+	80002:     "polygon-amoy",
+	81457:     "blast",
+	8453:      "base",
+	84532:     "base-sepolia",
+	97:        "bsc-testnet",
+	999999999: "zora-sepolia",
+	534351:    "scroll-sepolia",
+	534352:    "scroll",
+}
+
+type TenderlyVendor struct {
+	common.Vendor
+}
+
+func CreateTenderlyVendor() common.Vendor {
+	return &TenderlyVendor{}
+}
+
+func (v *TenderlyVendor) Name() string {
+	return "tenderly"
+}
+
+func (v *TenderlyVendor) SupportsNetwork(ctx context.Context, logger *zerolog.Logger, settings common.VendorSettings, networkId string) (bool, error) {
+	if !strings.HasPrefix(networkId, "evm:") {
+		return false, nil
+	}
+
+	chainID, err := strconv.ParseInt(strings.TrimPrefix(networkId, "evm:"), 10, 64)
+	if err != nil {
+		return false, err
+	}
+	_, ok := tenderlyNetworkNames[chainID]
+	return ok, nil
+}
+
+func (v *TenderlyVendor) GenerateConfigs(upstream *common.UpstreamConfig, settings common.VendorSettings) ([]*common.UpstreamConfig, error) {
+	if upstream.JsonRpc == nil {
+		upstream.JsonRpc = &common.JsonRpcUpstreamConfig{}
+	}
+
+	if upstream.Endpoint == "" {
+		if apiKey, ok := settings["apiKey"].(string); ok && apiKey != "" {
+			if upstream.Evm == nil {
+				return nil, fmt.Errorf("tenderly vendor requires upstream.evm to be defined")
+			}
+			chainID := upstream.Evm.ChainId
+			if chainID == 0 {
+				return nil, fmt.Errorf("tenderly vendor requires upstream.evm.chainId to be defined")
+			}
+			netName, ok := tenderlyNetworkNames[chainID]
+			if !ok {
+				return nil, fmt.Errorf("unsupported network chain ID for Tenderly: %d", chainID)
+			}
+			tenderlyURL := fmt.Sprintf("https://%s.gateway.tenderly.co/%s", netName, apiKey)
+			parsedURL, err := url.Parse(tenderlyURL)
+			if err != nil {
+				return nil, err
+			}
+
+			upstream.Endpoint = parsedURL.String()
+			upstream.Type = common.UpstreamTypeEvm
+		} else {
+			return nil, fmt.Errorf("apiKey is required in tenderly settings")
+		}
+	}
+
+	return []*common.UpstreamConfig{upstream}, nil
+}
+
+func (v *TenderlyVendor) GetVendorSpecificErrorIfAny(req *common.NormalizedRequest, resp *http.Response, jrr interface{}, details map[string]interface{}) error {
+	bodyMap, ok := jrr.(*common.JsonRpcResponse)
+	if !ok {
+		return nil
+	}
+
+	err := bodyMap.Error
+	if err.Data != "" {
+		details["data"] = err.Data
+	}
+
+	// Other errors can be properly handled by generic error handling
+	return nil
+}
+
+func (v *TenderlyVendor) OwnsUpstream(ups *common.UpstreamConfig) bool {
+	if strings.HasPrefix(ups.Endpoint, "tenderly://") || strings.HasPrefix(ups.Endpoint, "evm+tenderly://") {
+		return true
+	}
+
+	return strings.Contains(ups.Endpoint, ".gateway.tenderly.co")
path: thirdparty/tenderly.go
user: login: aramalipoor
body: ```suggestion
	return strings.Contains(ups.Endpoint, ".tenderly.co")
```
Not sure if that's the only domain supported, maybe safer to include the top-level, wdyt?
created_at: 2025-05-06T17:10:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,138 @@
+package thirdparty
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+
+	"github.com/erpc/erpc/common"
+	"github.com/rs/zerolog"
+)
+
+var tenderlyNetworkNames = map[int64]string{
+	1:         "mainnet",
+	10:        "optimism",
+	100:       "gnosis",
+	130:       "unichain",
+	10200:     "chiado",
+	1088:      "metis",
+	1101:      "polygon-zkevm",
+	11155111:  "sepolia",
+	11155420:  "optimism-sepolia",
+	137:       "polygon",
+	168587773: "blast-sepolia",
+	17000:     "holesky",
+	204:       "opbnb",
+	2442:      "polygon-zkevm-cardona",
+	250:       "fantom",
+	300:       "zksync-sepolia",
+	324:       "zksync",
+	4002:      "fantom-testnet",
+	42161:     "arbitrum",
+	421614:    "arbitrum-sepolia",
+	42170:     "arbitrum-nova",
+	43113:     "avalanche-fuji",
+	43114:     "avalanche",
+	5000:      "mantle",
+	56:        "bsc",
+	5611:      "opbnb-testnet",
+	59141:     "linea-sepolia",
+	59144:     "linea",
+	592:       "astar",
+	7000:      "zetachain",
+	7001:      "zetachain-testnet",
+	7777777:   "zora",
+	80002:     "polygon-amoy",
+	81457:     "blast",
+	8453:      "base",
+	84532:     "base-sepolia",
+	97:        "bsc-testnet",
+	999999999: "zora-sepolia",
+	534351:    "scroll-sepolia",
+	534352:    "scroll",
+}
+
+type TenderlyVendor struct {
+	common.Vendor
+}
+
+func CreateTenderlyVendor() common.Vendor {
+	return &TenderlyVendor{}
+}
+
+func (v *TenderlyVendor) Name() string {
+	return "tenderly"
+}
+
+func (v *TenderlyVendor) SupportsNetwork(ctx context.Context, logger *zerolog.Logger, settings common.VendorSettings, networkId string) (bool, error) {
+	if !strings.HasPrefix(networkId, "evm:") {
+		return false, nil
+	}
+
+	chainID, err := strconv.ParseInt(strings.TrimPrefix(networkId, "evm:"), 10, 64)
+	if err != nil {
+		return false, err
+	}
+	_, ok := tenderlyNetworkNames[chainID]
+	return ok, nil
+}
+
+func (v *TenderlyVendor) GenerateConfigs(upstream *common.UpstreamConfig, settings common.VendorSettings) ([]*common.UpstreamConfig, error) {
+	if upstream.JsonRpc == nil {
+		upstream.JsonRpc = &common.JsonRpcUpstreamConfig{}
+	}
+
+	if upstream.Endpoint == "" {
+		if apiKey, ok := settings["apiKey"].(string); ok && apiKey != "" {
+			if upstream.Evm == nil {
+				return nil, fmt.Errorf("tenderly vendor requires upstream.evm to be defined")
+			}
+			chainID := upstream.Evm.ChainId
+			if chainID == 0 {
+				return nil, fmt.Errorf("tenderly vendor requires upstream.evm.chainId to be defined")
+			}
+			netName, ok := tenderlyNetworkNames[chainID]
+			if !ok {
+				return nil, fmt.Errorf("unsupported network chain ID for Tenderly: %d", chainID)
+			}
+			tenderlyURL := fmt.Sprintf("https://%s.gateway.tenderly.co/%s", netName, apiKey)
+			parsedURL, err := url.Parse(tenderlyURL)
+			if err != nil {
+				return nil, err
+			}
+
+			upstream.Endpoint = parsedURL.String()
+			upstream.Type = common.UpstreamTypeEvm
+		} else {
+			return nil, fmt.Errorf("apiKey is required in tenderly settings")
+		}
+	}
+
+	return []*common.UpstreamConfig{upstream}, nil
+}
+
+func (v *TenderlyVendor) GetVendorSpecificErrorIfAny(req *common.NormalizedRequest, resp *http.Response, jrr interface{}, details map[string]interface{}) error {
+	bodyMap, ok := jrr.(*common.JsonRpcResponse)
+	if !ok {
+		return nil
+	}
+
+	err := bodyMap.Error
+	if err.Data != "" {
+		details["data"] = err.Data
+	}
+
+	// Other errors can be properly handled by generic error handling
+	return nil
+}
+
+func (v *TenderlyVendor) OwnsUpstream(ups *common.UpstreamConfig) bool {
+	if strings.HasPrefix(ups.Endpoint, "tenderly://") || strings.HasPrefix(ups.Endpoint, "evm+tenderly://") {
+		return true
+	}
+
+	return strings.Contains(ups.Endpoint, ".gateway.tenderly.co")
path: thirdparty/tenderly.go
user: login: zekiblue
body: for the nodes tenderly always uses gateway.tenderly.co. So this should be good. 
created_at: 2025-05-07T08:12:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -472,45 +473,6 @@ export default createConfig({
 </Tabs.Tab>
 </Tabs>
 
-#### `superchain`
path: docs/pages/config/projects/providers.mdx
user: login: aramalipoor
body: @zekiblue probably accidentally removed
created_at: 2025-05-08T08:46:35Z
author_association: MEMBER
diff_hunk: @@ -654,8 +616,4 @@ providers:
       apiKey: xxxxx
       networksUrl: https://api.conduit.xyz/public/network/all # (optional) Endpoint to fetch all supported networks
       recheckInterval: 24h # (optional) How often to recheck the API for newly added networks (default: 24h)
-  - vendor: superchain
path: docs/pages/config/projects/providers.mdx
user: login: aramalipoor
body: @zekiblue also here probably accidentally removed
created_at: 2025-05-08T08:46:54Z
author_association: MEMBER
diff_hunk: @@ -472,45 +473,6 @@ export default createConfig({
 </Tabs.Tab>
 </Tabs>
 
-#### `superchain`
path: docs/pages/config/projects/providers.mdx
user: login: zekiblue
body: ops, yes reverting these back
created_at: 2025-05-08T08:48:08Z
author_association: CONTRIBUTOR

PR comments:
user: login: aramalipoor
created_at: 2025-05-06T17:11:21Z
body: Thanks @zekiblue for this nice work 💯  I'll personally use it as soon as it's merged :D
author_association: MEMBER
user: login: zekiblue
created_at: 2025-05-08T07:47:14Z
body: @aramalipoor @kasrakhosravi what else needs to happen in this pr?
author_association: CONTRIBUTOR
user: login: aramalipoor
created_at: 2025-05-08T07:57:03Z
body: @zekiblue everything looks great. Just needed the conflict resolution which you already done. Merging soon 🙌
author_association: MEMBER
user: login: aramalipoor
created_at: 2025-05-08T07:58:07Z
body: I think there's a small syntax typo in defaults.go that tests are failing 
author_association: MEMBER
user: login: zekiblue
created_at: 2025-05-08T08:08:14Z
body: > I think there's a small syntax typo in defaults.go that tests are failing

It should be resolved now
author_association: CONTRIBUTOR