Skip to content
← Public packages

@kentcdodds/friction-log

Qualify and file friction papercuts to GitHub (any owner/repo) or wake Patch for Kody packages; daily sweep of kody-hq/kody friction issues.

FollowFork

src/issues.ts

107 lines · 3.2 KB · TypeScript
Raw
import githubRequest from 'kody:@kentcdodds/github/request'
import { frictionLabel, owner, repo, type FrictionIssue, type IssueComment } from './shared.ts'

type GithubRequestResult = {
	ok?: boolean
	status?: number
	data?: unknown
	text?: string
}

type GithubIssue = {
	number?: number
	title?: string | null
	body?: string | null
	html_url?: string | null
	user?: { login?: string | null } | null
	created_at?: string | null
	updated_at?: string | null
	pull_request?: unknown
	labels?: Array<{ name?: string } | string>
}

type GithubComment = {
	id?: number
	body?: string | null
	created_at?: string | null
	user?: { login?: string | null } | null
}

function requireOk(result: GithubRequestResult, path: string) {
	if (result?.ok === false || (result.status && result.status >= 400)) {
		throw new Error(
			`GitHub ${path} failed (${result.status}): ${String(result.text || '').slice(0, 200)}`,
		)
	}
	return result.data
}

function asIssues(data: unknown): Array<GithubIssue> {
	return Array.isArray(data) ? (data as Array<GithubIssue>) : []
}

function asComments(data: unknown): Array<GithubComment> {
	return Array.isArray(data) ? (data as Array<GithubComment>) : []
}

async function listLabeledIssues() {
	const issues: Array<GithubIssue> = []
	for (let page = 1; page <= 10; page += 1) {
		const path = `/repos/${owner}/${repo}/issues?labels=${encodeURIComponent(frictionLabel)}&state=open&per_page=100&page=${page}`
		const data = requireOk(await githubRequest({ account: 'bot', path }), path)
		const pageIssues = asIssues(data).filter((issue) => !issue.pull_request)
		issues.push(...pageIssues)
		if (asIssues(data).length < 100) break
	}
	return issues
}

async function listIssueComments(issueNumber: number) {
	const comments: Array<GithubComment> = []
	for (let page = 1; page <= 10; page += 1) {
		const path = `/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=100&page=${page}`
		const data = requireOk(await githubRequest({ account: 'bot', path }), path)
		const pageComments = asComments(data)
		comments.push(...pageComments)
		if (pageComments.length < 100) break
	}
	return comments
}

function toComment(comment: GithubComment): IssueComment {
	return {
		id: Number(comment.id || 0),
		userLogin: String(comment.user?.login || ''),
		createdAt: String(comment.created_at || ''),
		body: String(comment.body || ''),
	}
}

function issueLabels(issue: GithubIssue) {
	return (issue.labels || [])
		.map((label) => (typeof label === 'string' ? label : String(label?.name || '')))
		.filter(Boolean)
}

export async function loadFrictionIssues(): Promise<Array<FrictionIssue>> {
	const listed = await listLabeledIssues()
	const issues: Array<FrictionIssue> = []
	for (const issue of listed) {
		const number = Number(issue.number || 0)
		if (!number) continue
		const comments = await listIssueComments(number)
		issues.push({
			number,
			title: String(issue.title || ''),
			body: String(issue.body || ''),
			htmlUrl: String(issue.html_url || ''),
			userLogin: String(issue.user?.login || ''),
			createdAt: String(issue.created_at || ''),
			updatedAt: String(issue.updated_at || ''),
			labels: issueLabels(issue),
			comments: comments.map(toComment),
		})
	}
	issues.sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt))
	return issues
}