Skip to content

Commit 33f78e5

Browse files
authored
Merge pull request #84 from SecureToolsProject/fix/release-promotion-commit-validation
🐛[Fix] Allow release promotion commit validation
2 parents 9e959b6 + 00a458e commit 33f78e5

5 files changed

Lines changed: 27 additions & 11 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ jobs:
5757
BASE_SHA: ${{ github.event.pull_request.base.sha }}
5858
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
5959
PR_TITLE: ${{ github.event.pull_request.title }}
60+
COMMIT_CONVENTION_GRANDFATHER: edb0ade331c54f380ae33abe7816c5a92f3a590a
6061
run: |
61-
node scripts/validate-commit-message.mjs --range "$BASE_SHA" "$HEAD_SHA"
62+
node scripts/validate-commit-message.mjs --range "$BASE_SHA" "$HEAD_SHA" --grandfather-through "$COMMIT_CONVENTION_GRANDFATHER"
6263
node scripts/validate-commit-message.mjs --title "$PR_TITLE"
6364
6465
- name: Check changed files for whitespace errors

‎docs/development-workflow.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Human-authored commits use `<Gitmoji>[<Action>] <imperative subject>`. The prefi
3636

3737
There is no space between the Gitmoji and `[Action]`; exactly one space separates the closing bracket from a non-empty, concise imperative subject. Each commit represents one logical change. For example, `✨[Feat] Add Image to Text OCR` and `✅[Test] Cover OCR cancellation` are valid; `feat: add OCR`, `✨ [Feat] Add OCR`, and `✨[Fix] Add OCR` are invalid.
3838

39-
CI validates non-merge commits introduced by the pull request’s actual base-to-head range and validates the pull-request title with the same structural rule. Technical merge commits are excluded by their multiple-parent topology so normal merge commits remain supported. Published non-conforming history is retained and never rewritten solely for message compliance.
39+
CI validates non-merge commits introduced by the pull request’s actual base-to-head range and validates the pull-request title with the same structural rule. Technical merge commits are excluded by their multiple-parent topology so normal merge commits remain supported. Published non-conforming history through `edb0ade331c54f380ae33abe7816c5a92f3a590a` is an explicit grandfather boundary: ancestors of that commit are excluded from later ranges, while every human-authored commit after it remains subject to validation. That history is retained and never rewritten solely for message compliance.
4040

4141
## Production release
4242

