build: migrate developer tasks to mise #249
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| name: Linked Issue Check | |
| on: | |
| pull_request_target: | |
| types: [opened, edited, reopened, ready_for_review, synchronize] | |
| branches: [main] | |
| issues: | |
| types: [labeled] | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| check: | |
| name: Check linked issue | |
| 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 }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$PR_AUTHOR" = "dependabot[bot]" ]; then | |
| echo "is_collaborator=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| permission=$(gh api "repos/${{ github.repository }}/collaborators/${PR_AUTHOR}/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: Validate parser cases | |
| run: | | |
| set -euo pipefail | |
| extract_issue_num() { | |
| local body_file="$1" | |
| local clean_body_file | |
| clean_body_file="$(mktemp)" | |
| perl -0pe 's/<!--.*?-->//gs' "$body_file" > "$clean_body_file" | |
| grep -ioE '(fixes|closes|resolves)[[:space:]]+#[0-9]+' "$clean_body_file" \ | |
| | grep -oE '[0-9]+' \ | |
| | head -1 \ | |
| || true | |
| } | |
| expect_issue() { | |
| local body_file="$1" | |
| local expected="$2" | |
| local case_name="$3" | |
| local actual | |
| actual="$(extract_issue_num "$body_file")" | |
| if [ "$actual" != "$expected" ]; then | |
| echo "::error::$case_name should parse issue #$expected, got '${actual:-<none>}'." | |
| exit 1 | |
| fi | |
| } | |
| expect_no_issue() { | |
| local body_file="$1" | |
| local case_name="$2" | |
| local actual | |
| actual="$(extract_issue_num "$body_file")" | |
| if [ -n "$actual" ]; then | |
| echo "::error::$case_name should not parse an issue, got #$actual." | |
| exit 1 | |
| fi | |
| } | |
| untouched_template_case="$(mktemp)" | |
| cat > "$untouched_template_case" <<'BODY' | |
| <!-- Use "Fixes #123", "Closes #123", or "Resolves #123". --> | |
| <!-- If no issue is needed, write: "No linked issue required: <reason>". --> | |
| BODY | |
| expect_no_issue "$untouched_template_case" "Untouched template comments" | |
| visible_issue_case="$(mktemp)" | |
| cat > "$visible_issue_case" <<'BODY' | |
| Fixes #123 | |
| BODY | |
| expect_issue "$visible_issue_case" "123" "Visible issue reference" | |
| empty_exemption_case="$(mktemp)" | |
| cat > "$empty_exemption_case" <<'BODY' | |
| No linked issue required: | |
| BODY | |
| expect_no_issue "$empty_exemption_case" "Empty no-issue exemption" | |
| nonempty_exemption_case="$(mktemp)" | |
| cat > "$nonempty_exemption_case" <<'BODY' | |
| No linked issue required: Maintainer-requested docs-only cleanup. | |
| BODY | |
| expect_no_issue "$nonempty_exemption_case" "Nonempty no-issue exemption" | |
| - name: Parse issue reference from PR body | |
| id: parse | |
| if: steps.author.outputs.is_collaborator != 'true' | |
| env: | |
| PR_BODY: ${{ github.event.pull_request.body }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$PR_BODY" ] || [ "$PR_BODY" = "null" ]; then | |
| echo "issue_num=" >> "$GITHUB_OUTPUT" | |
| echo "No PR body found" | |
| exit 0 | |
| fi | |
| body_file="$(mktemp)" | |
| clean_body_file="$(mktemp)" | |
| printf '%s' "$PR_BODY" > "$body_file" | |
| perl -0pe 's/<!--.*?-->//gs' "$body_file" > "$clean_body_file" | |
| issue_num=$( | |
| grep -ioE '(fixes|closes|resolves)[[:space:]]+#[0-9]+' "$clean_body_file" \ | |
| | grep -oE '[0-9]+' \ | |
| | 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: | | |
| set -euo pipefail | |
| 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 | |
| } | |
| 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: | | |
| set -euo pipefail | |
| marker="<!-- linked-issue-check -->" | |
| 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" | |
| if [ -n "$comment_id" ]; then | |
| gh api -X DELETE "repos/${REPO}/issues/comments/${comment_id}" || true | |
| fi | |
| exit 0 | |
| fi | |
| 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/Anonymizer/issues/new/choose) | |
| and a maintainer will triage it. | |
| See [CONTRIBUTING.md](https://github.com/NVIDIA-NeMo/Anonymizer/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. When the issue is | |
| triaged, the workflow will rerun this linked-issue check for open PRs | |
| that reference the issue. | |
| MSG | |
| else | |
| status="pass" | |
| fi | |
| echo "status=${status}" >> "$GITHUB_OUTPUT" | |
| 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 | |
| retrigger: | |
| name: Recheck PRs when issue is triaged | |
| permissions: | |
| contents: read | |
| actions: write | |
| issues: read | |
| pull-requests: read | |
| if: >- | |
| github.repository_owner == 'NVIDIA-NeMo' | |
| && github.event_name == 'issues' | |
| && github.event.label.name == 'triaged' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Validate recheck selection cases | |
| run: | | |
| set -euo pipefail | |
| body_references_issue() { | |
| local body_file="$1" | |
| local issue_number="$2" | |
| local clean_body_file | |
| clean_body_file="$(mktemp)" | |
| perl -0pe 's/<!--.*?-->//gs' "$body_file" > "$clean_body_file" | |
| grep -iqE "(fixes|closes|resolves)[[:space:]]+#${issue_number}([^0-9]|$)" "$clean_body_file" | |
| } | |
| visible_reference_case="$(mktemp)" | |
| cat > "$visible_reference_case" <<'BODY' | |
| Fixes #123 | |
| BODY | |
| body_references_issue "$visible_reference_case" "123" | |
| commented_reference_case="$(mktemp)" | |
| cat > "$commented_reference_case" <<'BODY' | |
| <!-- Fixes #123 --> | |
| BODY | |
| if body_references_issue "$commented_reference_case" "123"; then | |
| echo "::error::HTML-commented issue reference should not select a PR for recheck." | |
| exit 1 | |
| fi | |
| runs_case="$(mktemp)" | |
| cat > "$runs_case" <<'JSON' | |
| [ | |
| { | |
| "workflow_runs": [ | |
| {"id": 10, "event": "pull_request_target", "head_sha": "abc", "status": "completed", "conclusion": "failure", "created_at": "2026-01-01T00:00:00Z"}, | |
| {"id": 11, "event": "pull_request_target", "head_sha": "abc", "status": "completed", "conclusion": "failure", "created_at": "2026-01-02T00:00:00Z"}, | |
| {"id": 12, "event": "issues", "head_sha": "abc", "status": "completed", "conclusion": "success", "created_at": "2026-01-03T00:00:00Z"} | |
| ] | |
| } | |
| ] | |
| JSON | |
| selected_run=$( | |
| jq -r --arg head_sha "abc" ' | |
| [.[].workflow_runs[]? | select(.event == "pull_request_target" and .head_sha == $head_sha)] | |
| | sort_by(.created_at) | |
| | last | |
| | .id // empty | |
| ' "$runs_case" | |
| ) | |
| if [ "$selected_run" != "11" ]; then | |
| echo "::error::Triaged-label recheck should select the latest pull_request_target run for the PR head SHA." | |
| exit 1 | |
| fi | |
| - name: Find PRs referencing this issue | |
| id: find-prs | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| run: | | |
| set -euo pipefail | |
| prs_json=$(gh pr list --repo "${{ github.repository }}" --state open --json number,body --limit 200) | |
| pr_numbers="" | |
| rm -f /tmp/prs-to-recheck.txt | |
| echo "$prs_json" | jq -c '.[]' | while IFS= read -r pr; do | |
| pr_num=$(echo "$pr" | jq -r '.number') | |
| body=$(echo "$pr" | jq -r '.body // ""') | |
| body_file="$(mktemp)" | |
| clean_body_file="$(mktemp)" | |
| printf '%s' "$body" > "$body_file" | |
| perl -0pe 's/<!--.*?-->//gs' "$body_file" > "$clean_body_file" | |
| if grep -iqE "(fixes|closes|resolves)[[:space:]]+#${ISSUE_NUMBER}([^0-9]|$)" "$clean_body_file"; then | |
| echo "$pr_num" >> /tmp/prs-to-recheck.txt | |
| fi | |
| done | |
| if [ -f /tmp/prs-to-recheck.txt ]; then | |
| pr_numbers=$(tr '\n' ' ' < /tmp/prs-to-recheck.txt) | |
| fi | |
| if [ -z "$pr_numbers" ]; then | |
| echo "No open PRs reference issue #${ISSUE_NUMBER}" | |
| echo "prs=" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Found PRs: ${pr_numbers}" | |
| echo "prs=${pr_numbers}" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Rerun linked issue checks | |
| if: steps.find-prs.outputs.prs != '' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| PR_NUMBERS: ${{ steps.find-prs.outputs.prs }} | |
| run: | | |
| set -euo pipefail | |
| for pr_num in $PR_NUMBERS; do | |
| echo "Finding latest linked-issue check run for PR #${pr_num}..." | |
| head_sha=$(gh pr view "$pr_num" --repo "$REPO" --json headRefOid -q '.headRefOid') | |
| # Triaged-label validation path: rerun the existing PR-head | |
| # pull_request_target workflow run directly. Do not edit the PR body | |
| # or require the contributor to push another commit. | |
| runs_json=$(gh api --paginate --slurp \ | |
| "repos/${REPO}/actions/workflows/pr-linked-issue.yml/runs?event=pull_request_target&per_page=100") | |
| run_json=$( | |
| echo "$runs_json" | jq -c --arg head_sha "$head_sha" ' | |
| [.[].workflow_runs[]? | select(.event == "pull_request_target" and .head_sha == $head_sha)] | |
| | sort_by(.created_at) | |
| | last // empty | |
| ' | |
| ) | |
| if [ -z "$run_json" ]; then | |
| echo " No linked-issue check run found for PR #${pr_num} at ${head_sha}; skipping." | |
| continue | |
| fi | |
| run_id=$(echo "$run_json" | jq -r '.id') | |
| run_status=$(echo "$run_json" | jq -r '.status') | |
| run_conclusion=$(echo "$run_json" | jq -r '.conclusion // ""') | |
| echo " Latest run ${run_id}: status=${run_status}, conclusion=${run_conclusion:-<none>}" | |
| if [ "$run_status" != "completed" ]; then | |
| echo " Skipping: linked-issue check is already ${run_status}." | |
| continue | |
| fi | |
| if [ "$run_conclusion" = "success" ]; then | |
| echo " Skipping: linked-issue check already passed." | |
| continue | |
| fi | |
| gh api -X POST "repos/${REPO}/actions/runs/${run_id}/rerun" | |
| echo " Requested rerun for linked-issue check run ${run_id}." | |
| done |