From 646c9a22f588693e516c7d9912ff95f69ca9a09c Mon Sep 17 00:00:00 2001 From: Andre Manoel Date: Wed, 13 May 2026 16:45:57 +0000 Subject: [PATCH 1/6] fix(ci): trust generated agentic CI PRs Signed-off-by: Andre Manoel --- .github/workflows/dco-assistant.yml | 39 ++++++++++++++++++++++++++- .github/workflows/pr-linked-issue.yml | 14 ++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dco-assistant.yml b/.github/workflows/dco-assistant.yml index dddb19bec..13e162229 100644 --- a/.github/workflows/dco-assistant.yml +++ b/.github/workflows/dco-assistant.yml @@ -25,8 +25,45 @@ jobs: if: github.repository_owner == 'NVIDIA-NeMo' runs-on: ubuntu-latest steps: + - name: Check trusted Agentic CI PR + id: trusted-agentic-ci + env: + GH_TOKEN: ${{ github.token }} + EVENT_NAME: ${{ github.event_name }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} + PR_BODY: ${{ github.event.pull_request.body }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + REPO: ${{ github.repository }} + run: | + TRUSTED=false + + if [ "$EVENT_NAME" = "issue_comment" ] && [ -n "$ISSUE_NUMBER" ]; then + PR_JSON=$(gh api "repos/${REPO}/pulls/${ISSUE_NUMBER}" 2>/dev/null || true) + if [ -n "$PR_JSON" ]; then + 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') + PR_BODY=$(printf '%s' "$PR_JSON" | jq -r '.body // ""') + fi + fi + + 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]" ] || [ "$PR_AUTHOR" = "agentic-ci" ]; } && \ + [ "$HEAD_REPO" = "$REPO" ] && \ + [[ "$HEAD_REF" == agentic-ci/* ]] && \ + grep -q '' /tmp/pr-body-raw.txt; then TRUSTED=true fi @@ -74,6 +79,7 @@ jobs: DCOAssistant: needs: pr if: always() + timeout-minutes: 5 runs-on: ubuntu-latest steps: - name: Validate authorization @@ -91,6 +97,7 @@ jobs: name: semantic-pull-request / semantic-pull-request needs: pr if: always() + timeout-minutes: 5 runs-on: ubuntu-latest steps: - name: Validate PR title @@ -120,6 +127,7 @@ jobs: check: needs: pr if: always() + timeout-minutes: 5 runs-on: ubuntu-latest steps: - name: Validate linked issue authorization diff --git a/.github/workflows/authorize-agentic-ci.yml b/.github/workflows/authorize-agentic-ci.yml index aab38b04b..70ebcc758 100644 --- a/.github/workflows/authorize-agentic-ci.yml +++ b/.github/workflows/authorize-agentic-ci.yml @@ -65,7 +65,7 @@ jobs: if { [ "$PR_AUTHOR" = "github-actions[bot]" ] || [ "$PR_AUTHOR" = "agentic-ci" ]; } && \ [ "$HEAD_REPO" = "$REPO" ] && \ [[ "$HEAD_REF" == agentic-ci/* ]] && \ - grep -q '' /tmp/pr-body-raw.txt; then TRUSTED=true fi @@ -97,10 +97,10 @@ jobs: fi BLOCKED=$(gh pr diff "$PR_NUMBER" --repo "$REPO" --name-only \ - | grep -E '^\.github/(workflows|actions|scripts)/' || true) + | grep -E '^\.github/' || true) if [ -n "$BLOCKED" ]; then { - echo "Agentic CI checks were not authorized because this PR changes privileged workflow files:" + 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 @@ -118,7 +118,8 @@ jobs: PR_NUMBER: ${{ github.event.issue.number }} REPO: ${{ github.repository }} run: | - gh workflow run ci.yml --repo "$REPO" --ref "$HEAD_REF" + 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" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 312bd1a32..428484e50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,10 +6,29 @@ on: pull_request: branches: [ main ] workflow_dispatch: + inputs: + expected_head_sha: + description: "Optional head SHA that this dispatched run must execute" + required: false + type: string permissions: {} jobs: + validate-dispatch: + name: Validate dispatched SHA + runs-on: ubuntu-latest + steps: + - name: Check expected SHA + env: + EXPECTED_HEAD_SHA: ${{ inputs.expected_head_sha }} + run: | + if [ -n "$EXPECTED_HEAD_SHA" ] && [ "$GITHUB_SHA" != "$EXPECTED_HEAD_SHA" ]; then + echo "::error::Workflow SHA ${GITHUB_SHA} does not match expected ${EXPECTED_HEAD_SHA}." + exit 1 + fi + echo "Dispatch target SHA validated." + # =========================================================================== # Independent Package Tests # Each package is tested in isolation to ensure proper dependency boundaries @@ -17,6 +36,7 @@ jobs: test-config: name: Test Config (Python ${{ matrix.python-version }} on ${{ matrix.os }}) + needs: validate-dispatch runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -48,6 +68,7 @@ jobs: test-engine: name: Test Engine (Python ${{ matrix.python-version }} on ${{ matrix.os }}) + needs: validate-dispatch runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -79,6 +100,7 @@ jobs: test-interface: name: Test Interface (Python ${{ matrix.python-version }} on ${{ matrix.os }}) + needs: validate-dispatch runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -119,6 +141,7 @@ jobs: coverage: name: Coverage Check (Python ${{ matrix.python-version }}) + needs: validate-dispatch runs-on: ubuntu-latest strategy: fail-fast: false @@ -156,6 +179,7 @@ jobs: test-e2e: name: End to end test (Python ${{ matrix.python-version }} on ${{ matrix.os }}) + needs: validate-dispatch runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -183,6 +207,7 @@ jobs: lint: name: Lint and Format Check + needs: validate-dispatch runs-on: ubuntu-latest steps: @@ -207,6 +232,7 @@ jobs: license-headers: name: Check License Headers + needs: validate-dispatch runs-on: ubuntu-latest steps: @@ -237,7 +263,7 @@ jobs: test-summary: name: Test (Python ${{ matrix.python-version }} on ${{ matrix.os }}) runs-on: ubuntu-latest - needs: [test-config, test-engine, test-interface] + needs: [validate-dispatch, test-config, test-engine, test-interface] if: always() strategy: matrix: @@ -247,10 +273,12 @@ jobs: steps: - name: Check all tests passed run: | - if [[ "${{ needs.test-config.result }}" != "success" ]] || \ + if [[ "${{ needs.validate-dispatch.result }}" != "success" ]] || \ + [[ "${{ needs.test-config.result }}" != "success" ]] || \ [[ "${{ needs.test-engine.result }}" != "success" ]] || \ [[ "${{ needs.test-interface.result }}" != "success" ]]; then echo "One or more test jobs failed" + echo "validate-dispatch: ${{ needs.validate-dispatch.result }}" echo "test-config: ${{ needs.test-config.result }}" echo "test-engine: ${{ needs.test-engine.result }}" echo "test-interface: ${{ needs.test-interface.result }}" diff --git a/.github/workflows/dco-assistant.yml b/.github/workflows/dco-assistant.yml index 13e162229..39b727eab 100644 --- a/.github/workflows/dco-assistant.yml +++ b/.github/workflows/dco-assistant.yml @@ -54,7 +54,7 @@ jobs: if { [ "$PR_AUTHOR" = "github-actions[bot]" ] || [ "$PR_AUTHOR" = "agentic-ci" ]; } && \ [ "$HEAD_REPO" = "$REPO" ] && \ [[ "$HEAD_REF" == agentic-ci/* ]] && \ - grep -q '' /tmp/pr-body-raw.txt; then TRUSTED=true fi diff --git a/.github/workflows/pr-linked-issue.yml b/.github/workflows/pr-linked-issue.yml index d9bdd4093..fcbf3c884 100644 --- a/.github/workflows/pr-linked-issue.yml +++ b/.github/workflows/pr-linked-issue.yml @@ -48,7 +48,7 @@ jobs: if { [ "$USER" = "github-actions[bot]" ] || [ "$USER" = "agentic-ci" ]; } && \ [ "$HEAD_REPO" = "$REPO" ] && \ [[ "$HEAD_REF" == agentic-ci/* ]] && \ - grep -q '' /tmp/pr-body-raw.txt; then echo "is_collaborator=true" >> "$GITHUB_OUTPUT" exit 0 fi From eef56e0de7a47ac38e5f32e68773deaab70ad818 Mon Sep 17 00:00:00 2001 From: Andre Manoel Date: Thu, 14 May 2026 17:35:42 +0000 Subject: [PATCH 4/6] fix(ci): narrow agentic CI trust --- .github/workflows/agentic-ci-authorized-checks.yml | 2 +- .github/workflows/authorize-agentic-ci.yml | 2 +- .github/workflows/dco-assistant.yml | 2 +- .github/workflows/pr-linked-issue.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/agentic-ci-authorized-checks.yml b/.github/workflows/agentic-ci-authorized-checks.yml index 51943123b..0db32cf6f 100644 --- a/.github/workflows/agentic-ci-authorized-checks.yml +++ b/.github/workflows/agentic-ci-authorized-checks.yml @@ -66,7 +66,7 @@ jobs: 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]" ] || [ "$PR_AUTHOR" = "agentic-ci" ]; } && \ + if [ "$PR_AUTHOR" = "github-actions[bot]" ] && \ [ "$HEAD_REPO" = "$REPO" ] && \ [[ "$HEAD_REF" == agentic-ci/* ]] && \ grep -Eq '' /tmp/pr-body-raw.txt; then diff --git a/.github/workflows/authorize-agentic-ci.yml b/.github/workflows/authorize-agentic-ci.yml index 70ebcc758..407b67b21 100644 --- a/.github/workflows/authorize-agentic-ci.yml +++ b/.github/workflows/authorize-agentic-ci.yml @@ -62,7 +62,7 @@ jobs: 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]" ] || [ "$PR_AUTHOR" = "agentic-ci" ]; } && \ + if [ "$PR_AUTHOR" = "github-actions[bot]" ] && \ [ "$HEAD_REPO" = "$REPO" ] && \ [[ "$HEAD_REF" == agentic-ci/* ]] && \ grep -Eq '' /tmp/pr-body-raw.txt; then diff --git a/.github/workflows/dco-assistant.yml b/.github/workflows/dco-assistant.yml index 39b727eab..7c8cbb45c 100644 --- a/.github/workflows/dco-assistant.yml +++ b/.github/workflows/dco-assistant.yml @@ -51,7 +51,7 @@ jobs: 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]" ] || [ "$PR_AUTHOR" = "agentic-ci" ]; } && \ + if [ "$PR_AUTHOR" = "github-actions[bot]" ] && \ [ "$HEAD_REPO" = "$REPO" ] && \ [[ "$HEAD_REF" == agentic-ci/* ]] && \ grep -Eq '' /tmp/pr-body-raw.txt; then diff --git a/.github/workflows/pr-linked-issue.yml b/.github/workflows/pr-linked-issue.yml index fcbf3c884..5cf7833df 100644 --- a/.github/workflows/pr-linked-issue.yml +++ b/.github/workflows/pr-linked-issue.yml @@ -45,7 +45,7 @@ jobs: printf '%s' "$PR_BODY" > /tmp/pr-body-raw.txt # Commit authors can be spoofed; trust only PR metadata GitHub controls. - if { [ "$USER" = "github-actions[bot]" ] || [ "$USER" = "agentic-ci" ]; } && \ + if [ "$USER" = "github-actions[bot]" ] && \ [ "$HEAD_REPO" = "$REPO" ] && \ [[ "$HEAD_REF" == agentic-ci/* ]] && \ grep -Eq '' /tmp/pr-body-raw.txt; then From 04404df29bf3b694e30119a12e01004502e818d7 Mon Sep 17 00:00:00 2001 From: Andre Manoel Date: Thu, 14 May 2026 17:46:18 +0000 Subject: [PATCH 5/6] fix(ci): reject stale agentic authorizations --- .github/workflows/authorize-agentic-ci.yml | 47 ++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/.github/workflows/authorize-agentic-ci.yml b/.github/workflows/authorize-agentic-ci.yml index 407b67b21..57985d648 100644 --- a/.github/workflows/authorize-agentic-ci.yml +++ b/.github/workflows/authorize-agentic-ci.yml @@ -77,6 +77,7 @@ jobs: - 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 }} @@ -96,6 +97,52 @@ jobs: 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 From 1eb24b06a4e4db857bc79418e5209f12eb24b235 Mon Sep 17 00:00:00 2001 From: Andre Manoel Date: Thu, 14 May 2026 21:45:31 +0000 Subject: [PATCH 6/6] fix(ci): serialize agentic authorization --- .github/workflows/authorize-agentic-ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/authorize-agentic-ci.yml b/.github/workflows/authorize-agentic-ci.yml index 57985d648..6fa69648f 100644 --- a/.github/workflows/authorize-agentic-ci.yml +++ b/.github/workflows/authorize-agentic-ci.yml @@ -14,6 +14,10 @@ defaults: run: shell: bash +concurrency: + group: authorize-agentic-ci-${{ github.event.issue.number }} + cancel-in-progress: false + jobs: authorize: if: >-