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
128 changes: 128 additions & 0 deletions .github/workflows/add-trailer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2025 Robin Jarry
---
name: Add Trailer

on:
issue_comment:
types: [created]
workflow_run:
workflows: ["Review"]
types: [completed]

permissions:
contents: read
pull-requests: read
actions: read

jobs:
add-trailer:
if: >-
${{ (github.event_name == 'issue_comment' && github.event.issue.pull_request
&& (contains(github.event.comment.body, 'Acked-by:') || contains(github.event.comment.body, 'Tested-by:')))
|| (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') }}
runs-on: ubuntu-latest
steps:
- name: Get PR info and trailer
id: info
uses: actions/github-script@v7
with:
script: |
let pr, trailer;

if (context.eventName === 'issue_comment') {
const body = context.payload.comment.body;
const regexp = /(acked|tested)-by:\s+(\w[^<]+\w)(?:\s+<([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>)?/i;
const match = body.match(regexp);
if (!match) {
throw new Error(`No valid trailer found in comment: ${body}`)
}

const { data: prData } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.issue.number
});

pr = prData;
trailer = `${match[1]}-by: ${match[2]} <${match[3] || 'UNKNOWN_EMAIL'}>`;

} else if (context.eventName === 'workflow_run') {
const headBranch = context.payload.workflow_run.head_branch;

const { data: allPRs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
});

const matchingPRs = allPRs.filter(p => p.head.ref === headBranch);

if (matchingPRs.length === 0) {
throw new Error(`No open PR found for ${headBranch}`)
}
pr = matchingPRs[0];

const actor = context.payload.workflow_run.triggering_actor.login;
const req = await fetch(`https://api.github.com/users/${actor}`);
const user = await req.json();
trailer = `Reviewed-by: ${user.name || actor} <${user.email || 'UNKNOWN_EMAIL'}>`;
}

return {
base_sha: pr.base.sha,
head_repo: pr.head.repo.full_name,
head_ref: pr.head.ref,
trailer: trailer,
};

- uses: actions/checkout@v4
with:
token: ${{ secrets.TRAILER_BOT_TOKEN }}
repository: ${{ fromJSON(steps.info.outputs.result).head_repo }}
ref: ${{ fromJSON(steps.info.outputs.result).head_ref }}
fetch-depth: 0
persist-credentials: true

- name: Amend all pull request commits with the trailer
env:
GIT_TRAILER_DEBUG: 1
BASE_SHA: ${{ fromJSON(steps.info.outputs.result).base_sha }}
HEAD_REF: ${{ fromJSON(steps.info.outputs.result).head_ref }}
TRAILER: ${{ fromJSON(steps.info.outputs.result).trailer }}
run: |
set -xe

if echo "$TRAILER" | grep -qw 'UNKNOWN_EMAIL'; then
NAME=$(echo "$TRAILER" | sed -En 's/.+-by:[[:blank:]]+([^<]+)[[:blank:]]<.+/\1/p')
EMAIL=$(git log --pretty=%aE --author="$NAME" | head -n1)

if [ -z "$EMAIL" ]; then
EMAIL=$(git shortlog -se -w0 --group=author --group=committer \
--group=trailer:acked-by --group=trailer:reviewed-by \
--group=trailer:reported-by --group=trailer:signed-off-by \
--group=trailer:tested-by HEAD | \
sed -En "s/^[[:space:]]+[0-9]+[[:space:]]+$NAME <([^@]+@[^>]+)>$/\\1/p" | head -n1)
fi

if [ -z "$EMAIL" ]; then
echo "Error: Could not find email for $NAME"
exit 1
fi

TRAILER=$(echo "$TRAILER" | sed "s/\\<UNKNOWN_EMAIL\\>/$EMAIL/")
fi

GIT_COMMITTER_NAME=$(git log -1 --pretty=%cN)
GIT_COMMITTER_EMAIL=$(git log -1 --pretty=%cE)
git config set user.name "$GIT_COMMITTER_NAME"
git config set user.email "$GIT_COMMITTER_EMAIL"
export GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL

rm -f .git/hooks/commit-msg
ln -s ../../devtools/commit-msg .git/hooks/commit-msg

git rebase "$BASE_SHA" \
--exec "git commit -C HEAD --no-edit --amend --trailer='$TRAILER'"

git push --force origin "$HEAD_REF"
168 changes: 0 additions & 168 deletions .github/workflows/apply.sh

This file was deleted.

32 changes: 0 additions & 32 deletions .github/workflows/apply.yml

This file was deleted.

3 changes: 3 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ concurrency:

jobs:
build-and-tests:
if: ${{ github.actor != 'grout-bot' }}
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -108,6 +109,7 @@ jobs:
- run: ccache -sv

build-cross-aarch64:
if: ${{ github.actor != 'grout-bot' }}
runs-on: ubuntu-24.04
container: debian:testing
env:
Expand Down Expand Up @@ -147,6 +149,7 @@ jobs:
- run: ccache -sv

lint:
if: ${{ github.actor != 'grout-bot' }}
runs-on: ubuntu-24.04
container: fedora:latest
steps:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:

jobs:
deb:
if: ${{ github.actor != 'grout-bot' }}
permissions:
actions: write
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -63,6 +64,7 @@ jobs:
retention-days: 5

rpm:
if: ${{ github.actor != 'grout-bot' }}
permissions:
actions: write
runs-on: ubuntu-24.04
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2025 Robin Jarry
---
name: Review

on:
pull_request_review:
types: [submitted]

permissions:
contents: read

jobs:
approved:
if: github.event.review.state == 'approved'
runs-on: ubuntu-latest
steps:
- name: Review approved
run: echo "PR approved by ${{ github.event.review.user.login }}"