Skip to content
Merged
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
142 changes: 142 additions & 0 deletions .github/workflows/agentic-ci-authorized-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
name: "Agentic CI Authorization Checks"

on:
workflow_dispatch:
inputs:
pr_number:
description: "Agentic CI PR number"
required: true
type: string
expected_head_sha:
description: "PR head SHA authorized by the maintainer"
required: true
type: string

permissions:
contents: read
pull-requests: read

concurrency:
group: agentic-ci-authorized-checks-${{ inputs.pr_number }}
cancel-in-progress: true

defaults:
run:
shell: bash

jobs:
pr:
timeout-minutes: 5
runs-on: ubuntu-latest
outputs:
title_b64: ${{ steps.metadata.outputs.title_b64 }}
trusted: ${{ steps.metadata.outputs.trusted }}
steps:
- name: Load PR metadata
id: metadata
env:
EXPECTED_HEAD_SHA: ${{ inputs.expected_head_sha }}
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ inputs.pr_number }}
REPO: ${{ github.repository }}
run: |
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::Invalid PR number: ${PR_NUMBER}"
exit 1
fi

PR_JSON=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}")
PR_AUTHOR=$(printf '%s' "$PR_JSON" | jq -r '.user.login')
HEAD_REPO=$(printf '%s' "$PR_JSON" | jq -r '.head.repo.full_name')
HEAD_REF=$(printf '%s' "$PR_JSON" | jq -r '.head.ref')
HEAD_SHA=$(printf '%s' "$PR_JSON" | jq -r '.head.sha')
TITLE=$(printf '%s' "$PR_JSON" | jq -r '.title')
PR_BODY=$(printf '%s' "$PR_JSON" | jq -r '.body // ""')

if [ "$HEAD_SHA" != "$EXPECTED_HEAD_SHA" ]; then
echo "::error::PR head moved from ${EXPECTED_HEAD_SHA} to ${HEAD_SHA}."
exit 1
fi

if [ "$GITHUB_SHA" != "$HEAD_SHA" ]; then
echo "::error::Workflow SHA ${GITHUB_SHA} does not match PR head ${HEAD_SHA}."
exit 1
fi

