Review exact item terminal-finalization:steipete/CodexBar#3009:7 rev 7 head na #975997
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: ClawSweeper | |
| run-name: >- | |
| ${{ | |
| (github.event_name == 'schedule' && | |
| (github.event.schedule == '4/20 * * * *' || | |
| github.event.schedule == '41/10 * * * *' || | |
| github.event.schedule == '37 */6 * * *')) && | |
| 'Fan out ClawSweeper targets' || | |
| (github.event_name == 'schedule' && github.event.schedule == '13 * * * *') && | |
| 'Retry failed Codex reviews' || | |
| (github.event_name == 'repository_dispatch' && | |
| github.event.action == 'clawsweeper_target_sweep' && | |
| github.event.client_payload.hot_intake == 'true') && | |
| format('Review hot target repo {0}', github.event.client_payload.target_repo || 'openclaw/openclaw') || | |
| (github.event_name == 'repository_dispatch' && | |
| github.event.action == 'clawsweeper_target_sweep') && | |
| format('Review target repo {0}', github.event.client_payload.target_repo || 'openclaw/openclaw') || | |
| (github.event_name == 'repository_dispatch' && github.event.client_payload.queue_lease_id != '') && | |
| github.event.client_payload.source_action == 'scheduled_hot_intake' && | |
| format('Review scheduled hot item {0} rev {1}', github.event.client_payload.queue_claim.item_key || github.event.client_payload.item_key || '?', github.event.client_payload.queue_claim.lease_revision || github.event.client_payload.lease_revision || '?') || | |
| (github.event_name == 'repository_dispatch' && github.event.client_payload.queue_lease_id != '') && | |
| github.event.client_payload.source_action == 'scheduled_normal_backfill' && | |
| format('Review scheduled normal item {0} rev {1}', github.event.client_payload.queue_claim.item_key || github.event.client_payload.item_key || '?', github.event.client_payload.queue_claim.lease_revision || github.event.client_payload.lease_revision || '?') || | |
| (github.event_name == 'repository_dispatch' && github.event.client_payload.queue_lease_id != '') && | |
| format('Review exact item {0} rev {1} head {2}', github.event.client_payload.queue_claim.item_key || github.event.client_payload.item_key || '?', github.event.client_payload.queue_claim.lease_revision || github.event.client_payload.lease_revision || '?', github.event.client_payload.queue_claim.source_head_sha || github.event.client_payload.source_head_sha || 'na') || | |
| (github.event_name == 'repository_dispatch' && github.event.client_payload.dispatch_key != '') && | |
| format('Review event item {0}#{1} [{2}]', github.event.client_payload.target_repo || 'openclaw/openclaw', github.event.client_payload.item_number || '?', github.event.client_payload.dispatch_key) || | |
| github.event_name == 'repository_dispatch' && | |
| format('Review event item {0}#{1}', github.event.client_payload.target_repo || 'openclaw/openclaw', github.event.client_payload.item_number || '?') || | |
| ((github.event_name == 'workflow_dispatch' && | |
| github.event.inputs.apply_existing == 'true' && | |
| github.event.inputs.apply_sync_comments_only == 'true') || | |
| (github.event_name == 'schedule' && github.event.schedule == '6,21,36,51 * * * *')) && | |
| format('Sync Codex review comments for {0}', github.event.inputs.target_repo || 'openclaw/openclaw') || | |
| (github.event_name == 'workflow_dispatch' && | |
| github.event.inputs.apply_existing == 'true' && | |
| github.event.inputs.apply_sync_comments_only != 'true' && | |
| (github.event.inputs.apply_item_numbers != '' || | |
| github.event.inputs.apply_limit != '40' || | |
| github.event.inputs.apply_min_age_days != '0' || | |
| github.event.inputs.apply_min_age_minutes != '' || | |
| github.event.inputs.apply_kind != 'all' || | |
| (github.event.inputs.apply_close_reasons != '' && github.event.inputs.apply_close_reasons != 'all') || | |
| github.event.inputs.apply_stale_min_age_days != '60' || | |
| github.event.inputs.apply_close_delay_ms != '2000' || | |
| github.event.inputs.apply_checkpoint_size != '40' || | |
| github.event.inputs.apply_comment_sync_min_age_days != '7')) && | |
| format('Apply custom ClawSweeper closures for {0}', github.event.inputs.target_repo || 'openclaw/openclaw') || | |
| ((github.event_name == 'workflow_dispatch' && github.event.inputs.apply_existing == 'true') || | |
| (github.event_name == 'schedule' && | |
| (github.event.schedule == '3 * * * *' || | |
| github.event.schedule == '18 * * * *' || | |
| github.event.schedule == '33 * * * *' || | |
| github.event.schedule == '48 * * * *' || | |
| github.event.schedule == '8,23,38,53 * * * *'))) && | |
| format('Apply default ClawSweeper closures for {0}', github.event.inputs.target_repo || ((github.event.schedule == '8,23,38,53 * * * *') && 'openclaw/clawhub' || 'openclaw/openclaw')) || | |
| (github.event_name == 'workflow_dispatch' && startsWith(github.event.inputs.item_numbers, 'router-')) && | |
| format('Review event item {0}#{1} [{2}]', github.event.inputs.target_repo || 'openclaw/openclaw', github.event.inputs.item_number || '?', github.event.inputs.item_numbers) || | |
| (github.event_name == 'workflow_dispatch' && | |
| github.event.inputs.item_number != '' && | |
| github.event.inputs.item_numbers != '') && | |
| format('Review event items {0}#{1},{2} [shards={3}]', github.event.inputs.target_repo || 'openclaw/openclaw', github.event.inputs.item_number, github.event.inputs.item_numbers, (github.event.inputs.hot_intake == 'true' && '1' || github.event.inputs.shard_count || '89')) || | |
| (github.event_name == 'workflow_dispatch' && | |
| github.event.inputs.item_number != '' && | |
| github.event.inputs.item_numbers == '') && | |
| format('Review event item {0}#{1}', github.event.inputs.target_repo || 'openclaw/openclaw', github.event.inputs.item_number) || | |
| (github.event_name == 'workflow_dispatch' && github.event.inputs.item_numbers != '') && | |
| format('Review event items {0}#{1} [shards={2}]', github.event.inputs.target_repo || 'openclaw/openclaw', github.event.inputs.item_numbers, (github.event.inputs.hot_intake == 'true' && '1' || github.event.inputs.shard_count || '89')) || | |
| ((github.event_name == 'workflow_dispatch' && github.event.inputs.audit_dashboard == 'true') || | |
| (github.event_name == 'schedule' && | |
| (github.event.schedule == '7 */6 * * *' || | |
| github.event.schedule == '12 */6 * * *' || | |
| github.event.schedule == '17 */6 * * *'))) && | |
| 'Audit ClawSweeper state' || | |
| ((github.event_name == 'workflow_dispatch' && github.event.inputs.hot_intake == 'true') || | |
| (github.event_name == 'schedule' && | |
| (github.event.schedule == '*/5 * * * *' || | |
| github.event.schedule == '2/5 * * * *'))) && | |
| 'Review hot ClawSweeper items' || | |
| 'Review ClawSweeper items' | |
| }} | |
| on: | |
| repository_dispatch: | |
| types: [clawsweeper_item, clawsweeper_target_sweep] | |
| workflow_dispatch: | |
| inputs: | |
| target_repo: | |
| description: "Repository to sweep" | |
| required: false | |
| default: "openclaw/openclaw" | |
| target_branch: | |
| description: "Target repository branch to review" | |
| required: false | |
| default: "" | |
| apply_existing: | |
| description: "Apply existing proposed close decisions without rerunning Codex; skips items changed since review" | |
| required: false | |
| default: "false" | |
| apply_limit: | |
| description: "Maximum existing proposed items to close in apply-existing mode" | |
| required: false | |
| default: "40" | |
| apply_min_age_days: | |
| description: "Minimum item age in days before apply-existing can close it" | |
| required: false | |
| default: "0" | |
| apply_min_age_minutes: | |
| description: "Optional minute-level minimum item age before apply-existing can close it" | |
| required: false | |
| default: "" | |
| apply_kind: | |
| description: "Item kind to close in apply-existing mode: issue, pull_request, or all" | |
| required: false | |
| default: "all" | |
| apply_close_reasons: | |
| description: "Optional apply-existing close-reason override; defaults to the shared automatic-close policy" | |
| required: false | |
| default: "" | |
| apply_stale_min_age_days: | |
| description: "Minimum item age in days before age-gated stale or mostly-implemented closes" | |
| required: false | |
| default: "60" | |
| apply_item_numbers: | |
| description: "Optional comma-separated item numbers to sync/apply first" | |
| required: false | |
| default: "" | |
| apply_sync_comments_only: | |
| description: "Only sync durable review comments; do not close items" | |
| required: false | |
| default: "false" | |
| apply_comment_sync_min_age_days: | |
| description: "Minimum age in days before comment-only sync rewrites an existing review comment" | |
| required: false | |
| default: "7" | |
| apply_close_delay_ms: | |
| description: "Delay after each close/comment pair to avoid GitHub secondary write throttling" | |
| required: false | |
| default: "2000" | |
| apply_checkpoint_size: | |
| description: "Fresh closes per checkpoint commit in apply-existing mode" | |
| required: false | |
| default: "40" | |
| batch_size: | |
| description: "Items per worker job" | |
| required: false | |
| default: "3" | |
| codex_timeout_ms: | |
| description: "Per-item Codex timeout in milliseconds" | |
| required: false | |
| default: "1200000" | |
| shard_count: | |
| description: "Parallel shards (capped by config/automation-limits.json)" | |
| required: false | |
| default: "89" | |
| item_number: | |
| description: "Optional single issue/PR number to review" | |
| required: false | |
| default: "" | |
| item_numbers: | |
| description: "Optional comma-separated issue/PR numbers to review" | |
| required: false | |
| default: "" | |
| additional_prompt: | |
| description: "Optional one-off instructions for the selected review item(s)" | |
| required: false | |
| default: "" | |
| hot_intake: | |
| description: "Run the low-latency intake lane for new/active issues and PRs" | |
| required: false | |
| default: "false" | |
| apply_after_review: | |
| description: "After publishing selected review artifacts, immediately apply safe close proposals for those items" | |
| required: false | |
| default: "false" | |
| apply_after_review_close_reasons: | |
| description: "Optional immediate-close reason override; defaults to the shared automatic-close policy" | |
| required: false | |
| default: "" | |
| apply_after_review_min_age_minutes: | |
| description: "Minute-level item age floor for immediate post-review apply" | |
| required: false | |
| default: "0" | |
| audit_dashboard: | |
| description: "Refresh audit state without running review or apply work" | |
| required: false | |
| default: "false" | |
| schedule: | |
| - cron: "*/5 * * * *" | |
| # ClawHub review/apply schedules stay opt-in until the ClawSweeper app is installed there. | |
| - cron: "2/5 * * * *" | |
| - cron: "7 */6 * * *" | |
| - cron: "12 */6 * * *" | |
| - cron: "17 */6 * * *" | |
| - cron: "1/5 * * * *" | |
| - cron: "22 * * * *" | |
| - cron: "3 * * * *" | |
| - cron: "18 * * * *" | |
| - cron: "33 * * * *" | |
| - cron: "48 * * * *" | |
| - cron: "8,23,38,53 * * * *" | |
| - cron: "6,21,36,51 * * * *" | |
| # Temporary containment: run hot fleet fanout every 20 minutes while trusted | |
| # ClawSweeper-owned activity can feed unchanged items back into hot review. | |
| # Restore a faster cadence only after that loop is fixed and quota telemetry | |
| # confirms the higher demand is safe. | |
| - cron: "4/20 * * * *" | |
| # Normal backfill every 10 minutes instead of hourly, for the same reason. | |
| - cron: "41/10 * * * *" | |
| - cron: "37 */6 * * *" | |
| - cron: "13 * * * *" | |
| permissions: | |
| contents: write | |
| actions: write | |
| issues: write | |
| pull-requests: write | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| CLAWSWEEPER_AUTO_CLOSE_REASONS: ${{ vars.CLAWSWEEPER_AUTO_CLOSE_REASONS || 'all' }} | |
| CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED || 'false' }} | |
| CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED || 'false' }} | |
| CLAWSWEEPER_STALE_VERSION_BUG_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_STALE_VERSION_BUG_CLOSE_ENABLED || 'false' }} | |
| CLAWSWEEPER_OBSOLETE_FIX_PR_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_OBSOLETE_FIX_PR_CLOSE_ENABLED || 'false' }} | |
| CLAWSWEEPER_AUTHOR_PR_BUDGET: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET || '15' }} | |
| CLAWSWEEPER_AUTHOR_PR_BUDGET_MAX_CLOSES_PER_RUN: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET_MAX_CLOSES_PER_RUN || '5' }} | |
| CLAWSWEEPER_IDEA_REVIVAL_REACTIONS: ${{ vars.CLAWSWEEPER_IDEA_REVIVAL_REACTIONS || '5' }} | |
| CLAWSWEEPER_APP_CLIENT_ID: Iv23liOECG0slfuhz093 | |
| CLAWSWEEPER_MODEL: internal | |
| CLAWSWEEPER_RUNNER: ${{ vars.CLAWSWEEPER_RUNNER || 'codex' }} | |
| CLAWSWEEPER_OPENCLAW_MODEL: ${{ secrets.CLAWSWEEPER_OPENCLAW_MODEL }} | |
| CLAWSWEEPER_OPENCLAW_PROVIDERS_JSON: ${{ secrets.CLAWSWEEPER_OPENCLAW_PROVIDERS_JSON }} | |
| CLAWSWEEPER_OPENCLAW_OPENAI_KEY: ${{ vars.CLAWSWEEPER_RUNNER == 'openclaw' && secrets.OPENAI_API_KEY || '' }} | |
| ANTHROPIC_API_KEY: ${{ vars.CLAWSWEEPER_RUNNER == 'openclaw' && secrets.ANTHROPIC_API_KEY || '' }} | |
| GEMINI_API_KEY: ${{ vars.CLAWSWEEPER_RUNNER == 'openclaw' && secrets.GEMINI_API_KEY || '' }} | |
| KIMI_API_KEY: ${{ vars.CLAWSWEEPER_RUNNER == 'openclaw' && secrets.KIMI_API_KEY || '' }} | |
| OPENROUTER_API_KEY: ${{ vars.CLAWSWEEPER_RUNNER == 'openclaw' && secrets.OPENROUTER_API_KEY || '' }} | |
| concurrency: | |
| group: ${{ (github.event_name == 'schedule' && (github.event.schedule == '4/20 * * * *' || github.event.schedule == '41/10 * * * *' || github.event.schedule == '37 */6 * * *')) && format('clawsweeper-target-fanout-{0}', github.event.schedule || github.run_id) || github.event_name == 'repository_dispatch' && format('clawsweeper-event-{0}-{1}', github.event.client_payload.target_repo || 'openclaw/openclaw', github.event.client_payload.queue_lease_id || github.event.client_payload.item_number || github.run_id) || format('{0}-{1}', (github.event_name == 'schedule' && github.event.schedule == '13 * * * *') && 'clawsweeper-failed-review-retry' || (github.event_name == 'workflow_dispatch' && github.event.inputs.apply_existing == 'true' && github.event.inputs.apply_sync_comments_only == 'true') && (github.event.inputs.apply_item_numbers == '__cursor__' && github.event.inputs.apply_kind == 'all' && github.event.inputs.apply_comment_sync_min_age_days == '0' && github.event.inputs.apply_limit == '40' && github.event.inputs.apply_min_age_days == '0' && github.event.inputs.apply_min_age_minutes == '' && (github.event.inputs.apply_close_reasons == '' || github.event.inputs.apply_close_reasons == (vars.CLAWSWEEPER_AUTO_CLOSE_REASONS || 'all')) && github.event.inputs.apply_stale_min_age_days == '60' && github.event.inputs.apply_close_delay_ms == '2000' && github.event.inputs.apply_checkpoint_size == '40' && 'clawsweeper-comment-sync' || format('clawsweeper-comment-sync-{0}', github.run_id)) || (github.event_name == 'schedule' && github.event.schedule == '6,21,36,51 * * * *') && 'clawsweeper-comment-sync' || (github.event_name == 'workflow_dispatch' && github.event.inputs.apply_existing == 'true') && format('clawsweeper-apply-{0}', github.run_id) || (github.event_name == 'schedule' && (github.event.schedule == '3 * * * *' || github.event.schedule == '18 * * * *' || github.event.schedule == '33 * * * *' || github.event.schedule == '48 * * * *' || github.event.schedule == '8,23,38,53 * * * *')) && 'clawsweeper-apply' || (github.event_name == 'workflow_dispatch' && (github.event.inputs.item_number != '' || github.event.inputs.item_numbers != '')) && format('clawsweeper-intake-exact-{0}', github.event.inputs.item_number || github.event.inputs.item_numbers) || (github.event_name == 'workflow_dispatch' && github.event.inputs.apply_existing != 'true') && format('clawsweeper-operator-dispatch-{0}', github.run_id) || ((github.event_name == 'workflow_dispatch' && github.event.inputs.hot_intake == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '*/5 * * * *' || github.event.schedule == '2/5 * * * *'))) && 'clawsweeper-intake-v2' || ((github.event_name == 'workflow_dispatch' && github.event.inputs.audit_dashboard == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '7 */6 * * *' || github.event.schedule == '12 */6 * * *' || github.event.schedule == '17 */6 * * *'))) && 'clawsweeper-audit' || 'clawsweeper-review', github.event.inputs.target_repo || github.event.client_payload.target_repo || ((github.event.schedule == '17 */6 * * *') && 'openclaw/clawsweeper' || ((github.event.schedule == '2/5 * * * *' || github.event.schedule == '22 * * * *' || github.event.schedule == '8,23,38,53 * * * *' || github.event.schedule == '12 */6 * * *') && 'openclaw/clawhub' || 'openclaw/openclaw'))) }} | |
| cancel-in-progress: false | |
| jobs: | |
| event-disabled-target: | |
| name: Skip disabled target event | |
| if: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.target_repo == 'openclaw/clawhub' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Explain skipped event | |
| run: | | |
| echo "Skipping ${{ github.event.client_payload.target_repo }}#${{ github.event.client_payload.item_number || '?' }}." | |
| echo "Set CLAWSWEEPER_ENABLE_CLAWHUB=1 after installing the ClawSweeper GitHub App on openclaw/clawhub." | |
| legacy-event-queue-intake: | |
| name: Queue legacy exact-review event | |
| if: ${{ github.event_name == 'repository_dispatch' && github.event.action != 'clawsweeper_target_sweep' && github.event.client_payload.queue_lease_id == '' && !(github.event.client_payload.target_repo == 'openclaw/clawhub' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Enqueue legacy event through the durable control plane | |
| env: | |
| CLIENT_PAYLOAD: ${{ toJson(github.event.client_payload) }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$CLAWSWEEPER_WEBHOOK_SECRET" | |
| queue_url="${QUEUE_URL%/}" | |
| mapfile -d '' -t legacy_intake_fields < <(node <<'NODE' | |
| const payload = JSON.parse(process.env.CLIENT_PAYLOAD || "{}"); | |
| const itemKind = payload.item_kind === "pull_request" ? "pull_request" : "issue"; | |
| const queueClaim = payload.queue_claim && typeof payload.queue_claim === "object" | |
| ? payload.queue_claim | |
| : {}; | |
| const installationId = Number(queueClaim.installation_id ?? payload.installation_id); | |
| const sourceHeadSha = String( | |
| queueClaim.source_head_sha ?? payload.source_head_sha ?? "", | |
| ) | |
| .trim() | |
| .toLowerCase(); | |
| const sourceBaseSha = String( | |
| queueClaim.source_base_sha ?? payload.source_base_sha ?? "", | |
| ) | |
| .trim() | |
| .toLowerCase(); | |
| const sourceIsDraft = queueClaim.source_is_draft ?? payload.source_is_draft; | |
| const sourceContentRevision = String( | |
| queueClaim.source_content_revision ?? payload.source_content_revision ?? "", | |
| ) | |
| .trim() | |
| .toLowerCase(); | |
| const ingressFingerprint = String(payload.ingress_fingerprint || "").trim().toLowerCase(); | |
| const targetDispatcherIngress = | |
| payload.ingress_route === "target_dispatcher" && | |
| itemKind === "pull_request" && | |
| (payload.source_event === "pull_request" || payload.source_event === "pull_request_target") && | |
| /^[0-9a-f]{64}$/.test(ingressFingerprint); | |
| process.stdout.write( | |
| `${String(payload.target_repo || "openclaw/openclaw").trim()}\0${String(payload.target_branch || "").trim()}\0${itemKind === "pull_request" && payload.source_event === "pull_request" && payload.source_action === "edited" && Number.isInteger(installationId) && installationId > 0 && /^[0-9a-f]{40}$/.test(sourceHeadSha) && /^[0-9a-f]{40}$/.test(sourceBaseSha) && typeof sourceIsDraft === "boolean" && /^[0-9a-f]{64}$/.test(sourceContentRevision) && !targetDispatcherIngress ? "1" : "0"}\0`, | |
| ); | |
| NODE | |
| ) | |
| if [ "${#legacy_intake_fields[@]}" -ne 3 ]; then | |
| echo "Invalid legacy intake field count." >&2 | |
| exit 1 | |
| fi | |
| target_repo="${legacy_intake_fields[0]}" | |
| target_branch="${legacy_intake_fields[1]}" | |
| use_source_authority="${legacy_intake_fields[2]}" | |
| if ! printf '%s' "$target_repo" | grep -Eq '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'; then | |
| echo "Invalid legacy target repository: $target_repo" >&2 | |
| exit 1 | |
| fi | |
| if [ -n "$target_branch" ] && ! printf '%s' "$target_branch" | grep -Eq '^[A-Za-z0-9_.\/-]+$'; then | |
| echo "Invalid legacy target branch for $target_repo: $target_branch" >&2 | |
| exit 1 | |
| fi | |
| queue_path="/internal/exact-review/enqueue" | |
| if [ -z "$target_branch" ]; then | |
| queue_path="/internal/exact-review/branch-authority" | |
| elif [ "$use_source_authority" = "1" ]; then | |
| queue_path="/internal/exact-review/source-authority" | |
| fi | |
| payload="$(TARGET_REPO="$target_repo" TARGET_BRANCH="$target_branch" USE_SOURCE_AUTHORITY="$use_source_authority" node <<'NODE' | |
| const payload = JSON.parse(process.env.CLIENT_PAYLOAD || "{}"); | |
| const itemKind = payload.item_kind === "pull_request" ? "pull_request" : "issue"; | |
| const sourceEvent = | |
| payload.source_event === "pull_request" || payload.source_event === "pull_request_target" | |
| ? "pull_request" | |
| : "issues"; | |
| const queueClaim = payload.queue_claim && typeof payload.queue_claim === "object" | |
| ? payload.queue_claim | |
| : {}; | |
| const reviewOptions = payload.review_options && typeof payload.review_options === "object" | |
| ? payload.review_options | |
| : {}; | |
| const dispatchKey = String(payload.dispatch_key || "").trim(); | |
| const ingressFingerprint = String(payload.ingress_fingerprint || "").trim().toLowerCase(); | |
| const ingress = | |
| payload.ingress_route === "target_dispatcher" && | |
| itemKind === "pull_request" && | |
| sourceEvent === "pull_request" && | |
| /^[0-9a-f]{64}$/.test(ingressFingerprint) | |
| ? { route: "target_dispatcher", fingerprint: ingressFingerprint } | |
| : undefined; | |
| const sourceHeadSha = String(queueClaim.source_head_sha ?? payload.source_head_sha ?? "").trim().toLowerCase(); | |
| const sourceBaseSha = String(queueClaim.source_base_sha ?? payload.source_base_sha ?? "").trim().toLowerCase(); | |
| const sourceIsDraft = queueClaim.source_is_draft ?? payload.source_is_draft; | |
| const sourceContentRevision = String(queueClaim.source_content_revision ?? payload.source_content_revision ?? "").trim().toLowerCase(); | |
| const sourceUpdatedAt = String(queueClaim.source_updated_at ?? payload.source_updated_at ?? "").trim(); | |
| const installationId = Number(queueClaim.installation_id ?? payload.installation_id); | |
| const targetBranch = String(process.env.TARGET_BRANCH || "").trim(); | |
| const sourceAuthorityRequired = process.env.USE_SOURCE_AUTHORITY === "1"; | |
| process.stdout.write( | |
| JSON.stringify({ | |
| delivery_id: dispatchKey | |
| ? `router:${dispatchKey}` | |
| : `legacy:${process.env.GITHUB_RUN_ID}:${process.env.GITHUB_RUN_ATTEMPT}`, | |
| ...(Number.isInteger(installationId) && installationId > 0 | |
| ? { installation_id: installationId } | |
| : {}), | |
| ...(!targetBranch && sourceAuthorityRequired | |
| ? { source_authority_required: true } | |
| : {}), | |
| decision: { | |
| targetRepo: process.env.TARGET_REPO, | |
| ...(targetBranch ? { targetBranch } : {}), | |
| itemNumber: Number(payload.item_number), | |
| itemKind, | |
| sourceEvent, | |
| sourceAction: payload.source_action || "legacy_dispatch", | |
| supersedesInProgress: payload.supersedes_in_progress === true, | |
| ...(typeof payload.source_delivery_id === "string" && payload.source_delivery_id | |
| ? { sourceDeliveryId: payload.source_delivery_id } | |
| : {}), | |
| ...(/^[0-9a-f]{40}$/.test(sourceHeadSha) | |
| ? { sourceHeadSha } | |
| : {}), | |
| ...(/^[0-9a-f]{40}$/.test(sourceBaseSha) | |
| ? { sourceBaseSha } | |
| : {}), | |
| ...(typeof sourceIsDraft === "boolean" ? { sourceIsDraft } : {}), | |
| ...(/^[0-9a-f]{64}$/.test(sourceContentRevision) | |
| ? { sourceContentRevision } | |
| : {}), | |
| ...(sourceUpdatedAt && Number.isFinite(Date.parse(sourceUpdatedAt)) | |
| ? { sourceUpdatedAt } | |
| : {}), | |
| ...(Number.isFinite( | |
| Number(queueClaim.codex_timeout_ms ?? reviewOptions.codex_timeout_ms ?? payload.codex_timeout_ms), | |
| ) | |
| ? { | |
| codexTimeoutMs: Number( | |
| queueClaim.codex_timeout_ms ?? reviewOptions.codex_timeout_ms ?? payload.codex_timeout_ms, | |
| ), | |
| } | |
| : {}), | |
| ...(Number.isFinite( | |
| Number( | |
| queueClaim.media_proof_timeout_ms ?? | |
| reviewOptions.media_proof_timeout_ms ?? | |
| payload.media_proof_timeout_ms, | |
| ), | |
| ) | |
| ? { | |
| mediaProofTimeoutMs: Number( | |
| queueClaim.media_proof_timeout_ms ?? | |
| reviewOptions.media_proof_timeout_ms ?? | |
| payload.media_proof_timeout_ms, | |
| ), | |
| } | |
| : {}), | |
| ...(Object.hasOwn(payload, "command_status_marker") | |
| ? { commandStatusMarker: payload.command_status_marker } | |
| : {}), | |
| ...(Object.hasOwn(payload, "status_comment_id") | |
| ? { statusCommentId: payload.status_comment_id } | |
| : {}), | |
| ...(Object.hasOwn(payload, "additional_prompt") | |
| ? { additionalPrompt: payload.additional_prompt } | |
| : {}), | |
| }, | |
| ...(ingress ? { ingress } : {}), | |
| }), | |
| ); | |
| NODE | |
| )" | |
| signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')" | |
| curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --header "x-clawsweeper-exact-review-signature: $signature" \ | |
| --data "$payload" \ | |
| "$queue_url$queue_path" >/dev/null | |
| event-review-apply: | |
| name: Review exact event item | |
| if: ${{ github.event_name == 'repository_dispatch' && github.event.action != 'clawsweeper_target_sweep' && github.event.client_payload.queue_lease_id != '' && github.event.client_payload.source_action != 'exact_review_artifact_publish' && github.event.client_payload.source_action != 'exact_review_command_acknowledgement' }} | |
| runs-on: ${{ vars.CLAWSWEEPER_REVIEW_RUNNER || 'ubuntu-latest' }} | |
| timeout-minutes: 120 | |
| concurrency: | |
| group: clawsweeper-event-review-${{ github.event.client_payload.queue_claim.item_key || github.event.client_payload.item_key || github.run_id }} | |
| cancel-in-progress: false | |
| permissions: | |
| actions: write | |
| checks: read | |
| contents: read | |
| issues: read | |
| pull-requests: read | |
| statuses: read | |
| steps: | |
| - name: Claim exact-review queue lease | |
| id: claim-exact-review-queue | |
| env: | |
| DISPATCH_PAYLOAD: ${{ toJSON(github.event.client_payload) }} | |
| ITEM_KEY: ${{ github.event.client_payload.queue_claim.item_key || github.event.client_payload.item_key }} | |
| QUEUE_LEASE_ID: ${{ github.event.client_payload.queue_lease_id }} | |
| QUEUE_LEASE_REVISION: ${{ github.event.client_payload.queue_claim.lease_revision || github.event.client_payload.lease_revision }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$QUEUE_LEASE_ID" | |
| printf 'claimed=false\ndecision={}\n' >> "$GITHUB_OUTPUT" | |
| queue_url="${QUEUE_URL%/}" | |
| payload="$(node -e ' | |
| const runAttempt = Number(process.env.RUN_ATTEMPT); | |
| if (!Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1); | |
| const itemKey = String(process.env.ITEM_KEY || "").trim(); | |
| const rawLeaseRevision = String(process.env.QUEUE_LEASE_REVISION || "").trim(); | |
| const hasTuple = Boolean(itemKey || rawLeaseRevision); | |
| const leaseRevision = Number(rawLeaseRevision); | |
| if (hasTuple && (!itemKey || !Number.isInteger(leaseRevision) || leaseRevision < 1)) { | |
| process.exit(1); | |
| } | |
| process.stdout.write(JSON.stringify({ | |
| lease_id: process.env.QUEUE_LEASE_ID, | |
| ...(hasTuple ? { item_key: itemKey, lease_revision: leaseRevision } : {}), | |
| run_id: process.env.GITHUB_RUN_ID, | |
| run_attempt: runAttempt, | |
| })); | |
| ')" | |
| response_file="$(mktemp)" | |
| error_file="$(mktemp)" | |
| trap 'rm -f "$response_file" "$error_file"' EXIT | |
| for attempt in 1 2 3; do | |
| : > "$response_file" | |
| : > "$error_file" | |
| if status="$(curl --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --output "$response_file" \ | |
| --write-out '%{http_code}' \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --data "$payload" \ | |
| "$queue_url/internal/exact-review/claim" 2>"$error_file")"; then | |
| response="$(<"$response_file")" | |
| if [ "$status" = "409" ]; then | |
| conflict_reason="$(RESPONSE="$response" node <<'NODE' | |
| const response = JSON.parse(process.env.RESPONSE || "{}"); | |
| const safeConflicts = new Set([ | |
| "lease_not_active", | |
| "lease_already_claimed", | |
| "lease_decision_unavailable", | |
| "stale_run_attempt", | |
| ]); | |
| if (!safeConflicts.has(response.error)) process.exit(1); | |
| process.stdout.write(response.error); | |
| NODE | |
| )" || { | |
| echo "Unexpected exact-review lease conflict: $response" >&2 | |
| exit 1 | |
| } | |
| echo "::notice::Skipping exact review because lease claim lost safely: $conflict_reason" | |
| exit 0 | |
| fi | |
| if [ "$status" != "200" ]; then | |
| if [[ "$status" != 5* ]]; then | |
| echo "Exact-review lease claim returned HTTP $status: $response" >&2 | |
| exit 1 | |
| fi | |
| elif RESPONSE="$response" node <<'NODE' | |
| const fs = require("node:fs"); | |
| const response = JSON.parse(process.env.RESPONSE || "{}"); | |
| const dispatch = JSON.parse(process.env.DISPATCH_PAYLOAD || "{}"); | |
| const requestedItemKey = String(process.env.ITEM_KEY || "").trim(); | |
| const requestedLeaseRevision = Number(process.env.QUEUE_LEASE_REVISION); | |
| const responseProtocol = Number(response.protocol_version || 1); | |
| if (responseProtocol !== 1 && responseProtocol !== 2) process.exit(1); | |
| if (response.claimed !== true) process.exit(1); | |
| const reviewOptions = | |
| dispatch.review_options && typeof dispatch.review_options === "object" | |
| ? dispatch.review_options | |
| : {}; | |
| const legacyDecision = { | |
| targetRepo: String(dispatch.target_repo || ""), | |
| targetBranch: String(dispatch.target_branch || "main"), | |
| itemNumber: Number(dispatch.item_number), | |
| itemKind: String(dispatch.item_kind || ""), | |
| sourceEvent: String(dispatch.source_event || ""), | |
| sourceAction: String(dispatch.source_action || "legacy_dispatch"), | |
| supersedesInProgress: dispatch.supersedes_in_progress === true, | |
| ...(/^[0-9a-f]{40}$/.test(String(dispatch.queue_claim?.source_head_sha || dispatch.source_head_sha || "").trim().toLowerCase()) | |
| ? { sourceHeadSha: String(dispatch.queue_claim?.source_head_sha || dispatch.source_head_sha).trim().toLowerCase() } | |
| : {}), | |
| ...(Number.isFinite(Number(reviewOptions.codex_timeout_ms)) | |
| ? { codexTimeoutMs: Number(reviewOptions.codex_timeout_ms) } | |
| : {}), | |
| ...(Number.isFinite(Number(reviewOptions.media_proof_timeout_ms)) | |
| ? { mediaProofTimeoutMs: Number(reviewOptions.media_proof_timeout_ms) } | |
| : {}), | |
| ...(Object.hasOwn(reviewOptions, "command_status_marker") | |
| ? { commandStatusMarker: reviewOptions.command_status_marker } | |
| : {}), | |
| ...(Object.hasOwn(reviewOptions, "status_comment_id") | |
| ? { statusCommentId: reviewOptions.status_comment_id } | |
| : {}), | |
| ...(Object.hasOwn(reviewOptions, "additional_prompt") | |
| ? { additionalPrompt: reviewOptions.additional_prompt } | |
| : {}), | |
| }; | |
| const decision = | |
| response.decision && typeof response.decision === "object" | |
| ? response.decision | |
| : legacyDecision; | |
| const targetRepo = String(decision?.targetRepo || ""); | |
| const itemNumber = Number(decision?.itemNumber); | |
| const itemKey = `${targetRepo}#${itemNumber}`; | |
| const leaseRevision = | |
| responseProtocol === 2 | |
| ? Number(response.lease_revision) | |
| : Number( | |
| response.revision || | |
| response.lease_revision || | |
| process.env.QUEUE_LEASE_REVISION, | |
| ); | |
| const claimGeneration = Number(response.claim_generation); | |
| const repeatRevision = response.repeat_revision; | |
| if ( | |
| !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(targetRepo) || | |
| !Number.isInteger(itemNumber) || | |
| itemNumber < 1 || | |
| (decision.itemKind !== "issue" && decision.itemKind !== "pull_request") || | |
| (decision.sourceEvent !== "issues" && decision.sourceEvent !== "pull_request") || | |
| typeof decision.sourceAction !== "string" || | |
| !decision.sourceAction | |
| ) { | |
| process.exit(1); | |
| } | |
| if (response.item_key && response.item_key !== itemKey) process.exit(1); | |
| if (requestedItemKey && requestedItemKey !== itemKey) process.exit(1); | |
| if ( | |
| responseProtocol === 2 && | |
| (!response.decision || | |
| typeof response.decision !== "object" || | |
| response.item_key !== requestedItemKey || | |
| response.lease_revision !== requestedLeaseRevision || | |
| !Number.isInteger(claimGeneration) || | |
| claimGeneration < 1 || | |
| typeof repeatRevision !== "boolean") | |
| ) { | |
| process.exit(1); | |
| } | |
| const output = [ | |
| "claimed=true", | |
| `lease_id=${process.env.QUEUE_LEASE_ID}`, | |
| `item_key=${itemKey}`, | |
| `lease_revision=${Number.isInteger(leaseRevision) ? leaseRevision : ""}`, | |
| `claim_generation=${responseProtocol === 2 ? claimGeneration : ""}`, | |
| `repeat_revision=${responseProtocol === 2 ? repeatRevision : false}`, | |
| `protocol_version=${responseProtocol}`, | |
| `decision=${JSON.stringify(decision)}`, | |
| ]; | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, `${output.join("\n")}\n`); | |
| NODE | |
| then | |
| exit 0 | |
| else | |
| echo "Exact-review lease claim returned an invalid success payload." >&2 | |
| exit 1 | |
| fi | |
| else | |
| cat "$error_file" >&2 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| sleep "$((attempt * 5))" | |
| fi | |
| done | |
| exit 1 | |
| - uses: actions/checkout@v7 | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' }} | |
| with: | |
| filter: blob:none | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: ./.github/actions/setup-action-ledger | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' }} | |
| continue-on-error: true | |
| - name: Resolve event payload | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' }} | |
| id: target | |
| env: | |
| CLAIM_DECISION: ${{ steps.claim-exact-review-queue.outputs.decision }} | |
| CLAWHUB_ENABLED: ${{ vars.CLAWSWEEPER_ENABLE_CLAWHUB }} | |
| CONFIGURED_CODEX_TIMEOUT_MS: ${{ vars.CLAWSWEEPER_CODEX_TIMEOUT_MS || '1200000' }} | |
| run: | | |
| set -euo pipefail | |
| node <<'NODE' | |
| const fs = require("node:fs"); | |
| const decision = JSON.parse(process.env.CLAIM_DECISION || "{}"); | |
| const targetRepo = String(decision.targetRepo || ""); | |
| const itemNumber = Number(decision.itemNumber); | |
| if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(targetRepo)) process.exit(1); | |
| if (!Number.isInteger(itemNumber) || itemNumber < 1) process.exit(1); | |
| // The review-start lease permits at most two hours, including its ten-minute | |
| // cushion. The 120-minute job leaves 64 minutes for target checkout/setup and | |
| // artifact/publication finalizers after this cap and the 11-minute review reserve. | |
| const maxExactReviewCodexTimeoutMs = 2_700_000; | |
| const configuredValue = Number(process.env.CONFIGURED_CODEX_TIMEOUT_MS); | |
| const configuredTimeout = | |
| Number.isInteger(configuredValue) && configuredValue > 0 | |
| ? Math.min(maxExactReviewCodexTimeoutMs, configuredValue) | |
| : 1_200_000; | |
| const adaptiveValue = Number(decision.codexTimeoutMs); | |
| const adaptiveTimeout = | |
| Number.isInteger(adaptiveValue) && adaptiveValue > 0 | |
| ? Math.min(1_800_000, Math.max(600_000, adaptiveValue)) | |
| : 0; | |
| const mediaValue = Number(decision.mediaProofTimeoutMs); | |
| const mediaTimeout = | |
| Number.isInteger(mediaValue) && mediaValue > 0 ? Math.min(480_000, mediaValue) : 0; | |
| const [owner, name] = targetRepo.split("/"); | |
| const targetSlug = targetRepo.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, "-"); | |
| const checkoutDir = targetRepo === process.env.GITHUB_REPOSITORY ? `${name}-target` : name; | |
| const hasCommandContext = Boolean(decision.commandStatusMarker || decision.statusCommentId); | |
| const targetEnabled = | |
| targetRepo !== "openclaw/clawhub" || process.env.CLAWHUB_ENABLED === "1"; | |
| const output = { | |
| target_repo: targetRepo, | |
| target_repo_owner: owner, | |
| target_repo_name: name, | |
| target_slug: targetSlug, | |
| target_checkout_dir: checkoutDir, | |
| item_number: itemNumber, | |
| codex_timeout_ms: Math.min( | |
| maxExactReviewCodexTimeoutMs, | |
| Math.max(configuredTimeout, adaptiveTimeout), | |
| ), | |
| media_proof_timeout_ms: mediaTimeout, | |
| has_command_context: hasCommandContext, | |
| target_enabled: targetEnabled, | |
| }; | |
| fs.appendFileSync( | |
| process.env.GITHUB_OUTPUT, | |
| `${Object.entries(output) | |
| .map(([key, value]) => `${key}=${value}`) | |
| .join("\n")}\n`, | |
| ); | |
| NODE | |
| - name: Skip disabled claimed target | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.target.outputs.target_enabled == 'false' }} | |
| run: echo "Completing the claimed exact-review lease without target access because this target is disabled." | |
| - name: Create target read token | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.target.outputs.target_enabled == 'true' }} | |
| id: target-read-token | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| owner: ${{ steps.target.outputs.target_repo_owner }} | |
| repositories: ${{ steps.target.outputs.target_repo_name }} | |
| permission-checks: read | |
| permission-contents: read | |
| permission-issues: read | |
| permission-pull-requests: read | |
| permission-statuses: read | |
| - name: Check live target item state | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.target.outputs.target_enabled == 'true' }} | |
| id: live-item | |
| env: | |
| CLAIM_DECISION: ${{ steps.claim-exact-review-queue.outputs.decision }} | |
| GH_TOKEN: ${{ steps.target.outputs.target_repo == 'openclaw/openclaw' && github.token || steps.target-read-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| CLAIM_TARGET_BRANCH: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).targetBranch }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$GH_TOKEN" | |
| target_branch="$CLAIM_TARGET_BRANCH" | |
| branch_is_usable() { | |
| local branch="$1" | |
| [ -n "$branch" ] && | |
| printf '%s' "$branch" | grep -Eq '^[A-Za-z0-9_.\/-]+$' && | |
| ! printf '%s' "$branch" | grep -Eq '^[0-9]+$' && | |
| [[ "$branch" != *".."* ]] | |
| } | |
| if ! branch_is_usable "$target_branch"; then | |
| queued_target_branch="$target_branch" | |
| branch_error="$(mktemp)" | |
| if ! target_branch="$(gh api "repos/$TARGET_REPO" --jq '.default_branch // empty' 2>"$branch_error")" || | |
| ! branch_is_usable "$target_branch"; then | |
| echo "::warning::Unable to resolve a usable default branch for $TARGET_REPO after queued target branch '$queued_target_branch' was rejected; releasing the claim for retry." | |
| cat "$branch_error" >&2 | |
| rm -f "$branch_error" | |
| retry_at="$(node -e 'process.stdout.write(new Date(Date.now() + 5 * 60_000).toISOString())')" | |
| { | |
| echo "admission_retry=true" | |
| echo "retry_at=$retry_at" | |
| echo "proceed=false" | |
| echo "terminal_noop=false" | |
| echo "terminal_missing=false" | |
| echo "guarded_open=false" | |
| echo "guarded_open_action=" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| rm -f "$branch_error" | |
| echo "::warning::Resolved invalid queued target branch '$queued_target_branch' to default branch '$target_branch' for $TARGET_REPO." | |
| fi | |
| effective_decision="$(CLAIM_DECISION="$CLAIM_DECISION" TARGET_BRANCH="$target_branch" node <<'NODE' | |
| const decision = JSON.parse(process.env.CLAIM_DECISION || "{}"); | |
| decision.targetBranch = process.env.TARGET_BRANCH; | |
| process.stdout.write(JSON.stringify(decision)); | |
| NODE | |
| )" | |
| { | |
| echo "admission_retry=false" | |
| echo "target_branch=$target_branch" | |
| echo "decision=$effective_decision" | |
| } >> "$GITHUB_OUTPUT" | |
| live_item_error="$(mktemp)" | |
| if ! live_item="$(gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER" 2>"$live_item_error")"; then | |
| if grep -Eqi 'HTTP 404|Not Found' "$live_item_error" && gh api "repos/$TARGET_REPO" >/dev/null; then | |
| rm -f "$live_item_error" | |
| echo "proceed=false" >> "$GITHUB_OUTPUT" | |
| echo "terminal_noop=false" >> "$GITHUB_OUTPUT" | |
| echo "terminal_missing=true" >> "$GITHUB_OUTPUT" | |
| echo "guarded_open=false" >> "$GITHUB_OUTPUT" | |
| echo "guarded_open_action=" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Completing $TARGET_REPO#$ITEM_NUMBER because the repository is accessible but the item is missing." | |
| exit 0 | |
| fi | |
| if grep -Eqi 'rate limit exceeded|secondary rate limit|HTTP 429' "$live_item_error"; then | |
| cat "$live_item_error" >&2 | |
| rm -f "$live_item_error" | |
| retry_at="$(node -e 'process.stdout.write(new Date(Date.now() + 20 * 60_000).toISOString())')" | |
| echo "::notice::GitHub throttled the live-item check for $TARGET_REPO#$ITEM_NUMBER; releasing the claim for retry at $retry_at without spending failure budget." | |
| { | |
| echo "admission_retry=true" | |
| echo "retry_kind=throttle" | |
| echo "retry_at=$retry_at" | |
| echo "proceed=false" | |
| echo "terminal_noop=false" | |
| echo "terminal_missing=false" | |
| echo "guarded_open=false" | |
| echo "guarded_open_action=" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| cat "$live_item_error" >&2 | |
| rm -f "$live_item_error" | |
| exit 1 | |
| fi | |
| rm -f "$live_item_error" | |
| live_state="$(jq -r '.state' <<< "$live_item")" | |
| live_locked="$(jq -r '.locked == true' <<< "$live_item")" | |
| item_kind="$(jq -r 'if .pull_request then "pull_request" else "issue" end' <<< "$live_item")" | |
| echo "item_kind=$item_kind" >> "$GITHUB_OUTPUT" | |
| if [ "$live_state" = "open" ] && [ "$live_locked" != "true" ]; then | |
| live_head_sha="" | |
| if [ "$item_kind" = "pull_request" ]; then | |
| if ! live_head_sha="$(gh api "repos/$TARGET_REPO/pulls/$ITEM_NUMBER" --jq '.head.sha')"; then | |
| echo "::warning::Unable to read the live pull request head; preserving normal review admission." | |
| live_head_sha="" | |
| fi | |
| fi | |
| comments_error="$(mktemp)" | |
| if live_comments="$(gh api --paginate --slurp "repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments?per_page=100" 2>"$comments_error" | jq 'add')"; then | |
| classification_output="$(mktemp)" | |
| if CLAIM_DECISION="$CLAIM_DECISION" LIVE_ITEM="$live_item" LIVE_COMMENTS="$live_comments" LIVE_HEAD_SHA="$live_head_sha" GITHUB_OUTPUT="$classification_output" \ | |
| node scripts/classify-scheduled-review-noop.ts; then | |
| cat "$classification_output" >> "$GITHUB_OUTPUT" | |
| if grep -qx 'scheduled_noop=true' "$classification_output"; then | |
| reason="$(sed -n 's/^scheduled_noop_reason=//p' "$classification_output")" | |
| rm -f "$classification_output" "$comments_error" | |
| { | |
| echo "proceed=false" | |
| echo "terminal_noop=false" | |
| echo "scheduled_semantic_noop=true" | |
| echo "terminal_missing=false" | |
| echo "guarded_open=false" | |
| echo "guarded_open_action=" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "::notice::Completing $TARGET_REPO#$ITEM_NUMBER as a scheduled no-op before target checkout because $reason." | |
| exit 0 | |
| fi | |
| else | |
| echo "::warning::Scheduled no-op classification failed; preserving normal review admission." | |
| fi | |
| rm -f "$classification_output" | |
| else | |
| echo "::warning::Unable to read comments for scheduled no-op classification; preserving normal review admission." | |
| cat "$comments_error" >&2 | |
| fi | |
| rm -f "$comments_error" | |
| fi | |
| case "$live_state" in | |
| open) | |
| echo "scheduled_semantic_noop=false" >> "$GITHUB_OUTPUT" | |
| echo "terminal_noop=false" >> "$GITHUB_OUTPUT" | |
| echo "terminal_missing=false" >> "$GITHUB_OUTPUT" | |
| if [ "$live_locked" = "true" ]; then | |
| echo "proceed=false" >> "$GITHUB_OUTPUT" | |
| echo "guarded_open=true" >> "$GITHUB_OUTPUT" | |
| echo "guarded_open_action=skipped_locked_conversation" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Completing $TARGET_REPO#$ITEM_NUMBER without Codex because the open conversation is locked." | |
| else | |
| echo "proceed=true" >> "$GITHUB_OUTPUT" | |
| echo "guarded_open=false" >> "$GITHUB_OUTPUT" | |
| echo "guarded_open_action=" >> "$GITHUB_OUTPUT" | |
| fi | |
| ;; | |
| closed) | |
| echo "scheduled_semantic_noop=false" >> "$GITHUB_OUTPUT" | |
| echo "proceed=false" >> "$GITHUB_OUTPUT" | |
| echo "terminal_noop=true" >> "$GITHUB_OUTPUT" | |
| echo "terminal_missing=false" >> "$GITHUB_OUTPUT" | |
| echo "guarded_open=false" >> "$GITHUB_OUTPUT" | |
| echo "guarded_open_action=" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Skipping terminal $TARGET_REPO#$ITEM_NUMBER because it is already closed." | |
| ;; | |
| *) | |
| echo "Unexpected live state for $TARGET_REPO#$ITEM_NUMBER: $live_state" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| - name: Create target write token | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outcome == 'success' && steps.live-item.outputs.scheduled_semantic_noop != 'true' && steps.live-item.outputs.admission_retry != 'true' }} | |
| id: target-write-token | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| owner: ${{ steps.target.outputs.target_repo_owner }} | |
| repositories: ${{ steps.target.outputs.target_repo_name }} | |
| permission-checks: read | |
| permission-contents: write | |
| permission-issues: write | |
| permission-pull-requests: write | |
| permission-statuses: read | |
| - name: React to target item review start | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }} | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$GH_TOKEN" | |
| react() { | |
| local content="$1" | |
| local err | |
| err="$(mktemp)" | |
| if gh api -X POST \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions" \ | |
| -f content="$content" 2>"$err" >/dev/null; then | |
| echo "Added $content reaction to $TARGET_REPO#$ITEM_NUMBER." | |
| elif grep -qi "HTTP 422\\|already exists" "$err"; then | |
| echo "$content reaction already exists on $TARGET_REPO#$ITEM_NUMBER." | |
| else | |
| cat "$err" >&2 | |
| return 1 | |
| fi | |
| rm -f "$err" | |
| } | |
| react "eyes" | |
| - uses: ./.github/actions/setup-pnpm | |
| id: setup-pnpm | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outcome == 'success' && steps.live-item.outputs.scheduled_semantic_noop != 'true' && steps.live-item.outputs.admission_retry != 'true' }} | |
| with: | |
| build-script: ${{ steps.live-item.outputs.proceed == 'true' && 'build:all' || 'build:repair' }} | |
| - uses: actions/cache@v6 | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }} | |
| with: | |
| path: ${{ steps.target.outputs.target_checkout_dir }}-cache.git | |
| key: ${{ steps.target.outputs.target_repo_name }}-event-git-${{ runner.os }}-${{ github.run_id }} | |
| restore-keys: | | |
| ${{ steps.target.outputs.target_repo_name }}-event-git-${{ runner.os }}- | |
| ${{ steps.target.outputs.target_repo_name }}-git-${{ runner.os }}- | |
| - name: Check out target repository | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }} | |
| run: | | |
| set -euo pipefail | |
| url="https://github.com/${{ steps.target.outputs.target_repo }}.git" | |
| cache_dir="${{ steps.target.outputs.target_checkout_dir }}-cache.git" | |
| checkout_dir="${{ steps.target.outputs.target_checkout_dir }}" | |
| target_branch="${{ steps.live-item.outputs.target_branch }}" | |
| if [ -d "$cache_dir" ]; then | |
| git -C "$cache_dir" remote set-url origin "$url" | |
| git -C "$cache_dir" config remote.origin.promisor true | |
| git -C "$cache_dir" config remote.origin.partialclonefilter blob:none | |
| if [ -f "$cache_dir/shallow" ]; then | |
| cache_fetch=(git -C "$cache_dir" fetch --prune --unshallow --filter=blob:none origin "$target_branch") | |
| else | |
| cache_fetch=(git -C "$cache_dir" fetch --prune --filter=blob:none origin "$target_branch") | |
| fi | |
| if ! "${cache_fetch[@]}"; then | |
| echo "::warning::Cached target repository fetch failed; rebuilding cache." | |
| rm -rf "$cache_dir" | |
| git clone --bare --filter=blob:none --single-branch --branch "$target_branch" "$url" "$cache_dir" | |
| fi | |
| else | |
| git clone --bare --filter=blob:none --single-branch --branch "$target_branch" "$url" "$cache_dir" | |
| fi | |
| if ! git clone --reference-if-able "$cache_dir" --dissociate --filter=blob:none --branch "$target_branch" --single-branch "$url" "$checkout_dir"; then | |
| echo "::warning::Cached target checkout failed; retrying without cache reference." | |
| rm -rf "$checkout_dir" "$cache_dir" | |
| git clone --bare --filter=blob:none --single-branch --branch "$target_branch" "$url" "$cache_dir" | |
| git clone --filter=blob:none --branch "$target_branch" --single-branch "$url" "$checkout_dir" | |
| fi | |
| git -C "$checkout_dir" rev-parse --short HEAD | |
| - name: Mark re-review command in progress | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }} | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| COMMAND_STATUS_MARKER: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).commandStatusMarker || '' }} | |
| STATUS_COMMENT_ID: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).statusCommentId || '' }} | |
| RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| pnpm run repair:update-command-status -- \ | |
| --repo "$TARGET_REPO" \ | |
| --item-number "$ITEM_NUMBER" \ | |
| --marker "$COMMAND_STATUS_MARKER" \ | |
| --status-comment-id "$STATUS_COMMENT_ID" \ | |
| --state "Review in progress" \ | |
| --detail "The exact-review queue leased this run; Codex is reviewing the item." \ | |
| --run-url "$RUN_URL" \ | |
| --wait-ms 120000 | |
| - uses: ./.github/actions/setup-codex | |
| if: ${{ env.CLAWSWEEPER_RUNNER != 'openclaw' && steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }} | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| CLAWSWEEPER_INTERNAL_MODEL: ${{ secrets.CLAWSWEEPER_MODEL }} | |
| with: | |
| login-status: "true" | |
| - uses: ./.github/actions/setup-openclaw | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }} | |
| - name: Reserve exact review lease | |
| id: reserve-exact-review-lease | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }} | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| CODEX_TIMEOUT_MS: ${{ steps.target.outputs.codex_timeout_ms }} | |
| MEDIA_PROOF_TIMEOUT_MS: ${{ steps.target.outputs.media_proof_timeout_ms }} | |
| EXACT_REVIEW_ITEM_KEY: ${{ steps.claim-exact-review-queue.outputs.item_key }} | |
| EXACT_REVIEW_LEASE_ID: ${{ steps.claim-exact-review-queue.outputs.lease_id }} | |
| EXACT_REVIEW_LEASE_REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }} | |
| EXACT_REVIEW_CLAIM_GENERATION: ${{ steps.claim-exact-review-queue.outputs.claim_generation }} | |
| EXACT_REVIEW_SOURCE_HEAD_SHA: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).sourceHeadSha || '' }} | |
| EXACT_REVIEW_QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$GH_TOKEN" | |
| if ! [[ "$CODEX_TIMEOUT_MS" =~ ^[0-9]+$ ]] || [ "$CODEX_TIMEOUT_MS" -lt 1 ]; then | |
| echo "Invalid Codex timeout: $CODEX_TIMEOUT_MS" >&2 | |
| exit 1 | |
| fi | |
| if ! [[ "$MEDIA_PROOF_TIMEOUT_MS" =~ ^[0-9]+$ ]] || [ "$MEDIA_PROOF_TIMEOUT_MS" -lt 0 ]; then | |
| echo "Invalid media proof timeout: $MEDIA_PROOF_TIMEOUT_MS" >&2 | |
| exit 1 | |
| fi | |
| codex_timeout_seconds=$(((CODEX_TIMEOUT_MS + 999) / 1000)) | |
| media_preprocessing_reserve_seconds=480 | |
| review_timeout_ms=$(((codex_timeout_seconds + media_preprocessing_reserve_seconds + 180) * 1000)) | |
| reservation="" | |
| reservation_error="$(mktemp)" | |
| trap 'rm -f "$reservation_error"' EXIT | |
| for attempt in 1 2 3 4 5; do | |
| : > "$reservation_error" | |
| if reservation="$(pnpm run --silent reserve-review-lease -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --item-number "$ITEM_NUMBER" \ | |
| --review-timeout-ms "$review_timeout_ms" 2>"$reservation_error")"; then | |
| break | |
| else | |
| reserve_exit=$? | |
| fi | |
| if grep -Eqi 'rate limit exceeded|secondary rate limit|HTTP 429' "$reservation_error"; then | |
| retry_at="$(date -u -d "@$(( $(date -u +%s) + 1200 ))" +%Y-%m-%dT%H:%M:%SZ)" | |
| echo "::notice::GitHub throttled the exact-review reservation; deferring until $retry_at so the durable queue retries without spending failure budget." | |
| reservation="{\"status\":\"held\",\"retryAt\":\"$retry_at\",\"retryKind\":\"throttle\"}" | |
| break | |
| fi | |
| if ! grep -Eqi '(exact-review queue authority changed|review revision changed|could not (confirm|identify).+review lease).+retry required' "$reservation_error"; then | |
| cat "$reservation_error" >&2 | |
| exit "$reserve_exit" | |
| fi | |
| if [ "$attempt" -eq 5 ]; then | |
| echo "::notice::Exact-review reservation contention did not settle after 5 attempts; queue authority moved on, so this run is a successful no-op." | |
| reservation='{"status":"superseded"}' | |
| break | |
| fi | |
| delay_seconds=$((attempt * 2 + RANDOM % 4)) | |
| echo "::notice::Exact-review reservation contention on attempt $attempt/5; retrying in ${delay_seconds}s." | |
| sleep "$delay_seconds" | |
| done | |
| echo "$reservation" | |
| RESERVATION="$reservation" node <<'NODE' | |
| const fs = require("node:fs"); | |
| const reservation = JSON.parse(process.env.RESERVATION || "{}"); | |
| const append = (key, value) => fs.appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`); | |
| if (reservation.status === "posted") { | |
| const owner = String(reservation.owner || ""); | |
| const commentId = Number(reservation.commentId); | |
| if (!/^[a-zA-Z0-9._-]{1,200}$/.test(owner) || !Number.isInteger(commentId) || commentId <= 0) { | |
| process.exit(1); | |
| } | |
| append("status", "posted"); | |
| append("owner", owner); | |
| append("comment_id", String(commentId)); | |
| process.exit(0); | |
| } | |
| if (reservation.status === "held") { | |
| const retryAt = String(reservation.retryAt || ""); | |
| if (!Number.isFinite(Date.parse(retryAt))) process.exit(1); | |
| const retryKind = | |
| reservation.retryKind === "throttle" ? "throttle" : "coordination"; | |
| append("status", "held"); | |
| append("retry_kind", retryKind); | |
| append("retry_at", new Date(retryAt).toISOString()); | |
| process.exit(0); | |
| } | |
| if (reservation.status === "superseded") { | |
| append("status", "superseded"); | |
| process.exit(0); | |
| } | |
| process.exit(1); | |
| NODE | |
| # At-least-once queue delivery can overlap an exact-head review that is already | |
| # running. The durable queue owns retry, so lease contention is deferred work. | |
| - name: Defer exact review while same-head lease is held | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && (steps.reserve-exact-review-lease.outputs.status == 'held' || steps.reserve-exact-review-lease.outputs.status == 'superseded') }} | |
| env: | |
| RESERVATION_STATUS: ${{ steps.reserve-exact-review-lease.outputs.status }} | |
| RETRY_AT: ${{ steps.reserve-exact-review-lease.outputs.retry_at }} | |
| run: | | |
| if [ "$RESERVATION_STATUS" = "superseded" ]; then | |
| echo "::notice::Exact-review queue authority moved to another worker; this run completed as a successful no-op." | |
| else | |
| echo "::notice::Another exact-head review is already active; retry deferred until $RETRY_AT." | |
| fi | |
| - name: Review exact event item | |
| id: review-exact-event-item | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' && steps.reserve-exact-review-lease.outputs.status == 'posted' }} | |
| continue-on-error: true | |
| env: | |
| # Review hydration is the largest read stream (~30 calls/review); it | |
| # stays on the App installation bucket so the per-repo Actions pool | |
| # keeps headroom for the comment-router and support lanes. | |
| GH_TOKEN: ${{ steps.target-read-token.outputs.token }} | |
| CLAWSWEEPER_PROOF_INSPECTION_TOKEN: ${{ steps.target-read-token.outputs.token }} | |
| ADDITIONAL_PROMPT: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).additionalPrompt || '' }} | |
| CLAWSWEEPER_RELATED_GITHUB_SEARCH: ${{ vars.CLAWSWEEPER_RELATED_GITHUB_SEARCH || '1' }} | |
| EXACT_REVIEW_ITEM_KEY: ${{ steps.claim-exact-review-queue.outputs.item_key }} | |
| EXACT_REVIEW_LEASE_ID: ${{ steps.claim-exact-review-queue.outputs.lease_id }} | |
| EXACT_REVIEW_LEASE_REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }} | |
| EXACT_REVIEW_CLAIM_GENERATION: ${{ steps.claim-exact-review-queue.outputs.claim_generation }} | |
| EXACT_REVIEW_SOURCE_HEAD_SHA: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).sourceHeadSha || '' }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| REVIEW_LEASE_OWNER: ${{ steps.reserve-exact-review-lease.outputs.owner }} | |
| REVIEW_LEASE_COMMENT_ID: ${{ steps.reserve-exact-review-lease.outputs.comment_id }} | |
| SOURCE_ACTION: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).sourceAction || '' }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$GH_TOKEN" | |
| heartbeat_pid="" | |
| review_pid="" | |
| review_pgid="" | |
| superseded_marker="$RUNNER_TEMP/exact-review-superseded-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | |
| rm -f "$superseded_marker" | |
| terminate_review_group() { | |
| [ -n "$review_pgid" ] || return 0 | |
| kill -TERM -- "-$review_pgid" 2>/dev/null || true | |
| } | |
| wait_for_review_group() { | |
| [ -n "$review_pgid" ] || return 0 | |
| for _ in {1..30}; do | |
| if ! kill -0 -- "-$review_pgid" 2>/dev/null; then | |
| return 0 | |
| fi | |
| sleep 1 | |
| done | |
| kill -KILL -- "-$review_pgid" 2>/dev/null || true | |
| for _ in {1..50}; do | |
| if ! kill -0 -- "-$review_pgid" 2>/dev/null; then | |
| return 0 | |
| fi | |
| sleep 0.1 | |
| done | |
| echo "::error::Exact-review process group $review_pgid did not terminate." | |
| return 1 | |
| } | |
| heartbeat_loop() { | |
| while true; do | |
| heartbeat_status="$(curl --silent --connect-timeout 5 --max-time 20 \ | |
| --output /dev/null \ | |
| --write-out '%{http_code}' \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --data-binary "$heartbeat_payload" \ | |
| "${QUEUE_URL%/}/internal/exact-review/heartbeat" 2>/dev/null)" || heartbeat_status="" | |
| if [ "$heartbeat_status" = "409" ]; then | |
| echo "::notice::Exact-review heartbeat lost its lease tuple." | |
| : > "$superseded_marker" | |
| terminate_review_group | |
| return | |
| fi | |
| sleep 60 | |
| done | |
| } | |
| start_heartbeat() { | |
| # Heartbeat authenticates by the full lease tuple (same capability model as | |
| # claim/complete), so this Codex-adjacent step never sees the webhook secret. | |
| heartbeat_payload="$(node -e ' | |
| const leaseRevision = Number(process.env.EXACT_REVIEW_LEASE_REVISION); | |
| const claimGeneration = Number(process.env.EXACT_REVIEW_CLAIM_GENERATION); | |
| const runAttempt = Number(process.env.GITHUB_RUN_ATTEMPT); | |
| const sourceHeadSha = String(process.env.EXACT_REVIEW_SOURCE_HEAD_SHA || "").trim().toLowerCase(); | |
| if (!process.env.EXACT_REVIEW_ITEM_KEY || !process.env.EXACT_REVIEW_LEASE_ID) process.exit(1); | |
| if (!Number.isInteger(leaseRevision) || leaseRevision < 1) process.exit(1); | |
| if (!Number.isInteger(claimGeneration) || claimGeneration < 1) process.exit(1); | |
| if (!Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1); | |
| if (sourceHeadSha && !/^[0-9a-f]{40}$/.test(sourceHeadSha)) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| item_key: process.env.EXACT_REVIEW_ITEM_KEY, | |
| lease_id: process.env.EXACT_REVIEW_LEASE_ID, | |
| lease_revision: leaseRevision, | |
| claim_generation: claimGeneration, | |
| run_id: process.env.GITHUB_RUN_ID, | |
| run_attempt: runAttempt, | |
| ...(sourceHeadSha ? { source_head_sha: sourceHeadSha } : {}), | |
| })); | |
| ')" || return 0 | |
| heartbeat_loop & | |
| heartbeat_pid=$! | |
| } | |
| cleanup_heartbeat() { | |
| [ -n "$heartbeat_pid" ] || return 0 | |
| kill "$heartbeat_pid" 2>/dev/null || true | |
| wait "$heartbeat_pid" 2>/dev/null || true | |
| } | |
| mark_finalizing() { | |
| finalizing_payload="$(HEARTBEAT_PAYLOAD="$heartbeat_payload" node -e ' | |
| const payload = JSON.parse(process.env.HEARTBEAT_PAYLOAD || "{}"); | |
| process.stdout.write(JSON.stringify({ ...payload, phase: "finalizing" })); | |
| ')" || return 1 | |
| for attempt in 1 2 3; do | |
| finalizing_status="$(curl --silent --connect-timeout 5 --max-time 20 \ | |
| --output /dev/null \ | |
| --write-out '%{http_code}' \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --data-binary "$finalizing_payload" \ | |
| "${QUEUE_URL%/}/internal/exact-review/heartbeat" 2>/dev/null)" || finalizing_status="" | |
| if [ "$finalizing_status" = "200" ]; then | |
| return 0 | |
| fi | |
| if [ "$finalizing_status" = "409" ]; then | |
| echo "::notice::Exact-review finalization lost its lease tuple." | |
| : > "$superseded_marker" | |
| return 0 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| sleep "$((attempt * 5))" | |
| fi | |
| done | |
| echo "::error::Exact-review lease could not enter finalization." | |
| return 1 | |
| } | |
| trap cleanup_heartbeat EXIT | |
| codex_timeout_ms="${{ steps.target.outputs.codex_timeout_ms }}" | |
| if ! [[ "$codex_timeout_ms" =~ ^[0-9]+$ ]] || [ "$codex_timeout_ms" -lt 1 ]; then | |
| echo "Invalid Codex timeout: $codex_timeout_ms" >&2 | |
| exit 1 | |
| fi | |
| additional_prompt_arg=() | |
| if [ -n "$ADDITIONAL_PROMPT" ]; then | |
| additional_prompt_arg=(--additional-prompt "$ADDITIONAL_PROMPT") | |
| fi | |
| codex_timeout_seconds=$(((codex_timeout_ms + 999) / 1000)) | |
| media_proof_timeout_ms="${{ steps.target.outputs.media_proof_timeout_ms }}" | |
| media_proof_timeout_seconds=$(((media_proof_timeout_ms + 999) / 1000)) | |
| media_preprocessing_reserve_seconds=480 | |
| review_timeout_seconds=$((codex_timeout_seconds + media_preprocessing_reserve_seconds + 180)) | |
| echo "::notice::Exact event review timeout is ${review_timeout_seconds}s for per-item Codex timeout ${codex_timeout_seconds}s, reserved media preprocessing ${media_preprocessing_reserve_seconds}s, and detected media allowance ${media_proof_timeout_seconds}s." | |
| set +e | |
| setsid timeout --kill-after=30s "${review_timeout_seconds}s" pnpm run review -- \ | |
| --target-repo "${{ steps.target.outputs.target_repo }}" \ | |
| --target-dir "${{ steps.target.outputs.target_checkout_dir }}" \ | |
| --artifact-dir artifacts/event \ | |
| --batch-size 1 \ | |
| --max-pages 1 \ | |
| --codex-model internal \ | |
| --codex-reasoning-effort high \ | |
| --codex-sandbox read-only \ | |
| --codex-timeout-ms "$codex_timeout_ms" \ | |
| --item-numbers "${{ steps.target.outputs.item_number }}" \ | |
| --readonly-openclaw \ | |
| --skip-start-comment \ | |
| --review-lease-owner "$REVIEW_LEASE_OWNER" \ | |
| --review-lease-comment-id "$REVIEW_LEASE_COMMENT_ID" \ | |
| --shard-index 0 \ | |
| --shard-count 1 \ | |
| --review-source-action "$SOURCE_ACTION" \ | |
| "${additional_prompt_arg[@]}" & | |
| review_pid=$! | |
| review_pgid=$review_pid | |
| start_heartbeat | |
| wait "$review_pid" | |
| review_exit_code=$? | |
| cleanup_heartbeat | |
| if [ "$review_exit_code" -eq 0 ] && [ ! -f "$superseded_marker" ]; then | |
| mark_finalizing || review_exit_code=1 | |
| fi | |
| if [ -f "$superseded_marker" ]; then | |
| wait_for_review_group | |
| echo "superseded=true" >> "$GITHUB_OUTPUT" | |
| echo "exit_code=0" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Exact-review generation stopped because another worker owns the newer queue authority; completing this run as a successful no-op." | |
| exit 0 | |
| fi | |
| set -e | |
| echo "exit_code=$review_exit_code" >> "$GITHUB_OUTPUT" | |
| if [ "$review_exit_code" -ne 0 ]; then | |
| exit "$review_exit_code" | |
| fi | |
| coordination_held_path="artifacts/event/coordination-held.json" | |
| if [ -f "$coordination_held_path" ]; then | |
| retry_at="$(node -e ' | |
| const data = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")); | |
| const value = String(data.retry_at || "").trim(); | |
| const timestamp = Date.parse(value); | |
| if (!value || !Number.isFinite(timestamp)) process.exit(1); | |
| process.stdout.write(new Date(timestamp).toISOString()); | |
| ' "$coordination_held_path")" | |
| echo "retry_kind=coordination" >> "$GITHUB_OUTPUT" | |
| echo "retry_at=$retry_at" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Review coordination is held until $retry_at." | |
| fi | |
| report_path="artifacts/event/${{ steps.target.outputs.item_number }}.md" | |
| if [ ! -f "$report_path" ]; then | |
| live_state="$(gh api "repos/${{ steps.target.outputs.target_repo }}/issues/${{ steps.target.outputs.item_number }}" --jq .state)" | |
| if [ "$live_state" != "closed" ]; then | |
| echo "Exact review produced no artifact for open item ${{ steps.target.outputs.target_repo }}#${{ steps.target.outputs.item_number }}." >&2 | |
| exit 1 | |
| fi | |
| echo "::notice::Exact review produced no artifact because the item closed during review." | |
| echo "terminal_during_review=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Finalize exact event action ledger | |
| if: ${{ always() && steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }} | |
| continue-on-error: true | |
| env: | |
| REVIEW_EXIT_CODE: ${{ steps.review-exact-event-item.outputs.exit_code || '' }} | |
| REVIEW_OUTCOME: ${{ steps.review-exact-event-item.outcome || 'not_started' }} | |
| REVIEW_SUPERSEDED: ${{ steps.review-exact-event-item.outputs.superseded || 'false' }} | |
| run: | | |
| args=() | |
| if [ "$REVIEW_SUPERSEDED" = "true" ] || [ "$REVIEW_OUTCOME" = "cancelled" ] || [ "$REVIEW_EXIT_CODE" = "130" ] || [ "$REVIEW_EXIT_CODE" = "143" ]; then | |
| args=(--interrupt-open-attempts --reason cancelled) | |
| elif [ "$REVIEW_EXIT_CODE" = "124" ] || [ "$REVIEW_EXIT_CODE" = "137" ]; then | |
| args=(--interrupt-open-attempts --reason timeout) | |
| elif [ "$REVIEW_OUTCOME" != "success" ]; then | |
| args=(--interrupt-open-attempts --reason workflow_failed) | |
| fi | |
| pnpm run --silent finalize-action-events -- "${args[@]}" | |
| - name: Create exact review artifact bundle | |
| id: create-exact-review-bundle | |
| if: ${{ always() && steps.claim-exact-review-queue.outputs.claimed == 'true' && !cancelled() && steps.target.outputs.target_enabled == 'true' && steps.live-item.outcome == 'success' && steps.live-item.outputs.scheduled_semantic_noop != 'true' && steps.live-item.outputs.admission_retry != 'true' && steps.setup-pnpm.outcome == 'success' && steps.review-exact-event-item.outputs.superseded != 'true' && (steps.live-item.outputs.proceed != 'true' || (steps.review-exact-event-item.outcome == 'success' && steps.review-exact-event-item.outputs.retry_at == '')) }} | |
| env: | |
| EXACT_REVIEW_ACTION_LEDGER_ROOT: ${{ env.CLAWSWEEPER_ACTION_LEDGER_OUTPUT_ROOT }} | |
| EXACT_REVIEW_BUNDLE_DIR: .artifacts/exact-review-bundle | |
| EXACT_REVIEW_CLAIM_GENERATION: ${{ steps.claim-exact-review-queue.outputs.claim_generation }} | |
| EXACT_REVIEW_DECISION: ${{ steps.live-item.outputs.decision }} | |
| EXACT_REVIEW_GENERATION_ATTEMPT: ${{ github.run_attempt }} | |
| EXACT_REVIEW_ITEM_KEY: ${{ steps.claim-exact-review-queue.outputs.item_key }} | |
| EXACT_REVIEW_ITEM_KIND: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).itemKind }} | |
| EXACT_REVIEW_ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| EXACT_REVIEW_LEASE_REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }} | |
| EXACT_REVIEW_LIVE_GUARDED_OPEN: ${{ steps.live-item.outputs.guarded_open }} | |
| EXACT_REVIEW_LIVE_PROCEEDED: ${{ steps.review-exact-event-item.outputs.terminal_during_review == 'true' && 'false' || steps.live-item.outputs.proceed }} | |
| EXACT_REVIEW_LIVE_TERMINAL_MISSING: ${{ steps.live-item.outputs.terminal_missing }} | |
| EXACT_REVIEW_LIVE_TERMINAL_NOOP: ${{ steps.review-exact-event-item.outputs.terminal_during_review == 'true' && 'true' || steps.live-item.outputs.terminal_noop }} | |
| EXACT_REVIEW_PRODUCER_JOB: event-review-apply | |
| EXACT_REVIEW_PROTOCOL_VERSION: ${{ steps.claim-exact-review-queue.outputs.protocol_version }} | |
| EXACT_REVIEW_REPORT_PATH: artifacts/event/${{ steps.target.outputs.item_number }}.md | |
| EXACT_REVIEW_TARGET_BRANCH: ${{ steps.live-item.outputs.target_branch }} | |
| EXACT_REVIEW_TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p .artifacts | |
| pnpm run --silent repair:exact-review-bundle create > .artifacts/exact-review-bundle-create.json | |
| artifact_name="exact-review-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | |
| { | |
| echo "artifact_name=$artifact_name" | |
| echo "generation_attempt=$GITHUB_RUN_ATTEMPT" | |
| } >> "$GITHUB_OUTPUT" | |
| - uses: ./.github/actions/setup-state | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.create-exact-review-bundle.outcome == 'success' && steps.live-item.outputs.proceed == 'true' && (vars.EXACT_REVIEW_DIRECT_PUBLICATION_ENABLED || '1') == '1' }} | |
| id: direct-setup-state | |
| with: | |
| coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| records-repo-slugs: ${{ steps.target.outputs.target_slug }} | |
| records-item-number: ${{ steps.target.outputs.item_number }} | |
| hydrate-git-state: "false" | |
| hydrate-state-blobs: "false" | |
| - uses: ./.github/actions/setup-github-egress-observer | |
| id: direct-github-egress-observer | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.direct-setup-state.outcome == 'success' }} | |
| continue-on-error: true | |
| with: | |
| metrics-path: .artifacts/direct-publication/github-egress-v2.jsonl | |
| rate-limit-path: .artifacts/direct-publication/github-rate-limit-details-v2.jsonl | |
| pool-class: target_app | |
| stage: publication_apply | |
| source-action: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).sourceAction }} | |
| claim-generation: ${{ steps.claim-exact-review-queue.outputs.claim_generation }} | |
| repeat-revision: ${{ steps.claim-exact-review-queue.outputs.repeat_revision }} | |
| - name: Record direct-publication member | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.direct-github-egress-observer.outcome == 'success' }} | |
| continue-on-error: true | |
| env: | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: node dist/github-egress-observer-cli.js record-member | |
| - name: Deliver GitHub effects and prepare direct state mutation | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.direct-setup-state.outcome == 'success' }} | |
| id: prepare-direct-exact-review-publication | |
| continue-on-error: true | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| EXACT_REVIEW_QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| CLAWSWEEPER_PUBLIC_GH_TOKEN: ${{ steps.target.outputs.target_repo == 'openclaw/openclaw' && github.token || '' }} | |
| REPO_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| MIN_AGE_MINUTES: "0" | |
| REVIEW_ONLY: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).sourceAction == 'failed_review_shard_recovery' && 'true' || 'false' }} | |
| EXACT_EVENT_PUBLICATION: "true" | |
| EXACT_REVIEW_CLOSE_COVERAGE_DEFERRED: "true" | |
| EXACT_REVIEW_BATCH_ITEM_KEY: ${{ steps.claim-exact-review-queue.outputs.item_key }} | |
| EXACT_REVIEW_BATCH_REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }} | |
| EXACT_REVIEW_BATCH_CLAIM_GENERATION: ${{ steps.claim-exact-review-queue.outputs.claim_generation }} | |
| EXACT_REVIEW_BATCH_MUTATION_OUTPUT: .artifacts/direct-publication-outcome.json | |
| run: pnpm run --silent repair:publish-event-result | |
| - name: Dispatch recommended live proofs | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.prepare-direct-exact-review-publication.outcome == 'success' }} | |
| uses: ./.github/actions/dispatch-live-proofs | |
| with: | |
| target-repo: ${{ steps.target.outputs.target_repo }} | |
| item-numbers: ${{ steps.target.outputs.item_number }} | |
| records-root: records | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| - name: Post direct exact review publication result | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.prepare-direct-exact-review-publication.outcome == 'success' }} | |
| id: direct-exact-review-publication | |
| env: | |
| EXACT_REVIEW_DIRECT_PUBLICATION_ENABLED: ${{ vars.EXACT_REVIEW_DIRECT_PUBLICATION_ENABLED || '1' }} | |
| EXACT_REVIEW_DIRECT_MUTATION_OUTPUT: .artifacts/direct-publication-outcome.json | |
| EXACT_REVIEW_DIRECT_REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }} | |
| EXACT_REVIEW_DIRECT_SOURCE_ACTION: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).sourceAction }} | |
| EXACT_REVIEW_QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| run: pnpm run --silent repair:exact-review-direct-publication | |
| - name: Finalize direct exact review lifecycle | |
| id: finalize-direct-exact-review-lifecycle | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.direct-exact-review-publication.outputs.accepted == 'true' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| CLAWSWEEPER_GITHUB_POOL_CLASS: repository_actions | |
| CLAWSWEEPER_GITHUB_STAGE: publication_router | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| TARGET_BRANCH: ${{ steps.target.outputs.target_branch }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| FENCE_KEY: ${{ steps.claim-exact-review-queue.outputs.item_key }} | |
| REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }} | |
| CLAIM_DECISION: ${{ steps.claim-exact-review-queue.outputs.decision }} | |
| DIRECT_PUBLICATION_SUPERSEDED: ${{ steps.direct-exact-review-publication.outputs.superseded }} | |
| DIRECT_OUTCOME: .artifacts/direct-publication-outcome.json | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES: ${{ vars.CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES || '180' }} | |
| CLAWSWEEPER_COMMENT_MAX_COMMENTS: ${{ vars.CLAWSWEEPER_COMMENT_MAX_COMMENTS || '1000' }} | |
| run: | | |
| set -euo pipefail | |
| test -s "$DIRECT_OUTCOME" | |
| jq -e '.kind == "eligible" and .disposition != null' "$DIRECT_OUTCOME" >/dev/null | |
| test -n "$CLAWSWEEPER_WEBHOOK_SECRET" | |
| source_action="$(node -e 'const decision=JSON.parse(process.env.CLAIM_DECISION || "{}"); process.stdout.write(decision.sourceAction || "")')" | |
| lifecycle_terminal="" | |
| lifecycle_router_outcome="" | |
| lifecycle_deferred_coverage="false" | |
| direct_lifecycle_requeue=false | |
| queue_url="${QUEUE_URL%/}" | |
| if [ "${DIRECT_PUBLICATION_SUPERSEDED:-false}" != "true" ]; then | |
| if jq -e '.disposition.requeueLatestExpected == true' "$DIRECT_OUTCOME" >/dev/null; then | |
| lifecycle_terminal="requeue" | |
| # The completion owns this direct receipt. Let its fenced queue | |
| # transition create the fresh source-drift revision atomically. | |
| direct_lifecycle_requeue=true | |
| elif jq -e '.disposition.terminalMissingExpected == true' "$DIRECT_OUTCOME" >/dev/null; then | |
| lifecycle_terminal="target_missing" | |
| elif jq -e '.disposition.terminalClosedExpected == true' "$DIRECT_OUTCOME" >/dev/null; then | |
| lifecycle_terminal="target_closed" | |
| elif jq -e '.disposition.guardedOpenAction != null and .disposition.guardedOpenAction != ""' "$DIRECT_OUTCOME" >/dev/null; then | |
| lifecycle_terminal="guarded_open" | |
| elif [ "$source_action" = "failed_review_shard_recovery" ]; then | |
| lifecycle_router_outcome="not_required" | |
| elif jq -e '.disposition.routableSyncExpected == true' "$DIRECT_OUTCOME" >/dev/null; then | |
| gh workflow run repair-comment-router.yml \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --ref main \ | |
| -f execute=true \ | |
| -f target_repo="$TARGET_REPO" \ | |
| -f target_branch="$TARGET_BRANCH" \ | |
| -f item_numbers="$ITEM_NUMBER" \ | |
| -f lookback_minutes="$CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES" \ | |
| -f max_comments="$CLAWSWEEPER_COMMENT_MAX_COMMENTS" | |
| lifecycle_router_outcome="durable" | |
| elif jq -e '.disposition.deferredCloseCoverageExpected == true' "$DIRECT_OUTCOME" >/dev/null; then | |
| # The close proof remains a separate later concern. The durable | |
| # handoff means this review itself is complete. | |
| lifecycle_router_outcome="durable" | |
| lifecycle_deferred_coverage="true" | |
| else | |
| lifecycle_terminal="policy_noop" | |
| fi | |
| fi | |
| if [ -n "$lifecycle_router_outcome" ]; then | |
| export LIFECYCLE_ROUTER_OUTCOME="$lifecycle_router_outcome" | |
| export LIFECYCLE_DEFERRED_COVERAGE="$lifecycle_deferred_coverage" | |
| lifecycle_payload="$(node -e ' | |
| const revision = Number(process.env.REVISION); | |
| if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY) process.exit(1); | |
| const prefix = process.env.LIFECYCLE_DEFERRED_COVERAGE === "true" | |
| ? "router-direct-proof" | |
| : process.env.LIFECYCLE_ROUTER_OUTCOME === "not_required" | |
| ? "router-direct-not-required" | |
| : "router-direct"; | |
| process.stdout.write(JSON.stringify({ | |
| canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`, | |
| fence_key: process.env.FENCE_KEY, | |
| revision, | |
| outcome: process.env.LIFECYCLE_ROUTER_OUTCOME, | |
| receipt_id: `${prefix}:${process.env.GITHUB_RUN_ID}:${process.env.GITHUB_RUN_ATTEMPT}`, | |
| })); | |
| ')" | |
| lifecycle_signature="$(PAYLOAD="$lifecycle_payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')" | |
| lifecycle_response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --header "x-clawsweeper-exact-review-signature: $lifecycle_signature" \ | |
| --data "$lifecycle_payload" \ | |
| "$queue_url/internal/exact-review/lifecycle/router-receipt")" | |
| jq -e '.ok == true' <<<"$lifecycle_response" >/dev/null | |
| elif [ -n "$lifecycle_terminal" ]; then | |
| export LIFECYCLE_TERMINAL="$lifecycle_terminal" | |
| lifecycle_payload="$(node -e ' | |
| const revision = Number(process.env.REVISION); | |
| if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY || !process.env.LIFECYCLE_TERMINAL) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`, | |
| fence_key: process.env.FENCE_KEY, | |
| revision, | |
| kind: process.env.LIFECYCLE_TERMINAL, | |
| })); | |
| ')" | |
| lifecycle_signature="$(PAYLOAD="$lifecycle_payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')" | |
| lifecycle_response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --header "x-clawsweeper-exact-review-signature: $lifecycle_signature" \ | |
| --data "$lifecycle_payload" \ | |
| "$queue_url/internal/exact-review/lifecycle/terminal-disposition")" | |
| jq -e '.ok == true' <<<"$lifecycle_response" >/dev/null | |
| fi | |
| echo "direct_lifecycle_requeue=$direct_lifecycle_requeue" >> "$GITHUB_OUTPUT" | |
| # This is deliberately after the durable lifecycle handoff. Command | |
| # acknowledgement is performed later by the dedicated finalizer and is | |
| # not a precondition for optional implementation dispatch. | |
| - name: Dispatch exact high-confidence bug implementation | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.finalize-direct-exact-review-lifecycle.outcome == 'success' && vars.CLAWSWEEPER_AUTO_IMPLEMENT_ISSUES == '1' && steps.target.outputs.target_repo == 'openclaw/openclaw' && fromJSON(steps.claim-exact-review-queue.outputs.decision).itemKind == 'issue' }} | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| MAX_DISPATCH: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_MAX_DISPATCH_PER_SWEEP || '' }} | |
| run: | | |
| node scripts/dispatch-issue-implementation-candidates.mjs \ | |
| --target-repo "$TARGET_REPO" \ | |
| --item-number "$ITEM_NUMBER" \ | |
| --artifact-dir artifacts/event | |
| - name: Upload exact review artifact bundle | |
| id: upload-exact-review-bundle | |
| # Keep the artifact if direct publication needs publisher recovery after | |
| # its lifecycle handoff fails. A committed handoff owns the final path. | |
| if: ${{ always() && steps.claim-exact-review-queue.outputs.claimed == 'true' && !cancelled() && steps.create-exact-review-bundle.outcome == 'success' && (steps.direct-exact-review-publication.outputs.accepted != 'true' || steps.finalize-direct-exact-review-lifecycle.outcome != 'success') }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ steps.create-exact-review-bundle.outputs.artifact_name }} | |
| path: .artifacts/exact-review-bundle | |
| include-hidden-files: true | |
| if-no-files-found: error | |
| retention-days: 90 | |
| - name: Queue durable exact review publication | |
| id: queue-exact-review-publication | |
| if: ${{ always() && steps.claim-exact-review-queue.outputs.claimed == 'true' && !cancelled() && steps.upload-exact-review-bundle.outcome == 'success' }} | |
| env: | |
| ARTIFACT_NAME: ${{ steps.create-exact-review-bundle.outputs.artifact_name }} | |
| CLAIM_DECISION: ${{ steps.live-item.outputs.decision }} | |
| CLAIM_GENERATION: ${{ steps.claim-exact-review-queue.outputs.claim_generation }} | |
| ITEM_KEY: ${{ steps.claim-exact-review-queue.outputs.item_key }} | |
| LEASE_REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }} | |
| LIVE_GUARDED_OPEN: ${{ steps.live-item.outputs.guarded_open }} | |
| LIVE_PROCEEDED: ${{ steps.review-exact-event-item.outputs.terminal_during_review == 'true' && 'false' || steps.live-item.outputs.proceed }} | |
| LIVE_TERMINAL_MISSING: ${{ steps.live-item.outputs.terminal_missing }} | |
| LIVE_TERMINAL_NOOP: ${{ steps.review-exact-event-item.outputs.terminal_during_review == 'true' && 'true' || steps.live-item.outputs.terminal_noop }} | |
| PROTOCOL_VERSION: ${{ steps.claim-exact-review-queue.outputs.protocol_version }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$CLAWSWEEPER_WEBHOOK_SECRET" | |
| queue_url="${QUEUE_URL%/}" | |
| payload="$(node <<'NODE' | |
| const producerDecision = JSON.parse(process.env.CLAIM_DECISION || "{}"); | |
| const producerRunAttempt = Number(process.env.GITHUB_RUN_ATTEMPT); | |
| const protocolVersion = Number(process.env.PROTOCOL_VERSION); | |
| const leaseRevision = process.env.LEASE_REVISION ? Number(process.env.LEASE_REVISION) : null; | |
| const claimGeneration = process.env.CLAIM_GENERATION ? Number(process.env.CLAIM_GENERATION) : null; | |
| const flag = (name) => { | |
| const value = process.env[name]; | |
| if (value === "true") return true; | |
| if (value === "false") return false; | |
| process.exit(1); | |
| }; | |
| if (!Number.isInteger(producerRunAttempt) || producerRunAttempt < 1) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| delivery_id: `publisher:${process.env.GITHUB_RUN_ID}:${producerRunAttempt}`, | |
| decision: { | |
| ...producerDecision, | |
| sourceAction: "exact_review_artifact_publish", | |
| supersedesInProgress: false, | |
| publication: { | |
| artifactName: process.env.ARTIFACT_NAME, | |
| producerRunId: process.env.GITHUB_RUN_ID, | |
| producerRunAttempt, | |
| sourceSha: process.env.GITHUB_SHA, | |
| itemKey: process.env.ITEM_KEY, | |
| protocolVersion, | |
| leaseRevision, | |
| claimGeneration, | |
| liveProceeded: flag("LIVE_PROCEEDED"), | |
| liveTerminalNoop: flag("LIVE_TERMINAL_NOOP"), | |
| liveTerminalMissing: flag("LIVE_TERMINAL_MISSING"), | |
| liveGuardedOpen: flag("LIVE_GUARDED_OPEN"), | |
| producerDecision, | |
| }, | |
| }, | |
| })); | |
| NODE | |
| )" | |
| signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')" | |
| for attempt in 1 2 3; do | |
| response="$( | |
| curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --header "x-clawsweeper-exact-review-signature: $signature" \ | |
| --data "$payload" \ | |
| "$queue_url/internal/exact-review/enqueue" || true | |
| )" | |
| if jq -e '.superseded == true' <<< "$response" >/dev/null 2>&1; then | |
| echo "::notice::Exact-review publication revision $(jq -r '.publication_revision // "unknown"' <<< "$response") was superseded by revision $(jq -r '.superseded_by_revision // "unknown"' <<< "$response"); the newer publisher owns final delivery." | |
| exit 0 | |
| fi | |
| if jq -e '.ok == true and (.queued == true or .deduped == true)' <<< "$response" >/dev/null; then | |
| exit 0 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| sleep "$((attempt * 5))" | |
| fi | |
| done | |
| exit 1 | |
| - name: Release unsuccessful workflow-owned review lease | |
| if: ${{ always() && steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' && steps.reserve-exact-review-lease.outputs.status != 'held' && steps.prepare-direct-exact-review-publication.outputs.failure_kind != 'github_rate_limit' && steps.prepare-direct-exact-review-publication.outputs.failure_kind != 'github_transient' && steps.direct-exact-review-publication.outputs.accepted != 'true' && steps.queue-exact-review-publication.outcome != 'success' }} | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| LEASE_OWNER: github-run-${{ github.run_id }}-${{ github.run_attempt }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$GH_TOKEN" | |
| lease_ids="$( | |
| gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments?per_page=100" \ | |
| --paginate \ | |
| --jq ".[] | select(.user.login == \"clawsweeper[bot]\" or .user.login == \"openclaw-clawsweeper[bot]\") | select(.body | contains(\"<!-- clawsweeper-review-lease item=$ITEM_NUMBER -->\")) | select(.body | contains(\"owner=$LEASE_OWNER \")) | .id" | |
| )" | |
| while IFS= read -r lease_id; do | |
| [ -n "$lease_id" ] || continue | |
| gh api "repos/$TARGET_REPO/issues/comments/$lease_id" --method DELETE | |
| done <<< "$lease_ids" | |
| reaction_ids="$(gh api -X GET \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions" \ | |
| -f content="eyes" -F per_page=100 --paginate \ | |
| --jq '.[] | select(.content == "eyes") | select(.user.login == "clawsweeper" or .user.login == "clawsweeper[bot]" or .user.login == "openclaw-clawsweeper[bot]") | .id')" | |
| while IFS= read -r reaction_id; do | |
| [ -n "$reaction_id" ] || continue | |
| gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions/$reaction_id" --method DELETE | |
| done <<< "$reaction_ids" | |
| - name: Mark unsuccessful re-review | |
| if: ${{ always() && steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.target.outputs.has_command_context == 'true' && steps.setup-pnpm.outcome == 'success' && steps.prepare-direct-exact-review-publication.outputs.failure_kind != 'github_rate_limit' && steps.prepare-direct-exact-review-publication.outputs.failure_kind != 'github_transient' && steps.direct-exact-review-publication.outputs.accepted != 'true' && steps.queue-exact-review-publication.outcome != 'success' && steps.target-write-token.outputs.token != '' }} | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| COMMAND_STATUS_MARKER: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).commandStatusMarker || '' }} | |
| STATUS_COMMENT_ID: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).statusCommentId || '' }} | |
| RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| REVIEW_OUTCOME: ${{ steps.review-exact-event-item.outcome }} | |
| REVIEW_SUPERSEDED: ${{ steps.review-exact-event-item.outputs.superseded || 'false' }} | |
| RESERVATION_STATUS: ${{ steps.reserve-exact-review-lease.outputs.status }} | |
| RETRY_AT: ${{ steps.reserve-exact-review-lease.outputs.retry_at || steps.review-exact-event-item.outputs.retry_at }} | |
| CLAWSWEEPER_ACTION_LEDGER_DISABLED: "1" | |
| run: | | |
| state="Failed" | |
| detail="The exact review did not produce a publishable artifact. The durable queue will retry it." | |
| if [ "$RESERVATION_STATUS" = "superseded" ] || [ "$REVIEW_SUPERSEDED" = "true" ]; then | |
| state="Waiting" | |
| detail="A newer exact-review queue owner superseded this run. This run completed as a no-op." | |
| elif [ "$RESERVATION_STATUS" = "held" ]; then | |
| state="Waiting" | |
| detail="Another exact-head review is already active. The durable queue will retry after its lease expires." | |
| elif [ "$REVIEW_OUTCOME" = "cancelled" ] || [ -n "$RETRY_AT" ]; then | |
| state="Interrupted" | |
| detail="The exact review was interrupted. The durable queue will retry it." | |
| fi | |
| pnpm run repair:update-command-status -- \ | |
| --repo "$TARGET_REPO" \ | |
| --item-number "$ITEM_NUMBER" \ | |
| --marker "$COMMAND_STATUS_MARKER" \ | |
| --status-comment-id "$STATUS_COMMENT_ID" \ | |
| --state "$state" \ | |
| --detail "$detail" \ | |
| --run-url "$RUN_URL" | |
| - name: Export exact review generation result | |
| id: exact-review-generation-result | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && always() }} | |
| env: | |
| ADMISSION_RETRY: ${{ steps.live-item.outputs.admission_retry }} | |
| RETRY_KIND: ${{ steps.live-item.outputs.retry_kind || steps.reserve-exact-review-lease.outputs.retry_kind || steps.review-exact-event-item.outputs.retry_kind }} | |
| RETRY_AT: ${{ steps.live-item.outputs.retry_at || steps.reserve-exact-review-lease.outputs.retry_at || steps.review-exact-event-item.outputs.retry_at }} | |
| DIRECT_PUBLICATION_FAILURE_KIND: ${{ steps.prepare-direct-exact-review-publication.outputs.failure_kind }} | |
| DIRECT_PUBLICATION_RETRY_AT: ${{ steps.prepare-direct-exact-review-publication.outputs.retry_at }} | |
| TARGET_ENABLED: ${{ steps.target.outputs.target_enabled }} | |
| LIVE_OUTCOME: ${{ steps.live-item.outcome }} | |
| SCHEDULED_SEMANTIC_NOOP: ${{ steps.live-item.outputs.scheduled_semantic_noop || 'false' }} | |
| REVIEW_OUTCOME: ${{ steps.review-exact-event-item.outcome }} | |
| REVIEW_SUPERSEDED: ${{ steps.review-exact-event-item.outputs.superseded || 'false' }} | |
| RESERVATION_STATUS: ${{ steps.reserve-exact-review-lease.outputs.status }} | |
| PUBLICATION_QUEUE_OUTCOME: ${{ steps.queue-exact-review-publication.outcome }} | |
| DIRECT_PUBLICATION_ACCEPTED: ${{ steps.direct-exact-review-publication.outputs.accepted }} | |
| DIRECT_PUBLICATION_SUPERSEDED: ${{ steps.direct-exact-review-publication.outputs.superseded }} | |
| DIRECT_LIFECYCLE_OUTCOME: ${{ steps.finalize-direct-exact-review-lifecycle.outcome }} | |
| DIRECT_LIFECYCLE_REQUEUE: ${{ steps.finalize-direct-exact-review-lifecycle.outputs.direct_lifecycle_requeue || 'false' }} | |
| run: | | |
| outcome=failure | |
| requeue_latest=false | |
| retry_kind="$RETRY_KIND" | |
| retry_at="$RETRY_AT" | |
| if [ "$DIRECT_PUBLICATION_FAILURE_KIND" = "github_rate_limit" ] && [ "$PUBLICATION_QUEUE_OUTCOME" != "success" ]; then | |
| test -n "$DIRECT_PUBLICATION_RETRY_AT" | |
| retry_kind=throttle | |
| retry_at="$DIRECT_PUBLICATION_RETRY_AT" | |
| fi | |
| if [ "$ADMISSION_RETRY" = "true" ] && [ -z "$retry_kind" ]; then | |
| outcome=success | |
| requeue_latest=true | |
| elif [ "$TARGET_ENABLED" = "false" ]; then | |
| outcome=success | |
| elif [ "$SCHEDULED_SEMANTIC_NOOP" = "true" ] && [ "$LIVE_OUTCOME" = "success" ]; then | |
| outcome=success | |
| elif [ "$RESERVATION_STATUS" = "superseded" ] || [ "$REVIEW_SUPERSEDED" = "true" ]; then | |
| outcome=success | |
| elif [ "$REVIEW_OUTCOME" = "cancelled" ]; then | |
| outcome=cancelled | |
| elif [ "$LIVE_OUTCOME" = "success" ] && { \ | |
| { [ "$DIRECT_PUBLICATION_ACCEPTED" = "true" ] && [ "$DIRECT_LIFECYCLE_OUTCOME" = "success" ]; } || \ | |
| { [ "$DIRECT_PUBLICATION_ACCEPTED" != "true" ] && [ "$PUBLICATION_QUEUE_OUTCOME" = "success" ]; }; \ | |
| }; then | |
| outcome=success | |
| fi | |
| echo "outcome=$outcome" >> "$GITHUB_OUTPUT" | |
| echo "requeue_latest=$requeue_latest" >> "$GITHUB_OUTPUT" | |
| echo "direct_lifecycle_requeue=$DIRECT_LIFECYCLE_REQUEUE" >> "$GITHUB_OUTPUT" | |
| echo "retry_kind=$retry_kind" >> "$GITHUB_OUTPUT" | |
| echo "retry_at=$retry_at" >> "$GITHUB_OUTPUT" | |
| - name: Complete exact-review queue lease | |
| id: complete-exact-review-queue | |
| if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && always() && (steps.direct-exact-review-publication.outputs.accepted != 'true' || steps.finalize-direct-exact-review-lifecycle.outcome == 'success') }} | |
| continue-on-error: true | |
| env: | |
| PRIMARY_OUTCOME: ${{ steps.exact-review-generation-result.outputs.outcome || 'failure' }} | |
| CLAIM_GENERATION: ${{ steps.claim-exact-review-queue.outputs.claim_generation }} | |
| ITEM_KEY: ${{ steps.claim-exact-review-queue.outputs.item_key }} | |
| PROTOCOL_VERSION: ${{ steps.claim-exact-review-queue.outputs.protocol_version }} | |
| QUEUE_LEASE_ID: ${{ steps.claim-exact-review-queue.outputs.lease_id }} | |
| QUEUE_LEASE_REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| REQUEUE_LATEST: ${{ steps.exact-review-generation-result.outputs.requeue_latest }} | |
| RETRY_KIND: ${{ steps.exact-review-generation-result.outputs.retry_kind }} | |
| RETRY_AT: ${{ steps.exact-review-generation-result.outputs.retry_at }} | |
| DIRECT_PUBLICATION_ACCEPTED: ${{ steps.direct-exact-review-publication.outputs.accepted }} | |
| DIRECT_PUBLICATION_SUPERSEDED: ${{ steps.direct-exact-review-publication.outputs.superseded }} | |
| DIRECT_LIFECYCLE_OUTCOME: ${{ steps.finalize-direct-exact-review-lifecycle.outcome }} | |
| DIRECT_LIFECYCLE_REQUEUE: ${{ steps.exact-review-generation-result.outputs.direct_lifecycle_requeue }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$QUEUE_LEASE_ID" | |
| queue_url="${QUEUE_URL%/}" | |
| payload="$(node -e ' | |
| const runAttempt = Number(process.env.RUN_ATTEMPT); | |
| const protocolVersion = Number(process.env.PROTOCOL_VERSION); | |
| const leaseRevision = Number(process.env.QUEUE_LEASE_REVISION); | |
| const claimGeneration = Number(process.env.CLAIM_GENERATION); | |
| if (!Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1); | |
| if (protocolVersion !== 1 && protocolVersion !== 2) process.exit(1); | |
| if ( | |
| protocolVersion === 2 && | |
| (!process.env.ITEM_KEY || | |
| !Number.isInteger(leaseRevision) || | |
| leaseRevision < 1 || | |
| !Number.isInteger(claimGeneration) || | |
| claimGeneration < 1) | |
| ) process.exit(1); | |
| const primaryOutcome = String(process.env.PRIMARY_OUTCOME || ""); | |
| const outcome = ["success", "cancelled", "failure"].includes(primaryOutcome) | |
| ? primaryOutcome | |
| : "failure"; | |
| const requeueLatest = process.env.REQUEUE_LATEST === "true"; | |
| const retryKind = String(process.env.RETRY_KIND || "").trim(); | |
| const retryAt = String(process.env.RETRY_AT || "").trim(); | |
| if (retryKind && !["coordination", "throttle"].includes(retryKind)) process.exit(1); | |
| if (retryKind && !retryAt) process.exit(1); | |
| const directPublicationCompleted = | |
| process.env.DIRECT_PUBLICATION_ACCEPTED === "true" && | |
| process.env.DIRECT_LIFECYCLE_OUTCOME === "success"; | |
| const directPublicationSuperseded = | |
| directPublicationCompleted && process.env.DIRECT_PUBLICATION_SUPERSEDED === "true"; | |
| const directLifecycleRequeue = | |
| directPublicationCompleted && process.env.DIRECT_LIFECYCLE_REQUEUE === "true"; | |
| if (requeueLatest && directLifecycleRequeue) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| lease_id: process.env.QUEUE_LEASE_ID, | |
| ...(protocolVersion === 2 | |
| ? { | |
| item_key: process.env.ITEM_KEY, | |
| lease_revision: leaseRevision, | |
| claim_generation: claimGeneration, | |
| } | |
| : {}), | |
| run_id: process.env.GITHUB_RUN_ID, | |
| run_attempt: runAttempt, | |
| outcome, | |
| ...(requeueLatest ? { requeue_latest: true } : {}), | |
| ...(retryKind ? { retry_kind: retryKind } : {}), | |
| ...(directPublicationCompleted | |
| ? directPublicationSuperseded | |
| ? { completion_kind: "superseded", reason_code: "remote_newer_tuple" } | |
| : { completion_kind: "published", reason_code: "publication_applied" } | |
| : {}), | |
| ...(directLifecycleRequeue ? { direct_lifecycle_requeue: true } : {}), | |
| ...(retryAt ? { retry_at: retryAt } : {}), | |
| })); | |
| ')" | |
| response_file="$(mktemp)" | |
| error_file="$(mktemp)" | |
| trap 'rm -f "$response_file" "$error_file"' EXIT | |
| for attempt in 1 2 3; do | |
| : > "$response_file" | |
| : > "$error_file" | |
| if status="$(curl --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --output "$response_file" \ | |
| --write-out '%{http_code}' \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --data "$payload" \ | |
| "$queue_url/internal/exact-review/complete" 2>"$error_file")"; then | |
| response="$(<"$response_file")" | |
| if [[ "$status" == 2* ]]; then | |
| exit 0 | |
| fi | |
| # Only this completion-specific conflict is emitted after the | |
| # durable queue verifies that this exact v2 lease tuple was fenced | |
| # by a newer source revision. Every generic ownership conflict | |
| # stays visible so a malformed or mismatched callback cannot turn | |
| # a failed completion into a green workflow. | |
| if [ "$status" = "409" ]; then | |
| conflict_reason="$(RESPONSE="$response" node <<'NODE' | |
| const response = JSON.parse(process.env.RESPONSE || "{}"); | |
| const safeConflicts = new Set(["lease_superseded"]); | |
| if (!safeConflicts.has(response.error)) process.exit(1); | |
| process.stdout.write(response.error); | |
| NODE | |
| )" || { | |
| echo "Unexpected exact-review completion conflict: $response" >&2 | |
| exit 1 | |
| } | |
| echo "::notice::Exact-review completion skipped because its lease was superseded: $conflict_reason" | |
| exit 0 | |
| fi | |
| echo "Exact-review completion returned HTTP $status: $response" >&2 | |
| if [[ "$status" != 5* ]]; then | |
| exit 1 | |
| fi | |
| else | |
| cat "$error_file" >&2 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| sleep "$((attempt * 5))" | |
| fi | |
| done | |
| exit 1 | |
| - name: Submit direct GitHub egress telemetry | |
| if: ${{ always() && steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.direct-github-egress-observer.outcome == 'success' }} | |
| continue-on-error: true | |
| env: | |
| EXACT_REVIEW_QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| run: pnpm run --silent repair:github-egress-telemetry submit | |
| - name: Fail unsuccessful exact review generation | |
| # A held review lease is a successful deferral only after the durable queue | |
| # accepts retry ownership; queue completion failures must remain visible. | |
| if: >- | |
| ${{ | |
| always() && | |
| steps.claim-exact-review-queue.outputs.claimed == 'true' && | |
| ( | |
| ( | |
| steps.direct-exact-review-publication.outputs.accepted != 'true' && | |
| steps.complete-exact-review-queue.outcome != 'success' && | |
| steps.reserve-exact-review-lease.outputs.status != 'superseded' && | |
| steps.review-exact-event-item.outputs.superseded != 'true' | |
| ) || | |
| ( | |
| steps.exact-review-generation-result.outputs.outcome != 'success' && | |
| steps.exact-review-generation-result.outputs.retry_kind == '' && | |
| steps.reserve-exact-review-lease.outputs.status != 'held' && | |
| steps.reserve-exact-review-lease.outputs.status != 'superseded' | |
| ) | |
| ) | |
| }} | |
| env: | |
| RESERVATION_STATUS: ${{ steps.reserve-exact-review-lease.outputs.status || 'unknown' }} | |
| REVIEW_EXIT_CODE: ${{ steps.review-exact-event-item.outputs.exit_code || 'unknown' }} | |
| REVIEW_OUTCOME: ${{ steps.review-exact-event-item.outcome || 'not_started' }} | |
| run: | | |
| echo "::error::Exact review generation failed: classification=codex_or_content_failure reservation=$RESERVATION_STATUS review_outcome=$REVIEW_OUTCOME review_exit=$REVIEW_EXIT_CODE" | |
| exit 1 | |
| event-review-publish: | |
| name: Publish exact review artifact | |
| if: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.queue_lease_id != '' && github.event.client_payload.source_action == 'exact_review_artifact_publish' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| permissions: | |
| actions: write | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| statuses: read | |
| steps: | |
| - name: Claim durable exact review publication | |
| id: publication-context | |
| env: | |
| ITEM_KEY: ${{ github.event.client_payload.queue_claim.item_key }} | |
| QUEUE_LEASE_ID: ${{ github.event.client_payload.queue_lease_id }} | |
| QUEUE_LEASE_REVISION: ${{ github.event.client_payload.queue_claim.lease_revision }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| run: | | |
| set -euo pipefail | |
| printf 'claimed=false\ndecision={}\n' >> "$GITHUB_OUTPUT" | |
| queue_url="${QUEUE_URL%/}" | |
| payload="$(node -e ' | |
| const leaseRevision = Number(process.env.QUEUE_LEASE_REVISION); | |
| const runAttempt = Number(process.env.RUN_ATTEMPT); | |
| if (!process.env.QUEUE_LEASE_ID || !process.env.ITEM_KEY) process.exit(1); | |
| if (!Number.isInteger(leaseRevision) || leaseRevision < 1) process.exit(1); | |
| if (!Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| lease_id: process.env.QUEUE_LEASE_ID, | |
| item_key: process.env.ITEM_KEY, | |
| lease_revision: leaseRevision, | |
| run_id: process.env.GITHUB_RUN_ID, | |
| run_attempt: runAttempt, | |
| })); | |
| ')" | |
| response_file="$(mktemp)" | |
| error_file="$(mktemp)" | |
| trap 'rm -f "$response_file" "$error_file"' EXIT | |
| for attempt in 1 2 3; do | |
| : > "$response_file" | |
| : > "$error_file" | |
| if status="$(curl --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --output "$response_file" \ | |
| --write-out '%{http_code}' \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --data "$payload" \ | |
| "$queue_url/internal/exact-review/claim" 2>"$error_file")"; then | |
| response="$(<"$response_file")" | |
| if [ "$status" = "409" ]; then | |
| conflict_reason="$(RESPONSE="$response" node <<'NODE' | |
| const response = JSON.parse(process.env.RESPONSE || "{}"); | |
| const safeConflicts = new Set([ | |
| "lease_not_active", | |
| "lease_already_claimed", | |
| "lease_decision_unavailable", | |
| "stale_run_attempt", | |
| ]); | |
| if (!safeConflicts.has(response.error)) process.exit(1); | |
| process.stdout.write(response.error); | |
| NODE | |
| )" || { | |
| echo "Unexpected exact-review publication conflict: $response" >&2 | |
| exit 1 | |
| } | |
| echo "::notice::Skipping exact review publication because lease claim lost safely: $conflict_reason" | |
| exit 0 | |
| fi | |
| if [ "$status" != "200" ]; then | |
| if [[ "$status" != 5* ]]; then | |
| echo "Exact-review publication claim returned HTTP $status: $response" >&2 | |
| exit 1 | |
| fi | |
| elif RESPONSE="$response" node <<'NODE' | |
| const fs = require("node:fs"); | |
| const response = JSON.parse(process.env.RESPONSE || "{}"); | |
| const decision = response.decision; | |
| const publication = decision?.publication; | |
| const producerDecision = publication?.producerDecision; | |
| const leaseRevision = Number(response.lease_revision); | |
| const claimGeneration = Number(response.claim_generation); | |
| const repeatRevision = response.repeat_revision; | |
| const publicationLeaseRevision = Number(publication?.leaseRevision); | |
| const directItemKey = `${decision?.targetRepo}#${decision?.itemNumber}`; | |
| const expectedDeferredItemKey = `${directItemKey}@publish:${publication?.producerRunId}:${publication?.producerRunAttempt}`; | |
| // Direct publication converts its already-leased base item into a | |
| // recoverable publication. Its saved revision must still match | |
| // this publisher lease before any external router handoff; | |
| // deferred publication has its own key. | |
| const directLifecycleRecovery = | |
| response.item_key === directItemKey && | |
| publication?.itemKey === directItemKey && | |
| Number.isInteger(publicationLeaseRevision) && | |
| publicationLeaseRevision === leaseRevision; | |
| const directLifecycle = directLifecycleRecovery ? publication?.directLifecycle : null; | |
| const directLifecyclePlan = directLifecycle?.plan; | |
| const directLifecycleReceiptOutcome = directLifecycle?.receiptOutcome; | |
| const directLifecycleKinds = new Set([ | |
| "router", | |
| "router_deferred_coverage", | |
| "router_not_required", | |
| "requeue", | |
| "target_missing", | |
| "target_closed", | |
| "guarded_open", | |
| "policy_noop", | |
| ]); | |
| // Pre-projection rows have no saved post-effect intent. They | |
| // deliberately retain artifact recovery rather than inferring a | |
| // router or terminal result from a newer publisher run. | |
| const directLifecycleRecoveryReady = | |
| directLifecycleRecovery && | |
| directLifecyclePlan && | |
| typeof directLifecyclePlan === "object" && | |
| !Array.isArray(directLifecyclePlan) && | |
| Object.keys(directLifecyclePlan).length === 1 && | |
| directLifecycleKinds.has(directLifecyclePlan.kind) && | |
| ["accepted", "deduped", "superseded"].includes(directLifecycleReceiptOutcome); | |
| const deferredPublication = response.item_key === expectedDeferredItemKey; | |
| if ( | |
| response.claimed !== true || | |
| response.protocol_version !== 2 || | |
| typeof repeatRevision !== "boolean" || | |
| response.item_key !== process.env.ITEM_KEY || | |
| (!deferredPublication && !directLifecycleRecovery) || | |
| decision?.sourceAction !== "exact_review_artifact_publish" || | |
| !publication || | |
| !producerDecision || | |
| producerDecision.targetRepo !== decision.targetRepo || | |
| producerDecision.targetBranch !== decision.targetBranch || | |
| producerDecision.itemNumber !== decision.itemNumber || | |
| producerDecision.itemKind !== decision.itemKind | |
| ) process.exit(1); | |
| if (!Number.isInteger(leaseRevision) || leaseRevision < 1) process.exit(1); | |
| if (!Number.isInteger(claimGeneration) || claimGeneration < 1) process.exit(1); | |
| const targetRepo = String(decision.targetRepo); | |
| const [targetRepoOwner, targetRepoName] = targetRepo.split("/"); | |
| const targetSlug = targetRepo.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, "-"); | |
| const values = { | |
| artifact_name: publication.artifactName, | |
| generation_attempt: publication.producerRunAttempt, | |
| producer_run_id: publication.producerRunId, | |
| source_sha: publication.sourceSha, | |
| decision: JSON.stringify(producerDecision), | |
| item_key: publication.itemKey, | |
| protocol_version: publication.protocolVersion, | |
| lease_revision: publication.leaseRevision ?? "", | |
| claim_generation: publication.claimGeneration ?? "", | |
| target_repo: targetRepo, | |
| target_repo_owner: targetRepoOwner, | |
| target_repo_name: targetRepoName, | |
| target_slug: targetSlug, | |
| target_branch: decision.targetBranch, | |
| item_number: decision.itemNumber, | |
| item_kind: decision.itemKind, | |
| has_command_context: Boolean( | |
| producerDecision.commandStatusMarker || producerDecision.statusCommentId, | |
| ), | |
| live_proceeded: publication.liveProceeded, | |
| live_terminal_noop: publication.liveTerminalNoop, | |
| live_terminal_missing: publication.liveTerminalMissing, | |
| live_guarded_open: publication.liveGuardedOpen, | |
| publisher_lease_id: process.env.QUEUE_LEASE_ID, | |
| publisher_item_key: response.item_key, | |
| publisher_lease_revision: leaseRevision, | |
| publisher_claim_generation: claimGeneration, | |
| repeat_revision: repeatRevision, | |
| direct_lifecycle_recovery: directLifecycleRecoveryReady, | |
| direct_lifecycle_plan: directLifecycleRecoveryReady | |
| ? JSON.stringify(directLifecyclePlan) | |
| : "", | |
| direct_lifecycle_receipt_outcome: directLifecycleRecoveryReady | |
| ? directLifecycleReceiptOutcome | |
| : "", | |
| }; | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, "claimed=true\n"); | |
| for (const [key, value] of Object.entries(values)) { | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`); | |
| } | |
| NODE | |
| then | |
| exit 0 | |
| else | |
| echo "Exact-review publication claim returned an invalid success payload." >&2 | |
| exit 1 | |
| fi | |
| else | |
| cat "$error_file" >&2 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| sleep "$((attempt * 5))" | |
| fi | |
| done | |
| exit 1 | |
| - name: Replay committed direct lifecycle handoff | |
| id: replay-direct-lifecycle | |
| # A direct receipt already owns the GitHub effect. Its persisted plan is | |
| # the only safe source for post-effect recovery; old receipts fall back | |
| # to the artifact path above rather than being inferred here. | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery == 'true' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }} | |
| TARGET_BRANCH: ${{ steps.publication-context.outputs.target_branch }} | |
| ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }} | |
| FENCE_KEY: ${{ steps.publication-context.outputs.publisher_item_key }} | |
| REVISION: ${{ steps.publication-context.outputs.publisher_lease_revision }} | |
| CLAIM_DECISION: ${{ steps.publication-context.outputs.decision }} | |
| DIRECT_LIFECYCLE_PLAN: ${{ steps.publication-context.outputs.direct_lifecycle_plan }} | |
| DIRECT_LIFECYCLE_RECEIPT_OUTCOME: ${{ steps.publication-context.outputs.direct_lifecycle_receipt_outcome }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES: ${{ vars.CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES || '180' }} | |
| CLAWSWEEPER_COMMENT_MAX_COMMENTS: ${{ vars.CLAWSWEEPER_COMMENT_MAX_COMMENTS || '1000' }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$CLAWSWEEPER_WEBHOOK_SECRET" | |
| queue_url="${QUEUE_URL%/}" | |
| lifecycle_kind="$(DIRECT_LIFECYCLE_PLAN="$DIRECT_LIFECYCLE_PLAN" node -e ' | |
| const plan = JSON.parse(process.env.DIRECT_LIFECYCLE_PLAN || ""); | |
| const kinds = new Set([ | |
| "router", "router_deferred_coverage", "router_not_required", "requeue", | |
| "target_missing", "target_closed", "guarded_open", "policy_noop", | |
| ]); | |
| if (!plan || typeof plan !== "object" || Array.isArray(plan) || Object.keys(plan).length !== 1 || !kinds.has(plan.kind)) process.exit(1); | |
| process.stdout.write(plan.kind); | |
| ')" | |
| receipt_outcome="$DIRECT_LIFECYCLE_RECEIPT_OUTCOME" | |
| case "$receipt_outcome" in | |
| accepted|deduped|superseded) ;; | |
| *) exit 1 ;; | |
| esac | |
| post_router_receipt() { | |
| local outcome="$1" | |
| local deferred_coverage="$2" | |
| lifecycle_payload="$(LIFECYCLE_ROUTER_OUTCOME="$outcome" LIFECYCLE_DEFERRED_COVERAGE="$deferred_coverage" node -e ' | |
| const revision = Number(process.env.REVISION); | |
| if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY) process.exit(1); | |
| const prefix = process.env.LIFECYCLE_DEFERRED_COVERAGE === "true" | |
| ? "router-direct-recovery-proof" | |
| : process.env.LIFECYCLE_ROUTER_OUTCOME === "not_required" | |
| ? "router-direct-recovery-not-required" | |
| : "router-direct-recovery"; | |
| process.stdout.write(JSON.stringify({ | |
| canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`, | |
| fence_key: process.env.FENCE_KEY, | |
| revision, | |
| outcome: process.env.LIFECYCLE_ROUTER_OUTCOME, | |
| receipt_id: `${prefix}:${process.env.GITHUB_RUN_ID}:${process.env.GITHUB_RUN_ATTEMPT}`, | |
| })); | |
| ')" | |
| lifecycle_signature="$(PAYLOAD="$lifecycle_payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')" | |
| lifecycle_response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --header "x-clawsweeper-exact-review-signature: $lifecycle_signature" \ | |
| --data "$lifecycle_payload" \ | |
| "$queue_url/internal/exact-review/lifecycle/router-receipt")" | |
| jq -e '.ok == true' <<<"$lifecycle_response" >/dev/null | |
| } | |
| post_terminal_disposition() { | |
| local terminal_kind="$1" | |
| lifecycle_payload="$(LIFECYCLE_TERMINAL="$terminal_kind" node -e ' | |
| const revision = Number(process.env.REVISION); | |
| if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY || !process.env.LIFECYCLE_TERMINAL) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`, | |
| fence_key: process.env.FENCE_KEY, | |
| revision, | |
| kind: process.env.LIFECYCLE_TERMINAL, | |
| })); | |
| ')" | |
| lifecycle_signature="$(PAYLOAD="$lifecycle_payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')" | |
| lifecycle_response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --header "x-clawsweeper-exact-review-signature: $lifecycle_signature" \ | |
| --data "$lifecycle_payload" \ | |
| "$queue_url/internal/exact-review/lifecycle/terminal-disposition")" | |
| jq -e '.ok == true' <<<"$lifecycle_response" >/dev/null | |
| } | |
| completion_kind=published | |
| reason_code=publication_applied | |
| requeue_latest=false | |
| direct_requeue=false | |
| if [ "$receipt_outcome" = "superseded" ]; then | |
| # The direct receipt already committed the terminal supersession. | |
| # Do not manufacture a router, requeue, or acknowledgement attempt. | |
| completion_kind=superseded | |
| reason_code=remote_newer_tuple | |
| else | |
| case "$lifecycle_kind" in | |
| router) | |
| gh workflow run repair-comment-router.yml \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --ref main \ | |
| -f execute=true \ | |
| -f target_repo="$TARGET_REPO" \ | |
| -f target_branch="$TARGET_BRANCH" \ | |
| -f item_numbers="$ITEM_NUMBER" \ | |
| -f lookback_minutes="$CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES" \ | |
| -f max_comments="$CLAWSWEEPER_COMMENT_MAX_COMMENTS" | |
| post_router_receipt durable false | |
| ;; | |
| router_deferred_coverage) | |
| post_router_receipt durable true | |
| ;; | |
| router_not_required) | |
| post_router_receipt not_required false | |
| ;; | |
| requeue) | |
| post_terminal_disposition requeue | |
| # The completion endpoint atomically changes this exact direct | |
| # publication into a new fenced source-drift review revision. | |
| # A separate enqueue here could be ignored while this lease is | |
| # still active and would leave the old receipt replayable. | |
| direct_requeue=true | |
| ;; | |
| target_missing|target_closed|guarded_open|policy_noop) | |
| post_terminal_disposition "$lifecycle_kind" | |
| ;; | |
| *) exit 1 ;; | |
| esac | |
| fi | |
| { | |
| echo "outcome=success" | |
| echo "completion_kind=$completion_kind" | |
| echo "reason_code=$reason_code" | |
| echo "requeue_latest=$requeue_latest" | |
| echo "direct_requeue=$direct_requeue" | |
| } >> "$GITHUB_OUTPUT" | |
| - uses: actions/checkout@v7 | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' }} | |
| with: | |
| # The artifact validator binds the producer SHA. The publisher itself | |
| # must run the current repaired implementation, not historical code. | |
| ref: main | |
| filter: blob:none | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: ./.github/actions/setup-action-ledger | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' }} | |
| continue-on-error: true | |
| - uses: ./.github/actions/setup-pnpm | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' }} | |
| id: setup-publish-pnpm | |
| with: | |
| build-script: build:all | |
| - uses: ./.github/actions/setup-github-egress-observer | |
| id: artifact-github-egress-observer | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.setup-publish-pnpm.outcome == 'success' }} | |
| continue-on-error: true | |
| with: | |
| metrics-path: .artifacts/artifact-publication/github-egress-v2.jsonl | |
| rate-limit-path: .artifacts/artifact-publication/github-rate-limit-details-v2.jsonl | |
| pool-class: target_app | |
| stage: publication_apply | |
| source-action: ${{ fromJSON(steps.publication-context.outputs.decision).sourceAction }} | |
| claim-generation: ${{ steps.publication-context.outputs.publisher_claim_generation }} | |
| repeat-revision: ${{ steps.publication-context.outputs.repeat_revision }} | |
| - name: Record artifact-publication member | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.artifact-github-egress-observer.outcome == 'success' }} | |
| continue-on-error: true | |
| env: | |
| TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }} | |
| run: node dist/github-egress-observer-cli.js record-member | |
| - name: Download exact review artifact bundle | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' }} | |
| id: download-exact-review-bundle | |
| continue-on-error: true | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: ${{ steps.publication-context.outputs.artifact_name }} | |
| run-id: ${{ steps.publication-context.outputs.producer_run_id }} | |
| github-token: ${{ github.token }} | |
| repository: ${{ github.repository }} | |
| path: .artifacts/exact-review-bundle | |
| - name: Record artifact download transport boundary | |
| if: ${{ always() && steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.artifact-github-egress-observer.outcome == 'success' && steps.download-exact-review-bundle.outcome != 'skipped' }} | |
| continue-on-error: true | |
| env: | |
| CLAWSWEEPER_GITHUB_POOL_CLASS: repository_actions | |
| CLAWSWEEPER_GITHUB_STAGE: publication_prepare | |
| run: node dist/github-egress-observer-cli.js record-unobserved -- run download | |
| - name: Validate exact review artifact bundle | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.download-exact-review-bundle.outcome == 'success' }} | |
| id: validate-exact-review-bundle | |
| continue-on-error: true | |
| env: | |
| EXACT_REVIEW_BUNDLE_DIR: .artifacts/exact-review-bundle | |
| EXACT_REVIEW_CLAIM_GENERATION: ${{ steps.publication-context.outputs.claim_generation }} | |
| EXACT_REVIEW_DECISION: ${{ steps.publication-context.outputs.decision }} | |
| EXACT_REVIEW_GENERATION_ATTEMPT: ${{ steps.publication-context.outputs.generation_attempt }} | |
| EXACT_REVIEW_ITEM_KEY: ${{ steps.publication-context.outputs.item_key }} | |
| EXACT_REVIEW_ITEM_KIND: ${{ steps.publication-context.outputs.item_kind }} | |
| EXACT_REVIEW_ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }} | |
| EXACT_REVIEW_LEASE_REVISION: ${{ steps.publication-context.outputs.lease_revision }} | |
| EXACT_REVIEW_LIVE_GUARDED_OPEN: ${{ steps.publication-context.outputs.live_guarded_open }} | |
| EXACT_REVIEW_LIVE_PROCEEDED: ${{ steps.publication-context.outputs.live_proceeded }} | |
| EXACT_REVIEW_LIVE_TERMINAL_MISSING: ${{ steps.publication-context.outputs.live_terminal_missing }} | |
| EXACT_REVIEW_LIVE_TERMINAL_NOOP: ${{ steps.publication-context.outputs.live_terminal_noop }} | |
| EXACT_REVIEW_PRODUCER_JOB: event-review-apply | |
| EXACT_REVIEW_PRODUCER_RUN_ID: ${{ steps.publication-context.outputs.producer_run_id }} | |
| EXACT_REVIEW_PROTOCOL_VERSION: ${{ steps.publication-context.outputs.protocol_version }} | |
| EXACT_REVIEW_SOURCE_SHA: ${{ steps.publication-context.outputs.source_sha }} | |
| EXACT_REVIEW_TARGET_BRANCH: ${{ steps.publication-context.outputs.target_branch }} | |
| EXACT_REVIEW_TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }} | |
| run: pnpm run --silent repair:exact-review-bundle validate | |
| - name: Identify legacy tuple-less exact artifact | |
| id: legacy-exact-artifact | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.validate-exact-review-bundle.outcome == 'success' }} | |
| env: | |
| REPORT_PATH: .artifacts/exact-review-bundle/review/${{ steps.publication-context.outputs.item_number }}.md | |
| run: | | |
| set -euo pipefail | |
| REPORT_PATH="$REPORT_PATH" node <<'NODE' | |
| const fs = require("node:fs"); | |
| const reportPath = process.env.REPORT_PATH; | |
| let legacyTupleless = false; | |
| if (fs.existsSync(reportPath)) { | |
| const markdown = fs.readFileSync(reportPath, "utf8"); | |
| const owner = /^review_lease_owner:\s*(.+)\s*$/m.exec(markdown)?.[1]?.trim() || ""; | |
| const commentId = Number( | |
| /^review_lease_comment_id:\s*(\d+)\s*$/m.exec(markdown)?.[1] || "0", | |
| ); | |
| legacyTupleless = | |
| (!owner || owner === "unknown") && | |
| (!Number.isInteger(commentId) || commentId <= 0); | |
| } | |
| fs.appendFileSync( | |
| process.env.GITHUB_OUTPUT, | |
| `legacy_tupleless=${legacyTupleless ? "true" : "false"}\n`, | |
| ); | |
| NODE | |
| - name: Stage validated exact review artifact | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.validate-exact-review-bundle.outcome == 'success' }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p artifacts/event | |
| report=".artifacts/exact-review-bundle/review/${{ steps.publication-context.outputs.item_number }}.md" | |
| if [ -f "$report" ]; then | |
| cp "$report" "artifacts/event/${{ steps.publication-context.outputs.item_number }}.md" | |
| fi | |
| - name: Create target write token | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.validate-exact-review-bundle.outcome == 'success' }} | |
| id: target-write-token | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| owner: ${{ steps.publication-context.outputs.target_repo_owner }} | |
| repositories: ${{ steps.publication-context.outputs.target_repo_name }} | |
| permission-checks: read | |
| permission-contents: write | |
| permission-issues: write | |
| permission-pull-requests: write | |
| permission-statuses: read | |
| - uses: ./.github/actions/setup-state | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.validate-exact-review-bundle.outcome == 'success' && steps.legacy-exact-artifact.outputs.legacy_tupleless != 'true' }} | |
| id: setup-state | |
| with: | |
| coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| records-repo-slugs: ${{ steps.publication-context.outputs.target_slug }} | |
| records-item-number: ${{ steps.publication-context.outputs.item_number }} | |
| hydrate-git-state: "false" | |
| hydrate-state-blobs: "false" | |
| - name: Publish event result and apply safe close | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.validate-exact-review-bundle.outcome == 'success' && steps.legacy-exact-artifact.outputs.legacy_tupleless != 'true' }} | |
| id: publish-event-result | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| CLAWSWEEPER_PUBLIC_GH_TOKEN: ${{ steps.publication-context.outputs.target_repo == 'openclaw/openclaw' && github.token || '' }} | |
| REPO_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }} | |
| MIN_AGE_MINUTES: "0" | |
| REVIEW_ONLY: ${{ fromJSON(steps.publication-context.outputs.decision).sourceAction == 'failed_review_shard_recovery' && 'true' || 'false' }} | |
| EXACT_EVENT_PUBLICATION: "true" | |
| EXACT_REVIEW_CLOSE_COVERAGE_DEFERRED: "true" | |
| LIVE_PROCEEDED: ${{ steps.publication-context.outputs.live_proceeded }} | |
| LIVE_TERMINAL_NOOP: ${{ steps.publication-context.outputs.live_terminal_noop }} | |
| LIVE_TERMINAL_MISSING: ${{ steps.publication-context.outputs.live_terminal_missing }} | |
| LIVE_GUARDED_OPEN: ${{ steps.publication-context.outputs.live_guarded_open }} | |
| EXACT_REVIEW_QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| EXACT_REVIEW_LEASE_ID: ${{ steps.publication-context.outputs.publisher_lease_id }} | |
| EXACT_REVIEW_ITEM_KEY: ${{ steps.publication-context.outputs.publisher_item_key }} | |
| EXACT_REVIEW_LEASE_REVISION: ${{ steps.publication-context.outputs.publisher_lease_revision }} | |
| EXACT_REVIEW_CLAIM_GENERATION: ${{ steps.publication-context.outputs.publisher_claim_generation }} | |
| run: | | |
| set -euo pipefail | |
| record_publication_failure_kind() { | |
| local error_file="$1" | |
| # Only explicit GitHub pressure signals may change global admission; | |
| # artifact, tuple, and policy failures must remain ordinary retries. | |
| if grep -Eqi 'HTTP 429|secondary rate limit|API rate limit|rate limit (exceeded|reached)' "$error_file"; then | |
| echo "failure_kind=github_rate_limit" >> "$GITHUB_OUTPUT" | |
| elif grep -Eqi 'HTTP 5[0-9]{2}|status( code)? 5[0-9]{2}|Bad Gateway|Service Unavailable|Gateway Timeout' "$error_file"; then | |
| echo "failure_kind=github_transient" >> "$GITHUB_OUTPUT" | |
| fi | |
| } | |
| if [ "$LIVE_PROCEEDED" != "true" ]; then | |
| terminal_noop="$LIVE_TERMINAL_NOOP" | |
| terminal_missing="$LIVE_TERMINAL_MISSING" | |
| guarded_open="$LIVE_GUARDED_OPEN" | |
| requeue_latest=false | |
| if [ "$LIVE_TERMINAL_NOOP" = "true" ] || [ "$LIVE_TERMINAL_MISSING" = "true" ] || [ "$LIVE_GUARDED_OPEN" = "true" ]; then | |
| live_item_error="$(mktemp)" | |
| live_locked=false | |
| if ! live_item="$(gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER" 2>"$live_item_error")"; then | |
| if grep -Eqi 'HTTP 404|Not Found' "$live_item_error" && gh api "repos/$TARGET_REPO" >/dev/null; then | |
| live_state=missing | |
| else | |
| record_publication_failure_kind "$live_item_error" | |
| cat "$live_item_error" >&2 | |
| rm -f "$live_item_error" | |
| exit 1 | |
| fi | |
| else | |
| live_state="$(jq -r '.state' <<< "$live_item")" | |
| live_locked="$(jq -r '.locked == true' <<< "$live_item")" | |
| fi | |
| rm -f "$live_item_error" | |
| case "$live_state" in | |
| open) | |
| terminal_noop=false | |
| terminal_missing=false | |
| if [ "$live_locked" = "true" ]; then | |
| guarded_open=true | |
| else | |
| guarded_open=false | |
| requeue_latest=true | |
| fi | |
| ;; | |
| closed) | |
| terminal_noop=true | |
| terminal_missing=false | |
| guarded_open=false | |
| ;; | |
| missing) | |
| terminal_noop=false | |
| terminal_missing=true | |
| guarded_open=false | |
| ;; | |
| *) | |
| echo "Unexpected live state for $TARGET_REPO#$ITEM_NUMBER: $live_state" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| fi | |
| { | |
| echo "terminal_noop=$terminal_noop" | |
| echo "terminal_missing=$terminal_missing" | |
| echo "terminal_closed=false" | |
| echo "guarded_open=$guarded_open" | |
| echo "policy_noop=false" | |
| echo "requeue_latest=$requeue_latest" | |
| echo "remote_tuple_verified=false" | |
| echo "routing_deferred=false" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| live_item_error="$(mktemp)" | |
| if ! live_state="$(gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER" --jq .state 2>"$live_item_error")"; then | |
| if grep -Eqi 'HTTP 404|Not Found' "$live_item_error" && gh api "repos/$TARGET_REPO" >/dev/null; then | |
| rm -f "$live_item_error" | |
| { | |
| echo "terminal_noop=false" | |
| echo "terminal_missing=true" | |
| echo "terminal_closed=false" | |
| echo "guarded_open=false" | |
| echo "policy_noop=false" | |
| echo "requeue_latest=false" | |
| echo "remote_tuple_verified=false" | |
| echo "routing_deferred=false" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| record_publication_failure_kind "$live_item_error" | |
| cat "$live_item_error" >&2 | |
| rm -f "$live_item_error" | |
| exit 1 | |
| fi | |
| rm -f "$live_item_error" | |
| if [ "$live_state" = "closed" ]; then | |
| { | |
| echo "terminal_noop=true" | |
| echo "terminal_missing=false" | |
| echo "terminal_closed=false" | |
| echo "guarded_open=false" | |
| echo "policy_noop=false" | |
| echo "requeue_latest=false" | |
| echo "remote_tuple_verified=false" | |
| echo "routing_deferred=false" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| test -f "artifacts/event/$ITEM_NUMBER.md" | |
| echo "terminal_noop=false" >> "$GITHUB_OUTPUT" | |
| publication_error="$(mktemp)" | |
| set +e | |
| pnpm run repair:publish-event-result 2>&1 | tee "$publication_error" | |
| publication_status="${PIPESTATUS[0]}" | |
| set -e | |
| if [ "$publication_status" -ne 0 ]; then | |
| record_publication_failure_kind "$publication_error" | |
| rm -f "$publication_error" | |
| exit "$publication_status" | |
| fi | |
| rm -f "$publication_error" | |
| - name: Dispatch recommended live proofs | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.validate-exact-review-bundle.outcome == 'success' && steps.legacy-exact-artifact.outputs.legacy_tupleless != 'true' && steps.publish-event-result.outcome == 'success' && (steps.publish-event-result.outputs.remote_tuple_verified == 'true' || (steps.publish-event-result.outputs.completion_kind == 'deferred' && steps.publish-event-result.outputs.reason_code == 'close_coverage_deferred')) }} | |
| uses: ./.github/actions/dispatch-live-proofs | |
| with: | |
| target-repo: ${{ steps.publication-context.outputs.target_repo }} | |
| item-numbers: ${{ steps.publication-context.outputs.item_number }} | |
| records-root: records | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| - name: Record fallback canonical exact review lifecycle receipt | |
| id: record-fallback-canonical-lifecycle-receipt | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.publish-event-result.outcome == 'success' && (steps.publish-event-result.outputs.remote_tuple_verified == 'true' || (steps.publish-event-result.outputs.completion_kind == 'deferred' && steps.publish-event-result.outputs.reason_code == 'close_coverage_deferred')) }} | |
| env: | |
| TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }} | |
| FENCE_KEY: ${{ steps.publication-context.outputs.publisher_item_key }} | |
| REVISION: ${{ steps.publication-context.outputs.publisher_lease_revision }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| run: | | |
| set -euo pipefail | |
| payload="$(node -e ' | |
| const revision = Number(process.env.REVISION); | |
| if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`, | |
| fence_key: process.env.FENCE_KEY, | |
| revision, | |
| outcome: "accepted", | |
| receipt_id: `fallback:${process.env.GITHUB_RUN_ID}:${process.env.GITHUB_RUN_ATTEMPT}`, | |
| })); | |
| ')" | |
| signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')" | |
| queue_url="${QUEUE_URL%/}" | |
| curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --header "x-clawsweeper-exact-review-signature: $signature" \ | |
| --data "$payload" \ | |
| "$queue_url/internal/exact-review/lifecycle/canonical-receipt" >/dev/null | |
| - name: Queue deferred exact verdict router | |
| id: queue-deferred-verdict-router | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && fromJSON(steps.publication-context.outputs.decision).sourceAction != 'failed_review_shard_recovery' && steps.publish-event-result.outcome == 'success' && steps.publish-event-result.outputs.remote_tuple_verified == 'true' && steps.publish-event-result.outputs.routing_deferred == 'true' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| CLAWSWEEPER_GITHUB_POOL_CLASS: repository_actions | |
| CLAWSWEEPER_GITHUB_STAGE: publication_router | |
| TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }} | |
| TARGET_BRANCH: ${{ steps.publication-context.outputs.target_branch }} | |
| ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }} | |
| FENCE_KEY: ${{ steps.publication-context.outputs.publisher_item_key }} | |
| REVISION: ${{ steps.publication-context.outputs.publisher_lease_revision }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES: ${{ vars.CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES || '180' }} | |
| CLAWSWEEPER_COMMENT_MAX_COMMENTS: ${{ vars.CLAWSWEEPER_COMMENT_MAX_COMMENTS || '1000' }} | |
| run: | | |
| gh workflow run repair-comment-router.yml \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --ref main \ | |
| -f execute=true \ | |
| -f target_repo="$TARGET_REPO" \ | |
| -f target_branch="$TARGET_BRANCH" \ | |
| -f item_numbers="$ITEM_NUMBER" \ | |
| -f lookback_minutes="$CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES" \ | |
| -f max_comments="$CLAWSWEEPER_COMMENT_MAX_COMMENTS" | |
| payload="$(node -e ' | |
| const revision = Number(process.env.REVISION); | |
| if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`, | |
| fence_key: process.env.FENCE_KEY, | |
| revision, | |
| receipt_id: `router:${process.env.GITHUB_RUN_ID}:${process.env.GITHUB_RUN_ATTEMPT}`, | |
| })); | |
| ')" | |
| signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')" | |
| queue_url="${QUEUE_URL%/}" | |
| curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --header "x-clawsweeper-exact-review-signature: $signature" \ | |
| --data "$payload" \ | |
| "$queue_url/internal/exact-review/lifecycle/router-receipt" >/dev/null | |
| - name: Record deferred close-proof exact review lifecycle receipt | |
| id: record-deferred-close-proof-lifecycle-receipt | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.publish-event-result.outcome == 'success' && steps.publish-event-result.outputs.completion_kind == 'deferred' && steps.publish-event-result.outputs.reason_code == 'close_coverage_deferred' }} | |
| env: | |
| TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }} | |
| FENCE_KEY: ${{ steps.publication-context.outputs.publisher_item_key }} | |
| REVISION: ${{ steps.publication-context.outputs.publisher_lease_revision }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| run: | | |
| set -euo pipefail | |
| payload="$(node -e ' | |
| const revision = Number(process.env.REVISION); | |
| if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| canonical_target_key: process.env.TARGET_REPO + "#" + process.env.ITEM_NUMBER, | |
| fence_key: process.env.FENCE_KEY, | |
| revision, | |
| outcome: "durable", | |
| receipt_id: "router-proof:" + process.env.GITHUB_RUN_ID + ":" + process.env.GITHUB_RUN_ATTEMPT, | |
| })); | |
| ')" | |
| signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write("sha256="+crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex"))')" | |
| queue_url="${QUEUE_URL%/}" | |
| curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --header "x-clawsweeper-exact-review-signature: $signature" \ | |
| --data "$payload" \ | |
| "$queue_url/internal/exact-review/lifecycle/router-receipt" >/dev/null | |
| - name: Record no-router exact review lifecycle receipt | |
| id: record-no-router-lifecycle-receipt | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && fromJSON(steps.publication-context.outputs.decision).sourceAction == 'failed_review_shard_recovery' && steps.publish-event-result.outcome == 'success' && steps.publish-event-result.outputs.remote_tuple_verified == 'true' }} | |
| env: | |
| TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }} | |
| FENCE_KEY: ${{ steps.publication-context.outputs.publisher_item_key }} | |
| REVISION: ${{ steps.publication-context.outputs.publisher_lease_revision }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| run: | | |
| set -euo pipefail | |
| payload="$(node -e ' | |
| const revision = Number(process.env.REVISION); | |
| if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`, | |
| fence_key: process.env.FENCE_KEY, | |
| revision, | |
| outcome: "not_required", | |
| receipt_id: `router-not-required:${process.env.GITHUB_RUN_ID}:${process.env.GITHUB_RUN_ATTEMPT}`, | |
| })); | |
| ')" | |
| signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')" | |
| queue_url="${QUEUE_URL%/}" | |
| curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --header "x-clawsweeper-exact-review-signature: $signature" \ | |
| --data "$payload" \ | |
| "$queue_url/internal/exact-review/lifecycle/router-receipt" >/dev/null | |
| - name: Dispatch deferred high-confidence bug implementation | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.publish-event-result.outcome == 'success' && steps.publish-event-result.outputs.remote_tuple_verified == 'true' && steps.record-fallback-canonical-lifecycle-receipt.outcome == 'success' && vars.CLAWSWEEPER_AUTO_IMPLEMENT_ISSUES == '1' && steps.publication-context.outputs.target_repo == 'openclaw/openclaw' && fromJSON(steps.publication-context.outputs.decision).itemKind == 'issue' && ((fromJSON(steps.publication-context.outputs.decision).sourceAction == 'failed_review_shard_recovery' && steps.record-no-router-lifecycle-receipt.outcome == 'success') || (fromJSON(steps.publication-context.outputs.decision).sourceAction != 'failed_review_shard_recovery' && (steps.publish-event-result.outputs.routing_deferred != 'true' || steps.queue-deferred-verdict-router.outcome == 'success'))) }} | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }} | |
| MAX_DISPATCH: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_MAX_DISPATCH_PER_SWEEP || '' }} | |
| run: | | |
| node scripts/dispatch-issue-implementation-candidates.mjs \ | |
| --target-repo "$TARGET_REPO" \ | |
| --item-number "$ITEM_NUMBER" \ | |
| --artifact-dir artifacts/event | |
| - name: Queue fresh review after source drift | |
| id: queue-source-drift-review | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && (steps.publish-event-result.outputs.requeue_latest == 'true' || steps.legacy-exact-artifact.outputs.legacy_tupleless == 'true') }} | |
| env: | |
| CLAIM_DECISION: ${{ steps.publication-context.outputs.decision }} | |
| PRODUCER_RUN_ATTEMPT: ${{ steps.publication-context.outputs.generation_attempt }} | |
| PRODUCER_RUN_ID: ${{ steps.publication-context.outputs.producer_run_id }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$CLAWSWEEPER_WEBHOOK_SECRET" | |
| queue_url="${QUEUE_URL%/}" | |
| payload="$(node <<'NODE' | |
| const decision = JSON.parse(process.env.CLAIM_DECISION || "{}"); | |
| process.stdout.write(JSON.stringify({ | |
| delivery_id: `publisher-source-drift:${process.env.PRODUCER_RUN_ID}:${process.env.PRODUCER_RUN_ATTEMPT}`, | |
| decision: { | |
| ...decision, | |
| sourceAction: | |
| decision.sourceAction === "failed_review_shard_recovery" | |
| ? decision.sourceAction | |
| : "source_drift_requeue", | |
| supersedesInProgress: true, | |
| }, | |
| })); | |
| NODE | |
| )" | |
| signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')" | |
| response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --header "x-clawsweeper-exact-review-signature: $signature" \ | |
| --data "$payload" \ | |
| "$queue_url/internal/exact-review/enqueue")" | |
| jq -e '.ok == true and (.queued == true or .deduped == true or .shed == true)' <<< "$response" >/dev/null | |
| if jq -e '.shed == true' <<< "$response" >/dev/null; then | |
| echo "Source-drift recovery shed by exact-review queue backpressure." | |
| fi | |
| - name: Release terminal review leases | |
| id: release-terminal-review-leases | |
| if: ${{ always() && steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && (steps.publish-event-result.outputs.terminal_noop == 'true' || steps.publish-event-result.outputs.terminal_closed == 'true') }} | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$GH_TOKEN" | |
| test "$(gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER" --jq .state)" = "closed" | |
| lease_ids="$(gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments?per_page=100" --paginate --jq ".[] | select(.user.login == \"clawsweeper[bot]\" or .user.login == \"openclaw-clawsweeper[bot]\") | select(.body | contains(\"<!-- clawsweeper-review-lease item=$ITEM_NUMBER -->\")) | .id")" | |
| while IFS= read -r lease_id; do | |
| [ -n "$lease_id" ] || continue | |
| gh api "repos/$TARGET_REPO/issues/comments/$lease_id" --method DELETE | |
| done <<< "$lease_ids" | |
| reaction_ids="$(gh api -X GET \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions" \ | |
| -f content="eyes" \ | |
| -F per_page=100 \ | |
| --paginate \ | |
| --jq '.[] | select(.content == "eyes") | select(.user.login == "clawsweeper" or .user.login == "clawsweeper[bot]" or .user.login == "openclaw-clawsweeper[bot]") | .id')" | |
| while IFS= read -r reaction_id; do | |
| [ -n "$reaction_id" ] || continue | |
| gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions/$reaction_id" --method DELETE | |
| done <<< "$reaction_ids" | |
| - name: Confirm terminal item remains closed | |
| id: confirm-terminal-item | |
| if: ${{ always() && steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.release-terminal-review-leases.outcome == 'success' }} | |
| env: | |
| GH_TOKEN: ${{ steps.publication-context.outputs.target_repo == 'openclaw/openclaw' && github.token || steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }} | |
| run: | | |
| test "$(gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER" --jq .state)" = "closed" | |
| echo "confirmed=true" >> "$GITHUB_OUTPUT" | |
| - name: React to target item completion | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.publish-event-result.outputs.requeue_latest != 'true' && (steps.publish-event-result.outputs.terminal_closed == 'true' || steps.publish-event-result.outputs.guarded_open == 'true' || steps.publish-event-result.outputs.policy_noop == 'true' || steps.queue-deferred-verdict-router.outcome == 'success' || (fromJSON(steps.publication-context.outputs.decision).sourceAction == 'failed_review_shard_recovery' && steps.publish-event-result.outputs.remote_tuple_verified == 'true')) }} | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }} | |
| POLICY_NOOP: ${{ steps.publish-event-result.outputs.policy_noop }} | |
| REVIEW_ONLY: ${{ fromJSON(steps.publication-context.outputs.decision).sourceAction == 'failed_review_shard_recovery' && 'true' || 'false' }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$POLICY_NOOP" != "true" ] && [ "$REVIEW_ONLY" != "true" ]; then | |
| gh api -X POST -H "Accept: application/vnd.github+json" \ | |
| "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions" -f content="+1" >/dev/null || true | |
| fi | |
| reaction_ids="$(gh api -X GET -H "Accept: application/vnd.github+json" \ | |
| "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions" \ | |
| -f content="eyes" -F per_page=100 --paginate \ | |
| --jq '.[] | select(.content == "eyes") | select(.user.login == "clawsweeper" or .user.login == "clawsweeper[bot]" or .user.login == "openclaw-clawsweeper[bot]") | .id')" | |
| while IFS= read -r reaction_id; do | |
| [ -n "$reaction_id" ] || continue | |
| gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions/$reaction_id" --method DELETE || true | |
| done <<< "$reaction_ids" | |
| - name: Release superseded or unsuccessful publisher-owned review lease | |
| if: ${{ always() && steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.publication-context.outputs.live_proceeded == 'true' && steps.publish-event-result.outputs.failure_kind != 'github_rate_limit' && steps.publish-event-result.outputs.failure_kind != 'github_transient' && (steps.publish-event-result.outputs.completion_kind == 'superseded' || steps.publish-event-result.outcome != 'success' || (steps.publish-event-result.outputs.requeue_latest == 'true' && steps.queue-source-drift-review.outcome != 'success')) }} | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }} | |
| LEASE_OWNER: github-run-${{ steps.publication-context.outputs.producer_run_id }}-${{ steps.publication-context.outputs.generation_attempt }} | |
| run: | | |
| lease_ids="$(gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments?per_page=100" --paginate --jq ".[] | select(.user.login == \"clawsweeper[bot]\" or .user.login == \"openclaw-clawsweeper[bot]\") | select(.body | contains(\"<!-- clawsweeper-review-lease item=$ITEM_NUMBER -->\")) | select(.body | contains(\"owner=$LEASE_OWNER \")) | .id")" | |
| while IFS= read -r lease_id; do | |
| [ -n "$lease_id" ] || continue | |
| gh api "repos/$TARGET_REPO/issues/comments/$lease_id" --method DELETE | |
| done <<< "$lease_ids" | |
| reaction_ids="$(gh api -X GET \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions" \ | |
| -f content="eyes" -F per_page=100 --paginate \ | |
| --jq '.[] | select(.content == "eyes") | select(.user.login == "clawsweeper" or .user.login == "clawsweeper[bot]" or .user.login == "openclaw-clawsweeper[bot]") | .id')" | |
| while IFS= read -r reaction_id; do | |
| [ -n "$reaction_id" ] || continue | |
| gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions/$reaction_id" --method DELETE | |
| done <<< "$reaction_ids" | |
| - name: Export exact review publication result | |
| id: exact-review-publication-result | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && always() }} | |
| env: | |
| PRIOR_JOB_STATUS: ${{ job.status }} | |
| PUBLISH_OUTCOME: ${{ steps.publish-event-result.outcome }} | |
| TERMINAL_NOOP: ${{ steps.publish-event-result.outputs.terminal_noop }} | |
| TERMINAL_MISSING: ${{ steps.publish-event-result.outputs.terminal_missing }} | |
| TERMINAL_CLOSED: ${{ steps.publish-event-result.outputs.terminal_closed }} | |
| GUARDED_OPEN: ${{ steps.publish-event-result.outputs.guarded_open }} | |
| POLICY_NOOP: ${{ steps.publish-event-result.outputs.policy_noop }} | |
| REQUEUE_LATEST: ${{ steps.publish-event-result.outputs.requeue_latest }} | |
| LEGACY_TUPLELESS: ${{ steps.legacy-exact-artifact.outputs.legacy_tupleless }} | |
| SOURCE_DRIFT_OUTCOME: ${{ steps.queue-source-drift-review.outcome }} | |
| DEFERRED_ROUTE_OUTCOME: ${{ steps.queue-deferred-verdict-router.outcome }} | |
| FAILURE_KIND: ${{ steps.publish-event-result.outputs.failure_kind }} | |
| DOWNLOAD_OUTCOME: ${{ steps.download-exact-review-bundle.outcome }} | |
| VALIDATE_OUTCOME: ${{ steps.validate-exact-review-bundle.outcome }} | |
| PUBLISH_COMPLETION_KIND: ${{ steps.publish-event-result.outputs.completion_kind }} | |
| PUBLISH_REASON_CODE: ${{ steps.publish-event-result.outputs.reason_code }} | |
| PUBLISH_RETRY_AT: ${{ steps.publish-event-result.outputs.retry_at }} | |
| ERROR_FINGERPRINT: ${{ steps.publish-event-result.outputs.error_fingerprint }} | |
| STATE_WRITER_JSON: ${{ steps.publish-event-result.outputs.state_writer_json }} | |
| DIRECT_RECOVERY_OUTCOME: ${{ steps.replay-direct-lifecycle.outputs.outcome }} | |
| DIRECT_RECOVERY_COMPLETION_KIND: ${{ steps.replay-direct-lifecycle.outputs.completion_kind }} | |
| DIRECT_RECOVERY_REASON_CODE: ${{ steps.replay-direct-lifecycle.outputs.reason_code }} | |
| DIRECT_RECOVERY_REQUEUE_LATEST: ${{ steps.replay-direct-lifecycle.outputs.requeue_latest }} | |
| DIRECT_RECOVERY_DIRECT_REQUEUE: ${{ steps.replay-direct-lifecycle.outputs.direct_requeue }} | |
| REVIEW_ONLY: ${{ fromJSON(steps.publication-context.outputs.decision).sourceAction == 'failed_review_shard_recovery' && 'true' || 'false' }} | |
| run: | | |
| set -euo pipefail | |
| outcome=failure | |
| completion_kind=permanent_failure | |
| reason_code=unknown_failure | |
| retry_at= | |
| requeue_latest="$REQUEUE_LATEST" | |
| direct_requeue=false | |
| if [ "$DIRECT_RECOVERY_OUTCOME" = "success" ]; then | |
| case "$DIRECT_RECOVERY_COMPLETION_KIND:$DIRECT_RECOVERY_REASON_CODE" in | |
| published:publication_applied|superseded:remote_newer_tuple) ;; | |
| *) exit 1 ;; | |
| esac | |
| if [ "$DIRECT_RECOVERY_REQUEUE_LATEST" != "" ] && [ "$DIRECT_RECOVERY_REQUEUE_LATEST" != "false" ]; then | |
| exit 1 | |
| fi | |
| if [ "$DIRECT_RECOVERY_DIRECT_REQUEUE" = "true" ]; then | |
| if [ "$DIRECT_RECOVERY_COMPLETION_KIND" != "published" ] || [ "$DIRECT_RECOVERY_REASON_CODE" != "publication_applied" ]; then | |
| exit 1 | |
| fi | |
| direct_requeue=true | |
| elif [ "$DIRECT_RECOVERY_DIRECT_REQUEUE" != "" ] && [ "$DIRECT_RECOVERY_DIRECT_REQUEUE" != "false" ]; then | |
| exit 1 | |
| fi | |
| outcome=success | |
| completion_kind="$DIRECT_RECOVERY_COMPLETION_KIND" | |
| reason_code="$DIRECT_RECOVERY_REASON_CODE" | |
| elif [ "$PUBLISH_COMPLETION_KIND" = "superseded" ] && { [ "$PUBLISH_REASON_CODE" = "remote_newer_tuple" ] || [ "$PUBLISH_REASON_CODE" = "remote_closed" ]; }; then | |
| outcome=success | |
| completion_kind=superseded | |
| reason_code="$PUBLISH_REASON_CODE" | |
| elif [ "$PUBLISH_COMPLETION_KIND" = "deferred" ] && [ "$PUBLISH_REASON_CODE" = "close_coverage_deferred" ]; then | |
| outcome=success | |
| completion_kind=deferred | |
| reason_code=close_coverage_deferred | |
| elif [ "$PUBLISH_COMPLETION_KIND" = "refresh_required" ] && [ "$PUBLISH_REASON_CODE" = "close_coverage_retry" ]; then | |
| # A pre-deployment publisher checked out the new helper but still runs the old | |
| # workflow without the deferred-lane opt-in. Preserve its established refresh | |
| # completion so the queue can create a fresh proof-capable review. | |
| outcome=success | |
| completion_kind=refresh_required | |
| reason_code=close_coverage_retry | |
| elif [ "$PUBLISH_COMPLETION_KIND" = "retryable_failure" ] && [ "$PUBLISH_REASON_CODE" = "review_lease_active" ] && [ -n "$PUBLISH_RETRY_AT" ]; then | |
| outcome=success | |
| completion_kind=retryable_failure | |
| reason_code=review_lease_active | |
| retry_at="$PUBLISH_RETRY_AT" | |
| elif [ "$LEGACY_TUPLELESS" = "true" ] && [ "$SOURCE_DRIFT_OUTCOME" = "success" ]; then | |
| outcome=success | |
| completion_kind=superseded | |
| reason_code=live_terminal | |
| elif [ "$PRIOR_JOB_STATUS" = "cancelled" ]; then | |
| outcome=cancelled | |
| completion_kind=retryable_failure | |
| reason_code=workflow_cancelled | |
| elif [ "$PRIOR_JOB_STATUS" = "success" ]; then | |
| if [ "$PUBLISH_OUTCOME" = "success" ] && [ "$REQUEUE_LATEST" = "true" ] && [ "$SOURCE_DRIFT_OUTCOME" = "success" ]; then | |
| outcome=success | |
| elif [ "$PUBLISH_OUTCOME" = "success" ] && { [ "$TERMINAL_NOOP" = "true" ] || [ "$TERMINAL_MISSING" = "true" ] || [ "$TERMINAL_CLOSED" = "true" ] || [ "$GUARDED_OPEN" = "true" ] || [ "$POLICY_NOOP" = "true" ] || [ "$REVIEW_ONLY" = "true" ]; }; then | |
| outcome=success | |
| elif [ "$PUBLISH_OUTCOME" = "success" ] && [ "$DEFERRED_ROUTE_OUTCOME" = "success" ]; then | |
| outcome=success | |
| fi | |
| fi | |
| if [ "$outcome" = "success" ] && [ "$completion_kind" != "superseded" ] && [ "$completion_kind" != "deferred" ] && [ "$completion_kind" != "refresh_required" ] && [ "$completion_kind" != "retryable_failure" ]; then | |
| if [ "$PUBLISH_COMPLETION_KIND" = "published" ] && [ "$PUBLISH_REASON_CODE" = "publication_applied" ]; then | |
| completion_kind=published | |
| reason_code=publication_applied | |
| else | |
| completion_kind=superseded | |
| reason_code=live_terminal | |
| fi | |
| elif [ "$outcome" != "success" ] && { [ "$FAILURE_KIND" = "github_rate_limit" ] || [ "$FAILURE_KIND" = "github_transient" ]; }; then | |
| completion_kind=retryable_failure | |
| reason_code="$FAILURE_KIND" | |
| retry_at="$PUBLISH_RETRY_AT" | |
| elif [ "$outcome" != "success" ] && [ "$DOWNLOAD_OUTCOME" = "failure" ]; then | |
| completion_kind=retryable_failure | |
| reason_code=artifact_unavailable | |
| elif [ "$outcome" != "success" ] && [ "$VALIDATE_OUTCOME" = "failure" ]; then | |
| completion_kind=permanent_failure | |
| reason_code=invalid_artifact | |
| elif [ "$outcome" != "success" ] && [ "$PUBLISH_COMPLETION_KIND" = "permanent_failure" ]; then | |
| completion_kind=permanent_failure | |
| reason_code="${PUBLISH_REASON_CODE:-unknown_failure}" | |
| elif [ "$outcome" != "success" ] && [ "$PUBLISH_COMPLETION_KIND" = "retryable_failure" ]; then | |
| completion_kind=retryable_failure | |
| reason_code="${PUBLISH_REASON_CODE:-unknown_failure}" | |
| fi | |
| echo "outcome=$outcome" >> "$GITHUB_OUTPUT" | |
| echo "completion_kind=$completion_kind" >> "$GITHUB_OUTPUT" | |
| echo "reason_code=$reason_code" >> "$GITHUB_OUTPUT" | |
| echo "requeue_latest=$requeue_latest" >> "$GITHUB_OUTPUT" | |
| echo "direct_requeue=$direct_requeue" >> "$GITHUB_OUTPUT" | |
| if [ -n "$ERROR_FINGERPRINT" ]; then | |
| echo "error_fingerprint=$ERROR_FINGERPRINT" >> "$GITHUB_OUTPUT" | |
| fi | |
| if [ -n "$retry_at" ]; then | |
| echo "retry_at=$retry_at" >> "$GITHUB_OUTPUT" | |
| fi | |
| if [ "$outcome" = "failure" ] && { [ "$FAILURE_KIND" = "github_rate_limit" ] || [ "$FAILURE_KIND" = "github_transient" ]; }; then | |
| echo "failure_kind=$FAILURE_KIND" >> "$GITHUB_OUTPUT" | |
| fi | |
| if [ -n "$STATE_WRITER_JSON" ]; then | |
| echo "state_writer_json=$STATE_WRITER_JSON" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Complete durable exact review publication | |
| id: complete-exact-review-publication | |
| if: ${{ steps.publication-context.outputs.claimed == 'true' && always() }} | |
| continue-on-error: true | |
| env: | |
| CLAIM_GENERATION: ${{ steps.publication-context.outputs.publisher_claim_generation }} | |
| ITEM_KEY: ${{ steps.publication-context.outputs.publisher_item_key }} | |
| LEASE_REVISION: ${{ steps.publication-context.outputs.publisher_lease_revision }} | |
| OUTCOME: ${{ steps.exact-review-publication-result.outputs.outcome || 'failure' }} | |
| FAILURE_KIND: ${{ steps.exact-review-publication-result.outputs.failure_kind }} | |
| COMPLETION_KIND: ${{ steps.exact-review-publication-result.outputs.completion_kind }} | |
| REASON_CODE: ${{ steps.exact-review-publication-result.outputs.reason_code }} | |
| ERROR_FINGERPRINT: ${{ steps.exact-review-publication-result.outputs.error_fingerprint }} | |
| RETRY_AT: ${{ steps.exact-review-publication-result.outputs.retry_at }} | |
| STATE_WRITER_JSON: ${{ steps.exact-review-publication-result.outputs.state_writer_json }} | |
| PUBLISH_COMPLETION_KIND: ${{ steps.publish-event-result.outputs.completion_kind }} | |
| PUBLISH_REASON_CODE: ${{ steps.publish-event-result.outputs.reason_code }} | |
| TERMINAL_NOOP: ${{ steps.publish-event-result.outputs.terminal_noop }} | |
| TERMINAL_MISSING: ${{ steps.publish-event-result.outputs.terminal_missing }} | |
| TERMINAL_CLOSED: ${{ steps.publish-event-result.outputs.terminal_closed }} | |
| GUARDED_OPEN: ${{ steps.publish-event-result.outputs.guarded_open }} | |
| POLICY_NOOP: ${{ steps.publish-event-result.outputs.policy_noop }} | |
| REQUEUE_LATEST: ${{ steps.publish-event-result.outputs.requeue_latest }} | |
| DIRECT_LIFECYCLE_REQUEUE: ${{ steps.exact-review-publication-result.outputs.direct_requeue }} | |
| LEGACY_TUPLELESS: ${{ steps.legacy-exact-artifact.outputs.legacy_tupleless }} | |
| QUEUE_LEASE_ID: ${{ steps.publication-context.outputs.publisher_lease_id }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| run: | | |
| set -euo pipefail | |
| queue_url="${QUEUE_URL%/}" | |
| payload="$(node -e ' | |
| const leaseRevision = Number(process.env.LEASE_REVISION); | |
| const claimGeneration = Number(process.env.CLAIM_GENERATION); | |
| const runAttempt = Number(process.env.RUN_ATTEMPT); | |
| if (!process.env.QUEUE_LEASE_ID || !process.env.ITEM_KEY) process.exit(1); | |
| if (!Number.isInteger(leaseRevision) || leaseRevision < 1) process.exit(1); | |
| if (!Number.isInteger(claimGeneration) || claimGeneration < 1) process.exit(1); | |
| if (!Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1); | |
| const outcome = ["success", "failure", "cancelled"].includes(process.env.OUTCOME) | |
| ? process.env.OUTCOME | |
| : "failure"; | |
| const failureKind = ["github_rate_limit", "github_transient"].includes(process.env.FAILURE_KIND) | |
| ? process.env.FAILURE_KIND | |
| : undefined; | |
| const completionKind = ["published", "superseded", "deferred", "retryable_failure", "refresh_required", "permanent_failure"].includes(process.env.COMPLETION_KIND) | |
| ? process.env.COMPLETION_KIND | |
| : undefined; | |
| const reasonCode = ["publication_applied", "remote_newer_tuple", "remote_closed", "live_terminal", "github_rate_limit", "github_transient", "state_contention", "review_lease_active", "workflow_cancelled", "artifact_unavailable", "artifact_expired", "close_coverage_retry", "close_coverage_deferred", "invalid_artifact", "missing_record_tuple", "tuple_protocol_invalid", "policy_invariant", "unknown_failure", "retry_exhausted"].includes(process.env.REASON_CODE) | |
| ? process.env.REASON_CODE | |
| : undefined; | |
| const errorFingerprint = /^[A-Za-z0-9:._-]{1,200}$/.test(process.env.ERROR_FINGERPRINT || "") | |
| ? process.env.ERROR_FINGERPRINT | |
| : undefined; | |
| const retryAt = String(process.env.RETRY_AT || "").trim(); | |
| if (retryAt && !Number.isFinite(Date.parse(retryAt))) process.exit(1); | |
| if (!completionKind || !reasonCode) process.exit(1); | |
| const directLifecycleRequeue = process.env.DIRECT_LIFECYCLE_REQUEUE === "true"; | |
| const lifecycleTerminal = | |
| directLifecycleRequeue || process.env.REQUEUE_LATEST === "true" | |
| ? "requeue" | |
| : process.env.LEGACY_TUPLELESS === "true" || ["retryable_failure", "refresh_required"].includes(completionKind) | |
| ? "requeue" | |
| : process.env.TERMINAL_MISSING === "true" | |
| ? "target_missing" | |
| : process.env.TERMINAL_CLOSED === "true" || process.env.TERMINAL_NOOP === "true" | |
| ? "target_closed" | |
| : process.env.POLICY_NOOP === "true" | |
| ? "policy_noop" | |
| : process.env.GUARDED_OPEN === "true" | |
| ? "guarded_open" | |
| : completionKind === "superseded" | |
| ? reasonCode === "remote_closed" | |
| ? "target_closed" | |
| : "superseded" | |
| : completionKind === "permanent_failure" | |
| ? "failure" | |
| : undefined; | |
| let stateWriter; | |
| try { | |
| const parsed = JSON.parse(process.env.STATE_WRITER_JSON || ""); | |
| if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) stateWriter = parsed; | |
| } catch {} | |
| process.stdout.write(JSON.stringify({ | |
| lease_id: process.env.QUEUE_LEASE_ID, | |
| item_key: process.env.ITEM_KEY, | |
| lease_revision: leaseRevision, | |
| claim_generation: claimGeneration, | |
| run_id: process.env.GITHUB_RUN_ID, | |
| run_attempt: runAttempt, | |
| outcome, | |
| completion_kind: completionKind, | |
| reason_code: reasonCode, | |
| ...(errorFingerprint ? { error_fingerprint: errorFingerprint } : {}), | |
| ...(retryAt ? { retry_at: retryAt } : {}), | |
| ...(outcome === "failure" && failureKind ? { failure_kind: failureKind } : {}), | |
| ...(directLifecycleRequeue ? { direct_lifecycle_requeue: true } : {}), | |
| ...(lifecycleTerminal ? { lifecycle_terminal_disposition: lifecycleTerminal } : {}), | |
| ...(stateWriter ? { state_writer: stateWriter } : {}), | |
| })); | |
| ')" | |
| for attempt in 1 2 3; do | |
| if curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --data "$payload" \ | |
| "$queue_url/internal/exact-review/complete" >/dev/null; then | |
| exit 0 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| sleep "$((attempt * 5))" | |
| fi | |
| done | |
| exit 1 | |
| - name: Mark active lease retry waiting | |
| if: ${{ always() && steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.has_command_context == 'true' && steps.exact-review-publication-result.outputs.outcome == 'success' && steps.exact-review-publication-result.outputs.completion_kind == 'retryable_failure' && steps.exact-review-publication-result.outputs.reason_code == 'review_lease_active' && steps.complete-exact-review-publication.outcome == 'success' }} | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }} | |
| COMMAND_STATUS_MARKER: ${{ fromJSON(steps.publication-context.outputs.decision).commandStatusMarker || '' }} | |
| STATUS_COMMENT_ID: ${{ fromJSON(steps.publication-context.outputs.decision).statusCommentId || '' }} | |
| RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| pnpm run repair:update-command-status -- \ | |
| --repo "$TARGET_REPO" \ | |
| --item-number "$ITEM_NUMBER" \ | |
| --marker "$COMMAND_STATUS_MARKER" \ | |
| --status-comment-id "$STATUS_COMMENT_ID" \ | |
| --state "Waiting" \ | |
| --detail "Another same-revision review is active; durable publication will retry after its lease expires." \ | |
| --run-url "$RUN_URL" | |
| - name: Submit artifact-publication GitHub egress telemetry | |
| if: ${{ always() && steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.artifact-github-egress-observer.outcome == 'success' }} | |
| continue-on-error: true | |
| env: | |
| EXACT_REVIEW_QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| run: pnpm run --silent repair:github-egress-telemetry submit | |
| - name: Fail unsuccessful exact review publication | |
| if: ${{ always() && steps.publication-context.outputs.claimed == 'true' && (steps.exact-review-publication-result.outputs.outcome != 'success' || steps.complete-exact-review-publication.outcome != 'success') }} | |
| run: exit 1 | |
| event-review-terminal-finalization: | |
| name: Finalize exact-review command acknowledgement | |
| if: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.queue_lease_id != '' && github.event.client_payload.source_action == 'exact_review_command_acknowledgement' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - name: Claim committed terminal finalization | |
| id: finalization-context | |
| env: | |
| ITEM_KEY: ${{ github.event.client_payload.queue_claim.item_key }} | |
| QUEUE_LEASE_ID: ${{ github.event.client_payload.queue_lease_id }} | |
| QUEUE_LEASE_REVISION: ${{ github.event.client_payload.queue_claim.lease_revision }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| run: | | |
| set -euo pipefail | |
| printf 'claimed=false\n' >> "$GITHUB_OUTPUT" | |
| queue_url="${QUEUE_URL%/}" | |
| payload="$(node -e ' | |
| const leaseRevision = Number(process.env.QUEUE_LEASE_REVISION); | |
| const runAttempt = Number(process.env.RUN_ATTEMPT); | |
| if (!process.env.QUEUE_LEASE_ID || !process.env.ITEM_KEY) process.exit(1); | |
| if (!Number.isInteger(leaseRevision) || leaseRevision < 1) process.exit(1); | |
| if (!Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| lease_id: process.env.QUEUE_LEASE_ID, | |
| item_key: process.env.ITEM_KEY, | |
| lease_revision: leaseRevision, | |
| run_id: process.env.GITHUB_RUN_ID, | |
| run_attempt: runAttempt, | |
| })); | |
| ')" | |
| response_file="$(mktemp)" | |
| error_file="$(mktemp)" | |
| trap 'rm -f "$response_file" "$error_file"' EXIT | |
| for attempt in 1 2 3; do | |
| : > "$response_file" | |
| : > "$error_file" | |
| if status="$(curl --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --output "$response_file" --write-out '%{http_code}' \ | |
| --request POST --header "content-type: application/json" --data "$payload" \ | |
| "$queue_url/internal/exact-review/claim" 2>"$error_file")"; then | |
| response="$(<"$response_file")" | |
| if [ "$status" = "409" ]; then | |
| conflict_reason="$(RESPONSE="$response" node <<'NODE' | |
| const response = JSON.parse(process.env.RESPONSE || "{}"); | |
| const safeConflicts = new Set([ | |
| "lease_not_active", | |
| "lease_already_claimed", | |
| "lease_decision_unavailable", | |
| "stale_run_attempt", | |
| ]); | |
| if (!safeConflicts.has(response.error)) process.exit(1); | |
| process.stdout.write(response.error); | |
| NODE | |
| )" || { | |
| echo "Unexpected terminal-finalization claim conflict: $response" >&2 | |
| exit 1 | |
| } | |
| echo "::notice::Skipping terminal finalization because lease claim lost safely: $conflict_reason" | |
| exit 0 | |
| fi | |
| if [ "$status" = "200" ]; then | |
| break | |
| fi | |
| if [[ "$status" != 5* ]]; then | |
| echo "Terminal-finalization claim returned HTTP $status: $response" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| if [ "$attempt" = "3" ]; then | |
| cat "$error_file" >&2 | |
| echo "Terminal-finalization claim failed after retries" >&2 | |
| exit 1 | |
| fi | |
| sleep "$attempt" | |
| done | |
| RESPONSE="$response" node <<'NODE' | |
| const fs = require("node:fs"); | |
| const response = JSON.parse(process.env.RESPONSE || "{}"); | |
| const decision = response.decision; | |
| const finalization = response.terminal_finalization; | |
| const lifecycleProjection = response.lifecycle_projection; | |
| const leaseRevision = Number(response.lease_revision); | |
| const claimGeneration = Number(response.claim_generation); | |
| if ( | |
| response.claimed !== true || response.protocol_version !== 2 || | |
| response.item_key !== process.env.ITEM_KEY || | |
| decision?.sourceAction !== "exact_review_artifact_publish" || | |
| !finalization || !["Complete", "Failed"].includes(finalization.statusState) || | |
| typeof finalization.statusDetail !== "string" || | |
| !lifecycleProjection || | |
| lifecycleProjection.canonicalTargetKey !== `${decision.targetRepo}#${decision.itemNumber}` || | |
| typeof lifecycleProjection.fenceKey !== "string" || !lifecycleProjection.fenceKey || | |
| !Number.isInteger(lifecycleProjection.revision) || lifecycleProjection.revision < 1 || | |
| !Number.isInteger(leaseRevision) || leaseRevision < 1 || | |
| !Number.isInteger(claimGeneration) || claimGeneration < 1 | |
| ) process.exit(1); | |
| const [targetRepoOwner, targetRepoName] = String(decision.targetRepo).split("/"); | |
| const values = { | |
| target_repo: decision.targetRepo, | |
| target_repo_owner: targetRepoOwner, | |
| target_repo_name: targetRepoName, | |
| item_number: decision.itemNumber, | |
| status_marker: decision.commandStatusMarker || "", | |
| status_comment_id: decision.statusCommentId || "", | |
| item_key: response.item_key, | |
| lease_revision: leaseRevision, | |
| lifecycle_fence_key: lifecycleProjection.fenceKey, | |
| lifecycle_revision: lifecycleProjection.revision, | |
| claim_generation: claimGeneration, | |
| status_state: finalization.statusState, | |
| status_detail: finalization.statusDetail, | |
| }; | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, "claimed=true\n"); | |
| for (const [key, value] of Object.entries(values)) { | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`); | |
| } | |
| NODE | |
| - uses: actions/checkout@v7 | |
| if: ${{ steps.finalization-context.outputs.claimed == 'true' }} | |
| with: | |
| # Fetch blobs during the retryable fetch instead of lazily fetching | |
| # them during checkout, which strands claimed finalizations on 5xx. | |
| fetch-depth: 1 | |
| - uses: ./.github/actions/setup-pnpm | |
| if: ${{ steps.finalization-context.outputs.claimed == 'true' }} | |
| with: | |
| build-script: build:repair | |
| - name: Create target write token | |
| id: target-write-token | |
| if: ${{ steps.finalization-context.outputs.claimed == 'true' }} | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| owner: ${{ steps.finalization-context.outputs.target_repo_owner }} | |
| repositories: ${{ steps.finalization-context.outputs.target_repo_name }} | |
| permission-issues: write | |
| permission-pull-requests: write | |
| - name: Begin fenced terminal acknowledgement | |
| id: terminal-acknowledgement | |
| if: ${{ steps.finalization-context.outputs.claimed == 'true' && steps.target-write-token.outcome == 'success' }} | |
| env: | |
| ITEM_KEY: ${{ steps.finalization-context.outputs.item_key }} | |
| QUEUE_LEASE_ID: ${{ github.event.client_payload.queue_lease_id }} | |
| LEASE_REVISION: ${{ steps.finalization-context.outputs.lease_revision }} | |
| CLAIM_GENERATION: ${{ steps.finalization-context.outputs.claim_generation }} | |
| STATUS_MARKER: ${{ steps.finalization-context.outputs.status_marker }} | |
| STATUS_COMMENT_ID: ${{ steps.finalization-context.outputs.status_comment_id }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| run: | | |
| set -euo pipefail | |
| payload="$(node -e ' | |
| const leaseRevision = Number(process.env.LEASE_REVISION); | |
| const claimGeneration = Number(process.env.CLAIM_GENERATION); | |
| const runAttempt = Number(process.env.RUN_ATTEMPT); | |
| const statusMarker = process.env.STATUS_MARKER || null; | |
| const statusCommentId = process.env.STATUS_COMMENT_ID === "" ? null : Number(process.env.STATUS_COMMENT_ID); | |
| if (!process.env.QUEUE_LEASE_ID || !process.env.ITEM_KEY) process.exit(1); | |
| if (!Number.isInteger(leaseRevision) || leaseRevision < 1 || !Number.isInteger(claimGeneration) || claimGeneration < 1 || !Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1); | |
| if (!statusMarker && statusCommentId === null) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| lease_id: process.env.QUEUE_LEASE_ID, item_key: process.env.ITEM_KEY, | |
| lease_revision: leaseRevision, claim_generation: claimGeneration, | |
| run_id: process.env.GITHUB_RUN_ID, run_attempt: runAttempt, | |
| ...(statusMarker ? { status_marker: statusMarker } : {}), | |
| ...(statusCommentId === null ? {} : { status_comment_id: statusCommentId }), | |
| })); | |
| ')" | |
| response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST --header "content-type: application/json" --data "$payload" \ | |
| "${QUEUE_URL%/}/internal/exact-review/terminal-finalization/attempt")" | |
| jq -e '.ok == true' <<< "$response" >/dev/null | |
| echo "allowed=$(jq -r '.allowed == true' <<< "$response")" >> "$GITHUB_OUTPUT" | |
| echo "acknowledgement_state=$(jq -r '.acknowledgement_state // empty' <<< "$response")" >> "$GITHUB_OUTPUT" | |
| echo "attempt_id=$(jq -r '.attempt_id // empty' <<< "$response")" >> "$GITHUB_OUTPUT" | |
| - name: Update final command status once | |
| id: update-final-command-status | |
| if: ${{ steps.terminal-acknowledgement.outputs.allowed == 'true' }} | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.finalization-context.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.finalization-context.outputs.item_number }} | |
| COMMAND_STATUS_MARKER: ${{ steps.finalization-context.outputs.status_marker }} | |
| STATUS_COMMENT_ID: ${{ steps.finalization-context.outputs.status_comment_id }} | |
| STATUS_STATE: ${{ steps.finalization-context.outputs.status_state }} | |
| STATUS_DETAIL: ${{ steps.finalization-context.outputs.status_detail }} | |
| RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| FENCE_KEY: ${{ steps.finalization-context.outputs.lifecycle_fence_key }} | |
| REVISION: ${{ steps.finalization-context.outputs.lifecycle_revision }} | |
| ATTEMPT_ID: ${{ steps.terminal-acknowledgement.outputs.attempt_id }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| run: | | |
| set -euo pipefail | |
| update_log="$(mktemp)" | |
| trap 'rm -f "$update_log"' EXIT | |
| pnpm run repair:update-command-status -- \ | |
| --repo "$TARGET_REPO" --item-number "$ITEM_NUMBER" \ | |
| --marker "$COMMAND_STATUS_MARKER" --status-comment-id "$STATUS_COMMENT_ID" \ | |
| --state "$STATUS_STATE" --detail "$STATUS_DETAIL" --run-url "$RUN_URL" \ | |
| --require-mutation --locked-conversation-terminal-skip --verify-terminal-status-receipt 2>&1 | tee "$update_log" || { | |
| update_status="$?" | |
| # End-of-window installation throttling is self-healing: the | |
| # requeue step re-arms the driver for after the rate window, so | |
| # the failure sentinel must not turn this run red. | |
| if grep -Eqi 'rate limit exceeded|secondary rate limit|HTTP 429' "$update_log"; then | |
| echo "throttled=true" >> "$GITHUB_OUTPUT" | |
| echo "::notice::GitHub throttled the terminal status update; the requeue step retries after the rate window." | |
| fi | |
| failure_payload="$(node -e ' | |
| const revision = Number(process.env.REVISION); | |
| if (!process.env.FENCE_KEY || !process.env.ATTEMPT_ID || !Number.isInteger(revision) || revision < 1) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`, | |
| fence_key: process.env.FENCE_KEY, revision, attempt_id: process.env.ATTEMPT_ID, | |
| ...(process.env.COMMAND_STATUS_MARKER ? { status_marker: process.env.COMMAND_STATUS_MARKER } : {}), | |
| ...(process.env.STATUS_COMMENT_ID ? { status_comment_id: Number(process.env.STATUS_COMMENT_ID) } : {}), | |
| })); | |
| ')" | |
| signature="$(PAYLOAD="$failure_payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')" | |
| curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST --header "content-type: application/json" \ | |
| --header "x-clawsweeper-exact-review-signature: $signature" \ | |
| --data "$failure_payload" \ | |
| "${QUEUE_URL%/}/internal/exact-review/lifecycle/command-ack/failed" >/dev/null | |
| exit "$update_status" | |
| } | |
| - name: Record verified terminal acknowledgement receipt | |
| id: observe-verified-terminal-acknowledgement | |
| if: ${{ always() && steps.finalization-context.outputs.claimed == 'true' && steps.terminal-acknowledgement.outputs.allowed == 'true' && steps.update-final-command-status.outcome == 'success' && steps.update-final-command-status.outputs.terminal_status_verified == 'true' }} | |
| env: | |
| TARGET_REPO: ${{ steps.finalization-context.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.finalization-context.outputs.item_number }} | |
| FENCE_KEY: ${{ steps.finalization-context.outputs.lifecycle_fence_key }} | |
| REVISION: ${{ steps.finalization-context.outputs.lifecycle_revision }} | |
| STATUS_MARKER: ${{ steps.finalization-context.outputs.status_marker }} | |
| STATUS_COMMENT_ID: ${{ steps.finalization-context.outputs.status_comment_id }} | |
| COMMAND_COMMENT_ID: ${{ steps.update-final-command-status.outputs.command_comment_id }} | |
| COMPLETION_COMMENT_ID: ${{ steps.update-final-command-status.outputs.completion_comment_id }} | |
| COMPLETION_COMPLETED_AT: ${{ steps.update-final-command-status.outputs.completion_completed_at }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| run: | | |
| set -euo pipefail | |
| payload="$(node -e ' | |
| const fenceKey = process.env.FENCE_KEY || ""; | |
| const revision = Number(process.env.REVISION); | |
| const statusMarker = process.env.STATUS_MARKER || null; | |
| const statusCommentId = process.env.STATUS_COMMENT_ID === "" ? null : Number(process.env.STATUS_COMMENT_ID); | |
| const commandCommentId = Number(process.env.COMMAND_COMMENT_ID); | |
| const completionCommentId = Number(process.env.COMPLETION_COMMENT_ID); | |
| const completedAt = process.env.COMPLETION_COMPLETED_AT || ""; | |
| if ( | |
| !fenceKey || !Number.isSafeInteger(revision) || revision < 1 || | |
| (!statusMarker && statusCommentId === null) || | |
| (statusCommentId !== null && (!Number.isInteger(statusCommentId) || statusCommentId < 1)) || | |
| !Number.isInteger(commandCommentId) || commandCommentId < 1 || | |
| !Number.isInteger(completionCommentId) || completionCommentId < 1 || | |
| !completedAt || !Number.isFinite(Date.parse(completedAt)) | |
| ) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`, | |
| fence_key: fenceKey, | |
| revision, | |
| ...(statusMarker ? { status_marker: statusMarker } : {}), | |
| ...(statusCommentId === null ? {} : { status_comment_id: statusCommentId }), | |
| command_comment_id: commandCommentId, | |
| completion_comment_id: completionCommentId, | |
| completed_at: completedAt, | |
| observed_at: Date.now(), | |
| })); | |
| ')" | |
| signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')" | |
| response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST --header "content-type: application/json" \ | |
| --header "x-clawsweeper-exact-review-signature: $signature" \ | |
| --data "$payload" \ | |
| "${QUEUE_URL%/}/internal/exact-review/lifecycle/command-ack/observed")" | |
| jq -e '.ok == true and .accepted == true and .acknowledgement_state == "observed"' <<< "$response" >/dev/null | |
| - name: Complete locked terminal acknowledgement skip | |
| id: complete-locked-terminal-acknowledgement | |
| if: ${{ always() && steps.finalization-context.outputs.claimed == 'true' && (steps.update-final-command-status.outputs.locked_conversation == 'true' || steps.update-final-command-status.outputs.missing_status_comment == 'true') }} | |
| env: | |
| ITEM_KEY: ${{ steps.finalization-context.outputs.item_key }} | |
| QUEUE_LEASE_ID: ${{ github.event.client_payload.queue_lease_id }} | |
| LEASE_REVISION: ${{ steps.finalization-context.outputs.lease_revision }} | |
| CLAIM_GENERATION: ${{ steps.finalization-context.outputs.claim_generation }} | |
| STATUS_MARKER: ${{ steps.finalization-context.outputs.status_marker }} | |
| STATUS_COMMENT_ID: ${{ steps.finalization-context.outputs.status_comment_id }} | |
| ATTEMPT_ID: ${{ steps.terminal-acknowledgement.outputs.attempt_id }} | |
| MISSING_STATUS_COMMENT: ${{ steps.update-final-command-status.outputs.missing_status_comment }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| run: | | |
| set -euo pipefail | |
| skip_reason="locked_conversation" | |
| expected_state="skipped_locked" | |
| if [ "$MISSING_STATUS_COMMENT" = "true" ]; then | |
| skip_reason="missing_status_comment" | |
| expected_state="skipped_missing_comment" | |
| fi | |
| payload="$(SKIP_REASON="$skip_reason" node -e ' | |
| const leaseRevision = Number(process.env.LEASE_REVISION); | |
| const claimGeneration = Number(process.env.CLAIM_GENERATION); | |
| const runAttempt = Number(process.env.RUN_ATTEMPT); | |
| const statusMarker = process.env.STATUS_MARKER || null; | |
| const statusCommentId = process.env.STATUS_COMMENT_ID === "" ? null : Number(process.env.STATUS_COMMENT_ID); | |
| if (!process.env.QUEUE_LEASE_ID || !process.env.ITEM_KEY || !process.env.ATTEMPT_ID) process.exit(1); | |
| if (!["locked_conversation", "missing_status_comment"].includes(process.env.SKIP_REASON)) process.exit(1); | |
| if (!Number.isInteger(leaseRevision) || leaseRevision < 1 || !Number.isInteger(claimGeneration) || claimGeneration < 1 || !Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1); | |
| if (!statusMarker && statusCommentId === null) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| lease_id: process.env.QUEUE_LEASE_ID, item_key: process.env.ITEM_KEY, | |
| lease_revision: leaseRevision, claim_generation: claimGeneration, | |
| run_id: process.env.GITHUB_RUN_ID, run_attempt: runAttempt, | |
| attempt_id: process.env.ATTEMPT_ID, reason: process.env.SKIP_REASON, | |
| ...(statusMarker ? { status_marker: statusMarker } : {}), | |
| ...(statusCommentId === null ? {} : { status_comment_id: statusCommentId }), | |
| })); | |
| ')" | |
| response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST --header "content-type: application/json" --data "$payload" \ | |
| "${QUEUE_URL%/}/internal/exact-review/terminal-finalization/skip")" | |
| jq -e --arg state "$expected_state" \ | |
| '.ok == true and .completed == true and .acknowledgement_state == $state' <<< "$response" >/dev/null | |
| - name: Requeue unobserved terminal acknowledgement | |
| if: ${{ always() && steps.finalization-context.outputs.claimed == 'true' && steps.observe-verified-terminal-acknowledgement.outcome != 'success' && !((steps.update-final-command-status.outputs.locked_conversation == 'true' || steps.update-final-command-status.outputs.missing_status_comment == 'true') && steps.complete-locked-terminal-acknowledgement.outcome == 'success') }} | |
| env: | |
| ITEM_KEY: ${{ steps.finalization-context.outputs.item_key }} | |
| QUEUE_LEASE_ID: ${{ github.event.client_payload.queue_lease_id }} | |
| LEASE_REVISION: ${{ steps.finalization-context.outputs.lease_revision }} | |
| CLAIM_GENERATION: ${{ steps.finalization-context.outputs.claim_generation }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| run: | | |
| set -euo pipefail | |
| payload="$(node -e ' | |
| const leaseRevision = Number(process.env.LEASE_REVISION); | |
| const claimGeneration = Number(process.env.CLAIM_GENERATION); | |
| const runAttempt = Number(process.env.RUN_ATTEMPT); | |
| if (!process.env.QUEUE_LEASE_ID || !process.env.ITEM_KEY || !Number.isInteger(leaseRevision) || leaseRevision < 1 || !Number.isInteger(claimGeneration) || claimGeneration < 1 || !Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1); | |
| process.stdout.write(JSON.stringify({ | |
| lease_id: process.env.QUEUE_LEASE_ID, item_key: process.env.ITEM_KEY, | |
| lease_revision: leaseRevision, claim_generation: claimGeneration, | |
| run_id: process.env.GITHUB_RUN_ID, run_attempt: runAttempt, | |
| })); | |
| ')" | |
| response_file="$(mktemp)" | |
| trap 'rm -f "$response_file"' EXIT | |
| response_status="$(curl --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --write-out '%{http_code}' --output "$response_file" \ | |
| --request POST --header "content-type: application/json" --data "$payload" \ | |
| "${QUEUE_URL%/}/internal/exact-review/terminal-finalization/retry")" | |
| if [ "$response_status" = "409" ]; then | |
| # A GitHub acknowledgement webhook can observe and consume the | |
| # terminal driver between the status mutation and this retry. | |
| # The old fenced lease must not make that successful handoff fail. | |
| jq -e '.error == "lease_not_active"' "$response_file" >/dev/null | |
| exit 0 | |
| fi | |
| test "$response_status" = "200" | |
| jq -e '.ok == true and (.requeued == true or .completed == true)' "$response_file" >/dev/null | |
| - name: Fail failed terminal acknowledgement | |
| if: ${{ (steps.update-final-command-status.outcome == 'failure' && steps.update-final-command-status.outputs.throttled != 'true') || steps.complete-locked-terminal-acknowledgement.outcome == 'failure' || steps.terminal-acknowledgement.outcome == 'failure' || steps.target-write-token.outcome == 'failure' }} | |
| run: exit 1 | |
| target-fanout: | |
| name: Fan out target repository sweeps | |
| if: ${{ github.event_name == 'schedule' && (github.event.schedule == '4/20 * * * *' || github.event.schedule == '41/10 * * * *' || github.event.schedule == '37 */6 * * *') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| actions: write | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| filter: blob:none | |
| fetch-depth: 0 | |
| - uses: ./.github/actions/setup-state | |
| with: | |
| coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| hydrate-git-state: "false" | |
| hydrate-state-blobs: "false" | |
| - uses: ./.github/actions/setup-pnpm | |
| with: | |
| build-script: build:repair | |
| - name: Create OpenClaw inventory token | |
| id: openclaw-token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| owner: openclaw | |
| - name: Create steipete inventory token | |
| id: steipete-token | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| owner: steipete | |
| - name: Dispatch selected targets | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| CLAWSWEEPER_DISPATCH_TOKEN: ${{ github.token }} | |
| CLAWSWEEPER_INVENTORY_TOKEN_OPENCLAW: ${{ steps.openclaw-token.outputs.token }} | |
| CLAWSWEEPER_INVENTORY_TOKEN_STEIPETE: ${{ steps.steipete-token.outputs.token || '__public__' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| FANOUT_MODE: ${{ github.event.schedule == '41/10 * * * *' && 'normal-review' || (github.event.schedule == '37 */6 * * *' && 'audit' || 'hot-intake') }} | |
| FANOUT_LIMIT: ${{ github.event.schedule == '41/10 * * * *' && '12' || (github.event.schedule == '37 */6 * * *' && '12' || '20') }} | |
| REVIEW_COVERAGE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| run: | | |
| set -euo pipefail | |
| pnpm run target-fanout -- \ | |
| --mode "$FANOUT_MODE" \ | |
| --limit "$FANOUT_LIMIT" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --workflow sweep.yml \ | |
| --ref main \ | |
| --cursor-store-url "$REVIEW_COVERAGE_URL" \ | |
| --coverage-tracked-items-manifest .artifacts/worker-records-manifest.json \ | |
| --publish-url "$REVIEW_COVERAGE_URL" | |
| - name: Summarize trailing weekly review coverage | |
| # Also runs for operator dispatches of the audit lane: the signed live | |
| # inventory is what gives /api/review-coverage its true denominator, and | |
| # gating it on one cron string left the metric reporting "missing" for | |
| # up to six hours after a deploy or an on-demand audit. | |
| if: ${{ github.event.schedule == '37 */6 * * *' || (github.event_name == 'workflow_dispatch' && github.event.inputs.audit_dashboard == 'true') }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| CLAWSWEEPER_DISPATCH_TOKEN: ${{ github.token }} | |
| CLAWSWEEPER_INVENTORY_TOKEN_OPENCLAW: ${{ steps.openclaw-token.outputs.token }} | |
| CLAWSWEEPER_INVENTORY_TOKEN_STEIPETE: ${{ steps.steipete-token.outputs.token || '__public__' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| REVIEW_COVERAGE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| run: pnpm run target-fanout -- coverage --window-days 7 --publish-url "$REVIEW_COVERAGE_URL" >> "$GITHUB_STEP_SUMMARY" | |
| plan: | |
| name: Plan review candidates | |
| if: ${{ (github.event_name != 'repository_dispatch' || github.event.action == 'clawsweeper_target_sweep') && !((github.event_name == 'workflow_dispatch' && (github.event.inputs.apply_existing == 'true' || github.event.inputs.audit_dashboard == 'true')) || (github.event_name == 'schedule' && ((github.event.schedule == '3 * * * *' || github.event.schedule == '18 * * * *' || github.event.schedule == '33 * * * *' || github.event.schedule == '48 * * * *' || github.event.schedule == '8,23,38,53 * * * *' || github.event.schedule == '6,21,36,51 * * * *') || (github.event.schedule == '7 */6 * * *' || github.event.schedule == '12 */6 * * *' || github.event.schedule == '17 */6 * * *') || (github.event.schedule == '4/20 * * * *' || github.event.schedule == '41/10 * * * *' || github.event.schedule == '37 */6 * * *') || github.event.schedule == '13 * * * *'))) && !(github.event_name == 'schedule' && (github.event.schedule == '2/5 * * * *' || github.event.schedule == '22 * * * *') && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') && !(github.event_name == 'repository_dispatch' && github.event.client_payload.target_repo == 'openclaw/clawhub' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| concurrency: | |
| group: ${{ format('clawsweeper-planner-{0}', (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.item_number == '' && github.event.inputs.item_numbers == '')) && (github.event.inputs.target_repo || ((github.event.schedule == '2/5 * * * *' || github.event.schedule == '22 * * * *') && 'openclaw/clawhub' || 'openclaw/openclaw')) || github.run_id) }} | |
| cancel-in-progress: false | |
| outputs: | |
| batch_size: ${{ steps.mode.outputs.batch_size }} | |
| codex_timeout_ms: ${{ steps.mode.outputs.codex_timeout_ms }} | |
| hot_intake: ${{ steps.mode.outputs.hot_intake }} | |
| queue_feed: ${{ steps.mode.outputs.queue_feed }} | |
| matrix: ${{ steps.select.outputs.matrix }} | |
| max_pages: ${{ steps.mode.outputs.max_pages }} | |
| planned_count: ${{ steps.select.outputs.planned_count }} | |
| planned_capacity: ${{ steps.select.outputs.planned_capacity }} | |
| planned_item_numbers: ${{ steps.select.outputs.planned_item_numbers }} | |
| planned_shards: ${{ steps.select.outputs.planned_shards }} | |
| due_backlog: ${{ steps.select.outputs.due_backlog }} | |
| oldest_unreviewed_at: ${{ steps.select.outputs.oldest_unreviewed_at }} | |
| capacity_reason: ${{ steps.select.outputs.capacity_reason }} | |
| shard_count: ${{ steps.mode.outputs.shard_count }} | |
| target_checkout_dir: ${{ steps.target.outputs.target_checkout_dir }} | |
| target_branch: ${{ steps.target.outputs.target_branch }} | |
| target_repo: ${{ steps.target.outputs.target_repo }} | |
| target_repo_name: ${{ steps.target.outputs.target_repo_name }} | |
| target_repo_owner: ${{ steps.target.outputs.target_repo_owner }} | |
| target_slug: ${{ steps.target.outputs.target_slug }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| path: clawsweeper | |
| filter: blob:none | |
| fetch-depth: 0 | |
| - name: Resolve target repository | |
| id: target | |
| run: | | |
| target_repo="${{ github.event.inputs.target_repo || github.event.client_payload.target_repo || '' }}" | |
| target_branch="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.target_branch || github.event.client_payload.target_branch || 'main' }}" | |
| if [ -z "$target_repo" ]; then | |
| case "${{ github.event.schedule || '' }}" in | |
| "2/5 * * * *"|"22 * * * *"|"8,23,38,53 * * * *"|"12 */6 * * *") | |
| target_repo="openclaw/clawhub" | |
| ;; | |
| *) | |
| target_repo="openclaw/openclaw" | |
| ;; | |
| esac | |
| fi | |
| if ! printf '%s' "$target_branch" | grep -Eq '^[A-Za-z0-9_.\/-]+$'; then | |
| echo "Invalid target_branch: $target_branch" >&2 | |
| exit 1 | |
| fi | |
| target_owner="${target_repo%%/*}" | |
| target_name="${target_repo#*/}" | |
| target_slug="$(printf '%s' "$target_repo" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_.-]+/-/g')" | |
| target_checkout_dir="$target_name" | |
| if [ "$target_repo" = "${{ github.repository }}" ]; then | |
| target_checkout_dir="${target_name}-target" | |
| fi | |
| { | |
| echo "target_repo=$target_repo" | |
| echo "target_repo_owner=$target_owner" | |
| echo "target_repo_name=$target_name" | |
| echo "target_slug=$target_slug" | |
| echo "target_branch=$target_branch" | |
| echo "target_checkout_dir=$target_checkout_dir" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Create target read token | |
| id: target-read-token | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| owner: ${{ steps.target.outputs.target_repo_owner }} | |
| repositories: ${{ steps.target.outputs.target_repo_name }} | |
| permission-contents: read | |
| permission-issues: read | |
| permission-pull-requests: read | |
| - name: Create state token | |
| id: state-token | |
| uses: ./clawsweeper/.github/actions/create-state-token | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| - uses: ./clawsweeper/.github/actions/setup-state | |
| with: | |
| coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| records-repo-slugs: ${{ steps.target.outputs.target_slug }} | |
| hydrate-state-blobs: "false" | |
| token: ${{ steps.state-token.outputs.token }} | |
| worktree-path: clawsweeper | |
| fetch-depth: 1 | |
| - uses: ./clawsweeper/.github/actions/setup-pnpm | |
| with: | |
| working-directory: clawsweeper | |
| build-script: build:all | |
| - name: Publish planning-started status | |
| if: ${{ !((github.event_name == 'workflow_dispatch' && github.event.inputs.apply_existing == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '3 * * * *' || github.event.schedule == '18 * * * *' || github.event.schedule == '33 * * * *' || github.event.schedule == '48 * * * *' || github.event.schedule == '6,21,36,51 * * * *'))) && !((github.event_name == 'workflow_dispatch' && github.event.inputs.hot_intake == 'true' && github.event.inputs.item_number == '' && github.event.inputs.item_numbers == '') || (github.event_name == 'schedule' && (github.event.schedule == '*/5 * * * *' || github.event.schedule == '2/5 * * * *'))) }} | |
| continue-on-error: true | |
| working-directory: clawsweeper | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| run: | | |
| target_slug="${{ steps.target.outputs.target_repo }}" | |
| target_slug="${target_slug//\//-}" | |
| pnpm run status -- \ | |
| --target-repo "${{ steps.target.outputs.target_repo }}" \ | |
| --state "Planning review" \ | |
| --detail "Planner is scanning GitHub for the next review candidates. Candidate counts and shard details will be posted after planning completes." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| timeout 20s pnpm run repair:publish-main -- \ | |
| --message "chore: mark sweep planning started" \ | |
| --path "results/sweep-status/${target_slug}.json" \ | |
| --rebase-strategy theirs || echo "::warning::Skipped slow planning-started dashboard publish so candidate selection can start." | |
| - id: mode | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_QUEUE_PRESSURE_SOFT_PENDING: ${{ vars.CLAWSWEEPER_QUEUE_PRESSURE_SOFT_PENDING || '' }} | |
| CLAWSWEEPER_QUEUE_PRESSURE_HARD_PENDING: ${{ vars.CLAWSWEEPER_QUEUE_PRESSURE_HARD_PENDING || '' }} | |
| CLAWSWEEPER_QUEUE_PRESSURE_SOFT_AGE_MS: ${{ vars.CLAWSWEEPER_QUEUE_PRESSURE_SOFT_AGE_MS || '' }} | |
| CLAWSWEEPER_QUEUE_PRESSURE_HARD_AGE_MS: ${{ vars.CLAWSWEEPER_QUEUE_PRESSURE_HARD_AGE_MS || '' }} | |
| run: | | |
| limit() { | |
| pnpm --dir clawsweeper run --silent workflow -- limit "$1" | |
| } | |
| worker_limit() { | |
| pnpm --dir clawsweeper run --silent workflow -- worker-limit "$@" | |
| } | |
| active_runs_json() { | |
| { | |
| for run_status in in_progress pending queued waiting requested; do | |
| gh api "repos/${{ github.repository }}/actions/runs?per_page=100&status=${run_status}" \ | |
| --paginate \ | |
| --jq '.workflow_runs[] | {databaseId:.id, workflowPath:.path, displayTitle:.display_title, event:.event, status:.status, createdAt:.created_at, updatedAt:.updated_at}' 2>/dev/null \ | |
| || true | |
| done | |
| } | jq -s 'unique_by(.databaseId)' | |
| } | |
| ACTIVE_RUNS_JSON="$(active_runs_json)" | |
| STALE_QUEUED_CUTOFF="$(date -u -d '6 hours ago' '+%Y-%m-%dT%H:%M:%SZ')" | |
| active_run_count() { | |
| printf '%s' "$ACTIVE_RUNS_JSON" \ | |
| | WORKFLOW_PATH="$1" STALE_QUEUED_CUTOFF="$STALE_QUEUED_CUTOFF" jq '[.[] | select(.workflowPath == env.WORKFLOW_PATH) | select(.status == "in_progress" or ((.status == "pending" or .status == "queued" or .status == "waiting" or .status == "requested") and ((.updatedAt // .createdAt // "") >= env.STALE_QUEUED_CUTOFF)))] | length' 2>/dev/null \ | |
| || printf '0' | |
| } | |
| active_sweep_exact_workers() { | |
| local runs total id title status run_event jobs_json active_shards total_shards | |
| local reserved_shards hard_cap requested_shards item_numbers commas item_count | |
| total=0 | |
| hard_cap="$(limit review_shards.hard_cap)" | |
| runs="$(printf '%s' "$ACTIVE_RUNS_JSON" \ | |
| | STALE_QUEUED_CUTOFF="$STALE_QUEUED_CUTOFF" jq -r '.[] | select(.workflowPath == ".github/workflows/sweep.yml") | select(.status == "in_progress" or ((.status == "pending" or .status == "queued" or .status == "waiting" or .status == "requested") and ((.updatedAt // .createdAt // "") >= env.STALE_QUEUED_CUTOFF))) | select((.displayTitle | startswith("Review exact item ")) or (.displayTitle | startswith("Review scheduled hot item ")) or (.displayTitle | startswith("Review scheduled normal item ")) or (.event == "workflow_dispatch" and ((.displayTitle | startswith("Review event item ")) or (.displayTitle | startswith("Review event items "))))) | [.databaseId, .displayTitle, .status, .event] | @tsv' 2>/dev/null || true)" | |
| while IFS=$'\t' read -r id title status run_event; do | |
| if [ -z "$id" ]; then | |
| continue | |
| fi | |
| active_shards=0 | |
| total_shards=0 | |
| if [[ "$title" == Review\ exact\ item\ * ]] || [[ "$title" == Review\ scheduled\ hot\ item\ * ]] || [[ "$title" == Review\ scheduled\ normal\ item\ * ]] || { [ "$run_event" = "workflow_dispatch" ] && [[ "$title" == Review\ event\ item\ * ]]; }; then | |
| active_shards=1 | |
| total=$((total + active_shards)) | |
| continue | |
| fi | |
| if [ "$status" = "in_progress" ]; then | |
| jobs_json="$(gh run view "$id" --repo "${{ github.repository }}" --json jobs 2>/dev/null || printf '{"jobs":[]}')" | |
| active_shards="$(printf '%s' "$jobs_json" \ | |
| | jq '[.jobs[]? | select(.name | startswith("Review shard ")) | select(.status == "in_progress" or .status == "pending" or .status == "queued" or .status == "waiting" or .status == "requested")] | length' 2>/dev/null \ | |
| || printf '0')" | |
| total_shards="$(printf '%s' "$jobs_json" \ | |
| | jq '[.jobs[]? | select(.name | startswith("Review shard "))] | length' 2>/dev/null \ | |
| || printf '0')" | |
| fi | |
| if ! [[ "$active_shards" =~ ^[0-9]+$ ]]; then | |
| active_shards=0 | |
| fi | |
| if ! [[ "$total_shards" =~ ^[0-9]+$ ]]; then | |
| total_shards=0 | |
| fi | |
| # Single-item event reviews reserve one worker. Explicit batches carry | |
| # enough run-name metadata to reserve their maximum possible matrix until | |
| # live shard jobs exist; after expansion, only live shard jobs consume slots. | |
| if [ "$active_shards" -lt 1 ] && [ "$total_shards" -lt 1 ]; then | |
| reserved_shards="$(limit review_shards.exact_item_default)" | |
| if [[ "$title" == Review\ event\ items\ * ]]; then | |
| reserved_shards="$hard_cap" | |
| if [[ "$title" =~ \[shards=([0-9]+)\]$ ]]; then | |
| requested_shards="${BASH_REMATCH[1]}" | |
| item_numbers="${title#*#}" | |
| item_numbers="${item_numbers%% *}" | |
| if [[ "$item_numbers" =~ ^[0-9]+(,[0-9]+)*$ ]]; then | |
| if [ "$requested_shards" -lt 1 ]; then | |
| requested_shards=1 | |
| fi | |
| commas="${item_numbers//[^,]/}" | |
| item_count=$((${#commas} + 1)) | |
| reserved_shards="$requested_shards" | |
| if [ "$reserved_shards" -gt "$item_count" ]; then | |
| reserved_shards="$item_count" | |
| fi | |
| if [ "$reserved_shards" -gt "$hard_cap" ]; then | |
| reserved_shards="$hard_cap" | |
| fi | |
| fi | |
| fi | |
| fi | |
| active_shards="$reserved_shards" | |
| fi | |
| total=$((total + active_shards)) | |
| done <<< "$runs" | |
| printf '%s' "$total" | |
| } | |
| active_sweep_background_workers() { | |
| local runs total id title status jobs_json active_shards total_shards | |
| total=0 | |
| runs="$(printf '%s' "$ACTIVE_RUNS_JSON" \ | |
| | CURRENT_RUN_ID="${GITHUB_RUN_ID:-0}" STALE_QUEUED_CUTOFF="$STALE_QUEUED_CUTOFF" jq -r '.[] | select((.databaseId | tostring) != env.CURRENT_RUN_ID) | select(.workflowPath == ".github/workflows/sweep.yml") | select(.status == "in_progress" or ((.status == "pending" or .status == "queued" or .status == "waiting" or .status == "requested") and ((.updatedAt // .createdAt // "") >= env.STALE_QUEUED_CUTOFF))) | select(.displayTitle == "Review ClawSweeper items" or .displayTitle == "Review hot ClawSweeper items" or (.displayTitle | startswith("Review target repo ")) or (.displayTitle | startswith("Review hot target repo "))) | [.databaseId, .displayTitle, .status] | @tsv' 2>/dev/null || true)" | |
| while IFS=$'\t' read -r id title status; do | |
| if [ -z "$id" ]; then | |
| continue | |
| fi | |
| active_shards=0 | |
| total_shards=0 | |
| if [ "$status" = "in_progress" ]; then | |
| jobs_json="$(gh run view "$id" --repo "${{ github.repository }}" --json jobs 2>/dev/null || printf '{"jobs":[]}')" | |
| active_shards="$(printf '%s' "$jobs_json" \ | |
| | jq '[.jobs[]? | select(.name | startswith("Review shard ")) | select(.status == "in_progress" or .status == "pending" or .status == "queued" or .status == "waiting" or .status == "requested")] | length' 2>/dev/null \ | |
| || printf '0')" | |
| total_shards="$(printf '%s' "$jobs_json" \ | |
| | jq '[.jobs[]? | select(.name | startswith("Review shard "))] | length' 2>/dev/null \ | |
| || printf '0')" | |
| fi | |
| if ! [[ "$active_shards" =~ ^[0-9]+$ ]]; then | |
| active_shards=0 | |
| fi | |
| if ! [[ "$total_shards" =~ ^[0-9]+$ ]]; then | |
| total_shards=0 | |
| fi | |
| # Planning, queued, and not-yet-expanded matrix runs reserve their quiet | |
| # lane size. Runs with completed shard jobs are publishing and consume no | |
| # Codex slots, so the next serialized planner may refill that capacity. | |
| if [ "$active_shards" -lt 1 ] && [ "$total_shards" -lt 1 ]; then | |
| if [ "$title" = "Review hot ClawSweeper items" ] || [[ "$title" == Review\ hot\ target\ repo\ * ]]; then | |
| active_shards="$(limit review_shards.hot_intake_default)" | |
| else | |
| active_shards="$(limit review_shards.normal_default)" | |
| fi | |
| fi | |
| total=$((total + active_shards)) | |
| done <<< "$runs" | |
| printf '%s' "$total" | |
| } | |
| exact_item_shards="$(limit review_shards.exact_item_default)" | |
| normal_active_floor="$(limit review_shards.normal_active_floor)" | |
| hard_shard_cap="$(limit review_shards.hard_cap)" | |
| active_critical_workers="$(( $(active_run_count ".github/workflows/repair-cluster-worker.yml") + $(active_sweep_exact_workers) ))" | |
| active_background_workers="$(active_sweep_background_workers)" | |
| exact_item="${{ github.event.client_payload.item_number || github.event.inputs.item_number || github.event.inputs.item_numbers || '' }}" | |
| pressure_level="unknown" | |
| queue_candidate_capacity="50" | |
| hot_intake_unpressured="$(worker_limit hot_intake --active-critical "$active_critical_workers" --active-background "$active_background_workers")" | |
| normal_unpressured="$(worker_limit normal_review --active-critical "$active_critical_workers" --active-background "$active_background_workers")" | |
| if [ -z "$exact_item" ]; then | |
| if ! pressure_json="$(pnpm --dir clawsweeper run --silent workflow -- queue-pressure --queue-url "$QUEUE_URL")"; then | |
| pressure_json='{"ok":false,"reason":"probe_failed","level":"unknown"}' | |
| fi | |
| if ! pressure_level="$(printf '%s' "$pressure_json" | jq -er '.level | select(. == "none" or . == "soft" or . == "hard" or . == "unknown")' 2>/dev/null)"; then | |
| pressure_level="unknown" | |
| fi | |
| if ! queue_candidate_capacity="$(printf '%s' "$pressure_json" | jq -er '.availableCandidateCapacity | select(type == "number" and floor == . and . >= 0)' 2>/dev/null)"; then | |
| queue_candidate_capacity="50" | |
| fi | |
| if [ "$queue_candidate_capacity" -lt 1 ]; then | |
| queue_candidate_capacity="1" | |
| fi | |
| fi | |
| hot_intake_shards="$(worker_limit hot_intake --active-critical "$active_critical_workers" --active-background "$active_background_workers" --pressure-level "$pressure_level")" | |
| normal_shards="$(worker_limit normal_review --active-critical "$active_critical_workers" --active-background "$active_background_workers" --pressure-level "$pressure_level")" | |
| if [ -z "$exact_item" ]; then | |
| pressure_numbers="$(printf '%s' "$pressure_json" | jq -r 'if .ok then "pending=\(.pendingCount), active=\(.activeCount // "unknown")/\(.capacity // "unknown"), candidate-capacity=\(.availableCandidateCapacity // "unknown"), oldest=\((.oldestPendingAgeMs / 3600000 * 10 | round) / 10)h" else "unavailable=\(.reason)" end' 2>/dev/null || printf 'unavailable=probe_failed')" | |
| echo "queue pressure: $pressure_level ($pressure_numbers) — hot_intake $hot_intake_unpressured->$hot_intake_shards, normal_review $normal_unpressured->$normal_shards" | |
| fi | |
| hot_intake="${{ ((github.event_name == 'repository_dispatch' && github.event.client_payload.hot_intake == 'true') || (github.event_name == 'workflow_dispatch' && github.event.inputs.hot_intake == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '*/5 * * * *' || github.event.schedule == '2/5 * * * *'))) && 'true' || 'false' }}" | |
| queue_feed="${{ ((github.event_name == 'repository_dispatch' && github.event.action == 'clawsweeper_target_sweep') || github.event_name == 'schedule') && 'true' || 'false' }}" | |
| target_repo="${{ steps.target.outputs.target_repo }}" | |
| if [ "$hot_intake" = "true" ] && [ -n "$exact_item" ]; then | |
| batch_size="1" | |
| shard_count="$exact_item_shards" | |
| max_pages="1" | |
| elif [ "$hot_intake" = "true" ]; then | |
| batch_size="1" | |
| shard_count="${{ github.event.client_payload.shard_count || '' }}" | |
| if [ -z "$shard_count" ]; then | |
| shard_count="$hot_intake_shards" | |
| fi | |
| max_pages="10" | |
| min_active_shards="0" | |
| min_backfill_review_age_minutes="360" | |
| else | |
| if [ "${{ github.event_name }}" = "schedule" ]; then | |
| batch_size="1" | |
| else | |
| batch_size="${{ github.event.client_payload.batch_size || github.event.inputs.batch_size || '3' }}" | |
| fi | |
| if [ "$target_repo" = "openclaw/openclaw" ]; then | |
| min_active_shards="$normal_active_floor" | |
| else | |
| min_active_shards="0" | |
| fi | |
| shard_count="${{ github.event.client_payload.shard_count || github.event.inputs.shard_count || '' }}" | |
| if [ -z "$shard_count" ]; then | |
| shard_count="$normal_shards" | |
| fi | |
| max_pages="250" | |
| min_backfill_review_age_minutes="360" | |
| fi | |
| if [ "$hot_intake" = "true" ] && [ -n "$exact_item" ]; then | |
| min_active_shards="0" | |
| min_backfill_review_age_minutes="360" | |
| fi | |
| if [ "$queue_feed" = "true" ] && [ -z "$exact_item" ]; then | |
| # The Durable Object turns every selected item into an independent exact-review | |
| # workflow. Direct schedules use current free queue capacity; target fanout may | |
| # request a smaller backlog-weighted share. The Worker remains the final owner of | |
| # the fleet-wide 300/hour admission target and queue backpressure. | |
| requested_batch_size="${{ github.event.client_payload.batch_size || '' }}" | |
| if ! [[ "$requested_batch_size" =~ ^[0-9]+$ ]] || [ "$requested_batch_size" -lt 1 ]; then | |
| requested_batch_size="$queue_candidate_capacity" | |
| fi | |
| if [ "$requested_batch_size" -gt "$queue_candidate_capacity" ]; then | |
| requested_batch_size="$queue_candidate_capacity" | |
| fi | |
| if [ "$requested_batch_size" -gt "$hard_shard_cap" ]; then | |
| requested_batch_size="$hard_shard_cap" | |
| fi | |
| batch_size="$requested_batch_size" | |
| shard_count="1" | |
| min_active_shards="0" | |
| fi | |
| if ! [[ "$shard_count" =~ ^[0-9]+$ ]]; then | |
| shard_count="$normal_shards" | |
| fi | |
| if [ "$shard_count" -gt "$hard_shard_cap" ]; then | |
| shard_count="$hard_shard_cap" | |
| fi | |
| if [ -z "$exact_item" ]; then | |
| lane_shard_cap="$normal_shards" | |
| if [ "$hot_intake" = "true" ]; then | |
| lane_shard_cap="$hot_intake_shards" | |
| fi | |
| if ! [[ "$lane_shard_cap" =~ ^[0-9]+$ ]] || [ "$lane_shard_cap" -lt 1 ]; then | |
| lane_shard_cap="1" | |
| fi | |
| if [ "$shard_count" -gt "$lane_shard_cap" ]; then | |
| echo "::notice::Capping broad background review shards from $shard_count to scheduler allowance $lane_shard_cap." | |
| shard_count="$lane_shard_cap" | |
| fi | |
| fi | |
| { | |
| echo "batch_size=$batch_size" | |
| echo "codex_timeout_ms=${{ github.event.client_payload.review_options.codex_timeout_ms || github.event.client_payload.codex_timeout_ms || github.event.inputs.codex_timeout_ms || vars.CLAWSWEEPER_CODEX_TIMEOUT_MS || '1200000' }}" | |
| echo "hot_intake=$hot_intake" | |
| echo "queue_feed=$queue_feed" | |
| echo "max_pages=$max_pages" | |
| echo "min_active_shards=$min_active_shards" | |
| echo "min_backfill_review_age_minutes=$min_backfill_review_age_minutes" | |
| echo "shard_count=$shard_count" | |
| } >> "$GITHUB_OUTPUT" | |
| - id: select | |
| working-directory: clawsweeper | |
| env: | |
| SHARD_COUNT: ${{ steps.mode.outputs.shard_count }} | |
| BATCH_SIZE: ${{ steps.mode.outputs.batch_size }} | |
| HOT_INTAKE: ${{ steps.mode.outputs.hot_intake }} | |
| ITEM_NUMBER: ${{ github.event.inputs.item_number || '' }} | |
| # A router fallback carries its receipt key in item_numbers because GitHub caps this | |
| # workflow at 25 inputs; item_number remains the exact review target in that case. | |
| ITEM_NUMBERS: ${{ (github.event.inputs.item_number != '' && startsWith(github.event.inputs.item_numbers, 'router-') && github.event.inputs.item_number) || github.event.inputs.item_numbers || github.event.client_payload.item_number || '' }} | |
| MAX_PAGES: ${{ steps.mode.outputs.max_pages }} | |
| MIN_ACTIVE_SHARDS: ${{ steps.mode.outputs.min_active_shards }} | |
| MIN_BACKFILL_REVIEW_AGE_MINUTES: ${{ steps.mode.outputs.min_backfill_review_age_minutes }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| GH_TOKEN: ${{ steps.target-read-token.outputs.token || github.token }} | |
| run: | | |
| set -euo pipefail | |
| item_arg=() | |
| if [ -n "$ITEM_NUMBER" ]; then | |
| item_arg=(--item-number "$ITEM_NUMBER") | |
| fi | |
| if [ -n "$ITEM_NUMBERS" ]; then | |
| item_arg+=("--item-numbers" "$ITEM_NUMBERS") | |
| fi | |
| hot_intake_arg=() | |
| if [ "$HOT_INTAKE" = "true" ]; then | |
| hot_intake_arg=(--hot-intake) | |
| fi | |
| pnpm run --silent plan -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --batch-size "$BATCH_SIZE" \ | |
| --max-pages "$MAX_PAGES" \ | |
| --shard-count "$SHARD_COUNT" \ | |
| --codex-model internal \ | |
| --codex-reasoning-effort high \ | |
| --codex-sandbox read-only \ | |
| --min-active-shards "$MIN_ACTIVE_SHARDS" \ | |
| --min-backfill-review-age-minutes "$MIN_BACKFILL_REVIEW_AGE_MINUTES" \ | |
| --coverage-tracked-items-manifest .artifacts/worker-records-manifest.json \ | |
| "${hot_intake_arg[@]}" \ | |
| "${item_arg[@]}" > plan.json | |
| pnpm run --silent workflow -- plan-output \ | |
| --plan plan.json \ | |
| --batch-size "$BATCH_SIZE" \ | |
| --shard-count "$SHARD_COUNT" >> "$GITHUB_OUTPUT" | |
| cat plan.json | |
| - name: Enqueue scheduled review candidates | |
| id: enqueue-scheduled | |
| if: ${{ steps.mode.outputs.queue_feed == 'true' && steps.select.outputs.planned_count != '0' }} | |
| working-directory: clawsweeper | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| GH_TOKEN: ${{ steps.target-read-token.outputs.token || github.token }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| run: | | |
| set -euo pipefail | |
| lane="normal_backfill" | |
| if [ "${{ steps.mode.outputs.hot_intake }}" = "true" ]; then | |
| lane="hot_intake" | |
| fi | |
| target_repo="${{ steps.target.outputs.target_repo }}" | |
| target_branch="$(gh api "repos/$target_repo" --jq '.default_branch // empty')" | |
| if [ -z "$target_branch" ] || | |
| ! printf '%s' "$target_branch" | grep -Eq '^[A-Za-z0-9_.\/-]+$' || | |
| printf '%s' "$target_branch" | grep -Eq '^[0-9]+$' || | |
| [[ "$target_branch" == *".."* ]]; then | |
| echo "Invalid default branch for scheduled target $target_repo: $target_branch" >&2 | |
| exit 1 | |
| fi | |
| echo "::notice::Scheduled review payloads will use default branch '$target_branch' for $target_repo." | |
| result="$(pnpm run --silent repair:scheduled-review-enqueue -- \ | |
| --plan plan.json \ | |
| --lane "$lane" \ | |
| --target-repo "$target_repo" \ | |
| --target-branch "$target_branch" \ | |
| --queue-url "$QUEUE_URL" \ | |
| --delivery-prefix "scheduled:${{ github.run_id }}:${{ github.run_attempt }}")" | |
| jq -e '.offered >= .attempted and .attempted >= (.queued + .deduped + .shed + .rejected)' <<<"$result" >/dev/null | |
| { | |
| echo "### Scheduled review funnel" | |
| echo | |
| echo "| Selected | Attempted | Enqueued | Deduped | Shed | Deferred |" | |
| echo "| ---: | ---: | ---: | ---: | ---: | ---: |" | |
| echo "| $(jq -r .offered <<<"$result") | $(jq -r .attempted <<<"$result") | $(jq -r .queued <<<"$result") | $(jq -r .deduped <<<"$result") | $(jq -r .shed <<<"$result") | $(jq -r .deferred <<<"$result") |" | |
| echo | |
| echo "Selected candidate age (hours): p50 $(jq -r '.ageHours.p50 // "n/a"' <<<"$result"), p90 $(jq -r '.ageHours.p90 // "n/a"' <<<"$result"), oldest $(jq -r '.ageHours.max // "n/a"' <<<"$result")." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Prepare review runtime artifact | |
| if: ${{ steps.mode.outputs.queue_feed != 'true' }} | |
| working-directory: clawsweeper | |
| env: | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| target_slug="${TARGET_REPO//\//-}" | |
| node scripts/prepare-review-runtime.mjs \ | |
| --output .artifacts/review-runtime \ | |
| --plan plan.json \ | |
| --state-root . \ | |
| --records-path "records/${target_slug}/items" | |
| tar -czf .artifacts/review-runtime.tar.gz -C .artifacts/review-runtime . | |
| - uses: actions/upload-artifact@v7 | |
| if: ${{ steps.mode.outputs.queue_feed != 'true' }} | |
| with: | |
| name: clawsweeper-runtime-dist | |
| path: clawsweeper/.artifacts/review-runtime.tar.gz | |
| include-hidden-files: true | |
| if-no-files-found: error | |
| retention-days: 1 | |
| - name: Publish planning status | |
| if: ${{ steps.mode.outputs.queue_feed != 'true' && (github.event_name != 'workflow_dispatch' || github.event.inputs.apply_existing != 'true') && (steps.mode.outputs.hot_intake != 'true' || github.event.inputs.item_number != '' || github.event.inputs.item_numbers != '') }} | |
| continue-on-error: true | |
| working-directory: clawsweeper | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| GH_TOKEN: ${{ github.token }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| target_slug="$TARGET_REPO" | |
| target_slug="${target_slug//\//-}" | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "${{ steps.mode.outputs.hot_intake == 'true' && 'Hot intake in progress' || 'Review in progress' }}" \ | |
| --detail "${{ steps.mode.outputs.hot_intake == 'true' && 'Hot intake planned' || 'Planned' }} ${{ steps.select.outputs.planned_count }} items across ${{ steps.select.outputs.planned_shards }} shards. Capacity is ${{ steps.select.outputs.planned_capacity }} items; due backlog scanned is ${{ steps.select.outputs.due_backlog }}. Capacity reason: ${{ steps.select.outputs.capacity_reason }}. Review shards are starting; publish will merge artifacts when they finish." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ | |
| --planned-count "${{ steps.select.outputs.planned_count }}" \ | |
| --planned-capacity "${{ steps.select.outputs.planned_capacity }}" \ | |
| --planned-shards "${{ steps.select.outputs.planned_shards }}" \ | |
| --active-codex "${{ steps.select.outputs.active_codex_target }}" \ | |
| --due-backlog "${{ steps.select.outputs.due_backlog }}" \ | |
| --oldest-unreviewed-at "${{ steps.select.outputs.oldest_unreviewed_at }}" \ | |
| --capacity-reason "${{ steps.select.outputs.capacity_reason }}" | |
| timeout 20s pnpm run repair:publish-main -- \ | |
| --message "chore: mark sweep review in progress" \ | |
| --path "results/sweep-status/${target_slug}.json" \ | |
| --rebase-strategy theirs || echo "::warning::Skipped slow in-progress dashboard publish so review shards can start." | |
| review: | |
| name: Review shard ${{ matrix.shard }} · ${{ needs.plan.outputs.target_repo }}#${{ matrix.item_numbers }} | |
| needs: plan | |
| if: ${{ needs.plan.outputs.queue_feed != 'true' }} | |
| runs-on: ${{ vars.CLAWSWEEPER_REVIEW_RUNNER || 'ubuntu-latest' }} | |
| timeout-minutes: 75 | |
| continue-on-error: true | |
| permissions: | |
| actions: read | |
| checks: read | |
| contents: read | |
| issues: read | |
| pull-requests: read | |
| statuses: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJson(needs.plan.outputs.matrix) }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| path: clawsweeper | |
| filter: blob:none | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - uses: ./clawsweeper/.github/actions/setup-action-ledger | |
| continue-on-error: true | |
| with: | |
| worktree-path: clawsweeper | |
| - name: Create target review token | |
| id: target-read-token | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| owner: ${{ needs.plan.outputs.target_repo_owner }} | |
| repositories: ${{ needs.plan.outputs.target_repo_name }} | |
| permission-checks: read | |
| permission-contents: read | |
| permission-issues: write | |
| permission-pull-requests: write | |
| permission-statuses: read | |
| - name: Create target Codex inspection token | |
| id: codex-inspection-token | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| owner: ${{ needs.plan.outputs.target_repo_owner }} | |
| repositories: ${{ needs.plan.outputs.target_repo_name }} | |
| permission-contents: read | |
| permission-issues: read | |
| permission-pull-requests: read | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: clawsweeper-runtime-dist | |
| path: clawsweeper/.artifacts | |
| - name: Extract review runtime artifact | |
| working-directory: clawsweeper | |
| run: | | |
| tar -xzf .artifacts/review-runtime.tar.gz | |
| - name: Install review compiler service | |
| continue-on-error: true | |
| working-directory: clawsweeper | |
| run: node scripts/install-review-native-compiler.mjs | |
| - uses: ./clawsweeper/.github/actions/setup-codex | |
| if: ${{ env.CLAWSWEEPER_RUNNER != 'openclaw' }} | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| CLAWSWEEPER_INTERNAL_MODEL: ${{ secrets.CLAWSWEEPER_MODEL }} | |
| with: | |
| login-status: "true" | |
| - uses: ./clawsweeper/.github/actions/setup-openclaw | |
| - uses: actions/cache@v6 | |
| with: | |
| path: ${{ needs.plan.outputs.target_checkout_dir }}-cache.git | |
| key: ${{ needs.plan.outputs.target_repo_name }}-git-${{ runner.os }}-${{ github.run_id }} | |
| restore-keys: | | |
| ${{ needs.plan.outputs.target_repo_name }}-git-${{ runner.os }}- | |
| - name: Check out target repository | |
| run: | | |
| set -euo pipefail | |
| url="https://github.com/${{ needs.plan.outputs.target_repo }}.git" | |
| cache_dir="${{ needs.plan.outputs.target_checkout_dir }}-cache.git" | |
| checkout_dir="${{ needs.plan.outputs.target_checkout_dir }}" | |
| target_branch="${{ needs.plan.outputs.target_branch }}" | |
| if [ -d "$cache_dir" ]; then | |
| git -C "$cache_dir" remote set-url origin "$url" | |
| git -C "$cache_dir" config remote.origin.promisor true | |
| git -C "$cache_dir" config remote.origin.partialclonefilter blob:none | |
| if [ -f "$cache_dir/shallow" ]; then | |
| cache_fetch=(git -C "$cache_dir" fetch --prune --unshallow --filter=blob:none origin "$target_branch") | |
| else | |
| cache_fetch=(git -C "$cache_dir" fetch --prune --filter=blob:none origin "$target_branch") | |
| fi | |
| if ! "${cache_fetch[@]}"; then | |
| echo "::warning::Cached target repository fetch failed; rebuilding cache." | |
| rm -rf "$cache_dir" | |
| git clone --bare --filter=blob:none --single-branch --branch "$target_branch" "$url" "$cache_dir" | |
| fi | |
| else | |
| git clone --bare --filter=blob:none --single-branch --branch "$target_branch" "$url" "$cache_dir" | |
| fi | |
| if ! git clone --reference-if-able "$cache_dir" --dissociate --filter=blob:none --branch "$target_branch" --single-branch "$url" "$checkout_dir"; then | |
| echo "::warning::Cached target checkout failed; retrying without cache reference." | |
| rm -rf "$checkout_dir" "$cache_dir" | |
| git clone --bare --filter=blob:none --single-branch --branch "$target_branch" "$url" "$cache_dir" | |
| git clone --filter=blob:none --branch "$target_branch" --single-branch "$url" "$checkout_dir" | |
| fi | |
| git -C "$checkout_dir" rev-parse --short HEAD | |
| - name: Mark shard start | |
| id: shard-start | |
| run: echo "started_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT" | |
| - name: Review shard | |
| id: review-shard | |
| continue-on-error: true | |
| working-directory: clawsweeper | |
| env: | |
| GH_TOKEN: ${{ steps.target-read-token.outputs.token || github.token }} | |
| CLAWSWEEPER_PUBLIC_GH_TOKEN: ${{ needs.plan.outputs.target_repo == 'openclaw/openclaw' && github.token || '' }} | |
| CLAWSWEEPER_PROOF_INSPECTION_TOKEN: ${{ steps.codex-inspection-token.outputs.token }} | |
| ADDITIONAL_PROMPT: ${{ github.event.inputs.additional_prompt || github.event.client_payload.review_options.additional_prompt || github.event.client_payload.additional_prompt || '' }} | |
| EXACT_ITEM: ${{ github.event.client_payload.item_number || github.event.inputs.item_number || github.event.inputs.item_numbers || '' }} | |
| EXPECTED_SOURCE_REVISION: ${{ github.event.client_payload.expected_source_revision || '' }} | |
| PR_COMMENT_ACTIVITY_REVISIONS: ${{ matrix.pr_comment_activity_revisions }} | |
| run: | | |
| hot_intake_arg=() | |
| if [ "${{ needs.plan.outputs.hot_intake }}" = "true" ]; then | |
| hot_intake_arg=(--hot-intake) | |
| fi | |
| additional_prompt_arg=() | |
| if [ -n "$ADDITIONAL_PROMPT" ]; then | |
| additional_prompt_arg=(--additional-prompt "$ADDITIONAL_PROMPT") | |
| fi | |
| expected_source_revision_arg=() | |
| if [ -n "$EXPECTED_SOURCE_REVISION" ]; then | |
| expected_source_revision_arg=(--expected-source-revision "$EXPECTED_SOURCE_REVISION") | |
| fi | |
| planned_automatic_review_arg=() | |
| if [ -z "$EXACT_ITEM" ]; then | |
| planned_automatic_review_arg=(--planned-automatic-review) | |
| fi | |
| pr_comment_activity_arg=() | |
| if [ -n "$PR_COMMENT_ACTIVITY_REVISIONS" ]; then | |
| pr_comment_activity_arg=(--pr-comment-activity-revisions "$PR_COMMENT_ACTIVITY_REVISIONS") | |
| fi | |
| codex_timeout_seconds=$(((${{ needs.plan.outputs.codex_timeout_ms }} + 999) / 1000)) | |
| review_timeout_seconds=$(((codex_timeout_seconds + 180) * ${{ needs.plan.outputs.batch_size }})) | |
| if [ "$review_timeout_seconds" -lt 300 ]; then | |
| review_timeout_seconds=300 | |
| fi | |
| if [ "$review_timeout_seconds" -gt 4200 ]; then | |
| review_timeout_seconds=4200 | |
| fi | |
| echo "::notice::Review shard timeout is ${review_timeout_seconds}s for batch size ${{ needs.plan.outputs.batch_size }} and per-item Codex timeout ${codex_timeout_seconds}s." | |
| set +e | |
| timeout --kill-after=30s "${review_timeout_seconds}s" node dist/clawsweeper.js review \ | |
| --target-repo "${{ needs.plan.outputs.target_repo }}" \ | |
| --target-dir "../${{ needs.plan.outputs.target_checkout_dir }}" \ | |
| --artifact-dir ../review-artifacts/shard-${{ matrix.shard }} \ | |
| --batch-size ${{ needs.plan.outputs.batch_size }} \ | |
| --max-pages ${{ needs.plan.outputs.max_pages }} \ | |
| --codex-model internal \ | |
| --codex-reasoning-effort high \ | |
| --codex-sandbox read-only \ | |
| --codex-timeout-ms ${{ needs.plan.outputs.codex_timeout_ms }} \ | |
| --item-numbers "${{ matrix.item_numbers }}" \ | |
| "${pr_comment_activity_arg[@]}" \ | |
| "${planned_automatic_review_arg[@]}" \ | |
| "${hot_intake_arg[@]}" \ | |
| --readonly-openclaw \ | |
| --shard-index ${{ matrix.shard }} \ | |
| --shard-count ${{ needs.plan.outputs.planned_shards }} \ | |
| "${additional_prompt_arg[@]}" \ | |
| "${expected_source_revision_arg[@]}" | |
| review_exit_code=$? | |
| set -e | |
| echo "exit_code=$review_exit_code" >> "$GITHUB_OUTPUT" | |
| exit "$review_exit_code" | |
| - name: Finalize review action ledger | |
| if: always() | |
| continue-on-error: true | |
| working-directory: clawsweeper | |
| env: | |
| REVIEW_EXIT_CODE: ${{ steps.review-shard.outputs.exit_code || '' }} | |
| REVIEW_OUTCOME: ${{ steps.review-shard.outcome || 'not_started' }} | |
| run: | | |
| args=() | |
| if [ "$REVIEW_OUTCOME" = "cancelled" ] || [ "$REVIEW_EXIT_CODE" = "130" ] || [ "$REVIEW_EXIT_CODE" = "143" ]; then | |
| args=(--interrupt-open-attempts --reason cancelled) | |
| elif [ "$REVIEW_EXIT_CODE" = "124" ] || [ "$REVIEW_EXIT_CODE" = "137" ]; then | |
| args=(--interrupt-open-attempts --reason timeout) | |
| elif [ "$REVIEW_OUTCOME" != "success" ]; then | |
| args=(--interrupt-open-attempts --reason workflow_failed) | |
| fi | |
| node dist/clawsweeper.js finalize-action-events "${args[@]}" | |
| - name: Record shard metrics | |
| if: always() | |
| env: | |
| ITEM_NUMBERS: ${{ matrix.item_numbers }} | |
| REVIEW_OUTCOME: ${{ steps.review-shard.outcome || 'not_started' }} | |
| STARTED_AT: ${{ steps.shard-start.outputs.started_at || '' }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p review-artifacts/metrics | |
| jq -n \ | |
| --arg shard "${{ matrix.shard }}" \ | |
| --arg item_numbers "$ITEM_NUMBERS" \ | |
| --arg review_outcome "$REVIEW_OUTCOME" \ | |
| --arg started_at "$STARTED_AT" \ | |
| --arg completed_at "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \ | |
| --arg target_repo "$TARGET_REPO" \ | |
| '{ | |
| shard: ($shard | tonumber), | |
| item_numbers: $item_numbers, | |
| review_outcome: $review_outcome, | |
| started_at: $started_at, | |
| completed_at: $completed_at, | |
| target_repo: $target_repo | |
| }' > review-artifacts/metrics/shard-${{ matrix.shard }}.json | |
| - name: Record failed review shard | |
| if: ${{ failure() || steps.review-shard.outcome == 'failure' }} | |
| run: | | |
| mkdir -p review-artifacts/failed-shards | |
| cat > review-artifacts/failed-shards/shard-${{ matrix.shard }}.json <<JSON | |
| { | |
| "shard": ${{ matrix.shard }}, | |
| "item_numbers": "${{ matrix.item_numbers }}" | |
| } | |
| JSON | |
| - uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: review-shard-${{ matrix.shard }} | |
| path: | | |
| review-artifacts/shard-${{ matrix.shard }}/*.md | |
| review-artifacts/shard-${{ matrix.shard }}/review-cache-metrics.json | |
| if-no-files-found: ignore | |
| - uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: action-ledger-review-${{ matrix.shard }} | |
| path: ${{ runner.temp }}/clawsweeper-action-ledger/${{ github.run_id }}/${{ github.run_attempt }}/${{ github.job }}/** | |
| include-hidden-files: true | |
| if-no-files-found: ignore | |
| - uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: review-failed-shard-${{ matrix.shard }} | |
| path: review-artifacts/failed-shards/shard-${{ matrix.shard }}.json | |
| if-no-files-found: ignore | |
| - uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: review-metrics-${{ matrix.shard }} | |
| path: review-artifacts/metrics/shard-${{ matrix.shard }}.json | |
| if-no-files-found: ignore | |
| - uses: actions/upload-artifact@v7 | |
| if: ${{ always() && github.event_name == 'repository_dispatch' && github.event.client_payload.expected_source_revision != '' }} | |
| with: | |
| name: review-source-revision-mismatch-${{ matrix.shard }} | |
| path: review-artifacts/shard-${{ matrix.shard }}/source-revision-mismatch.json | |
| if-no-files-found: ignore | |
| requeue-source-revision-drift: | |
| name: Requeue source-revision drift | |
| needs: [plan, review] | |
| if: ${{ always() && needs.plan.result == 'success' && needs.review.result != 'cancelled' && github.event_name == 'repository_dispatch' && github.event.action == 'clawsweeper_target_sweep' && github.event.client_payload.expected_source_revision != '' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read | |
| contents: write | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| continue-on-error: true | |
| with: | |
| pattern: review-source-revision-mismatch-* | |
| path: source-revision-mismatches | |
| merge-multiple: true | |
| - name: Dispatch exact review at current revision | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| EXPECTED_SOURCE_REVISION: ${{ github.event.client_payload.expected_source_revision || '' }} | |
| REQUEUE_COUNT: ${{ github.event.client_payload.source_revision_requeue_count || '0' }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| TARGET_BRANCH: ${{ needs.plan.outputs.target_branch }} | |
| CODEX_TIMEOUT_MS: ${{ needs.plan.outputs.codex_timeout_ms }} | |
| run: | | |
| set -euo pipefail | |
| marker="source-revision-mismatches/source-revision-mismatch.json" | |
| if [ ! -f "$marker" ]; then | |
| exit 0 | |
| fi | |
| if ! [[ "$REQUEUE_COUNT" =~ ^[0-9]+$ ]] || [ "$REQUEUE_COUNT" -ge 1 ]; then | |
| echo "::warning::Source revision changed again; leaving the normal scheduler to pick up the latest revision." | |
| exit 0 | |
| fi | |
| marker_expected="$(jq -r '.expected_source_revision // empty' "$marker")" | |
| actual_revision="$(jq -r '.actual_source_revision // empty' "$marker")" | |
| item_number="$(jq -r '.item_number // empty' "$marker")" | |
| if [ "$marker_expected" != "$EXPECTED_SOURCE_REVISION" ] || \ | |
| ! [[ "$actual_revision" =~ ^[0-9a-f]{64}$ ]] || \ | |
| ! [[ "$item_number" =~ ^[0-9]+$ ]]; then | |
| echo "::warning::Ignoring invalid source-revision mismatch marker." | |
| exit 0 | |
| fi | |
| jq -n \ | |
| --arg target_repo "$TARGET_REPO" \ | |
| --arg target_branch "$TARGET_BRANCH" \ | |
| --arg item_number "$item_number" \ | |
| --arg codex_timeout_ms "$CODEX_TIMEOUT_MS" \ | |
| --arg expected_source_revision "$actual_revision" \ | |
| '{ | |
| event_type: "clawsweeper_target_sweep", | |
| client_payload: { | |
| target_repo: $target_repo, | |
| target_branch: $target_branch, | |
| item_number: $item_number, | |
| batch_size: "1", | |
| shard_count: "1", | |
| hot_intake: "false", | |
| codex_timeout_ms: $codex_timeout_ms, | |
| expected_source_revision: $expected_source_revision, | |
| source_revision_requeue_count: "1" | |
| } | |
| }' | gh api --method POST "repos/$GITHUB_REPOSITORY/dispatches" --input - | |
| echo "::notice::Requeued $TARGET_REPO#$item_number at source revision $actual_revision." | |
| publish: | |
| name: Publish review artifacts | |
| needs: [plan, review] | |
| if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.queue_feed != 'true' && needs.review.result != 'cancelled' && needs.plan.outputs.target_repo != '' && (github.event_name != 'repository_dispatch' || github.event.action == 'clawsweeper_target_sweep') && !((github.event_name == 'workflow_dispatch' && (github.event.inputs.apply_existing == 'true' || github.event.inputs.audit_dashboard == 'true')) || (github.event_name == 'schedule' && ((github.event.schedule == '3 * * * *' || github.event.schedule == '18 * * * *' || github.event.schedule == '33 * * * *' || github.event.schedule == '48 * * * *' || github.event.schedule == '8,23,38,53 * * * *' || github.event.schedule == '6,21,36,51 * * * *') || (github.event.schedule == '7 */6 * * *' || github.event.schedule == '12 */6 * * *' || github.event.schedule == '17 */6 * * *')))) }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| concurrency: | |
| group: clawsweeper-target-review-publish-${{ needs.plan.outputs.target_repo }} | |
| cancel-in-progress: false | |
| queue: max | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| filter: blob:none | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: ./.github/actions/setup-action-ledger | |
| continue-on-error: true | |
| - name: Create target write token | |
| id: target-write-token | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| owner: ${{ needs.plan.outputs.target_repo_owner }} | |
| repositories: ${{ needs.plan.outputs.target_repo_name }} | |
| permission-contents: write | |
| permission-issues: write | |
| permission-pull-requests: write | |
| - name: Create state token | |
| id: state-token | |
| uses: ./.github/actions/create-state-token | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| - uses: ./.github/actions/setup-state | |
| id: setup-publish-state | |
| with: | |
| coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| records-repo-slugs: ${{ needs.plan.outputs.target_slug }} | |
| hydrate-state-blobs: "false" | |
| token: ${{ steps.state-token.outputs.token }} | |
| - uses: ./.github/actions/setup-pnpm | |
| id: setup-publish-pnpm | |
| with: | |
| build-script: build:all | |
| - uses: actions/download-artifact@v8 | |
| id: download-review-artifacts | |
| with: | |
| pattern: review-shard-* | |
| path: artifacts | |
| merge-multiple: true | |
| - uses: actions/download-artifact@v8 | |
| id: download-review-metrics | |
| continue-on-error: true | |
| with: | |
| pattern: review-metrics-* | |
| path: review-metrics | |
| merge-multiple: true | |
| - uses: actions/download-artifact@v8 | |
| id: download-review-action-ledger | |
| continue-on-error: true | |
| with: | |
| pattern: action-ledger-review-* | |
| path: .clawsweeper-repair/action-ledger-download | |
| merge-multiple: true | |
| - name: Import immutable review action events | |
| id: import-review-action-ledger | |
| if: ${{ always() && steps.setup-publish-state.outcome == 'success' && steps.setup-publish-pnpm.outcome == 'success' && steps.download-review-action-ledger.outcome == 'success' }} | |
| continue-on-error: true | |
| run: | | |
| set -euo pipefail | |
| mkdir -p .artifacts | |
| : > .artifacts/action-ledger-paths.txt | |
| pnpm run --silent publish-action-events -- \ | |
| --source-root .clawsweeper-repair/action-ledger-download \ | |
| --state-root . \ | |
| --expected-producer-job review \ | |
| --expected-producer-max-run-attempt "$GITHUB_RUN_ATTEMPT" | | |
| jq -r '.paths[]?' >> .artifacts/action-ledger-paths.txt | |
| pnpm run --silent publish-action-events -- \ | |
| --source-root "$CLAWSWEEPER_ACTION_LEDGER_OUTPUT_ROOT" \ | |
| --state-root . \ | |
| --expected-producer-job "$GITHUB_JOB" | | |
| jq -r '.paths[]?' >> .artifacts/action-ledger-paths.txt | |
| sort -u -o .artifacts/action-ledger-paths.txt .artifacts/action-ledger-paths.txt | |
| - name: Publish immutable review action ledger | |
| if: ${{ always() && steps.import-review-action-ledger.outcome == 'success' }} | |
| continue-on-error: true | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| run: | | |
| if [ ! -s .artifacts/action-ledger-paths.txt ]; then | |
| echo "No immutable review action events to publish." | |
| exit 0 | |
| fi | |
| node dist/clawsweeper.js publish-action-event-paths \ | |
| --paths-file .artifacts/action-ledger-paths.txt | |
| - name: Sync before applying artifacts | |
| id: sync-review-artifacts | |
| if: ${{ always() && !cancelled() && steps.setup-publish-state.outcome == 'success' && steps.setup-publish-pnpm.outcome == 'success' && steps.download-review-artifacts.outcome == 'success' }} | |
| run: | | |
| if ! pull_output="$(git pull --rebase --autostash 2>&1)"; then | |
| printf '%s\n' "$pull_output" >&2 | |
| # A reviewed PR head branch can vanish mid-review (force-push or | |
| # deletion). The newer head triggers its own review event, so this | |
| # publication completes as a superseded no-op instead of failing. | |
| if grep -qiE "no such ref was fetched|couldn.t find remote ref" <<<"$pull_output"; then | |
| echo "::notice::The reviewed branch disappeared upstream; the newer head owns its own review. Skipping artifact publication as a superseded no-op." | |
| echo "superseded=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| exit 1 | |
| fi | |
| - name: Apply review artifacts | |
| id: apply-review-artifacts | |
| if: ${{ always() && !cancelled() && steps.sync-review-artifacts.outcome == 'success' && steps.sync-review-artifacts.outputs.superseded != 'true' }} | |
| env: | |
| CLAWSWEEPER_CANONICAL_RECORD_BASELINE_DIR: .artifacts/review-canonical-baseline | |
| GH_TOKEN: ${{ github.token }} | |
| EXACT_ITEM: ${{ github.event.client_payload.item_number || github.event.inputs.item_number || github.event.inputs.item_numbers || '' }} | |
| EXPECTED_METRIC_COUNT: ${{ needs.plan.outputs.planned_shards }} | |
| HOT_INTAKE: ${{ needs.plan.outputs.hot_intake }} | |
| MAX_PAGES: ${{ needs.plan.outputs.max_pages }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| apply_artifacts_args=(--target-repo "$TARGET_REPO" --artifact-dir artifacts --skip-dashboard --skip-reconcile) | |
| publish_state="Review publish complete" | |
| mkdir -p review-metrics | |
| reviewed_count="$(find artifacts -type f -name '*.md' | wc -l | tr -d ' ')" | |
| metric_count="$(find review-metrics -type f -name '*.json' | wc -l | tr -d ' ')" | |
| non_success_metric_count="$(find review-metrics -type f -name '*.json' -print0 | xargs -0 -r jq -r 'select(.review_outcome != "success") | .shard' | wc -l | tr -d ' ')" | |
| review_batch_succeeded=false | |
| if [[ "$EXPECTED_METRIC_COUNT" =~ ^[1-9][0-9]*$ ]] && | |
| [ "$metric_count" -eq "$EXPECTED_METRIC_COUNT" ] && | |
| [ "$non_success_metric_count" -eq 0 ]; then | |
| review_batch_succeeded=true | |
| fi | |
| echo "review_batch_succeeded=$review_batch_succeeded" >> "$GITHUB_OUTPUT" | |
| publish_detail="Merged ${reviewed_count} review artifacts for run ${{ github.run_id }}. Captured ${metric_count} shard metrics; ${non_success_metric_count} shards reported non-success review status. Folder reconciliation moved tracked files to match current GitHub open/closed state." | |
| if [ "$HOT_INTAKE" = "true" ] && [ -z "$EXACT_ITEM" ]; then | |
| apply_artifacts_args+=(--max-pages "$MAX_PAGES" --skip-reconcile) | |
| publish_state="Hot intake publish complete" | |
| publish_detail="Merged ${reviewed_count} hot intake artifacts for run ${{ github.run_id }} without full folder reconciliation. Captured ${metric_count} shard metrics; ${non_success_metric_count} shards reported non-success review status." | |
| fi | |
| pnpm run apply-artifacts -- "${apply_artifacts_args[@]}" | |
| echo "artifacts_applied=true" >> "$GITHUB_OUTPUT" | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "$publish_state" \ | |
| --detail "$publish_detail" \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ | |
| --planned-count "${{ needs.plan.outputs.planned_count }}" \ | |
| --planned-capacity "${{ needs.plan.outputs.planned_capacity }}" \ | |
| --planned-shards "${{ needs.plan.outputs.planned_shards }}" \ | |
| --active-codex "0" \ | |
| --due-backlog "${{ needs.plan.outputs.due_backlog }}" \ | |
| --oldest-unreviewed-at "${{ needs.plan.outputs.oldest_unreviewed_at }}" \ | |
| --capacity-reason "${{ needs.plan.outputs.capacity_reason }}" | |
| - name: Publish review artifact action ledger | |
| if: ${{ always() && steps.apply-review-artifacts.outputs.artifacts_applied == 'true' }} | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| run: | | |
| set -euo pipefail | |
| source_root="$CLAWSWEEPER_ACTION_LEDGER_OUTPUT_ROOT" | |
| if [ ! -d "$source_root/ledger" ]; then | |
| echo "No review artifact action event shards were finalized." | |
| exit 0 | |
| fi | |
| event_paths_file=".artifacts/review-artifact-action-ledger-paths.txt" | |
| mkdir -p .artifacts | |
| pnpm run --silent publish-action-events -- \ | |
| --source-root "$source_root" \ | |
| --state-root . \ | |
| --expected-producer-job "$GITHUB_JOB" | | |
| jq -r '.paths[]?' | | |
| sort -u > "$event_paths_file" | |
| if [ ! -s "$event_paths_file" ]; then | |
| echo "Review artifact action event shards existed but no paths were imported." >&2 | |
| exit 1 | |
| fi | |
| node dist/clawsweeper.js publish-action-event-paths \ | |
| --paths-file "$event_paths_file" | |
| - name: Commit review records | |
| id: commit-review-records | |
| if: ${{ always() && !cancelled() && steps.apply-review-artifacts.outputs.artifacts_applied == 'true' }} | |
| env: | |
| CLAWSWEEPER_CANONICAL_RECORD_BASELINE_DIR: .artifacts/review-canonical-baseline | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| EXACT_ITEM: ${{ github.event.client_payload.item_number || github.event.inputs.item_number || github.event.inputs.item_numbers || '' }} | |
| GH_TOKEN: ${{ github.token }} | |
| HOT_INTAKE: ${{ needs.plan.outputs.hot_intake }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| run: | | |
| target_slug="$TARGET_REPO" | |
| target_slug="${target_slug//\//-}" | |
| if [ "$HOT_INTAKE" = "true" ] && [ -z "$EXACT_ITEM" ]; then | |
| echo "Skipping full reconcile for broad hot-intake publish." | |
| else | |
| pnpm run reconcile -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --skip-closed-at \ | |
| --canonical-record-baseline-dir "$CLAWSWEEPER_CANONICAL_RECORD_BASELINE_DIR" | |
| fi | |
| pnpm run repair:publish-main -- \ | |
| --message "chore: update sweep records" \ | |
| --path "records/${target_slug}" \ | |
| --rebase-strategy normal | |
| echo "records_published=true" >> "$GITHUB_OUTPUT" | |
| if ! pnpm run repair:publish-main -- \ | |
| --message "chore: update sweep status" \ | |
| --path "results/sweep-status/${target_slug}.json" \ | |
| --rebase-strategy theirs; then | |
| echo "::warning title=Sweep status publish failed::Review records are canonical; continuing without the best-effort Git dashboard update." | |
| fi | |
| - name: Dispatch high-confidence bug implementation candidates | |
| if: ${{ always() && !cancelled() && steps.commit-review-records.outputs.records_published == 'true' && vars.CLAWSWEEPER_AUTO_IMPLEMENT_ISSUES == '1' && needs.plan.outputs.target_repo == 'openclaw/openclaw' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| ENABLED: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_ISSUES == '1' && 'true' || 'false' }} | |
| MAX_DISPATCH: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_MAX_DISPATCH_PER_SWEEP || '' }} | |
| run: | | |
| target_slug="${TARGET_REPO//\//-}" | |
| node scripts/dispatch-issue-implementation-candidates.mjs \ | |
| --target-repo "$TARGET_REPO" \ | |
| --artifact-dir artifacts \ | |
| --report-dir "records/$target_slug/items" | |
| - name: Collect published review item numbers | |
| id: published-review-items | |
| if: ${{ always() && !cancelled() && steps.commit-review-records.outputs.records_published == 'true' }} | |
| run: | | |
| set -euo pipefail | |
| item_numbers="$(pnpm run --silent workflow -- artifact-item-numbers --artifact-dir artifacts)" | |
| echo "item_numbers=$item_numbers" >> "$GITHUB_OUTPUT" | |
| - name: Dispatch recommended live proofs | |
| if: ${{ always() && !cancelled() && steps.commit-review-records.outputs.records_published == 'true' && steps.published-review-items.outcome == 'success' }} | |
| uses: ./.github/actions/dispatch-live-proofs | |
| with: | |
| target-repo: ${{ needs.plan.outputs.target_repo }} | |
| item-numbers: ${{ steps.published-review-items.outputs.item_numbers }} | |
| records-root: records | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| - name: Dispatch vision-fit implementation candidates | |
| if: ${{ always() && !cancelled() && steps.commit-review-records.outputs.records_published == 'true' && vars.CLAWSWEEPER_AUTO_IMPLEMENT_ISSUES == '1' && needs.plan.outputs.target_repo == 'openclaw/openclaw' && vars.CLAWSWEEPER_AUTO_IMPLEMENT_VISION_FIT == '1' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| ENABLED: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_VISION_FIT == '1' && 'true' || 'false' }} | |
| MAX_DISPATCH: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_VISION_FIT_MAX_DISPATCH_PER_SWEEP || vars.CLAWSWEEPER_AUTO_IMPLEMENT_MAX_DISPATCH_PER_SWEEP || '' }} | |
| run: | | |
| set -euo pipefail | |
| target_slug="${TARGET_REPO//\//-}" | |
| if [ -z "$MAX_DISPATCH" ]; then | |
| MAX_DISPATCH="$(pnpm run --silent workflow -- limit issue_implementation.dispatches_per_sweep_default)" | |
| fi | |
| candidate_output="$(pnpm run --silent repair:issue-implementation-intake -- candidates \ | |
| --enabled "$ENABLED" \ | |
| --candidate-kind vision_fit \ | |
| --target-repo "$TARGET_REPO" \ | |
| --artifact-dir artifacts \ | |
| --report-dir "records/$target_slug/items")" | |
| echo "$candidate_output" | |
| candidates_json="$(CANDIDATE_OUTPUT="$candidate_output" node <<'NODE' | |
| const output = JSON.parse(process.env.CANDIDATE_OUTPUT || "{}"); | |
| console.log(JSON.stringify(output.candidates || [])); | |
| NODE | |
| )" | |
| if [ -z "$candidates_json" ] || [ "$candidates_json" = "[]" ]; then | |
| echo "No vision-fit implementation candidates." | |
| exit 0 | |
| fi | |
| CANDIDATES_JSON="$candidates_json" MAX_DISPATCH="$MAX_DISPATCH" node <<'NODE' > /tmp/vision-fit-implementation-candidates.tsv | |
| const candidates = JSON.parse(process.env.CANDIDATES_JSON || "[]"); | |
| const limit = Math.max(0, Number(process.env.MAX_DISPATCH || "0")); | |
| for (const candidate of candidates.slice(0, limit)) { | |
| console.log([ | |
| candidate.item_number, | |
| candidate.report_path, | |
| candidate.report_url, | |
| ].join("\t")); | |
| } | |
| NODE | |
| if [ ! -s /tmp/vision-fit-implementation-candidates.tsv ]; then | |
| echo "No vision-fit candidates remained after dispatch cap." | |
| exit 0 | |
| fi | |
| while IFS=$'\t' read -r item_number report_path report_url; do | |
| echo "Dispatching vision-fit implementation intake for $TARGET_REPO#$item_number" | |
| gh workflow run repair-issue-implementation-intake.yml \ | |
| --ref main \ | |
| -f enabled=true \ | |
| -f target_repo="$TARGET_REPO" \ | |
| -f item_number="$item_number" \ | |
| -f candidate_kind=vision_fit \ | |
| -f report_path="$report_path" \ | |
| -f report_url="$report_url" | |
| done < /tmp/vision-fit-implementation-candidates.tsv | |
| - name: Backfill viable open issue implementation candidates | |
| if: ${{ always() && !cancelled() && steps.commit-review-records.outputs.records_published == 'true' && vars.CLAWSWEEPER_AUTO_IMPLEMENT_ISSUES == '1' && steps.target-write-token.outputs.token != '' && needs.plan.outputs.target_repo != 'openclaw/openclaw' && needs.plan.outputs.target_repo != 'openclaw/clawhub' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| MAX_DISPATCH: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_MAX_DISPATCH_PER_SWEEP || '' }} | |
| run: | | |
| set -euo pipefail | |
| target_slug="${TARGET_REPO//\//-}" | |
| if [ -z "$MAX_DISPATCH" ]; then | |
| MAX_DISPATCH="$(pnpm run --silent workflow -- limit issue_implementation.dispatches_per_sweep_default)" | |
| fi | |
| candidate_output="$(pnpm run --silent repair:issue-implementation-intake -- candidates \ | |
| --enabled true \ | |
| --candidate-kind viable \ | |
| --target-repo "$TARGET_REPO" \ | |
| --artifact-dir artifacts \ | |
| --report-dir "records/$target_slug/items" \ | |
| --report-repo openclaw/clawsweeper-state)" | |
| echo "$candidate_output" | |
| CANDIDATE_OUTPUT="$candidate_output" MAX_DISPATCH="$MAX_DISPATCH" node <<'NODE' > /tmp/viable-implementation-candidates.tsv | |
| const output = JSON.parse(process.env.CANDIDATE_OUTPUT || "{}"); | |
| const limit = Math.max(0, Number(process.env.MAX_DISPATCH || "0")); | |
| for (const candidate of (output.candidates || []).slice(0, limit)) { | |
| console.log([candidate.item_number, candidate.report_path, candidate.report_url].join("\t")); | |
| } | |
| NODE | |
| if [ ! -s /tmp/viable-implementation-candidates.tsv ]; then | |
| echo "No viable issue implementation candidates." | |
| exit 0 | |
| fi | |
| while IFS=$'\t' read -r item_number report_path report_url; do | |
| echo "Dispatching viable issue implementation intake for $TARGET_REPO#$item_number" | |
| gh workflow run repair-issue-implementation-intake.yml \ | |
| --ref main \ | |
| -f enabled=true \ | |
| -f target_repo="$TARGET_REPO" \ | |
| -f item_number="$item_number" \ | |
| -f candidate_kind=viable \ | |
| -f report_repo=openclaw/clawsweeper-state \ | |
| -f report_path="$report_path" \ | |
| -f report_url="$report_url" | |
| done < /tmp/viable-implementation-candidates.tsv | |
| - name: Dispatch background review comment sync | |
| if: ${{ always() && !cancelled() && steps.commit-review-records.outputs.records_published == 'true' && steps.target-write-token.outputs.token != '' && needs.plan.outputs.hot_intake != 'true' && (github.event_name != 'repository_dispatch' || github.event.action == 'clawsweeper_target_sweep') && (github.event_name != 'workflow_dispatch' || (github.event.inputs.item_number == '' && github.event.inputs.item_numbers == '')) }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| item_numbers="$(pnpm run --silent workflow -- artifact-item-numbers --artifact-dir artifacts)" | |
| if [ -z "$item_numbers" ]; then | |
| echo "No review artifacts to sync comments for." | |
| exit 0 | |
| fi | |
| for attempt in 1 2 3; do | |
| if gh workflow run sweep.yml \ | |
| --ref main \ | |
| -f target_repo="$TARGET_REPO" \ | |
| -f apply_existing=true \ | |
| -f apply_sync_comments_only=true \ | |
| -f apply_item_numbers=__cursor__ \ | |
| -f apply_kind=all \ | |
| -f apply_limit=40 \ | |
| -f apply_min_age_days=0 \ | |
| -f apply_comment_sync_min_age_days=0; then | |
| echo "Dispatched coalesced durable comment sync for item numbers: $item_numbers" | |
| exit 0 | |
| fi | |
| sleep "$((attempt * 10))" | |
| done | |
| echo "::warning::Unable to dispatch background comment sync after three attempts; apply/comment-sync backstops can pick it up later." | |
| - name: Sync selected review comments | |
| id: sync-selected-review-comments | |
| if: ${{ always() && !cancelled() && steps.commit-review-records.outputs.records_published == 'true' && steps.target-write-token.outputs.token != '' && ((github.event_name == 'repository_dispatch' && github.event.action != 'clawsweeper_target_sweep') || github.event.inputs.item_number != '' || github.event.inputs.item_numbers != '') }} | |
| timeout-minutes: 15 | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| CLAWSWEEPER_PUBLIC_GH_TOKEN: ${{ needs.plan.outputs.target_repo == 'openclaw/openclaw' && github.token || '' }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$GH_TOKEN" | |
| source scripts/apply-workflow-helpers.sh | |
| begin_canonical_record_mutation | |
| target_slug="$TARGET_REPO" | |
| target_slug="${target_slug//\//-}" | |
| item_numbers="$(pnpm run --silent workflow -- artifact-item-numbers --artifact-dir artifacts)" | |
| if [ -z "$item_numbers" ]; then | |
| echo "No review artifacts to sync comments for." | |
| exit 0 | |
| fi | |
| git pull --rebase --autostash | |
| set +e | |
| timeout --kill-after=30s 840s pnpm run apply-decisions -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --skip-dashboard \ | |
| --item-numbers "$item_numbers" \ | |
| --sync-comments-only \ | |
| --apply-kind all \ | |
| --limit 0 \ | |
| --processed-limit 1000 \ | |
| --max-runtime-ms 720000 \ | |
| --comment-sync-min-age-days 7 \ | |
| --progress-every 25 | |
| selected_comment_exit_code=$? | |
| set -e | |
| echo "exit_code=$selected_comment_exit_code" >> "$GITHUB_OUTPUT" | |
| if [ "$selected_comment_exit_code" -ne 0 ]; then | |
| exit "$selected_comment_exit_code" | |
| fi | |
| echo "sync_succeeded=true" >> "$GITHUB_OUTPUT" | |
| synced_count="$(pnpm run --silent workflow -- count-actions --report apply-report.json --action review_comment_synced)" | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "Review comments checked" \ | |
| --detail "Checked selected durable Codex review comments and synced missing or stale comments. Synced: $synced_count. Item numbers: $item_numbers." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ | |
| --planned-count "${{ needs.plan.outputs.planned_count }}" \ | |
| --planned-capacity "${{ needs.plan.outputs.planned_capacity }}" \ | |
| --planned-shards "${{ needs.plan.outputs.planned_shards }}" \ | |
| --active-codex "0" \ | |
| --due-backlog "${{ needs.plan.outputs.due_backlog }}" \ | |
| --oldest-unreviewed-at "${{ needs.plan.outputs.oldest_unreviewed_at }}" \ | |
| --capacity-reason "${{ needs.plan.outputs.capacity_reason }}" | |
| pnpm run repair:publish-main -- \ | |
| --message "chore: sync selected review comments" \ | |
| --path "records/${target_slug}" \ | |
| --rebase-strategy normal | |
| if ! pnpm run repair:publish-main -- \ | |
| --message "chore: publish selected review comment status" \ | |
| --path apply-report.json \ | |
| --path "results/sweep-status/${target_slug}.json" \ | |
| --rebase-strategy theirs; then | |
| echo "::warning title=Selected-comment bookkeeping failed::Canonical comment records are published; continuing without the best-effort Git report update." | |
| fi | |
| - name: Finalize selected review comment action ledger | |
| id: finalize-selected-review-comment-action-ledger | |
| if: ${{ always() && steps.sync-selected-review-comments.outcome != 'skipped' }} | |
| env: | |
| SELECTED_COMMENT_EXIT_CODE: ${{ steps.sync-selected-review-comments.outputs.exit_code || '' }} | |
| SELECTED_COMMENT_OUTCOME: ${{ steps.sync-selected-review-comments.outcome || 'not_started' }} | |
| run: | | |
| args=() | |
| if [ "$SELECTED_COMMENT_OUTCOME" = "cancelled" ] || [ "$SELECTED_COMMENT_EXIT_CODE" = "130" ] || [ "$SELECTED_COMMENT_EXIT_CODE" = "143" ]; then | |
| args=(--interrupt-open-attempts --reason cancelled) | |
| elif [ "$SELECTED_COMMENT_EXIT_CODE" = "124" ] || [ "$SELECTED_COMMENT_EXIT_CODE" = "137" ]; then | |
| args=(--interrupt-open-attempts --reason timeout) | |
| elif [ "$SELECTED_COMMENT_OUTCOME" != "success" ]; then | |
| args=(--interrupt-open-attempts --reason workflow_failed) | |
| fi | |
| pnpm run --silent finalize-action-events -- "${args[@]}" | |
| - name: Publish selected review comment action ledger | |
| if: ${{ always() && steps.sync-selected-review-comments.outcome != 'skipped' && steps.finalize-selected-review-comment-action-ledger.outcome == 'success' }} | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| run: | | |
| set -euo pipefail | |
| if [ ! -d "$CLAWSWEEPER_ACTION_LEDGER_OUTPUT_ROOT/ledger" ]; then | |
| echo "No selected review comment action shards were finalized." | |
| exit 0 | |
| fi | |
| event_paths_file=".artifacts/selected-comment-action-ledger-paths.txt" | |
| mkdir -p .artifacts | |
| pnpm run --silent publish-action-events -- \ | |
| --source-root "$CLAWSWEEPER_ACTION_LEDGER_OUTPUT_ROOT" \ | |
| --state-root . \ | |
| --expected-producer-job "$GITHUB_JOB" | | |
| jq -r '.paths[]?' | | |
| sort -u > "$event_paths_file" | |
| if [ ! -s "$event_paths_file" ]; then | |
| echo "Selected review comment action shards existed but no paths were imported." >&2 | |
| exit 1 | |
| fi | |
| node dist/clawsweeper.js publish-action-event-paths \ | |
| --paths-file "$event_paths_file" | |
| - name: Dispatch selected safe close proposals to isolated apply | |
| if: ${{ always() && !cancelled() && steps.sync-selected-review-comments.outputs.sync_succeeded == 'true' && steps.target-write-token.outputs.token != '' && github.event.inputs.apply_after_review == 'true' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| item_numbers="$(pnpm run --silent workflow -- artifact-item-numbers --artifact-dir artifacts)" | |
| if [ -z "$item_numbers" ]; then | |
| echo "No review artifacts to dispatch for isolated apply." | |
| exit 0 | |
| fi | |
| item_count="$(pnpm run --silent workflow -- count-csv --items "$item_numbers")" | |
| close_reasons="${{ github.event.inputs.apply_after_review_close_reasons || env.CLAWSWEEPER_AUTO_CLOSE_REASONS }}" | |
| min_age_minutes="${{ github.event.inputs.apply_after_review_min_age_minutes || '0' }}" | |
| for attempt in 1 2 3; do | |
| if gh workflow run sweep.yml \ | |
| --ref main \ | |
| -f target_repo="$TARGET_REPO" \ | |
| -f apply_existing=true \ | |
| -f apply_item_numbers="$item_numbers" \ | |
| -f apply_limit="$item_count" \ | |
| -f apply_min_age_minutes="$min_age_minutes" \ | |
| -f apply_kind=all \ | |
| -f apply_close_reasons="$close_reasons" \ | |
| -f apply_stale_min_age_days=60 \ | |
| -f apply_close_delay_ms=1000 \ | |
| -f apply_comment_sync_min_age_days=0; then | |
| echo "Dispatched isolated apply for item numbers: $item_numbers" | |
| exit 0 | |
| fi | |
| sleep "$((attempt * 10))" | |
| done | |
| echo "Unable to dispatch isolated apply after three attempts." >&2 | |
| exit 1 | |
| - name: Continue sweep | |
| if: ${{ always() && !cancelled() && steps.apply-review-artifacts.outputs.review_batch_succeeded == 'true' && steps.commit-review-records.outputs.records_published == 'true' && needs.plan.outputs.planned_count == needs.plan.outputs.planned_capacity && github.event_name != 'repository_dispatch' && (github.event_name != 'workflow_dispatch' || (github.event.inputs.item_number == '' && github.event.inputs.item_numbers == '')) }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| for attempt in 1 2 3; do | |
| if gh workflow run sweep.yml \ | |
| --ref main \ | |
| -f apply_existing=false \ | |
| -f hot_intake="${{ needs.plan.outputs.hot_intake }}" \ | |
| -f target_repo="${{ needs.plan.outputs.target_repo }}" \ | |
| -f target_branch="${{ needs.plan.outputs.target_branch }}" \ | |
| -f batch_size="${{ needs.plan.outputs.batch_size }}" \ | |
| -f shard_count="${{ needs.plan.outputs.shard_count }}" \ | |
| -f codex_timeout_ms="${{ needs.plan.outputs.codex_timeout_ms }}"; then | |
| exit 0 | |
| fi | |
| sleep "$((attempt * 10))" | |
| done | |
| echo "::warning::Unable to dispatch the next sweep after three attempts; the scheduled backstop will pick it up." | |
| recover-review-failures: | |
| name: Recover failed review shards | |
| needs: [plan, review, publish] | |
| if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.queue_feed != 'true' && needs.review.result != 'skipped' && !contains(github.event.inputs.additional_prompt || '', '[clawsweeper-recovery-attempt=1]') && needs.plan.outputs.planned_item_numbers != '' && github.event_name != 'repository_dispatch' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| actions: read | |
| contents: read | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| id: failed-shards | |
| continue-on-error: true | |
| with: | |
| pattern: review-failed-shard-* | |
| path: failed-review-shards | |
| merge-multiple: true | |
| - name: Requeue planned review items once | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| ADDITIONAL_PROMPT: ${{ github.event.inputs.additional_prompt || '' }} | |
| MATRIX_JSON: ${{ needs.plan.outputs.matrix }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$CLAWSWEEPER_WEBHOOK_SECRET" | |
| queue_url="${QUEUE_URL%/}" | |
| recovery_marker="[clawsweeper-recovery-attempt=1]" | |
| max_additional_prompt_bytes=$((5000 - ${#recovery_marker} - 2)) | |
| additional_prompt_bytes="$(printf '%s' "$ADDITIONAL_PROMPT" | LC_ALL=C wc -c | tr -d '[:space:]')" | |
| recovery_prefix="$ADDITIONAL_PROMPT" | |
| if [ "$additional_prompt_bytes" -gt "$max_additional_prompt_bytes" ]; then | |
| # The queue validates JavaScript string length. A UTF-8 byte cap is deliberately | |
| # stricter, so no recovery delivery can exceed that character limit. | |
| recovery_prefix="$( | |
| set +o pipefail | |
| printf '%s' "$ADDITIONAL_PROMPT" | LC_ALL=C head -c "$max_additional_prompt_bytes" | iconv -f UTF-8 -t UTF-8 -c | |
| )" | |
| fi | |
| recovery_prompt="$recovery_marker" | |
| if [ -n "$recovery_prefix" ]; then | |
| recovery_prompt="$(printf '%s\n\n%s' "$recovery_prefix" "$recovery_marker")" | |
| fi | |
| failed_shards="" | |
| if [ -d failed-review-shards ]; then | |
| failed_shards="$(find failed-review-shards -type f -name 'shard-*.json' -print0 \ | |
| | xargs -0 -r jq -r '.shard // empty' \ | |
| | paste -sd, -)" | |
| fi | |
| if [ -z "$failed_shards" ]; then | |
| failed_shards="$( | |
| gh run view "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" --json jobs --jq ' | |
| .jobs[] | |
| | select((.conclusion == "failure" or .conclusion == "cancelled") and (.name | startswith("Review shard "))) | |
| | .name | |
| | sub("^Review shard ([0-9]+).*$"; "\\1") | |
| ' | paste -sd, - | |
| )" | |
| fi | |
| if [ -z "$failed_shards" ]; then | |
| echo "No failed review shard jobs found; nothing to requeue." | |
| exit 0 | |
| fi | |
| item_numbers="$( | |
| jq -r --arg failed_shards ",$failed_shards," ' | |
| [ | |
| .[] | |
| | . as $matrix_entry | |
| | select($failed_shards | contains("," + ($matrix_entry.shard | tostring) + ",")) | |
| | $matrix_entry.item_numbers | |
| | split(",")[] | |
| | select(test("^[0-9]+$")) | |
| ] | |
| | unique | |
| | join(",") | |
| ' <<<"$MATRIX_JSON" | |
| )" | |
| if [ -z "$item_numbers" ]; then | |
| echo "Failed shards had no item numbers to requeue: $failed_shards" | |
| exit 0 | |
| fi | |
| IFS=',' read -r -a recovery_items <<<"$item_numbers" | |
| failed_recovery_dispatches=() | |
| for item_number in "${recovery_items[@]}"; do | |
| if ! [[ "$item_number" =~ ^[0-9]+$ ]]; then | |
| echo "Invalid failed-review recovery item number: $item_number" >&2 | |
| exit 1 | |
| fi | |
| dispatch_key="failed-review-recovery-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${item_number}" | |
| payload="$(jq -n \ | |
| --arg target_repo "${{ needs.plan.outputs.target_repo }}" \ | |
| --arg target_branch "${{ needs.plan.outputs.target_branch }}" \ | |
| --arg item_number "$item_number" \ | |
| --arg dispatch_key "$dispatch_key" \ | |
| --arg codex_timeout_ms "${{ needs.plan.outputs.codex_timeout_ms }}" \ | |
| --arg additional_prompt "$recovery_prompt" \ | |
| '{ | |
| delivery_id: ("router:" + $dispatch_key), | |
| decision: { | |
| targetRepo: $target_repo, | |
| targetBranch: $target_branch, | |
| itemNumber: ($item_number | tonumber), | |
| itemKind: "issue", | |
| sourceEvent: "issues", | |
| sourceAction: "failed_review_shard_recovery", | |
| supersedesInProgress: false, | |
| codexTimeoutMs: ($codex_timeout_ms | tonumber), | |
| additionalPrompt: $additional_prompt | |
| } | |
| }')" | |
| queued=false | |
| for attempt in 1 2 3; do | |
| # shellcheck disable=SC2016 # The Node source must retain its literal template expression. | |
| signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')" | |
| response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \ | |
| --request POST \ | |
| --header "content-type: application/json" \ | |
| --header "x-clawsweeper-exact-review-signature: $signature" \ | |
| --data "$payload" \ | |
| "$queue_url/internal/exact-review/enqueue")" || response="" | |
| if jq -e '.ok == true and (.queued == true or .deduped == true or .shed == true or .accepted == false)' <<<"$response" >/dev/null; then | |
| if jq -e '.accepted == false' <<<"$response" >/dev/null; then | |
| echo "Recovery skipped because the target is disabled." | |
| elif jq -e '.shed == true' <<<"$response" >/dev/null; then | |
| echo "Recovery shed by exact-review queue backpressure." | |
| fi | |
| queued=true | |
| break | |
| fi | |
| echo "Recovery queue acknowledgement was not accepted on attempt $attempt." >&2 | |
| if [ "$attempt" -lt 3 ]; then | |
| sleep "$((attempt * 5))" | |
| fi | |
| done | |
| if [ "$queued" != "true" ]; then | |
| failed_recovery_dispatches+=("$item_number") | |
| fi | |
| done | |
| if [ "${#failed_recovery_dispatches[@]}" -gt 0 ]; then | |
| echo "Unable to queue failed review recovery for item numbers: ${failed_recovery_dispatches[*]}" >&2 | |
| exit 1 | |
| fi | |
| echo "Queued failed review shards $failed_shards through the exact-review control plane: $item_numbers" | |
| retry-failed-reviews: | |
| name: Retry failed Codex reviews | |
| if: ${{ github.event_name == 'schedule' && github.event.schedule == '13 * * * *' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| actions: write | |
| contents: write | |
| issues: read | |
| pull-requests: read | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| filter: blob:none | |
| fetch-depth: 0 | |
| - uses: ./.github/actions/setup-action-ledger | |
| continue-on-error: true | |
| - name: Create state token | |
| id: state-token | |
| uses: ./.github/actions/create-state-token | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| - uses: ./.github/actions/setup-state | |
| with: | |
| coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| records-repo-slugs: openclaw-openclaw | |
| hydrate-state-blobs: "false" | |
| token: ${{ steps.state-token.outputs.token }} | |
| - uses: ./.github/actions/setup-pnpm | |
| with: | |
| build-script: build:all | |
| - name: Plan or dispatch failed-review retries | |
| id: retry-failed-reviews-run | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: openclaw/openclaw | |
| DRY_RUN: ${{ vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_ENABLED == '1' && 'false' || 'true' }} | |
| RETRY_LIMIT: ${{ vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_LIMIT || '3' }} | |
| RETRY_MAX_ATTEMPTS: ${{ vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_MAX_ATTEMPTS || '2' }} | |
| RETRY_COOLDOWN_MINUTES: ${{ vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_COOLDOWN_MINUTES || '45' }} | |
| RETRY_MAX_RUNTIME_MS: ${{ vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_MAX_RUNTIME_MS || '600000' }} | |
| CODEX_TIMEOUT_MS: ${{ vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_CODEX_TIMEOUT_MS || vars.CLAWSWEEPER_CODEX_TIMEOUT_MS || '1200000' }} | |
| run: | | |
| set -euo pipefail | |
| git pull --rebase --autostash | |
| dry_run_arg=() | |
| if [ "$DRY_RUN" = "true" ]; then | |
| dry_run_arg=(--dry-run) | |
| echo "::notice::Failed-review retry is running in dry-run mode. Set CLAWSWEEPER_FAILED_REVIEW_RETRY_ENABLED=1 to dispatch exact-item retries." | |
| fi | |
| set +e | |
| pnpm run retry-failed-reviews -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --limit "$RETRY_LIMIT" \ | |
| --max-attempts "$RETRY_MAX_ATTEMPTS" \ | |
| --cooldown-minutes "$RETRY_COOLDOWN_MINUTES" \ | |
| --max-runtime-ms "$RETRY_MAX_RUNTIME_MS" \ | |
| --codex-timeout-ms "$CODEX_TIMEOUT_MS" \ | |
| --workflow-repo "$GITHUB_REPOSITORY" \ | |
| --workflow-ref main \ | |
| --state-dir results/failed-review-retries/openclaw-openclaw \ | |
| --report-path artifacts/failed-review-retry-report.json \ | |
| "${dry_run_arg[@]}" | |
| retry_exit_code=$? | |
| set -e | |
| echo "exit_code=$retry_exit_code" >> "$GITHUB_OUTPUT" | |
| exit "$retry_exit_code" | |
| - name: Finalize failed-review retry action ledger | |
| if: ${{ always() }} | |
| env: | |
| RETRY_EXIT_CODE: ${{ steps.retry-failed-reviews-run.outputs.exit_code || '' }} | |
| RETRY_OUTCOME: ${{ steps.retry-failed-reviews-run.outcome || 'not_started' }} | |
| run: | | |
| args=() | |
| if [ "$RETRY_OUTCOME" = "cancelled" ] || [ "$RETRY_EXIT_CODE" = "130" ] || [ "$RETRY_EXIT_CODE" = "143" ]; then | |
| args=(--interrupt-open-attempts --reason cancelled) | |
| elif [ "$RETRY_EXIT_CODE" = "124" ] || [ "$RETRY_EXIT_CODE" = "137" ]; then | |
| args=(--interrupt-open-attempts --reason timeout) | |
| elif [ "$RETRY_OUTCOME" != "success" ]; then | |
| args=(--interrupt-open-attempts --reason workflow_failed) | |
| fi | |
| pnpm run --silent finalize-action-events -- "${args[@]}" | |
| - name: Publish failed-review retry state | |
| if: ${{ always() && vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_ENABLED == '1' && hashFiles('results/failed-review-retries/openclaw-openclaw/*.json') != '' }} | |
| continue-on-error: true | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| run: | | |
| pnpm run repair:publish-main -- \ | |
| --message "chore: update failed review retry state" \ | |
| --path results/failed-review-retries/openclaw-openclaw \ | |
| --rebase-strategy theirs | |
| - name: Publish failed-review retry action ledger | |
| if: ${{ always() }} | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| run: | | |
| set -euo pipefail | |
| source_root="$CLAWSWEEPER_ACTION_LEDGER_OUTPUT_ROOT" | |
| if [ ! -d "$source_root/ledger" ]; then | |
| echo "No failed-review retry action event shards were finalized." | |
| exit 0 | |
| fi | |
| event_paths_file=".artifacts/failed-review-retry-action-ledger-paths.txt" | |
| mkdir -p .artifacts | |
| pnpm run --silent publish-action-events -- \ | |
| --source-root "$source_root" \ | |
| --state-root . \ | |
| --expected-producer-job "$GITHUB_JOB" | | |
| jq -r '.paths[]?' | | |
| sort -u > "$event_paths_file" | |
| if [ ! -s "$event_paths_file" ]; then | |
| echo "Failed-review retry action shards existed but no paths were imported." >&2 | |
| exit 1 | |
| fi | |
| node dist/clawsweeper.js publish-action-event-paths \ | |
| --paths-file "$event_paths_file" | |
| - uses: actions/upload-artifact@v7 | |
| if: ${{ always() }} | |
| with: | |
| name: failed-review-retry-report | |
| path: artifacts/failed-review-retry-report.json | |
| if-no-files-found: error | |
| retention-days: 14 | |
| - name: Restore failed-review retry outcome | |
| if: ${{ always() && steps.retry-failed-reviews-run.outcome != 'success' && steps.retry-failed-reviews-run.outcome != 'skipped' }} | |
| env: | |
| RETRY_EXIT_CODE: ${{ steps.retry-failed-reviews-run.outputs.exit_code || '1' }} | |
| run: | | |
| retry_exit_code="$RETRY_EXIT_CODE" | |
| if ! [[ "$retry_exit_code" =~ ^[1-9][0-9]*$ ]] || [ "$retry_exit_code" -gt 255 ]; then | |
| retry_exit_code=1 | |
| fi | |
| echo "Restoring failed-review retry exit code $retry_exit_code after ledger cleanup." | |
| exit "$retry_exit_code" | |
| audit-dashboard: | |
| name: Audit state | |
| if: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.audit_dashboard == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '7 */6 * * *' || github.event.schedule == '12 */6 * * *' || github.event.schedule == '17 */6 * * *')) }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| filter: blob:none | |
| fetch-depth: 0 | |
| - name: Resolve target repository | |
| id: target | |
| run: | | |
| target_repo="${{ github.event.inputs.target_repo || github.event.client_payload.target_repo || '' }}" | |
| if [ -z "$target_repo" ]; then | |
| case "${{ github.event.schedule || '' }}" in | |
| "12 */6 * * *") | |
| target_repo="openclaw/clawhub" | |
| ;; | |
| "17 */6 * * *") | |
| target_repo="openclaw/clawsweeper" | |
| ;; | |
| *) | |
| target_repo="openclaw/openclaw" | |
| ;; | |
| esac | |
| fi | |
| target_owner="${target_repo%%/*}" | |
| target_name="${target_repo#*/}" | |
| { | |
| echo "target_repo=$target_repo" | |
| echo "target_repo_owner=$target_owner" | |
| echo "target_repo_name=$target_name" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Create target read token | |
| id: target-read-token | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| owner: ${{ steps.target.outputs.target_repo_owner }} | |
| repositories: ${{ steps.target.outputs.target_repo_name }} | |
| permission-contents: read | |
| permission-issues: read | |
| permission-pull-requests: read | |
| - name: Select target read token | |
| id: target-token | |
| env: | |
| PRIMARY_TOKEN: ${{ github.token }} | |
| APP_FALLBACK_TOKEN: ${{ steps.target-read-token.outputs.token }} | |
| run: | | |
| if [ -n "$PRIMARY_TOKEN" ]; then | |
| echo "Using workflow token for public audit reads." | |
| echo "token=$PRIMARY_TOKEN" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Using ClawSweeper App token fallback for audit reads." | |
| echo "token=$APP_FALLBACK_TOKEN" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Create state token | |
| id: state-token | |
| uses: ./.github/actions/create-state-token | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| - uses: ./.github/actions/setup-state | |
| with: | |
| coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| hydrate-state-blobs: "false" | |
| token: ${{ steps.state-token.outputs.token }} | |
| - uses: ./.github/actions/setup-pnpm | |
| with: | |
| build-script: build:all | |
| - name: Refresh Audit Health | |
| env: | |
| CLAWSWEEPER_CANONICAL_RECORD_BASELINE_DIR: .artifacts/audit-canonical-baseline | |
| GH_TOKEN: ${{ steps.target-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| git pull --rebase --autostash | |
| reconcile_json="$(pnpm run --silent reconcile -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --max-pages 250 \ | |
| --skip-closed-at \ | |
| --canonical-record-baseline-dir "$CLAWSWEEPER_CANONICAL_RECORD_BASELINE_DIR")" | |
| echo "$reconcile_json" | |
| moved_to_closed="$(jq -r '.movedToClosed' <<<"$reconcile_json")" | |
| moved_to_items="$(jq -r '.movedToItems' <<<"$reconcile_json")" | |
| removed_stale_closed_copies="$(jq -r '.removedStaleClosedCopies' <<<"$reconcile_json")" | |
| mkdir -p .artifacts | |
| pnpm run audit -- --target-repo "$TARGET_REPO" --max-pages 250 --sample-limit 25 --output .artifacts/clawsweeper-audit.json --update-dashboard | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "Audit finished" \ | |
| --detail "Reconciled durable $TARGET_REPO records before audit: moved ${moved_to_closed} closed records, restored ${moved_to_items} reopened records, removed ${removed_stale_closed_copies} stale archived copies. Refreshed audit state from a full live scan." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| - name: Upload canonical close-verdict audit | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: close-verdict-audit-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: .artifacts/clawsweeper-audit.json | |
| if-no-files-found: error | |
| retention-days: 14 | |
| - name: Commit Audit Health | |
| env: | |
| CLAWSWEEPER_CANONICAL_RECORD_BASELINE_DIR: .artifacts/audit-canonical-baseline | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| target_slug="$TARGET_REPO" | |
| target_slug="${target_slug//\//-}" | |
| pnpm run repair:publish-main -- \ | |
| --message "chore: update sweep audit state" \ | |
| --path "records/${target_slug}" \ | |
| --rebase-strategy normal | |
| if ! pnpm run repair:publish-main -- \ | |
| --message "chore: publish sweep audit status" \ | |
| --path README.md \ | |
| --path "results/audit/${target_slug}.json" \ | |
| --path "results/sweep-status/${target_slug}.json" \ | |
| --rebase-strategy theirs; then | |
| echo "::warning title=Audit dashboard publish failed::Canonical reconciliation is complete; continuing without the best-effort Git dashboard update." | |
| fi | |
| - name: Refresh state dashboard | |
| env: | |
| GH_TOKEN: ${{ steps.state-token.outputs.token }} | |
| run: | | |
| gh workflow run dashboard.yml \ | |
| --repo openclaw/clawsweeper-state \ | |
| --ref main || echo "Best-effort dashboard refresh dispatch failed; scheduled state dashboard will retry." | |
| apply-proof: | |
| name: Prove close coverage without write credentials | |
| if: ${{ ((github.event_name == 'workflow_dispatch' && github.event.inputs.apply_existing == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '3 * * * *' || github.event.schedule == '18 * * * *' || github.event.schedule == '33 * * * *' || github.event.schedule == '48 * * * *' || github.event.schedule == '8,23,38,53 * * * *' || github.event.schedule == '6,21,36,51 * * * *'))) && !(github.event_name == 'repository_dispatch' && github.event.client_payload.target_repo == 'openclaw/clawhub' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') && !(github.event_name == 'schedule' && github.event.schedule == '8,23,38,53 * * * *' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| permissions: | |
| actions: read | |
| contents: read | |
| issues: read | |
| pull-requests: read | |
| env: | |
| CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED || 'false' }} | |
| CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED || 'false' }} | |
| CLAWSWEEPER_IDEA_REVIVAL_REACTIONS: ${{ vars.CLAWSWEEPER_IDEA_REVIVAL_REACTIONS || '5' }} | |
| CLAWSWEEPER_AUTHOR_PR_BUDGET_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET_CLOSE_ENABLED || 'false' }} | |
| CLAWSWEEPER_AUTHOR_PR_BUDGET: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET || '15' }} | |
| CLAWSWEEPER_AUTHOR_PR_BUDGET_MAX_CLOSES_PER_RUN: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET_MAX_CLOSES_PER_RUN || '5' }} | |
| outputs: | |
| target_repo: ${{ steps.target.outputs.target_repo }} | |
| artifact_name: ${{ steps.proof-artifact.outputs.name }} | |
| proof_ready: ${{ steps.primary-proof-result.outputs.ready }} | |
| action_ledger_artifact_name: ${{ steps.publishable-action-ledger.outputs.name }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| filter: blob:none | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - uses: ./.github/actions/setup-action-ledger | |
| continue-on-error: true | |
| - name: Resolve proof target repository | |
| id: target | |
| run: | | |
| target_repo="${{ github.event.inputs.target_repo || '' }}" | |
| if [ -z "$target_repo" ]; then | |
| case "${{ github.event.schedule || '' }}" in | |
| "8,23,38,53 * * * *") target_repo="openclaw/clawhub" ;; | |
| *) target_repo="openclaw/openclaw" ;; | |
| esac | |
| fi | |
| if ! printf '%s' "$target_repo" | grep -Eq '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'; then | |
| echo "Invalid target repository: $target_repo" >&2 | |
| exit 1 | |
| fi | |
| target_slug="$(printf '%s' "$target_repo" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_.-]+/-/g')" | |
| target_owner="${target_repo%%/*}" | |
| target_name="${target_repo#*/}" | |
| echo "target_repo=$target_repo" >> "$GITHUB_OUTPUT" | |
| echo "target_slug=$target_slug" >> "$GITHUB_OUTPUT" | |
| echo "target_repo_owner=$target_owner" >> "$GITHUB_OUTPUT" | |
| echo "target_repo_name=$target_name" >> "$GITHUB_OUTPUT" | |
| - name: Resolve proof artifact identity | |
| id: proof-artifact | |
| run: | | |
| echo "name=apply-coverage-proofs-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_OUTPUT" | |
| echo "action_ledger_name=action-ledger-apply-proof-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_OUTPUT" | |
| - uses: ./.github/actions/setup-state | |
| with: | |
| coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| records-repo-slugs: ${{ steps.target.outputs.target_slug }} | |
| hydrate-state-blobs: "false" | |
| token: ${{ github.token }} | |
| persist-credentials: "false" | |
| - uses: ./.github/actions/setup-pnpm | |
| with: | |
| build-script: build:all | |
| - name: Reconcile read-only proof inputs | |
| if: ${{ !(github.event_name == 'schedule' && github.event.schedule == '6,21,36,51 * * * *') && !(github.event_name == 'workflow_dispatch' && (github.event.inputs.apply_sync_comments_only == 'true' || github.event.inputs.apply_item_numbers == '__cursor__')) }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| item_numbers="${{ github.event.inputs.apply_item_numbers || '' }}" | |
| if [ "$item_numbers" = "__cursor__" ]; then | |
| item_numbers="" | |
| fi | |
| reconcile_args=(--target-repo "$TARGET_REPO" --skip-closed-at) | |
| if [ -n "$item_numbers" ]; then | |
| reconcile_args+=(--item-numbers "$item_numbers") | |
| fi | |
| pnpm run reconcile -- "${reconcile_args[@]}" | |
| - name: Select bounded coverage proof work | |
| id: proof-select | |
| env: | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| min_age_days="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_min_age_days || '0' }}" | |
| min_age_minutes="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_min_age_minutes || '' }}" | |
| apply_kind="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_kind || 'all' }}" | |
| apply_close_reasons="${{ github.event.inputs.apply_close_reasons || env.CLAWSWEEPER_AUTO_CLOSE_REASONS }}" | |
| stale_min_age_days="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_stale_min_age_days || '60' }}" | |
| item_numbers="${{ github.event.inputs.apply_item_numbers || '' }}" | |
| sync_comments_only="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_sync_comments_only || 'false' }}" | |
| if [ "${{ github.event_name }}" = "schedule" ] && | |
| [ "${{ github.event.schedule || '' }}" = "6,21,36,51 * * * *" ]; then | |
| sync_comments_only="true" | |
| fi | |
| if [ "$item_numbers" = "__cursor__" ]; then | |
| item_numbers="" | |
| sync_comments_only="true" | |
| fi | |
| product_direction_enabled=false | |
| case "${CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED,,}" in | |
| true|1|yes|on) product_direction_enabled=true ;; | |
| esac | |
| unsponsored_feature_enabled=false | |
| case "${CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED,,}" in | |
| true|1|yes|on) unsponsored_feature_enabled=true ;; | |
| esac | |
| author_pr_budget_enabled=false | |
| case "${CLAWSWEEPER_AUTHOR_PR_BUDGET_CLOSE_ENABLED,,}" in | |
| true|1|yes|on) author_pr_budget_enabled=true ;; | |
| esac | |
| if [ "$sync_comments_only" != "true" ] && [ "$product_direction_enabled" != "true" ] && [ "$apply_close_reasons" != "all" ]; then | |
| apply_close_reasons="$(printf '%s\n' "$apply_close_reasons" | tr ',' '\n' | awk '$0 != "unconfirmed_product_direction" && NF' | paste -sd, -)" | |
| fi | |
| if [ "$sync_comments_only" != "true" ] && [ "$unsponsored_feature_enabled" != "true" ] && [ "$apply_close_reasons" != "all" ]; then | |
| apply_close_reasons="$(printf '%s\n' "$apply_close_reasons" | tr ',' '\n' | awk '$0 != "unsponsored_feature_request" && NF' | paste -sd, -)" | |
| fi | |
| if [ "$sync_comments_only" != "true" ] && [ "$author_pr_budget_enabled" != "true" ] && [ "$apply_close_reasons" != "all" ]; then | |
| apply_close_reasons="$(printf '%s\n' "$apply_close_reasons" | tr ',' '\n' | awk '$0 != "author_pr_budget_exceeded" && NF' | paste -sd, -)" | |
| fi | |
| selected="" | |
| if [ "$sync_comments_only" != "true" ] && [ -n "$apply_close_reasons" ]; then | |
| proof_args=( | |
| --target-repo "$TARGET_REPO" | |
| --apply-kind "$apply_kind" | |
| --apply-close-reasons "$apply_close_reasons" | |
| --stale-min-age-days "$stale_min_age_days" | |
| --min-age-days "$min_age_days" | |
| --min-age-minutes "$min_age_minutes" | |
| --batch-size 2 | |
| --close-limit 2 | |
| --coverage-proof-limit 2 | |
| ) | |
| if [ -n "$item_numbers" ]; then | |
| proof_args+=(--item-numbers "$item_numbers") | |
| else | |
| target_slug="${TARGET_REPO//\//-}" | |
| proof_args+=(--cursor-path "results/apply-cursors/${target_slug}.json") | |
| fi | |
| selected="$(pnpm run --silent workflow -- proposed-pr-close-coverage-item-numbers "${proof_args[@]}")" | |
| fi | |
| echo "item_numbers=$selected" >> "$GITHUB_OUTPUT" | |
| echo "Selected read-only coverage proof items: ${selected:-none}" | |
| - uses: ./.github/actions/setup-codex | |
| if: ${{ env.CLAWSWEEPER_RUNNER != 'openclaw' && steps.proof-select.outputs.item_numbers != '' }} | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| CLAWSWEEPER_INTERNAL_MODEL: ${{ secrets.CLAWSWEEPER_MODEL }} | |
| - uses: ./.github/actions/setup-openclaw | |
| if: ${{ steps.proof-select.outputs.item_numbers != '' }} | |
| - name: Create target proof inspection token | |
| id: proof-inspection-token | |
| if: ${{ steps.proof-select.outputs.item_numbers != '' }} | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| owner: ${{ steps.target.outputs.target_repo_owner }} | |
| repositories: ${{ steps.target.outputs.target_repo_name }} | |
| permission-contents: read | |
| permission-issues: read | |
| permission-pull-requests: read | |
| - name: Generate bound close coverage proofs | |
| id: generate-apply-proofs | |
| if: ${{ steps.proof-select.outputs.item_numbers != '' }} | |
| timeout-minutes: 50 | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| GH_TOKEN: ${{ github.token }} | |
| CLAWSWEEPER_PROOF_INSPECTION_TOKEN: ${{ steps.proof-inspection-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| item_numbers="${{ steps.proof-select.outputs.item_numbers }}" | |
| item_count="$(pnpm run --silent workflow -- count-csv --items "$item_numbers")" | |
| min_age_minutes="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_min_age_minutes || '' }}" | |
| min_age_minutes_arg=() | |
| if [ -n "$min_age_minutes" ]; then | |
| min_age_minutes_arg=(--min-age-minutes "$min_age_minutes") | |
| fi | |
| mkdir -p .artifacts/apply-proof/pr-close-coverage-proof | |
| pnpm run apply-decisions -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --skip-dashboard \ | |
| --dry-run \ | |
| --item-numbers "$item_numbers" \ | |
| --apply-kind "${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_kind || 'all' }}" \ | |
| --apply-close-reasons "${{ github.event.inputs.apply_close_reasons || env.CLAWSWEEPER_AUTO_CLOSE_REASONS }}" \ | |
| --stale-min-age-days "${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_stale_min_age_days || '60' }}" \ | |
| --min-age-days "${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_min_age_days || '0' }}" \ | |
| "${min_age_minutes_arg[@]}" \ | |
| --limit "$item_count" \ | |
| --processed-limit "$((item_count * 10))" \ | |
| --close-delay-ms 0 \ | |
| --max-runtime-ms 2700000 \ | |
| --codex-model internal \ | |
| --codex-reasoning-effort high \ | |
| --artifact-dir .artifacts/apply-proof \ | |
| --report-path .artifacts/apply-proof/apply-report.json \ | |
| --progress-every 1 | |
| - name: Create bound close coverage proof manifest | |
| id: proof-manifest | |
| if: ${{ always() && !cancelled() && steps.proof-select.outcome == 'success' && (steps.proof-select.outputs.item_numbers == '' || steps.generate-apply-proofs.outcome == 'success') }} | |
| env: | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| source scripts/apply-workflow-helpers.sh | |
| write_coverage_proof_manifest \ | |
| ".artifacts/apply-proof/pr-close-coverage-proof" \ | |
| "$TARGET_REPO" \ | |
| "${{ steps.proof-select.outputs.item_numbers }}" | |
| - name: Export primary apply proof result | |
| id: primary-proof-result | |
| if: ${{ always() && !cancelled() && steps.proof-select.outcome == 'success' && (steps.proof-select.outputs.item_numbers == '' || steps.generate-apply-proofs.outcome == 'success') && steps.proof-manifest.outcome == 'success' }} | |
| run: echo "ready=true" >> "$GITHUB_OUTPUT" | |
| - name: Finalize apply proof action ledger | |
| if: ${{ always() && steps.proof-select.outputs.item_numbers != '' }} | |
| env: | |
| APPLY_PROOF_OUTCOME: ${{ steps.generate-apply-proofs.outcome || 'not_started' }} | |
| run: | | |
| args=() | |
| if [ "$APPLY_PROOF_OUTCOME" = "cancelled" ]; then | |
| args=(--interrupt-open-attempts --reason cancelled) | |
| elif [ "$APPLY_PROOF_OUTCOME" != "success" ]; then | |
| args=(--interrupt-open-attempts --reason workflow_failed) | |
| fi | |
| pnpm run --silent finalize-action-events -- "${args[@]}" | |
| - name: Upload bound close coverage proofs | |
| if: ${{ always() && steps.proof-manifest.outcome == 'success' }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ steps.proof-artifact.outputs.name }} | |
| path: .artifacts/apply-proof/pr-close-coverage-proof/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| - name: Upload apply proof action events | |
| id: upload-action-events | |
| if: ${{ always() && steps.proof-select.outputs.item_numbers != '' }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ steps.proof-artifact.outputs.action_ledger_name }} | |
| path: ${{ runner.temp }}/clawsweeper-action-ledger/${{ github.run_id }}/${{ github.run_attempt }}/${{ github.job }}/** | |
| include-hidden-files: true | |
| if-no-files-found: error | |
| retention-days: 7 | |
| - name: Export uploaded apply proof action ledger identity | |
| id: publishable-action-ledger | |
| if: ${{ always() && steps.upload-action-events.outputs.artifact-id != '' }} | |
| run: echo "name=${{ steps.proof-artifact.outputs.action_ledger_name }}" >> "$GITHUB_OUTPUT" | |
| publish-apply-proof-action-ledger: | |
| name: Publish immutable apply proof action ledger | |
| needs: apply-proof | |
| if: ${{ always() && needs.apply-proof.result != 'skipped' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| filter: blob:none | |
| - name: Download apply proof action events | |
| if: ${{ needs.apply-proof.outputs.action_ledger_artifact_name != '' }} | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: ${{ needs.apply-proof.outputs.action_ledger_artifact_name }} | |
| path: .clawsweeper-repair/action-ledger-proof | |
| - uses: ./.github/actions/setup-pnpm | |
| if: ${{ needs.apply-proof.outputs.action_ledger_artifact_name != '' }} | |
| with: | |
| build-script: build:all | |
| - name: Publish apply proof action events | |
| if: ${{ needs.apply-proof.outputs.action_ledger_artifact_name != '' }} | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| run: | | |
| set -euo pipefail | |
| source_root=".clawsweeper-repair/action-ledger-proof" | |
| if [ ! -d "$source_root/ledger" ]; then | |
| echo "The uploaded apply proof artifact did not contain action event shards." >&2 | |
| exit 1 | |
| fi | |
| event_paths_file=".artifacts/apply-proof-action-ledger-paths.txt" | |
| mkdir -p .artifacts | |
| pnpm run --silent publish-action-events -- \ | |
| --source-root "$source_root" \ | |
| --state-root . \ | |
| --expected-producer-job apply-proof | | |
| jq -r '.paths[]?' | | |
| sort -u > "$event_paths_file" | |
| if [ ! -s "$event_paths_file" ]; then | |
| echo "Apply proof action event shards existed but no paths were imported." >&2 | |
| exit 1 | |
| fi | |
| node dist/clawsweeper.js publish-action-event-paths \ | |
| --paths-file "$event_paths_file" | |
| apply-existing: | |
| name: Apply close proposals | |
| needs: [apply-proof, publish-apply-proof-action-ledger] | |
| if: ${{ always() && !cancelled() && needs.apply-proof.outputs.proof_ready == 'true' && ((github.event_name == 'workflow_dispatch' && github.event.inputs.apply_existing == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '3 * * * *' || github.event.schedule == '18 * * * *' || github.event.schedule == '33 * * * *' || github.event.schedule == '48 * * * *' || github.event.schedule == '8,23,38,53 * * * *' || github.event.schedule == '6,21,36,51 * * * *'))) && !(github.event_name == 'repository_dispatch' && github.event.client_payload.target_repo == 'openclaw/clawhub' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') && !(github.event_name == 'schedule' && github.event.schedule == '8,23,38,53 * * * *' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') }} | |
| outputs: | |
| target_repo: ${{ steps.target.outputs.target_repo }} | |
| action_ledger_outcome: ${{ steps.finalize-apply.outcome || 'not_started' }} | |
| state_status_outcome: ${{ steps.retry-final-apply-status-publication.outcome || 'not_started' }} | |
| state_publication_outcome: ${{ steps.publish-apply-action-events.outcome || 'not_started' }} | |
| observability_started_at: ${{ steps.apply-telemetry-start.outputs.started_at || '' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 360 | |
| concurrency: | |
| group: clawsweeper-target-apply-${{ needs.apply-proof.outputs.target_repo }} | |
| cancel-in-progress: false | |
| queue: max | |
| env: | |
| CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED || 'false' }} | |
| CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED || 'false' }} | |
| CLAWSWEEPER_IDEA_REVIVAL_REACTIONS: ${{ vars.CLAWSWEEPER_IDEA_REVIVAL_REACTIONS || '5' }} | |
| CLAWSWEEPER_AUTHOR_PR_BUDGET_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET_CLOSE_ENABLED || 'false' }} | |
| CLAWSWEEPER_AUTHOR_PR_BUDGET: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET || '15' }} | |
| CLAWSWEEPER_AUTHOR_PR_BUDGET_MAX_CLOSES_PER_RUN: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET_MAX_CLOSES_PER_RUN || '5' }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| filter: blob:none | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: ./.github/actions/setup-action-ledger | |
| continue-on-error: true | |
| - name: Resolve target repository | |
| id: target | |
| env: | |
| PROOF_TARGET_REPO: ${{ needs.apply-proof.outputs.target_repo }} | |
| run: | | |
| target_repo="${{ github.event_name == 'repository_dispatch' && github.event.client_payload.target_repo || github.event.inputs.target_repo || '' }}" | |
| if [ -z "$target_repo" ]; then | |
| case "${{ github.event.schedule || '' }}" in | |
| "8,23,38,53 * * * *") | |
| target_repo="openclaw/clawhub" | |
| ;; | |
| *) | |
| target_repo="openclaw/openclaw" | |
| ;; | |
| esac | |
| fi | |
| if [ -z "$PROOF_TARGET_REPO" ] || [ "$target_repo" != "$PROOF_TARGET_REPO" ]; then | |
| echo "Resolved mutation target '$target_repo' differs from proof target '${PROOF_TARGET_REPO:-missing}'." >&2 | |
| exit 1 | |
| fi | |
| target_owner="${target_repo%%/*}" | |
| target_name="${target_repo#*/}" | |
| target_slug="$(printf '%s' "$target_repo" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_.-]+/-/g')" | |
| { | |
| echo "target_repo=$target_repo" | |
| echo "target_repo_owner=$target_owner" | |
| echo "target_repo_name=$target_name" | |
| echo "target_slug=$target_slug" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Publish apply telemetry start | |
| id: apply-telemetry-start | |
| continue-on-error: true | |
| env: | |
| APPLY_OUTCOME: in_progress | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$CLAWSWEEPER_WEBHOOK_SECRET" | |
| started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| mkdir -p .artifacts | |
| printf '{"started_at":"%s"}\n' "$started_at" > .artifacts/apply-observability-context.json | |
| echo "started_at=$started_at" >> "$GITHUB_OUTPUT" | |
| node scripts/publish-apply-observability.mjs | |
| - name: Create target write token | |
| id: target-write-token | |
| continue-on-error: true | |
| uses: ./.github/actions/create-target-write-token | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| owner: ${{ steps.target.outputs.target_repo_owner }} | |
| repository: ${{ steps.target.outputs.target_repo_name }} | |
| - name: Create state token | |
| id: state-token | |
| uses: ./.github/actions/create-state-token | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| - uses: ./.github/actions/setup-state | |
| with: | |
| coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| records-repo-slugs: ${{ steps.target.outputs.target_slug }} | |
| hydrate-state-blobs: "false" | |
| token: ${{ steps.state-token.outputs.token }} | |
| - uses: ./.github/actions/setup-pnpm | |
| with: | |
| build-script: build:all | |
| - uses: ./.github/actions/setup-openclaw | |
| continue-on-error: true | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: ${{ needs.apply-proof.outputs.artifact_name }} | |
| path: .artifacts/apply-proof/pr-close-coverage-proof | |
| - name: Validate downloaded close coverage proof tree | |
| run: | | |
| set -euo pipefail | |
| source scripts/apply-workflow-helpers.sh | |
| # Two source items, with at most four canonical candidates per item. | |
| validate_coverage_proof_tree ".artifacts/apply-proof/pr-close-coverage-proof" 8 262144 2097152 | |
| - name: Reconcile before apply preselect | |
| if: ${{ !(github.event_name == 'schedule' && github.event.schedule == '6,21,36,51 * * * *') && !(github.event_name == 'workflow_dispatch' && (github.event.inputs.apply_sync_comments_only == 'true' || github.event.inputs.apply_item_numbers == '__cursor__')) }} | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| GH_TOKEN: ${{ steps.target.outputs.target_repo == 'openclaw/openclaw' && github.token || steps.target-write-token.outputs.token }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| source scripts/apply-workflow-helpers.sh | |
| item_numbers="${{ github.event_name == 'repository_dispatch' && github.event.client_payload.item_number || github.event.inputs.apply_item_numbers || '' }}" | |
| prepare_apply_reconciliation_args | |
| persist_reconciliation "${reconcile_args[@]}" | |
| - name: Apply unchanged proposed decisions with checkpoints | |
| id: apply-existing-run | |
| timeout-minutes: 70 | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| CLAWSWEEPER_PUBLIC_GH_TOKEN: ${{ steps.target.outputs.target_repo == 'openclaw/openclaw' && github.token || '' }} | |
| CLAWSWEEPER_APPLY_TOKEN_MINTED_AT_MS: ${{ steps.target-write-token.outputs.minted-at-ms }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$GH_TOKEN" | |
| source scripts/apply-workflow-helpers.sh | |
| initialize_apply_token_budget | |
| limit="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_limit || '40' }}" | |
| min_age_days="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_min_age_days || '0' }}" | |
| min_age_minutes="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_min_age_minutes || '' }}" | |
| apply_kind="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_kind || 'all' }}" | |
| apply_close_reasons="${{ github.event.inputs.apply_close_reasons || env.CLAWSWEEPER_AUTO_CLOSE_REASONS }}" | |
| stale_min_age_days="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_stale_min_age_days || '60' }}" | |
| close_delay_ms="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_close_delay_ms || '2000' }}" | |
| checkpoint_size="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_checkpoint_size || '40' }}" | |
| comment_sync_processed_limit=40 | |
| base_close_processed_limit=600 | |
| close_processed_limit="$base_close_processed_limit" | |
| adaptive_apply_scan_reason="base_window" | |
| sync_batch_size="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_limit || '40' }}" | |
| item_numbers="${{ github.event_name == 'repository_dispatch' && github.event.client_payload.item_number || github.event.inputs.apply_item_numbers || '' }}" | |
| scheduled_comment_sync="${{ github.event_name == 'schedule' && github.event.schedule == '6,21,36,51 * * * *' && 'true' || 'false' }}" | |
| sync_open_pr_batch="$scheduled_comment_sync" | |
| if [ "$item_numbers" = "__cursor__" ]; then | |
| sync_open_pr_batch="true" | |
| item_numbers="" | |
| fi | |
| explicit_item_numbers="$item_numbers" | |
| auto_selected_apply_batch=false | |
| sync_comments_only="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_sync_comments_only || 'false' }}" | |
| product_direction_enabled=false | |
| case "${CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED,,}" in | |
| true|1|yes|on) product_direction_enabled=true ;; | |
| esac | |
| unsponsored_feature_enabled=false | |
| case "${CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED,,}" in | |
| true|1|yes|on) unsponsored_feature_enabled=true ;; | |
| esac | |
| author_pr_budget_enabled=false | |
| case "${CLAWSWEEPER_AUTHOR_PR_BUDGET_CLOSE_ENABLED,,}" in | |
| true|1|yes|on) author_pr_budget_enabled=true ;; | |
| esac | |
| if [ "$sync_comments_only" != "true" ] && [ "$product_direction_enabled" != "true" ] && [ "$apply_close_reasons" != "all" ]; then | |
| apply_close_reasons="$(printf '%s\n' "$apply_close_reasons" | tr ',' '\n' | awk '$0 != "unconfirmed_product_direction" && NF' | paste -sd, -)" | |
| fi | |
| if [ "$sync_comments_only" != "true" ] && [ "$unsponsored_feature_enabled" != "true" ] && [ "$apply_close_reasons" != "all" ]; then | |
| apply_close_reasons="$(printf '%s\n' "$apply_close_reasons" | tr ',' '\n' | awk '$0 != "unsponsored_feature_request" && NF' | paste -sd, -)" | |
| fi | |
| if [ "$sync_comments_only" != "true" ] && [ "$author_pr_budget_enabled" != "true" ] && [ "$apply_close_reasons" != "all" ]; then | |
| apply_close_reasons="$(printf '%s\n' "$apply_close_reasons" | tr ',' '\n' | awk '$0 != "author_pr_budget_exceeded" && NF' | paste -sd, -)" | |
| fi | |
| if [ "$sync_comments_only" != "true" ] && [ -z "$apply_close_reasons" ]; then | |
| echo "No enabled close reasons remain after policy filtering; skipping apply." | |
| echo "APPLY_NOOP=true" >> "$GITHUB_ENV" | |
| exit 0 | |
| fi | |
| comment_sync_min_age_days="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_comment_sync_min_age_days || '7' }}" | |
| target_slug="$TARGET_REPO" | |
| target_slug="${target_slug//\//-}" | |
| cursor_path="results/comment-sync-cursors/${target_slug}.json" | |
| apply_cursor_path="results/apply-cursors/${target_slug}.json" | |
| next_cursor="" | |
| apply_ready_count="" | |
| candidate_counts_json="" | |
| cursor_advance_count="" | |
| coverage_proof_item_numbers="" | |
| if [ "$checkpoint_size" -gt 40 ]; then | |
| echo "Capping apply checkpoint size at 40" | |
| checkpoint_size=40 | |
| fi | |
| normalize_comment_sync_mode | |
| prepare_comment_sync_batch | |
| sync_comments_arg=() | |
| if [ "$sync_comments_only" = "true" ]; then | |
| sync_comments_arg=(--sync-comments-only) | |
| fi | |
| prepare_comment_sync_cursor_arg | |
| min_age_minutes_arg=() | |
| if [ -n "$min_age_minutes" ]; then | |
| min_age_minutes_arg=(--min-age-minutes "$min_age_minutes") | |
| fi | |
| select_adaptive_apply_batch | |
| mkdir -p .artifacts/apply-reports | |
| if [ "$sync_open_pr_batch" = "true" ] && [ -z "$item_numbers" ]; then | |
| batch_env=".artifacts/comment-sync-batch.env" | |
| pnpm run --silent workflow -- comment-sync-batch \ | |
| --target-repo "$TARGET_REPO" \ | |
| --apply-kind "$apply_kind" \ | |
| --batch-size "$sync_batch_size" \ | |
| --cursor-path "$cursor_path" > "$batch_env" | |
| cat "$batch_env" | |
| item_numbers="$(awk -F= '$1 == "item_numbers" { print $2 }' "$batch_env")" | |
| next_cursor="$(awk -F= '$1 == "next_cursor" { print $2 }' "$batch_env")" | |
| batch_count="$(awk -F= '$1 == "count" { print $2 }' "$batch_env")" | |
| trim_comment_sync_cycle_batch | |
| [ -n "$item_numbers" ] || batch_count=0 | |
| if [ "${batch_count:-0}" -eq 0 ]; then | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "Apply comments idle" \ | |
| --detail "No open PR review records were available for cursor-based comment sync. Cursor remains ${next_cursor:-0}." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| publish_status "chore: update idle sweep comment sync status" | |
| { | |
| echo "APPLY_CLOSED_TOTAL=0" | |
| echo "APPLY_LIMIT=1" | |
| echo "APPLY_MIN_AGE_DAYS=$min_age_days" | |
| echo "APPLY_MIN_AGE_MINUTES=$min_age_minutes" | |
| echo "APPLY_KIND=$apply_kind" | |
| echo "APPLY_CLOSE_REASONS=$apply_close_reasons" | |
| echo "APPLY_STALE_MIN_AGE_DAYS=$stale_min_age_days" | |
| echo "APPLY_CLOSE_DELAY_MS=$close_delay_ms" | |
| echo "APPLY_PROGRESS_EVERY=$progress_every" | |
| echo "APPLY_CHECKPOINT_SIZE=$checkpoint_size" | |
| echo "APPLY_ITEM_NUMBERS=" | |
| echo "APPLY_TARGET_REPO=$TARGET_REPO" | |
| echo "APPLY_SYNC_COMMENTS_ONLY=true" | |
| echo "APPLY_SYNC_OPEN_PR_BATCH=true" | |
| echo "APPLY_COMMENT_SYNC_MIN_AGE_DAYS=$comment_sync_min_age_days" | |
| echo "APPLY_NOOP=true" | |
| } >> "$GITHUB_ENV" | |
| exit 0 | |
| fi | |
| echo "Selected cursor-based comment sync batch: $item_numbers" | |
| fi | |
| prepare_apply_reconciliation_args | |
| persist_reconciliation "${reconcile_args[@]}" | |
| summarize_apply_candidate_quality | |
| if [ "$sync_comments_only" != "true" ] && [ -z "$item_numbers" ]; then | |
| auto_selected_apply_batch=true | |
| select_apply_candidate_inventory | |
| if [ -n "$item_numbers" ]; then | |
| select_bounded_coverage_proof_tail | |
| proposed_count="$(pnpm run --silent workflow -- count-csv --items "$item_numbers")" | |
| if [ "$proposed_count" -lt "$limit" ]; then | |
| limit="$proposed_count" | |
| fi | |
| echo "Selected $proposed_count from $close_processed_limit ($adaptive_apply_scan_reason); proof $coverage_proof_count/$coverage_proof_limit: $item_numbers; currently eligible: ${apply_ready_count:-unknown}; inventory: ${candidate_counts_json:-unknown}" | |
| fi | |
| fi | |
| item_numbers_arg=() | |
| if [ -n "$item_numbers" ]; then | |
| item_numbers_arg=(--item-numbers "$item_numbers") | |
| fi | |
| select_automatic_apply_runtime | |
| if [ "$sync_comments_only" != "true" ] && [ -z "$item_numbers" ]; then | |
| publish_automatic_apply_idle | |
| exit 0 | |
| fi | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "Apply in progress" \ | |
| --detail "Starting apply/comment-sync run for up to $limit fresh $apply_kind closes. Close reasons: $apply_close_reasons.$candidate_quality_detail Scan window: $close_processed_limit records ($adaptive_apply_scan_reason). Existing Codex automated review comments are updated in place when closing or when comment-only sync is stale by ${comment_sync_min_age_days} day(s); checkpoints commit every $checkpoint_size fresh closes; close delay is ${close_delay_ms}ms; sync-comments-only=$sync_comments_only; item numbers=${item_numbers:-all}." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| publish_status "chore: mark sweep apply in progress" | |
| closed_total=0 | |
| continue_apply=false | |
| checkpoint=0 | |
| if [ "$sync_comments_only" = "true" ]; then | |
| checkpoint=1 | |
| echo "::group::Apply checkpoint $checkpoint" | |
| echo "Syncing durable review comments only for item numbers: ${item_numbers:-all}" | |
| begin_canonical_record_mutation | |
| pnpm run apply-decisions -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --skip-dashboard \ | |
| --limit 0 \ | |
| --min-age-days "$min_age_days" \ | |
| "${min_age_minutes_arg[@]}" \ | |
| --apply-kind "$apply_kind" \ | |
| --apply-close-reasons "$apply_close_reasons" \ | |
| --stale-min-age-days "$stale_min_age_days" \ | |
| --close-delay-ms "$close_delay_ms" \ | |
| --progress-every "$progress_every" \ | |
| --processed-limit "$comment_sync_processed_limit" \ | |
| --max-runtime-ms 300000 \ | |
| --cursor-trace ".artifacts/comment-sync-trace-$checkpoint.json" \ | |
| "${comment_sync_cursor_arg[@]}" \ | |
| --comment-sync-min-age-days "$comment_sync_min_age_days" \ | |
| "${item_numbers_arg[@]}" \ | |
| "${sync_comments_arg[@]}" | |
| cp apply-report.json ".artifacts/apply-reports/apply-report-$checkpoint.json" | |
| result_count="$(pnpm run --silent workflow -- count-report --report ".artifacts/apply-reports/apply-report-$checkpoint.json")" | |
| synced_count="$(pnpm run --silent workflow -- count-actions --report ".artifacts/apply-reports/apply-report-$checkpoint.json" --action review_comment_synced)" | |
| comment_sync_checkpoint_items="$item_numbers" | |
| complete_comment_sync_batch ".artifacts/apply-reports/apply-report-$checkpoint.json" ".artifacts/comment-sync-trace-$checkpoint.json" | |
| publish_changes "chore: sync sweep review comments checkpoint $checkpoint" records apply-report.json results/comment-sync-cursors | |
| comment_sync_health_cursor_path="" | |
| comment_sync_health_cursor_required="false" | |
| comment_sync_health_processed_limit="$comment_sync_processed_limit" | |
| if [ "$sync_open_pr_batch" = "true" ]; then | |
| comment_sync_health_cursor_path="$cursor_path" | |
| comment_sync_health_cursor_required="true" | |
| comment_sync_health_processed_limit="$sync_batch_size" | |
| fi | |
| write_comment_sync_health ".artifacts/apply-reports/apply-report-$checkpoint.json" ".artifacts/apply-health-$checkpoint.json" | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "Apply comments synced" \ | |
| --detail "Comment-only apply checkpoint $checkpoint finished. Synced durable review comments: $synced_count. Result records: $result_count. Item numbers: ${comment_sync_checkpoint_items:-all}. Next cursor: ${next_cursor:-none}." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ | |
| --apply-health-file ".artifacts/apply-health-$checkpoint.json" | |
| publish_status "chore: update sweep comment sync status" | |
| report_apply_token_budget_stop ".artifacts/apply-reports/apply-report-$checkpoint.json" "$result_count" "unknown" | |
| echo "::endgroup::" | |
| fi | |
| while [ "$closed_total" -lt "$limit" ]; do | |
| if [ "$sync_comments_only" = "true" ]; then | |
| break | |
| fi | |
| remaining=$((limit - closed_total)) | |
| chunk_limit="$checkpoint_size" | |
| if [ "$remaining" -lt "$chunk_limit" ]; then | |
| chunk_limit="$remaining" | |
| fi | |
| checkpoint=$((checkpoint + 1)) | |
| cursor_trace_path=".artifacts/apply-cursor-trace-$checkpoint.json" | |
| cursor_trace_arg=() | |
| if [ "$auto_selected_apply_batch" = "true" ]; then | |
| cursor_trace_arg=(--cursor-trace "$cursor_trace_path") | |
| fi | |
| echo "::group::Apply checkpoint $checkpoint" | |
| begin_canonical_record_mutation | |
| CLAWSWEEPER_APPLY_CHECKPOINT="$checkpoint" pnpm run apply-decisions -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --skip-dashboard \ | |
| --limit "$chunk_limit" \ | |
| --min-age-days "$min_age_days" \ | |
| "${min_age_minutes_arg[@]}" \ | |
| --apply-kind "$apply_kind" \ | |
| --apply-close-reasons "$apply_close_reasons" \ | |
| --stale-min-age-days "$stale_min_age_days" \ | |
| --close-delay-ms "$close_delay_ms" \ | |
| --progress-every "$progress_every" \ | |
| --processed-limit "$close_processed_limit" \ | |
| --comment-sync-min-age-days "$comment_sync_min_age_days" \ | |
| "${item_numbers_arg[@]}" \ | |
| "${max_runtime_arg[@]}" \ | |
| "${cursor_trace_arg[@]}" \ | |
| "${sync_comments_arg[@]}" \ | |
| --artifact-dir .artifacts/apply-proof \ | |
| --require-precomputed-pr-close-coverage-proof | |
| cp apply-report.json ".artifacts/apply-reports/apply-report-$checkpoint.json" | |
| pnpm run workflow -- merge-apply-reports --dir .artifacts/apply-reports --output apply-report.json | |
| closed_in_chunk="$(pnpm run --silent workflow -- count-actions --report ".artifacts/apply-reports/apply-report-$checkpoint.json" --action closed)" | |
| result_count="$(pnpm run --silent workflow -- count-report --report ".artifacts/apply-reports/apply-report-$checkpoint.json")" | |
| closed_total=$((closed_total + closed_in_chunk)) | |
| echo "Checkpoint $checkpoint result_count=$result_count closed_in_chunk=$closed_in_chunk closed_total=$closed_total/$limit" | |
| apply_publish_paths=(records apply-report.json) | |
| if [ "$auto_selected_apply_batch" = "true" ]; then | |
| pnpm run workflow -- write-apply-cursor \ | |
| --cursor-path "$apply_cursor_path" \ | |
| --report ".artifacts/apply-reports/apply-report-$checkpoint.json" \ | |
| --target-repo "$TARGET_REPO" \ | |
| --item-numbers "$item_numbers" \ | |
| --coverage-proof-item-numbers "$coverage_proof_item_numbers" \ | |
| --cursor-trace "$cursor_trace_path" | |
| cursor_advance_count="$(pnpm run --silent workflow -- apply-cursor-advance-count \ | |
| --report ".artifacts/apply-reports/apply-report-$checkpoint.json" \ | |
| --item-numbers "$item_numbers" \ | |
| --cursor-trace "$cursor_trace_path")" | |
| apply_publish_paths+=(results/apply-cursors) | |
| fi | |
| examined_count="$(apply_checkpoint_examined_count)" | |
| publish_changes "chore: apply sweep decisions checkpoint $checkpoint" "${apply_publish_paths[@]}" | |
| if [ "$auto_selected_apply_batch" = "true" ]; then | |
| select_apply_candidate_inventory false | |
| fi | |
| close_health_cursor_path="" | |
| if [ "$auto_selected_apply_batch" = "true" ]; then | |
| close_health_cursor_path="$apply_cursor_path" | |
| fi | |
| write_apply_health ".artifacts/apply-reports/apply-report-$checkpoint.json" ".artifacts/apply-health-$checkpoint.json" "close" "$close_processed_limit" "$close_health_cursor_path" "$auto_selected_apply_batch" "$apply_ready_count" "15" "$cursor_advance_count" "$candidate_counts_json" | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "Apply in progress" \ | |
| --detail "Checkpoint $checkpoint finished. Fresh closes in checkpoint: $closed_in_chunk. Total fresh closes in this run: $closed_total/$limit. Candidates examined: $examined_count. Action records: $result_count, including durable review comment syncs." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ | |
| --apply-health-file ".artifacts/apply-health-$checkpoint.json" | |
| publish_status "chore: update sweep apply checkpoint $checkpoint status" | |
| echo "::endgroup::" | |
| drop_bounded_coverage_proof_tail "$cursor_trace_path" | |
| if apply_checkpoint_runtime_reached ".artifacts/apply-reports/apply-report-$checkpoint.json" "$result_count" "$((limit - closed_total))"; then break; fi | |
| if [ "$result_count" -ge "$close_processed_limit" ]; then | |
| if [ "$closed_in_chunk" -gt 0 ]; then | |
| echo "Close scan reached its $close_processed_limit-record budget; queueing a fresh-token continuation." | |
| continue_apply=true | |
| elif [ "$auto_selected_apply_batch" = "true" ]; then | |
| echo "Close scan reached its $close_processed_limit-record budget with no fresh closes; cursor is persisted and the next scheduled apply run will advance the next window." | |
| else | |
| echo "Explicit close scan reached its $close_processed_limit-record budget without fresh closes; stopping to avoid a retry loop." | |
| fi | |
| break | |
| fi | |
| if [ "$result_count" -eq 0 ]; then | |
| echo "No more applicable proposed closes found." | |
| break | |
| fi | |
| if [ "$closed_in_chunk" -eq 0 ]; then | |
| echo "Checkpoint made no fresh closes; stopping to avoid a retry loop." | |
| break | |
| fi | |
| continue_apply=true | |
| break | |
| done | |
| final_health_mode="close" | |
| final_health_processed_limit="$close_processed_limit" | |
| final_health_cursor_path="" | |
| final_health_cursor_required="$auto_selected_apply_batch" | |
| final_health_candidate_count="" | |
| final_health_scheduled_interval_minutes="" | |
| final_health_cursor_advance_count="" | |
| final_health_candidate_counts_json="" | |
| if [ "$auto_selected_apply_batch" = "true" ]; then | |
| final_health_cursor_path="$apply_cursor_path" | |
| final_health_candidate_count="$apply_ready_count" | |
| final_health_scheduled_interval_minutes="15" | |
| final_health_cursor_advance_count="$cursor_advance_count" | |
| final_health_candidate_counts_json="$candidate_counts_json" | |
| fi | |
| if [ "$sync_comments_only" = "true" ]; then | |
| final_health_mode="comment_sync" | |
| final_health_processed_limit="$comment_sync_processed_limit" | |
| final_health_cursor_required="false" | |
| final_health_candidate_count="" | |
| final_health_scheduled_interval_minutes="" | |
| final_health_cursor_advance_count="" | |
| final_health_candidate_counts_json="" | |
| if [ "$sync_open_pr_batch" = "true" ]; then | |
| final_health_cursor_path="$cursor_path" | |
| final_health_cursor_required="true" | |
| final_health_processed_limit="$sync_batch_size" | |
| final_health_cursor_advance_count="$comment_sync_cursor_advance_count" | |
| fi | |
| fi | |
| write_apply_health "apply-report.json" ".artifacts/apply-health-final.json" "$final_health_mode" "$final_health_processed_limit" "$final_health_cursor_path" "$final_health_cursor_required" "$final_health_candidate_count" "$final_health_scheduled_interval_minutes" "$final_health_cursor_advance_count" "$final_health_candidate_counts_json" | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "Apply finished" \ | |
| --detail "Apply/comment-sync run finished with $closed_total fresh closes out of requested limit $limit. See apply-report.json for per-item results." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ | |
| --apply-health-file ".artifacts/apply-health-final.json" | |
| publish_status "chore: mark sweep apply finished" | |
| next_apply_item_numbers="$item_numbers" | |
| if [ "$auto_selected_apply_batch" = "true" ] && [ -z "$explicit_item_numbers" ]; then | |
| next_apply_item_numbers="" | |
| fi | |
| { | |
| echo "APPLY_CLOSED_TOTAL=$closed_total" | |
| echo "APPLY_LIMIT=$limit" | |
| echo "APPLY_CONTINUE=$continue_apply" | |
| echo "APPLY_MIN_AGE_DAYS=$min_age_days" | |
| echo "APPLY_MIN_AGE_MINUTES=$min_age_minutes" | |
| echo "APPLY_KIND=$apply_kind" | |
| echo "APPLY_CLOSE_REASONS=$apply_close_reasons" | |
| echo "APPLY_STALE_MIN_AGE_DAYS=$stale_min_age_days" | |
| echo "APPLY_CLOSE_DELAY_MS=$close_delay_ms" | |
| echo "APPLY_PROGRESS_EVERY=$progress_every" | |
| echo "APPLY_CHECKPOINT_SIZE=$checkpoint_size" | |
| echo "APPLY_ITEM_NUMBERS=$next_apply_item_numbers" | |
| echo "APPLY_TARGET_REPO=$TARGET_REPO" | |
| echo "APPLY_SYNC_COMMENTS_ONLY=$sync_comments_only" | |
| echo "APPLY_SYNC_OPEN_PR_BATCH=$sync_open_pr_batch" | |
| echo "APPLY_AUTO_SELECTED_BATCH=$auto_selected_apply_batch" | |
| echo "APPLY_COMMENT_SYNC_MIN_AGE_DAYS=$comment_sync_min_age_days" | |
| echo "APPLY_CLOSE_PROCESSED_LIMIT=$close_processed_limit" | |
| echo "APPLY_ADAPTIVE_SCAN_REASON=$adaptive_apply_scan_reason" | |
| echo "APPLY_CANDIDATE_QUALITY_SUMMARY=$candidate_quality_summary" | |
| } >> "$GITHUB_ENV" | |
| - name: Export primary apply result | |
| id: primary-apply-result | |
| if: ${{ always() && !cancelled() && steps.apply-existing-run.outcome == 'success' }} | |
| run: echo "succeeded=true" >> "$GITHUB_OUTPUT" | |
| - name: Finalize apply action ledger | |
| id: finalize-apply | |
| if: ${{ always() }} | |
| env: | |
| APPLY_OUTCOME: ${{ steps.apply-existing-run.outcome || 'not_started' }} | |
| run: | | |
| args=() | |
| if [ "$APPLY_OUTCOME" = "cancelled" ]; then | |
| args=(--interrupt-open-attempts --reason cancelled) | |
| elif [ "$APPLY_OUTCOME" != "success" ]; then | |
| args=(--interrupt-open-attempts --reason workflow_failed) | |
| fi | |
| pnpm run --silent finalize-action-events -- "${args[@]}" | |
| - name: Retry final apply status publication | |
| id: retry-final-apply-status-publication | |
| if: ${{ always() && !cancelled() && steps.primary-apply-result.outputs.succeeded == 'true' }} | |
| continue-on-error: true | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| run: | | |
| if [ "${APPLY_NOOP:-false}" = "true" ]; then | |
| echo "No proposed closes were ready; no final apply status to retry." | |
| exit 0 | |
| fi | |
| target_slug="$APPLY_TARGET_REPO" | |
| target_slug="${target_slug//\//-}" | |
| pnpm run repair:publish-main -- \ | |
| --message "chore: mark sweep apply finished" \ | |
| --path "results/sweep-status/${target_slug}.json" \ | |
| --rebase-strategy theirs | |
| - name: Publish apply action events | |
| id: publish-apply-action-events | |
| if: ${{ always() }} | |
| env: | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| run: | | |
| set -euo pipefail | |
| source_root="$CLAWSWEEPER_ACTION_LEDGER_OUTPUT_ROOT" | |
| if [ ! -d "$source_root/ledger" ]; then | |
| echo "No apply action event shards were finalized." | |
| exit 0 | |
| fi | |
| event_paths_file=".artifacts/apply-action-ledger-paths.txt" | |
| mkdir -p .artifacts | |
| pnpm run --silent publish-action-events -- \ | |
| --source-root "$source_root" \ | |
| --state-root . \ | |
| --expected-producer-job "$GITHUB_JOB" | | |
| jq -r '.paths[]?' | | |
| sort -u > "$event_paths_file" | |
| if [ ! -s "$event_paths_file" ]; then | |
| echo "Apply action event shards existed but no paths were imported." >&2 | |
| exit 1 | |
| fi | |
| node dist/clawsweeper.js publish-action-event-paths \ | |
| --paths-file "$event_paths_file" | |
| - name: Continue apply sweep | |
| if: ${{ always() && !cancelled() && steps.primary-apply-result.outputs.succeeded == 'true' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| if [ "${APPLY_NOOP:-false}" = "true" ]; then | |
| echo "No proposed closes were ready; not queueing another apply run." | |
| exit 0 | |
| fi | |
| if [ "${APPLY_CONTINUE:-false}" != "true" ]; then | |
| echo "Apply checkpoint has no continuation work; not queueing another apply run." | |
| exit 0 | |
| fi | |
| can_share_apply_continuation=false | |
| if [ "${APPLY_AUTO_SELECTED_BATCH:-false}" = "true" ] && | |
| [ -z "${APPLY_ITEM_NUMBERS:-}" ] && | |
| [ "${APPLY_LIMIT:-40}" = "40" ] && | |
| [ "${APPLY_MIN_AGE_DAYS:-0}" = "0" ] && | |
| [ -z "${APPLY_MIN_AGE_MINUTES:-}" ] && | |
| [ "${APPLY_KIND:-all}" = "all" ] && | |
| [ "${APPLY_CLOSE_REASONS:-all}" = "all" ] && | |
| [ "${APPLY_STALE_MIN_AGE_DAYS:-60}" = "60" ] && | |
| [ "${APPLY_CLOSE_DELAY_MS:-2000}" = "2000" ] && | |
| [ "${APPLY_CHECKPOINT_SIZE:-40}" = "40" ] && | |
| [ "${APPLY_COMMENT_SYNC_MIN_AGE_DAYS:-7}" = "7" ]; then | |
| can_share_apply_continuation=true | |
| fi | |
| if [ "$can_share_apply_continuation" != "true" ]; then | |
| echo "Apply continuation uses explicit or non-default inputs; preserving exact continuation dispatch." | |
| else | |
| continuation_env="$(mktemp)" | |
| runs_json="$(mktemp)" | |
| { | |
| for run_status in in_progress pending queued waiting requested; do | |
| gh api --paginate "repos/${{ github.repository }}/actions/runs?per_page=100&status=${run_status}" \ | |
| --jq '.workflow_runs[] | {databaseId:.id, workflowPath:.path, displayTitle:.display_title, status:.status, createdAt:.created_at, updatedAt:.updated_at}' 2>/dev/null || true | |
| done | |
| } | jq -s '.' > "$runs_json" | |
| pnpm run --silent workflow -- apply-continuation-blocker \ | |
| --runs "$runs_json" \ | |
| --current-run-id "${{ github.run_id }}" \ | |
| --target-repo "${APPLY_TARGET_REPO:-openclaw/openclaw}" > "$continuation_env" | |
| rm -f "$runs_json" | |
| cat "$continuation_env" | |
| APPLY_CONTINUATION_BLOCKED="$(awk -F= '$1 == "APPLY_CONTINUATION_BLOCKED" { print $2 }' "$continuation_env")" | |
| APPLY_CONTINUATION_BLOCKER_RUN_ID="$(awk -F= '$1 == "APPLY_CONTINUATION_BLOCKER_RUN_ID" { print $2 }' "$continuation_env")" | |
| APPLY_CONTINUATION_BLOCKER_STATUS="$(awk -F= '$1 == "APPLY_CONTINUATION_BLOCKER_STATUS" { print $2 }' "$continuation_env")" | |
| rm -f "$continuation_env" | |
| if [ "${APPLY_CONTINUATION_BLOCKED:-false}" = "true" ]; then | |
| echo "Apply continuation for $APPLY_TARGET_REPO is already covered by ${APPLY_CONTINUATION_BLOCKER_STATUS:-active} run ${APPLY_CONTINUATION_BLOCKER_RUN_ID:-unknown}; not queueing another apply run." | |
| echo "The existing default cursor run will continue the lane." | |
| exit 0 | |
| fi | |
| fi | |
| run_workflow_with_retry() { | |
| local label="$1" | |
| shift | |
| for attempt in 1 2 3; do | |
| if gh workflow run sweep.yml "$@"; then | |
| return 0 | |
| fi | |
| echo "::warning::Failed to queue ${label} on attempt ${attempt}; retrying." | |
| sleep "$((attempt * 10))" | |
| done | |
| echo "::warning::Unable to queue ${label} after three attempts; scheduled runs will backstop it." | |
| return 0 | |
| } | |
| echo "Apply reached requested limit; queueing another apply run." | |
| run_workflow_with_retry "apply continuation" \ | |
| --ref main \ | |
| -f apply_existing=true \ | |
| -f target_repo="$APPLY_TARGET_REPO" \ | |
| -f apply_limit="$APPLY_LIMIT" \ | |
| -f apply_min_age_days="$APPLY_MIN_AGE_DAYS" \ | |
| -f apply_min_age_minutes="$APPLY_MIN_AGE_MINUTES" \ | |
| -f apply_kind="$APPLY_KIND" \ | |
| -f apply_close_reasons="$APPLY_CLOSE_REASONS" \ | |
| -f apply_stale_min_age_days="$APPLY_STALE_MIN_AGE_DAYS" \ | |
| -f apply_item_numbers="$APPLY_ITEM_NUMBERS" \ | |
| -f apply_sync_comments_only="$APPLY_SYNC_COMMENTS_ONLY" \ | |
| -f apply_comment_sync_min_age_days="$APPLY_COMMENT_SYNC_MIN_AGE_DAYS" \ | |
| -f apply_close_delay_ms="$APPLY_CLOSE_DELAY_MS" \ | |
| -f apply_checkpoint_size="$APPLY_CHECKPOINT_SIZE" | |
| - name: Save apply telemetry context | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| run: | | |
| mkdir -p .artifacts | |
| noop=false | |
| started_at="${{ steps.apply-telemetry-start.outputs.started_at || '' }}" | |
| if [ "${APPLY_NOOP:-false}" = "true" ]; then | |
| noop=true | |
| fi | |
| if [ -n "$started_at" ]; then | |
| printf '{"noop":%s,"started_at":"%s"}\n' "$noop" "$started_at" > .artifacts/apply-observability-context.json | |
| else | |
| printf '{"noop":%s}\n' "$noop" > .artifacts/apply-observability-context.json | |
| fi | |
| - name: Upload apply telemetry health | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: apply-observability-health-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: | | |
| .artifacts/apply-health-*.json | |
| .artifacts/apply-observability-context.json | |
| include-hidden-files: true | |
| if-no-files-found: ignore | |
| retention-days: 1 | |
| - name: Queue review backstops | |
| if: ${{ always() && !cancelled() && steps.primary-apply-result.outputs.succeeded == 'true' && steps.target.outputs.target_repo == 'openclaw/openclaw' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| hot_intake_shards="$(pnpm run --silent workflow -- limit review_shards.hot_intake_default)" | |
| normal_shards="$(pnpm run --silent workflow -- limit review_shards.normal_default)" | |
| runs_json="$({ | |
| gh api "repos/${{ github.repository }}/actions/runs?per_page=100" \ | |
| --jq '.workflow_runs[] | {databaseId:.id, workflowPath:.path, displayTitle:.display_title, status:.status, createdAt:.created_at, updatedAt:.updated_at}' 2>/dev/null \ | |
| || true | |
| } | jq -s 'unique_by(.databaseId)')" | |
| eval "$( | |
| RUNS_JSON="$runs_json" node <<'NODE' | |
| const runs = JSON.parse(process.env.RUNS_JSON || "[]"); | |
| const now = Date.now(); | |
| const staleQueuedMs = 6 * 60 * 60 * 1000; | |
| const active = new Set(["in_progress", "pending", "queued", "waiting", "requested"]); | |
| const queued = new Set(["pending", "queued", "waiting", "requested"]); | |
| function activeRun(run) { | |
| if (!active.has(String(run.status))) return false; | |
| if (!queued.has(String(run.status))) return true; | |
| const lastChangedAt = Date.parse(String(run.updatedAt || run.createdAt || "")); | |
| return !Number.isFinite(lastChangedAt) || now - lastChangedAt <= staleQueuedMs; | |
| } | |
| function recent(title, windowMs) { | |
| return runs.some((run) => { | |
| if (run.workflowPath !== ".github/workflows/sweep.yml") return false; | |
| if (run.displayTitle !== title) return false; | |
| if (activeRun(run)) return true; | |
| const createdAt = Date.parse(String(run.createdAt || "")); | |
| return Number.isFinite(createdAt) && now - createdAt < windowMs; | |
| }); | |
| } | |
| console.log(`HOT_RECENT=${recent("Review hot ClawSweeper items", 10 * 60 * 1000) ? "true" : "false"}`); | |
| console.log(`REVIEW_RECENT=${recent("Review ClawSweeper items", 70 * 60 * 1000) ? "true" : "false"}`); | |
| NODE | |
| )" | |
| run_workflow_with_retry() { | |
| local label="$1" | |
| shift | |
| for attempt in 1 2 3; do | |
| if gh workflow run sweep.yml "$@"; then | |
| return 0 | |
| fi | |
| echo "::warning::Failed to queue ${label} on attempt ${attempt}; retrying." | |
| sleep "$((attempt * 10))" | |
| done | |
| echo "::warning::Unable to queue ${label} after three attempts; scheduled runs will backstop it." | |
| return 0 | |
| } | |
| if [ "$HOT_RECENT" != "true" ]; then | |
| echo "No recent hot-intake run found; queueing backstop." | |
| run_workflow_with_retry "hot review backstop" \ | |
| --ref main \ | |
| -f apply_existing=false \ | |
| -f hot_intake=true \ | |
| -f target_repo=openclaw/openclaw \ | |
| -f batch_size=1 \ | |
| -f shard_count="$hot_intake_shards" \ | |
| -f codex_timeout_ms=1200000 | |
| fi | |
| if [ "$REVIEW_RECENT" != "true" ]; then | |
| echo "No recent normal review run found; queueing backstop." | |
| run_workflow_with_retry "normal review backstop" \ | |
| --ref main \ | |
| -f apply_existing=false \ | |
| -f hot_intake=false \ | |
| -f target_repo=openclaw/openclaw \ | |
| -f batch_size=3 \ | |
| -f shard_count="$normal_shards" \ | |
| -f codex_timeout_ms=1200000 | |
| fi | |
| publish-apply-observability: | |
| name: Publish apply telemetry | |
| needs: [apply-proof, publish-apply-proof-action-ledger, apply-existing] | |
| if: ${{ always() && (needs.apply-proof.result == 'failure' || needs.publish-apply-proof-action-ledger.result == 'failure' || needs.apply-existing.result == 'failure' || needs.apply-existing.result == 'cancelled' || needs.apply-existing.result == 'success') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - name: Download apply telemetry health | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: apply-observability-health-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: .artifacts | |
| - name: Publish apply telemetry | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| env: | |
| QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }} | |
| CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }} | |
| APPLY_OUTCOME: ${{ (needs.apply-proof.result == 'failure' || needs.apply-existing.result == 'failure') && 'failure' || needs.apply-existing.result == 'cancelled' && 'cancelled' || needs.apply-existing.result == 'success' && 'success' || 'skipped' }} | |
| ACTION_LEDGER_OUTCOME: ${{ (needs.publish-apply-proof-action-ledger.result == 'failure' || needs.apply-existing.outputs.action_ledger_outcome == 'failure') && 'failure' || needs.apply-existing.outputs.action_ledger_outcome || 'not_started' }} | |
| STATE_STATUS_OUTCOME: ${{ needs.apply-existing.outputs.state_status_outcome || 'not_started' }} | |
| STATE_PUBLICATION_OUTCOME: ${{ needs.apply-existing.outputs.state_publication_outcome || 'not_started' }} | |
| APPLY_STARTED_AT: ${{ needs.apply-existing.outputs.observability_started_at || '' }} | |
| TARGET_REPO: ${{ needs.apply-existing.outputs.target_repo || needs.apply-proof.outputs.target_repo || (github.event_name == 'schedule' && github.event.schedule == '8,23,38,53 * * * *' && 'openclaw/clawhub') || github.event.inputs.target_repo || 'openclaw/openclaw' }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$CLAWSWEEPER_WEBHOOK_SECRET" | |
| node scripts/publish-apply-observability.mjs \ | |
| --health-file ".artifacts/apply-health-final.json" |