name: Mantis Resolve Request on: workflow_call: inputs: request-pattern: description: JavaScript regex source matched against the normalized comment body required: true type: string exclude-pattern: description: Optional JavaScript regex source that suppresses a matching request required: false default: "" type: string skip-notice: description: Notice emitted when a Mantis mention does not request this workflow required: true type: string dispatch-params: description: Workflow-specific dispatch inputs or issue-comment defaults as JSON required: false default: "{}" type: string runner: description: Runner used for request resolution required: false default: ubuntu-24.04 type: string secrets: MANTIS_GITHUB_APP_ID: required: true MANTIS_GITHUB_APP_PRIVATE_KEY: required: true outputs: should_run: value: ${{ jobs.resolve.outputs.should_run }} request_source: value: ${{ jobs.resolve.outputs.request_source }} pr_number: value: ${{ jobs.resolve.outputs.pr_number }} candidate_ref: value: ${{ jobs.resolve.outputs.candidate_ref }} baseline_ref: value: ${{ jobs.resolve.outputs.baseline_ref }} reaction_id: value: ${{ jobs.resolve.outputs.reaction_id }} params: value: ${{ jobs.resolve.outputs.params }} env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" jobs: resolve: name: Resolve Mantis request runs-on: ${{ inputs.runner }} outputs: baseline_ref: ${{ steps.resolve.outputs.baseline_ref }} candidate_ref: ${{ steps.resolve.outputs.candidate_ref }} params: ${{ steps.resolve.outputs.params }} pr_number: ${{ steps.resolve.outputs.pr_number }} reaction_id: ${{ steps.add_reaction.outputs.reaction_id }} request_source: ${{ steps.resolve.outputs.request_source }} should_run: ${{ steps.resolve.outputs.should_run }} steps: - name: Resolve refs and target PR id: resolve uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 env: DISPATCH_PARAMS: ${{ inputs.dispatch-params }} EXCLUDE_PATTERN: ${{ inputs.exclude-pattern }} REQUEST_PATTERN: ${{ inputs.request-pattern }} SKIP_NOTICE: ${{ inputs.skip-notice }} with: script: | const eventName = context.eventName; const dispatchParams = JSON.parse(process.env.DISPATCH_PARAMS || "{}"); function stringParam(value) { return typeof value === "string" ? value : ""; } function setOutput(name, value) { core.setOutput(name, value ?? ""); core.info(`${name}=${value ?? ""}`); } function setResolvedOutputs({ baselineRef, candidateRef, params, prNumber, source }) { setOutput("should_run", "true"); setOutput("baseline_ref", baselineRef); setOutput("candidate_ref", candidateRef); setOutput("pr_number", prNumber); setOutput("request_source", source); setOutput("params", JSON.stringify(params)); } async function getPullRequest(pullNumber) { const { owner, repo } = context.repo; const { data: pullRequest } = await github.rest.pulls.get({ owner, repo, pull_number: Number(pullNumber), }); return pullRequest; } if (eventName === "workflow_dispatch") { const prNumber = stringParam(dispatchParams.pr_number); const pullRequest = prNumber ? await getPullRequest(prNumber) : undefined; const candidateRef = stringParam(dispatchParams.candidate_ref) || pullRequest?.head.sha || ""; const baselineRef = stringParam(dispatchParams.baseline_ref) || pullRequest?.base.sha || ""; const params = { ...dispatchParams, baseline_ref: baselineRef, candidate_ref: candidateRef, crabbox_provider: stringParam(dispatchParams.crabbox_provider) || stringParam(dispatchParams.provider), lease_id: stringParam(dispatchParams.crabbox_lease_id) || stringParam(dispatchParams.lease_id) || stringParam(dispatchParams.lease), pr_number: prNumber, provider_mode: stringParam(dispatchParams.provider_mode) || stringParam(dispatchParams["provider-mode"]), scenario: stringParam(dispatchParams.scenario), }; setResolvedOutputs({ baselineRef, candidateRef, params, prNumber, source: "workflow_dispatch", }); return; } if (eventName !== "issue_comment") { core.setFailed(`Unsupported event: ${eventName}`); return; } const issue = context.payload.issue; const body = context.payload.comment?.body ?? ""; if (!issue?.pull_request) { core.setFailed("Mantis issue_comment trigger requires a pull request comment."); return; } const normalized = body.toLowerCase(); const mentionsMantis = normalized.includes("@openclaw-mantis") || normalized.includes("/openclaw-mantis"); const requested = new RegExp(process.env.REQUEST_PATTERN, "u").test(normalized); const excluded = process.env.EXCLUDE_PATTERN !== "" && new RegExp(process.env.EXCLUDE_PATTERN, "u").test(normalized); if (!mentionsMantis || !requested || excluded) { core.notice(process.env.SKIP_NOTICE); setOutput("should_run", "false"); setOutput("baseline_ref", ""); setOutput("candidate_ref", ""); setOutput("pr_number", ""); setOutput("request_source", "unsupported_issue_comment"); setOutput("params", "{}"); return; } const pullRequest = await getPullRequest(issue.number); const token = (keys) => { const pattern = new RegExp( `\\b(?:${keys.join("|")})\\s*[:=]\\s*\`?([^\\s\`]+)\`?`, "i", ); return body.match(pattern)?.[1] ?? ""; }; const rawCandidate = token(["candidate", "head"]); const candidateRef = rawCandidate && !["head", "pr", "pr-head"].includes(rawCandidate.toLowerCase()) ? rawCandidate : pullRequest.head.sha; const baselineRef = token(["baseline", "base"]) || stringParam(dispatchParams.baseline_ref) || pullRequest.base.sha; const params = { ...dispatchParams, baseline_ref: baselineRef, candidate_ref: candidateRef, crabbox_provider: token(["provider", "crabbox_provider"]) || stringParam(dispatchParams.crabbox_provider) || stringParam(dispatchParams.provider), lease_id: token(["lease", "lease_id", "crabbox_lease_id"]) || stringParam(dispatchParams.crabbox_lease_id) || stringParam(dispatchParams.lease_id) || stringParam(dispatchParams.lease), pr_number: String(issue.number), provider_mode: token(["provider_mode", "provider-mode"]) || stringParam(dispatchParams.provider_mode) || stringParam(dispatchParams["provider-mode"]), scenario: token(["scenario", "scenarios"]) || stringParam(dispatchParams.scenario), }; setResolvedOutputs({ baselineRef, candidateRef, params, prNumber: String(issue.number), source: "issue_comment", }); - name: Create Mantis reaction GitHub App token id: mantis_reaction_token if: ${{ steps.resolve.outputs.request_source == 'issue_comment' }} continue-on-error: true uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 with: app-id: ${{ secrets.MANTIS_GITHUB_APP_ID }} private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }} owner: ${{ github.repository_owner }} repositories: ${{ github.event.repository.name }} permission-issues: write - name: Add Mantis eyes reaction id: add_reaction if: ${{ steps.resolve.outputs.request_source == 'issue_comment' && steps.mantis_reaction_token.outcome == 'success' }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: github-token: ${{ steps.mantis_reaction_token.outputs.token }} script: | const { owner, repo } = context.repo; await github.rest.reactions .createForIssueComment({ owner, repo, comment_id: context.payload.comment.id, content: "eyes", }) .then(({ data: reaction }) => core.setOutput("reaction_id", String(reaction.id))) .catch((error) => core.warning(`Could not add eyes reaction: ${error.message}`));