Skip to content

Commit f9480c8

Browse files
committed
feat(ci): add SDK proto sync CI with drift detection and dashboard
Add automated proto drift detection for multi-language SDKs: - sdk_sync.py: Python script with three subcommands (drift-report, dashboard, issue-body) for testable, lintable sync logic - sdk_sync_test.py: 12 pytest tests covering drift detection, dashboard generation, issue body formatting, and log truncation - go:proto:drift mise task: calls sdk_sync.py drift-report - go:proto:build-check mise task: runs full sync+gen+build+test pipeline - sdk-proto-check.yml: PR gate workflow with non-blocking warning annotations - sdk-sync-dashboard.yml: scheduled daily workflow for wiki dashboard, agent-actionable issue creation, and repository_dispatch Workflows are thin orchestration (gh CLI for GitHub API). All data processing and formatting is in the Python script, following the existing tasks/scripts/ pattern (release.py, sync_docs_website.py). Signed-off-by: Roland Huß <rhuss@redhat.com>
1 parent d556748 commit f9480c8

5 files changed

Lines changed: 894 additions & 0 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: SDK Proto Check
2+
3+
on:
4+
push:
5+
branches:
6+
- "pull-request/[0-9]+"
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
packages: read
12+
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
pr_metadata:
19+
name: Resolve PR metadata
20+
runs-on: ubuntu-latest
21+
permissions:
22+
contents: read
23+
pull-requests: read
24+
outputs:
25+
should_run: ${{ steps.gate.outputs.should_run }}
26+
steps:
27+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
28+
29+
- id: gate
30+
uses: ./.github/actions/pr-gate
31+
32+
sdk_proto_drift:
33+
name: Proto Drift (${{ matrix.sdk.name }})
34+
needs: pr_metadata
35+
if: needs.pr_metadata.outputs.should_run == 'true'
36+
runs-on: ubuntu-latest
37+
container:
38+
image: ghcr.io/nvidia/openshell/ci:latest
39+
credentials:
40+
username: ${{ github.actor }}
41+
password: ${{ secrets.GITHUB_TOKEN }}
42+
strategy:
43+
fail-fast: false
44+
matrix:
45+
sdk:
46+
- name: go
47+
drift_task: "go:proto:drift"
48+
steps:
49+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
50+
51+
- name: Install tools
52+
run: mise install --locked
53+
54+
- name: Check proto drift
55+
id: drift
56+
run: |
57+
REPORT=$(uv run python tasks/scripts/sdk_sync.py drift-report \
58+
--sdk ${{ matrix.sdk.name }} \
59+
--upstream-path proto \
60+
--sdk-path sdk/${{ matrix.sdk.name }}/proto 2>&1) || true
61+
62+
if echo "$REPORT" | jq -e '.synced' >/dev/null 2>&1; then
63+
SYNCED=$(echo "$REPORT" | jq -r '.synced')
64+
{
65+
echo "report<<REPORT_EOF"
66+
echo "$REPORT"
67+
echo "REPORT_EOF"
68+
} >> "$GITHUB_OUTPUT"
69+
echo "synced=$SYNCED" >> "$GITHUB_OUTPUT"
70+
else
71+
echo "::warning::Proto drift check failed: $REPORT"
72+
echo "synced=error" >> "$GITHUB_OUTPUT"
73+
fi
74+
75+
- name: Annotate drift warning
76+
if: steps.drift.outputs.synced == 'false'
77+
env:
78+
DRIFT_REPORT: ${{ steps.drift.outputs.report }}
79+
SDK_NAME: ${{ matrix.sdk.name }}
80+
run: |
81+
SUMMARY=$(echo "$DRIFT_REPORT" | jq -r '.summary')
82+
FILES=$(echo "$DRIFT_REPORT" | jq -r '.files[] | select(.status != "synced") | " - \(.name) (\(.status), \(.diff_lines) lines changed)"' | sed ':a;N;$!ba;s/\n/%0A/g')
83+
echo "::warning::SDK proto drift detected for ${SDK_NAME}: ${SUMMARY}%0A${FILES}"
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
name: SDK Sync Dashboard
2+
3+
on:
4+
schedule:
5+
- cron: "0 6 * * *"
6+
workflow_dispatch:
7+
8+
permissions:
9+
actions: read
10+
contents: write
11+
packages: read
12+
issues: write
13+
14+
concurrency:
15+
group: sdk-sync-dashboard
16+
cancel-in-progress: true
17+
18+
jobs:
19+
sdk_sync_check:
20+
name: Sync Check (${{ matrix.sdk.name }})
21+
runs-on: ubuntu-latest
22+
container:
23+
image: ghcr.io/nvidia/openshell/ci:latest
24+
credentials:
25+
username: ${{ github.actor }}
26+
password: ${{ secrets.GITHUB_TOKEN }}
27+
strategy:
28+
fail-fast: false
29+
matrix:
30+
sdk:
31+
- name: go
32+
build_task: "go:proto:build-check"
33+
label: "sdk-sync:go"
34+
outputs:
35+
drift_report: ${{ steps.drift.outputs.report }}
36+
has_drift: ${{ steps.drift.outputs.has_drift }}
37+
build_report: ${{ steps.build.outputs.report || '' }}
38+
build_failed: ${{ steps.build.outputs.build_failed || 'false' }}
39+
steps:
40+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
41+
42+
- name: Install tools
43+
run: mise install --locked
44+
45+
- name: Check proto drift
46+
id: drift
47+
run: |
48+
REPORT=$(uv run python tasks/scripts/sdk_sync.py drift-report \
49+
--sdk ${{ matrix.sdk.name }} \
50+
--upstream-path proto \
51+
--sdk-path sdk/${{ matrix.sdk.name }}/proto 2>&1) || true
52+
53+
if echo "$REPORT" | jq -e '.synced' >/dev/null 2>&1; then
54+
SYNCED=$(echo "$REPORT" | jq -r '.synced')
55+
{
56+
echo "report<<REPORT_EOF"
57+
echo "$REPORT"
58+
echo "REPORT_EOF"
59+
} >> "$GITHUB_OUTPUT"
60+
if [ "$SYNCED" = "true" ]; then
61+
echo "has_drift=false" >> "$GITHUB_OUTPUT"
62+
else
63+
echo "has_drift=true" >> "$GITHUB_OUTPUT"
64+
fi
65+
else
66+
echo "::error::Proto drift check failed: $REPORT"
67+
echo "report={}" >> "$GITHUB_OUTPUT"
68+
echo "has_drift=error" >> "$GITHUB_OUTPUT"
69+
fi
70+
71+
- name: Run build check
72+
id: build
73+
if: steps.drift.outputs.has_drift == 'true'
74+
env:
75+
BUILD_TASK: ${{ matrix.sdk.build_task }}
76+
run: |
77+
if REPORT=$(mise run "$BUILD_TASK" 2>&1); then
78+
{
79+
echo "report<<REPORT_EOF"
80+
echo "$REPORT"
81+
echo "REPORT_EOF"
82+
} >> "$GITHUB_OUTPUT"
83+
echo "build_failed=false" >> "$GITHUB_OUTPUT"
84+
else
85+
if echo "$REPORT" | jq -e '.sdk' >/dev/null 2>&1; then
86+
{
87+
echo "report<<REPORT_EOF"
88+
echo "$REPORT"
89+
echo "REPORT_EOF"
90+
} >> "$GITHUB_OUTPUT"
91+
else
92+
echo "report={}" >> "$GITHUB_OUTPUT"
93+
fi
94+
echo "build_failed=true" >> "$GITHUB_OUTPUT"
95+
fi
96+
97+
wiki_dashboard:
98+
name: Update Wiki Dashboard
99+
needs: sdk_sync_check
100+
if: always() && needs.sdk_sync_check.result != 'cancelled'
101+
runs-on: ubuntu-latest
102+
steps:
103+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
104+
105+
- name: Install tools
106+
run: mise install --locked
107+
108+
- name: Generate dashboard
109+
id: gen
110+
env:
111+
DRIFT_REPORT: ${{ needs.sdk_sync_check.outputs.drift_report }}
112+
BUILD_REPORT: ${{ needs.sdk_sync_check.outputs.build_report }}
113+
run: |
114+
WIKI_DIR=$(mktemp -d)
115+
echo "wiki_dir=$WIKI_DIR" >> "$GITHUB_OUTPUT"
116+
117+
uv run python tasks/scripts/sdk_sync.py dashboard \
118+
--drift-report "${DRIFT_REPORT:-{}}" \
119+
--build-report "${BUILD_REPORT:-{}}" \
120+
--output "$WIKI_DIR/SDK-Sync-Status.md"
121+
122+
- name: Push to wiki
123+
env:
124+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
125+
WIKI_DIR: ${{ steps.gen.outputs.wiki_dir }}
126+
run: |
127+
CLONE_DIR=$(mktemp -d)
128+
trap 'rm -rf "$CLONE_DIR"' EXIT
129+
130+
if ! git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.wiki.git" "$CLONE_DIR" 2>/dev/null; then
131+
echo "::warning::Failed to clone wiki repository. Dashboard update skipped."
132+
exit 0
133+
fi
134+
135+
cp "$WIKI_DIR/SDK-Sync-Status.md" "$CLONE_DIR/"
136+
cd "$CLONE_DIR"
137+
git config user.name "github-actions[bot]"
138+
git config user.email "github-actions[bot]@users.noreply.github.com"
139+
git add SDK-Sync-Status.md
140+
141+
if git diff --cached --quiet; then
142+
echo "No changes to wiki dashboard."
143+
exit 0
144+
fi
145+
146+
TIMESTAMP=$(date -u '+%Y-%m-%d %H:%M UTC')
147+
git commit -m "Update SDK sync dashboard ($TIMESTAMP)"
148+
149+
if ! git push 2>/dev/null; then
150+
echo "::warning::Failed to push wiki update. Dashboard update skipped."
151+
exit 0
152+
fi
153+
154+
echo "Wiki dashboard updated successfully."
155+
156+
issue_management:
157+
name: Manage Drift Issues
158+
needs: sdk_sync_check
159+
if: needs.sdk_sync_check.outputs.has_drift == 'true' && needs.sdk_sync_check.outputs.build_failed == 'true'
160+
runs-on: ubuntu-latest
161+
outputs:
162+
issue_url: ${{ steps.issue.outputs.issue_url }}
163+
steps:
164+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
165+
166+
- name: Install tools
167+
run: mise install --locked
168+
169+
- name: Create or update drift issue
170+
id: issue
171+
env:
172+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
173+
DRIFT_REPORT: ${{ needs.sdk_sync_check.outputs.drift_report }}
174+
BUILD_REPORT: ${{ needs.sdk_sync_check.outputs.build_report }}
175+
GH_REPO: ${{ github.repository }}
176+
run: |
177+
set -euo pipefail
178+
179+
SDK="go"
180+
LABEL="sdk-sync:go"
181+
182+
# Ensure label exists
183+
if ! gh label view "$LABEL" --repo "$GH_REPO" >/dev/null 2>&1; then
184+
gh label create "$LABEL" \
185+
--repo "$GH_REPO" \
186+
--description "Proto drift detected for $SDK SDK" \
187+
--color "D93F0B" 2>/dev/null || true
188+
fi
189+
190+
# Generate issue body via Python script
191+
ISSUE_BODY=$(uv run python tasks/scripts/sdk_sync.py issue-body \
192+
--drift-report "${DRIFT_REPORT:-{}}" \
193+
--build-report "${BUILD_REPORT:-{}}" \
194+
--sdk "$SDK")
195+
196+
ISSUE_TITLE="SDK proto drift: $SDK"
197+
198+
# Check for existing open issue
199+
EXISTING_URL=$(gh issue list \
200+
--repo "$GH_REPO" \
201+
--label "$LABEL" \
202+
--state open \
203+
--json url \
204+
--jq '.[0].url // empty' 2>/dev/null) || true
205+
206+
if [ -n "$EXISTING_URL" ]; then
207+
ISSUE_NUM=$(basename "$EXISTING_URL")
208+
if gh issue edit "$ISSUE_NUM" \
209+
--repo "$GH_REPO" \
210+
--body "$ISSUE_BODY" 2>/dev/null; then
211+
echo "Updated existing issue: $EXISTING_URL"
212+
echo "issue_url=$EXISTING_URL" >> "$GITHUB_OUTPUT"
213+
else
214+
echo "::warning::Failed to update issue (possible rate limit)"
215+
echo "issue_url=" >> "$GITHUB_OUTPUT"
216+
fi
217+
else
218+
if CREATED_URL=$(gh issue create \
219+
--repo "$GH_REPO" \
220+
--title "$ISSUE_TITLE" \
221+
--body "$ISSUE_BODY" \
222+
--label "$LABEL" 2>/dev/null); then
223+
echo "Created new issue: $CREATED_URL"
224+
echo "issue_url=$CREATED_URL" >> "$GITHUB_OUTPUT"
225+
else
226+
echo "::warning::Failed to create drift issue (possible rate limit)"
227+
echo "issue_url=" >> "$GITHUB_OUTPUT"
228+
fi
229+
fi
230+
231+
- name: Fire repository_dispatch
232+
if: steps.issue.outputs.issue_url != ''
233+
env:
234+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
235+
GH_REPO: ${{ github.repository }}
236+
ISSUE_URL: ${{ steps.issue.outputs.issue_url }}
237+
DRIFT_REPORT: ${{ needs.sdk_sync_check.outputs.drift_report }}
238+
run: |
239+
set -euo pipefail
240+
241+
SDK="go"
242+
DRIFT_SUMMARY="unknown"
243+
if [ -n "$DRIFT_REPORT" ] && [ "$DRIFT_REPORT" != "{}" ]; then
244+
DRIFT_SUMMARY=$(echo "$DRIFT_REPORT" | jq -r '.summary // "unknown"')
245+
fi
246+
247+
PAYLOAD=$(jq -n -c \
248+
--arg issue_url "$ISSUE_URL" \
249+
--arg sdk "$SDK" \
250+
--arg drift_summary "$DRIFT_SUMMARY" \
251+
'{issue_url: $issue_url, sdk: $sdk, drift_summary: $drift_summary}')
252+
253+
gh api "repos/$GH_REPO/dispatches" \
254+
--method POST \
255+
-f event_type="sdk-sync-drift" \
256+
--argjson client_payload "$PAYLOAD" 2>/dev/null || {
257+
echo "::warning::Failed to fire repository_dispatch"
258+
}

