Bladeren bron

feat(github): support Dependabot secrets (#6878)

Signed-off-by: Sander Knape <sknape@stackoverflow.com>
Co-authored-by: Gergely Bräutigam <gergely.brautigam@sap.com>
Sander Knape 6 dagen geleden
bovenliggende
commit
279f56c84d

+ 17 - 0
apis/externalsecrets/v1/secretstore_github_types.go

@@ -20,7 +20,19 @@ import (
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 )
 
+// GithubSecretType specifies the GitHub secret service to use.
+// +kubebuilder:validation:Enum=Actions;Dependabot
+type GithubSecretType string
+
+const (
+	// GithubSecretTypeActions selects GitHub Actions secrets.
+	GithubSecretTypeActions GithubSecretType = "Actions"
+	// GithubSecretTypeDependabot selects GitHub Dependabot secrets.
+	GithubSecretTypeDependabot GithubSecretType = "Dependabot"
+)
+
 // GithubProvider provides access and authentication to a GitHub instance .
+// +kubebuilder:validation:XValidation:rule="self.secretType != 'Dependabot' || !has(self.environment) || size(self.environment) == 0",message="Dependabot secrets do not support environments"
 type GithubProvider struct {
 	// URL configures the Github instance URL. Defaults to https://github.com/.
 	//+kubebuilder:default="https://github.com/"
@@ -30,6 +42,11 @@ type GithubProvider struct {
 	UploadURL string `json:"uploadURL,omitempty"`
 	// auth configures how secret-manager authenticates with a Github instance.
 	Auth GithubAppAuth `json:"auth"`
+	// secretType specifies which GitHub secret service to use.
+	// Defaults to Actions for backwards compatibility.
+	// +optional
+	// +kubebuilder:default=Actions
+	SecretType GithubSecretType `json:"secretType,omitempty"`
 
 	// appID specifies the Github APP that will be used to authenticate the client
 	AppID int64 `json:"appID"`

+ 1 - 1
apis/externalsecrets/v1/secretstore_types.go

@@ -153,7 +153,7 @@ type SecretStoreProvider struct {
 	// +optional
 	YandexLockbox *YandexLockboxProvider `json:"yandexlockbox,omitempty"`
 
-	// Github configures this store to push GitHub Actions secrets using the GitHub API provider.
+	// Github configures this store to push GitHub Actions or Dependabot secrets using the GitHub API provider.
 	// Note: This provider only supports write operations (PushSecret) and cannot fetch secrets from GitHub
 	// +optional
 	Github *GithubProvider `json:"github,omitempty"`

+ 14 - 1
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -2798,7 +2798,7 @@ spec:
                     type: object
                   github:
                     description: |-
-                      Github configures this store to push GitHub Actions secrets using the GitHub API provider.
+                      Github configures this store to push GitHub Actions or Dependabot secrets using the GitHub API provider.
                       Note: This provider only supports write operations (PushSecret) and cannot fetch secrets from GitHub
                     properties:
                       appID:
@@ -2869,6 +2869,15 @@ spec:
                         description: repository will be used to fetch secrets from
                           the Github repository within an organization
                         type: string
+                      secretType:
+                        default: Actions
+                        description: |-
+                          secretType specifies which GitHub secret service to use.
+                          Defaults to Actions for backwards compatibility.
+                        enum:
+                        - Actions
+                        - Dependabot
+                        type: string
                       uploadURL:
                         description: Upload URL for enterprise instances. Default
                           to URL.
@@ -2884,6 +2893,10 @@ spec:
                     - installationID
                     - organization
                     type: object
+                    x-kubernetes-validations:
+                    - message: Dependabot secrets do not support environments
+                      rule: self.secretType != 'Dependabot' || !has(self.environment)
+                        || size(self.environment) == 0
                   gitlab:
                     description: GitLab configures this store to sync secrets using
                       GitLab Variables provider

+ 14 - 1
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -2798,7 +2798,7 @@ spec:
                     type: object
                   github:
                     description: |-
-                      Github configures this store to push GitHub Actions secrets using the GitHub API provider.
+                      Github configures this store to push GitHub Actions or Dependabot secrets using the GitHub API provider.
                       Note: This provider only supports write operations (PushSecret) and cannot fetch secrets from GitHub
                     properties:
                       appID:
@@ -2869,6 +2869,15 @@ spec:
                         description: repository will be used to fetch secrets from
                           the Github repository within an organization
                         type: string
+                      secretType:
+                        default: Actions
+                        description: |-
+                          secretType specifies which GitHub secret service to use.
+                          Defaults to Actions for backwards compatibility.
+                        enum:
+                        - Actions
+                        - Dependabot
+                        type: string
                       uploadURL:
                         description: Upload URL for enterprise instances. Default
                           to URL.
@@ -2884,6 +2893,10 @@ spec:
                     - installationID
                     - organization
                     type: object
+                    x-kubernetes-validations:
+                    - message: Dependabot secrets do not support environments
+                      rule: self.secretType != 'Dependabot' || !has(self.environment)
+                        || size(self.environment) == 0
                   gitlab:
                     description: GitLab configures this store to sync secrets using
                       GitLab Variables provider

+ 26 - 2
deploy/crds/bundle.yaml

@@ -4933,7 +4933,7 @@ spec:
                       type: object
                     github:
                       description: |-
-                        Github configures this store to push GitHub Actions secrets using the GitHub API provider.
+                        Github configures this store to push GitHub Actions or Dependabot secrets using the GitHub API provider.
                         Note: This provider only supports write operations (PushSecret) and cannot fetch secrets from GitHub
                       properties:
                         appID:
@@ -4997,6 +4997,15 @@ spec:
                         repository:
                           description: repository will be used to fetch secrets from the Github repository within an organization
                           type: string
+                        secretType:
+                          default: Actions
+                          description: |-
+                            secretType specifies which GitHub secret service to use.
+                            Defaults to Actions for backwards compatibility.
+                          enum:
+                            - Actions
+                            - Dependabot
+                          type: string
                         uploadURL:
                           description: Upload URL for enterprise instances. Default to URL.
                           type: string
@@ -5010,6 +5019,9 @@ spec:
                         - installationID
                         - organization
                       type: object
+                      x-kubernetes-validations:
+                        - message: Dependabot secrets do not support environments
+                          rule: self.secretType != 'Dependabot' || !has(self.environment) || size(self.environment) == 0
                     gitlab:
                       description: GitLab configures this store to sync secrets using GitLab Variables provider
                       properties:
@@ -18284,7 +18296,7 @@ spec:
                       type: object
                     github:
                       description: |-
-                        Github configures this store to push GitHub Actions secrets using the GitHub API provider.
+                        Github configures this store to push GitHub Actions or Dependabot secrets using the GitHub API provider.
                         Note: This provider only supports write operations (PushSecret) and cannot fetch secrets from GitHub
                       properties:
                         appID:
@@ -18348,6 +18360,15 @@ spec:
                         repository:
                           description: repository will be used to fetch secrets from the Github repository within an organization
                           type: string
+                        secretType:
+                          default: Actions
+                          description: |-
+                            secretType specifies which GitHub secret service to use.
+                            Defaults to Actions for backwards compatibility.
+                          enum:
+                            - Actions
+                            - Dependabot
+                          type: string
                         uploadURL:
                           description: Upload URL for enterprise instances. Default to URL.
                           type: string
@@ -18361,6 +18382,9 @@ spec:
                         - installationID
                         - organization
                       type: object
+                      x-kubernetes-validations:
+                        - message: Dependabot secrets do not support environments
+                          rule: self.secretType != 'Dependabot' || !has(self.environment) || size(self.environment) == 0
                     gitlab:
                       description: GitLab configures this store to sync secrets using GitLab Variables provider
                       properties:

+ 40 - 1
docs/api/spec.md

@@ -6830,6 +6830,21 @@ GithubAppAuth
 </tr>
 <tr>
 <td>
+<code>secretType</code></br>
+<em>
+<a href="#external-secrets.io/v1.GithubSecretType">
+GithubSecretType
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>secretType specifies which GitHub secret service to use.
+Defaults to Actions for backwards compatibility.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>appID</code></br>
 <em>
 int64
@@ -6902,6 +6917,30 @@ whatever visibility they already have in GitHub.</p>
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1.GithubSecretType">GithubSecretType
+(<code>string</code> alias)</p></h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.GithubProvider">GithubProvider</a>)
+</p>
+<p>
+<p>GithubSecretType specifies the GitHub secret service to use.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Value</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody><tr><td><p>&#34;Actions&#34;</p></td>
+<td><p>GithubSecretTypeActions selects GitHub Actions secrets.</p>
+</td>
+</tr><tr><td><p>&#34;Dependabot&#34;</p></td>
+<td><p>GithubSecretTypeDependabot selects GitHub Dependabot secrets.</p>
+</td>
+</tr></tbody>
+</table>
 <h3 id="external-secrets.io/v1.GitlabAuth">GitlabAuth
 </h3>
 <p>
@@ -11164,7 +11203,7 @@ GithubProvider
 </td>
 <td>
 <em>(Optional)</em>
-<p>Github configures this store to push GitHub Actions secrets using the GitHub API provider.
+<p>Github configures this store to push GitHub Actions or Dependabot secrets using the GitHub API provider.
 Note: This provider only supports write operations (PushSecret) and cannot fetch secrets from GitHub</p>
 </td>
 </tr>

+ 2 - 2
docs/introduction/stability-support.md

@@ -84,7 +84,7 @@ The following table describes the stability level of each provider and who's res
 | [Fortanix](https://external-secrets.io/latest/provider/fortanix)                                               |     alpha | [@RecuencoJones](https://github.com/RecuencoJones)                                                  |
 | [GCP Secret Manager](https://external-secrets.io/latest/provider/google-secrets-manager/)                      |    stable | [external-secrets](https://github.com/external-secrets)                                             |
 | [Generic Webhook](https://external-secrets.io/latest/provider/webhook)                                         |     alpha | [@willemm](https://github.com/willemm)                                                              |
-| [GitHub Actions Secrets](https://external-secrets.io/latest/provider/github)                               |     alpha | [@gusfcarvalho](https://github.com/gusfcarvalho)                                                    |
+| [GitHub Actions and Dependabot Secrets](https://external-secrets.io/latest/provider/github)                |     alpha | [@gusfcarvalho](https://github.com/gusfcarvalho)                                                    |
 | [GitLab Variables](https://external-secrets.io/latest/provider/gitlab-variables/)                              |     alpha | [@Jabray5](https://github.com/Jabray5)                                                              |
 | [Hashicorp Vault](https://external-secrets.io/latest/provider/hashicorp-vault/)                                |    stable | [external-secrets](https://github.com/external-secrets)                                             |
 | [IBM Cloud Secrets Manager](https://external-secrets.io/latest/provider/ibm-secrets-manager/)                  |    stable | [@IdanAdar](https://github.com/IdanAdar)                                                            |
@@ -133,7 +133,7 @@ The following table show the support for features across different providers.
 | Doppler                          |      x       |              |                      |                         |        x         |             |                             |
 | Fortanix                         |              |              |                      |            x            |        x         |             |                             |
 | GCP Secret Manager               |      x       |      x       |          x           |            x            |        x         |      x      |              x              |
-| GitHub Actions Secrets           |              |              |                      |            x            |                  |      x      |                             |
+| GitHub Actions and Dependabot Secrets |         |              |                      |            x            |                  |      x      |                             |
 | GitLab Variables                 |      x       |      x       |                      |                         |        x         |             |                             |
 | Generic Webhook                  |              |              |                      |                         |                  |             |              x              |
 | Hashicorp Vault                  |      x       |      x       |          x           |            x            |        x         |      x      |              x              |

+ 15 - 6
docs/provider/github.md

@@ -1,23 +1,32 @@
 # GitHub
 
-External Secrets Operator integrates with GitHub to sync Kubernetes secrets with [GitHub Actions secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions).
+External Secrets Operator integrates with GitHub to sync Kubernetes secrets with [GitHub Actions secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) or [Dependabot secrets](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/managing-encrypted-secrets-for-dependabot).
 
 ## Limitations
 
-The GitHub provider is **write-only**, designed specifically to **create and update** GitHub Actions secrets using the
-[GitHub REST API](https://docs.github.com/en/rest/actions/secrets), and does not support **fetching the secret values**.
+The GitHub provider is **write-only**, designed specifically to **create and update** GitHub Actions or Dependabot secrets using the
+[GitHub REST API](https://docs.github.com/en/rest/actions/secrets) or the [Dependabot secrets API](https://docs.github.com/en/rest/dependabot/secrets), and does not support **fetching the secret values**.
 
 ## Configuring GitHub provider
 
-The GitHub API requires to install the ESO app to your GitHub organisation in order to use the GitHub provider features.
+The GitHub API requires to install the ESO app to your GitHub organisation in order to use the GitHub provider features. The same App ID, installation ID, and private key authentication are used for both secret types. Grant the app read and write access to **Secrets** for Actions secrets, or **Dependabot secrets** for Dependabot secrets, at the target organization or repository scope.
 
 ## Configuring the secret store
 
 Verify that `github` provider is listed in the `Kind=SecretStore`. The properties `appID`, `installationID`, `organization` are required to register the provider. In addition, authentication has to be provided.
 
-Optionally, to target `repository` and `environment` secrets, the fields `repository` and `environment` need also to be added.
+Set `secretType` to `Actions` or `Dependabot`. `Actions` is the default when the field is omitted, preserving compatibility with existing stores.
 
-For organization secrets, the optional `orgSecretVisibility` field controls the visibility of secrets created via PushSecret. Valid values are `all` or `private`. When unset, new secrets are created with visibility `all` and existing secrets keep whatever visibility they already have in GitHub.
+Optionally, to target Actions `repository` and `environment` secrets, the fields `repository` and `environment` need also to be added. For Dependabot repository secrets, add the `repository` field.
+
+| Secret type | Organization | Repository | Environment |
+| :-- | :--: | :--: | :--: |
+| `Actions` | Yes | Yes | Yes |
+| `Dependabot` | Yes | Yes | No |
+
+Organization scope is used when `repository` is omitted. Combining `secretType: Dependabot` with `environment` is invalid.
+
+For organization secrets, the optional `orgSecretVisibility` field controls the visibility of secrets created via PushSecret. Valid values are `all` or `private`. When unset, new secrets are created with visibility `all` and existing secrets keep whatever visibility they already have in GitHub. Updates also preserve existing selected-repository associations. Repository-scoped secrets do not use organization visibility or selected-repository associations.
 
 ```yaml
 {% include 'github-secret-store.yaml' %}

+ 1 - 0
docs/snippets/github-secret-store.yaml

@@ -6,6 +6,7 @@ spec:
   provider:
     # provider type: github
     github:
+      secretType: Actions # Optional. Actions (default) or Dependabot
       appID: "**app ID goes here**"
       # url: Default "https://github.com/", for enterprise Github instances uncomment and add your domain like "https://github.domain.com/"
       # uploadURL: Default "https://github.com"

+ 1 - 1
hack/api-docs/mkdocs.yml

@@ -142,7 +142,7 @@ nav:
       - Yandex Certificate Manager: provider/yandex-certificate-manager.md
       - Yandex Lockbox: provider/yandex-lockbox.md
       - GitLab Variables: provider/gitlab-variables.md
-      - Github Actions Secrets: provider/github.md
+      - GitHub Actions and Dependabot Secrets: provider/github.md
       - Oracle Vault: provider/oracle-vault.md
       - OVHcloud: provider/ovhcloud.md
       - 1Password Connect Server: provider/1password-automation.md

+ 6 - 5
providers/v1/github/client.go

@@ -51,12 +51,13 @@ type ActionsServiceClient interface {
 	ListOrgSecrets(ctx context.Context, org string, opts *github.ListOptions) (*github.Secrets, *github.Response, error)
 }
 
-// Client implements the External Secrets Kubernetes provider for GitHub Actions secrets.
+// Client implements the External Secrets Kubernetes provider for GitHub Actions or Dependabot secrets.
 type Client struct {
 	crClient         client.Client
 	store            esv1.GenericStore
 	provider         *esv1.GithubProvider
 	baseClient       github.ActionsService
+	dependabotClient github.DependabotService
 	namespace        string
 	storeKind        string
 	repoID           int64
@@ -70,7 +71,7 @@ type Client struct {
 	listSelectedReposFn func(ctx context.Context, name string) (github.SelectedRepoIDs, error)
 }
 
-// DeleteSecret deletes a secret from GitHub Actions.
+// DeleteSecret deletes a secret from the configured GitHub secrets service.
 func (g *Client) DeleteSecret(ctx context.Context, remoteRef esv1.PushSecretRemoteRef) error {
 	_, err := g.deleteSecretFn(ctx, remoteRef)
 	if err != nil {
@@ -79,7 +80,7 @@ func (g *Client) DeleteSecret(ctx context.Context, remoteRef esv1.PushSecretRemo
 	return nil
 }
 
-// SecretExists checks if a secret exists in GitHub Actions.
+// SecretExists checks if a secret exists in the configured GitHub secrets service.
 func (g *Client) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
 	githubSecret, _, err := g.getSecretFn(ctx, ref)
 	if err != nil {
@@ -91,7 +92,7 @@ func (g *Client) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef)
 	return false, nil
 }
 
-// PushSecret pushes a new secret to GitHub Actions.
+// PushSecret pushes a new secret to the configured GitHub secrets service.
 func (g *Client) PushSecret(ctx context.Context, secret *corev1.Secret, remoteRef esv1.PushSecretData) error {
 	githubSecret, response, err := g.getSecretFn(ctx, remoteRef)
 	if err != nil && (response == nil || response.StatusCode != 404) {
@@ -199,7 +200,7 @@ func (g *Client) Close(_ context.Context) error {
 	return nil
 }
 
-// Validate checks if the client is properly configured and has access to the GitHub Actions API.
+// Validate checks if the client is properly configured and has access to the configured GitHub secrets API.
 func (g *Client) Validate() (esv1.ValidationResult, error) {
 	if g.store.GetKind() == esv1.ClusterSecretStoreKind {
 		return esv1.ValidationResultUnknown, nil

+ 89 - 0
providers/v1/github/client_test.go

@@ -36,6 +36,9 @@ import (
 	"crypto/x509"
 	"encoding/pem"
 	"errors"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
 	"testing"
 
 	"github.com/bradleyfalzon/ghinstallation/v2"
@@ -52,6 +55,92 @@ import (
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 )
 
+func TestConfigureSecretClientRoutesBySecretTypeAndScope(t *testing.T) {
+	tests := []struct {
+		name         string
+		provider     *esv1.GithubProvider
+		wantListPath string
+		wantRepoGet  bool
+	}{
+		{
+			name: "omitted type uses Actions organization secrets",
+			provider: &esv1.GithubProvider{
+				Organization: "acme",
+			},
+			wantListPath: "/orgs/acme/actions/secrets",
+		},
+		{
+			name: "explicit Actions uses repository secrets",
+			provider: &esv1.GithubProvider{
+				SecretType:   esv1.GithubSecretTypeActions,
+				Organization: "acme",
+				Repository:   "widgets",
+			},
+			wantListPath: "/repos/acme/widgets/actions/secrets",
+		},
+		{
+			name: "Actions environment remains supported",
+			provider: &esv1.GithubProvider{
+				SecretType:   esv1.GithubSecretTypeActions,
+				Organization: "acme",
+				Repository:   "widgets",
+				Environment:  "production",
+			},
+			wantListPath: "/repositories/42/environments/production/secrets",
+			wantRepoGet:  true,
+		},
+		{
+			name: "Dependabot uses organization secrets",
+			provider: &esv1.GithubProvider{
+				SecretType:   esv1.GithubSecretTypeDependabot,
+				Organization: "acme",
+			},
+			wantListPath: "/orgs/acme/dependabot/secrets",
+		},
+		{
+			name: "Dependabot uses repository secrets without repository lookup",
+			provider: &esv1.GithubProvider{
+				SecretType:   esv1.GithubSecretTypeDependabot,
+				Organization: "acme",
+				Repository:   "widgets",
+			},
+			wantListPath: "/repos/acme/widgets/dependabot/secrets",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var gotPaths []string
+			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				gotPaths = append(gotPaths, r.URL.Path)
+				if r.URL.Path == "/repos/acme/widgets" {
+					_, _ = fmt.Fprint(w, `{"id":42}`)
+					return
+				}
+				if r.URL.Path != tt.wantListPath {
+					http.Error(w, "unexpected request", http.StatusNotFound)
+					return
+				}
+				_, _ = fmt.Fprint(w, `{"total_count":0,"secrets":[]}`)
+			}))
+			t.Cleanup(server.Close)
+
+			g := &Client{provider: tt.provider}
+			secretType, err := validateGithubProvider(tt.provider)
+			require.NoError(t, err)
+			require.NoError(t, g.configureSecretClient(context.Background(), newGithubTestClient(t, server), secretType))
+			_, _, err = g.listSecretsFn(context.Background())
+			require.NoError(t, err)
+
+			if tt.wantRepoGet {
+				assert.Equal(t, []string{"/repos/acme/widgets", tt.wantListPath}, gotPaths)
+			} else {
+				assert.Equal(t, []string{tt.wantListPath}, gotPaths)
+			}
+		})
+	}
+}
+
 type getSecretFn func(ctx context.Context, ref esv1.PushSecretRemoteRef) (*github.Secret, *github.Response, error)
 
 func withGetSecretFn(secret *github.Secret, response *github.Response, err error) getSecretFn {

+ 94 - 0
providers/v1/github/dependabot_secrets.go

@@ -0,0 +1,94 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 github
+
+import (
+	"context"
+
+	github "github.com/google/go-github/v56/github"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+func adaptDependabotEncryptedSecret(secret *github.EncryptedSecret) *github.DependabotEncryptedSecret {
+	return &github.DependabotEncryptedSecret{
+		Name:                  secret.Name,
+		KeyID:                 secret.KeyID,
+		EncryptedValue:        secret.EncryptedValue,
+		Visibility:            secret.Visibility,
+		SelectedRepositoryIDs: github.DependabotSecretsSelectedRepoIDs(secret.SelectedRepositoryIDs),
+	}
+}
+
+func (g *Client) dependabotOrgGetSecretFn(ctx context.Context, ref esv1.PushSecretRemoteRef) (*github.Secret, *github.Response, error) {
+	return g.dependabotClient.GetOrgSecret(ctx, g.provider.Organization, ref.GetRemoteKey())
+}
+
+func (g *Client) dependabotOrgGetPublicKeyFn(ctx context.Context) (*github.PublicKey, *github.Response, error) {
+	return g.dependabotClient.GetOrgPublicKey(ctx, g.provider.Organization)
+}
+
+func (g *Client) dependabotOrgCreateOrUpdateSecret(ctx context.Context, secret *github.EncryptedSecret) (*github.Response, error) {
+	return g.dependabotClient.CreateOrUpdateOrgSecret(ctx, g.provider.Organization, adaptDependabotEncryptedSecret(secret))
+}
+
+func (g *Client) dependabotOrgListSecretsFn(ctx context.Context) (*github.Secrets, *github.Response, error) {
+	return g.dependabotClient.ListOrgSecrets(ctx, g.provider.Organization, &github.ListOptions{})
+}
+
+func (g *Client) dependabotOrgDeleteSecretFn(ctx context.Context, ref esv1.PushSecretRemoteRef) (*github.Response, error) {
+	return g.dependabotClient.DeleteOrgSecret(ctx, g.provider.Organization, ref.GetRemoteKey())
+}
+
+func (g *Client) dependabotOrgListSelectedRepoIDs(ctx context.Context, name string) (github.SelectedRepoIDs, error) {
+	ids := github.SelectedRepoIDs{}
+	opts := &github.ListOptions{PerPage: 100}
+	for {
+		repos, resp, err := g.dependabotClient.ListSelectedReposForOrgSecret(ctx, g.provider.Organization, name, opts)
+		if err != nil {
+			return nil, err
+		}
+		for _, repo := range repos.Repositories {
+			ids = append(ids, repo.GetID())
+		}
+		if resp == nil || resp.NextPage == 0 {
+			break
+		}
+		opts.Page = resp.NextPage
+	}
+	return ids, nil
+}
+
+func (g *Client) dependabotRepoGetSecretFn(ctx context.Context, ref esv1.PushSecretRemoteRef) (*github.Secret, *github.Response, error) {
+	return g.dependabotClient.GetRepoSecret(ctx, g.provider.Organization, g.provider.Repository, ref.GetRemoteKey())
+}
+
+func (g *Client) dependabotRepoGetPublicKeyFn(ctx context.Context) (*github.PublicKey, *github.Response, error) {
+	return g.dependabotClient.GetRepoPublicKey(ctx, g.provider.Organization, g.provider.Repository)
+}
+
+func (g *Client) dependabotRepoCreateOrUpdateSecret(ctx context.Context, secret *github.EncryptedSecret) (*github.Response, error) {
+	return g.dependabotClient.CreateOrUpdateRepoSecret(ctx, g.provider.Organization, g.provider.Repository, adaptDependabotEncryptedSecret(secret))
+}
+
+func (g *Client) dependabotRepoListSecretsFn(ctx context.Context) (*github.Secrets, *github.Response, error) {
+	return g.dependabotClient.ListRepoSecrets(ctx, g.provider.Organization, g.provider.Repository, &github.ListOptions{})
+}
+
+func (g *Client) dependabotRepoDeleteSecretFn(ctx context.Context, ref esv1.PushSecretRemoteRef) (*github.Response, error) {
+	return g.dependabotClient.DeleteRepoSecret(ctx, g.provider.Organization, g.provider.Repository, ref.GetRemoteKey())
+}

+ 253 - 0
providers/v1/github/dependabot_secrets_test.go

@@ -0,0 +1,253 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 github
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"testing"
+
+	github "github.com/google/go-github/v56/github"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+)
+
+func TestAdaptDependabotEncryptedSecret(t *testing.T) {
+	actionsSecret := &github.EncryptedSecret{
+		Name:                  "TOKEN",
+		KeyID:                 "key-id",
+		EncryptedValue:        "encrypted-value",
+		Visibility:            "selected",
+		SelectedRepositoryIDs: github.SelectedRepoIDs{12, 34},
+	}
+
+	got := adaptDependabotEncryptedSecret(actionsSecret)
+
+	assert.Equal(t, "TOKEN", got.Name)
+	assert.Equal(t, "key-id", got.KeyID)
+	assert.Equal(t, "encrypted-value", got.EncryptedValue)
+	assert.Equal(t, "selected", got.Visibility)
+	assert.Equal(t, github.DependabotSecretsSelectedRepoIDs{12, 34}, got.SelectedRepositoryIDs)
+}
+
+func TestDependabotSecretLifecycle(t *testing.T) {
+	tests := []struct {
+		name       string
+		provider   *esv1.GithubProvider
+		pathPrefix string
+	}{
+		{
+			name: "organization",
+			provider: &esv1.GithubProvider{
+				SecretType:   esv1.GithubSecretTypeDependabot,
+				Organization: "acme",
+			},
+			pathPrefix: "/orgs/acme/dependabot/secrets",
+		},
+		{
+			name: "repository",
+			provider: &esv1.GithubProvider{
+				SecretType:   esv1.GithubSecretTypeDependabot,
+				Organization: "acme",
+				Repository:   "widgets",
+			},
+			pathPrefix: "/repos/acme/widgets/dependabot/secrets",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var requests []string
+			var putBody map[string]any
+			var putDecodeErr error
+			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				requests = append(requests, r.Method+" "+r.URL.Path)
+				switch {
+				case r.Method == http.MethodGet && r.URL.Path == tt.pathPrefix+"/public-key":
+					_, _ = fmt.Fprint(w, `{"key_id":"key-id","key":"a2V5"}`)
+				case r.Method == http.MethodGet && r.URL.Path == tt.pathPrefix+"/TOKEN":
+					_, _ = fmt.Fprint(w, `{"name":"TOKEN","visibility":"selected"}`)
+				case r.Method == http.MethodGet && r.URL.Path == tt.pathPrefix:
+					_, _ = fmt.Fprint(w, `{"total_count":1,"secrets":[{"name":"TOKEN"}]}`)
+				case r.Method == http.MethodPut && r.URL.Path == tt.pathPrefix+"/TOKEN":
+					putDecodeErr = json.NewDecoder(r.Body).Decode(&putBody)
+					if putDecodeErr != nil {
+						http.Error(w, putDecodeErr.Error(), http.StatusBadRequest)
+						return
+					}
+					w.WriteHeader(http.StatusCreated)
+				case r.Method == http.MethodDelete && r.URL.Path == tt.pathPrefix+"/TOKEN":
+					w.WriteHeader(http.StatusNoContent)
+				default:
+					http.Error(w, "unexpected request", http.StatusNotFound)
+				}
+			}))
+			t.Cleanup(server.Close)
+
+			g := &Client{provider: tt.provider}
+			ghClient := newGithubTestClient(t, server)
+			require.NoError(t, g.configureSecretClient(context.Background(), ghClient, esv1.GithubSecretTypeDependabot))
+
+			ref := esv1alpha1.PushSecretData{
+				Match: esv1alpha1.PushSecretMatch{
+					RemoteRef: esv1alpha1.PushSecretRemoteRef{RemoteKey: "TOKEN"},
+				},
+			}
+			secret, _, err := g.getSecretFn(context.Background(), ref)
+			require.NoError(t, err)
+			assert.Equal(t, "TOKEN", secret.Name)
+
+			secrets, _, err := g.listSecretsFn(context.Background())
+			require.NoError(t, err)
+			assert.Equal(t, 1, secrets.TotalCount)
+
+			key, _, err := g.getPublicKeyFn(context.Background())
+			require.NoError(t, err)
+			assert.Equal(t, "key-id", key.GetKeyID())
+
+			_, err = g.createOrUpdateFn(context.Background(), &github.EncryptedSecret{
+				Name:           "TOKEN",
+				KeyID:          "key-id",
+				EncryptedValue: "encrypted-value",
+				Visibility:     "selected",
+			})
+			require.NoError(t, err)
+			_, err = g.deleteSecretFn(context.Background(), ref)
+			require.NoError(t, err)
+			require.NoError(t, putDecodeErr)
+
+			assert.Equal(t, []string{
+				http.MethodGet + " " + tt.pathPrefix + "/TOKEN",
+				http.MethodGet + " " + tt.pathPrefix,
+				http.MethodGet + " " + tt.pathPrefix + "/public-key",
+				http.MethodPut + " " + tt.pathPrefix + "/TOKEN",
+				http.MethodDelete + " " + tt.pathPrefix + "/TOKEN",
+			}, requests)
+			assert.Equal(t, "key-id", putBody["key_id"])
+			assert.Equal(t, "encrypted-value", putBody["encrypted_value"])
+			assert.Equal(t, "selected", putBody["visibility"])
+		})
+	}
+}
+
+func TestDependabotPushSecretPreservesSelectedRepositories(t *testing.T) {
+	var putBody map[string]json.RawMessage
+	var putDecodeErr error
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		switch {
+		case r.Method == http.MethodGet && r.URL.Path == "/orgs/acme/dependabot/secrets/TOKEN":
+			_, _ = fmt.Fprint(w, `{"name":"TOKEN","visibility":"selected"}`)
+		case r.Method == http.MethodGet && r.URL.Path == "/orgs/acme/dependabot/secrets/public-key":
+			_, _ = fmt.Fprint(w, `{"key_id":"key-id","key":"Zm9vYmFyCg=="}`)
+		case r.Method == http.MethodGet && r.URL.Path == "/orgs/acme/dependabot/secrets/TOKEN/repositories":
+			_, _ = fmt.Fprint(w, `{"total_count":2,"repositories":[{"id":12},{"id":34}]}`)
+		case r.Method == http.MethodPut && r.URL.Path == "/orgs/acme/dependabot/secrets/TOKEN":
+			putDecodeErr = json.NewDecoder(r.Body).Decode(&putBody)
+			w.WriteHeader(http.StatusCreated)
+		default:
+			http.Error(w, "unexpected request", http.StatusNotFound)
+		}
+	}))
+	t.Cleanup(server.Close)
+
+	provider := &esv1.GithubProvider{
+		SecretType:   esv1.GithubSecretTypeDependabot,
+		Organization: "acme",
+	}
+	g := &Client{provider: provider}
+	require.NoError(t, g.configureSecretClient(context.Background(), newGithubTestClient(t, server), esv1.GithubSecretTypeDependabot))
+
+	remoteRef := esv1alpha1.PushSecretData{
+		Match: esv1alpha1.PushSecretMatch{
+			SecretKey: "value",
+			RemoteRef: esv1alpha1.PushSecretRemoteRef{RemoteKey: "TOKEN"},
+		},
+	}
+	err := g.PushSecret(context.Background(), &corev1.Secret{Data: map[string][]byte{"value": []byte("secret")}}, remoteRef)
+	require.NoError(t, err)
+	require.NoError(t, putDecodeErr)
+	assert.JSONEq(t, `"selected"`, string(putBody["visibility"]))
+	assert.JSONEq(t, `["12","34"]`, string(putBody["selected_repository_ids"]))
+}
+
+func TestDependabotOrgSelectedRepositoriesPagination(t *testing.T) {
+	var server *httptest.Server
+	server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, "/orgs/acme/dependabot/secrets/TOKEN/repositories", r.URL.Path)
+		switch r.URL.Query().Get("page") {
+		case "":
+			w.Header().Set("Link", "<"+server.URL+r.URL.Path+"?page=2>; rel=\"next\"")
+			_, _ = fmt.Fprint(w, `{"total_count":2,"repositories":[{"id":12}]}`)
+		case "2":
+			_, _ = fmt.Fprint(w, `{"total_count":2,"repositories":[{"id":34}]}`)
+		default:
+			http.Error(w, "unexpected page", http.StatusBadRequest)
+		}
+	}))
+	t.Cleanup(server.Close)
+
+	ghClient := newGithubTestClient(t, server)
+	g := &Client{
+		provider:         &esv1.GithubProvider{Organization: "acme"},
+		dependabotClient: *ghClient.Dependabot,
+	}
+
+	ids, err := g.dependabotOrgListSelectedRepoIDs(context.Background(), "TOKEN")
+	require.NoError(t, err)
+	assert.Equal(t, github.SelectedRepoIDs{12, 34}, ids)
+}
+
+func TestDependabotErrorsPropagate(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+		http.Error(w, "boom", http.StatusInternalServerError)
+	}))
+	t.Cleanup(server.Close)
+
+	ghClient := newGithubTestClient(t, server)
+	g := &Client{
+		provider:         &esv1.GithubProvider{Organization: "acme"},
+		dependabotClient: *ghClient.Dependabot,
+	}
+
+	ref := esv1alpha1.PushSecretData{
+		Match: esv1alpha1.PushSecretMatch{
+			RemoteRef: esv1alpha1.PushSecretRemoteRef{RemoteKey: "TOKEN"},
+		},
+	}
+	_, _, err := g.dependabotOrgGetSecretFn(context.Background(), ref)
+	assert.Error(t, err)
+	_, err = g.dependabotOrgListSelectedRepoIDs(context.Background(), "TOKEN")
+	assert.Error(t, err)
+}
+
+func newGithubTestClient(t *testing.T, server *httptest.Server) *github.Client {
+	t.Helper()
+	client := github.NewClient(server.Client())
+	baseURL, err := url.Parse(server.URL + "/")
+	require.NoError(t, err)
+	client.BaseURL = baseURL
+	client.UploadURL = baseURL
+	return client
+}

+ 60 - 12
providers/v1/github/provider.go

@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 // Package github implements a provider for GitHub secrets, allowing
-// External Secrets to write secrets to GitHub Actions.
+// External Secrets to write secrets to GitHub Actions or Dependabot.
 package github
 
 import (
@@ -23,6 +23,7 @@ import (
 	"errors"
 	"fmt"
 
+	github "github.com/google/go-github/v56/github"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
@@ -37,7 +38,7 @@ const (
 	errInvalidStore        = "invalid store"
 )
 
-// Provider implements the GitHub provider for managing secrets through GitHub Actions.
+// Provider implements the GitHub provider for managing secrets through GitHub Actions or Dependabot.
 type Provider struct {
 }
 
@@ -58,6 +59,10 @@ func newClient(ctx context.Context, store esv1.GenericStore, kube client.Client,
 	if err != nil {
 		return nil, err
 	}
+	secretType, err := validateGithubProvider(provider)
+	if err != nil {
+		return nil, err
+	}
 	g := &Client{
 		crClient:  kube,
 		store:     store,
@@ -65,18 +70,45 @@ func newClient(ctx context.Context, store esv1.GenericStore, kube client.Client,
 		provider:  provider,
 		storeKind: store.GetObjectKind().GroupVersionKind().Kind,
 	}
+	ghClient, err := g.AuthWithPrivateKey(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("could not get private key: %w", err)
+	}
+	if err := g.configureSecretClient(ctx, ghClient, secretType); err != nil {
+		return nil, err
+	}
+
+	return g, nil
+}
+
+func (g *Client) configureSecretClient(ctx context.Context, ghClient *github.Client, secretType esv1.GithubSecretType) error {
+	if secretType == esv1.GithubSecretTypeDependabot {
+		g.dependabotClient = *ghClient.Dependabot
+		g.getSecretFn = g.dependabotOrgGetSecretFn
+		g.getPublicKeyFn = g.dependabotOrgGetPublicKeyFn
+		g.createOrUpdateFn = g.dependabotOrgCreateOrUpdateSecret
+		g.listSecretsFn = g.dependabotOrgListSecretsFn
+		g.deleteSecretFn = g.dependabotOrgDeleteSecretFn
+		g.listSelectedReposFn = g.dependabotOrgListSelectedRepoIDs
+		if g.provider.Repository != "" {
+			g.getSecretFn = g.dependabotRepoGetSecretFn
+			g.getPublicKeyFn = g.dependabotRepoGetPublicKeyFn
+			g.createOrUpdateFn = g.dependabotRepoCreateOrUpdateSecret
+			g.listSecretsFn = g.dependabotRepoListSecretsFn
+			g.deleteSecretFn = g.dependabotRepoDeleteSecretFn
+			g.listSelectedReposFn = nil
+		}
+		return nil
+	}
+
+	g.baseClient = *ghClient.Actions
 	g.getSecretFn = g.orgGetSecretFn
 	g.getPublicKeyFn = g.orgGetPublicKeyFn
 	g.createOrUpdateFn = g.orgCreateOrUpdateSecret
 	g.listSecretsFn = g.orgListSecretsFn
 	g.deleteSecretFn = g.orgDeleteSecretsFn
 	g.listSelectedReposFn = g.orgListSelectedRepoIDs
-	ghClient, err := g.AuthWithPrivateKey(ctx)
-	if err != nil {
-		return nil, fmt.Errorf("could not get private key: %w", err)
-	}
-	g.baseClient = *ghClient.Actions
-	if provider.Repository != "" {
+	if g.provider.Repository != "" {
 		g.getSecretFn = g.repoGetSecretFn
 		g.getPublicKeyFn = g.repoGetPublicKeyFn
 		g.createOrUpdateFn = g.repoCreateOrUpdateSecret
@@ -84,11 +116,11 @@ func newClient(ctx context.Context, store esv1.GenericStore, kube client.Client,
 		g.deleteSecretFn = g.repoDeleteSecretsFn
 		// Repo and env secrets have no "selected repositories" concept.
 		g.listSelectedReposFn = nil
-		if provider.Environment != "" {
+		if g.provider.Environment != "" {
 			// For environment to work, we need the repository ID instead of its name.
 			repo, _, err := ghClient.Repositories.Get(ctx, g.provider.Organization, g.provider.Repository)
 			if err != nil {
-				return nil, fmt.Errorf("error fetching repository: %w", err)
+				return fmt.Errorf("error fetching repository: %w", err)
 			}
 			g.repoID = repo.GetID()
 			g.getSecretFn = g.envGetSecretFn
@@ -98,8 +130,7 @@ func newClient(ctx context.Context, store esv1.GenericStore, kube client.Client,
 			g.deleteSecretFn = g.envDeleteSecretsFn
 		}
 	}
-
-	return g, nil
+	return nil
 }
 
 func getProvider(store esv1.GenericStore) (*esv1.GithubProvider, error) {
@@ -127,10 +158,27 @@ func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, e
 	if prov == nil {
 		return nil, errors.New(errInvalidGithubProv)
 	}
+	if _, err := validateGithubProvider(prov); err != nil {
+		return nil, err
+	}
 
 	return nil, nil
 }
 
+func validateGithubProvider(provider *esv1.GithubProvider) (esv1.GithubSecretType, error) {
+	secretType := provider.SecretType
+	if secretType == "" {
+		secretType = esv1.GithubSecretTypeActions
+	}
+	if secretType != esv1.GithubSecretTypeActions && secretType != esv1.GithubSecretTypeDependabot {
+		return "", fmt.Errorf("unsupported GitHub secret type %q", secretType)
+	}
+	if secretType == esv1.GithubSecretTypeDependabot && provider.Environment != "" {
+		return "", errors.New("Dependabot secrets do not support environments")
+	}
+	return secretType, nil
+}
+
 // NewProvider creates a new Provider instance.
 func NewProvider() esv1.Provider {
 	return &Provider{}

+ 150 - 0
providers/v1/github/provider_test.go

@@ -0,0 +1,150 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 github
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+func TestValidateGithubProvider(t *testing.T) {
+	tests := []struct {
+		name     string
+		provider *esv1.GithubProvider
+		wantType esv1.GithubSecretType
+		wantErr  string
+	}{
+		{
+			name:     "omitted secret type defaults to Actions",
+			provider: &esv1.GithubProvider{},
+			wantType: esv1.GithubSecretTypeActions,
+		},
+		{
+			name:     "explicit Actions",
+			provider: &esv1.GithubProvider{SecretType: esv1.GithubSecretTypeActions},
+			wantType: esv1.GithubSecretTypeActions,
+		},
+		{
+			name: "Dependabot with empty environment",
+			provider: &esv1.GithubProvider{
+				SecretType:  esv1.GithubSecretTypeDependabot,
+				Environment: "",
+			},
+			wantType: esv1.GithubSecretTypeDependabot,
+		},
+		{
+			name: "Dependabot environment is unsupported",
+			provider: &esv1.GithubProvider{
+				SecretType:  esv1.GithubSecretTypeDependabot,
+				Repository:  "repository",
+				Environment: "production",
+			},
+			wantErr: "Dependabot secrets do not support environments",
+		},
+		{
+			name:     "unsupported secret type",
+			provider: &esv1.GithubProvider{SecretType: esv1.GithubSecretType("Unsupported")},
+			wantErr:  "unsupported GitHub secret type",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotType, err := validateGithubProvider(tt.provider)
+			if tt.wantErr != "" {
+				assert.ErrorContains(t, err, tt.wantErr)
+				return
+			}
+			require.NoError(t, err)
+			assert.Equal(t, tt.wantType, gotType)
+		})
+	}
+}
+
+func TestProviderValidateStoreRejectsInvalidSecretTypeConfiguration(t *testing.T) {
+	tests := []struct {
+		name     string
+		provider *esv1.GithubProvider
+		wantErr  string
+	}{
+		{
+			name:     "unsupported secret type",
+			provider: &esv1.GithubProvider{SecretType: esv1.GithubSecretType("Unsupported")},
+			wantErr:  "unsupported GitHub secret type",
+		},
+		{
+			name: "Dependabot environment",
+			provider: &esv1.GithubProvider{
+				SecretType:  esv1.GithubSecretTypeDependabot,
+				Environment: "production",
+			},
+			wantErr: "Dependabot secrets do not support environments",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			store := &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{Github: tt.provider},
+				},
+			}
+			_, err := (&Provider{}).ValidateStore(store)
+			require.ErrorContains(t, err, tt.wantErr)
+		})
+	}
+}
+
+func TestNewClientRejectsInvalidSecretTypeBeforeAuthentication(t *testing.T) {
+	tests := []struct {
+		name     string
+		provider *esv1.GithubProvider
+		wantErr  string
+	}{
+		{
+			name:     "unsupported secret type",
+			provider: &esv1.GithubProvider{SecretType: esv1.GithubSecretType("Unsupported")},
+			wantErr:  "unsupported GitHub secret type",
+		},
+		{
+			name: "Dependabot environment",
+			provider: &esv1.GithubProvider{
+				SecretType:  esv1.GithubSecretTypeDependabot,
+				Environment: "production",
+			},
+			wantErr: "Dependabot secrets do not support environments",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			store := &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{Github: tt.provider},
+				},
+			}
+			_, err := newClient(context.Background(), store, nil, "default")
+			require.ErrorContains(t, err, tt.wantErr)
+			assert.NotContains(t, err.Error(), "private key")
+		})
+	}
+}

+ 1 - 0
tests/__snapshot__/clustersecretstore-v1.yaml

@@ -424,6 +424,7 @@ spec:
       orgSecretVisibility: "all" # "all", "private"
       organization: string
       repository: string
+      secretType: "Actions"
       uploadURL: string
       url: "https://github.com/"
     gitlab:

+ 1 - 0
tests/__snapshot__/secretstore-v1.yaml

@@ -424,6 +424,7 @@ spec:
       orgSecretVisibility: "all" # "all", "private"
       organization: string
       repository: string
+      secretType: "Actions"
       uploadURL: string
       url: "https://github.com/"
     gitlab: