257 lines
6.9 KiB
JavaScript
257 lines
6.9 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const execFileSync = require('child_process').execFileSync;
|
|
|
|
const MONITORED_APPS = {
|
|
portal: {
|
|
packageName: '@tryghost/portal',
|
|
path: 'apps/portal'
|
|
},
|
|
sodoSearch: {
|
|
packageName: '@tryghost/sodo-search',
|
|
path: 'apps/sodo-search'
|
|
},
|
|
comments: {
|
|
packageName: '@tryghost/comments-ui',
|
|
path: 'apps/comments-ui'
|
|
},
|
|
announcementBar: {
|
|
packageName: '@tryghost/announcement-bar',
|
|
path: 'apps/announcement-bar'
|
|
},
|
|
signupForm: {
|
|
packageName: '@tryghost/signup-form',
|
|
path: 'apps/signup-form'
|
|
}
|
|
};
|
|
|
|
const MONITORED_APP_ENTRIES = Object.entries(MONITORED_APPS);
|
|
const MONITORED_APP_PATHS = MONITORED_APP_ENTRIES.map(([, app]) => app.path);
|
|
|
|
function runGit(args) {
|
|
try {
|
|
return execFileSync('git', args, {encoding: 'utf8'}).trim();
|
|
} catch (error) {
|
|
const stderr = error.stderr ? error.stderr.toString().trim() : '';
|
|
const stdout = error.stdout ? error.stdout.toString().trim() : '';
|
|
const message = stderr || stdout || error.message;
|
|
throw new Error(`Failed to run "git ${args.join(' ')}": ${message}`);
|
|
}
|
|
}
|
|
|
|
function readVersionFromPackageJson(packageJsonContent, sourceLabel) {
|
|
let parsedPackageJson;
|
|
|
|
try {
|
|
parsedPackageJson = JSON.parse(packageJsonContent);
|
|
} catch (error) {
|
|
throw new Error(`Unable to parse ${sourceLabel}: ${error.message}`);
|
|
}
|
|
|
|
if (!parsedPackageJson.version || typeof parsedPackageJson.version !== 'string') {
|
|
throw new Error(`${sourceLabel} does not contain a valid "version" field`);
|
|
}
|
|
|
|
return parsedPackageJson.version;
|
|
}
|
|
|
|
function parseSemver(version) {
|
|
const match = version.match(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/);
|
|
|
|
if (!match) {
|
|
throw new Error(`Invalid semver version "${version}"`);
|
|
}
|
|
|
|
const prerelease = match[4] ? match[4].split('.').map((identifier) => {
|
|
if (/^\d+$/.test(identifier)) {
|
|
return Number(identifier);
|
|
}
|
|
|
|
return identifier;
|
|
}) : [];
|
|
|
|
return {
|
|
major: Number(match[1]),
|
|
minor: Number(match[2]),
|
|
patch: Number(match[3]),
|
|
prerelease
|
|
};
|
|
}
|
|
|
|
function comparePrereleaseIdentifier(a, b) {
|
|
const isANumber = typeof a === 'number';
|
|
const isBNumber = typeof b === 'number';
|
|
|
|
if (isANumber && isBNumber) {
|
|
if (a === b) {
|
|
return 0;
|
|
}
|
|
|
|
return a > b ? 1 : -1;
|
|
}
|
|
|
|
if (isANumber) {
|
|
return -1;
|
|
}
|
|
|
|
if (isBNumber) {
|
|
return 1;
|
|
}
|
|
|
|
if (a === b) {
|
|
return 0;
|
|
}
|
|
|
|
return a > b ? 1 : -1;
|
|
}
|
|
|
|
function compareSemver(a, b) {
|
|
const aVersion = parseSemver(a);
|
|
const bVersion = parseSemver(b);
|
|
|
|
if (aVersion.major !== bVersion.major) {
|
|
return aVersion.major > bVersion.major ? 1 : -1;
|
|
}
|
|
|
|
if (aVersion.minor !== bVersion.minor) {
|
|
return aVersion.minor > bVersion.minor ? 1 : -1;
|
|
}
|
|
|
|
if (aVersion.patch !== bVersion.patch) {
|
|
return aVersion.patch > bVersion.patch ? 1 : -1;
|
|
}
|
|
|
|
const aPrerelease = aVersion.prerelease;
|
|
const bPrerelease = bVersion.prerelease;
|
|
|
|
if (!aPrerelease.length && !bPrerelease.length) {
|
|
return 0;
|
|
}
|
|
|
|
if (!aPrerelease.length) {
|
|
return 1;
|
|
}
|
|
|
|
if (!bPrerelease.length) {
|
|
return -1;
|
|
}
|
|
|
|
const maxLength = Math.max(aPrerelease.length, bPrerelease.length);
|
|
for (let i = 0; i < maxLength; i += 1) {
|
|
const aIdentifier = aPrerelease[i];
|
|
const bIdentifier = bPrerelease[i];
|
|
|
|
if (aIdentifier === undefined) {
|
|
return -1;
|
|
}
|
|
|
|
if (bIdentifier === undefined) {
|
|
return 1;
|
|
}
|
|
|
|
const identifierComparison = comparePrereleaseIdentifier(aIdentifier, bIdentifier);
|
|
if (identifierComparison !== 0) {
|
|
return identifierComparison;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
function getChangedFiles(baseSha, compareSha) {
|
|
let mergeBaseSha;
|
|
|
|
try {
|
|
mergeBaseSha = runGit(['merge-base', baseSha, compareSha]);
|
|
} catch (error) {
|
|
throw new Error(`Unable to determine merge-base for ${baseSha} and ${compareSha}. Ensure the base branch history is available in the checkout.\n${error.message}`);
|
|
}
|
|
|
|
return runGit(['diff', '--name-only', mergeBaseSha, compareSha, '--', ...MONITORED_APP_PATHS])
|
|
.split('\n')
|
|
.map(file => file.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function getChangedApps(changedFiles) {
|
|
return MONITORED_APP_ENTRIES
|
|
.filter(([, app]) => {
|
|
return changedFiles.some((file) => {
|
|
return file === app.path || file.startsWith(`${app.path}/`);
|
|
});
|
|
})
|
|
.map(([key, app]) => ({key, ...app}));
|
|
}
|
|
|
|
function getPrVersion(app) {
|
|
const packageJsonPath = path.resolve(__dirname, `../../${app.path}/package.json`);
|
|
|
|
if (!fs.existsSync(packageJsonPath)) {
|
|
throw new Error(`${app.path}/package.json does not exist in this PR`);
|
|
}
|
|
|
|
return readVersionFromPackageJson(
|
|
fs.readFileSync(packageJsonPath, 'utf8'),
|
|
`${app.path}/package.json from PR`
|
|
);
|
|
}
|
|
|
|
function getMainVersion(app) {
|
|
return readVersionFromPackageJson(
|
|
runGit(['show', `origin/main:${app.path}/package.json`]),
|
|
`${app.path}/package.json from main`
|
|
);
|
|
}
|
|
|
|
function main() {
|
|
const baseSha = process.env.PR_BASE_SHA;
|
|
const compareSha = process.env.PR_COMPARE_SHA || process.env.GITHUB_SHA;
|
|
|
|
if (!baseSha) {
|
|
throw new Error('Missing PR_BASE_SHA environment variable');
|
|
}
|
|
|
|
if (!compareSha) {
|
|
throw new Error('Missing PR_COMPARE_SHA/GITHUB_SHA environment variable');
|
|
}
|
|
|
|
const changedFiles = getChangedFiles(baseSha, compareSha);
|
|
const changedApps = getChangedApps(changedFiles);
|
|
|
|
if (changedApps.length === 0) {
|
|
console.log(`No app changes detected. Skipping version bump check.`);
|
|
return;
|
|
}
|
|
|
|
console.log(`Checking version bump for apps: ${changedApps.map(app => app.key).join(', ')}`);
|
|
|
|
const failedApps = [];
|
|
|
|
for (const app of changedApps) {
|
|
const prVersion = getPrVersion(app);
|
|
const mainVersion = getMainVersion(app);
|
|
|
|
if (compareSemver(prVersion, mainVersion) <= 0) {
|
|
failedApps.push(
|
|
`${app.key} (${app.packageName}) was changed but version was not bumped above main (${prVersion} <= ${mainVersion}). Please run "pnpm ship" in ${app.path} to bump the package version.`
|
|
);
|
|
continue;
|
|
}
|
|
|
|
console.log(`${app.key} version bump check passed (${prVersion} > ${mainVersion})`);
|
|
}
|
|
|
|
if (failedApps.length) {
|
|
throw new Error(`Version bump checks failed:\n- ${failedApps.join('\n- ')}`);
|
|
}
|
|
|
|
console.log('All monitored app version bump checks passed.');
|
|
}
|
|
|
|
try {
|
|
main();
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
process.exit(1);
|
|
}
|