From feb9a2d7fce26438c351bf7b189af7f8766be3cf Mon Sep 17 00:00:00 2001 From: Bryan Cox Date: Fri, 6 Mar 2026 06:37:42 -0500 Subject: [PATCH] feat: add dependabot-triage periodic job for HyperShift Co-Authored-By: Claude Opus 4.6 --- .../hypershift/openshift-hypershift-main.yaml | 4 + .../openshift-hypershift-main-periodics.yaml | 70 ++- .../hypershift/dependabot-triage/OWNERS | 13 + ...t-dependabot-triage-workflow.metadata.json | 20 + ...hypershift-dependabot-triage-workflow.yaml | 21 + .../dependabot-triage/process/OWNERS | 13 + ...hift-dependabot-triage-process-commands.sh | 574 ++++++++++++++++++ ...ependabot-triage-process-ref.metadata.json | 20 + ...ershift-dependabot-triage-process-ref.yaml | 45 ++ .../dependabot-triage/report/OWNERS | 13 + ...shift-dependabot-triage-report-commands.sh | 232 +++++++ ...dependabot-triage-report-ref.metadata.json | 20 + ...pershift-dependabot-triage-report-ref.yaml | 11 + .../hypershift/dependabot-triage/setup/OWNERS | 13 + ...rshift-dependabot-triage-setup-commands.sh | 10 + ...-dependabot-triage-setup-ref.metadata.json | 20 + ...ypershift-dependabot-triage-setup-ref.yaml | 34 ++ ...01-21-dependabot-triage-prow-job-design.md | 248 ++++++++ 18 files changed, 1379 insertions(+), 2 deletions(-) create mode 100644 ci-operator/step-registry/hypershift/dependabot-triage/OWNERS create mode 100644 ci-operator/step-registry/hypershift/dependabot-triage/hypershift-dependabot-triage-workflow.metadata.json create mode 100644 ci-operator/step-registry/hypershift/dependabot-triage/hypershift-dependabot-triage-workflow.yaml create mode 100644 ci-operator/step-registry/hypershift/dependabot-triage/process/OWNERS create mode 100755 ci-operator/step-registry/hypershift/dependabot-triage/process/hypershift-dependabot-triage-process-commands.sh create mode 100644 ci-operator/step-registry/hypershift/dependabot-triage/process/hypershift-dependabot-triage-process-ref.metadata.json create mode 100644 ci-operator/step-registry/hypershift/dependabot-triage/process/hypershift-dependabot-triage-process-ref.yaml create mode 100644 ci-operator/step-registry/hypershift/dependabot-triage/report/OWNERS create mode 100644 ci-operator/step-registry/hypershift/dependabot-triage/report/hypershift-dependabot-triage-report-commands.sh create mode 100644 ci-operator/step-registry/hypershift/dependabot-triage/report/hypershift-dependabot-triage-report-ref.metadata.json create mode 100644 ci-operator/step-registry/hypershift/dependabot-triage/report/hypershift-dependabot-triage-report-ref.yaml create mode 100644 ci-operator/step-registry/hypershift/dependabot-triage/setup/OWNERS create mode 100755 ci-operator/step-registry/hypershift/dependabot-triage/setup/hypershift-dependabot-triage-setup-commands.sh create mode 100644 ci-operator/step-registry/hypershift/dependabot-triage/setup/hypershift-dependabot-triage-setup-ref.metadata.json create mode 100644 ci-operator/step-registry/hypershift/dependabot-triage/setup/hypershift-dependabot-triage-setup-ref.yaml create mode 100644 docs/plans/2026-01-21-dependabot-triage-prow-job-design.md diff --git a/ci-operator/config/openshift/hypershift/openshift-hypershift-main.yaml b/ci-operator/config/openshift/hypershift/openshift-hypershift-main.yaml index 2683a4e20d334..47690f2c8c368 100644 --- a/ci-operator/config/openshift/hypershift/openshift-hypershift-main.yaml +++ b/ci-operator/config/openshift/hypershift/openshift-hypershift-main.yaml @@ -489,6 +489,10 @@ tests: dependencies: OPENSHIFT_INSTALL_RELEASE_IMAGE_OVERRIDE: release:latest-421 workflow: hypershift-aws-reqserving-e2e +- as: dependabot-triage + cron: 0 12 * * 5 + steps: + workflow: hypershift-dependabot-triage - as: periodic-jira-agent cron: 30 8 * * 1 steps: diff --git a/ci-operator/jobs/openshift/hypershift/openshift-hypershift-main-periodics.yaml b/ci-operator/jobs/openshift/hypershift/openshift-hypershift-main-periodics.yaml index b18ab0d179b3c..7a75d4d0943bf 100644 --- a/ci-operator/jobs/openshift/hypershift/openshift-hypershift-main-periodics.yaml +++ b/ci-operator/jobs/openshift/hypershift/openshift-hypershift-main-periodics.yaml @@ -1,4 +1,70 @@ periodics: +- agent: kubernetes + cluster: build05 + cron: 0 12 * * 5 + decorate: true + extra_refs: + - base_ref: main + org: openshift + repo: hypershift + labels: + ci.openshift.io/generator: prowgen + pj-rehearse.openshift.io/can-be-rehearsed: "true" + name: periodic-ci-openshift-hypershift-main-dependabot-triage + spec: + containers: + - args: + - --gcs-upload-secret=/secrets/gcs/service-account.json + - --image-import-pull-secret=/etc/pull-secret/.dockerconfigjson + - --report-credentials-file=/etc/report/credentials + - --secret-dir=/secrets/ci-pull-credentials + - --target=dependabot-triage + command: + - ci-operator + env: + - name: HTTP_SERVER_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + image: quay-proxy.ci.openshift.org/openshift/ci:ci_ci-operator_latest + imagePullPolicy: Always + name: "" + ports: + - containerPort: 8080 + name: http + resources: + requests: + cpu: 10m + volumeMounts: + - mountPath: /secrets/ci-pull-credentials + name: ci-pull-credentials + readOnly: true + - mountPath: /secrets/gcs + name: gcs-credentials + readOnly: true + - mountPath: /secrets/manifest-tool + name: manifest-tool-local-pusher + readOnly: true + - mountPath: /etc/pull-secret + name: pull-secret + readOnly: true + - mountPath: /etc/report + name: result-aggregator + readOnly: true + serviceAccountName: ci-operator + volumes: + - name: ci-pull-credentials + secret: + secretName: ci-pull-credentials + - name: manifest-tool-local-pusher + secret: + secretName: manifest-tool-local-pusher + - name: pull-secret + secret: + secretName: registry-pull-credentials + - name: result-aggregator + secret: + secretName: result-aggregator - agent: kubernetes cluster: build10 cron: '*/30 * * * *' @@ -164,7 +230,7 @@ periodics: secret: secretName: result-aggregator - agent: kubernetes - cluster: build08 + cluster: build10 cron: 30 8 * * 1 decorate: true extra_refs: @@ -230,7 +296,7 @@ periodics: secret: secretName: result-aggregator - agent: kubernetes - cluster: build08 + cluster: build10 cron: 0 8-23/3 * * 1-5 decorate: true extra_refs: diff --git a/ci-operator/step-registry/hypershift/dependabot-triage/OWNERS b/ci-operator/step-registry/hypershift/dependabot-triage/OWNERS new file mode 100644 index 0000000000000..a0c4da21c466b --- /dev/null +++ b/ci-operator/step-registry/hypershift/dependabot-triage/OWNERS @@ -0,0 +1,13 @@ +approvers: +- csrwng +- enxebre +- sjenning +- LiangquanLi930 +- bryan-cox +- jparrill +reviewers: +- csrwng +- enxebre +- sjenning +- bryan-cox +- jparrill diff --git a/ci-operator/step-registry/hypershift/dependabot-triage/hypershift-dependabot-triage-workflow.metadata.json b/ci-operator/step-registry/hypershift/dependabot-triage/hypershift-dependabot-triage-workflow.metadata.json new file mode 100644 index 0000000000000..675e31d0724c6 --- /dev/null +++ b/ci-operator/step-registry/hypershift/dependabot-triage/hypershift-dependabot-triage-workflow.metadata.json @@ -0,0 +1,20 @@ +{ + "path": "hypershift/dependabot-triage/hypershift-dependabot-triage-workflow.yaml", + "owners": { + "approvers": [ + "csrwng", + "enxebre", + "sjenning", + "LiangquanLi930", + "bryan-cox", + "jparrill" + ], + "reviewers": [ + "csrwng", + "enxebre", + "sjenning", + "bryan-cox", + "jparrill" + ] + } +} \ No newline at end of file diff --git a/ci-operator/step-registry/hypershift/dependabot-triage/hypershift-dependabot-triage-workflow.yaml b/ci-operator/step-registry/hypershift/dependabot-triage/hypershift-dependabot-triage-workflow.yaml new file mode 100644 index 0000000000000..ebe4eef3e6ea8 --- /dev/null +++ b/ci-operator/step-registry/hypershift/dependabot-triage/hypershift-dependabot-triage-workflow.yaml @@ -0,0 +1,21 @@ +workflow: + as: hypershift-dependabot-triage + steps: + pre: + - ref: hypershift-dependabot-triage-setup + test: + - ref: hypershift-dependabot-triage-process + post: + - ref: hypershift-dependabot-triage-report + documentation: |- + Periodic workflow that triages dependabot PRs in the HyperShift repository. + + This workflow: + 1. Setup: Clones HyperShift repo from fork, configures git credentials, sets up Claude CLI + 2. Process: Queries open dependabot PRs, invokes Claude to process and consolidate into single PR + 3. Report: Generates HTML report with token usage, cost estimates, and processing details + + The workflow uses the /fix-hypershift-repo-robot-pr command from HyperShift in non-interactive mode. + All dependabot PRs are consolidated into a single PR with organized commits. + + Schedule: Weekly on Fridays at 12:00 UTC (7:00 AM ET) diff --git a/ci-operator/step-registry/hypershift/dependabot-triage/process/OWNERS b/ci-operator/step-registry/hypershift/dependabot-triage/process/OWNERS new file mode 100644 index 0000000000000..a0c4da21c466b --- /dev/null +++ b/ci-operator/step-registry/hypershift/dependabot-triage/process/OWNERS @@ -0,0 +1,13 @@ +approvers: +- csrwng +- enxebre +- sjenning +- LiangquanLi930 +- bryan-cox +- jparrill +reviewers: +- csrwng +- enxebre +- sjenning +- bryan-cox +- jparrill diff --git a/ci-operator/step-registry/hypershift/dependabot-triage/process/hypershift-dependabot-triage-process-commands.sh b/ci-operator/step-registry/hypershift/dependabot-triage/process/hypershift-dependabot-triage-process-commands.sh new file mode 100755 index 0000000000000..3e927e01127e4 --- /dev/null +++ b/ci-operator/step-registry/hypershift/dependabot-triage/process/hypershift-dependabot-triage-process-commands.sh @@ -0,0 +1,574 @@ +#!/bin/bash +set -euo pipefail +export GOTOOLCHAIN=auto + +echo "=== HyperShift Dependabot Triage Process ===" + +# Generate GitHub App installation token +echo "Generating GitHub App token..." + +GITHUB_APP_CREDS_DIR="/var/run/claude-code-service-account" +APP_ID_FILE="${GITHUB_APP_CREDS_DIR}/app-id" +INSTALLATION_ID_FILE="${GITHUB_APP_CREDS_DIR}/installation-id" +PRIVATE_KEY_FILE="${GITHUB_APP_CREDS_DIR}/private-key" +INSTALLATION_ID_UPSTREAM_FILE="${GITHUB_APP_CREDS_DIR}/o-h-installation-id" + +# Check if all required credentials exist +if [ ! -f "$APP_ID_FILE" ] || [ ! -f "$INSTALLATION_ID_FILE" ] || [ ! -f "$PRIVATE_KEY_FILE" ] || [ ! -f "$INSTALLATION_ID_UPSTREAM_FILE" ]; then + echo "GitHub App credentials not yet available in ${GITHUB_APP_CREDS_DIR}" + echo "Available files:" + ls -la "${GITHUB_APP_CREDS_DIR}/" || echo "Directory does not exist" + echo "" + echo "Waiting for Vault secretsync to complete. The following keys are required:" + echo " - app-id" + echo " - installation-id (for hypershift-community fork)" + echo " - o-h-installation-id (for openshift/hypershift upstream)" + echo " - private-key" + echo "" + echo "Exiting gracefully. Re-run once secrets are synced." + exit 0 +fi + +APP_ID=$(cat "$APP_ID_FILE") +INSTALLATION_ID_FORK=$(cat "$INSTALLATION_ID_FILE") +INSTALLATION_ID_UPSTREAM=$(cat "$INSTALLATION_ID_UPSTREAM_FILE") + +# Function to generate GitHub App token for a given installation ID +generate_github_token() { + local INSTALL_ID=$1 + local NOW + local IAT + local EXP + local HEADER + local PAYLOAD + local SIGNATURE + local JWT + + NOW=$(date +%s) + IAT=$((NOW - 60)) + EXP=$((NOW + 600)) + + HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n') + PAYLOAD=$(echo -n "{\"iat\":${IAT},\"exp\":${EXP},\"iss\":\"${APP_ID}\"}" | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n') + SIGNATURE=$(echo -n "${HEADER}.${PAYLOAD}" | openssl dgst -sha256 -sign "$PRIVATE_KEY_FILE" | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n') + JWT="${HEADER}.${PAYLOAD}.${SIGNATURE}" + + curl -s -X POST \ + -H "Authorization: Bearer ${JWT}" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/app/installations/${INSTALL_ID}/access_tokens" \ + | jq -r '.token' +} + +# Generate token for fork (hypershift-community/hypershift) - for pushing branches +echo "Generating GitHub App token for fork..." +GITHUB_TOKEN_FORK=$(generate_github_token "$INSTALLATION_ID_FORK") +if [ -z "$GITHUB_TOKEN_FORK" ] || [ "$GITHUB_TOKEN_FORK" = "null" ]; then + echo "ERROR: Failed to generate GitHub App token for fork" + exit 1 +fi +echo "Fork token generated successfully" + +# Generate token for upstream (openshift/hypershift) - for creating PRs +echo "Generating GitHub App token for upstream..." +GITHUB_TOKEN_UPSTREAM=$(generate_github_token "$INSTALLATION_ID_UPSTREAM") +if [ -z "$GITHUB_TOKEN_UPSTREAM" ] || [ "$GITHUB_TOKEN_UPSTREAM" = "null" ]; then + echo "ERROR: Failed to generate GitHub App token for upstream" + exit 1 +fi +echo "Upstream token generated successfully" + +# Clone HyperShift repository from fork +echo "Cloning HyperShift repository..." +mkdir -p /tmp/dependabot-triage +cd /tmp/dependabot-triage +git clone https://github.com/hypershift-community/hypershift hypershift +cd hypershift + +# Add upstream remote +echo "Adding upstream remote..." +git remote add upstream https://github.com/openshift/hypershift.git +git fetch upstream + +# Configure git +echo "Configuring git..." +git config user.name "OpenShift CI Bot" +git config user.email "ci-bot@redhat.com" + +# Configure git to use the fork token for push operations via credential helper +# Using credential helper instead of URL rewriting prevents token leaking in git remote output +git config --global credential.helper "!f() { echo username=x-access-token; echo password=${GITHUB_TOKEN_FORK}; }; f" + +# Export upstream token as GITHUB_TOKEN for gh CLI (used for PR operations) +export GITHUB_TOKEN="$GITHUB_TOKEN_UPSTREAM" +echo "GitHub App tokens configured successfully" + +# Query GitHub for open dependabot PRs +echo "Querying GitHub for open dependabot PRs..." +DEPENDABOT_PRS=$(gh pr list \ + --repo openshift/hypershift \ + --author "app/dependabot" \ + --state open \ + --json number,title,headRefName \ + --limit 50) + +# Filter out PRs that bump k8s.io or sigs.k8s.io dependencies (managed manually) +echo "Filtering out k8s.io and sigs.k8s.io dependency bumps..." +FILTERED_PRS="[]" +while IFS= read -r pr_json; do + pr_num=$(echo "$pr_json" | jq -r '.number') + pr_title=$(echo "$pr_json" | jq -r '.title') + pr_diff=$(gh api "repos/openshift/hypershift/pulls/${pr_num}/files" \ + --jq '.[] | select(.filename == "go.mod" or .filename == "api/go.mod") | .patch' 2>/dev/null || true) + if echo "$pr_diff" | grep -vE '// indirect' | grep -qE '^\+[^+].*\b(k8s\.io|sigs\.k8s\.io)/'; then + echo " Skipping PR #${pr_num}: ${pr_title} (contains k8s.io/sigs.k8s.io changes)" + else + FILTERED_PRS=$(echo "$FILTERED_PRS" | jq --argjson pr "$pr_json" '. + [$pr]') + fi +done < <(echo "$DEPENDABOT_PRS" | jq -c '.[]') +DEPENDABOT_PRS="$FILTERED_PRS" + +PR_COUNT=$(echo "$DEPENDABOT_PRS" | jq 'length') +echo "Found $PR_COUNT open dependabot PRs" + +if [ "$PR_COUNT" -eq 0 ]; then + echo "No open dependabot PRs found. Nothing to do." + exit 0 +fi + +# Extract PR numbers +PR_NUMBERS=$(echo "$DEPENDABOT_PRS" | jq -r '.[].number' | tr '\n' ' ') +echo "PR numbers to process: $PR_NUMBERS" + +# Display PR titles for logging +echo "" +echo "PRs to process:" +echo "$DEPENDABOT_PRS" | jq -r '.[] | " #\(.number): \(.title)"' +echo "" + +# Build the Claude prompt +read -r -d '' CLAUDE_PROMPT << 'PROMPT_EOF' || true +Process the following dependabot PRs and consolidate them into a single branch. + +PR Numbers: ${PR_NUMBERS} + +## Critical: Process Each PR Individually with Validation + +You MUST process each PR one at a time, validating after each before moving to the next. +This ensures we know exactly which PR fails if something breaks. + +### Phase 1: Setup +1. Create a new branch 'fix/weekly-dependabot-consolidation' from upstream/main +2. Initialize tracking lists for succeeded_prs and failed_prs + +### Phase 2: Process Each PR (one at a time, in order) + +For EACH PR in the list above, do the following steps IN ORDER: + +1. **Save current state**: Note the current HEAD commit SHA before starting this PR + +2. **Cherry-pick**: Fetch and cherry-pick the PR's commits onto the branch + - Convert commit messages to conventional format (chore(deps): ...) + - If cherry-pick fails: record PR as failed with reason, run `git reset --hard && git clean -fd` to fully revert all changes, continue to next PR + +3. **Run make verify**: Regenerate all necessary files + - Run: `make verify 2>&1 | tee /tmp/make-verify-pr.log; VERIFY_EXIT=${PIPESTATUS[0]}` + - If VERIFY_EXIT is non-zero, determine if gitlint is the ONLY failure by running: + `NON_GITLINT=$(grep 'make:.*\*\*\*' /tmp/make-verify-pr.log | grep -vi 'gitlint' || true)` + - If NON_GITLINT is empty: gitlint is the only failure, ignore it and continue + - If NON_GITLINT is NOT empty: there are real failures. Record PR as failed with the NON_GITLINT output as reason, run `git reset --hard && git clean -fd` to fully revert all changes, continue to next PR + +4. **Run UPDATE=true make test**: Update test fixtures + - If make test fails: record PR as failed with reason, run `git reset --hard && git clean -fd` to fully revert all changes, continue to next PR + +5. **Commit generated changes**: Commit any files changed by make verify/test + - Use message: "chore: regenerate files for PR #" + +6. **Record success**: Add this PR to succeeded_prs list + +7. **Move to next PR**: Repeat steps 1-6 for the next PR + +### Phase 3: Output Results and Exit + +IMPORTANT: Do NOT run final make verify/test. Do NOT push the branch. Do NOT create a PR. +The bash script that invoked you will handle final validation, push, and PR creation. + +Write the results to the file ${CLAUDE_RESULTS_FILE} using these EXACT structured markers (one per line, appended with >>): +- For each successfully processed PR: echo "SUCCEEDED_PR::" >> ${CLAUDE_RESULTS_FILE} +- For each failed PR: echo "FAILED_PR:<number>:<reason>" >> ${CLAUDE_RESULTS_FILE} + +Write each marker IMMEDIATELY after processing that PR (do not wait until the end). + +After processing all PRs, you are DONE. Exit immediately. +PROMPT_EOF + +# Create temp files before substituting into prompt +CLAUDE_OUTPUT_FILE=$(mktemp /tmp/claude-output.XXXXXX) +CLAUDE_RESULTS_FILE=$(mktemp /tmp/claude-results.XXXXXX) + +# Substitute variables into prompt +CLAUDE_PROMPT="${CLAUDE_PROMPT//\$\{PR_NUMBERS\}/$PR_NUMBERS}" +CLAUDE_PROMPT="${CLAUDE_PROMPT//\$\{CLAUDE_RESULTS_FILE\}/$CLAUDE_RESULTS_FILE}" + +echo "Invoking Claude to process and consolidate PRs..." +echo "==========================================" + +# Run Claude with explicit tool allowlist +set +e +echo "$CLAUDE_PROMPT" | claude --print \ + --model "$CLAUDE_MODEL" \ + --allowedTools "Bash,Read,Write,Edit,Grep,Glob,WebFetch,Skill,Task,TodoWrite" \ + --verbose \ + --output-format stream-json \ + --max-turns 100 \ + 2> "/tmp/claude-dependabot-output.log" \ + | tee "$CLAUDE_OUTPUT_FILE" +CLAUDE_EXIT_CODE=$? +set -e + +echo "==========================================" +echo "" + +# Extract token usage from stream-json result message (includes subagent costs) +grep '"type":"result"' "$CLAUDE_OUTPUT_FILE" \ + | head -1 \ + | jq '{ + total_cost_usd: (.total_cost_usd // 0), + duration_ms: (.duration_ms // 0), + num_turns: (.num_turns // 0), + input_tokens: (.usage.input_tokens // 0), + output_tokens: (.usage.output_tokens // 0), + cache_read_input_tokens: (.usage.cache_read_input_tokens // 0), + cache_creation_input_tokens: (.usage.cache_creation_input_tokens // 0), + model_usage: (.modelUsage // {}), + model: ((.modelUsage // {} | keys | first) // "unknown") + }' > "${SHARED_DIR}/claude-dependabot-tokens.json" 2>/dev/null \ + || echo '{"total_cost_usd":0,"duration_ms":0,"num_turns":0,"input_tokens":0,"output_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"model_usage":{},"model":"unknown"}' > "${SHARED_DIR}/claude-dependabot-tokens.json" +echo "Token usage: $(cat "${SHARED_DIR}/claude-dependabot-tokens.json")" + +# Extract Claude text output and tool usage summaries +jq -r ' + if .type == "assistant" then + .message.content[]? | + if .type == "text" then + .text // empty + elif .type == "tool_use" and .name == "Bash" then + "\n$ " + (.input.description // .input.command[:80]) + "" + else empty end + elif .type == "user" then + .message.content[]? | + if .type == "tool_result" then + (.content // "" | split("\n") | first // "") + "" + else empty end + else empty end +' "$CLAUDE_OUTPUT_FILE" > "${SHARED_DIR}/claude-dependabot-output-text.txt" 2>/dev/null || true +jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | "\(.name): \(.input | keys | join(", "))"' "$CLAUDE_OUTPUT_FILE" 2>/dev/null | sort | uniq -c | sort -rn > "${SHARED_DIR}/claude-dependabot-output-tools.txt" 2>/dev/null || true + +if [ $CLAUDE_EXIT_CODE -ne 0 ]; then + echo "==========================================" + echo "CLAUDE PROCESSING FAILED" + echo "==========================================" + echo "Exit code: $CLAUDE_EXIT_CODE" + echo "" + echo "Claude stderr log:" + cat "/tmp/claude-dependabot-output.log" 2>/dev/null || echo "(no stderr log found)" + cp "/tmp/claude-dependabot-output.log" "${ARTIFACT_DIR}/claude-stderr.log" 2>/dev/null || true + rm -f "$CLAUDE_OUTPUT_FILE" + rm -f "$CLAUDE_RESULTS_FILE" + exit 1 +fi + +echo "Claude processing completed. Starting bash-level validation..." + +# Parse structured markers from results file (written by Claude during processing) +rm -f "$CLAUDE_OUTPUT_FILE" +SUCCEEDED_PRS=$(grep -E '^SUCCEEDED_PR:' "$CLAUDE_RESULTS_FILE" || true) +FAILED_PRS=$(grep -E '^FAILED_PR:' "$CLAUDE_RESULTS_FILE" || true) +# Copy results to SHARED_DIR for report step +cp "$CLAUDE_RESULTS_FILE" "${SHARED_DIR}/dependabot-results.txt" +rm -f "$CLAUDE_RESULTS_FILE" + +SUCCEEDED_COUNT=$(echo "$SUCCEEDED_PRS" | grep -c '^SUCCEEDED_PR:' || true) +echo "Successfully processed PRs: $SUCCEEDED_COUNT" + +if [ "$SUCCEEDED_COUNT" -eq 0 ]; then + echo "No PRs were successfully processed. Nothing to do." + if [ -n "$FAILED_PRS" ]; then + echo "" + echo "Failed PRs:" + echo "$FAILED_PRS" + fi + exit 0 +fi + +# Verify the consolidation branch exists +CONSOLIDATION_BRANCH="fix/weekly-dependabot-consolidation" +if ! git rev-parse --verify "$CONSOLIDATION_BRANCH" >/dev/null 2>&1; then + echo "ERROR: Consolidation branch '$CONSOLIDATION_BRANCH' does not exist" + exit 1 +fi + +# Check for actual changes vs upstream/main +if git diff --quiet upstream/main..."$CONSOLIDATION_BRANCH"; then + echo "No actual changes between upstream/main and consolidation branch. Nothing to do." + exit 0 +fi + +# Checkout the consolidation branch +git checkout "$CONSOLIDATION_BRANCH" + +# Reorganize commits into logical groups +echo "" +echo "==========================================" +echo "Reorganizing commits into logical groups..." +echo "==========================================" +MERGE_BASE=$(git merge-base upstream/main HEAD) +echo "Merge base: $MERGE_BASE" +echo "Files changed: $(git diff --stat "$MERGE_BASE" HEAD | tail -1)" + +# Mixed reset to merge base - keeps working tree, unstages everything +git reset "$MERGE_BASE" + +# Commit 1: Root module go.mod/go.sum +git add go.mod go.sum 2>/dev/null || true +git diff --cached --quiet 2>/dev/null || git commit -m "$(cat <<'CMSG' +chore(deps): update root module dependencies + +Weekly dependabot dependency consolidation. +CMSG +)" + +# Commit 2: Root vendor/ +git add vendor/ 2>/dev/null || true +git diff --cached --quiet 2>/dev/null || git commit -m "$(cat <<'CMSG' +chore(deps): update vendored dependencies + +Vendor updates for root module dependency changes. +CMSG +)" + +# Commit 3: api/ module go.mod/go.sum +git add api/go.mod api/go.sum 2>/dev/null || true +git diff --cached --quiet 2>/dev/null || git commit -m "$(cat <<'CMSG' +chore(deps): update API module dependencies + +Weekly dependabot dependency consolidation for api/ module. +CMSG +)" + +# Commit 4: api/ vendor +git add api/vendor/ 2>/dev/null || true +git diff --cached --quiet 2>/dev/null || git commit -m "$(cat <<'CMSG' +chore(deps): update API vendored dependencies + +Vendor updates for api/ module dependency changes. +CMSG +)" + +# Commit 5: hack/tools/ module go.mod/go.sum +git add hack/tools/go.mod hack/tools/go.sum 2>/dev/null || true +git diff --cached --quiet 2>/dev/null || git commit -m "$(cat <<'CMSG' +chore(deps): update hack/tools module dependencies + +Weekly dependabot dependency consolidation for hack/tools/ module. +CMSG +)" + +# Commit 6: hack/tools/ vendor +git add hack/tools/vendor/ 2>/dev/null || true +git diff --cached --quiet 2>/dev/null || git commit -m "$(cat <<'CMSG' +chore(deps): update hack/tools vendored dependencies + +Vendor updates for hack/tools/ module dependency changes. +CMSG +)" + +# Commit 7: Regenerated CRD assets +git add cmd/install/assets/ 2>/dev/null || true +git diff --cached --quiet 2>/dev/null || git commit -m "$(cat <<'CMSG' +chore: regenerate CRD assets + +Regenerated CRD manifests after dependency updates. +CMSG +)" + +# Commit 8: Everything else +git add -A +if ! git diff --cached --quiet 2>/dev/null; then + echo "Remaining files in catch-all commit:" + git diff --cached --stat +fi +git diff --cached --quiet 2>/dev/null || git commit -m "$(cat <<'CMSG' +chore: update remaining generated files + +Additional generated file updates from dependency changes. +CMSG +)" + +echo "Reorganization complete. Commits:" +git log --oneline "$MERGE_BASE"..HEAD +echo "" + +# Run make verify - two-pass: first to fix, second to gate +echo "" +echo "==========================================" +echo "Running make verify (pass 1: fix stale generated files)..." +echo "==========================================" +make verify || true + +# Commit any changes from first pass +if ! git diff --quiet; then + git add -A + git commit -m "$(cat <<'CMSG' +chore: apply make verify fixes + +Auto-generated changes from make verify on consolidated branch. +CMSG +)" + echo "Committed make verify changes" +fi + +echo "Running make verify (pass 2: hard gate)..." +VERIFY_LOG=$(mktemp /tmp/make-verify.XXXXXX) +if ! make verify 2>&1 | tee "$VERIFY_LOG"; then + # Check if any make target OTHER than run-gitlint failed + # make failure lines look like: make: *** [Makefile:394: run-gitlint] Error 254 + NON_GITLINT_FAILURES=$(grep 'make:.*\*\*\*' "$VERIFY_LOG" | grep -vi 'gitlint' || true) + if [ -z "$NON_GITLINT_FAILURES" ]; then + echo "make verify failed due to gitlint only - ignoring" + else + echo "" + echo "==========================================" + echo "MAKE VERIFY FAILED - NO PR WILL BE CREATED" + echo "==========================================" + echo "Non-gitlint make target failures:" + echo "$NON_GITLINT_FAILURES" + cp "$VERIFY_LOG" "${ARTIFACT_DIR}/make-verify-failure.log" 2>/dev/null || true + rm -f "$VERIFY_LOG" + echo "FINAL_VERIFY_FAILED:make verify failed on consolidation branch" >> "${SHARED_DIR}/dependabot-results.txt" + exit 0 + fi +fi +rm -f "$VERIFY_LOG" + +# Run make test - two-pass: first to fix, second to gate +echo "" +echo "==========================================" +echo "Running make test (pass 1: update test fixtures)..." +echo "==========================================" +make test || true + +# Commit any changes from first pass +if ! git diff --quiet; then + git add -A + git commit -m "$(cat <<'CMSG' +chore: apply make test fixes + +Auto-generated changes from make test on consolidated branch. +CMSG +)" + echo "Committed make test changes" +fi + +echo "Running make test (pass 2: hard gate)..." +if ! make test; then + echo "" + echo "==========================================" + echo "MAKE TEST FAILED - NO PR WILL BE CREATED" + echo "==========================================" + echo "FINAL_TEST_FAILED:make test failed on consolidation branch" >> "${SHARED_DIR}/dependabot-results.txt" + exit 0 +fi + +echo "" +echo "make verify and make test passed. Pushing branch and creating PR..." + +# Refresh GitHub App tokens (originals likely expired after long processing) +echo "Refreshing GitHub App tokens..." +GITHUB_TOKEN_FORK=$(generate_github_token "$INSTALLATION_ID_FORK") +if [ -z "$GITHUB_TOKEN_FORK" ] || [ "$GITHUB_TOKEN_FORK" = "null" ]; then + echo "ERROR: Failed to refresh GitHub App token for fork" + exit 1 +fi +git config --global credential.helper "!f() { echo username=x-access-token; echo password=${GITHUB_TOKEN_FORK}; }; f" + +GITHUB_TOKEN_UPSTREAM=$(generate_github_token "$INSTALLATION_ID_UPSTREAM") +if [ -z "$GITHUB_TOKEN_UPSTREAM" ] || [ "$GITHUB_TOKEN_UPSTREAM" = "null" ]; then + echo "ERROR: Failed to refresh GitHub App token for upstream" + exit 1 +fi +export GITHUB_TOKEN="$GITHUB_TOKEN_UPSTREAM" +echo "Tokens refreshed successfully" + +# Push branch to origin +git push origin "$CONSOLIDATION_BRANCH" --force + +# Build PR body from structured markers +PR_BODY="## Summary +Weekly consolidation of dependabot dependency updates. + +## Consolidated PRs" + +while IFS= read -r line; do + if [ -z "$line" ]; then continue; fi + pr_num=$(echo "$line" | cut -d: -f2) + pr_title=$(echo "$line" | cut -d: -f3-) + PR_BODY="${PR_BODY} +- #${pr_num}: ${pr_title}" +done <<< "$SUCCEEDED_PRS" + +MERGE_BASE_PR=$(git merge-base upstream/main HEAD) +COMMIT_LIST="" +COMMIT_NUM=0 +while IFS= read -r commit_msg; do + COMMIT_NUM=$((COMMIT_NUM + 1)) + COMMIT_LIST="${COMMIT_LIST} +${COMMIT_NUM}. ${commit_msg}" +done < <(git log --format='%s' --reverse "$MERGE_BASE_PR"..HEAD) + +PR_BODY="${PR_BODY} + +## Commits${COMMIT_LIST} + +--- +Assisted-by: Claude (via Claude Code)" + +# Create PR +NEW_PR_URL=$(gh pr create \ + --repo openshift/hypershift \ + --head "hypershift-community:${CONSOLIDATION_BRANCH}" \ + --title "NO-JIRA: chore(deps): weekly dependabot consolidation" \ + --body "$PR_BODY" \ + --no-maintainer-edit) + +# Save consolidated PR URL for report step +echo "$NEW_PR_URL" > "${SHARED_DIR}/consolidated-pr-url.txt" + +# Append report link to PR description +PR_NUM=$(echo "$NEW_PR_URL" | grep -o '[0-9]*$' || true) +if [ -n "$PR_NUM" ] && [ -n "${BUILD_ID:-}" ] && [ -n "${JOB_NAME:-}" ]; then + REPORT_URL="" + if [ "${JOB_TYPE:-}" = "periodic" ]; then + REPORT_URL="https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/logs/${JOB_NAME}/${BUILD_ID}/artifacts/dependabot-triage/hypershift-dependabot-triage-report/artifacts/dependabot-triage-report.html" + else + REPORT_URL="https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/pr-logs/pull/openshift_release/${PULL_NUMBER:-0}/${JOB_NAME}/${BUILD_ID}/artifacts/dependabot-triage/hypershift-dependabot-triage-report/artifacts/dependabot-triage-report.html" + fi + echo "Appending report link to PR #${PR_NUM} description..." + CURRENT_BODY=$(gh pr view "$PR_NUM" --repo openshift/hypershift --json body -q .body 2>/dev/null || echo "") + REPORT_SECTION="--- + +> **Note:** This PR was auto-generated by the [dependabot-triage](https://github.com/openshift/release/tree/main/ci-operator/step-registry/hypershift/dependabot-triage) periodic CI job. See the [full report](${REPORT_URL}) for token usage, cost breakdown, and detailed output." + UPDATED_BODY="${CURRENT_BODY} + +${REPORT_SECTION}" + gh pr edit "$PR_NUM" --repo openshift/hypershift --body "$UPDATED_BODY" 2>/dev/null || echo "Warning: Failed to update PR #${PR_NUM} description" +fi + +echo "" +echo "==========================================" +echo "SUCCESS" +echo "==========================================" +echo "Consolidated PR: $NEW_PR_URL" + +echo "" +echo "=== Dependabot Triage Complete ===" diff --git a/ci-operator/step-registry/hypershift/dependabot-triage/process/hypershift-dependabot-triage-process-ref.metadata.json b/ci-operator/step-registry/hypershift/dependabot-triage/process/hypershift-dependabot-triage-process-ref.metadata.json new file mode 100644 index 0000000000000..b4e2470536d65 --- /dev/null +++ b/ci-operator/step-registry/hypershift/dependabot-triage/process/hypershift-dependabot-triage-process-ref.metadata.json @@ -0,0 +1,20 @@ +{ + "path": "hypershift/dependabot-triage/process/hypershift-dependabot-triage-process-ref.yaml", + "owners": { + "approvers": [ + "csrwng", + "enxebre", + "sjenning", + "LiangquanLi930", + "bryan-cox", + "jparrill" + ], + "reviewers": [ + "csrwng", + "enxebre", + "sjenning", + "bryan-cox", + "jparrill" + ] + } +} \ No newline at end of file diff --git a/ci-operator/step-registry/hypershift/dependabot-triage/process/hypershift-dependabot-triage-process-ref.yaml b/ci-operator/step-registry/hypershift/dependabot-triage/process/hypershift-dependabot-triage-process-ref.yaml new file mode 100644 index 0000000000000..18bf3ebf9fa34 --- /dev/null +++ b/ci-operator/step-registry/hypershift/dependabot-triage/process/hypershift-dependabot-triage-process-ref.yaml @@ -0,0 +1,45 @@ +ref: + as: hypershift-dependabot-triage-process + from: claude-ai-helpers + commands: hypershift-dependabot-triage-process-commands.sh + timeout: 2h + env: + - name: CLAUDE_CODE_USE_VERTEX + default: "1" + documentation: |- + Enable Vertex AI for Claude Code. + - name: CLOUD_ML_REGION + default: "us-east5" + documentation: |- + Google Cloud region for Vertex AI. + - name: ANTHROPIC_VERTEX_PROJECT_ID + default: "itpc-gcp-hybrid-pe-eng-claude" + documentation: |- + Google Cloud project ID for Vertex AI authentication. + - name: GOOGLE_APPLICATION_CREDENTIALS + default: "/var/run/claude-code-service-account/claude-prow" + documentation: |- + Path to the Google Cloud service account JSON key file for Vertex AI authentication. + - name: CLAUDE_MODEL + default: "claude-opus-4-6" + documentation: |- + Claude model to use for processing dependabot PRs. + resources: + requests: + cpu: 500m + memory: 1Gi + credentials: + - namespace: test-credentials + name: hypershift-team-claude-prow + mount_path: /var/run/claude-code-service-account + documentation: |- + Process step for the HyperShift dependabot triage periodic job. + This step: + - Generates GitHub App tokens for fork and upstream access + - Clones HyperShift from the fork and adds upstream remote + - Queries GitHub for open dependabot PRs in openshift/hypershift + - Invokes Claude with all PR numbers and consolidation instructions + - Claude processes each PR with validation before moving to next + - Creates a single consolidated PR with all dependency updates + - Uses best-effort: processes as many PRs as possible, reports failures + - Uses Vertex AI for Claude authentication via GCP service account diff --git a/ci-operator/step-registry/hypershift/dependabot-triage/report/OWNERS b/ci-operator/step-registry/hypershift/dependabot-triage/report/OWNERS new file mode 100644 index 0000000000000..a0c4da21c466b --- /dev/null +++ b/ci-operator/step-registry/hypershift/dependabot-triage/report/OWNERS @@ -0,0 +1,13 @@ +approvers: +- csrwng +- enxebre +- sjenning +- LiangquanLi930 +- bryan-cox +- jparrill +reviewers: +- csrwng +- enxebre +- sjenning +- bryan-cox +- jparrill diff --git a/ci-operator/step-registry/hypershift/dependabot-triage/report/hypershift-dependabot-triage-report-commands.sh b/ci-operator/step-registry/hypershift/dependabot-triage/report/hypershift-dependabot-triage-report-commands.sh new file mode 100644 index 0000000000000..043d0ff51dce0 --- /dev/null +++ b/ci-operator/step-registry/hypershift/dependabot-triage/report/hypershift-dependabot-triage-report-commands.sh @@ -0,0 +1,232 @@ +#!/bin/bash +set -euo pipefail + +echo "=== Dependabot Triage Report Generation ===" + +RESULTS_FILE="${SHARED_DIR}/dependabot-results.txt" +TOKEN_FILE="${SHARED_DIR}/claude-dependabot-tokens.json" +REPORT_FILE="${ARTIFACT_DIR}/dependabot-triage-report.html" + +if [ ! -f "$RESULTS_FILE" ]; then + echo "No results file found. Nothing to report." + exit 0 +fi + +# Count PRs by status +SUCCEEDED_COUNT=$(grep -c '^SUCCEEDED_PR:' "$RESULTS_FILE" 2>/dev/null || true) +FAILED_COUNT=$(grep -c '^FAILED_PR:' "$RESULTS_FILE" 2>/dev/null || true) +: "${SUCCEEDED_COUNT:=0}" +: "${FAILED_COUNT:=0}" +TOTAL=$((SUCCEEDED_COUNT + FAILED_COUNT)) +RUN_TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC") + +echo "Generating report for $TOTAL PRs ($SUCCEEDED_COUNT succeeded, $FAILED_COUNT failed)" + +# Read a pre-extracted text file, or return a placeholder +read_extracted() { + local file=$1 + if [ -f "$file" ] && [ -s "$file" ]; then + cat "$file" + else + echo "(no output captured)" + fi +} + +# Read a JSON token file and extract a field, defaulting to 0 +read_token_field() { + local file=$1 + local field=$2 + if [ -f "$file" ] && [ -s "$file" ]; then + jq -r ".${field} // 0" "$file" 2>/dev/null || echo "0" + else + echo "0" + fi +} + +# Format token count with comma separators +format_number() { + local num=$1 + printf "%s" "$num" | sed -e ':a' -e 's/\([0-9]\)\([0-9]\{3\}\)\(\b\)/\1,\2\3/' -e 'ta' +} + +# Format a cost value (already in USD) for display +format_cost() { + local cost=${1:-0} + printf '$%s' "$(awk "BEGIN {printf \"%.4f\", $cost}" 2>/dev/null || echo "0.0000")" +} + +# HTML-escape a string +html_escape() { + sed 's/&/\&/g; s/</\</g; s/>/\>/g; s/"/\"/g' +} + +# Read token data +INPUT_TOKENS=$(read_token_field "$TOKEN_FILE" "input_tokens") +OUTPUT_TOKENS=$(read_token_field "$TOKEN_FILE" "output_tokens") +CACHE_READ=$(read_token_field "$TOKEN_FILE" "cache_read_input_tokens") +CACHE_CREATE=$(read_token_field "$TOKEN_FILE" "cache_creation_input_tokens") +MODEL=$(read_token_field "$TOKEN_FILE" "model") +TOTAL_COST_RAW=$(read_token_field "$TOKEN_FILE" "total_cost_usd") +TOTAL_COST=$(format_cost "$TOTAL_COST_RAW") +DURATION_MS=$(read_token_field "$TOKEN_FILE" "duration_ms") +NUM_TURNS=$(read_token_field "$TOKEN_FILE" "num_turns") + +# Read consolidated PR URL +CONSOLIDATED_PR_URL="" +if [ -f "${SHARED_DIR}/consolidated-pr-url.txt" ] && [ -s "${SHARED_DIR}/consolidated-pr-url.txt" ]; then + CONSOLIDATED_PR_URL=$(cat "${SHARED_DIR}/consolidated-pr-url.txt") +fi + +# Build succeeded PRs table rows +SUCCEEDED_ROWS="" +while IFS= read -r line; do + if [ -z "$line" ]; then continue; fi + pr_num=$(echo "$line" | cut -d: -f2) + pr_title=$(echo "$line" | cut -d: -f3- | html_escape) + SUCCEEDED_ROWS="${SUCCEEDED_ROWS}<tr><td><a href=\"https://github.com/openshift/hypershift/pull/${pr_num}\">#${pr_num}</a></td><td>${pr_title}</td></tr>" +done < <(grep '^SUCCEEDED_PR:' "$RESULTS_FILE" 2>/dev/null || true) + +# Build failed PRs table rows +FAILED_ROWS="" +while IFS= read -r line; do + if [ -z "$line" ]; then continue; fi + pr_num=$(echo "$line" | cut -d: -f2) + pr_reason=$(echo "$line" | cut -d: -f3- | html_escape) + FAILED_ROWS="${FAILED_ROWS}<tr><td><a href=\"https://github.com/openshift/hypershift/pull/${pr_num}\">#${pr_num}</a></td><td>${pr_reason}</td></tr>" +done < <(grep '^FAILED_PR:' "$RESULTS_FILE" 2>/dev/null || true) + +# Read Claude output text and tool summaries +CLAUDE_TEXT=$(read_extracted "${SHARED_DIR}/claude-dependabot-output-text.txt" | html_escape) +CLAUDE_TOOLS=$(read_extracted "${SHARED_DIR}/claude-dependabot-output-tools.txt" | html_escape) + +# Consolidated PR banner (full-width, prominent) +CONSOLIDATED_PR_BANNER="" +if [ -n "$CONSOLIDATED_PR_URL" ]; then + PR_NUMBER="${CONSOLIDATED_PR_URL##*/}" + CONSOLIDATED_PR_BANNER="<div style=\"background:#dcffe4; border:2px solid #22863a; border-radius:8px; padding:1em 1.5em; margin:1em 0;\"><strong style=\"font-size:1.2em;\">Consolidated PR: <a href=\"${CONSOLIDATED_PR_URL}\">#${PR_NUMBER}</a></strong></div>" +fi + +# Succeeded PRs table +SUCCEEDED_TABLE="" +if [ -n "$SUCCEEDED_ROWS" ]; then + SUCCEEDED_TABLE=" +<h2>Succeeded PRs</h2> +<table> +<thead><tr><th>PR</th><th>Title</th></tr></thead> +<tbody> +${SUCCEEDED_ROWS} +</tbody> +</table>" +fi + +# Failed PRs table +FAILED_TABLE="" +if [ -n "$FAILED_ROWS" ]; then + FAILED_TABLE=" +<h2>Failed PRs</h2> +<table> +<thead><tr><th>PR</th><th>Failure Reason</th></tr></thead> +<tbody> +${FAILED_ROWS} +</tbody> +</table>" +fi + +# Build per-model breakdown rows for the token table +MODEL_BREAKDOWN_ROWS="" +if [ -f "$TOKEN_FILE" ] && [ -s "$TOKEN_FILE" ]; then + while IFS= read -r model_line; do + model_name=$(echo "$model_line" | jq -r '.key') + model_input=$(echo "$model_line" | jq -r '.value.input_tokens // 0') + model_output=$(echo "$model_line" | jq -r '.value.output_tokens // 0') + MODEL_BREAKDOWN_ROWS="${MODEL_BREAKDOWN_ROWS}<tr><td>${model_name}</td><td>$(format_number "$model_input")</td><td>$(format_number "$model_output")</td><td>—</td><td>—</td><td>—</td></tr>" + done < <(jq -c '.model_usage // {} | to_entries[]' "$TOKEN_FILE" 2>/dev/null || true) +fi + +# Final validation failure section (when make verify/test fails on the consolidation branch) +FINAL_FAIL_MSG=$(grep -E '^FINAL_(VERIFY|TEST)_FAILED:' "$RESULTS_FILE" 2>/dev/null | head -1 | cut -d: -f2- | html_escape || true) +FINAL_FAIL_SECTION="" +if [ -n "$FINAL_FAIL_MSG" ]; then + FINAL_FAIL_SECTION="<div class='output-card' style='border-left:4px solid #cb2431'> +<h3 style='color:#cb2431'>Final Validation Failed</h3> +<p>${FINAL_FAIL_MSG}</p> +<p>No PR was created. Check <code>make-verify-failure.log</code> in artifacts for details.</p></div>" +fi + +# Write the HTML report +cat > "$REPORT_FILE" <<EOF +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<title>Dependabot Triage Report + + + +

