From 1c69344634ceca02b069e677b775b08100d25bd9 Mon Sep 17 00:00:00 2001 From: "bootc-bot[bot]" <225049296+bootc-bot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:21:54 +0000 Subject: [PATCH 1/3] Add /signoff slash command workflow Implements a /signoff command that maintainers can use to add DCO signoff to PR commits. When a maintainer with triage role or higher comments /signoff on a pull request, this workflow: 1. Verifies the commenter has sufficient permissions (triage or higher) 2. Checks out the PR branch and verifies the tip commit SHA matches 3. Adds a DCO signoff based on the commenter's GitHub identity 4. Force pushes the signed commit The workflow provides clear feedback for each outcome: - Permission denied if user lacks triage role - Error if commit SHA has changed since comment was posted - Success message when signoff is added - Notice if commit is already signed off This reduces friction for maintainers who want to sign off on behalf of contributors, while maintaining DCO compliance. Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/signoff.yml | 182 ++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 .github/workflows/signoff.yml diff --git a/.github/workflows/signoff.yml b/.github/workflows/signoff.yml new file mode 100644 index 0000000..0db1bbd --- /dev/null +++ b/.github/workflows/signoff.yml @@ -0,0 +1,182 @@ +name: DCO Signoff Command + +on: + issue_comment: + types: [created] + +# Only run on PR comments that start with /signoff +jobs: + signoff: + if: | + github.event.issue.pull_request && + startsWith(github.event.comment.body, '/signoff') + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Generate App token + id: app-token + uses: actions/create-github-app-token@v3.2.0 + with: + client-id: ${{ vars.GH_AW_APP_CLIENT_ID }} + private-key: ${{ secrets.GH_AW_APP_PRIVATE_KEY }} + repositories: ${{ github.event.repository.name }} + + - name: Check commenter permissions + id: check-permissions + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + COMMENTER: ${{ github.event.comment.user.login }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + + # Get the user's permission level + PERMISSION=$(gh api \ + "/repos/$REPO/collaborators/$COMMENTER/permission" \ + --jq '.permission') + + echo "User $COMMENTER has permission: $PERMISSION" + + # Check if user has triage or higher (triage, write, maintain, admin) + case "$PERMISSION" in + admin|maintain|write|triage) + echo "authorized=true" >> $GITHUB_OUTPUT + ;; + *) + echo "authorized=false" >> $GITHUB_OUTPUT + ;; + esac + + - name: Post unauthorized message + if: steps.check-permissions.outputs.authorized != 'true' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + COMMENTER: ${{ github.event.comment.user.login }} + run: | + gh pr comment "$ISSUE_NUMBER" --repo "$REPO" --body \ + "❌ @$COMMENTER does not have sufficient permissions to use /signoff (requires triage role or higher)" + exit 1 + + - name: Checkout repository + if: steps.check-permissions.outputs.authorized == 'true' + uses: actions/checkout@v4 + with: + token: ${{ steps.app-token.outputs.token }} + fetch-depth: 0 + + - name: Get PR details and add signoff + if: steps.check-permissions.outputs.authorized == 'true' + id: signoff + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + COMMENTER: ${{ github.event.comment.user.login }} + run: | + set -euo pipefail + + # Get PR details + PR_DATA=$(gh pr view "$ISSUE_NUMBER" --repo "$REPO" --json headRefName,headRefOid) + BRANCH=$(echo "$PR_DATA" | jq -r '.headRefName') + EXPECTED_SHA=$(echo "$PR_DATA" | jq -r '.headRefOid') + + echo "Branch: $BRANCH" + echo "Expected SHA: $EXPECTED_SHA" + + # Checkout the PR branch + git fetch origin "$BRANCH" + git checkout "$BRANCH" + + # Verify tip commit matches expected SHA + ACTUAL_SHA=$(git rev-parse HEAD) + if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then + echo "sha_mismatch=true" >> $GITHUB_OUTPUT + echo "expected_sha=$EXPECTED_SHA" >> $GITHUB_OUTPUT + echo "actual_sha=$ACTUAL_SHA" >> $GITHUB_OUTPUT + exit 1 + fi + + # Get commenter's email from GitHub API + COMMENTER_DATA=$(gh api "/users/$COMMENTER") + COMMENTER_NAME=$(echo "$COMMENTER_DATA" | jq -r '.name // .login') + COMMENTER_EMAIL=$(echo "$COMMENTER_DATA" | jq -r '.email // empty') + + # If no public email, use the noreply address + if [ -z "$COMMENTER_EMAIL" ] || [ "$COMMENTER_EMAIL" = "null" ]; then + COMMENTER_ID=$(echo "$COMMENTER_DATA" | jq -r '.id') + COMMENTER_EMAIL="${COMMENTER_ID}+${COMMENTER}@users.noreply.github.com" + fi + + echo "Signing off as: $COMMENTER_NAME <$COMMENTER_EMAIL>" + + # Get current commit message + COMMIT_MSG=$(git log -1 --pretty=%B) + + # Check if signoff already exists + SIGNOFF_LINE="Signed-off-by: $COMMENTER_NAME <$COMMENTER_EMAIL>" + if echo "$COMMIT_MSG" | grep -qF "$SIGNOFF_LINE"; then + echo "already_signed=true" >> $GITHUB_OUTPUT + exit 0 + fi + + # Amend the commit with signoff + git config user.name "$COMMENTER_NAME" + git config user.email "$COMMENTER_EMAIL" + + # Add signoff to commit message + NEW_COMMIT_MSG="${COMMIT_MSG} + + $SIGNOFF_LINE" + + git commit --amend -m "$NEW_COMMIT_MSG" + + # Force push + git push --force-with-lease origin "$BRANCH" + + echo "signed_off=true" >> $GITHUB_OUTPUT + + - name: Post SHA mismatch message + if: | + steps.check-permissions.outputs.authorized == 'true' && + steps.signoff.outputs.sha_mismatch == 'true' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + EXPECTED_SHA: ${{ steps.signoff.outputs.expected_sha }} + ACTUAL_SHA: ${{ steps.signoff.outputs.actual_sha }} + run: | + gh pr comment "$ISSUE_NUMBER" --repo "$REPO" --body \ + "❌ Cannot add signoff: the tip commit has changed. + Expected: \`$EXPECTED_SHA\` + Actual: \`$ACTUAL_SHA\` + + Please try again with the current commit." + + - name: Post already signed message + if: | + steps.check-permissions.outputs.authorized == 'true' && + steps.signoff.outputs.already_signed == 'true' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + run: | + gh pr comment "$ISSUE_NUMBER" --repo "$REPO" --body \ + "✅ This commit is already signed off by you." + + - name: Post success message + if: | + steps.check-permissions.outputs.authorized == 'true' && + steps.signoff.outputs.signed_off == 'true' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + run: | + gh pr comment "$ISSUE_NUMBER" --repo "$REPO" --body \ + "✅ DCO signoff added successfully." From d6026506a73a308fa3a9a3c0a1787348c58e445c Mon Sep 17 00:00:00 2001 From: "bootc-bot[bot]" <225049296+bootc-bot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:39:11 +0000 Subject: [PATCH 2/3] Fix /signoff workflow based on review feedback - Change ubuntu-latest to ubuntu-24.04 for explicit version pinning - Handle multiple commits in PR using git rebase --signoff - Update success messages to reflect multi-commit handling Addresses review comments from cgwalters on PR #261 --- .github/workflows/signoff.yml | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/.github/workflows/signoff.yml b/.github/workflows/signoff.yml index 0db1bbd..dbf579a 100644 --- a/.github/workflows/signoff.yml +++ b/.github/workflows/signoff.yml @@ -10,7 +10,7 @@ jobs: if: | github.event.issue.pull_request && startsWith(github.event.comment.body, '/signoff') - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: write pull-requests: write @@ -80,11 +80,13 @@ jobs: set -euo pipefail # Get PR details - PR_DATA=$(gh pr view "$ISSUE_NUMBER" --repo "$REPO" --json headRefName,headRefOid) + PR_DATA=$(gh pr view "$ISSUE_NUMBER" --repo "$REPO" --json headRefName,headRefOid,baseRefName) BRANCH=$(echo "$PR_DATA" | jq -r '.headRefName') EXPECTED_SHA=$(echo "$PR_DATA" | jq -r '.headRefOid') + BASE_BRANCH=$(echo "$PR_DATA" | jq -r '.baseRefName') echo "Branch: $BRANCH" + echo "Base branch: $BASE_BRANCH" echo "Expected SHA: $EXPECTED_SHA" # Checkout the PR branch @@ -113,26 +115,21 @@ jobs: echo "Signing off as: $COMMENTER_NAME <$COMMENTER_EMAIL>" - # Get current commit message - COMMIT_MSG=$(git log -1 --pretty=%B) - - # Check if signoff already exists + # Check if all commits already have the signoff SIGNOFF_LINE="Signed-off-by: $COMMENTER_NAME <$COMMENTER_EMAIL>" - if echo "$COMMIT_MSG" | grep -qF "$SIGNOFF_LINE"; then + UNSIGNED_COMMITS=$(git log "origin/$BASE_BRANCH..HEAD" --grep="$SIGNOFF_LINE" --invert-grep --oneline | wc -l) + + if [ "$UNSIGNED_COMMITS" -eq 0 ]; then echo "already_signed=true" >> $GITHUB_OUTPUT exit 0 fi - # Amend the commit with signoff + # Configure git identity git config user.name "$COMMENTER_NAME" git config user.email "$COMMENTER_EMAIL" - # Add signoff to commit message - NEW_COMMIT_MSG="${COMMIT_MSG} - - $SIGNOFF_LINE" - - git commit --amend -m "$NEW_COMMIT_MSG" + # Use git rebase --signoff to add signoff to all commits + git rebase --signoff "origin/$BASE_BRANCH" # Force push git push --force-with-lease origin "$BRANCH" @@ -167,7 +164,7 @@ jobs: ISSUE_NUMBER: ${{ github.event.issue.number }} run: | gh pr comment "$ISSUE_NUMBER" --repo "$REPO" --body \ - "✅ This commit is already signed off by you." + "✅ All commits are already signed off by you." - name: Post success message if: | @@ -179,4 +176,4 @@ jobs: ISSUE_NUMBER: ${{ github.event.issue.number }} run: | gh pr comment "$ISSUE_NUMBER" --repo "$REPO" --body \ - "✅ DCO signoff added successfully." + "✅ DCO signoff added to all commits successfully." From 665403cde0c2dea7a260a9deede79e4ac12f1c7e Mon Sep 17 00:00:00 2001 From: "bootc-bot[bot]" <225049296+bootc-bot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:06:33 +0000 Subject: [PATCH 3/3] Convert /signoff to gh-aw workflow with label_command trigger Addresses review feedback about runner dispatch efficiency. The previous implementation used issue_comment trigger which dispatched a runner for every comment, then filtered with a job-level 'if' condition. This was wasteful. Changes: - Convert signoff.yml to signoff.md (gh-aw workflow format) - Use label_command trigger which only dispatches when /signoff is used - Permission validation now handled by label_command (triage+ required) - Reuses gh-aw patterns for event triggering, filtering, and authorization - Maintains same functionality: adds DCO signoff to all PR commits Note: Workflow compilation to generate signoff.lock.yml was not possible in the sandbox environment (gh aw compile not available). This needs to be compiled separately using 'gh aw compile' or the standard build process. Fixes review feedback from cgwalters on PR #261 --- .github/workflows/signoff.md | 76 +++++++++++++++ .github/workflows/signoff.yml | 179 ---------------------------------- 2 files changed, 76 insertions(+), 179 deletions(-) create mode 100644 .github/workflows/signoff.md delete mode 100644 .github/workflows/signoff.yml diff --git a/.github/workflows/signoff.md b/.github/workflows/signoff.md new file mode 100644 index 0000000..e064d28 --- /dev/null +++ b/.github/workflows/signoff.md @@ -0,0 +1,76 @@ +--- +description: | + DCO signoff command handler. Maintainers can comment /signoff on a pull + request to add DCO signoff to all commits. Only dispatches a runner when + the /signoff command is used, avoiding unnecessary runs for every comment. + +label_command: + command: signoff + permission: triage + +permissions: + contents: write + pull-requests: write + +safe-outputs: + github-app: + client-id: ${{ vars.GH_AW_APP_CLIENT_ID }} + private-key: ${{ secrets.GH_AW_APP_PRIVATE_KEY }} + add-comment: + max: 1 +--- + +# DCO Signoff Handler + +You are a DCO signoff automation agent. A maintainer has used the `/signoff` +command on pull request #{{ github.event.issue.number }}. + +## Your Task + +Add DCO signoff to all commits on this pull request: + +1. Get the PR details (branch name, head SHA, base branch): + ```bash + gh pr view {{ github.event.issue.number }} --json headRefName,headRefOid,baseRefName + ``` + +2. Check out the PR branch and verify the tip commit SHA matches the current + state to prevent race conditions. + +3. Get the commenter's identity: + ```bash + gh api /users/{{ github.event.comment.user.login }} + ``` + Extract name and email. If no public email, use the noreply format: + `{user_id}+{username}@users.noreply.github.com` + +4. Check if all commits already have the signoff from this commenter: + ```bash + git log origin/$BASE_BRANCH..HEAD --grep="Signed-off-by: $NAME <$EMAIL>" --invert-grep --oneline | wc -l + ``` + If count is 0, all commits are already signed off. + +5. If not already signed off, configure git identity and use `git rebase --signoff` + to add signoff to all commits: + ```bash + git config user.name "$NAME" + git config user.email "$EMAIL" + git rebase --signoff origin/$BASE_BRANCH + ``` + +6. Push the signed commits using `push-to-pull-request-branch` safe-output. + +7. Post feedback using `add-comment` safe-output: + - ✅ "All commits are already signed off by you." (if already signed) + - ✅ "DCO signoff added to all commits successfully." (if signed off) + - ❌ "Cannot add signoff: the tip commit has changed. Expected: `$SHA` + Actual: `$ACTUAL_SHA`. Please try again." (if SHA mismatch) + +## Important Notes + +- The `label_command` trigger already handles permission validation (triage or + higher required), so you don't need to check permissions yourself. +- Use `git rebase --signoff` to add signoff to all commits in the PR, not just + the tip commit. +- Always verify the SHA before modifying commits to prevent race conditions. +- Use `--force-with-lease` when pushing to ensure safe force push. diff --git a/.github/workflows/signoff.yml b/.github/workflows/signoff.yml deleted file mode 100644 index dbf579a..0000000 --- a/.github/workflows/signoff.yml +++ /dev/null @@ -1,179 +0,0 @@ -name: DCO Signoff Command - -on: - issue_comment: - types: [created] - -# Only run on PR comments that start with /signoff -jobs: - signoff: - if: | - github.event.issue.pull_request && - startsWith(github.event.comment.body, '/signoff') - runs-on: ubuntu-24.04 - permissions: - contents: write - pull-requests: write - steps: - - name: Generate App token - id: app-token - uses: actions/create-github-app-token@v3.2.0 - with: - client-id: ${{ vars.GH_AW_APP_CLIENT_ID }} - private-key: ${{ secrets.GH_AW_APP_PRIVATE_KEY }} - repositories: ${{ github.event.repository.name }} - - - name: Check commenter permissions - id: check-permissions - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - COMMENTER: ${{ github.event.comment.user.login }} - REPO: ${{ github.repository }} - run: | - set -euo pipefail - - # Get the user's permission level - PERMISSION=$(gh api \ - "/repos/$REPO/collaborators/$COMMENTER/permission" \ - --jq '.permission') - - echo "User $COMMENTER has permission: $PERMISSION" - - # Check if user has triage or higher (triage, write, maintain, admin) - case "$PERMISSION" in - admin|maintain|write|triage) - echo "authorized=true" >> $GITHUB_OUTPUT - ;; - *) - echo "authorized=false" >> $GITHUB_OUTPUT - ;; - esac - - - name: Post unauthorized message - if: steps.check-permissions.outputs.authorized != 'true' - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - REPO: ${{ github.repository }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - COMMENTER: ${{ github.event.comment.user.login }} - run: | - gh pr comment "$ISSUE_NUMBER" --repo "$REPO" --body \ - "❌ @$COMMENTER does not have sufficient permissions to use /signoff (requires triage role or higher)" - exit 1 - - - name: Checkout repository - if: steps.check-permissions.outputs.authorized == 'true' - uses: actions/checkout@v4 - with: - token: ${{ steps.app-token.outputs.token }} - fetch-depth: 0 - - - name: Get PR details and add signoff - if: steps.check-permissions.outputs.authorized == 'true' - id: signoff - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - REPO: ${{ github.repository }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - COMMENTER: ${{ github.event.comment.user.login }} - run: | - set -euo pipefail - - # Get PR details - PR_DATA=$(gh pr view "$ISSUE_NUMBER" --repo "$REPO" --json headRefName,headRefOid,baseRefName) - BRANCH=$(echo "$PR_DATA" | jq -r '.headRefName') - EXPECTED_SHA=$(echo "$PR_DATA" | jq -r '.headRefOid') - BASE_BRANCH=$(echo "$PR_DATA" | jq -r '.baseRefName') - - echo "Branch: $BRANCH" - echo "Base branch: $BASE_BRANCH" - echo "Expected SHA: $EXPECTED_SHA" - - # Checkout the PR branch - git fetch origin "$BRANCH" - git checkout "$BRANCH" - - # Verify tip commit matches expected SHA - ACTUAL_SHA=$(git rev-parse HEAD) - if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then - echo "sha_mismatch=true" >> $GITHUB_OUTPUT - echo "expected_sha=$EXPECTED_SHA" >> $GITHUB_OUTPUT - echo "actual_sha=$ACTUAL_SHA" >> $GITHUB_OUTPUT - exit 1 - fi - - # Get commenter's email from GitHub API - COMMENTER_DATA=$(gh api "/users/$COMMENTER") - COMMENTER_NAME=$(echo "$COMMENTER_DATA" | jq -r '.name // .login') - COMMENTER_EMAIL=$(echo "$COMMENTER_DATA" | jq -r '.email // empty') - - # If no public email, use the noreply address - if [ -z "$COMMENTER_EMAIL" ] || [ "$COMMENTER_EMAIL" = "null" ]; then - COMMENTER_ID=$(echo "$COMMENTER_DATA" | jq -r '.id') - COMMENTER_EMAIL="${COMMENTER_ID}+${COMMENTER}@users.noreply.github.com" - fi - - echo "Signing off as: $COMMENTER_NAME <$COMMENTER_EMAIL>" - - # Check if all commits already have the signoff - SIGNOFF_LINE="Signed-off-by: $COMMENTER_NAME <$COMMENTER_EMAIL>" - UNSIGNED_COMMITS=$(git log "origin/$BASE_BRANCH..HEAD" --grep="$SIGNOFF_LINE" --invert-grep --oneline | wc -l) - - if [ "$UNSIGNED_COMMITS" -eq 0 ]; then - echo "already_signed=true" >> $GITHUB_OUTPUT - exit 0 - fi - - # Configure git identity - git config user.name "$COMMENTER_NAME" - git config user.email "$COMMENTER_EMAIL" - - # Use git rebase --signoff to add signoff to all commits - git rebase --signoff "origin/$BASE_BRANCH" - - # Force push - git push --force-with-lease origin "$BRANCH" - - echo "signed_off=true" >> $GITHUB_OUTPUT - - - name: Post SHA mismatch message - if: | - steps.check-permissions.outputs.authorized == 'true' && - steps.signoff.outputs.sha_mismatch == 'true' - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - REPO: ${{ github.repository }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - EXPECTED_SHA: ${{ steps.signoff.outputs.expected_sha }} - ACTUAL_SHA: ${{ steps.signoff.outputs.actual_sha }} - run: | - gh pr comment "$ISSUE_NUMBER" --repo "$REPO" --body \ - "❌ Cannot add signoff: the tip commit has changed. - Expected: \`$EXPECTED_SHA\` - Actual: \`$ACTUAL_SHA\` - - Please try again with the current commit." - - - name: Post already signed message - if: | - steps.check-permissions.outputs.authorized == 'true' && - steps.signoff.outputs.already_signed == 'true' - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - REPO: ${{ github.repository }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - run: | - gh pr comment "$ISSUE_NUMBER" --repo "$REPO" --body \ - "✅ All commits are already signed off by you." - - - name: Post success message - if: | - steps.check-permissions.outputs.authorized == 'true' && - steps.signoff.outputs.signed_off == 'true' - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - REPO: ${{ github.repository }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - run: | - gh pr comment "$ISSUE_NUMBER" --repo "$REPO" --body \ - "✅ DCO signoff added to all commits successfully."