Skip to content

Commit f9511ec

Browse files
committed
fix(ci): authorize generated PR checks
Signed-off-by: Andre Manoel <amanoel@nvidia.com>
1 parent 646c9a2 commit f9511ec

2 files changed

Lines changed: 261 additions & 0 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
name: "Agentic CI Authorization Checks"
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
pr_number:
7+
description: "Agentic CI PR number"
8+
required: true
9+
type: string
10+
expected_head_sha:
11+
description: "PR head SHA authorized by the maintainer"
12+
required: true
13+
type: string
14+
15+
permissions:
16+
contents: read
17+
pull-requests: read
18+
19+
defaults:
20+
run:
21+
shell: bash
22+
23+
jobs:
24+
pr:
25+
runs-on: ubuntu-latest
26+
outputs:
27+
title_b64: ${{ steps.metadata.outputs.title_b64 }}
28+
trusted: ${{ steps.metadata.outputs.trusted }}
29+
steps:
30+
- name: Load PR metadata
31+
id: metadata
32+
env:
33+
EXPECTED_HEAD_SHA: ${{ inputs.expected_head_sha }}
34+
GH_TOKEN: ${{ github.token }}
35+
PR_NUMBER: ${{ inputs.pr_number }}
36+
REPO: ${{ github.repository }}
37+
run: |
38+
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
39+
echo "::error::Invalid PR number: ${PR_NUMBER}"
40+
exit 1
41+
fi
42+
43+
PR_JSON=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}")
44+
PR_AUTHOR=$(printf '%s' "$PR_JSON" | jq -r '.user.login')
45+
HEAD_REPO=$(printf '%s' "$PR_JSON" | jq -r '.head.repo.full_name')
46+
HEAD_REF=$(printf '%s' "$PR_JSON" | jq -r '.head.ref')
47+
HEAD_SHA=$(printf '%s' "$PR_JSON" | jq -r '.head.sha')
48+
TITLE=$(printf '%s' "$PR_JSON" | jq -r '.title')
49+
PR_BODY=$(printf '%s' "$PR_JSON" | jq -r '.body // ""')
50+
51+
if [ "$HEAD_SHA" != "$EXPECTED_HEAD_SHA" ]; then
52+
echo "::error::PR head moved from ${EXPECTED_HEAD_SHA} to ${HEAD_SHA}."
53+
exit 1
54+
fi
55+
56+
if [ "$GITHUB_SHA" != "$HEAD_SHA" ]; then
57+
echo "::error::Workflow SHA ${GITHUB_SHA} does not match PR head ${HEAD_SHA}."
58+
exit 1
59+
fi
60+
61+
TRUSTED=false
62+
printf '%s' "$PR_BODY" > /tmp/pr-body-raw.txt
63+
# Commit authors can be spoofed; trust only PR metadata GitHub controls.
64+
if { [ "$PR_AUTHOR" = "github-actions[bot]" ] || [ "$PR_AUTHOR" = "agentic-ci" ]; } && \
65+
[ "$HEAD_REPO" = "$REPO" ] && \
66+
[[ "$HEAD_REF" == agentic-ci/* ]] && \
67+
grep -q '<!-- agentic-ci ' /tmp/pr-body-raw.txt; then
68+
TRUSTED=true
69+
fi
70+
71+
echo "trusted=${TRUSTED}" >> "$GITHUB_OUTPUT"
72+
echo "title_b64=$(printf '%s' "$TITLE" | base64 -w0)" >> "$GITHUB_OUTPUT"
73+
74+
DCOAssistant:
75+
needs: pr
76+
if: always()
77+
runs-on: ubuntu-latest
78+
steps:
79+
- name: Validate authorization
80+
env:
81+
PR_RESULT: ${{ needs.pr.result }}
82+
TRUSTED: ${{ needs.pr.outputs.trusted }}
83+
run: |
84+
if [ "$PR_RESULT" != "success" ] || [ "$TRUSTED" != "true" ]; then
85+
echo "::error::This PR is not an authorized Agentic CI PR."
86+
exit 1
87+
fi
88+
echo "Trusted Agentic CI PR authorized by a maintainer."
89+
90+
semantic-pull-request:
91+
name: semantic-pull-request / semantic-pull-request
92+
needs: pr
93+
if: always()
94+
runs-on: ubuntu-latest
95+
steps:
96+
- name: Validate PR title
97+
env:
98+
PR_RESULT: ${{ needs.pr.result }}
99+
TITLE_B64: ${{ needs.pr.outputs.title_b64 }}
100+
TRUSTED: ${{ needs.pr.outputs.trusted }}
101+
run: |
102+
if [ "$PR_RESULT" != "success" ] || [ "$TRUSTED" != "true" ]; then
103+
echo "::error::This PR is not an authorized Agentic CI PR."
104+
exit 1
105+
fi
106+
107+
TITLE=$(printf '%s' "$TITLE_B64" | base64 -d)
108+
TYPES='feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|cp'
109+
REGEX="^(${TYPES})(\\([^)]+\\))?!?: .+"
110+
if ! [[ "$TITLE" =~ $REGEX ]]; then
111+
echo "::error::PR title is not semantic: ${TITLE}"
112+
exit 1
113+
fi
114+
115+
if [ "${#TITLE}" -gt 80 ]; then
116+
echo "::error::PR title is longer than 80 characters: ${#TITLE}"
117+
exit 1
118+
fi
119+
120+
check:
121+
needs: pr
122+
if: always()
123+
runs-on: ubuntu-latest
124+
steps:
125+
- name: Validate linked issue authorization
126+
env:
127+
PR_RESULT: ${{ needs.pr.result }}
128+
TRUSTED: ${{ needs.pr.outputs.trusted }}
129+
run: |
130+
if [ "$PR_RESULT" != "success" ] || [ "$TRUSTED" != "true" ]; then
131+
echo "::error::This PR is not an authorized Agentic CI PR."
132+
exit 1
133+
fi
134+
echo "Trusted Agentic CI PRs do not require a linked issue."
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
name: "Authorize Agentic CI"
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
7+
permissions:
8+
actions: write
9+
contents: read
10+
issues: write
11+
pull-requests: read
12+
13+
defaults:
14+
run:
15+
shell: bash
16+
17+
jobs:
18+
authorize:
19+
if: >-
20+
github.repository_owner == 'NVIDIA-NeMo'
21+
&& github.event.issue.pull_request != null
22+
&& github.event.comment.body == '/authorize-agentic-ci'
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Check commenter permission
26+
env:
27+
GH_TOKEN: ${{ github.token }}
28+
COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
29+
PR_NUMBER: ${{ github.event.issue.number }}
30+
REPO: ${{ github.repository }}
31+
run: |
32+
PERMISSION=$(gh api "repos/${REPO}/collaborators/${COMMENT_AUTHOR}/permission" \
33+
--jq '.permission' 2>/dev/null || echo "none")
34+
echo "Comment author ${COMMENT_AUTHOR} has ${PERMISSION} permission."
35+
36+
case "$PERMISSION" in
37+
admin|maintain|write)
38+
;;
39+
*)
40+
gh issue comment "$PR_NUMBER" --repo "$REPO" --body \
41+
"Only maintainers with write access can authorize Agentic CI checks."
42+
exit 1
43+
;;
44+
esac
45+
46+
- name: Load PR metadata
47+
id: pr
48+
env:
49+
GH_TOKEN: ${{ github.token }}
50+
PR_NUMBER: ${{ github.event.issue.number }}
51+
REPO: ${{ github.repository }}
52+
run: |
53+
PR_JSON=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}")
54+
55+
PR_AUTHOR=$(printf '%s' "$PR_JSON" | jq -r '.user.login')
56+
HEAD_REPO=$(printf '%s' "$PR_JSON" | jq -r '.head.repo.full_name')
57+
HEAD_REF=$(printf '%s' "$PR_JSON" | jq -r '.head.ref')
58+
HEAD_SHA=$(printf '%s' "$PR_JSON" | jq -r '.head.sha')
59+
STATE=$(printf '%s' "$PR_JSON" | jq -r '.state')
60+
PR_BODY=$(printf '%s' "$PR_JSON" | jq -r '.body // ""')
61+
62+
TRUSTED=false
63+
printf '%s' "$PR_BODY" > /tmp/pr-body-raw.txt
64+
# Commit authors can be spoofed; trust only PR metadata GitHub controls.
65+
if { [ "$PR_AUTHOR" = "github-actions[bot]" ] || [ "$PR_AUTHOR" = "agentic-ci" ]; } && \
66+
[ "$HEAD_REPO" = "$REPO" ] && \
67+
[[ "$HEAD_REF" == agentic-ci/* ]] && \
68+
grep -q '<!-- agentic-ci ' /tmp/pr-body-raw.txt; then
69+
TRUSTED=true
70+
fi
71+
72+
echo "author=${PR_AUTHOR}" >> "$GITHUB_OUTPUT"
73+
echo "head_ref=${HEAD_REF}" >> "$GITHUB_OUTPUT"
74+
echo "head_sha=${HEAD_SHA}" >> "$GITHUB_OUTPUT"
75+
echo "state=${STATE}" >> "$GITHUB_OUTPUT"
76+
echo "trusted=${TRUSTED}" >> "$GITHUB_OUTPUT"
77+
78+
- name: Validate Agentic CI PR
79+
env:
80+
GH_TOKEN: ${{ github.token }}
81+
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
82+
PR_NUMBER: ${{ github.event.issue.number }}
83+
REPO: ${{ github.repository }}
84+
STATE: ${{ steps.pr.outputs.state }}
85+
TRUSTED: ${{ steps.pr.outputs.trusted }}
86+
run: |
87+
if [ "$STATE" != "open" ]; then
88+
gh issue comment "$PR_NUMBER" --repo "$REPO" --body \
89+
"Agentic CI checks were not authorized because this PR is not open."
90+
exit 1
91+
fi
92+
93+
if [ "$TRUSTED" != "true" ]; then
94+
gh issue comment "$PR_NUMBER" --repo "$REPO" --body \
95+
"Agentic CI checks were not authorized because this PR does not match the trusted Agentic CI metadata."
96+
exit 1
97+
fi
98+
99+
BLOCKED=$(gh pr diff "$PR_NUMBER" --repo "$REPO" --name-only \
100+
| grep -E '^\.github/(workflows|actions|scripts)/' || true)
101+
if [ -n "$BLOCKED" ]; then
102+
{
103+
echo "Agentic CI checks were not authorized because this PR changes privileged workflow files:"
104+
echo
105+
printf '%s\n' "$BLOCKED" | sed 's/^/- `/' | sed 's/$/`/'
106+
} > /tmp/agentic-ci-auth-failed.md
107+
gh issue comment "$PR_NUMBER" --repo "$REPO" --body-file /tmp/agentic-ci-auth-failed.md
108+
exit 1
109+
fi
110+
111+
echo "Authorizing checks for ${HEAD_SHA}."
112+
113+
- name: Dispatch checks
114+
env:
115+
GH_TOKEN: ${{ github.token }}
116+
HEAD_REF: ${{ steps.pr.outputs.head_ref }}
117+
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
118+
PR_NUMBER: ${{ github.event.issue.number }}
119+
REPO: ${{ github.repository }}
120+
run: |
121+
gh workflow run ci.yml --repo "$REPO" --ref "$HEAD_REF"
122+
gh workflow run agentic-ci-authorized-checks.yml --repo "$REPO" --ref "$HEAD_REF" \
123+
-f pr_number="$PR_NUMBER" \
124+
-f expected_head_sha="$HEAD_SHA"
125+
126+
gh issue comment "$PR_NUMBER" --repo "$REPO" --body \
127+
"Authorized Agentic CI checks for \`${HEAD_SHA}\`. Launched CI and authorization checks."

0 commit comments

Comments
 (0)