mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix: dispatch ClawScan through repository events (#3031)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
name: Security Scan Codex Worker
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [clawhub-security-scan]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
limit:
|
||||
@@ -39,9 +41,9 @@ jobs:
|
||||
shard: [0, 1, 2, 3]
|
||||
env:
|
||||
CONVEX_URL: ${{ vars.CONVEX_URL || vars.VITE_CONVEX_URL || 'https://wry-manatee-359.convex.cloud' }}
|
||||
CODEX_SECURITY_SCAN_LIMIT: ${{ inputs.limit || inputs['batch-limit'] || '4' }}
|
||||
CODEX_SECURITY_SCAN_MAX_JOBS: ${{ inputs['max-jobs'] || '' }}
|
||||
CODEX_SECURITY_SCAN_MAX_RUNTIME_MINUTES: ${{ inputs['max-runtime-minutes'] || '8' }}
|
||||
CODEX_SECURITY_SCAN_LIMIT: ${{ github.event.client_payload.batch_limit || inputs.limit || inputs['batch-limit'] || '4' }}
|
||||
CODEX_SECURITY_SCAN_MAX_JOBS: ${{ github.event.client_payload.max_jobs || inputs['max-jobs'] || '' }}
|
||||
CODEX_SECURITY_SCAN_MAX_RUNTIME_MINUTES: ${{ github.event.client_payload.max_runtime_minutes || inputs['max-runtime-minutes'] || '8' }}
|
||||
CODEX_SECURITY_SCAN_TIMEOUT_MS: ${{ vars.CODEX_SECURITY_SCAN_TIMEOUT_MS || '240000' }}
|
||||
CODEX_SECURITY_SCAN_SHADOW_CLAWSCAN: "1"
|
||||
CODEX_SECURITY_SCAN_SHADOW_CLAWSCAN_SANDBOX: ${{ vars.CODEX_SECURITY_SCAN_SHADOW_CLAWSCAN_SANDBOX || 'docker' }}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
beginSecurityScanDispatchInternal,
|
||||
dispatchSecurityScanWorkflow,
|
||||
finishSecurityScanDispatchInternal,
|
||||
getGitHubRepositoryDispatchPermission,
|
||||
requestSecurityScanDispatchInternal,
|
||||
} from "./securityScanDispatch";
|
||||
|
||||
@@ -352,26 +353,19 @@ describe("securityScanDispatch", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("refuses to dispatch when the GitHub App lacks Actions write permission", async () => {
|
||||
const fetchImpl = vi.fn();
|
||||
|
||||
await expect(
|
||||
dispatchSecurityScanWorkflow(
|
||||
{
|
||||
token: "installation-token",
|
||||
permissions: { actions: "read", contents: "read" },
|
||||
},
|
||||
fetchImpl,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
ok: false,
|
||||
reason: "actions-write-required",
|
||||
it("reports whether the GitHub App can create repository dispatches", () => {
|
||||
expect(getGitHubRepositoryDispatchPermission({ contents: "read" })).toEqual({
|
||||
contentsPermission: "read",
|
||||
canDispatch: false,
|
||||
});
|
||||
expect(getGitHubRepositoryDispatchPermission({ contents: "write" })).toEqual({
|
||||
contentsPermission: "write",
|
||||
canDispatch: true,
|
||||
});
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("dispatches the production workflow on main with bounded worker inputs", async () => {
|
||||
const fetchImpl = vi.fn(async () => new Response(null, { status: 204 }));
|
||||
it("refuses to dispatch when the GitHub App lacks Contents write permission", async () => {
|
||||
const fetchImpl = vi.fn();
|
||||
|
||||
await expect(
|
||||
dispatchSecurityScanWorkflow(
|
||||
@@ -381,20 +375,38 @@ describe("securityScanDispatch", () => {
|
||||
},
|
||||
fetchImpl,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
ok: false,
|
||||
reason: "contents-write-required",
|
||||
});
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("dispatches the production workflow through a narrowly typed repository event", async () => {
|
||||
const fetchImpl = vi.fn(async () => new Response(null, { status: 204 }));
|
||||
|
||||
await expect(
|
||||
dispatchSecurityScanWorkflow(
|
||||
{
|
||||
token: "installation-token",
|
||||
permissions: { contents: "write" },
|
||||
},
|
||||
fetchImpl,
|
||||
),
|
||||
).resolves.toEqual({ ok: true });
|
||||
|
||||
expect(fetchImpl).toHaveBeenCalledWith(
|
||||
"https://api.github.com/repos/openclaw/clawhub/actions/workflows/security-scan-codex.yml/dispatches",
|
||||
"https://api.github.com/repos/openclaw/clawhub/dispatches",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: expect.objectContaining({
|
||||
Authorization: "Bearer installation-token",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
ref: "main",
|
||||
inputs: {
|
||||
"batch-limit": "4",
|
||||
"max-runtime-minutes": "8",
|
||||
event_type: "clawhub-security-scan",
|
||||
client_payload: {
|
||||
batch_limit: "4",
|
||||
max_runtime_minutes: "8",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -7,8 +7,7 @@ import { createGitHubAppInstallationToken, isGitHubAppConfigured } from "./lib/g
|
||||
const DISPATCH_STATE_KEY = "codex-worker";
|
||||
const DISPATCH_LEASE_MS = 5 * 60 * 1000;
|
||||
const SCHEDULE_STALE_MS = 60 * 1000;
|
||||
const GITHUB_WORKFLOW_DISPATCH_URL =
|
||||
"https://api.github.com/repos/openclaw/clawhub/actions/workflows/security-scan-codex.yml/dispatches";
|
||||
const GITHUB_REPOSITORY_DISPATCH_URL = "https://api.github.com/repos/openclaw/clawhub/dispatches";
|
||||
|
||||
const internalRefs = internal as unknown as {
|
||||
securityScanDispatch: {
|
||||
@@ -38,6 +37,14 @@ export function isSecurityScanEventDispatchEnabled(env: NodeJS.ProcessEnv = proc
|
||||
);
|
||||
}
|
||||
|
||||
export function getGitHubRepositoryDispatchPermission(permissions: Record<string, string>) {
|
||||
const contentsPermission = permissions.contents ?? "none";
|
||||
return {
|
||||
contentsPermission,
|
||||
canDispatch: contentsPermission === "write",
|
||||
};
|
||||
}
|
||||
|
||||
export async function dispatchSecurityScanWorkflow(
|
||||
installationToken: {
|
||||
token: string;
|
||||
@@ -45,11 +52,11 @@ export async function dispatchSecurityScanWorkflow(
|
||||
},
|
||||
fetchImpl: typeof fetch = fetch,
|
||||
) {
|
||||
if (installationToken.permissions.actions !== "write") {
|
||||
return { ok: false as const, reason: "actions-write-required" as const };
|
||||
if (!getGitHubRepositoryDispatchPermission(installationToken.permissions).canDispatch) {
|
||||
return { ok: false as const, reason: "contents-write-required" as const };
|
||||
}
|
||||
|
||||
const response = await fetchImpl(GITHUB_WORKFLOW_DISPATCH_URL, {
|
||||
const response = await fetchImpl(GITHUB_REPOSITORY_DISPATCH_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
@@ -59,10 +66,10 @@ export async function dispatchSecurityScanWorkflow(
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ref: "main",
|
||||
inputs: {
|
||||
"batch-limit": "4",
|
||||
"max-runtime-minutes": "8",
|
||||
event_type: "clawhub-security-scan",
|
||||
client_payload: {
|
||||
batch_limit: "4",
|
||||
max_runtime_minutes: "8",
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -210,24 +217,23 @@ export const finishSecurityScanDispatchInternal = internalMutation({
|
||||
},
|
||||
});
|
||||
|
||||
export const checkGitHubActionsPermissionInternal = internalAction({
|
||||
export const checkGitHubRepositoryDispatchPermissionInternal = internalAction({
|
||||
args: {},
|
||||
handler: async () => {
|
||||
if (!isGitHubAppConfigured()) {
|
||||
return {
|
||||
configured: false as const,
|
||||
actionsPermission: null,
|
||||
contentsPermission: null,
|
||||
canDispatch: false,
|
||||
};
|
||||
}
|
||||
const installationToken = await createGitHubAppInstallationToken({
|
||||
userAgent: "clawhub/security-scan-dispatch-preflight",
|
||||
});
|
||||
const actionsPermission = installationToken.permissions.actions ?? "none";
|
||||
const permission = getGitHubRepositoryDispatchPermission(installationToken.permissions);
|
||||
return {
|
||||
configured: true as const,
|
||||
actionsPermission,
|
||||
canDispatch: actionsPermission === "write",
|
||||
...permission,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -264,9 +270,9 @@ export const dispatchSecurityScanWorkerInternal = internalAction({
|
||||
}
|
||||
|
||||
const error =
|
||||
result.reason === "actions-write-required"
|
||||
? "GitHub App Actions write permission is required"
|
||||
: `GitHub workflow dispatch rejected with HTTP ${result.status}`;
|
||||
result.reason === "contents-write-required"
|
||||
? "GitHub App Contents write permission is required"
|
||||
: `GitHub repository dispatch rejected with HTTP ${result.status}`;
|
||||
await runMutationRef(
|
||||
ctx,
|
||||
internalRefs.securityScanDispatch.finishSecurityScanDispatchInternal,
|
||||
|
||||
@@ -41,6 +41,7 @@ describe("security-scan-codex workflow", () => {
|
||||
};
|
||||
};
|
||||
on?: {
|
||||
repository_dispatch?: { types?: string[] };
|
||||
schedule?: Array<{ cron?: string }>;
|
||||
workflow_dispatch?: unknown;
|
||||
};
|
||||
@@ -69,11 +70,18 @@ describe("security-scan-codex workflow", () => {
|
||||
expect(uploadStep?.with?.path).toBe("${{ env.CODEX_SECURITY_SCAN_DIAGNOSTICS_DIR }}");
|
||||
expect(workflow.jobs["codex-security-scan"]["timeout-minutes"]).toBe(20);
|
||||
expect(workflow.on?.workflow_dispatch).toBeDefined();
|
||||
expect(workflow.on?.repository_dispatch?.types).toEqual(["clawhub-security-scan"]);
|
||||
expect(workflow.on?.schedule).toBeUndefined();
|
||||
expect(workflow.jobs["codex-security-scan"].strategy?.["max-parallel"]).toBe(4);
|
||||
expect(workflow.jobs["codex-security-scan"].strategy?.matrix?.shard).toEqual([0, 1, 2, 3]);
|
||||
expect(jobEnv.CODEX_SECURITY_SCAN_LIMIT).toBe(
|
||||
"${{ github.event.client_payload.batch_limit || inputs.limit || inputs['batch-limit'] || '4' }}",
|
||||
);
|
||||
expect(jobEnv.CODEX_SECURITY_SCAN_MAX_JOBS).toBe(
|
||||
"${{ github.event.client_payload.max_jobs || inputs['max-jobs'] || '' }}",
|
||||
);
|
||||
expect(jobEnv.CODEX_SECURITY_SCAN_MAX_RUNTIME_MINUTES).toBe(
|
||||
"${{ inputs['max-runtime-minutes'] || '8' }}",
|
||||
"${{ github.event.client_payload.max_runtime_minutes || inputs['max-runtime-minutes'] || '8' }}",
|
||||
);
|
||||
expect(jobEnv.CODEX_SECURITY_SCAN_TIMEOUT_MS).toBe(
|
||||
"${{ vars.CODEX_SECURITY_SCAN_TIMEOUT_MS || '240000' }}",
|
||||
|
||||
@@ -247,8 +247,10 @@ See also: [acceptable-usage.md](./acceptable-usage.md) for the marketplace polic
|
||||
- Claimable queue work edge-triggers a coalesced GitHub Actions worker dispatch.
|
||||
Successful completion requests another dispatch while queued work remains; a
|
||||
five-minute Convex cron is only a recovery watchdog for lost dispatch signals.
|
||||
Event-driven dispatch stays disabled until the production GitHub App is
|
||||
verified to have Actions write permission.
|
||||
Convex emits the narrowly typed `clawhub-security-scan`
|
||||
`repository_dispatch` event using the production GitHub App. Event-driven
|
||||
dispatch stays disabled until that installation is verified to have Contents
|
||||
write permission.
|
||||
- Queue source priority is `manual`, `backfill`, `publish`, `vt-update`, then
|
||||
`bulk-rescan`. A later VirusTotal update may make a waiting publish job
|
||||
claimable immediately, but it must not demote that job from publish priority.
|
||||
|
||||
Reference in New Issue
Block a user