Skip to content
Draft
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
157 changes: 157 additions & 0 deletions .github/workflows/vault-audit-commands.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
name: Vault Audit Commands

# Handles vault audit slash commands from authorized reviewers:
# /vault-audit — run the audit (dispatches to cre-docs)
# /vault-audit skip <reason> — bypass the audit entirely
#
# To resolve individual findings, reply /resolved <reason> directly on each
# blocking finding thread (handled by vault-audit-thread-commands.yml).
#
# Authorization: commenter must have write or admin permission on this repository.

on:
issue_comment:
types: [created]

jobs:
handle-command:
if: |
github.event.issue.pull_request != null &&
(
startsWith(github.event.comment.body, '/vault-audit skip ') ||
github.event.comment.body == '/vault-audit' ||
startsWith(github.event.comment.body, '/vault-audit ')
)
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
statuses: write

steps:
- name: Check commenter authorization
id: auth
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
COMMENTER: ${{ github.event.comment.user.login }}
run: |
PERMISSION=$(gh api "/repos/${REPO}/collaborators/${COMMENTER}/permission" \
--jq '.permission' 2>/dev/null || echo "none")

if [[ "$PERMISSION" == "write" || "$PERMISSION" == "admin" ]]; then
echo "authorized=true" >> "$GITHUB_OUTPUT"
else
echo "authorized=false" >> "$GITHUB_OUTPUT"
fi

- name: Reject unauthorized commenter
if: steps.auth.outputs.authorized == 'false'
env:
GH_TOKEN: ${{ github.token }}
COMMENTER: ${{ github.event.comment.user.login }}
PR_NUMBER: ${{ github.event.issue.number }}
run: |
gh pr comment "$PR_NUMBER" \
--body "⛔ @${COMMENTER} — only authorized reviewers (repository write access) can use vault audit commands."
exit 1

- name: Get PR details
id: pr
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.issue.number }}
run: |
PR=$(gh api "/repos/${REPO}/pulls/${PR_NUMBER}" \
--jq '{head_sha: .head.sha, base_sha: .base.sha}')
echo "head_sha=$(echo "$PR" | jq -r '.head_sha')" >> "$GITHUB_OUTPUT"
echo "base_sha=$(echo "$PR" | jq -r '.base_sha')" >> "$GITHUB_OUTPUT"

- name: Parse command
id: cmd
env:
COMMENT_BODY: ${{ github.event.comment.body }}
run: |
if echo "$COMMENT_BODY" | grep -qP '^/vault-audit skip .+'; then
echo "type=skip" >> "$GITHUB_OUTPUT"
REASON="${COMMENT_BODY#/vault-audit skip }"
echo "reason=${REASON}" >> "$GITHUB_OUTPUT"
else
echo "type=run" >> "$GITHUB_OUTPUT"
fi

# ── /vault-audit skip <reason> ────────────────────────────────────────────
- name: Handle skip
if: steps.cmd.outputs.type == 'skip'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.issue.number }}
COMMENTER: ${{ github.event.comment.user.login }}
REASON: ${{ steps.cmd.outputs.reason }}
SHA: ${{ steps.pr.outputs.head_sha }}
run: |
gh api "/repos/${REPO}/statuses/${SHA}" \
--method POST \
-f state=success \
-f context="vault-audit" \
-f description="Vault audit skipped by ${COMMENTER}: ${REASON}"

gh pr comment "$PR_NUMBER" \
--body "✅ Vault audit skipped by @${COMMENTER}

**Reason:** ${REASON}

