diff --git a/.github/workflows/pr-linked-issue.yml b/.github/workflows/pr-linked-issue.yml index 5cf7833df..ad1d59150 100644 --- a/.github/workflows/pr-linked-issue.yml +++ b/.github/workflows/pr-linked-issue.yml @@ -9,9 +9,9 @@ on: types: [opened, edited, synchronize, reopened] branches: [main] - # Re-check open PRs when a maintainer adds the "triaged" label to an issue. + # Re-check open PRs when linked-issue validity changes. issues: - types: [labeled] + types: [labeled, unlabeled, closed, reopened] permissions: contents: read @@ -88,7 +88,7 @@ jobs: echo "issue_num=${ISSUE_NUM}" >> "$GITHUB_OUTPUT" echo "Parsed issue number: ${ISSUE_NUM:-}" - - name: Validate issue exists and is triaged + - name: Validate issue is open and triaged id: validate if: steps.author.outputs.is_collaborator != 'true' && steps.parse.outputs.issue_num != '' env: @@ -97,6 +97,7 @@ jobs: run: | RESPONSE=$(gh api "repos/${{ github.repository }}/issues/${ISSUE_NUM}" 2>/dev/null) || { echo "issue_exists=false" >> "$GITHUB_OUTPUT" + echo "is_open=false" >> "$GITHUB_OUTPUT" echo "is_triaged=false" >> "$GITHUB_OUTPUT" echo "Issue #${ISSUE_NUM} not found" exit 0 @@ -106,6 +107,7 @@ jobs: IS_PR=$(echo "$RESPONSE" | jq -r 'has("pull_request")') if [ "$IS_PR" = "true" ]; then echo "issue_exists=false" >> "$GITHUB_OUTPUT" + echo "is_open=false" >> "$GITHUB_OUTPUT" echo "is_triaged=false" >> "$GITHUB_OUTPUT" echo "#${ISSUE_NUM} is a pull request, not an issue" exit 0 @@ -113,9 +115,17 @@ jobs: echo "issue_exists=true" >> "$GITHUB_OUTPUT" + ISSUE_OPEN=$(echo "$RESPONSE" | jq -r '.state == "open"') + echo "is_open=${ISSUE_OPEN}" >> "$GITHUB_OUTPUT" + if [ "$ISSUE_OPEN" != "true" ]; then + echo "is_triaged=false" >> "$GITHUB_OUTPUT" + echo "Issue #${ISSUE_NUM} is closed" + exit 0 + fi + TRIAGED=$(echo "$RESPONSE" | jq -r '[.labels[].name] | any(. == "triaged")') echo "is_triaged=${TRIAGED}" >> "$GITHUB_OUTPUT" - echo "Issue #${ISSUE_NUM} exists, triaged=${TRIAGED}" + echo "Issue #${ISSUE_NUM} is open, triaged=${TRIAGED}" - name: Build comment body and post result id: comment @@ -124,6 +134,7 @@ jobs: IS_COLLABORATOR: ${{ steps.author.outputs.is_collaborator }} ISSUE_NUM: ${{ steps.parse.outputs.issue_num }} ISSUE_EXISTS: ${{ steps.validate.outputs.issue_exists }} + ISSUE_OPEN: ${{ steps.validate.outputs.is_open }} IS_TRIAGED: ${{ steps.validate.outputs.is_triaged }} PR_NUMBER: ${{ github.event.pull_request.number }} REPO: ${{ github.repository }} @@ -131,9 +142,11 @@ jobs: MARKER="" # 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" \ + COMMENT_IDS=$(gh api --paginate \ + "repos/${REPO}/issues/${PR_NUMBER}/comments?per_page=100" \ + --jq ".[] | select(.user.login == \"github-actions[bot]\") | select(.body | contains(\"${MARKER}\")) | .id" \ 2>/dev/null || echo "") + COMMENT_ID=$(printf '%s\n' "$COMMENT_IDS" | tail -1) if [ "$IS_COLLABORATOR" = "true" ]; then echo "status=pass" >> "$GITHUB_OUTPUT" @@ -152,7 +165,7 @@ jobs: ### Linked Issue Check This PR does not reference an issue. External contributions must link to - a triaged issue before the PR can be merged. + an open, triaged issue for this check to pass. Add one of the following to your PR description: - `Fixes #` @@ -174,6 +187,15 @@ jobs: The referenced issue #${ISSUE_NUM} was not found. Please check the issue number in your PR description. MSG + elif [ "$ISSUE_OPEN" != "true" ]; then + STATUS="fail" + cat > /tmp/comment-body.md < + ### Linked Issue Check + + Issue #${ISSUE_NUM} is closed. Please link an open, triaged issue in your + PR description. + MSG elif [ "$IS_TRIAGED" != "true" ]; then STATUS="fail" cat > /tmp/comment-body.md <- github.repository_owner == 'NVIDIA-NeMo' && github.event_name == 'issues' - && github.event.label.name == 'triaged' + && ( + github.event.action == 'closed' + || github.event.action == 'reopened' + || ( + (github.event.action == 'labeled' || github.event.action == 'unlabeled') + && github.event.label.name == 'triaged' + && github.event.issue.state == 'open' + ) + ) runs-on: ubuntu-latest steps: - name: Find PRs referencing this issue @@ -228,7 +258,13 @@ jobs: # 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] | .[]") + | jq -r --arg issue "$ISSUE_NUMBER" ' + .[] + | select(.body != null) + | (.body | ascii_downcase | capture("(?:fixes|closes|resolves)\\s+#(?[0-9]+)")?) as $reference + | select($reference.issue == $issue) + | .number + ') if [ -z "$PRS" ]; then echo "No open PRs reference issue #${ISSUE_NUMBER}" @@ -242,11 +278,31 @@ jobs: if: steps.find-prs.outputs.prs != '' env: GH_TOKEN: ${{ github.token }} + ISSUE_ACTION: ${{ github.event.action }} ISSUE_NUMBER: ${{ github.event.issue.number }} PR_NUMBERS: ${{ steps.find-prs.outputs.prs }} run: | TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) + case "$ISSUE_ACTION" in + labeled) + EVENT_DESCRIPTION='received the `triaged` label' + ;; + unlabeled) + EVENT_DESCRIPTION='lost the `triaged` label' + ;; + closed) + EVENT_DESCRIPTION="was closed" + ;; + reopened) + EVENT_DESCRIPTION="was reopened" + ;; + *) + echo "::error::Unsupported issue action: ${ISSUE_ACTION}" + exit 1 + ;; + esac + for PR_NUM in $PR_NUMBERS; do echo "Re-triggering check for PR #${PR_NUM}..." @@ -268,5 +324,5 @@ jobs: # 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." + "Issue #${ISSUE_NUMBER} ${EVENT_DESCRIPTION}. The linked issue check is being re-evaluated." done diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fe104d5cf..4306f2709 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,7 @@ The repository includes skills for common development tasks. These are located i ## Pull Requests -- PRs must link to the issue they address (`Fixes #NNN` or `Closes #NNN`). For external contributors, this is enforced by a required status check: the linked issue must exist and carry the `triaged` label (added by a maintainer after review). Collaborators are exempt from this check. You can open the PR before the issue is triaged - the check re-runs automatically once a maintainer adds the label. +- PRs should link to the issue they address (`Fixes #NNN` or `Closes #NNN`). For external contributors, the linked-issue workflow reports whether the issue exists, is open, and carries the `triaged` label (added by a maintainer after review). Collaborators are exempt from this check. Repository rules must separately require `Linked Issue Check / check` for the result to block merging. You can open the PR before the issue is triaged - the check re-runs automatically when the selected issue gains or loses the label, is closed, or is reopened. - PRs with failing checks that remain inactive are automatically reminded after 7 days and closed after 14 days (collaborators: 14/28 days). Push an update or leave a comment to reset the timer. If you need more time, ask a maintainer to add the `keep-open` label. - Use the `create-pr` skill for well-formatted PR descriptions, or follow the PR template - Ensure all checks pass before requesting review: