Chore: fix prev version detection in the update changelog action (#105964)

This commit is contained in:
Denis Vodopianov
2025-05-26 11:55:58 +03:00
committed by GitHub
parent ae2556b204
commit 88355cc2b9
2 changed files with 127 additions and 60 deletions
+35 -60
View File
@@ -1,6 +1,7 @@
import { appendFileSync, writeFileSync } from 'fs';
import { exec as execCallback } from 'node:child_process';
import { promisify } from 'node:util';
import {appendFileSync, writeFileSync} from 'fs';
import {exec as execCallback} from 'node:child_process';
import {promisify} from 'node:util';
import {findPreviousVersion, semverParse} from "./semver.js";
//
// Github Action core utils: logging (notice + debug log levels), must escape
@@ -9,35 +10,6 @@ import { promisify } from 'node:util';
const escapeData = (s) => s.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
const LOG = (msg) => console.log(`::notice::${escapeData(msg)}`);
//
// Semver utils: parse, compare, sort etc (using official regexp)
// https://regex101.com/r/Ly7O1x/3/
//
const semverRegExp =
/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
const semverParse = (tag) => {
const m = tag.match(semverRegExp);
if (!m) {
return;
}
const [_, major, minor, patch, prerelease] = m;
return [+major, +minor, +patch, prerelease, tag];
};
// semverCompare takes two parsed semver tags and comparest them more or less
// according to the semver specs
const semverCompare = (a, b) => {
for (let i = 0; i < 3; i++) {
if (a[i] !== b[i]) {
return a[i] < b[i] ? 1 : -1;
}
}
if (a[3] !== b[3]) {
return a[3] < b[3] ? 1 : -1;
}
return 0;
};
// Using `git tag -l` output find the tag (version) that goes semantically
// right before the given version. This might not work correctly with some
@@ -45,29 +17,32 @@ const semverCompare = (a, b) => {
// into this action explicitly to avoid this step.
const getPreviousVersion = async (version) => {
const exec = promisify(execCallback);
const { stdout } = await exec('git tag -l');
const prev = stdout
const {stdout} = await exec('git for-each-ref --sort=-creatordate --format \'%(refname:short)\' refs/tags');
const parsedTags = stdout
.split('\n')
.map(semverParse)
.filter((tag) => tag)
.sort(semverCompare)
.find((tag) => semverCompare(tag, semverParse(version)) > 0);
.filter(Boolean);
const parsedVersion = semverParse(version);
const prev = findPreviousVersion(parsedTags, parsedVersion);
if (!prev) {
throw `Could not find previous git tag for ${version}`;
}
return prev[4];
return prev[5];
};
// A helper for Github GraphQL API endpoint
const graphql = async (ghtoken, query, variables) => {
const { env } = process;
const {env} = process;
const results = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${ghtoken}`,
},
body: JSON.stringify({ query, variables }),
body: JSON.stringify({query, variables}),
});
const res = await results.json();
@@ -100,7 +75,7 @@ const getCommitishDate = async (name, owner, target) => {
}
}
`,
{ name, owner, target }
{name, owner, target}
);
return result.repository.object.committedDate;
};
@@ -160,7 +135,7 @@ const getHistory = async (name, owner, from, to) => {
let cursor;
let nodes = [];
for (;;) {
for (; ;) {
const result = await graphql(ghtoken, query, {
name,
owner,
@@ -170,7 +145,7 @@ const getHistory = async (name, owner, from, to) => {
});
LOG(`GraphQL: ${JSON.stringify(result)}`);
nodes = [...nodes, ...result.repository.ref.compare.commits.nodes];
const { hasNextPage, endCursor } = result.repository.ref.compare.commits.pageInfo;
const {hasNextPage, endCursor} = result.repository.ref.compare.commits.pageInfo;
if (!hasNextPage) {
break;
}
@@ -186,7 +161,7 @@ const getHistory = async (name, owner, from, to) => {
// PR grouping relies on Github labels only, not on the PR contents.
const getChangeLogItems = async (name, owner, from, to) => {
// check if a node contains a certain label
const hasLabel = ({ labels }, label) => labels.nodes.some(({ name }) => name === label);
const hasLabel = ({labels}, label) => labels.nodes.some(({name}) => name === label);
// get all the PRs between the two "commitish" items
const history = await getHistory(name, owner, from, to);
@@ -197,17 +172,17 @@ const getChangeLogItems = async (name, owner, from, to) => {
return [];
}
const item = changes[0];
const { number, url, labels } = item;
const {number, url, labels} = item;
const title = item.title.replace(/^\[[^\]]+\]:?\s*/, '');
// for changelog PRs try to find a suitable category.
// Note that we can not detect "deprecation notices" like that
// as there is no suitable label yet.
const isBug = /fix/i.test(title) || hasLabel({ labels }, 'type/bug');
const isBreaking = hasLabel({ labels }, 'breaking change');
const isBug = /fix/i.test(title) || hasLabel({labels}, 'type/bug');
const isBreaking = hasLabel({labels}, 'breaking change');
const isPlugin =
hasLabel({ labels }, 'area/grafana/ui') ||
hasLabel({ labels }, 'area/grafana/toolkit') ||
hasLabel({ labels }, 'area/grafana/runtime');
hasLabel({labels}, 'area/grafana/ui') ||
hasLabel({labels}, 'area/grafana/toolkit') ||
hasLabel({labels}, 'area/grafana/runtime');
const author = item.commits.nodes[0].commit.author.user?.login;
return {
repo: name,
@@ -227,7 +202,7 @@ const getChangeLogItems = async (name, owner, from, to) => {
// ======================================================
LOG(`Changelog action started`);
console.log(process.argv);
const ghtoken = process.env.GITHUB_TOKEN || process.env.INPUT_GITHUB_TOKEN;
if (!ghtoken) {
throw 'GITHUB_TOKEN is not set and "github_token" input is empty';
@@ -286,15 +261,15 @@ const markdown = (changelog) => {
: `### ${title}
${items
.map(
(item) =>
`- ${item.title.replace(/^([^:]*:)/gm, '**$1**')} ${
item.repo === 'grafana-enterprise'
? '(Enterprise)'
: `${pullRequestLink(item.number)}${item.author ? ', ' + userLink(item.author) : ''}`
}`
)
.join('\n')}
.map(
(item) =>
`- ${item.title.replace(/^([^:]*:)/gm, '**$1**')} ${
item.repo === 'grafana-enterprise'
? '(Enterprise)'
: `${pullRequestLink(item.number)}${item.author ? ', ' + userLink(item.author) : ''}`
}`
)
.join('\n')}
`;
// Render all present sections for the given changelog
+92
View File
@@ -0,0 +1,92 @@
//
// Semver utils: parse, compare, sort etc (using official regexp)
// https://regex101.com/r/Ly7O1x/3/
//
const semverRegExp =
/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
export function semverParse(tag) {
const m = tag.match(semverRegExp);
if (!m) {
return;
}
const [_, major, minor, patch, prerelease, build] = m;
return [+major, +minor, +patch, prerelease, build, tag];
};
// semverCompare takes two parsed semver tags and comparest them more or less
// according to the semver specs
export function semverCompare(a, b) {
for (let i = 0; i < 3; i++) {
if (a[i] !== b[i]) {
return a[i] < b[i] ? 1 : -1;
}
}
if (a[3] !== b[3]) {
return a[3] < b[3] ? 1 : -1;
}
return 0;
};
// Finds the highest version that is lower than the target version.
//
// This function relies on the following invariant: versions are sorted by the release date.
// It will produce wrong result if invariant doesn't hold.
export const findPreviousVersion = (versionByDate, target) => {
let prev = null;
for (let i = 0; i < versionByDate.length; i++) {
const version = versionByDate[i];
// version is greater than the target
if (semverCompare(target, version) > 0) {
continue;
}
// we came across the target version, all versions seen previously have greater release date.
if (semverCompare(target, version) === 0 && target[4] === version[4]) {
prev = null;
continue;
}
if (prev == null) {
prev = version;
continue;
}
if (semverCompare(prev, version) > 0) {
prev = version;
}
}
return prev;
};
const versionsByDate = [
"v10.4.19", "v12.0.1", "v11.6.2", "v11.5.5", "v11.4.5", "v11.3.7", "v11.2.10", "v12.0.0+security-01", "v11.2.9+security-01", "v11.3.6+security-01",
"v11.6.1+security-01", "v11.4.4+security-01", "v11.5.4+security-01", "v10.4.18+security-01", "v12.0.0", "v11.6.1",
"v11.5.4", "v11.4.4", "v11.3.6", "v11.2.9", "v10.4.18", "v11.6.0+security-01", "v11.5.3+security-01", "v11.4.3+security-01",
"v11.3.5+security-01", "v11.2.8+security-01", "v10.4.17+security-01", "v11.2.8", "v11.6.0", "v11.5.2", "v11.4.2",
"v11.3.4", "v11.2.7", "v11.1.12", "v11.0.11", "v10.4.16", "v11.5.1", "v11.5.0", "v11.3.3", "v11.1.11", "v11.2.6",
"v11.0.10", "v10.4.15", "v11.4.1", "v11.4.0", "v11.3.2", "v11.2.5", "v11.1.10", "v11.0.9", "v10.4.14", "v11.3.1",
"v11.2.4", "v11.1.9", "v11.0.8", "v10.4.13", "v11.0.2", "v10.4.6", "v10.3.8", "v10.2.9", "v11.1.0", "v11.0.1",
"v10.4.5", "v10.3.7", "v10.2.8", "v9.5.20", "v10.4.4", "v9.5.19", "v10.1.10", "v10.2.7", "v10.3.6", "v10.4.3",
"v11.0.0", "v10.4.2", "v11.0.0-preview", "v10.1.9", "v10.0.13", "v9.2.0", "v9.1.8",
].map(semverParse);
function test(version, expected) {
const v1 = semverParse(version);
const prev = findPreviousVersion(versionsByDate, v1);
const failureMessage = `FAIILED. Expected ${expected}, but was ${prev[5]}`;
console.log(`Test ${version}, ${prev[5] === expected ? 'PASSED' : failureMessage}`);
}
test("v11.5.4+security-01", "v11.5.4");
test("v11.5.4", "v11.5.3+security-01");
test("v12.0.0", "v11.6.1");
test("v12.0.0+security-01", "v12.0.0");
test("v11.0.0", "v11.0.0-preview");