Skip to content
Open
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:
- dev
- release
Comment on lines +1 to +8
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 PR description says this change only adds a workflow to verify PR titles, but this PR also includes App Service CLI behavior/tests and an Azure Pipelines + PowerShell automation script. Please either update the PR description to cover these additional changes (and testing/rollout impact) or split them into separate PRs so the CI workflow change can be reviewed/rolled out independently.

Copilot uses AI. Check for mistakes.

jobs:
verify-pr-title:
runs-on: ubuntu-latest
steps:
- name: Validate PR title format
uses: actions/github-script@v7
with:
Comment on lines +3 to +16
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.

Because this workflow runs on pull_request_target, the default GITHUB_TOKEN typically has elevated write permissions. Since the script only needs to read the PR title, it should explicitly set minimal permissions (e.g., pull-requests: read and contents: none) to reduce the blast radius if the workflow is ever extended in the future.

Copilot uses AI. Check for mistakes.
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();
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.

The repo’s documented PR title format has additional mandatory structure beyond the prefix (e.g., if the title includes BREAKING CHANGE it must be followed by a colon; hotfixes should use Hotfix as the second part; issue fixes should use Fix #number as the second part). The current checks only validate the prefix and a partial Fix # pattern, so titles like [X] BREAKING CHANGE something would incorrectly pass. Consider extending validation to enforce the documented BREAKING CHANGE: / Hotfix: / Fix #12345: patterns when those keywords are present (see doc/authoring_command_modules/README.md “Format PR Title”).

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: Enforce structured patterns after the prefix (BREAKING CHANGE, Hotfix, Fix #12345)
if (prefixMatch) {
const rest = title.slice(prefixMatch[0].length).trim();
// 2a: Issue fixes must use `Fix #12345:` immediately after the prefix
const fixIndex = rest.search(/Fix\s+#/i);
if (fixIndex !== -1) {
if (fixIndex !== 0) {
errors.push(
"`Fix #<number>:` must appear immediately after the component name (e.g., `[Component] Fix #12345: description`)."
);
}
if (!/^Fix\s+#\d+:/i.test(rest)) {
errors.push(
"Issue fix titles must use the `Fix #<number>:` pattern (e.g., `[Component] Fix #12345: description`)."
);
}
}
// 2b: Breaking changes must use `BREAKING CHANGE:` immediately after the prefix
const breakingIndex = rest.indexOf("BREAKING CHANGE");
if (breakingIndex !== -1) {
if (breakingIndex !== 0) {
errors.push(
"`BREAKING CHANGE` must appear immediately after the component name (e.g., `[Component] BREAKING CHANGE: description`)."
);
}
const afterBreaking = rest.slice("BREAKING CHANGE".length);
if (!afterBreaking.startsWith(":")) {
errors.push(
"`BREAKING CHANGE` must be followed by a colon (e.g., `[Component] BREAKING CHANGE: description`)."
);
}
}
// 2c: Hotfix titles must use `Hotfix:` immediately after the prefix
const hotfixIndex = rest.search(/hotfix/i);
if (hotfixIndex !== -1) {
if (!/^Hotfix:/i.test(rest)) {
errors.push(
"Hotfix PR titles must start with `Hotfix:` after the component name (e.g., `[Component] Hotfix: 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