mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-15 11:36:06 +00:00
* ci(mantis): add web ui chat proof lane * ci(mantis): tighten web ui proof candidate parsing * ci: tighten Mantis Web UI proof lane --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
411 lines
16 KiB
YAML
411 lines
16 KiB
YAML
name: Mantis Web UI Chat Proof
|
|
|
|
on:
|
|
issue_comment: # zizmor: ignore[dangerous-triggers] maintainer-only Mantis command; candidate refs are trusted before execution and publishing runs in a separate job
|
|
types: [created]
|
|
workflow_dispatch:
|
|
inputs:
|
|
candidate_ref:
|
|
description: Ref, tag, or SHA expected to prove Control UI web chat behavior
|
|
required: true
|
|
default: main
|
|
type: string
|
|
pr_number:
|
|
description: Optional PR number to receive the QA evidence comment
|
|
required: false
|
|
type: string
|
|
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
issues: write
|
|
pull-requests: write
|
|
|
|
concurrency:
|
|
group: mantis-web-ui-chat-proof-${{ github.event.issue.number || inputs.pr_number || inputs.candidate_ref || github.run_id }}-${{ github.run_attempt }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
NODE_VERSION: "24.x"
|
|
|
|
jobs:
|
|
authorize_actor:
|
|
name: Authorize workflow actor
|
|
if: >-
|
|
${{
|
|
github.event_name == 'workflow_dispatch' ||
|
|
(
|
|
github.event_name == 'issue_comment' &&
|
|
github.event.issue.pull_request &&
|
|
(
|
|
contains(github.event.comment.body, '@openclaw-mantis') ||
|
|
contains(github.event.comment.body, '/openclaw-mantis')
|
|
)
|
|
)
|
|
}}
|
|
runs-on: blacksmith-8vcpu-ubuntu-2404
|
|
outputs:
|
|
authorized: ${{ steps.permission.outputs.authorized }}
|
|
steps:
|
|
- name: Require maintainer-level repository access
|
|
id: permission
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
|
with:
|
|
script: |
|
|
const allowed = new Set(["admin", "maintain", "write"]);
|
|
const { owner, repo } = context.repo;
|
|
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
|
owner,
|
|
repo,
|
|
username: context.actor,
|
|
});
|
|
const permission = data.permission;
|
|
core.info(`Actor ${context.actor} permission: ${permission}`);
|
|
if (!allowed.has(permission)) {
|
|
core.notice(
|
|
`Workflow requires write/maintain/admin access. Actor "${context.actor}" has "${permission}".`,
|
|
);
|
|
core.setOutput("authorized", "false");
|
|
return;
|
|
}
|
|
core.setOutput("authorized", "true");
|
|
|
|
resolve_request:
|
|
name: Resolve Mantis request
|
|
needs: authorize_actor
|
|
if: needs.authorize_actor.outputs.authorized == 'true'
|
|
runs-on: blacksmith-8vcpu-ubuntu-2404
|
|
outputs:
|
|
candidate_ref: ${{ steps.resolve.outputs.candidate_ref }}
|
|
pr_number: ${{ steps.resolve.outputs.pr_number }}
|
|
request_source: ${{ steps.resolve.outputs.request_source }}
|
|
should_run: ${{ steps.resolve.outputs.should_run }}
|
|
steps:
|
|
- name: Resolve ref and target PR
|
|
id: resolve
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
|
with:
|
|
script: |
|
|
const eventName = context.eventName;
|
|
|
|
function setOutput(name, value) {
|
|
core.setOutput(name, value ?? "");
|
|
core.info(`${name}=${value ?? ""}`);
|
|
}
|
|
|
|
if (eventName === "workflow_dispatch") {
|
|
const inputs = context.payload.inputs ?? {};
|
|
setOutput("should_run", "true");
|
|
setOutput("candidate_ref", inputs.candidate_ref || "main");
|
|
setOutput("pr_number", inputs.pr_number || "");
|
|
setOutput("request_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 requestedWebUiChat =
|
|
normalized.includes("web-ui-chat") ||
|
|
normalized.includes("web ui chat") ||
|
|
(normalized.includes("web ui") && normalized.includes("chat")) ||
|
|
(normalized.includes("control ui") && normalized.includes("chat"));
|
|
if (!mentionsMantis || !requestedWebUiChat) {
|
|
core.notice("Comment mentioned Mantis but did not request web UI chat proof.");
|
|
setOutput("should_run", "false");
|
|
setOutput("candidate_ref", "");
|
|
setOutput("pr_number", "");
|
|
setOutput("request_source", "unsupported_issue_comment");
|
|
return;
|
|
}
|
|
|
|
const { owner, repo } = context.repo;
|
|
const { data: pr } = await github.rest.pulls.get({
|
|
owner,
|
|
repo,
|
|
pull_number: issue.number,
|
|
});
|
|
const candidateMatch = body.match(/\b(?:candidate|head)\s*[:=]\s*`?([^\s`]+)`?/i);
|
|
const rawCandidate = candidateMatch?.[1];
|
|
const candidate =
|
|
rawCandidate && !["head", "pr", "pr-head"].includes(rawCandidate.toLowerCase())
|
|
? rawCandidate
|
|
: pr.head.sha;
|
|
|
|
setOutput("should_run", "true");
|
|
setOutput("candidate_ref", candidate);
|
|
setOutput("pr_number", String(issue.number));
|
|
setOutput("request_source", "issue_comment");
|
|
|
|
await github.rest.reactions.createForIssueComment({
|
|
owner,
|
|
repo,
|
|
comment_id: context.payload.comment.id,
|
|
content: "eyes",
|
|
}).catch((error) => core.warning(`Could not add eyes reaction: ${error.message}`));
|
|
|
|
validate_candidate:
|
|
name: Validate selected candidate
|
|
needs: resolve_request
|
|
if: ${{ needs.resolve_request.outputs.should_run == 'true' }}
|
|
runs-on: blacksmith-8vcpu-ubuntu-2404
|
|
outputs:
|
|
candidate_revision: ${{ steps.validate.outputs.candidate_revision }}
|
|
steps:
|
|
- name: Checkout harness ref
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 0
|
|
|
|
- name: Validate candidate ref is trusted
|
|
id: validate
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
CANDIDATE_REF: ${{ needs.resolve_request.outputs.candidate_ref }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
|
|
|
revision="$(git rev-parse "${CANDIDATE_REF}^{commit}")"
|
|
reason=""
|
|
if git merge-base --is-ancestor "$revision" refs/remotes/origin/main; then
|
|
reason="main-ancestor"
|
|
elif git tag --points-at "$revision" | grep -Eq '^v'; then
|
|
reason="release-tag"
|
|
else
|
|
pr_head_count="$(
|
|
gh api \
|
|
-H "Accept: application/vnd.github+json" \
|
|
"repos/${GITHUB_REPOSITORY}/commits/${revision}/pulls" \
|
|
--jq '[.[] | select(.state == "open" and .head.repo.full_name == "'"${GITHUB_REPOSITORY}"'" and .head.sha == "'"${revision}"'")] | length'
|
|
)"
|
|
if [[ "$pr_head_count" != "0" ]]; then
|
|
reason="open-pr-head"
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$reason" ]]; then
|
|
echo "Candidate ref '${CANDIDATE_REF}' resolved to ${revision}, which is not trusted for this Mantis run." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "candidate_revision=${revision}" >> "$GITHUB_OUTPUT"
|
|
{
|
|
echo "candidate: \`${CANDIDATE_REF}\`"
|
|
echo "candidate SHA: \`${revision}\`"
|
|
echo "candidate trust reason: \`${reason}\`"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
run_web_ui_chat:
|
|
name: Run Control UI web chat proof
|
|
needs: [resolve_request, validate_candidate]
|
|
if: ${{ needs.resolve_request.outputs.should_run == 'true' }}
|
|
permissions:
|
|
contents: read
|
|
runs-on: blacksmith-8vcpu-ubuntu-2404
|
|
timeout-minutes: 60
|
|
outputs:
|
|
artifact_name: ${{ steps.run_mantis.outputs.artifact_name }}
|
|
output_dir: ${{ steps.run_mantis.outputs.output_dir }}
|
|
proof_status: ${{ steps.run_mantis.outputs.proof_status }}
|
|
steps:
|
|
- name: Checkout harness ref
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 0
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
install-bun: "false"
|
|
install-deps: "false"
|
|
|
|
- name: Prepare candidate worktree
|
|
env:
|
|
CANDIDATE_SHA: ${{ needs.validate_candidate.outputs.candidate_revision }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
worktree_root=".artifacts/qa-e2e/mantis/web-ui-chat-proof-worktrees"
|
|
mkdir -p "$worktree_root"
|
|
git worktree add --detach "$worktree_root/candidate" "$CANDIDATE_SHA"
|
|
pnpm --dir "$worktree_root/candidate" install --frozen-lockfile --prefer-offline
|
|
|
|
- name: Run web UI chat proof
|
|
id: run_mantis
|
|
env:
|
|
CANDIDATE_REF: ${{ needs.resolve_request.outputs.candidate_ref }}
|
|
CANDIDATE_SHA: ${{ needs.validate_candidate.outputs.candidate_revision }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
artifact_name="mantis-web-ui-chat-proof-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
|
candidate_repo="$(pwd)/.artifacts/qa-e2e/mantis/web-ui-chat-proof-worktrees/candidate"
|
|
output_rel=".artifacts/qa-e2e/mantis/web-ui-chat-proof"
|
|
root="$candidate_repo/$output_rel"
|
|
mkdir -p "$root"
|
|
echo "artifact_name=${artifact_name}" >> "$GITHUB_OUTPUT"
|
|
echo "output_dir=${root}" >> "$GITHUB_OUTPUT"
|
|
|
|
install -D -m 0644 \
|
|
"${GITHUB_WORKSPACE}/ui/src/e2e/mantis-chat-proof.e2e.test.ts" \
|
|
"$candidate_repo/ui/src/e2e/mantis-chat-proof.e2e.test.ts"
|
|
install -D -m 0644 \
|
|
"${GITHUB_WORKSPACE}/ui/src/test-helpers/control-ui-e2e.ts" \
|
|
"$candidate_repo/ui/src/test-helpers/control-ui-e2e.ts"
|
|
|
|
cd "$candidate_repo"
|
|
node scripts/ensure-playwright-chromium.mjs
|
|
|
|
set +e
|
|
OPENCLAW_MANTIS_WEB_UI_CHAT_OUTPUT_DIR="$root" \
|
|
node scripts/run-vitest.mjs run \
|
|
--config test/vitest/vitest.ui-e2e.config.ts \
|
|
--configLoader runner \
|
|
ui/src/e2e/mantis-chat-proof.e2e.test.ts \
|
|
2>&1 | tee "$root/vitest.log"
|
|
vitest_exit="${PIPESTATUS[0]}"
|
|
set -e
|
|
|
|
proof_status="pass"
|
|
if [[ "$vitest_exit" -ne 0 ]]; then
|
|
proof_status="fail"
|
|
fi
|
|
echo "proof_status=${proof_status}" >> "$GITHUB_OUTPUT"
|
|
|
|
node "${GITHUB_WORKSPACE}/scripts/mantis/build-web-ui-chat-evidence.mjs" \
|
|
--output-dir "$root" \
|
|
--candidate-ref "$CANDIDATE_REF" \
|
|
--candidate-sha "$CANDIDATE_SHA" \
|
|
--status "$proof_status"
|
|
|
|
cat "$root/mantis-report.md" >> "$GITHUB_STEP_SUMMARY"
|
|
if [[ "$vitest_exit" -ne 0 ]]; then
|
|
exit "$vitest_exit"
|
|
fi
|
|
|
|
- name: Upload Mantis web UI chat artifacts
|
|
if: ${{ always() && steps.run_mantis.outputs.output_dir != '' }}
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: ${{ steps.run_mantis.outputs.artifact_name }}
|
|
path: ${{ steps.run_mantis.outputs.output_dir }}
|
|
retention-days: 14
|
|
if-no-files-found: error
|
|
|
|
publish_evidence:
|
|
name: Publish Mantis web UI chat evidence
|
|
needs: [resolve_request, run_web_ui_chat]
|
|
if: ${{ always() && needs.resolve_request.outputs.pr_number != '' && needs.run_web_ui_chat.outputs.artifact_name != '' }}
|
|
runs-on: ubuntu-24.04
|
|
environment: qa-live-shared
|
|
steps:
|
|
- name: Checkout harness ref
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Download Mantis web UI chat artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
with:
|
|
name: ${{ needs.run_web_ui_chat.outputs.artifact_name }}
|
|
path: .artifacts/qa-e2e/mantis/web-ui-chat-proof
|
|
|
|
- name: Create Mantis GitHub App token
|
|
id: mantis_app_token
|
|
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
|
|
permission-pull-requests: write
|
|
|
|
- name: Comment PR with inline QA evidence
|
|
env:
|
|
GH_TOKEN: ${{ steps.mantis_app_token.outputs.token }}
|
|
MANTIS_ARTIFACT_R2_ACCESS_KEY_ID: ${{ secrets.MANTIS_ARTIFACT_R2_ACCESS_KEY_ID }}
|
|
MANTIS_ARTIFACT_R2_BUCKET: openclaw-crabbox-artifacts
|
|
MANTIS_ARTIFACT_R2_ENDPOINT: ${{ vars.MANTIS_ARTIFACT_R2_ENDPOINT }}
|
|
MANTIS_ARTIFACT_R2_PUBLIC_BASE_URL: https://artifacts.openclaw.ai
|
|
MANTIS_ARTIFACT_R2_REGION: auto
|
|
MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY: ${{ secrets.MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY }}
|
|
REQUEST_SOURCE: ${{ needs.resolve_request.outputs.request_source }}
|
|
TARGET_PR: ${{ needs.resolve_request.outputs.pr_number }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
root=".artifacts/qa-e2e/mantis/web-ui-chat-proof"
|
|
if [[ ! -f "$root/mantis-evidence.json" ]]; then
|
|
echo "No Mantis evidence manifest found; skipping PR evidence comment."
|
|
exit 0
|
|
fi
|
|
node scripts/mantis/publish-pr-evidence.mjs \
|
|
--manifest "$root/mantis-evidence.json" \
|
|
--target-pr "$TARGET_PR" \
|
|
--artifact-root "mantis/web-ui-chat-proof/pr-${TARGET_PR}/run-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \
|
|
--marker "<!-- mantis-web-ui-chat-proof -->" \
|
|
--run-url "https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
|
--request-source "$REQUEST_SOURCE"
|
|
|
|
clear_issue_comment_reaction:
|
|
name: Clear Mantis command reaction
|
|
needs: [resolve_request, validate_candidate, run_web_ui_chat, publish_evidence]
|
|
if: ${{ always() && github.event_name == 'issue_comment' && needs.resolve_request.outputs.request_source == 'issue_comment' }}
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
issues: write
|
|
steps:
|
|
- name: Remove workflow eyes reaction
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
|
with:
|
|
script: |
|
|
const { owner, repo } = context.repo;
|
|
const commentId = context.payload.comment?.id;
|
|
if (!commentId) {
|
|
core.info("No issue comment id found; skipping reaction cleanup.");
|
|
return;
|
|
}
|
|
|
|
const reactions = await github.paginate(github.rest.reactions.listForIssueComment, {
|
|
owner,
|
|
repo,
|
|
comment_id: commentId,
|
|
per_page: 100,
|
|
});
|
|
const eyes = reactions.filter(
|
|
(reaction) => reaction.content === "eyes" && reaction.user?.login === "github-actions[bot]",
|
|
);
|
|
for (const reaction of eyes) {
|
|
await github.rest.reactions.deleteForIssueComment({
|
|
owner,
|
|
repo,
|
|
comment_id: commentId,
|
|
reaction_id: reaction.id,
|
|
});
|
|
core.info(`Removed eyes reaction ${reaction.id} from comment ${commentId}.`);
|
|
}
|
|
if (eyes.length === 0) {
|
|
core.info(`No workflow eyes reaction found on comment ${commentId}.`);
|
|
}
|