From 48a32772f797469c1b5e46da4ac601190bad74f5 Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Thu, 13 Aug 2026 14:25:27 +0200 Subject: [PATCH] Add periodic Konflux retest --- .github/workflows/README.md | 46 +++--- .../periodic-retest-konflux-builds.yml | 148 ++++++++++++++++++ 2 files changed, 166 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/periodic-retest-konflux-builds.yml diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 5c18c29f..57329385 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -59,51 +59,41 @@ jobs: workflow-ref: v1 ``` -## Auto retest failed Konflux builds +## Periodic retest failed Konflux builds ### Overview -When a Konflux build check fails on a pull request, this action will automatically post a `/retest ` comment to trigger a rebuild. It includes retry limits to prevent infinite retry loops and automatically cleans up old retest comments when new commits are pushed. +Periodically scans all open pull requests for failed Konflux build checks and posts a +`/retest ` comment to trigger a rebuild. Retries up to `max_retries` times +per check per commit, then stops. Old retest comments from previous commit cycles are +cleaned up automatically so the retry counter always reflects the current commit only. + +Add the `disable-konflux-auto-retest` label to a PR to opt it out of automatic retesting. ### All options | Input | Description | Required | Default | |-------|-------------|----------|---------| -| `max_retries` | Maximum number of retries for failed builds | No | `3` | -| `check_name_suffix` | Suffix to filter Konflux build check names (e.g., `-on-push`) | No | `-on-push` | -| `retest_command` | Command to trigger Konflux retest (e.g., /retest). Useful to use non default when OpenShift CI uses the same /retest syntax - prevents OpenShift CI from spamming comments saying it does not understand Konflux-specific retest commands. | No | `/retest` | - -## Detailed options - -- **Automatic Retesting**: Posts retest commands when Konflux builds fail -- **Configurable Retry Limit**: Set maximum retry attempts to prevent infinite loops -- **Auto-Cleanup**: Removes old retest comments when new commits are pushed -- **Filtered Checks**: Only retests checks matching a specific name suffix (e.g., `-on-push`) -- **Custom Retest Command**: Configure the command used to trigger retests (default: `/retest`) -- **Disable via Label**: Add the `disable-konflux-auto-retest` label to a PR to skip automatic retesting - +| `max_retries` | Maximum number of retries per failed check per commit | No | `3` | +| `check_name_suffix` | Suffix to filter Konflux check names (e.g. `-on-push`, `-on-pull-request`) | No | `-on-push` | +| `retest_command` | Comment body used to trigger a Konflux retest. Use a non-default value when OpenShift CI shares the same `/retest` syntax, to avoid cross-system noise. | No | `/retest` | +| `konflux_app_id` | GitHub App ID for Red Hat Konflux, used to filter check suites | No | `296509` | ### Usage -Add this to your repository's workflow file (e.g., `.github/workflows/konflux-auto-retest.yml`): +Create a workflow file in your repository (e.g. `.github/workflows/konflux-retest-periodic.yml`): ```yaml -name: Auto-retest Konflux Builds +name: Periodic Retest Failed Konflux Builds on: - check_run: - types: [completed] - pull_request: - types: [synchronize] + schedule: + - cron: '5,15,25,35,45,55 * * * *' # every 10 minutes + workflow_dispatch: jobs: - retest-failed-konflux-builds: - uses: stackrox/actions/.github/workflows/retest-konflux-builds.yml@v1 - permissions: - pull-requests: write - issues: write + retest: + uses: stackrox/actions/.github/workflows/periodic-retest-konflux-builds.yml@main with: - max_retries: 3 check_name_suffix: '-on-push' - retest_command: '/retest' ``` diff --git a/.github/workflows/periodic-retest-konflux-builds.yml b/.github/workflows/periodic-retest-konflux-builds.yml new file mode 100644 index 00000000..66d8ec99 --- /dev/null +++ b/.github/workflows/periodic-retest-konflux-builds.yml @@ -0,0 +1,148 @@ +# NOTE on `issues: write` permission: GitHub treats PR conversation comments as issue comments +# (both share the same /issues/{number}/comments API endpoint). +# The `pull-requests: write` permission only controls review-specific actions (approvals, review comments, dismissals). +# To post a plain comment in a PR conversation, `issues: write` is required. +name: Periodic Retest Failed Konflux Builds + +on: + workflow_call: + inputs: + max_retries: + description: 'Maximum number of retries per failed check per commit' + required: false + type: number + default: 3 + check_name_suffix: + description: 'Suffix to filter Konflux check names (e.g. -on-push, -on-pull-request)' + required: false + type: string + default: '-on-push' + retest_command: + description: 'Comment body used to trigger a Konflux retest' + required: false + type: string + default: '/retest' + konflux_app_id: + description: 'GitHub App ID for Red Hat Konflux, used to filter check suites' + required: false + type: number + default: 296509 + +jobs: + periodic-retest-failed-konflux-builds: + runs-on: ubuntu-latest + + permissions: + pull-requests: write + # We need `issues: write` permission to write conversation comments. + # See top of the file comment for `issues` and conversation comments explanation. + issues: write + # required for fetching checks data via GraphQL API + checks: read + # required for getting check's content + contents: read + + steps: + - name: Scan and retest failed Konflux builds + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MAX_RETRIES: ${{ inputs.max_retries }} + CHECK_NAME_SUFFIX: ${{ inputs.check_name_suffix }} + RETEST_COMMAND: ${{ inputs.retest_command }} + KONFLUX_APP_ID: ${{ inputs.konflux_app_id }} + run: | + set -euo pipefail + + echo "Starting periodic scan for failed Konflux builds..." + + # A single GraphQL query returns all open PRs with failed Konflux check names and + # the last commit time, avoiding per-PR gh-pr-checks calls. + # filterBy:{conclusions:[FAILURE]} only matches completed runs, so in-progress + # re-runs are naturally excluded. + # Konflux ignores /retest commands sent to already-running pipelines, so any duplicate + # comment posted between a /retest and Konflux picking it up is harmless. + # github.repository is expanded by GHA before the shell sees the string; single quotes + # are intentional for jq's $appId. + # shellcheck disable=SC2016 + PR_DATA=$(gh api graphql \ + -F appId="${KONFLUX_APP_ID}" \ + -f query='query($appId: Int!) { + search(query: "repo:${{ github.repository }} is:pr is:open -label:disable-konflux-auto-retest", type: ISSUE, first: 100) { + nodes { ... on PullRequest { number + commits(last: 1) { nodes { commit { + committedDate + # Filtered to a single app, usually 1 Konflux check suite per PR in practice; 10 is a safe ceiling. + checkSuites(first: 10, filterBy: {appId: $appId}) { nodes { + # Konflux exposes one check run per pipeline component; 50 covers even large repos. + checkRuns(first: 50, filterBy: {conclusions: [FAILURE]}) { nodes { + name + completedAt + }} + }} + }}} + }} + } + }' \ + --jq '[.data.search.nodes[] | { + pr: .number, + last_commit: .commits.nodes[0].commit.committedDate, + failed: [.commits.nodes[0].commit.checkSuites.nodes[].checkRuns.nodes[] + | select(.name | ltrimstr("Red Hat Konflux / ") | endswith("'"$CHECK_NAME_SUFFIX"'")) + | {name: (.name | ltrimstr("Red Hat Konflux / ")), completed_at: .completedAt}] + } | select(.failed | length > 0)]') + + PR_COUNT=$(echo "$PR_DATA" | jq 'length') + if [ "$PR_COUNT" -eq 0 ]; then + echo "No open PRs with failed Konflux checks found" + exit 0 + fi + + echo "Found $PR_COUNT PRs with failed Konflux checks" + + echo "$PR_DATA" | jq -c '.[]' | while read -r PR_ENTRY; do + PR_NUMBER=$(echo "$PR_ENTRY" | jq -r '.pr') + LAST_COMMIT_TIME=$(echo "$PR_ENTRY" | jq -r '.last_commit') + echo "" + echo "Processing PR #$PR_NUMBER (last commit: $LAST_COMMIT_TIME)..." + + echo "$PR_ENTRY" | jq -c '.failed[]' | while IFS= read -r CHECK_ENTRY; do + BASE_CHECK_NAME=$(echo "$CHECK_ENTRY" | jq -r '.name') + COMPLETED_AT=$(echo "$CHECK_ENTRY" | jq -r '.completed_at') + + if [ -z "$BASE_CHECK_NAME" ]; then + continue + fi + + echo " Found failed check: $BASE_CHECK_NAME (failed at: $COMPLETED_AT)" + + # Delete retest comments from previous commit cycles so they cannot be + # miscounted against the current commit's retry budget. + gh api --paginate "repos/${{ github.repository }}/issues/$PR_NUMBER/comments" \ + --jq '[.[] | select( + .user.login == "github-actions[bot]" and + (.body | contains("'"$RETEST_COMMAND $BASE_CHECK_NAME"'")) and + .created_at < "'"$LAST_COMMIT_TIME"'" + ) | .id] | .[]' | \ + while read -r COMMENT_ID; do + gh api -X DELETE "repos/${{ github.repository }}/issues/comments/$COMMENT_ID" + done + + # Count retest comments posted since the last commit. + RETRY_COUNT="$(gh api --paginate "repos/${{ github.repository }}/issues/$PR_NUMBER/comments" \ + --jq '[.[] | select( + .user.login == "github-actions[bot]" and + (.body | contains("'"$RETEST_COMMAND $BASE_CHECK_NAME"'")) and + .created_at > "'"$LAST_COMMIT_TIME"'" + )] | length')" + + if [ "$RETRY_COUNT" -ge "$MAX_RETRIES" ]; then + echo " Maximum retry limit ($MAX_RETRIES) reached for $BASE_CHECK_NAME on PR #$PR_NUMBER" + else + echo " Retrying $BASE_CHECK_NAME (attempt $((RETRY_COUNT + 1))/$MAX_RETRIES)" + gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "$RETEST_COMMAND $BASE_CHECK_NAME" + fi + done + done + + echo "" + echo "Periodic scan complete"