> ⚠️ This skip applies to the current HEAD (\`${SHA:0:8}\`). Pushing new vault file changes will re-require an audit."

# ── /vault-audit ─────────────────────────────────────────────────────────
- name: Get GATI token for cre-docs dispatch
id: gati
if: steps.cmd.outputs.type == 'run'
uses: smartcontractkit/chainlink-github-actions/apps/setup-github-token@main
with:
app-id: ${{ secrets.GATI_APP_ID }}
private-key: ${{ secrets.GATI_PRIVATE_KEY }}
# Needs contents:write on cre-docs to trigger repository_dispatch
repositories: cre-docs

- name: Set commit status pending
if: steps.cmd.outputs.type == 'run'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
SHA: ${{ steps.pr.outputs.head_sha }}
run: |
gh api "/repos/${REPO}/statuses/${SHA}" \
--method POST \
-f state=pending \
-f context="vault-audit" \
-f description="Vault audit queued..."

- name: Dispatch audit to cre-docs
if: steps.cmd.outputs.type == 'run'
env:
GH_TOKEN: ${{ steps.gati.outputs.token }}
PR_NUMBER: ${{ github.event.issue.number }}
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
BASE_SHA: ${{ steps.pr.outputs.base_sha }}
CHAINLINK_REPO: ${{ github.repository }}
run: |
gh api /repos/smartcontractkit/cre-docs/dispatches \
--method POST \
-f event_type=vault-audit \
-F 'client_payload[pr_number]'"=$PR_NUMBER" \
-f 'client_payload[head_sha]'"=$HEAD_SHA" \
-f 'client_payload[base_sha]'"=$BASE_SHA" \
-f 'client_payload[chainlink_repo]'"=$CHAINLINK_REPO"

- name: Acknowledge audit request
if: steps.cmd.outputs.type == 'run'
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.issue.number }}
COMMENTER: ${{ github.event.comment.user.login }}
run: |
gh pr comment "$PR_NUMBER" \
--body "🔍 Vault audit triggered by @${COMMENTER} — running now. Results will appear as a new comment when complete."
32 changes: 32 additions & 0 deletions .github/workflows/vault-audit-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Vault Audit Gate

# Posts a "pending" commit status whenever vault-related files are touched, requiring
# an authorized reviewer to trigger the audit before the PR can merge.

on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'core/capabilities/vault/**'
- 'core/services/ocr2/plugins/vault/**'
- 'core/services/gateway/handlers/vault/**'
- 'core/services/workflows/v2/secrets.go'
- 'system-tests/tests/smoke/cre/vault_don_test.go'
- 'system-tests/tests/smoke/cre/vault_don_test_helpers.go'
- 'system-tests/lib/cre/vault/**'

jobs:
gate:
runs-on: ubuntu-latest
permissions:
statuses: write
steps:
- name: Post pending status
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api /repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }} \
--method POST \
-f state=pending \
-f context="vault-audit" \
-f description="Vault audit required — an authorized reviewer must comment /vault-audit"
184 changes: 184 additions & 0 deletions .github/workflows/vault-audit-thread-commands.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
name: Vault Audit Thread Commands

# Handles /resolved replies on individual vault audit finding threads.
# When an authorized reviewer replies /resolved <reason> on a blocking finding thread,
# this workflow checks whether all blocking findings are now resolved and updates
# the commit status accordingly.

on:
pull_request_review_comment:
types: [created]

jobs:
handle-resolved:
if: startsWith(github.event.comment.body, '/resolved')
runs-on: ubuntu-latest
permissions:
pull-requests: write
statuses: write

steps:
- name: Check commenter authorization
id: auth
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
COMMENTER: ${{ github.event.comment.user.login }}
run: |
PERMISSION=$(gh api "/repos/${REPO}/collaborators/${COMMENTER}/permission" \
--jq '.permission' 2>/dev/null || echo "none")
if [[ "$PERMISSION" == "write" || "$PERMISSION" == "admin" ]]; then
echo "authorized=true" >> "$GITHUB_OUTPUT"
else
echo "authorized=false" >> "$GITHUB_OUTPUT"
fi

- name: Skip if unauthorized
if: steps.auth.outputs.authorized == 'false'
run: |
echo "Commenter is not authorized — ignoring /resolved."
exit 0

- name: Find root comment of this thread
id: thread
if: steps.auth.outputs.authorized == 'true'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
IN_REPLY_TO: ${{ github.event.comment.in_reply_to_id }}
run: |
# If this isn't a reply at all, it's not a thread response — ignore
if [ -z "$IN_REPLY_TO" ] || [ "$IN_REPLY_TO" = "0" ] || [ "$IN_REPLY_TO" = "null" ]; then
echo "Comment is not a reply — ignoring."
echo "root_id=" >> "$GITHUB_OUTPUT"
exit 0
fi

# Walk up in_reply_to_id chain to find the root comment of the thread
CURRENT="${IN_REPLY_TO}"
while true; do
PARENT=$(gh api "/repos/${REPO}/pulls/comments/${CURRENT}" \
--jq '.in_reply_to_id // empty' 2>/dev/null || echo "")
if [ -z "$PARENT" ] || [ "$PARENT" = "0" ] || [ "$PARENT" = "null" ]; then
break
fi
CURRENT="$PARENT"
done

echo "root_id=${CURRENT}" >> "$GITHUB_OUTPUT"

- name: Find vault audit meta comment
id: meta
if: steps.thread.outputs.root_id != ''
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
COMMENT=$(gh api "/repos/${REPO}/issues/${PR_NUMBER}/comments" \
--jq '[.[] | select(.body | contains("vault-audit-meta"))] | last')

if [ -z "$COMMENT" ] || [ "$COMMENT" = "null" ]; then
echo "No vault audit comment found on this PR — ignoring."
echo "found=false" >> "$GITHUB_OUTPUT"
exit 0
fi

BODY=$(echo "$COMMENT" | jq -r '.body')
SHA=$(echo "$BODY" | grep -oP '(?<=sha: )[a-f0-9]+')
THREAD_MAP=$(echo "$BODY" | grep -oP '(?<=thread_map: ).+')

echo "found=true" >> "$GITHUB_OUTPUT"
echo "sha=${SHA}" >> "$GITHUB_OUTPUT"
echo "thread_map=${THREAD_MAP}" >> "$GITHUB_OUTPUT"

- name: Check if this thread is a known blocking finding
id: finding
if: steps.meta.outputs.found == 'true'
env:
THREAD_MAP: ${{ steps.meta.outputs.thread_map }}
ROOT_ID: ${{ steps.thread.outputs.root_id }}
run: |
FINDING_ID=$(echo "$THREAD_MAP" | jq -r \
--argjson rid "$ROOT_ID" \
'[.[] | select(.comment_id == $rid)] | first | .finding_id // empty')

if [ -z "$FINDING_ID" ]; then
echo "This thread is not a vault audit blocking finding — ignoring."
echo "is_finding=false" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "is_finding=true" >> "$GITHUB_OUTPUT"
echo "finding_id=${FINDING_ID}" >> "$GITHUB_OUTPUT"

- name: Check whether all blocking findings are resolved
id: check_all
if: steps.finding.outputs.is_finding == 'true'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
THREAD_MAP: ${{ steps.meta.outputs.thread_map }}
run: |
ALL_COMMENTS=$(gh api "/repos/${REPO}/pulls/${PR_NUMBER}/comments" --jq '.')

ALL_RESOLVED=true
UNRESOLVED_IDS=""
TOTAL=0
RESOLVED_COUNT=0

while IFS= read -r entry; do
COMMENT_ID=$(echo "$entry" | jq -r '.comment_id')
FINDING_ID=$(echo "$entry" | jq -r '.finding_id')
TOTAL=$((TOTAL + 1))

HAS_RESOLVED=$(echo "$ALL_COMMENTS" | jq \
--argjson cid "$COMMENT_ID" \
'[.[] | select(.in_reply_to_id == $cid) | select(.body | startswith("/resolved"))] | length')

if [ "$HAS_RESOLVED" -gt 0 ]; then
RESOLVED_COUNT=$((RESOLVED_COUNT + 1))
else
ALL_RESOLVED=false
UNRESOLVED_IDS="${UNRESOLVED_IDS} ${FINDING_ID}"
fi
done < <(echo "$THREAD_MAP" | jq -c '.[]')

echo "all_resolved=${ALL_RESOLVED}" >> "$GITHUB_OUTPUT"
echo "resolved_count=${RESOLVED_COUNT}" >> "$GITHUB_OUTPUT"
echo "total=${TOTAL}" >> "$GITHUB_OUTPUT"
echo "unresolved_ids=${UNRESOLVED_IDS}" >> "$GITHUB_OUTPUT"

- name: Update commit status
if: steps.finding.outputs.is_finding == 'true'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
SHA: ${{ steps.meta.outputs.sha }}
ALL_RESOLVED: ${{ steps.check_all.outputs.all_resolved }}
RESOLVED_COUNT: ${{ steps.check_all.outputs.resolved_count }}
TOTAL: ${{ steps.check_all.outputs.total }}
UNRESOLVED_IDS: ${{ steps.check_all.outputs.unresolved_ids }}
COMMENTER: ${{ github.event.comment.user.login }}
FINDING_ID: ${{ steps.finding.outputs.finding_id }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
if [ "$ALL_RESOLVED" = "true" ]; then
gh api "/repos/${REPO}/statuses/${SHA}" \
--method POST \
-f state=success \
-f context="vault-audit" \
-f description="Vault audit: all ${TOTAL} finding(s) resolved by reviewers"
else
gh api "/repos/${REPO}/statuses/${SHA}" \
--method POST \
-f state=failure \
-f context="vault-audit" \
-f description="Vault audit: ${RESOLVED_COUNT}/${TOTAL} finding(s) resolved — outstanding:${UNRESOLVED_IDS}"

gh pr comment "$PR_NUMBER" \
--body "✅ **${FINDING_ID}** marked as resolved by @${COMMENTER} (${RESOLVED_COUNT}/${TOTAL} findings resolved).

Remaining: \`${UNRESOLVED_IDS}\` — reply \`/resolved <reason>\` directly on each finding thread to unblock merge."
fi
Loading