mirror of
https://github.com/NVIDIA-NeMo/DataDesigner
synced 2026-05-24 09:48:29 +00:00
* fix(ci): trust generated agentic CI PRs Signed-off-by: Andre Manoel <amanoel@nvidia.com> * fix(ci): authorize generated PR checks Signed-off-by: Andre Manoel <amanoel@nvidia.com> * fix(ci): pin authorized agentic checks Signed-off-by: Andre Manoel <amanoel@nvidia.com> * fix(ci): narrow agentic CI trust * fix(ci): reject stale agentic authorizations * fix(ci): serialize agentic authorization --------- Signed-off-by: Andre Manoel <amanoel@nvidia.com>
272 lines
11 KiB
YAML
272 lines
11 KiB
YAML
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
name: "Linked Issue Check"
|
|
|
|
on:
|
|
# Re-check when PR is opened or body is edited (author adds Fixes #N).
|
|
pull_request_target:
|
|
types: [opened, edited, synchronize, reopened]
|
|
branches: [main]
|
|
|
|
# Re-check open PRs when a maintainer adds the "triaged" label to an issue.
|
|
issues:
|
|
types: [labeled]
|
|
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
issues: read
|
|
|
|
jobs:
|
|
# ── Job 1: validate linked issue on PR events ─────────────────────────
|
|
# SECURITY: This workflow uses pull_request_target to get write access for
|
|
# posting comments on fork PRs. It MUST NOT check out or execute code from
|
|
# the PR branch. All inputs from the PR (body, author) are read via API
|
|
# only. Adding actions/checkout here would run untrusted fork code with
|
|
# base repo write permissions.
|
|
check:
|
|
if: >-
|
|
github.repository_owner == 'NVIDIA-NeMo'
|
|
&& github.event_name != 'issues'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Check author permissions
|
|
id: author
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
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 }}
|
|
REPO: ${{ github.repository }}
|
|
run: |
|
|
USER="$PR_AUTHOR"
|
|
|
|
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]" ] && \
|
|
[ "$HEAD_REPO" = "$REPO" ] && \
|
|
[[ "$HEAD_REF" == agentic-ci/* ]] && \
|
|
grep -Eq '<!-- agentic-ci finding=[^[:space:]]+ suite=[^[:space:]]+ -->' /tmp/pr-body-raw.txt; then
|
|
echo "is_collaborator=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
# Bots that are always allowed (match DCO allowlist pattern).
|
|
if [ "$USER" = "dependabot[bot]" ]; then
|
|
echo "is_collaborator=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
PERMISSION=$(gh api "repos/${{ github.repository }}/collaborators/${USER}/permission" \
|
|
--jq '.permission' 2>/dev/null || echo "none")
|
|
echo "permission=${PERMISSION}"
|
|
|
|
if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "write" ]; then
|
|
echo "is_collaborator=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "is_collaborator=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Parse issue reference from PR body
|
|
id: parse
|
|
if: steps.author.outputs.is_collaborator != 'true'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
PR_BODY: ${{ github.event.pull_request.body }}
|
|
run: |
|
|
if [ -z "$PR_BODY" ] || [ "$PR_BODY" = "null" ]; then
|
|
echo "issue_num=" >> "$GITHUB_OUTPUT"
|
|
echo "No PR body found"
|
|
exit 0
|
|
fi
|
|
|
|
# Case-insensitive match for Fixes #N, Closes #N, Resolves #N.
|
|
printf '%s' "$PR_BODY" > /tmp/pr-body-raw.txt
|
|
ISSUE_NUM=$(grep -ioP '(?:fixes|closes|resolves)\s+#\K\d+' /tmp/pr-body-raw.txt | head -1 || true)
|
|
echo "issue_num=${ISSUE_NUM}" >> "$GITHUB_OUTPUT"
|
|
echo "Parsed issue number: ${ISSUE_NUM:-<none>}"
|
|
|
|
- name: Validate issue exists and is triaged
|
|
id: validate
|
|
if: steps.author.outputs.is_collaborator != 'true' && steps.parse.outputs.issue_num != ''
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
ISSUE_NUM: ${{ steps.parse.outputs.issue_num }}
|
|
run: |
|
|
RESPONSE=$(gh api "repos/${{ github.repository }}/issues/${ISSUE_NUM}" 2>/dev/null) || {
|
|
echo "issue_exists=false" >> "$GITHUB_OUTPUT"
|
|
echo "is_triaged=false" >> "$GITHUB_OUTPUT"
|
|
echo "Issue #${ISSUE_NUM} not found"
|
|
exit 0
|
|
}
|
|
|
|
# Verify it's an issue, not a PR (GitHub's issues API returns both).
|
|
IS_PR=$(echo "$RESPONSE" | jq -r 'has("pull_request")')
|
|
if [ "$IS_PR" = "true" ]; then
|
|
echo "issue_exists=false" >> "$GITHUB_OUTPUT"
|
|
echo "is_triaged=false" >> "$GITHUB_OUTPUT"
|
|
echo "#${ISSUE_NUM} is a pull request, not an issue"
|
|
exit 0
|
|
fi
|
|
|
|
echo "issue_exists=true" >> "$GITHUB_OUTPUT"
|
|
|
|
TRIAGED=$(echo "$RESPONSE" | jq -r '[.labels[].name] | any(. == "triaged")')
|
|
echo "is_triaged=${TRIAGED}" >> "$GITHUB_OUTPUT"
|
|
echo "Issue #${ISSUE_NUM} exists, triaged=${TRIAGED}"
|
|
|
|
- name: Build comment body and post result
|
|
id: comment
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
IS_COLLABORATOR: ${{ steps.author.outputs.is_collaborator }}
|
|
ISSUE_NUM: ${{ steps.parse.outputs.issue_num }}
|
|
ISSUE_EXISTS: ${{ steps.validate.outputs.issue_exists }}
|
|
IS_TRIAGED: ${{ steps.validate.outputs.is_triaged }}
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
REPO: ${{ github.repository }}
|
|
run: |
|
|
MARKER="<!-- linked-issue-check -->"
|
|
|
|
# Find existing bot comment with our marker.
|
|
COMMENT_ID=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
|
|
--jq "[.[] | select(.user.login == \"github-actions[bot]\") | select(.body | contains(\"${MARKER}\"))] | last | .id // empty" \
|
|
2>/dev/null || echo "")
|
|
|
|
if [ "$IS_COLLABORATOR" = "true" ]; then
|
|
echo "status=pass" >> "$GITHUB_OUTPUT"
|
|
# Clean up any leftover comment from before the author became a collaborator.
|
|
if [ -n "$COMMENT_ID" ]; then
|
|
gh api -X DELETE "repos/${REPO}/issues/comments/${COMMENT_ID}" || true
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# Build comment body.
|
|
if [ -z "$ISSUE_NUM" ]; then
|
|
STATUS="fail"
|
|
cat > /tmp/comment-body.md <<'MSG'
|
|
<!-- linked-issue-check -->
|
|
### Linked Issue Check
|
|
|
|
This PR does not reference an issue. External contributions must link to
|
|
a triaged issue before the PR can be merged.
|
|
|
|
Add one of the following to your PR description:
|
|
- `Fixes #<issue-number>`
|
|
- `Closes #<issue-number>`
|
|
- `Resolves #<issue-number>`
|
|
|
|
If no issue exists yet, [open one](https://github.com/NVIDIA-NeMo/DataDesigner/issues/new/choose)
|
|
and a maintainer will triage it.
|
|
|
|
See [CONTRIBUTING.md](https://github.com/NVIDIA-NeMo/DataDesigner/blob/main/CONTRIBUTING.md)
|
|
for details.
|
|
MSG
|
|
elif [ "$ISSUE_EXISTS" != "true" ]; then
|
|
STATUS="fail"
|
|
cat > /tmp/comment-body.md <<MSG
|
|
<!-- linked-issue-check -->
|
|
### Linked Issue Check
|
|
|
|
The referenced issue #${ISSUE_NUM} was not found. Please check the issue
|
|
number in your PR description.
|
|
MSG
|
|
elif [ "$IS_TRIAGED" != "true" ]; then
|
|
STATUS="fail"
|
|
cat > /tmp/comment-body.md <<MSG
|
|
<!-- linked-issue-check -->
|
|
### Linked Issue Check
|
|
|
|
Issue #${ISSUE_NUM} has not been triaged yet. A maintainer needs to review
|
|
the issue and add the \`triaged\` label before this PR can be merged.
|
|
|
|
You can continue working on the PR in the meantime. The check will
|
|
re-run automatically once the issue is triaged.
|
|
MSG
|
|
else
|
|
STATUS="pass"
|
|
fi
|
|
|
|
echo "status=${STATUS}" >> "$GITHUB_OUTPUT"
|
|
|
|
# Post, update, or delete the comment.
|
|
if [ "$STATUS" = "fail" ]; then
|
|
if [ -n "$COMMENT_ID" ]; then
|
|
gh api -X PATCH "repos/${REPO}/issues/comments/${COMMENT_ID}" \
|
|
-f body="$(cat /tmp/comment-body.md)"
|
|
else
|
|
gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
|
|
-f body="$(cat /tmp/comment-body.md)"
|
|
fi
|
|
elif [ -n "$COMMENT_ID" ]; then
|
|
gh api -X DELETE "repos/${REPO}/issues/comments/${COMMENT_ID}" || true
|
|
fi
|
|
|
|
- name: Set check result
|
|
if: steps.comment.outputs.status == 'fail'
|
|
run: |
|
|
echo "::error::Linked issue check failed. See the PR comment for details."
|
|
exit 1
|
|
|
|
# ── Job 2: re-trigger check when an issue gets triaged ────────────────
|
|
retrigger:
|
|
if: >-
|
|
github.repository_owner == 'NVIDIA-NeMo'
|
|
&& github.event_name == 'issues'
|
|
&& github.event.label.name == 'triaged'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Find PRs referencing this issue
|
|
id: find-prs
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
run: |
|
|
# List open PRs and find those whose body references this issue.
|
|
PRS=$(gh pr list --repo "${{ github.repository }}" --state open \
|
|
--json number,body --limit 200 \
|
|
| jq -r "[.[] | select(.body != null) | select(.body | test(\"(?i)(fixes|closes|resolves)\\\\s+#${ISSUE_NUMBER}\\\\b\")) | .number] | .[]")
|
|
|
|
if [ -z "$PRS" ]; then
|
|
echo "No open PRs reference issue #${ISSUE_NUMBER}"
|
|
echo "prs=" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "Found PRs: ${PRS}"
|
|
echo "prs=$(echo "$PRS" | tr '\n' ' ')" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Re-trigger linked issue check
|
|
if: steps.find-prs.outputs.prs != ''
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
PR_NUMBERS: ${{ steps.find-prs.outputs.prs }}
|
|
run: |
|
|
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
|
|
for PR_NUM in $PR_NUMBERS; do
|
|
echo "Re-triggering check for PR #${PR_NUM}..."
|
|
|
|
# Read current PR body to a file to avoid shell expansion issues.
|
|
gh pr view "$PR_NUM" --repo "${{ github.repository }}" --json body -q '.body' > /tmp/current-body.txt
|
|
|
|
# Append or update hidden timestamp to trigger the 'edited' event,
|
|
# which re-runs the check job.
|
|
MARKER="<!-- triaged-recheck"
|
|
if grep -q "$MARKER" /tmp/current-body.txt; then
|
|
sed "s|<!-- triaged-recheck[^>]*-->|<!-- triaged-recheck: ${TIMESTAMP} -->|" \
|
|
/tmp/current-body.txt > /tmp/pr-body.md
|
|
else
|
|
cp /tmp/current-body.txt /tmp/pr-body.md
|
|
printf '\n<!-- triaged-recheck: %s -->' "$TIMESTAMP" >> /tmp/pr-body.md
|
|
fi
|
|
|
|
gh pr edit "$PR_NUM" --repo "${{ github.repository }}" --body-file /tmp/pr-body.md
|
|
|
|
# Post a visible comment so the author knows what happened.
|
|
gh pr comment "$PR_NUM" --repo "${{ github.repository }}" --body \
|
|
"Issue #${ISSUE_NUMBER} has been triaged. The linked issue check is being re-evaluated."
|
|
done
|