TRUSTED=false
printf '%s' "$PR_BODY" > /tmp/pr-body-raw.txt
# Commit authors can be spoofed; trust only PR metadata GitHub controls.
if [ "$PR_AUTHOR" = "github-actions[bot]" ] && \
[ "$HEAD_REPO" = "$REPO" ] && \
[[ "$HEAD_REF" == agentic-ci/* ]] && \
grep -Eq '<!-- agentic-ci finding=[^[:space:]]+ suite=[^[:space:]]+ -->' /tmp/pr-body-raw.txt; then
TRUSTED=true
fi

echo "trusted=${TRUSTED}" >> "$GITHUB_OUTPUT"
echo "title_b64=$(printf '%s' "$TITLE" | base64 -w0)" >> "$GITHUB_OUTPUT"

DCOAssistant:
needs: pr
if: always()
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Validate authorization
env:
PR_RESULT: ${{ needs.pr.result }}
TRUSTED: ${{ needs.pr.outputs.trusted }}
run: |
if [ "$PR_RESULT" != "success" ] || [ "$TRUSTED" != "true" ]; then
echo "::error::This PR is not an authorized Agentic CI PR."
exit 1
fi
echo "Trusted Agentic CI PR authorized by a maintainer."

semantic-pull-request:
name: semantic-pull-request / semantic-pull-request
needs: pr
if: always()
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Validate PR title
env:
PR_RESULT: ${{ needs.pr.result }}
TITLE_B64: ${{ needs.pr.outputs.title_b64 }}
TRUSTED: ${{ needs.pr.outputs.trusted }}
run: |
if [ "$PR_RESULT" != "success" ] || [ "$TRUSTED" != "true" ]; then
echo "::error::This PR is not an authorized Agentic CI PR."
exit 1
fi

TITLE=$(printf '%s' "$TITLE_B64" | base64 -d)
TYPES='feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|cp'
REGEX="^(${TYPES})(\\([^)]+\\))?!?: .+"
if ! [[ "$TITLE" =~ $REGEX ]]; then
echo "::error::PR title is not semantic: ${TITLE}"
exit 1
fi

if [ "${#TITLE}" -gt 80 ]; then
echo "::error::PR title is longer than 80 characters: ${#TITLE}"
exit 1
fi

check:
Comment thread
andreatgretel marked this conversation as resolved.
needs: pr
if: always()
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Validate linked issue authorization
env:
PR_RESULT: ${{ needs.pr.result }}
TRUSTED: ${{ needs.pr.outputs.trusted }}
run: |
if [ "$PR_RESULT" != "success" ] || [ "$TRUSTED" != "true" ]; then
echo "::error::This PR is not an authorized Agentic CI PR."
exit 1
fi
echo "Trusted Agentic CI PRs do not require a linked issue."
179 changes: 179 additions & 0 deletions .github/workflows/authorize-agentic-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
name: "Authorize Agentic CI"

on:
issue_comment:
types: [created]

permissions:
actions: write
contents: read
issues: write
pull-requests: read

defaults:
run:
shell: bash

concurrency:
group: authorize-agentic-ci-${{ github.event.issue.number }}
cancel-in-progress: false

jobs:
authorize:
Comment thread
andreatgretel marked this conversation as resolved.
if: >-
github.repository_owner == 'NVIDIA-NeMo'
&& github.event.issue.pull_request != null
&& github.event.comment.body == '/authorize-agentic-ci'
runs-on: ubuntu-latest
steps:
- name: Check commenter permission
env:
GH_TOKEN: ${{ github.token }}
COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
PR_NUMBER: ${{ github.event.issue.number }}
REPO: ${{ github.repository }}
run: |
PERMISSION=$(gh api "repos/${REPO}/collaborators/${COMMENT_AUTHOR}/permission" \
--jq '.permission' 2>/dev/null || echo "none")
echo "Comment author ${COMMENT_AUTHOR} has ${PERMISSION} permission."

case "$PERMISSION" in
admin|maintain|write)
;;
*)
gh issue comment "$PR_NUMBER" --repo "$REPO" --body \
"Only maintainers with write access can authorize Agentic CI checks."
exit 1
;;
esac

- name: Load PR metadata
id: pr
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.issue.number }}
REPO: ${{ github.repository }}
run: |
PR_JSON=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}")

PR_AUTHOR=$(printf '%s' "$PR_JSON" | jq -r '.user.login')
HEAD_REPO=$(printf '%s' "$PR_JSON" | jq -r '.head.repo.full_name')
HEAD_REF=$(printf '%s' "$PR_JSON" | jq -r '.head.ref')
HEAD_SHA=$(printf '%s' "$PR_JSON" | jq -r '.head.sha')
STATE=$(printf '%s' "$PR_JSON" | jq -r '.state')
PR_BODY=$(printf '%s' "$PR_JSON" | jq -r '.body // ""')

TRUSTED=false
printf '%s' "$PR_BODY" > /tmp/pr-body-raw.txt
# Commit authors can be spoofed; trust only PR metadata GitHub controls.
if [ "$PR_AUTHOR" = "github-actions[bot]" ] && \
[ "$HEAD_REPO" = "$REPO" ] && \
[[ "$HEAD_REF" == agentic-ci/* ]] && \
grep -Eq '<!-- agentic-ci finding=[^[:space:]]+ suite=[^[:space:]]+ -->' /tmp/pr-body-raw.txt; then
TRUSTED=true
fi

echo "author=${PR_AUTHOR}" >> "$GITHUB_OUTPUT"
echo "head_ref=${HEAD_REF}" >> "$GITHUB_OUTPUT"
echo "head_sha=${HEAD_SHA}" >> "$GITHUB_OUTPUT"
echo "state=${STATE}" >> "$GITHUB_OUTPUT"
echo "trusted=${TRUSTED}" >> "$GITHUB_OUTPUT"

- name: Validate Agentic CI PR
env:
COMMENT_ID: ${{ github.event.comment.id }}
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
PR_NUMBER: ${{ github.event.issue.number }}
REPO: ${{ github.repository }}
STATE: ${{ steps.pr.outputs.state }}
TRUSTED: ${{ steps.pr.outputs.trusted }}
run: |
if [ "$STATE" != "open" ]; then
gh issue comment "$PR_NUMBER" --repo "$REPO" --body \
"Agentic CI checks were not authorized because this PR is not open."
exit 1
fi

if [ "$TRUSTED" != "true" ]; then
gh issue comment "$PR_NUMBER" --repo "$REPO" --body \
"Agentic CI checks were not authorized because this PR does not match the trusted Agentic CI metadata."
exit 1
fi

if [ -z "$COMMENT_ID" ]; then
gh issue comment "$PR_NUMBER" --repo "$REPO" --body \
"Agentic CI checks were not authorized because the authorization comment ID was missing."
exit 1
fi

COMMENT_FOUND=false
for ATTEMPT in 1 2 3; do
gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/timeline?per_page=100" \
-H "Accept: application/vnd.github+json" \
--jq '.[] | [.event, ((.id // .sha // "") | tostring)] | @tsv' > /tmp/pr-timeline.tsv
if awk -F '\t' -v comment_id="$COMMENT_ID" '
$1 == "commented" && $2 == comment_id { found = 1 }
END { exit found ? 0 : 1 }
' /tmp/pr-timeline.tsv; then
COMMENT_FOUND=true
break
fi
sleep 2
done
if [ "$COMMENT_FOUND" != "true" ]; then
gh issue comment "$PR_NUMBER" --repo "$REPO" --body \
"Agentic CI checks were not authorized because the authorization comment was not found in the PR timeline."
exit 1
fi

HEAD_EVENT_AFTER_COMMENT=$(awk -F '\t' -v comment_id="$COMMENT_ID" '
$1 == "commented" && $2 == comment_id { seen_comment = 1; next }
seen_comment && ($1 == "committed" || $1 == "head_ref_force_pushed" || $1 == "head_ref_deleted" || $1 == "head_ref_restored") {
print $1 " " $2
exit
}
' /tmp/pr-timeline.tsv)
if [ -n "$HEAD_EVENT_AFTER_COMMENT" ]; then
{
echo "Agentic CI checks were not authorized because the PR head changed after the authorization comment."
echo
echo "Latest PR head: \`${HEAD_SHA}\`"
echo "Detected update: \`${HEAD_EVENT_AFTER_COMMENT}\`"
echo
echo "Please review the latest commit and comment \`/authorize-agentic-ci\` again."
} > /tmp/agentic-ci-auth-stale.md
gh issue comment "$PR_NUMBER" --repo "$REPO" --body-file /tmp/agentic-ci-auth-stale.md
exit 1
fi

BLOCKED=$(gh pr diff "$PR_NUMBER" --repo "$REPO" --name-only \
| grep -E '^\.github/' || true)
if [ -n "$BLOCKED" ]; then
{
echo "Agentic CI checks were not authorized because this PR changes privileged repository files:"
echo
printf '%s\n' "$BLOCKED" | sed 's/^/- `/' | sed 's/$/`/'
} > /tmp/agentic-ci-auth-failed.md
gh issue comment "$PR_NUMBER" --repo "$REPO" --body-file /tmp/agentic-ci-auth-failed.md
exit 1
fi

echo "Authorizing checks for ${HEAD_SHA}."

- name: Dispatch checks
env:
GH_TOKEN: ${{ github.token }}
HEAD_REF: ${{ steps.pr.outputs.head_ref }}
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
PR_NUMBER: ${{ github.event.issue.number }}
REPO: ${{ github.repository }}
run: |
gh workflow run ci.yml --repo "$REPO" --ref "$HEAD_REF" \
-f expected_head_sha="$HEAD_SHA"
gh workflow run agentic-ci-authorized-checks.yml --repo "$REPO" --ref "$HEAD_REF" \
-f pr_number="$PR_NUMBER" \
-f expected_head_sha="$HEAD_SHA"

gh issue comment "$PR_NUMBER" --repo "$REPO" --body \
"Authorized Agentic CI checks for \`${HEAD_SHA}\`. Launched CI and authorization checks."
Loading
Loading