* Main handler factory for dispatch_workflow * Returns a message handler function that processes individual dispatch_workflow messages * @type {HandlerFactoryFunction}
(config = {})
| 22 | * @type {HandlerFactoryFunction} |
| 23 | */ |
| 24 | async function main(config = {}) { |
| 25 | // Extract configuration |
| 26 | const allowedWorkflows = config.workflows || []; |
| 27 | const maxCount = config.max || 1; |
| 28 | const workflowFiles = config.workflow_files || {}; // Map of workflow name to file extension |
| 29 | const awContextWorkflows = new Set(config.aw_context_workflows || []); // Workflows that accept aw_context input |
| 30 | const githubClient = await createAuthenticatedGitHubClient(config); |
| 31 | const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config); |
| 32 | |
| 33 | // Resolve the dispatch destination repository from target-repo config, falling back to context.repo |
| 34 | const contextRepoSlug = `${context.repo.owner}/${context.repo.repo}`; |
| 35 | const normalizedTargetRepo = (defaultTargetRepo ?? "").toString().trim(); |
| 36 | |
| 37 | let resolvedRepoSlug = contextRepoSlug; |
| 38 | let repo = context.repo; |
| 39 | |
| 40 | if (normalizedTargetRepo) { |
| 41 | const parsedRepo = parseRepoSlug(normalizedTargetRepo); |
| 42 | if (!parsedRepo) { |
| 43 | core.warning(`Invalid 'target-repo' configuration value '${normalizedTargetRepo}'; falling back to workflow context repository ${contextRepoSlug}.`); |
| 44 | } else { |
| 45 | resolvedRepoSlug = normalizedTargetRepo; |
| 46 | repo = parsedRepo; |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | const isCrossRepoDispatch = resolvedRepoSlug !== contextRepoSlug; |
| 51 | |
| 52 | // SEC-005: Enforce cross-repository allowlist per Safe Outputs Specification §3.2.6 (SP6). |
| 53 | // Default-deny: cross-repo dispatch is only permitted when an explicit allowlist is configured |
| 54 | // and the resolved target repo is present in that list. Uses validateTargetRepo from |
| 55 | // repo_helpers.cjs for consistent slug validation and glob-pattern matching (e.g. "org/*"). |
| 56 | if (isCrossRepoDispatch) { |
| 57 | if (allowedRepos.size === 0) { |
| 58 | throw new Error( |
| 59 | `E004: Cross-repository dispatch to '${resolvedRepoSlug}' is not permitted. No allowlist is configured. Define 'allowed-repos' in the workflow's 'safe-outputs.dispatch-workflow' section to enable cross-repository dispatch.` |
| 60 | ); |
| 61 | } |
| 62 | const repoValidation = validateTargetRepo(resolvedRepoSlug, contextRepoSlug, allowedRepos); |
| 63 | if (!repoValidation.valid) { |
| 64 | throw new Error(`E004: ${repoValidation.error}`); |
| 65 | } |
| 66 | core.info(`Cross-repo allowlist check passed for ${resolvedRepoSlug}`); |
| 67 | } |
| 68 | |
| 69 | core.info(`Dispatch workflow configuration: max=${maxCount}`); |
| 70 | if (allowedWorkflows.length > 0) { |
| 71 | core.info(`Allowed workflows: ${allowedWorkflows.join(", ")}`); |
| 72 | } |
| 73 | if (Object.keys(workflowFiles).length > 0) { |
| 74 | core.info(`Workflow files: ${JSON.stringify(workflowFiles)}`); |
| 75 | } |
| 76 | if (isCrossRepoDispatch) { |
| 77 | core.info(`Dispatching to target repo: ${resolvedRepoSlug}`); |
| 78 | } |
| 79 | |
| 80 | // Track how many items we've processed for max limit |
| 81 | let processedCount = 0; |
nothing calls this directly
no test coverage detected