Files
reverse-engineering-anything/.github/workflows/pr-title.yml
T
morluto 5efaedda88 ci: run required checks on merge-queue groups (#11297)
* ci: run required checks on merge-queue groups

Add the merge_group trigger to CI and PR title validation so every required
check reports on the queued group's exact head. Group runs use exact-head
concurrency groups that pushes cannot cancel, classify the group's combined
diff from its base (a non-ancestor base fails planning), and keep the CI
required gate running when a group is cancelled so it fails instead of
reporting a skipped check. Title validation checks every squash commit
subject between the group base and head.

* ci: read the merge-group comparison from a file
2026-11-15 11:29:13 -05:00

78 lines
3.4 KiB
YAML

name: PR title
on:
pull_request:
branches: [main, "release/*"]
types: [opened, synchronize, reopened, edited]
# A queued group lands its squash commits on main. Their subjects are the
# queued PR titles, so the queue revalidates exactly what Release Please reads.
merge_group:
types: [checks_requested]
permissions: {}
concurrency:
group: pr-title-${{ github.event.pull_request.number || github.event.merge_group.head_sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
pr-title:
name: Conventional PR title
runs-on: ubuntu-24.04
timeout-minutes: 2
permissions:
contents: read
steps:
- name: Validate title for Release Please
env:
PR_TITLE: ${{ github.event.pull_request.title }}
GH_TOKEN: ${{ github.token }}
MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
MERGE_GROUP_HEAD_SHA: ${{ github.event.merge_group.head_sha }}
run: |
set -euo pipefail
# The comparison includes file patches and can exceed the size of one
# environment string or argument, so it travels through a file.
comparison_file=""
if [ "${GITHUB_EVENT_NAME}" = "merge_group" ]; then
comparison_file="$(mktemp)"
trap 'rm -f "${comparison_file}"' EXIT
gh api "repos/${GITHUB_REPOSITORY}/compare/${MERGE_GROUP_BASE_SHA}...${MERGE_GROUP_HEAD_SHA}" > "${comparison_file}"
fi
MERGE_GROUP_COMPARISON_FILE="${comparison_file}" node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
const conventional = /^(?:feat|fix|docs|refactor|perf|test|build|ci|chore|revert)(?:\([a-z0-9._/-]+\))?!?: .+/u;
const event = process.env.GITHUB_EVENT_NAME;
let titles;
if (event === "pull_request") {
titles = [process.env.PR_TITLE ?? ""];
} else if (event === "merge_group") {
// Every queued squash commit between the group base and head lands on
// main. An incomplete or unexpected comparison fails closed.
const comparison = JSON.parse(
readFileSync(process.env.MERGE_GROUP_COMPARISON_FILE ?? "", "utf8") || "null",
);
const commits = Array.isArray(comparison?.commits) ? comparison.commits : [];
if (
comparison?.status !== "ahead" ||
comparison.behind_by !== 0 ||
commits.length === 0 ||
comparison.total_commits !== commits.length ||
commits.at(-1)?.sha !== process.env.MERGE_GROUP_HEAD_SHA
) {
console.error("Merge group commits between its base and head could not be identified");
process.exit(1);
}
titles = commits.map((entry) => String(entry?.commit?.message ?? "").split("\n")[0]);
} else {
console.error(`Unsupported title validation event: ${event}`);
process.exit(1);
}
const invalid = titles.filter((title) => !conventional.test(title));
if (invalid.length) {
console.error("PR title must use Conventional Commits, for example: feat: add historical source import");
if (event === "merge_group") for (const title of invalid) console.error(`Queued commit subject: ${title}`);
process.exit(1);
}
EOF