Files

164 lines
6.6 KiB
YAML

# Warden security clearance.
#
# Grants or revokes a "security clearance" review on PRs based on blocking
# findings from the Warden skills: all security findings plus high desktop↔den
# sync findings. Advisory sync findings never withhold clearance. Humans still
# review and merge; the clearance only satisfies the required-review gate in
# the branch ruleset for clean diffs.
#
# Why workflow_run: this file always executes from the default branch, so a
# PR cannot tamper with the clearance logic. The verdict step additionally
# refuses to self-clear any PR that touches review machinery
# (.github/, warden.toml, .warden/, .agents/skills/, .claude/skills/).
#
# Identity: a dedicated GitHub App (WARDEN_APP_ID / WARDEN_PRIVATE_KEY in the
# `warden-clearance` environment). The app never pushes commits, so
# require_last_push_approval always holds (clearance identity != pusher !=
# author). Until the app secrets are configured this workflow no-ops.
name: Warden Clearance
on:
workflow_run:
workflows: [Warden]
types: [completed]
permissions:
actions: read
contents: read
pull-requests: read
concurrency:
group: warden-clearance-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: false
jobs:
clearance:
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_repository.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 10
environment: warden-clearance
steps:
- name: Check clearance identity is configured
id: identity
env:
APP_ID: ${{ secrets.WARDEN_APP_ID }}
run: |
if [ -z "$APP_ID" ]; then
echo "WARDEN_APP_ID is not configured; clearance disabled."
echo "configured=false" >> "$GITHUB_OUTPUT"
else
echo "configured=true" >> "$GITHUB_OUTPUT"
fi
- name: Download clearance summary
if: steps.identity.outputs.configured == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: warden-summary
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Evaluate verdict
if: steps.identity.outputs.configured == 'true'
id: verdict
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
verdict() { echo "verdict=$1" >> "$GITHUB_OUTPUT"; exit 0; }
summary_sha="$(jq -r '.head_sha' warden-summary.json)"
blocking="$(jq -r '.blocking_count // .findings_count' warden-summary.json)"
advisory="$(jq -r '.sync_advisory_count // 0' warden-summary.json)"
echo "advisory=$advisory" >> "$GITHUB_OUTPUT"
if [ "$summary_sha" != "$HEAD_SHA" ]; then
echo "Summary SHA ($summary_sha) does not match analyzed SHA ($HEAD_SHA); skipping."
verdict skip
fi
pr="$(gh api "repos/$REPO/commits/$HEAD_SHA/pulls" --jq '.[0].number // empty')"
if [ -z "$pr" ]; then
echo "No PR associated with $HEAD_SHA; skipping."
verdict skip
fi
echo "pr=$pr" >> "$GITHUB_OUTPUT"
current_head="$(gh api "repos/$REPO/pulls/$pr" --jq '.head.sha')"
if [ "$current_head" != "$HEAD_SHA" ]; then
echo "PR #$pr head moved to $current_head; skipping stale run."
verdict skip
fi
# Never self-clear a PR that touches the review machinery.
guarded="$(gh api --paginate "repos/$REPO/pulls/$pr/files" --jq '.[].filename' \
| grep -E '^(\.github/|warden\.toml$|\.warden/|\.agents/skills/|\.claude/skills/)' || true)"
if [ -n "$guarded" ]; then
echo "PR #$pr touches review machinery; human review required:"
echo "$guarded"
verdict guarded
fi
if [ "$blocking" = "0" ]; then
echo "No blocking findings for $HEAD_SHA."
verdict clear
else
echo "$blocking blocking finding(s) on $HEAD_SHA."
verdict flagged
fi
- name: Mint clearance token
if: steps.verdict.outputs.verdict == 'clear' || steps.verdict.outputs.verdict == 'flagged'
id: app
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ secrets.WARDEN_APP_ID }}
private-key: ${{ secrets.WARDEN_PRIVATE_KEY }}
- name: Grant clearance
if: steps.verdict.outputs.verdict == 'clear'
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
REPO: ${{ github.repository }}
PR: ${{ steps.verdict.outputs.pr }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
RUN_URL: ${{ github.event.workflow_run.html_url }}
ADVISORY: ${{ steps.verdict.outputs.advisory }}
run: |
set -euo pipefail
BODY="**Warden security clearance: clear.** No blocking findings (security or desktop↔den sync) in this diff (\`$HEAD_SHA\`)."
if [[ "$ADVISORY" =~ ^[0-9]+$ ]] && [ "$ADVISORY" -gt 0 ]; then
BODY="$BODY $ADVISORY advisory desktop↔den sync note(s) were posted as review comments; they do not affect clearance."
fi
BODY="$BODY Automated clearance satisfies the required-review gate only — a human still reviews and merges. [Analysis run]($RUN_URL)"
gh api "repos/$REPO/pulls/$PR/reviews" \
-f event=APPROVE \
-f commit_id="$HEAD_SHA" \
-f body="$BODY"
- name: Revoke stale clearance
if: steps.verdict.outputs.verdict == 'flagged'
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
REPO: ${{ github.repository }}
PR: ${{ steps.verdict.outputs.pr }}
APP_SLUG: ${{ steps.app.outputs.app-slug }}
run: |
set -euo pipefail
rid="$(gh api "repos/$REPO/pulls/$PR/reviews" \
--jq "[.[] | select(.user.login == \"${APP_SLUG}[bot]\" and .state == \"APPROVED\")] | last | .id // empty")"
if [ -n "$rid" ]; then
gh api -X PUT "repos/$REPO/pulls/$PR/reviews/$rid/dismissals" \
-f message="Warden found new blocking findings (security or desktop↔den sync) on the latest diff; clearance revoked." \
|| echo "Could not dismiss review $rid (may already be dismissed)."
else
echo "No active clearance to revoke."
fi