feat(api): implement get issue templates

This commit is contained in:
2025-10-25 17:58:36 +01:00
parent aa1013185a
commit 90ca2c2156
4 changed files with 208 additions and 1 deletions

51
src/apis/gitea.ts Normal file
View File

@@ -0,0 +1,51 @@
import { URL } from "url"
import { BaseApi } from "./index.js"
// /api/swagger
const Prefix = "/api/v1"
const IssueTemplatesUrl = (user: string, repo: string) =>
`${Prefix}/repos/${user}/${repo}/issue_templates`
type GiteaIssueTemplatesResponse = {
name: string,
title: string,
about: string,
labels: string[] | null,
assignees: string[] | null,
ref: string,
content: string,
body: unknown | null,
file_name: string
}[]
class Gitea extends BaseApi {
headers: Record<string, string>
constructor(...options: ConstructorParameters<typeof BaseApi>) {
super(...options)
this.headers = {
"Authorization": `token ${this.token}`
}
}
async getIssueTemplates() {
const url = new URL(IssueTemplatesUrl(this.user, this.repo), this.host)
const response = await fetch(url, { headers: this.headers })
switch (response.status) {
case 200: break
case 404: return { error: "Issue templates not found" }
default: return { error: "Unknown status code" }
}
const json: GiteaIssueTemplatesResponse = await response.json()
return {
value: json.map(template => template.file_name)
}
}
}
export default Gitea

118
src/apis/github.ts Normal file
View File

@@ -0,0 +1,118 @@
import { URL } from "url"
import {BaseApi, type Result} from "./index.js"
// docs.github.com/en/rest
/*
Folders searched by gitea:
"ISSUE_TEMPLATE"
"issue_template"
".gitea/ISSUE_TEMPLATE"
".gitea/issue_template"
".github/ISSUE_TEMPLATE"
".github/issue_template"
*/
const IssueTemplateFolders = [
"ISSUE_TEMPLATE",
"issue_template",
".gitea/ISSUE_TEMPLATE",
".gitea/issue_template",
".github/ISSUE_TEMPLATE",
".github/issue_template"
]
// /repos/${user}/${repo} -> .default_branch
// /repos/${user}/${repo}/git/trees/{branch}?recursive=1 -> search for templates
const Prefix = "https://api.github.com"
// https://docs.github.com/en/rest/repos/repos#get-a-repository
const RepoInfoUrl = (user: string, repo: string) =>
`${Prefix}/repos/${user}/${repo}`
interface GitHubRepoInfoResponse {
default_branch: string
// ...
}
// https://docs.github.com/en/rest/git/trees#get-a-tree
const TreeUrl = (user: string, repo: string, branch: string) =>
`${Prefix}/repos/${user}/${repo}/git/trees/${branch}?recursive=1`
interface GitHubTreeResponse {
sha: string
url: string
tree: {
path: string
mode: string
type: "tree" | "blob"
sha: string
size: number
url: string
}[]
}
// fine-grained tokens required permissions: metadata, content
class GitHub extends BaseApi {
headers: Record<string, string>
constructor(...options: ConstructorParameters<typeof BaseApi>) {
super(...options)
this.headers = {
"Authorization": `token ${this.token}`
}
}
async getDefaultBranch(): Promise<Result<string, string>> {
const url = new URL(RepoInfoUrl(this.user, this.repo), this.host)
const response = await fetch(url, { headers: this.headers })
switch (response.status) {
case 301: return { error: "Moved permanently" }
case 403: return { error: "Forbidden" }
case 404: return { error: "Resource not found" }
case 200: break
default: return { error: "Unknown status code" }
}
const json: GitHubRepoInfoResponse = await response.json()
return { value: json.default_branch }
}
async getRepoFiles(): Promise<Result<string[], string>> {
const branch = await this.getDefaultBranch()
if (!branch.value) return { error: `Default branch error: ${branch.error}` }
const url = new URL(TreeUrl(this.user, this.repo, branch.value), this.host)
const response = await fetch(url, { headers: this.headers })
switch (response.status) {
case 200: break
case 404: return { error: "Resource not found" }
case 409: return { error: "Conflict" }
case 422: return { error: "Validation failed, or the endpoint has been spammed" }
default: return { error: "Unknown status code" }
}
const json: GitHubTreeResponse = await response.json()
return {
value: json.tree
.filter(entry => entry.type == "blob")
.map(entry => entry.path)
}
}
async getIssueTemplates() {
const files = await this.getRepoFiles()
if (!files.value) return { error: `Repo files error: ${files.error}` }
const issue_templates = files.value.filter(
file => IssueTemplateFolders.some(folder => file.startsWith(folder))
)
return { value: issue_templates }
}
}
export default GitHub

21
src/apis/index.ts Normal file
View File

@@ -0,0 +1,21 @@
export type Result<Type, Err> = {
value: Type,
error?: null
} | {
value?: null,
error: Err
}
export abstract class BaseApi {
constructor(
readonly host: string,
readonly token: string,
readonly user: string,
readonly repo: string
) {}
/**
* @returns Array of filepath's
*/
abstract getIssueTemplates(): Promise<Result<string[], string>>
}

View File

@@ -2,4 +2,21 @@ import * as cli from "./cli.js"
const config = cli.parseArgs(process.argv) const config = cli.parseArgs(process.argv)
console.log(config) // console.log(config)
import type { BaseApi } from "./apis";
import Gitea from "./apis/gitea.js"
import GitHub from "./apis/github.js";
let api: BaseApi
api = new Gitea(
"http://localhost:3000", "...",
"admin", "test_repo"
)
api = new GitHub(
"https://api.github.com", "...",
"TxFig", "test-api")
const result = await api.getIssueTemplates()
console.log(result.value ?? result.error)