Dependabot Triage Report

+

Generated: ${RUN_TIMESTAMP}

+ +${CONSOLIDATED_PR_BANNER} + +
+
${TOTAL}
Total PRs
+
${SUCCEEDED_COUNT}
Succeeded
+
${FAILED_COUNT}
Failed
+
$(format_number "$INPUT_TOKENS")
Input Tokens
+
$(format_number "$OUTPUT_TOKENS")
Output Tokens
+
${TOTAL_COST}
Cost
+
+ +${SUCCEEDED_TABLE} + +${FAILED_TABLE} + +${FINAL_FAIL_SECTION} + +

Token Usage & Cost

+ + + + +${MODEL_BREAKDOWN_ROWS} + +
CategoryInput TokensOutput TokensCache ReadCache CreateCost
Total$(format_number "$INPUT_TOKENS")$(format_number "$OUTPUT_TOKENS")$(format_number "$CACHE_READ")$(format_number "$CACHE_CREATE")${TOTAL_COST}
+

Primary model: ${MODEL} · ${NUM_TURNS} turns · $((DURATION_MS / 1000))s

+ +
+

Claude Output

+
${CLAUDE_TEXT}
+
Tool calls
${CLAUDE_TOOLS}
+
+ + + +EOF + +echo "Report written to ${REPORT_FILE}" + +echo "=== Report generation complete ===" diff --git a/ci-operator/step-registry/hypershift/dependabot-triage/report/hypershift-dependabot-triage-report-ref.metadata.json b/ci-operator/step-registry/hypershift/dependabot-triage/report/hypershift-dependabot-triage-report-ref.metadata.json new file mode 100644 index 0000000000000..30a2a3745bc37 --- /dev/null +++ b/ci-operator/step-registry/hypershift/dependabot-triage/report/hypershift-dependabot-triage-report-ref.metadata.json @@ -0,0 +1,20 @@ +{ + "path": "hypershift/dependabot-triage/report/hypershift-dependabot-triage-report-ref.yaml", + "owners": { + "approvers": [ + "csrwng", + "enxebre", + "sjenning", + "LiangquanLi930", + "bryan-cox", + "jparrill" + ], + "reviewers": [ + "csrwng", + "enxebre", + "sjenning", + "bryan-cox", + "jparrill" + ] + } +} \ No newline at end of file diff --git a/ci-operator/step-registry/hypershift/dependabot-triage/report/hypershift-dependabot-triage-report-ref.yaml b/ci-operator/step-registry/hypershift/dependabot-triage/report/hypershift-dependabot-triage-report-ref.yaml new file mode 100644 index 0000000000000..c204808952f51 --- /dev/null +++ b/ci-operator/step-registry/hypershift/dependabot-triage/report/hypershift-dependabot-triage-report-ref.yaml @@ -0,0 +1,11 @@ +ref: + as: hypershift-dependabot-triage-report + from: claude-ai-helpers + commands: hypershift-dependabot-triage-report-commands.sh + resources: + requests: + cpu: 100m + memory: 256Mi + documentation: |- + Generates an HTML report from the dependabot-triage processing output. + Parses stream-json token data and produces a readable report in ${ARTIFACT_DIR}. diff --git a/ci-operator/step-registry/hypershift/dependabot-triage/setup/OWNERS b/ci-operator/step-registry/hypershift/dependabot-triage/setup/OWNERS new file mode 100644 index 0000000000000..a0c4da21c466b --- /dev/null +++ b/ci-operator/step-registry/hypershift/dependabot-triage/setup/OWNERS @@ -0,0 +1,13 @@ +approvers: +- csrwng +- enxebre +- sjenning +- LiangquanLi930 +- bryan-cox +- jparrill +reviewers: +- csrwng +- enxebre +- sjenning +- bryan-cox +- jparrill diff --git a/ci-operator/step-registry/hypershift/dependabot-triage/setup/hypershift-dependabot-triage-setup-commands.sh b/ci-operator/step-registry/hypershift/dependabot-triage/setup/hypershift-dependabot-triage-setup-commands.sh new file mode 100755 index 0000000000000..bc82768923742 --- /dev/null +++ b/ci-operator/step-registry/hypershift/dependabot-triage/setup/hypershift-dependabot-triage-setup-commands.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +echo "=== HyperShift Dependabot Triage Setup ===" + +# Verify Claude Code is available (Vertex AI authentication is handled via GOOGLE_APPLICATION_CREDENTIALS env var) +echo "Verifying Claude Code CLI..." +claude --version || { echo "ERROR: Claude Code CLI not found"; exit 1; } + +echo "Setup complete" diff --git a/ci-operator/step-registry/hypershift/dependabot-triage/setup/hypershift-dependabot-triage-setup-ref.metadata.json b/ci-operator/step-registry/hypershift/dependabot-triage/setup/hypershift-dependabot-triage-setup-ref.metadata.json new file mode 100644 index 0000000000000..d6e02191ff9c0 --- /dev/null +++ b/ci-operator/step-registry/hypershift/dependabot-triage/setup/hypershift-dependabot-triage-setup-ref.metadata.json @@ -0,0 +1,20 @@ +{ + "path": "hypershift/dependabot-triage/setup/hypershift-dependabot-triage-setup-ref.yaml", + "owners": { + "approvers": [ + "csrwng", + "enxebre", + "sjenning", + "LiangquanLi930", + "bryan-cox", + "jparrill" + ], + "reviewers": [ + "csrwng", + "enxebre", + "sjenning", + "bryan-cox", + "jparrill" + ] + } +} \ No newline at end of file diff --git a/ci-operator/step-registry/hypershift/dependabot-triage/setup/hypershift-dependabot-triage-setup-ref.yaml b/ci-operator/step-registry/hypershift/dependabot-triage/setup/hypershift-dependabot-triage-setup-ref.yaml new file mode 100644 index 0000000000000..0b0fcd00aca3d --- /dev/null +++ b/ci-operator/step-registry/hypershift/dependabot-triage/setup/hypershift-dependabot-triage-setup-ref.yaml @@ -0,0 +1,34 @@ +ref: + as: hypershift-dependabot-triage-setup + from: claude-ai-helpers + commands: hypershift-dependabot-triage-setup-commands.sh + env: + - name: CLAUDE_CODE_USE_VERTEX + default: "1" + documentation: |- + Enable Vertex AI for Claude Code. + - name: CLOUD_ML_REGION + default: "us-east5" + documentation: |- + Google Cloud region for Vertex AI. + - name: ANTHROPIC_VERTEX_PROJECT_ID + default: "itpc-gcp-hybrid-pe-eng-claude" + documentation: |- + Google Cloud project ID for Vertex AI authentication. + - name: GOOGLE_APPLICATION_CREDENTIALS + default: "/var/run/claude-code-service-account/claude-prow" + documentation: |- + Path to the Google Cloud service account JSON key file for Vertex AI authentication. + resources: + requests: + cpu: 100m + memory: 200Mi + credentials: + - namespace: test-credentials + name: hypershift-team-claude-prow + mount_path: /var/run/claude-code-service-account + documentation: |- + Setup step for the HyperShift dependabot triage periodic job. + This step: + - Verifies Claude Code CLI is available + - Uses Vertex AI for Claude authentication via GCP service account diff --git a/docs/plans/2026-01-21-dependabot-triage-prow-job-design.md b/docs/plans/2026-01-21-dependabot-triage-prow-job-design.md new file mode 100644 index 0000000000000..6d22c7bf41a9f --- /dev/null +++ b/docs/plans/2026-01-21-dependabot-triage-prow-job-design.md @@ -0,0 +1,248 @@ +# Dependabot Triage Prow Job Design + +**Jira:** CNTRLPLANE-2588 +**Date:** 2026-01-21 +**Author:** Bryan Cox + +## Overview + +Create a periodic Prow job that automatically triages dependabot pull requests in the HyperShift repository. The job runs weekly, queries all open dependabot PRs, processes them using Claude's `/fix-hypershift-repo-robot-pr` command, and consolidates all changes into a single reviewed PR. + +## Goals + +- Reduce manual maintenance burden for dependabot PRs +- Consolidate multiple dependency updates into a single reviewable PR +- Automate file regeneration (`make verify`, `make test`) +- Run weekly on Fridays at 12:00 UTC (7:00 AM ET) + +## File Structure + +### New Files (step-registry) + +``` +ci-operator/step-registry/hypershift/dependabot-triage/ +├── setup/ +│ ├── hypershift-dependabot-triage-setup-ref.yaml +│ └── hypershift-dependabot-triage-setup-commands.sh +├── process/ +│ ├── hypershift-dependabot-triage-process-ref.yaml +│ └── hypershift-dependabot-triage-process-commands.sh +├── hypershift-dependabot-triage-workflow.yaml +├── OWNERS +└── README.md +``` + +### Modified Files + +``` +ci-operator/config/openshift/hypershift/openshift-hypershift-main.yaml + → Add periodic job definition +``` + +### Auto-generated (via make update) + +``` +ci-operator/jobs/openshift/hypershift/openshift-hypershift-main-periodics.yaml +``` + +## Workflow Definition + +```yaml +workflow: + as: hypershift-dependabot-triage + documentation: |- + Periodic workflow that triages dependabot PRs in the HyperShift repository. + Queries all open dependabot PRs, invokes Claude to process and consolidate + them into a single PR with regenerated files. + steps: + pre: + - ref: hypershift-dependabot-triage-setup + test: + - ref: hypershift-dependabot-triage-process +``` + +## Step Definitions + +### Setup Step + +**File:** `hypershift-dependabot-triage-setup-ref.yaml` + +```yaml +ref: + as: hypershift-dependabot-triage-setup + documentation: |- + Verifies Claude CLI is available and configures git credentials + for fork and upstream authentication. + from: cli + credentials: + - mount_path: /etc/claude-token + name: claude-api-token + namespace: test-credentials + - mount_path: /etc/github-fork-token + name: hypershift-community-github-app + namespace: test-credentials + - mount_path: /etc/github-upstream-token + name: openshift-hypershift-github-app + namespace: test-credentials + commands: hypershift-dependabot-triage-setup-commands.sh +``` + +**Responsibilities:** +1. Install Claude CLI if not present +2. Verify Claude CLI works with the provided token +3. Configure git credential helper for fork vs upstream tokens +4. Clone hypershift repository from fork +5. Set up git remotes (origin = fork, upstream = openshift/hypershift) + +### Process Step + +**File:** `hypershift-dependabot-triage-process-ref.yaml` + +```yaml +ref: + as: hypershift-dependabot-triage-process + documentation: |- + Queries open dependabot PRs from openshift/hypershift, invokes Claude + to process them using /fix-hypershift-repo-robot-pr, and consolidates + all changes into a single PR. Uses best-effort processing. + from: cli + commands: hypershift-dependabot-triage-process-commands.sh + resources: + requests: + cpu: 100m + memory: 200Mi + env: + - name: REPO_OWNER + default: openshift + - name: REPO_NAME + default: hypershift + - name: FORK_OWNER + default: hypershift-community +``` + +**Responsibilities:** +1. Query open dependabot PRs using `gh pr list` +2. Exit gracefully if no PRs found +3. Invoke Claude with consolidation instructions +4. Report results (successes, failures, final PR URL) + +## Periodic Job Configuration + +Addition to `openshift-hypershift-main.yaml`: + +```yaml +- as: dependabot-triage + cron: "0 12 * * 5" + steps: + cluster_profile: hypershift + workflow: hypershift-dependabot-triage +``` + +## Claude Invocation + +The Claude prompt instructs processing each PR individually with validation: + +``` +Phase 1: Setup +- Create branch 'fix/weekly-dependabot-consolidation' from upstream/main +- Initialize tracking for succeeded/failed PRs + +Phase 2: Process Each PR (one at a time) +For each PR: +1. Save current HEAD SHA +2. Cherry-pick commits (convert to conventional format) +3. Run make verify +4. Run make test +5. Commit generated changes +6. If any step fails: reset to saved SHA, record failure, continue to next PR +7. If all pass: record success, move to next PR + +Phase 3: Reorganize Commits +After all PRs processed, reorganize into logical groups: +1. go.mod/go.sum changes ONLY +2. vendor/ updates +3. api/ module changes +4. Regenerated assets +5. Other generated changes + +Phase 4: Final Validation +- Run make verify (must pass) +- Run make test (must pass) + +Phase 5: Create Consolidated PR +- Title: 'NO-JIRA: chore(deps): weekly dependabot consolidation' +- List all consolidated PRs with titles +- List failed PRs with reasons +- Close/comment on successfully processed original PRs + +Phase 6: Report Results +``` + +**Key aspects:** +- Uses `--allowedTools "Bash,Read,Write,Edit,Grep,Glob,WebFetch,Skill,Task,TodoWrite"` +- JSON output format for structured results +- 100 max turns to handle multiple PRs +- Each PR validated independently before moving to next + +## Authentication + +Uses the fork pattern established by the jira-agent job: + +| Token | Purpose | Repository | +|-------|---------|------------| +| Fork token | Push branches, create PRs | hypershift-community/hypershift | +| Upstream token | Close/comment on original PRs | openshift/hypershift | + +Git credential helper prevents token exposure in logs. + +## Failure Handling + +**Best-effort processing with per-PR validation:** + +Each PR is processed and validated independently: +1. Cherry-pick fails → reset branch, record failure, continue to next PR +2. `make verify` fails → reset branch, record failure, continue to next PR +3. `make test` fails → reset branch, record failure, continue to next PR +4. All steps pass → record success, proceed to next PR + +**Result:** +- Consolidated PR includes only validated, passing changes +- Failed PRs remain open with failure reasons documented +- Clear audit trail of what succeeded and what failed + +## Schedule + +- **Cron:** `0 12 * * 5` +- **Frequency:** Weekly, Fridays +- **Time:** 12:00 UTC (7:00 AM ET) + +## Dependencies + +- Claude CLI with API token +- GitHub App tokens (fork + upstream) +- Same credential infrastructure as jira-agent job (CNTRLPLANE-2186) + +## k8s.io / sigs.k8s.io Dependency Filtering + +After fetching the list of open dependabot PRs and before processing, the workflow +filters out PRs that bump `k8s.io` or `sigs.k8s.io` dependencies. These dependencies +are managed manually by the team as part of coordinated Kubernetes rebase efforts and +should not be consolidated automatically. + +**How it works:** + +1. For each candidate PR, the workflow calls `gh api repos/openshift/hypershift/pulls//files` + to retrieve the file-level patch data. +2. It inspects the patches for `go.mod` and `api/go.mod` specifically. +3. If any added line (lines starting with `+`, excluding the `+++` header) contains a + `k8s.io/` or `sigs.k8s.io/` module path, the PR is excluded from processing. +4. Excluded PRs are logged and left open for manual handling. + +This filtering runs after the `gh pr list` fetch and before the PR count check, so if +all open dependabot PRs are k8s.io bumps the job exits gracefully with nothing to do. + +## Reference Implementation + +Based on patterns from: +- jira-agent job: https://github.com/openshift/release/pull/70147 +- `/fix-hypershift-repo-robot-pr` command in hypershift repo