# Non-Blocking Review (NBR) Workflow for Trunk-Based Development
#
# Inspired by Martin's "Trunktopus" pattern for server-side review management.
#
# This workflow provides:
# - Automatic issue creation for every push to main (audit trail)
# - Commit status gates (pending → success) for deploy blocking
# - Inline diffs for small changes (< 60 lines)
# - Multi-reviewer support via checklist
#
# To use this workflow:
# 1. Rename this file to `nbr-review.yml`
# 2. Configure your .tbdflow.yml:
# review:
# enabled: true
# strategy: github-workflow
# workflow: nbr-review.yml
# default_reviewers:
# - reviewer-username
#
# For teams NOT using tbdflow, you can trigger reviews in three ways:
#
# A) Tag-based trigger — add to the `on:` section:
# push:
# tags:
# - 'review/*'
#
# Examples:
# git tag -a "review/abc1234" -m "Asking for review of abc1234"
# or by using the lightweight approach: git tag "review/abc1234"
# git push origin review/abc1234
#
# B) Conventional commit filter — add to the `create-review` job:
# if: >-
# github.event_name == 'workflow_dispatch' ||
# contains(github.event.head_commit.message, 'feat:') ||
# contains(github.event.head_commit.message, 'fix:') ||
# contains(github.event.head_commit.message, 'refactor:') ||
# contains(github.event.head_commit.message, 'perf:')
#
# C) Commit message marker — see the "Check if review needed" step below
#
# Option B (conventional commit job filter) is recommended (if you really don't want to use tbdflow :)).
# Most teams already use conventional commits, so it's zero friction,
# just commit as usual and reviews happen automatically for meaningful changes while skipping chore: and ci: commits.
#
# Option C is essentially a more flexible version of B (regex instead of contains()), so teams that want finer control will graduate to that.
#
name: Non-Blocking Review
on:
# Triggered by tbdflow CLI
workflow_dispatch:
inputs:
commit_sha:
description: 'The full commit SHA to review'
required: true
type: string
commit_message:
description: 'The commit message'
required: true
type: string
author:
description: 'The commit author'
required: true
type: string
reviewers:
description: 'Comma-separated list of reviewers'
required: false
type: string
default: ''
# Also trigger on every push to main for full audit trail
push:
branches:
- main
# --- (A) Uncomment to also trigger on review tags ---
#tags:
# - 'review/*'
# Trigger when review issues are closed (updates commit status)
issues:
types: [closed]
# Prevent duplicate issues from rapid pushes
concurrency:
group: nbr-review-${{ github.sha }}
cancel-in-progress: false
jobs:
create-review:
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
# --- (B) Uncomment to only review conventional commit types ---
# if: >-
# github.event_name == 'workflow_dispatch' ||
# contains(github.event.head_commit.message, 'feat:') ||
# contains(github.event.head_commit.message, 'fix:') ||
# contains(github.event.head_commit.message, 'refactor:') ||
# contains(github.event.head_commit.message, 'perf:')
permissions:
issues: write
statuses: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2 # Need parent commit for diff
# --- (C) Uncomment to filter by commit message marker [review] or
# conventional commit prefixes. Gate subsequent steps with:
# if: steps.check-review.outputs.needs_review == 'true'
#
# - name: Check if review needed
# id: check-review
# run: |
# MSG=$(git log -1 --format=%s)
# if [[ "$MSG" =~ ^(feat|fix|refactor|perf|docs)\(?.*\)?:.* ]]; then
# echo "needs_review=true" >> $GITHUB_OUTPUT
# elif [[ "$MSG" =~ \[review\] ]]; then
# echo "needs_review=true" >> $GITHUB_OUTPUT
# else
# echo "needs_review=false" >> $GITHUB_OUTPUT
# fi
- name: Get commit info
id: commit-info
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_SHA: ${{ inputs.commit_sha }}
INPUT_MESSAGE: ${{ inputs.commit_message }}
INPUT_AUTHOR: ${{ inputs.author }}
INPUT_REVIEWERS: ${{ inputs.reviewers }}
PUSH_SHA: ${{ github.sha }}
run: |
if [ "$EVENT_NAME" == "workflow_dispatch" ]; then
echo "sha=$INPUT_SHA" >> $GITHUB_OUTPUT
# Use heredoc for message to safely handle special characters
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
echo "message<<$EOF" >> $GITHUB_OUTPUT
echo "$INPUT_MESSAGE" >> $GITHUB_OUTPUT
echo "$EOF" >> $GITHUB_OUTPUT
echo "author=$INPUT_AUTHOR" >> $GITHUB_OUTPUT
echo "reviewers=$INPUT_REVIEWERS" >> $GITHUB_OUTPUT
else
echo "sha=$PUSH_SHA" >> $GITHUB_OUTPUT
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
echo "message<<$EOF" >> $GITHUB_OUTPUT
git log -1 --format=%s >> $GITHUB_OUTPUT
echo "$EOF" >> $GITHUB_OUTPUT
echo "author=$(git log -1 --format=%an)" >> $GITHUB_OUTPUT
echo "reviewers=" >> $GITHUB_OUTPUT
fi
- name: Check for existing review issue
id: existing
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMIT_SHA: ${{ steps.commit-info.outputs.sha }}
run: |
SHORT_SHA="${COMMIT_SHA:0:7}"
EXISTING=$(gh issue list --state open --label "review-pending" --search "$SHORT_SHA in:title" --json number --jq '.[0].number // empty')
if [ -n "$EXISTING" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "issue_number=$EXISTING" >> $GITHUB_OUTPUT
echo "Review issue #$EXISTING already exists for $SHORT_SHA"
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Generate inline diff (for small changes)
if: steps.existing.outputs.exists != 'true'
id: diff
env:
COMMIT_SHA: ${{ steps.commit-info.outputs.sha }}
run: |
DIFF=$(git show --no-color --stat --patch --no-prefix "$COMMIT_SHA" | tail -n +2)
DIFF_LINES=$(echo "$DIFF" | wc -l)
if [ "$DIFF_LINES" -le 60 ]; then
echo "include_diff=true" >> $GITHUB_OUTPUT
echo "diff_lines=$DIFF_LINES" >> $GITHUB_OUTPUT
# Escape for GitHub Actions
DIFF_ESCAPED=$(echo "$DIFF" | sed 's/`/\\`/g')
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
echo "diff<<$EOF" >> $GITHUB_OUTPUT
echo "$DIFF" >> $GITHUB_OUTPUT
echo "$EOF" >> $GITHUB_OUTPUT
else
echo "include_diff=false" >> $GITHUB_OUTPUT
echo "diff_lines=$DIFF_LINES" >> $GITHUB_OUTPUT
fi
- name: Ensure labels exist
if: steps.existing.outputs.exists != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh label create "review-pending" --color "1D76DB" --description "Review awaiting attention" 2>/dev/null || true
gh label create "review-accepted" --color "0E8A16" --description "Review approved" 2>/dev/null || true
gh label create "review-concern" --color "E4E669" --description "Concern raised - needs attention" 2>/dev/null || true
gh label create "review-dismissed" --color "6E7681" --description "Review dismissed" 2>/dev/null || true
- name: Create review issue
if: steps.existing.outputs.exists != 'true'
id: create-issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMIT_SHA: ${{ steps.commit-info.outputs.sha }}
COMMIT_MESSAGE: ${{ steps.commit-info.outputs.message }}
COMMIT_AUTHOR: ${{ steps.commit-info.outputs.author }}
COMMIT_REVIEWERS: ${{ steps.commit-info.outputs.reviewers }}
INCLUDE_DIFF: ${{ steps.diff.outputs.include_diff }}
DIFF_LINES: ${{ steps.diff.outputs.diff_lines }}
DIFF_CONTENT: ${{ steps.diff.outputs.diff }}
REPO_FULL_NAME: ${{ github.repository }}
run: |
SHORT_SHA="${COMMIT_SHA:0:7}"
REPO_URL="https://github.com/$REPO_FULL_NAME"
# Build diff section
if [ "$INCLUDE_DIFF" == "true" ]; then
DIFF_SECTION="
<details>
<summary>📝 Inline diff (${DIFF_LINES} lines)</summary>
\`\`\`diff
${DIFF_CONTENT}
\`\`\`
</details>"
else
DIFF_SECTION="
> 📄 Large change (${DIFF_LINES} lines) - [view full diff]($REPO_URL/commit/$COMMIT_SHA)"
fi
# Build reviewer checklist
REVIEWER_CHECKLIST=""
if [ -n "$COMMIT_REVIEWERS" ]; then
IFS=',' read -ra REVIEWER_ARRAY <<< "$COMMIT_REVIEWERS"
for reviewer in "${REVIEWER_ARRAY[@]}"; do
reviewer=$(echo "$reviewer" | xargs) # trim whitespace
if [ -n "$reviewer" ]; then
REVIEWER_CHECKLIST="$REVIEWER_CHECKLIST
- [ ] @$reviewer"
fi
done
fi
BODY="## Non-Blocking Review Request
**Commit:** [\`$SHORT_SHA\`]($REPO_URL/commit/$COMMIT_SHA)
**Author:** $COMMIT_AUTHOR
**Message:** $COMMIT_MESSAGE
$DIFF_SECTION
---
> In Trunk-Based Development, this code is already in the trunk.
> Your goal is **Course Correction** and **Knowledge Sharing**, not gatekeeping.
### 🔍 What to Look For
| Focus | Question |
|-------|----------|
| **Design & Intent** | Does the implementation align with our architectural patterns? |
| **Logic & Edge Cases** | Are there logical flaws or unhappy paths that tests might miss? |
| **Readability** | Are names descriptive? (Code as Documentation) |
| **Simplification** | Can this be done with less code or lower complexity? |
### 💬 How to Comment
- **Questions > Commands**: _\"Could we use the existing helper here?\"_ instead of _\"Change this.\"_
- **Praise**: If you see something clever or clean, say so! NBR boosts team morale.
- **Nitpicking**: Label minor style issues as \`(nit)\` so the author knows they're optional.
### ✅ Reviewer Checklist
$REVIEWER_CHECKLIST
### When to Close
- **Approve & Close**: Code is safe and understandable.
- **Comment & Close**: Non-critical improvements noted.
- **Keep Open**: Only for critical bugs or security flaws requiring immediate fix-forward.
---
To approve via CLI: \`tbdflow review --approve $SHORT_SHA\`"
# Create the issue
ISSUE_URL=$(gh issue create \
--title "[Review] $COMMIT_MESSAGE ($SHORT_SHA)" \
--body "$BODY" \
--label "review-pending")
ISSUE_NUM=$(echo "$ISSUE_URL" | grep -oE '[0-9]+$')
echo "issue_number=$ISSUE_NUM" >> $GITHUB_OUTPUT
echo "✅ Created review issue #$ISSUE_NUM: $ISSUE_URL"
# Set pending commit status with link to the review issue
gh api "repos/$REPO_FULL_NAME/statuses/$COMMIT_SHA" \
-f state='pending' \
-f context='peer-review' \
-f description='Awaiting non-blocking review' \
-f target_url="$ISSUE_URL"
echo "✅ Commit status set to pending"
# Update commit status when review issue is closed.
# Matches any review label to ensure commit status is always updated.
on-review-closed:
runs-on: ubuntu-latest
if: >-
github.event_name == 'issues' &&
github.event.action == 'closed' &&
(contains(github.event.issue.labels.*.name, 'review-pending') ||
contains(github.event.issue.labels.*.name, 'review-accepted') ||
contains(github.event.issue.labels.*.name, 'review-dismissed') ||
contains(github.event.issue.labels.*.name, 'invalid'))
permissions:
statuses: write
issues: write # needs write to swap labels on close
steps:
- name: Extract commit SHA and mark as reviewed
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_URL: ${{ github.event.issue.html_url }}
ISSUE_LABELS: ${{ join(github.event.issue.labels.*.name, ',') }}
REPO_FULL_NAME: ${{ github.repository }}
run: |
# Extract commit SHA from issue body (looks for /commit/SHA pattern)
FULL_SHA=$(echo "$ISSUE_BODY" | grep -oE 'github\.com/.*/commit/[a-f0-9]+' | grep -oE '[a-f0-9]{40}' | head -n1)
if [ -z "$FULL_SHA" ]; then
# Try short SHA from title
SHORT_SHA=$(echo "$ISSUE_TITLE" | grep -oE '\([a-f0-9]{7}\)' | tr -d '()')
if [ -n "$SHORT_SHA" ]; then
# Get full SHA from short
FULL_SHA=$(gh api "repos/$REPO_FULL_NAME/commits/$SHORT_SHA" --jq '.sha' 2>/dev/null || echo "")
fi
fi
if [ -n "$FULL_SHA" ]; then
ISSUE_NUM=$(echo "$ISSUE_URL" | grep -oE '[0-9]+$')
# Swap labels only if no terminal label is already set
# (tbdflow CLI or a human may have already set review-accepted or review-dismissed)
HAS_ACCEPTED=$(echo "$ISSUE_LABELS" | grep -c "review-accepted" || true)
HAS_DISMISSED=$(echo "$ISSUE_LABELS" | grep -c "review-dismissed" || true)
IS_INVALID=$(echo "$ISSUE_LABELS" | grep -c "invalid" || true)
if [ "$HAS_ACCEPTED" -eq 0 ] && [ "$HAS_DISMISSED" -eq 0 ] && [ "$IS_INVALID" -eq 0 ]; then
gh issue edit "$ISSUE_NUM" --repo "$REPO_FULL_NAME" --remove-label "review-pending" 2>/dev/null || true
gh issue edit "$ISSUE_NUM" --repo "$REPO_FULL_NAME" --add-label "review-accepted" 2>/dev/null || true
fi
# Set commit status based on review outcome
if [ "$HAS_DISMISSED" -gt 0 ]; then
STATUS_DESC="Review dismissed"
elif [ "$IS_INVALID" -gt 0 ]; then
STATUS_DESC="Invalid review"
else
STATUS_DESC="Non-blocking review completed ✅"
fi
gh api "repos/$REPO_FULL_NAME/statuses/$FULL_SHA" \
-f state='success' \
-f context='peer-review' \
-f description="$STATUS_DESC" \
-f target_url="$ISSUE_URL"
echo "✅ Commit $FULL_SHA marked as reviewed"
else
echo "⚠️ Could not extract commit SHA from issue"
fi