From 60e1c69665137eaedde3ad875fbdfc6ef5c9b393 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 23 Jun 2025 23:47:59 +0200 Subject: [PATCH] feat: Parse versions from metadata links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dependabot PRs that update a **single** dependency include version details in the commit message introduction, e.g., > "Bumps `` from `` to ``" This is the format generated by the [`commit_message_intro`](https://github.com/dependabot/dependabot-core/blob/cc4b4eaade37da0a19e0897e6897bab613064e74/common/lib/dependabot/pull_request_creator/message_builder.rb#L320-L325) method in Dependabot Core. However, when **multiple dependencies** are updated in a single PR, this format isn't used consistently, which limits the action’s ability to extract accurate version information. This change improves version parsing for multi-dependency PRs by introducing two additional detection strategies: 1. **YAML metadata parsing** Dependabot includes a YAML block in the commit message with structured details for each updated dependency: ```yaml updated-dependencies: - dependency-name: commons-codec:commons-codec dependency-version: 1.18.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: non-breaking ``` This is the most reliable and stable source for the **new** version of each dependency, though it does **not** include the previous version. 2. **Metadata links parsing** In multi-dependency updates, Dependabot also appends “metadata links” with a format like: > "Updates `` from `` to ``" These lines are generated bythe [`metadata_links`](https://github.com/dependabot/dependabot-core/blob/cc4b4eaade37da0a19e0897e6897bab613064e74/common/lib/dependabot/pull_request_creator/message_builder.rb#L664-L678) method and provide **both** the old and new versions. By combining these sources, the action now supports version parsing for PRs with multiple updated dependencies—broadening its coverage and improving reliability. Closes #402 --- src/dependabot/update_metadata.test.ts | 51 ++++++++++++++++++++++++++ src/dependabot/update_metadata.ts | 45 ++++++++++++++++++++--- 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/src/dependabot/update_metadata.test.ts b/src/dependabot/update_metadata.test.ts index 39adb14..7f6e2cf 100644 --- a/src/dependabot/update_metadata.test.ts +++ b/src/dependabot/update_metadata.test.ts @@ -507,3 +507,54 @@ test('calculateUpdateType should handle all paths', () => { expect(updateMetadata.calculateUpdateType('1.1.1', '1.1.2')).toEqual('version-update:semver-patch') expect(updateMetadata.calculateUpdateType('1.1.1.1', '1.1.1.2')).toEqual('version-update:semver-patch') }) + +test('it handles versions from `metadataLinks`', async () => { + const commitMessage = `Bump the non-breaking group in /log4j-parent with 2 updates + +Bumps the non-breaking group in /log4j-parent with 2 updates: + + +Updates \`commons-codec:commons-codec\` from 1.17.0 to 1.18.0 +- [Changelog](https://github.com/apache/commons-codec/blob/master/RELEASE-NOTES.txt) +- [Commits](apache/commons-codec@rel/commons-codec-1.17.0...rel/commons-codec-1.18.0) + +Updates \`org.apache.commons:commons-compress\` to 1.27.1 + +--- +updated-dependencies: +- dependency-name: commons-codec:commons-codec +- dependency-name: org.apache.commons:commons-compress +... +` + const updatedDependencies = await updateMetadata.parse(commitMessage, '', 'dependabot/maven/non-breaking-cc60d48967', '2.x') + expect(updatedDependencies).toHaveLength(2) + expect(updatedDependencies[0].dependencyName).toEqual('commons-codec:commons-codec') + expect(updatedDependencies[0].prevVersion).toEqual('1.17.0') + expect(updatedDependencies[0].newVersion).toEqual('1.18.0') + expect(updatedDependencies[1].dependencyName).toEqual('org.apache.commons:commons-compress') + expect(updatedDependencies[1].prevVersion).toEqual('') + expect(updatedDependencies[1].newVersion).toEqual('1.27.1') +}) + +test('it handles new versions from YAML', async () => { + const commitMessage = `Bump the non-breaking group in /log4j-parent with 2 updates + +Bumps the non-breaking group in /log4j-parent with 2 updates: + +--- +updated-dependencies: +- dependency-name: commons-codec:commons-codec + dependency-version: 1.18.0 +- dependency-name: org.apache.commons:commons-compress + dependency-version: 1.27.1 +... +` + const updatedDependencies = await updateMetadata.parse(commitMessage, '', 'dependabot/maven/non-breaking-cc60d48967', '2.x') + expect(updatedDependencies).toHaveLength(2) + expect(updatedDependencies[0].dependencyName).toEqual('commons-codec:commons-codec') + expect(updatedDependencies[0].prevVersion).toEqual('') + expect(updatedDependencies[0].newVersion).toEqual('1.18.0') + expect(updatedDependencies[1].dependencyName).toEqual('org.apache.commons:commons-compress') + expect(updatedDependencies[1].prevVersion).toEqual('') + expect(updatedDependencies[1].newVersion).toEqual('1.27.1') +}) diff --git a/src/dependabot/update_metadata.ts b/src/dependabot/update_metadata.ts index aa5f691..cd9d160 100644 --- a/src/dependabot/update_metadata.ts +++ b/src/dependabot/update_metadata.ts @@ -6,15 +6,18 @@ export interface dependencyAlert { cvss: number } -export interface updatedDependency extends dependencyAlert { +interface dependencyVersions { + prevVersion: string, + newVersion: string +} + +export interface updatedDependency extends dependencyAlert, dependencyVersions { dependencyName: string, dependencyType: string, updateType: string, directory: string, packageEcosystem: string, targetBranch: string, - prevVersion: string, - newVersion: string, compatScore: number, maintainerChanges: boolean, dependencyGroup: string @@ -83,11 +86,14 @@ export async function parse (commitMessage: string, body: string, branchName: st const dependencyGroup = groupName?.groups?.name ?? '' if (data['updated-dependencies']) { + const updatedVersions = parseMetadataLinks(commitMessage) const dirname = branchNameToDirectoryName(chunks, delim, data['updated-dependencies'], dependencyGroup) return await Promise.all(data['updated-dependencies'].map(async (dependency, index) => { - const lastVersion = index === 0 ? prev : '' - const nextVersion = index === 0 ? next : '' + const dependencyName = dependency['dependency-name'] + const updatedVersion = updatedVersions.get(dependencyName) + const lastVersion = updatedVersion?.prevVersion || (index === 0 ? prev : '') + const nextVersion = dependency['dependency-version'] || updatedVersion?.newVersion || (index === 0 ? next : '') const updateType = dependency['update-type'] || calculateUpdateType(lastVersion, nextVersion) return { dependencyName: dependency['dependency-name'], @@ -110,6 +116,35 @@ export async function parse (commitMessage: string, body: string, branchName: st return Promise.resolve([]) } +/** + * Parses the human-readable metadata links from a commit message. + *

+ * See {@code Dependabot::PullRequestCreator::MessageBuilder#metadata_links} in the Ruby codebase for more details + * on the current format. + *

+ *

+ * Note: This data is only available if more than one dependency is updated in a single PR. + * + * @param commitMessage - The commit message containing metadata links. + * @returns A map from the name of the dependency to an updatedDependency object containing the old and new versions. + */ +function parseMetadataLinks(commitMessage: string): Map { + const updates: Map = new Map() + const updatesExpr: RegExp = /^Updates `(?\S+)` (from (?\S+) )?to (?\S+)$/gm + let match: RegExpExecArray | null + while ((match = updatesExpr.exec(commitMessage)) !== null) { + const groups = match.groups + if (groups) { + const dependencyName = groups.dependencyName + updates.set(dependencyName, { + prevVersion: groups.from ?? '', + newVersion: groups.to + }) + } + } + return updates +} + export function calculateUpdateType (lastVersion: string, nextVersion: string) { if (!lastVersion || !nextVersion || lastVersion === nextVersion) { return ''