mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-26 07:20:39 +00:00
7573fa98f2
* fix(platform/forgejo): replace getJsonUnchecked with validated getJson Migrate all getJsonUnchecked calls in the Forgejo platform module to use getJson with Zod schemas for response validation. Add comprehensive Zod schemas to schema.ts and update test mocks to use proper data formats. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * simplify by using infered types * first pass on reducing duplications * move PrStatus and CommitStatus to schema * remove code handling BranchStatus values now ensured by schema * remove now unreachable code * fix coverage * use forgejo schemas instead gitea for changelog fetching * feat: do not abort repo on unrecognized default merge style * refactor: instead of usages of `?? {}` with a default parameter * tighten required parameters and move contentString generation to schema * refactor: merge both content schemas and use a discriminated union * refactor: move patchJson and postJson to schema validation * Make body field required * make schema more strict * use DeepNullish * simplify catch * revert the gitea Content type change * unify User * log more nicely if accidentally not a file has been referenced * Use fakeSha and LongCommitSha schema --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
203 lines
5.4 KiB
TypeScript
203 lines
5.4 KiB
TypeScript
import { isNullOrUndefined } from '@sindresorhus/is';
|
|
import { dequal } from 'dequal';
|
|
import { DateTime } from 'luxon';
|
|
import { TEMPORARY_ERROR } from '../../../constants/error-messages.ts';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import * as memCache from '../../../util/cache/memory/index.ts';
|
|
import { getCache } from '../../../util/cache/repository/index.ts';
|
|
import type { ForgejoHttp } from '../../../util/http/forgejo.ts';
|
|
import {
|
|
getQueryString,
|
|
parseLinkHeader,
|
|
parseUrl,
|
|
} from '../../../util/url.ts';
|
|
import type { Pr } from '../types.ts';
|
|
import type { PR } from './schema.ts';
|
|
import { PRList } from './schema.ts';
|
|
import type { ForgejoPrCacheData } from './types.ts';
|
|
import { API_PATH, toRenovatePR } from './utils.ts';
|
|
|
|
export class ForgejoPrCache {
|
|
private cache: ForgejoPrCacheData;
|
|
private items: Pr[] = [];
|
|
private repo: string;
|
|
private readonly ignorePrAuthor: boolean;
|
|
private author: string | null;
|
|
|
|
private constructor(
|
|
repo: string,
|
|
ignorePrAuthor: boolean,
|
|
author: string | null,
|
|
) {
|
|
this.repo = repo;
|
|
this.ignorePrAuthor = ignorePrAuthor;
|
|
this.author = author;
|
|
const repoCache = getCache();
|
|
repoCache.platform ??= {};
|
|
repoCache.platform.forgejo ??= {};
|
|
let pullRequestCache = repoCache.platform.forgejo.pullRequestsCache as
|
|
| ForgejoPrCacheData
|
|
| undefined;
|
|
if (
|
|
isNullOrUndefined(pullRequestCache) ||
|
|
pullRequestCache.author !== author
|
|
) {
|
|
pullRequestCache = {
|
|
items: {},
|
|
updated_at: null,
|
|
author,
|
|
};
|
|
}
|
|
repoCache.platform.forgejo.pullRequestsCache = pullRequestCache;
|
|
this.cache = pullRequestCache;
|
|
this.updateItems();
|
|
}
|
|
|
|
static forceSync(): void {
|
|
memCache.set('forgejo-pr-cache-synced', false);
|
|
}
|
|
|
|
private static async init(
|
|
http: ForgejoHttp,
|
|
repo: string,
|
|
ignorePrAuthor: boolean,
|
|
author: string | null,
|
|
): Promise<ForgejoPrCache> {
|
|
const res = new ForgejoPrCache(repo, ignorePrAuthor, author);
|
|
const isSynced = memCache.get<true | undefined>('forgejo-pr-cache-synced');
|
|
|
|
if (!isSynced) {
|
|
await res.sync(http);
|
|
memCache.set('forgejo-pr-cache-synced', true);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
private getPrs(): Pr[] {
|
|
return this.items;
|
|
}
|
|
|
|
static async getPrs(
|
|
http: ForgejoHttp,
|
|
repo: string,
|
|
ignorePrAuthor: boolean,
|
|
author: string,
|
|
): Promise<Pr[]> {
|
|
const prCache = await ForgejoPrCache.init(
|
|
http,
|
|
repo,
|
|
ignorePrAuthor,
|
|
author,
|
|
);
|
|
return prCache.getPrs();
|
|
}
|
|
|
|
private setPr(item: Pr): void {
|
|
this.cache.items[item.number] = item;
|
|
this.updateItems();
|
|
}
|
|
|
|
static async setPr(
|
|
http: ForgejoHttp,
|
|
repo: string,
|
|
ignorePrAuthor: boolean,
|
|
author: string,
|
|
item: Pr,
|
|
): Promise<void> {
|
|
const prCache = await ForgejoPrCache.init(
|
|
http,
|
|
repo,
|
|
ignorePrAuthor,
|
|
author,
|
|
);
|
|
prCache.setPr(item);
|
|
}
|
|
|
|
private reconcile(rawItems: (PR | null)[]): boolean {
|
|
const { items } = this.cache;
|
|
let { updated_at } = this.cache;
|
|
const cacheTime = updated_at ? DateTime.fromISO(updated_at) : null;
|
|
|
|
let needNextPage = true;
|
|
|
|
for (const rawItem of rawItems) {
|
|
if (!rawItem) {
|
|
logger.warn('Forgejo PR is empty, throwing temporary error');
|
|
// Forgejo API sometimes returns empty PRs, so we throw a temporary error
|
|
// https://github.com/go-forgejo/forgejo/blob/fcd096231ac2deaefbca10a7db1b9b01f1da93d7/services/convert/pull.go#L34-L52
|
|
throw new Error(TEMPORARY_ERROR);
|
|
}
|
|
const id = rawItem.number;
|
|
|
|
const newItem = toRenovatePR(rawItem, this.author);
|
|
if (!newItem) {
|
|
continue;
|
|
}
|
|
|
|
const oldItem = items[id];
|
|
if (dequal(oldItem, newItem)) {
|
|
needNextPage = false;
|
|
continue;
|
|
}
|
|
|
|
items[id] = newItem;
|
|
|
|
const itemTime = DateTime.fromISO(rawItem.updated_at);
|
|
if (!cacheTime || itemTime > cacheTime) {
|
|
updated_at = rawItem.updated_at;
|
|
}
|
|
}
|
|
|
|
this.cache.updated_at = updated_at;
|
|
|
|
return needNextPage;
|
|
}
|
|
|
|
private async sync(http: ForgejoHttp): Promise<ForgejoPrCache> {
|
|
let query: string | null = getQueryString({
|
|
state: 'all',
|
|
sort: 'recentupdate',
|
|
// Fetch 100 PRs on the first run to ensure we have the most recent PRs.
|
|
// Forgejo will cap appropriate (50 by default, see `MAX_RESPONSE_ITEMS`).
|
|
// https://docs.forgejo.com/administration/config-cheat-sheet#api-api
|
|
// https://forgejo.org/docs/latest/admin/config-cheat-sheet/#api-api
|
|
limit: this.items.length ? 20 : 100,
|
|
// Supported since Forgejo v10.0.0.
|
|
// Will be ignored by older instances.
|
|
...(this.ignorePrAuthor ? {} : { poster: this.author }),
|
|
});
|
|
|
|
while (query) {
|
|
const res = await http.getJson(
|
|
`${API_PATH}/repos/${this.repo}/pulls?${query}`,
|
|
{
|
|
memCache: false,
|
|
paginate: false,
|
|
},
|
|
PRList,
|
|
);
|
|
|
|
const needNextPage = this.reconcile(res.body);
|
|
if (!needNextPage) {
|
|
break;
|
|
}
|
|
|
|
const uri = parseUrl(parseLinkHeader(res.headers.link)?.next?.url);
|
|
query = uri ? uri.search : null;
|
|
}
|
|
|
|
this.updateItems();
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Ensure the pr cache starts with the most recent PRs.
|
|
* JavaScript ensures that the cache is sorted by PR number.
|
|
*/
|
|
private updateItems(): void {
|
|
this.items = Object.values(this.cache.items).reverse();
|
|
}
|
|
}
|