Compare commits

...

11 commits

Author SHA1 Message Date
Maxime Morille
b2b2808cbd
Merge a0c37f83e8 into d018ae7711 2025-01-02 17:24:30 +00:00
renovate[bot]
d018ae7711
chore(deps): update prom/prometheus docker tag to v3.1.0 (#33375)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 16:41:03 +00:00
renovate[bot]
3eb405d9ed
chore(deps): update dependency @swc/core to v1.10.2 (#33374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 16:40:45 +00:00
Michael Kriese
e811b23df5
fix(platform): ensure order for cached pr's on gitea and bitbucket (#33373) 2025-01-02 16:39:43 +00:00
Tobias Bieniek
5390390b7d
feat(presets): Add axum monorepo (#33362)
Some checks are pending
Build / setup (push) Waiting to run
Build / setup-build (push) Waiting to run
Build / prefetch (push) Blocked by required conditions
Build / lint-eslint (push) Blocked by required conditions
Build / lint-prettier (push) Blocked by required conditions
Build / lint-docs (push) Blocked by required conditions
Build / lint-other (push) Blocked by required conditions
Build / (push) Blocked by required conditions
Build / codecov (push) Blocked by required conditions
Build / coverage-threshold (push) Blocked by required conditions
Build / test-success (push) Blocked by required conditions
Build / build (push) Blocked by required conditions
Build / build-docs (push) Blocked by required conditions
Build / test-e2e (push) Blocked by required conditions
Build / release (push) Blocked by required conditions
Code scanning / CodeQL-Build (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
whitesource-scan / WS_SCAN (push) Waiting to run
2025-01-02 08:01:58 +00:00
renovate[bot]
351db7750e
chore(deps): update dependency markdownlint-cli2 to v0.17.0 (#33365)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 04:33:52 +00:00
Janus Troelsen
1caffcc310
feat(vulnerabilities): Add Hackage support (#33328)
Some checks are pending
Build / setup (push) Waiting to run
Build / setup-build (push) Waiting to run
Build / prefetch (push) Blocked by required conditions
Build / lint-eslint (push) Blocked by required conditions
Build / lint-prettier (push) Blocked by required conditions
Build / lint-docs (push) Blocked by required conditions
Build / lint-other (push) Blocked by required conditions
Build / (push) Blocked by required conditions
Build / codecov (push) Blocked by required conditions
Build / coverage-threshold (push) Blocked by required conditions
Build / test-success (push) Blocked by required conditions
Build / build (push) Blocked by required conditions
Build / build-docs (push) Blocked by required conditions
Build / test-e2e (push) Blocked by required conditions
Build / release (push) Blocked by required conditions
Code scanning / CodeQL-Build (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
whitesource-scan / WS_SCAN (push) Waiting to run
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
2025-01-01 21:17:44 +00:00
renovate[bot]
dd903881c6
build(deps): update dependency @renovatebot/osv-offline to v1.5.11 (#33364)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-01 20:52:14 +00:00
renovate[bot]
bea61f528b
chore(deps): update dependency type-fest to v4.31.0 (#33363)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-01 18:54:00 +00:00
Maxime Morille
a0c37f83e8
Add a specific implementation to parse Gitlab CODEOWNERS 2024-12-10 21:23:15 +00:00
Maxime Morille
6a69f1f9a3
Add support of Gitlab's sections for CODEOWNERS (#29202) 2024-12-10 19:51:49 +01:00
17 changed files with 773 additions and 150 deletions

View file

@ -2426,6 +2426,7 @@ Renovate only queries the OSV database for dependencies that use one of these da
- [`crate`](./modules/datasource/crate/index.md)
- [`go`](./modules/datasource/go/index.md)
- [`hackage`](./modules/datasource/hackage/index.md)
- [`hex`](./modules/datasource/hex/index.md)
- [`maven`](./modules/datasource/maven/index.md)
- [`npm`](./modules/datasource/npm/index.md)

View file

@ -22,7 +22,7 @@ services:
# Prometheus for storing metrics
prometheus:
image: prom/prometheus:v3.0.1
image: prom/prometheus:v3.1.0
ports:
- '9090:9090' # Web UI
- '4318' # OTLP HTTP

View file

@ -55,6 +55,7 @@
"https://github.com/awslabs/aws-sdk-rust"
],
"awsappsync": "https://github.com/awslabs/aws-mobile-appsync-sdk-js",
"axum": "https://github.com/tokio-rs/axum",
"azure-functions-dotnet-worker": "https://github.com/Azure/azure-functions-dotnet-worker",
"azure azure-libraries-for-net": "https://github.com/Azure/azure-libraries-for-net",
"azure azure-sdk-for-net": "https://github.com/Azure/azure-sdk-for-net",

View file

@ -166,8 +166,8 @@ describe('modules/platform/bitbucket/pr-cache', () => {
);
expect(res).toMatchObject([
{ number: 1, title: 'title' },
{ number: 2, title: 'title' },
{ number: 1, title: 'title' },
]);
expect(cache).toEqual({
httpCache: {},

View file

@ -11,6 +11,7 @@ import type { BitbucketPrCacheData, PagedResult, PrResponse } from './types';
import { prFieldsFilter, prInfo, prStates } from './utils';
export class BitbucketPrCache {
private items: Pr[] = [];
private cache: BitbucketPrCacheData;
private constructor(
@ -41,6 +42,7 @@ export class BitbucketPrCache {
}
repoCache.platform.bitbucket.pullRequestsCache = pullRequestCache;
this.cache = pullRequestCache;
this.updateItems();
}
private static async init(
@ -62,7 +64,7 @@ export class BitbucketPrCache {
}
private getPrs(): Pr[] {
return Object.values(this.cache.items);
return this.items;
}
static async getPrs(
@ -77,6 +79,7 @@ export class BitbucketPrCache {
private setPr(pr: Pr): void {
logger.debug(`Adding PR #${pr.number} to the PR cache`);
this.cache.items[pr.number] = pr;
this.updateItems();
}
static async setPr(
@ -161,6 +164,16 @@ export class BitbucketPrCache {
},
`PR cache sync finished`,
);
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();
}
}

View file

@ -1166,10 +1166,10 @@ describe('modules/platform/gitea/index', () => {
const res = await gitea.getPrList();
expect(res).toMatchObject([
{ number: 1, title: 'Some PR' },
{ number: 2, title: 'Other PR' },
{ number: 3, title: 'Draft PR' },
{ number: 4, title: 'Merged PR' },
{ number: 3, title: 'Draft PR' },
{ number: 2, title: 'Other PR' },
{ number: 1, title: 'Some PR' },
]);
});
@ -1209,10 +1209,10 @@ describe('modules/platform/gitea/index', () => {
const res = await gitea.getPrList();
expect(res).toMatchObject([
{ number: 1, title: 'Some PR' },
{ number: 2, title: 'Other PR' },
{ number: 3, title: 'Draft PR' },
{ number: 4, title: 'Merged PR' },
{ number: 3, title: 'Draft PR' },
{ number: 2, title: 'Other PR' },
{ number: 1, title: 'Some PR' },
]);
});
@ -1244,16 +1244,16 @@ describe('modules/platform/gitea/index', () => {
await initFakeRepo(scope);
const res1 = await gitea.getPrList();
expect(res1).toMatchObject([{ number: 1 }, { number: 2 }]);
expect(res1).toMatchObject([{ number: 2 }, { number: 1 }]);
memCache.set('gitea-pr-cache-synced', false);
const res2 = await gitea.getPrList();
expect(res2).toMatchObject([
{ number: 1 },
{ number: 2 },
{ number: 3 },
{ number: 4 },
{ number: 3 },
{ number: 2 },
{ number: 1 },
]);
});
});

View file

@ -11,6 +11,7 @@ import { API_PATH, toRenovatePR } from './utils';
export class GiteaPrCache {
private cache: GiteaPrCacheData;
private items: Pr[] = [];
private constructor(
private repo: string,
@ -31,6 +32,7 @@ export class GiteaPrCache {
}
repoCache.platform.gitea.pullRequestsCache = pullRequestCache;
this.cache = pullRequestCache;
this.updateItems();
}
static forceSync(): void {
@ -54,7 +56,7 @@ export class GiteaPrCache {
}
private getPrs(): Pr[] {
return Object.values(this.cache.items);
return this.items;
}
static async getPrs(
@ -68,6 +70,7 @@ export class GiteaPrCache {
private setPr(item: Pr): void {
this.cache.items[item.number] = item;
this.updateItems();
}
static async setPr(
@ -137,6 +140,16 @@ export class GiteaPrCache {
url = parseLinkHeader(res.headers.link)?.next?.url;
}
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();
}
}

View file

@ -0,0 +1,58 @@
import ignore from 'ignore';
import { regEx } from '../../../util/regex';
import type { FileOwnerRule } from '../types';
export class CodeOwnersParser {
private currentSection: { name: string; defaultUsers: string[] };
private internalRules: FileOwnerRule[];
constructor() {
this.currentSection = { name: '', defaultUsers: [] };
this.internalRules = [];
}
private changeCurrentSection(line: string): void {
const [name, ...usernames] = line.split(regEx(/\s+/));
this.currentSection = { name, defaultUsers: usernames };
}
private addRule(rule: FileOwnerRule): void {
this.internalRules.push(rule);
}
private extractOwnersFromLine(
line: string,
defaultUsernames: string[],
): FileOwnerRule {
const [pattern, ...usernames] = line.split(regEx(/\s+/));
const matchPattern = ignore().add(pattern);
return {
usernames: usernames.length > 0 ? usernames : defaultUsernames,
pattern,
score: pattern.length,
match: (path: string) => matchPattern.ignores(path),
};
}
parseLine(line: string): CodeOwnersParser {
if (CodeOwnersParser.isSectionHeader(line)) {
this.changeCurrentSection(line);
} else {
const rule = this.extractOwnersFromLine(
line,
this.currentSection.defaultUsers,
);
this.addRule(rule);
}
return this;
}
get rules(): FileOwnerRule[] {
return this.internalRules;
}
private static isSectionHeader(line: string): boolean {
return line.startsWith('[') || line.startsWith('^[');
}
}

View file

@ -25,7 +25,7 @@ import { setBaseUrl } from '../../../util/http/gitlab';
import type { HttpResponse } from '../../../util/http/types';
import { parseInteger } from '../../../util/number';
import * as p from '../../../util/promises';
import { regEx } from '../../../util/regex';
import { newlineRegex, regEx } from '../../../util/regex';
import { sanitize } from '../../../util/sanitize';
import {
ensureTrailingSlash,
@ -39,6 +39,7 @@ import type {
EnsureCommentConfig,
EnsureCommentRemovalConfig,
EnsureIssueConfig,
FileOwnerRule,
FindPRConfig,
GitUrlOption,
Issue,
@ -54,6 +55,7 @@ import type {
} from '../types';
import { repoFingerprint } from '../util';
import { smartTruncate } from '../utils/pr-body';
import { CodeOwnersParser } from './code-owners';
import {
getMemberUserIDs,
getMemberUsernames,
@ -1470,3 +1472,23 @@ export async function expandGroupMembers(
}
return expandedReviewersOrAssignees;
}
export function extractRulesFromCodeOwnersFile(
codeOwnersFile: string,
): FileOwnerRule[] {
const parser = new CodeOwnersParser();
const cleanedLines = codeOwnersFile
.split(newlineRegex)
// Remove comments
.map((line) => line.split('#')[0])
// Remove empty lines
.map((line) => line.trim())
.filter(is.nonEmptyString);
for (const line of cleanedLines) {
parser.parseLine(line);
}
return parser.rules;
}

View file

@ -221,6 +221,13 @@ export interface AutodiscoverConfig {
projects?: string[];
}
export interface FileOwnerRule {
usernames: string[];
pattern: string;
score: number;
match: (path: string) => boolean;
}
export interface Platform {
findIssue(title: string): Promise<Issue | null>;
getIssueList(): Promise<Issue[]>;
@ -280,6 +287,7 @@ export interface Platform {
filterUnavailableUsers?(users: string[]): Promise<string[]>;
commitFiles?(config: CommitFilesConfig): Promise<LongCommitSha | null>;
expandGroupMembers?(reviewersOrAssignees: string[]): Promise<string[]>;
extractRulesFromCodeOwnersFile?(fileContent: string): FileOwnerRule[];
maxBodyLength(): number;
labelCharLimit?(): number;

View file

@ -47,7 +47,7 @@ export function getFixedVersionByDatasource(
return `[${fixedVersion},)`;
}
// crates.io, Go, Hex, npm, RubyGems, PyPI
// crates.io, Go, Hackage, Hex, npm, RubyGems, PyPI
return `>= ${fixedVersion}`;
}

View file

@ -840,6 +840,63 @@ describe('workers/repository/process/vulnerabilities', () => {
]);
});
it('returns packageRules for Hackage', async () => {
const packageFiles: Record<string, PackageFile[]> = {
hackage: [
{
deps: [
{
depName: 'aeson',
currentValue: '0.4.0.0',
datasource: 'hackage',
},
],
packageFile: 'some-file',
},
],
};
getVulnerabilitiesMock.mockResolvedValueOnce([
{
id: 'HSEC-2023-0001',
summary: 'Hash flooding vulnerability in aeson',
details:
'# Hash flooding vulnerability in aeson\n\n*aeson* was vulnerable to hash flooding (a.k.a. hash DoS). The\nissue is a consequence of the HashMap implementation from\n*unordered-containers*. It results in a denial of service through\nCPU consumption. This technique has been used in real-world attacks\nagainst a variety of languages, libraries and frameworks over the\nyears.\n',
aliases: ['CVE-2022-3433'],
modified: '2023-06-13T09:03:52Z',
affected: [
{
package: {
ecosystem: 'Hackage',
name: 'aeson',
},
ranges: [
{
type: 'ECOSYSTEM',
events: [{ introduced: '0.4.0.0' }, { fixed: '2.0.1.0' }],
},
],
},
],
},
]);
await vulnerabilities.appendVulnerabilityPackageRules(
config,
packageFiles,
);
expect(config.packageRules).toHaveLength(1);
expect(config.packageRules).toMatchObject([
{
matchDatasources: ['hackage'],
matchPackageNames: ['aeson'],
matchCurrentVersion: '0.4.0.0',
allowedVersions: '>= 2.0.1.0',
isVulnerabilityAlert: true,
},
]);
});
it('filters not applicable vulnerability based on last_affected version', async () => {
const packageFiles: Record<string, PackageFile[]> = {
poetry: [

View file

@ -35,6 +35,7 @@ export class Vulnerabilities {
> = {
crate: 'crates.io',
go: 'Go',
hackage: 'Hackage',
hex: 'Hex',
maven: 'Maven',
npm: 'npm',

View file

@ -2,12 +2,19 @@ import { codeBlock } from 'common-tags';
import { mock } from 'jest-mock-extended';
import { fs, git } from '../../../../../test/util';
import type { Pr } from '../../../../modules/platform';
import { platform } from '../../../../modules/platform';
import * as gitlab from '../../../../modules/platform/gitlab';
import { codeOwnersForPr } from './code-owners';
jest.mock('../../../../util/fs');
jest.mock('../../../../util/git');
jest.mock('../../../../modules/platform');
describe('workers/repository/update/pr/code-owners', () => {
beforeAll(() => {
platform.extractRulesFromCodeOwnersFile = undefined;
});
describe('codeOwnersForPr', () => {
let pr: Pr;
@ -170,6 +177,102 @@ describe('workers/repository/update/pr/code-owners', () => {
});
});
describe('supports Gitlab sections', () => {
beforeAll(() => {
platform.extractRulesFromCodeOwnersFile =
gitlab.extractRulesFromCodeOwnersFile;
});
it('returns section code owner', async () => {
fs.readLocalFile.mockResolvedValueOnce(
['[team] @jimmy', '*'].join('\n'),
);
git.getBranchFiles.mockResolvedValueOnce(['README.md']);
const codeOwners = await codeOwnersForPr(pr);
expect(codeOwners).toEqual(['@jimmy']);
});
const codeOwnerFileWithDefaultApproval = codeBlock`
# Required for all files
* @general-approvers
[Documentation] @docs-team
docs/
README.md
*.txt
# Invalid section
Something before [Tests] @tests-team
tests/
# Optional section
^[Optional] @optional-team
optional/
[Database] @database-team
model/db/
config/db/database-setup.md @docs-team
`;
it('returns code owners of multiple sections', async () => {
fs.readLocalFile.mockResolvedValueOnce(
codeOwnerFileWithDefaultApproval,
);
git.getBranchFiles.mockResolvedValueOnce([
'config/db/database-setup.md',
]);
const codeOwners = await codeOwnersForPr(pr);
expect(codeOwners).toEqual(['@docs-team', '@general-approvers']);
});
it('returns default owners when none is explicitly set', async () => {
fs.readLocalFile.mockResolvedValueOnce(
codeOwnerFileWithDefaultApproval,
);
git.getBranchFiles.mockResolvedValueOnce(['model/db/CHANGELOG.txt']);
const codeOwners = await codeOwnersForPr(pr);
expect(codeOwners).toEqual([
'@database-team',
'@docs-team',
'@general-approvers',
]);
});
it('parses only sections that start at the beginning of a line', async () => {
fs.readLocalFile.mockResolvedValueOnce(
codeOwnerFileWithDefaultApproval,
);
git.getBranchFiles.mockResolvedValueOnce(['tests/setup.ts']);
const codeOwners = await codeOwnersForPr(pr);
expect(codeOwners).not.toInclude('@tests-team');
});
it('returns code owners for optional sections', async () => {
fs.readLocalFile.mockResolvedValueOnce(
codeOwnerFileWithDefaultApproval,
);
git.getBranchFiles.mockResolvedValueOnce([
'optional/optional-file.txt',
]);
const codeOwners = await codeOwnersForPr(pr);
expect(codeOwners).toEqual([
'@optional-team',
'@docs-team',
'@general-approvers',
]);
});
});
it('does not require all files to match a single rule, regression test for #12611', async () => {
fs.readLocalFile.mockResolvedValueOnce(
codeBlock`

View file

@ -1,16 +1,15 @@
import is from '@sindresorhus/is';
import ignore from 'ignore';
import { logger } from '../../../../logger';
import type { Pr } from '../../../../modules/platform';
import type { FileOwnerRule, Pr } from '../../../../modules/platform';
import { platform } from '../../../../modules/platform';
import { readLocalFile } from '../../../../util/fs';
import { getBranchFiles } from '../../../../util/git';
import { newlineRegex, regEx } from '../../../../util/regex';
interface FileOwnerRule {
usernames: string[];
pattern: string;
score: number;
match: (path: string) => boolean;
interface FileOwnersScore {
file: string;
userScoreMap: Map<string, number>;
}
function extractOwnersFromLine(line: string): FileOwnerRule {
@ -24,11 +23,6 @@ function extractOwnersFromLine(line: string): FileOwnerRule {
};
}
interface FileOwnersScore {
file: string;
userScoreMap: Map<string, number>;
}
function matchFileToOwners(
file: string,
rules: FileOwnerRule[],
@ -75,6 +69,22 @@ function getOwnerList(filesWithOwners: FileOwnersScore[]): OwnerFileScore[] {
}));
}
function extractRulesFromCodeOwnersFile(
codeOwnersFile: string,
): FileOwnerRule[] {
return (
codeOwnersFile
.split(newlineRegex)
// Remove comments
.map((line) => line.split('#')[0])
// Remove empty lines
.map((line) => line.trim())
.filter(is.nonEmptyString)
// Extract pattern & usernames
.map(extractOwnersFromLine)
);
}
export async function codeOwnersForPr(pr: Pr): Promise<string[]> {
logger.debug('Searching for CODEOWNERS file');
try {
@ -102,15 +112,9 @@ export async function codeOwnersForPr(pr: Pr): Promise<string[]> {
}
// Convert CODEOWNERS file into list of matching rules
const fileOwnerRules = codeOwnersFile
.split(newlineRegex)
// Remove comments
.map((line) => line.split('#')[0])
// Remove empty lines
.map((line) => line.trim())
.filter(is.nonEmptyString)
// Extract pattern & usernames
.map(extractOwnersFromLine);
const fileOwnerRules = platform.extractRulesFromCodeOwnersFile
? platform.extractRulesFromCodeOwnersFile(codeOwnersFile)
: extractRulesFromCodeOwnersFile(codeOwnersFile);
logger.debug(
{ prFiles, fileOwnerRules },

View file

@ -164,7 +164,7 @@
"@qnighy/marshal": "0.1.3",
"@renovatebot/detect-tools": "1.1.0",
"@renovatebot/kbpgp": "4.0.1",
"@renovatebot/osv-offline": "1.5.10",
"@renovatebot/osv-offline": "1.5.11",
"@renovatebot/pep440": "4.0.1",
"@renovatebot/ruby-semver": "4.0.0",
"@sindresorhus/is": "4.6.0",
@ -269,7 +269,7 @@
"@openpgp/web-stream-tools": "0.1.3",
"@renovate/eslint-plugin": "file:tools/eslint",
"@semantic-release/exec": "6.0.3",
"@swc/core": "1.10.1",
"@swc/core": "1.10.2",
"@types/auth-header": "1.0.6",
"@types/aws4": "1.11.6",
"@types/better-sqlite3": "7.6.12",
@ -335,7 +335,7 @@
"jest-mock": "29.7.0",
"jest-mock-extended": "3.0.7",
"jest-snapshot": "29.7.0",
"markdownlint-cli2": "0.16.0",
"markdownlint-cli2": "0.17.0",
"memfs": "4.15.1",
"nock": "13.5.6",
"npm-run-all2": "7.0.2",
@ -347,7 +347,7 @@
"tmp-promise": "3.0.3",
"ts-jest": "29.2.5",
"ts-node": "10.9.2",
"type-fest": "4.30.2",
"type-fest": "4.31.0",
"typescript": "5.7.2",
"unified": "9.2.2"
},

File diff suppressed because it is too large Load diff