diff --git a/src/openhound_github/kinds/edges.py b/src/openhound_github/kinds/edges.py index 54cce07..cc89c84 100644 --- a/src/openhound_github/kinds/edges.py +++ b/src/openhound_github/kinds/edges.py @@ -13,6 +13,7 @@ # Access and capability edges CAN_ACCESS = "GH_CanAccess" +CAN_USE_RUNNER = "GH_CanUseRunner" CAN_CREATE_BRANCH = "GH_CanCreateBranch" CAN_EDIT_PROTECTION = "GH_CanEditProtection" CAN_WRITE_BRANCH = "GH_CanWriteBranch" diff --git a/src/openhound_github/kinds/nodes.py b/src/openhound_github/kinds/nodes.py index 4e5a2ac..4eb9355 100644 --- a/src/openhound_github/kinds/nodes.py +++ b/src/openhound_github/kinds/nodes.py @@ -30,9 +30,15 @@ REPO_ROLE = "GH_RepoRole" REPO_SECRET = "GH_RepoSecret" REPO_VARIABLE = "GH_RepoVariable" +REPO_RUNNER = "GH_RepoRunner" DEFAULT_ROLE = "GH_RepoRole" +# Runner nodes +RUNNER = "GH_Runner" +RUNNER_GROUP = "GH_RunnerGroup" +ORG_RUNNER = "GH_OrgRunner" + # Security nodes SECRET_SCANNING_ALERT = "GH_SecretScanningAlert" diff --git a/src/openhound_github/models/__init__.py b/src/openhound_github/models/__init__.py index 36b0086..b4b1c1d 100644 --- a/src/openhound_github/models/__init__.py +++ b/src/openhound_github/models/__init__.py @@ -23,6 +23,7 @@ from .repository_role import BaseRepoRole, RepoRole from .repository_secret import RepoSecret from .repository_variable import RepoVariable +from .runner import OrgRunner, OrgRunnerGroupMembership, RepoRunner, RunnerGroup from .saml_provider import SamlProvider from .scim_user import ScimResource from .secret_scanning_alert import SecretScanningAlert @@ -60,6 +61,10 @@ "SelectedOrgVariable", "RepoSecret", "RepoVariable", + "RunnerGroup", + "OrgRunner", + "OrgRunnerGroupMembership", + "RepoRunner", "SecretScanningAlert", "AppInstallation", "BaseRepoRole", diff --git a/src/openhound_github/models/org.py b/src/openhound_github/models/org.py index 8483161..9ab87e4 100644 --- a/src/openhound_github/models/org.py +++ b/src/openhound_github/models/org.py @@ -264,6 +264,12 @@ class GHOrganizationProperties(GHNodeProperties): default=None, metadata={"description": "Whether SHA pinning is required for GitHub Actions."}, ) + self_hosted_runners_enabled_repositories: str | None = field( + default=None, + metadata={ + "description": "Which repositories may use self-hosted runners: `all`, `selected`, or `none`." + }, + ) default_workflow_permissions: str | None = None can_approve_pull_request_reviews: bool | None = None query_organization_roles: str = "" @@ -345,6 +351,7 @@ class Organization(BaseAsset): actions_enabled_repositories: str | None = None actions_allowed_actions: str | None = None actions_sha_pinning_required: bool | None = None + self_hosted_runners_enabled_repositories: str | None = None default_workflow_permissions: str | None = None can_approve_pull_request_reviews: bool | None = None @@ -415,6 +422,7 @@ def as_node(self) -> GHNode: actions_enabled_repositories=self.actions_enabled_repositories, actions_allowed_actions=self.actions_allowed_actions, actions_sha_pinning_required=self.actions_sha_pinning_required, + self_hosted_runners_enabled_repositories=self.self_hosted_runners_enabled_repositories, default_workflow_permissions=self.default_workflow_permissions, can_approve_pull_request_reviews=self.can_approve_pull_request_reviews, query_organization_roles=f"MATCH (:GH_Organization {{node_id:'{oid}'}})-[:GH_Contains]->(n:GH_OrgRole) RETURN n", diff --git a/src/openhound_github/models/repository.py b/src/openhound_github/models/repository.py index f8472e1..5932687 100644 --- a/src/openhound_github/models/repository.py +++ b/src/openhound_github/models/repository.py @@ -93,6 +93,12 @@ class GHRepositoryProperties(GHNodeProperties): "description": "Whether GitHub Actions is enabled for this repository." }, ) + self_hosted_runners_enabled: bool | None = field( + default=None, + metadata={ + "description": "Whether the repository may use self-hosted runners." + }, + ) secret_scanning: str | None = field( default=None, metadata={ @@ -105,6 +111,7 @@ class GHRepositoryProperties(GHNodeProperties): query_roles: str = "" query_teams: str = "" query_workflows: str = "" + query_runners: str = "" query_environments: str = "" query_secrets: str = "" query_variables: str = "" @@ -206,6 +213,8 @@ class Repository(BaseAsset): forks: int | None = None open_issues: int | None = None watchers: int | None = None + actions_enabled: bool | None = None + self_hosted_runners_enabled: bool | None = None @property def owner_id(self) -> str: @@ -245,7 +254,8 @@ def as_node(self) -> GHNode: owner_id=self.owner_id or "", environment_name=self._lookup.org_login(), environmentid=self._lookup.org_id(), - # actions_enabled=self.actions_enabled, + actions_enabled=self.actions_enabled, + self_hosted_runners_enabled=self.self_hosted_runners_enabled, # secret_scanning=self.secret_scanning, query_branches=f"MATCH p=(:GH_Repository {{node_id: '{rid}'}})-[:GH_HasBranch]->(:GH_Branch) RETURN p", query_protected_branches=f"MATCH p=(:GH_Repository {{node_id: '{rid}'}})-[:GH_HasBranch]->(:GH_Branch)<-[:GH_ProtectedBy]-(:GH_BranchProtectionRule) RETURN p", @@ -253,6 +263,7 @@ def as_node(self) -> GHNode: query_roles=f"MATCH p=(:GH_RepoRole)-[*1..]->(:GH_Repository {{node_id: '{rid}'}}) RETURN p", query_teams=f"MATCH p=(:GH_Team)-[:GH_MemberOf|GH_HasRole*1..]->(:GH_RepoRole)-[]->(:GH_Repository {{node_id: '{rid}'}}) RETURN p", query_workflows=f"MATCH p=(:GH_Repository {{node_id:'{rid}'}})-[:GH_HasWorkflow]->(w:GH_Workflow) RETURN p", + query_runners=f"MATCH p=(:GH_Repository {{node_id:'{rid}'}})-[:GH_CanUseRunner]->(:GH_Runner) RETURN p", query_environments=f"MATCH p=(:GH_Repository {{node_id: '{rid}'}})-[:GH_HasEnvironment]->(:GH_Environment) RETURN p", query_secrets=f"MATCH p=(:GH_Repository {{node_id:'{rid}'}})-[:GH_HasSecret]->(:GH_Secret) RETURN p", query_variables=f"MATCH p=(:GH_Repository {{node_id:'{rid}'}})-[:GH_HasVariable]->(:GH_Variable) RETURN p", diff --git a/src/openhound_github/models/runner.py b/src/openhound_github/models/runner.py new file mode 100644 index 0000000..07e8e91 --- /dev/null +++ b/src/openhound_github/models/runner.py @@ -0,0 +1,382 @@ +import json +from dataclasses import dataclass, field + +from openhound.core.asset import BaseAsset, EdgeDef, NodeDef +from openhound.core.models.entries_dataclass import Edge, EdgePath, EdgeProperties +from pydantic import Field + +from openhound_github.graph import GHNode, GHNodeProperties +from openhound_github.kinds import edges as ek +from openhound_github.kinds import nodes as nk +from openhound_github.main import app + + +@dataclass +class GHRunnerGroupProperties(GHNodeProperties): + group_id: int | None = field( + default=None, metadata={"description": "The GitHub runner group ID."} + ) + group_name: str | None = field( + default=None, metadata={"description": "The runner group display name."} + ) + visibility: str | None = field( + default=None, + metadata={ + "description": "Which repositories can use this group: `all`, `private`, or `selected`." + }, + ) + default: bool | None = field( + default=None, + metadata={"description": "Whether this is the default runner group."}, + ) + inherited: bool | None = field( + default=None, + metadata={"description": "Whether this runner group is inherited."}, + ) + allows_public_repositories: bool | None = field( + default=None, + metadata={"description": "Whether public repositories may use this group."}, + ) + restricted_to_workflows: bool | None = field( + default=None, + metadata={"description": "Whether access is restricted to selected workflows."}, + ) + selected_workflows: str | None = field( + default=None, + metadata={"description": "JSON array of selected workflows, if configured."}, + ) + runners_url: str | None = field( + default=None, + metadata={"description": "API URL for runners in this group."}, + ) + environment_name: str | None = field( + default=None, + metadata={"description": "The name of the environment (GitHub organization)."}, + ) + query_runners: str | None = None + query_repositories: str | None = None + + +@app.asset( + node=NodeDef( + kind=nk.RUNNER_GROUP, + description="GitHub self-hosted runner group", + icon="server", + properties=GHRunnerGroupProperties, + ), + edges=[ + EdgeDef( + start=nk.ORGANIZATION, + end=nk.RUNNER_GROUP, + kind=ek.CONTAINS, + description="Organization contains runner group", + traversable=False, + ), + ], +) +class RunnerGroup(BaseAsset): + id: int + name: str + visibility: str | None = None + default: bool | None = None + inherited: bool | None = None + allows_public_repositories: bool | None = None + restricted_to_workflows: bool | None = None + selected_workflows: list[str] | None = None + runners_url: str | None = None + + @property + def node_id(self) -> str: + return f"{self._lookup.org_id()}_runner_group_{self.id}" + + @property + def as_node(self) -> GHNode: + gid = self.node_id + return GHNode( + kinds=[nk.RUNNER_GROUP], + properties=GHRunnerGroupProperties( + name=f"{self._lookup.org_login()}/{self.name}", + displayname=self.name, + node_id=gid, + group_id=self.id, + group_name=self.name, + visibility=self.visibility, + default=self.default, + inherited=self.inherited, + allows_public_repositories=self.allows_public_repositories, + restricted_to_workflows=self.restricted_to_workflows, + selected_workflows=json.dumps(self.selected_workflows or []), + runners_url=self.runners_url, + environment_name=self._lookup.org_login(), + environmentid=self._lookup.org_id(), + query_runners=f"MATCH p=(:GH_RunnerGroup {{node_id:'{gid}'}})-[:GH_Contains]->(:GH_OrgRunner) RETURN p", + query_repositories=f"MATCH p=(:GH_Repository)-[:GH_CanUseRunner]->(:GH_OrgRunner)<-[:GH_Contains]-(:GH_RunnerGroup {{node_id:'{gid}'}}) RETURN p", + ), + ) + + @property + def edges(self): + yield Edge( + kind=ek.CONTAINS, + start=EdgePath(value=self._lookup.org_id(), match_by="id"), + end=EdgePath(value=self.node_id, match_by="id"), + properties=EdgeProperties(traversable=False), + ) + + +@dataclass +class GHRunnerProperties(GHNodeProperties): + scope: str = field( + default="", + metadata={ + "description": "Whether the runner is organization or repository scoped." + }, + ) + runner_id: int | None = field( + default=None, metadata={"description": "The GitHub runner ID."} + ) + os: str | None = field( + default=None, metadata={"description": "The runner operating system."} + ) + status: str | None = field( + default=None, metadata={"description": "The runner status."} + ) + busy: bool | None = field( + default=None, metadata={"description": "Whether the runner is currently busy."} + ) + ephemeral: bool | None = field( + default=None, + metadata={"description": "Whether the runner is ephemeral."}, + ) + labels: str | None = field( + default=None, metadata={"description": "JSON array of runner labels."} + ) + runner_group_id: int | None = field( + default=None, metadata={"description": "The associated runner group ID."} + ) + runner_group_name: str | None = field( + default=None, metadata={"description": "The associated runner group name."} + ) + runner_group_visibility: str | None = field( + default=None, + metadata={"description": "Runner group visibility when organization scoped."}, + ) + repository_name: str | None = field( + default=None, + metadata={"description": "The repository name for repository-scoped runners."}, + ) + repository_id: str | None = field( + default=None, + metadata={ + "description": "The repository node_id for repository-scoped runners." + }, + ) + repository_full_name: str | None = field( + default=None, + metadata={ + "description": "The full repository name for repository-scoped runners." + }, + ) + environment_name: str = field( + default="", + metadata={"description": "The name of the environment (GitHub organization)."}, + ) + query_group: str | None = None + query_repositories: str | None = None + + +@app.asset( + node=NodeDef( + kind=nk.ORG_RUNNER, + description="GitHub organization-scoped self-hosted runner", + icon="microchip", + properties=GHRunnerProperties, + ) +) +class OrgRunner(BaseAsset): + id: int + name: str + os: str | None = None + status: str | None = None + busy: bool | None = None + ephemeral: bool | None = None + labels: list[dict] = Field(default_factory=list) + + @property + def node_id(self) -> str: + return f"{self._lookup.org_id()}_org_runner_{self.id}" + + @property + def as_node(self) -> GHNode: + rid = self.node_id + return GHNode( + kinds=[nk.ORG_RUNNER, nk.RUNNER], + properties=GHRunnerProperties( + name=self.name, + displayname=self.name, + node_id=rid, + scope="organization", + runner_id=self.id, + os=self.os, + status=self.status, + busy=self.busy, + ephemeral=self.ephemeral, + labels=json.dumps(self.labels), + environment_name=self._lookup.org_login(), + environmentid=self._lookup.org_id(), + query_group=f"MATCH p=(:GH_RunnerGroup)-[:GH_Contains]->(:GH_OrgRunner {{node_id:'{rid}'}}) RETURN p", + query_repositories=f"MATCH p=(:GH_Repository)-[:GH_CanUseRunner]->(:GH_OrgRunner {{node_id:'{rid}'}}) RETURN p", + ), + ) + + @property + def edges(self): + return [] + + +@app.asset( + edges=[ + EdgeDef( + start=nk.RUNNER_GROUP, + end=nk.ORG_RUNNER, + kind=ek.CONTAINS, + description="Runner group contains organization runner", + traversable=False, + ), + EdgeDef( + start=nk.REPOSITORY, + end=nk.ORG_RUNNER, + kind=ek.CAN_USE_RUNNER, + description="Repository can dispatch jobs to runner", + traversable=False, + ), + ], +) +class OrgRunnerGroupMembership(BaseAsset): + runner_group_id: int + runner_id: int + accessible_repo_node_ids: list[str] = Field(default_factory=list) + + @property + def as_node(self): + return None + + @property + def _runner_node_id(self): + return f"{self._lookup.org_id()}_org_runner_{self.runner_id}" + + @property + def _contains_edge(self): + yield Edge( + kind=ek.CONTAINS, + start=EdgePath( + value=f"{self._lookup.org_id()}_runner_group_{self.runner_group_id}", + match_by="id", + ), + end=EdgePath(value=self._runner_node_id, match_by="id"), + properties=EdgeProperties(traversable=False), + ) + + @property + def _can_use_runner_edges(self): + for repo_node_id in self.accessible_repo_node_ids: + yield Edge( + kind=ek.CAN_USE_RUNNER, + start=EdgePath(value=repo_node_id, match_by="id"), + end=EdgePath(value=self._runner_node_id, match_by="id"), + properties=EdgeProperties(traversable=False), + ) + + @property + def edges(self): + yield from self._can_use_runner_edges + yield from self._contains_edge + + +@app.asset( + node=NodeDef( + kind=nk.REPO_RUNNER, + description="GitHub repository-scoped self-hosted runner", + icon="microchip", + properties=GHRunnerProperties, + ), + edges=[ + EdgeDef( + start=nk.REPOSITORY, + end=nk.REPO_RUNNER, + kind=ek.CONTAINS, + description="Repository contains repository runner", + traversable=False, + ), + EdgeDef( + start=nk.REPOSITORY, + end=nk.REPO_RUNNER, + kind=ek.CAN_USE_RUNNER, + description="Repository can dispatch jobs to repository runner", + traversable=False, + ), + ], +) +class RepoRunner(BaseAsset): + id: int + name: str + os: str | None = None + status: str | None = None + busy: bool | None = None + ephemeral: bool | None = None + labels: list[dict] = Field(default_factory=list) + repository_name: str + repository_node_id: str + repository_full_name: str + + @property + def node_id(self) -> str: + return f"{self.repository_node_id}_repo_runner_{self.id}" + + @property + def as_node(self) -> GHNode: + rid = self.node_id + return GHNode( + kinds=[nk.REPO_RUNNER, nk.RUNNER], + properties=GHRunnerProperties( + name=self.name, + displayname=self.name, + node_id=rid, + scope="repository", + runner_id=self.id, + os=self.os, + status=self.status, + busy=self.busy, + ephemeral=self.ephemeral, + labels=json.dumps(self.labels), + repository_name=self.repository_name, + repository_id=self.repository_node_id, + repository_full_name=self.repository_full_name, + environment_name=self._lookup.org_login(), + environmentid=self._lookup.org_id(), + query_repositories=f"MATCH p=(:GH_Repository {{node_id:'{self.repository_node_id}'}})-[:GH_CanUseRunner]->(:GH_RepoRunner {{node_id:'{rid}'}}) RETURN p", + ), + ) + + @property + def _contains_edge(self): + yield Edge( + kind=ek.CONTAINS, + start=EdgePath(value=self.repository_node_id, match_by="id"), + end=EdgePath(value=self.node_id, match_by="id"), + properties=EdgeProperties(traversable=False), + ) + + @property + def _can_use_runner_edge(self): + yield Edge( + kind=ek.CAN_USE_RUNNER, + start=EdgePath(value=self.repository_node_id, match_by="id"), + end=EdgePath(value=self.node_id, match_by="id"), + properties=EdgeProperties(traversable=False), + ) + + @property + def edges(self): + yield from self._can_use_runner_edge + yield from self._contains_edge diff --git a/src/openhound_github/source.py b/src/openhound_github/source.py index 48675e4..e12cc04 100644 --- a/src/openhound_github/source.py +++ b/src/openhound_github/source.py @@ -1,7 +1,6 @@ from dataclasses import dataclass from datetime import datetime -from typing import Any, Iterator, Optional -from typing import Union +from typing import Any, Iterator, Optional, Union import dlt from dlt.common.configuration import configspec @@ -43,6 +42,8 @@ OrgRole, OrgRoleMember, OrgRoleTeam, + OrgRunner, + OrgRunnerGroupMembership, OrgSecret, OrgVariable, PatRepoAccess, @@ -50,10 +51,12 @@ PersonalAccessTokenRequest, RepoRole, RepoRoleAssignment, + RepoRunner, RepoSecret, Repository, RepositoryQL, RepoVariable, + RunnerGroup, SamlProvider, ScimResource, SecretScanningAlert, @@ -77,6 +80,37 @@ class SourceContext: org_name: str +def _runner_group_repo_node_ids( + group: dict[str, Any], ctx: SourceContext, repos: list +) -> list[str]: + visibility = group.get("visibility") + if visibility == "selected": + repo_node_ids: list[str] = [] + try: + for page in ctx.client.paginate( + f"/orgs/{ctx.org_name}/actions/runner-groups/{group['id']}/repositories", + params={"per_page": 100}, + data_selector="repositories", + ): + repo_node_ids.extend( + repo.get("node_id") for repo in page if repo.get("node_id") + ) + except Exception: + return [] + return repo_node_ids + + repo_node_ids = [] + for repo in repos: + if visibility == "all": + repo_node_ids.append(repo["node_id"]) + elif visibility == "private" and repo.get("visibility") in { + "private", + "internal", + }: + repo_node_ids.append(repo["node_id"]) + return repo_node_ids + + @configspec class GithubCredentials(CredentialsConfiguration): org_name: str = None @@ -134,6 +168,9 @@ def organizations(ctx: SourceContext): org_data = ctx.client.get(f"/orgs/{ctx.org_name}").json() actions = ctx.client.get(f"/orgs/{ctx.org_name}/actions/permissions").json() + self_hosted_runners = ctx.client.get( + f"/orgs/{ctx.org_name}/actions/permissions/self-hosted-runners" + ).json() workflow_perms = ctx.client.get( f"/orgs/{ctx.org_name}/actions/permissions/workflow" ).json() @@ -141,6 +178,9 @@ def organizations(ctx: SourceContext): org_data["actions_enabled_repositories"] = actions.get("enabled_repositories") org_data["actions_allowed_actions"] = actions.get("allowed_actions") org_data["actions_sha_pinning_required"] = actions.get("sha_pinning_required") + org_data["self_hosted_runners_enabled_repositories"] = self_hosted_runners.get( + "enabled_repositories" + ) org_data["default_workflow_permissions"] = workflow_perms.get( "default_workflow_permissions" ) @@ -448,11 +488,54 @@ def repositories(ctx: SourceContext): Yields: Repository (Repository): Repository record. """ + actions = ctx.client.get(f"/orgs/{ctx.org_name}/actions/permissions").json() + runner_settings = ctx.client.get( + f"/orgs/{ctx.org_name}/actions/permissions/self-hosted-runners" + ).json() + + enabled_repo_ids: set[str] | None = None + if actions.get("enabled_repositories") == "selected": + enabled_repo_ids = set() + for page in ctx.client.paginate( + f"/orgs/{ctx.org_name}/actions/permissions/repositories", + params={"per_page": 100}, + data_selector="repositories", + ): + enabled_repo_ids.update( + repo["node_id"] for repo in page if repo.get("node_id") + ) + + runner_enabled_repo_ids: set[str] | None = None + if runner_settings.get("enabled_repositories") == "selected": + runner_enabled_repo_ids = set() + for page in ctx.client.paginate( + f"/orgs/{ctx.org_name}/actions/permissions/self-hosted-runners/repositories", + params={"per_page": 100}, + data_selector="repositories", + ): + runner_enabled_repo_ids.update( + repo["node_id"] for repo in page if repo.get("node_id") + ) + for page in ctx.client.paginate( f"/orgs/{ctx.org_name}/repos", params={"per_page": 100} ): for repo in page: - yield repo + repo_node_id = repo.get("node_id") + actions_enabled = actions.get("enabled_repositories") == "all" or ( + enabled_repo_ids is not None and repo_node_id in enabled_repo_ids + ) + self_hosted_runners_enabled = runner_settings.get( + "enabled_repositories" + ) == "all" or ( + runner_enabled_repo_ids is not None + and repo_node_id in runner_enabled_repo_ids + ) + yield { + **repo, + "actions_enabled": actions_enabled, + "self_hosted_runners_enabled": self_hosted_runners_enabled, + } @app.transformer( @@ -764,6 +847,75 @@ def environments(repo: Repository, ctx: SourceContext): } +@app.resource(name="runner_groups", columns=RunnerGroup, parallelized=True) +def runner_groups(ctx: SourceContext): + for page in ctx.client.paginate( + f"/orgs/{ctx.org_name}/actions/runner-groups", + params={"per_page": 100}, + data_selector="runner_groups", + ): + for group in page: + yield group + + +@app.resource(name="org_runners", columns=OrgRunner, parallelized=True) +def org_runners(ctx: SourceContext): + for page in ctx.client.paginate( + f"/orgs/{ctx.org_name}/actions/runners", + params={"per_page": 100}, + data_selector="runners", + ): + for runner in page: + yield runner + + +@app.resource( + name="org_runner_group_memberships", + columns=OrgRunnerGroupMembership, + parallelized=True, +) +def org_runner_group_memberships(ctx: SourceContext, repos: list): + for group_page in ctx.client.paginate( + f"/orgs/{ctx.org_name}/actions/runner-groups", + params={"per_page": 100}, + data_selector="runner_groups", + ): + for group in group_page: + accessible_repo_node_ids = _runner_group_repo_node_ids(group, ctx, repos) + try: + for runner_page in ctx.client.paginate( + f"/orgs/{ctx.org_name}/actions/runner-groups/{group['id']}/runners", + params={"per_page": 100}, + data_selector="runners", + ): + for runner in runner_page: + yield { + "runner_group_id": group["id"], + "runner_id": runner["id"], + "accessible_repo_node_ids": accessible_repo_node_ids, + } + except Exception: + continue + + +@app.transformer(name="repo_runners", columns=RepoRunner, parallelized=True) +def repo_runners(repo: Repository, ctx: SourceContext): + if not repo.self_hosted_runners_enabled: + return + for page in ctx.client.paginate( + f"/repos/{repo.full_name}/actions/runners", + params={"per_page": 100}, + data_selector="runners", + ): + for runner in page: + yield { + **runner, + "repository_name": repo.name, + "repository_node_id": repo.node_id, + "repository_full_name": repo.full_name, + } + + @app.transformer( name="environment_variables", columns=EnvironmentVariable, parallelized=True ) @@ -1271,6 +1423,7 @@ def retry_policy( teams_resource = teams(ctx) repositories_graphql_resource = repositories_graphql(ctx) app_installs_resource = app_installations(ctx) + runner_groups_resource = runner_groups(ctx) branch_prot_rules_resource = ( repositories_graphql_resource | branch_protection_rules(ctx) ) @@ -1286,6 +1439,7 @@ def retry_policy( repos_resource, repos_resource | repository_roles(repo_roles_base), repos_resource | workflows(ctx), + repos_resource | repo_runners(ctx), repos_resource | repository_secrets(ctx), repos_resource | repository_variables(ctx), repos_resource | repo_role_assignments(ctx, repo_roles_base), @@ -1296,6 +1450,9 @@ def retry_policy( teams_resource, teams_resource | team_members(ctx), teams_resource | team_roles(), + runner_groups_resource, + org_runners(ctx), + org_runner_group_memberships(ctx, list(repos_resource)), personal_access_tokens_resource, personal_access_tokens_resource | pat_repo_access(ctx), organization_secrets_resource,