Compare commits
2 Commits
c111fd6689
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
90ca2c2156
|
|||
|
aa1013185a
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "issue-scheduler",
|
||||
"description": "Schedule issues in Gitea or Github from issue templates",
|
||||
"description": "Schedule issues in Gitea or GitHub using issue templates",
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.js",
|
||||
|
||||
|
||||
51
src/apis/gitea.ts
Normal file
51
src/apis/gitea.ts
Normal 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
118
src/apis/github.ts
Normal 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
21
src/apis/index.ts
Normal 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>>
|
||||
}
|
||||
27
src/cli.ts
27
src/cli.ts
@@ -8,6 +8,10 @@ export interface CLIConfig {
|
||||
// doesn't log to stdout/stderr (still logs to file if --log-file)
|
||||
quiet: boolean
|
||||
}
|
||||
// TODO: Git client shouldn't be identified from the config,
|
||||
// but from the host url or a custom config property
|
||||
// TODO: add --list-issue-templates <user> <repo> <token> : list results from api.getIssueTemplates
|
||||
// TODO: add other utility commands
|
||||
|
||||
export const defaults: CLIConfig = {
|
||||
config_filepath: "config.toml",
|
||||
@@ -97,14 +101,37 @@ export function optionalBooleanFlag(args: string[], flags: string[]): boolean {
|
||||
return value === null;
|
||||
}
|
||||
|
||||
const Usage = (program_name: string) => `\
|
||||
Usage: ${program_name} [options...]
|
||||
|
||||
Issue Scheduler - Schedule issues in Gitea or GitHub using issue templates
|
||||
|
||||
Options:
|
||||
-h, --help Display this message
|
||||
-c, --config <filepath> Config file [default: "config.toml"]
|
||||
-l, --log-file <filepath> Logs output to file
|
||||
-v, --verbose Verbose logging
|
||||
-q, --quiet Only logs to --log-file (if provided)
|
||||
`
|
||||
|
||||
/**
|
||||
* Parse command line arguments from a string array (process.argv)
|
||||
* @param args Command line arguments
|
||||
* @return {CLIConfig} Parsed flags
|
||||
*/
|
||||
export function parseArgs(args: string[]): CLIConfig {
|
||||
if (args.length < 2) { // [node executable, program path, ...]
|
||||
// TODO: log error
|
||||
process.exit(1)
|
||||
}
|
||||
const config = defaults
|
||||
|
||||
const helpValue = optionalBooleanFlag(args, ["--help", "-h"])
|
||||
if (helpValue) {
|
||||
console.log(Usage(args[1]!))
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const configValue = optionalStringFlag(args, ["--config", "-c"])
|
||||
if (configValue) config.config_filepath = configValue
|
||||
|
||||
|
||||
19
src/index.ts
19
src/index.ts
@@ -2,4 +2,21 @@ import * as cli from "./cli.js"
|
||||
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user