import { DateTime } from 'luxon'; import { z } from 'zod'; import { logger } from '../../lib/logger'; import { exec } from '../../lib/util/exec'; export interface ItemsEntity { url: string; number: number; title: string; labels: LabelsEntity[]; issueType: 'Bug' | 'Feature'; } export interface LabelsEntity { name: string; } export interface RenovateOpenItems { managers: OpenItems; platforms: OpenItems; datasources: OpenItems; versionings: OpenItems; } export type OpenItems = Record; export interface Items { bugs: ItemsEntity[]; features: ItemsEntity[]; } const GhOutput = z.array( z.object({ url: z.string(), title: z.string(), labels: z.array( z.object({ name: z.string(), }), ), number: z.number(), }), ); async function getIssuesByIssueType( issueType: 'Bug' | 'Feature', ): Promise { const command = `gh issue list --json "title,number,url,labels" --search "type:${issueType}" --limit 1000`; const execRes = await exec(command); const res = GhOutput.safeParse(JSON.parse(execRes.stdout)); if (res.error) { throw res.error; } return res.data.map((issue) => { return { ...issue, issueType }; }); } export async function getOpenGitHubItems(): Promise { const result: RenovateOpenItems = { managers: {}, platforms: {}, datasources: {}, versionings: {}, }; if (process.env.SKIP_GITHUB_ISSUES) { logger.warn('Skipping GitHub issues'); return result; } if (!process.env.GITHUB_TOKEN) { logger.warn( 'No GITHUB_TOKEN found in env, cannot fetch Github issues. Skipping...', ); return result; } if (process.env.CI) { return result; } try { const rawItems = (await getIssuesByIssueType('Bug')).concat( await getIssuesByIssueType('Feature'), ); result.managers = extractIssues(rawItems, 'manager:'); result.platforms = extractIssues(rawItems, 'platform:'); result.datasources = extractIssues(rawItems, 'datasource:'); result.versionings = extractIssues(rawItems, 'versioning:'); return result; } catch (err) { logger.error({ err }, 'Error getting query results'); if (process.env.CI) { throw err; } return result; } } function extractIssues(items: ItemsEntity[], labelPrefix: string): OpenItems { const issuesMap: OpenItems = {}; for (const item of items) { const type = item.issueType; if (!type) { continue; } const label = item.labels .find((l) => l.name.startsWith(labelPrefix)) ?.name.split(':')[1]; if (!label) { continue; } issuesMap[label] ??= { bugs: [], features: [] }; switch (type) { case 'Bug': issuesMap[label]?.bugs.push(item); break; case 'Feature': issuesMap[label]?.features.push(item); break; default: break; } } return issuesMap; } function stringifyIssues(items: ItemsEntity[] | undefined): string { if (!items) { return ''; } let list = ''; for (const item of items) { list += ` - ${item.title} [#${item.number}](${item.url})\n`; } return list; } export function generateFeatureAndBugMarkdown( issuesMap: OpenItems, key: string, ): string { let md = '\n\n'; const featureList = stringifyIssues(issuesMap[key]?.features); const bugList = stringifyIssues(issuesMap[key]?.bugs); if (featureList || bugList) { md += '## Open items\n\n'; } if (featureList || bugList) { const now = DateTime.utc().toFormat('MMMM dd, yyyy'); const lists = `list of ${featureList ? 'features' : ''}${ featureList && bugList ? ' and ' : '' }${bugList ? 'bugs' : ''}`; md += `The below ${lists} were current when this page was generated on ${now}.\n\n`; } if (featureList) { md += '### Feature requests\n\n'; md += featureList; md += '\n'; } if (bugList) { md += '### Bug reports\n\n'; md += bugList; md += '\n'; } return md; }