CI / policy (push) Successful in 48s
CI / test (push) Canceled after 0s
CI / benchmark (push) Canceled after 0s
CI / build (push) Canceled after 0s
CI / security (push) Canceled after 0s
CI / images (push) Canceled after 0s
CI / validate (push) Canceled after 1m8s
CI / policy (pull_request) Successful in 9s
CI / validate (pull_request) Successful in 1m6s
CI / test (pull_request) Successful in 3m30s
CI / benchmark (pull_request) Failing after 5m39s
CI / build (pull_request) Successful in 1m5s
CI / security (pull_request) Successful in 1m27s
CI / required (push) Failing after 1s
CI / images (pull_request) Failing after 18s
CI / required (pull_request) Failing after 1s
88 lines
3.0 KiB
JavaScript
88 lines
3.0 KiB
JavaScript
import { execFileSync, spawnSync } from 'node:child_process';
|
|
import { appendFileSync, readFileSync } from 'node:fs';
|
|
import { readCiContext } from './ci-context.mjs';
|
|
|
|
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, tag, branch, slug, pullRequest, before } = readCiContext();
|
|
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 (tag) {
|
|
requirePolicy(
|
|
/^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(tag),
|
|
'Release tags must use vMAJOR.MINOR.PATCH without leading zeroes.',
|
|
);
|
|
const { version } = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
requirePolicy(tag === `v${version}`, 'Release tag must match package.json.');
|
|
requirePolicy(ancestor(commit, 'origin/main'), 'Release commit must belong to main.');
|
|
} else {
|
|
const target = pullRequest
|
|
? pullRequest.base?.ref
|
|
: branch === 'main' || branch?.startsWith('hotfix/')
|
|
? 'main'
|
|
: 'develop';
|
|
requirePolicy(branch, 'A branch name is required.');
|
|
git('check-ref-format', `refs/heads/${branch}`);
|
|
requirePolicy(['main', 'develop'].includes(target), 'PRs must target main or develop.');
|
|
|
|
if (pullRequest) {
|
|
const title = pullRequest.title?.replace(
|
|
/^(?:Draft:|WIP:|\[Draft\]|\[WIP\]|\(Draft\)|\(WIP\))\s*/i,
|
|
'',
|
|
);
|
|
requirePolicy(
|
|
conventional.test(title ?? ''),
|
|
'PR 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 base =
|
|
!pullRequest && 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}`,
|
|
);
|
|
}
|
|
}
|
|
if (env.GITHUB_OUTPUT) {
|
|
appendFileSync(env.GITHUB_OUTPUT, `slug=${slug}\ntag=${tag}\n`);
|
|
}
|
|
console.log('GitFlow, version and commit policy passed.');
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
process.exitCode = 1;
|
|
}
|