From 44737ae5887579a09f70ec4551f9f60a7030b5e2 Mon Sep 17 00:00:00 2001 From: yashrajbasav Date: Fri, 7 Aug 2026 13:13:16 +0530 Subject: [PATCH] feat(ci): restore fork pull request support Signed-off-by: yashrajbasav --- .github/workflows/require-nvskills-status.yml | 104 ++++++++++++++++-- .github/workflows/team-request.yml | 28 ++++- 2 files changed, 121 insertions(+), 11 deletions(-) diff --git a/.github/workflows/require-nvskills-status.yml b/.github/workflows/require-nvskills-status.yml index 146c98cc..489ccc24 100644 --- a/.github/workflows/require-nvskills-status.yml +++ b/.github/workflows/require-nvskills-status.yml @@ -28,9 +28,12 @@ jobs: HEAD_REF: ${{ github.event.pull_request.head.ref || '' }} HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.sha || github.sha }} STATUS_CONTEXT: ${{ vars.NVSKILLS_STATUS_CONTEXT || 'NVSkills CI' }} + SIGNATURE_COMMIT_TITLE: ${{ vars.NVSKILLS_SIGNATURE_COMMIT_TITLE || 'Attach NVSkills validation signatures' }} + SIGNATURE_PUSH_ACTOR: ${{ vars.NVSKILLS_SIGNATURE_PUSH_ACTOR || 'nv-skills-ci[bot]' }} + SIGNATURE_SERVICE_ACCOUNT_LOGIN: ${{ vars.NVSKILLS_FORK_SERVICE_ACCOUNT_LOGIN || vars.NVSKILLS_SIGNATURE_COMMIT_LOGIN || 'svc-nvskills-signing' }} STATUS_POLL_SECONDS: ${{ vars.NVSKILLS_STATUS_POLL_SECONDS || '30' }} MISSING_STATUS_GRACE_SECONDS: ${{ vars.NVSKILLS_MISSING_STATUS_GRACE_SECONDS || '120' }} - MAX_STATUS_WAIT_SECONDS: ${{ vars.NVSKILLS_MAX_STATUS_WAIT_SECONDS || '3600' }} + MAX_STATUS_WAIT_SECONDS: ${{ vars.NVSKILLS_MAX_STATUS_WAIT_SECONDS || '3300' }} run: | set -euo pipefail @@ -50,6 +53,25 @@ jobs: esac } + bounded_positive_integer() { + local value + local maximum="$3" + + value="$(positive_integer "$1" "$2")" + while [ "${value#0}" != "${value}" ]; do + value="${value#0}" + done + if [ -z "${value}" ] || [ "${value}" = "0" ]; then + value="$2" + fi + if [ "${#value}" -gt "${#maximum}" ] || + { [ "${#value}" -eq "${#maximum}" ] && [[ "${value}" > "${maximum}" ]]; }; then + echo "::warning::Configured wait value ${value}s exceeds the safe maximum ${maximum}s; clamping it." >&2 + value="${maximum}" + fi + printf '%s' "${value}" + } + case "${HEAD_REF}" in automated/sync-skills|bot/regenerate-skill-metadata) append_summary \ @@ -120,6 +142,47 @@ jobs: ' } + is_generated_signature_artifact_commit() { + local commit_json="$1" + local commit_title + + commit_title="$(printf '%s' "${commit_json}" | jq -r '.commit.message | split("\n")[0]')" + [ "${commit_title}" = "${SIGNATURE_COMMIT_TITLE}" ] || return 1 + + printf '%s' "${commit_json}" | jq -e ' + def generated_artifact: + test( + "^(skills/[^/]+|team-skills/[^/]+/[^/]+|" + + "plugins/[^/]+|plugins/[^/]+/skills/[^/]+)/" + + "(BENCHMARK\\.md|skill-card\\.md|skill\\.oms\\.sig|" + + "skill-card-review-needed\\.md|[^/]+-review-needed\\.md)$" + ); + any(.files[]?; .filename | endswith("/skill.oms.sig")) and + all(.files[]?; + ((.previous_filename? // "") | length) == 0 and + (.filename | generated_artifact) + ) + ' >/dev/null + } + + is_service_generated_signature_commit() { + local commit_json="$1" + + is_generated_signature_artifact_commit "${commit_json}" || return 1 + printf '%s' "${commit_json}" | jq -e \ + --arg actor "${SIGNATURE_PUSH_ACTOR}" \ + --arg service_login "${SIGNATURE_SERVICE_ACCOUNT_LOGIN}" ' + ((.author.login? // "") | ascii_downcase) == ($actor | ascii_downcase) or + ((.committer.login? // "") | ascii_downcase) == ($actor | ascii_downcase) or + ( + ($service_login | length) > 0 and + ( + ((.author.login? // "") | ascii_downcase) == ($service_login | ascii_downcase) or + ((.committer.login? // "") | ascii_downcase) == ($service_login | ascii_downcase) + ) + ) + ' >/dev/null + } if [ -z "${pr_number}" ] || [ -z "${head_sha}" ]; then echo "Pull request metadata is incomplete." exit 1 @@ -188,6 +251,7 @@ jobs: latest_watched_sha="" latest_watched_path="" + latest_generated_signature_sha="" for ((idx=${#commit_shas[@]} - 1; idx >= 0; idx--)); do commit_sha="${commit_shas[idx]}" if [ "${commit_sha}" = "${head_sha}" ]; then @@ -195,6 +259,12 @@ jobs: else commit_json="$(get_commit_json "${commit_sha}")" fi + if is_service_generated_signature_commit "${commit_json}"; then + if [ -z "${latest_generated_signature_sha}" ]; then + latest_generated_signature_sha="${commit_sha}" + fi + continue + fi watched_path="$(first_watched_path "${commit_json}")" if [ -n "${watched_path}" ]; then latest_watched_sha="${commit_sha}" @@ -204,6 +274,13 @@ jobs: done if [ -z "${latest_watched_sha}" ]; then + if [ -n "${latest_generated_signature_sha}" ]; then + append_summary \ + "## NVSkills CI required status" \ + "" \ + "Failed: generated signature commit \`${latest_generated_signature_sha}\` has no preceding watched-path content commit to validate." + exit 1 + fi append_summary \ "## NVSkills CI required status" \ "" \ @@ -211,9 +288,14 @@ jobs: exit 0 fi - poll_seconds="$(positive_integer "${STATUS_POLL_SECONDS}" 30)" - missing_grace_seconds="$(positive_integer "${MISSING_STATUS_GRACE_SECONDS}" 120)" - max_wait_seconds="$(positive_integer "${MAX_STATUS_WAIT_SECONDS}" 3600)" + status_floor_sha="${latest_watched_sha}" + if [ -n "${latest_generated_signature_sha}" ]; then + status_floor_sha="${latest_generated_signature_sha}" + fi + + poll_seconds="$(bounded_positive_integer "${STATUS_POLL_SECONDS}" 30 300)" + missing_grace_seconds="$(bounded_positive_integer "${MISSING_STATUS_GRACE_SECONDS}" 120 600)" + max_wait_seconds="$(bounded_positive_integer "${MAX_STATUS_WAIT_SECONDS}" 3300 3300)" elapsed_seconds=0 status_sha="" status_state="missing" @@ -237,7 +319,7 @@ jobs: '[.[] | select(.context == $context)][0].target_url // ""')" break fi - if [ "${commit_sha}" = "${latest_watched_sha}" ]; then + if [ "${commit_sha}" = "${status_floor_sha}" ]; then break fi done @@ -263,7 +345,7 @@ jobs: ;; esac - echo "Waiting for ${STATUS_CONTEXT} at or after ${latest_watched_sha} (state: ${status_state}, elapsed: ${elapsed_seconds}s)." + echo "Waiting for ${STATUS_CONTEXT} at or after ${status_floor_sha} (state: ${status_state}, elapsed: ${elapsed_seconds}s)." sleep "${poll_seconds}" elapsed_seconds=$((elapsed_seconds + poll_seconds)) done @@ -275,6 +357,11 @@ jobs: "Passed: \`${STATUS_CONTEXT}\` succeeded for validation evidence commit \`${status_sha}\`." \ "" \ "Latest watched-path change: \`${latest_watched_path}\` at \`${latest_watched_sha}\`." + if [ -n "${latest_generated_signature_sha}" ]; then + append_summary \ + "" \ + "Latest generated signature commit: \`${latest_generated_signature_sha}\`." + fi if [ "${status_sha}" != "${head_sha}" ]; then append_summary \ "" \ @@ -286,15 +373,14 @@ jobs: append_summary \ "## NVSkills CI required status" \ "" \ - "Failed: no successful \`${STATUS_CONTEXT}\` status was found at or after latest watched commit \`${latest_watched_sha}\` (state: \`${status_state}\`)." \ + "Failed: no successful \`${STATUS_CONTEXT}\` status was found at or after required evidence commit \`${status_floor_sha}\` (state: \`${status_state}\`)." \ "" \ "Latest watched-path change: \`${latest_watched_path}\`." \ "" \ "Comment \`/nvskills-ci\` on this PR after every commit that changes" \ "\`skills/\`, \`team-skills/\`, \`rules/team-rules/\`, or \`plugins/\`." \ "" \ - "\`/nvskills-ci\` only works on branches in \`NVIDIA/skills\`, not forks." \ - "If this PR is from a fork, move the changes to a branch in \`NVIDIA/skills\` first." + "For a fork pull request, add \`${SIGNATURE_SERVICE_ACCOUNT_LOGIN}\`, the configured NVSkills signing service account, as a collaborator with write access to the fork repository, then ask a maintainer of the base repository to comment \`/nvskills-ci\`." if [ -n "${status_sha}" ]; then append_summary "" "Latest NVSkills CI status checked: \`${status_state}\` on \`${status_sha}\`." fi diff --git a/.github/workflows/team-request.yml b/.github/workflows/team-request.yml index e3bc7cd3..f762a49c 100644 --- a/.github/workflows/team-request.yml +++ b/.github/workflows/team-request.yml @@ -33,8 +33,31 @@ jobs: group: nvskills-ci-request-${{ github.repository }}-${{ github.event.issue.number || github.sha }} cancel-in-progress: true steps: + - name: Check dispatch availability + id: dispatch + env: + DISPATCH_TOKEN: ${{ secrets.NVSKILLS_CI_DISPATCH_TOKEN }} + EVENT_NAME: ${{ github.event_name }} + run: | + set -euo pipefail + if [ -n "${DISPATCH_TOKEN}" ]; then + echo "enabled=true" >> "${GITHUB_OUTPUT}" + exit 0 + fi + if [ "${EVENT_NAME}" = "push" ]; then + { + echo "## NVSkills CI request" + echo + echo "Skipped: this trusted signature push has no dispatch secret. The originating central run verifies the generated signature commit." + } >> "${GITHUB_STEP_SUMMARY}" + echo "enabled=false" >> "${GITHUB_OUTPUT}" + exit 0 + fi + echo "Missing NVSKILLS_CI_DISPATCH_TOKEN secret." + exit 1 + - name: Validate requester permission - if: ${{ github.event_name == 'issue_comment' }} + if: ${{ steps.dispatch.outputs.enabled == 'true' && github.event_name == 'issue_comment' }} env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} @@ -53,6 +76,7 @@ jobs: - name: Resolve request context id: context + if: ${{ steps.dispatch.outputs.enabled == 'true' }} env: GH_TOKEN: ${{ github.token }} EVENT_NAME: ${{ github.event_name }} @@ -153,7 +177,7 @@ jobs: } >> "${GITHUB_OUTPUT}" - name: Dispatch NVSkills CI - if: steps.context.outputs.should_dispatch == 'true' + if: steps.dispatch.outputs.enabled == 'true' && steps.context.outputs.should_dispatch == 'true' env: DISPATCH_TOKEN: ${{ secrets.NVSKILLS_CI_DISPATCH_TOKEN }} REPO: ${{ github.repository }}