Files
vac-optimizer/scripts/ci-policy.mjs
T

86 lines
3.0 KiB
JavaScript

import { execFileSync, spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
const env = process.env;
const git = (...args) => execFileSync('git', args, { encoding: 'utf8' }).trim();
const ancestor = (base, commit) =>
spawnSync('git', ['merge-base', '--is-ancestor', base, commit]).status === 0;
const conventional = /^[a-z]+(?:\([^()\r\n]+\))?!?: \S.*$/i;
const requirePolicy = (condition, message) => {
if (!condition) throw new Error(message);
};
try {
const commit = env.CI_COMMIT_SHA;
requirePolicy(commit === git('rev-parse', 'HEAD'), 'CI must validate the checked-out commit.');
git(
'fetch',
'--no-tags',
'origin',
'+refs/heads/main:refs/remotes/origin/main',
'+refs/heads/develop:refs/remotes/origin/develop',
);
if (env.CI_COMMIT_TAG) {
requirePolicy(
/^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(env.CI_COMMIT_TAG),
'Release tags must use vMAJOR.MINOR.PATCH without leading zeroes.',
);
const { version } = JSON.parse(readFileSync('package.json', 'utf8'));
requirePolicy(env.CI_COMMIT_TAG === `v${version}`, 'Release tag must match package.json.');
requirePolicy(ancestor(commit, 'origin/main'), 'Release commit must belong to main.');
} else {
const mr = env.CI_PIPELINE_SOURCE === 'merge_request_event';
const branch = mr ? env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME : env.CI_COMMIT_BRANCH;
const target = mr
? env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME
: branch === 'main' || branch?.startsWith('hotfix/')
? 'main'
: 'develop';
requirePolicy(branch, 'A branch name is required.');
requirePolicy(['main', 'develop'].includes(target), 'MRs must target main or develop.');
if (mr) {
const title = env.CI_MERGE_REQUEST_TITLE?.replace(
/^(?:Draft:|WIP:|\[Draft\]|\[WIP\]|\(Draft\)|\(WIP\))\s*/i,
'',
);
requirePolicy(
conventional.test(title ?? ''),
'MR title must use Conventional Commits.',
);
requirePolicy(
target !== 'main' || /^(release|hotfix)\/.+/.test(branch),
'Only release/* and hotfix/* may merge into main.',
);
if (target === 'develop' && branch !== 'main' && !/^(release|hotfix)\//.test(branch)) {
requirePolicy(
ancestor('origin/develop', commit),
'Rebase the working branch onto origin/develop before merging.',
);
requirePolicy(
!git('rev-list', '--merges', `origin/develop..${commit}`),
'Working branches must use rebase, not merge commits.',
);
}
}
const before = env.CI_COMMIT_BEFORE_SHA;
const base =
!mr && before && !/^0+$/.test(before) && ancestor(before, commit)
? before
: git('merge-base', `origin/${target}`, commit);
const subjects = git('log', '--format=%h %s', `${base}..${commit}`);
for (const line of subjects ? subjects.split('\n') : []) {
requirePolicy(
conventional.test(line.slice(line.indexOf(' ') + 1)),
`Commit must use Conventional Commits: ${line}`,
);
}
}
console.log('GitFlow, version and commit policy passed.');
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}