Skip to content

👮 Justice Bot

👮 Justice Bot #162

Workflow file for this run

# nullvariant-justice[bot] - The code enforcer
# "テストは?型定義は?エラーハンドリングは?"
name: 👮 Justice Bot
permissions: {}
on:
pull_request_target:
types: [opened, synchronize]
paths:
- '**/package.json'
- '**/package-lock.json'
workflow_run:
workflows: ['CI']
types: [completed]
workflow_dispatch:
inputs:
message:
description: 'What should Justice inspect? (optional)'
required: false
default: 'Code review in progress...'
jobs:
dependency-review:
name: 👮 Dependency Safety Review
runs-on: ubuntu-latest
if: >
github.event_name == 'pull_request_target' &&
(github.actor == 'dependabot[bot]' || github.actor == 'renovate[bot]')
env:
GH_REPO: ${{ github.repository }}
permissions:
contents: read
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
- name: 🔑 Get App Token
id: app-token
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2
with:
app_id: ${{ secrets.JUSTICE_BOT_APP_ID }}
private_key: ${{ secrets.JUSTICE_BOT_PRIVATE_KEY }}
# SECURITY: Verify PR author via API (not github.actor which is forgeable).
# The job-level `if` on github.actor is a performance optimization only.
- name: 🔒 Verify PR author
id: verify
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
PR_AUTHOR=$(gh pr view "$PR_NUMBER" --json author --jq '.author.login')
if [ "$PR_AUTHOR" != "dependabot[bot]" ] && [ "$PR_AUTHOR" != "renovate[bot]" ]; then
echo "::notice::PR author ($PR_AUTHOR) is not a dependency bot. Skipping review."
echo "is_bot=false" >> "$GITHUB_OUTPUT"
else
echo "Verified: PR authored by $PR_AUTHOR"
echo "is_bot=true" >> "$GITHUB_OUTPUT"
fi
# SECURITY: No code checkout. All file reads use the GitHub Contents API.
# This avoids OpenSSF Scorecard "Dangerous-Workflow" alerts for
# pull_request_target + actions/checkout combinations.
- name: 👮 Analyze dependency changes
if: steps.verify.outputs.is_bot == 'true'
id: analyze
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
VERDICT="approve"
: > /tmp/justice-findings.md
# === Load rules from base branch via API (no checkout needed) ===
gh api "repos/${GITHUB_REPOSITORY}/contents/.github/justice-rules.json?ref=${BASE_SHA}" \
--jq '.content' | base64 -d > /tmp/justice-rules.json
RULES="/tmp/justice-rules.json"
# === Check for security labels ===
SECURITY_LABELS=$(gh pr view "$PR_NUMBER" --json labels \
--jq '[.labels[].name | select(test("security"; "i"))] | length' 2>/dev/null || echo "0")
if [ "$SECURITY_LABELS" -gt 0 ]; then
printf '🔴 **PRIORITY**: Security advisory detected. Expedite review and merge.\n\n' >> /tmp/justice-findings.md
VERDICT="security"
fi
# === Get changed package.json files ===
CHANGED_FILES=$(gh pr diff "$PR_NUMBER" --name-only 2>/dev/null || true)
PKG_FILES=$(echo "$CHANGED_FILES" | grep 'package\.json$' || true)
if [ -z "$PKG_FILES" ]; then
echo "No package.json changes detected."
echo "should_comment=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# === Check blocked packages ===
# Get patches for package.json files only (via API for precision)
PKG_PATCHES=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" \
--jq '[.[] | select(.filename | endswith("package.json")) | .patch // ""] | join("\n")' 2>/dev/null || true)
while IFS= read -r pkg; do
[ -z "$pkg" ] && continue
# Only check added lines to avoid false positives from removals
if echo "$PKG_PATCHES" | grep '^+' | grep -v '^+++' | grep -qF "\"${pkg}\""; then
VERDICT="block"
REASON=$(jq -r --arg name "$pkg" '.blocked_packages[] | select(.name == $name) | .reason' "$RULES")
printf '🔴 **BLOCKED**: `%s` update detected.\n%s\n\n' "$pkg" "$REASON" >> /tmp/justice-findings.md
fi
done < <(jq -r '.blocked_packages[].name' "$RULES")
# === Check blocked field changes ===
while IFS= read -r rule; do
[ -z "$rule" ] || [ "$rule" = "null" ] && continue
FILE=$(echo "$rule" | jq -r '.file')
FIELD=$(echo "$rule" | jq -r '.field_path')
REASON=$(echo "$rule" | jq -r '.reason')
# Only check if the file is in the changed files
if ! echo "$PKG_FILES" | grep -qF "$FILE"; then
continue
fi
# Get base value via API
BASE_VAL=$(gh api "repos/${GITHUB_REPOSITORY}/contents/${FILE}?ref=${BASE_SHA}" \
--jq '.content' 2>/dev/null | base64 -d 2>/dev/null | jq -r "${FIELD} // empty" 2>/dev/null || true)
# Get head value via API
HEAD_VAL=$(gh api "repos/${GITHUB_REPOSITORY}/contents/${FILE}?ref=${HEAD_SHA}" \
--jq '.content' 2>/dev/null | base64 -d 2>/dev/null | jq -r "${FIELD} // empty" 2>/dev/null || true)
if [ -n "$BASE_VAL" ] && [ -n "$HEAD_VAL" ] && [ "$BASE_VAL" != "$HEAD_VAL" ]; then
VERDICT="block"
printf '🔴 **BLOCKED**: `%s` changed in `%s`\n- Base: `%s`\n- Head: `%s`\n%s\n\n' \
"$FIELD" "$FILE" "$BASE_VAL" "$HEAD_VAL" "$REASON" >> /tmp/justice-findings.md
fi
done < <(jq -c '.blocked_field_changes[]' "$RULES" 2>/dev/null)
# === Check for major version bumps ===
ESLINT_PKGS=$(jq -r '.major_update_review.eslint_packages[]' "$RULES" 2>/dev/null || true)
ESLINT_PREFIXES=$(jq -r '.major_update_review.eslint_prefixes[]' "$RULES" 2>/dev/null || true)
ESLINT_REASON=$(jq -r '.major_update_review.eslint_reason' "$RULES" 2>/dev/null || true)
DEFAULT_REASON=$(jq -r '.major_update_review.default_reason' "$RULES" 2>/dev/null || true)
while IFS= read -r pkg_file; do
[ -z "$pkg_file" ] && continue
# Get base deps via API
BASE_RAW=$(gh api "repos/${GITHUB_REPOSITORY}/contents/${pkg_file}?ref=${BASE_SHA}" \
--jq '.content' 2>/dev/null | base64 -d 2>/dev/null || true)
[ -z "$BASE_RAW" ] && continue
BASE_DEPS=$(echo "$BASE_RAW" | jq -S '(.dependencies // {}) + (.devDependencies // {})' 2>/dev/null || echo '{}')
# Get head deps via API
HEAD_RAW=$(gh api "repos/${GITHUB_REPOSITORY}/contents/${pkg_file}?ref=${HEAD_SHA}" \
--jq '.content' 2>/dev/null | base64 -d 2>/dev/null || true)
[ -z "$HEAD_RAW" ] && continue
HEAD_DEPS=$(echo "$HEAD_RAW" | jq -S '(.dependencies // {}) + (.devDependencies // {})' 2>/dev/null || echo '{}')
# Find packages where major version changed
MAJOR_CHANGES=$(jq -n --argjson base "$BASE_DEPS" --argjson head "$HEAD_DEPS" '
$head | to_entries[] |
select($base[.key] != null) |
{
key,
old_major: ($base[.key] | gsub("[^0-9.]"; "") | split(".") | if length > 0 then .[0] else "0" end),
new_major: (.value | gsub("[^0-9.]"; "") | split(".") | if length > 0 then .[0] else "0" end),
old_ver: $base[.key],
new_ver: .value
} |
select(.old_major != .new_major)
' 2>/dev/null || true)
[ -z "$MAJOR_CHANGES" ] && continue
while IFS= read -r change; do
[ -z "$change" ] && continue
PKG_NAME=$(echo "$change" | jq -r '.key')
OLD_VER=$(echo "$change" | jq -r '.old_ver')
NEW_VER=$(echo "$change" | jq -r '.new_ver')
# Check if ESLint-related
IS_ESLINT=false
for ep in $ESLINT_PKGS; do
[ "$PKG_NAME" = "$ep" ] && IS_ESLINT=true
done
for prefix in $ESLINT_PREFIXES; do
if echo "$PKG_NAME" | grep -q "^${prefix}"; then
IS_ESLINT=true
fi
done
if [ "$IS_ESLINT" = true ]; then
printf '⚠️ **CAUTION**: Major update for `%s` (%s → %s). %s\n\n' \
"$PKG_NAME" "$OLD_VER" "$NEW_VER" "$ESLINT_REASON" >> /tmp/justice-findings.md
else
printf '⚠️ **CAUTION**: Major update for `%s` (%s → %s). %s\n\n' \
"$PKG_NAME" "$OLD_VER" "$NEW_VER" "$DEFAULT_REASON" >> /tmp/justice-findings.md
fi
done < <(echo "$MAJOR_CHANGES" | jq -c '.')
done <<< "$PKG_FILES"
# Update verdict based on findings
if grep -q 'CAUTION.*Major' /tmp/justice-findings.md 2>/dev/null && [ "$VERDICT" = "approve" ]; then
VERDICT="warn"
fi
# === Default: safe to merge ===
if [ ! -s /tmp/justice-findings.md ]; then
printf '🟢 **APPROVED**: Patch/minor update. No blocked patterns detected. Safe to merge after CI passes.\n' >> /tmp/justice-findings.md
fi
echo "should_comment=true" >> "$GITHUB_OUTPUT"
echo "verdict=${VERDICT}" >> "$GITHUB_OUTPUT"
- name: 📬 Post review comment
if: steps.verify.outputs.is_bot == 'true' && steps.analyze.outputs.should_comment == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
VERDICT: ${{ steps.analyze.outputs.verdict }}
run: |
set -euo pipefail
# Determine verdict emoji
case "$VERDICT" in
block) EMOJI="🚫" ;;
warn) EMOJI="⚠️" ;;
security) EMOJI="🚨" ;;
approve) EMOJI="✅" ;;
*) EMOJI="👮" ;;
esac
# Build comment
{
printf '## 👮 ⚖️ Justice'"'"'s Dependency Verdict %s\n\n' "$EMOJI"
cat /tmp/justice-findings.md
printf '\n---\n\n'
printf '> テストは?型定義は?エラーハンドリングは?\n\n'
printf '*Verdict filed by nullvariant-justice[bot]*\n'
} > /tmp/justice-comment.md
# Find existing Justice comment (update if exists, create if not)
# Filter by bot author to prevent spoofing via user comments with the same string
EXISTING_ID=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
--jq '.[] | select(.user.login == "nullvariant-justice[bot]") | select(.body | contains("Justice\u0027s Dependency Verdict")) | .id' 2>/dev/null | head -1 || true)
if [ -n "$EXISTING_ID" ]; then
gh api "repos/${GITHUB_REPOSITORY}/issues/comments/${EXISTING_ID}" \
-X PATCH -F body=@/tmp/justice-comment.md
echo "Updated existing Justice verdict comment."
else
gh pr comment "$PR_NUMBER" --body-file /tmp/justice-comment.md
echo "Posted new Justice verdict comment."
fi
# NOTE: ci-gated-approve also approves after CI passes. This early approval
# allows dependency bot PRs to merge without waiting for full CI when the
# safety verdict is clear. ci-gated-approve has a duplicate check to skip
# if Justice has already approved.
- name: ✅ Approve PR
if: >
steps.verify.outputs.is_bot == 'true' &&
steps.analyze.outputs.should_comment == 'true' &&
(steps.analyze.outputs.verdict == 'approve' || steps.analyze.outputs.verdict == 'security')
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" \
-f event="APPROVE" \
-f body="⚖️ Justice approves this dependency update. All safety checks passed."
- name: ⚖️ Verdict delivered
if: steps.verify.outputs.is_bot == 'true' && steps.analyze.outputs.should_comment == 'true'
env:
VERDICT: ${{ steps.analyze.outputs.verdict }}
run: |
echo "⚖️ JUSTICE HAS SPOKEN ⚖️"
echo "Verdict: ${VERDICT}"
if [ "$VERDICT" = "block" ]; then
echo "::error::Justice has blocked this PR. See the review comment for details."
exit 1
fi
ci-gated-approve:
name: 👮 CI-Gated Approval
runs-on: ubuntu-latest
# SECURITY: Reject fork PRs — only same-repo branches are trusted for auto-approval.
if: >
github.event_name == 'workflow_run' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_repository != null &&
github.event.workflow_run.head_repository.full_name == github.repository
permissions:
contents: read
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
- name: 🔑 Get App Token
id: app-token
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2
with:
app_id: ${{ secrets.JUSTICE_BOT_APP_ID }}
private_key: ${{ secrets.JUSTICE_BOT_PRIVATE_KEY }}
# SECURITY: workflow_run.pull_requests may be empty when CI was triggered
# by push (not pull_request). Use commits API for reliable PR resolution.
- name: 🔍 Find associated PR
id: find-pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
PR_DATA=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${HEAD_SHA}/pulls" \
2>/dev/null || echo "[]")
PR_COUNT=$(echo "$PR_DATA" | jq 'length')
if [ "$PR_COUNT" -eq 0 ]; then
echo "No open PR found for SHA ${HEAD_SHA}. Skipping."
echo "found=false" >> "$GITHUB_OUTPUT"
elif [ "$PR_COUNT" -gt 1 ]; then
echo "::warning::Multiple PRs ($PR_COUNT) found for SHA ${HEAD_SHA}. Skipping auto-approval."
echo "found=false" >> "$GITHUB_OUTPUT"
else
PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number')
echo "Found PR #${PR_NUMBER}"
echo "found=true" >> "$GITHUB_OUTPUT"
echo "number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
fi
- name: ✅ Approve PR
if: steps.find-pr.outputs.found == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.find-pr.outputs.number }}
run: |
# Avoid duplicate approvals
EXISTING=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" \
--jq '[.[] | select(.user.login == "nullvariant-justice[bot]" and .state == "APPROVED")] | length' \
2>/dev/null || echo "0")
if [ "$EXISTING" -gt 0 ]; then
echo "PR already approved by Justice. Skipping."
else
gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" \
-f event="APPROVE" \
-f body="⚖️ Justice grants passage. CI checks passed — this code meets the garden's standards."
fi
justice-commit:
name: 👮 Enforce the law
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
- name: 👮 Begin inspection
run: |
echo "⚖️ JUSTICE HAS ARRIVED ⚖️"
echo "Let me check your code quality..."
echo "Tests? Types? Error handling?"
- name: 🔑 Get App Token
id: app-token
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2
with:
app_id: ${{ secrets.JUSTICE_BOT_APP_ID }}
private_key: ${{ secrets.JUSTICE_BOT_PRIVATE_KEY }}
- name: 📥 Checkout (for inspection)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.app-token.outputs.token }}
# SECURITY: Use environment variable to prevent script injection
# See: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injection
- name: 📝 Leave a verdict
env:
USER_MESSAGE: ${{ github.event.inputs.message }}
run: |
# Sanitize: truncate to 500 chars, strip control characters except newlines
USER_MESSAGE=$(printf '%s' "$USER_MESSAGE" | head -c 500 | tr -d '\000-\010\013\014\016-\037')
export USER_MESSAGE
mkdir -p docs
cat > docs/JUSTICE.md << 'EOF'
# 👮 Justice's Code Review
> "The law applies equally to all code."
## Today's Verdict
EOF
printf '%s\n' "${USER_MESSAGE}" >> docs/JUSTICE.md
cat >> docs/JUSTICE.md << 'EOF'
### Checklist
- [ ] Tests written?
- [ ] Types defined?
- [ ] Errors handled?
- [ ] Edge cases covered?
---
EOF
echo "*Last inspected: $(date -u +"%Y-%m-%d %H:%M:%S UTC")*" >> docs/JUSTICE.md
cat >> docs/JUSTICE.md << 'EOF'
> テストは?型定義は?エラーハンドリングは?
EOF
- name: 📬 Create PR with verified commit
env:
USER_MESSAGE: ${{ github.event.inputs.message }}
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ steps.app-token.outputs.token }}
sign-commits: true
author: nullvariant-justice[bot] <2610732+nullvariant-justice[bot]@users.noreply.github.com>
committer: nullvariant-justice[bot] <2610732+nullvariant-justice[bot]@users.noreply.github.com>
commit-message: |
👮 Code inspection complete
${{ env.USER_MESSAGE }}
Remember: Tests? Types? Error handling?
branch: justice/verdict-${{ github.run_number }}
delete-branch: true
title: "👮 Justice's code review update"
body: |
## 👮 ⚖️ VERDICT ⚖️
${{ env.USER_MESSAGE }}
---
> テストは?型定義は?エラーハンドリングは?
*This PR was officially filed by nullvariant-justice[bot]*
- name: ⚖️ Case closed
run: |
echo "Inspection complete."
echo "Stay vigilant. Write tests."
echo "⚖️ JUSTICE NEVER SLEEPS ⚖️"