From fb5b9ec35337c56d9cd23e94c9a10c5e52491952 Mon Sep 17 00:00:00 2001 From: anderdc Date: Thu, 2 Jul 2026 12:40:30 -0500 Subject: [PATCH] feat(repos): add GET /api/v1/repos/:owner/:repo/installation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a read-only endpoint that reports whether the Gittensor Mirror GitHub App is installed on a repo (installation_id present), independent of the "registered" flag. The registration UI verifies the install step (Step 2) by looking a repo up in GET /api/v1/health. Since #180 narrowed /health to installed AND registered repos, a freshly-installed-but-unregistered repo — exactly the state the registration page handles — no longer appears there, so the UI wrongly reports "App not installed". Registration is a chicken-and-egg deadlock: the install gate now requires the repo to already be registered. This endpoint restores install-only semantics for that gate without touching #180's health-monitoring behavior. Matches owner/repo case-insensitively (#120). Unknown repo returns installed=false, not 404. --- .../das/src/api/repos/repos.controller.ts | 19 +++++++++++ packages/das/src/api/repos/repos.service.ts | 32 +++++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/das/src/api/repos/repos.controller.ts b/packages/das/src/api/repos/repos.controller.ts index 98c001c..6f173ad 100644 --- a/packages/das/src/api/repos/repos.controller.ts +++ b/packages/das/src/api/repos/repos.controller.ts @@ -24,4 +24,23 @@ export class ReposController { ): Promise { return this.repos.getMaintainers(owner, repo); } + + @Get(":owner/:repo/installation") + @ApiOperation({ + summary: "GitHub App installation status for a repo", + description: + "Returns whether the Gittensor Mirror GitHub App is installed on the " + + "repo (installation_id present), independent of whether the repo is " + + "registered. The registration UI uses this to verify the install step " + + "before a repo is registered. An unknown repo returns installed=false, " + + "not a 404.", + }) + @ApiParam({ name: "owner", description: "Repository owner (org or user)" }) + @ApiParam({ name: "repo", description: "Repository name" }) + async getInstallationStatus( + @Param("owner") owner: string, + @Param("repo") repo: string, + ): Promise { + return this.repos.getInstallationStatus(owner, repo); + } } diff --git a/packages/das/src/api/repos/repos.service.ts b/packages/das/src/api/repos/repos.service.ts index a16e496..d2a4f9d 100644 --- a/packages/das/src/api/repos/repos.service.ts +++ b/packages/das/src/api/repos/repos.service.ts @@ -1,10 +1,16 @@ /* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */ import { Injectable } from "@nestjs/common"; -import { DataSource } from "typeorm"; +import { InjectRepository } from "@nestjs/typeorm"; +import { DataSource, IsNull, Not, Raw, Repository } from "typeorm"; +import { Repo } from "../../entities"; @Injectable() export class ReposService { - constructor(private readonly dataSource: DataSource) {} + constructor( + private readonly dataSource: DataSource, + @InjectRepository(Repo) + private readonly repoRepo: Repository, + ) {} async getMaintainers( owner: string, @@ -38,4 +44,26 @@ export class ReposService { maintainers: rows, }; } + + async getInstallationStatus( + owner: string, + repo: string, + ): Promise<{ repo_full_name: string; installed: boolean }> { + const repoFullName = `${owner}/${repo}`; + + // Case-insensitive match (#120); installed regardless of `registered`. + const count = await this.repoRepo.count({ + where: { + repoFullName: Raw((alias) => `LOWER(${alias}) = LOWER(:repoFullName)`, { + repoFullName, + }), + installationId: Not(IsNull()), + }, + }); + + return { + repo_full_name: repoFullName.toLowerCase(), + installed: count > 0, + }; + } }