diff --git a/backend/api/services.py b/backend/api/services.py index e74b5a1e5..7910dfe24 100644 --- a/backend/api/services.py +++ b/backend/api/services.py @@ -129,6 +129,13 @@ class ServiceConfig: "provider": Providers.GITHUB, "resource_type": "repo", } + + GITHUB_DEPENDABOT = { + "id": "github_dependabot", + "name": "GitHub Dependabot", + "provider": Providers.GITHUB, + "resource_type": "repo", + } GITLAB_CI = { "id": "gitlab_ci", diff --git a/backend/api/tasks/syncing.py b/backend/api/tasks/syncing.py index 3b42bbb00..b6430c812 100644 --- a/backend/api/tasks/syncing.py +++ b/backend/api/tasks/syncing.py @@ -8,6 +8,11 @@ from api.utils.syncing.github.actions import ( get_gh_actions_credentials, sync_github_secrets, + sync_github_org_secrets, +) +from api.utils.syncing.github.dependabot import ( + sync_github_dependabot_secrets, + sync_github_dependabot_org_secrets, ) from api.utils.syncing.vault.main import sync_vault_secrets from api.utils.syncing.nomad.main import sync_nomad_secrets @@ -84,6 +89,15 @@ def trigger_sync_tasks(env_sync): EnvironmentSyncEvent.objects.create(id=job_id, env_sync=env_sync) + elif env_sync.service == ServiceConfig.GITHUB_DEPENDABOT["id"]: + env_sync.status = EnvironmentSync.IN_PROGRESS + env_sync.save() + + job = perform_github_dependabot_sync.delay(env_sync) + job_id = job.get_id() + + EnvironmentSyncEvent.objects.create(id=job_id, env_sync=env_sync) + elif env_sync.service == ServiceConfig.HASHICORP_VAULT["id"]: env_sync.status = EnvironmentSync.IN_PROGRESS env_sync.save() @@ -255,19 +269,64 @@ def perform_cloudflare_pages_sync(environment_sync): def perform_github_actions_sync(environment_sync): access_token, api_host = get_gh_actions_credentials(environment_sync) - repo_name = environment_sync.options.get("repo_name") - repo_owner = environment_sync.options.get("owner") - environment_name = environment_sync.options.get("environment_name") + is_org_sync = environment_sync.options.get("org_sync", False) - handle_sync_event( - environment_sync, - sync_github_secrets, - access_token, - repo_name, - repo_owner, - api_host, - environment_name, - ) + if is_org_sync: + org = environment_sync.options.get("org") + visibility = environment_sync.options.get("visibility", "all") + handle_sync_event( + environment_sync, + sync_github_org_secrets, + access_token, + org, + api_host, + visibility, + ) + else: + repo_name = environment_sync.options.get("repo_name") + repo_owner = environment_sync.options.get("owner") + environment_name = environment_sync.options.get("environment_name") + + handle_sync_event( + environment_sync, + sync_github_secrets, + access_token, + repo_name, + repo_owner, + api_host, + environment_name, + ) + + +@job("default", timeout=DEFAULT_TIMEOUT) +def perform_github_dependabot_sync(environment_sync): + + access_token, api_host = get_gh_actions_credentials(environment_sync) + is_org_sync = environment_sync.options.get("org_sync", False) + + if is_org_sync: + org = environment_sync.options.get("org") + visibility = environment_sync.options.get("visibility", "all") + handle_sync_event( + environment_sync, + sync_github_dependabot_org_secrets, + access_token, + org, + api_host, + visibility, + ) + else: + repo_name = environment_sync.options.get("repo_name") + repo_owner = environment_sync.options.get("owner") + + handle_sync_event( + environment_sync, + sync_github_dependabot_secrets, + access_token, + repo_name, + repo_owner, + api_host, + ) @job("default", timeout=DEFAULT_TIMEOUT) diff --git a/backend/api/utils/syncing/github/actions.py b/backend/api/utils/syncing/github/actions.py index 00e621c67..c463c7ab9 100644 --- a/backend/api/utils/syncing/github/actions.py +++ b/backend/api/utils/syncing/github/actions.py @@ -20,11 +20,17 @@ class GitHubRepoType(ObjectType): type = graphene.String() +class GitHubOrgType(ObjectType): + name = graphene.String() + role = graphene.String() + + def normalize_api_host(api_host): if not api_host or api_host.strip() == "": api_host = GITHUB_CLOUD_API_URL - if settings.APP_HOST == "cloud": + app_host = getattr(settings, "APP_HOST", None) + if app_host == "cloud": validate_url_is_safe(api_host) stripped_host = api_host.rstrip("/") @@ -92,6 +98,52 @@ def serialize_repos(repos): return all_repos +def list_orgs(credential_id): + ProviderCredentials = apps.get_model("api", "ProviderCredentials") + + pk, sk = get_server_keypair() + credential = ProviderCredentials.objects.get(id=credential_id) + + access_token = decrypt_asymmetric( + credential.credentials["access_token"], sk.hex(), pk.hex() + ) + + api_host = GITHUB_CLOUD_API_URL + if "host" in credential.credentials: + api_host = decrypt_asymmetric( + credential.credentials["api_url"], sk.hex(), pk.hex() + ) + + api_host = normalize_api_host(api_host) + + headers = { + "Authorization": f"Bearer {access_token}", + "Accept": "application/vnd.github+json", + } + + all_orgs = [] + page = 1 + + while True: + response = requests.get( + f"{api_host}/user/orgs?per_page=100&page={page}", headers=headers + ) + if response.status_code != 200: + raise Exception(f"Error fetching organizations: {response.text}") + + orgs_on_page = response.json() + if not orgs_on_page: + break + + for org in orgs_on_page: + role = org.get("role", "member") + all_orgs.append({"name": org["login"], "role": role}) + + page += 1 + + return all_orgs + + def get_gh_actions_credentials(environment_sync): pk, sk = get_server_keypair() @@ -265,7 +317,10 @@ def sync_github_secrets( for key, value in local_secrets.items(): encrypted_value = encrypt_secret(public_key_value, value) if len(encrypted_value) > 64 * 1024: - continue # Skip oversized secret + message = ( + f"Secret '{key}' is too large to sync. GitHub Actions has a limit of 64KB for secrets." + ) + return False, {"message": message} secret_data = {"encrypted_value": encrypted_value, "key_id": key_id} if environment_name: @@ -306,3 +361,110 @@ def sync_github_secrets( traceback.print_exc() return False, {"message": f"An unexpected error occurred: {str(e)}"} + + +def get_all_org_secrets(org, headers, api_host=GITHUB_CLOUD_API_URL): + api_host = normalize_api_host(api_host) + all_secrets = [] + page = 1 + while True: + response = requests.get( + f"{api_host}/orgs/{org}/actions/secrets?page={page}", + headers=headers, + ) + if response.status_code != 200: + break + try: + data = response.json() + except json.JSONDecodeError: + break + if not data.get("secrets"): + break + all_secrets.extend(data["secrets"]) + page += 1 + return all_secrets + + +def sync_github_org_secrets( + secrets, + access_token, + org, + api_host=GITHUB_CLOUD_API_URL, + visibility="all", +): + api_host = normalize_api_host(api_host) + + try: + if not check_rate_limit(access_token, api_host): + return False, {"message": "Rate limit exceeded"} + + headers = { + "Authorization": f"Bearer {access_token}", + "Accept": "application/vnd.github+json", + } + + public_key_url = f"{api_host}/orgs/{org}/actions/secrets/public-key" + + public_key_response = requests.get(public_key_url, headers=headers) + if public_key_response.status_code != 200: + if public_key_response.status_code == 404: + return False, { + "response_code": public_key_response.status_code, + "message": "Unable to access organization. Please verify the organization exists and your access token has the required permissions (admin:org scope).", + } + return False, { + "response_code": public_key_response.status_code, + "message": f"Failed to fetch organization public key: {public_key_response.text}", + } + + public_key = public_key_response.json() + key_id = public_key["key_id"] + public_key_value = public_key["key"] + + local_secrets = {k: v for k, v, _ in secrets} + existing_secrets = get_all_org_secrets(org, headers, api_host) + existing_secret_names = {secret["name"] for secret in existing_secrets} + + for key, value in local_secrets.items(): + encrypted_value = encrypt_secret(public_key_value, value) + if len(encrypted_value) > 64 * 1024: + message = ( + f"Secret '{key}' is too large to sync. GitHub Actions has a limit of 64KB for secrets." + ) + return False, {"message": message} + + secret_data = { + "encrypted_value": encrypted_value, + "key_id": key_id, + "visibility": visibility, + } + secret_url = f"{api_host}/orgs/{org}/actions/secrets/{key}" + response = requests.put(secret_url, headers=headers, json=secret_data) + + if response.status_code not in [201, 204]: + return False, { + "response_code": response.status_code, + "message": f"Error syncing secret '{key}': {response.text}", + } + + for secret_name in existing_secret_names: + if secret_name not in local_secrets: + delete_url = f"{api_host}/orgs/{org}/actions/secrets/{secret_name}" + delete_response = requests.delete(delete_url, headers=headers) + if delete_response.status_code != 204: + return False, { + "response_code": delete_response.status_code, + "message": f"Error deleting secret '{secret_name}': {delete_response.text}", + } + + return True, {"message": "Organization secrets synced successfully"} + + except requests.RequestException as e: + return False, {"message": f"HTTP request error: {str(e)}"} + except json.JSONDecodeError: + return False, {"message": "Error decoding JSON response"} + except Exception as e: + import traceback + + traceback.print_exc() + return False, {"message": f"An unexpected error occurred: {str(e)}"} diff --git a/backend/api/utils/syncing/github/dependabot.py b/backend/api/utils/syncing/github/dependabot.py new file mode 100644 index 000000000..aa976b04a --- /dev/null +++ b/backend/api/utils/syncing/github/dependabot.py @@ -0,0 +1,225 @@ +import json +import requests + +from .actions import normalize_api_host, check_rate_limit, encrypt_secret + +GITHUB_CLOUD_API_URL = "https://api.github.com" + + +def get_all_dependabot_repo_secrets( + repo, owner, headers, api_host=GITHUB_CLOUD_API_URL +): + api_host = normalize_api_host(api_host) + all_secrets = [] + page = 1 + while True: + response = requests.get( + f"{api_host}/repos/{owner}/{repo}/dependabot/secrets?page={page}", + headers=headers, + ) + if response.status_code != 200: + break + try: + data = response.json() + except json.JSONDecodeError: + break + if not data.get("secrets"): + break + all_secrets.extend(data["secrets"]) + page += 1 + return all_secrets + + +def get_all_dependabot_org_secrets(org, headers, api_host=GITHUB_CLOUD_API_URL): + api_host = normalize_api_host(api_host) + all_secrets = [] + page = 1 + while True: + response = requests.get( + f"{api_host}/orgs/{org}/dependabot/secrets?page={page}", + headers=headers, + ) + if response.status_code != 200: + break + try: + data = response.json() + except json.JSONDecodeError: + break + if not data.get("secrets"): + break + all_secrets.extend(data["secrets"]) + page += 1 + return all_secrets + + +def sync_github_dependabot_secrets( + secrets, + access_token, + repo, + owner, + api_host=GITHUB_CLOUD_API_URL, +): + api_host = normalize_api_host(api_host) + + try: + if not check_rate_limit(access_token, api_host): + return False, {"message": "Rate limit exceeded"} + + headers = { + "Authorization": f"Bearer {access_token}", + "Accept": "application/vnd.github+json", + } + + public_key_url = ( + f"{api_host}/repos/{owner}/{repo}/dependabot/secrets/public-key" + ) + + public_key_response = requests.get(public_key_url, headers=headers) + if public_key_response.status_code != 200: + if public_key_response.status_code == 404: + return False, { + "response_code": public_key_response.status_code, + "message": "Unable to access repository. Please verify the repository exists and your access token has the required permissions (Dependabot secrets read/write).", + } + return False, { + "response_code": public_key_response.status_code, + "message": f"Failed to fetch repository public key: {public_key_response.text}", + } + + public_key = public_key_response.json() + key_id = public_key["key_id"] + public_key_value = public_key["key"] + + local_secrets = {k: v for k, v, _ in secrets} + existing_secrets = get_all_dependabot_repo_secrets( + repo, owner, headers, api_host + ) + existing_secret_names = {secret["name"] for secret in existing_secrets} + + for key, value in local_secrets.items(): + encrypted_value = encrypt_secret(public_key_value, value) + if len(encrypted_value) > 64 * 1024: + message = ( + f"Secret '{key}' is too large to sync. GitHub Dependabot has a limit of 64KB for secrets." + ) + return False, {"message": message} + + secret_data = {"encrypted_value": encrypted_value, "key_id": key_id} + secret_url = f"{api_host}/repos/{owner}/{repo}/dependabot/secrets/{key}" + response = requests.put(secret_url, headers=headers, json=secret_data) + + if response.status_code not in [201, 204]: + return False, { + "response_code": response.status_code, + "message": f"Error syncing secret '{key}': {response.text}", + } + + for secret_name in existing_secret_names: + if secret_name not in local_secrets: + delete_url = ( + f"{api_host}/repos/{owner}/{repo}/dependabot/secrets/{secret_name}" + ) + delete_response = requests.delete(delete_url, headers=headers) + if delete_response.status_code != 204: + return False, { + "response_code": delete_response.status_code, + "message": f"Error deleting secret '{secret_name}': {delete_response.text}", + } + + return True, {"message": "Dependabot secrets synced successfully"} + + except requests.RequestException as e: + return False, {"message": f"HTTP request error: {str(e)}"} + except json.JSONDecodeError: + return False, {"message": "Error decoding JSON response"} + except Exception as e: + import traceback + + traceback.print_exc() + return False, {"message": f"An unexpected error occurred: {str(e)}"} + + +def sync_github_dependabot_org_secrets( + secrets, + access_token, + org, + api_host=GITHUB_CLOUD_API_URL, + visibility="all", +): + api_host = normalize_api_host(api_host) + + try: + if not check_rate_limit(access_token, api_host): + return False, {"message": "Rate limit exceeded"} + + headers = { + "Authorization": f"Bearer {access_token}", + "Accept": "application/vnd.github+json", + } + + public_key_url = f"{api_host}/orgs/{org}/dependabot/secrets/public-key" + + public_key_response = requests.get(public_key_url, headers=headers) + if public_key_response.status_code != 200: + if public_key_response.status_code == 404: + return False, { + "response_code": public_key_response.status_code, + "message": "Unable to access organization. Please verify the organization exists and your access token has the required permissions (Dependabot secrets read/write).", + } + return False, { + "response_code": public_key_response.status_code, + "message": f"Failed to fetch organization public key: {public_key_response.text}", + } + + public_key = public_key_response.json() + key_id = public_key["key_id"] + public_key_value = public_key["key"] + + local_secrets = {k: v for k, v, _ in secrets} + existing_secrets = get_all_dependabot_org_secrets(org, headers, api_host) + existing_secret_names = {secret["name"] for secret in existing_secrets} + + for key, value in local_secrets.items(): + encrypted_value = encrypt_secret(public_key_value, value) + if len(encrypted_value) > 64 * 1024: + message = ( + f"Secret '{key}' is too large to sync. GitHub Dependabot has a limit of 64KB for secrets." + ) + return False, {"message": message} + + secret_data = { + "encrypted_value": encrypted_value, + "key_id": key_id, + "visibility": visibility or "all", + } + secret_url = f"{api_host}/orgs/{org}/dependabot/secrets/{key}" + response = requests.put(secret_url, headers=headers, json=secret_data) + + if response.status_code not in [201, 204]: + return False, { + "response_code": response.status_code, + "message": f"Error syncing secret '{key}': {response.text}", + } + + for secret_name in existing_secret_names: + if secret_name not in local_secrets: + delete_url = f"{api_host}/orgs/{org}/dependabot/secrets/{secret_name}" + delete_response = requests.delete(delete_url, headers=headers) + if delete_response.status_code != 204: + return False, { + "response_code": delete_response.status_code, + "message": f"Error deleting secret '{secret_name}': {delete_response.text}", + } + + return True, {"message": "Dependabot secrets synced successfully"} + + except requests.RequestException as e: + return False, {"message": f"HTTP request error: {str(e)}"} + except json.JSONDecodeError: + return False, {"message": "Error decoding JSON response"} + except Exception as e: + import traceback + + traceback.print_exc() + return False, {"message": f"An unexpected error occurred: {str(e)}"} + diff --git a/backend/backend/graphene/mutations/syncing.py b/backend/backend/graphene/mutations/syncing.py index dfc6d981e..0d4155fc0 100644 --- a/backend/backend/graphene/mutations/syncing.py +++ b/backend/backend/graphene/mutations/syncing.py @@ -276,15 +276,27 @@ class Arguments: env_id = graphene.ID() path = graphene.String() credential_id = graphene.ID() - repo_name = graphene.String() + repo_name = graphene.String(required=False) owner = graphene.String() environment_name = graphene.String(required=False) + org_sync = graphene.Boolean(required=False) + repo_visibility = graphene.String(required=False) sync = graphene.Field(EnvironmentSyncType) @classmethod def mutate( - cls, root, info, env_id, path, credential_id, repo_name, owner, environment_name=None + cls, + root, + info, + env_id, + path, + credential_id, + repo_name=None, + owner=None, + environment_name=None, + org_sync=False, + repo_visibility="all", ): service_id = "github_actions" service_config = ServiceConfig.get_service_config(service_id) @@ -303,16 +315,31 @@ def mutate( if not user_can_access_app(info.context.user.userId, env.app.id): raise GraphQLError("You don't have access to this app") - sync_options = {"repo_name": repo_name, "owner": owner} - if environment_name: - sync_options["environment_name"] = environment_name + if org_sync: + sync_options = { + "org": owner, + "org_sync": True, + "visibility": repo_visibility or "all", + } + else: + if not repo_name: + raise GraphQLError("Repository name is required for repository syncs") + sync_options = {"repo_name": repo_name, "owner": owner} + if environment_name: + sync_options["environment_name"] = environment_name existing_syncs = EnvironmentSync.objects.filter( environment__app_id=env.app.id, service=service_id, deleted_at=None ) for es in existing_syncs: - if es.options == sync_options: + # Block duplicate org syncs to the same org regardless of visibility + if org_sync and es.options.get("org") == owner and es.options.get("org_sync"): + raise GraphQLError( + "A sync already exists for this GitHub organization!" + ) + # Repo syncs must match all options to be considered duplicate + if not org_sync and es.options == sync_options: raise GraphQLError("A sync already exists for this GitHub repo!") sync = EnvironmentSync.objects.create( @@ -328,6 +355,86 @@ def mutate( return CreateGitHubActionsSync(sync=sync) +class CreateGitHubDependabotSync(graphene.Mutation): + class Arguments: + env_id = graphene.ID() + path = graphene.String() + credential_id = graphene.ID() + repo_name = graphene.String(required=False) + owner = graphene.String() + org_sync = graphene.Boolean(required=False) + repo_visibility = graphene.String(required=False) + + sync = graphene.Field(EnvironmentSyncType) + + @classmethod + def mutate( + cls, + root, + info, + env_id, + path, + credential_id, + repo_name=None, + owner=None, + org_sync=False, + repo_visibility="all", + ): + service_id = "github_dependabot" + + env = Environment.objects.get(id=env_id) + + if not owner: + raise GraphQLError("Owner is required for GitHub Dependabot syncs") + + if not env.app.sse_enabled: + raise GraphQLError("Syncing is not enabled for this environment!") + + if not user_can_access_app(info.context.user.userId, env.app.id): + raise GraphQLError("You don't have access to this app") + + if org_sync: + sync_options = { + "org": owner, + "org_sync": True, + "visibility": repo_visibility or "all", + } + else: + if not repo_name: + raise GraphQLError("Repository name is required for repository syncs") + sync_options = {"repo_name": repo_name, "owner": owner} + + existing_syncs = EnvironmentSync.objects.filter( + environment__app_id=env.app.id, service=service_id, deleted_at=None + ) + + for es in existing_syncs: + if ( + org_sync + and es.options.get("org") == owner + and es.options.get("org_sync") + and es.options.get("visibility", "all") + == sync_options.get("visibility", "all") + ): + raise GraphQLError( + "A sync already exists for this GitHub organization!" + ) + if not org_sync and es.options == sync_options: + raise GraphQLError("A sync already exists for this GitHub repo!") + + sync = EnvironmentSync.objects.create( + environment=env, + path=normalize_path_string(path), + service=service_id, + options=sync_options, + authentication_id=credential_id, + ) + + trigger_sync_tasks(sync) + + return CreateGitHubDependabotSync(sync=sync) + + class CreateVaultSync(graphene.Mutation): class Arguments: env_id = graphene.ID() diff --git a/backend/backend/graphene/queries/syncing.py b/backend/backend/graphene/queries/syncing.py index 5e82a692f..2d1e107fc 100644 --- a/backend/backend/graphene/queries/syncing.py +++ b/backend/backend/graphene/queries/syncing.py @@ -20,7 +20,7 @@ ) from api.services import Providers, ServiceConfig from api.utils.syncing.aws.secrets_manager import list_aws_secrets -from api.utils.syncing.github.actions import list_repos, list_environments +from api.utils.syncing.github.actions import list_repos, list_environments, list_orgs from api.utils.syncing.vault.main import test_vault_creds from api.utils.syncing.nomad.main import test_nomad_creds from api.utils.syncing.gitlab.main import list_gitlab_groups, list_gitlab_projects @@ -215,6 +215,14 @@ def resolve_github_environments(root, info, credential_id, owner, repo_name): raise GraphQLError(ex) +def resolve_gh_orgs(root, info, credential_id): + try: + orgs = list_orgs(credential_id) + return orgs + except Exception as ex: + raise GraphQLError(ex) + + def resolve_test_vault_creds(root, info, credential_id): try: valid = test_vault_creds(credential_id) diff --git a/backend/backend/schema.py b/backend/backend/schema.py index a62210c6e..8cd43793a 100644 --- a/backend/backend/schema.py +++ b/backend/backend/schema.py @@ -1,7 +1,7 @@ from api.utils.syncing.cloudflare.pages import CloudFlarePagesType from api.utils.syncing.cloudflare.workers import CloudflareWorkerType from api.utils.syncing.aws.secrets_manager import AWSSecretType -from api.utils.syncing.github.actions import GitHubRepoType +from api.utils.syncing.github.actions import GitHubRepoType, GitHubOrgType from api.utils.syncing.gitlab.main import GitLabGroupType, GitLabProjectType from api.utils.syncing.railway.main import RailwayProjectType from api.utils.syncing.render.main import RenderEnvGroupType, RenderServiceType @@ -71,6 +71,7 @@ from .graphene.queries.syncing import ( resolve_aws_secret_manager_secrets, resolve_gh_repos, + resolve_gh_orgs, resolve_github_environments, resolve_gitlab_projects, resolve_gitlab_groups, @@ -139,6 +140,7 @@ CreateAWSSecretsManagerSync, CreateCloudflarePagesSync, CreateGitHubActionsSync, + CreateGitHubDependabotSync, CreateGitLabCISync, CreateNomadSync, CreateProviderCredentials, @@ -395,6 +397,10 @@ class Query(graphene.ObjectType): GitHubRepoType, credential_id=graphene.ID(), ) + github_orgs = graphene.List( + GitHubOrgType, + credential_id=graphene.ID(), + ) github_environments = graphene.List( graphene.String, credential_id=graphene.ID(), @@ -475,6 +481,7 @@ class Query(graphene.ObjectType): resolve_aws_secrets = resolve_aws_secret_manager_secrets resolve_github_repos = resolve_gh_repos + resolve_github_orgs = resolve_gh_orgs resolve_github_environments = resolve_github_environments resolve_gitlab_projects = resolve_gitlab_projects @@ -1094,6 +1101,7 @@ class Mutation(graphene.ObjectType): # GitHub create_gh_actions_sync = CreateGitHubActionsSync.Field() + create_gh_dependabot_sync = CreateGitHubDependabotSync.Field() # Vault create_vault_sync = CreateVaultSync.Field() diff --git a/backend/tests/utils/syncing/test_github_actions.py b/backend/tests/utils/syncing/test_github_actions.py index 7059b397a..22a853b59 100644 --- a/backend/tests/utils/syncing/test_github_actions.py +++ b/backend/tests/utils/syncing/test_github_actions.py @@ -115,8 +115,8 @@ def test_sync_skips_oversized_secret(mock_get, mock_put, mock_encrypt, mock_exis success, result = sync_github_secrets( secrets, MOCK_ACCESS_TOKEN, MOCK_REPO, MOCK_OWNER ) - assert success - assert "synced successfully" in result["message"] + assert not success + assert "too large to sync" in result["message"] mock_put.assert_not_called() diff --git a/backend/tests/utils/syncing/test_github_dependabot.py b/backend/tests/utils/syncing/test_github_dependabot.py new file mode 100644 index 000000000..411490d71 --- /dev/null +++ b/backend/tests/utils/syncing/test_github_dependabot.py @@ -0,0 +1,174 @@ +import base64 +from unittest.mock import Mock, patch + +from api.utils.syncing.github.dependabot import ( + get_all_dependabot_repo_secrets, + get_all_dependabot_org_secrets, + sync_github_dependabot_secrets, + sync_github_dependabot_org_secrets, +) + +MOCK_ENCRYPTED_VALUE = base64.b64encode(b"encrypted").decode("utf-8") +MOCK_ACCESS_TOKEN = "ghp_x96ociStAXnGrAzhTrOJknn3vCzbqi1hsz4HR8iBPqk" +MOCK_REPO = "demo-repo" +MOCK_OWNER = "demo-owner" +MOCK_ORG = "demo-org" + + +def get_mocked_response(url, *args, **kwargs): + mock_response = Mock() + + if "/rate_limit" in url: + mock_response.status_code = 200 + mock_response.json.return_value = { + "resources": {"core": {"remaining": 5000, "reset": 9999999999}} + } + return mock_response + + if "/dependabot/secrets/public-key" in url: + mock_response.status_code = 200 + mock_response.json.return_value = { + "key_id": "1380204578043523344", + "key": "V+kIG2GZl25Tlr9LC6uPA5EzVfrow9I3VcVCKFkkwVM=", + } + return mock_response + + if "/dependabot/secrets" in url: + mock_response.status_code = 200 + mock_response.json.return_value = {"secrets": [{"name": "EXISTING_SECRET"}]} + return mock_response + + raise ValueError(f"Unhandled GET URL: {url}") + + +@patch("api.utils.syncing.github.dependabot.get_all_dependabot_repo_secrets", return_value=[]) +@patch( + "api.utils.syncing.github.dependabot.encrypt_secret", return_value=MOCK_ENCRYPTED_VALUE +) +@patch("api.utils.syncing.github.dependabot.requests.put") +@patch("api.utils.syncing.github.dependabot.requests.get") +def test_sync_dependabot_secrets_success(mock_get, mock_put, mock_encrypt, mock_existing): + mock_get.side_effect = get_mocked_response + mock_put.return_value.status_code = 201 + + secrets = [("MY_SECRET", "value", None)] + success, result = sync_github_dependabot_secrets( + secrets, MOCK_ACCESS_TOKEN, MOCK_REPO, MOCK_OWNER + ) + assert success + assert result["message"] == "Dependabot secrets synced successfully" + assert mock_encrypt.called + + +@patch("api.utils.syncing.github.dependabot.get_all_dependabot_repo_secrets", return_value=[]) +@patch( + "api.utils.syncing.github.dependabot.encrypt_secret", + return_value="A" * (64 * 1024 + 1), +) # too large +@patch("api.utils.syncing.github.dependabot.requests.put") +@patch("api.utils.syncing.github.dependabot.requests.get") +def test_sync_dependabot_oversized_secret(mock_get, mock_put, mock_encrypt, mock_existing): + mock_get.side_effect = get_mocked_response + secrets = [("HUGE_SECRET", "value", None)] + success, result = sync_github_dependabot_secrets( + secrets, MOCK_ACCESS_TOKEN, MOCK_REPO, MOCK_OWNER + ) + assert not success + assert "too large to sync" in result["message"] + mock_put.assert_not_called() + + +@patch( + "api.utils.syncing.github.dependabot.get_all_dependabot_repo_secrets", + return_value=[{"name": "OLD_SECRET"}], +) +@patch( + "api.utils.syncing.github.dependabot.encrypt_secret", return_value=MOCK_ENCRYPTED_VALUE +) +@patch("api.utils.syncing.github.dependabot.requests.delete") +@patch("api.utils.syncing.github.dependabot.requests.put") +@patch("api.utils.syncing.github.dependabot.requests.get") +def test_sync_dependabot_deletes_missing_secrets( + mock_get, mock_put, mock_delete, mock_encrypt, mock_existing +): + mock_get.side_effect = get_mocked_response + mock_put.return_value.status_code = 201 + mock_delete.return_value.status_code = 204 + + secrets = [("NEW_SECRET", "value", None)] + success, result = sync_github_dependabot_secrets( + secrets, MOCK_ACCESS_TOKEN, MOCK_REPO, MOCK_OWNER + ) + assert success + mock_delete.assert_called_once() + assert "synced successfully" in result["message"] + + +@patch("api.utils.syncing.github.dependabot.get_all_dependabot_repo_secrets", return_value=[]) +@patch( + "api.utils.syncing.github.dependabot.encrypt_secret", return_value=MOCK_ENCRYPTED_VALUE +) +@patch("api.utils.syncing.github.dependabot.requests.put") +@patch("api.utils.syncing.github.dependabot.requests.get") +def test_sync_dependabot_secrets_fails_on_put_error( + mock_get, mock_put, mock_encrypt, mock_existing +): + mock_get.side_effect = get_mocked_response + mock_put.return_value.status_code = 422 + mock_put.return_value.text = "Unprocessable Entity" + + secrets = [("BAD_SECRET", "value", None)] + success, result = sync_github_dependabot_secrets( + secrets, MOCK_ACCESS_TOKEN, MOCK_REPO, MOCK_OWNER + ) + assert not success + assert "Error syncing secret" in result["message"] + + +@patch("api.utils.syncing.github.dependabot.requests.get") +def test_get_all_dependabot_repo_secrets_pagination(mock_get): + page1_response = Mock() + page1_response.status_code = 200 + page1_response.json.return_value = {"secrets": [{"name": "SECRET1"}]} + + page2_response = Mock() + page2_response.status_code = 200 + page2_response.json.return_value = {"secrets": [{"name": "SECRET2"}]} + + page3_response = Mock() + page3_response.status_code = 200 + page3_response.json.return_value = {"secrets": []} + + mock_get.side_effect = [page1_response, page2_response, page3_response] + + headers = {"Authorization": f"Bearer {MOCK_ACCESS_TOKEN}"} + secrets = get_all_dependabot_repo_secrets(MOCK_REPO, MOCK_OWNER, headers) + + assert len(secrets) == 2 + assert secrets[0]["name"] == "SECRET1" + assert secrets[1]["name"] == "SECRET2" + + +@patch("api.utils.syncing.github.dependabot.requests.get") +def test_get_all_dependabot_org_secrets_pagination(mock_get): + page1_response = Mock() + page1_response.status_code = 200 + page1_response.json.return_value = {"secrets": [{"name": "ORG_SECRET1"}]} + + page2_response = Mock() + page2_response.status_code = 200 + page2_response.json.return_value = {"secrets": [{"name": "ORG_SECRET2"}]} + + page3_response = Mock() + page3_response.status_code = 200 + page3_response.json.return_value = {"secrets": []} + + mock_get.side_effect = [page1_response, page2_response, page3_response] + + headers = {"Authorization": f"Bearer {MOCK_ACCESS_TOKEN}"} + secrets = get_all_dependabot_org_secrets(MOCK_ORG, headers) + + assert len(secrets) == 2 + assert secrets[0]["name"] == "ORG_SECRET1" + assert secrets[1]["name"] == "ORG_SECRET2" + diff --git a/frontend/apollo/gql.ts b/frontend/apollo/gql.ts index b7f9ad524..2c9c0d2ed 100644 --- a/frontend/apollo/gql.ts +++ b/frontend/apollo/gql.ts @@ -84,7 +84,8 @@ const documents = { "mutation CreateNewCfWorkersSync($envId: ID!, $path: String!, $workerName: String!, $credentialId: ID!) {\n createCloudflareWorkersSync(\n envId: $envId\n path: $path\n workerName: $workerName\n credentialId: $credentialId\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}": types.CreateNewCfWorkersSyncDocument, "mutation DeleteProviderCreds($credentialId: ID!) {\n deleteProviderCredentials(credentialId: $credentialId) {\n ok\n }\n}": types.DeleteProviderCredsDocument, "mutation DeleteSync($syncId: ID!) {\n deleteEnvSync(syncId: $syncId) {\n ok\n }\n}": types.DeleteSyncDocument, - "mutation CreateNewGhActionsSync($envId: ID!, $path: String!, $repoName: String!, $owner: String!, $credentialId: ID!, $environmentName: String) {\n createGhActionsSync(\n envId: $envId\n path: $path\n repoName: $repoName\n owner: $owner\n credentialId: $credentialId\n environmentName: $environmentName\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}": types.CreateNewGhActionsSyncDocument, + "mutation CreateNewGhActionsSync($envId: ID!, $path: String!, $repoName: String, $owner: String!, $credentialId: ID!, $environmentName: String, $orgSync: Boolean, $repoVisibility: String) {\n createGhActionsSync(\n envId: $envId\n path: $path\n repoName: $repoName\n owner: $owner\n credentialId: $credentialId\n environmentName: $environmentName\n orgSync: $orgSync\n repoVisibility: $repoVisibility\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}": types.CreateNewGhActionsSyncDocument, + "mutation CreateNewGhDependabotSync($envId: ID!, $path: String!, $repoName: String, $owner: String!, $credentialId: ID!, $orgSync: Boolean, $repoVisibility: String) {\n createGhDependabotSync(\n envId: $envId\n path: $path\n repoName: $repoName\n owner: $owner\n credentialId: $credentialId\n orgSync: $orgSync\n repoVisibility: $repoVisibility\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}": types.CreateNewGhDependabotSyncDocument, "mutation CreateNewGitlabCiSync($envId: ID!, $path: String!, $credentialId: ID!, $resourcePath: String!, $resourceId: String!, $isGroup: Boolean!, $isMasked: Boolean!, $isProtected: Boolean!) {\n createGitlabCiSync(\n envId: $envId\n path: $path\n credentialId: $credentialId\n resourcePath: $resourcePath\n resourceId: $resourceId\n isGroup: $isGroup\n masked: $isMasked\n protected: $isProtected\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}": types.CreateNewGitlabCiSyncDocument, "mutation InitAppSyncing($appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n initEnvSync(appId: $appId, envKeys: $envKeys) {\n app {\n id\n sseEnabled\n }\n }\n}": types.InitAppSyncingDocument, "mutation CreateNewNomadSync($envId: ID!, $path: String!, $nomadPath: String!, $nomadNamespace: String!, $credentialId: ID!) {\n createNomadSync(\n envId: $envId\n path: $path\n nomadPath: $nomadPath\n nomadNamespace: $nomadNamespace\n credentialId: $credentialId\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}": types.CreateNewNomadSyncDocument, @@ -156,6 +157,7 @@ const documents = { "query GetServerKey {\n serverPublicKey\n}": types.GetServerKeyDocument, "query GetServiceList {\n services {\n id\n name\n provider {\n id\n }\n }\n}": types.GetServiceListDocument, "query GetGithubEnvironments($credentialId: ID!, $owner: String!, $repoName: String!) {\n githubEnvironments(\n credentialId: $credentialId\n owner: $owner\n repoName: $repoName\n )\n}": types.GetGithubEnvironmentsDocument, + "query GetGithubOrgs($credentialId: ID!) {\n githubOrgs(credentialId: $credentialId) {\n name\n role\n }\n}": types.GetGithubOrgsDocument, "query GetGithubRepos($credentialId: ID!) {\n githubRepos(credentialId: $credentialId) {\n name\n owner\n type\n }\n}": types.GetGithubReposDocument, "query GetGitLabResources($credentialId: ID!) {\n gitlabProjects(credentialId: $credentialId) {\n id\n name\n namespace {\n name\n fullPath\n }\n pathWithNamespace\n webUrl\n }\n gitlabGroups(credentialId: $credentialId) {\n id\n fullName\n fullPath\n webUrl\n }\n}": types.GetGitLabResourcesDocument, "query TestNomadAuth($credentialId: ID!) {\n testNomadCreds(credentialId: $credentialId)\n}": types.TestNomadAuthDocument, @@ -468,7 +470,11 @@ export function graphql(source: "mutation DeleteSync($syncId: ID!) {\n deleteEn /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "mutation CreateNewGhActionsSync($envId: ID!, $path: String!, $repoName: String!, $owner: String!, $credentialId: ID!, $environmentName: String) {\n createGhActionsSync(\n envId: $envId\n path: $path\n repoName: $repoName\n owner: $owner\n credentialId: $credentialId\n environmentName: $environmentName\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}"): (typeof documents)["mutation CreateNewGhActionsSync($envId: ID!, $path: String!, $repoName: String!, $owner: String!, $credentialId: ID!, $environmentName: String) {\n createGhActionsSync(\n envId: $envId\n path: $path\n repoName: $repoName\n owner: $owner\n credentialId: $credentialId\n environmentName: $environmentName\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}"]; +export function graphql(source: "mutation CreateNewGhActionsSync($envId: ID!, $path: String!, $repoName: String, $owner: String!, $credentialId: ID!, $environmentName: String, $orgSync: Boolean, $repoVisibility: String) {\n createGhActionsSync(\n envId: $envId\n path: $path\n repoName: $repoName\n owner: $owner\n credentialId: $credentialId\n environmentName: $environmentName\n orgSync: $orgSync\n repoVisibility: $repoVisibility\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}"): (typeof documents)["mutation CreateNewGhActionsSync($envId: ID!, $path: String!, $repoName: String, $owner: String!, $credentialId: ID!, $environmentName: String, $orgSync: Boolean, $repoVisibility: String) {\n createGhActionsSync(\n envId: $envId\n path: $path\n repoName: $repoName\n owner: $owner\n credentialId: $credentialId\n environmentName: $environmentName\n orgSync: $orgSync\n repoVisibility: $repoVisibility\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation CreateNewGhDependabotSync($envId: ID!, $path: String!, $repoName: String, $owner: String!, $credentialId: ID!, $orgSync: Boolean, $repoVisibility: String) {\n createGhDependabotSync(\n envId: $envId\n path: $path\n repoName: $repoName\n owner: $owner\n credentialId: $credentialId\n orgSync: $orgSync\n repoVisibility: $repoVisibility\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}"): (typeof documents)["mutation CreateNewGhDependabotSync($envId: ID!, $path: String!, $repoName: String, $owner: String!, $credentialId: ID!, $orgSync: Boolean, $repoVisibility: String) {\n createGhDependabotSync(\n envId: $envId\n path: $path\n repoName: $repoName\n owner: $owner\n credentialId: $credentialId\n orgSync: $orgSync\n repoVisibility: $repoVisibility\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -753,6 +759,10 @@ export function graphql(source: "query GetServiceList {\n services {\n id\n * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "query GetGithubEnvironments($credentialId: ID!, $owner: String!, $repoName: String!) {\n githubEnvironments(\n credentialId: $credentialId\n owner: $owner\n repoName: $repoName\n )\n}"): (typeof documents)["query GetGithubEnvironments($credentialId: ID!, $owner: String!, $repoName: String!) {\n githubEnvironments(\n credentialId: $credentialId\n owner: $owner\n repoName: $repoName\n )\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetGithubOrgs($credentialId: ID!) {\n githubOrgs(credentialId: $credentialId) {\n name\n role\n }\n}"): (typeof documents)["query GetGithubOrgs($credentialId: ID!) {\n githubOrgs(credentialId: $credentialId) {\n name\n role\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/frontend/apollo/graphql.ts b/frontend/apollo/graphql.ts index 21c966e43..b5c39b359 100644 --- a/frontend/apollo/graphql.ts +++ b/frontend/apollo/graphql.ts @@ -357,6 +357,11 @@ export type CreateGitHubActionsSync = { sync?: Maybe; }; +export type CreateGitHubDependabotSync = { + __typename?: 'CreateGitHubDependabotSync'; + sync?: Maybe; +}; + export type CreateGitLabCiSync = { __typename?: 'CreateGitLabCISync'; sync?: Maybe; @@ -730,6 +735,12 @@ export type EnvironmentTypeSecretsArgs = { path?: InputMaybe; }; +export type GitHubOrgType = { + __typename?: 'GitHubOrgType'; + name?: Maybe; + role?: Maybe; +}; + export type GitHubRepoType = { __typename?: 'GitHubRepoType'; name?: Maybe; @@ -907,6 +918,7 @@ export type Mutation = { createEnvironmentKey?: Maybe; createEnvironmentToken?: Maybe; createGhActionsSync?: Maybe; + createGhDependabotSync?: Maybe; createGitlabCiSync?: Maybe; createIdentity?: Maybe; createLockbox?: Maybe; @@ -1107,9 +1119,22 @@ export type MutationCreateGhActionsSyncArgs = { credentialId?: InputMaybe; envId?: InputMaybe; environmentName?: InputMaybe; + orgSync?: InputMaybe; owner?: InputMaybe; path?: InputMaybe; repoName?: InputMaybe; + repoVisibility?: InputMaybe; +}; + + +export type MutationCreateGhDependabotSyncArgs = { + credentialId?: InputMaybe; + envId?: InputMaybe; + orgSync?: InputMaybe; + owner?: InputMaybe; + path?: InputMaybe; + repoName?: InputMaybe; + repoVisibility?: InputMaybe; }; @@ -1793,6 +1818,7 @@ export type Query = { environmentTokens?: Maybe>>; folders?: Maybe>>; githubEnvironments?: Maybe>>; + githubOrgs?: Maybe>>; githubRepos?: Maybe>>; gitlabGroups?: Maybe>>; gitlabProjects?: Maybe>>; @@ -1923,6 +1949,11 @@ export type QueryGithubEnvironmentsArgs = { }; +export type QueryGithubOrgsArgs = { + credentialId?: InputMaybe; +}; + + export type QueryGithubReposArgs = { credentialId?: InputMaybe; }; @@ -3196,15 +3227,30 @@ export type DeleteSyncMutation = { __typename?: 'Mutation', deleteEnvSync?: { __ export type CreateNewGhActionsSyncMutationVariables = Exact<{ envId: Scalars['ID']['input']; path: Scalars['String']['input']; - repoName: Scalars['String']['input']; + repoName?: InputMaybe; owner: Scalars['String']['input']; credentialId: Scalars['ID']['input']; environmentName?: InputMaybe; + orgSync?: InputMaybe; + repoVisibility?: InputMaybe; }>; export type CreateNewGhActionsSyncMutation = { __typename?: 'Mutation', createGhActionsSync?: { __typename?: 'CreateGitHubActionsSync', sync?: { __typename?: 'EnvironmentSyncType', id: string, isActive: boolean, lastSync?: any | null, createdAt?: any | null, environment: { __typename?: 'EnvironmentType', id: string, name: string, envType: ApiEnvironmentEnvTypeChoices }, serviceInfo?: { __typename?: 'ServiceType', id?: string | null, name?: string | null } | null } | null } | null }; +export type CreateNewGhDependabotSyncMutationVariables = Exact<{ + envId: Scalars['ID']['input']; + path: Scalars['String']['input']; + repoName?: InputMaybe; + owner: Scalars['String']['input']; + credentialId: Scalars['ID']['input']; + orgSync?: InputMaybe; + repoVisibility?: InputMaybe; +}>; + + +export type CreateNewGhDependabotSyncMutation = { __typename?: 'Mutation', createGhDependabotSync?: { __typename?: 'CreateGitHubDependabotSync', sync?: { __typename?: 'EnvironmentSyncType', id: string, isActive: boolean, lastSync?: any | null, createdAt?: any | null, environment: { __typename?: 'EnvironmentType', id: string, name: string, envType: ApiEnvironmentEnvTypeChoices }, serviceInfo?: { __typename?: 'ServiceType', id?: string | null, name?: string | null } | null } | null } | null }; + export type CreateNewGitlabCiSyncMutationVariables = Exact<{ envId: Scalars['ID']['input']; path: Scalars['String']['input']; @@ -3762,6 +3808,13 @@ export type GetGithubEnvironmentsQueryVariables = Exact<{ export type GetGithubEnvironmentsQuery = { __typename?: 'Query', githubEnvironments?: Array | null }; +export type GetGithubOrgsQueryVariables = Exact<{ + credentialId: Scalars['ID']['input']; +}>; + + +export type GetGithubOrgsQuery = { __typename?: 'Query', githubOrgs?: Array<{ __typename?: 'GitHubOrgType', name?: string | null, role?: string | null } | null> | null }; + export type GetGithubReposQueryVariables = Exact<{ credentialId: Scalars['ID']['input']; }>; @@ -3898,7 +3951,8 @@ export const CreateNewCfPagesSyncDocument = {"kind":"Document","definitions":[{" export const CreateNewCfWorkersSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewCfWorkersSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workerName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createCloudflareWorkersSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"workerName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workerName"}}},{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sync"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"lastSync"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const DeleteProviderCredsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteProviderCreds"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteProviderCredentials"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; export const DeleteSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"syncId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteEnvSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"syncId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"syncId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; -export const CreateNewGhActionsSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewGhActionsSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"repoName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"owner"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"environmentName"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createGhActionsSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"repoName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"repoName"}}},{"kind":"Argument","name":{"kind":"Name","value":"owner"},"value":{"kind":"Variable","name":{"kind":"Name","value":"owner"}}},{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"environmentName"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sync"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"lastSync"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateNewGhActionsSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewGhActionsSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"repoName"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"owner"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"environmentName"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgSync"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"repoVisibility"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createGhActionsSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"repoName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"repoName"}}},{"kind":"Argument","name":{"kind":"Name","value":"owner"},"value":{"kind":"Variable","name":{"kind":"Name","value":"owner"}}},{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"environmentName"}}},{"kind":"Argument","name":{"kind":"Name","value":"orgSync"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgSync"}}},{"kind":"Argument","name":{"kind":"Name","value":"repoVisibility"},"value":{"kind":"Variable","name":{"kind":"Name","value":"repoVisibility"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sync"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"lastSync"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateNewGhDependabotSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewGhDependabotSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"repoName"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"owner"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgSync"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"repoVisibility"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createGhDependabotSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"repoName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"repoName"}}},{"kind":"Argument","name":{"kind":"Name","value":"owner"},"value":{"kind":"Variable","name":{"kind":"Name","value":"owner"}}},{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}},{"kind":"Argument","name":{"kind":"Name","value":"orgSync"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgSync"}}},{"kind":"Argument","name":{"kind":"Name","value":"repoVisibility"},"value":{"kind":"Variable","name":{"kind":"Name","value":"repoVisibility"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sync"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"lastSync"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateNewGitlabCiSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewGitlabCiSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"resourcePath"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"resourceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"isGroup"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"isMasked"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"isProtected"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createGitlabCiSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}},{"kind":"Argument","name":{"kind":"Name","value":"resourcePath"},"value":{"kind":"Variable","name":{"kind":"Name","value":"resourcePath"}}},{"kind":"Argument","name":{"kind":"Name","value":"resourceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"resourceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"isGroup"},"value":{"kind":"Variable","name":{"kind":"Name","value":"isGroup"}}},{"kind":"Argument","name":{"kind":"Name","value":"masked"},"value":{"kind":"Variable","name":{"kind":"Name","value":"isMasked"}}},{"kind":"Argument","name":{"kind":"Name","value":"protected"},"value":{"kind":"Variable","name":{"kind":"Name","value":"isProtected"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sync"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"lastSync"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const InitAppSyncingDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InitAppSyncing"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"initEnvSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"envKeys"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"sseEnabled"}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateNewNomadSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewNomadSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"nomadPath"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"nomadNamespace"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createNomadSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"nomadPath"},"value":{"kind":"Variable","name":{"kind":"Name","value":"nomadPath"}}},{"kind":"Argument","name":{"kind":"Name","value":"nomadNamespace"},"value":{"kind":"Variable","name":{"kind":"Name","value":"nomadNamespace"}}},{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sync"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"lastSync"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; @@ -3970,6 +4024,7 @@ export const GetSavedCredentialsDocument = {"kind":"Document","definitions":[{"k export const GetServerKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetServerKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverPublicKey"}}]}}]} as unknown as DocumentNode; export const GetServiceListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetServiceList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"provider"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetGithubEnvironmentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetGithubEnvironments"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"owner"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"repoName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"githubEnvironments"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}},{"kind":"Argument","name":{"kind":"Name","value":"owner"},"value":{"kind":"Variable","name":{"kind":"Name","value":"owner"}}},{"kind":"Argument","name":{"kind":"Name","value":"repoName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"repoName"}}}]}]}}]} as unknown as DocumentNode; +export const GetGithubOrgsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetGithubOrgs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"githubOrgs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]} as unknown as DocumentNode; export const GetGithubReposDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetGithubRepos"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"githubRepos"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"owner"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]}}]} as unknown as DocumentNode; export const GetGitLabResourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetGitLabResources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gitlabProjects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"namespace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"fullPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pathWithNamespace"}},{"kind":"Field","name":{"kind":"Name","value":"webUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"gitlabGroups"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"fullPath"}},{"kind":"Field","name":{"kind":"Name","value":"webUrl"}}]}}]}}]} as unknown as DocumentNode; export const TestNomadAuthDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"TestNomadAuth"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"testNomadCreds"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}]}]}}]} as unknown as DocumentNode; diff --git a/frontend/apollo/schema.graphql b/frontend/apollo/schema.graphql index 16dbd9b12..7c6441e4c 100644 --- a/frontend/apollo/schema.graphql +++ b/frontend/apollo/schema.graphql @@ -42,6 +42,7 @@ type Query { cloudflareWorkers(credentialId: ID): [CloudflareWorkerType] awsSecrets(credentialId: ID): [AWSSecretType] githubRepos(credentialId: ID): [GitHubRepoType] + githubOrgs(credentialId: ID): [GitHubOrgType] githubEnvironments(credentialId: ID, owner: String, repoName: String): [String] gitlabProjects(credentialId: ID): [GitLabProjectType] gitlabGroups(credentialId: ID): [GitLabGroupType] @@ -81,6 +82,7 @@ value as specified by """ scalar DateTime +"""An enumeration.""" enum ApiOrganisationPlanChoices { """Free""" FR @@ -189,6 +191,7 @@ type EnvironmentType { syncs: [EnvironmentSyncType]! } +"""An enumeration.""" enum ApiEnvironmentEnvTypeChoices { """Development""" DEV @@ -346,6 +349,7 @@ type AwsIamConfigType { stsEndpoint: String } +"""An enumeration.""" enum ApiSecretEventEventTypeChoices { """Create""" C @@ -409,6 +413,7 @@ type ProviderType { authScheme: String } +"""An enumeration.""" enum ApiDynamicSecretProviderChoices { """AWS""" AWS @@ -456,6 +461,7 @@ type DynamicSecretLeaseType { events: [DynamicSecretLeaseEventType] } +"""An enumeration.""" enum ApiDynamicSecretLeaseStatusChoices { """Created""" CREATED @@ -493,6 +499,7 @@ type DynamicSecretLeaseEventType { createdAt: DateTime! } +"""An enumeration.""" enum ApiDynamicSecretLeaseEventEventTypeChoices { """Created""" CREATED @@ -524,6 +531,7 @@ type EnvironmentSyncType { history: [EnvironmentSyncEventType!]! } +"""An enumeration.""" enum ApiEnvironmentSyncStatusChoices { """In progress""" IN_PROGRESS @@ -557,6 +565,7 @@ type EnvironmentSyncEventType { completedAt: DateTime } +"""An enumeration.""" enum ApiEnvironmentSyncEventStatusChoices { """In progress""" IN_PROGRESS @@ -632,6 +641,7 @@ type ActivatedPhaseLicenseType { activatedAt: DateTime! } +"""An enumeration.""" enum ApiActivatedPhaseLicensePlanChoices { """Free""" FR @@ -782,6 +792,11 @@ type GitHubRepoType { type: String } +type GitHubOrgType { + name: String + role: String +} + type GitLabProjectType { id: ID name: String @@ -1000,7 +1015,8 @@ type Mutation { createCloudflarePagesSync(credentialId: ID, deploymentId: ID, envId: ID, path: String, projectEnv: String, projectName: String): CreateCloudflarePagesSync createCloudflareWorkersSync(credentialId: ID, envId: ID, path: String, workerName: String): CreateCloudflareWorkersSync createAwsSecretSync(credentialId: ID, envId: ID, kmsId: String, path: String, secretName: String): CreateAWSSecretsManagerSync - createGhActionsSync(credentialId: ID, envId: ID, environmentName: String, owner: String, path: String, repoName: String): CreateGitHubActionsSync + createGhActionsSync(credentialId: ID, envId: ID, environmentName: String, orgSync: Boolean, owner: String, path: String, repoName: String, repoVisibility: String): CreateGitHubActionsSync + createGhDependabotSync(credentialId: ID, envId: ID, orgSync: Boolean, owner: String, path: String, repoName: String, repoVisibility: String): CreateGitHubDependabotSync createVaultSync(credentialId: ID, engine: String, envId: ID, path: String, vaultPath: String): CreateVaultSync createNomadSync(credentialId: ID, envId: ID, nomadNamespace: String, nomadPath: String, path: String): CreateNomadSync createGitlabCiSync(credentialId: ID, envId: ID, isGroup: Boolean, masked: Boolean, path: String, protected: Boolean, resourceId: String, resourcePath: String): CreateGitLabCISync @@ -1302,6 +1318,10 @@ type CreateGitHubActionsSync { sync: EnvironmentSyncType } +type CreateGitHubDependabotSync { + sync: EnvironmentSyncType +} + type CreateVaultSync { sync: EnvironmentSyncType } diff --git a/frontend/components/syncing/CreateSyncDialog.tsx b/frontend/components/syncing/CreateSyncDialog.tsx index 87e8d74ea..c3d5f3412 100644 --- a/frontend/components/syncing/CreateSyncDialog.tsx +++ b/frontend/components/syncing/CreateSyncDialog.tsx @@ -6,6 +6,7 @@ import { CreateCloudflarePagesSync } from './Cloudflare/CreateCloudflarePagesSyn import React from 'react' import { CreateAWSSecretsSync } from './AWS/CreateAWSSecretsSync' import { CreateGhActionsSync } from './GitHub/CreateGhActionsSync' +import { CreateGhDependabotSync } from './GitHub/CreateGhDependabotSync' import { CreateVaultSync } from './Vault/CreateVaultSync' import { CreateNomadSync } from './Nomad/CreateNomadSync' import { CreateGitLabCISync } from './GitLab/CreateGitLabCISync' @@ -58,6 +59,8 @@ export const CreateSyncDialog = (props: { return case 'github_actions': return + case 'github_dependabot': + return case 'gitlab_ci': return case 'hashicorp_vault': diff --git a/frontend/components/syncing/GitHub/CreateGhActionsSync.tsx b/frontend/components/syncing/GitHub/CreateGhActionsSync.tsx index ca5332a5c..8ea696360 100644 --- a/frontend/components/syncing/GitHub/CreateGhActionsSync.tsx +++ b/frontend/components/syncing/GitHub/CreateGhActionsSync.tsx @@ -1,4 +1,5 @@ import GetGithubRepos from '@/graphql/queries/syncing/github/getRepos.gql' +import GetGithubOrgs from '@/graphql/queries/syncing/github/getOrgs.gql' import GetAppSyncStatus from '@/graphql/queries/syncing/getAppSyncStatus.gql' import GetAppEnvironments from '@/graphql/queries/secrets/getAppEnvironments.gql' import GetSavedCredentials from '@/graphql/queries/syncing/getSavedCredentials.gql' @@ -7,8 +8,13 @@ import GetGithubEnvironments from '@/graphql/queries/syncing/github/getEnvironme import { useLazyQuery, useMutation, useQuery } from '@apollo/client' import { Fragment, useContext, useEffect, useState } from 'react' import { Button } from '../../common/Button' -import { EnvironmentType, GitHubRepoType, ProviderCredentialsType } from '@/apollo/graphql' -import { Combobox, RadioGroup, Transition } from '@headlessui/react' +import { + EnvironmentType, + GitHubRepoType, + GitHubOrgType, + ProviderCredentialsType, +} from '@/apollo/graphql' +import { Combobox, RadioGroup, Tab, Transition } from '@headlessui/react' import clsx from 'clsx' import { FaAngleDoubleDown, @@ -30,21 +36,44 @@ export const CreateGhActionsSync = (props: { appId: string; closeModal: () => vo const { appId, closeModal } = props const [createGhActionsSync, { data: syncData, loading: creating }] = - useMutation(CreateNewGhActionsSync) + useMutation(CreateNewGhActionsSync, { + onCompleted: () => { + toast.success('Created new Sync!') + closeModal() + }, + }) const [credential, setCredential] = useState(null) const [selectedRepo, setSelectedRepo] = useState(undefined) + const [selectedOrg, setSelectedOrg] = useState(undefined) const [selectedEnvironment, setSelectedEnvironment] = useState(undefined) const [query, setQuery] = useState('') + const [orgQuery, setOrgQuery] = useState('') const [envQuery, setEnvQuery] = useState('') const [repos, setRepos] = useState([]) + const [orgs, setOrgs] = useState([]) const [phaseEnv, setPhaseEnv] = useState(null) const [path, setPath] = useState('/') + const [isOrgSync, setIsOrgSync] = useState(false) + const [orgVisibility, setOrgVisibility] = useState<'all' | 'private'>('all') const [credentialsValid, setCredentialsValid] = useState(false) + const visibilityOptions = [ + { + value: 'all', + label: 'All repositories', + description: 'Make this secret available to all repositories in the organization.', + }, + { + value: 'private', + label: 'Private repositories', + description: 'Make this secret available only to private repositories in the organization.', + }, + ] + const { data: appEnvsData } = useQuery(GetAppEnvironments, { variables: { appId, @@ -55,6 +84,7 @@ export const CreateGhActionsSync = (props: { appId: string; closeModal: () => vo }) const [getGhRepos, { loading: loadingRepos }] = useLazyQuery(GetGithubRepos) + const [getGhOrgs, { loading: loadingOrgs }] = useLazyQuery(GetGithubOrgs) const { data: environmentsData } = useQuery(GetGithubEnvironments, { variables: { @@ -88,32 +118,50 @@ export const CreateGhActionsSync = (props: { appId: string; closeModal: () => vo toast.error('Please select credential to use for this sync') return false } else if (!credentialsValid) { - const { data: reposData } = await getGhRepos({ - variables: { - credentialId: credential.id, - }, - }) - if (reposData?.githubRepos) { - setRepos(reposData?.githubRepos) - setCredentialsValid(true) + const [reposResult, orgsResult] = await Promise.all([ + getGhRepos({ variables: { credentialId: credential.id } }), + getGhOrgs({ variables: { credentialId: credential.id } }), + ]) + if (reposResult.data?.githubRepos) { + setRepos(reposResult.data.githubRepos) + } + if (orgsResult.data?.githubOrgs) { + setOrgs(orgsResult.data.githubOrgs) } - } else if (selectedRepo === undefined) { + setCredentialsValid(true) + } else if (isOrgSync && selectedOrg === undefined) { + toast.error('Please select an organization to sync with!') + return false + } else if (!isOrgSync && selectedRepo === undefined) { toast.error('Please select a repo to sync with!') return false } else { - await createGhActionsSync({ - variables: { - envId: phaseEnv?.id, - path, - repoName: selectedRepo.name, - owner: selectedRepo.owner, - credentialId: credential.id, - environmentName: selectedEnvironment || null, - }, - refetchQueries: [{ query: GetAppSyncStatus, variables: { appId } }], - }) - toast.success('Created new Sync!') - closeModal() + if (isOrgSync) { + await createGhActionsSync({ + variables: { + envId: phaseEnv?.id, + path, + owner: selectedOrg!.name, + credentialId: credential.id, + orgSync: true, + repoVisibility: orgVisibility, + }, + refetchQueries: [{ query: GetAppSyncStatus, variables: { appId } }], + }) + } else { + await createGhActionsSync({ + variables: { + envId: phaseEnv?.id, + path, + repoName: selectedRepo!.name, + owner: selectedRepo!.owner, + credentialId: credential.id, + environmentName: selectedEnvironment || null, + orgSync: false, + }, + refetchQueries: [{ query: GetAppSyncStatus, variables: { appId } }], + }) + } } } @@ -129,6 +177,13 @@ export const CreateGhActionsSync = (props: { appId: string; closeModal: () => vo return repoNameMatches || repoOwnerMatches // Include the repo if either name or owner matches the query }) + const filteredOrgs = orgs.filter((org) => { + if (orgQuery === '') { + return true + } + return org.name?.toLowerCase().includes(orgQuery.toLowerCase()) || false + }) + return (
@@ -200,197 +255,381 @@ export const CreateGhActionsSync = (props: { appId: string; closeModal: () => vo
- - {({ open }) => ( - <> -
- - - -
- setQuery(event.target.value)} - required - displayValue={(repo: GitHubRepoType) => - repo ? `${repo?.owner}/${repo?.name}` : query || '' - } - /> -
- - - -
+ { + const orgSync = index === 1 + setIsOrgSync(orgSync) + if (orgSync) { + setSelectedRepo(undefined) + } else { + setSelectedOrg(undefined) + } + }} + > + + + {({ selected }) => ( +
+ Repository
-
- - -
- {filteredRepos.map((repo: GitHubRepoType) => ( - + + {({ selected }) => ( +
+ Organization +
+ )} +
+ + + +
+ + {({ open }) => ( + <> +
+ + + +
+ setQuery(event.target.value)} + required + displayValue={(repo: GitHubRepoType) => + repo ? `${repo?.owner}/${repo?.name}` : query || '' + } + /> +
+ + + +
+
+
+ - {({ active, selected }) => ( -
+
+ {filteredRepos.map((repo: GitHubRepoType) => ( + + {({ active, selected }) => ( +
+
+ +
+
+ {repo.name}{' '} + + {repo.type} + +
+
+ {repo.owner} +
+
+
+ {selected && ( + + )} +
+ )} +
+ ))} +
+ + + + )} + + + + {({ open }) => ( + <> +
+ + + +
+ -
- -
-
- {repo.name}{' '} - setEnvQuery(e.target.value)} + displayValue={(env?: string) => (env ? env : envQuery)} + aria-disabled={!selectedRepo} + placeholder={ + !selectedRepo + ? 'Select a repository' + : environments.length === 0 + ? 'No environments found' + : 'Select an environment' + } + /> +
+ + + +
+
+
+ {selectedRepo && ( + + +
+ + {({ active, selected }) => ( +
- {repo.type} - -
-
- {repo.owner} -
-
+
+ No environment (repo-level) +
+ {selected && ( + + )} +
+ )} + + {environments + .filter((env: string) => + envQuery + ? env.toLowerCase().includes(envQuery.toLowerCase()) + : true + ) + .map((env: string) => ( + + {({ active, selected }) => ( +
+
+ {env} +
+ {selected && ( + + )} +
+ )} +
+ ))}
- {selected && ( - + + + )} + + )} + +
+ + +
+ + {({ open }) => ( + <> +
+ + + +
+ setOrgQuery(event.target.value)} + required + displayValue={(org: GitHubOrgType) => + org ? org.name || '' : orgQuery || '' + } + placeholder={ + orgs.length === 0 + ? 'No organizations found' + : 'Select an organization' + } + /> +
+ + + +
+
+
+ + +
+ {filteredOrgs.length === 0 ? ( +
+ No organizations found +
+ ) : ( + filteredOrgs.map((org: GitHubOrgType) => ( + + {({ active, selected }) => ( +
+
+ +
+
+ {org.name} +
+
+ {org.role} +
+
+
+ {selected && ( + + )} +
+ )} +
+ )) )}
- )} - - ))} -
- - - - )} -
-
-
- - {({ open }) => ( - <> -
- - - -
- setEnvQuery(e.target.value)} - displayValue={(env?: string) => (env ? env : envQuery)} - aria-disabled={!selectedRepo} - placeholder={ - !selectedRepo - ? 'Select a repository' - : environments.length === 0 - ? 'No environments found' - : 'Select an environment' - } - /> -
- - - -
-
-
- {selectedRepo && ( - - -
- - {({ active, selected }) => ( + + + + )} + + + + + + +
+ {visibilityOptions.map((option) => ( + + {({ active, checked }) => (
-
- No environment (repo-level) -
- {selected && ( - + {checked ? ( + + ) : ( + )} + {option.label}
)} - - {environments - .filter((env: string) => - envQuery - ? env.toLowerCase().includes(envQuery.toLowerCase()) - : true - ) - .map((env: string) => ( - - {({ active, selected }) => ( -
-
- {env} -
- {selected && ( - - )} -
- )} -
- ))} -
- - - )} - - )} - + + ))} +
+

+ {visibilityOptions.find((o) => o.value === orgVisibility)?.description} +

+ +
+ + +
@@ -403,7 +642,11 @@ export const CreateGhActionsSync = (props: { appId: string; closeModal: () => vo )}
-
diff --git a/frontend/components/syncing/GitHub/CreateGhDependabotSync.tsx b/frontend/components/syncing/GitHub/CreateGhDependabotSync.tsx new file mode 100644 index 000000000..6baa0057e --- /dev/null +++ b/frontend/components/syncing/GitHub/CreateGhDependabotSync.tsx @@ -0,0 +1,518 @@ +import GetGithubRepos from '@/graphql/queries/syncing/github/getRepos.gql' +import GetGithubOrgs from '@/graphql/queries/syncing/github/getOrgs.gql' +import GetAppSyncStatus from '@/graphql/queries/syncing/getAppSyncStatus.gql' +import GetAppEnvironments from '@/graphql/queries/secrets/getAppEnvironments.gql' +import GetSavedCredentials from '@/graphql/queries/syncing/getSavedCredentials.gql' +import CreateNewGhDependabotSync from '@/graphql/mutations/syncing/github/CreateGhDependabotSync.gql' +import { useLazyQuery, useMutation, useQuery } from '@apollo/client' +import { Fragment, useContext, useEffect, useState } from 'react' +import { Button } from '../../common/Button' +import { EnvironmentType, GitHubRepoType, GitHubOrgType, ProviderCredentialsType } from '@/apollo/graphql' +import { Combobox, RadioGroup, Tab, Transition } from '@headlessui/react' +import clsx from 'clsx' +import { FaAngleDoubleDown, FaCheckCircle, FaChevronDown, FaCircle, FaDotCircle } from 'react-icons/fa' +import { toast } from 'react-toastify' + +import { organisationContext } from '@/contexts/organisationContext' +import { ProviderCredentialPicker } from '../ProviderCredentialPicker' +import { SiGithub } from 'react-icons/si' +import { Input } from '@/components/common/Input' + +export const CreateGhDependabotSync = (props: { appId: string; closeModal: () => void }) => { + const { activeOrganisation: organisation } = useContext(organisationContext) + + const { appId, closeModal } = props + + const [createGhDependabotSync, { loading: creating }] = useMutation(CreateNewGhDependabotSync, { + onCompleted: () => { + toast.success('Created new Sync!') + closeModal() + }, + }) + + const [credential, setCredential] = useState(null) + + const [selectedRepo, setSelectedRepo] = useState(undefined) + const [selectedOrg, setSelectedOrg] = useState(undefined) + const [query, setQuery] = useState('') + const [orgQuery, setOrgQuery] = useState('') + + const [repos, setRepos] = useState([]) + const [orgs, setOrgs] = useState([]) + const [phaseEnv, setPhaseEnv] = useState(null) + const [path, setPath] = useState('/') + + const [isOrgSync, setIsOrgSync] = useState(false) + const [orgVisibility, setOrgVisibility] = useState<'all' | 'private'>('all') + const [credentialsValid, setCredentialsValid] = useState(false) + + const visibilityOptions = [ + { + value: 'all', + label: 'All repositories', + description: 'Make this secret available to any repository in the organization.', + }, + { + value: 'private', + label: 'Private repositories', + description: 'Make this secret available only to private repositories in the organization.', + }, + ] + + const { data: appEnvsData } = useQuery(GetAppEnvironments, { + variables: { + appId, + }, + }) + const { data: credentialsData } = useQuery(GetSavedCredentials, { + variables: { orgId: organisation!.id }, + }) + + const [getGhRepos, { loading: loadingRepos }] = useLazyQuery(GetGithubRepos) + const [getGhOrgs, { loading: loadingOrgs }] = useLazyQuery(GetGithubOrgs) + + useEffect(() => { + if (credentialsData && credentialsData.savedCredentials.length > 0) { + setCredential(credentialsData.savedCredentials[0]) + } + }, [credentialsData]) + + useEffect(() => { + if (appEnvsData?.appEnvironments.length > 0) { + const defaultEnv: EnvironmentType = appEnvsData.appEnvironments[0] + setPhaseEnv(defaultEnv) + } + }, [appEnvsData]) + + const handleSubmit = async (e: { preventDefault: () => void }) => { + e.preventDefault() + + if (credential === null) { + toast.error('Please select credential to use for this sync') + return false + } else if (!credentialsValid) { + const [reposResult, orgsResult] = await Promise.all([ + getGhRepos({ variables: { credentialId: credential.id } }), + getGhOrgs({ variables: { credentialId: credential.id } }), + ]) + if (reposResult.data?.githubRepos) { + setRepos(reposResult.data.githubRepos) + } + if (orgsResult.data?.githubOrgs) { + setOrgs(orgsResult.data.githubOrgs) + } + setCredentialsValid(true) + } else if (isOrgSync && selectedOrg === undefined) { + toast.error('Please select an organization to sync with!') + return false + } else if (!isOrgSync && selectedRepo === undefined) { + toast.error('Please select a repo to sync with!') + return false + } else { + if (isOrgSync) { + await createGhDependabotSync({ + variables: { + envId: phaseEnv?.id, + path, + owner: selectedOrg!.name, + credentialId: credential.id, + orgSync: true, + repoVisibility: orgVisibility, + }, + refetchQueries: [{ query: GetAppSyncStatus, variables: { appId } }], + }) + } else { + await createGhDependabotSync({ + variables: { + envId: phaseEnv?.id, + path, + repoName: selectedRepo!.name, + owner: selectedRepo!.owner, + credentialId: credential.id, + orgSync: false, + }, + refetchQueries: [{ query: GetAppSyncStatus, variables: { appId } }], + }) + } + } + } + + const filteredRepos = repos.filter((repo) => { + if (query === '') { + return true + } + + const queryLower = query.toLowerCase() + const repoNameMatches = repo.name?.toLowerCase().includes(queryLower) || false + const repoOwnerMatches = repo.owner?.toLowerCase().includes(queryLower) || false + + return repoNameMatches || repoOwnerMatches + }) + + const filteredOrgs = orgs.filter((org) => { + if (orgQuery === '') { + return true + } + return org.name?.toLowerCase().includes(orgQuery.toLowerCase()) || false + }) + + return ( +
+
+
+ + GitHub Dependabot +
+
Sync an environment with GitHub Dependabot.
+
+ +
+ {!credentialsValid && ( +
+
+ Step 1: Choose authentication credentials +
+
+
+ setCredential(cred)} + orgId={organisation!.id} + providerFilter={'github'} + setDefault={true} + /> +
+
+
+ )} + + {credentialsValid && ( +
+
+ Step 2: Select source and destination for Secrets +
+
+ + + + +
+ {appEnvsData.appEnvironments.map((env: EnvironmentType) => ( + + {({ active, checked }) => ( +
+ {checked ? : } + {env.name} +
+ )} +
+ ))} +
+
+ + +
+ +
+
+ +
+
+ +
+
+ { + const orgSync = index === 1 + setIsOrgSync(orgSync) + if (orgSync) { + setSelectedRepo(undefined) + } else { + setSelectedOrg(undefined) + } + }} + > + + + {({ selected }) => ( +
+ Repository +
+ )} +
+ + {({ selected }) => ( +
+ Organization +
+ )} +
+
+ + +
+ + {({ open }) => ( + <> +
+ + + +
+ setQuery(event.target.value)} + required + displayValue={(repo: GitHubRepoType) => + repo ? `${repo?.owner}/${repo?.name}` : query || '' + } + /> +
+ + + +
+
+
+ + +
+ {filteredRepos.map((repo: GitHubRepoType) => ( + + {({ active, selected }) => ( +
+
+ +
+
+ {repo.name}{' '} + + {repo.type} + +
+
+ {repo.owner} +
+
+
+ {selected && ( + + )} +
+ )} +
+ ))} +
+
+
+ + )} +
+
+
+ +
+ + {({ open }) => ( + <> +
+ + + +
+ setOrgQuery(event.target.value)} + required + displayValue={(org: GitHubOrgType) => + org ? org.name || '' : orgQuery || '' + } + placeholder={ + orgs.length === 0 + ? 'No organizations found' + : 'Select an organization' + } + /> +
+ + + +
+
+
+ + +
+ {filteredOrgs.length === 0 ? ( +
+ No organizations found +
+ ) : ( + filteredOrgs.map((org: GitHubOrgType) => ( + + {({ active, selected }) => ( +
+
+ +
+
+ {org.name} +
+
+ {org.role} +
+
+
+ {selected && ( + + )} +
+ )} +
+ )) + )} +
+
+
+ + )} +
+ + + + + +
+ {visibilityOptions.map((option) => ( + + {({ active, checked }) => ( +
+ {checked ? ( + + ) : ( + + )} + {option.label} +
+ )} +
+ ))} +
+

+ {visibilityOptions.find((o) => o.value === orgVisibility)?.description} +

+
+
+
+
+
+
+
+
+ )} +
+
+ {credentialsValid && ( + + )} +
+ +
+
+
+ ) +} + diff --git a/frontend/components/syncing/GitHub/SetupGhAuth.tsx b/frontend/components/syncing/GitHub/SetupGhAuth.tsx index 3be25d1b2..25e9e2c3f 100644 --- a/frontend/components/syncing/GitHub/SetupGhAuth.tsx +++ b/frontend/components/syncing/GitHub/SetupGhAuth.tsx @@ -38,7 +38,7 @@ export const SetupGhAuth = () => { const hostname = `${window.location.protocol}//${window.location.host}` const redirectUri = `${hostname}/service/oauth/github/callback` - const scope = 'user,repo,admin:repo_hook,read:org' + const scope = 'user,repo,admin:repo_hook,admin:org' const statePayload = { returnUrl: path, diff --git a/frontend/components/syncing/ServiceInfo.tsx b/frontend/components/syncing/ServiceInfo.tsx index 1afe11a62..b1f6ed716 100644 --- a/frontend/components/syncing/ServiceInfo.tsx +++ b/frontend/components/syncing/ServiceInfo.tsx @@ -21,6 +21,22 @@ export const ServiceInfo = (props: { sync: EnvironmentSyncType }) => { return
{secretName}
} else if (sync.serviceInfo?.id?.includes('github')) { const ghSyncMeta = JSON.parse(sync.options) + const isOrgSync = ghSyncMeta['org_sync'] + + if (isOrgSync) { + const org = ghSyncMeta['org'] + const visibility = ghSyncMeta['visibility'] + + return ( +
+ {org}{' '} + + ({visibility === 'private' ? 'private repos' : 'all repos'}) + +
+ ) + } + const repoName = ghSyncMeta['repo_name'] const owner = ghSyncMeta['owner'] const ghEnv = ghSyncMeta['environment_name'] @@ -31,6 +47,32 @@ export const ServiceInfo = (props: { sync: EnvironmentSyncType }) => { {ghEnv && ({ghEnv})}
) + } else if (sync.serviceInfo?.id?.includes('dependabot')) { + const ghSyncMeta = JSON.parse(sync.options) + const isOrgSync = ghSyncMeta['org_sync'] + + if (isOrgSync) { + const org = ghSyncMeta['org'] + const visibility = ghSyncMeta['visibility'] + + return ( +
+ {org}{' '} + + ({visibility === 'private' ? 'private repos' : 'all repos'}) + +
+ ) + } + + const repoName = ghSyncMeta['repo_name'] + const owner = ghSyncMeta['owner'] + + return ( +
+ {owner}/{repoName} +
+ ) } else if (sync.serviceInfo?.id?.includes('hashicorp_vault')) { const engine = JSON.parse(sync.options)['engine'] const path = JSON.parse(sync.options)['path'] diff --git a/frontend/components/syncing/SyncHistory.tsx b/frontend/components/syncing/SyncHistory.tsx index 89c2864e9..e6f1d7bb2 100644 --- a/frontend/components/syncing/SyncHistory.tsx +++ b/frontend/components/syncing/SyncHistory.tsx @@ -22,7 +22,7 @@ const FormattedJSON = (props: { jsonData: string }) => { // Format the JSON (or keep the original string if it's not valid JSON) const formattedJSON = jsonData ? jsonData.message || jsonData.error || JSON.stringify(jsonData, null, 2) - : 'No detail available' + : 'No logs available' return (
diff --git a/frontend/graphql/mutations/syncing/github/CreateGhActionsSync.gql b/frontend/graphql/mutations/syncing/github/CreateGhActionsSync.gql index ba487e5b3..b99d2b1fe 100644 --- a/frontend/graphql/mutations/syncing/github/CreateGhActionsSync.gql +++ b/frontend/graphql/mutations/syncing/github/CreateGhActionsSync.gql @@ -1,5 +1,5 @@ -mutation CreateNewGhActionsSync($envId: ID!, $path: String!, $repoName: String!, $owner: String!, $credentialId: ID!, $environmentName: String) { - createGhActionsSync(envId: $envId, path: $path, repoName: $repoName, owner: $owner, credentialId: $credentialId, environmentName: $environmentName) { +mutation CreateNewGhActionsSync($envId: ID!, $path: String!, $repoName: String, $owner: String!, $credentialId: ID!, $environmentName: String, $orgSync: Boolean, $repoVisibility: String) { + createGhActionsSync(envId: $envId, path: $path, repoName: $repoName, owner: $owner, credentialId: $credentialId, environmentName: $environmentName, orgSync: $orgSync, repoVisibility: $repoVisibility) { sync { id environment { diff --git a/frontend/graphql/mutations/syncing/github/CreateGhDependabotSync.gql b/frontend/graphql/mutations/syncing/github/CreateGhDependabotSync.gql new file mode 100644 index 000000000..5357ebe17 --- /dev/null +++ b/frontend/graphql/mutations/syncing/github/CreateGhDependabotSync.gql @@ -0,0 +1,36 @@ +mutation CreateNewGhDependabotSync( + $envId: ID! + $path: String! + $repoName: String + $owner: String! + $credentialId: ID! + $orgSync: Boolean + $repoVisibility: String +) { + createGhDependabotSync( + envId: $envId + path: $path + repoName: $repoName + owner: $owner + credentialId: $credentialId + orgSync: $orgSync + repoVisibility: $repoVisibility + ) { + sync { + id + environment { + id + name + envType + } + serviceInfo { + id + name + } + isActive + lastSync + createdAt + } + } +} + diff --git a/frontend/graphql/queries/syncing/github/getOrgs.gql b/frontend/graphql/queries/syncing/github/getOrgs.gql new file mode 100644 index 000000000..5a624f824 --- /dev/null +++ b/frontend/graphql/queries/syncing/github/getOrgs.gql @@ -0,0 +1,7 @@ +query GetGithubOrgs($credentialId: ID!) { + githubOrgs(credentialId: $credentialId) { + name + role + } +} +