Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/VerifyPRTitle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Verify PR Title

on:
pull_request_target:
types: [opened, edited, synchronize, reopened]
branches:
Comment on lines +3 to +6
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For pull_request_target workflows, it’s best to explicitly set a minimal permissions block (e.g., permissions: {} or only pull-requests: read) so the default GITHUB_TOKEN doesn’t get broader scopes than needed. This workflow only reads the PR title from the event payload and writes a job summary, so it shouldn’t require write permissions.

Copilot uses AI. Check for mistakes.
- dev
- release

jobs:
verify-pr-title:
runs-on: ubuntu-latest
steps:
- name: Validate PR title format
uses: actions/github-script@v7
with:
script: |
const title = context.payload.pull_request.title;
const errors = [];

// Rule 1: Title must start with [Component Name] or {Component Name}
const prefixMatch = title.match(/^(\[([^\]]*)\]|\{([^}]*)\})/);
if (!prefixMatch) {
errors.push(
"PR title must start with `[Component Name]` (customer-facing) or `{Component Name}` (non-customer-facing)."
);
} else {
const componentName = (prefixMatch[2] || prefixMatch[3] || "").trim();
if (!componentName) {
errors.push(
"Component name inside the brackets must not be empty."
);
}
}

// Rule 2: If "Fix #" appears after the prefix, it must be followed by a number
if (prefixMatch) {
const rest = title.slice(prefixMatch[0].length).trim();
Comment on lines +36 to +38
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation currently allows titles like [Storage] / {Core} with no description after the component prefix. The documented PR title format requires the prefix plus a meaningful description; consider failing when the remainder after the prefix is empty/whitespace-only.

Suggested change
// Rule 2: If "Fix #" appears after the prefix, it must be followed by a number
if (prefixMatch) {
const rest = title.slice(prefixMatch[0].length).trim();
// Rule 2: The title must contain a description after the prefix, and if "Fix #" appears there, it must be followed by a number
if (prefixMatch) {
const rest = title.slice(prefixMatch[0].length).trim();
if (!rest) {
errors.push(
"PR title must include a description after the component prefix."
);
}

Copilot uses AI. Check for mistakes.
const fixMatch = rest.match(/^Fix\s+#/i);
if (fixMatch) {
const validFix = rest.match(/^Fix\s+#\d+/i);
if (!validFix) {
errors.push(
"`Fix #` must be followed by a valid issue number (e.g., `Fix #12345`)."
);
}
}
Comment on lines +36 to +47
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow’s checks don’t align with the repo’s documented mandatory PR title rules (e.g., BREAKING CHANGE: must include a colon; hotfix titles should use Hotfix:; issue fixes should use Fix #<number> as the second part). Right now only a partial Fix # pattern is checked (and only when it’s the first token after the prefix). Consider updating the regex/logic to enforce the full set of mandatory formats from doc/authoring_command_modules/README.md and ensure the checks match the examples shown in the summary.

Suggested change
// Rule 2: If "Fix #" appears after the prefix, it must be followed by a number
if (prefixMatch) {
const rest = title.slice(prefixMatch[0].length).trim();
const fixMatch = rest.match(/^Fix\s+#/i);
if (fixMatch) {
const validFix = rest.match(/^Fix\s+#\d+/i);
if (!validFix) {
errors.push(
"`Fix #` must be followed by a valid issue number (e.g., `Fix #12345`)."
);
}
}
// Rule 2: After the prefix, enforce documented patterns (BREAKING CHANGE:, Hotfix:, Fix #<number>:)
if (prefixMatch) {
const rest = title.slice(prefixMatch[0].length).trim();
// 2a: BREAKING CHANGE must include a colon and description
if (/^BREAKING CHANGE/i.test(rest) && !/^BREAKING CHANGE:\s+\S/.test(rest)) {
errors.push(
"`BREAKING CHANGE` titles must use `BREAKING CHANGE:` followed by a description (e.g., `[Component] BREAKING CHANGE: description`)."
);
}
// 2b: Hotfix titles should use `Hotfix:` with a colon and description
if (/^Hotfix/i.test(rest) && !/^Hotfix:\s+\S/.test(rest)) {
errors.push(
"Hotfix titles must use `Hotfix:` followed by a description (e.g., `[Component] Hotfix: description`)."
);
}
// 2c: If "Fix #" appears after the prefix, it must be followed by a number
if (/Fix\s+#/i.test(rest)) {
const allFixFragments = rest.match(/Fix\s+#/gi) || [];
const allValidFixFragments = rest.match(/Fix\s+#\d+/gi) || [];
if (allFixFragments.length !== allValidFixFragments.length) {
errors.push(
"`Fix #` must be followed by a valid issue number (e.g., `Fix #12345`)."
);
}
}
// 2d: When the second part is a Fix reference, enforce `Fix #<number>:` with description
if (/^Fix\s+#/i.test(rest) && !/^Fix\s+#\d+:\s+\S/.test(rest)) {
errors.push(
"When used as the second part of the title, fixes must follow `Fix #<number>:` with a description (e.g., `[Component] Fix #12345: description`)."
);
}

Copilot uses AI. Check for mistakes.
}

if (errors.length > 0) {
const message = [
"## ❌ PR Title Validation Failed\n",
...errors.map(e => `- ${e}`),
"",
"### Expected format",
"```",
"[Component Name] description of the change",
"{Component Name} description of a non-customer-facing change",
"[Component Name] BREAKING CHANGE: description",
"[Component Name] Fix #12345: description",
"```",
"",
"### Examples",
"```",
"[Storage] BREAKING CHANGE: `az storage remove`: Remove --auth-mode argument",
"[ARM] Fix #10246: `az resource tag`: Fix crash when --ids is a resource group ID",
"{Aladdin} Add help example for dns",
"[API Management] Add new operation support",
"```",
].join("\n");

core.summary.addRaw(message);
await core.summary.write();
core.setFailed("PR title does not follow the required format.");
} else {
core.summary.addRaw("## ✅ PR Title Validation Passed");
await core.summary.write();
core.info("PR title format is valid.");
}
Loading