mirror of
https://github.com/openclaw/openclaw.git
synced 2026-09-28 14:12:28 +08:00
* chore(deps): refresh dependencies with a seven-day cutoff * fix(deps): preserve Teams and jsdom integration contracts Use the Teams SDK public token and processing APIs while keeping SSO sender checks ahead of native token operations. Remove obsolete ambient declarations and route workarounds, and cover the SDK routing with real processing tests. Adapt the test environment to jsdom private-field bindings, preserve file bytes and registry cleanup, and preload it through native Node and Bun workers. * fix(test): preserve jsdom window and fixture contracts * fix(ci): keep typecheck cache reuse within matching inputs
450 lines
20 KiB
YAML
450 lines
20 KiB
YAML
name: ClawSweeper Dispatch
|
|
|
|
on:
|
|
issues:
|
|
types: [opened, reopened, edited, labeled, unlabeled]
|
|
issue_comment:
|
|
types: [created, edited]
|
|
push:
|
|
branches: [main]
|
|
pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned external dispatch; no checkout or untrusted PR code execution
|
|
types: [opened, reopened, synchronize, ready_for_review, edited, labeled, unlabeled]
|
|
pull_request_review:
|
|
types: [submitted, edited, dismissed]
|
|
pull_request_review_comment:
|
|
types: [created, edited]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
dispatch:
|
|
runs-on: ubuntu-latest
|
|
if: >-
|
|
${{ (
|
|
(github.event.action != 'edited' ||
|
|
(github.event_name != 'issues' && github.event_name != 'pull_request_target') ||
|
|
!github.event.changes || toJSON(github.event.changes) != '{}') &&
|
|
(github.event_name != 'issue_comment' ||
|
|
(github.actor != 'clawsweeper[bot]' && github.actor != 'openclaw-clawsweeper[bot]')) &&
|
|
!(
|
|
endsWith(github.actor, '[bot]') &&
|
|
(github.event.action == 'labeled' || github.event.action == 'unlabeled') &&
|
|
!(
|
|
github.event_name == 'issues' &&
|
|
github.event.action == 'labeled' &&
|
|
github.event.label.name == 'stale' &&
|
|
contains(github.event.issue.labels.*.name, 'bug') &&
|
|
(github.actor_id == '257215752' || github.actor_id == '264559031')
|
|
)
|
|
)
|
|
) }}
|
|
# Admission runs before job concurrency, so an empty metadata edit cannot
|
|
# cancel a useful dispatch. Other changes retain the receiver's policy.
|
|
concurrency:
|
|
group: ${{ github.event_name == 'push' && format('clawsweeper-dispatch-{0}-{1}', github.repository, github.ref) || format('clawsweeper-dispatch-{0}-{1}', github.repository, github.event.issue.number || github.event.pull_request.number || github.run_id) }}
|
|
cancel-in-progress: ${{ github.event_name == 'push' || github.event.action == 'edited' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review' }}
|
|
env:
|
|
HAS_CLAWSWEEPER_APP_PRIVATE_KEY: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY != '' }}
|
|
CLAWSWEEPER_APP_CLIENT_ID: Iv23liOECG0slfuhz093
|
|
SUPERSEDES_IN_PROGRESS: ${{ (github.event.action == 'edited' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review') && 'true' || 'false' }}
|
|
steps:
|
|
- name: Debounce bursty metadata events
|
|
if: ${{ github.event.action == 'labeled' || github.event.action == 'unlabeled' }}
|
|
run: sleep 20
|
|
|
|
- name: Debounce main push dispatch
|
|
if: ${{ github.event_name == 'push' }}
|
|
run: sleep 45
|
|
|
|
- name: Install GitHub API backoff helper
|
|
run: |
|
|
cat > "$RUNNER_TEMP/github-api-backoff.sh" <<'BASH'
|
|
gh_api_with_retry() {
|
|
local attempt output status lower_output
|
|
for attempt in 1 2 3 4 5; do
|
|
if output="$(gh api "$@" 2>&1)"; then
|
|
printf '%s\n' "$output"
|
|
return 0
|
|
else
|
|
status=$?
|
|
fi
|
|
lower_output="${output,,}"
|
|
if [[ "$lower_output" != *"rate limit"* && "$output" != *"HTTP 429"* ]]; then
|
|
printf '%s\n' "$output" >&2
|
|
return "$status"
|
|
fi
|
|
echo "::warning::GitHub API throttled ClawSweeper dispatch on attempt ${attempt}; retrying after backoff." >&2
|
|
sleep $((attempt * attempt * 5))
|
|
done
|
|
printf '%s\n' "$output" >&2
|
|
return "$status"
|
|
}
|
|
BASH
|
|
|
|
- name: Create ClawSweeper dispatch token
|
|
id: token
|
|
if: ${{ env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == '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: openclaw
|
|
repositories: clawsweeper
|
|
permission-contents: write
|
|
|
|
- name: Pre-filter ClawSweeper comment
|
|
id: comment_filter
|
|
if: ${{ github.event_name == 'issue_comment' }}
|
|
env:
|
|
COMMENT_BODY: ${{ github.event.comment.body }}
|
|
run: |
|
|
set -euo pipefail
|
|
if grep -Eiq '(^|[[:space:]])@(clawsweeper|openclaw-clawsweeper)\b(\[bot\])?|(^|[[:space:]])/(clawsweeper|review|autoclose|auto([[:space:]]+|-)?merge)\b' <<< "$COMMENT_BODY"; then
|
|
echo "is_command=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "is_command=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Create target comment token
|
|
id: target_token
|
|
if: >-
|
|
${{
|
|
github.event_name == 'issue_comment' &&
|
|
steps.comment_filter.outputs.is_command == 'true' &&
|
|
env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == '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: ${{ github.repository_owner }}
|
|
repositories: ${{ github.event.repository.name }}
|
|
permission-issues: write
|
|
permission-pull-requests: read
|
|
|
|
- name: Create target PR acknowledgement token
|
|
id: pr_ack_token
|
|
if: >-
|
|
${{
|
|
github.event_name == 'pull_request_target' &&
|
|
(
|
|
github.event.action == 'ready_for_review' ||
|
|
(github.event.action == 'opened' && github.event.pull_request.draft == false)
|
|
) &&
|
|
env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == '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: ${{ github.repository_owner }}
|
|
repositories: ${{ github.event.repository.name }}
|
|
permission-issues: write
|
|
|
|
- name: Acknowledge received pull request
|
|
if: >-
|
|
${{
|
|
github.event_name == 'pull_request_target' &&
|
|
(
|
|
github.event.action == 'ready_for_review' ||
|
|
(github.event.action == 'opened' && github.event.pull_request.draft == false)
|
|
) &&
|
|
env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true'
|
|
}}
|
|
continue-on-error: true
|
|
env:
|
|
ACK_TOKEN: ${{ steps.pr_ack_token.outputs.token }}
|
|
TARGET_REPO: ${{ github.repository }}
|
|
ITEM_NUMBER: ${{ github.event.pull_request.number }}
|
|
SOURCE_ACTION: ${{ github.event.action }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "$ACK_TOKEN" ]; then
|
|
echo "::notice::Skipping ClawSweeper pull request acknowledgement because no target credential is configured."
|
|
exit 0
|
|
fi
|
|
has_ack_marker() {
|
|
jq -e \
|
|
--arg marker_prefix "clawsweeper-pr-ack:" \
|
|
--arg marker_suffix " item=$ITEM_NUMBER -->" \
|
|
'any(.[]; (.body // "") as $body | ($body | contains($marker_prefix)) and ($body | contains($marker_suffix)))' \
|
|
<<< "$1" >/dev/null
|
|
}
|
|
comments="$(GH_TOKEN="$ACK_TOKEN" gh api \
|
|
"repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments?per_page=100")"
|
|
if has_ack_marker "$comments"; then
|
|
echo "ClawSweeper pull request acknowledgement already exists."
|
|
exit 0
|
|
fi
|
|
# opened and ready_for_review can fire seconds apart for the same
|
|
# pull request, and both runs can list comments before either
|
|
# acknowledgement is visible. Wait, then recheck right before
|
|
# posting; a superseding run cancels this one while it sleeps.
|
|
sleep 15
|
|
comments="$(GH_TOKEN="$ACK_TOKEN" gh api \
|
|
"repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments?per_page=100")"
|
|
if has_ack_marker "$comments"; then
|
|
echo "ClawSweeper pull request acknowledgement already exists."
|
|
exit 0
|
|
fi
|
|
ack_body="$(printf '%s\n' \
|
|
"<!-- clawsweeper-pr-ack:$SOURCE_ACTION item=$ITEM_NUMBER -->" \
|
|
"🦞👀" \
|
|
"ClawSweeper picked this up." \
|
|
"" \
|
|
"Pull request received. I will update this pull request when review starts.")"
|
|
ack_payload="$(jq -nc --arg body "$ack_body" '{body:$body}')"
|
|
GH_TOKEN="$ACK_TOKEN" gh api \
|
|
"repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments" \
|
|
--method POST \
|
|
--input - <<< "$ack_payload"
|
|
|
|
- name: Dispatch GitHub activity to ClawSweeper
|
|
env:
|
|
GH_TOKEN: ${{ steps.token.outputs.token }}
|
|
TARGET_REPO: ${{ github.repository }}
|
|
SOURCE_EVENT: ${{ github.event_name }}
|
|
SOURCE_ACTION: ${{ github.event.action }}
|
|
ACTOR: ${{ github.actor }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "$GH_TOKEN" ]; then
|
|
echo "::notice::Skipping GitHub activity dispatch because no ClawSweeper app token is configured."
|
|
exit 0
|
|
fi
|
|
. "$RUNNER_TEMP/github-api-backoff.sh"
|
|
activity="$(jq -c \
|
|
--arg target_repo "$TARGET_REPO" \
|
|
--arg event_name "$SOURCE_EVENT" \
|
|
--arg source_action "$SOURCE_ACTION" \
|
|
--arg actor "$ACTOR" \
|
|
'
|
|
def body_excerpt(value):
|
|
if (value // "" | type) == "string" then
|
|
((value // "") | gsub("\\s+"; " ") | .[0:1200])
|
|
else null end;
|
|
{
|
|
type: $event_name,
|
|
repo: $target_repo,
|
|
action: $source_action,
|
|
actor: $actor,
|
|
subject: (
|
|
if .pull_request then {
|
|
kind: "pull_request",
|
|
number: .pull_request.number,
|
|
title: .pull_request.title,
|
|
url: .pull_request.html_url,
|
|
state: (if .pull_request.merged == true then "merged" else .pull_request.state end)
|
|
} elif .issue then {
|
|
kind: (if .issue.pull_request then "pull_request" else "issue" end),
|
|
number: .issue.number,
|
|
title: .issue.title,
|
|
url: .issue.html_url,
|
|
state: .issue.state
|
|
} elif $event_name == "push" then {
|
|
kind: "push",
|
|
title: (.head_commit.message // .after // "push"),
|
|
url: (.head_commit.url // .compare),
|
|
state: .ref
|
|
} else {
|
|
kind: $event_name
|
|
} end),
|
|
comment: (if .comment then {
|
|
id: .comment.id,
|
|
url: .comment.html_url,
|
|
body_excerpt: body_excerpt(.comment.body)
|
|
} else null end),
|
|
review: (if .review then {
|
|
id: .review.id,
|
|
state: .review.state,
|
|
url: .review.html_url,
|
|
body_excerpt: body_excerpt(.review.body)
|
|
} else null end),
|
|
review_comment: (if .comment and $event_name == "pull_request_review_comment" then {
|
|
id: .comment.id,
|
|
path: .comment.path,
|
|
line: (.comment.line // .comment.original_line),
|
|
url: .comment.html_url,
|
|
body_excerpt: body_excerpt(.comment.body)
|
|
} else null end),
|
|
push: (if $event_name == "push" then {
|
|
before: .before,
|
|
after: .after,
|
|
ref: .ref,
|
|
compare: .compare,
|
|
head_commit: .head_commit.id
|
|
} else null end),
|
|
delivery_id: (.comment.id // .review.id // .pull_request.head.sha // .issue.updated_at // .after // env.GITHUB_RUN_ID)
|
|
} | del(.. | nulls)
|
|
' "$GITHUB_EVENT_PATH")"
|
|
payload="$(jq -nc --argjson activity "$activity" \
|
|
'{event_type:"github_activity",client_payload:{activity:$activity}}')"
|
|
if gh_api_with_retry repos/openclaw/clawsweeper/dispatches \
|
|
--method POST \
|
|
--input - <<< "$payload"; then
|
|
echo "Dispatched GitHub activity to ClawSweeper."
|
|
else
|
|
echo "::warning::Skipping GitHub activity dispatch because the configured credential could not dispatch to openclaw/clawsweeper."
|
|
fi
|
|
|
|
- name: Setup supported Node runtime
|
|
if: ${{ github.event_name == 'issues' || github.event_name == 'pull_request_target' }}
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version: "24.21.0"
|
|
package-manager-cache: false
|
|
|
|
- name: Dispatch exact ClawSweeper review
|
|
if: ${{ github.event_name == 'issues' || github.event_name == 'pull_request_target' }}
|
|
env:
|
|
GH_TOKEN: ${{ steps.token.outputs.token }}
|
|
TARGET_REPO: ${{ github.repository }}
|
|
TARGET_BRANCH: ${{ github.event.repository.default_branch }}
|
|
ITEM_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
|
|
ITEM_KIND: ${{ github.event_name == 'pull_request_target' && 'pull_request' || 'issue' }}
|
|
SOURCE_EVENT: ${{ github.event_name }}
|
|
SOURCE_ACTION: ${{ github.event.action }}
|
|
run: |
|
|
if [ -z "$GH_TOKEN" ]; then
|
|
echo "::notice::Skipping ClawSweeper dispatch because no ClawSweeper app token is configured. Not falling back to a maintainer token."
|
|
exit 0
|
|
fi
|
|
. "$RUNNER_TEMP/github-api-backoff.sh"
|
|
ingress_fingerprint="$(node <<'NODE'
|
|
const crypto = require("node:crypto");
|
|
const fs = require("node:fs");
|
|
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
|
|
const pullRequest = event.pull_request && typeof event.pull_request === "object"
|
|
? event.pull_request
|
|
: {};
|
|
const headSha = String(pullRequest.head?.sha || "").trim().toLowerCase();
|
|
const updatedAt = String(pullRequest.updated_at || "").trim();
|
|
if (
|
|
process.env.ITEM_KIND !== "pull_request" ||
|
|
!/^[0-9a-f]{40}$/.test(headSha) ||
|
|
!updatedAt
|
|
) {
|
|
process.stdout.write("");
|
|
} else {
|
|
process.stdout.write(
|
|
crypto
|
|
.createHash("sha256")
|
|
.update(
|
|
JSON.stringify({
|
|
version: 1,
|
|
target_repo: String(process.env.TARGET_REPO || "").toLowerCase(),
|
|
item_number: Number(process.env.ITEM_NUMBER),
|
|
action: String(process.env.SOURCE_ACTION || ""),
|
|
head_sha: headSha,
|
|
updated_at: updatedAt,
|
|
body: typeof pullRequest.body === "string" ? pullRequest.body : "",
|
|
label: String(event.label?.name || ""),
|
|
}),
|
|
)
|
|
.digest("hex"),
|
|
);
|
|
}
|
|
NODE
|
|
)"
|
|
payload="$(jq -nc \
|
|
--arg target_repo "$TARGET_REPO" \
|
|
--arg target_branch "$TARGET_BRANCH" \
|
|
--argjson item_number "$ITEM_NUMBER" \
|
|
--arg item_kind "$ITEM_KIND" \
|
|
--arg source_event "$SOURCE_EVENT" \
|
|
--arg source_action "$SOURCE_ACTION" \
|
|
--arg ingress_fingerprint "$ingress_fingerprint" \
|
|
--argjson supersedes_in_progress "$SUPERSEDES_IN_PROGRESS" \
|
|
'{event_type:"clawsweeper_item",client_payload:({target_repo:$target_repo,target_branch:$target_branch,item_number:$item_number,item_kind:$item_kind,source_event:$source_event,source_action:$source_action,supersedes_in_progress:$supersedes_in_progress} + (if $ingress_fingerprint != "" then {ingress_route:"target_dispatcher",ingress_fingerprint:$ingress_fingerprint} else {} end))}')"
|
|
if gh_api_with_retry repos/openclaw/clawsweeper/dispatches \
|
|
--method POST \
|
|
--input - <<< "$payload"; then
|
|
echo "Dispatched ClawSweeper review."
|
|
else
|
|
echo "::warning::Skipping ClawSweeper dispatch because the configured credential could not dispatch to openclaw/clawsweeper."
|
|
fi
|
|
|
|
- name: Acknowledge and dispatch ClawSweeper comment
|
|
if: >-
|
|
${{
|
|
github.event_name == 'issue_comment' &&
|
|
steps.comment_filter.outputs.is_command == 'true'
|
|
}}
|
|
env:
|
|
DISPATCH_TOKEN: ${{ steps.token.outputs.token }}
|
|
TARGET_TOKEN: ${{ steps.target_token.outputs.token }}
|
|
TARGET_REPO: ${{ github.repository }}
|
|
ITEM_NUMBER: ${{ github.event.issue.number }}
|
|
COMMENT_ID: ${{ github.event.comment.id }}
|
|
COMMENT_BODY: ${{ github.event.comment.body }}
|
|
AUTHOR_ASSOCIATION: ${{ github.event.comment.author_association }}
|
|
SOURCE_ACTION: ${{ github.event.action }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "$DISPATCH_TOKEN" ]; then
|
|
echo "::notice::Skipping ClawSweeper comment dispatch because no ClawSweeper app token is configured."
|
|
exit 0
|
|
fi
|
|
. "$RUNNER_TEMP/github-api-backoff.sh"
|
|
body_file="$RUNNER_TEMP/clawsweeper-comment-body.txt"
|
|
printf '%s\n' "$COMMENT_BODY" > "$body_file"
|
|
if [ -n "$TARGET_TOKEN" ]; then
|
|
err="$(mktemp)"
|
|
if GH_TOKEN="$TARGET_TOKEN" gh_api_with_retry -X POST \
|
|
-H "Accept: application/vnd.github+json" \
|
|
"repos/$TARGET_REPO/issues/comments/$COMMENT_ID/reactions" \
|
|
-f content="eyes" 2>"$err" >/dev/null; then
|
|
echo "Acknowledged ClawSweeper command comment."
|
|
elif grep -qi "HTTP 422\\|already exists" "$err"; then
|
|
echo "ClawSweeper command comment already acknowledged."
|
|
else
|
|
cat "$err" >&2
|
|
echo "::warning::Could not acknowledge ClawSweeper command comment."
|
|
fi
|
|
rm -f "$err"
|
|
else
|
|
echo "::notice::Skipping ClawSweeper comment acknowledgement because no target token is configured."
|
|
fi
|
|
status_comment_id=""
|
|
if [ -n "$TARGET_TOKEN" ]; then
|
|
case "$AUTHOR_ASSOCIATION" in
|
|
OWNER|MEMBER|COLLABORATOR)
|
|
status_body="$(printf '%s\n' \
|
|
"<!-- clawsweeper-command-ack:$COMMENT_ID -->" \
|
|
"🦞👀" \
|
|
"ClawSweeper picked this up." \
|
|
"" \
|
|
"Command router queued. I will update this comment with the next step.")"
|
|
status_payload="$(jq -nc --arg body "$status_body" '{body:$body}')"
|
|
status_err="$(mktemp)"
|
|
if status_response="$(GH_TOKEN="$TARGET_TOKEN" gh_api_with_retry \
|
|
"repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments" \
|
|
--method POST \
|
|
--input - <<< "$status_payload" 2>"$status_err")"; then
|
|
status_comment_id="$(jq -r '.id // empty' <<< "$status_response")"
|
|
else
|
|
cat "$status_err" >&2
|
|
echo "::warning::Could not create ClawSweeper queued status comment; dispatching command router without one."
|
|
fi
|
|
rm -f "$status_err"
|
|
;;
|
|
esac
|
|
fi
|
|
payload="$(jq -nc \
|
|
--arg target_repo "$TARGET_REPO" \
|
|
--argjson item_number "$ITEM_NUMBER" \
|
|
--argjson comment_id "$COMMENT_ID" \
|
|
--arg status_comment_id "$status_comment_id" \
|
|
--arg source_event "issue_comment" \
|
|
--arg source_action "$SOURCE_ACTION" \
|
|
'{event_type:"clawsweeper_comment",client_payload:({target_repo:$target_repo,item_number:$item_number,comment_id:$comment_id,source_event:$source_event,source_action:$source_action,max_comments:"1"} + (if $status_comment_id != "" then {status_comment_id:($status_comment_id|tonumber)} else {} end))}')"
|
|
if GH_TOKEN="$DISPATCH_TOKEN" gh_api_with_retry repos/openclaw/clawsweeper/dispatches \
|
|
--method POST \
|
|
--input - <<< "$payload"; then
|
|
echo "Dispatched ClawSweeper comment router."
|
|
else
|
|
echo "::warning::Skipping ClawSweeper comment dispatch because the configured credential could not dispatch to openclaw/clawsweeper."
|
|
fi
|