Skip to content
Merged
Show file tree
Hide file tree
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
69 changes: 16 additions & 53 deletions .github/workflows/branch-name.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,6 @@ jobs:
branch-name:
runs-on: ubuntu-latest
steps:
- name: Check out Issue Type mapping
uses: actions/checkout@v4
with:
sparse-checkout: |
scripts/issue-type.mjs
sparse-checkout-cone-mode: false

- name: Validate source branch name
env:
BRANCH_NAME: ${{ github.head_ref }}
Expand All @@ -35,34 +28,22 @@ jobs:
echo "Branch name valid: ${BRANCH_NAME}"

- name: Validate linked issue
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const path = require("node:path");
const { pathToFileURL } = require("node:url");
const {
BRANCH_ISSUE_PATTERN,
PR_TITLE_PATTERN,
closingReferencePattern,
expectedIssueTypeFor,
validateIssueType,
} = await import(
pathToFileURL(
path.join(process.env.GITHUB_WORKSPACE, "scripts/issue-type.mjs"),
).href,
);

const branch = context.payload.pull_request.head.ref;
const branchMatch = BRANCH_ISSUE_PATTERN.exec(branch);
const branchMatch = /^(?:feat|fix|refactor|docs|chore)\/(\d+)-/.exec(branch);

if (!branchMatch) {
core.setFailed(`Could not extract issue number from branch '${branch}'.`);
return;
}

const [, type, issueNumber] = branchMatch;
const expectedIssueType = expectedIssueTypeFor(type);
const [, issueNumber] = branchMatch;
const pullRequest = context.payload.pull_request;
const titleMatch = PR_TITLE_PATTERN.exec(pullRequest.title.trim());
const titleMatch = /^\[(?:feat|fix|refactor|docs|chore)\]\s+(.+?)\s+\(#(\d+)\)$/.exec(
pullRequest.title.trim(),
);

if (!titleMatch) {
core.setFailed(
Expand All @@ -71,45 +52,27 @@ jobs:
return;
}

const [, titleType, , titleIssueNumber] = titleMatch;
if (titleType !== type) {
core.setFailed(
`PR title type '${titleType}' must match branch type '${type}'.`,
);
return;
}

const [, , titleIssueNumber] = titleMatch;
if (titleIssueNumber !== issueNumber) {
core.setFailed(
`PR title issue number #${titleIssueNumber} must match branch issue #${issueNumber}.`,
);
return;
}

const text = `${pullRequest.title}\n${pullRequest.body ?? ""}`;
const closingKeyword = closingReferencePattern(issueNumber);
if (!closingKeyword.test(text)) {
core.setFailed(
`PR must include 'Fixes #${issueNumber}' (or an equivalent closing keyword).`,
);
const closingKeyword = new RegExp(
`(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s+#${issueNumber}\\b`,
"i",
);
if (!closingKeyword.test(pullRequest.body ?? "")) {
core.setFailed(`PR body must reference the linked issue with a closing keyword, such as 'Fixes #${issueNumber}'.`);
return;
}

const { data: issue } = await github.rest.issues.get({
await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: Number(issueNumber),
});
const actualIssueType = issue.type?.name ?? null;
const issueTypeCheck = validateIssueType(type, actualIssueType);

if (!issueTypeCheck.ok) {
core.setFailed(
`Issue #${issueNumber} must use native GitHub Issue Type '${expectedIssueType}' for branch '${type}', but found '${actualIssueType ?? "none"}'.`,
);
return;
}

core.info(
`PR linked Issue #${issueNumber} uses native GitHub Issue Type '${actualIssueType}'. Legacy type:* labels are not required.`,
);
core.info(`Linked issue #${issueNumber} validated.`);
2 changes: 1 addition & 1 deletion scripts/cleanup-merged.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ async function main() {
}

console.log(`Deleting local branch ${item.branch}...`);
runGit(["branch", "-d", "--", item.branch], { cwd: repoRoot });
runGit(["branch", "-D", "--", item.branch], { cwd: repoRoot });
}

console.log("Cleanup complete.");
Expand Down
21 changes: 21 additions & 0 deletions scripts/cleanup-merged.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,24 @@ test("prunes missing worktree registrations without deleting unmerged branches",
removeRepository(repo, linkedPath);
}
});

test("deletes branches merged into the selected base even when HEAD differs", () => {
const repo = createRepository();

try {
git(repo, ["checkout", "-b", "chore/current"]);
git(repo, ["checkout", "--quiet", "main"]);
git(repo, ["checkout", "-b", "chore/merged-elsewhere"]);
commitFile(repo, "merged-elsewhere.txt", "merged into main\n");
mergeBranch(repo, "chore/merged-elsewhere");
git(repo, ["checkout", "--quiet", "chore/current"]);

const result = runCleanup(repo, ["--base", "main", "--yes"]);

assert.equal(result.status, 0, result.stderr);
assert.equal(branchExists(repo, "chore/merged-elsewhere"), false);
assert.equal(git(repo, ["branch", "--show-current"]), "chore/current");
} finally {
removeRepository(repo, "");
}
});
Loading