Skip to content

PR #1324 · internal CI dispatch #1386

PR #1324 · internal CI dispatch

PR #1324 · internal CI dispatch #1386

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.

Check warning on line 1 in .github/workflows/internal-ci-bridge.yml

View workflow run for this annotation

GitHub Actions / TensorRT-Model-Connect Internal CI Bridge

Workflow execution policy warning (evaluate mode)

On November 2, 2026, GitHub will restrict `pull_request_target` on public repositories by default. To continue allowing the event trigger, configure an Actions policy. Learn more: https://gh.io/securely-using-pull_request_target#default-policy-for-pull_request_target
# SPDX-License-Identifier: Apache-2.0
name: TensorRT-Model-Connect Internal CI Bridge
run-name: >-
PR #${{ github.event.pull_request.number || inputs.pr_number }} · internal CI dispatch
on:
pull_request_target:
branches:
- main
types: [labeled]
workflow_dispatch:
inputs:
pr_number:
description: Open pull request number to test
required: true
type: string
permissions: {}
concurrency:
group: >-
trtmc-private-dispatch-${{ github.event.pull_request.number || inputs.pr_number }}
cancel-in-progress: false
jobs:
authorize:
name: Authorize trusted trigger
if: >-
github.repository == 'NVIDIA/TensorRT-Model-Connect' &&
(
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'pull_request_target' &&
github.ref == 'refs/heads/main' &&
github.event.label.name == 'run-internal-ci'
)
)
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
actions: read
contents: read
pull-requests: write
outputs:
pr_number: ${{ steps.snapshot.outputs.pr_number }}
head_sha: ${{ steps.snapshot.outputs.head_sha }}
base_sha: ${{ steps.snapshot.outputs.base_sha }}
steps:
# This trusted workflow reads PR metadata only. It never checks out or
# executes the pull-request head.
- name: Capture the exact pull-request snapshot
id: snapshot
env:
GH_TOKEN: ${{ github.token }}
ACTOR: ${{ github.actor }}
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
EVENT_NAME: ${{ github.event_name }}
EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
[[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]
actor_role="$(
gh api --method GET \
"/repos/$GITHUB_REPOSITORY/collaborators/$ACTOR/permission" \
--jq '.role_name'
)"
case "$actor_role" in
maintain|admin) ;;
*)
echo "::error::Only actors with maintain or admin access may dispatch CI."
exit 1
;;
esac
echo "trigger_authorized=true" >> "$GITHUB_OUTPUT"
if [ "$EVENT_NAME" = "pull_request_target" ]; then
[[ "$EVENT_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]
fi
pull="$(gh api --method GET "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")"
state="$(jq -er '.state' <<<"$pull")"
base_repo="$(jq -er '.base.repo.full_name' <<<"$pull")"
base_ref="$(jq -er '.base.ref' <<<"$pull")"
head_sha="$(jq -er '.head.sha' <<<"$pull")"
merge_sha="$(jq -r '.merge_commit_sha // empty' <<<"$pull")"
test "$state" = "open"
test "$base_repo" = "$GITHUB_REPOSITORY"
test "$base_ref" = "main"
[[ "$head_sha" =~ ^[0-9a-f]{40}$ ]]
[[ "$merge_sha" =~ ^[0-9a-f]{40}$ ]] || {
echo "::error::Pull request #$PR_NUMBER has no testable merge commit."
exit 1
}
if [ "$EVENT_NAME" = "pull_request_target" ] \
&& [ "$head_sha" != "$EVENT_HEAD_SHA" ]; then
echo "::error::The CI trigger was superseded by a newer PR head."
exit 1
fi
merge="$(gh api --method GET "/repos/$GITHUB_REPOSITORY/git/commits/$merge_sha")"
resolved_merge="$(jq -er '.sha' <<<"$merge")"
parent_count="$(jq -er '.parents | length' <<<"$merge")"
base_sha="$(jq -er '.parents[0].sha' <<<"$merge")"
merge_head="$(jq -er '.parents[1].sha' <<<"$merge")"
merge_tree="$(jq -er '.tree.sha' <<<"$merge")"
test "$resolved_merge" = "$merge_sha"
test "$parent_count" = "2"
test "$merge_head" = "$head_sha"
[[ "$base_sha" =~ ^[0-9a-f]{40}$ ]]
[[ "$merge_tree" =~ ^[0-9a-f]{40}$ ]]
# CPU success belongs to the exact PR head. Main may advance after
# CPU finishes; accept an older tested base in the authorized base's
# history. Internal CI resolves and pins its own merge snapshot.
if ! community_ci_runs="$(
gh api --method GET \
"/repos/$GITHUB_REPOSITORY/actions/workflows/community-ci.yml/runs?event=pull_request&head_sha=$head_sha&per_page=100"
)"; then
echo "::error::Unable to query Community CI runs for PR head $head_sha."
exit 1
fi
community_ci_title_prefix="PR #$PR_NUMBER · community CI · head $head_sha · merge "
community_ci_run=""
community_cpu_job=""
cpu_reason="No eligible Community CI run was found."
if [ -n "$community_ci_runs" ]; then
community_ci_candidates="$(
jq -r \
--arg title_prefix "$community_ci_title_prefix" \
--arg head_sha "$head_sha" \
'.workflow_runs
| map(
select(.event == "pull_request" and .head_sha == $head_sha)
| select(.display_title | startswith($title_prefix))
| {
id,
updated_at,
merge_sha: (.display_title | ltrimstr($title_prefix))
}
)
| sort_by(.updated_at)
| reverse
| .[]
| [.id, .merge_sha]
| @tsv' \
<<<"$community_ci_runs"
)"
while IFS=$'\t' read -r candidate_run candidate_merge_sha; do
[[ "$candidate_run" =~ ^[1-9][0-9]*$ ]] || continue
[[ "$candidate_merge_sha" =~ ^[0-9a-f]{40}$ ]] || continue
if ! candidate_merge="$(
gh api --method GET \
"/repos/$GITHUB_REPOSITORY/git/commits/$candidate_merge_sha"
)"; then
echo "::error::Unable to read the merge snapshot for Community CI run $candidate_run."
exit 1
fi
candidate_resolved="$(jq -r '.sha // empty' <<<"$candidate_merge")"
candidate_parent_count="$(jq -r '.parents | length' <<<"$candidate_merge")"
candidate_base="$(jq -r '.parents[0].sha // empty' <<<"$candidate_merge")"
candidate_head="$(jq -r '.parents[1].sha // empty' <<<"$candidate_merge")"
candidate_tree="$(jq -r '.tree.sha // empty' <<<"$candidate_merge")"
if [ "$candidate_resolved" != "$candidate_merge_sha" ] \
|| [ "$candidate_parent_count" != "2" ] \
|| [ "$candidate_head" != "$head_sha" ]; then
continue
fi
[[ "$candidate_base" =~ ^[0-9a-f]{40}$ ]] || continue
[[ "$candidate_tree" =~ ^[0-9a-f]{40}$ ]] || continue
if [ "$candidate_base" = "$base_sha" ]; then
# Regenerating a merge with the same parents must preserve its tree.
[ "$candidate_tree" = "$merge_tree" ] || continue
else
if ! comparison="$(
gh api --method GET \
"/repos/$GITHUB_REPOSITORY/compare/$candidate_base...$base_sha?per_page=1"
)"; then
echo "::error::Unable to verify base ancestry for Community CI run $candidate_run."
exit 1
fi
if ! jq -e --arg base "$candidate_base" \
'.status == "ahead" and .merge_base_commit.sha == $base' \
<<<"$comparison" > /dev/null; then
continue
fi
fi
if ! cpu_job="$(
gh api --method GET \
"/repos/$GITHUB_REPOSITORY/actions/runs/$candidate_run/jobs?filter=latest&per_page=100" \
--jq '.jobs | map(select(.name == "Community CPU / Required")) | last // {}'
)"; then
echo "::error::Unable to query the CPU gate for Community CI run $candidate_run."
exit 1
fi
cpu_status="$(jq -r '.status // "missing"' <<<"$cpu_job")"
cpu_conclusion="$(jq -r '.conclusion // "none"' <<<"$cpu_job")"
candidate_cpu_job="$(jq -r '.id // empty' <<<"$cpu_job")"
cpu_reason="Run $candidate_run: CPU gate status=$cpu_status, conclusion=$cpu_conclusion."
if [ "$cpu_status" = "completed" ] && [ "$cpu_conclusion" = "success" ] \
&& [[ "$candidate_cpu_job" =~ ^[1-9][0-9]*$ ]]; then
community_ci_run="$candidate_run"
community_cpu_job="$candidate_cpu_job"
echo "Accepted CPU run $candidate_run for head $head_sha: tested base $candidate_base, authorized base $base_sha."
break
fi
done <<<"$community_ci_candidates"
fi
if ! [[ "$community_cpu_job" =~ ^[1-9][0-9]*$ ]]; then
echo "::error::Community CPU / Required must pass on PR head $head_sha before internal CI can run. $cpu_reason"
exit 1
fi
{
echo "pr_number=$PR_NUMBER"
echo "head_sha=$head_sha"
echo "base_sha=$base_sha"
} >> "$GITHUB_OUTPUT"
- name: Consume the trusted trigger label
if: ${{ always() && github.event_name == 'pull_request_target' && steps.snapshot.outputs.trigger_authorized == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
gh api --silent --method DELETE \
"/repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/labels/run-internal-ci"
announce:
name: Publish pending result
needs: authorize
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
pull-requests: write
statuses: write
steps:
- name: Publish the pending automated status
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
run: |
set -euo pipefail
gh api --silent --method POST \
"/repos/$GITHUB_REPOSITORY/statuses/$HEAD_SHA" \
-f state=pending \
-f context="TRTMC Internal CI / Automated premerge gate" \
-f description="Automated internal CI is running" \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
- name: Remove the previous automated failure comment
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
run: |
set -euo pipefail
gh api --paginate \
"/repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments?per_page=100" \
--jq '.[] | select(.user.login == "github-actions[bot]") | select(.body | contains("<!-- trtmc-internal-ci-result -->")) | .id' \
| while read -r comment_id; do
[[ "$comment_id" =~ ^[1-9][0-9]*$ ]] || continue
gh api --silent --method DELETE \
"/repos/$GITHUB_REPOSITORY/issues/comments/$comment_id" || true
done
dispatch:
name: Internal CI dispatch
needs:
- authorize
- announce
if: ${{ always() && needs.authorize.result == 'success' }}
runs-on: ubuntu-24.04
environment:
name: ci-dispatch
deployment: false
timeout-minutes: 360
permissions:
contents: read
outputs:
private_conclusion: ${{ steps.private_result.outputs.conclusion }}
steps:
- name: Dispatch internal premerge CI
id: private_run
env:
GH_TOKEN: ${{ secrets.TRTMC_CI_DISPATCH_TOKEN }}
PRIVATE_CI_OWNER: ${{ secrets.TRTMC_PRIVATE_CI_OWNER }}
PRIVATE_CI_REPOSITORY: ${{ secrets.TRTMC_PRIVATE_CI_REPOSITORY }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
BASE_SHA: ${{ needs.authorize.outputs.base_sha }}
run: |
set -euo pipefail
[[ "$PRIVATE_CI_OWNER" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,37}[A-Za-z0-9])?$ ]]
[[ "$PRIVATE_CI_REPOSITORY" =~ ^[A-Za-z0-9]([A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$ ]]
umask 077
dispatch_nonce="$(openssl rand -hex 16)"
[[ "$dispatch_nonce" =~ ^[0-9a-f]{32}$ ]]
echo "::add-mask::$dispatch_nonce"
payload="$RUNNER_TEMP/internal-ci-dispatch.json"
trap 'rm -f "$payload"' EXIT
jq -n \
--arg pr_number "$PR_NUMBER" \
--arg head_sha "$HEAD_SHA" \
--arg base_sha "$BASE_SHA" \
--arg dispatch_nonce "$dispatch_nonce" \
'{
ref: "main",
inputs: {
pr_number: $pr_number,
head_sha: $head_sha,
base_sha: $base_sha,
dispatch_nonce: $dispatch_nonce
}
}' > "$payload"
gh api --silent --method POST \
"/repos/$PRIVATE_CI_OWNER/$PRIVATE_CI_REPOSITORY/actions/workflows/premerge.yml/dispatches" \
--input "$payload"
expected_title="Source PR #$PR_NUMBER · $HEAD_SHA · dispatch $dispatch_nonce"
run_id=""
for _ in $(seq 1 20); do
runs="$(
gh api --method GET \
"/repos/$PRIVATE_CI_OWNER/$PRIVATE_CI_REPOSITORY/actions/workflows/premerge.yml/runs?event=workflow_dispatch&per_page=100"
)"
run_id="$(
jq -r \
--arg title "$expected_title" \
'[
.workflow_runs[]
| select(.display_title == $title)
]
| sort_by(.created_at)
| last
| .id // empty' <<<"$runs"
)"
if [[ "$run_id" =~ ^[1-9][0-9]*$ ]]; then
break
fi
sleep 3
done
if ! [[ "$run_id" =~ ^[1-9][0-9]*$ ]]; then
echo "::error::The exact dispatched Internal CI run was not found."
exit 1
fi
echo "::add-mask::$run_id"
{
echo "run_id=$run_id"
echo "dispatch_nonce=$dispatch_nonce"
} >> "$GITHUB_OUTPUT"
- name: Wait for the exact Internal CI result
id: private_result
env:
GH_TOKEN: ${{ secrets.TRTMC_CI_DISPATCH_TOKEN }}
PRIVATE_CI_OWNER: ${{ secrets.TRTMC_PRIVATE_CI_OWNER }}
PRIVATE_CI_REPOSITORY: ${{ secrets.TRTMC_PRIVATE_CI_REPOSITORY }}
RUN_ID: ${{ steps.private_run.outputs.run_id }}
run: |
set -euo pipefail
[[ "$RUN_ID" =~ ^[1-9][0-9]*$ ]]
# The exact private run can queue behind GPU work. Let the job-level
# timeout bound this wait instead of misreporting queue delay as a
# terminal Internal CI failure.
while true; do
run="$(
gh api --method GET \
"/repos/$PRIVATE_CI_OWNER/$PRIVATE_CI_REPOSITORY/actions/runs/$RUN_ID"
)"
status="$(jq -er '.status' <<<"$run")"
if [ "$status" = "completed" ]; then
conclusion="$(jq -r '.conclusion // "unknown"' <<<"$run")"
case "$conclusion" in
success|failure|cancelled|timed_out|skipped|neutral|action_required) ;;
*) conclusion=unknown ;;
esac
echo "conclusion=$conclusion" >> "$GITHUB_OUTPUT"
exit 0
fi
sleep 15
done
publish:
name: Publish automated result
needs:
- authorize
- announce
- dispatch
if: ${{ always() && needs.authorize.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
actions: read
pull-requests: write
statuses: write
steps:
- name: Resolve the contributor-visible result
id: result
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
DISPATCH_RESULT: ${{ needs.dispatch.result }}
PRIVATE_CONCLUSION: ${{ needs.dispatch.outputs.private_conclusion }}
run: |
set -euo pipefail
state=failure
description="Automated internal CI did not complete; details withheld"
publish_comment=true
# Main may advance while this exact PR head is in CI. pull.base.sha
# and the synthetic merge's first parent refresh independently and
# are not comparable snapshot identities. Authorization already
# binds the downstream run to the tested merge, so publication is
# invalidated only when the PR itself is superseded.
snapshot_current=false
if pull="$(gh api --method GET "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")"; then
current_state="$(jq -r '.state // ""' <<<"$pull")"
current_repo="$(jq -r '.base.repo.full_name // ""' <<<"$pull")"
current_ref="$(jq -r '.base.ref // ""' <<<"$pull")"
current_head="$(jq -r '.head.sha // ""' <<<"$pull")"
if [ "$current_state" = "open" ] \
&& [ "$current_repo" = "$GITHUB_REPOSITORY" ] \
&& [ "$current_ref" = "main" ] \
&& [ "$current_head" = "$HEAD_SHA" ]; then
snapshot_current=true
elif [ "$current_head" != "$HEAD_SHA" ]; then
description="Automated internal CI result was superseded by a newer PR head"
publish_comment=false
fi
fi
if [ "$snapshot_current" = "true" ] && [ "$DISPATCH_RESULT" = "success" ]; then
case "$PRIVATE_CONCLUSION" in
success)
state=success
description="Automated internal CI passed"
publish_comment=false
;;
failure)
description="Automated internal CI failed; details withheld"
;;
cancelled|timed_out|skipped|neutral|action_required|unknown|"")
description="Automated internal CI did not complete; details withheld"
;;
esac
fi
{
echo "state=$state"
echo "description=$description"
echo "publish_comment=$publish_comment"
} >> "$GITHUB_OUTPUT"
- name: Prepare the contributor-visible log and comment
if: ${{ steps.result.outputs.state != 'success' }}
env:
PUBLIC_FAILURE_LOG: ${{ runner.temp }}/contributor-result/public-failure.log
COMMENT_BODY: ${{ runner.temp }}/internal-ci-comment.md
DESCRIPTION: ${{ steps.result.outputs.description }}
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
run: |
set -euo pipefail
mkdir -p "$(dirname "$PUBLIC_FAILURE_LOG")"
{
echo "TRTMC Protected CI result"
echo "========================="
echo
echo "Status: FAILED"
echo "Pull request: #$PR_NUMBER"
echo "Head commit: $HEAD_SHA"
echo "Reason: $DESCRIPTION"
echo
echo "Protected failure details are not transferred to the public repository."
} > "$PUBLIC_FAILURE_LOG"
{
echo '<!-- trtmc-internal-ci-result -->'
echo 'This is an automated Internal CI result; no review from an individual maintainer is requested.'
echo
echo '```text'
sed -n '1,240p' "$PUBLIC_FAILURE_LOG"
echo '```'
echo
echo 'Open the public Source Actions run from the automated status link above.'
} > "$COMMENT_BODY"
[ "$(wc -c < "$COMMENT_BODY")" -le 60000 ]
- name: Publish the terminal automated status
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
STATE: ${{ steps.result.outputs.state }}
DESCRIPTION: ${{ steps.result.outputs.description }}
run: |
set -euo pipefail
gh api --silent --method POST \
"/repos/$GITHUB_REPOSITORY/statuses/$HEAD_SHA" \
-f state="$STATE" \
-f context="TRTMC Internal CI / Automated premerge gate" \
-f description="$DESCRIPTION" \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
- name: Print public-failure.log
if: ${{ steps.result.outputs.state != 'success' }}
run: cat "${{ runner.temp }}/contributor-result/public-failure.log"
- name: Upload public failure summary
if: ${{ steps.result.outputs.state != 'success' }}
continue-on-error: true
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: public-failure-log
path: ${{ runner.temp }}/contributor-result/public-failure.log
if-no-files-found: error
retention-days: 7
- name: Upsert the automated failure comment
if: ${{ steps.result.outputs.state != 'success' && steps.result.outputs.publish_comment == 'true' }}
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
COMMENT_BODY: ${{ runner.temp }}/internal-ci-comment.md
run: |
set -euo pipefail
mapfile -t comment_ids < <(
gh api --paginate \
"/repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments?per_page=100" \
--jq '.[] | select(.user.login == "github-actions[bot]") | select(.body | contains("<!-- trtmc-internal-ci-result -->")) | .id'
)
payload="$RUNNER_TEMP/internal-ci-comment.json"
jq -n --rawfile body "$COMMENT_BODY" '{body: $body}' > "$payload"
if [ "${#comment_ids[@]}" -eq 0 ]; then
gh api --silent --method POST \
"/repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
--input "$payload"
else
gh api --silent --method PATCH \
"/repos/$GITHUB_REPOSITORY/issues/comments/${comment_ids[0]}" \
--input "$payload"
for comment_id in "${comment_ids[@]:1}"; do
gh api --silent --method DELETE \
"/repos/$GITHUB_REPOSITORY/issues/comments/$comment_id" || true
done
fi
- name: Preserve the failed automated verdict
if: ${{ steps.result.outputs.state != 'success' }}
run: exit 1