From 9f511e04a781ff77b09766ae05fb255906133171 Mon Sep 17 00:00:00 2001 From: anderdc Date: Sat, 16 May 2026 14:18:58 -0500 Subject: [PATCH] feat(api): add repo maintainers endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add GET /api/v1/repos/:owner/:repo/maintainers, returning users whose latest known association for the repo is OWNER, MEMBER, or COLLABORATOR. Reads the existing contributor_repo_roles view — no new table or GitHub API permission. An unknown repo returns an empty maintainers list. Consumed by the gittensor validator to route the per-repo maintainer_cut emission carve-out. --- packages/das/src/api/api.module.ts | 4 ++ .../das/src/api/repos/repos.controller.ts | 27 ++++++++++++ packages/das/src/api/repos/repos.service.ts | 41 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 packages/das/src/api/repos/repos.controller.ts create mode 100644 packages/das/src/api/repos/repos.service.ts diff --git a/packages/das/src/api/api.module.ts b/packages/das/src/api/api.module.ts index 14b6fcc..c6cb2a0 100644 --- a/packages/das/src/api/api.module.ts +++ b/packages/das/src/api/api.module.ts @@ -19,6 +19,8 @@ import { MinersController } from "./miners/miners.controller"; import { MinersService } from "./miners/miners.service"; import { PullsController } from "./pulls/pulls.controller"; import { PullsService } from "./pulls/pulls.service"; +import { ReposController } from "./repos/repos.controller"; +import { ReposService } from "./repos/repos.service"; @Module({ imports: [ @@ -36,6 +38,7 @@ import { PullsService } from "./pulls/pulls.service"; DashboardController, MinersController, PullsController, + ReposController, AdminController, HealthController, ], @@ -43,6 +46,7 @@ import { PullsService } from "./pulls/pulls.service"; DashboardService, MinersService, PullsService, + ReposService, RequireApiKeyGuard, ], }) diff --git a/packages/das/src/api/repos/repos.controller.ts b/packages/das/src/api/repos/repos.controller.ts new file mode 100644 index 0000000..09314a7 --- /dev/null +++ b/packages/das/src/api/repos/repos.controller.ts @@ -0,0 +1,27 @@ +import { Controller, Get, Param } from "@nestjs/common"; +import { ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger"; +import { ReposService } from "./repos.service"; + +@ApiTags("Repos") +@Controller("api/v1/repos") +export class ReposController { + constructor(private readonly repos: ReposService) {} + + @Get(":owner/:repo/maintainers") + @ApiOperation({ + summary: "Maintainer-role contributors for a repo", + description: + "Returns users whose latest known GitHub association for the repo " + + "is OWNER, MEMBER, or COLLABORATOR, synthesized from PR/issue/" + + "review/comment activity (contributor_repo_roles view). An unknown " + + "repo returns an empty maintainers list, not a 404.", + }) + @ApiParam({ name: "owner", description: "Repository owner (org or user)" }) + @ApiParam({ name: "repo", description: "Repository name" }) + async getMaintainers( + @Param("owner") owner: string, + @Param("repo") repo: string, + ): Promise { + return this.repos.getMaintainers(owner, repo); + } +} diff --git a/packages/das/src/api/repos/repos.service.ts b/packages/das/src/api/repos/repos.service.ts new file mode 100644 index 0000000..18e3b9c --- /dev/null +++ b/packages/das/src/api/repos/repos.service.ts @@ -0,0 +1,41 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */ +import { Injectable } from "@nestjs/common"; +import { DataSource } from "typeorm"; + +@Injectable() +export class ReposService { + constructor(private readonly dataSource: DataSource) {} + + async getMaintainers( + owner: string, + repo: string, + ): Promise<{ + repo_full_name: string; + generated_at: string; + maintainers: unknown[]; + }> { + const repoFullName = `${owner}/${repo}`; + + // The association literals must stay in sync with gittensor + // constants.py MAINTAINER_ASSOCIATIONS. + const rows = await this.dataSource.query( + ` + SELECT + cr.author_github_id AS github_id, + cr.author_login AS login, + cr.author_association AS association + FROM contributor_repo_roles cr + WHERE LOWER(cr.repo_full_name) = LOWER($1) + AND cr.author_association IN ('OWNER', 'MEMBER', 'COLLABORATOR') + ORDER BY cr.author_github_id + `, + [repoFullName], + ); + + return { + repo_full_name: repoFullName.toLowerCase(), + generated_at: new Date().toISOString(), + maintainers: rows, + }; + } +}