Skip to content

Commit eb05d11

Browse files
committed
Better naming for PR message/title & squash merge for PR added
1 parent f9b040d commit eb05d11

4 files changed

Lines changed: 147 additions & 57 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/bin/bash
2+
# Generate detailed commit message body from PR changed files
3+
# Usage: generate_commit_message.sh <pr_number>
4+
5+
set -e
6+
7+
PR_NUMBER=$1
8+
9+
if [ -z "$PR_NUMBER" ]; then
10+
echo "Usage: $0 <pr_number>"
11+
exit 1
12+
fi
13+
14+
# Get changed .qmd files only
15+
changed_files=$(gh pr view "$PR_NUMBER" --json files -q '.files[].path' | grep '^DOCS/.*\.qmd$' || echo "")
16+
17+
if [ -z "$changed_files" ]; then
18+
echo "No documentation files (.qmd) changed"
19+
exit 0
20+
fi
21+
22+
# Group files by category subdirectory
23+
declare -A categories
24+
while IFS= read -r file; do
25+
[ -z "$file" ] && continue
26+
# Extract category (first directory after DOCS/)
27+
category=$(echo "$file" | sed 's|^DOCS/\([^/]*\)/.*|\1|')
28+
# Extract filename without path and .qmd extension
29+
filename=$(basename "$file" .qmd)
30+
31+
# Add to category array
32+
if [ -n "${categories[$category]}" ]; then
33+
categories[$category]="${categories[$category]}\n- ${filename}"
34+
else
35+
categories[$category]="- ${filename}"
36+
fi
37+
done <<< "$changed_files"
38+
39+
# Build message body, sorted by category name
40+
body=""
41+
for category in $(echo "${!categories[@]}" | tr ' ' '\n' | sort); do
42+
# Format category name (replace underscores with spaces, title case)
43+
formatted_category=$(echo "$category" | sed 's/_/ /g')
44+
body="${body}\n\n${formatted_category}:"
45+
body="${body}\n${categories[$category]}"
46+
done
47+
48+
# Output the body
49+
echo -e "$body"
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Generate descriptive PR title based on changed files
2+
// Usage: Called from GitHub Actions with context
3+
4+
module.exports = async ({ github, context, core, version }) => {
5+
const pr = context.payload.pull_request;
6+
if (!pr) {
7+
core.setFailed("No PR context found.");
8+
return;
9+
}
10+
11+
const base = pr.base.ref;
12+
const head = pr.head.ref;
13+
14+
// Define semantic-release type and scope
15+
const typeMap = {
16+
test: "feat",
17+
main: "release",
18+
};
19+
const type = typeMap[base] || "chore";
20+
21+
// Get changed files to make title more descriptive
22+
const { data: files } = await github.rest.pulls.listFiles({
23+
owner: context.repo.owner,
24+
repo: context.repo.repo,
25+
pull_number: pr.number,
26+
});
27+
28+
// Count only .qmd documentation changes (ignore media files)
29+
const docsFiles = files.filter((f) => f.filename.startsWith("DOCS/") && f.filename.endsWith(".qmd"));
30+
31+
// Group by category (first directory after DOCS/)
32+
const categories = {};
33+
docsFiles.forEach((f) => {
34+
const match = f.filename.match(/^DOCS\/([^/]+)\//);
35+
if (match) {
36+
const category = match[1];
37+
categories[category] = (categories[category] || 0) + 1;
38+
}
39+
});
40+
41+
// Build descriptive content from categories
42+
const categoryList = Object.entries(categories)
43+
.sort(([a], [b]) => a.localeCompare(b))
44+
.map(([cat, count]) => {
45+
const formatted = cat.replace(/_/g, " ");
46+
return count > 1 ? `${count} ${formatted}` : formatted;
47+
});
48+
49+
// Construct title
50+
let title;
51+
if (categoryList.length > 0) {
52+
const what = categoryList.slice(0, 3).join(", "); // Max 3 categories in title
53+
const more = categoryList.length > 3 ? ` +${categoryList.length - 3} more` : "";
54+
title = `${type}(docs): update ${what}${more}`;
55+
} else {
56+
// Fallback to branch-based title
57+
title = `${type}(merge): promote ${head} to ${base}`;
58+
}
59+
60+
// Truncate to 100 characters safely
61+
if (title.length > 100) {
62+
title = title.slice(0, 97) + "...";
63+
}
64+
65+
console.log(`Setting PR title to: ${title}`);
66+
67+
await github.rest.pulls.update({
68+
owner: context.repo.owner,
69+
repo: context.repo.repo,
70+
pull_number: pr.number,
71+
title: title,
72+
});
73+
};

.github/workflows/auto-merge.yml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ jobs:
1616
steps:
1717
- name: Checkout
1818
uses: actions/checkout@v4
19+
with:
20+
fetch-depth: 0
1921

2022
- name: Get PR title
2123
id: title
@@ -25,10 +27,30 @@ jobs:
2527
env:
2628
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2729

30+
- name: Generate commit message body
31+
id: commit_body
32+
run: |
33+
body=$(.github/scripts/generate_commit_message.sh ${{ github.event.pull_request.number }})
34+
35+
# Save to output (use delimiter for multiline)
36+
{
37+
echo 'body<<EOF'
38+
echo "$body"
39+
echo 'EOF'
40+
} >> $GITHUB_OUTPUT
41+
env:
42+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43+
2844
- name: Auto-merge the PR
2945
run: |
46+
# Create commit message file
47+
cat > /tmp/commit_msg.txt << 'EOF'
48+
${{ steps.title.outputs.title }}
49+
${{ steps.commit_body.outputs.body }}
50+
EOF
51+
3052
gh pr merge ${{ github.event.pull_request.number }} \
3153
--squash \
32-
--subject "${{ steps.title.outputs.title }}"
54+
--body "$(cat /tmp/commit_msg.txt)"
3355
env:
3456
GH_TOKEN: ${{ secrets.GH_PAT }}

.github/workflows/pull-request.yml

Lines changed: 2 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -14,67 +14,13 @@ jobs:
1414
steps:
1515
- uses: actions/checkout@v4
1616

17-
- uses: actions/setup-node@v4
18-
with:
19-
node-version: "20"
20-
21-
- name: Get next version (dry run)
22-
id: version
23-
run: |
24-
npx semantic-release --dry-run --no-ci > output.log || true
25-
version=$(grep -oE 'The next release version is [0-9]+\.[0-9]+\.[0-9]+' output.log | tail -1 | awk '{print $6}')
26-
echo "Next version: $version"
27-
echo "version=$version" >> $GITHUB_OUTPUT
28-
2917
- name: Generate PR Title
3018
uses: actions/github-script@v7
3119
with:
3220
github-token: ${{ secrets.GITHUB_TOKEN }}
3321
script: |
34-
const pr = context.payload.pull_request;
35-
if (!pr) {
36-
core.setFailed("No PR context found.");
37-
return;
38-
}
39-
40-
const base = pr.base.ref;
41-
const head = pr.head.ref;
42-
43-
// Define semantic-release type
44-
const typeMap = {
45-
test: 'feat',
46-
main: 'release'
47-
};
48-
const type = typeMap[base] || 'chore';
49-
50-
// Sanitize and prepare title content
51-
const sanitize = (name) => name.toLowerCase().replace(/[^\w.-]/g, '-');
52-
53-
// Add version from previous step if available
54-
const version = "${{ steps.version.outputs.version }}";
55-
const hasVersion = version && version.trim() !== "";
56-
57-
// Construct title
58-
const contentBase = `promote ${sanitize(head)} to ${sanitize(base)}`;
59-
const versionSuffix = hasVersion ? ` for v${version}` : '';
60-
const content = `${contentBase}${versionSuffix}`;
61-
let title = `${type}(merge): ${content}`;
62-
63-
// Truncate to 100 characters safely
64-
if (title.length > 100) {
65-
const maxContentLength = 100 - `${type}(merge): `.length;
66-
const truncatedContent = content.slice(0, maxContentLength - 1) + '…';
67-
title = `${type}(merge): ${truncatedContent}`;
68-
}
69-
70-
console.log(`Setting PR title to: ${title}`);
71-
72-
await github.rest.pulls.update({
73-
owner: context.repo.owner,
74-
repo: context.repo.repo,
75-
pull_number: pr.number,
76-
title: title
77-
});
22+
const script = require('./.github/scripts/generate_pr_title.js');
23+
await script({github, context, core, version: ""});
7824
7925
validate_merge_origin:
8026
needs: generate_pr_title

0 commit comments

Comments
 (0)