Files
renovate/tools/docs/github-query-items.ts
T
Jamie Tanna e2a8843196 fix(docs): look up open issues on main builds for Renovate docs (#39818)
We're currently always skipping the detection of open bugs/features, so
even though [0] introduced authentication to GitHub, this didn't
actually lead to the lookup when running in CI.

If we're not running in Renovate's CI build (i.e. as part of the release
process) or the docs site's CI build (as part of post-release
operations) we should continue to skip the build, when running in CI.

This also makes it clearer with a WARN log line to explain why this is
being skipped.

This is a re-apply of ae46c57f56, as well
as making sure that the `GITHUB_TOKEN` is explicitly passed as
`extraEnv`, otherwise we see i.e.

```
ERROR: Error getting query results
       "err": {
         "cmd": "/bin/sh -c gh issue list --json \"title,number,url,labels\" --search \"type:Bug\" --limit 1000",
         "stderr": "gh: To use GitHub CLI in automation, set the GH_TOKEN environment variable.\n",
         "stdout": "",
         "options": {
           "encoding": "utf-8",
           "env": ["CI", "HOME", "PATH", "LANG"],
           "maxBuffer": 10485760,
           "timeout": 900000
         },
         "exitCode": 4,
         "name": "ExecError",
         "message": "Command failed: gh issue list ...\ngh: To use GitHub CLI in automation, set the GH_TOKEN environment variable.\n",
         "stack": "ExecError: Command failed: gh issue list ...\ngh: To use GitHub CLI in automation, set the GH_TOKEN environment variable.\n\n    at ..."
       }
```

As we can see, the `env` isn't automagically picking up the
`GITHUB_TOKEN`, so we should explicitly pass it.

[0]: https://github.com/renovatebot/renovatebot.github.io/pull/746
2025-12-09 10:28:39 +00:00

198 lines
4.5 KiB
TypeScript

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<string, Items | undefined>;
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<ItemsEntity[]> {
const command = `gh issue list --json "title,number,url,labels" --search "type:${issueType}" --limit 1000`;
const execRes = await exec(command, {
extraEnv: {
GITHUB_TOKEN: process.env.GITHUB_TOKEN,
},
});
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<RenovateOpenItems> {
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) {
if (
process.env.GITHUB_REF === 'main' &&
process.env.GITHUB_REPOSITORY !== 'renovatebot/renovatebot.github.io' &&
process.env.GITHUB_REPOSITORY !== 'renovatebot/renovate'
) {
logger.warn(
{
repository: process.env.GITHUB_REPOSITORY,
ref: process.env.GITHUB_REF,
},
"Skipping collection of open GitHub Issues, as we're running CI on a non-HEAD branch of Renovate or its docs site",
);
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;
}