‎tasks/go.toml‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# Go SDK proto drift detection tasks
5+
# NOTE: This file is merged with the full tasks/go.toml from PR #2271.
6+
# The drift and build-check tasks below extend the Go SDK task set.
7+
8+
["go:proto:drift"]
9+
description = "Check Go SDK proto files for drift against root proto definitions"
10+
run = "uv run python tasks/scripts/sdk_sync.py drift-report --sdk go --upstream-path proto --sdk-path sdk/go/proto"
11+
hide = true
12+
13+
["go:proto:build-check"]
14+
description = "Run full Go SDK proto sync, generate, build, and test pipeline"
15+
dir = "sdk/go"
16+
run = '''
17+
#!/usr/bin/env bash
18+
set -euo pipefail
19+
20+
STEP_NAMES=("sync" "gen" "build" "test")
21+
STEP_TASKS=("go:proto:sync" "go:proto:gen" "go:build" "go:test")
22+
LOG_FILE=$(mktemp)
23+
trap 'rm -f "$LOG_FILE"' EXIT
24+
25+
FAILED_STEP=""
26+
for i in "${!STEP_NAMES[@]}"; do
27+
STEP="${STEP_NAMES[$i]}"
28+
TASK="${STEP_TASKS[$i]}"
29+
30+
if ! mise run "$TASK" > "$LOG_FILE" 2>&1; then
31+
FAILED_STEP="$STEP"
32+
break
33+
fi
34+
done
35+
36+
LOG_CONTENT=$(tail -500 "$LOG_FILE")
37+
38+
if [ -z "$FAILED_STEP" ]; then
39+
jq -n -c --arg sdk "go" \
40+
'{sdk: $sdk, success: true, failed_step: null, log: ""}'
41+
else
42+
jq -n -c --arg sdk "go" --arg step "$FAILED_STEP" --arg log "$LOG_CONTENT" \
43+
'{sdk: $sdk, success: false, failed_step: $step, log: $log}'
44+
exit 1
45+
fi
46+
'''
47+
hide = true

0 commit comments

Comments
 (0)