mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix: synchronize ClawSweeper dispatch identity (#3449)
This commit is contained in:
@@ -56,6 +56,7 @@ jobs:
|
||||
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 }}
|
||||
@@ -65,14 +66,52 @@ jobs:
|
||||
echo "::notice::Skipping ClawSweeper dispatch because no dispatch credential is configured."
|
||||
exit 0
|
||||
fi
|
||||
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,item_number:$item_number,item_kind:$item_kind,source_event:$source_event,source_action:$source_action,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))}')"
|
||||
gh api repos/openclaw/clawsweeper/dispatches \
|
||||
--method POST \
|
||||
--input - <<< "$payload"
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
/* @vitest-environment node */
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
|
||||
type WorkflowStep = {
|
||||
env?: Record<string, string>;
|
||||
name?: string;
|
||||
run?: string;
|
||||
};
|
||||
|
||||
const behaviorIt = process.platform === "win32" ? it.skip : it;
|
||||
|
||||
function executeExactReview(run: string, event: object, environment: Record<string, string>) {
|
||||
const directory = mkdtempSync(join(tmpdir(), "clawhub-clawsweeper-dispatch-"));
|
||||
const eventPath = join(directory, "event.json");
|
||||
const capturePath = join(directory, "dispatch.json");
|
||||
const scriptPath = join(directory, "dispatch.sh");
|
||||
const ghPath = join(directory, "gh");
|
||||
|
||||
try {
|
||||
writeFileSync(eventPath, JSON.stringify(event), "utf8");
|
||||
writeFileSync(scriptPath, `#!/usr/bin/env bash\nset -euo pipefail\n${run}\n`, "utf8");
|
||||
writeFileSync(
|
||||
ghPath,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
'test "$#" -eq 6',
|
||||
'test "$1" = "api"',
|
||||
'test "$2" = "repos/openclaw/clawsweeper/dispatches"',
|
||||
'test "$3" = "--method"',
|
||||
'test "$4" = "POST"',
|
||||
'test "$5" = "--input"',
|
||||
'test "$6" = "-"',
|
||||
'cat > "$GH_CAPTURE"',
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
chmodSync(scriptPath, 0o755);
|
||||
chmodSync(ghPath, 0o755);
|
||||
|
||||
const result = spawnSync("bash", [scriptPath], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
...environment,
|
||||
GH_CAPTURE: capturePath,
|
||||
GH_TOKEN: "proof-token",
|
||||
GITHUB_EVENT_PATH: eventPath,
|
||||
PATH: `${directory}:${process.env.PATH ?? ""}`,
|
||||
SUPERSEDES_IN_PROGRESS: "false",
|
||||
},
|
||||
});
|
||||
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
||||
return JSON.parse(readFileSync(capturePath, "utf8")) as {
|
||||
event_type: string;
|
||||
client_payload: Record<string, unknown>;
|
||||
};
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
describe("ClawSweeper dispatch workflow", () => {
|
||||
it("carries trusted branch authority and canonical PR ingress identity", async () => {
|
||||
const workflow = parseYaml(
|
||||
await readFile(".github/workflows/clawsweeper-dispatch.yml", "utf8"),
|
||||
) as {
|
||||
jobs: { dispatch: { steps: WorkflowStep[] } };
|
||||
};
|
||||
const exactReview = workflow.jobs.dispatch.steps.find(
|
||||
(step) => step.name === "Dispatch exact ClawSweeper review",
|
||||
);
|
||||
|
||||
expect(exactReview?.env?.TARGET_BRANCH).toBe("${{ github.event.repository.default_branch }}");
|
||||
|
||||
const run = exactReview?.run ?? "";
|
||||
expect(run).toContain('--arg target_branch "$TARGET_BRANCH"');
|
||||
expect(run).toContain("target_branch:$target_branch");
|
||||
expect(run).toContain('process.env.ITEM_KIND !== "pull_request"');
|
||||
expect(run).toContain("/^[0-9a-f]{40}$/.test(headSha)");
|
||||
expect(run).toContain("version: 1");
|
||||
expect(run).toContain('target_repo: String(process.env.TARGET_REPO || "").toLowerCase()');
|
||||
expect(run).toContain("item_number: Number(process.env.ITEM_NUMBER)");
|
||||
expect(run).toContain('action: String(process.env.SOURCE_ACTION || "")');
|
||||
expect(run).toContain("head_sha: headSha");
|
||||
expect(run).toContain("updated_at: updatedAt");
|
||||
expect(run).toContain('body: typeof pullRequest.body === "string" ? pullRequest.body : ""');
|
||||
expect(run).toContain('label: String(event.label?.name || "")');
|
||||
expect(run).toContain('ingress_route:"target_dispatcher"');
|
||||
expect(run).toContain("ingress_fingerprint:$ingress_fingerprint");
|
||||
});
|
||||
|
||||
behaviorIt("serializes PR identity and leaves issue dispatches unpaired", async () => {
|
||||
const workflow = parseYaml(
|
||||
await readFile(".github/workflows/clawsweeper-dispatch.yml", "utf8"),
|
||||
) as {
|
||||
jobs: { dispatch: { steps: WorkflowStep[] } };
|
||||
};
|
||||
const run =
|
||||
workflow.jobs.dispatch.steps.find((step) => step.name === "Dispatch exact ClawSweeper review")
|
||||
?.run ?? "";
|
||||
const pullRequest = {
|
||||
head: { sha: "A".repeat(40) },
|
||||
updated_at: "2026-08-10T22:00:00Z",
|
||||
body: "proof body",
|
||||
};
|
||||
const prPayload = executeExactReview(
|
||||
run,
|
||||
{ pull_request: pullRequest, label: { name: "proof: sufficient" } },
|
||||
{
|
||||
TARGET_REPO: "openclaw/clawhub",
|
||||
TARGET_BRANCH: "trunk",
|
||||
ITEM_NUMBER: "3359",
|
||||
ITEM_KIND: "pull_request",
|
||||
SOURCE_EVENT: "pull_request_target",
|
||||
SOURCE_ACTION: "synchronize",
|
||||
},
|
||||
);
|
||||
const fingerprint = createHash("sha256")
|
||||
.update(
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
target_repo: "openclaw/clawhub",
|
||||
item_number: 3359,
|
||||
action: "synchronize",
|
||||
head_sha: "a".repeat(40),
|
||||
updated_at: pullRequest.updated_at,
|
||||
body: pullRequest.body,
|
||||
label: "proof: sufficient",
|
||||
}),
|
||||
)
|
||||
.digest("hex");
|
||||
expect(prPayload).toEqual({
|
||||
event_type: "clawsweeper_item",
|
||||
client_payload: {
|
||||
target_repo: "openclaw/clawhub",
|
||||
target_branch: "trunk",
|
||||
item_number: 3359,
|
||||
item_kind: "pull_request",
|
||||
source_event: "pull_request_target",
|
||||
source_action: "synchronize",
|
||||
supersedes_in_progress: false,
|
||||
ingress_route: "target_dispatcher",
|
||||
ingress_fingerprint: fingerprint,
|
||||
},
|
||||
});
|
||||
|
||||
const issuePayload = executeExactReview(
|
||||
run,
|
||||
{ issue: { number: 3360 } },
|
||||
{
|
||||
TARGET_REPO: "openclaw/clawhub",
|
||||
TARGET_BRANCH: "trunk",
|
||||
ITEM_NUMBER: "3360",
|
||||
ITEM_KIND: "issue",
|
||||
SOURCE_EVENT: "issues",
|
||||
SOURCE_ACTION: "opened",
|
||||
},
|
||||
);
|
||||
expect(issuePayload.client_payload).toEqual({
|
||||
target_repo: "openclaw/clawhub",
|
||||
target_branch: "trunk",
|
||||
item_number: 3360,
|
||||
item_kind: "issue",
|
||||
source_event: "issues",
|
||||
source_action: "opened",
|
||||
supersedes_in_progress: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user