‎scripts/validate-commit-message.mjs‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,15 @@ function git(argumentsList, cwd) {
3838
return execFileSync("git", argumentsList, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
3939
}
4040

41-
export function nonMergeCommits(base, head, cwd = process.cwd()) {
42-
const output = git(["rev-list", "--reverse", "--no-merges", `${base}..${head}`], cwd);
41+
export function nonMergeCommits(base, head, cwd = process.cwd(), grandfatherThrough = null) {
42+
const argumentsList = ["rev-list", "--reverse", "--no-merges", `${base}..${head}`];
43+
if (grandfatherThrough) argumentsList.push(`^${grandfatherThrough}`);
44+
const output = git(argumentsList, cwd);
4345
return output ? output.split(/\r?\n/) : [];
4446
}
4547

46-
export function validateCommitRange(base, head, cwd = process.cwd()) {
47-
return nonMergeCommits(base, head, cwd).map((sha) => {
48+
export function validateCommitRange(base, head, cwd = process.cwd(), grandfatherThrough = null) {
49+
return nonMergeCommits(base, head, cwd, grandfatherThrough).map((sha) => {
4850
const subject = git(["show", "-s", "--format=%s", sha], cwd);
4951
return { sha, subject, ...validateCommitMessage(subject) };
5052
});
@@ -69,17 +71,19 @@ export function run(argumentsList = process.argv.slice(2), cwd = process.cwd())
6971
return;
7072
}
7173

72-
if (mode === "--range" && values.length === 2) {
73-
const results = validateCommitRange(values[0], values[1], cwd);
74+
if (mode === "--range" && (values.length === 2 || (values.length === 4 && values[2] === "--grandfather-through"))) {
75+
const grandfatherThrough = values[3] || null;
76+
const results = validateCommitRange(values[0], values[1], cwd, grandfatherThrough);
7477
const failures = results.filter((result) => !result.valid);
7578
if (failures.length) {
7679
throw new Error(failures.map((failure) => failureText(`commit ${failure.sha}`, failure.subject, failure.reason)).join("\n\n"));
7780
}
78-
console.log(`Validated ${results.length} non-merge commit${results.length === 1 ? "" : "s"} in ${values[0]}..${values[1]}.`);
81+
const boundary = grandfatherThrough ? ` after grandfather boundary ${grandfatherThrough}` : "";
82+
console.log(`Validated ${results.length} non-merge commit${results.length === 1 ? "" : "s"} in ${values[0]}..${values[1]}${boundary}.`);
7983
return;
8084
}
8185

82-
throw new Error("Usage: node scripts/validate-commit-message.mjs --message <subject> | --title <title> | --range <base-sha> <head-sha>");
86+
throw new Error("Usage: node scripts/validate-commit-message.mjs --message <subject> | --title <title> | --range <base-sha> <head-sha> [--grandfather-through <sha>]");
8387
}
8488

8589
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {

‎tests/ci-foundation.test.mjs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ assert.match(workflow, /name: Enforce commit and pull request title conventions/
2121
assert.match(workflow, /BASE_SHA: \$\{\{ github\.event\.pull_request\.base\.sha \}\}/);
2222
assert.match(workflow, /HEAD_SHA: \$\{\{ github\.event\.pull_request\.head\.sha \}\}/);
2323
assert.match(workflow, /PR_TITLE: \$\{\{ github\.event\.pull_request\.title \}\}/);
24-
assert.match(workflow, /node scripts\/validate-commit-message\.mjs --range "\$BASE_SHA" "\$HEAD_SHA"/);
24+
assert.match(workflow, /COMMIT_CONVENTION_GRANDFATHER: edb0ade331c54f380ae33abe7816c5a92f3a590a/);
25+
assert.match(workflow, /node scripts\/validate-commit-message\.mjs --range "\$BASE_SHA" "\$HEAD_SHA" --grandfather-through "\$COMMIT_CONVENTION_GRANDFATHER"/);
2526
assert.match(workflow, /node scripts\/validate-commit-message\.mjs --title "\$PR_TITLE"/);
2627
assert.match(workflow, /git diff --check/);
2728
assert.match(workflow, /run: npm ci --ignore-scripts/);

‎tests/commit-message.test.mjs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ try {
8181
assert.equal(failures.length, 1);
8282
assert.equal(failures[0].subject, "test: add invalid fixture");
8383
assert.match(failures[0].sha, /^[0-9a-f]{40}$/);
84+
85+
fs.writeFileSync(path.join(temporaryRepository, "current.txt"), "current\n");
86+
git("add", "current.txt");
87+
git("commit", "-q", "-m", "🐛[Fix] Validate commits after enforcement");
88+
const currentHead = git("rev-parse", "HEAD");
89+
const grandfathered = validateCommitRange(base, currentHead, temporaryRepository, invalidHead);
90+
assert.equal(grandfathered.length, 1, "published ancestors through the enforcement boundary are excluded");
91+
assert.equal(grandfathered[0].subject, "🐛[Fix] Validate commits after enforcement");
92+
assert.equal(grandfathered[0].valid, true);
93+
assert.doesNotThrow(() => run(["--range", base, currentHead, "--grandfather-through", invalidHead], temporaryRepository));
8494
} finally {
8595
fs.rmSync(temporaryRepository, { recursive: true, force: true });
8696
}

0 commit comments

Comments
 (0)