Skip to content
Closed
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
76 changes: 76 additions & 0 deletions .github/workflows/commit_signoff.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Commit Signoff

'on':
pull_request:
merge_group:
types: [checks_requested]

permissions:
contents: read
pull-requests: read

jobs:
commit-signoff:
name: commit-signoff
runs-on: ubuntu-latest
steps:
- name: Check PR commits for Signed-off-by
if: ${{ github.event_name == 'pull_request' }}
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const pull_number = context.payload.pull_request.number;

const commits = await github.paginate(
github.rest.pulls.listCommits,
{
owner,
repo,
pull_number,
per_page: 100,
}
);

const trailerPattern = /^Signed-off-by:\s+.+\s+<[^<>]+>$/im;
const missing = commits.filter(({ commit }) => !trailerPattern.test(commit.message));

Comment on lines +36 to +38
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The Signed-off-by detection regex will treat any line starting with “Signed-off-by:” anywhere in the commit message as valid, even if it appears in the body (e.g., quoted text) rather than as a trailer/footer. That can lead to false positives and undermines the “trailer” requirement. Consider validating only the trailer block at the end of the commit message (e.g., scan upward from the end after the last blank line, or use a trailer parser strategy) instead of a whole-message regex match.

Suggested change
const trailerPattern = /^Signed-off-by:\s+.+\s+<[^<>]+>$/im;
const missing = commits.filter(({ commit }) => !trailerPattern.test(commit.message));
const hasValidSignedOffTrailer = (message) => {
if (typeof message !== 'string') {
return false;
}
const lines = message.split('\n');
// Find the last non-empty line
let i = lines.length - 1;
while (i >= 0 && lines[i].trim() === '') {
i--;
}
if (i < 0) {
return false;
}
// Collect the trailer block (continuous non-empty lines at the end)
const trailerLines = [];
for (; i >= 0 && lines[i].trim() !== ''; i--) {
trailerLines.unshift(lines[i]);
}
const trailerPattern = /^Signed-off-by:\s+.+\s+<[^<>]+>$/;
return trailerLines.some((line) => trailerPattern.test(line));
};
const missing = commits.filter(({ commit }) => !hasValidSignedOffTrailer(commit.message));

Copilot uses AI. Check for mistakes.
const writeSummary = async (lines) => {
await core.summary
.addHeading('Signed-off-by check')
.addRaw(lines.join('\n') + '\n')
.write();
};

if (missing.length === 0) {
await writeSummary([
`Checked ${commits.length} commit(s).`,
'All commits include a Signed-off-by trailer.',
]);
return;
}

await writeSummary([
`Found ${missing.length} commit(s) without a valid Signed-off-by trailer:`,
'',
...missing.map(({ sha, commit }) => `- ${sha.slice(0, 12)} ${commit.message.split('\n')[0]}`),
'',
'Please rewrite the commit message(s) and push again.',
'Examples: `git commit --amend -s` or `git rebase --signoff <base>`.',
]);

core.setFailed(
[
`Found ${missing.length} commit(s) without a valid Signed-off-by trailer.`,
...missing.map(({ sha, commit }) => `- ${sha.slice(0, 12)} ${commit.message.split('\n')[0]}`),
'Please rewrite the commit message(s) and push again.',
'Examples: git commit --amend -s or git rebase --signoff <base>.',
].join('\n')
);

- name: Report merge queue compatibility
if: ${{ github.event_name == 'merge_group' }}
run: |
echo "Signed-off-by is enforced on pull_request workflows."
echo "This merge_group run reports the required status for merge queue compatibility."
Comment on lines +72 to +76
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The merge_group path is currently a no-op (it always exits successfully) so this workflow will report success in the merge queue even when a PR’s commits are missing a Signed-off-by trailer. If this check is marked as required for merge queue, that can effectively bypass the intended enforcement. Consider performing the same validation during merge_group (e.g., checkout the merge_group ref and validate commit messages in the head range), or remove merge_group from this workflow and ensure branch protection requires the pull_request run instead.

Copilot uses AI. Check for mistakes.
2 changes: 2 additions & 0 deletions src/developers.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,5 @@ Then you just add a line saying:
```

using your real name (sorry, no pseudonyms or anonymous contributions.)
Pull requests are checked automatically, and any commit without a valid
`Signed-off-by:` trailer will be blocked until it is fixed.